diff --git a/dev/convert.html b/dev/convert.html index 96c013a..9f18bfd 100644 --- a/dev/convert.html +++ b/dev/convert.html @@ -26,7 +26,7 @@ chunked: true, chunkSize: 2**20 }); - const outputFormat = new Mediabunny.Mp4OutputFormat({}); + const outputFormat = new Mediabunny.Mp4OutputFormat({fastStart: 'fragmented'}); const button = document.createElement('button'); button.textContent = 'Cancel'; diff --git a/dev/demux.html b/dev/demux.html index 233aae3..baa5541 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -18,10 +18,11 @@ }); const videoTrack = await input.getPrimaryVideoTrack(); - const sink = new Mediabunny.VideoSampleSink(videoTrack); + const sink = new Mediabunny.EncodedPacketSink(videoTrack); - const sample = await sink.getSample(await videoTrack.getFirstTimestamp()); - console.log(sample); + for await (const packet of sink.packets()) { + console.log(packet.timestamp); + } /* diff --git a/src/isobmff/isobmff-muxer.ts b/src/isobmff/isobmff-muxer.ts index 4256f53..b7a6158 100644 --- a/src/isobmff/isobmff-muxer.ts +++ b/src/isobmff/isobmff-muxer.ts @@ -1245,11 +1245,13 @@ export class IsobmffMuxer extends Muxer { override async onTrackClose(track: OutputTrack) { const release = await this.mutex.acquire(); - if (track.type === 'subtitle' && track.source._codec === 'webvtt') { - const trackData = this.trackDatas.find(x => x.track === track) as IsobmffSubtitleTrackData; - if (trackData) { + const trackData = this.trackDatas.find(x => x.track === track); + if (trackData) { + if (trackData.type === 'subtitle' && track.source._codec === 'webvtt') { await this.processWebVTTCues(trackData, Infinity); } + + this.processTimestamps(trackData); } if (this.allTracksAreKnown()) { @@ -1274,19 +1276,15 @@ export class IsobmffMuxer extends Muxer { if (trackData.type === 'subtitle' && trackData.track.source._codec === 'webvtt') { await this.processWebVTTCues(trackData, Infinity); } + + this.processTimestamps(trackData); } if (this.isFragmented) { await this.interleaveSamples(true); - - for (const trackData of this.trackDatas) { - this.processTimestamps(trackData); - } - await this.finalizeFragment(false); // Don't flush the last fragment as we will flush it with the mfra box } else { for (const trackData of this.trackDatas) { - this.processTimestamps(trackData); await this.finalizeCurrentChunk(trackData); } } diff --git a/test/node/isobmff-muxer.test.ts b/test/node/isobmff-muxer.test.ts index 55cee50..83d1d9e 100644 --- a/test/node/isobmff-muxer.test.ts +++ b/test/node/isobmff-muxer.test.ts @@ -61,3 +61,46 @@ test('ISOBMFF muxer internally converts ADTS to AAC', async () => { expect(count).toBe(4557); }); + +test('Fragmented fMP4 with video+audio preserves B-frame CTS', async () => { + using input = new Input({ + source: new FilePathSource(path.join(__dirname, '../public/video.mp4')), + formats: ALL_FORMATS, + }); + + const videoTrack = await input.getPrimaryVideoTrack(); + const audioTrack = await input.getPrimaryAudioTrack(); + assert(videoTrack); + assert(audioTrack); + + const originalVideoSink = new EncodedPacketSink(videoTrack); + const originalTimestamps: number[] = []; + for await (const packet of originalVideoSink.packets()) { + originalTimestamps.push(packet.timestamp); + } + + const output = new Output({ + format: new Mp4OutputFormat({ fastStart: 'fragmented' }), + target: new BufferTarget(), + }); + + const conversion = await Conversion.init({ input, output, showWarnings: false }); + await conversion.execute(); + + using outputAsInput = new Input({ + source: new BufferSource(output.target.buffer!), + formats: ALL_FORMATS, + }); + + const outputVideoTrack = await outputAsInput.getPrimaryVideoTrack(); + assert(outputVideoTrack); + + const videoSink = new EncodedPacketSink(outputVideoTrack); + + const timestamps: number[] = []; + for await (const packet of videoSink.packets()) { + timestamps.push(packet.timestamp); + } + + expect(timestamps).toEqual(originalTimestamps); +});