Summary

A complete, cross-platform solution to record, convert and stream audio and video. https://ffmpeg.org/

Examples

  • Encoding or converting will be as easy as
ffmpeg -i input.mp4 output.avi
  • Video streams can be converted and broken into appropriate segments by FFmpeg with a command such as
ffmpeg -i inputFile.mkv -c:v h264 -flags +cgop -g 30 -hls_time 1 outputFile.m3u8

Where I used this?

In tfarraj project, this library was used in the upload-encoder project, which uses fluent-ffmpeg npm module or library to make complex command-line into easy and fluent.

In the example below, the nodejs file is converting the uploaded videos from video format, mov, to HLS Format.

import express from 'express';
import fileUpload from 'express-fileupload';
import ffmpeg from 'fluent-ffmpeg';
import fs from 'fs';
import path from 'path';
import { Transcoder } from 'simple-hls';

import config from './config/index.js';
import logger, { httpLogger } from './config/logger.js';

const app = express();
const { port } = config;
let customRenditions = null;

app.use(fileUpload({
  useTempFiles: true,
  tempFileDir: config.tempPath,
}));

// HTTP logger
// Must be after `fileUpload` to be able to log body fields
app.use(httpLogger);

// TODO: support NVIDIA hardware acceleration
// TODO: clean up uploads directory

app.listen(port, () => {
  logger.info(`Tfarraj upload encoder API is running on port ${port}.`);
});

