在客户端带宽较低的 Nginx 中流式传输 Mjpeg

Streaming Mjpeg in Nginx with low client side bandwidth

我正在使用 Nginx 流式传输 MJPEG。只要客户端的带宽足够,这就可以正常工作。带宽不够的时候,好像落后2分钟左右,然后跳到当前帧,又开始往回掉。

有什么方法可以控制缓冲区永远不会存储超过 2 帧? -- 这样如果客户端跟不上,它永远不会落后超过一两秒?

编辑:基本上服务器(目前 python 龙卷风在 nginx 反向代理后面)以 5mbit 发送流,客户端有 1mbit 带宽(为了论证)- 服务器(nginx 或 python) 需要能够检测到这一点并丢帧。问题是如何?

这在很大程度上取决于您实际部署 M-JPEG 帧的方式,以及您是否必须使用内置的浏览器支持,或者您是否可以编写自己的 javascript。

背景

请记住,当从服务器流式传输 M-JPEG 时,它实际上只是发送一系列 JPEG 文件,但作为对单个 Web 请求的响应。也就是说,一个普通的网络请求看起来像

Client             Server
   | --- Request ---> |
   |                  |
   | <-- JPEG File -- |

虽然请求 M-JPEG 看起来更像

Client             Server
   | --- Request ---> |
   |                  |
   | <- JPEG part 1 - |
   | <- JPEG part 2 - |
   | <- JPEG part 3 - |

所以问题不在于客户端缓冲,而是一旦 M-JPEG 启动,服务器就会发送每一帧,即使下载每一帧的时间比指定的显示时间更长。

纯 JS 解决方案

如果您可以在您的应用程序中编写 javascript,请考虑将应用程序的请求/响应部分显式化。也就是说,对于每个所需的帧,从您的 javascript 向服务器发送一个明确的请求以获取所需的帧(作为单个 JPEG)。如果javascript开始落后,那么你有两个选择

  1. 丢帧。 运行 需要 50% 的带宽?每隔一帧请求一次。
  2. 请求较小的文件。 运行 带宽为 25%?从服务器请求一个 50% 宽度和高度的文件版本。

很久以前,从 javascript 发出额外的请求会引入额外的开销,因为每个请求都需要一个新的 TCP 连接。如果您通过 Nginx 在您的服务器上使用 Keep-Alive 或更好的 SpdyHTTP/2,那么使用 javascript 发出这些请求几乎没有开销。最后,使用 javascript 将允许您实际显式缓冲几帧,并控制缓冲超时。

对于一个非常基本的示例,(使用 jQuery imgload plugin 以便于示例):

var timeout = 250; // 4 frames per second, adjust as necessary
var image = // A reference to the <img> tag for display
var accumulatedError = 0; // How late we are

var doFrame = function(frameId) {
    var loaded = false, timedOut = false, startTime = (new Date()).getTime();
    $(image).bind("load", function(e) {
        var tardiness = (new Date()).getTime() - startTime - timeout;
        accumulatedError += tardiness; // Add or subtract tardiness
        accumulatedError = Math.max(accumulatedError, 0); // but never negative
        if (!timedOut) {
            loaded = true;
        } else {
            doFrame(frameId + 1);
        }
    }
    var timeCallback = function() {
        if (loaded) {
            doFrame(frameId + 1); // Just do the next frame, we're on time
        } else {
            timedOut = true;
        }
    }
    while(accumulatedError > timeout) {
        // If we've accumulated more than 1 frame or error
        // skip a frame
        frameId += 1;
        accumulatedError -= timeout;
    }
    // Load the image
    $(image).src = "http://example.com/images/frame-" + frameId + ".jpg";
    // Start the display timer
    setTimeout(timeCallback, timeout);
}

doFrame(1); // Start the process

为了使这段代码真正无缝,您可能需要使用两个图像标签并在加载完成时交换它们,这样就没有可见的加载伪影(例如 Double Buffering)。

Websocket 解决方案

如果您不能在您的应用程序中写入 javascript,或者您需要高帧率,那么您需要修改服务器以检测它发送帧的速率。例如,假设帧速率为 4 fps,如果写出每个帧需要超过 250 毫秒,则丢弃下一帧并将 250 毫秒添加到帧偏移缓冲区。不幸的是,这只会修改发送帧的速率。虽然服务器发送的速率和客户端接收的速率在长 运行 中相似,但在短 运行 中它们可能由于 TCP 缓冲等原因而完全不同

但是,如果您可以限制自己使用大多数浏览器的最新实现(请参阅 support here) then Websockets should provide a good mechanism for sending frames on the server to client channel and sending back performance information on the client to server channel. In addition, Nginx is capable of proxying Websockets

在客户端,建立一个Websocket。开始从服务器发送 jpeg 帧的速度略快于所需的呈现速率(例如,对于每秒 30 帧,每 20-25 毫秒发送一帧可能是一个很好的起点,如果服务器上有一些缓冲区——没有缓冲区,以最大可用帧速率发送)。在客户端完全接收到每个帧后,将消息发送回服务器,其中包含帧 ID 以及客户端在帧之间经过的时间。

使用从客户端接收到的帧之间的时间,使用与上一个示例相同的方法开始在服务器上累积一个 accumulatedError 变量(从实际帧间时间中减去所需的帧间时间) .当 accumulatedError 达到一帧(甚至可能接近一帧)时,跳过发送帧并重置 accumulatedError.

但是请注意,此解决方案可能会导致视频播放出现卡顿,因为您只会在绝对必要时才跳过一帧,这意味着不会以常规节奏跳过帧。理想的解决方案是将帧发送计时器视为 PID 控制变量,并使用实际帧接收时间作为 PID loop 的反馈。在较长的运行中,PID循环可能会提供最稳定的视频呈现,但accumulatedErrror方法仍应提供令人满意(且相对简单)的解决方案。