diff --git a/src/conversion.ts b/src/conversion.ts index 2c010cd..f3ad24c 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -1781,8 +1781,16 @@ export class Conversion { break; } - const adjustedSampleTimestamp = Math.max(0, sample.timestamp + this._timestampOffset); - sample.setTimestamp(adjustedSampleTimestamp); + const clampedStartTimestamp = Math.max(this._startTimestamp, sample.timestamp); + const clampedEndTimestamp = Math.min(this._endTimestamp, sample.timestamp + sample.duration); + + if (clampedStartTimestamp >= clampedEndTimestamp) { + // Wholly out of the trim region + continue; + } + + sample.setTimestamp(clampedStartTimestamp + this._timestampOffset); + sample.setDuration(clampedEndTimestamp - clampedStartTimestamp); this._reportProgress(outputTrackId, sample.timestamp + sample.duration); await source.add(sample); @@ -1874,8 +1882,17 @@ export class Conversion { if ( startPacket - && startPacket.timestamp < this._startTimestamp - && this._copyBoundaryPolicy === 'shrink' + && ( + ( + this._copyBoundaryPolicy === 'shrink' + && startPacket.timestamp < this._startTimestamp + ) + || ( + this._copyBoundaryPolicy === 'expand' + // Check if packet is wholly before the start + && startPacket.timestamp + startPacket.duration <= this._startTimestamp + ) + ) ) { startPacket = await sink.getNextKeyPacket(startPacket); } diff --git a/test/browser/conversion.test.ts b/test/browser/conversion.test.ts index 05e98b9..9a24e85 100644 --- a/test/browser/conversion.test.ts +++ b/test/browser/conversion.test.ts @@ -939,8 +939,8 @@ test('Packet copy, delta frame trim, video transcoded, Matroska', async () => { shiftTolerance: Infinity, }, }, - expectedTimeOffset: 1.484, - videoStartTimestamp: 0.008, + expectedTimeOffset: 1.472, + videoStartTimestamp: 0.028, videoEndTimestamp: 1.008, audioStartTimestamp: 0, audioEndTimestamp: 1.024, @@ -1053,3 +1053,130 @@ const testCopy = async (options: { } } }; + +test('Trim wholly before media data, transcode', async () => { + using input = new Input({ + source: new UrlSource('/demo.mp4'), + formats: ALL_FORMATS, + }); + const output = new Output({ + format: new Mp4OutputFormat(), + target: new BufferTarget(), + }); + + const conversion = await Conversion.init({ + input, + output, + video: { + forceTranscode: true, + }, + audio: { + forceTranscode: true, + }, + trim: { + start: -10, + end: -5, + }, + }); + await conversion.execute(); + + using newInput = new Input({ + source: new BufferSource(output.target.buffer!), + formats: ALL_FORMATS, + }); + expect(await newInput.getPrimaryVideoTrack()).toBeNull(); // No video data + expect(await newInput.getPrimaryAudioTrack()).toBeNull(); // No audio data +}); + +test('Trim wholly before media data, copy', async () => { + using input = new Input({ + source: new UrlSource('/demo.mp4'), + formats: ALL_FORMATS, + }); + const output = new Output({ + format: new Mp4OutputFormat(), + target: new BufferTarget(), + }); + + const conversion = await Conversion.init({ + input, + output, + copy: { mode: 'forced' }, + trim: { + start: -10, + end: -5, + }, + }); + await conversion.execute(); + + using newInput = new Input({ + source: new BufferSource(output.target.buffer!), + formats: ALL_FORMATS, + }); + expect(await newInput.getPrimaryVideoTrack()).toBeNull(); // No video data + expect(await newInput.getPrimaryAudioTrack()).toBeNull(); // No audio data +}); + +test('Trim wholly past media data, transcode', async () => { + using input = new Input({ + source: new UrlSource('/demo.mp4'), + formats: ALL_FORMATS, + }); + const output = new Output({ + format: new Mp4OutputFormat(), + target: new BufferTarget(), + }); + + const conversion = await Conversion.init({ + input, + output, + video: { + forceTranscode: true, + }, + audio: { + forceTranscode: true, + }, + trim: { + start: 10, + end: 15, + }, + }); + await conversion.execute(); + + using newInput = new Input({ + source: new BufferSource(output.target.buffer!), + formats: ALL_FORMATS, + }); + expect(await newInput.getPrimaryVideoTrack()).toBeNull(); // No video data + expect(await newInput.getPrimaryAudioTrack()).toBeNull(); // No audio data +}); + +test('Trim wholly past media data, copy', async () => { + using input = new Input({ + source: new UrlSource('/demo.mp4'), + formats: ALL_FORMATS, + }); + const output = new Output({ + format: new Mp4OutputFormat(), + target: new BufferTarget(), + }); + + const conversion = await Conversion.init({ + input, + output, + copy: { mode: 'forced' }, + trim: { + start: 10, + end: 15, + }, + }); + await conversion.execute(); + + using newInput = new Input({ + source: new BufferSource(output.target.buffer!), + formats: ALL_FORMATS, + }); + // Don't test video yet; requires extensions to packet fetching logic. B-frames!! + // expect(await newInput.getPrimaryVideoTrack()).toBeNull(); // No video data + expect(await newInput.getPrimaryAudioTrack()).toBeNull(); // No audio data +});