Last active
December 17, 2023 05:31
-
-
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).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage
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