import { useCallback, useEffect, useRef, useState } from 'react'; import { AppShell } from '@add-ideas/toolbox-shell-react'; import '@add-ideas/toolbox-shell-react/styles.css'; import './index.css'; import { createAsset, type ImportedMediaAsset, type WorkspaceMode, } from './app/application-state'; import { AppErrorBoundary } from './app/AppErrorBoundary'; import { SourceInspectionRegistry, type SourceInspectionToken, } from './app/source-inspection-registry'; import { FileDropZone } from './components/FileDropZone'; import { MediaBin } from './components/MediaBin'; import { MediaPreview, type PreviewProxySettings, } from './components/MediaPreview'; import { QuickConvert, type QuickExportConfiguration, } from './components/QuickConvert'; import { CapabilityPanel } from './components/CapabilityPanel'; import { JobPanel } from './components/JobPanel'; import { StoragePanel } from './components/StoragePanel'; import { HelpDialog } from './components/HelpDialog'; import { Icon } from './components/Icon'; import { Timeline } from './components/Timeline'; import { AdvancedOperations, type AdvancedAudioRequest, type AdvancedCapabilityKey, type AdvancedCapabilityStatus, type AdvancedExportPresetOption, type AdvancedSplitBatchStatus, type AdvancedSplitRequest, type ChapterExecutionRequest, type ContactSheetRequest, type MetadataExecutionRequest, type SubtitleExecutionRequest, type ThumbnailSeriesRequest, type UserPresetAction, } from './components/AdvancedOperations'; import { StructuralOperations, type StructuralCompatibilityDiagnostic, type StructuralConcatRequest, type StructuralTrimRequest, } from './components/StructuralOperations'; import { Inspector, type ThumbnailRequest } from './components/Inspector'; import { engineManager } from './ffmpeg/EngineManager'; import type { EngineJobContext, EnginePreference, EngineState, } from './ffmpeg/ffmpeg.types'; import { EngineError } from './ffmpeg/ffmpeg-error'; import { normalizeFfprobeReport } from './media'; import { createEmptyProject, importProjectDocument, projectDocumentFileName, projectReducer, rankReattachmentCandidates, serializeProject, type AvProjectV1, type MediaAssetReference, type OutputSettings, type ProjectAction, type TimelineClip, } from './project'; import type { FFmpegCommandPlan } from './commands/command-plan'; import { buildConvertPlan, requirementsForUserPreset, } from './commands/convert'; import { buildRemuxPlan, type RemuxStream } from './commands/remux'; import { buildSingleThumbnailPlan } from './commands/thumbnails'; import { buildPreviewProxyPlan } from './commands/preview-proxy'; import { buildTimelineExportPlan } from './commands/timeline-export'; import { buildContactSheetPlan, buildSceneChangeAnalysisPlan, buildSceneThumbnailSeriesPlan, buildThumbnailSeriesPlan, parseSceneChangeFrames, } from './commands/thumbnails'; import { buildSplitPlan } from './commands/split'; import { buildTrimPlan } from './commands/trim'; import { buildFastConcatPlan, buildNormalizedConcatPlan, validateConcatCompatibility, type ConcatSource, } from './commands/concatenate'; import { buildStaticWaveformPlan, buildWaveformAnalysisPlan, } from './commands/waveform'; import { buildLoudnessAnalysisPlan, buildLoudnessApplyPlan, buildPeakAnalysisPlan, buildPeakNormalizationPlan, parseLoudnessMeasurement, parsePeakMeasurement, type LoudnessMeasurement, } from './commands/loudness'; import { buildMetadataEditPlan, type MetadataEdits } from './commands/metadata'; import { buildChapterMuxPlan } from './commands/chapters'; import { buildSubtitleBurnPlan, buildSubtitleConversionPlan, buildSubtitleExtractionPlan, buildSubtitleMuxPlan, } from './commands/subtitles'; import { calculateResize, validateCrop } from './commands/transform'; import { BUILT_IN_PRESETS, findBuiltInPreset, presetAvailability, } from './presets/preset-registry'; import { IndexedDbUserPresetPersistence, MemoryUserPresetPersistence, UserPresetRegistry, } from './presets/user-presets'; import type { UserExportPreset } from './presets/preset.types'; import { collectCommandOutputs, createExportReport, downloadBlob, ExportResultCollection, saveCollectionResult, saveExportReport, saveResultsZip, safeExportFileName, type ExportReport, type ExportResult, } from './export'; import { manifest } from './toolbox/manifest'; import { cacheDerivative, readCachedDerivative } from './app/derived-cache'; import { DEFAULT_RESOURCE_POLICY, estimatePlanResources, policyForDeviceMemory, preflightResources, type ResourcePolicy, } from './storage'; import { assessGeneratedResultPreview, type MediaProbe } from './media'; import { createWaveformRuntime, deserializeWaveformPeaks, serializeWaveformPeaks, WAVEFORM_PEAKS_MEDIA_TYPE, type WaveformPeakData, } from './waveform'; import type { MediaJob, MediaJobStep } from './jobs'; import { loadBundledContactSheetFont } from './assets/bundled-font'; import { FFMPEG_CORE_VERSION, FFMPEG_OPUS_REGRESSION_VERIFIED, } from './version'; function timestamp(): string { return new Date().toISOString(); } function canPreviewGeneratedResult( result: Pick, probe?: MediaProbe ): boolean { const element = document.createElement( result.mimeType.startsWith('video/') ? 'video' : 'audio' ); return assessGeneratedResultPreview( { fileName: result.fileName, mimeType: result.mimeType, ...(probe ? { probe } : {}), }, (mimeType) => element.canPlayType(mimeType) ).previewable; } interface RuntimeResourceContext { readonly deviceMemoryGiB?: number; readonly policy: ResourcePolicy; } function createRuntimeResourceContext(): RuntimeResourceContext { const reportedDeviceMemory = typeof globalThis.navigator === 'undefined' ? undefined : ( globalThis.navigator as Navigator & { readonly deviceMemory?: unknown; } ).deviceMemory; const deviceMemoryGiB = typeof reportedDeviceMemory === 'number' && Number.isFinite(reportedDeviceMemory) && reportedDeviceMemory > 0 ? reportedDeviceMemory : undefined; return { ...(deviceMemoryGiB === undefined ? {} : { deviceMemoryGiB }), policy: policyForDeviceMemory(DEFAULT_RESOURCE_POLICY, deviceMemoryGiB), }; } function fileDescriptor(file: File) { return { name: file.name, size: file.size, lastModified: file.lastModified, mimeType: file.type, }; } function extensionFromName(fileName: string): string { const extension = fileName.split('.').at(-1)?.toLowerCase(); return extension && /^[a-z0-9]{1,10}$/u.test(extension) ? extension : 'mkv'; } function muxerForExtension(extension: string): string { const muxers: Readonly> = { m4a: 'ipod', mkv: 'matroska', jpg: 'image2', jpeg: 'image2', png: 'image2', }; return muxers[extension] ?? extension; } function outputWithOptionalNumber( output: OutputSettings, field: 'width' | 'height' | 'frameRate', rawValue: string ): OutputSettings { const next = { ...output }; if (rawValue.trim() === '') { delete next[field]; return next; } const value = Number(rawValue); if (!Number.isFinite(value)) return output; if (field === 'frameRate') { next.frameRate = Math.min(240, Math.max(0.01, value)); } else { next[field] = Math.min(8192, Math.max(2, Math.round(value / 2) * 2)); } return next; } function subtitleCodecForFormat(format: 'srt' | 'vtt' | 'ass'): string { return format === 'srt' ? 'subrip' : format === 'vtt' ? 'webvtt' : 'ass'; } function projectMetadataFromEdits( edits: MetadataEdits ): AvProjectV1['metadata'] { const format = edits.format ?? {}; const text = (key: string) => { const value = format[key]; return value === null || value === undefined ? undefined : String(value); }; const known = new Set(['title', 'artist', 'album', 'comment']); return { ...(text('title') ? { title: text('title') } : {}), ...(text('artist') ? { artist: text('artist') } : {}), ...(text('album') ? { album: text('album') } : {}), ...(text('comment') ? { comment: text('comment') } : {}), custom: Object.fromEntries( Object.entries(format) .filter(([key, value]) => !known.has(key) && value !== null) .map(([key, value]) => [key, String(value)]) ), }; } function metadataEditsFromProject( metadata: AvProjectV1['metadata'] ): MetadataEdits { return { format: { ...(metadata.title ? { title: metadata.title } : {}), ...(metadata.artist ? { artist: metadata.artist } : {}), ...(metadata.album ? { album: metadata.album } : {}), ...(metadata.comment ? { comment: metadata.comment } : {}), ...metadata.custom, }, }; } function resultAsFile(result: ExportResult): File { return new File([result.blob], result.fileName, { type: result.mimeType, lastModified: Date.now(), }); } function concatSourceForClip( clip: TimelineClip, asset: ImportedMediaAsset, sourceIndex: number ): ConcatSource { const durationSeconds = asset.probe?.durationSeconds; if (durationSeconds === undefined || durationSeconds <= 0) { throw new Error(`Source ${asset.file.name} has no validated duration.`); } const streams = asset.probe?.streams.flatMap((stream) => stream.type === 'video' || stream.type === 'audio' || stream.type === 'subtitle' ? [ { kind: stream.type, codec: stream.codecName ?? 'unknown', ...(stream.width === undefined ? {} : { width: stream.width }), ...(stream.height === undefined ? {} : { height: stream.height }), ...(stream.pixelFormat === undefined ? {} : { pixelFormat: stream.pixelFormat }), ...(stream.timeBase === undefined ? {} : { timeBase: stream.timeBase }), ...(stream.sampleRate === undefined ? {} : { sampleRate: stream.sampleRate }), ...(stream.channelLayout === undefined ? {} : { channelLayout: stream.channelLayout }), }, ] : [] ) ?? []; return { id: clip.id, sourceIndex, fileName: asset.file.name, durationSeconds, sourceInSeconds: clip.sourceInSeconds, sourceOutSeconds: clip.sourceOutSeconds, streams, }; } function remuxStreamsFromProbe(probe: MediaProbe): readonly RemuxStream[] { return probe.streams.flatMap((stream) => stream.type === 'video' || stream.type === 'audio' || stream.type === 'subtitle' || stream.type === 'attachment' || stream.type === 'data' ? [ { index: stream.index, kind: stream.type, codec: stream.codecName ?? 'unknown', }, ] : [] ); } function createPresetRegistry(): { readonly registry: UserPresetRegistry; readonly persistent: boolean; } { try { return { registry: new UserPresetRegistry(new IndexedDbUserPresetPersistence()), persistent: true, }; } catch { return { registry: new UserPresetRegistry(new MemoryUserPresetPersistence()), persistent: false, }; } } function replaceVirtualPaths( plan: FFmpegCommandPlan, context: EngineJobContext ): readonly string[] { const replacements = new Map(); plan.inputs.forEach((input, index) => { const actual = context.inputPaths[index]; if (actual) replacements.set(input.path, actual); }); plan.outputs.forEach((output, index) => { const actual = context.outputPaths[index]; if (actual) replacements.set(output.path, actual); }); plan.temporaryFiles.forEach((temporary, index) => { const actual = context.temporaryPaths[index]; if (actual) replacements.set(temporary.path, actual); }); return plan.args.map((argument) => [...replacements].reduce( (resolved, [virtualPath, actualPath]) => resolved.replaceAll(virtualPath, actualPath), argument ) ); } function replaceTemporaryContentPaths( content: string | Uint8Array, plan: FFmpegCommandPlan, context: EngineJobContext ): string | Uint8Array { if (typeof content !== 'string') return content; const replacements = new Map(); plan.inputs.forEach((input, index) => { const actual = context.inputPaths[index]; if (actual) replacements.set(input.path, actual); }); plan.outputs.forEach((output, index) => { const actual = context.outputPaths[index]; if (actual) replacements.set(output.path, actual); }); plan.temporaryFiles.forEach((temporary, index) => { const actual = context.temporaryPaths[index]; if (actual) replacements.set(temporary.path, actual); }); return [...replacements].reduce( (resolved, [virtualPath, actualPath]) => resolved.replaceAll(virtualPath, actualPath), content ); } export interface OutputVerification { readonly probe?: MediaProbe; readonly warning?: string; } interface PlanExecution { readonly results: readonly ExportResult[]; readonly logText: string; readonly verifications?: Readonly>; readonly report?: ExportReport; } type ExecutePlan = ( plan: FFmpegCommandPlan, inputs: readonly File[], operation: string, onStage?: (stage: 'reading-output') => void ) => Promise; interface ScheduledWorkDefinition { readonly operation: string; readonly projectId: string; readonly totalSteps: number; readonly retryCount: number; readonly work: (executeStep: ExecutePlan) => Promise; cancelled: boolean; counted: boolean; started: boolean; } function createQueuedJob( id: string, operation: string, totalSteps: number, createdAt: string, retryCount: number ): MediaJob { return { id, operation, status: 'queued', steps: Array.from({ length: totalSteps }, (_, index) => ({ id: `${id}:step-${index + 1}`, name: totalSteps === 1 ? operation : `Step ${index + 1}`, weight: 1 / totalSteps, status: 'queued', progress: 0, planId: 'pending', })), progress: 0, createdAt, updatedAt: createdAt, retryCount, }; } function replaceJob( jobs: readonly MediaJob[], id: string, update: (job: MediaJob) => MediaJob ): readonly MediaJob[] { return jobs.map((job) => (job.id === id ? update(job) : job)); } export function AvToolsApplication() { const [mode, setMode] = useState('quick-convert'); const [assets, setAssets] = useState([]); const [selectedAssetId, setSelectedAssetId] = useState(); const [selectedClipId, setSelectedClipId] = useState(); const [project, setProject] = useState(() => createEmptyProject() ); const [subtitleFiles, setSubtitleFiles] = useState>( () => new Map() ); const [waveformByAsset, setWaveformByAsset] = useState< Readonly> >({}); const [splitMarkersByAsset, setSplitMarkersByAsset] = useState< Readonly> >({}); const [splitBatch, setSplitBatch] = useState(); const [loudnessByAsset, setLoudnessByAsset] = useState< Readonly> >({}); const [waveformProgress, setWaveformProgress] = useState(); const [playheadByAsset, setPlayheadByAsset] = useState< Readonly> >({}); const [previewProxySettings, setPreviewProxySettings] = useState({ maxDurationSeconds: 30, maxWidth: 960, }); const [presetSetup] = useState(createPresetRegistry); const [userPresets, setUserPresets] = useState( [] ); const [waveformRuntime] = useState(createWaveformRuntime); const [engineState, setEngineState] = useState( engineManager.state ); const [enginePreference, setEnginePreference] = useState('automatic'); const [resourceContext] = useState(createRuntimeResourceContext); const [logs, setLogs] = useState([]); const [resultCollection] = useState(() => new ExportResultCollection()); const [results, setResults] = useState([]); const [previewResultId, setPreviewResultId] = useState(); const [outputVerifications, setOutputVerifications] = useState< Readonly> >({}); const [latestReport, setLatestReport] = useState(); const [operationError, setOperationError] = useState(); const [projectNotice, setProjectNotice] = useState(); const [sourceProbeBusy, setSourceProbeBusy] = useState(false); const [scheduledPlanCount, setScheduledPlanCount] = useState(0); const [scheduledJobs, setScheduledJobs] = useState([]); const [editDrawer, setEditDrawer] = useState< 'media' | 'inspector' | undefined >(); const projectInput = useRef(null); const mediaDrawerTrigger = useRef(null); const inspectorDrawerTrigger = useRef(null); const assetsRef = useRef(assets); const projectRef = useRef(project); const sourceProbeTail = useRef>(Promise.resolve()); const sourceProbePendingRef = useRef(0); const sourceInspectionRegistry = useRef(new SourceInspectionRegistry()); const planExecutionTail = useRef>(Promise.resolve()); const scheduledPlanCountRef = useRef(0); const scheduledWorkRef = useRef(new Map()); const activeScheduledJobIdRef = useRef(undefined); const activeScheduledStepIndexRef = useRef(undefined); useEffect(() => { assetsRef.current = assets; }, [assets]); useEffect(() => { projectRef.current = project; }, [project]); useEffect(() => { const activeJobId = activeScheduledJobIdRef.current; const activeStepIndex = activeScheduledStepIndexRef.current; if ( !activeJobId || activeStepIndex === undefined || engineState.status !== 'running' || engineState.progress === undefined ) { return; } setScheduledJobs((current) => replaceJob(current, activeJobId, (job) => { const steps = job.steps.map((step, index) => index === activeStepIndex ? { ...step, progress: Math.min( 0.99, Math.max(0, engineState.progress ?? 0) ), } : step ); const progress = Math.min( 0.99, steps.reduce( (sum, step) => sum + step.weight * (step.status === 'completed' ? 1 : step.progress), 0 ) ); return { ...job, status: 'running', steps, progress, updatedAt: timestamp(), }; }) ); }, [engineState]); useEffect(() => { const activeJobId = activeScheduledJobIdRef.current; if (!activeJobId) return; const status = engineState.status === 'recovering' ? ('recovering' as const) : engineState.status === 'loading' ? ('preparing' as const) : undefined; if (!status) return; setScheduledJobs((current) => replaceJob(current, activeJobId, (job) => ({ ...job, status, updatedAt: timestamp(), })) ); }, [engineState.status]); const closeEditDrawer = useCallback(() => { const closing = editDrawer; setEditDrawer(undefined); globalThis.queueMicrotask(() => { (closing === 'media' ? mediaDrawerTrigger.current : inspectorDrawerTrigger.current )?.focus(); }); }, [editDrawer]); const selectWorkspaceMode = useCallback((nextMode: WorkspaceMode) => { setEditDrawer(undefined); setMode(nextMode); }, []); useEffect(() => { if (!editDrawer) return; const closeOnEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') { event.preventDefault(); closeEditDrawer(); } }; globalThis.addEventListener('keydown', closeOnEscape); const closeButton = document.querySelector( editDrawer === 'media' ? '#edit-media-drawer .mobile-edit-drawer__close' : '#edit-inspector-drawer .mobile-edit-drawer__close' ); closeButton?.focus(); return () => globalThis.removeEventListener('keydown', closeOnEscape); }, [closeEditDrawer, editDrawer]); useEffect( () => engineManager.subscribe((state) => { setEngineState(state); }), [] ); useEffect(() => { let active = true; void presetSetup.registry .initialize() .then(() => { if (active) setUserPresets(presetSetup.registry.list()); }) .catch((error: unknown) => { if (active) { setProjectNotice( error instanceof Error ? `User presets could not be loaded: ${error.message}` : 'User presets could not be loaded.' ); } }); return () => { active = false; }; }, [presetSetup]); useEffect( () => engineManager.onLog(({ message }) => { setLogs((current) => [...current.slice(-399), message]); }), [] ); useEffect(() => { const inspectionRegistry = sourceInspectionRegistry.current; return () => { const cancelActiveInspection = inspectionRegistry.clear(); if (cancelActiveInspection) void engineManager.cancelActive(); for (const asset of assetsRef.current) { URL.revokeObjectURL(asset.objectUrl); } resultCollection.dispose(); waveformRuntime.terminate(); }; }, [resultCollection, waveformRuntime]); const applyProjectAction = useCallback((action: ProjectAction) => { setProject((current) => projectReducer(current, action)); }, []); const updateAssetState = useCallback( (id: string, changes: Partial) => { setAssets((current) => current.map((asset) => asset.id === id ? { ...asset, ...changes } : asset ) ); }, [] ); const runSourceProbe = useCallback( (probe: () => Promise): Promise => { sourceProbePendingRef.current += 1; setSourceProbeBusy(true); const execution = sourceProbeTail.current.then(probe, probe); sourceProbeTail.current = execution.then( () => undefined, () => undefined ); void execution .finally(() => { sourceProbePendingRef.current = Math.max( 0, sourceProbePendingRef.current - 1 ); setSourceProbeBusy(sourceProbePendingRef.current > 0); }) .catch(() => undefined); return execution; }, [] ); const inspectAsset = useCallback( async ( asset: ImportedMediaAsset, existingAsset: boolean, token: SourceInspectionToken ) => { if (!sourceInspectionRegistry.current.isCurrent(token)) return; updateAssetState(asset.id, { phase: 'probing', error: undefined, }); try { const probeResult = await runSourceProbe(async () => { if (!sourceInspectionRegistry.current.activate(token)) { return undefined; } try { return await engineManager.probe( asset.file, undefined, token.signal ); } finally { sourceInspectionRegistry.current.finish(token); } }); if ( !probeResult || !sourceInspectionRegistry.current.isCurrent(token) ) { return; } const probe = normalizeFfprobeReport(probeResult.json); const rawProbe = `${JSON.stringify(probeResult.json, null, 2)}\n`; const importedChapters = probe.chapters.map((chapter) => ({ id: `${asset.id}-chapter-${chapter.id}`, startSeconds: chapter.startSeconds, endSeconds: chapter.endSeconds, title: chapter.title ?? `Chapter ${chapter.id + 1}`, ...(chapter.timeBase ? { timeBase: chapter.timeBase } : {}), ...(Object.keys(chapter.tags).length > 0 ? { metadata: chapter.tags } : {}), })); if (!sourceInspectionRegistry.current.isCurrent(token)) return; updateAssetState(asset.id, { phase: 'ready', probe, rawProbe }); setProject((current) => { if ( !sourceInspectionRegistry.current.isCurrent(token) || !current.assets.some((entry) => entry.id === asset.id) ) { return current; } let next = projectReducer(current, { type: 'asset/update', assetId: asset.id, changes: { probe, mimeType: asset.file.type }, updatedAt: timestamp(), }); if ( existingAsset && next.assets.find((entry) => entry.id === asset.id)?.sourceStatus === 'missing' ) { next = projectReducer(next, { type: 'asset/set-source-status', assetId: asset.id, sourceStatus: 'attached', updatedAt: timestamp(), }); } const hasClip = next.timeline.some( (clip) => clip.assetId === asset.id ); if ( !hasClip && probe.durationSeconds !== undefined && probe.durationSeconds >= 0.001 ) { next = projectReducer(next, { type: 'clip/add', clip: { id: crypto.randomUUID(), assetId: asset.id, sourceInSeconds: 0, sourceOutSeconds: probe.durationSeconds, audio: { enabled: true }, video: { enabled: true }, }, updatedAt: timestamp(), }); } if (next.chapters.length === 0 && importedChapters.length > 0) { next = projectReducer(next, { type: 'chapters/set', chapters: importedChapters, updatedAt: timestamp(), }); } return next; }); } catch (error) { if (!sourceInspectionRegistry.current.isCurrent(token)) return; updateAssetState(asset.id, { phase: 'error', error: error instanceof Error ? error.message : 'The file could not be inspected.', }); } }, [runSourceProbe, updateAssetState] ); const addFiles = useCallback( async (files: File[]) => { setOperationError(undefined); setProjectNotice(undefined); if ( scheduledPlanCountRef.current > 0 || (engineManager.state.status === 'running' && sourceProbePendingRef.current === 0) ) { setOperationError( 'Wait for the active export or analysis job before importing media.' ); return; } const importDecision = preflightResources( { inputBytes: [ ...assetsRef.current.map((asset) => asset.file.size), ...files.map((file) => file.size), ], }, resourceContext.policy ); if (importDecision.status === 'blocked') { setOperationError( importDecision.issues .map((issue) => `${issue.message} ${issue.suggestions.join(' / ')}`) .join(' ') ); return; } if ( importDecision.status === 'warning' && !globalThis.confirm( `${importDecision.issues .map((issue) => issue.message) .join( '\n' )}\n\n${importDecision.estimateDisclaimer}\n\nImport and inspect anyway?` ) ) { return; } const pending: Array<{ asset: ImportedMediaAsset; existingAsset: boolean; token: SourceInspectionToken; }> = []; const claimedIds = new Set(assetsRef.current.map((asset) => asset.id)); for (const file of files) { const candidateMatches = project.assets .filter( (entry) => entry.sourceStatus === 'missing' && !claimedIds.has(entry.id) ) .map((entry) => ({ entry, match: rankReattachmentCandidates(entry, [fileDescriptor(file)])[0], })) .filter( ( value ): value is { entry: MediaAssetReference; match: NonNullable<(typeof value)['match']>; } => value.match?.confidence !== 'none' ) .sort((left, right) => right.match.score - left.match.score); const match = candidateMatches[0]; const reattach = match !== undefined && globalThis.confirm( `Reattach “${file.name}” to the missing project source “${match.entry.originalName}”?\n\n${match.match.evidence .map((evidence) => evidence.message) .join(' ')}` ); const asset = createAsset(file); if (reattach && match) { asset.id = match.entry.id; claimedIds.add(match.entry.id); pending.push({ asset, existingAsset: true, token: sourceInspectionRegistry.current.register(asset.id), }); } else { setProject((current) => projectReducer(current, { type: 'asset/add', asset: { id: asset.id, originalName: file.name, size: file.size, lastModified: file.lastModified, mimeType: file.type, sourceStatus: 'attached', }, updatedAt: timestamp(), }) ); pending.push({ asset, existingAsset: false, token: sourceInspectionRegistry.current.register(asset.id), }); } } setAssets((current) => { const next = [...current, ...pending.map(({ asset }) => asset)]; assetsRef.current = next; return next; }); setSelectedAssetId((current) => current ?? pending[0]?.asset.id); for (const item of pending) { await inspectAsset(item.asset, item.existingAsset, item.token); } }, [inspectAsset, project.assets, resourceContext.policy] ); const removeAsset = useCallback( (id: string) => { const asset = assets.find((entry) => entry.id === id); if (asset) URL.revokeObjectURL(asset.objectUrl); const cancelActiveInspection = sourceInspectionRegistry.current.remove(id); setAssets((current) => { const next = current.filter((entry) => entry.id !== id); assetsRef.current = next; return next; }); setWaveformByAsset((current) => Object.fromEntries( Object.entries(current).filter(([assetId]) => assetId !== id) ) ); setSplitMarkersByAsset((current) => Object.fromEntries( Object.entries(current).filter(([assetId]) => assetId !== id) ) ); setLoudnessByAsset((current) => Object.fromEntries( Object.entries(current).filter(([assetId]) => assetId !== id) ) ); setPlayheadByAsset((current) => Object.fromEntries( Object.entries(current).filter(([assetId]) => assetId !== id) ) ); if (selectedAssetId === id) { setSelectedAssetId(assets.find((entry) => entry.id !== id)?.id); } applyProjectAction({ type: 'asset/remove', assetId: id, updatedAt: timestamp(), }); if (cancelActiveInspection) void engineManager.cancelActive(); }, [applyProjectAction, assets, selectedAssetId] ); const selectedAsset = assets.find((asset) => asset.id === selectedAssetId); const selectedClip = project.timeline.find( (clip) => clip.id === selectedClipId ); const inspectedClipAsset = selectedClip ? assets.find((asset) => asset.id === selectedClip.assetId) : selectedAsset; const previewOutput = results.find((entry) => entry.id === previewResultId); const previewProbe = previewOutput ? outputVerifications[previewOutput.id]?.probe : undefined; const previewResult = previewOutput ? { name: previewOutput.fileName, mimeType: previewOutput.mimeType, objectUrl: resultCollection.objectUrl(previewOutput.id), ...(previewProbe ? { probe: previewProbe } : {}), } : undefined; const refreshResults = useCallback(() => { const current = resultCollection.list(); setResults(current); setPreviewResultId((selected) => selected && current.some((entry) => entry.id === selected) ? selected : current.find((entry) => canPreviewGeneratedResult(entry))?.id ); }, [resultCollection]); const clearResult = useCallback(() => { resultCollection.clear(); setResults([]); setPreviewResultId(undefined); setOutputVerifications({}); setLatestReport(undefined); }, [resultCollection]); const executePlan = useCallback( async ( plan: FFmpegCommandPlan, inputs: readonly File[], operation: string, onStage?: (stage: 'reading-output') => void ): Promise => { if ( plan.diagnostics.some((diagnostic) => diagnostic.severity === 'error') ) { throw new Error( plan.diagnostics .filter((diagnostic) => diagnostic.severity === 'error') .map((diagnostic) => diagnostic.message) .join(' ') ); } if (sourceProbePendingRef.current > 0) { throw new EngineError( 'busy', 'Wait for media inspection to finish before starting an export.' ); } const resourceSources = inputs.map((file) => { const asset = assetsRef.current.find((entry) => entry.file === file); return { size: file.size, ...(asset?.probe ? { probe: asset.probe } : {}), }; }); const resourceDecision = preflightResources( { inputBytes: resourceSources.map((source) => source.size), ...estimatePlanResources(plan, resourceSources), retainedOutputs: resultCollection.size, streamsPerInput: resourceSources.map( (source) => source.probe?.streams.length ?? 0 ), }, resourceContext.policy ); if (resourceDecision.status === 'blocked') { throw new Error( resourceDecision.issues .map((issue) => `${issue.message} ${issue.suggestions.join(' / ')}`) .join(' ') ); } if ( resourceDecision.status === 'warning' && !globalThis.confirm( `${resourceDecision.issues .map((issue) => issue.message) .join('\n')}\n\n${resourceDecision.estimateDisclaimer}\n\nContinue?` ) ) { throw new EngineError('cancelled', 'The operation was not started.'); } setOperationError(undefined); setLogs([]); await engineManager.initialize(); const readyState = engineManager.state; if (readyState.status !== 'ready') { throw new Error('The local FFmpeg engine did not become ready.'); } const missing = ( [ ['muxers', plan.requiredCapabilities.muxers], ['encoders', plan.requiredCapabilities.encoders], ['decoders', plan.requiredCapabilities.decoders], ['filters', plan.requiredCapabilities.filters], ] as const ).flatMap(([kind, required]) => (required ?? []) .filter((name) => !readyState.capabilities[kind].has(name)) .map((name) => `${kind.slice(0, -1)} ${name}`) ); if (missing.length > 0) { throw new Error( `The pinned FFmpeg core is missing required capabilities: ${missing.join(', ')}.` ); } const completed = await engineManager.runJob({ id: plan.id.split(':')[0] ?? crypto.randomUUID(), operation, coreWorkload: plan.operation, inputs, temporaryFiles: plan.temporaryFiles.map((temporary) => ({ name: temporary.path.split('/').at(-1) ?? 'temporary.dat', content: (context: EngineJobContext) => replaceTemporaryContentPaths(temporary.content, plan, context), })), outputs: plan.outputs.map((output) => ({ name: output.fileName, mimeType: output.mediaType, })), expectedDurationSeconds: plan.expectedDurationSeconds, timeoutMilliseconds: plan.timeoutMs, buildArguments: (context) => replaceVirtualPaths(plan, context), }); onStage?.('reading-output'); const logText = completed.logs.map((entry) => entry.message).join('\n'); if (plan.outputs.length === 0) { return { results: [], logText }; } const outputMap = new Map( completed.outputs.flatMap((output, index) => { const planned = plan.outputs[index]; return planned ? ([[planned.id, Uint8Array.from(output.bytes)] as const] as const) : []; }) ); const collected = collectCommandOutputs( plan, outputMap, resultCollection ); const verifications: Record = {}; const probeLimit = 8; for (const [index, output] of collected.entries()) { if ( !output.mimeType.startsWith('video/') && !output.mimeType.startsWith('audio/') ) { continue; } if (index >= probeLimit) { verifications[output.id] = { warning: 'This batch item was not re-probed because output verification is bounded to the first eight results.', }; continue; } try { const probeResult = await engineManager.probe( new File([output.blob], output.fileName, { type: output.mimeType, lastModified: Date.now(), }) ); verifications[output.id] = { probe: normalizeFfprobeReport(probeResult.json), }; } catch (error) { verifications[output.id] = { warning: error instanceof Error ? `Output was created, but its verification probe failed: ${error.message}` : 'Output was created, but its verification probe failed.', }; } } setOutputVerifications((current) => ({ ...current, ...verifications, })); const readyStateAfterRun = engineManager.state; const report = createExportReport({ plan, results: collected, execution: { status: 'completed', exitCode: completed.exitCode, elapsedSeconds: (completed.timings.conversionMilliseconds + completed.timings.outputReadMilliseconds + completed.timings.cleanupMilliseconds) / 1_000, conversionMilliseconds: completed.timings.conversionMilliseconds, outputReadMilliseconds: completed.timings.outputReadMilliseconds, cleanupMilliseconds: completed.timings.cleanupMilliseconds, ...(readyStateAfterRun.status === 'ready' ? { engineMode: readyStateAfterRun.mode === 'multithread' ? ('multi-thread' as const) : ('single-thread' as const), ffmpegVersion: readyStateAfterRun.capabilities.versionText, coreVersion: FFMPEG_CORE_VERSION, } : {}), }, diagnostics: Object.values(verifications).flatMap((verification) => verification.warning ? [ { code: 'output-probe-failed', severity: 'warning' as const, message: verification.warning, }, ] : [] ), }); setLatestReport(report); refreshResults(); setPreviewResultId( collected.find((entry) => canPreviewGeneratedResult(entry, verifications[entry.id]?.probe) )?.id ); return { results: collected, logText, verifications, report }; }, [refreshResults, resourceContext.policy, resultCollection] ); const enqueueWork = useCallback( ( operation: string, totalSteps: number, work: (executeStep: ExecutePlan) => Promise, retryCount = 0 ): Promise => { if (sourceProbePendingRef.current > 0) { return Promise.reject( new EngineError( 'busy', 'Wait for media inspection to finish before adding an export.' ) ); } if (scheduledPlanCountRef.current >= 8) { return Promise.reject( new EngineError( 'busy', 'The local queue already contains eight operations. Wait for one to finish or cancel a waiting job.' ) ); } const id = crypto.randomUUID(); const createdAt = timestamp(); const definition: ScheduledWorkDefinition = { operation, projectId: projectRef.current.id, totalSteps: Math.max(1, Math.floor(totalSteps)), retryCount, work: work as (executeStep: ExecutePlan) => Promise, cancelled: false, counted: true, started: false, }; scheduledWorkRef.current.set(id, definition); setScheduledJobs((current) => { const retained = current.slice(-23); const retainedIds = new Set(retained.map((job) => job.id)); for (const job of current) { if (!retainedIds.has(job.id)) { scheduledWorkRef.current.delete(job.id); } } return [ ...retained, createQueuedJob( id, operation, definition.totalSteps, createdAt, definition.retryCount ), ]; }); scheduledPlanCountRef.current += 1; setScheduledPlanCount(scheduledPlanCountRef.current); const start = async (): Promise => { definition.started = true; activeScheduledJobIdRef.current = id; if (definition.cancelled) { activeScheduledJobIdRef.current = undefined; throw new EngineError('cancelled', 'The queued job was cancelled.'); } setScheduledJobs((current) => replaceJob(current, id, (job) => ({ ...job, status: 'preparing', startedAt: timestamp(), updatedAt: timestamp(), })) ); let stepIndex = 0; const executeTrackedStep: ExecutePlan = async ( plan, inputs, stepOperation ) => { const currentStepIndex = stepIndex; stepIndex += 1; activeScheduledStepIndexRef.current = currentStepIndex; setScheduledJobs((current) => replaceJob(current, id, (job) => { const steps = [...job.steps]; const existing = steps[currentStepIndex]; const nextStep: MediaJobStep = { id: existing?.id ?? `${id}:step-${currentStepIndex + 1}`, name: stepOperation, weight: existing?.weight ?? 1 / definition.totalSteps, status: 'running', progress: 0, planId: plan.id, }; steps[currentStepIndex] = nextStep; return { ...job, status: 'running', steps, updatedAt: timestamp(), }; }) ); try { const result = await executePlan( plan, inputs, stepOperation, () => { setScheduledJobs((current) => replaceJob(current, id, (job) => ({ ...job, status: 'reading-output', updatedAt: timestamp(), })) ); } ); setScheduledJobs((current) => replaceJob(current, id, (job) => { const steps = job.steps.map((step, index) => index === currentStepIndex ? { ...step, status: 'completed' as const, progress: 1 } : step ); return { ...job, steps, progress: Math.min( 0.99, steps.reduce( (sum, step) => sum + step.weight * (step.status === 'completed' ? 1 : step.progress), 0 ) ), updatedAt: timestamp(), }; }) ); return result; } catch (error) { const cancelled = definition.cancelled || (error instanceof EngineError && error.code === 'cancelled'); setScheduledJobs((current) => replaceJob(current, id, (job) => ({ ...job, steps: job.steps.map((step, index) => index === currentStepIndex ? { ...step, status: cancelled ? ('cancelled' as const) : ('failed' as const), progress: Math.min(step.progress, 0.99), } : step ), updatedAt: timestamp(), })) ); throw error; } }; try { const result = await work(executeTrackedStep); setScheduledJobs((current) => replaceJob(current, id, (job) => ({ ...job, status: 'completed', progress: 1, completedAt: timestamp(), updatedAt: timestamp(), })) ); return result; } catch (error) { const cancelled = definition.cancelled || (error instanceof EngineError && error.code === 'cancelled'); setScheduledJobs((current) => replaceJob(current, id, (job) => ({ ...job, status: cancelled ? 'cancelled' : 'failed', completedAt: timestamp(), updatedAt: timestamp(), error: cancelled ? undefined : { code: 'execution-failed', message: error instanceof Error ? error.message : 'The local job failed.', recoverable: true, }, steps: job.steps.map((step) => step.status === 'queued' ? { ...step, status: cancelled ? ('cancelled' as const) : ('failed' as const), } : step ), })) ); throw error; } finally { if (activeScheduledJobIdRef.current === id) { activeScheduledJobIdRef.current = undefined; activeScheduledStepIndexRef.current = undefined; } } }; const execution = planExecutionTail.current.then(start, start); planExecutionTail.current = execution.then( () => undefined, () => undefined ); void execution .finally(() => { if (definition.counted) { definition.counted = false; scheduledPlanCountRef.current = Math.max( 0, scheduledPlanCountRef.current - 1 ); setScheduledPlanCount(scheduledPlanCountRef.current); } }) .catch(() => undefined); return execution; }, [executePlan] ); const runPlan = useCallback( ( plan: FFmpegCommandPlan, inputs: readonly File[], operation: string ): Promise => enqueueWork(operation, 1, (executeStep) => executeStep(plan, inputs, operation) ), [enqueueWork] ); const runWorkflow = useCallback( ( operation: string, totalSteps: number, work: (executeStep: ExecutePlan) => Promise ): Promise => enqueueWork(operation, totalSteps, work), [enqueueWork] ); const cancelScheduledJob = useCallback(async (id: string) => { const definition = scheduledWorkRef.current.get(id); if (!definition) return; definition.cancelled = true; setScheduledJobs((current) => replaceJob(current, id, (job) => ({ ...job, status: job.status === 'queued' ? 'cancelled' : 'cancelling', completedAt: job.status === 'queued' ? timestamp() : job.completedAt, updatedAt: timestamp(), steps: job.steps.map((step) => step.status === 'queued' ? { ...step, status: 'cancelled' as const } : step ), })) ); if (!definition.started && definition.counted) { definition.counted = false; scheduledPlanCountRef.current = Math.max( 0, scheduledPlanCountRef.current - 1 ); setScheduledPlanCount(scheduledPlanCountRef.current); return; } if (activeScheduledJobIdRef.current === id) { await engineManager.cancelActive(); } }, []); const retryScheduledJob = useCallback( (id: string) => { const definition = scheduledWorkRef.current.get(id); if (!definition || definition.projectId !== projectRef.current.id) { setOperationError( 'This job cannot be retried after its project has been replaced.' ); return; } void enqueueWork( definition.operation, definition.totalSteps, definition.work, definition.retryCount + 1 ).catch(() => undefined); }, [enqueueWork] ); const clearScheduledJobHistory = useCallback(() => { setScheduledJobs((current) => { const retained = current.filter( (job) => job.status !== 'completed' && job.status !== 'failed' && job.status !== 'cancelled' ); const retainedIds = new Set(retained.map((job) => job.id)); for (const id of scheduledWorkRef.current.keys()) { if (!retainedIds.has(id)) scheduledWorkRef.current.delete(id); } return retained; }); }, []); const exportQuick = useCallback( async (configuration: QuickExportConfiguration) => { if (!selectedAsset?.probe) return; const jobId = crypto.randomUUID(); try { let plan: FFmpegCommandPlan; if (configuration.operation === 'remux') { const container = configuration.remuxContainer ?? 'matroska'; plan = buildRemuxPlan({ jobId, source: { id: selectedAsset.id, sourceIndex: 0, fileName: selectedAsset.file.name, }, targetContainer: container, fileExtension: container === 'matroska' ? 'mkv' : container === 'ogg' ? 'ogg' : container, description: { streams: selectedAsset.probe.streams.flatMap((stream) => stream.type === 'video' || stream.type === 'audio' || stream.type === 'subtitle' || stream.type === 'attachment' || stream.type === 'data' ? [ { index: stream.index, kind: stream.type, codec: stream.codecName ?? 'unknown', }, ] : [] ), hasChapters: selectedAsset.probe.chapters.length > 0, hasMetadata: Object.keys(selectedAsset.probe.tags).length > 0, }, streamSelection: configuration.streamSelection, metadataPolicy: configuration.removeMetadata ? 'remove' : 'copy', chapterPolicy: configuration.removeChapters ? 'remove' : 'keep', expectedDurationSeconds: selectedAsset.probe.durationSeconds, }); } else { if (!configuration.preset) return; plan = buildConvertPlan({ jobId, source: { id: selectedAsset.id, sourceIndex: 0, fileName: selectedAsset.file.name, }, preset: { ...configuration.preset, metadataPolicy: configuration.removeMetadata ? 'remove' : 'copy', chapterPolicy: configuration.removeChapters ? 'remove' : 'keep', }, streamSelection: configuration.streamSelection, expectedDurationSeconds: selectedAsset.probe.durationSeconds, }); } await runPlan(plan, [selectedAsset.file], plan.operation); } catch (error) { setOperationError( error instanceof EngineError && error.code === 'cancelled' ? 'The operation was cancelled and the engine was recreated.' : error instanceof Error ? error.message : 'The output could not be generated.' ); } }, [runPlan, selectedAsset] ); const extractThumbnail = useCallback( async (request: ThumbnailRequest) => { const asset = inspectedClipAsset; if (!asset) return; try { const plan = buildSingleThumbnailPlan({ jobId: crypto.randomUUID(), source: { id: asset.id, sourceIndex: 0, fileName: asset.file.name, }, timeSeconds: request.timeSeconds, size: { ...(request.width === undefined ? {} : { width: request.width }), ...(request.height === undefined ? {} : { height: request.height }), maintainAspect: request.maintainAspect, }, format: request.format === 'jpg' ? 'jpeg' : request.format, quality: request.quality, }); const execution = await runPlan( plan, [asset.file], 'Extract thumbnail' ); const derivative = execution.results[0]; if (!derivative) { throw new Error('The thumbnail output was not returned.'); } try { await cacheDerivative( asset.file, 'thumbnail', 'thumbnail', project.id, derivative.blob, { timeSeconds: request.timeSeconds, width: request.width, height: request.height, maintainAspect: request.maintainAspect, format: request.format, quality: request.quality, } ); } catch { setProjectNotice( 'Thumbnail created, but this browser could not cache the derivative.' ); } } catch (error) { setOperationError( error instanceof Error ? error.message : 'Frame extraction failed.' ); } }, [inspectedClipAsset, project.id, runPlan] ); const createPreviewProxy = useCallback( async (asset: ImportedMediaAsset | undefined) => { const duration = asset?.probe?.durationSeconds; if (!asset || duration === undefined || duration <= 0) return; try { const includeAudio = asset.probe?.streams.some((stream) => stream.type === 'audio') === true; const hasVideo = asset.probe?.streams.some( (stream) => stream.type === 'video' && stream.disposition.attached_pic !== true ) === true; const cacheSettings = { ...previewProxySettings, includeAudio, hasVideo, }; const plan = buildPreviewProxyPlan({ jobId: crypto.randomUUID(), source: { id: asset.id, sourceIndex: 0, fileName: asset.file.name, }, sourceDurationSeconds: duration, ...previewProxySettings, includeAudio, hasVideo, }); let cached: Blob | undefined; try { cached = await readCachedDerivative( asset.file, 'preview-proxy', cacheSettings ); } catch { // Cache availability is optional; generation remains the fallback. } const plannedOutput = plan.outputs[0]; if (cached && plannedOutput) { const cachedResult = resultCollection.add({ id: `${plan.id}:${plannedOutput.id}:cached`, planId: plan.id, outputId: plannedOutput.id, fileName: plannedOutput.fileName, blob: cached, mimeType: plannedOutput.mediaType, role: plannedOutput.role, }); refreshResults(); setPreviewResultId( canPreviewGeneratedResult(cachedResult) ? cachedResult.id : undefined ); setOutputVerifications((current) => ({ ...current, [cachedResult.id]: { warning: 'Reused the matching local preview derivative; it was not re-probed in this run.', }, })); setProjectNotice('Reused the matching local preview proxy.'); return; } const execution = await runPlan( plan, [asset.file], 'Create preview proxy' ); const derivative = execution.results[0]; if (!derivative) { throw new Error('The preview proxy output was not returned.'); } try { await cacheDerivative( asset.file, 'preview-proxy', 'preview-proxy', project.id, derivative.blob, cacheSettings ); } catch { setProjectNotice( 'Preview proxy created, but this browser could not cache the derivative.' ); } } catch (error) { setOperationError( error instanceof Error ? error.message : 'The preview proxy could not be generated.' ); } }, [ previewProxySettings, project.id, refreshResults, resultCollection, runPlan, ] ); const exportTimeline = useCallback(async () => { const preset = findBuiltInPreset(project.output.presetId) ?? userPresets.find((entry) => entry.id === project.output.presetId); if (!preset?.video) { setOperationError( 'Choose an available video preset in the timeline output settings.' ); return; } const timelineVideoPreset = preset.video; if (!['mp4', 'webm', 'matroska'].includes(preset.container)) { setOperationError( 'Timeline export currently supports MP4, WebM, or Matroska video presets.' ); return; } const timelineContainer = preset.container as 'mp4' | 'webm' | 'matroska'; try { const enabledSubtitleTracks = project.subtitles.filter( (entry) => entry.mode !== 'exclude' ); const timelineStepCount = 2 + enabledSubtitleTracks.length + enabledSubtitleTracks.filter( (entry) => (entry.offsetSeconds ?? 0) !== 0 ).length + (project.chapters.length > 0 ? 1 : 0); await runWorkflow( 'Render timeline project', timelineStepCount, async (executeStep) => { const resolved = project.timeline.map((clip, index) => { const asset = assets.find((entry) => entry.id === clip.assetId); const video = asset?.probe?.streams.find( (stream) => stream.type === 'video' && stream.disposition.attached_pic !== true ); if (!asset || !video || !video.width || !video.height) { throw new Error( `Clip ${index + 1} has no attached, inspected video source.` ); } if (clip.video?.enabled === false) { throw new Error( `Clip ${index + 1} has video disabled. The sequential video timeline requires video in every clip; use Quick Convert for an audio-only export.` ); } const crop = clip.crop ? validateCrop({ ...clip.crop, sourceWidth: video.width, sourceHeight: video.height, requireEvenDimensions: true, }) : undefined; const croppedWidth = crop?.width ?? video.width; const croppedHeight = crop?.height ?? video.height; const rotationSwapsDimensions = clip.rotation === 90 || clip.rotation === 270; const resizeSourceWidth = rotationSwapsDimensions ? croppedHeight : croppedWidth; const resizeSourceHeight = rotationSwapsDimensions ? croppedWidth : croppedHeight; const resize = clip.resize ? { sourceWidth: resizeSourceWidth, sourceHeight: resizeSourceHeight, width: clip.resize.width, height: clip.resize.height, mode: clip.resize.mode === 'fit' ? ('contain' as const) : clip.resize.mode === 'fill' ? ('cover' as const) : ('stretch' as const), noUpscaling: clip.resize.allowUpscale === false, algorithm: clip.resize.algorithm, paddingColor: clip.resize.paddingColor, } : undefined; const audioNormalization = clip.audio?.normalization ? (() => { const measurement = loudnessByAsset[asset.id]; if (!measurement) { throw new Error( `Clip ${index + 1} requests two-pass loudness normalization. Measure its source in Advanced operations first.` ); } return { mode: 'loudness' as const, targets: { integratedLufs: clip.audio.normalization.targetLufs, truePeakDbtp: clip.audio.normalization.truePeakDb, loudnessRangeLu: clip.audio.normalization.loudnessRangeLufs ?? 11, }, measurement, }; })() : undefined; const calculatedResize = resize ? calculateResize(resize) : undefined; const targetBoxIsFinal = resize !== undefined && resize.width !== undefined && resize.height !== undefined && (resize.mode === 'contain' || resize.mode === 'cover'); const transformedWidth = targetBoxIsFinal ? (resize.width as number) : (calculatedResize?.width ?? resizeSourceWidth); const transformedHeight = targetBoxIsFinal ? (resize.height as number) : (calculatedResize?.height ?? resizeSourceHeight); return { clip: { id: clip.id, source: { id: asset.id, sourceIndex: index, fileName: asset.file.name, }, sourceInSeconds: clip.sourceInSeconds, sourceOutSeconds: clip.sourceOutSeconds, sourceDurationSeconds: asset.probe?.durationSeconds, hasVideo: true, hasAudio: project.output.audioEnabled && clip.audio?.enabled !== false && asset.probe?.streams.some( (stream) => stream.type === 'audio' ) === true, transform: crop || resize || clip.rotation ? { crop, resize, rotation: clip.rotation, } : undefined, audioGainDb: clip.audio?.gainDb, audioNormalization, audioFadeInSeconds: clip.audio?.fadeInSeconds, audioFadeOutSeconds: clip.audio?.fadeOutSeconds, audioFadeCurve: clip.audio?.fadeCurve, videoFadeInSeconds: clip.video?.fadeInSeconds, videoFadeOutSeconds: clip.video?.fadeOutSeconds, videoFadeCurve: clip.video?.fadeCurve, }, file: asset.file, width: transformedWidth, height: transformedHeight, requestedFrameRate: clip.frameRate, sourceFrameRate: video.frameRate, }; }); const first = resolved[0]; if (!first) throw new Error('Add at least one timeline clip.'); const even = (value: number) => Math.max(2, Math.round(value / 2) * 2); const requestedWidth = project.output.width ?? timelineVideoPreset.width ?? undefined; const requestedHeight = project.output.height ?? timelineVideoPreset.height ?? undefined; let outputWidth: number; let outputHeight: number; if (requestedWidth && requestedHeight) { outputWidth = even(requestedWidth); outputHeight = even(requestedHeight); } else if (requestedWidth) { outputWidth = even(requestedWidth); outputHeight = even((requestedWidth * first.height) / first.width); } else if (requestedHeight) { outputHeight = even(requestedHeight); outputWidth = even((requestedHeight * first.width) / first.height); } else { const scale = Math.min(1, 1920 / first.width, 1080 / first.height); outputWidth = even(first.width * scale); outputHeight = even(first.height * scale); } const requestedClipFrameRates = [ ...new Set( resolved.flatMap((entry) => entry.requestedFrameRate === undefined ? [] : [entry.requestedFrameRate] ) ), ]; if ( project.output.frameRate === undefined && timelineVideoPreset.frameRate === undefined && requestedClipFrameRates.length > 1 ) { throw new Error( 'Timeline clips contain conflicting legacy frame-rate settings. Choose one frame rate in Timeline output.' ); } const frameRate = project.output.frameRate ?? timelineVideoPreset.frameRate ?? requestedClipFrameRates[0] ?? first.sourceFrameRate ?? 30; const sampleRate = project.output.sampleRate ?? preset.audio?.sampleRate ?? 48_000; const channelLayout = project.output.channelLayout ?? (preset.audio?.channels === 1 ? 'mono' : preset.audio?.channels === 6 ? '5.1' : 'stereo'); const missingAudioPolicy = !project.output.audioEnabled || !preset.audio ? ('drop-all' as const) : (project.output.missingAudioPolicy ?? 'insert-silence'); const outputFileName = safeExportFileName(project.output.fileName, { extension: preset.fileExtension, fallback: 'timeline', }); const plan = buildTimelineExportPlan({ jobId: crypto.randomUUID(), clips: resolved.map(({ clip }) => clip), preset, outputWidth, outputHeight, frameRate, sampleRate, channelLayout, missingAudioPolicy, outputFileName, }); const rendered = await executeStep( plan, resolved.map(({ file }) => file), 'Render timeline' ); const renderedResult = rendered.results[0]; if (!renderedResult) { throw new Error('The rendered timeline output was not returned.'); } let currentResult = renderedResult; let currentFile = resultAsFile(currentResult); const totalDuration = resolved.reduce( (sum, entry) => sum + entry.clip.sourceOutSeconds - entry.clip.sourceInSeconds, 0 ); const activeSubtitleTracks = project.subtitles .filter((entry) => entry.mode !== 'exclude') .sort((left, right) => { if (left.mode === right.mode) return 0; return left.mode === 'burn-in' ? -1 : 1; }); for (const track of activeSubtitleTracks) { const originalSubtitle = subtitleFiles.get(track.id); if (!originalSubtitle || !track.sourceFormat) { throw new Error( `Reattach subtitle ${track.sourceName ?? track.id} before rendering the project.` ); } let subtitleFile = originalSubtitle; let shiftedResultForCleanup: ExportResult | undefined; if ((track.offsetSeconds ?? 0) !== 0) { const shifted = await executeStep( buildSubtitleConversionPlan({ jobId: crypto.randomUUID(), subtitle: { id: track.id, sourceIndex: 0, fileName: originalSubtitle.name, }, outputFormat: track.sourceFormat, offsetSeconds: track.offsetSeconds ?? 0, }), [originalSubtitle], `Shift subtitle ${track.sourceName ?? track.id}` ); const shiftedResult = shifted.results[0]; if (!shiftedResult) { throw new Error( 'The shifted subtitle output was not returned.' ); } shiftedResultForCleanup = shiftedResult; subtitleFile = resultAsFile(shiftedResult); } const mediaSource = { id: `timeline-${project.id}`, sourceIndex: 0, fileName: currentFile.name, }; const subtitleSource = { id: track.id, sourceIndex: 1, fileName: subtitleFile.name, }; const sourceStreams = track.mode === 'burn-in' ? [] : remuxStreamsFromProbe( normalizeFfprobeReport( (await engineManager.probe(currentFile)).json ) ); const subtitlePlan = track.mode === 'burn-in' ? buildSubtitleBurnPlan({ jobId: crypto.randomUUID(), source: mediaSource, subtitle: subtitleSource, preset, expectedDurationSeconds: totalDuration, }) : buildSubtitleMuxPlan({ jobId: crypto.randomUUID(), source: mediaSource, subtitle: subtitleSource, subtitleCodec: subtitleCodecForFormat(track.sourceFormat), sourceStreams, targetContainer: timelineContainer, targetExtension: preset.fileExtension, ...(track.language ? { language: track.language } : {}), ...(track.title ? { title: track.title } : {}), default: track.default ?? false, forced: track.forced ?? false, expectedDurationSeconds: totalDuration, }); const subtitled = await executeStep( subtitlePlan, [currentFile, subtitleFile], track.mode === 'burn-in' ? 'Burn project subtitle' : 'Mux project subtitle' ); const subtitledResult = subtitled.results[0]; if (!subtitledResult) { throw new Error( 'The subtitled timeline output was not returned.' ); } resultCollection.remove(currentResult.id); if (shiftedResultForCleanup) resultCollection.remove(shiftedResultForCleanup.id); currentResult = subtitledResult; currentFile = resultAsFile(subtitledResult); } if (project.chapters.length > 0) { const chaptered = await executeStep( buildChapterMuxPlan({ jobId: crypto.randomUUID(), source: { id: `timeline-${project.id}`, sourceIndex: 0, fileName: currentFile.name, }, chapters: project.chapters.map((chapter) => ({ id: chapter.id, title: chapter.title, startSeconds: chapter.startSeconds, endSeconds: chapter.endSeconds, timeBase: chapter.timeBase ?? '1/1000', })), targetExtension: preset.fileExtension, targetMuxer: preset.container, durationSeconds: totalDuration, }), [currentFile], 'Mux project chapters' ); const chapteredResult = chaptered.results[0]; if (!chapteredResult) { throw new Error( 'The chaptered timeline output was not returned.' ); } resultCollection.remove(currentResult.id); currentResult = chapteredResult; currentFile = resultAsFile(chapteredResult); } const metadataEdits = metadataEditsFromProject(project.metadata); const finalized = await executeStep( buildMetadataEditPlan({ jobId: crypto.randomUUID(), source: { id: `timeline-${project.id}`, sourceIndex: 0, fileName: currentFile.name, }, edits: metadataEdits, targetExtension: preset.fileExtension, targetMuxer: preset.container, sourceMetadataPolicy: preset.metadataPolicy === 'remove' ? 'remove' : 'copy', chapterPolicy: 'keep', expectedDurationSeconds: totalDuration, outputFileName, }), [currentFile], 'Finalize timeline output' ); if (!finalized.results[0]) { throw new Error('The finalized timeline output was not returned.'); } resultCollection.remove(currentResult.id); refreshResults(); } ); } catch (error) { setOperationError( error instanceof Error ? error.message : 'The timeline could not be rendered.' ); } }, [ assets, loudnessByAsset, project, refreshResults, resultCollection, runWorkflow, subtitleFiles, userPresets, ]); const resetProject = useCallback(() => { for (const asset of assets) URL.revokeObjectURL(asset.objectUrl); const cancelActiveInspection = sourceInspectionRegistry.current.clear(); if (cancelActiveInspection) void engineManager.cancelActive(); assetsRef.current = []; clearResult(); setAssets([]); setSelectedAssetId(undefined); setSelectedClipId(undefined); setSubtitleFiles(new Map()); setWaveformByAsset({}); setSplitMarkersByAsset({}); setSplitBatch(undefined); setLoudnessByAsset({}); setPlayheadByAsset({}); setScheduledJobs([]); scheduledWorkRef.current.clear(); setProject(createEmptyProject()); setProjectNotice('Started a new empty project.'); }, [assets, clearResult]); const exportProject = useCallback(() => { const json = serializeProject(project, { pretty: true, includeUiState: true, }); downloadBlob( new Blob([json], { type: 'application/json' }), projectDocumentFileName(project) ); setProjectNotice( 'Project document downloaded. Source files are not embedded.' ); }, [project]); const importProject = useCallback( async (file: File) => { try { const imported = importProjectDocument(await file.text()); for (const asset of assets) URL.revokeObjectURL(asset.objectUrl); const cancelActiveInspection = sourceInspectionRegistry.current.clear(); if (cancelActiveInspection) void engineManager.cancelActive(); assetsRef.current = []; clearResult(); setAssets([]); setSelectedAssetId(undefined); setSelectedClipId(imported.project.timeline[0]?.id); setSubtitleFiles(new Map()); setWaveformByAsset({}); setSplitMarkersByAsset({}); setSplitBatch(undefined); setLoudnessByAsset({}); setPlayheadByAsset({}); setScheduledJobs([]); scheduledWorkRef.current.clear(); setProject(imported.project); selectWorkspaceMode('edit'); setProjectNotice( imported.warnings.length > 0 ? imported.warnings.map((warning) => warning.message).join(' ') : 'Project loaded.' ); } catch (error) { setProjectNotice( error instanceof Error ? error.message : 'Project import failed.' ); } }, [assets, clearResult, selectWorkspaceMode] ); const moveClip = useCallback( (clipId: string, toIndex: number) => { if (toIndex < 0 || toIndex >= project.timeline.length) return; applyProjectAction({ type: 'clip/move', clipId, toIndex, updatedAt: timestamp(), }); }, [applyProjectAction, project.timeline.length] ); const updateClip = useCallback( (clipId: string, changes: Partial) => { const clip = project.timeline.find((entry) => entry.id === clipId); if (!clip) return; const sourceIn = changes.sourceInSeconds ?? clip.sourceInSeconds; const sourceOut = changes.sourceOutSeconds ?? clip.sourceOutSeconds; if ( !Number.isFinite(sourceIn) || !Number.isFinite(sourceOut) || sourceIn < 0 || sourceOut - sourceIn < 0.001 ) { setProjectNotice('Clip out-point must remain after its in-point.'); return; } applyProjectAction({ type: 'clip/update', clipId, changes, updatedAt: timestamp(), }); }, [applyProjectAction, project.timeline] ); const changeSubtitles = useCallback( (subtitles: AvProjectV1['subtitles']) => { const retainedIds = new Set(subtitles.map((track) => track.id)); setSubtitleFiles( (current) => new Map([...current].filter(([trackId]) => retainedIds.has(trackId))) ); applyProjectAction({ type: 'subtitles/set', subtitles, updatedAt: timestamp(), }); }, [applyProjectAction] ); const importSubtitleFile = useCallback( (file: File) => { const extension = file.name.split('.').at(-1)?.toLowerCase(); if (extension !== 'srt' && extension !== 'vtt' && extension !== 'ass') { setProjectNotice('Choose an SRT, WebVTT, or ASS subtitle file.'); return; } if (file.size > 20 * 1024 * 1024) { setProjectNotice('Subtitle files are limited to 20 MiB.'); return; } const missing = project.subtitles.find( (track) => track.sourceStatus === 'missing' && track.sourceName?.toLocaleLowerCase() === file.name.toLocaleLowerCase() ); const trackId = missing?.id ?? crypto.randomUUID(); const tracks: AvProjectV1['subtitles'] = missing ? project.subtitles.map((track) => track.id === missing.id ? { ...track, sourceName: file.name, sourceFormat: extension, sourceStatus: 'attached' as const, } : track ) : [ ...project.subtitles, { id: trackId, sourceName: file.name, sourceFormat: extension, mode: 'mux' as const, sourceStatus: 'attached' as const, }, ]; setSubtitleFiles((current) => new Map(current).set(trackId, file)); applyProjectAction({ type: 'subtitles/set', subtitles: tracks, updatedAt: timestamp(), }); setProjectNotice( missing ? `Reattached subtitle ${file.name}.` : `Imported subtitle ${file.name}.` ); }, [applyProjectAction, project.subtitles] ); const resolvePreset = useCallback( (presetId: string) => findBuiltInPreset(presetId) ?? userPresets.find((preset) => preset.id === presetId), [userPresets] ); const handleStructuralTrim = useCallback( async (request: StructuralTrimRequest) => { const clip = project.timeline.find( (entry) => entry.id === request.clipId ); const asset = clip ? assets.find((entry) => entry.id === clip.assetId) : undefined; if (!clip || !asset?.probe) { throw new Error('The selected clip source is not attached and probed.'); } const preset = request.mode === 'accurate' ? resolvePreset(request.presetId) : undefined; if (request.mode === 'accurate' && !preset) { throw new Error('The accurate trim preset is unavailable.'); } const selection = request.mode === 'accurate' && preset ? { video: preset.video ? asset.probe.streams .filter( (stream) => stream.type === 'video' && stream.disposition.attached_pic !== true ) .slice(0, 1) .map((stream) => stream.index) : [], audio: preset.audio ? asset.probe.streams .filter((stream) => stream.type === 'audio') .slice(0, 1) .map((stream) => stream.index) : [], subtitles: [], attachments: [], } : undefined; const plan = buildTrimPlan({ jobId: crypto.randomUUID(), source: { id: asset.id, sourceIndex: 0, fileName: asset.file.name, }, mode: request.mode, startSeconds: request.startSeconds, endSeconds: request.endSeconds, sourceDurationSeconds: asset.probe.durationSeconds, ...(selection ? { streamSelection: selection } : {}), ...(preset ? { preset } : {}), ...(request.mode === 'fast' ? { targetExtension: request.targetExtension } : {}), }); await runPlan( plan, [asset.file], request.mode === 'fast' ? 'Fast keyframe trim' : 'Accurate trim' ); }, [assets, project.timeline, resolvePreset, runPlan] ); const handleStructuralConcat = useCallback( async (request: StructuralConcatRequest) => { const resolved = request.clipIds.map((clipId, index) => { const clip = project.timeline.find((entry) => entry.id === clipId); const asset = clip ? assets.find((entry) => entry.id === clip.assetId) : undefined; if (!clip || !asset?.probe) { throw new Error( `Timeline clip ${index + 1} has no attached, probed source.` ); } return { clip, asset, source: concatSourceForClip(clip, asset, index), }; }); if (resolved.length < 2) { throw new Error('Concatenation requires at least two timeline clips.'); } const plan = request.mode === 'fast' ? buildFastConcatPlan({ jobId: crypto.randomUUID(), sources: resolved.map((entry) => entry.source), targetExtension: request.targetExtension, targetMuxer: request.targetMuxer, }) : (() => { const preset = resolvePreset(request.presetId); if (!preset?.video) { throw new Error( 'The normalized concat video preset is unavailable.' ); } const firstVideo = resolved[0]?.asset.probe?.streams.find( (stream) => stream.type === 'video' && stream.disposition.attached_pic !== true ); if (!firstVideo?.width || !firstVideo.height) { throw new Error( 'The first concat source has no validated video dimensions.' ); } const even = (value: number) => Math.max(2, Math.round(value / 2) * 2); const scale = Math.min( 1, 1920 / firstVideo.width, 1080 / firstVideo.height ); return buildNormalizedConcatPlan({ jobId: crypto.randomUUID(), sources: resolved.map((entry) => entry.source), targetExtension: preset.fileExtension, targetMuxer: preset.container, preset, width: even(firstVideo.width * scale), height: even(firstVideo.height * scale), frameRate: firstVideo.frameRate ?? 30, sampleRate: 48_000, channelLayout: 'stereo', missingAudioPolicy: request.missingAudioPolicy, subtitlePolicy: request.subtitlePolicy, crossfadeSeconds: request.crossfadeSeconds, }); })(); await runPlan( plan, resolved.map((entry) => entry.asset.file), request.mode === 'fast' ? 'Fast compatible concat' : 'Normalized concat' ); }, [assets, project.timeline, resolvePreset, runPlan] ); const handleAdvancedSplit = useCallback( async (request: AdvancedSplitRequest) => { const asset = inspectedClipAsset; const duration = asset?.probe?.durationSeconds; if (!asset || duration === undefined) { throw new Error( 'Select an attached, inspected source before splitting.' ); } const sourceExtension = extensionFromName(asset.file.name); const preset = request.mode === 'accurate' ? resolvePreset(request.presetId) : undefined; if (request.mode === 'accurate' && !preset) { throw new Error('The accurate split preset is unavailable.'); } if ( request.mode === 'accurate' && preset && request.targetExtension !== preset.fileExtension ) { throw new Error( `Accurate split output must use the preset extension .${preset.fileExtension}.` ); } if ( request.mode === 'fast' && request.targetExtension !== sourceExtension ) { throw new Error( `Fast stream-copy split keeps the source container .${sourceExtension}; choose accurate mode to change format.` ); } const plan = buildSplitPlan({ jobId: crypto.randomUUID(), source: { id: asset.id, sourceIndex: 0, fileName: asset.file.name, }, durationSeconds: duration, definition: request.definition, mode: request.mode, targetExtension: request.targetExtension, ...(preset ? { preset } : {}), }); const pendingBatch: AdvancedSplitBatchStatus = { planId: plan.id, segments: plan.outputs.map((output) => { if (!output.timeRange) { throw new Error( `Split output ${output.id} has no declared segment range.` ); } return { outputId: output.id, fileName: output.fileName, startSeconds: output.timeRange.startSeconds, endSeconds: output.timeRange.endSeconds, state: 'queued', }; }), }; setSplitBatch(pendingBatch); try { const execution = await runWorkflow( 'Split media', 1, async (executeStep) => { setSplitBatch((current) => current?.planId === plan.id ? { ...current, segments: current.segments.map((segment) => ({ ...segment, state: 'running', })), } : current ); return executeStep(plan, [asset.file], 'Split media'); } ); const resultIds = new Set( execution.results.map((result) => result.outputId) ); setSplitBatch((current) => current?.planId === plan.id ? { ...current, segments: current.segments.map((segment) => { const result = execution.results.find( (entry) => entry.outputId === segment.outputId ); if (!result || !resultIds.has(segment.outputId)) { return { ...segment, state: 'failed', message: 'FFmpeg returned successfully, but this expected segment was not retained.', }; } const verification = execution.verifications?.[result.id]; if (verification?.warning) { return { ...segment, state: 'warning', message: verification.warning, }; } return { ...segment, state: verification?.probe ? 'verified' : 'completed', }; }), } : current ); } catch (error) { const cancelled = error instanceof EngineError && error.code === 'cancelled'; const message = error instanceof Error ? error.message : 'The shared FFmpeg split execution failed.'; setSplitBatch((current) => current?.planId === plan.id ? { ...current, segments: current.segments.map((segment) => ({ ...segment, state: cancelled ? 'cancelled' : 'failed', message, })), } : current ); throw error; } }, [inspectedClipAsset, resolvePreset, runWorkflow] ); const handleAdvancedAudio = useCallback( async (request: AdvancedAudioRequest) => { const asset = inspectedClipAsset; const duration = asset?.probe?.durationSeconds; if (!asset || duration === undefined) { throw new Error('Select an attached, inspected source first.'); } const firstAudioIndex = asset.probe?.streams.find( (stream) => stream.type === 'audio' )?.index; const audioStreamIndex = request.audioStreamIndex ?? firstAudioIndex; if (audioStreamIndex === undefined) { throw new Error('The source has no selected audio stream.'); } const source = { id: asset.id, sourceIndex: 0, fileName: asset.file.name, }; if (request.type === 'static-waveform') { const plan = buildStaticWaveformPlan({ jobId: crypto.randomUUID(), source, audioStreamIndex, width: request.width, height: request.height, color: request.foreground, background: request.background, expectedDurationSeconds: duration, }); const execution = await runPlan( plan, [asset.file], 'Create static waveform' ); const output = execution.results[0]; if (!output) throw new Error('The waveform image was not returned.'); try { await cacheDerivative( asset.file, 'waveform-image', 'waveform-image', project.id, output.blob, { audioStreamIndex, width: request.width, height: request.height, foreground: request.foreground, background: request.background, } ); } catch { setProjectNotice( 'Waveform image created, but this browser could not cache the derivative.' ); } return; } if (request.type === 'waveform-peaks') { const cacheSettings = { audioStreamIndex, sampleRate: request.sampleRate, bucketCount: request.bucketCount, }; try { const cached = await readCachedDerivative( asset.file, 'waveform-peaks', cacheSettings ); if (cached) { const peaks = deserializeWaveformPeaks(await cached.arrayBuffer()); setWaveformByAsset((current) => ({ ...current, [asset.id]: peaks, })); setProjectNotice('Reused matching bounded waveform peak data.'); return; } } catch { // A missing or invalid derivative is safely regenerated below. } const plan = buildWaveformAnalysisPlan({ jobId: crypto.randomUUID(), source, audioStreamIndex, sampleRate: request.sampleRate, expectedDurationSeconds: duration, }); const execution = await runPlan( plan, [asset.file], 'Extract reduced waveform audio' ); const output = execution.results[0]; if (!output) { throw new Error('The reduced PCM analysis output was not returned.'); } setWaveformProgress(0); try { const peaks = await waveformRuntime.aggregate( { container: 'wav', data: new Uint8Array(await output.blob.arrayBuffer()), }, { bucketCount: request.bucketCount, onProgress: setWaveformProgress, } ); setWaveformByAsset((current) => ({ ...current, [asset.id]: peaks, })); const serialized = serializeWaveformPeaks(peaks); try { await cacheDerivative( asset.file, 'waveform-peaks', 'waveform-peaks', project.id, new Blob([Uint8Array.from(serialized)], { type: WAVEFORM_PEAKS_MEDIA_TYPE, }), cacheSettings ); } catch { setProjectNotice( 'Waveform peaks created, but this browser could not cache the derivative.' ); } } finally { setWaveformProgress(undefined); resultCollection.remove(output.id); refreshResults(); } return; } if (request.type === 'loudness-measurement') { const plan = buildLoudnessAnalysisPlan({ jobId: crypto.randomUUID(), source, targets: request.targets, audioStreamIndex, expectedDurationSeconds: duration, }); const execution = await runPlan( plan, [asset.file], 'Measure EBU R128 loudness' ); const measurement = parseLoudnessMeasurement(execution.logText); setLoudnessByAsset((current) => ({ ...current, [asset.id]: measurement, })); setProjectNotice( `Measured ${measurement.inputIntegratedLufs.toFixed(1)} LUFS, ${measurement.inputTruePeakDbtp.toFixed(1)} dBTP, LRA ${measurement.inputLoudnessRangeLu.toFixed(1)} LU.` ); return; } const preset = resolvePreset(request.presetId); if (!preset) throw new Error('The selected audio preset is unavailable.'); if (request.type === 'peak-normalization') { const analysis = await runWorkflow( 'Peak-normalize audio', 2, async (executeStep) => { const measured = await executeStep( buildPeakAnalysisPlan({ jobId: crypto.randomUUID(), source, audioStreamIndex, preset, expectedDurationSeconds: duration, }), [asset.file], 'Measure source peak' ); const measurement = parsePeakMeasurement(measured.logText); await executeStep( buildPeakNormalizationPlan({ jobId: crypto.randomUUID(), source, audioStreamIndex, targetPeakDb: request.targetPeakDb, measurement, preset, expectedDurationSeconds: duration, }), [asset.file], 'Apply measured peak normalization' ); return measured; } ); setLogs((current) => [ ...analysis.logText.split('\n').filter(Boolean).slice(-120), ...current, ].slice(-400) ); return; } const plan = buildLoudnessApplyPlan({ jobId: crypto.randomUUID(), source, audioStreamIndex, targets: request.targets, measurement: request.measurement, preset, expectedDurationSeconds: duration, }); await runPlan( plan, [asset.file], 'Apply measured EBU R128 normalization' ); }, [ inspectedClipAsset, project.id, refreshResults, resultCollection, resolvePreset, runPlan, runWorkflow, waveformRuntime, ] ); const handleThumbnailSeries = useCallback( async (request: ThumbnailSeriesRequest) => { const asset = inspectedClipAsset; const duration = asset?.probe?.durationSeconds; if (!asset || duration === undefined) { throw new Error('Select an attached, inspected video source first.'); } const source = { id: asset.id, sourceIndex: 0, fileName: asset.file.name, }; const size = { ...(request.width ? { width: request.width } : {}), ...(request.height ? { height: request.height } : {}), }; const sceneDistribution = request.distribution.type === 'scene-changes' ? request.distribution : undefined; const execution = sceneDistribution ? await runWorkflow( 'Create scene-change thumbnails', 2, async (executeStep) => { const analysis = await executeStep( buildSceneChangeAnalysisPlan({ jobId: crypto.randomUUID(), source, durationSeconds: duration, threshold: sceneDistribution.threshold, maxFrames: sceneDistribution.count, }), [asset.file], 'Analyze scene changes' ); const frames = parseSceneChangeFrames(analysis.logText).slice( 0, sceneDistribution.count ); if (frames.length === 0) { throw new Error( 'No scene changes met this threshold. Lower the threshold or use evenly spaced frames.' ); } return executeStep( buildSceneThumbnailSeriesPlan({ jobId: crypto.randomUUID(), source, durationSeconds: duration, frames, size, format: request.format, quality: request.quality, }), [asset.file], 'Extract analyzed scene frames' ); } ) : await runPlan( buildThumbnailSeriesPlan({ jobId: crypto.randomUUID(), source, durationSeconds: duration, ...(request.distribution.type === 'evenly-spaced' ? { count: request.distribution.count } : request.distribution.type === 'fixed-interval' ? { intervalSeconds: request.distribution.intervalSeconds, } : request.distribution.type === 'scene-changes' ? { count: request.distribution.count } : { timesSeconds: request.distribution.timesSeconds }), size, format: request.format, quality: request.quality, }), [asset.file], 'Create thumbnail series' ); try { await Promise.all( execution.results.map((output, index) => cacheDerivative( asset.file, `thumbnail-series-${index}`, 'thumbnail', project.id, output.blob, { distribution: request.distribution, index, width: request.width ?? null, height: request.height ?? null, format: request.format, } ) ) ); } catch { setProjectNotice( 'Thumbnail series created, but this browser could not cache every derivative.' ); } }, [inspectedClipAsset, project.id, runPlan, runWorkflow] ); const handleContactSheet = useCallback( async (request: ContactSheetRequest) => { const asset = inspectedClipAsset; const duration = asset?.probe?.durationSeconds; if (!asset || duration === undefined) { throw new Error('Select an attached, inspected video source first.'); } const source = { id: asset.id, sourceIndex: 0, fileName: asset.file.name, }; const labelsRequested = request.timestampLabels || request.sourceLabel; const font = labelsRequested ? await loadBundledContactSheetFont() : undefined; const buildSheet = ( sceneFrames?: ReturnType ) => buildContactSheetPlan({ jobId: crypto.randomUUID(), source, durationSeconds: duration, ...request, ...(sceneFrames ? { frameCount: sceneFrames.length, sceneFrames } : {}), drawtextAvailable: Boolean(font), ...(font ? { font: { id: 'bundled-contact-sheet-font', sourceIndex: 1, fileName: font.name, }, } : {}), }); const inputs = font ? [asset.file, font] : [asset.file]; const sceneSelection = request.selection.type === 'scene-changes' ? request.selection : undefined; const execution = sceneSelection ? await runWorkflow( 'Create scene-change contact sheet', 2, async (executeStep) => { const analysis = await executeStep( buildSceneChangeAnalysisPlan({ jobId: crypto.randomUUID(), source, durationSeconds: duration, threshold: sceneSelection.threshold, maxFrames: request.frameCount, }), [asset.file], 'Analyze scene changes' ); const frames = parseSceneChangeFrames(analysis.logText).slice( 0, request.frameCount ); if (frames.length === 0) { throw new Error( 'No scene changes met this threshold. Lower the threshold or use evenly spaced frames.' ); } return executeStep( buildSheet(frames), inputs, 'Render scene-change contact sheet' ); } ) : await runPlan(buildSheet(), inputs, 'Create contact sheet'); const output = execution.results[0]; if (!output) throw new Error('The contact sheet was not returned.'); try { await cacheDerivative( asset.file, 'contact-sheet', 'contact-sheet', project.id, output.blob, request as unknown as Readonly> ); } catch { setProjectNotice( 'Contact sheet created, but this browser could not cache the derivative.' ); } }, [inspectedClipAsset, project.id, runPlan, runWorkflow] ); const handleMetadataAction = useCallback( async (request: MetadataExecutionRequest) => { if (request.type === 'import') { applyProjectAction({ type: 'metadata/set', metadata: projectMetadataFromEdits(request.edits), updatedAt: timestamp(), }); setProjectNotice('Imported validated metadata JSON into the project.'); return; } if (request.type === 'export') { downloadBlob( new Blob([request.json], { type: 'application/json' }), 'av-tools-metadata.json' ); return; } const asset = inspectedClipAsset; if (!asset?.probe) { throw new Error('Select an attached, inspected source first.'); } const extension = extensionFromName(asset.file.name); const selection = request.coverArtPolicy === 'remove' ? { video: asset.probe.streams .filter( (stream) => stream.type === 'video' && stream.disposition.attached_pic !== true ) .map((stream) => stream.index), audio: asset.probe.streams .filter((stream) => stream.type === 'audio') .map((stream) => stream.index), subtitles: asset.probe.streams .filter((stream) => stream.type === 'subtitle') .map((stream) => stream.index), attachments: [], } : undefined; const plan = buildMetadataEditPlan({ jobId: crypto.randomUUID(), source: { id: asset.id, sourceIndex: 0, fileName: asset.file.name, }, edits: request.edits, targetExtension: extension, targetMuxer: muxerForExtension(extension), ...(selection ? { streamSelection: selection } : {}), sourceMetadataPolicy: request.sourceMetadataPolicy, chapterPolicy: request.chapterPolicy, expectedDurationSeconds: asset.probe.durationSeconds, }); await runPlan(plan, [asset.file], 'Write metadata'); }, [applyProjectAction, inspectedClipAsset, runPlan] ); const handleChapterAction = useCallback( async (request: ChapterExecutionRequest) => { if (request.type === 'import') { applyProjectAction({ type: 'chapters/set', chapters: request.chapters.map((chapter) => ({ id: chapter.id, startSeconds: chapter.startSeconds, endSeconds: chapter.endSeconds, title: chapter.title, timeBase: chapter.timeBase ?? '1/1000', })), updatedAt: timestamp(), }); setProjectNotice('Imported validated chapter JSON into the project.'); return; } if (request.type === 'export') { downloadBlob( new Blob([request.json], { type: 'application/json' }), 'av-tools-chapters.json' ); return; } const asset = inspectedClipAsset; if (!asset?.probe) { throw new Error('Select an attached, inspected source first.'); } const extension = extensionFromName(asset.file.name); const source = { id: asset.id, sourceIndex: 0, fileName: asset.file.name, }; const plan = request.type === 'remove' ? buildMetadataEditPlan({ jobId: crypto.randomUUID(), source, edits: {}, targetExtension: extension, targetMuxer: muxerForExtension(extension), sourceMetadataPolicy: 'copy', chapterPolicy: 'remove', expectedDurationSeconds: asset.probe.durationSeconds, }) : buildChapterMuxPlan({ jobId: crypto.randomUUID(), source, chapters: request.chapters, targetExtension: extension, targetMuxer: muxerForExtension(extension), durationSeconds: asset.probe.durationSeconds, }); await runPlan( plan, [asset.file], request.type === 'remove' ? 'Remove chapters' : 'Replace chapters' ); }, [applyProjectAction, inspectedClipAsset, runPlan] ); const handleSubtitleAction = useCallback( async (request: SubtitleExecutionRequest) => { const asset = inspectedClipAsset; if (request.type === 'extract') { if (!asset) throw new Error('Select an inspected media source first.'); const plan = buildSubtitleExtractionPlan({ jobId: crypto.randomUUID(), source: { id: asset.id, sourceIndex: 0, fileName: asset.file.name, }, stream: request.stream, outputFormat: request.outputFormat, }); await runPlan(plan, [asset.file], 'Extract text subtitle'); return; } const attachedFile = [...subtitleFiles.values()].find( (file) => file.name === request.file.name ) ?? request.file; if (request.type === 'convert') { const plan = buildSubtitleConversionPlan({ jobId: crypto.randomUUID(), subtitle: { id: crypto.randomUUID(), sourceIndex: 0, fileName: attachedFile.name, }, outputFormat: request.outputFormat, offsetSeconds: request.offsetSeconds, }); await runPlan(plan, [attachedFile], 'Convert text subtitle'); return; } if (!asset?.probe) { throw new Error('Select an attached, inspected video source first.'); } const mediaSource = { id: asset.id, sourceIndex: 0, fileName: asset.file.name, }; const subtitleSource = { id: crypto.randomUUID(), sourceIndex: 1, fileName: attachedFile.name, }; if (request.type === 'soft-mux') { const targetExtension = request.targetContainer === 'matroska' ? 'mkv' : request.targetContainer; const plan = buildSubtitleMuxPlan({ jobId: crypto.randomUUID(), source: mediaSource, subtitle: subtitleSource, subtitleCodec: request.subtitleCodec || subtitleCodecForFormat(request.inputFormat), sourceStreams: remuxStreamsFromProbe(asset.probe), targetContainer: request.targetContainer, targetExtension, ...(request.language ? { language: request.language } : {}), ...(request.title ? { title: request.title } : {}), default: request.default, forced: request.forced, expectedDurationSeconds: asset.probe.durationSeconds, }); await runPlan(plan, [asset.file, attachedFile], 'Mux soft subtitle'); return; } const preset = resolvePreset(request.presetId); if (!preset) throw new Error('The subtitle burn-in preset is unavailable.'); const plan = buildSubtitleBurnPlan({ jobId: crypto.randomUUID(), source: mediaSource, subtitle: subtitleSource, preset, expectedDurationSeconds: asset.probe.durationSeconds, }); await runPlan( plan, [asset.file, attachedFile], 'Burn subtitle into video' ); }, [inspectedClipAsset, resolvePreset, runPlan, subtitleFiles] ); const handleUserPresetAction = useCallback( async (action: UserPresetAction) => { const registry = presetSetup.registry; if (action.type === 'create' || action.type === 'update') { await registry.save(action.preset); } else if (action.type === 'clone') { await registry.clone(action.presetId, action.newId, action.newName); } else if (action.type === 'rename') { await registry.rename(action.presetId, action.name); } else if (action.type === 'delete') { await registry.delete(action.presetId); } else if (action.type === 'reset-all') { if ( !globalThis.confirm( 'Delete every user-created export preset from this browser?' ) ) { return; } await registry.reset(); } else if (action.type === 'import') { await registry.save(action.preset); } else { downloadBlob( new Blob([action.json], { type: 'application/json' }), `${action.preset.id}.json` ); return; } setUserPresets(registry.list()); if (!presetSetup.persistent) { setProjectNotice( 'IndexedDB is unavailable; user presets will last only for this page session.' ); } }, [presetSetup] ); const saveManagedResult = useCallback( async (output: ExportResult) => { try { await saveCollectionResult(resultCollection, output.id); refreshResults(); } catch (error) { setOperationError( error instanceof Error ? error.message : 'The result could not be saved.' ); } }, [refreshResults, resultCollection] ); const saveManagedZip = useCallback( async (selected: readonly ExportResult[]) => { if (selected.length === 0) return; try { const saved = await saveResultsZip(selected, { fileName: `${project.name || 'av-tools'}-results.zip`, ...(latestReport ? { report: latestReport } : {}), // Compression finishes asynchronously, after transient picker // activation has expired. A normal download is the honest fallback. saver: async (blob, fileName) => { downloadBlob(blob, fileName); return 'downloaded'; }, }); if (saved.outcome !== 'cancelled') { for (const result of selected) resultCollection.remove(result.id); } refreshResults(); } catch (error) { setOperationError( error instanceof Error ? error.message : 'The ZIP could not be created.' ); } }, [latestReport, project.name, refreshResults, resultCollection] ); const saveManagedReport = useCallback(async () => { if (!latestReport) return; try { await saveExportReport(latestReport); } catch (error) { setOperationError( error instanceof Error ? error.message : 'The validation report could not be saved.' ); } }, [latestReport]); const activeCapabilities = engineState.status === 'ready' ? engineState.capabilities : undefined; const capabilityStatus = ( requirements: { readonly muxers?: readonly string[]; readonly encoders?: readonly string[]; readonly filters?: readonly string[]; }, unavailableReason?: string ): AdvancedCapabilityStatus => { if (!activeCapabilities) { return { available: false, reason: 'Inspect a source or initialize the local FFmpeg engine first.', }; } const missing = [ ...(requirements.muxers ?? []) .filter((name) => !activeCapabilities.muxers.has(name)) .map((name) => `muxer ${name}`), ...(requirements.encoders ?? []) .filter((name) => !activeCapabilities.encoders.has(name)) .map((name) => `encoder ${name}`), ...(requirements.filters ?? []) .filter((name) => !activeCapabilities.filters.has(name)) .map((name) => `filter ${name}`), ]; return missing.length === 0 && !unavailableReason ? { available: true } : { available: false, reason: unavailableReason ?? `The loaded core is missing ${missing.join(', ')}.`, }; }; const advancedPresetOptions: readonly AdvancedExportPresetOption[] = [ ...BUILT_IN_PRESETS, ...userPresets, ].map((preset) => { const requirements = 'builtIn' in preset ? preset.requirements : requirementsForUserPreset(preset); const availability = activeCapabilities ? presetAvailability({ requirements }, activeCapabilities) : { status: 'unavailable' as const, reasons: ['Initialize the engine to verify this preset.'], }; const opusBlocked = preset.audio?.codec === 'libopus' && !FFMPEG_OPUS_REGRESSION_VERIFIED; return { id: preset.id, name: preset.name, fileExtension: preset.fileExtension, supportsAudio: preset.audio !== undefined, supportsVideo: preset.video !== undefined, ...(availability.status === 'unavailable' || opusBlocked ? { disabledReason: opusBlocked ? 'Opus output is disabled until the reviewed core passes the non-empty ST/MT regression encode and re-probe gate.' : availability.reasons.join(' '), } : {}), }; }); const timelinePresetOptions = advancedPresetOptions.filter((option) => { const preset = resolvePreset(option.id); return ( option.supportsVideo && preset !== undefined && ['mp4', 'webm', 'matroska'].includes(preset.container) ); }); const selectedTimelinePreset = resolvePreset(project.output.presetId); const selectedTimelinePresetOption = timelinePresetOptions.find( (option) => option.id === project.output.presetId ); const timelineOutputReason = project.timeline.length === 0 ? 'Add at least one timeline clip.' : !selectedTimelinePreset?.video ? 'Choose a video export preset.' : selectedTimelinePresetOption?.disabledReason; const anyAccuratePreset = advancedPresetOptions.some( (preset) => !preset.disabledReason ); const anyAudioPreset = advancedPresetOptions.some( (preset) => preset.supportsAudio && !preset.disabledReason ); const anyVideoPreset = advancedPresetOptions.some( (preset) => preset.supportsVideo && !preset.disabledReason ); const structuralContext = (() => { const diagnostics: StructuralCompatibilityDiagnostic[] = []; const sources: ConcatSource[] = []; const clips = project.timeline.map((clip, index) => { const asset = assets.find((entry) => entry.id === clip.assetId); const projectAsset = project.assets.find( (entry) => entry.id === clip.assetId ); const label = `${index + 1}. ${ asset?.file.name ?? projectAsset?.originalName ?? 'Missing source' }`; if (!asset?.probe) { diagnostics.push({ code: 'source-unavailable', severity: 'error', message: `${label} is not attached and probed.`, path: `timeline.${index}.assetId`, }); return { id: clip.id, label, hasAudio: false, hasVideo: false, hasSubtitles: false, durationSeconds: clip.sourceOutSeconds - clip.sourceInSeconds, }; } const unsupportedStreams = asset.probe.streams.filter( (stream) => stream.type === 'attachment' || stream.type === 'data' || stream.type === 'unknown' ); if (unsupportedStreams.length > 0) { diagnostics.push({ code: 'unvalidated-stream-type', severity: 'error', message: `${label} contains ${unsupportedStreams .map((stream) => `${stream.type} stream ${stream.index}`) .join( ', ' )}; fast concat is disabled because those streams cannot yet be validated safely.`, path: `timeline.${index}.streams`, }); } try { sources.push(concatSourceForClip(clip, asset, index)); } catch (error) { diagnostics.push({ code: 'source-range', severity: 'error', message: error instanceof Error ? `${label}: ${error.message}` : `${label} has an invalid source range.`, path: `timeline.${index}`, }); } return { id: clip.id, label, durationSeconds: clip.sourceOutSeconds - clip.sourceInSeconds, hasAudio: asset.probe.streams.some((stream) => stream.type === 'audio'), hasVideo: asset.probe.streams.some( (stream) => stream.type === 'video' && stream.disposition.attached_pic !== true ), hasSubtitles: asset.probe.streams.some( (stream) => stream.type === 'subtitle' ), }; }); if ( project.timeline.length >= 2 && sources.length === project.timeline.length ) { diagnostics.push(...validateConcatCompatibility(sources)); } const firstAsset = project.timeline[0] ? assets.find((entry) => entry.id === project.timeline[0]?.assetId) : undefined; const fastTargetExtension = firstAsset ? extensionFromName(firstAsset.file.name) : ''; return { clips, sources, diagnostics, fastTargetExtension, fastTargetMuxer: fastTargetExtension ? muxerForExtension(fastTargetExtension) : '', }; })(); const structuralSelectedClip = selectedClip && inspectedClipAsset?.probe ? { id: selectedClip.id, label: inspectedClipAsset.file.name, sourceInSeconds: selectedClip.sourceInSeconds, sourceOutSeconds: selectedClip.sourceOutSeconds, ...(inspectedClipAsset.probe.durationSeconds === undefined ? {} : { sourceDurationSeconds: inspectedClipAsset.probe.durationSeconds, }), sourceExtension: extensionFromName(inspectedClipAsset.file.name), hasAudio: inspectedClipAsset.probe.streams.some( (stream) => stream.type === 'audio' ), hasVideo: inspectedClipAsset.probe.streams.some( (stream) => stream.type === 'video' && stream.disposition.attached_pic !== true ), } : undefined; const structuralHasAnyAudio = structuralContext.clips.some( (clip) => clip.hasAudio ); const structuralHasMissingAudio = structuralContext.clips.some( (clip) => !clip.hasAudio ); const sourceExtension = inspectedClipAsset ? extensionFromName(inspectedClipAsset.file.name) : ''; const sourceMuxer = sourceExtension ? muxerForExtension(sourceExtension) : undefined; const advancedAvailability: Partial< Readonly> > = { 'split-fast': sourceMuxer ? capabilityStatus({ muxers: [sourceMuxer] }) : { available: false, reason: 'Select a source first.' }, 'split-accurate': capabilityStatus( {}, anyAccuratePreset ? undefined : 'No verified export preset is available.' ), 'waveform-static': capabilityStatus({ muxers: ['image2'], encoders: ['png'], filters: ['color', 'aformat', 'showwavespic', 'overlay'], }), 'waveform-peaks': capabilityStatus({ muxers: ['wav'], encoders: ['pcm_s16le'], }), 'loudness-measure': capabilityStatus({ muxers: ['null'], filters: ['loudnorm'], }), 'peak-normalize': capabilityStatus( { filters: ['volume'] }, anyAudioPreset ? undefined : 'No verified audio preset is available.' ), 'loudness-normalize': capabilityStatus( { filters: ['loudnorm'] }, anyAudioPreset ? undefined : 'No verified audio preset is available.' ), 'thumbnail-series': capabilityStatus({ filters: ['split', 'select', 'setpts'], }), 'contact-sheet': capabilityStatus({ filters: ['fps', 'scale', 'tile'], }), 'contact-sheet-labels': { ...capabilityStatus({ filters: ['drawtext'] }), }, 'scene-change-analysis': capabilityStatus({ muxers: ['null'], filters: ['select', 'showinfo'], }), 'metadata-write': sourceMuxer ? capabilityStatus({ muxers: [sourceMuxer] }) : { available: false, reason: 'Select a source first.' }, 'chapter-write': sourceMuxer ? capabilityStatus({ muxers: [sourceMuxer] }) : { available: false, reason: 'Select a source first.' }, 'subtitle-extract': capabilityStatus({}), 'subtitle-convert': capabilityStatus({}), 'subtitle-mux': capabilityStatus({}), 'subtitle-burn': capabilityStatus( { filters: ['subtitles'] }, anyVideoPreset ? undefined : 'No verified video preset is available.' ), 'preset-storage': { available: true }, }; const structuralAvailability = { 'trim-fast': sourceMuxer ? capabilityStatus({ muxers: [sourceMuxer] }) : { available: false, reason: 'Select an attached source first.' }, 'trim-accurate': capabilityStatus( {}, anyAccuratePreset ? undefined : 'No verified export preset is available.' ), 'concat-fast': structuralContext.fastTargetMuxer ? capabilityStatus({ muxers: [structuralContext.fastTargetMuxer], }) : { available: false, reason: 'Attach and probe the first timeline source.', }, 'concat-normalized': capabilityStatus( { filters: [ 'trim', 'setpts', 'scale', 'pad', 'setsar', 'fps', 'format', 'concat', ...(structuralHasAnyAudio ? ['atrim', 'asetpts', 'aresample', 'aformat'] : []), ...(structuralHasAnyAudio && structuralHasMissingAudio ? ['anullsrc'] : []), ], }, anyVideoPreset ? undefined : 'No verified video preset is available.' ), 'concat-crossfade': capabilityStatus({ filters: ['xfade', ...(structuralHasAnyAudio ? ['acrossfade'] : [])], }), } as const; const advancedSource = inspectedClipAsset?.probe ? { sourceId: inspectedClipAsset.id, ...(inspectedClipAsset.probe.durationSeconds === undefined ? {} : { durationSeconds: inspectedClipAsset.probe.durationSeconds, }), sourceExtension, hasAudio: inspectedClipAsset.probe.streams.some( (stream) => stream.type === 'audio' ), hasVideo: inspectedClipAsset.probe.streams.some( (stream) => stream.type === 'video' && stream.disposition.attached_pic !== true ), ...(() => { const index = inspectedClipAsset.probe?.streams.find( (stream) => stream.type === 'audio' )?.index; return index === undefined ? {} : { selectedAudioStreamIndex: index }; })(), } : undefined; const advancedMetadataStreams = inspectedClipAsset?.probe?.streams.map((stream) => ({ index: stream.index, type: stream.type, codec: stream.codecName ?? 'unknown', ...(stream.language ? { language: stream.language } : {}), ...(stream.title ? { title: stream.title } : {}), disposition: stream.disposition, })) ?? []; const advancedChapters = project.chapters.map((chapter) => ({ id: chapter.id, title: chapter.title, startSeconds: chapter.startSeconds, endSeconds: chapter.endSeconds, timeBase: chapter.timeBase ?? '1/1000', })); const advancedTimelineClips = inspectedClipAsset ? project.timeline .filter((clip) => clip.assetId === inspectedClipAsset.id) .map((clip, index) => ({ startSeconds: clip.sourceInSeconds, endSeconds: clip.sourceOutSeconds, label: `clip-${index + 1}`, })) : []; const advancedSubtitleStreams = inspectedClipAsset?.probe?.streams.flatMap((stream) => stream.type === 'subtitle' ? [ { index: stream.index, codec: stream.codecName ?? 'unknown', ...(stream.language ? { language: stream.language } : {}), ...(stream.title ? { title: stream.title } : {}), default: stream.disposition.default ?? false, forced: stream.disposition.forced ?? false, }, ] : [] ) ?? []; const quickBusy = engineState.status === 'loading' || engineState.status === 'recovering' || sourceProbeBusy || waveformProgress !== undefined || scheduledPlanCount >= 8; const sourceManagementBusy = sourceProbeBusy || scheduledPlanCount > 0 || waveformProgress !== undefined; const editWorkspace = ( <>

Local sources

Media bin

{assets.length}

Browser playback

Preview

Object URL
void createPreviewProxy(inspectedClipAsset)} proxySettings={previewProxySettings} onProxySettingsChange={setPreviewProxySettings} proxyBusy={quickBusy} {...(inspectedClipAsset ? { currentTimeSeconds: playheadByAsset[inspectedClipAsset.id] ?? 0, onCurrentTimeChange: (timeSeconds: number) => setPlayheadByAsset((current) => ({ ...current, [inspectedClipAsset.id]: timeSeconds, })), } : {})} />
{ if (selectedClip) updateClip(selectedClip.id, changes); }} onMetadataChange={(metadata) => applyProjectAction({ type: 'metadata/set', metadata, updatedAt: timestamp(), }) } onChaptersChange={(chapters) => applyProjectAction({ type: 'chapters/set', chapters, updatedAt: timestamp(), }) } onSubtitlesChange={changeSubtitles} onSubtitleImport={importSubtitleFile} onThumbnail={extractThumbnail} currentPreviewTimeSeconds={ inspectedClipAsset ? (playheadByAsset[inspectedClipAsset.id] ?? 0) : undefined } drawerOpen={editDrawer === 'inspector'} onDrawerClose={closeEditDrawer} />
{ if (!selectedClip) return; setPlayheadByAsset((current) => ({ ...current, [selectedClip.assetId]: timeSeconds, })); }} onSelect={(clipId) => { setSelectedClipId(clipId); const clip = project.timeline.find((entry) => entry.id === clipId); if (clip) setSelectedAssetId(clip.assetId); }} onMove={moveClip} onUpdate={updateClip} onSelectionChange={(selection) => { if (!selectedClip) return; setSplitMarkersByAsset((current) => ({ ...current, [selectedClip.assetId]: selection.markers, })); }} onRemove={(clipId) => { applyProjectAction({ type: 'clip/remove', clipId, updatedAt: timestamp(), }); if (selectedClipId === clipId) setSelectedClipId(undefined); }} /> = 2 ? { timeline: { clips: structuralContext.clips, fastTargetExtension: structuralContext.fastTargetExtension, fastTargetMuxer: structuralContext.fastTargetMuxer, fastCompatibilityDiagnostics: structuralContext.diagnostics, }, } : {})} exportPresets={advancedPresetOptions} availability={structuralAvailability} busy={quickBusy} onTrim={handleStructuralTrim} onConcat={handleStructuralConcat} />

