feat: publish av-tools 0.1.0
This commit is contained in:
122
src/commands/fades.ts
Normal file
122
src/commands/fades.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import type { ExportPreset } from './convert';
|
||||
import { buildConvertPlan } from './convert';
|
||||
import type { SourceReference } from './command-utils';
|
||||
import type { FFmpegCommandPlan } from './command-plan';
|
||||
import {
|
||||
audioFadeFilters,
|
||||
videoFadeFilters,
|
||||
type FadeCurve,
|
||||
type AudioFilterStage,
|
||||
type FilterNode,
|
||||
type VideoFilterStage,
|
||||
} from './filtergraph-builder';
|
||||
|
||||
export {
|
||||
audioFadeFilters,
|
||||
videoFadeFilters,
|
||||
type AudioFilterStage,
|
||||
type FilterNode,
|
||||
type VideoFilterStage,
|
||||
};
|
||||
|
||||
export interface FadePlanOptions {
|
||||
readonly jobId: string;
|
||||
readonly source: SourceReference;
|
||||
readonly preset: ExportPreset;
|
||||
readonly durationSeconds: number;
|
||||
readonly audioFadeInSeconds?: number;
|
||||
readonly audioFadeOutSeconds?: number;
|
||||
readonly videoFadeInSeconds?: number;
|
||||
readonly videoFadeOutSeconds?: number;
|
||||
readonly audioCurve?: FadeCurve;
|
||||
readonly videoCurve?: FadeCurve;
|
||||
}
|
||||
|
||||
export function buildFadePlan(options: FadePlanOptions): FFmpegCommandPlan {
|
||||
if (
|
||||
!Number.isFinite(options.durationSeconds) ||
|
||||
options.durationSeconds <= 0
|
||||
) {
|
||||
throw new RangeError('Fade clip duration must be positive');
|
||||
}
|
||||
validatePair(
|
||||
options.durationSeconds,
|
||||
options.audioFadeInSeconds,
|
||||
options.audioFadeOutSeconds,
|
||||
'Audio'
|
||||
);
|
||||
validatePair(
|
||||
options.durationSeconds,
|
||||
options.videoFadeInSeconds,
|
||||
options.videoFadeOutSeconds,
|
||||
'Video'
|
||||
);
|
||||
const audioNodes = audioFadeFilters(
|
||||
options.durationSeconds,
|
||||
options.audioFadeInSeconds,
|
||||
options.audioFadeOutSeconds,
|
||||
options.audioCurve
|
||||
);
|
||||
const videoNodes = videoFadeFilters(
|
||||
options.durationSeconds,
|
||||
options.videoFadeInSeconds,
|
||||
options.videoFadeOutSeconds,
|
||||
options.videoCurve
|
||||
);
|
||||
if (audioNodes.length > 0 && !options.preset.audio) {
|
||||
throw new TypeError('Audio fades require a preset with audio');
|
||||
}
|
||||
if (videoNodes.length > 0 && !options.preset.video) {
|
||||
throw new TypeError('Video fades require a preset with video');
|
||||
}
|
||||
if (audioNodes.length === 0 && videoNodes.length === 0) {
|
||||
throw new TypeError('At least one fade duration is required');
|
||||
}
|
||||
return buildConvertPlan({
|
||||
jobId: options.jobId,
|
||||
source: options.source,
|
||||
preset: options.preset,
|
||||
operation: 'fades',
|
||||
expectedDurationSeconds: options.durationSeconds,
|
||||
...(audioNodes.length > 0
|
||||
? {
|
||||
audioFiltergraph: audioNodes.map((node) => node.expression).join(','),
|
||||
}
|
||||
: {}),
|
||||
...(videoNodes.length > 0
|
||||
? {
|
||||
videoFiltergraph: videoNodes.map((node) => node.expression).join(','),
|
||||
}
|
||||
: {}),
|
||||
additionalRequirements: {
|
||||
filters: [
|
||||
...new Set(
|
||||
[...audioNodes, ...videoNodes].flatMap(
|
||||
(node) => node.requiredCapabilities?.filters ?? []
|
||||
)
|
||||
),
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function validatePair(
|
||||
durationSeconds: number,
|
||||
fadeInSeconds: number | undefined,
|
||||
fadeOutSeconds: number | undefined,
|
||||
label: string
|
||||
): void {
|
||||
const fadeIn = fadeInSeconds ?? 0;
|
||||
const fadeOut = fadeOutSeconds ?? 0;
|
||||
if (
|
||||
!Number.isFinite(fadeIn) ||
|
||||
!Number.isFinite(fadeOut) ||
|
||||
fadeIn < 0 ||
|
||||
fadeOut < 0 ||
|
||||
fadeIn + fadeOut > durationSeconds
|
||||
) {
|
||||
throw new RangeError(
|
||||
`${label} fade durations must be nonnegative and cannot overlap`
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user