transavormer
Version:
Transform (transcode, transmux, etc) audio/video formats using libav.js and WebCodecs
186 lines (170 loc) • 8.25 kB
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>