PageSourceSearch

https://kyle.kiwi/av/

html kyle.kiwi collected 2026-10-03 22:39:23 UTC 11,884 bytes, 412 lines download raw bytes

1<!DOCTYPE html>
2<html>
3  <head>
4    <meta charset="utf-8" />
5    <title>AV Debugger</title>
6    <meta name="description" content="🦺 Debug Audio and Video inputs" />
7    <meta name="theme-color" content="#00bcd4" />
8    <link rel="icon" href="../icon.png" />
9    <meta name="viewport" content="width=device-width, initial-scale=1" />
10    <style>
11      body {
12        padding: 0;
13        margin: 0;
14        font-family: sans-serif;
15      }
16      body {
17        margin: 4px;
18      }
19      header {
20        background: tomato;
21        padding: 8px;
22        border-radius: 4px;
23      }
24      #warning {
25        color: tomato;
26        font-weight: 800;
27      }
28      aside {
29        display: inline-block;
30        --volume: 0%;
31        position: relative;
32        width: 200px;
33        height: 20px;
34        background-color: #ddd;
35      }
36
37      aside::before {
38        content: '';
39        position: absolute;
40        top: 0;
41        bottom: 0;
42        left: 0;
43        width: var(--volume);
44        background-color: #00bcd4;
45        transition: width 100ms linear;
46      }
47      #vibrate {
48        padding: 16px;
49      }
50    </style>
51  </head>
52  <body>
53    <header hidden></header>
54    <h1>Media Player</h1>
55    <table>
56      <tr>
57        <td>Track:</td>
58        <td>
59          <audio id="mp" title="Media Player" controls loop>
60            <source src="../av/example.mp3" type="audio/mp3" />
61          </audio>
62        </td>
63      </tr>
64      <tr>
65        <td>Output:</td>
66        <td>
67          <select id="mp-out" onchange="selectMPOut()">
68            <option selected disabled>Loading...</option>
69          </select>
70          <span id="warning" hidden>
71            Output selection not supported in your browser
72          </span>
73        </td>
74      </tr>
75    </table>
76
77    <h1>Mic</h1>
78    <table>
79      <tr>
80        <td>Input:</td>
81        <td>
82          <select id="audio-in" onchange="selectAudioIn()">
83            <option selected disabled>Loading...</option>
84          </select>
85        </td>
86      </tr>
87      <tr>
88        <td>Level:</td>
89        <td><aside></aside></td>
90      </tr>
91      <tr>
92        <td>FX Engine:</td>
93        <td>
94          Delay: 0<input
95            type="range"
96            min="0"
97            max="10"
98            value="1"
99            step="0.01"
100          />10 (seconds)
101        </td>
102      </tr>
103      <tr>
104        <td>Output:</td>
105        <td>
106          <select id="audio-out">
107            <option selected disabled>(default output will be used)</option>
108          </select>
109          <button id="startAudioBtn" onclick="startAudioOut()">Start</button>
110        </td>
111      </tr>
112    </table>
113
114    <h1>Video Input</h1>
115    <table>
116      <tr>
117        <td>Quality:</td>
118        <td>
119          <select
120            id="video-quality"
121            onchange="localStorage.q = this.value;location.reload()"
122          >
123            <option>Default</option>
124            <option>Best</option>
125          </select>
126          (changing this reloads the page)
127        </td>
128      </tr>
129      <tr>
130        <td>Input:</td>
131        <td>
132          <select id="video-in" onchange="selectVideoIn()">
133            <option selected disabled>Loading...</option>
134          </select>
135        </td>
136      </tr>
137      <tr>
138        <td>Resolution:</td>
139        <td id="video-res"></td>
140      </tr>
141      <tr>
142        <td>Preview:</td>
143        <td>
144          <video autoplay controls />
145        </td>
146      </tr>
147    </table>
148
149    <button
150      id="vibrate"
151      onmousedown="navigator.vibrate(9999)"
152      onmouseup="navigator.vibrate(0)"
153      ontouchstart="navigator.vibrate(9999)"
154      ontouchend="navigator.vibrate(0)"
155      hidden
156    >
157      Vibrate
158    </button>
159    <footer></footer>
160
161    
161<script>
162      /** @type {typeof document.querySelector} */
163      const $ = (id) => document.querySelector(id);
164
165      if ('vibrate' in navigator) $('#vibrate').hidden = false;
166      const supportsSink = 'setSinkId' in new Audio();
167      if (!supportsSink) $('#warning').hidden = false;
168
169      /**
170       * converts a decimal to a nice aspect ratio)
171       * from https://stackoverflow.com/a/14011299/5470183
172       */
173      function niceAspectRatio(num) {
174        const eps = 1e-15;
175        let x = num;
176        let a = Math.floor(x);
177        let h1 = 1;
178        let k1 = 0;
179        let h = a;
180        let k = 1;
181
182        while (x - a > eps * k * k) {
183          x = 1 / (x - a);
184          a = Math.floor(x);
185          const h2 = h1;
186          h1 = h;
187          const k2 = k1;
188          k1 = k;
189          h = h2 + a * h1;
190          k = k2 + a * k1;
191        }
192
193        return `${h}:${k}`;
194      }
195
196      async function queryPermission() {
197        // not all browsers support this approach
198        try {
199          await navigator.permissions.request({ name: 'camera' });
200          await navigator.permissions.request({ name: 'microphone' });
201        } catch (ex) {
202          console.log('navigator.permissions.request failed', ex);
203        }
204
205        // this will try prompt for video consent
206        try {
207          await navigator.mediaDevices.getUserMedia({
208            video: true,
209            audio: false,
210          });
211          // this will prompt for audio consent
212          await navigator.mediaDevices.getUserMedia({
213            video: false,
214            audio: true,
215          });
216        } catch (ex) {
217          console.log('getUserMedia failed', ex);
218        }
219
220        try {
221          if (!navigator.permissions) throw new Error();
222
223          const { state: camera } = await navigator.permissions.query({
224            name: 'camera',
225          });
226          const { state: mic } = await navigator.permissions.query({
227            name: 'microphone',
228          });
229          return { camera, mic };
230        } catch {
231          return { camera: 'unknown', mic: 'unknown' };
232        }
233      }
234
235      /** @param {MediaDeviceInfo} x @param {number} i*/
236      const toOption = (x, i) =>
237        `<option value="${x.deviceId}">${x.label || `Unnamed #${i}`}</option>`;
238
239      function onError(ex) {
240        $('header').innerHTML = `${ex}`;
241        $('header').hidden = false;
242      }
243
244      async function init() {
245        try {
246          const perm = await queryPermission();
247          $('footer').innerHTML = `Camera: ${perm.camera}, Mic: ${perm.mic}`;
248
249          const devices = await navigator.mediaDevices.enumerateDevices();
250
251          const list = [
252            ...devices,
253            { kind: 'audioinput', deviceId: '--None', label: '✨ None' },
254            { kind: 'audioinput', deviceId: '--MP1', label: '✨ Media Player' },
255          ];
256
257          $('#video-quality').value = localStorage.q || 'Default';
258
259          $('#audio-in').innerHTML = list
260            .filter((x) => x.kind === 'audioinput')
261            .map(toOption)
262            .join('');
263          await window.selectAudioIn();
264
265          $('#video-in').innerHTML = list
266            .filter((x) => x.kind === 'videoinput')
267            .map(toOption)
268            .join('');
269          await window.selectVideoIn();
270
271          $('#mp-out').innerHTML = list
272            .filter((x) => x.kind === 'audiooutput')
273            .map(toOption)
274            .join('');
275          await window.selectMPOut();
276
277          // $('#audio-out').innerHTML = $('#mp-out').innerHTML;
278        } catch (ex) {
279          onError(ex);
280        }
281      }
282
283      navigator.mediaDevices.ondevicechange = init;
284      init();
285
286      window.selectVideoIn = async () => {
287        const deviceId = $('#video-in').value;
288        const bestQuality = $('#video-quality').value === 'Best';
289
290        const videoStream = await navigator.mediaDevices.getUserMedia({
291          video: bestQuality
292            ? { deviceId, width: { ideal: 9999 }, height: { ideal: 9999 } }
293            : { deviceId },
294        });
295
296        const s = videoStream.getVideoTracks()[0].getSettings();
297
298        $('video').srcObject = videoStream;
299        $('video').play();
300
301        $('#video-res').innerHTML = `${s.width}x${s.height} (${niceAspectRatio(
302          s.aspectRatio
303        )}) @${s.frameRate}fps`;
304      };
305
306      let audioLoop;
307      /** @type {MediaStreamAudioSourceNode} */
308      let micSource;
309      window.selectAudioIn = async () => {
310        try {
311          const deviceId = $('#audio-in').value;
312          if (audioLoop) {
313            clearInterval(audioLoop);
314            micSource = null;
315          }
316
317          if (deviceId === '--None') {
318            $('aside').style.setProperty('--volume', '0%');
319            return;
320          }
321
322          const audioStream =
323            deviceId === '--MP1'
324              ? $('audio#mp').captureStream()
325              : await navigator.mediaDevices.getUserMedia({
326                  audio: { deviceId },
327                });
328
329          const audioCtx = new AudioContext();
330          window.audioCtx = audioCtx;
331          const anal = audioCtx.createAnalyser();
332          anal.fftSize = 512;
333          anal.minDecibels = -127;
334          anal.maxDecibels = 0;
335          anal.smoothingTimeConstant = 0.4;
336          const range = anal.maxDecibels - anal.minDecibels;
337
338          micSource = audioCtx.createMediaStreamSource(audioStream);
339          micSource.connect(anal);
340
341          const levelChunks = new Uint8Array(anal.frequencyBinCount);
342
343          const el = $('aside');
344          audioLoop = setInterval(() => {
345            anal.getByteFrequencyData(levelChunks);
346            let level = 0;
347            for (const chunk of levelChunks) level += chunk;
348            const averageVolume = level / levelChunks.length;
349            const percent = `${(averageVolume / range) * 100}%`;
350            el.style.setProperty('--volume', percent);
351          }, 100);
352        } catch (ex) {
353          onError(ex);
354        }
355      };
356
357      window.selectMPOut = async () => {
358        try {
359          if (!supportsSink) return;
360
361          const deviceId = $('#mp-out').value;
362          await $('audio#mp').setSinkId(deviceId);
363        } catch (ex) {
364          onError(ex);
365        }
366      };
367
368      window.startAudioOut = async () => {
369        try {
370          const { audioCtx } = window;
371          if (!audioCtx) {
372            // eslint-disable-next-line no-alert
373            alert('No audio feed from input');
374            return;
375          }
376
377          if ($('#audio-in').value === '--None') {
378            // eslint-disable-next-line no-alert
379            alert('Select an audio input');
380            return;
381          }
382          if ($('#audio-in').value === '--MP1') {
383            // eslint-disable-next-line no-restricted-globals, no-alert
384            const shouldContinue = confirm(
385              'Note that the Media Player is routed to the output before and after the FX Engine\n\nYou also need to press play in the Media Player'
386            );
387            if (!shouldContinue) return;
388          }
389
390          const startStopBtn = $('#startAudioBtn');
391          startStopBtn.onclick = () => window.location.reload();
392          startStopBtn.innerHTML = 'Stop';
393
394          const delaySeconds = +$('input[type=range]').value;
395
396          const delay = audioCtx.createDelay(10);
397          delay.delayTime.value = delaySeconds;
398          micSource.connect(delay);
399          delay.connect(audioCtx.destination);
400
401          const audioEl = new Audio();
402          audioEl.srcObject = audioCtx.createMediaStreamDestination().stream;
403          // audioEl.setSinkId($('#audio-out').value); // this isn't supported yet by any browser
404          audioEl.play();
405        } catch (ex) {
406          onError(ex);
407        }
408      };
409    </script>
409
410  
410<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"b10fbfa1a5974fda80cf8c07cdf2c60c","r":1,"spa":2}' crossorigin="anonymous"></script>
410
411</body>
412</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.