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