Skip to content

Commit

Permalink
Merge pull request #314 from mifi/refactor-config
Browse files Browse the repository at this point in the history
Refactoring: introduce Configuration class
  • Loading branch information
bkeepers authored Jan 27, 2025
2 parents e4ed047 + b75b10b commit dc300c8
Show file tree
Hide file tree
Showing 16 changed files with 426 additions and 295 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"scripts": {
"build": "pkgroll --clean-dist --sourcemap",
"prepublishOnly": "npm run build",
"test": "tsx test/index.ts",
"test": "vitest",
"lint": "eslint ."
},
"repository": {
Expand All @@ -60,6 +60,7 @@
"pkgroll": "^2.6.1",
"tsx": "^4.19.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
"typescript-eslint": "^8.20.0",
"vitest": "^3.0.4"
}
}
3 changes: 2 additions & 1 deletion src/api/defineFrameSource.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { BaseLayer, DebugOptions, OptionalPromise } from "../types.js";
import type { BaseLayer, OptionalPromise } from "../types.js";
import type { DebugOptions } from "../configuration.js";
import type { StaticCanvas } from "fabric/node";

/**
Expand Down
5 changes: 3 additions & 2 deletions src/audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import { flatMap } from 'lodash-es';
import { getCutFromArgs, ffmpeg } from './ffmpeg.js';
import { readFileStreams } from './ffmpeg.js';

import type { AudioLayer, AudioNormalizationOptions, AudioTrack, Clip, Config, Transition, VideoLayer } from './types.js'
import type { AudioLayer, AudioNormalizationOptions, AudioTrack, Clip, Transition, VideoLayer } from './types.js'
import type { Configuration } from './configuration.js'

export type AudioOptions = {
verbose: boolean;
tmpDir: string;
}

export type EditAudioOptions = Pick<Config, "keepSourceAudio" | "clips" | "clipsAudioVolume" | "audioNorm" | "outputVolume"> & {
export type EditAudioOptions = Pick<Configuration, "keepSourceAudio" | "clips" | "clipsAudioVolume" | "audioNorm" | "outputVolume"> & {
arbitraryAudio: AudioTrack[]
};

Expand Down
7 changes: 4 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import pMap from 'p-map';
import JSON5 from 'json5';
import assert from 'assert';

import Editly, { Config, Layer } from './index.js';
import Editly, { Layer } from './index.js';
import { ConfigurationOptions } from './configuration.js';

// See also readme
const cli = meow(`
Expand Down Expand Up @@ -72,7 +73,7 @@ const cli = meow(`
let { json } = cli.flags;
if (cli.input.length === 1 && /\.(json|json5|js)$/.test(cli.input[0].toLowerCase())) json = cli.input[0];

let params: Partial<Config> = {
let params: Partial<ConfigurationOptions> = {
defaults: {},
};

Expand Down Expand Up @@ -141,7 +142,7 @@ const cli = meow(`

if (!params.outPath) params.outPath = './editly-out.mp4';

await Editly(params as Config);
await Editly(params as ConfigurationOptions);
})().catch((err) => {
console.error('Caught error', err);
process.exitCode = 1;
Expand Down
248 changes: 248 additions & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import { AudioNormalizationOptions, AudioTrack, Clip, DefaultOptions } from "./types.js";
import { dirname, join } from "path";
import assert from "assert";
import { nanoid } from "nanoid";
import { merge } from "lodash-es"
import { expandLayerAliases } from "./sources/index.js";

export type DebugOptions = {
verbose?: boolean;
logTimes?: boolean;
}

export type FfmpegConfig = {
ffmpegPath?: string;
ffprobePath?: string;
enableFfmpegLog?: boolean;
}

export type ConfigurationOptions = {
/**
* Output path (`.mp4` or `.mkv`, can also be a `.gif`).
*/
outPath: string;

/**
* List of clip objects that will be played in sequence.
* Each clip can have one or more layers.
*
* @default []
*/
clips: Clip[];

/**
* Width which all media will be converted to.
*
* @default 640
*/
width?: number;

/**
* Height which all media will be converted to.
* Decides height based on `width` and aspect ratio of the first video by default.
*/
height?: number;

/**
* FPS which all videos will be converted to.
* Defaults to first video's FPS or `25`.
*/
fps?: number;

/**
* Specify custom output codec/format arguments for ffmpeg.
* Automatically adds codec options (normally `h264`) by default.
*
* @see [Example]{@link https://github.com/mifi/editly/blob/master/examples/customOutputArgs.json5}
*/
customOutputArgs?: string[];

/**
* Allow remote URLs as paths.
*
* @default false
*/
allowRemoteRequests?: boolean;

/**
* Fast mode (low resolution and FPS, useful for getting a quick preview ⏩).
*
* @default false
*/
fast?: boolean;

/**
* An object describing default options for clips and layers.
*/
defaults?: DefaultOptions;

/**
* List of arbitrary audio tracks.
*
* @default []
* @see [Audio tracks]{@link https://github.com/mifi/editly#arbitrary-audio-tracks}
*/
audioTracks?: AudioTrack[];

/**
* Set an audio track for the whole video..
*
* @see [Audio tracks]{@link https://github.com/mifi/editly#arbitrary-audio-tracks}
*/
audioFilePath?: string;

/**
* Background Volume
*
* @see [Audio tracks]{@link https://github.com/mifi/editly#arbitrary-audio-tracks}
*/
backgroundAudioVolume?: string | number;

/**
* Loop the audio track if it is shorter than video?
*
* @default false
*/
loopAudio?: boolean;

