Rumble emote

The live video streaming site rumble.com allows the usage of small images and animated GIF emotes in their chat. Every channel owner can set multiple emotes. However creating animated emotes can be hard because these need to be square and at a maximum 1 megabyte in size. Hindering the creative process.

This recipe is made to produce the optimal result, by automatically reducing the file size cropping, scaling the a video down and reducing the framerate . Just drop a video or GIF file and it will convert (part of) it for you.

Best of all, you can use it for FREE, just download magnetron.app and install the recipe.

Download the recipe from the extended collection here:
https://magnetron.app/recipes/rumble-emote/

For Windows & macOS

Change the filename to the name you want in your chat. Then drop the file at https://rumble.com/account/emotes

dialog


With magnetron.dev you can also use the source to create your own custom processes.

Take a closer look at the complete source below:

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