diff --git a/docs/guide/packets-and-samples.md b/docs/guide/packets-and-samples.md index aa5c295..8b37d3c 100644 --- a/docs/guide/packets-and-samples.md +++ b/docs/guide/packets-and-samples.md @@ -176,7 +176,7 @@ Negative sequence numbers mean the packet's ordering is undefined. When creating ### Cloning packets -Use the `clone` method to create a new packet from an existing packet. While doing so, you can change its timestamp and duration. +Use the `clone` method to create a new packet from an existing packet. While doing so, you can partially change its data. ```ts // Creates a clone identical to the original: packet.clone(); diff --git a/examples/file-compression/file-compression.ts b/examples/file-compression/file-compression.ts index 08b821d..0992dca 100644 --- a/examples/file-compression/file-compression.ts +++ b/examples/file-compression/file-compression.ts @@ -75,7 +75,7 @@ const compressFile = async (resource: File | string) => { let progress = 0; currentConversion.onProgress = newProgress => progress = newProgress; - const fileDuration = await input.computeDuration(); + const fileDuration = (await input.computeDuration()) - (await input.getFirstTimestamp()); const startTime = performance.now(); const updateProgress = () => { diff --git a/src/conversion.ts b/src/conversion.ts index db0f55a..a303c58 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -84,7 +84,7 @@ export type ConversionOptions = { trim?: { /** * The time in the input file in seconds at which the output file should start. Must be less than `end`. - * Defaults to 0 when omitted. + * When omitted, defaults to the start timestamp of the input or to 0, whichever is higher. */ start?: number; /** @@ -462,9 +462,9 @@ export class Conversion { /** @internal */ _options: ConversionOptions; /** @internal */ - _startTimestamp: number; + _startTimestamp!: number; /** @internal */ - _endTimestamp: number; + _endTimestamp!: number; /** @internal */ _addedCounts: Record = { @@ -592,9 +592,6 @@ export class Conversion { this.input = options.input; this.output = options.output; - this._startTimestamp = options.trim?.start ?? 0; - this._endTimestamp = options.trim?.end ?? Infinity; - const { promise: started, resolve: start } = promiseWithResolvers(); this._started = started; this._start = start; @@ -602,6 +599,14 @@ export class Conversion { /** @internal */ async _init() { + this._startTimestamp = this._options.trim?.start ?? Math.max( + await this.input.getFirstTimestamp(), + // Samples can also have negative timestamps, but the meaning typically is "don't present me", so let's cut + // those out by default. + 0, + ); + this._endTimestamp = this._options.trim?.end ?? Infinity; + const inputTracks = await this.input.getTracks(); const outputTrackCounts = this.output.format.getSupportedTrackCounts(); @@ -900,8 +905,7 @@ export class Conversion { const firstTimestamp = await track.getFirstTimestamp(); const needsTranscode = !!trackOptions.forceTranscode - || this._startTimestamp > 0 - || firstTimestamp < 0 + || firstTimestamp < this._startTimestamp || !!trackOptions.frameRate || trackOptions.keyFrameInterval !== undefined || trackOptions.process !== undefined; @@ -943,17 +947,19 @@ export class Conversion { return; } - if (alpha === 'discard') { - // Feels hacky given that the rest of the packet is readonly. But, works for now. - delete packet.sideData.alpha; - delete packet.sideData.alphaByteLength; - } + const modifiedPacket = packet.clone({ + timestamp: packet.timestamp - this._startTimestamp, + sideData: alpha === 'discard' + ? {} // Remove alpha side data + : packet.sideData, + }); + assert(modifiedPacket.timestamp >= 0); - this._reportProgress(track.id, packet.timestamp); - await source.add(packet, meta); + this._reportProgress(track.id, modifiedPacket.timestamp); + await source.add(modifiedPacket, meta); - if (this._synchronizer.shouldWait(track.id, packet.timestamp)) { - await this._synchronizer.wait(packet.timestamp); + if (this._synchronizer.shouldWait(track.id, modifiedPacket.timestamp)) { + await this._synchronizer.wait(modifiedPacket.timestamp); } } @@ -1314,8 +1320,7 @@ export class Conversion { let sampleRate = trackOptions.sampleRate ?? originalSampleRate; let needsResample = numberOfChannels !== originalNumberOfChannels || sampleRate !== originalSampleRate - || this._startTimestamp > 0 - || firstTimestamp < 0; + || firstTimestamp < this._startTimestamp; let audioCodecs = this.output.format.getSupportedAudioCodecs(); if ( @@ -1346,11 +1351,16 @@ export class Conversion { return; } - this._reportProgress(track.id, packet.timestamp); - await source.add(packet, meta); + const modifiedPacket = packet.clone({ + timestamp: packet.timestamp - this._startTimestamp, + }); + assert(modifiedPacket.timestamp >= 0); - if (this._synchronizer.shouldWait(track.id, packet.timestamp)) { - await this._synchronizer.wait(packet.timestamp); + this._reportProgress(track.id, modifiedPacket.timestamp); + await source.add(modifiedPacket, meta); + + if (this._synchronizer.shouldWait(track.id, modifiedPacket.timestamp)) { + await this._synchronizer.wait(modifiedPacket.timestamp); } } @@ -1449,6 +1459,9 @@ export class Conversion { return; } + // Offset the timestamp as needed + sample.setTimestamp(sample.timestamp - this._startTimestamp); + await this._registerAudioSample(track, trackOptions, source, sample); sample.close(); } diff --git a/src/mpeg-ts/mpeg-ts-demuxer.ts b/src/mpeg-ts/mpeg-ts-demuxer.ts index 4a253ce..f5272c4 100644 --- a/src/mpeg-ts/mpeg-ts-demuxer.ts +++ b/src/mpeg-ts/mpeg-ts-demuxer.ts @@ -228,20 +228,13 @@ export class MpegTsDemuxer extends Demuxer { switch (streamType) { case MpegTsStreamType.MP3_MPEG1: - case MpegTsStreamType.MP3_MPEG2: { - info = { - type: 'audio', - codec: 'mp3', - aacCodecInfo: null, - numberOfChannels: -1, - sampleRate: -1, - }; - }; break; - + case MpegTsStreamType.MP3_MPEG2: case MpegTsStreamType.AAC: { + const codec = streamType === MpegTsStreamType.AAC ? 'aac' : 'mp3'; + info = { type: 'audio', - codec: 'aac', + codec, aacCodecInfo: null, numberOfChannels: -1, sampleRate: -1, @@ -268,6 +261,11 @@ export class MpegTsDemuxer extends Demuxer { reorderSize: -1, }; }; break; + + default: { + // If we don't recognize the codec, we don't surface the track at all. This is because + // we can't determine its metadata and also have no idea how to packetize its data. + } } if (info) { diff --git a/src/packet.ts b/src/packet.ts index fd0e93d..2fe7269 100644 --- a/src/packet.ts +++ b/src/packet.ts @@ -234,31 +234,51 @@ export class EncodedPacket { ); } - /** Clones this packet while optionally updating timing information. */ + /** Clones this packet while optionally modifying the new packet's data. */ clone(options?: { + /** The data of the cloned packet. */ + data?: Uint8Array; + /** The type of the cloned packet. */ + type?: PacketType; /** The timestamp of the cloned packet in seconds. */ timestamp?: number; /** The duration of the cloned packet in seconds. */ duration?: number; + /** The sequence number of the cloned packet. */ + sequenceNumber?: number; + /** The side data of the cloned packet. */ + sideData?: EncodedPacketSideData; }): EncodedPacket { if (options !== undefined && (typeof options !== 'object' || options === null)) { throw new TypeError('options, when provided, must be an object.'); } + if (options?.data !== undefined && !(options.data instanceof Uint8Array)) { + throw new TypeError('options.data, when provided, must be a Uint8Array.'); + } + if (options?.type !== undefined && options.type !== 'key' && options.type !== 'delta') { + throw new TypeError('options.type, when provided, must be either "key" or "delta".'); + } if (options?.timestamp !== undefined && !Number.isFinite(options.timestamp)) { throw new TypeError('options.timestamp, when provided, must be a number.'); } if (options?.duration !== undefined && !Number.isFinite(options.duration)) { throw new TypeError('options.duration, when provided, must be a number.'); } + if (options?.sequenceNumber !== undefined && !Number.isFinite(options.sequenceNumber)) { + throw new TypeError('options.sequenceNumber, when provided, must be a number.'); + } + if (options?.sideData !== undefined && (typeof options.sideData !== 'object' || options.sideData === null)) { + throw new TypeError('options.sideData, when provided, must be an object.'); + } return new EncodedPacket( - this.data, - this.type, + options?.data ?? this.data, + options?.type ?? this.type, options?.timestamp ?? this.timestamp, options?.duration ?? this.duration, - this.sequenceNumber, + options?.sequenceNumber ?? this.sequenceNumber, this.byteLength, - this.sideData, + options?.sideData ?? this.sideData, ); } } diff --git a/todo.txt b/todo.txt deleted file mode 100644 index bad55fa..0000000 --- a/todo.txt +++ /dev/null @@ -1 +0,0 @@ -- Conversion should offset the start, no? \ No newline at end of file