Add proper mp4 encoding

This commit is contained in:
Skyler Lehmkuhl 2025-01-14 19:34:05 -05:00
parent 4bb27e5e8c
commit 29f1b8cda2
7 changed files with 3065 additions and 13 deletions

View File

@ -22,6 +22,7 @@
<script src="/libav-6.5.7.1-webm-vp9.js"></script> <script src="/libav-6.5.7.1-webm-vp9.js"></script>
<script src="/libavjs-webcodecs-polyfill.js"></script> <script src="/libavjs-webcodecs-polyfill.js"></script>
<script src="/webm-muxer.js"></script> <script src="/webm-muxer.js"></script>
<script src="/mp4-muxer.js"></script>
<script src="UPNG.js"></script> <script src="UPNG.js"></script>
<script src="pako.js"></script> <script src="pako.js"></script>
<script type="module" src="/d3-interpolate-path.js"></script> <script type="module" src="/d3-interpolate-path.js"></script>

1
src/libav-6.5.7.1-all.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -623,7 +623,8 @@
["libvorbis", "vorbis"], ["libvorbis", "vorbis"],
["libaom-av1", "av01"], ["libaom-av1", "av01"],
["libvpx-vp9", "vp09"], ["libvpx-vp9", "vp09"],
["libvpx", "vp8"] ["libvpx", "vp8"],
["libopenh264", "avc"]
]) { ]) {
if (encoders) { if (encoders) {
if (yield libav.avcodec_find_encoder_by_name(avname)) if (yield libav.avcodec_find_encoder_by_name(avname))
@ -807,6 +808,14 @@
options["cpu-used"] = "8"; options["cpu-used"] = "8";
} }
break; break;
case "h264":
video = true;
outCodec = "libopenh264";
if (config.latencyMode === "realtime") {
options.quality = "realtime";
options["cpu-used"] = "8";
}
break;
// Unsupported // Unsupported
case "mp3": case "mp3":
case "mp4a": case "mp4a":

View File

@ -5205,18 +5205,6 @@ async function render() {
selection: [], selection: [],
shapeselection: [], shapeselection: [],
}; };
switch (ext) {
case "mp4":
exportMp4(path)
return
break
case "webm":
createProgressModal();
// Store the original context
const oldContext = context; const oldContext = context;
context = exportContext; context = exportContext;
@ -5224,11 +5212,113 @@ async function render() {
let currentFrame = 0; let currentFrame = 0;
const bitrate = 1e6 const bitrate = 1e6
const frameTimeMicroseconds = parseInt(1_000_000 / config.framerate) const frameTimeMicroseconds = parseInt(1_000_000 / config.framerate)
let target;
let muxer;
let videoEncoder;
switch (ext) {
case "mp4":
// exportMp4(path)
createProgressModal();
// Store the original context
await LibAVWebCodecs.load() await LibAVWebCodecs.load()
console.log("Codecs loaded") console.log("Codecs loaded")
const target = new WebMMuxer.ArrayBufferTarget() target = new Mp4Muxer.ArrayBufferTarget()
const muxer = new WebMMuxer.Muxer({ muxer = new Mp4Muxer.Muxer({
target: target,
video: {
codec: 'avc',
width: config.fileWidth,
height: config.fileHeight,
frameRate: config.framerate,
},
fastStart: 'in-memory',
firstTimestampBehavior: 'offset',
})
videoEncoder = new VideoEncoder({
output: (chunk, meta) => muxer.addVideoChunk(chunk, meta, undefined, undefined, frameTimeMicroseconds),//, currentFrame * frameTimeMicroseconds),
error: (e) => console.error(e),
})
videoEncoder.configure({
codec: 'avc1.42001f',
width: 1280,
height: 720,
bitrate: 1e6
});
async function finishMp4Encoding() {
const progressText = document.getElementById('progressText');
progressText.innerText = 'Finalizing...';
const progressBar = document.getElementById('progressBar');
progressBar.value = 100;
await videoEncoder.flush()
muxer.finalize()
await writeFile(
path,
new Uint8Array(target.buffer),
);
const modal = document.getElementById('progressModal');
modal.style.display = 'none';
document.querySelector("body").style.cursor = "default";
}
const processMp4Frame = async () => {
if (currentFrame < root.maxFrame) {
// Update progress bar
const progressText = document.getElementById('progressText');
progressText.innerText = `Rendering frame ${currentFrame + 1} of ${root.maxFrame}`;
const progressBar = document.getElementById('progressBar');
const progress = Math.round(((currentFrame + 1) / root.maxFrame) * 100);
progressBar.value = progress;
root.setFrameNum(currentFrame)
exportContext.ctx.fillStyle = "white";
exportContext.ctx.rect(0, 0, config.fileWidth, config.fileHeight);
exportContext.ctx.fill();
root.draw(exportContext.ctx);
const frame = new VideoFrame(
await LibAVWebCodecs.createImageBitmap(canvas),
{ timestamp: currentFrame * frameTimeMicroseconds }
);
async function encodeFrame(frame) {
// const keyFrame = true
const keyFrame = currentFrame % 60 === 0
videoEncoder.encode(frame, { keyFrame })
frame.close()
}
await encodeFrame(frame)
frame.close()
currentFrame++;
setTimeout(processMp4Frame, 4);
} else {
// Once all frames are processed, reset context and export
context = oldContext;
root.setFrameNum(oldRootFrame)
finishMp4Encoding()
}
};
processMp4Frame();
return
break
case "webm":
createProgressModal();
await LibAVWebCodecs.load()
console.log("Codecs loaded")
target = new WebMMuxer.ArrayBufferTarget()
muxer = new WebMMuxer.Muxer({
target: target, target: target,
video: { video: {
codec: 'V_VP9', codec: 'V_VP9',
@ -5238,7 +5328,7 @@ async function render() {
}, },
firstTimestampBehavior: 'offset', firstTimestampBehavior: 'offset',
}) })
let videoEncoder = new VideoEncoder({ videoEncoder = new VideoEncoder({
output: (chunk, meta) => muxer.addVideoChunk(chunk, meta),//, currentFrame * frameTimeMicroseconds), output: (chunk, meta) => muxer.addVideoChunk(chunk, meta),//, currentFrame * frameTimeMicroseconds),
error: (e) => console.error(e), error: (e) => console.error(e),
}) })

1908
src/mp4-muxer.js Normal file

File diff suppressed because it is too large Load Diff