feat: publish av-tools 0.2.0
This commit is contained in:
@@ -2,7 +2,11 @@ import { useId, useState } from 'react';
|
||||
import './StructuralOperations.css';
|
||||
|
||||
export type StructuralCapabilityKey =
|
||||
'trim-fast' | 'trim-accurate' | 'concat-fast' | 'concat-normalized';
|
||||
| 'trim-fast'
|
||||
| 'trim-accurate'
|
||||
| 'concat-fast'
|
||||
| 'concat-normalized'
|
||||
| 'concat-crossfade';
|
||||
|
||||
export interface StructuralCapabilityStatus {
|
||||
readonly available: boolean;
|
||||
@@ -35,6 +39,7 @@ export interface StructuralTimelineClip {
|
||||
readonly hasAudio: boolean;
|
||||
readonly hasVideo: boolean;
|
||||
readonly hasSubtitles: boolean;
|
||||
readonly durationSeconds?: number;
|
||||
}
|
||||
|
||||
export interface StructuralCompatibilityDiagnostic {
|
||||
@@ -92,6 +97,7 @@ export type StructuralConcatRequest =
|
||||
readonly presetId: string;
|
||||
readonly missingAudioPolicy: MissingAudioPolicy;
|
||||
readonly subtitlePolicy: NormalizedSubtitlePolicy;
|
||||
readonly crossfadeSeconds: number;
|
||||
};
|
||||
|
||||
type RequestHandler<T> = (request: T) => void | Promise<void>;
|
||||
@@ -135,6 +141,7 @@ export function StructuralOperations({
|
||||
const [subtitlePolicy, setSubtitlePolicy] = useState<
|
||||
NormalizedSubtitlePolicy | ''
|
||||
>('');
|
||||
const [crossfadeSeconds, setCrossfadeSeconds] = useState('0');
|
||||
const [pending, setPending] = useState(false);
|
||||
const [status, setStatus] = useState<string>();
|
||||
|
||||
@@ -156,6 +163,7 @@ export function StructuralOperations({
|
||||
preset: concatPreset,
|
||||
missingAudioPolicy,
|
||||
subtitlePolicy,
|
||||
crossfadeSeconds,
|
||||
availability,
|
||||
connected: Boolean(onConcat),
|
||||
busy: busy || pending,
|
||||
@@ -445,6 +453,30 @@ export function StructuralOperations({
|
||||
)}
|
||||
onChange={setSubtitlePolicy}
|
||||
/>
|
||||
<label
|
||||
className="structural-field"
|
||||
htmlFor={`${componentId}-crossfade`}
|
||||
>
|
||||
<span>Crossfade duration</span>
|
||||
<input
|
||||
id={`${componentId}-crossfade`}
|
||||
type="number"
|
||||
aria-label="Crossfade duration"
|
||||
min="0"
|
||||
max="10"
|
||||
step="0.04"
|
||||
inputMode="decimal"
|
||||
value={crossfadeSeconds}
|
||||
disabled={busy || pending}
|
||||
onChange={(event) =>
|
||||
setCrossfadeSeconds(event.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
<small>
|
||||
0 keeps hard cuts. A value from 0.04–10 seconds overlaps video
|
||||
and, when retained, audio at every edit.
|
||||
</small>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -488,6 +520,7 @@ export function StructuralOperations({
|
||||
presetId: concatPreset.id,
|
||||
missingAudioPolicy,
|
||||
subtitlePolicy,
|
||||
crossfadeSeconds: Number(crossfadeSeconds),
|
||||
},
|
||||
'Normalized concat queued.'
|
||||
);
|
||||
@@ -856,6 +889,7 @@ function reasonForConcat({
|
||||
preset,
|
||||
missingAudioPolicy,
|
||||
subtitlePolicy,
|
||||
crossfadeSeconds,
|
||||
availability,
|
||||
connected,
|
||||
busy,
|
||||
@@ -865,6 +899,7 @@ function reasonForConcat({
|
||||
readonly preset: StructuralExportPresetOption | undefined;
|
||||
readonly missingAudioPolicy: MissingAudioPolicy | '';
|
||||
readonly subtitlePolicy: NormalizedSubtitlePolicy | '';
|
||||
readonly crossfadeSeconds: string;
|
||||
readonly availability: StructuralOperationsProps['availability'];
|
||||
readonly connected: boolean;
|
||||
readonly busy: boolean;
|
||||
@@ -924,13 +959,39 @@ function reasonForConcat({
|
||||
) {
|
||||
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.';
|
||||
}
|
||||
return capabilityReason(
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user