/**
* Keep source audio from `clips`?
*
* @default false
*/
keepSourceAudio?: boolean;

/**
* Volume of audio from `clips` relative to `audioTracks`.
*
* @default 1
* @see [Audio tracks]{@link https://github.com/mifi/editly#arbitrary-audio-tracks}
*/
clipsAudioVolume?: number | string;

/**
* Adjust output [volume]{@link http://ffmpeg.org/ffmpeg-filters.html#volume} (final stage).
*
* @default 1
* @see [Example]{@link https://github.com/mifi/editly/blob/master/examples/audio-volume.json5}
* @example
* 0.5
* @example
* '10db'
*/
outputVolume?: number | string;

/**
* Audio normalization.
*/
audioNorm?: AudioNormalizationOptions;

/**
* WARNING: Undocumented feature!
*/
keepTmp?: boolean;
} & DebugOptions & FfmpegConfig;

export type LayerSourceConfig = Pick<Configuration, "verbose" | "allowRemoteRequests" | "logTimes" | "tmpDir">;

const globalDefaults = {
duration: 4,
transition: {
duration: 0.5,
name: 'random',
audioOutCurve: 'tri',
audioInCurve: 'tri',
},
};

export class Configuration {
clips: Clip[];
outPath: string;
tmpDir: string;
allowRemoteRequests: boolean;
customOutputArgs?: string[];
defaults: DefaultOptions;

// Video
width?: number;
height?: number;
fps?: number;

// Audio
audioFilePath?: string;
backgroundAudioVolume?: string | number;
loopAudio?: boolean;
keepSourceAudio?: boolean;
audioNorm?: AudioNormalizationOptions;
outputVolume?: number | string;
clipsAudioVolume: string | number;
audioTracks: AudioTrack[];

// Debug
enableFfmpegLog: boolean;
verbose: boolean;
logTimes: boolean;
keepTmp: boolean;
fast: boolean;
ffmpegPath: string;
ffprobePath: string;

constructor(input: ConfigurationOptions) {
assert(input.outPath, 'Please provide an output path');
assert(Array.isArray(input.clips) && input.clips.length > 0, 'Please provide at least 1 clip');
assert(!input.customOutputArgs || Array.isArray(input.customOutputArgs), 'customOutputArgs must be an array of arguments');

this.outPath = input.outPath;
this.width = input.width
this.height = input.height
this.fps = input.fps
this.audioFilePath = input.audioFilePath
this.backgroundAudioVolume = input.backgroundAudioVolume
this.loopAudio = input.loopAudio
this.clipsAudioVolume = input.clipsAudioVolume ?? 1
this.audioTracks = input.audioTracks ?? []
this.keepSourceAudio = input.keepSourceAudio
this.allowRemoteRequests = input.allowRemoteRequests ?? false
this.audioNorm = input.audioNorm
this.outputVolume = input.outputVolume
this.customOutputArgs = input.customOutputArgs
this.defaults = merge({}, globalDefaults, input.defaults);

this.clips = input.clips.map(clip => {
const { transition, duration } = merge({}, this.defaults, clip)
let { layers } = clip

if (layers && !Array.isArray(layers)) layers = [layers]; // Allow single layer for convenience
assert(Array.isArray(layers) && layers.length > 0, 'clip.layers must be an array with at least one layer.');
assert(transition == null || typeof transition === 'object', 'Transition must be an object');

layers = layers.map(expandLayerAliases).flat().map(layer => {
assert(layer.type, 'All "layers" must have a type');
return merge({}, this.defaults.layer ?? {}, this.defaults.layerType?.[layer.type] ?? {}, layer)
});

return { transition, duration, layers };
});

// Testing options:
this.verbose = input.verbose ?? false
this.enableFfmpegLog = input.enableFfmpegLog ?? this.verbose
this.logTimes = input.logTimes ?? false
this.keepTmp = input.keepTmp ?? false
this.fast = input.fast ?? false

this.defaults = input.defaults ?? {}
this.ffmpegPath = input.ffmpegPath ?? 'ffmpeg'
this.ffprobePath = input.ffprobePath ?? 'ffprobe'

this.tmpDir = join(this.outDir, `editly-tmp-${nanoid()}`);
}

get outDir() {
return dirname(this.outPath);
}

get isGif() {
return this.outPath.toLowerCase().endsWith('.gif');
}
}
7 changes: 1 addition & 6 deletions src/ffmpeg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fsExtra from 'fs-extra';
import { execa, type Options } from 'execa';
import assert from 'assert';
import { compareVersions } from 'compare-versions';
import { FfmpegConfig } from './configuration.js';

export type Stream = {
codec_type: string;
Expand All @@ -17,12 +18,6 @@ export type Stream = {
}[];
};

export type FfmpegConfig = {
ffmpegPath?: string;
ffprobePath?: string;
enableFfmpegLog?: boolean;
}

const config: Required<FfmpegConfig> = {
ffmpegPath: 'ffmpeg',
ffprobePath: 'ffprobe',
Expand Down
2 changes: 1 addition & 1 deletion src/frameSource.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pMap from 'p-map';

import { rgbaToFabricImage, createFabricCanvas, renderFabricCanvas } from './sources/fabric.js';
import type { DebugOptions } from './types.js';
import type { DebugOptions } from './configuration.js';
import type { ProcessedClip } from './parseConfig.js';
import { createLayerSource } from './sources/index.js';

Expand Down
Loading

0 comments on commit dc300c8

Please sign in to comment.