diff --git a/dev/demux.html b/dev/demux.html index 84fe728..9ac4f2f 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -11,14 +11,14 @@ const file = fileInput.files[0]; const input = new Mediabunny.Input({ formats: Mediabunny.ALL_FORMATS, - source: new Mediabunny.BufferSource(await file.arrayBuffer()), + source: new Mediabunny.BlobSource(file), }); const track = await input.getPrimaryVideoTrack(); const sink = new Mediabunny.EncodedPacketSink(track); for await (const packet of sink.packets()) { - //console.log(packet.timestamp, packet.duration, packet.type); + console.log(packet); } /* diff --git a/src/mpeg-ts/mpeg-ts-muxer.ts b/src/mpeg-ts/mpeg-ts-muxer.ts index 9cc3a92..b0774bc 100644 --- a/src/mpeg-ts/mpeg-ts-muxer.ts +++ b/src/mpeg-ts/mpeg-ts-muxer.ts @@ -192,13 +192,17 @@ export class MpegTsMuxer extends Muxer { try { const trackData = this.getVideoTrackData(track, meta); - this.validateAndNormalizeTimestamp(trackData.track, packet.timestamp, packet.type === 'key'); + const timestamp = this.validateAndNormalizeTimestamp( + trackData.track, + packet.timestamp, + packet.type === 'key', + ); const preparedData = this.prepareVideoPacket(trackData, packet, meta); trackData.packetQueue.push({ data: preparedData, - timestamp: packet.timestamp, + timestamp, isKeyframe: packet.type === 'key', }); @@ -218,13 +222,17 @@ export class MpegTsMuxer extends Muxer { try { const trackData = this.getAudioTrackData(track, meta); - this.validateAndNormalizeTimestamp(trackData.track, packet.timestamp, packet.type === 'key'); + const timestamp = this.validateAndNormalizeTimestamp( + trackData.track, + packet.timestamp, + packet.type === 'key', + ); const preparedData = this.prepareAudioPacket(trackData, packet, meta); trackData.packetQueue.push({ data: preparedData, - timestamp: packet.timestamp, + timestamp, isKeyframe: packet.type === 'key', }); diff --git a/test/browser/mpeg-ts-muxing.test.ts b/test/browser/mpeg-ts-muxing.test.ts index aa056cb..4b91cc8 100644 --- a/test/browser/mpeg-ts-muxing.test.ts +++ b/test/browser/mpeg-ts-muxing.test.ts @@ -5,7 +5,7 @@ import { ALL_FORMATS, MPEG_TS } from '../../src/input-format.js'; import { Output } from '../../src/output.js'; import { MpegTsOutputFormat } from '../../src/output-format.js'; import { BufferTarget, StreamTarget, StreamTargetChunk } from '../../src/target.js'; -import { CanvasSource, EncodedAudioPacketSource } from '../../src/media-source.js'; +import { CanvasSource, EncodedAudioPacketSource, EncodedVideoPacketSource } from '../../src/media-source.js'; import { QUALITY_HIGH } from '../../src/encode.js'; import { EncodedPacketSink } from '../../src/media-sink.js'; import { assert } from '../../src/misc.js'; @@ -215,15 +215,7 @@ test('MPEG-TS muxing with HEVC and MP3', async () => { target: new BufferTarget(), }); - const canvas = new OffscreenCanvas(1280, 720); - const context = canvas.getContext('2d')!; - context.fillStyle = '#000000'; - context.fillRect(0, 0, canvas.width, canvas.height); - - const videoSource = new CanvasSource(canvas, { - codec: 'hevc', - bitrate: QUALITY_HIGH, - }); + const videoSource = new EncodedVideoPacketSource('hevc'); output.addVideoTrack(videoSource); const audioSource = new EncodedAudioPacketSource('mp3'); @@ -231,13 +223,32 @@ test('MPEG-TS muxing with HEVC and MP3', async () => { await output.start(); - const fps = 30; const duration = 5; - const frameCount = fps * duration; - const frameDuration = 1 / fps; - for (let i = 0; i < frameCount; i++) { - await videoSource.add(i * frameDuration, frameDuration); + // Extract HEVC packets from existing file + using hevcInput = new Input({ + source: new UrlSource('/video-h265.mp4'), + formats: ALL_FORMATS, + }); + + const hevcTrack = await hevcInput.getPrimaryVideoTrack(); + assert(hevcTrack); + + const hevcSink = new EncodedPacketSink(hevcTrack); + + let isFirstVideo = true; + let videoPacketCountWritten = 0; + for await (const packet of hevcSink.packets()) { + console.log(packet.timestamp); + if (packet.timestamp >= duration) break; + + await videoSource.add(packet, { + decoderConfig: isFirstVideo + ? (await hevcTrack.getDecoderConfig())! + : undefined, + }); + isFirstVideo = false; + videoPacketCountWritten++; } // Extract MP3 packets from existing file @@ -251,16 +262,16 @@ test('MPEG-TS muxing with HEVC and MP3', async () => { const mp3Sink = new EncodedPacketSink(mp3Track); - let isFirst = true; + let isFirstAudio = true; for await (const packet of mp3Sink.packets()) { if (packet.timestamp >= duration) break; await audioSource.add(packet, { - decoderConfig: isFirst + decoderConfig: isFirstAudio ? (await mp3Track.getDecoderConfig())! : undefined, }); - isFirst = false; + isFirstAudio = false; } await output.finalize(); @@ -281,14 +292,10 @@ test('MPEG-TS muxing with HEVC and MP3', async () => { expect(videoTrack.id).toBe(0x100); expect(videoTrack.codec).toBe('hevc'); - expect(videoTrack.displayWidth).toBe(1280); - expect(videoTrack.displayHeight).toBe(720); const videoDecoderConfig = await videoTrack.getDecoderConfig(); assert(videoDecoderConfig); expect(videoDecoderConfig.codec).toMatch(/^hev1\./); - expect(videoDecoderConfig.codedWidth).toBe(1280); - expect(videoDecoderConfig.codedHeight).toBe(720); expect(videoDecoderConfig.description).toBeUndefined(); // Annex B, no description // Verify audio track @@ -316,7 +323,7 @@ test('MPEG-TS muxing with HEVC and MP3', async () => { videoPacketCount++; } - expect(videoPacketCount).toBe(frameCount); + expect(videoPacketCount).toBe(videoPacketCountWritten); // Verify audio packets are MP3 frames const audioSink = new EncodedPacketSink(audioTrack); @@ -333,7 +340,7 @@ test('MPEG-TS muxing with HEVC and MP3', async () => { const videoDuration = await videoTrack.computeDuration(); const audioDuration = await audioTrack.computeDuration(); - expect(videoDuration).toBeCloseTo(5, 1); + expect(videoDuration).toBeCloseTo(4.933333333333334, 1); expect(audioDuration).toBeCloseTo(5, 1); }); @@ -482,32 +489,35 @@ test('MPEG-TS muxing with two video tracks', async () => { target: new BufferTarget(), }); - const canvas1 = new OffscreenCanvas(640, 480); - const ctx1 = canvas1.getContext('2d')!; - ctx1.fillStyle = '#ff0000'; - ctx1.fillRect(0, 0, canvas1.width, canvas1.height); - - const canvas2 = new OffscreenCanvas(320, 240); - const ctx2 = canvas2.getContext('2d')!; - ctx2.fillStyle = '#00ff00'; - ctx2.fillRect(0, 0, canvas2.width, canvas2.height); - - const videoSource1 = new CanvasSource(canvas1, { codec: 'hevc', bitrate: QUALITY_HIGH }); - const videoSource2 = new CanvasSource(canvas2, { codec: 'hevc', bitrate: QUALITY_HIGH }); + const videoSource1 = new EncodedVideoPacketSource('hevc'); + const videoSource2 = new EncodedVideoPacketSource('hevc'); output.addVideoTrack(videoSource1); output.addVideoTrack(videoSource2); await output.start(); - const fps = 30; const duration = 1; - const frameCount = fps * duration; - const frameDuration = 1 / fps; - for (let i = 0; i < frameCount; i++) { - await videoSource1.add(i * frameDuration, frameDuration); - await videoSource2.add(i * frameDuration, frameDuration); + using hevcInput = new Input({ + source: new UrlSource('/video-h265.mp4'), + formats: ALL_FORMATS, + }); + + const hevcTrack = await hevcInput.getPrimaryVideoTrack(); + assert(hevcTrack); + + const hevcSink = new EncodedPacketSink(hevcTrack); + const hevcDecoderConfig = await hevcTrack.getDecoderConfig(); + + let isFirst = true; + for await (const packet of hevcSink.packets()) { + if (packet.timestamp >= duration) break; + + const meta = { decoderConfig: isFirst ? hevcDecoderConfig! : undefined }; + await videoSource1.add(packet, meta); + await videoSource2.add(packet, meta); + isFirst = false; } await output.finalize(); diff --git a/test/public/video-h265.mp4 b/test/public/video-h265.mp4 new file mode 100644 index 0000000..46ead9c Binary files /dev/null and b/test/public/video-h265.mp4 differ