feat: publish av-tools 0.2.0
This commit is contained in:
@@ -191,12 +191,12 @@ test.describe('nested static deployment and Toolbox context', () => {
|
||||
|
||||
const coreScript = runtime.responses.find((response) =>
|
||||
response.url.endsWith(
|
||||
'/deep/nested/av/vendor/ffmpeg/0.12.10/st/ffmpeg-core.js'
|
||||
'/deep/nested/av/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/st/ffmpeg-core.js'
|
||||
)
|
||||
);
|
||||
const coreWasm = runtime.responses.find((response) =>
|
||||
response.url.endsWith(
|
||||
'/deep/nested/av/vendor/ffmpeg/0.12.10/st/ffmpeg-core.wasm'
|
||||
'/deep/nested/av/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/st/ffmpeg-core.wasm'
|
||||
)
|
||||
);
|
||||
expect(coreScript).toMatchObject({
|
||||
|
||||
@@ -75,9 +75,9 @@ test.describe.serial('real pinned FFmpeg browser runtime', () => {
|
||||
);
|
||||
|
||||
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',
|
||||
'/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/ffmpeg-core.js',
|
||||
'/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/ffmpeg-core.wasm',
|
||||
'/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/ffmpeg-core.worker.js',
|
||||
];
|
||||
for (const suffix of expectedAssets) {
|
||||
expect(
|
||||
@@ -87,11 +87,15 @@ test.describe.serial('real pinned FFmpeg browser runtime', () => {
|
||||
).toBe(true);
|
||||
}
|
||||
const wasm = runtime.responses.find((response) =>
|
||||
response.url.endsWith('/vendor/ffmpeg/0.12.10/mt/ffmpeg-core.wasm')
|
||||
response.url.endsWith(
|
||||
'/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/ffmpeg-core.wasm'
|
||||
)
|
||||
);
|
||||
expect(wasm?.contentType).toContain('application/wasm');
|
||||
expect(
|
||||
runtime.requests.some((url) => url.includes('/vendor/ffmpeg/0.12.10/st/'))
|
||||
runtime.requests.some((url) =>
|
||||
url.includes('/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/st/')
|
||||
)
|
||||
).toBe(false);
|
||||
|
||||
await mediaInput(page).setInputFiles(PRIMARY_FIXTURE);
|
||||
@@ -119,8 +123,9 @@ test.describe.serial('real pinned FFmpeg browser runtime', () => {
|
||||
page,
|
||||
}) => {
|
||||
const runtime = observeRuntime(page);
|
||||
await page.route('**/vendor/ffmpeg/0.12.10/mt/**', (route) =>
|
||||
route.abort('failed')
|
||||
await page.route(
|
||||
'**/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/**',
|
||||
(route) => route.abort('failed')
|
||||
);
|
||||
await page.goto('/');
|
||||
const environment = await page.evaluate(() => ({
|
||||
@@ -147,13 +152,17 @@ test.describe.serial('real pinned FFmpeg browser runtime', () => {
|
||||
)
|
||||
).toBeVisible();
|
||||
expect(
|
||||
runtime.requests.some((url) => url.includes('/vendor/ffmpeg/0.12.10/mt/'))
|
||||
runtime.requests.some((url) =>
|
||||
url.includes('/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/')
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
runtime.responses.some(
|
||||
(response) =>
|
||||
response.status === 200 &&
|
||||
response.url.endsWith('/vendor/ffmpeg/0.12.10/st/ffmpeg-core.wasm')
|
||||
response.url.endsWith(
|
||||
'/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/st/ffmpeg-core.wasm'
|
||||
)
|
||||
)
|
||||
).toBe(true);
|
||||
expect(runtime.pageErrors).toEqual([]);
|
||||
@@ -247,7 +256,9 @@ test.describe.serial('real pinned FFmpeg browser runtime', () => {
|
||||
|
||||
expect(
|
||||
runtime.requests.some((url) =>
|
||||
url.endsWith('/vendor/ffmpeg/0.12.10/st/ffmpeg-core.wasm')
|
||||
url.endsWith(
|
||||
'/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/st/ffmpeg-core.wasm'
|
||||
)
|
||||
)
|
||||
).toBe(true);
|
||||
assertLocalOnly(runtime);
|
||||
@@ -303,7 +314,9 @@ test.describe.serial('real pinned FFmpeg browser runtime', () => {
|
||||
).toHaveCount(4);
|
||||
expect(
|
||||
runtime.requests.filter((url) =>
|
||||
url.endsWith('/vendor/ffmpeg/0.12.10/st/ffmpeg-core.wasm')
|
||||
url.endsWith(
|
||||
'/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/st/ffmpeg-core.wasm'
|
||||
)
|
||||
)
|
||||
).toHaveLength(2);
|
||||
assertLocalOnly(runtime);
|
||||
@@ -356,4 +369,43 @@ test.describe.serial('real pinned FFmpeg browser runtime', () => {
|
||||
await expect(page.getByRole('alert')).toHaveCount(0);
|
||||
assertLocalOnly(runtime);
|
||||
});
|
||||
|
||||
test('removes active and queued inspections without applying stale probe results', async ({
|
||||
page,
|
||||
}) => {
|
||||
const runtime = observeRuntime(page);
|
||||
await page.goto('/');
|
||||
await selectSingleThread(page);
|
||||
|
||||
await mediaInput(page).setInputFiles([
|
||||
cancellationFixture.filePath,
|
||||
PRIMARY_FIXTURE,
|
||||
]);
|
||||
const activeName = basename(cancellationFixture.filePath);
|
||||
const activeCard = page.locator('.media-card').filter({
|
||||
hasText: activeName,
|
||||
});
|
||||
const queuedCard = page.locator('.media-card').filter({
|
||||
hasText: 'pattern-av.mp4',
|
||||
});
|
||||
await expect(activeCard.locator('.phase--probing')).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(queuedCard.locator('.phase--queued')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Remove pattern-av.mp4' }).click();
|
||||
await page.getByRole('button', { name: `Remove ${activeName}` }).click();
|
||||
await expect(page.locator('.media-card')).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Choose files' })
|
||||
).toBeEnabled({ timeout: 120_000 });
|
||||
|
||||
await mediaInput(page).setInputFiles(PRIMARY_FIXTURE);
|
||||
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
|
||||
'Ready',
|
||||
{ timeout: 120_000 }
|
||||
);
|
||||
await expect(page.getByRole('alert')).toHaveCount(0);
|
||||
assertLocalOnly(runtime);
|
||||
});
|
||||
});
|
||||
|
||||
61
tests/browser/large-media-preview.spec.ts
Normal file
61
tests/browser/large-media-preview.spec.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
import { existsSync } from 'node:fs';
|
||||
import { basename } from 'node:path';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
const mediaPath = process.env.AV_TOOLS_LARGE_MEDIA;
|
||||
|
||||
test('plays a recognized large local video while inspection continues', async ({
|
||||
page,
|
||||
}) => {
|
||||
test.skip(
|
||||
!mediaPath || !existsSync(mediaPath),
|
||||
'Set AV_TOOLS_LARGE_MEDIA to a local video for this opt-in evidence run.'
|
||||
);
|
||||
test.setTimeout(360_000);
|
||||
await page.goto('/');
|
||||
|
||||
const startedAt = Date.now();
|
||||
await page
|
||||
.locator('input[type="file"][accept*="audio"]')
|
||||
.first()
|
||||
.setInputFiles(mediaPath as string);
|
||||
const name = basename(mediaPath as string);
|
||||
const card = page.locator('.media-card').filter({ hasText: name });
|
||||
const preview = page.locator('.preview video');
|
||||
await expect(preview).toBeVisible({ timeout: 15_000 });
|
||||
const previewVisibleMilliseconds = Date.now() - startedAt;
|
||||
expect(previewVisibleMilliseconds).toBeLessThan(15_000);
|
||||
await expect(card.locator('.phase--probing')).toBeVisible();
|
||||
|
||||
const elementIdentity = await preview.evaluate((element) => {
|
||||
element.dataset.evidenceIdentity = crypto.randomUUID();
|
||||
return element.dataset.evidenceIdentity;
|
||||
});
|
||||
await preview.evaluate(async (element) => {
|
||||
const video = element as HTMLVideoElement;
|
||||
video.muted = true;
|
||||
await video.play();
|
||||
});
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
preview.evaluate(
|
||||
(element) => (element as HTMLVideoElement).currentTime
|
||||
),
|
||||
{
|
||||
timeout: 15_000,
|
||||
}
|
||||
)
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
await expect(card.locator('.phase--ready')).toBeVisible({
|
||||
timeout: 330_000,
|
||||
});
|
||||
await expect(preview).toHaveAttribute(
|
||||
'data-evidence-identity',
|
||||
elementIdentity
|
||||
);
|
||||
await expect(page.getByRole('alert')).toHaveCount(0);
|
||||
});
|
||||
@@ -149,7 +149,7 @@ function probe(filePath: string): {
|
||||
tags?: Record<string, string>;
|
||||
disposition?: Record<string, number>;
|
||||
}>;
|
||||
format?: { tags?: Record<string, string> };
|
||||
format?: { duration?: string; tags?: Record<string, string> };
|
||||
chapters?: Array<{ tags?: Record<string, string> }>;
|
||||
} {
|
||||
return JSON.parse(
|
||||
@@ -547,6 +547,31 @@ test.describe.serial('real ffmpeg.wasm operation matrix', () => {
|
||||
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');
|
||||
@@ -687,6 +712,72 @@ test.describe.serial('real ffmpeg.wasm operation matrix', () => {
|
||||
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 () => {
|
||||
@@ -988,7 +1079,9 @@ test.describe.serial('real ffmpeg.wasm operation matrix', () => {
|
||||
|
||||
expect(
|
||||
runtime.requests.some((url) =>
|
||||
url.endsWith('/vendor/ffmpeg/0.12.10/st/ffmpeg-core.wasm')
|
||||
url.endsWith(
|
||||
'/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/st/ffmpeg-core.wasm'
|
||||
)
|
||||
)
|
||||
).toBe(true);
|
||||
assertLocalOnly(runtime);
|
||||
|
||||
Reference in New Issue
Block a user