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