Skip to content

Instantly share code, notes, and snippets.

@api-haus
Last active December 17, 2023 05:31
Show Gist options
  • Save api-haus/e4845156845ce07de1744a5fa45db5be to your computer and use it in GitHub Desktop.
Save api-haus/e4845156845ce07de1744a5fa45db5be to your computer and use it in GitHub Desktop.
Make perfectly looping cross-fading video. Using FFmpeg with transparency support (on .webm and .mov).
#!/usr/bin/env zx
// Establish program arguments
const input = argv.i;
const output = argv.o;
const fadeDuration = parseInt(argv.f);
const useTransparency = argv.t;
// Quality options
const bitRate = argv.b;
const constantRateFactor = argv.crf;
// Establish temporary files
const tmpDir = path.join(os.tmpdir(), Math.random().toString(36).slice(2));
await fs.mkdirp(tmpDir);
// Try encoding and then cleanup temporary files even if the process fails
try {
await crossFade(input, output, fadeDuration);
console.log(`Encoded ${output}`)
} finally {
await cleanup();
}
// CrossFade provided `input` file as `output` file with `fadeDuration`
async function crossFade(input, output, fadeDuration) {
// Determine input duration
const duration = await determineDuration(input);
// Construct ffmpeg command
const args = [
`-i "${input}"`,
`-filter_complex '
[0]split[body][pre];
[pre]trim=duration='${fadeDuration}',format=yuva420p,fade=d='${fadeDuration}':alpha=1,setpts=PTS+('${duration - fadeDuration * 2}'/TB)[jt];
[body]trim='${fadeDuration}',format=yuva420p,setpts=PTS-STARTPTS[main];
[main][jt]overlay,format=yuva420p'`,
];
// Fix for .webm with transparency (force codec to VP8)
if (input.endsWith('.webm') && useTransparency) {
args.unshift(`-vcodec libvpx`);
args.push('-auto-alt-ref 0');
}
if (output.endsWith('.mov') && useTransparency) {
args.push(`-vcodec png`);
}
// Set configured quality
if (constantRateFactor) {
args.push(`-crf ${constantRateFactor}`, `-b:v ${bitRate}`);
}
// Avoid escaping args...
$.quote = v => v;
// Run command
await $`ffmpeg -y ${args.join(' ')} "${output}"`;
}
// Determine duration in seconds (as integer)
async function determineDuration(input) {
let duration = await $`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 ${input}`;
return parseInt(duration);
}
// Remove temporary dir with all its files
async function cleanup() {
await fs.rm(tmpDir, {recursive: true});
}
@api-haus
Copy link
Author

api-haus commented Sep 16, 2022

Usage

$ chmod +x cf.mjs
$ ./cf.mjs -i <input> -o <output> -f <fade-duration-seconds> [-b <bitrate>] [--crf <crf-value>] [-t]

You need npm i -g zx (google/zx)

Options

  • -i path to input video
  • -o path to output video
  • -f fade duration as integer, in seconds
  • -t set this flag to enable transparency fix
  • -b set bitrate
  • --crf set CRF

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment