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