Video to GIF

GIFs are still used, most often as emojis/stickers. This recipe quickly converts a video file in to a GIF file.

For each file dropped in the list a dialog will show for specific settings, more below.

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

For Windows & macOS.

As there is no video preview, get the start and stop time from your media player.

- Enter the start and stop time in the dialog. The dialog will restrict values that are outside of the video duration.
- Frames per sec, lower it for smaller file size. default value matched the input video.
- Loop/repeat the video, 0 is infinite.
- Image pixel width, for convenience the height will be scaled in proportion.
- The output folder, default is the same as the input file parent folder.
- File tag, will be added to the filename.

The recipe uses the FFmpeg 'palettegen' filter for high quality results.

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:

(added Sept 2023)

header={
  "recipe_version": "1.15",
  "title": "Video to GIF",
  "description": "Create quality GIFs from video",
  "category": "video",
  "chef": "BeatRig",
  "spice": "BQ==:G+Hy36kJcnQQYYavb/JV2Il6eCYr6h4hRJHCF9rRbhX5BcazL6cuTgUMdz0xA50VWA5PmmV0Q7aWkiUtNFtbk991l+t0e8IBXMReMIT1GNjnZZDv7n2YRAo0xcSQbY0ixjG6LLH4wGKnspKAoA/v69iLn6N2IH0XzIZwTGdHSo4=",
  "palette": "Razzmic Berry",
  "flavour": "2n3gtSDXrVcfKsKTmkw9guqc2jjrikuQqQ7S9hZezFxknCA3Tms7xsBBgWuEoWv7scc75B4lWyutlAbkbEx6gyvmoCohd/L6AKuNtVVCEu7zA9YRqtIhwCxnlh9Vs6ux6zKXkh90SKmoz3Y3LLED/7effNXI87eUTpW2t95EoWY=",
  "time": 1694721421,
  "core_version": "0.5.7",
  "magnetron_version": "1.0.261",
  "type": "default",
  "os": "windows,macOS",
  "functions": "main,onConfig,onAbout",
  "dependencies": "ffprobe,ffmpeg",
  "tags": "video,convert",
  "uuid": "f9bac831cbdf455bbcf6c6913f598c3d",
  "instructions": "Drop a video file and press play to create a copy as GIF file"
};

//----------------------------------------------------------------------------
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));
    var fracsec = (dur_tc.substring(9, 9+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);
    fracsec = parseFloat("0." + fracsec);

    hours = parseInt(hours);
    minutes = parseInt(minutes);
    seconds = parseInt(seconds);
    
    totalsec = ((hours * 60 * 60) + (minutes * 60) + seconds) + fracsec;
    return totalsec;
}

