Skip to content

Instantly share code, notes, and snippets.

@malcolmocean
Created January 15, 2023 04:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save malcolmocean/02a8ca40cb30e21db7d42eaee944f468 to your computer and use it in GitHub Desktop.
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
#! /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