feat: publish av-tools 0.2.0

This commit is contained in:
2026-07-27 01:04:21 +02:00
parent fcc537b024
commit 0a4548851c
68 changed files with 4130 additions and 605 deletions

View File

@@ -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);