Skip to content

Commit

Permalink
add multi cut modes (separate, merge) (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
f0e committed Jan 17, 2024
1 parent f02b726 commit fcda027
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 33 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ Note: mpv-cut should have its own folder inside your scripts folder. (`scripts/m

`script-opts/mpv-cut.conf`:

- `output_dir` - The output directory for clips, can be relative or absolute. Defaults to `.`, which will place clips in the same directory as the original video.
- `output_dir` - The output directory for cuts, can be relative or absolute.
- Default value: `.` (will place cuts in the same directory as the original video)
- `multi_cut_mode` - The mode for handling multiple cuts for a single video. Options:
- `separate`: create separate cut files (default)
- `join`: merge cut files into a single cut.

## usage

Expand Down
3 changes: 2 additions & 1 deletion script-opts/mpv-cut.conf
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
output_dir=.
output_dir=.
multi_cut_mode=separate
9 changes: 6 additions & 3 deletions scripts/mpv-cut/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ mp.options = require 'mp.options'
MAKE_CUTS_SCRIPT_PATH = mp.utils.join_path(mp.get_script_directory(), "make_cuts")

options = {
output_dir = "."
output_dir = ".",
multi_cut_mode = "separate"
}

mp.options.read_options(options, "mpv-cut")
Expand All @@ -25,6 +26,7 @@ function cut_render()
end

local cuts_json = mp.utils.format_json(cuts)
local options_json = mp.utils.format_json(options)

local inpath = mp.get_property("path")
local filename = mp.get_property("filename")
Expand All @@ -34,15 +36,16 @@ function cut_render()
log("Rendering...")
print("making cut")

local args = { "node", MAKE_CUTS_SCRIPT_PATH, indir, options.output_dir, filename, cuts_json }
local args = { "node", MAKE_CUTS_SCRIPT_PATH,
indir, options_json, filename, cuts_json }

res, err = mp.command_native({
name = "subprocess",
playback_only = false,
args = args,
})

if res.status == 0 then
if res and res.status == 0 then
log("Rendered cuts")
else
log("Failed to render cuts")
Expand Down
117 changes: 89 additions & 28 deletions scripts/mpv-cut/make_cuts.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const isSubdirectory = (parent, child) => {
return relative && !relative.startsWith('..') && !path.isAbsolute(relative);
};

const ffmpegEscapeFilepath = (path) =>
path.replaceAll('\\', '\\\\').replaceAll("'", "'\\''");

function quit(s) {
console.log('' + red + s + ', quitting.' + plain + '\n');
return process.exit(1);
Expand Down Expand Up @@ -53,14 +56,86 @@ async function transferTimestamps(inPath, outPath, offset = 0) {
}
}

async function ffmpeg(args) {
const cmd = 'ffmpeg';
const baseArgs = [
// hide output
'-nostdin',
'-loglevel',
'error',
// overwrite existing files
'-y',
];

const fullArgs = baseArgs.concat(args);

const cmdStr = '' + cmd + ' ' + fullArgs.join(' ');
console.log('' + purple + cmdStr + plain + '\n');

child_process.spawnSync(cmd, fullArgs, { stdio: 'inherit' });
}

async function renderCut(inpath, outpath, start, duration) {
const args = [
// seek to start before loading file (faster) https://trac.ffmpeg.org/wiki/Seeking#Inputseeking
'-ss',
start,
'-t',
duration,
'-i',
inpath,
// don't re-encode
'-c',
'copy',
// shift timestamps so they start at 0
'-avoid_negative_ts',
'make_zero',
outpath,
];

await ffmpeg(args);

await transferTimestamps(inpath, outpath);
}

async function mergeCuts(tempPath, filepaths, outpath) {
// i hate that you have to do a separate command and render each cut separately first, i tried using
// filter_complex for merging with multiple inputs but it wouldn't let me. todo: look into this further

const mergeFile = path.join(tempPath, 'merging.txt');
await fs.promises.writeFile(
mergeFile,
filepaths.map((path) => `file '${ffmpegEscapeFilepath(path)}`).join('\n')
);

await ffmpeg([
'-f',
'concat',
'-safe',
0,
'-i',
mergeFile,
'-c',
'copy',
outpath,
]);

await fs.promises.unlink(mergeFile);

for (const path of filepaths) {
await fs.promises.unlink(path);
}
}

async function main() {
const argv = process.argv.slice(2);

const [indir, outdir_raw, filename, cutsStr] = argv;
const [indir, optionsStr, filename, cutsStr] = argv;

if (!isDir(indir)) quit('Input directory is invalid');

const outdir = path.resolve(indir, outdir_raw);
const options = JSON.parse(optionsStr);
const outdir = path.resolve(indir, options.output_dir);

if (!isDir(outdir)) {
if (!isSubdirectory(indir, outdir)) quit('Output directory is invalid');
Expand All @@ -72,10 +147,13 @@ async function main() {
const cutsMap = JSON.parse(cutsStr);
const cuts = Object.values(cutsMap).sort((a, b) => a.start - b.start);

const { name: filename_noext, ext: ext } = path.parse(filename);

const outpaths = [];

for (const [i, cut] of cuts.entries()) {
if (!('end' in cut)) continue;

const { name: filename_noext, ext: ext } = path.parse(filename);
const duration = parseFloat(cut.end) - parseFloat(cut.start);

const cutName =
Expand All @@ -91,39 +169,22 @@ async function main() {
const inpath = path.join(indir, filename);
const outpath = path.join(outdir, cutName);

const cmd = 'ffmpeg';
const args = [
'-nostdin',
'-y',
'-loglevel',
'error',
'-ss',
cut.start,
'-t',
duration,
'-i',
inpath,
'-c',
'copy',
'-map',
'0',
'-avoid_negative_ts',
'make_zero',
outpath,
];

const progress = '(' + (i + 1) + '/' + cuts.length + ')';
const cmdStr = '' + cmd + ' ' + args.join(' ');

console.log(
'' + green + progress + plain + ' ' + inpath + ' ' + green + '->' + plain
);
console.log('' + outpath + '\n');
console.log('' + purple + cmdStr + plain + '\n');

child_process.spawnSync(cmd, args, { stdio: 'inherit' });
await renderCut(inpath, outpath, cut.start, duration);
outpaths.push(outpath);
}

if (outpaths.length > 1 && options.multi_cut_mode == 'merge') {
const cutName = `(${outpaths.length} merged cuts) ` + filename;
const outpath = path.join(outdir, cutName);

await transferTimestamps(inpath, outpath);
await mergeCuts(indir, outpaths, outpath);
}

return console.log('Done.\n');
Expand Down

0 comments on commit fcda027

Please sign in to comment.