ffmpeg + node 实现流式视频处理

7,327 阅读2分钟

1. 流式处理简介

先来看个示例:

ffmpeg -s 0 -i input.mp4 -t 10 output.mp4

这段命令将截取输入视频 input.mp4 从0秒开始到第10秒之间的片段,并保存的 output.mp4 文件。命令可以分为三个部分:

image.png

ffmpeg 接受三种类型的输出:

  1. 文件位置
  2. 网络位置,用于实现网络推流
  3. - ,以标准输出流方式输出命令结果


借助 - 特性,我们可以以管道风格调用ffmpeg命令,例如:

ffmpeg -s 0 -i input.mp4 -t 10 -f mpegts - | tee output.ts

提示:

使用 - 时,ffmpeg 会尽量遵循管道通讯约束,将命令处理后的视频流输出到标准输出流;将命令运行的过程信息输出到标准错误流。

2. 在node中使用


流式处理最大的好处是不用等整个视频命令处理完就可以先拿到一部分数据,这在一些性能敏感场景,比如web视频服务、直播流等能够提高响应速度,在我的另一篇博客《HLS + ffmpeg 实现动态码流视频服务》有更详细的介绍。在node实现中,本质上是使用 spawn 接口调用 ffmpeg 命令,借助父子进程间的管道通讯机制,将 ffmpeg 命令的标准输出流输出到目标视频文件中,实现保存效果,完整示例:

const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');

function transform(sourceFile, outputStream, start, duration) {
	const params = [`-ss`, start, `-i`, sourceFile, `-t`, duration, '-f', 'mpegts', '-'];
	return new Promise((r, j) => {
    const cp = spawn('ffmpeg', params);
    // ffmpeg 标准输出流
		cp.stdout.pipe(outputStream);
    // 将 ffmpeg 执行的过程日志输出到程序的标准输出流,通常为console
		cp.stderr.pipe(process.stdout);
		cp.on('error', (err) => {
			j(err);
		});
		cp.on('close', (code) => {
			console.log(`ffmpeg process close all stdio with code ${code}`);
			r(code);
		});
		cp.on('exit', (code) => {
			console.log(`ffmpeg process exited with code ${code}`);
		});
	});
}

async function run() {
	const inputFile = path.join(__dirname, 'test.mp4');
	const output = fs.createWriteStream(path.join(__dirname, 'output.mp4'));
	await transform(inputFile, output, 0, 100);
}

run();

运行效果:

3. 注意点

ffmpeg 虽然提供了流式输出功能,但并不适用于所有场景,我简单测试了一下发现:

  1. 当输出封装格式为 mp4 时会报错 muxer does not support non seekable output ,这个异常是因为 mp4 不是顺序写的,在写不同类型的box时需要多次执行seek操作将数据插入适当位置,因此 mp4 格式要求输出流是 seekable 的,那用 stdout 自然就抓瞎了。
  2. 当输出封装格式为 hls 时会警告 Cannot use rename on non file protocol, this may lead to races and temporary partial files ,这是因为hls需要输出多个文件,包括 m3u8 和一堆 ts ,默认情况下 ts 分片会按照分片序列命名,这个时候如果用标准输出流这种没法重命名的流,自然也是抓瞎。