Instagram video clip

Instagram has length and size restrictions for their videos. This recipe crops videos into the Instagram's square format and cuts them into 60-second reels or 15-second stories.

Download this recipe from the extended collection here:
https://magnetron.app/recipes/instagram-video-clip

For Windows & macOS

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:

the dialog

(added Aug 2023)

header={
  "recipe_version": "1.10",
  "title": "Instagram video",
  "description": "Convert a video to the instagram share screen ratio and time limit",
  "category": "video",
  "chef": "BeatRig",
  "spice": "BQ==:G+Hy36kJcnQQYYavb/JV2Il6eCYr6h4hRJHCF9rRbhX5BcazL6cuTgUMdz0xA50VWA5PmmV0Q7aWkiUtNFtbk991l+t0e8IBXMReMIT1GNjnZZDv7n2YRAo0xcSQbY0ixjG6LLH4wGKnspKAoA/v69iLn6N2IH0XzIZwTGdHSo4=",
  "palette": "Velvet Cake",
  "flavour": "lEM7evufJIDvc6sefJlH8F+Uqr5hrIMUpiZ5sJ3x5Mp/ByprgaufRakE0HAm5qH7suEok0tlL24HM8sNWbaV1nxTR4hxkveG7qisQ+yj1ZZTJeB/UccN19lqiMNCDDT1Wp0x8yGcr9BJUzysp05kY1ypS85rT0yVcO1WPViyoiE=",
  "time": 1694382987,
  "core_version": "0.5.7",
  "magnetron_version": "1.0.261",
  "type": "default",
  "os": "windows,macOS",
  "cmds": "ffprobe,ffmpeg",
  "functions": "main",
  "dependencies": "ffprobe,ffmpeg",
  "tags": "video,instagram,square",
  "uuid": "d01828bc987346378739461d75cae89a",
  "instructions": "Drop file(s) to crop them to a square aspect ratio and cut to a stories or reels length"
};

//----------------------------------------------------------------------------
var config = {
    "scale": 1,
    "framerate": 0,
    "makeMono": false,
    "crf": 40,
    "parts": 1,
    "makeMono": true,
    "partItt": 0,

    "cutLength": 0,
    "sizeRatio": 1,
    "totalsec": 0,
    "fps": 0,
    
    "maxItt": 4,
    "maxSize": 4 * 1024 * 1024 * 1024, // to 4 gb
    "mincrf": 20,
    "maxcrf": 55,
    "minframerate": 10,
    "maxframerate": 25,
    "minScale": 1,
    "maxScale": 4,
};

//----------------------------------------------------------------------------
function cmd_callback(cmd_name, cmd_output)
{
    if(cmd_name == "ffmpeg" && cmd_output.length > 0)
    {
        var progress = calculateSeconds(findSubString(cmd_output, "time=", " bitrate"));
        progress = Math.round(progress / config.cutLength /*config.totalsec*/ * 100);        
        setProgress(progress);
    }

    return {
        "terminate": isCanceled(),
        "input": "" // send console input
    };
}

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 getFileProps(infile)
{
    var probeoutput = cmd("ffprobe", [infile]);
    echo(probeoutput);
    var dur_tc = findSubString(probeoutput, "Duration: ", ", s");
    
    config.totalsec = calculateSeconds(dur_tc);
    config.fps = parseInt(findSubString(probeoutput, "kb/s, ", " fps"));
        
    echo("dur:" + config.totalsec);
    echo("fps:" + config.fps);

    return dur_tc;
}

