Ffmpeg - 如何强制输出整个帧的 MJPEG?
Ffmpeg - How to force MJPEG output of whole frames?
我正在使用 ffmpeg 处理来自远程摄像机的传入 MPEGTS 流,并使用我的应用程序将其传送给多个客户端。
从技术上讲,我正在使用 ffmpeg 将传入流转换为 MJPEG 输出,并将数据块(从 ffmpeg 进程标准输出)通过管道传输到客户端 http 响应上的可写流。
但是,我遇到了一个问题 - 并非所有数据块都代表完整的 'whole' 帧。因此,在浏览器中连续显示它们会随机产生闪烁的视频,其中帧不完整。
我知道这一点是因为在打印每个块长度时,大部分时间都会得到一个很大的值 (X),但有时我会得到 2 个连续的块,例如长度为 (2/5X),后跟 (3/5X)。
所以问题 - 有没有办法强制 ffmpeg 进程只输出整帧?如果没有,我有没有办法检查每个数据块 'manually' 并寻找 headers/metadata/flags 来指示帧 start/end?
我用于输出 MJPEG 的 ffmpeg 命令是:
ffmpeg -i - -c:v mjpeg -f mjpeg -
解释:
“-i -”:(输入)是进程的标准输入(而不是静态文件)
“-c:v mjpeg”:使用 mjpeg 编解码器
“-f mjpeg”:输出将采用 mjpeg 格式
“-”:未指定输出(文件或 url)- 将是进程标准输出
编辑:
这里有一些 console.log 打印来可视化问题:
%%% FFMPEG Info %%%
frame= 832 fps= 39 q=24.8 q=29.0 size= 49399kB time=00:00:27.76 bitrate=14577.1kbits/s speed=1.29x
data.length: 60376
data.length: 60411
data.length: 60465
data.length: 32768
data.length: 27688
data.length: 32768
data.length: 27689
data.length: 60495
data.length: 60510
data.length: 60457
data.length: 59811
data.length: 59953
data.length: 59889
data.length: 59856
data.length: 59936
data.length: 60049
data.length: 60091
data.length: 60012
%%% FFMPEG Info %%%
frame= 848 fps= 38 q=24.8 q=29.0 size= 50340kB time=00:00:28.29 bitrate=14574.4kbits/s speed=1.28x
data.length: 60025
data.length: 60064
data.length: 60122
data.length: 60202
data.length: 60113
data.length: 60211
data.length: 60201
data.length: 60195
data.length: 60116
data.length: 60167
data.length: 60273
data.length: 60222
data.length: 60223
data.length: 60267
data.length: 60329
%%% FFMPEG Info %%%
frame= 863 fps= 38 q=24.8 q=29.0 size= 51221kB time=00:00:28.79 bitrate=14571.9kbits/s speed=1.27x
如您所见,整个帧约为 60k(我的指示是我正在浏览器上查看的干净视频流),但有时输出由 2 个连续的块组成,加起来为~60k。当传送到浏览器时,这些是 'half frames'.
根据此处和 StackExchange 上的评论,似乎从 ffmpeg 进程输出的 MJPEG 流应该由整个帧组成。监听 ffmpeg ChildProcess stdout 会产生不同大小的数据块——这意味着它们并不总是代表一整帧(完整的 JPEG)图像。
因此,我没有将它们推送给消费者(目前是一个显示视频流的网络浏览器),而是编写了一些代码来处理内存中的 'half-chunks' 并将它们附加在一起,直到帧完成。
这似乎解决了问题,因为我的视频没有闪烁。
const _SOI = Buffer.from([0xff, 0xd8]);
const _EOI = Buffer.from([0xff, 0xd9]);
private size: number = 0;
private chunks: any[] = [];
private jpegInst: any = null;
private pushWholeMjpegFrame(chunk: any): void {
const chunkLength = chunk.length;
let pos = 0;
while (true) {
if (this.size) {
const eoi = chunk.indexOf(_EOI);
if (eoi === -1) {
this.chunks.push(chunk);
this.size += chunkLength;
break;
} else {
pos = eoi + 2;
const sliced = chunk.slice(0, pos);
this.chunks.push(sliced);
this.size += sliced.length;
this.jpegInst = Buffer.concat(this.chunks, this.size);
this.chunks = [];
this.size = 0;
this.sendJpeg();
if (pos === chunkLength) {
break;
}
}
} else {
const soi = chunk.indexOf(_SOI, pos);
if (soi === -1) {
break;
} else {
pos = soi + 500;
}
const eoi = chunk.indexOf(_EOI, pos);
if (eoi === -1) {
const sliced = chunk.slice(soi);
this.chunks = [sliced];
this.size = sliced.length;
break;
} else {
pos = eoi + 2;
this.jpegInst = chunk.slice(soi, pos);
this.sendJpeg();
if (pos === chunkLength) {
break;
}
}
}
}
}
如果我的解决方案可以得到改进和优化,我很乐意得到一些更有教养的意见,以及关于问题根源的更多知识,也许还有一种方法可以使所需的行为脱离- 带 ffmpeg 的盒子,
所以请随时通过更多的答案和评论来保持这个问题的活力。
我遇到了同样的问题,最后到了这里。正如其他人所说,这种 ffmpeg 行为是设计使然,如 OP 所示,问题可以在 ffmpeg 之外轻松解决。将 ffmpeg 输出视为一个流。与一般的流一样,内容是分段发送的。这使得数据流更加一致,因为块的大小与每个帧的大小没有直接关系。它允许吞吐量在一定程度上保持一致(相对于它的相邻块),即使压缩方案导致某些帧由于运动、纯色等而在大小上有很大不同。
OP 的回答帮助我指明了正确的方向,我编写了自己的稍微简单的实现来在 vanilla ES6 中构建完整的 JPG 图像。如果它对其他人有帮助,以下内容对我来说效果很好。它采用管道传输到标准输出的 ffmpeg mjpeg 块,并查找 SOI 和 EOI 标记(参见 https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format#File_format_structure)以构建完整的 base64 JPG 图像,准备在 或
let chunks = [];
// See https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format#File_format_structure
// for SOI and EOI explanation.
const SOI = Buffer.from([0xff, 0xd8]);
const EOI = Buffer.from([0xff, 0xd9]);
function handleFfmpegOutputData(chunk) {
const eoiPos = chunk.indexOf(EOI);
const soiPos = chunk.indexOf(SOI);
if (eoiPos === -1) {
// No EOI - just append to chunks.
chunks.push(chunk);
} else {
// EOI is within chunk. Append everything before EOI to chunks
// and send the full frame.
const part1 = chunk.slice(0, eoiPos + 2);
if (part1.length) {
chunks.push(part1);
}
if (chunks.length) {
writeFullFrame(chunks);
}
// Reset chunks.
chunks = [];
}
if (soiPos > -1) {
// SOI is present. Ensure chunks has been reset and append
// everything after SOI to chunks.
chunks = [];
const part2 = chunk.slice(soiPos)
chunks.push(part2);
}
}
function writeFullFrame(frameChunks) {
// Concatenate chunks together.
const bufferData = Buffer.concat([...frameChunks]);
// Convert buffer to base64 for display.
const base64Data = Buffer.from(bufferData).toString('base64');
const imageSrc = `data:image/jpeg;base64,${base64Data}`;
// Do whatever you want with base64 src string...
}
感谢您的提示。为我工作。我在 C# 中的实现:
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize + 1);
var imageData = new List<byte>();
try
{
var bytesLidos = 0;
while (true)
{
bytesLidos = await data.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken);
if (bytesLidos == 0)
{
break;
}
var eoiPos = Search(buffer, EOI);
var soiPos = Search(buffer, SOI);
if (eoiPos == -1)
{
imageData.AddRange(buffer[0..bytesLidos]);
}
else
{
var part1 = buffer.Take(eoiPos + 2);
if (part1.Any())
{
imageData.AddRange(part1);
}
if (imageData.Count > 0)
{
WriteFullFrame(imageData);
}
imageData.Clear();
}
if (soiPos > -1)
{
imageData.Clear();
imageData.AddRange(buffer[soiPos..]);
}
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
我正在使用 ffmpeg 处理来自远程摄像机的传入 MPEGTS 流,并使用我的应用程序将其传送给多个客户端。
从技术上讲,我正在使用 ffmpeg 将传入流转换为 MJPEG 输出,并将数据块(从 ffmpeg 进程标准输出)通过管道传输到客户端 http 响应上的可写流。
但是,我遇到了一个问题 - 并非所有数据块都代表完整的 'whole' 帧。因此,在浏览器中连续显示它们会随机产生闪烁的视频,其中帧不完整。 我知道这一点是因为在打印每个块长度时,大部分时间都会得到一个很大的值 (X),但有时我会得到 2 个连续的块,例如长度为 (2/5X),后跟 (3/5X)。
所以问题 - 有没有办法强制 ffmpeg 进程只输出整帧?如果没有,我有没有办法检查每个数据块 'manually' 并寻找 headers/metadata/flags 来指示帧 start/end?
我用于输出 MJPEG 的 ffmpeg 命令是:
ffmpeg -i - -c:v mjpeg -f mjpeg -
解释:
“-i -”:(输入)是进程的标准输入(而不是静态文件)
“-c:v mjpeg”:使用 mjpeg 编解码器
“-f mjpeg”:输出将采用 mjpeg 格式
“-”:未指定输出(文件或 url)- 将是进程标准输出
编辑: 这里有一些 console.log 打印来可视化问题:
%%% FFMPEG Info %%%
frame= 832 fps= 39 q=24.8 q=29.0 size= 49399kB time=00:00:27.76 bitrate=14577.1kbits/s speed=1.29x
data.length: 60376
data.length: 60411
data.length: 60465
data.length: 32768
data.length: 27688
data.length: 32768
data.length: 27689
data.length: 60495
data.length: 60510
data.length: 60457
data.length: 59811
data.length: 59953
data.length: 59889
data.length: 59856
data.length: 59936
data.length: 60049
data.length: 60091
data.length: 60012
%%% FFMPEG Info %%%
frame= 848 fps= 38 q=24.8 q=29.0 size= 50340kB time=00:00:28.29 bitrate=14574.4kbits/s speed=1.28x
data.length: 60025
data.length: 60064
data.length: 60122
data.length: 60202
data.length: 60113
data.length: 60211
data.length: 60201
data.length: 60195
data.length: 60116
data.length: 60167
data.length: 60273
data.length: 60222
data.length: 60223
data.length: 60267
data.length: 60329
%%% FFMPEG Info %%%
frame= 863 fps= 38 q=24.8 q=29.0 size= 51221kB time=00:00:28.79 bitrate=14571.9kbits/s speed=1.27x
如您所见,整个帧约为 60k(我的指示是我正在浏览器上查看的干净视频流),但有时输出由 2 个连续的块组成,加起来为~60k。当传送到浏览器时,这些是 'half frames'.
根据此处和 StackExchange 上的评论,似乎从 ffmpeg 进程输出的 MJPEG 流应该由整个帧组成。监听 ffmpeg ChildProcess stdout 会产生不同大小的数据块——这意味着它们并不总是代表一整帧(完整的 JPEG)图像。
因此,我没有将它们推送给消费者(目前是一个显示视频流的网络浏览器),而是编写了一些代码来处理内存中的 'half-chunks' 并将它们附加在一起,直到帧完成。
这似乎解决了问题,因为我的视频没有闪烁。
const _SOI = Buffer.from([0xff, 0xd8]);
const _EOI = Buffer.from([0xff, 0xd9]);
private size: number = 0;
private chunks: any[] = [];
private jpegInst: any = null;
private pushWholeMjpegFrame(chunk: any): void {
const chunkLength = chunk.length;
let pos = 0;
while (true) {
if (this.size) {
const eoi = chunk.indexOf(_EOI);
if (eoi === -1) {
this.chunks.push(chunk);
this.size += chunkLength;
break;
} else {
pos = eoi + 2;
const sliced = chunk.slice(0, pos);
this.chunks.push(sliced);
this.size += sliced.length;
this.jpegInst = Buffer.concat(this.chunks, this.size);
this.chunks = [];
this.size = 0;
this.sendJpeg();
if (pos === chunkLength) {
break;
}
}
} else {
const soi = chunk.indexOf(_SOI, pos);
if (soi === -1) {
break;
} else {
pos = soi + 500;
}
const eoi = chunk.indexOf(_EOI, pos);
if (eoi === -1) {
const sliced = chunk.slice(soi);
this.chunks = [sliced];
this.size = sliced.length;
break;
} else {
pos = eoi + 2;
this.jpegInst = chunk.slice(soi, pos);
this.sendJpeg();
if (pos === chunkLength) {
break;
}
}
}
}
}
如果我的解决方案可以得到改进和优化,我很乐意得到一些更有教养的意见,以及关于问题根源的更多知识,也许还有一种方法可以使所需的行为脱离- 带 ffmpeg 的盒子, 所以请随时通过更多的答案和评论来保持这个问题的活力。
我遇到了同样的问题,最后到了这里。正如其他人所说,这种 ffmpeg 行为是设计使然,如 OP 所示,问题可以在 ffmpeg 之外轻松解决。将 ffmpeg 输出视为一个流。与一般的流一样,内容是分段发送的。这使得数据流更加一致,因为块的大小与每个帧的大小没有直接关系。它允许吞吐量在一定程度上保持一致(相对于它的相邻块),即使压缩方案导致某些帧由于运动、纯色等而在大小上有很大不同。
OP 的回答帮助我指明了正确的方向,我编写了自己的稍微简单的实现来在 vanilla ES6 中构建完整的 JPG 图像。如果它对其他人有帮助,以下内容对我来说效果很好。它采用管道传输到标准输出的 ffmpeg mjpeg 块,并查找 SOI 和 EOI 标记(参见 https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format#File_format_structure)以构建完整的 base64 JPG 图像,准备在 或
let chunks = [];
// See https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format#File_format_structure
// for SOI and EOI explanation.
const SOI = Buffer.from([0xff, 0xd8]);
const EOI = Buffer.from([0xff, 0xd9]);
function handleFfmpegOutputData(chunk) {
const eoiPos = chunk.indexOf(EOI);
const soiPos = chunk.indexOf(SOI);
if (eoiPos === -1) {
// No EOI - just append to chunks.
chunks.push(chunk);
} else {
// EOI is within chunk. Append everything before EOI to chunks
// and send the full frame.
const part1 = chunk.slice(0, eoiPos + 2);
if (part1.length) {
chunks.push(part1);
}
if (chunks.length) {
writeFullFrame(chunks);
}
// Reset chunks.
chunks = [];
}
if (soiPos > -1) {
// SOI is present. Ensure chunks has been reset and append
// everything after SOI to chunks.
chunks = [];
const part2 = chunk.slice(soiPos)
chunks.push(part2);
}
}
function writeFullFrame(frameChunks) {
// Concatenate chunks together.
const bufferData = Buffer.concat([...frameChunks]);
// Convert buffer to base64 for display.
const base64Data = Buffer.from(bufferData).toString('base64');
const imageSrc = `data:image/jpeg;base64,${base64Data}`;
// Do whatever you want with base64 src string...
}
感谢您的提示。为我工作。我在 C# 中的实现:
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize + 1);
var imageData = new List<byte>();
try
{
var bytesLidos = 0;
while (true)
{
bytesLidos = await data.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken);
if (bytesLidos == 0)
{
break;
}
var eoiPos = Search(buffer, EOI);
var soiPos = Search(buffer, SOI);
if (eoiPos == -1)
{
imageData.AddRange(buffer[0..bytesLidos]);
}
else
{
var part1 = buffer.Take(eoiPos + 2);
if (part1.Any())
{
imageData.AddRange(part1);
}
if (imageData.Count > 0)
{
WriteFullFrame(imageData);
}
imageData.Clear();
}
if (soiPos > -1)
{
imageData.Clear();
imageData.AddRange(buffer[soiPos..]);
}
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}