/// import { execFileSync } from 'node:child_process'; import { readFileSync, statSync } from 'node:fs'; import { basename, join } from 'node:path'; import { expect, test, type Download, type Locator, type Page, } from '@playwright/test'; import { createCancellationFixture, GENERATED_FIXTURES, observeRuntime, type RuntimeObservation, type TemporaryMediaFixture, } from './browser-helpers'; const fixture = (name: string): string => join(GENERATED_FIXTURES, name); function mediaInput(page: Page): Locator { return page.locator('input[type="file"][accept*="audio"]').first(); } async function importMedia( page: Page, names: readonly string[] ): Promise { await mediaInput(page).setInputFiles(names.map((name) => fixture(name))); for (const name of names) { const card = page.locator('.media-card').filter({ hasText: name }); await expect(card.locator('.phase--ready')).toBeVisible({ timeout: 120_000, }); } } async function initializeSingleThread(page: Page): Promise { await page.getByText('Engine & browser').click(); await page.getByLabel('Engine mode').selectOption('force-single-thread'); await page.getByRole('button', { name: 'Initialize engine' }).click(); await expect( page .locator('.engine-pill') .filter({ hasText: 'Single-threaded compatibility mode' }) ).toBeVisible({ timeout: 120_000 }); } const RESULT_SNAPSHOT_ATTRIBUTE = 'data-playwright-existing-result'; async function markExistingResults(page: Page): Promise { await page.locator('.result-card').evaluateAll((cards) => { for (const card of cards) { card.setAttribute('data-playwright-existing-result', ''); } }); } function newResultCards(page: Page): Locator { return page.locator(`.result-card:not([${RESULT_SNAPSHOT_ATTRIBUTE}])`); } function accumulatedOutputName(fileName: string): RegExp { const dot = fileName.lastIndexOf('.'); const stem = dot > 0 ? fileName.slice(0, dot) : fileName; const extension = dot > 0 ? fileName.slice(dot) : ''; const escape = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); return new RegExp(`^${escape(stem)}(?:-\\d+)?${escape(extension)}$`, 'u'); } async function runForResults( page: Page, action: () => Promise, expectedFileNames: readonly string[], options: { verifyMedia?: boolean } = {} ): Promise { await markExistingResults(page); await action(); const cards = expectedFileNames.map((_, index) => newResultCards(page).nth(index) ); await expect .poll( async () => { if ((await cards[0]?.locator('strong').count()) === 1) { return 'result'; } if ((await page.getByRole('alert').count()) > 0) { const alert = (await page.getByRole('alert').allTextContents()).join( ' | ' ); const diagnostics = (await page.locator('.diagnostics pre').textContent()) ?? ''; throw new Error( `${alert}\n\nFFmpeg diagnostics:\n${diagnostics .split('\n') .slice(-160) .join('\n')}` ); } return 'waiting'; }, { timeout: 120_000 } ) .toBe('result'); for (const [index, card] of cards.entries()) { await expect(card.locator('strong')).toHaveText( accumulatedOutputName(expectedFileNames[index] as string), { timeout: 120_000 } ); await expect(card).toContainText( /created · (?:verified|verification warning)|completed locally/u, { timeout: 120_000 } ); if (options.verifyMedia) { await expect(card).toContainText(/verified stream/u, { timeout: 120_000, }); } } await expect(page.getByRole('alert')).toHaveCount(0); return cards; } async function downloadResult( page: Page, card: Locator ): Promise<{ download: Download; filePath: string }> { const downloadPromise = page.waitForEvent('download'); await card.locator('button.button--primary').click(); const download = await downloadPromise; const filePath = await download.path(); expect(filePath).not.toBeNull(); expect(statSync(filePath as string).size).toBeGreaterThan(0); return { download, filePath: filePath as string }; } function probe(filePath: string): { streams?: Array<{ codec_name?: string; codec_type?: string; width?: number; height?: number; avg_frame_rate?: string; tags?: Record; disposition?: Record; }>; format?: { duration?: string; tags?: Record }; chapters?: Array<{ tags?: Record }>; } { return JSON.parse( execFileSync( 'ffprobe', [ '-v', 'error', '-show_streams', '-show_format', '-show_chapters', '-of', 'json', filePath, ], { encoding: 'utf8' } ) ) as ReturnType; } function assertLocalOnly(runtime: RuntimeObservation): void { const networkUrls = runtime.requests.filter((url) => /^https?:/u.test(url)); expect( networkUrls.every((url) => new URL(url).origin === 'http://127.0.0.1:4173') ).toBe(true); const materialFailures = runtime.failedRequests.filter( (failure) => !(failure.startsWith('blob:') && failure.endsWith('net::ERR_ABORTED')) ); expect(materialFailures).toEqual([]); expect(runtime.pageErrors).toEqual([]); expect(runtime.consoleErrors).toEqual([]); } test.describe.serial('real ffmpeg.wasm operation matrix', () => { let cancellationFixture: TemporaryMediaFixture; test.beforeAll(() => { cancellationFixture = createCancellationFixture(); }); test.afterAll(() => { cancellationFixture.dispose(); }); test('executes conversion, structural, analysis, image, metadata, chapter and subtitle workflows in one local engine session', async ({ page, }) => { test.setTimeout(300_000); page.setDefaultTimeout(15_000); await page.addInitScript(() => { Reflect.deleteProperty(globalThis, 'showSaveFilePicker'); }); const runtime = observeRuntime(page); await page.goto('/'); await initializeSingleThread(page); await test.step('queues a second browser job, cancels the active job and runs the waiting job after recovery', async () => { const sourceName = basename(cancellationFixture.filePath); await mediaInput(page).setInputFiles(cancellationFixture.filePath); await expect( page .locator('.media-card') .filter({ hasText: sourceName }) .locator('.phase--ready') ).toBeVisible({ timeout: 120_000 }); await page.getByRole('button', { name: 'Convert', exact: true }).click(); const enqueueButton = page.getByRole('button', { name: 'Add to queue', }); await expect(enqueueButton).toBeVisible(); await enqueueButton.click(); await expect(page.getByText('1 waiting', { exact: true })).toBeVisible(); const firstCancelButton = page.getByRole('button', { name: 'Cancel & recover', }); const firstCancelElement = await firstCancelButton.elementHandle(); await firstCancelButton.click(); await expect .poll( () => firstCancelElement?.evaluate((element) => element.isConnected) ?? false, { timeout: 120_000 } ) .toBe(false); await expect(page.getByText('1 waiting', { exact: true })).toBeHidden({ timeout: 120_000, }); const secondCancelButton = page.getByRole('button', { name: 'Cancel & recover', }); await expect(secondCancelButton).toBeVisible({ timeout: 120_000, }); await secondCancelButton.click(); await expect(secondCancelButton).toBeHidden({ timeout: 120_000 }); await expect(page.getByRole('alert')).toContainText( 'The operation was cancelled and the engine was recreated.', { timeout: 120_000 } ); await expect( page .locator('.engine-pill') .filter({ hasText: 'Single-threaded compatibility mode' }) ).toBeVisible({ timeout: 120_000 }); await page.getByRole('button', { name: 'New', exact: true }).click(); }); await test.step('audio conversion and WebM remux/re-encode', async () => { await importMedia(page, ['tone.flac']); await page.getByLabel('Export preset').selectOption('audio-mp3'); const [mp3Card] = await runForResults( page, () => page.getByRole('button', { name: 'Convert', exact: true }).click(), ['tone-convert.mp3'], { verifyMedia: true } ); const mp3 = await downloadResult(page, mp3Card as Locator); expect(probe(mp3.filePath).streams).toEqual( expect.arrayContaining([ expect.objectContaining({ codec_name: 'mp3', codec_type: 'audio', }), ]) ); await page.getByLabel('Export preset').selectOption('audio-ogg-vorbis'); const [oggCard] = await runForResults( page, () => page.getByRole('button', { name: 'Convert', exact: true }).click(), ['tone-convert.ogg'], { verifyMedia: true } ); expect( probe((await downloadResult(page, oggCard as Locator)).filePath).streams ).toEqual( expect.arrayContaining([ expect.objectContaining({ codec_name: 'vorbis', codec_type: 'audio', }), ]) ); await page.getByLabel('Export preset').selectOption('audio-flac'); const [flacCard] = await runForResults( page, () => page.getByRole('button', { name: 'Convert', exact: true }).click(), ['tone-convert.flac'], { verifyMedia: true } ); expect( probe((await downloadResult(page, flacCard as Locator)).filePath) .streams ).toEqual( expect.arrayContaining([ expect.objectContaining({ codec_name: 'flac', codec_type: 'audio', }), ]) ); await page.getByLabel('Export preset').selectOption('audio-wav-pcm'); const [wavCard] = await runForResults( page, () => page.getByRole('button', { name: 'Convert', exact: true }).click(), ['tone-convert.wav'], { verifyMedia: true } ); expect( probe((await downloadResult(page, wavCard as Locator)).filePath).streams ).toEqual( expect.arrayContaining([ expect.objectContaining({ codec_name: 'pcm_s16le', codec_type: 'audio', }), ]) ); await importMedia(page, ['pattern-av.mp4']); await page .locator('.media-card') .filter({ hasText: 'pattern-av.mp4' }) .click(); await page.getByLabel('Export preset').selectOption('audio-mp3'); await page.getByRole('checkbox', { name: /#0 · h264/u }).uncheck(); const [extractedAudioCard] = await runForResults( page, () => page.getByRole('button', { name: 'Convert', exact: true }).click(), ['pattern-av-convert.mp3'], { verifyMedia: true } ); const extractedAudio = probe( (await downloadResult(page, extractedAudioCard as Locator)).filePath ); expect( extractedAudio.streams?.map((stream) => stream.codec_type) ).toEqual(['audio']); expect(extractedAudio.streams?.[0]?.codec_name).toBe('mp3'); await page .getByLabel('Export preset') .selectOption('mp4-h264-compatibility'); await page.getByRole('checkbox', { name: /#0 · h264/u }).check(); await page.getByRole('checkbox', { name: /#1 · aac/u }).uncheck(); const [videoOnlyCard] = await runForResults( page, () => page.getByRole('button', { name: 'Convert', exact: true }).click(), ['pattern-av-convert.mp4'], { verifyMedia: true } ); const videoOnly = probe( (await downloadResult(page, videoOnlyCard as Locator)).filePath ); expect(videoOnly.streams?.map((stream) => stream.codec_type)).toEqual([ 'video', ]); expect(videoOnly.streams?.[0]?.codec_name).toBe('h264'); await importMedia(page, ['pattern.webm']); await page .locator('.media-card') .filter({ hasText: 'pattern.webm' }) .click(); await page.getByRole('button', { name: 'Fast remux' }).click(); await page.getByLabel('Target container').selectOption('matroska'); const [remuxCard] = await runForResults( page, () => page.getByRole('button', { name: 'Remux', exact: true }).click(), ['pattern-remux.mkv'], { verifyMedia: true } ); const remux = await downloadResult(page, remuxCard as Locator); expect(probe(remux.filePath).streams).toEqual( expect.arrayContaining([ expect.objectContaining({ codec_name: 'vp8', codec_type: 'video' }), expect.objectContaining({ codec_name: 'vorbis', codec_type: 'audio', }), ]) ); await page.getByRole('button', { name: 'Re-encode' }).click(); await page.getByLabel('Export preset').selectOption('webm-vp8-vorbis'); const [webmCard] = await runForResults( page, () => page.getByRole('button', { name: 'Convert', exact: true }).click(), ['pattern-convert.webm'], { verifyMedia: true } ); const webm = await downloadResult(page, webmCard as Locator); expect(probe(webm.filePath).streams).toEqual( expect.arrayContaining([ expect.objectContaining({ codec_name: 'vp8', codec_type: 'video' }), expect.objectContaining({ codec_name: 'vorbis', codec_type: 'audio', }), ]) ); await importMedia(page, ['metadata.mp4']); await page .locator('.media-card') .filter({ hasText: 'metadata.mp4' }) .click(); await page.getByRole('button', { name: 'Fast remux' }).click(); await page.getByLabel('Target container').selectOption('mp4'); await page.getByRole('checkbox', { name: 'Remove metadata' }).check(); const [metadataRemovalCard] = await runForResults( page, () => page.getByRole('button', { name: 'Remux', exact: true }).click(), ['metadata-remux.mp4'], { verifyMedia: true } ); const metadataRemovalProbe = probe( (await downloadResult(page, metadataRemovalCard as Locator)).filePath ); expect(metadataRemovalProbe.format?.tags?.title).toBeUndefined(); expect(metadataRemovalProbe.format?.tags?.artist).toBeUndefined(); expect(metadataRemovalProbe.format?.tags?.comment).toBeUndefined(); }); await test.step('fast and accurate trim plus fast and normalized concat', async () => { await page.getByRole('button', { name: 'New', exact: true }).click(); await page.getByRole('tab', { name: 'Edit' }).click(); await importMedia(page, ['compatible-a.mp4', 'compatible-b.mp4']); const selectedTimelineClip = page .locator('.timeline-clip') .filter({ hasText: 'compatible-a.mp4' }); await selectedTimelineClip.locator('.timeline-clip__main').click(); await selectedTimelineClip.getByLabel('In').fill('0.1'); await selectedTimelineClip.getByLabel('Out').fill('0.8'); const structural = page.locator('.structural-operations'); const [fastTrimCard] = await runForResults( page, () => structural.getByRole('button', { name: 'Export fast trim' }).click(), ['compatible-a-trim.mp4'], { verifyMedia: true } ); expect( probe((await downloadResult(page, fastTrimCard as Locator)).filePath) .streams ).toEqual( expect.arrayContaining([ expect.objectContaining({ codec_name: 'h264', codec_type: 'video' }), ]) ); await structural.getByLabel('Accurate re-encode').check(); await structural .getByLabel('Accurate trim preset') .selectOption('mp4-h264-compatibility'); const [accurateTrimCard] = await runForResults( page, () => structural.getByRole('button', { name: 'Export exact trim' }).click(), ['compatible-a-trim.mp4'], { verifyMedia: true } ); expect( probe( (await downloadResult(page, accurateTrimCard as Locator)).filePath ).streams ).toEqual( expect.arrayContaining([ expect.objectContaining({ codec_name: 'h264', codec_type: 'video' }), expect.objectContaining({ codec_name: 'aac', codec_type: 'audio' }), ]) ); await expect( structural.getByRole('heading', { name: 'Fast compatibility passed', }) ).toBeVisible(); const [fastConcatCard] = await runForResults( page, () => structural .getByRole('button', { name: 'Export fast concat' }) .click(), ['compatible-a-concat.mp4'], { verifyMedia: true } ); expect( probe((await downloadResult(page, fastConcatCard as Locator)).filePath) .streams ).toEqual( expect.arrayContaining([ expect.objectContaining({ codec_name: 'h264', codec_type: 'video' }), expect.objectContaining({ codec_name: 'aac', codec_type: 'audio' }), ]) ); await structural.getByLabel('Normalized re-encode').check(); await structural .getByLabel('Normalized concat preset') .selectOption('mp4-h264-compatibility'); await structural.getByLabel('Insert silence').check(); await structural.getByLabel('Reject subtitle input').check(); const [normalizedConcatCard] = await runForResults( page, () => structural .getByRole('button', { name: 'Export normalized concat' }) .click(), ['compatible-a-concat.mp4'], { verifyMedia: true } ); expect( probe( (await downloadResult(page, normalizedConcatCard as Locator)).filePath ).streams ).toEqual( expect.arrayContaining([ expect.objectContaining({ codec_name: 'h264', codec_type: 'video' }), expect.objectContaining({ codec_name: 'aac', codec_type: 'audio' }), ]) ); await structural.getByLabel('Crossfade duration').fill('0.2'); const crossfadeAction = structural.getByRole('button', { name: 'Export normalized concat', }); if (await crossfadeAction.isEnabled()) { const [crossfadeCard] = await runForResults( page, () => crossfadeAction.click(), ['compatible-a-concat.mp4'], { verifyMedia: true } ); const crossfadeDuration = Number( probe((await downloadResult(page, crossfadeCard as Locator)).filePath) .format?.duration ); expect(crossfadeDuration).toBeGreaterThan(1.35); expect(crossfadeDuration).toBeLessThan(1.75); } else { await expect( structural.getByText( /required browser and FFmpeg capabilities have not been verified|filter (?:x|a)fade/iu ) ).toBeVisible(); } }); const advanced = page.locator('.advanced-operations'); await test.step('fast/accurate split and waveform/loudness analysis', async () => { await advanced.getByLabel('Marker time (seconds)').fill('0.5'); await advanced.getByRole('button', { name: 'Add marker' }).click(); await runForResults( page, () => advanced.getByRole('button', { name: 'Queue 2 segments' }).click(), ['compatible-a-split-001.mp4', 'compatible-a-split-002.mp4'], { verifyMedia: true } ); await advanced.getByLabel('Accurate re-encode').check(); await advanced .getByLabel('Accurate split preset') .selectOption('mp4-h264-compatibility'); await runForResults( page, () => advanced.getByRole('button', { name: 'Queue 2 segments' }).click(), ['compatible-a-split-001.mp4', 'compatible-a-split-002.mp4'], { verifyMedia: true } ); await advanced .getByRole('tab', { name: 'Waveform and loudness' }) .click(); const [waveformCard] = await runForResults( page, () => advanced .getByRole('button', { name: 'Generate static waveform' }) .click(), ['compatible-a-waveform-static.png'] ); expect( statSync((await downloadResult(page, waveformCard as Locator)).filePath) .size ).toBeGreaterThan(1_000); await advanced .getByRole('button', { name: 'Analyze peak waveform' }) .click(); await expect( page.getByRole('region', { name: 'Waveform editor for compatible-a.mp4', }) ).toBeVisible({ timeout: 120_000 }); await advanced.getByRole('button', { name: 'Measure EBU R128' }).click(); await expect(page.locator('.project-notice')).toContainText('Measured', { timeout: 120_000, }); await expect(advanced.getByText('Measured loudness')).toBeVisible(); await advanced .getByLabel('Normalization preset') .selectOption('audio-mp3'); const [normalizedAudioCard] = await runForResults( page, () => advanced .getByRole('button', { name: 'Apply measured normalization' }) .click(), ['compatible-a-normalize.mp3'], { verifyMedia: true } ); expect( probe( (await downloadResult(page, normalizedAudioCard as Locator)).filePath ).streams ).toEqual( expect.arrayContaining([ expect.objectContaining({ codec_name: 'mp3', codec_type: 'audio' }), ]) ); const [peakAudioCard] = await runForResults( page, () => advanced .getByRole('button', { name: 'Apply peak normalization' }) .click(), ['compatible-a-peak-normalize.mp3'], { verifyMedia: true } ); expect( probe((await downloadResult(page, peakAudioCard as Locator)).filePath) .streams ).toEqual( expect.arrayContaining([ expect.objectContaining({ codec_name: 'mp3', codec_type: 'audio' }), ]) ); }); await test.step('thumbnail series and contact sheet', async () => { await advanced .getByRole('tab', { name: 'Thumbnails and contact sheets' }) .click(); await advanced.getByLabel('Number of frames').fill('3'); await runForResults( page, () => advanced .getByRole('button', { name: 'Generate thumbnail series' }) .click(), [ 'compatible-a-thumbnail-001-250ms.jpg', 'compatible-a-thumbnail-002-500ms.jpg', 'compatible-a-thumbnail-003-750ms.jpg', ] ); await advanced .getByRole('spinbutton', { name: 'Frames', exact: true }) .fill('4'); await advanced .getByRole('spinbutton', { name: 'Rows', exact: true }) .fill('2'); await advanced .getByRole('spinbutton', { name: 'Columns', exact: true }) .fill('2'); await advanced .getByRole('spinbutton', { name: /^Cell width/u }) .fill('128'); const [sheetCard] = await runForResults( page, () => advanced .getByRole('button', { name: 'Generate contact sheet' }) .click(), ['compatible-a-contact-sheet.jpg'] ); expect( statSync((await downloadResult(page, sheetCard as Locator)).filePath) .size ).toBeGreaterThan(500); await importMedia(page, ['pattern-av.mp4']); const patternClip = page .locator('.timeline-clip') .filter({ hasText: 'pattern-av.mp4' }); await patternClip.locator('.timeline-clip__main').click(); const imageSelection = advanced.getByLabel('Frame selection').first(); const sceneOption = imageSelection.locator( 'option[value="scene-changes"]' ); if (!(await sceneOption.isDisabled())) { await imageSelection.selectOption('scene-changes'); await advanced.getByLabel('Number of frames').fill('2'); await advanced.getByLabel('Scene sensitivity threshold').fill('0.01'); await runForResults( page, () => advanced .getByRole('button', { name: 'Generate thumbnail series' }) .click(), [ 'pattern-av-thumbnail-001-83ms.jpg', 'pattern-av-thumbnail-002-1000ms.jpg', ] ); const contactSelection = advanced.getByLabel('Frame selection').nth(1); await contactSelection.selectOption('scene-changes'); await advanced.getByLabel('Contact-sheet scene threshold').fill('0.01'); await advanced .getByRole('spinbutton', { name: 'Frames', exact: true }) .fill('2'); await advanced .getByRole('spinbutton', { name: 'Rows', exact: true }) .fill('1'); await advanced .getByRole('spinbutton', { name: 'Columns', exact: true }) .fill('2'); const timestampLabels = advanced.getByLabel('Timestamp labels'); if (await timestampLabels.isEnabled()) { await timestampLabels.check(); await advanced.getByLabel('Source filename label').check(); } const [sceneSheetCard] = await runForResults( page, () => advanced .getByRole('button', { name: 'Generate contact sheet' }) .click(), ['pattern-av-contact-sheet.jpg'] ); expect( statSync( (await downloadResult(page, sceneSheetCard as Locator)).filePath ).size ).toBeGreaterThan(500); } await patternClip.getByRole('button', { name: 'Remove clip' }).click(); await expect(patternClip).toHaveCount(0); await page .locator('.timeline-clip') .filter({ hasText: 'compatible-a.mp4' }) .first() .locator('.timeline-clip__main') .click(); }); await test.step('single frame and a transformed timeline with audio/video fades', async () => { const inspector = page.locator('.inspector'); await inspector.getByRole('tab', { name: 'Frames' }).click(); const inspectorBody = inspector.locator('.inspector__body'); await inspectorBody .getByRole('spinbutton', { name: 'Time (seconds)', exact: true }) .fill('0.2'); await inspectorBody .getByRole('spinbutton', { name: 'Width', exact: true }) .fill('96'); await inspectorBody .getByRole('combobox', { name: 'Format', exact: true }) .selectOption('png'); const [singleFrameCard] = await runForResults( page, () => inspectorBody.getByRole('button', { name: 'Extract frame' }).click(), ['compatible-a-thumbnail-200ms.png'] ); expect( statSync( (await downloadResult(page, singleFrameCard as Locator)).filePath ).size ).toBeGreaterThan(100); await inspector.getByRole('tab', { name: 'Transform' }).click(); const cropFields = inspector.locator('.crop-editor__numeric'); await cropFields .getByRole('spinbutton', { name: 'X', exact: true }) .fill('4'); await cropFields .getByRole('spinbutton', { name: 'Y', exact: true }) .fill('2'); await cropFields .getByRole('spinbutton', { name: 'Width', exact: true }) .fill('120'); await cropFields .getByRole('spinbutton', { name: 'Height', exact: true }) .fill('68'); const resizeFields = inspector.locator('.inspector-section > .mini-grid'); await resizeFields .getByRole('spinbutton', { name: 'Width', exact: true }) .fill('96'); await resizeFields .getByRole('spinbutton', { name: 'Height', exact: true }) .fill('54'); await resizeFields .getByRole('combobox', { name: 'Rotate', exact: true }) .selectOption('90'); await resizeFields .getByRole('combobox', { name: 'Scale algorithm', exact: true }) .selectOption('bicubic'); await resizeFields .getByRole('textbox', { name: 'Padding colour', exact: true }) .fill('#112233'); await page .getByRole('spinbutton', { name: 'Output frame rate', exact: true }) .fill('12'); const videoFades = inspector .locator('fieldset.subpanel') .filter({ hasText: 'Video fades' }); await videoFades .getByRole('spinbutton', { name: 'Fade in from black (s)' }) .fill('0.1'); await videoFades .getByRole('spinbutton', { name: 'Fade out to black (s)' }) .fill('0.1'); await inspector.getByRole('tab', { name: 'Audio' }).click(); await expect( inspectorBody.getByRole('checkbox', { name: 'Include audio' }) ).toBeChecked(); await inspectorBody .getByRole('spinbutton', { name: 'Fade in (s)', exact: true }) .fill('0.1'); await inspectorBody .getByRole('spinbutton', { name: 'Fade out (s)', exact: true }) .fill('0.1'); const [timelineCard] = await runForResults( page, () => page.getByRole('button', { name: 'Render timeline' }).click(), ['output.mp4'], { verifyMedia: true } ); const timelineProbe = probe( (await downloadResult(page, timelineCard as Locator)).filePath ); const timelineVideo = timelineProbe.streams?.find( (stream) => stream.codec_type === 'video' ); expect(timelineVideo).toMatchObject({ codec_name: 'h264', width: 96, height: 54, avg_frame_rate: '12/1', }); expect( timelineProbe.streams?.some( (stream) => stream.codec_type === 'audio' && stream.codec_name === 'aac' ) ).toBe(true); }); await test.step('metadata and chapters survive round-trip probing', async () => { await advanced.getByRole('tab', { name: 'Metadata policies' }).click(); const metadataPanel = advanced.getByRole('tabpanel'); await metadataPanel .getByRole('textbox', { name: 'Title', exact: true }) .fill('Browser Matrix'); await metadataPanel .getByRole('textbox', { name: 'Artist', exact: true }) .fill('av-tools Playwright'); const [metadataCard] = await runForResults( page, () => advanced .getByRole('button', { name: 'Apply metadata policies' }) .click(), ['compatible-a-metadata.mp4'], { verifyMedia: true } ); const metadataProbe = probe( (await downloadResult(page, metadataCard as Locator)).filePath ); expect(metadataProbe.format?.tags).toMatchObject({ title: 'Browser Matrix', artist: 'av-tools Playwright', }); await advanced.getByRole('tab', { name: 'Chapter execution' }).click(); await advanced .getByLabel('Create output chapters from') .selectOption('fixed'); await advanced.getByLabel('Chapter interval').fill('0.5'); const [chapterCard] = await runForResults( page, () => advanced .getByRole('button', { name: 'Replace output chapters' }) .click(), ['compatible-a-chapters.mp4'], { verifyMedia: true } ); const chapterProbe = probe( (await downloadResult(page, chapterCard as Locator)).filePath ); expect(chapterProbe.chapters).toHaveLength(2); expect( chapterProbe.chapters?.map((chapter) => chapter.tags?.title) ).toEqual(['Chapter 1', 'Chapter 2']); }); await test.step('subtitle convert, mux, burn-in and extraction', async () => { await advanced.getByRole('tab', { name: 'Subtitle operations' }).click(); const subtitlePanel = advanced.getByRole('tabpanel'); const subtitleOperation = subtitlePanel .locator('label.advanced-field') .filter({ hasText: /^Operation/u }) .locator('select'); const outputTextFormat = subtitlePanel .locator('label.advanced-field') .filter({ hasText: /^Output text format/u }) .locator('select'); await subtitleOperation.selectOption('convert'); const localSubtitleInput = subtitlePanel.locator( 'label.advanced-file-field input[type="file"]' ); await markExistingResults(page); await localSubtitleInput.setInputFiles({ name: 'invalid-subtitle.srt', mimeType: 'application/x-subrip', buffer: Buffer.from('this is not a timed subtitle\n'), }); await subtitlePanel .getByRole('button', { name: 'Convert subtitle' }) .click(); await expect( advanced.locator('.advanced-operations__status') ).toContainText(/FFmpeg (?:exited|produced)/u, { timeout: 120_000, }); await expect(newResultCards(page)).toHaveCount(0); await expect( page .locator('.engine-pill') .filter({ hasText: 'Single-threaded compatibility mode' }) ).toBeVisible({ timeout: 120_000 }); await localSubtitleInput.setInputFiles(fixture('subtitle.srt')); await outputTextFormat.selectOption('vtt'); await subtitlePanel.getByLabel('Time offset').fill('0.1'); const [convertedCard] = await runForResults( page, () => subtitlePanel .getByRole('button', { name: 'Convert subtitle' }) .click(), ['subtitle-subtitle.vtt'] ); const converted = await downloadResult(page, convertedCard as Locator); expect(readFileSync(converted.filePath, 'utf8')).toContain('WEBVTT'); await subtitleOperation.selectOption('soft-mux'); await subtitlePanel .locator('label.advanced-field') .filter({ hasText: /^Target container/u }) .locator('select') .selectOption('matroska'); await subtitlePanel .getByRole('textbox', { name: 'Language', exact: true }) .fill('eng'); await subtitlePanel .getByRole('textbox', { name: 'Track title', exact: true }) .fill('Browser subtitle'); await subtitlePanel .getByRole('checkbox', { name: 'Default track' }) .check(); const [muxedCard] = await runForResults( page, () => subtitlePanel .getByRole('button', { name: 'Mux soft subtitle' }) .click(), ['compatible-a-subtitled.mkv'], { verifyMedia: true } ); const muxed = await downloadResult(page, muxedCard as Locator); expect(probe(muxed.filePath).streams).toEqual( expect.arrayContaining([ expect.objectContaining({ codec_name: 'subrip', codec_type: 'subtitle', tags: expect.objectContaining({ language: 'eng', title: 'Browser subtitle', }), disposition: expect.objectContaining({ default: 1 }), }), ]) ); await subtitleOperation.selectOption('burn-in'); await subtitlePanel .locator('label.advanced-field') .filter({ hasText: /^Video export preset/u }) .locator('select') .selectOption('mp4-h264-compatibility'); const [burnedCard] = await runForResults( page, () => subtitlePanel .getByRole('button', { name: 'Burn subtitle into video' }) .click(), ['compatible-a-subtitle-burn.mp4'], { verifyMedia: true } ); expect( probe((await downloadResult(page, burnedCard as Locator)).filePath) .streams ).toEqual( expect.arrayContaining([ expect.objectContaining({ codec_name: 'h264', codec_type: 'video' }), ]) ); await page.getByRole('button', { name: 'New', exact: true }).click(); await mediaInput(page).setInputFiles({ name: 'subtitled-source.mkv', mimeType: 'video/x-matroska', buffer: readFileSync(muxed.filePath), }); await expect( page .locator('.media-card') .filter({ hasText: 'subtitled-source.mkv' }) .locator('.phase--ready') ).toBeVisible({ timeout: 120_000 }); await subtitleOperation.selectOption('extract'); await outputTextFormat.selectOption('srt'); const [extractedCard] = await runForResults( page, () => subtitlePanel .getByRole('button', { name: 'Extract subtitle' }) .click(), ['subtitled-source-subtitle-eng.srt'] ); const extracted = await downloadResult(page, extractedCard as Locator); expect(readFileSync(extracted.filePath, 'utf8')).toContain( 'Generated subtitle' ); }); expect( runtime.requests.some((url) => url.endsWith( '/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/st/ffmpeg-core.wasm' ) ) ).toBe(true); assertLocalOnly(runtime); }); });