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