MPEG DASH with only FFMPEG

Sometimes you want to host a video yourself without using youtube or other service. Plus you decide you want adaptive quality to give a good experience.
It is actually pretty simple if you don’t have a ton of content and scale; there is the MPEG DASH standard that you can host on any web server and is supported by most javascript video players.
I have had a good experience with clappr player.

It is possible to encode video for MPEG DASH streaming using only FFMPEG.

The parameters regarding keyframes and encoding profiles I mostly took from this article by facebook so should be sensible.

In the example I have chosen the following video sizes and bitrates:
W: 240 Bitrate: 350
W: 480 Bitrate: 700
W: 720 Bitrate: 2500
and a fallback version for browsers that don’t support DASH with W: 480 Bitrate: 400

Here I use Node JS module fluent-ffmpeg to build the extremely long command line and execute ffmpeg. To get the multiple video qualities in one step you use map to create multiple virtual video streams which you can then target in ffmpeg commands. You will end up with a directory with a .MPD file and video segment files as well as a .MP4 fallback video.

const path = require('path');
const ffmpeg = require('fluent-ffmpeg');
const os = require('os');
var fs = require('fs');

if (os.platform() == 'win32') {
    let binarypath = path.resolve('./ffmpeg/bin/');
    let FfmpegPath = path.join(binarypath,'ffmpeg.exe');

    try {
        var FfmpegPathInfo = fs.statSync(FfmpegPath);
    } catch (err) {
        throw err;
    }

    ffmpeg.setFfmpegPath(FfmpegPath);
    ffmpeg.setFfprobePath(path.join(binarypath,'ffprobe.exe'));

    console.log('binarypath',path.join(binarypath,'ffmpeg.exe'));
}

function consoleEncode(fn) {
    // height, bitrate
    const sizes = [
        [240, 350],
        [480, 700],
        [720, 2500],
    ];
    const fallback = [480,400];

    let name = path.basename(fn, path.extname(fn));
    const targetdir = path.join(__dirname, name);
    const sourcefn = path.resolve(fn);

    console.log('source', sourcefn);
    console.log('info', sizes);
    console.log('info', targetdir);

    try {
        var targetdirInfo = fs.statSync(targetdir);
    } catch (err) {
        if (err.code === 'ENOENT') {
            fs.mkdirSync(targetdir);
        } else {
            throw err;
        }
    }

    var proc = ffmpeg({
        source: sourcefn,
        cwd: targetdir
    });

    var targetfn = path.join(targetdir, `${name}.mpd`);

    proc
        .output(targetfn)
        .format('dash')
        .videoCodec('libx264')
        .audioCodec('aac')
        .audioChannels(2)
        .audioFrequency(44100)
        .outputOptions([
            '-preset veryfast',
            '-keyint_min 60',
            '-g 60',
            '-sc_threshold 0',
            '-profile:v main',
            '-use_template 1',
            '-use_timeline 1',
            '-b_strategy 0',
            '-bf 1',
            '-map 0:a',
            '-b:a 96k'
        ]);


    for (var size of sizes) {
        let index = sizes.indexOf(size);

        proc
            .outputOptions([
                `-filter_complex [0]format=pix_fmts=yuv420p[temp${index}];[temp${index}]scale=-2:${size[0]}[A${index}]`,
                `-map [A${index}]:v`,
                `-b:v:${index} ${size[1]}k`,
            ]);
    }

    //Fallback version
    proc
        .output(path.join(targetdir, `${name}.mp4`))
        .format('mp4')
        .videoCodec('libx264')
        .videoBitrate(fallback[1])
        .size(`?x${fallback[0]}`)
        .audioCodec('aac')
        .audioChannels(2)
        .audioFrequency(44100)
        .audioBitrate(128)
        .outputOptions([
            '-preset veryfast',
            '-movflags +faststart',
            '-keyint_min 60',
            '-refs 5',
            '-g 60',
            '-pix_fmt yuv420p',
            '-sc_threshold 0',
            '-profile:v main',
        ]);

    proc.on('start', function(commandLine) {
        console.log('progress', 'Spawned Ffmpeg with command: ' + commandLine);
    });

    proc.on('progress', function(info) {
            console.log('progress', info);
        })
        .on('end', function() {
            console.log('complete');
        })
        .on('error', function(err) {
            console.log('error', err);
        });
    return proc.run();
}

consoleEncode('trailer.mov');

The command you end up executing looks like:

ffmpeg -i trailer.mov -y -acodec aac -ac 2 -ar 44100 -vcodec libx264 -f dash -preset veryfast -keyint_min 60 -g 60 -sc_threshold 0 -profile:v main -use_template 1 -use_timeline 1 -b_strategy 0 -bf 1 -map 0:a -b:a 96k -filter_complex [0]format=pix_fmts=yuv420p[temp0];[temp0]scale=-2:240[A0] -map [A0]:v -b:v:0 350k -filter_complex [0]format=pix_fmts=yuv420p[temp1];[temp1]scale=-2:480[A1] -map [A1]:v -b:v:1 700k -filter_complex [0]format=pix_fmts=yuv420p[temp2];[temp2]scale=-2:720[A2] -map [A2]:v -b:v:2 2500k trailer\trailer.mpd -acodec aac -ac 2 -ar 44100 -b:a 128k -vcodec libx264 -b:v 400k -filter:v scale=w=trunc(oh*a/2)*2:h=480 -f mp4 -preset veryfast -movflags +faststart -keyint_min 60 -refs 5 -g 60 -pix_fmt yuv420p -sc_threshold 0 -profile:v main trailer\trailer.mp4

Download the code: FFMPEGDASH.zip

And finally an example video encoded using the above code:
[open in new tab]

Comments are closed.