header={ "recipe_version": "1.24", "title": "Turbo trim video", "description": "Trims a video by cutting the start and/or end of the file, using keyframes without re-encoding to keep the original quality", "tags": "trim,video,fast,cut,start,end", "chef": "BeatRig", "dependencies": "ffprobe,ffmpeg", "spice": "BQ==:G+Hy36kJcnQQYYavb/JV2Il6eCYr6h4hRJHCF9rRbhX5BcazL6cuTgUMdz0xA50VWA5PmmV0Q7aWkiUtNFtbk991l+t0e8IBXMReMIT1GNjnZZDv7n2YRAo0xcSQbY0ixjG6LLH4wGKnspKAoA/v69iLn6N2IH0XzIZwTGdHSo4=", "flavour": "8ZWaU2WsLOoaTkkKXiMkaJJkKsQ5LVza6+GXflPe05PMp3BYbeXR8EB81BowiXh9U0u55TYUXrRdr1P7lae8Cn46+DtrzDb55HTpNTETlUZBRCZQ/IJbWTGroH26VWc5c251grDMqyFt1N1USA4f+L4BtqBi1aH8PJ8w2NiznEI=", "time": 1697553485, "core_version": "0.6.1", "magnetron_version": "1.0.275", "functions": "main,onAbout", "uuid": "30a3a4b9284847bdb3a26963fa1de858", "instructions": "Drop a video file and run. The output will be placed next to the original.", "type": "default", "os": "windows,macOS", "palette": "Blueberry Slate" }; var config = { "slider_in": 0, "slider_out": 0, "sync_end_to_keyframe":false, "totalsec": 0, "fps": 25 }; function onAbout() { launchInBrowser("https://magnetron.dev/lab/"); } function CheckDependencies() { // check the listed depenancies from the header if (header.dependencies.length > 0) { dependencies = stringToObject("[\"" + replaceInString(header.dependencies, ",", "\",\"") + "\"]"); for (i = 0; i < dependencies.length; i++) { if (getAllowedApps(dependencies[i]) == '') abort(dependencies[i] + " is not installed!"); } } } function main() { CheckDependencies(header.dependencies); items = getItems(); // this recipe takes 1 file from the filelist var files = getFiles(); if (files.length > 0) { setMainMessage("Getting file info"); // get input path details inputpath = files[0].path; pathinfo = getPathInfo(inputpath); echo(objectToString(pathinfo)); // make unique output path outpath = pathinfo.folder + pathinfo.sep + pathinfo.basename + "-Turbo_trim." + pathinfo.ext; var outcount = 2; while (fileExists(outpath) && !isCanceled()) outpath = pathinfo.folder + pathinfo.sep + pathinfo.basename + "-Turbo_trim-[" + (outcount++) + "]." + pathinfo.ext; outpath_margin = pathinfo.folder + pathinfo.sep + pathinfo.basename + "-margin." + pathinfo.ext; outcount = 2; while (fileExists(outpath) && !isCanceled()) outpath_margin = pathinfo.folder + pathinfo.sep + pathinfo.basename + "-margin-[" + (outcount++) + "]." + pathinfo.ext; // get duration probe_args = ["-i", inputpath, "-show_entries", "format=duration", "-v", "quiet", "-of", "csv=\"p=0\""]; echo("ffprobe " + objectToString(probe_args)); items.totalsec = parseFloat(cmd("ffprobe", probe_args)); echo("duration: " + items.totalsec); config.totalsec = items.totalsec; setMainMessage("Waiting for dialog..."); setProgress(101); if (items.totalsec != config.totalsec) // file changed. reset time { config.seconds_start = 0.; config.seconds_stop = config.totalsec; } else { config.seconds_start = items.seconds_start; config.seconds_stop = items.seconds_stop > 0 ? items.seconds_stop : config.totalsec; } var dialog_width = 500; var time_start = secondsToRelativeTime(config.seconds_start); var time_stop = secondsToRelativeTime(config.seconds_stop); var time_total = secondsToRelativeTime(config.totalsec); var indent_s=60; var form = { "header" : { "type" : "text", "label" : "Turbo trim video file\nnote: start cut will be adjusted to a leading keyframe", "just" : "l", "bounds" : { "x": 10, "h": 40, "w" : dialog_width, }, }, "filename" : { "type" : "text", "label" : pathinfo.filename, "just" : "l", "bounds" : { "x": 10, "w" : dialog_width, }, }, "param_header1" : { "type" : "text", "label" : "Start", "just" : "l", "bounds" : { "w" : indent_s, "h" : 50 }, }, "slider_start_h": { "type": "slider", "style": "incdec", "textpos": "b", "range" : { "min": 0., "max" : time_total[0], "interval" : 1., "decimals" : 0 }, "bounds" : { "x" : indent_s, "y" : -1, "w" : 70, "h" : 50 }, "value": time_start[0] }, "slider_start_h_l" : { "type" : "text", "label" : "h", "bounds" : { "x" : indent_s + 70, "y" : -1, "w" : 30, "h" : 50 }, }, "slider_start_m": { "type": "slider", "style": "incdec", "textpos": "b", "range" : { "min": 0, "max" : (time_total[0] > 0 ? 59 : time_total[1]), "interval" : 1., "decimals" : 0 }, "bounds" : { "x" : indent_s + 70 + 30, "y" : -1, "w" : 70, "h" : 50 }, "value": time_start[1] }, "slider_start_m_l" : { "type" : "text", "label" : "m", "bounds" : { "x" : indent_s + 70 + 30 + 70, "y" : -1, "w" : 30, "h" : 50 }, }, "slider_start_s": { "type": "slider", "style": "incdec", "textpos": "b", "range" : { "min": 0, "max" : (time_total[0] + time_total[1] > 0 ? 59 : time_total[2]), "interval" : 1., "decimals" : 0 }, "bounds" : { "x" : indent_s + 70 + 30 + 70 + 30, "y" : -1, "w" : 70, "h" : 50 }, "value": time_start[2] }, "slider_start_s_l" : { "type" : "text", "label" : "s", "bounds" : { "x" : indent_s + 70 + 30 + 70 + 30 + 70, "y" : -1, "w" : 30, "h" : 50 }, }, "slider_start_ms": { "type": "slider", "style": "incdec", "textpos": "b", "range" : { "min": 0, "max" : 999, "interval" : 1000/config.fps, "decimals" : 0 }, "bounds" : { "x" : indent_s + 70 + 30 + 70 + 30 + 70 + 30, "y" : -1, "w" : 70, "h" : 50 }, "value": time_start[3] }, "slider_start_ms_l" : { "type" : "text", "label" : "ms", "bounds" : { "x" : indent_s + 70 + 30 + 70 + 30 + 70 + 30 + 70, "y" : -1, "w" : 30, "h" : 50 }, }, "param_header1stop" : { "type" : "text", "label" : "Stop", "just" : "l", "bounds" : { "w" : indent_s, "h" : 50 }, }, "slider_stop_h": { "type": "slider", "style": "incdec", "textpos": "b", "range" : { "min": 0., "max" : time_total[0], "interval" : 1., "decimals" : 0 }, "bounds" : { "x" : indent_s, "y" : -1, "w" : 70, "h" : 50 }, "value": time_stop[0] }, "slider_stop_h_l" : { "type" : "text", "label" : "h", "bounds" : { "x" : indent_s + 70, "y" : -1, "w" : 30, "h" : 50 }, }, "slider_stop_m": { "type": "slider", "style": "incdec", "textpos": "b", "range" : { "min": 0, "max" : (time_total[0] > 0 ? 59 : time_total[1]), "interval" : 1., "decimals" : 0 }, "bounds" : { "x" : indent_s + 70 + 30, "y" : -1, "w" : 70, "h" : 50 }, "value": time_stop[1] }, "slider_stop_m_l" : { "type" : "text", "label" : "m", "bounds" : { "x" : indent_s + 70 + 30 + 70, "y" : -1, "w" : 30, "h" : 50 }, }, "slider_stop_s": { "type": "slider", "style": "incdec", "textpos": "b", "range" : { "min": 0, "max" : (time_total[0] + time_total[1] > 0 ? 59 : time_total[2]), "interval" : 1., "decimals" : 0 }, "bounds" : { "x" : indent_s + 70 + 30 + 70 + 30, "y" : -1, "w" : 70, "h" : 50 }, "value": time_stop[2] }, "slider_stop_s_l" : { "type" : "text", "label" : "s", "bounds" : { "x" : indent_s + 70 + 30 + 70 + 30 + 70, "y" : -1, "w" : 30, "h" : 50 }, }, "slider_stop_ms": { "type": "slider", "style": "incdec", "textpos": "b", "range" : { "min": 0, "max" : 999, "interval" : 1000/config.fps, "decimals" : 0 }, "bounds" : { "x" : indent_s + 70 + 30 + 70 + 30 + 70 + 30, "y" : -1, "w" : 70, "h" : 50 }, "value": time_stop[3] }, "slider_stop_ms_l" : { "type" : "text", "label" : "ms", "bounds" : { "x" : indent_s + 70 + 30 + 70 + 30 + 70 + 30 + 70, "y" : -1, "w" : 30, "h" : 50 }, }, "okay" : { "type" : "button", "label" : "Turbo trim", "bounds" : { "w" : dialog_width / 2 - 10, }, "returns" : 1 }, "cancel" : { "type" : "button", "label" : "cancel", "bounds" : { "y": -1, "x": dialog_width / 2 + 10, "w" : dialog_width / 2 - 10 }, "returns" : 0 }, }; // open dialog var r = dialog(form); echo(objectToString(r)); // values captured by callback // save state items.seconds_start = config.seconds_start; items.seconds_stop = config.seconds_stop; items.totalsec = config.totalsec; setItems(items); echo("cutting start: " + items.seconds_start + " secs"); echo("cutting end: " + items.seconds_stop + " secs"); var margin_size = 10.; if (r.okay) { if(pathinfo.isfile) { { setMainMessage("Trimming file"); var args = ["-y", "-avoid_negative_ts", "make_zero", "-i", inputpath]; if (items.seconds_start > 0) { ss = items.seconds_start; ss_margin = Math.max(ss - margin_size, 0.); // set start time args.push("-ss"); args.push(ss_margin); } else { ss = 0.; ss_margin = 0.; } tt = items.seconds_stop; // set end time. first trimmed with margin to ensure we have a leading keyframe trimmed_length = Math.min(tt - ss_margin, margin_size * 2.); args.push("-t"); args.push(trimmed_length); // copy settings audio video args.push("-c:v"); args.push("copy"); args.push("-c:a"); args.push("copy"); args.push(outpath_margin); // execute cmd echo(objectToString(args)); echo(cmd("ffmpeg", args)); } setMainMessage("Getting keyframes"); // within the first get key frames and adjust start time to keyframe before start time cmd_output = (cmd("ffprobe", ["-v", "error", "-select_streams", "v:0", "-show_entries", "frame=pts_time,key_frame,pkt_pts_time", "-of", "csv=s=x:p=0", outpath_margin])); foundKeyframes = searchRegEx(cmd_output, "^1x([0-9]+\\.[0-9]+)x?", "i", -1); // filters all instances of 'x' marked key frames echo(objectToString(foundKeyframes)); ss_keyframe = 0.; for (j=0; j 0.) { args_margin.push("-ss"); args_margin.push(ss_keyframe); } trimmed_length_margin = tt - ss_keyframe; args_margin.push("-t"); args_margin.push(trimmed_length_margin); // copy settings audio video args_margin.push("-c:v"); args_margin.push("copy"); args_margin.push("-c:a"); args_margin.push("copy"); args_margin.push(outpath); // execute cmd echo(objectToString(args_margin)); echo(cmd("ffmpeg", args_margin)); deleteFile(outpath_margin); if (fileExists(outpath)) revealPath(outpath); } else abort("drop a file to Turbo trim"); } setProgress(100); setMainMessage(""); } else setMainMessage("First drop a file to Turbo trim !"); } function timecodeToText(seconds) { // hh:mm:ss:mls hours = Math.floor(seconds / 3600); minutes = Math.floor((seconds % 3600) / 60); remainingSeconds = seconds % 60; milliseconds = Math.floor((remainingSeconds - Math.floor(remainingSeconds)) * 1000); return paddedLeftString(parseInt(hours), "0", 2) + ":" + paddedLeftString(parseInt(minutes) % 60, "0", 2) + ":" + paddedLeftString(parseInt(remainingSeconds), "0", 2) + ":" + paddedLeftString(parseInt(milliseconds), "0", 3); } function calculateSeconds(dur_tc) { var hours = (dur_tc.substring(0, 2)); var minutes = (dur_tc.substring(3, 3+2)); var seconds = (dur_tc.substring(6, 6+2)); if (hours.substring(0, 1) == "0") hours = hours.substring(1,2); if (minutes.substring(0, 1) == "0") minutes = minutes.substring(1,2); if (seconds.substring(0, 1) == "0") seconds = seconds.substring(1,2); hours = parseInt(hours); minutes = parseInt(minutes); seconds = parseInt(seconds); totalsec_ = (hours * 60 * 60) + (minutes * 60) + seconds; return totalsec_; } function cmd_callback(cmd_name, cmd_output) { return { "terminate": isCanceled(), "input": "" }; } function dialog_callback(props) { // keep the start and stop times aligned and within the max duration if (props["name"] == "slider_start_h") { config.h_start = props["value"]; updateSlidersStart(); } if (props["name"] == "slider_start_m") { config.m_start = props["value"]; updateSlidersStart(); } if (props["name"] == "slider_start_s") { config.s_start = props["value"]; updateSlidersStart(); } if (props["name"] == "slider_start_ms") { config.ms_start = props["value"]; updateSlidersStart(); } if (props["name"] == "slider_stop_h") { config.h_stop = props["value"]; updateSlidersStop(); } if (props["name"] == "slider_stop_m") { config.m_stop = props["value"]; updateSlidersStop(); } if (props["name"] == "slider_stop_s") { config.s_stop = props["value"]; updateSlidersStop(); } if (props["name"] == "slider_stop_ms") { config.ms_stop = props["value"]; updateSlidersStop(); } } function updateSlidersStart() { config.seconds_start = timeToSeconds(config.h_start, config.m_start, config.s_start, config.ms_start); config.seconds_start = Math.min(Math.max(config.seconds_start, 0), config.totalsec); time_start = secondsToRelativeTime(config.seconds_start); config.h_start = time_start[0]; config.m_start = time_start[1]; config.s_start = time_start[2]; config.ms_start = time_start[3]; if (config.seconds_start > config.seconds_stop) { config.seconds_stop = config.seconds_start; config.h_stop = time_start[0]; config.m_stop = time_start[1]; config.s_stop = time_start[2]; config.ms_stop = time_start[3]; dialog({ "slider_start_h" : { "value" : config.h_start }, "slider_start_m" : { "value" : config.m_start }, "slider_start_s" : { "value" : config.s_start }, "slider_start_ms" : { "value" : config.ms_start }, "slider_stop_h" : { "value" : config.h_stop }, "slider_stop_m" : { "value" : config.m_stop }, "slider_stop_s" : { "value" : config.s_stop }, "slider_stop_ms" : { "value" : config.ms_stop }, }); } else { dialog({ "slider_start_h" : { "value" : config.h_start }, "slider_start_m" : { "value" : config.m_start }, "slider_start_s" : { "value" : config.s_start }, "slider_start_ms" : { "value" : config.ms_start }, }); } } function updateSlidersStop() { config.seconds_stop = timeToSeconds(config.h_stop, config.m_stop, config.s_stop, config.ms_stop); config.seconds_stop = Math.min(Math.max(config.seconds_stop, 0), config.totalsec); time_stop = secondsToRelativeTime(config.seconds_stop); config.h_stop = time_stop[0]; config.m_stop = time_stop[1]; config.s_stop = time_stop[2]; config.ms_stop = time_stop[3]; if (config.seconds_stop < config.seconds_start) { config.seconds_start = config.seconds_stop; config.h_start = time_stop[0]; config.m_start = time_stop[1]; config.s_start = time_stop[2]; config.ms_start = time_stop[3]; dialog({ "slider_stop_h" : { "value" : config.h_stop }, "slider_stop_m" : { "value" : config.m_stop }, "slider_stop_s" : { "value" : config.s_stop }, "slider_stop_ms" : { "value" : config.ms_stop }, "slider_start_h" : { "value" : config.h_start }, "slider_start_m" : { "value" : config.m_start }, "slider_start_s" : { "value" : config.s_start }, "slider_start_ms" : { "value" : config.ms_start }, }); } else { dialog({ "slider_stop_h" : { "value" : config.h_stop }, "slider_stop_m" : { "value" : config.m_stop }, "slider_stop_s" : { "value" : config.s_stop }, "slider_stop_ms" : { "value" : config.ms_stop }, }); } } function timeToSeconds(hours, minutes, seconds, ms) { if (hours < 0 || minutes < 0 || seconds < 0 || ms < 0) { return 0; // Invalid input, return 0 seconds } return (hours * 3600 + minutes * 60 + seconds) + (ms / 1000.); } function secondsToRelativeTime(seconds) { if (seconds <= 0) { return [0,0,0,0]; } hours = Math.floor(seconds / 3600); minutes = Math.floor((seconds % 3600) / 60); remainingSeconds = seconds % 60; ms = (seconds - Math.floor(seconds)) * 1000.; return [ parseInt(hours), parseInt(minutes), parseInt(remainingSeconds), parseInt(ms) ]; }