diff --git a/dev/convert.html b/dev/convert.html index f85cc61..c94dbb9 100644 --- a/dev/convert.html +++ b/dev/convert.html @@ -96,7 +96,8 @@ input, output, audio: { - discard: true, + codec: 'aac', + forceTranscode: true, //forceTranscode: true, //sampleFormat: 's16', }, @@ -125,7 +126,7 @@ }, */ video: { - //discard: true, + discard: true, }, tags: {} ?? { title: 'Bigggy', @@ -145,7 +146,9 @@ } }, trim: { - end: 10, + start: 300.14984567374756 - 100, + end: 310.1548298151939 - 100, + //end: 10, //start: startTime, //end: startTime + 2, }, diff --git a/docs/index.md b/docs/index.md index 94351ff..a67c670 100644 --- a/docs/index.md +++ b/docs/index.md @@ -113,6 +113,7 @@ const sponsors = { ], individual: [ { image: '/sponsors/pintura-labs.png', name: 'Pintura Labs', url: 'https://pqina.nl/pintura/' }, + { image: 'https://avatars.githubusercontent.com/u/189556080', name: 'Cursorful', url: 'https://github.com/cursorful' }, { image: 'https://avatars.githubusercontent.com/u/11537072', name: 'Threema', url: 'https://threema.com/en' }, { image: 'https://avatars.githubusercontent.com/u/82552321', name: 'Polotno', url: 'https://github.com/polotno-project' }, { image: 'https://avatars.githubusercontent.com/u/489051', name: 'Roman Rädle', url: 'https://github.com/raedle' }, @@ -134,6 +135,7 @@ const sponsors = { { image: 'https://avatars.githubusercontent.com/u/255616819', name: 'cronischarles-del', url: 'https://github.com/cronischarles-del' }, { image: 'https://avatars.githubusercontent.com/u/37973863', name: 'Gomi', url: 'https://github.com/gxy5202' }, { image: 'https://avatars.githubusercontent.com/u/36898190', name: 'jepcd', url: 'https://github.com/jepcd' }, + { image: 'https://avatars.githubusercontent.com/u/695166', name: 'Takenori Nakagawa', url: 'https://github.com/ww24' }, { image: 'https://avatars.githubusercontent.com/u/63088713', name: 'taf2000', url: 'https://github.com/taf2000' }, { image: 'https://avatars.githubusercontent.com/u/58149663', name: 'H7GhosT', url: 'https://github.com/H7GhosT' }, { image: 'https://avatars.githubusercontent.com/u/91711202', name: 'ihasq', url: 'https://github.com/ihasq' }, diff --git a/package-lock.json b/package-lock.json index 0e4eb7d..11177a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.43.1", + "version": "1.44.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.43.1", + "version": "1.44.2", "license": "MPL-2.0", "workspaces": [ "packages/*" @@ -8278,9 +8278,9 @@ } }, "node_modules/mediabunny": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.43.0.tgz", - "integrity": "sha512-QGj6PcXXQvhs+29Pl25z3THv17g31k18zHUrJ25UQc5KWVN0vMKbMbB8KuHXNcseALIhbs6nh+wskimaeH/EEw==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.44.0.tgz", + "integrity": "sha512-rs54ixLAe5MSa6q35MVsVXyXZ+aYkEZYhAw57bseil+mmxFuqAS8YrELaWBcsqAJLCTb8Tse4AoYEhtkwCHYlQ==", "license": "MPL-2.0", "peer": true, "workspaces": [ @@ -12851,7 +12851,7 @@ }, "packages/aac-encoder": { "name": "@mediabunny/aac-encoder", - "version": "1.43.1", + "version": "1.44.1", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -12866,7 +12866,7 @@ }, "packages/ac3": { "name": "@mediabunny/ac3", - "version": "1.43.1", + "version": "1.44.1", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -12881,7 +12881,7 @@ }, "packages/flac-encoder": { "name": "@mediabunny/flac-encoder", - "version": "1.43.1", + "version": "1.44.1", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" @@ -12896,7 +12896,7 @@ }, "packages/mp3-encoder": { "name": "@mediabunny/mp3-encoder", - "version": "1.43.1", + "version": "1.44.1", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" diff --git a/package.json b/package.json index 9199a69..bf48225 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "author": "Vanilagy", - "version": "1.43.1", + "version": "1.44.2", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "type": "module", "workspaces": [ diff --git a/packages/aac-encoder/package.json b/packages/aac-encoder/package.json index 40b21e5..9a770f4 100644 --- a/packages/aac-encoder/package.json +++ b/packages/aac-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/aac-encoder", "author": "Vanilagy", - "version": "1.43.1", + "version": "1.44.2", "description": "AAC encoder extension for Mediabunny, based on FFmpeg.", "main": "./dist/bundles/mediabunny-aac-encoder.mjs", "module": "./dist/bundles/mediabunny-aac-encoder.mjs", diff --git a/packages/ac3/package.json b/packages/ac3/package.json index 1c483fe..973d354 100644 --- a/packages/ac3/package.json +++ b/packages/ac3/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/ac3", "author": "Vanilagy", - "version": "1.43.1", + "version": "1.44.2", "description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on FFmpeg.", "main": "./dist/bundles/mediabunny-ac3.mjs", "module": "./dist/bundles/mediabunny-ac3.mjs", diff --git a/packages/flac-encoder/package.json b/packages/flac-encoder/package.json index f0d2bd7..a0d7950 100644 --- a/packages/flac-encoder/package.json +++ b/packages/flac-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/flac-encoder", "author": "Vanilagy", - "version": "1.43.1", + "version": "1.44.2", "description": "FLAC encoder extension for Mediabunny, based on libFLAC.", "main": "./dist/bundles/mediabunny-flac-encoder.mjs", "module": "./dist/bundles/mediabunny-flac-encoder.mjs", diff --git a/packages/mp3-encoder/package.json b/packages/mp3-encoder/package.json index 619e2bd..97b4dc1 100644 --- a/packages/mp3-encoder/package.json +++ b/packages/mp3-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/mp3-encoder", "author": "Vanilagy", - "version": "1.43.1", + "version": "1.44.2", "description": "MP3 encoder extension for Mediabunny, based on LAME.", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs", diff --git a/src/adts/adts-muxer.ts b/src/adts/adts-muxer.ts index def99c2..d0e49a0 100644 --- a/src/adts/adts-muxer.ts +++ b/src/adts/adts-muxer.ts @@ -60,7 +60,7 @@ export class AdtsMuxer extends Muxer { const release = await this.mutex.acquire(); try { - this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key'); + this.validateTimestamp(track, packet.timestamp, packet.type === 'key'); // First packet - determine input format from metadata if (this.inputIsAdts === null) { diff --git a/src/conversion.ts b/src/conversion.ts index 36948f2..f286ade 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -1754,6 +1754,7 @@ export class Conversion { startTime: this._startTimestamp, endTime: this._endTimestamp, onSample: async (sample) => { + assert(sample.timestamp >= this._startTimestamp); sample.setTimestamp(sample.timestamp - this._startTimestamp); await this._registerAudioSample(trackOptions, outputTrackId, source, sample); diff --git a/src/flac/flac-muxer.ts b/src/flac/flac-muxer.ts index b022dbe..02e2067 100644 --- a/src/flac/flac-muxer.ts +++ b/src/flac/flac-muxer.ts @@ -214,7 +214,7 @@ export class FlacMuxer extends Muxer { const release = await this.mutex.acquire(); try { - this.validateAndNormalizeTimestamp( + this.validateTimestamp( track, packet.timestamp, packet.type === 'key', diff --git a/src/hls/hls-muxer.ts b/src/hls/hls-muxer.ts index f5d88e9..fb2e39e 100644 --- a/src/hls/hls-muxer.ts +++ b/src/hls/hls-muxer.ts @@ -614,17 +614,15 @@ export class HlsMuxer extends Muxer { const release = await playlist.mutex.acquire(); try { - const timestamp = this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key'); - const adjustedPacket = packet.clone({ timestamp }); - - trackData.packets.push(adjustedPacket); + this.validateTimestamp(track, packet.timestamp, packet.type === 'key'); + trackData.packets.push(packet); if (playlist.currentSegmentStartTimestamp === null) { - playlist.currentSegmentStartTimestamp = adjustedPacket.timestamp; + playlist.currentSegmentStartTimestamp = packet.timestamp; } else if (!playlist.currentSegmentStartTimestampIsFixed) { playlist.currentSegmentStartTimestamp = Math.min( playlist.currentSegmentStartTimestamp, - adjustedPacket.timestamp, + packet.timestamp, ); } @@ -645,17 +643,15 @@ export class HlsMuxer extends Muxer { const release = await playlist.mutex.acquire(); try { - const timestamp = this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key'); - const adjustedPacket = packet.clone({ timestamp }); - - trackData.packets.push(adjustedPacket); + this.validateTimestamp(track, packet.timestamp, packet.type === 'key'); + trackData.packets.push(packet); if (playlist.currentSegmentStartTimestamp === null) { - playlist.currentSegmentStartTimestamp = adjustedPacket.timestamp; + playlist.currentSegmentStartTimestamp = packet.timestamp; } else if (!playlist.currentSegmentStartTimestampIsFixed) { playlist.currentSegmentStartTimestamp = Math.min( playlist.currentSegmentStartTimestamp, - adjustedPacket.timestamp, + packet.timestamp, ); } diff --git a/src/isobmff/isobmff-muxer.ts b/src/isobmff/isobmff-muxer.ts index d456d04..42c6ce8 100644 --- a/src/isobmff/isobmff-muxer.ts +++ b/src/isobmff/isobmff-muxer.ts @@ -600,7 +600,7 @@ export class IsobmffMuxer extends Muxer { packetData = concatNalUnitsInLengthPrefixed(nalUnits, 4); } - const timestamp = this.validateAndNormalizeTimestamp( + this.validateTimestamp( trackData.track, packet.timestamp, packet.type === 'key', @@ -608,7 +608,7 @@ export class IsobmffMuxer extends Muxer { const internalSample = this.createSampleForTrack( trackData, packetData, - timestamp, + packet.timestamp, packet.duration, packet.type, ); @@ -638,11 +638,13 @@ export class IsobmffMuxer extends Muxer { packetData = packetData.subarray(headerLength); } - let timestamp = this.validateAndNormalizeTimestamp( + this.validateTimestamp( trackData.track, packet.timestamp, packet.type === 'key', ); + + let timestamp = packet.timestamp; let duration = packet.duration; if (trackData.info.requiresPcmTransformation) { @@ -718,7 +720,7 @@ export class IsobmffMuxer extends Muxer { try { const trackData = this.getSubtitleTrackData(track, meta); - this.validateAndNormalizeTimestamp(trackData.track, cue.timestamp, true); + this.validateTimestamp(trackData.track, cue.timestamp, true); if (track.source._codec === 'webvtt') { trackData.cueQueue.push(cue); diff --git a/src/matroska/matroska-muxer.ts b/src/matroska/matroska-muxer.ts index da60565..dabeaa4 100644 --- a/src/matroska/matroska-muxer.ts +++ b/src/matroska/matroska-muxer.ts @@ -907,7 +907,9 @@ export class MatroskaMuxer extends Muxer { const trackData = this.getVideoTrackData(track, packet, meta); const isKeyFrame = packet.type === 'key'; - let timestamp = this.validateAndNormalizeTimestamp(trackData.track, packet.timestamp, isKeyFrame); + this.validateTimestamp(trackData.track, packet.timestamp, isKeyFrame); + + let timestamp = packet.timestamp; let duration = packet.duration; if (track.metadata.frameRate !== undefined) { @@ -950,9 +952,9 @@ export class MatroskaMuxer extends Muxer { } const isKeyFrame = packet.type === 'key'; - const timestamp = this.validateAndNormalizeTimestamp(trackData.track, packet.timestamp, isKeyFrame); - const audioChunk = this.createInternalChunk(packetData, timestamp, packet.duration, packet.type); + this.validateTimestamp(trackData.track, packet.timestamp, isKeyFrame); + const audioChunk = this.createInternalChunk(packetData, packet.timestamp, packet.duration, packet.type); trackData.chunkQueue.push(audioChunk); await this.interleaveChunks(); } finally { @@ -966,10 +968,10 @@ export class MatroskaMuxer extends Muxer { try { const trackData = this.getSubtitleTrackData(track, meta); - const timestamp = this.validateAndNormalizeTimestamp(trackData.track, cue.timestamp, true); + this.validateTimestamp(trackData.track, cue.timestamp, true); let bodyText = cue.text; - const timestampMs = Math.round(timestamp * 1000); + const timestampMs = Math.round(cue.timestamp * 1000); // Replace in-body timestamps so that they're relative to the cue start time inlineTimestampRegex.lastIndex = 0; @@ -985,7 +987,7 @@ export class MatroskaMuxer extends Muxer { const subtitleChunk = this.createInternalChunk( body, - timestamp, + cue.timestamp, cue.duration, 'key', additions.trim() ? textEncoder.encode(additions) : null, diff --git a/src/media-source.ts b/src/media-source.ts index c24fd79..c739cc2 100644 --- a/src/media-source.ts +++ b/src/media-source.ts @@ -78,11 +78,6 @@ export abstract class MediaSource { _closingPromise: Promise | null = null; /** @internal */ _closed = false; - /** - * @internal - * A time offset in seconds that is added to all timestamps generated by this source. - */ - _timestampOffset = 0; /** @internal */ _ensureValidAdd() { @@ -1664,7 +1659,7 @@ export class MediaStreamVideoTrackSource extends VideoSource { let frameCount = 0; let errored = false; let lastSampleTimestamp: number | null = null; - let pauseOffset = 0; + let timestampOffset = 0; const tick = () => { assert(frameRate !== null); @@ -1732,9 +1727,7 @@ export class MediaStreamVideoTrackSource extends VideoSource { // pause. Doing it like this instead of simply keeping track of the paused time is better since // it retains the frame rate of the underlying source. const timeDelta = currentTimestamp - lastSampleTimestamp; - // We modify this field instead of _timestampOffset since we still might have data in flight - // in the encoder, with which we don't want to mess. - pauseOffset -= timeDelta; + timestampOffset -= timeDelta; } lastSampleTimestamp = currentTimestamp; } @@ -1762,7 +1755,7 @@ export class MediaStreamVideoTrackSource extends VideoSource { } } - this._timestampOffset = target - firstVideoFrameTimestamp; + timestampOffset = target - firstVideoFrameTimestamp; } lastSampleTimestamp = currentTimestamp; @@ -1774,7 +1767,7 @@ export class MediaStreamVideoTrackSource extends VideoSource { } const sample = new VideoSample(videoFrame, { - timestamp: currentTimestamp + pauseOffset, + timestamp: currentTimestamp + timestampOffset, }); void this._encoder.add(sample, true) @@ -2763,7 +2756,7 @@ export class MediaStreamAudioTrackSource extends AudioSource { let firstAudioDataTimestamp: number | null = null; let errored = false; let lastSampleTimestamp: number | null = null; - let pauseOffset = 0; + let timestampOffset = 0; const onAudioSample = (audioSample: AudioSample) => { if (errored) { @@ -2781,9 +2774,7 @@ export class MediaStreamAudioTrackSource extends AudioSource { // the pause. Doing it like this instead of simply keeping track of the paused time is better // since it retains the sample rate of the underlying source. const timeDelta = currentTimestamp - lastSampleTimestamp; - // We modify this field instead of _timestampOffset since we still might have data in flight - // in the encoder, with which we don't want to mess. - pauseOffset -= timeDelta; + timestampOffset -= timeDelta; } lastSampleTimestamp = currentTimestamp; } @@ -2811,7 +2802,7 @@ export class MediaStreamAudioTrackSource extends AudioSource { } } - this._timestampOffset = target - firstAudioDataTimestamp; + timestampOffset = target - firstAudioDataTimestamp; } lastSampleTimestamp = currentTimestamp; @@ -2822,7 +2813,7 @@ export class MediaStreamAudioTrackSource extends AudioSource { return; } - audioSample.setTimestamp(currentTimestamp + pauseOffset); + audioSample.setTimestamp(currentTimestamp + timestampOffset); void this._encoder.add(audioSample, true) .catch((error) => { diff --git a/src/mp3/mp3-muxer.ts b/src/mp3/mp3-muxer.ts index 97546b3..f1a1413 100644 --- a/src/mp3/mp3-muxer.ts +++ b/src/mp3/mp3-muxer.ts @@ -110,7 +110,7 @@ export class Mp3Muxer extends Muxer { this.frameCount++; } - this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key'); + this.validateTimestamp(track, packet.timestamp, packet.type === 'key'); if (writeXingHeader) { this.framePositions.push(this.writer.getPos()); diff --git a/src/mpeg-ts/mpeg-ts-muxer.ts b/src/mpeg-ts/mpeg-ts-muxer.ts index dfa0f7e..014918a 100644 --- a/src/mpeg-ts/mpeg-ts-muxer.ts +++ b/src/mpeg-ts/mpeg-ts-muxer.ts @@ -227,7 +227,7 @@ export class MpegTsMuxer extends Muxer { try { const trackData = this.getVideoTrackData(track, meta); - const timestamp = this.validateAndNormalizeTimestamp( + this.validateTimestamp( trackData.track, packet.timestamp, packet.type === 'key', @@ -241,7 +241,7 @@ export class MpegTsMuxer extends Muxer { trackData.timestampProcessingQueue.push({ data: preparedData, - presentationTimestamp: timestamp, + presentationTimestamp: packet.timestamp, decodeTimestamp: null, isKeyframe: packet.type === 'key', }); @@ -260,7 +260,7 @@ export class MpegTsMuxer extends Muxer { try { const trackData = this.getAudioTrackData(track, meta); - const timestamp = this.validateAndNormalizeTimestamp( + this.validateTimestamp( trackData.track, packet.timestamp, packet.type === 'key', @@ -274,7 +274,7 @@ export class MpegTsMuxer extends Muxer { trackData.timestampProcessingQueue.push({ data: preparedData, - presentationTimestamp: timestamp, + presentationTimestamp: packet.timestamp, decodeTimestamp: null, isKeyframe: packet.type === 'key', }); diff --git a/src/muxer.ts b/src/muxer.ts index fc9df5c..b945770 100644 --- a/src/muxer.ts +++ b/src/muxer.ts @@ -42,9 +42,7 @@ export abstract class Muxer { maxTimestampBeforeLastKeyPacket: number | null; }>(); - protected validateAndNormalizeTimestamp(track: OutputTrack, timestampInSeconds: number, isKeyPacket: boolean) { - timestampInSeconds += track.source._timestampOffset; - + protected validateTimestamp(track: OutputTrack, timestampInSeconds: number, isKeyPacket: boolean) { if (timestampInSeconds < 0) { throw new Error(`Timestamps must be non-negative (got ${timestampInSeconds}s).`); } @@ -78,7 +76,5 @@ export abstract class Muxer { timestampInfo.maxTimestamp = Math.max(timestampInfo.maxTimestamp, timestampInSeconds); } - - return timestampInSeconds; } } diff --git a/src/ogg/ogg-muxer.ts b/src/ogg/ogg-muxer.ts index 987eef8..7798f84 100644 --- a/src/ogg/ogg-muxer.ts +++ b/src/ogg/ogg-muxer.ts @@ -267,7 +267,7 @@ export class OggMuxer extends Muxer { try { const trackData = this.getTrackData(track, meta); - this.validateAndNormalizeTimestamp(trackData.track, packet.timestamp, packet.type === 'key'); + this.validateTimestamp(trackData.track, packet.timestamp, packet.type === 'key'); const currentTimestampInSamples = trackData.currentTimestampInSamples; diff --git a/src/resample.ts b/src/resample.ts index 727492d..4686a9d 100644 --- a/src/resample.ts +++ b/src/resample.ts @@ -20,7 +20,6 @@ export class AudioResampler { targetSampleRate: number; sourceNumberOfChannels: number | null = null; targetNumberOfChannels: number; - startTime: number; endTime: number; onSample: (sample: AudioSample) => Promise; @@ -33,6 +32,7 @@ export class AudioResampler { maxWrittenFrame: number | null = null; channelMixer!: (sourceData: Float32Array, sourceFrameIndex: number, targetChannelIndex: number) => number; tempSourceBuffer!: Float32Array; + timestampOffset: number; constructor(options: { targetSampleRate: number; @@ -43,7 +43,6 @@ export class AudioResampler { }) { this.targetSampleRate = options.targetSampleRate; this.targetNumberOfChannels = options.targetNumberOfChannels; - this.startTime = options.startTime; this.endTime = options.endTime; this.onSample = options.onSample; @@ -51,7 +50,11 @@ export class AudioResampler { this.bufferSizeInSamples = this.bufferSizeInFrames * this.targetNumberOfChannels; this.outputBuffer = new Float32Array(this.bufferSizeInSamples); - this.bufferStartFrame = Math.floor(this.startTime * this.targetSampleRate); + + this.bufferStartFrame = Math.floor(options.startTime * this.targetSampleRate); + // Set to ensure that if the buffer start frame lands on a fractional sample, that the first timestamp still + // comes out as exactly startTime + this.timestampOffset = options.startTime - this.bufferStartFrame / this.targetSampleRate; } /** @@ -273,7 +276,7 @@ export class AudioResampler { format: 'f32', sampleRate: this.targetSampleRate, numberOfChannels: this.targetNumberOfChannels, - timestamp: timestampSeconds, + timestamp: timestampSeconds + this.timestampOffset, data: outputData, }); diff --git a/src/wave/wave-muxer.ts b/src/wave/wave-muxer.ts index 7e43a0f..68b1072 100644 --- a/src/wave/wave-muxer.ts +++ b/src/wave/wave-muxer.ts @@ -79,7 +79,7 @@ export class WaveMuxer extends Muxer { this.headerWritten = true; } - this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key'); + this.validateTimestamp(track, packet.timestamp, packet.type === 'key'); if (!this.isRf64 && this.writer.getPos() + packet.data.byteLength >= 2 ** 32) { throw new Error( diff --git a/test/browser/conversion.test.ts b/test/browser/conversion.test.ts index 561fe4f..ceab56c 100644 --- a/test/browser/conversion.test.ts +++ b/test/browser/conversion.test.ts @@ -1,6 +1,12 @@ import { ALL_FORMATS } from '../../src/input-format.js'; import { Input } from '../../src/input.js'; -import { AdtsOutputFormat, HlsOutputFormat, Mp4OutputFormat, MpegTsOutputFormat } from '../../src/output-format.js'; +import { + AdtsOutputFormat, + HlsOutputFormat, + Mp4OutputFormat, + MpegTsOutputFormat, + WavOutputFormat, +} from '../../src/output-format.js'; import { Output, OutputTrackGroup } from '../../src/output.js'; import { BufferSource, CustomPathedSource, UrlSource } from '../../src/source.js'; import { expect, test } from 'vitest'; @@ -369,3 +375,30 @@ test('HLS track assignability can be overridden', async () => { expect(newMasterPlayist).not.toBe(masterPlayist); expect(newMasterPlayist.match(/\.m3u8/g)?.length).toBe(1); }); + +test('Fractional audio sample boundary', async () => { + using input = new Input({ + source: new UrlSource('/trim-buck-bunny-ffmpeg.ts'), + formats: ALL_FORMATS, + }); + + const output = new Output({ + format: new WavOutputFormat(), + target: new BufferTarget(), + }); + + const conversion = await Conversion.init({ + input, + output, + video: { + discard: true, + }, + audio: { + forceTranscode: true, + }, + trim: { + start: 0.4 / 48000, + }, + }); + await conversion.execute(); +});