Rotate Video

It is frustrating to have a video vertically recorded. What should be an easy fix is often more work then it should be with standard video editing tools. This recipe makes this task easy and fast. It fixes the rotation angle of a vertical or upside down recorded video.

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

For Windows & macOS

I started recording the video by holding the phone wrong. It was an easy fix by rotating the video, very convenient.

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 Sept 2023)

header={
  "recipe_version": "1.12",
  "title": "Rotate video",
  "description": "Fix the rotation angle of a vertical or upside down video",
  "category": "video",
  "chef": "BeatRig",
  "spice": "BQ==:G+Hy36kJcnQQYYavb/JV2Il6eCYr6h4hRJHCF9rRbhX5BcazL6cuTgUMdz0xA50VWA5PmmV0Q7aWkiUtNFtbk991l+t0e8IBXMReMIT1GNjnZZDv7n2YRAo0xcSQbY0ixjG6LLH4wGKnspKAoA/v69iLn6N2IH0XzIZwTGdHSo4=",
  "palette": "Blueberry Slate",
  "flavour": "e2cFtjUx7qy0fKB6fIXQnfo6wJp78m3l/PmWse3Dk47onMX1gy5Sgo1oXjczO59vegK6cHRZ4oNA7L9XaAAgd6UgefcXGbR3IvwUtgQzACShPIzfoNontWpSE/sXa9FhLrzk8dMA2gmErLOKzIFpsAJJ0cN0cbrOkhWxYj9RH2o=",
  "time": 1694462067,
  "core_version": "0.5.7",
  "magnetron_version": "1.0.261",
  "type": "default",
  "os": "windows,macOS",
  "functions": "main,onConfig,onAbout",
  "dependencies": "ffprobe,ffmpeg",
  "uuid": "7f35e5e5cf164b1887658aa8abb3701b",
  "instructions": "Drop a video file and press play to create a rotated copy",
  "tags": "video,rotate,vvs,fix"
};

//----------------------------------------------------------------------------
var config = {
    "totalsec": 0,
};

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.totalsec * 100);        
        setProgress(progress);
        setMainMessage(progress + "%");
    }

    return {
        "terminate": isCanceled()
    };
}

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);
        
    echo("dur:" + config.totalsec);
    return dur_tc;
}

function setupArgs(infile)
{
    var args1 = ["-y", "-i", infile];

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

    args1.push("-filter_complex");

    var filterArg="";
    filterArg = "[0]" + config.transpose + "[out1]";
    if (gvar.isWindows) // needs quotes
        filterArg = "\"" + filterArg + "\"";        
        
    args1.push(filterArg);
    echo("filter:" + filterArg);

    args1.push("-map");
    args1.push("[out1]");
    args1.push("-map");
    args1.push("0:a?");

    args1.push(config.outfile);

    return args1;
}

//----------------------------------------------------------------------------
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 transpose = 0;
    var options = ["Clockwise","Counter-clockwise","Upside-down"];
    var files = getFiles();
    var numFiles = files.length;
    var infile = "";
    config.tag = "-rotated";
    config.transposetype = options[0];

    for (i = 0; i < Math.max(numFiles, 1) && isCanceled() == false; i++)
    {
        setProgress(100. / numFiles * i);
                        
        infile = numFiles > 0 ? files[i].path : "";        
        var infileinfo = getPathInfo(numFiles > 0 ? infile : folders.output);
        var ouputFolderPath = numFiles > 0 ? infileinfo.folder : folders.output;

        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, 
                }, 
            },
            "transposetype" : {
                "type" : "combobox",
                "default" : config.transposetype,
                "items" : options,
                "label" : "Select rotation:",
                "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" : "Rotate",
                "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);

        if (r.okay == 1)
        {
            infile = r.inputfile;
            config.tag = r.tag;
            config.transposetype = r.transposetype;
            var pathinfo = getPathInfo(infile);

            echo(infile);
            getFileProps(infile); // calc the duration in sec                
            echo("file size:" + bytesToDescription(filesize));

            if (config.totalsec <= 0.)
            {
                setMainMessage("cancelled");
                abort("file is empty\n" + pathinfo.basename);
            }

            /* 0 = 90CounterCLockwise and Vertical Flip (default)
               1 = 90Clockwise
               2 = 90CounterClockwise
               3 = 90Clockwise and Vertical Flip*/
            if (r.transposetype == options[0])
            {
                    config.transpose = "transpose=1";
                    config.tranform_tag = "-C";//"Clockwise";
            }
            else if (r.transposetype == options[1])
            {
                    config.transpose = "transpose=2";
                    config.tranform_tag = "-CC";//"Counter-clockwise";
            }
            else if (r.transposetype == options[2]) // 180
            {
                    config.transpose = "transpose=2[temp];[temp]transpose=2";
                    config.tranform_tag = "-UD";//"Upside-down";
            }

            if (gvar.isWindows)
                    config.transpose = "\"" + config.transpose + "\"";

            config.outfile = r.outputfolder + gvar.pss + pathinfo.basename + r.tag + config.tranform_tag + "." + pathinfo.ext;
            if (config.outfile.length > 0)
            {
                var msg = ("busy please wait");
                setMainMessage(msg);
                echo(msg);
                
                var args1 = setupArgs(infile);
                echo(cmd("ffmpeg", args1)); // perform cmd

                newfilesize = getFileBytes(config.outfile);
                echo("new file size:" + bytesToDescription(newfilesize));
                if (newfilesize <= 0)
                {
                    setMainMessage("can't encode this file");
                    abort("can't encode this file");
                }

                if(isCanceled())
                {
                    setMainMessage("cancelled");
                    abort("cancelled");
                }
            }            
            else
            {
                setMainMessage("cancelled");
                abort("cancelled");
            }
        }
        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 onAbout()
{
    dialog(header.title, header.description + "\n\nby " + header.chef, "i" );
}

function onConfig()
{
    dialog(header.title, "No config needed. This recipe will convert video files from the file list.");
}

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