v0.27.2 5960dd8

webrtc.ts RawHistoryFolder

1import "../styles/app.css";
2
3const activeStreams: MediaStream[] = [];
4
5function now(): string {
6  return new Date().toLocaleTimeString();
7}
8
9function appendLog(output: HTMLPreElement, message: string): void {
10  output.textContent = `${output.textContent}[${now()}] ${message}\n`;
11  output.scrollTop = output.scrollHeight;
12}
13
14function setStatus(status: HTMLElement, value: string): void {
15  status.textContent = value;
16}
17
18function stopAllStreams(video: HTMLVideoElement, output: HTMLPreElement, status: HTMLElement): void {
19  for (const stream of activeStreams.splice(0)) {
20    for (const track of stream.getTracks()) {
21      track.stop();
22    }
23  }
24
25  video.pause();
26  video.srcObject = null;
27  setStatus(status, "idle");
28  appendLog(output, "Stopped all active tracks");
29}
30
31async function probeDevices(output: HTMLPreElement): Promise<void> {
32  if (!("mediaDevices" in navigator) || typeof navigator.mediaDevices.enumerateDevices !== "function") {
33    appendLog(output, "enumerateDevices is unavailable");
34    return;
35  }
36
37  try {
38    const devices = await navigator.mediaDevices.enumerateDevices();
39    const summary = devices
40      .map((device) => `${device.kind} ${device.label || "(label hidden until permission)"}`)
41      .join(" | ");
42
43    appendLog(output, `Devices: ${summary || "none"}`);
44  } catch (err) {
45    appendLog(output, `enumerateDevices failed: ${String(err)}`);
46    appendLog(output, "Devices: none");
47  }
48}
49
50async function requestUserMedia(video: HTMLVideoElement, output: HTMLPreElement, status: HTMLElement): Promise<void> {
51  if (!("mediaDevices" in navigator) || typeof navigator.mediaDevices.getUserMedia !== "function") {
52    appendLog(output, "getUserMedia is unavailable");
53    return;
54  }
55
56  try {
57    setStatus(status, "requesting camera + microphone");
58    const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
59    activeStreams.push(stream);
60    video.srcObject = stream;
61    await video.play();
62    setStatus(status, "camera + microphone granted");
63    appendLog(output, "camera + microphone permission granted");
64    await probeDevices(output);
65  } catch (err) {
66    setStatus(status, "camera + microphone blocked");
67    appendLog(output, `camera + microphone request failed: ${String(err)}`);
68  }
69}
70
71async function requestDisplayMedia(video: HTMLVideoElement, output: HTMLPreElement, status: HTMLElement): Promise<void> {
72  if (!("mediaDevices" in navigator) || typeof navigator.mediaDevices.getDisplayMedia !== "function") {
73    appendLog(output, "getDisplayMedia is unavailable");
74    return;
75  }
76
77  try {
78    setStatus(status, "requesting screen capture");
79    const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
80    activeStreams.push(stream);
81    video.srcObject = stream;
82    await video.play();
83    setStatus(status, "screen capture granted");
84    appendLog(output, "screen capture permission granted");
85  } catch (err) {
86    setStatus(status, "screen capture blocked");
87    appendLog(output, `screen capture request failed: ${String(err)}`);
88  }
89}
90
91function mount(): void {
92  document.title = "WebRTC Diagnostics";
93  document.body.innerHTML = `
94    <main class="page-shell" style="padding: 24px; max-width: 920px; margin: 0 auto; display: grid; gap: 16px;">
95      <header style="display: grid; gap: 8px;">
96        <h1 style="margin: 0; font-size: 1.8rem;">WebRTC Diagnostics</h1>
97        <p style="margin: 0; opacity: 0.85;">Use this page to validate camera, microphone, and screen permission behavior.</p>
98      </header>
99
100      <section style="display: flex; flex-wrap: wrap; gap: 10px;">
101        <button id="btn-av" type="button">Request camera + microphone</button>
102        <button id="btn-screen" type="button">Request screen capture</button>
103        <button id="btn-stop" type="button">Stop all streams</button>
104      </section>
105
106      <section style="display: grid; gap: 8px;">
107        <strong>Status: <span id="status">idle</span></strong>
108        <video id="preview" autoplay playsinline muted style="width: 100%; max-height: 320px; border-radius: 10px; background: #0c1522;"></video>
109      </section>
110
111      <section style="display: grid; gap: 8px;">
112        <strong>Event Log</strong>
113        <pre id="output" style="margin: 0; padding: 12px; border-radius: 10px; background: #0d1727; color: #dbe8f4; min-height: 180px; max-height: 280px; overflow: auto;">[${now()}] Ready\n</pre>
114      </section>
115    </main>
116  `;
117
118  const status = document.getElementById("status");
119  const preview = document.getElementById("preview");
120  const output = document.getElementById("output");
121  const btnAV = document.getElementById("btn-av");
122  const btnScreen = document.getElementById("btn-screen");
123  const btnStop = document.getElementById("btn-stop");
124
125  if (!(status instanceof HTMLElement)) {
126    return;
127  }
128  if (!(preview instanceof HTMLVideoElement)) {
129    return;
130  }
131  if (!(output instanceof HTMLPreElement)) {
132    return;
133  }
134  if (!(btnAV instanceof HTMLButtonElement)) {
135    return;
136  }
137  if (!(btnScreen instanceof HTMLButtonElement)) {
138    return;
139  }
140  if (!(btnStop instanceof HTMLButtonElement)) {
141    return;
142  }
143
144  btnAV.addEventListener("click", () => {
145    requestUserMedia(preview, output, status).catch((err: unknown) => {
146      appendLog(output, `unexpected user media error: ${String(err)}`);
147    });
148  });
149
150  btnScreen.addEventListener("click", () => {
151    requestDisplayMedia(preview, output, status).catch((err: unknown) => {
152      appendLog(output, `unexpected display media error: ${String(err)}`);
153    });
154  });
155
156  btnStop.addEventListener("click", () => {
157    stopAllStreams(preview, output, status);
158  });
159
160  window.addEventListener("beforeunload", () => {
161    stopAllStreams(preview, output, status);
162  });
163}
164
165mount();

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close