Timeline export

Render the sequence

Normalize the sequential clips to these explicit typed settings. The source files remain local.

Empty canvas dimensions preserve the first transformed clip up to 1920×1080 without upscaling. Video is required for this sequential timeline renderer.

{timelineOutputReason ? ( {timelineOutputReason} ) : null}
); return (
{project.name} { const file = event.currentTarget.files?.[0]; if (file) void importProject(file); event.currentTarget.value = ''; }} />
{projectNotice ? (

{projectNotice}

) : null} {waveformProgress !== undefined ? (
Building bounded waveform peaks locally…{' '} {Math.round(waveformProgress * 100)}%
) : null} {mode === 'quick-convert' ? (
{assets.length > 0 ? (

Imported locally

Sources

{assets.length}
) : null}
0 } presets={[...BUILT_IN_PRESETS, ...userPresets]} onExport={exportQuick} />

Source or latest result

Preview

void createPreviewProxy(selectedAsset)} proxySettings={previewProxySettings} onProxySettingsChange={setPreviewProxySettings} proxyBusy={quickBusy} {...(selectedAsset ? { currentTimeSeconds: playheadByAsset[selectedAsset.id] ?? 0, onCurrentTimeChange: (timeSeconds: number) => setPlayheadByAsset((current) => ({ ...current, [selectedAsset.id]: timeSeconds, })), } : {})} />
) : ( editWorkspace )} { const activeJobId = activeScheduledJobIdRef.current; if (activeJobId) void cancelScheduledJob(activeJobId); else void engineManager.cancelActive(); }} onCancelJob={(job) => void cancelScheduledJob(job.id)} onRetryJob={(job) => retryScheduledJob(job.id)} onClearJobHistory={clearScheduledJobHistory} onSave={(output) => void saveManagedResult(output)} onSaveZip={(selected) => void saveManagedZip(selected)} onSaveReport={() => void saveManagedReport()} onPreview={(output) => setPreviewResultId(output.id)} onRemove={(output) => { resultCollection.remove(output.id); refreshResults(); }} onClearResults={clearResult} /> { setEnginePreference(preference); engineManager.setPreference(preference); }} onInitialize={() => { if (engineManager.state.status === 'error') { engineManager.dispose(); } void engineManager.initialize().catch(() => undefined); }} />
); } export default function App() { const [helpOpen, setHelpOpen] = useState(false); return ( setHelpOpen(true) }} onContextError={(error) => { console.warn( 'Toolbox context unavailable; continuing standalone.', error ); }} > setHelpOpen(false)} /> ); }