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

我正在使用Nginx流式传输MJPEG.只要客户端的带宽足够,这种方法就可以正常工作.当带宽不够时,它似乎落后约2分钟,然后它跳到当前帧并开始再次回落.

有没有办法控制缓冲区永远不会存储超过2帧? – 这种方式如果客户端无法跟上,它将永远不会落后一两秒钟?

编辑:基本上服务器(目前在nginx反向代理背后的python龙卷风)正在以5mbit发送流,客户端有1mbit带宽(为了参数) – 服务器(nginx或python)需要能够检测到这一点和丢帧.问题是怎么样的?

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

背景

请记住,从服务器流式传输M-JPEG时,它实际上只是发送一系列JPEG文件,但作为对单个Web请求的响应.也就是说,正常的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开始落后,那么你有两个选择

>丢帧.运行50%所需带宽?请求每隔一帧.
>请求较小的文件.以25%的带宽运行?从服务器请求50%宽度和高度的文件版本.

很久以前,从javascript发出额外的请求会为每个需要新TCP连接的请求带来额外的开销.如果您通过Nginx在服务器上使用Keep-Alive或更好,Spdy或HTTP / 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 ms,则丢弃下一帧并向帧偏移缓冲区添加250 ms.不幸的是,这只会修改帧的发送速率.虽然从长远来看服务器发送的速率和客户端接收的速率是相似的,但在短期内,由于TCP缓冲等原因,它们可能会大不相同.

但是,如果您可以将自己局限于大多数浏览器的最新实现(请参阅support here),那么Websockets应该提供一种良好的机制,用于将服务器上的帧发送到客户端通道,并将客户端上的性能信息发送回服务器通道.此外,Nginx能够proxying Websockets.

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

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

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

点赞