mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 10:53:50 +02:00
* Fix CTS=0 in fragmented fMP4 with multiple tracks When muxing fragmented MP4 with both video and audio tracks, compositionTimeOffset (CTS) was zero for all video samples, causing B-frame content to display in decode order instead of presentation order (visible judder). Root cause: During finalize(), interleaveSamples(true) triggers finalizeFragment() via the cross-track keyframe check in addSampleToTrack. This writes the trun box before processTimestamps has computed correct decodeTimestamp values, so CTS = PTS - DTS = 0. Fix: Call processTimestamps() for all tracks at the start of finalizeFragment(). This is safe because processTimestamps is a no-op when the timestampProcessingQueue is empty. Video-only fragmented muxing was unaffected because the single-track case never triggers the cross-track keyframe check during interleaveSamples. Co-Authored-By: Claude Opus 4.6 <[email protected]> * Fix the problem at the actual root * Oopsie doopsie --------- Co-authored-by: Claude Opus 4.6 <[email protected]> Co-authored-by: Vanilagy <[email protected]>
234 lines
5.4 KiB
HTML
234 lines
5.4 KiB
HTML
<!DOCTYPE html>
|
|
|
|
<script src="../dist/bundles/mediabunny.cjs"></script>
|
|
<script src="../packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.js"></script>
|
|
<script src="../packages/ac3/dist/bundles/mediabunny-ac3.js"></script>
|
|
|
|
<script type="module">
|
|
//MediabunnyMp3Encoder.registerMp3Encoder();
|
|
MediabunnyAc3.registerAc3Encoder();
|
|
|
|
const fileInput = document.createElement('input');
|
|
fileInput.type = 'file';
|
|
document.body.append(fileInput);
|
|
|
|
const progressElement = document.createElement('progress');
|
|
progressElement.max = 1;
|
|
document.body.append(progressElement);
|
|
|
|
fileInput.addEventListener('change', async () => {
|
|
const file = fileInput.files[0];
|
|
|
|
const source = new Mediabunny.BlobSource(file);
|
|
const target = new Mediabunny.BufferTarget() ?? new Mediabunny.StreamTarget(new WritableStream({
|
|
write: console.log
|
|
}), {
|
|
chunked: true,
|
|
chunkSize: 2**20
|
|
});
|
|
const outputFormat = new Mediabunny.Mp4OutputFormat({fastStart: 'fragmented'});
|
|
|
|
const button = document.createElement('button');
|
|
button.textContent = 'Cancel';
|
|
button.onclick = () => conversion.cancel();
|
|
document.body.append(button);
|
|
|
|
const yo = document.createElement('canvas');
|
|
yo.width = 512;
|
|
yo.height = 512;
|
|
const context = yo.getContext('2d', { alpha: false });
|
|
context.fillStyle = 'red';
|
|
context.fillRect(100, 100, 300, 300);
|
|
const blob = await new Promise(resolve => yo.toBlob(resolve, 'image/jpeg', 0.92));
|
|
const blobData = new Uint8Array(await blob.arrayBuffer());
|
|
|
|
const mh = new Blob([blobData], { type: 'image/jpeg' });
|
|
console.log(URL.createObjectURL(mh))
|
|
|
|
const output = new Mediabunny.Output({
|
|
format: outputFormat,
|
|
target
|
|
});
|
|
|
|
let ctx = null;
|
|
const conversion = await Mediabunny.Conversion.init({
|
|
input: new Mediabunny.Input({
|
|
formats: Mediabunny.ALL_FORMATS,
|
|
source
|
|
}),
|
|
output,
|
|
audio: (_, n) => ({
|
|
//codec: 'eac3',
|
|
//discard: true,
|
|
//discard: n > 1,
|
|
//codec: 'opus',
|
|
//codec: 'opus',
|
|
/*
|
|
process: (sample) => {
|
|
return sample;
|
|
},
|
|
*/
|
|
//codec: 'pcm-s16',
|
|
//sampleRate: 16000,
|
|
//numberOfChannels: 1,
|
|
//discard: true,
|
|
//codec: 'opus',
|
|
//bitrate: 128000,
|
|
//numberOfChannels: 1,
|
|
//sampleRate: 4000
|
|
//discard: true
|
|
//forceTranscode: true,
|
|
}),
|
|
/*
|
|
video: {
|
|
discard: true,
|
|
bitrate: Mediabunny.QUALITY_VERY_HIGH,
|
|
//bitrate: Mediabunny.QUALITY_VERY_HIGH
|
|
codec: 'av1',
|
|
//width: 800,
|
|
//fit: 'fill',
|
|
//rotate: 90,
|
|
},
|
|
audio: {
|
|
bitrate: Mediabunny.QUALITY_HIGH,
|
|
codec: 'aac',
|
|
},
|
|
*/
|
|
/*
|
|
video: {
|
|
codec: 'avc',
|
|
},
|
|
audio: {
|
|
codec: 'mp3',
|
|
bitrate: 320000
|
|
},
|
|
*/
|
|
video: () => ({
|
|
//forceTranscode: true,
|
|
//forceTranscode: true,
|
|
//width: 1280,
|
|
//forceTranscode: true,
|
|
//allowRotationMetadata: false,
|
|
//width: 720,
|
|
//frameRate: 30,
|
|
//bitrate: Mediabunny.QUALITY_VERY_LOW,
|
|
//discard: true,
|
|
/*
|
|
process: (sample) => {
|
|
if (!ctx) {
|
|
// Create a canvas for image compositing
|
|
const canvas = new OffscreenCanvas(
|
|
sample.displayWidth,
|
|
sample.displayHeight,
|
|
);
|
|
ctx = canvas.getContext('2d');
|
|
}
|
|
|
|
console.log(ctx.canvas.width, ctx.canvas.height);
|
|
|
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
sample.drawWithFit(ctx, { fit: 'fill' });
|
|
//ctx.drawImage(watermark, 32, 32);
|
|
|
|
return ctx.canvas;
|
|
},
|
|
*/
|
|
//width: 300,
|
|
//alpha: 'keep',
|
|
//width: 320,
|
|
//discard: true,
|
|
//discard: true,
|
|
//crop: {
|
|
// left: 0,
|
|
// top: 0,
|
|
// width: 500,
|
|
// height: 500,
|
|
//},
|
|
//rotate: 90,
|
|
//width: 200,
|
|
//height: 500,
|
|
//fit: 'contain',
|
|
//forceTranscode: true,
|
|
//codec: 'avc',
|
|
//fit: 'contain',
|
|
//frameRate: 27.123,
|
|
//width: 320,
|
|
//forceTranscode: true,
|
|
//codec: 'av1',
|
|
//discard: true,
|
|
//width: 1280,
|
|
//discard: true,
|
|
//width: 640
|
|
//forceTranscode: true,
|
|
//rotate: 90
|
|
//width: 720 ?? 2160,
|
|
//height: 1280 ?? 3840,
|
|
//fit: 'contain',
|
|
//rotate: 90,
|
|
//width: 512,
|
|
//height: 512,
|
|
//width: 200,
|
|
//height: 100,
|
|
}),
|
|
tags: {} ?? {
|
|
title: 'Bigggy',
|
|
artist: 'Buck Bunny',
|
|
images: [{
|
|
data: blobData,
|
|
kind: 'coverFront',
|
|
mimeType: 'image/jpeg'
|
|
}],
|
|
trackNumber: 4,
|
|
tracksTotal: 10,
|
|
discNumber: 5,
|
|
discsTotal: 8,
|
|
lyrics: "There's no way\nThat it's not going there",
|
|
raw: {
|
|
'AIGC': 'Mediabunny epic own encoder'
|
|
}
|
|
},
|
|
trim: {
|
|
//start: 0,
|
|
//end: 10
|
|
},
|
|
});
|
|
console.log(conversion);
|
|
|
|
let progress = 0;
|
|
conversion.onProgress = newProgress => progress = newProgress;
|
|
|
|
function updateProgress() {
|
|
progressElement.value = progress;
|
|
|
|
if (progress === 1) {
|
|
return;
|
|
}
|
|
|
|
setTimeout(updateProgress, 1000/60);
|
|
}
|
|
updateProgress();
|
|
|
|
console.time();
|
|
await conversion.execute();
|
|
console.timeEnd()
|
|
|
|
console.log("Done", target.buffer);
|
|
|
|
const video = document.createElement('video');
|
|
video.src = URL.createObjectURL(new Blob([target.buffer], { type: outputFormat.mimeType }));
|
|
video.controls = true;
|
|
document.body.append(video);
|
|
video.play();
|
|
|
|
//download(new Blob([target.buffer]), 'converted' + outputFormat.fileExtension);
|
|
|
|
function download(blob, filename) {
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
});
|
|
</script> |