app.post('/upload', (req, res) => {
  if (!req.files || Object.keys(req.files).length === 0) {
    const message = 'No files were uploaded.';
    res.err = { message };
    return res.status(400).send(message);
  }

  // The name of the input field (i.e. "video") is used to retrieve the uploaded file
  const { videoId } = req.body;
  const { video } = req.files;
  video.name = `${videoId}${path.parse(video.name).ext}`;
  const uploadPath = config.uploadPath + video.name;
  const filetype = path.extname(video.name);

  video.mv(uploadPath, (err) => {
    if (err) {
      res.err = err;
      return res.status(500).send(err);
    }

    ffmpeg.ffprobe(config.uploadPath + video.name, (err, metadata) => {
      const [width, height] = (filetype === '.mov' || filetype === '.MOV' || filetype === '.m4v')
        ? [
          metadata.streams[1].width,
          metadata.streams[1].height,
        ] : [
          metadata.streams[0].width,
          metadata.streams[0].height,
        ];

      {
        const dir = config.hlsPath + videoId;
        if (!fs.existsSync(dir)) {
          fs.mkdirSync(dir);
        }
      }

      ffmpeg(config.uploadPath + video.name)
        .screenshots({
          count: 3,
          filename: 'thumbnail-%i.jpg',
          folder: config.hlsPath + videoId,
        });

      // Produces Video Preview
      // ffmpeg(config.uploadPath + video.name)
      //   .videoCodec('h264_nvenc')
      //   .inputOptions('-ss 10')
      //   .outputOptions('-t 4')
      //   .withSize('320x240')
      //   .withAspect('16:9')
      //   .applyAutopadding(true, 'black')
      //   .noAudio()
      //   .save(`${config.hlsPath}${videoId}/preview.mp4`);

      ffmpeg(config.uploadPath + video.name)
        .inputOptions('-ss 10')
        .outputOption('-t', '4', '-vf', 'scale=160👎flags=lanczos,fps=15')
        .save(`${config.hlsPath}${videoId}/preview.gif`);

      if (height && width >= 640) {
        if (width <= 854) {
          customRenditions = [{
            width: 640,
            height: 360,
            profile: 'main',
            hlsTime: '4',
            bv: '800k',
            maxrate: '856k',
            bufsize: '1200k',
            ba: '96k',
            ts_title: '360p',
            master_title: '360p',
          },
          {
            width: 854,
            height: 480,
            profile: 'main',
            hlsTime: '4',
            bv: '1400k',
            maxrate: '1498k',
            bufsize: '2100k',
            ba: '128k',
            ts_title: '480p',
            master_title: '480p',
          }];
        } else if (width <= 1280) {
          customRenditions = [{
            width: 640,
            height: 360,
            profile: 'main',
            hlsTime: '4',
            bv: '800k',
            maxrate: '856k',
            bufsize: '1200k',
            ba: '96k',
            ts_title: '360p',
            master_title: '360p',
          },
          {
            width: 854,
            height: 480,
            profile: 'main',
            hlsTime: '4',
            bv: '1400k',
            maxrate: '1498k',
            bufsize: '2100k',
            ba: '128k',
            ts_title: '480p',
            master_title: '480p',
          },
          {
            width: 1280,
            height: 720,
            profile: 'main',
            hlsTime: '4',
            bv: '2800k',
            maxrate: '2996k',
            bufsize: '4200k',
            ba: '128k',
            ts_title: '720p',
            master_title: '720p',
          }];
        } else if (width <= 1920) {
          customRenditions = [{
            width: 640,
            height: 360,
            profile: 'main',
            hlsTime: '4',
            bv: '800k',
            maxrate: '856k',
            bufsize: '1200k',
            ba: '96k',
            ts_title: '360p',
            master_title: '360p',
          },
          {
            width: 854,
            height: 480,
            profile: 'main',
            hlsTime: '4',
            bv: '1400k',
            maxrate: '1498k',
            bufsize: '2100k',
            ba: '128k',
            ts_title: '480p',
            master_title: '480p',
          },
          {
            width: 1280,
            height: 720,
            profile: 'main',
            hlsTime: '4',
            bv: '2800k',
            maxrate: '2996k',
            bufsize: '4200k',
            ba: '128k',
            ts_title: '720p',
            master_title: '720p',
          },
          {
            width: 1920,
            height: 1080,
            profile: 'main',
            hlsTime: '4',
            bv: '5000k',
            maxrate: '5350k',
            bufsize: '7500k',
            ba: '192k',
            ts_title: '1080p',
            master_title: '1080p',
          }];
        } else {
          customRenditions = [{
            width: 640,
            height: 360,
            profile: 'main',
            hlsTime: '4',
            bv: '800k',
            maxrate: '856k',
            bufsize: '1200k',
            ba: '96k',
            ts_title: '360p',
            master_title: '360p',
          },
          {
            width: 854,
            height: 480,
            profile: 'main',
            hlsTime: '4',
            bv: '1400k',
            maxrate: '1498k',
            bufsize: '2100k',
            ba: '128k',
            ts_title: '480p',
            master_title: '480p',
          },
          {
            width: 1280,
            height: 720,
            profile: 'main',
            hlsTime: '4',
            bv: '2800k',
            maxrate: '2996k',
            bufsize: '4200k',
            ba: '128k',
            ts_title: '720p',
            master_title: '720p',
          },
          {
            width: 1920,
            height: 1080,
            profile: 'main',
            hlsTime: '4',
            bv: '5000k',
            maxrate: '5350k',
            bufsize: '7500k',
            ba: '192k',
            ts_title: '1080p',
            master_title: '1080p',
          },
          {
            width: 3840,
            height: 2160,
            profile: 'main',
            hlsTime: '4',
            bv: '20000k',
            maxrate: '21900k',
            bufsize: '25000k',
            ba: '192k',
            ts_title: '2160p',
            master_title: '2160p',
          }];
        }
      } else {
        const message = `height: ${height} width: ${width} Not In Spec or Missing`;
        res.err = { message };
        return res.status(400).send(message);
      }
      req.log.info(`height: ${height} width: ${width}`);

      async function transcode() {
        const inputPath = config.uploadPath + video.name;
        const outputPath = config.hlsPath + videoId;

        const t = new Transcoder(inputPath, outputPath, {
          renditions: customRenditions, showLogs: false,
        });

        try {
          const encodeStart = performance.now();
          await t.transcode();
          const encodeEnd = performance.now();
          res.encodeTime = Math.round((encodeEnd - encodeStart) / 10) / 100;
        } catch (transcodeError) {
          res.err = transcodeError;
          res.status(500).json({ msg: 'Transcoding error', error: transcodeError });
        }

        ffmpeg.ffprobe(`${outputPath}/index.m3u8`, (hlsProbeError, hlsMetadata) => {
          if (hlsProbeError) {
            res.err = hlsProbeError;
            res.status(500).send({ msg: 'Error reading HLS metadata', error: hlsProbeError });
          }
          req.log.debug(hlsMetadata);
          res.status(200).json(hlsMetadata);
        });
      }
      transcode();
    });
  });
});