function getFileProps(infile)
{
    var probeoutput = cmd("ffprobe", [infile]);
    echo(probeoutput);
    var dur_tc = findSubString(probeoutput, "Duration: ", ", s");
    echo("dur_tc: " + dur_tc);
    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 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 = "-[gif]";
    config.fps = 10;
    config.loop = 0;
    config.width = 300;

    var numFiles = files.length;
    if (numFiles <= 0)
        abort("Drop a video file to use first");

    for (i = 0; i < numFiles && isCanceled() == false; i++)
    {
        var infile = files[i].path;
        var pathinfo = getPathInfo(infile);
        var ouputFolderPath = pathinfo.folder;
        echo(infile);

        // calc the duration in sec
        getFileProps(infile);
        
        if (config.totalsec <= 0)
        {
            setMainMessage("no content found in " + infileinfo.filename);
            abort("no content found in " + infileinfo.filename);
        }
        config.slider_start = 0;
        config.slider_stop = config.totalsec;

        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 newfilesize = 0;
        var dialog_width = 500;
        var indent = 150;
        var indent_s = 100;
        config.seconds_start = 0;
        config.seconds_stop = config.totalsec;

        var time = secondsToRelativeTime(config.totalsec);
        var form1 = {
            "header" : {
                "type" : "text",
                "label" : "Convert a video to GIF",
                "just" : "l",
                "bounds" : { 
                    "x": 10, 
                    "y" : 5, 
                    "w" : dialog_width, 
                }, 
            },
            "file1_header" : {
                "type" : "text",
                "label" : "Source file",
                "just" : "l",
                "bounds" : { 
                    "w" : indent-10, 
                }, 
            },
            "inputfile_header" : {
                "type" : "text",
                "label" : pathinfo.filename,
                "just" : "l",
                "bounds" : { 
                    "x" : indent,
                    "y" : -1, 
                    "w" : dialog_width - indent,
                }, 
            },


            "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[0], 
                    "interval" : 1., 
                    "decimals" : 0 
                }, 
                "bounds" : {
                    "x" : indent_s,
                    "y" : -1, 
                    "w" : 70,
                    "h" : 50
                }, 
                "value": 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[0] > 0 ? 59 : time[1]),
                    "interval" : 1., 
                    "decimals" : 0 
                }, 
                "bounds" : {
                    "x" : indent_s + 70 + 30,
                    "y" : -1, 
                    "w" : 70,
                    "h" : 50
                }, 
                "value": 0
            }, 
            "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[0] + time[1] > 0 ? 59 : time[2]),
                    "interval" : 1., 
                    "decimals" : 0 
                }, 
                "bounds" : {
                    "x" : indent_s + 70 + 30 + 70 + 30,
                    "y" : -1, 
                    "w" : 70,
                    "h" : 50
                }, 
                "value": 0
            }, 
            "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": 0
            }, 
            "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[0], 
                    "interval" : 1., 
                    "decimals" : 0 
                }, 
                "bounds" : {
                    "x" : indent_s,
                    "y" : -1, 
                    "w" : 70,
                    "h" : 50
                }, 
                "value": time[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[0] > 0 ? 59 : time[1]),
                    "interval" : 1., 
                    "decimals" : 0 
                }, 
                "bounds" : {
                    "x" : indent_s + 70 + 30,
                    "y" : -1, 
                    "w" : 70,
                    "h" : 50
                }, 
                "value": time[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[0] + time[1] > 0 ? 59 : time[2]),
                    "interval" : 1., 
                    "decimals" : 0 
                }, 
                "bounds" : {
                    "x" : indent_s + 70 + 30 + 70 + 30,
                    "y" : -1, 
                    "w" : 70,
                    "h" : 50
                }, 
                "value": time[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[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
                }, 
            },

            "param_header3" : {
                "type" : "text",
                "label" : "Frame per sec",
                "just" : "l",
                "bounds" : { 
                    "w" : indent-10, 
                }, 
            },
            "slider_fps": { 
                "type": "slider", 
                "style": "incdec",
                "range" : { 
                    "min": 1.0, 
                    "max" : 60., 
                    "interval" : 1, 
                    "decimals" : 0 
                }, 
                "bounds" : {
                    "x" : indent,
                    "y" : -1, 
                    "w" : dialog_width - (indent*2),
                }, 
                "value": config.fps 
            }, 
            "param_header4" : {
                "type" : "text",
                "label" : "Loop (0=forever)",
                "just" : "l",
                "bounds" : { 
                    "w" : indent-10, 
                }, 
            },
            "slider_loop": { 
                "type": "slider", 
                "style": "incdec",
                "range" : { 
                    "min": 0, 
                    "max" : 1000, 
                    "interval" : 1,
                    "decimals" : 0
                }, 
                "bounds" : {
                    "x" : indent,
                    "y" : -1, 
                    "w" : dialog_width - (indent*2),
                }, 
                "value": config.loop 
            }, 
            "param_header5" : {
                "type" : "text",
                "label" : "Output width",
                "just" : "l",
                "bounds" : { 
                    "w" : indent-10, 
                }, 
            },
            "slider_width": { 
                "type": "slider", 
                "style": "incdec",
                "range" : { 
                    "min": 2, 
                    "max" : 1024, 
                    "interval" : 1,
                    "decimals" : 0
                }, 
                "bounds" : {
                    "x" : indent,
                    "y" : -1, 
                    "w" : dialog_width - (indent*2),
                }, 
                "value": config.width 
            }, 
            "file2_header" : {
                "type" : "text",
                "label" : "Output folder",
                "just" : "l",
                "bounds" : { 
                "w" : indent-10, 
                }, 
            },
            "outputfolder" : {
                "type" : "fileselect",
                "path" : ouputFolderPath,
                "editable" : false,
                "dir" : true,
                "saving" : true,
                "label" : "select output folder",
                "bounds" : {
                    "x" : indent,
                    "y" : -1, 
                    "w" : dialog_width - indent,
                }, 
            },
            "tag_header" : {
                "type" : "text",
                "label" : "Add file name tag:",
                "just" : "l",
                "bounds" : { 
                "w" : indent-10, 
                }, 
            },
            "tag" : {
                "type" : "textedit",
                "default" : config.tag,
                "bounds" : {
                    "x" : indent,
                    "y" : -1,
                    "w" : dialog_width - indent,
                }
            },
            "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);
        if(r.okay == 1)
        {
            var startTime = config.seconds_start;
            var length = config.seconds_stop - config.seconds_start;
            config.fps = r.slider_fps;
            config.loop = r.slider_loop;            
            config.width = r.slider_width;
            config.outfile = r.outputfolder + gvar.pss + pathinfo.basename + r.tag + ".gif";
            config.tag = r.tag;

            setMainMessage("busy...");                

            if (config.outfile.length > 0)
            {
                var args1 = ["-y"];

                args1.push("-ss");
                args1.push(startTime);
                
                args1.push("-t");
                args1.push(length);
                
                args1.push("-i");
                args1.push(infile);
                
                args1.push("-f");
                args1.push("gif");

                args1.push("-filter_complex");
                    
                // stats_mode parameter of the filter. The argument single generates a new palette for every input frame
                var filter = "[0:v]fps=" + config.fps + ",scale=w=" + config.width + ":h=-2,split[a][b];[a]palettegen=stats_mode=single[p];[b][p]paletteuse=new=1";

                if (gvar.isWindows == 1)
                    filter = "\"" + filter + "\"";
                                        
                echo("filter:" + filter);
                args1.push(filter);
                
                args1.push("-loop");
                args1.push(parseInt(config.loop));

                args1.push(config.outfile);
                
                echo(objectToString(args1));
                echo(cmd("ffmpeg", args1));

                newfilesize = getFileBytes(config.outfile);
                echo("new file size:" + bytesToDescription(newfilesize));

                if(isCanceled())
                {
                    setMainMessage("cancelled");
                    abort("cancelled");                
                }
            }

            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
		{
            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");
            abort("cancelled");
        }
    }    
        
    var then = getCurrentEpoch(); // secs since 1 jan 1970    
    setMainMessage("done in " + (then-now) + " sec");
    setProgress(100);
}

function onAbout()
{
    dialog(header.title, header.description + "\n\nby " + header.chef, "i" );
}

function onConfig()
{
    dialog("Config", "No config needed. This recipe will compress 1 video file from the file list.");
}

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