189 lines
4.7 KiB
JavaScript
Executable File
189 lines
4.7 KiB
JavaScript
Executable File
#!/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`}`;
|