function setupArgs(infile, cutItt)
{
    var args1 = ["-y", "-i", infile];
          
    if (config.parts > 1)
    {
        echo("start time:" + cutItt * config.cutLength);
        args1.push("-ss");
        args1.push(cutItt * config.cutLength);
    }
    if (cutItt < config.parts - 1)
    {
        echo("length:" + config.cutLength);
        args1.push("-t");
        args1.push(config.cutLength);
    }

    args1.push("-vcodec");
    args1.push("libx264"); 
              
    if (config.framerate > 0)
    {
        echo("frame rate:" + config.framerate);
        args1.push("-r");
        args1.push(config.framerate);
    }
                
    echo("compression:" + config.crf);
    args1.push("-crf");
    args1.push(config.crf);
                
    args1.push("-preset");
    args1.push("slow");
                
    args1.push("-c:a");
    args1.push("aac");

    if (config.makeMono)
    {
        echo("mono");
        args1.push("-ac");
        args1.push("1");
    }

    args1.push("-movflags");
    args1.push("+faststart");

    {   
        args1.push("-filter_complex");

        var filterArg="[0]crop=min(iw\\,ih):min(iw\\,ih),format=yuv420p[out1]";
        if (gvar.isWindows) // needs quotes
            filterArg = "\"" + filterArg + "\"";        
        
        args1.push(filterArg);
        echo("filter:" + filterArg);

        // map the inputs
        args1.push("-map");
        args1.push("[out1]");
        args1.push("-map");
        args1.push("0:a?");
    }
    args1.push(config.outfile);

    return args1;
}

//----------------------------------------------------------------------------
function setOutputSettings()
{
    var partRatio = config.sizeRatio / config.parts;

    config.makeMono = (partRatio > 1.5);
    config.crf = config.mincrf;

    // some size ratio based presets
    if (partRatio > 8)
    {
        config.scale = config.maxScale;
        config.framerate = config.minframerate;
    }
    else if (partRatio > 4)
    {
        config.scale = 2;
        config.framerate = Math.min(Math.max(parseInt(config.fps / (partRatio / config.scale)), config.minframerate), config.maxframerate);
    }
    else if (partRatio > 2)
        config.scale = 2;
}

