【问题标题】:Ffmpeg - How to force MJPEG output of whole frames?Ffmpeg - 如何强制整个帧的 MJPEG 输出?
【发布时间】:2020-04-15 12:21:03
【问题描述】:

我正在使用 ffmpeg 处理来自远程摄像机的传入 MPEGTS 流,并使用我的应用将其传送给多个客户端。

从技术上讲,我使用 ffmpeg 将传入流转换为 MJPEG 输出,并将数据块(来自 ffmpeg 进程标准输出)通过管道传输到客户端 http 响应上的可写流。

但是,我面临一个问题 - 并非所有数据块都代表一个完整的“完整”帧。因此,在浏览器中连续显示它们会导致随机播放带有半完整帧的闪烁视频。 我知道这一点是因为在打印每个块长度时,大部分时间都会产生一个很大的值(X),但是我时不时地得到 2 个长度为(2/5X)的连续块,然后是(3/5X)例如。

所以问题 - 有没有办法强制 ffmpeg 进程只输出整个帧?如果没有,我有没有办法“手动”检查每个数据块并查找标头/元数据/标志来指示帧开始/结束?


我输出 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 .当传送到浏览器时,这些是“半帧”。

【问题讨论】:

  • 添加-thread_type frame并检查。
  • 我实际上将传入流映射到 2 个输出 - 1 用于以 30 秒的块在本地磁盘上写入文件(所有文件都播放完美),另一个用于 mjpeg 输出流.那么我应该在哪里添加你的建议?在整个命令的乞求上,还是在 mjpeg 的“-map”上具体?
  • 分享你的完整命令。
  • ffmpeg -i - -map 0:v -c:v mjpeg -f mjpeg - -map 0:v -c:v libx264 -f 段 -reset_timestamps 1 -segment_time 30 -segment_format mp4 -strftime 1 "${dir}/${cameraId}_recording_%d-%m-%Y_%H:%M:%S.mp4"
  • -c:v mjpeg之后直接添加

标签: node.js ffmpeg video-streaming mjpeg


【解决方案1】:

按照此处和 StackExchange 上的 cmets,从 ffmpeg 进程输出的 MJPEG 流似乎应该由整个帧组成。收听 ffmpeg ChildProcess 标准输出会产生不同大小的数据块——这意味着它们并不总是代表整个帧(完整的 JPEG)图像。

因此,我编写了一些代码来处理内存中的“半块”并将它们附加在一起直到帧完成,而不是仅仅将它们推送给消费者(目前是一个显示视频流的网络浏览器)。

这似乎解决了问题,因为我在视频中没有闪烁。

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 的盒子, 因此,请随时通过更多答案和 cmets 来保持这个问题的活力。

【讨论】:

    【解决方案2】:

    我遇到了同样的问题,最终来到了这里。正如其他人所说,这种 ffmpeg 行为是设计使然,问题可以在 ffmpeg 之外轻松解决,正如 OP 所示。将 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...
    
          }
    

    【讨论】:

      【解决方案3】:

      感谢您的提示。为我工作。我在 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);
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-12-17
        • 1970-01-01
        • 2019-10-28
        • 2017-11-23
        • 1970-01-01
        • 2018-11-05
        • 1970-01-01
        • 2022-10-14
        相关资源
        最近更新 更多