Skip to content

Instantly share code, notes, and snippets.

@tg44
Last active April 12, 2023 20:16
Show Gist options
  • Save tg44/bf6756c461ec17269264b7fccb314538 to your computer and use it in GitHub Desktop.
Save tg44/bf6756c461ec17269264b7fccb314538 to your computer and use it in GitHub Desktop.
Cheap IP camera video converter

install nodejs 12

run with node app.js cameraRootDir

Almost works with Blitzwolf BW-SHC2 (no audio for me).

Not tested on windows, but probably changing the ffmpeg in the first two function-returns to something like ffmpeg.exe will make this work.

The script will recursively walk on every sudir, group the files ased on parent dir, checks if the parentdir is an epoch_something or not. In the correctly named dirs will convert the *.media files to .mp4 (fileConverterCommand function), and concat them to one video (with fileConcatCommand).

Use it for your own risk!

Ideas are welcome!

ffmpeg installed on my mac with;

brew uninstall ffmpeg
brew tap homebrew-ffmpeg/ffmpeg
brew install amiaopensource/amiaos/decklinksdk
brew install homebrew-ffmpeg/ffmpeg/ffmpeg $(brew options homebrew-ffmpeg/ffmpeg/ffmpeg | grep -vE '\s' | grep -- '--with-' | grep -vi chromaprint | grep -vi game-music-emu | tr '\n' ' ')

Based on this conversation.

const { resolve } = require('path');
const { readdir } = require('fs').promises;
const { unlink } = require('fs').promises;
const path = require('path');
const { exec } = require("child_process");
function fileConverterCommand(file) {
return `ffmpeg -i "${file}" -acodec mp3 "${file}.mp4"`
}
function fileConcatCommand(files, outputFile) {
const inputs = files.join("|")
return `ffmpeg -i "concat:${inputs}" -c copy ${outputFile}`
}
const dir = process.argv[2] || "."
main()
async function main() {
const files = await getFiles(dir);
const wParents = files.map(f => {return {
file: f,
parentDir: path.basename(path.dirname(f))
}})
const onlyTSDirs = wParents.filter(d => splitDirNameToTs(d.parentDir)).map(d => {return {
file: d.file,
fname: path.basename(d.file),
date: splitDirNameToTs(d.parentDir),
}})
const grupped = groupBy(onlyTSDirs, "date")
//console.log(Object.values(grupped))
await mapAllSettled(Object.values(grupped), arr => convertDir(arr), 2)
}
function splitDirNameToTs(dirName) {
try{
return (new Date(dirName.split("_")[0] * 1000))
} catch {
return null;
}
}
async function convertDir(arr) {
//console.log()
//console.log("=========")
//console.log(arr)
const farr = arr.filter(e => {
try{
//console.log(Number.isInteger(e.fname.split(".")[0]))
return !Number.isInteger(e.fname.split(".")[0]) && e.fname!==".info"
} catch (e) {
return false
}
})
//console.log(farr)
farr.sort(function(a, b) {
return Number(a.fname.split(".")[0]) - Number(b.fname.split(".")[0]);
});
//console.log(farr)
try {
await mapAllSettled(farr, async d => {
const out = await execPromise(fileConverterCommand(d.file))
//console.log(out)
console.log(`converted ${d.file}`)
}, 10)
} catch (e) {
console.log(e)
}
const outputFile = dir+"/clip_" + arr[0].date.toISOString().replace("T", "_").replace(".000Z", "") + ".mp4"
await execPromise(fileConcatCommand(farr.map(d => `${d.file}.mp4`), outputFile))
await Promise.all(farr.map(async d => await unlink(`${d.file}.mp4`)))
console.log(`created ${outputFile}`)
}
//https://codereview.stackexchange.com/a/66752/178655
function groupBy(array, keyOrIterator) {
var iterator, key;
// use the function passed in, or create one
if(typeof key !== 'function') {
key = String(keyOrIterator);
iterator = function (item) { return item[key]; };
} else {
iterator = keyOrIterator;
}
return array.reduce(function (memo, item) {
var key = iterator(item);
memo[key] = memo[key] || [];
memo[key].push(item);
return memo;
}, {});
}
//https://stackoverflow.com/a/36960207/2118749
async function execPromise(cmd) {
console.log(cmd)
return new Promise(function(resolve, reject) {
exec(cmd, function(err, stdout) {
if (err) return reject(err);
resolve(stdout);
});
});
}
//https://stackoverflow.com/a/45130990/2118749
async function getFiles(dir) {
const dirents = await readdir(dir, { withFileTypes: true });
const files = await Promise.all(dirents.map((dirent) => {
const res = resolve(dir, dirent.name);
return dirent.isDirectory() ? getFiles(res) : res;
}));
return Array.prototype.concat(...files);
}
//https://codeburst.io/async-map-with-limited-parallelism-in-node-js-2b91bd47af70
const { promisify } = require('util')
const { setImmediate } = require('timers')
const setImmediateP = promisify(setImmediate)
async function mapItem(mapFn, currentValue, index, array) {
try {
await setImmediateP()
return {
status: 'fulfilled',
value: await mapFn(currentValue, index, array)
}
} catch (reason) {
return {
status: 'rejected',
reason
}
}
}
async function worker(id, gen, mapFn, result) {
//console.time(`Worker ${id}`)
for (let [ currentValue, index, array ] of gen) {
//console.time(`Worker ${id} --- index ${index} item ${currentValue}`)
result[index] = await mapItem(mapFn, currentValue, index, array)
//console.timeEnd(`Worker ${id} --- index ${index} item ${currentValue}`)
}
//console.timeEnd(`Worker ${id}`)
}
function* arrayGenerator(array) {
for (let index = 0; index < array.length; index++) {
const currentValue = array[index]
yield [ currentValue, index, array ]
}
}
async function mapAllSettled(arr, mapFn, limit = arr.length) {
const result = []
if (arr.length === 0) {
return result
}
const gen = arrayGenerator(arr)
limit = Math.min(limit, arr.length)
const workers = new Array(limit)
for (let i = 0; i < limit; i++) {
workers.push(worker(i, gen, mapFn, result))
}
//console.log(`Initialized ${limit} workers`)
await Promise.all(workers)
return result
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment