/// import { execFileSync } from 'node:child_process'; import { basename, join } from 'node:path'; import { expect, test } from '@playwright/test'; import { createCancellationFixture, GENERATED_FIXTURES, observeRuntime, PRIMARY_FIXTURE, type TemporaryMediaFixture, } from './browser-helpers'; function mediaInput(page: import('@playwright/test').Page) { return page.locator('input[type="file"][accept*="audio"]'); } async function selectSingleThread( page: import('@playwright/test').Page ): Promise { await page.getByText('Engine & browser').click(); await page.getByLabel('Engine mode').selectOption('force-single-thread'); } function assertLocalOnly(runtime: ReturnType): 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')) ); // Chromium aborts the superseded local preview blob when React switches from // the source object URL to the generated result object URL. expect(materialFailures).toEqual([]); expect(runtime.pageErrors).toEqual([]); expect(runtime.consoleErrors).toEqual([]); } test.describe.serial('real pinned FFmpeg browser runtime', () => { let cancellationFixture: TemporaryMediaFixture; test.beforeAll(() => { cancellationFixture = createCancellationFixture(); }); test.afterAll(() => { cancellationFixture.dispose(); }); test('loads and queries the multithread core under cross-origin isolation', async ({ page, }) => { const runtime = observeRuntime(page); await page.goto('/'); const environment = await page.evaluate(() => ({ isolated: globalThis.crossOriginIsolated, sharedArrayBuffer: typeof globalThis.SharedArrayBuffer, })); test.skip( !environment.isolated || environment.sharedArrayBuffer !== 'function', 'Chromium did not expose SharedArrayBuffer in this server context' ); await page.getByText('Engine & browser').click(); await page.getByRole('button', { name: 'Initialize engine' }).click(); await expect( page.locator('.engine-pill').filter({ hasText: 'Multithreaded' }) ).toBeVisible({ timeout: 120_000 }); await expect(page.getByText('0.12.15 / 0.12.10')).toBeVisible(); await expect(page.getByText('Isolation').locator('..')).toContainText( 'Enabled' ); const expectedAssets = [ '/vendor/ffmpeg/0.12.10/mt/ffmpeg-core.js', '/vendor/ffmpeg/0.12.10/mt/ffmpeg-core.wasm', '/vendor/ffmpeg/0.12.10/mt/ffmpeg-core.worker.js', ]; for (const suffix of expectedAssets) { expect( runtime.responses.some( (response) => response.status === 200 && response.url.endsWith(suffix) ) ).toBe(true); } const wasm = runtime.responses.find((response) => response.url.endsWith('/vendor/ffmpeg/0.12.10/mt/ffmpeg-core.wasm') ); expect(wasm?.contentType).toContain('application/wasm'); expect( runtime.requests.some((url) => url.includes('/vendor/ffmpeg/0.12.10/st/')) ).toBe(false); await mediaInput(page).setInputFiles(PRIMARY_FIXTURE); await expect(page.locator('.source-summary .phase--ready')).toHaveText( 'Ready', { timeout: 120_000 } ); await page.getByRole('button', { name: 'Convert', exact: true }).click(); const result = page.locator('.result-card'); await expect(result).toContainText('pattern-av-convert.mp4', { timeout: 120_000, }); await expect(result).toContainText(/created · verified|completed locally/u); const outputBytes = await page .locator('.quick-preview video') .evaluate(async (element) => { const response = await fetch((element as HTMLVideoElement).src); return (await response.arrayBuffer()).byteLength; }); expect(outputBytes).toBeGreaterThan(1_000); assertLocalOnly(runtime); }); test('falls back to the single-thread core when the preferred multithread core cannot load', async ({ page, }) => { const runtime = observeRuntime(page); await page.route('**/vendor/ffmpeg/0.12.10/mt/**', (route) => route.abort('failed') ); await page.goto('/'); const environment = await page.evaluate(() => ({ isolated: globalThis.crossOriginIsolated, sharedArrayBuffer: typeof globalThis.SharedArrayBuffer, })); test.skip( !environment.isolated || environment.sharedArrayBuffer !== 'function', 'Chromium did not expose SharedArrayBuffer in this server context' ); await page.getByText('Engine & browser').click(); await page.getByLabel('Engine mode').selectOption('prefer-multithread'); await page.getByRole('button', { name: 'Initialize engine' }).click(); await expect( page .locator('.engine-pill') .filter({ hasText: 'Single-threaded compatibility mode' }) ).toBeVisible({ timeout: 120_000 }); await expect( page.getByText( 'The multithread core failed to load. The app recovered with the single-thread compatibility core.' ) ).toBeVisible(); expect( runtime.requests.some((url) => url.includes('/vendor/ffmpeg/0.12.10/mt/')) ).toBe(true); expect( runtime.responses.some( (response) => response.status === 200 && response.url.endsWith('/vendor/ffmpeg/0.12.10/st/ffmpeg-core.wasm') ) ).toBe(true); expect(runtime.pageErrors).toEqual([]); expect(runtime.consoleErrors).toEqual([]); }); test('probes H.264/AAC media and produces a verified H.264/AAC result with the single-thread core', async ({ page, }) => { await page.addInitScript(() => { Reflect.deleteProperty(globalThis, 'showSaveFilePicker'); }); const runtime = observeRuntime(page); await page.goto('/'); await selectSingleThread(page); expect( runtime.requests.some((url) => url.includes('/vendor/ffmpeg/')) ).toBe(false); await mediaInput(page).setInputFiles(PRIMARY_FIXTURE); await expect(page.locator('.source-summary .phase--ready')).toHaveText( 'Ready', { timeout: 120_000 } ); await expect(page.locator('.source-summary')).toContainText( 'pattern-av.mp4' ); await expect(page.locator('.source-summary')).toContainText('0:02.00'); await expect( page.getByRole('checkbox', { name: /#0 · h264/u }) ).toBeChecked(); await expect( page.getByRole('checkbox', { name: /#1 · aac/u }) ).toBeChecked(); await expect( page .locator('.engine-pill') .filter({ hasText: 'Single-threaded compatibility mode' }) ).toBeVisible(); await page.getByRole('button', { name: 'Convert', exact: true }).click(); const result = page.locator('.result-card'); await expect(result).toBeVisible({ timeout: 120_000 }); await expect(result).toContainText('pattern-av-convert.mp4'); await expect(result).toContainText(/created · verified|completed locally/u); const resultVideo = page.locator('.quick-preview video'); await expect(resultVideo).toHaveAttribute('src', /^blob:/u); const outputBytes = await resultVideo.evaluate(async (element) => { const video = element as HTMLVideoElement; const response = await fetch(video.src); return new Uint8Array(await response.arrayBuffer()).length; }); expect(outputBytes).toBeGreaterThan(1_000); const downloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'Save result' }).click(); const download = await downloadPromise; expect(download.suggestedFilename()).toBe('pattern-av-convert.mp4'); const downloadedPath = await download.path(); expect(downloadedPath).not.toBeNull(); const probeJson = execFileSync( 'ffprobe', [ '-v', 'error', '-show_entries', 'stream=codec_name,codec_type', '-of', 'json', downloadedPath as string, ], { encoding: 'utf8' } ); const nativeProbe = JSON.parse(probeJson) as { streams?: Array<{ codec_name?: string; codec_type?: string }>; }; expect(nativeProbe.streams).toEqual( expect.arrayContaining([ expect.objectContaining({ codec_name: 'h264', codec_type: 'video', }), expect.objectContaining({ codec_name: 'aac', codec_type: 'audio', }), ]) ); expect( runtime.requests.some((url) => url.endsWith('/vendor/ffmpeg/0.12.10/st/ffmpeg-core.wasm') ) ).toBe(true); assertLocalOnly(runtime); }); test('refreshes retained tile-filter state before a fourth contact sheet', async ({ page, }) => { test.setTimeout(120_000); const runtime = observeRuntime(page); await page.goto('/'); await selectSingleThread(page); await page.getByRole('tab', { name: 'Edit' }).click(); await mediaInput(page) .first() .setInputFiles(join(GENERATED_FIXTURES, 'compatible-a.mp4')); await expect( page .locator('.media-card') .filter({ hasText: 'compatible-a.mp4' }) .locator('.phase--ready') ).toBeVisible({ timeout: 120_000 }); const advanced = page.locator('.advanced-operations'); await advanced .getByRole('tab', { name: 'Thumbnails and contact sheets' }) .click(); 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 results = page.locator('.result-card'); const create = advanced.getByRole('button', { name: 'Generate contact sheet', }); for (let index = 1; index <= 4; index += 1) { await create.click(); await expect(results).toHaveCount(index, { timeout: 120_000 }); await expect(page.getByRole('alert')).toHaveCount(0); } await expect( results.filter({ hasText: 'compatible-a-contact-sheet' }) ).toHaveCount(4); expect( runtime.requests.filter((url) => url.endsWith('/vendor/ffmpeg/0.12.10/st/ffmpeg-core.wasm') ) ).toHaveLength(2); assertLocalOnly(runtime); }); test('cancels an active transcode, recreates the engine and completes another job', async ({ page, }) => { const runtime = observeRuntime(page); await page.goto('/'); await selectSingleThread(page); await mediaInput(page).setInputFiles(cancellationFixture.filePath); await expect(page.locator('.source-summary .phase--ready')).toHaveText( 'Ready', { timeout: 120_000 } ); await page.getByRole('button', { name: 'Convert', exact: true }).click(); const cancelButton = page.getByRole('button', { name: 'Cancel & recover', }); await expect(cancelButton).toBeVisible({ timeout: 30_000 }); await cancelButton.click(); 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: `Remove ${basename(cancellationFixture.filePath)}`, }) .click(); await mediaInput(page).setInputFiles(PRIMARY_FIXTURE); await expect(page.locator('.source-summary .phase--ready')).toHaveText( 'Ready', { timeout: 120_000 } ); await page.getByRole('button', { name: 'Convert', exact: true }).click(); await expect(page.locator('.result-card')).toContainText( 'pattern-av-convert.mp4', { timeout: 120_000 } ); await expect(page.getByRole('alert')).toHaveCount(0); assertLocalOnly(runtime); }); });