import { useId, useState } from 'react'; import './StructuralOperations.css'; export type StructuralCapabilityKey = | 'trim-fast' | 'trim-accurate' | 'concat-fast' | 'concat-normalized' | 'concat-crossfade'; export interface StructuralCapabilityStatus { readonly available: boolean; readonly reason?: string; } export interface StructuralExportPresetOption { readonly id: string; readonly name: string; readonly fileExtension: string; readonly supportsAudio: boolean; readonly supportsVideo: boolean; readonly disabledReason?: string; } export interface StructuralSelectedClip { readonly id: string; readonly label: string; readonly sourceInSeconds: number; readonly sourceOutSeconds: number; readonly sourceDurationSeconds?: number; readonly sourceExtension: string; readonly hasAudio: boolean; readonly hasVideo: boolean; } export interface StructuralTimelineClip { readonly id: string; readonly label: string; readonly hasAudio: boolean; readonly hasVideo: boolean; readonly hasSubtitles: boolean; readonly durationSeconds?: number; } export interface StructuralCompatibilityDiagnostic { readonly code: string; readonly severity: 'info' | 'warning' | 'error'; readonly message: string; readonly path?: string; } export interface StructuralTimeline { /** Clips in their exact export order. */ readonly clips: readonly StructuralTimelineClip[]; readonly fastTargetExtension: string; readonly fastTargetMuxer: string; /** * `undefined` means the command layer has not checked the inputs. An empty * array means it checked them and found no compatibility diagnostics. */ readonly fastCompatibilityDiagnostics?: readonly StructuralCompatibilityDiagnostic[]; } export type MissingAudioPolicy = 'insert-silence' | 'drop-all' | 'reject'; export type NormalizedSubtitlePolicy = 'drop-all' | 'reject'; export type StructuralTrimRequest = | { readonly operation: 'trim'; readonly mode: 'fast'; readonly clipId: string; readonly startSeconds: number; readonly endSeconds: number; readonly targetExtension: string; } | { readonly operation: 'trim'; readonly mode: 'accurate'; readonly clipId: string; readonly startSeconds: number; readonly endSeconds: number; readonly presetId: string; }; export type StructuralConcatRequest = | { readonly operation: 'concat'; readonly mode: 'fast'; readonly clipIds: readonly string[]; readonly targetExtension: string; readonly targetMuxer: string; } | { readonly operation: 'concat'; readonly mode: 'normalized'; readonly clipIds: readonly string[]; readonly presetId: string; readonly missingAudioPolicy: MissingAudioPolicy; readonly subtitlePolicy: NormalizedSubtitlePolicy; readonly crossfadeSeconds: number; }; type RequestHandler = (request: T) => void | Promise; export interface StructuralOperationsProps { readonly selectedClip?: StructuralSelectedClip; readonly timeline?: StructuralTimeline; readonly exportPresets?: readonly StructuralExportPresetOption[]; /** * Missing entries fail closed. The integration layer must opt in only after * checking the loaded FFmpeg core and the current browser environment. */ readonly availability?: Partial< Readonly> >; readonly busy?: boolean; readonly onTrim?: RequestHandler; readonly onConcat?: RequestHandler; } type TrimMode = StructuralTrimRequest['mode']; type ConcatMode = StructuralConcatRequest['mode']; export function StructuralOperations({ selectedClip, timeline, exportPresets = [], availability = {}, busy = false, onTrim, onConcat, }: StructuralOperationsProps) { const componentId = useId(); const [trimMode, setTrimMode] = useState('fast'); const [concatMode, setConcatMode] = useState('fast'); const [trimPresetId, setTrimPresetId] = useState(''); const [concatPresetId, setConcatPresetId] = useState(''); const [missingAudioPolicy, setMissingAudioPolicy] = useState< MissingAudioPolicy | '' >(''); const [subtitlePolicy, setSubtitlePolicy] = useState< NormalizedSubtitlePolicy | '' >(''); const [crossfadeSeconds, setCrossfadeSeconds] = useState('0'); const [pending, setPending] = useState(false); const [status, setStatus] = useState(); const trimPreset = exportPresets.find((preset) => preset.id === trimPresetId); const concatPreset = exportPresets.find( (preset) => preset.id === concatPresetId ); const trimReason = reasonForTrim({ mode: trimMode, selectedClip, preset: trimPreset, availability, connected: Boolean(onTrim), busy: busy || pending, }); const concatReason = reasonForConcat({ mode: concatMode, timeline, preset: concatPreset, missingAudioPolicy, subtitlePolicy, crossfadeSeconds, availability, connected: Boolean(onConcat), busy: busy || pending, }); const run = ( handler: RequestHandler | undefined, request: T, successMessage: string ) => { if (!handler) { return; } setPending(true); setStatus(undefined); void Promise.resolve() .then(() => handler(request)) .then(() => setStatus(successMessage)) .catch((error: unknown) => { setStatus( error instanceof Error ? `The operation could not be queued: ${error.message}` : 'The operation could not be queued.' ); }) .finally(() => setPending(false)); }; return (

Structural exports

Trim or join without guesswork

Choose stream-copy speed or a preset-backed re-encode. Every action runs locally in this browser.

Local only
1

Trim the selected clip

The current timeline in/out points are used as-is; this panel does not change the edit.

{selectedClip ? (
Clip
{selectedClip.label}
Current range
{formatTime(selectedClip.sourceInSeconds)}– {formatTime(selectedClip.sourceOutSeconds)}
Export duration
{formatDuration( selectedClip.sourceOutSeconds - selectedClip.sourceInSeconds )}
) : (

Select one timeline clip to export its current range.

)}
Trim method
{trimMode === 'accurate' ? ( selectedClip?.hasVideo ? preset.supportsVideo : Boolean(selectedClip?.hasAudio && preset.supportsAudio) } disabled={busy || pending} onChange={setTrimPresetId} /> ) : (

The fast export keeps the source streams and writes{' '} . {normalizeExtension(selectedClip?.sourceExtension) || 'source format'} .

)} { if (!selectedClip) { return; } if (trimMode === 'fast') { run( onTrim, { operation: 'trim', mode: 'fast', clipId: selectedClip.id, startSeconds: selectedClip.sourceInSeconds, endSeconds: selectedClip.sourceOutSeconds, targetExtension: normalizeExtension( selectedClip.sourceExtension ), }, 'Fast trim queued.' ); return; } if (!trimPreset) { return; } run( onTrim, { operation: 'trim', mode: 'accurate', clipId: selectedClip.id, startSeconds: selectedClip.sourceInSeconds, endSeconds: selectedClip.sourceOutSeconds, presetId: trimPreset.id, }, 'Accurate trim queued.' ); }} />
2

