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

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) ];
}