header={ "recipe_version": "1.11", "title": "Rumble emote", "description": "Create gif emotes from video clips compatible with Rumble", "category": "video", "chef": "BeatRig", "spice": "BQ==:G+Hy36kJcnQQYYavb/JV2Il6eCYr6h4hRJHCF9rRbhX5BcazL6cuTgUMdz0xA50VWA5PmmV0Q7aWkiUtNFtbk991l+t0e8IBXMReMIT1GNjnZZDv7n2YRAo0xcSQbY0ixjG6LLH4wGKnspKAoA/v69iLn6N2IH0XzIZwTGdHSo4=", "palette": "Mint Cream", "flavour": "xYLvgKclckn+p90nR4e5DUAhovor/0xKXNOsOLsHSs76xqS9T5boURYYgz2ijaS7mseRUCB/Q/5vUfnKf0HnErBmbov0OKJZf74Iyd9fXTyYamoRx/zRv8XGJSo2b8INe9b+qhnp79rH1fHcyI/AaDlT8844KdZQBCprk9yQ7D8=", "time": 1697918357, "core_version": "0.6.1", "magnetron_version": "1.0.275", "type": "demo", "os": "windows,macOS", "functions": "main,onAbout", "dependencies": "ffprobe,ffmpeg", "tags": "video,convert,rumble", "uuid": "8db55f70ad67437fbf43d32cc36dad9c", "instructions": "Drop a video file and run to create a 1 mb square GIF emote 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.seconds_stop - config.seconds_start) * 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", ["-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height,r_frame_rate,DURATION", "-of", "default=noprint_wrappers=1:nokey=1", infile]); echo(probeoutput); probearray = probeoutput.split("\n"); if (probearray.length > 3) { echo(objectToString(probearray)); config.width = parseInt(trimString(probearray[0])); config.height = parseInt(trimString(probearray[1])); fps_num = findSubString(trimString(probearray[2]), "", "/"); fps_den = trimString(probearray[2]).substring(fps_num.length + 1, trimString(probearray[2]).length); config.fps = parseFloat(fps_num) / parseFloat(fps_den); config.totalsec = parseFloat(trimString(probearray[3])); // fallback get duration and fps if (config.totalsec <= 0.) { var probeoutput = cmd("ffprobe", [infile]); var dur_tc = findSubString(probeoutput, "Duration: ", ", s"); config.totalsec = calculateSeconds(dur_tc); if (config.fps <= 0.) config.fps = parseInt(findSubString(probeoutput, "kb/s, ", " fps")); } } echo(objectToString(config)); } //---------------------------------------------------------------------------- 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 = "-[rumble]"; config.fps = 10; config.loop = 0; config.output_size = 128; 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); // get file data getFileProps(infile); if (config.totalsec <= 0) { setMainMessage("no content found in " + pathinfo.filename); abort("no content found in " + pathinfo.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 a Rumble emote, 1 mb square .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 }, }, "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.outfile = r.outputfolder + gvar.pss + pathinfo.basename + r.tag + ".gif"; var file_count =2; while (!isCanceled() && fileExists(config.outfile)) config.outfile = r.outputfolder + gvar.pss + pathinfo.basename + r.tag + "-" + (file_count++) + ".gif"; config.tag = r.tag; setMainMessage("busy..."); while (config.outfile.length > 0 && (newfilesize == 0 || newfilesize > 1024 * 1024)) // 1048576 1 megabyte { if (newfilesize == 0) { config.output_size = 256; config.fps = 20; setMainMessage("creating profile"); } else { var ratio = (newfilesize / (1024. * 1024.)); var pixelratio = ((ratio - 1.) / 6. * 5.) + 1.; // shave off pixels pixelratio = (pixelratio %2 != 0) ? pixelratio : pixelratio + 1; // keep it even numbers var fpsratio = ((ratio - 1.) / 16.) + 1.; // reduce frames new_output_size = parseInt(Math.sqrt(Math.pow(config.output_size, 2.) / pixelratio)); // resize to squared ratio new_fps = parseInt(config.fps / fpsratio); if (config.output_size != new_output_size || config.fps != new_fps) // there was an adjustment { config.output_size = new_output_size; config.fps = new_fps; } else abort("unable to process this file correctly"); echo("resizing px:" + config.output_size + " fps:" + config.fps); } var args1 = ["-y"]; // overwrite 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=-1:h=" + (config.output_size) + ",crop=" + config.output_size + ":" + config.output_size + ",split[a][b];[a]palettegen=stats_mode=single[p];[b][p]paletteuse=new=1"; if (config.width < config.height) // vertical video file filter = "[0:v]fps=" + config.fps + ",scale=w=" + (config.output_size) + ":h=-1,crop=" + config.output_size + ":" + config.output_size + ",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 (newfilesize <= 0) abort("failed to process file " + pathinfo.filename); if(isCanceled()) { setMainMessage("cancelled"); abort("cancelled"); } } if (gvar.demo && i == numFiles - 1) // last file only { r = dialog(getFormDialog("Thank you for using magnetron.app\nGet a license for full access and support further development", "Continue", "Get license")); if (r.returns == 0) launchInBrowser("https://magnetron.app/?edd_action=add_to_cart&download_id=17"); } 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("canceled"); } } var then = getCurrentEpoch(); // secs since 1 jan 1970 setMainMessage("done in " + (then-now) + " sec"); setProgress(100); } function onAbout() { launchInBrowser("https://magnetron.dev/blog/"); } 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) ]; }