Join the sequential timeline

Clips are submitted in their visible timeline order. Fast concatenation is enabled only after an explicit stream check.

{timeline ? ( ) : (

Add at least two clips to the sequential timeline.

)}
Join method
{concatMode === 'fast' ? ( ) : ( <> preset.supportsVideo} disabled={busy || pending} onChange={setConcatPresetId} /> clip.hasSubtitles) )} onChange={setSubtitlePolicy} /> )} { if (!timeline) { return; } const clipIds = timeline.clips.map((clip) => clip.id); if (concatMode === 'fast') { run( onConcat, { operation: 'concat', mode: 'fast', clipIds, targetExtension: normalizeExtension( timeline.fastTargetExtension ), targetMuxer: timeline.fastTargetMuxer.trim(), }, 'Fast concat queued.' ); return; } if (!concatPreset || !missingAudioPolicy || !subtitlePolicy) { return; } run( onConcat, { operation: 'concat', mode: 'normalized', clipIds, presetId: concatPreset.id, missingAudioPolicy, subtitlePolicy, crossfadeSeconds: Number(crossfadeSeconds), }, 'Normalized concat queued.' ); }} />

{pending ? 'Queueing local operation…' : status}

); } function TimelineSummary({ clips, }: { readonly clips: readonly StructuralTimelineClip[]; }) { const audioCount = clips.filter((clip) => clip.hasAudio).length; const subtitleCount = clips.filter((clip) => clip.hasSubtitles).length; const missingAudio = clips.filter((clip) => !clip.hasAudio); return (

{clips.length} clips in sequence ·{' '} {audioCount} with audio ·{' '} {subtitleCount} with subtitles

{missingAudio.length > 0 ? (

No audio: {missingAudio.map((clip) => clip.label).join(', ')}

) : (

Every clip has an audio stream.

)}
); } function CompatibilityReport({ diagnostics, clipCount, }: { readonly diagnostics: readonly StructuralCompatibilityDiagnostic[] | undefined; readonly clipCount: number; }) { if (diagnostics === undefined) { return (

Fast compatibility not checked

Inspect every input stream before enabling stream-copy concatenation.

); } if (diagnostics.length === 0) { return (

Fast compatibility passed

All {clipCount} clips have matching stream properties for stream copy.

); } const hasErrors = diagnostics.some( (diagnostic) => diagnostic.severity === 'error' ); return (

{hasErrors ? 'Fast compatibility failed' : 'Fast compatibility notes'}

    {diagnostics.map((diagnostic, index) => (
  • {diagnostic.severity} {diagnostic.message} {diagnostic.path ? ( Checked field: {diagnostic.path} ) : null}
  • ))}
); } function PresetField({ id, label, value, presets, suitable, disabled, onChange, }: { readonly id: string; readonly label: string; readonly value: string; readonly presets: readonly StructuralExportPresetOption[]; readonly suitable: (preset: StructuralExportPresetOption) => boolean; readonly disabled: boolean; readonly onChange: (value: string) => void; }) { const selectableCount = presets.filter( (preset) => suitable(preset) && !preset.disabledReason ).length; return ( ); } function MissingAudioPolicyField({ componentId, value, disabled, onChange, }: { readonly componentId: string; readonly value: MissingAudioPolicy | ''; readonly disabled: boolean; readonly onChange: (value: MissingAudioPolicy) => void; }) { const options: readonly { readonly value: MissingAudioPolicy; readonly title: string; readonly detail: string; }[] = [ { value: 'insert-silence', title: 'Insert silence', detail: 'Keep an audio track and fill each silent clip to its duration.', }, { value: 'drop-all', title: 'Drop all audio', detail: 'Export one video-only sequence, even from clips with audio.', }, { value: 'reject', title: 'Reject mixed audio', detail: 'Stop if some clips have audio and others do not.', }, ]; return (
Missing-audio policy

Required: choose how a normalized sequence handles clips without audio.

{options.map((option) => ( ))}
); } function SubtitlePolicyField({ componentId, value, disabled, hasSubtitles, onChange, }: { readonly componentId: string; readonly value: NormalizedSubtitlePolicy | ''; readonly disabled: boolean; readonly hasSubtitles: boolean; readonly onChange: (value: NormalizedSubtitlePolicy) => void; }) { return (
Subtitle policy

Required: normalized concat has no reviewed, container-safe subtitle retiming path. The selected preset and loaded FFmpeg capabilities still gate the final export.

); } function StructuralAction({ label, reason, onClick, }: { readonly label: string; readonly reason?: string; readonly onClick: () => void; }) { const reasonId = useId(); return (
{reason ? {reason} : null}
); } function reasonForTrim({ mode, selectedClip, preset, availability, connected, busy, }: { readonly mode: TrimMode; readonly selectedClip: StructuralSelectedClip | undefined; readonly preset: StructuralExportPresetOption | undefined; readonly availability: StructuralOperationsProps['availability']; readonly connected: boolean; readonly busy: boolean; }): string | undefined { if (busy) { return 'Another local operation is currently running.'; } if (!selectedClip) { return 'Select a timeline clip first.'; } if (!validRange(selectedClip)) { return 'The selected clip must have a finite in/out range inside its source.'; } if (mode === 'fast' && !normalizeExtension(selectedClip.sourceExtension)) { return 'The selected source has no safe output extension.'; } if (mode === 'accurate') { if (!preset) { return 'Choose an available typed preset for the accurate trim.'; } if (preset.disabledReason) { return preset.disabledReason; } if ( (selectedClip.hasVideo && !preset.supportsVideo) || (!selectedClip.hasVideo && selectedClip.hasAudio && !preset.supportsAudio) ) { return 'The selected preset does not encode this clip type.'; } } if (!connected) { return 'This action has not been connected to the job runner.'; } return capabilityReason( availability?.[mode === 'fast' ? 'trim-fast' : 'trim-accurate'] ); } function reasonForConcat({ mode, timeline, preset, missingAudioPolicy, subtitlePolicy, crossfadeSeconds, availability, connected, busy, }: { readonly mode: ConcatMode; readonly timeline: StructuralTimeline | undefined; readonly preset: StructuralExportPresetOption | undefined; readonly missingAudioPolicy: MissingAudioPolicy | ''; readonly subtitlePolicy: NormalizedSubtitlePolicy | ''; readonly crossfadeSeconds: string; readonly availability: StructuralOperationsProps['availability']; readonly connected: boolean; readonly busy: boolean; }): string | undefined { if (busy) { return 'Another local operation is currently running.'; } if (!timeline || timeline.clips.length < 2) { return 'Add at least two timeline clips in the order to be joined.'; } const clipIds = timeline.clips.map((clip) => clip.id); if ( clipIds.some((id) => id.trim() === '') || new Set(clipIds).size !== clipIds.length ) { return 'Every timeline clip must have a unique nonempty ID.'; } if (mode === 'fast') { if (!normalizeExtension(timeline.fastTargetExtension)) { return 'The sequence has no safe fast-concat output extension.'; } if (timeline.fastCompatibilityDiagnostics === undefined) { return 'Fast concat remains disabled until every input stream is checked.'; } if ( timeline.fastCompatibilityDiagnostics.some( (diagnostic) => diagnostic.severity === 'error' ) ) { return 'Resolve the reported stream incompatibilities or use normalized concat.'; } if (!/^[a-z0-9][a-z0-9_-]{0,31}$/u.test(timeline.fastTargetMuxer.trim())) { return 'The fast-concat output muxer is unknown.'; } } else { if (timeline.clips.some((clip) => !clip.hasVideo)) { return 'Normalized concat currently requires video in every clip.'; } if (!preset) { return 'Choose an available typed video preset for normalized concat.'; } if (preset.disabledReason) { return preset.disabledReason; } if (!preset.supportsVideo) { return 'Normalized concat requires a video preset.'; } if (!missingAudioPolicy) { return 'Choose an explicit missing-audio policy.'; } if (!subtitlePolicy) { return 'Choose an explicit subtitle policy.'; } if ( subtitlePolicy === 'reject' && timeline.clips.some((clip) => clip.hasSubtitles) ) { return 'This sequence contains subtitles. Explicitly drop all subtitles, or remove them before normalized concat.'; } const crossfade = Number(crossfadeSeconds); if ( !Number.isFinite(crossfade) || crossfade < 0 || (crossfade > 0 && (crossfade < 0.04 || crossfade > 10)) ) { return 'Crossfade duration must be 0, or a value from 0.04–10 seconds.'; } if (crossfade > 0) { for (const [index, clip] of timeline.clips.entries()) { if (clip.durationSeconds === undefined) { continue; } const required = index === 0 || index === timeline.clips.length - 1 ? crossfade : crossfade * 2; if (clip.durationSeconds < required) { return `Clip ${index + 1} is too short for ${crossfade} second crossfades at its edit boundaries.`; } } } } if (!connected) { return 'This action has not been connected to the job runner.'; } const baseReason = capabilityReason( availability?.[mode === 'fast' ? 'concat-fast' : 'concat-normalized'] ); if (baseReason || mode === 'fast' || Number(crossfadeSeconds) === 0) { return baseReason; } return capabilityReason(availability?.['concat-crossfade']); } function capabilityReason( status: StructuralCapabilityStatus | undefined ): string | undefined { if (!status?.available) { return ( status?.reason ?? 'The required browser and FFmpeg capabilities have not been verified.' ); } return undefined; } function validRange(clip: StructuralSelectedClip): boolean { if ( !Number.isFinite(clip.sourceInSeconds) || !Number.isFinite(clip.sourceOutSeconds) || clip.sourceInSeconds < 0 || clip.sourceOutSeconds <= clip.sourceInSeconds ) { return false; } return ( clip.sourceDurationSeconds === undefined || (Number.isFinite(clip.sourceDurationSeconds) && clip.sourceDurationSeconds > 0 && clip.sourceOutSeconds <= clip.sourceDurationSeconds) ); } function normalizeExtension(extension: string | undefined): string { const normalized = (extension ?? '') .trim() .replace(/^\.+/u, '') .toLowerCase(); return /^[a-z0-9]{1,10}$/u.test(normalized) ? normalized : ''; } function formatDuration(seconds: number): string { if (!Number.isFinite(seconds) || seconds < 0) { return 'Invalid range'; } return `${seconds.toFixed(3)} s`; } function formatTime(seconds: number): string { if (!Number.isFinite(seconds) || seconds < 0) { return '—'; } const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const remainder = seconds % 60; return `${hours.toString().padStart(2, '0')}:${minutes .toString() .padStart(2, '0')}:${remainder.toFixed(3).padStart(6, '0')}`; }