import { spawnSync } from 'node:child_process'; import { copyFileSync, existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { basename, extname, join, resolve } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { buildAudioExtractionPlan, buildChapterMuxPlan, buildContactSheetPlan, buildConvertPlan, buildFadePlan, buildFastConcatPlan, buildLoudnessAnalysisPlan, buildLoudnessApplyPlan, buildMetadataEditPlan, buildNormalizedConcatPlan, buildPeakAnalysisPlan, buildPeakNormalizationFilter, buildPeakNormalizationPlan, buildPreviewProxyPlan, buildRemuxPlan, buildSceneChangeAnalysisPlan, buildSceneThumbnailSeriesPlan, buildSingleThumbnailPlan, buildSplitPlan, buildStaticWaveformPlan, buildStreamRemovalPlan, buildSubtitleBurnPlan, buildSubtitleConversionPlan, buildSubtitleExtractionPlan, buildSubtitleMuxPlan, buildThumbnailSeriesPlan, buildTimelineExportPlan, buildTransformPlan, buildTrimPlan, buildWaveformAnalysisPlan, createConcatListEntries, parseLoudnessMeasurement, parsePeakMeasurement, parseSceneChangeFrames, resolveThumbnailTimes, streamMapArguments, type ConcatSource, type FFmpegCommandPlan, type SourceReference, } from '../../src/commands'; import { findBuiltInPreset } from '../../src/presets'; const FIXTURES = resolve(process.cwd(), 'tests/fixtures/generated'); const NATIVE_FFMPEG_AVAILABLE = spawnSync('ffmpeg', ['-version'], { stdio: 'ignore' }).status === 0 && spawnSync('ffprobe', ['-version'], { stdio: 'ignore' }).status === 0; const NATIVE_SUBTITLES_FILTER_AVAILABLE = NATIVE_FFMPEG_AVAILABLE && /\bsubtitles\b/u.test( spawnSync('ffmpeg', ['-hide_banner', '-filters'], { encoding: 'utf8', }).stdout ?? '' ); const NATIVE_DRAWTEXT_FILTER_AVAILABLE = NATIVE_FFMPEG_AVAILABLE && /\bdrawtext\b/u.test( spawnSync('ffmpeg', ['-hide_banner', '-filters'], { encoding: 'utf8', }).stdout ?? '' ); const describeNative = NATIVE_FFMPEG_AVAILABLE ? describe : describe.skip; const itNativeSubtitles = NATIVE_SUBTITLES_FILTER_AVAILABLE ? it : it.skip; const itNativeDrawtext = NATIVE_DRAWTEXT_FILTER_AVAILABLE ? it : it.skip; interface NativeExecution { readonly outputPaths: readonly string[]; readonly stdout: string; readonly stderr: string; } let suiteDirectory = ''; let executionSequence = 0; beforeAll(() => { suiteDirectory = mkdtempSync(join(tmpdir(), 'av-tools-command-runtime-')); }); afterAll(() => { if (suiteDirectory) { rmSync(suiteDirectory, { recursive: true, force: true }); } }); function fixture(name: string): string { const path = join(FIXTURES, name); if (!existsSync(path)) { throw new Error(`Missing generated fixture: ${name}`); } return path; } function source( id: string, sourceIndex: number, fileName: string ): SourceReference { return { id, sourceIndex, fileName }; } function runNative( plan: FFmpegCommandPlan, sourceFiles: Readonly>, logLevel: 'warning' | 'info' = 'warning' ): NativeExecution { executionSequence += 1; const directory = join( suiteDirectory, `${String(executionSequence).padStart(3, '0')}-${plan.operation}` ); mkdirSync(directory, { recursive: true }); const replacements = new Map(); plan.inputs.forEach((input, index) => { const sourcePath = sourceFiles[input.id]; if (!sourcePath) { throw new Error(`No fixture was supplied for planned input ${input.id}`); } const extension = extname(sourcePath) || '.bin'; const inputPath = join( directory, `input-${String(index).padStart(3, '0')}${extension}` ); copyFileSync(sourcePath, inputPath); replacements.set(input.path, inputPath); }); const outputPaths = plan.outputs.map((output, index) => { const outputPath = join( directory, `${String(index).padStart(3, '0')}-${output.fileName}` ); replacements.set(output.path, outputPath); return outputPath; }); const temporaryPaths = plan.temporaryFiles.map((temporary, index) => { const extension = extname(temporary.path) || '.tmp'; const temporaryPath = join( directory, `temporary-${String(index).padStart(3, '0')}${extension}` ); replacements.set(temporary.path, temporaryPath); return temporaryPath; }); const substitute = (value: string): string => { let result = value; for (const [virtualPath, nativePath] of [...replacements.entries()].sort( ([left], [right]) => right.length - left.length )) { result = result.replaceAll(virtualPath, nativePath); } return result; }; plan.temporaryFiles.forEach((temporary, index) => { const path = temporaryPaths[index]; if (!path) { throw new Error(`Missing native temporary path ${index}`); } if (typeof temporary.content === 'string') { writeFileSync(path, substitute(temporary.content), 'utf8'); } else { writeFileSync(path, temporary.content); } }); const execution = spawnSync( 'ffmpeg', [ '-hide_banner', '-nostdin', '-y', '-loglevel', logLevel, ...plan.args.map(substitute), ], { encoding: 'utf8', maxBuffer: 20 * 1024 * 1024, timeout: 60_000, } ); const stdout = execution.stdout ?? ''; const stderr = execution.stderr ?? ''; if (execution.status !== 0) { throw new Error( [ `Native FFmpeg rejected ${plan.operation} (status ${String(execution.status)}, signal ${String(execution.signal)}).`, `argv: ${plan.args.map(substitute).join(' ')}`, stderr, stdout, ].join('\n') ); } for (const path of outputPaths) { expect(statSync(path).size, basename(path)).toBeGreaterThan(0); } return { outputPaths, stdout, stderr }; } function nativeCommand(args: readonly string[], outputPath: string): void { const execution = spawnSync( 'ffmpeg', ['-hide_banner', '-nostdin', '-y', '-loglevel', 'error', ...args], { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, timeout: 60_000, } ); if (execution.status !== 0) { throw new Error(execution.stderr || `Could not create ${outputPath}`); } expect(statSync(outputPath).size).toBeGreaterThan(0); } function probe(path: string): { readonly streams?: readonly { readonly index?: number; readonly codec_name?: string; readonly codec_type?: string; readonly width?: number; readonly height?: number; readonly tags?: Readonly>; readonly disposition?: Readonly>; }[]; readonly chapters?: readonly { readonly start_time?: string; readonly end_time?: string; readonly tags?: Readonly>; }[]; readonly format?: { readonly duration?: string; readonly tags?: Readonly>; }; } { const result = spawnSync( 'ffprobe', [ '-v', 'error', '-show_streams', '-show_chapters', '-show_format', '-of', 'json', path, ], { encoding: 'utf8', timeout: 20_000 } ); if (result.status !== 0) { throw new Error(result.stderr || `Could not probe ${path}`); } return JSON.parse(result.stdout) as ReturnType; } function preset(id: string) { const result = findBuiltInPreset(id); if (!result) { throw new Error(`Missing built-in preset ${id}`); } return result; } function concatSource( id: string, sourceIndex: number, fileName: string, width: number, height: number, hasAudio = true ): ConcatSource { return { id, sourceIndex, fileName, durationSeconds: 1, streams: [ { kind: 'video', codec: 'h264', width, height, pixelFormat: 'yuv420p', timeBase: '1/10240', }, ...(hasAudio ? [ { kind: 'audio' as const, codec: 'aac', sampleRate: 16_000, channelLayout: 'mono', timeBase: '1/16000', }, ] : []), ], }; } describe('command-plan runtime hardening', () => { it('serializes validated ffconcat in/out directives without source names', () => { expect( createConcatListEntries([ { path: '/input/job-safe/source-0.mp4', inPointSeconds: 0.25, outPointSeconds: 0.75, }, ]) ).toBe( "file '/input/job-safe/source-0.mp4'\ninpoint 0.25\noutpoint 0.75\n" ); expect(() => createConcatListEntries([ { path: '/input/job-safe/source-0.mp4', inPointSeconds: 1, outPointSeconds: 0.5, }, ]) ).toThrow(/outpoint/iu); }); it('bounds and uniquely names split outputs and gives fast segments independent inputs', () => { const media = source('media', 0, 'pattern-av.mp4'); const plan = buildSplitPlan({ jobId: 'split-hardening', source: media, durationSeconds: 2, definition: { type: 'clips', clips: [ { startSeconds: 0, endSeconds: 0.8, label: 'same' }, { startSeconds: 1, endSeconds: 1.8, label: 'same' }, ], }, mode: 'fast', targetExtension: 'mp4', }); expect(plan.outputs.map((output) => output.fileName)).toEqual([ 'pattern-av-split-001-same.mp4', 'pattern-av-split-002-same.mp4', ]); expect(plan.args.filter((argument) => argument === '-i')).toHaveLength(2); expect(() => buildSplitPlan({ jobId: 'too-many-splits', source: media, durationSeconds: 100, definition: { type: 'equal-duration', segmentDurationSeconds: 0.1, }, mode: 'fast', targetExtension: 'mp4', }) ).toThrow(/at most 255/iu); }); it('rejects container mismatches, ambiguous stream groups, and unsafe transform values', () => { expect(() => buildMetadataEditPlan({ jobId: 'muxer-mismatch', source: source('media', 0, 'pattern-av.mp4'), edits: {}, targetExtension: 'mkv', targetMuxer: 'mp4', }) ).toThrow(/does not match/iu); expect(() => streamMapArguments({ video: [0], audio: [0] })).toThrow( /more than one stream group/iu ); expect(() => buildTransformPlan({ jobId: 'fractional-resize', source: source('media', 0, 'pattern-av.mp4'), preset: preset('mp4-h264-balanced'), settings: { resize: { sourceWidth: 160, sourceHeight: 90, width: 80.5, mode: 'fit', }, }, }) ).toThrow(/integers/iu); expect(() => buildTransformPlan({ jobId: 'filter-injection', source: source('media', 0, 'pattern-av.mp4'), preset: preset('mp4-h264-balanced'), settings: { resize: { sourceWidth: 160, sourceHeight: 90, width: 80, height: 80, mode: 'contain', paddingColor: 'black,scale=1:1', }, }, }) ).toThrow(/padding colour/iu); }); it('requires explicit subtitle loss and bounds generated image counts', () => { const sources = [ { ...concatSource('first', 0, 'compatible-a.mp4', 128, 72), streams: [ ...concatSource('first', 0, 'compatible-a.mp4', 128, 72).streams, { kind: 'subtitle' as const, codec: 'mov_text', timeBase: '1/1000' }, ], }, concatSource('second', 1, 'compatible-b.mp4', 128, 72), ]; const options = { jobId: 'subtitle-concat-policy', sources, targetExtension: 'mp4', targetMuxer: 'mp4', preset: preset('mp4-h264-balanced'), width: 128, height: 72, frameRate: 10, missingAudioPolicy: 'reject' as const, }; expect(() => buildNormalizedConcatPlan(options)).toThrow( /cannot retain subtitle/iu ); expect( buildNormalizedConcatPlan({ ...options, subtitlePolicy: 'drop-all', }).diagnostics ).toContainEqual(expect.objectContaining({ code: 'dropped-subtitles' })); expect(() => resolveThumbnailTimes({ durationSeconds: 1000, intervalSeconds: 1, }) ).toThrow(/at most 255/iu); }); it('requires finite measured peaks and validates subtitle stream indexes', () => { expect(() => parsePeakMeasurement('max_volume: -inf dB')).toThrow( /silent audio/iu ); expect(() => buildPeakNormalizationFilter(-1, { inputPeakDbfs: -80 }) ).toThrow(/more than 60 dB/iu); expect(() => buildSubtitleExtractionPlan({ jobId: 'invalid-subtitle-index', source: source('media', 0, 'pattern-av.mp4'), stream: { index: -1, codec: 'subrip' }, outputFormat: 'srt', }) ).toThrow(/stream index/iu); }); it('makes the contact-sheet aspect toggle affect the typed filter graph', () => { const plan = buildContactSheetPlan({ jobId: 'square-contact-cells', source: source('media', 0, 'pattern-av.mp4'), durationSeconds: 2, frameCount: 4, rows: 2, columns: 2, cellWidth: 80, maintainAspect: false, format: 'png', }); expect(plan.args.find((argument) => argument.includes('tile='))).toContain( 'scale=80:80' ); }); }); describeNative('native execution of structured command plans', () => { it('executes conversion, remux, fast/accurate trim, transform, fades, and preview plans', () => { const media = source('media', 0, 'pattern-av.mp4'); const mediaFile = fixture('pattern-av.mp4'); const h264 = preset('mp4-h264-balanced'); const converted = runNative( buildConvertPlan({ jobId: 'convert-native', source: media, preset: h264, streamSelection: { video: [0], audio: [1] }, expectedDurationSeconds: 2, }), { media: mediaFile } ); expect(probe(converted.outputPaths[0]!).streams?.map(streamType)).toEqual([ 'video:h264', 'audio:aac', ]); const gif = runNative( buildConvertPlan({ jobId: 'gif-native', source: media, preset: preset('animated-gif'), streamSelection: { video: [0] }, }), { media: mediaFile } ); expect(probe(gif.outputPaths[0]!).streams?.[0]?.codec_name).toBe('gif'); const attachedPictureGif = runNative( buildConvertPlan({ jobId: 'gif-absolute-stream-native', source: source('cover-audio', 0, 'attached-cover.mp3'), preset: preset('animated-gif'), streamSelection: { video: [1] }, }), { 'cover-audio': fixture('attached-cover.mp3') } ); expect( probe(attachedPictureGif.outputPaths[0]!).streams?.[0]?.codec_name ).toBe('gif'); const remuxed = runNative( buildRemuxPlan({ jobId: 'remux-native', source: media, targetContainer: 'matroska', fileExtension: 'mkv', description: { streams: [ { index: 0, kind: 'video', codec: 'h264' }, { index: 1, kind: 'audio', codec: 'aac' }, ], }, expectedDurationSeconds: 2, }), { media: mediaFile } ); expect(probe(remuxed.outputPaths[0]!).streams).toHaveLength(2); const fastTrim = runNative( buildTrimPlan({ jobId: 'trim-fast-native', source: media, mode: 'fast', startSeconds: 0.4, endSeconds: 1.6, sourceDurationSeconds: 2, targetExtension: 'mp4', }), { media: mediaFile } ); expect( Number(probe(fastTrim.outputPaths[0]!).format?.duration) ).toBeGreaterThan(0); const accurateTrim = runNative( buildTrimPlan({ jobId: 'trim-accurate-native', source: media, mode: 'accurate', startSeconds: 0.4, endSeconds: 1.6, sourceDurationSeconds: 2, preset: h264, }), { media: mediaFile } ); expect( Number(probe(accurateTrim.outputPaths[0]!).format?.duration) ).toBeGreaterThanOrEqual(1.15); expect( Number(probe(accurateTrim.outputPaths[0]!).format?.duration) ).toBeLessThanOrEqual(1.3); const transformed = runNative( buildTransformPlan({ jobId: 'transform-native', source: media, preset: h264, expectedDurationSeconds: 2, settings: { crop: { x: 0, y: 0, width: 120, height: 80, sourceWidth: 160, sourceHeight: 90, requireEvenDimensions: true, }, resize: { sourceWidth: 120, sourceHeight: 80, width: 96, height: 72, mode: 'contain', }, rotation: 180, frameRate: 8, }, }), { media: mediaFile } ); expect(probe(transformed.outputPaths[0]!).streams?.[0]).toMatchObject({ codec_type: 'video', width: 96, height: 72, }); runNative( buildFadePlan({ jobId: 'fades-native', source: media, preset: h264, durationSeconds: 2, audioFadeInSeconds: 0.2, audioFadeOutSeconds: 0.2, videoFadeInSeconds: 0.2, videoFadeOutSeconds: 0.2, }), { media: mediaFile } ); const preview = runNative( buildPreviewProxyPlan({ jobId: 'preview-native', source: media, sourceDurationSeconds: 2, startSeconds: 0.25, maxDurationSeconds: 1, maxWidth: 160, }), { media: mediaFile } ); expect( Number(probe(preview.outputPaths[0]!).format?.duration) ).toBeLessThanOrEqual(1.1); const audioPreview = runNative( buildPreviewProxyPlan({ jobId: 'audio-preview-native', source: source('tone', 0, 'tone.wav'), sourceDurationSeconds: 1, maxDurationSeconds: 0.5, hasVideo: false, }), { tone: fixture('tone.wav') } ); expect(audioPreview.outputPaths[0]).toMatch(/\.m4a$/u); expect(probe(audioPreview.outputPaths[0]!).streams?.[0]).toMatchObject({ codec_type: 'audio', codec_name: 'aac', }); }, 120_000); it('executes fast and accurate multi-output split plans', () => { const media = source('media', 0, 'pattern-av.mp4'); const mediaFile = fixture('pattern-av.mp4'); for (const mode of ['fast', 'accurate'] as const) { const plan = buildSplitPlan({ jobId: `split-${mode}-native`, source: media, durationSeconds: 2, definition: { type: 'markers', markers: [0.7, 1.3] }, mode, targetExtension: 'mp4', ...(mode === 'accurate' ? { preset: preset('mp4-h264-balanced') } : {}), }); const execution = runNative(plan, { media: mediaFile }); expect(execution.outputPaths).toHaveLength(3); for (const path of execution.outputPaths) { expect( probe(path).streams?.some((stream) => stream.codec_type === 'video'), `${mode} split output ${basename(path)} must retain video` ).toBe(true); } } }, 120_000); it('executes fast and normalized concat, including generated silence', () => { const first = concatSource('first', 8, 'compatible-a.mp4', 128, 72); const second = concatSource('second', 3, 'compatible-b.mp4', 128, 72); const firstRange = { ...first, sourceInSeconds: 0.2, sourceOutSeconds: 0.8, }; const secondRange = { ...second, sourceInSeconds: 0.1, sourceOutSeconds: 0.9, }; const fastPlan = buildFastConcatPlan({ jobId: 'fast-concat-native', sources: [firstRange, secondRange], targetExtension: 'mp4', targetMuxer: 'mp4', }); expect(String(fastPlan.temporaryFiles[0]?.content)).toContain( 'inpoint 0.2' ); expect(fastPlan.expectedDurationSeconds).toBeCloseTo(1.4, 5); const fast = runNative(fastPlan, { first: fixture('compatible-a.mp4'), second: fixture('compatible-b.mp4'), }); expect( Number(probe(fast.outputPaths[0]!).format?.duration) ).toBeGreaterThan(1.2); expect(Number(probe(fast.outputPaths[0]!).format?.duration)).toBeLessThan( 2.1 ); const normalized = runNative( buildNormalizedConcatPlan({ jobId: 'normalized-concat-native', sources: [ concatSource('first', 0, 'compatible-a.mp4', 128, 72), { ...concatSource('different', 1, 'different-dimensions.mp4', 96, 96), sourceInSeconds: 0.2, sourceOutSeconds: 0.8, }, ], targetExtension: 'mp4', targetMuxer: 'mp4', preset: preset('mp4-h264-balanced'), width: 128, height: 72, frameRate: 10, sampleRate: 16_000, channelLayout: 'mono', missingAudioPolicy: 'reject', }), { first: fixture('compatible-a.mp4'), different: fixture('different-dimensions.mp4'), } ); expect(probe(normalized.outputPaths[0]!).streams?.[0]).toMatchObject({ width: 128, height: 72, }); expect( Number(probe(normalized.outputPaths[0]!).format?.duration) ).toBeCloseTo(1.6, 1); const crossfaded = runNative( buildNormalizedConcatPlan({ jobId: 'crossfade-concat-native', sources: [ concatSource('first', 0, 'compatible-a.mp4', 128, 72), concatSource('second', 1, 'compatible-b.mp4', 128, 72), ], targetExtension: 'mp4', targetMuxer: 'mp4', preset: preset('mp4-h264-balanced'), width: 128, height: 72, frameRate: 10, sampleRate: 16_000, channelLayout: 'mono', missingAudioPolicy: 'reject', crossfadeSeconds: 0.2, }), { first: fixture('compatible-a.mp4'), second: fixture('compatible-b.mp4'), } ); expect( Number(probe(crossfaded.outputPaths[0]!).format?.duration) ).toBeCloseTo(1.8, 1); const videoOnly = join(suiteDirectory, 'compatible-video-only.mp4'); nativeCommand( [ '-i', fixture('compatible-b.mp4'), '-map', '0:v:0', '-c', 'copy', videoOnly, ], videoOnly ); const silence = runNative( buildNormalizedConcatPlan({ jobId: 'silence-concat-native', sources: [ concatSource('first', 0, 'compatible-a.mp4', 128, 72), concatSource( 'video-only', 1, 'compatible-video-only.mp4', 128, 72, false ), ], targetExtension: 'mp4', targetMuxer: 'mp4', preset: preset('mp4-h264-balanced'), width: 128, height: 72, frameRate: 10, sampleRate: 16_000, channelLayout: 'mono', missingAudioPolicy: 'insert-silence', crossfadeSeconds: 0.2, }), { first: fixture('compatible-a.mp4'), 'video-only': videoOnly } ); expect( probe(silence.outputPaths[0]!).streams?.some( (stream) => stream.codec_type === 'audio' ) ).toBe(true); }, 120_000); it('executes audio extraction, waveform, loudness, and normalization plans', () => { const media = source('media', 0, 'pattern-av.mp4'); const mediaFile = fixture('pattern-av.mp4'); const copiedAudio = runNative( buildAudioExtractionPlan({ jobId: 'audio-copy-native', source: media, audioStreamIndex: 1, mode: 'copy', inputCodec: 'aac', targetMuxer: 'ipod', targetExtension: 'm4a', }), { media: mediaFile } ); expect(probe(copiedAudio.outputPaths[0]!).streams?.[0]?.codec_name).toBe( 'aac' ); const encodedAudio = runNative( buildAudioExtractionPlan({ jobId: 'audio-encode-native', source: media, audioStreamIndex: 1, mode: 'encode', preset: preset('audio-wav-pcm'), }), { media: mediaFile } ); expect(probe(encodedAudio.outputPaths[0]!).streams?.[0]?.codec_name).toBe( 'pcm_s16le' ); runNative( buildWaveformAnalysisPlan({ jobId: 'waveform-data-native', source: media, audioStreamIndex: 1, sampleRate: 8_000, }), { media: mediaFile } ); const staticWaveform = runNative( buildStaticWaveformPlan({ jobId: 'waveform-static-native', source: media, audioStreamIndex: 1, width: 320, height: 80, color: '#ffffff', background: '#000000', }), { media: mediaFile } ); expect(probe(staticWaveform.outputPaths[0]!).streams?.[0]).toMatchObject({ codec_name: 'png', width: 320, height: 80, }); const targets = { integratedLufs: -16, truePeakDbtp: -1, loudnessRangeLu: 11, }; const analysis = runNative( buildLoudnessAnalysisPlan({ jobId: 'loudness-analysis-native', source: media, targets, audioStreamIndex: 1, }), { media: mediaFile }, 'info' ); const measurement = parseLoudnessMeasurement( `${analysis.stderr}\n${analysis.stdout}` ); expect(measurement.inputIntegratedLufs).toBeTypeOf('number'); runNative( buildLoudnessApplyPlan({ jobId: 'loudness-apply-native', source: media, targets, audioStreamIndex: 1, measurement, preset: preset('audio-m4a-aac'), }), { media: mediaFile } ); const peakAnalysis = runNative( buildPeakAnalysisPlan({ jobId: 'peak-analysis-native', source: media, audioStreamIndex: 1, preset: preset('audio-m4a-aac'), }), { media: mediaFile }, 'info' ); const peakMeasurement = parsePeakMeasurement( `${peakAnalysis.stderr}\n${peakAnalysis.stdout}` ); expect(peakMeasurement.inputPeakDbfs).toBeLessThanOrEqual(0); const peakApplied = runNative( buildPeakNormalizationPlan({ jobId: 'peak-native', source: media, audioStreamIndex: 1, targetPeakDb: -1, measurement: peakMeasurement, preset: preset('audio-m4a-aac'), }), { media: mediaFile } ); const verifiedPeak = runNative( buildPeakAnalysisPlan({ jobId: 'peak-verify-native', source: source('normalized', 0, 'peak-normalized.m4a'), audioStreamIndex: 0, preset: preset('audio-m4a-aac'), }), { normalized: peakApplied.outputPaths[0]! }, 'info' ); const outputPeak = parsePeakMeasurement( `${verifiedPeak.stderr}\n${verifiedPeak.stdout}` ).inputPeakDbfs; expect(outputPeak).toBeGreaterThanOrEqual(-2); expect(outputPeak).toBeLessThanOrEqual(0); }, 120_000); it('executes stream removal, metadata editing, and chapter mux plans', () => { const media = source('media', 0, 'metadata.mp4'); const mediaFile = fixture('metadata.mp4'); const removed = runNative( buildStreamRemovalPlan({ jobId: 'streams-native', source: media, selection: { audio: [1] }, targetExtension: 'm4a', targetMuxer: 'ipod', }), { media: mediaFile } ); expect(probe(removed.outputPaths[0]!).streams?.map(streamType)).toEqual([ 'audio:aac', ]); const metadataPlan = buildMetadataEditPlan({ jobId: 'metadata-native', source: media, edits: { format: { title: 'Runtime title', artist: 'Local artist' }, streams: { 1: { language: 'deu' } }, dispositions: { 0: [], 1: ['forced'] }, }, targetExtension: 'mp4', targetMuxer: 'mp4', sourceMetadataPolicy: 'remove', chapterPolicy: 'remove', }); expect(metadataPlan.args).toEqual( expect.arrayContaining(['-disposition:0', '0']) ); const metadata = runNative(metadataPlan, { media: mediaFile }); expect(probe(metadata.outputPaths[0]!).format?.tags).toMatchObject({ title: 'Runtime title', artist: 'Local artist', }); expect(probe(metadata.outputPaths[0]!).streams).toEqual( expect.arrayContaining([ expect.objectContaining({ index: 1, disposition: expect.objectContaining({ forced: 1 }), }), ]) ); const chaptered = runNative( buildChapterMuxPlan({ jobId: 'chapters-native', source: source('media', 0, 'pattern-av.mp4'), chapters: [ { id: 'intro', title: 'Intro', startSeconds: 0, endSeconds: 0.75, timeBase: '1/48000', }, { id: 'main', title: 'Main', startSeconds: 0.75, endSeconds: 2, timeBase: '1/1000', }, ], targetExtension: 'mp4', targetMuxer: 'mp4', durationSeconds: 2, }), { media: fixture('pattern-av.mp4') } ); const chapterProbe = probe(chaptered.outputPaths[0]!).chapters; expect(chapterProbe?.map(chapterTitle)).toEqual(['Intro', 'Main']); expect(Number(chapterProbe?.[0]?.end_time)).toBeCloseTo(0.75, 3); expect(Number(chapterProbe?.[1]?.start_time)).toBeCloseTo(0.75, 3); }, 120_000); it('executes subtitle extraction, conversion, mux, and burn-in plans', () => { const subtitledSourcePath = join(suiteDirectory, 'subtitled-source.mp4'); nativeCommand( [ '-i', fixture('pattern-av.mp4'), '-i', fixture('subtitle.srt'), '-map', '0', '-map', '1:s:0', '-c', 'copy', '-c:s', 'mov_text', subtitledSourcePath, ], subtitledSourcePath ); const subtitledSource = source('media', 0, 'subtitled-source.mp4'); const convertedMedia = runNative( buildConvertPlan({ jobId: 'convert-selected-subtitle-native', source: subtitledSource, preset: preset('mp4-h264-balanced'), streamSelection: { video: [0], audio: [1], subtitles: [2] }, }), { media: subtitledSourcePath } ); expect( probe(convertedMedia.outputPaths[0]!).streams?.map(streamType) ).toEqual(['video:h264', 'audio:aac', 'subtitle:mov_text']); const extracted = runNative( buildSubtitleExtractionPlan({ jobId: 'subtitle-extract-native', source: subtitledSource, stream: { index: 2, codec: 'mov_text', language: 'eng' }, outputFormat: 'srt', }), { media: subtitledSourcePath } ); expect(readFileSync(extracted.outputPaths[0]!, 'utf8')).toContain( 'Generated subtitle' ); const externalSubtitle = source('subtitle', 1, 'subtitle.srt'); const converted = runNative( buildSubtitleConversionPlan({ jobId: 'subtitle-convert-native', subtitle: externalSubtitle, outputFormat: 'vtt', offsetSeconds: 0.1, }), { subtitle: fixture('subtitle.srt') } ); expect(readFileSync(converted.outputPaths[0]!, 'utf8')).toContain('WEBVTT'); const muxed = runNative( buildSubtitleMuxPlan({ jobId: 'subtitle-mux-native', source: subtitledSource, subtitle: externalSubtitle, subtitleCodec: 'subrip', sourceStreams: [ { index: 0, kind: 'video', codec: 'h264' }, { index: 1, kind: 'audio', codec: 'aac' }, { index: 2, kind: 'subtitle', codec: 'mov_text' }, ], targetContainer: 'mp4', targetExtension: 'mp4', language: 'eng', title: 'English', default: true, }), { media: subtitledSourcePath, subtitle: fixture('subtitle.srt'), } ); expect( probe(muxed.outputPaths[0]!).streams?.filter( (stream) => stream.codec_name === 'mov_text' ) ).toHaveLength(2); expect( probe(muxed.outputPaths[0]!).streams?.filter( (stream) => stream.codec_type === 'subtitle' )[1]?.tags ).toMatchObject({ language: 'eng', handler_name: 'English' }); }, 120_000); itNativeSubtitles( 'executes subtitle burn-in when the native test binary has libass', () => { const burned = runNative( buildSubtitleBurnPlan({ jobId: 'subtitle-burn-native', source: source('media', 0, 'pattern-av.mp4'), subtitle: source('subtitle', 1, 'subtitle.srt'), preset: preset('mp4-h264-balanced'), expectedDurationSeconds: 2, }), { media: fixture('pattern-av.mp4'), subtitle: fixture('subtitle.srt'), } ); expect( probe(burned.outputPaths[0]!).streams?.some( (stream) => stream.codec_type === 'subtitle' ) ).toBe(false); }, 120_000 ); it('executes single/series thumbnail and contact-sheet plans', () => { const media = source('media', 0, 'pattern-av.mp4'); const mediaFile = fixture('pattern-av.mp4'); const single = runNative( buildSingleThumbnailPlan({ jobId: 'thumbnail-native', source: media, timeSeconds: 0.5, size: { width: 80 }, format: 'png', }), { media: mediaFile } ); expect(probe(single.outputPaths[0]!).streams?.[0]).toMatchObject({ codec_name: 'png', width: 80, }); const series = runNative( buildThumbnailSeriesPlan({ jobId: 'thumbnail-series-native', source: media, durationSeconds: 2, timesSeconds: [0.2, 0.8, 1.4], size: { width: 80 }, format: 'jpeg', quality: 75, }), { media: mediaFile } ); expect(series.outputPaths).toHaveLength(3); expect( series.outputPaths.map((path) => probe(path).streams?.[0]?.codec_name) ).toEqual(['mjpeg', 'mjpeg', 'mjpeg']); const sheet = runNative( buildContactSheetPlan({ jobId: 'contact-sheet-native', source: media, durationSeconds: 2, frameCount: 4, rows: 2, columns: 2, cellWidth: 80, spacing: 2, timestampLabels: false, sourceLabel: false, format: 'png', }), { media: mediaFile } ); expect(probe(sheet.outputPaths[0]!).streams?.[0]).toMatchObject({ codec_name: 'png', width: 166, }); const sceneAnalysis = runNative( buildSceneChangeAnalysisPlan({ jobId: 'scene-analysis-native', source: media, durationSeconds: 2, threshold: 0.01, maxFrames: 4, }), { media: mediaFile }, 'info' ); const sceneFrames = parseSceneChangeFrames(sceneAnalysis.stderr); expect(sceneFrames.length).toBeGreaterThan(0); const sceneSeries = runNative( buildSceneThumbnailSeriesPlan({ jobId: 'scene-series-native', source: media, durationSeconds: 2, frames: sceneFrames, size: { width: 80 }, format: 'png', }), { media: mediaFile } ); expect(sceneSeries.outputPaths).toHaveLength(sceneFrames.length); const sceneSheet = runNative( buildContactSheetPlan({ jobId: 'scene-contact-sheet-native', source: media, durationSeconds: 2, frameCount: sceneFrames.length, rows: 1, columns: sceneFrames.length, cellWidth: 80, spacing: 2, sceneFrames, format: 'png', }), { media: mediaFile } ); expect(statSync(sceneSheet.outputPaths[0]!).size).toBeGreaterThan(500); }, 120_000); itNativeDrawtext( 'executes contact-sheet labels with the reviewed bundled font', () => { const media = source('media', 0, 'pattern-av.mp4'); const labeledSheet = runNative( buildContactSheetPlan({ jobId: 'labeled-contact-sheet-native', source: media, durationSeconds: 2, frameCount: 2, rows: 1, columns: 2, cellWidth: 120, timestampLabels: true, sourceLabel: true, drawtextAvailable: true, font: source('font', 1, 'DejaVuSans.ttf'), format: 'png', }), { media: fixture('pattern-av.mp4'), font: resolve(process.cwd(), 'public/fonts/DejaVuSans.ttf'), } ); expect(statSync(labeledSheet.outputPaths[0]!).size).toBeGreaterThan(500); }, 120_000 ); it('executes a transformed, faded, normalized sequential timeline', () => { const execution = runNative( buildTimelineExportPlan({ jobId: 'timeline-native', clips: [ { id: 'clip-a', source: source('first', 0, 'compatible-a.mp4'), sourceInSeconds: 0.1, sourceOutSeconds: 0.9, sourceDurationSeconds: 1, hasVideo: true, hasAudio: true, transform: { rotation: 180 }, audioGainDb: -1, audioNormalization: { mode: 'peak', targetPeakDb: -1, measurement: { inputPeakDbfs: -18 }, }, audioFadeInSeconds: 0.1, audioFadeCurve: 'qsin', videoFadeOutSeconds: 0.1, videoFadeCurve: 'exp', }, { id: 'clip-b', source: source('second', 1, 'different-dimensions.mp4'), sourceInSeconds: 0, sourceOutSeconds: 0.8, sourceDurationSeconds: 1, hasVideo: true, hasAudio: true, transform: { crop: { x: 0, y: 0, width: 80, height: 80, sourceWidth: 96, sourceHeight: 96, requireEvenDimensions: true, }, }, audioFadeOutSeconds: 0.1, audioFadeCurve: 'exp', videoFadeInSeconds: 0.1, videoFadeCurve: 'qsin', }, ], preset: preset('mp4-h264-balanced'), outputWidth: 128, outputHeight: 72, frameRate: 10, sampleRate: 16_000, channelLayout: 'mono', missingAudioPolicy: 'reject', }), { first: fixture('compatible-a.mp4'), second: fixture('different-dimensions.mp4'), } ); const result = probe(execution.outputPaths[0]!); expect(result.streams?.[0]).toMatchObject({ width: 128, height: 72 }); expect( result.streams?.some((stream) => stream.codec_type === 'audio') ).toBe(true); expect(Number(result.format?.duration)).toBeCloseTo(1.6, 1); }, 120_000); }); function streamType(stream: { readonly codec_type?: string; readonly codec_name?: string; }): string { return `${stream.codec_type ?? 'unknown'}:${stream.codec_name ?? 'unknown'}`; } function chapterTitle(chapter: { readonly tags?: Readonly>; }): string | undefined { return chapter.tags?.title; }