UNPKG

transavormer

Version:

Transform (transcode, transmux, etc) audio/video formats using libav.js and WebCodecs

186 lines (170 loc) 8.25 kB
<!doctype html> <!-- * This (un)license applies only to this sample code, and not to TransAVormer as * a whole: * * This is free and unencumbered software released into the public domain. * * Anyone is free to copy, modify, publish, use, compile, sell, or distribute * this software, either in source code form or as a compiled binary, for any * purpose, commercial or non-commercial, and by any means. * * In jurisdictions that recognize copyright laws, the author or authors of * this software dedicate any and all copyright interest in the software to the * public domain. We make this dedication for the benefit of the public at * large and to the detriment of our heirs and successors. We intend this * dedication to be an overt act of relinquishment in perpetuity of all present * and future rights to this software under copyright law. * * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --> <html> <head> <meta charset="utf8" /> <title>Transcoding demo</title> </head> <body> <script type="text/javascript" src="../node_modules/@libav.js/variant-webcodecs-avf/dist/libav-webcodecs-avf.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/weasound@1.2.6/dist/weasound.min.js"></script> <script type="text/javascript" src="../dist/transavormer.js"></script> <input type="file" id="file" /> <br/> <canvas width="640" height="360" id="playerCanvas"></canvas> <br/> <input type="range" min="0" max="128" id="seekBar" /> <script type="text/javascript">(function() { const tav = TransAVormer; const fileBox = document.getElementById("file"); const canvas = document.getElementById("playerCanvas"); const seekBar = document.getElementById("seekBar"); fileBox.onchange = async function() { const file = fileBox.files[0]; if (!file) return; const ac = new AudioContext(); if (ac.state !== "running") ac.resume(); ac.destination.channelCount = ac.destination.maxChannelCount; const playback = await Weasound.createAudioPlayback(ac); (playback.unsharedNode() || playback.sharedNode()).connect(ac.destination); const ctx = canvas.getContext("2d"); const la = await LibAV.LibAV(); const out = await tav.build(la, { type: "play-normalizer", sampleRate: ac.sampleRate, channels: ac.destination.channelCount, input: { type: "demuxer", chunkSize: 1, input: file } }); // Set up the seek bar let seekTo = null; let seeking = false; let duration = 2; for (const stream of await out.streams) { if (stream.duration > duration) duration = stream.duration; } seekBar.max = Math.round(duration * 1000); seekBar.value = 0; seekBar.oninput = async () => { seekTo = seekBar.value / 1000; if (!seeking) { seeking = true; while (seekTo !== null) { const to = seekTo; seekTo = null; await out.sendCommands([{ c: "seek", time: to }]); await new Promise(res => setTimeout(res, 100)); } seeking = false; } }; const streams = out.stream.tee(); let videoOffset = null; let audioOffset = null; // Video playback (async function() { const rdr = streams[0].getReader(); while (true) { const rd = await rdr.read(); if (rd.done) break; for (const sf of rd.value) { const frame = sf.frame; if (frame.codedWidth || frame.width) { let image, frameTime; if (frame.codedWidth) { // WebCodecs image = frame; frameTime = frame.timestamp / 1000; } else { // libav.js image = frame.data; frameTime = frame.pts * frame.time_base_num / frame.time_base_den * 1000; } const now = performance.now(); let targetTime = now; if (audioOffset !== null) { targetTime = frameTime + audioOffset; } else if (videoOffset !== null) { targetTime = frameTime + videoOffset; } else { videoOffset = now - frameTime; } const delay = targetTime - now; if (delay > 100 || delay < -100) { videoOffset = now - frameTime; } if (delay > 0) { await new Promise(res => setTimeout( res, Math.min(delay, 100) )); } canvas.width = frame.codedWidth || frame.width; canvas.height = frame.codedHeight || frame.height; ctx.drawImage(image, 0, 0); image.close(); seekBar.value = Math.round(frameTime); } } } })(); // Audio playback (async function() { const rdr = streams[1].getReader(); while (true) { const rd = await rdr.read(); if (rd.done) break; for (const sf of rd.value) { const frame = sf.frame; const now = performance.now(); if (frame.sample_rate) { const frameTime = frame.pts * frame.time_base_num / frame.time_base_den * 1000; const delay = playback.play(frame.data); audioOffset = performance.now() + delay - frameTime; seekBar.value = Math.round(frameTime - delay); if (delay > 200) await new Promise(res => setTimeout(res, delay - 100)); } } } })(); } })(); </script> </body> </html>