#!/usr/bin/zx const { extname } = require("path"); const args = process.argv.slice(-2); const video = args[0]; const labels = args[1]; const ext = extname(video); const FREQ = 48000; const FRAMERATE = parseFloat( (await $`mediainfo --Output="Video;%FrameRate%" ${video}`).stdout ); const BITRATE = parseFloat( (await $`mediainfo --Inform="Video;%BitRate/String%" ${video}`).stdout ); const labels_contents = (await $`awk '{print $3, $4, $5}' < ${labels}`).stdout; const cuts = labels_contents .split(os.EOL) .slice(0, -1) .map((e) => e .split(" ") .slice(1) .map((x) => parseFloat(x)) ) .map(([samples, start]) => ({ duration: samples / FREQ, start, end: start + samples / FREQ, })); console.log(cuts); let all_frames = []; const frame_count = parseInt( (await $`mediainfo --Output="Video;%FrameCount%" ${video}`).stdout ); for (let i = 0; i < frame_count; i++) { all_frames.push(i); } console.log(all_frames); for (let cut of cuts) { all_frames = [ ...all_frames.slice(0, Math.round(cut.start * FRAMERATE)), ...all_frames.slice(Math.round(cut.end * FRAMERATE)), ]; } console.log(all_frames); const pieces = []; let current_piece = { start: all_frames[0] }; let predicted_frame_number = 0; // skipping the first one, as we're doing lookback for (let i = 1; i < all_frames.length; i++) { if (all_frames[i] != all_frames[i - 1] + 1) { current_piece.end = all_frames[i - 1]; pieces.push(current_piece); current_piece = { start: all_frames[i] }; } } current_piece.end = all_frames[all_frames.length - 1]; pieces.push(current_piece); // this approach is using a filter, so it requires reencoding the entire video, but lets us be very precise: // const filter = pieces // .map( // ({ start, end }) => // `between(t,${(start / FRAMERATE).toFixed(5)},${(end / FRAMERATE).toFixed( // 5 // )})` // ) // .join("+"); // await $`ffmpeg -i ${video} \ // -vf ${`select='${filter}',setpts=N/FRAME_RATE/TB`} \ // -af ${`aselect='${filter}',asetpts=N/SR/TB`} \ // ${video + ".cut.mp4"}`; // let ffmpeg_args = []; // const parts = []; // for (let i in pieces) { // const piece = pieces[i]; // const output_filename = video + "-" + i + ".mp4"; // ffmpeg_args = [ // ...ffmpeg_args, // "-ss", // (piece.start / FRAMERATE).toFixed(5), // "-t", // ((piece.end - piece.start) / FRAMERATE).toFixed(5), // "-c", // "copy", // output_filename, // ]; // parts.push(output_filename); // } // console.log(ffmpeg_args); // await $`ffmpeg -i ${video} ${ffmpeg_args}`; // await $`echo ${parts.map((s) => `file ${s}`).join("\n")} > /tmp/vdlist.txt`; // await $`ffmpeg -f concat -safe 0 -i /tmp/vdlist.txt -c copy -y ${ // video + "final.mp4" // } `; function range(n) { return [...Array(n).keys()]; } for (let piece of pieces) { console.log(piece); } const ns = range(pieces.length); const sample_rate = parseInt( ( await $`ffprobe ${video} 2>&1 | grep 'Audio:' | grep --only-matching --extended-regexp '[0-9]+ Hz' | sed 's/ Hz//'` ).stdout ); // in Hz const samples_per_frame = sample_rate / FRAMERATE; function generateSplit(pieces, prefix = "") { return `[0] ${prefix}split=${pieces.length} ${ns .map((n) => `[copy${n}${prefix}]`) .join("")}`; } function generateFilter(pieces, prefix = "") { return pieces .map( (piece, n) => `[copy${n}${prefix}]${prefix}trim=start_frame=${piece.start}:end_frame=${piece.end},setpts=PTS-STARTPTS [copy${n}t${prefix}]` ) .join("; "); } function generateAudioFilter(pieces) { return pieces .map( (piece, n) => `[copy${n}a]atrim=start_sample=${ piece.start * samples_per_frame }:end_sample=${ (piece.end + 1) * samples_per_frame },asetpts=PTS-STARTPTS [copy${n}ta]` ) .join("; "); } // version with audio, that doesn't work yet: // const filter = `${generateSplit(pieces)}; ${generateSplit( // pieces, // "a" // )}; ${generateFilter(pieces)}; ${generateAudioFilter(pieces)}; ${ns // .map((n) => `[copy${n}t]`) // .join("")} concat=n=${pieces.length} [out_video]; ${ns // .map((n) => `[copy${n}ta]`) // .join("")} concat=n=${pieces.length} [out_audio]`; // video-only version const audio_filter = `${generateSplit(pieces, "a")}; ${generateAudioFilter( pieces )};`; const filter = `${generateSplit(pieces)}; ${generateFilter(pieces)}; ${audio_filter} ${ns.map((n) => `[copy${n}t][copy${n}ta]`).join(" ")} concat=n=${pieces.length}:v=1:a=1 [out_video] [out_audio]`; await $`ffmpeg -i ${video} -b:v ${BITRATE}M -filter_complex ${filter} -map '[out_video]' -map '[out_audio]' ${`${video}.cut-complex.mp4`}`;