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();