Turbo Trim video
Making a quick clip of a video can be very time consuming. This recipe will take a video and directly copy the clip without reencoding. Because encoded videos use key frames, the start time will be corrected to the nearest leading keyframe (usually within 10 seconds). This will prevent a blank black screens at the start of the video clip,
Download the recipe from the extended collection here:
https://magnetron.app/recipes/turbo-trim-video
For Windows & macOS
![quote](https://magnetron.dev/media/pages/blog/turbo-trim-video/0658beb953-1712920495/turbo-trim-video-quote-960x.png)
You can run this recipe with a magnetron.app license key.
With magnetron.dev you can also use the source to create your own custom processes.
Take a closer look at the complete source below:
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<foundKeyframes.length; j++)
{
if(parseFloat(foundKeyframes[j]) < (ss - ss_margin))
ss_keyframe = parseFloat(foundKeyframes[j]) + ss_margin - (0.15); // ss selection needs to be just in front of the keyframe
else break;
}
setMainMessage("Trimming to key frame");
var args_margin = ["-y", "-avoid_negative_ts", "make_zero", "-i", inputpath];
// set start time
if (ss_keyframe > 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) ];
}