//----------------------------------------------------------------------------
function main()
{
    // check if ffmpeg is installed
    if (fileExists(getAllowedApps("ffmpeg")) == false)
        abort("no ffmpeg installed");    
    if (fileExists(getAllowedApps("ffprobe")) == false)
        abort("no ffprobe installed");    

    setMainMessage("starting");
    setProgress(0);

    var now = getCurrentEpoch(); // secs since 1 jan 1970    
    var files = getFiles();    
    
    config.tag = "-[insta]";

    var numFiles = files.length;
    for (i = 0; i < Math.max(numFiles, 1); i++)
    {
        // base path
        var infile = numFiles > 0 ? files[i].path : "";
        var filesize = getFileBytes(infile);
        var infileinfo = getPathInfo(numFiles > 0 ? infile : folders.output);
        var ouputFolderPath = numFiles > 0 ? infileinfo.folder : folders.output;

        echo(infile);
        echo("file size:" + bytesToDescription(filesize));

        setFileIcon(files[i].path, files[i].index, "HourglassHalf");
        setFileIconColor(files[i].path, files[i].index, "FF5299D3");
        setFileStatus(files[i].path, files[i].index, "busy");
        
        var maxClipLength = 60; // limits 15 sec or 60 sec
        var options = ["15 sec Stories","60 sec Reels"];
        config.videotype = options[1];
 
        var dialog_width = 600;
        form1 = {
            "header" : {
                "type" : "text",
                "label" : "This process will convert videos to instagram spec",
                "just" : "l",
                "bounds" : { 
                    "x": 10, 
                    "y" : 5, 
                    "w" : dialog_width, 
                }, 
            },
            "file1_header" : {
                "type" : "text",
                "label" : "source file",
                "just" : "l",
                "bounds" : { 
                    "w" : 120, 
                }, 
            },
            "inputfile" : {
                "type" : "fileselect",
                "path" : infile,
                "editable" : false,
                "dir" : false,
                "saving" : false,
                "label" : "source file",
                "bounds" : {
                    "x" : 130,
                    "y" : -1, 
                    "w" : dialog_width - 130,
                }, 
            },
            "param_header" : {
                "type" : "text",
                "label" : "Video type",
                "just" : "l",
                "bounds" : { 
                    "w" : 120, 
                }, 
            },
            "maxClipLength" : {
                "type" : "combobox",
                "default" : config.videotype,
                "items" : options,
                "label" : "Select length:",
                "bounds" : {
                    "x" : 130,
                    "y" : -1, 
                    "w" : dialog_width - 130,
                }
            }, 
            "file2_header" : {
                "type" : "text",
                "label" : "output folder",
                "just" : "l",
                "bounds" : { 
                "w" : 120, 
                }, 
            },
            "outputfolder" : {
                "type" : "fileselect",
                "path" : ouputFolderPath,
                "editable" : false,
                "dir" : true,
                "saving" : true,
                "label" : "select output folder",
                "bounds" : {
                    "x" : 130,
                    "y" : -1, 
                    "w" : dialog_width - 130,
                }, 
            },
            "tag" : {
                "type" : "textedit",
                "default" : config.tag,
                "label" : "add file name tag:",
                "bounds" : {
                    "x" : dialog_width/2,
                    "y" : -1,
                    "w" : dialog_width/2
                }
            },
            "okay" : {
                "type" : "button",
                "label" : "Start",
                "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
            },
        };    

        var r = dialog(form1);
        config.cutLength = 60;
        if (r.maxClipLength == options[0])
            config.cutLength = 15;
        config.videotype = r.maxClipLength;
        config.tag = r.tag;
        infile = r.inputfile;
        var pathinfo = getPathInfo(infile);
        var ouputFolderPath = numFiles > 0 ? infileinfo.folder : folders.output;

        if (r.okay == 1)
        {
            getFileProps(infile); // calc the duration in sec
        
            if (config.totalsec <= 0)
                abort("no content found in this file");        

            config.parts = (config.totalsec / config.cutLength);
            if (parseInt(config.parts) < config.parts)
                config.parts = parseInt(config.parts) + 1;
            else config.parts = parseInt(config.parts);

            setOutputSettings(); // compression setup

            config.framecount = config.fps * config.totalsec;
            echo("framecount:" + config.framecount); // for progress calculation

            setProgress(101);
            
            var newfilesize = 0;
            config.partItt = 0;
            while (config.partItt < config.parts)
            {
                if (config.parts > 1)
                {
                    config.outfile = r.outputfolder + gvar.pss + pathinfo.basename + r.tag + "-[" + (config.partItt + 1) + "_" + config.parts + "]." + pathinfo.ext;
                    if (fileExists(config.outfile))
                    {
                        var form2 = {
                            "overwrite" : {
                                "type" : "button",
                                "label" : "overwrite",
                                "returns" : 1
                            },
                            "cancel" : {
                                "type" : "button",
                                "label" : "cancel",
                                "returns" : 0
                            }
                        };  
                        if (dialog(header.title, "file already exists\n" + config.outfile, "w", form2).cancel == 1)
                            abort("cancelled");
                    }
                }
                else config.outfile = r.outputfolder + gvar.pss + pathinfo.basename + r.tag + "." + pathinfo.ext;
                
                var msg = ("busy please wait");
                setMainMessage(msg);
                echo(msg);
                
                var args1 = setupArgs(infile, config.partItt);
                echo(cmd("ffmpeg", args1)); // perform cmd

                newfilesize = getFileBytes(config.outfile);
                echo("new file size:" + bytesToDescription(newfilesize));
                if (newfilesize <= 0)
                    config.partItt = config.parts; // stop

                if(isCanceled())
                    abort("cancelled");            

                config.partItt++;
            }
            if (newfilesize <= 0)
		    {
                setFileIcon(files[i].path, files[i].index, "ExclamationTriangle");
                setFileIconColor(files[i].path, files[i].index, "FFb11414");
                setFileStatus(files[i].path, files[i].index, "can't encode this file", "w");
            }
            else
            {
                setFileIcon(files[i].path, files[i].index, "CheckCircleO");
                setFileIconColor(files[i].path, files[i].index, "FF3aac4d");
                setFileStatus(files[i].path, files[i].index, "done");
            }
        }
        else i = numFiles; // end-loop
    }

    var then = getCurrentEpoch(); // secs since 1 jan 1970    
    setMainMessage("done in " + (then - now) + " sec");
    echo(objectToString(config));
    setProgress(100);
}

function dialog_callback(props)
{
    if (props["name"] == "inputfile")
    {
        var info = getPathInfo(props["path"]);
        var newprops = { "outputfolder" : { "path" : info.folder } };
        dialog(newprops);
    }
}