Created
January 15, 2023 04:54
-
-
Save malcolmocean/02a8ca40cb30e21db7d42eaee944f468 to your computer and use it in GitHub Desktop.
node command-line script that uses ffmpeg to cut a video into clips in one command
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 node | |
// created by Malcolm Ocean (malcolmocean.com) mid-2021, published Jan 2023 | |
// CC-BY-SA license | |
// call as fclips.js source.mp4 list.txt | |
// where list.txt contains lines like | |
// 59:36 to 1:00:53 | |
// (millis allowed) | |
const fs = require('fs') | |
const argv = require('minimist')(process.argv.slice(2)) | |
async function execShellCommand(cmd) { | |
console.log(cmd) | |
const exec = require('child_process').exec | |
return new Promise((resolve, reject) => { | |
exec(cmd, (error, stdout, stderr) => { | |
if (error) { | |
console.warn(error || stderr) | |
return reject(error || stderr) | |
} | |
// sometimes stderr even when goes fine | |
resolve(stdout || stderr) | |
}) | |
}) | |
} | |
async function ffmpegClip (inputFile, outputFilename, ss, t) { | |
const copyMaybe = /\.(mp3|m4a|aac|ogg)$/.test(inputFile) ? '-c copy' : '' | |
return execShellCommand(`ffmpeg -ss ${ss} -i '${inputFile}' -t ${t} ${copyMaybe} ${outputFilename}`) | |
} | |
async function ffmpegConcat (inputListFilename, outputFilename) { | |
return execShellCommand(`ffmpeg -f concat -i '${inputListFilename}' -c copy "${outputFilename}"`) | |
} | |
if (!argv._.length) { | |
return console.log(`How to use: | |
fclips media.mp4 list.txt | |
where list.txt has the format | |
0:00 to 10:34 | |
15:29 to 1:02:11 | |
`) | |
} | |
console.log("argv", argv) | |
const list = String(fs.readFileSync(argv._[1])).split('\n').filter(line => !line.startsWith('#')) | |
const randTempName = Math.random().toString('36').replace(/[0-9.]/g, '').substr(0,5) | |
const inputFilename = argv._[0] | |
console.log("inputFilename", inputFilename) | |
const clipListFilename = randTempName + '_list.txt' | |
console.log("clipListFilename", clipListFilename) | |
const ext = inputFilename.replace(/.*\./, '') | |
console.log("ext", ext) | |
function hmsToSeconds (hms) { | |
const millis = /\./.test(hms) ? parseInt(hms.replace(/.*\./, '')) : 0 | |
hms = hms.replace(/\..*/, '') | |
const a = hms.split(':').reverse() | |
return (+a[2] || 0) * 60 * 60 + (+a[1]) * 60 + (+a[0]) + millis/1000 | |
} | |
async function doStuff () { | |
if (argv.nodelete) {console.log('no delete')} | |
let totalTime = 0 | |
let outputFiles = [] | |
for (var n in list) { | |
const line = list[n].trim() | |
if (!line) {continue} | |
if (line.startsWith('#')) {continue} | |
const ss = line.replace(/ +to .*/, '') | |
const to = line.replace(/.* to +/, '') | |
const t = hmsToSeconds(to) - hmsToSeconds(ss) | |
totalTime += t | |
const outputFilename = `${randTempName}_${n}.${ext}` | |
outputFiles.push(outputFilename) | |
await ffmpegClip(inputFilename, outputFilename, ss, t) | |
} | |
console.log('totalTime = ' + totalTime + ' seconds') | |
const clipList = outputFiles.map(x => 'file ' + x).join('\n')+'\n' | |
fs.writeFileSync(clipListFilename, clipList) | |
const finalOutputFilename = `clips from ${inputFilename}` | |
await ffmpegConcat(clipListFilename, finalOutputFilename) | |
if (argv.nodelete) {return} | |
for (var filename of outputFiles) { | |
await execShellCommand(`rm ${filename}`) | |
} | |
await execShellCommand(`rm ${clipListFilename}`) | |
} | |
doStuff().then(() => console.log('complete'), err => console.log('err', err)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment