乐趣区

RTSP不存在的-前端实时流探索记

作为一个从未接触过实时流(直播流)的人,我之前对实时视频一直没有概念,而最近参与的项目刚好有视频监控的需求,在参与技术选型之前,我对前端实时流的展示进行了一下摸底。

概览

实时视频与视频文件不同,有一个流的概念,这里很好理解,因为视频是实时的,需要有一个地方不停地输出视频出来,所以整个视频可以用流来称呼。那么视频可否直接输出到前端页面上呢?可惜,如果可以的话,就没有我这篇文章了。现在摄像头的实时视频流普遍采用的是 RTSP 协议,而前端并不能直接播放 RTSP 的视频流。

  • RTSP(Real-Time Stream Protocol),是 TCP/UDP 协议体系中的一个应用层协议,跟 HTTP 处在同一层。RTSP 在体系结构上位于 RTP 和 RTCP 之上,它使用 TCP 或者 RTP 完成数据传输。RTSP 实时效果非常好,适合视频聊天、视频监控等方向。

那么我们就需要一层中间层,来将 RTSP 流转成前端可以支持的协议,这也引申出了目前实时流技术的几种方向:

  • RTSP -> RTMP
  • RTSP -> HLS
  • RTSP -> RTMP -> HTTP-FLV

RTMP

RTMP(Real Time Messaging Protocol)是属于 Adobe 的一套视频协议,这套方案需要专门的 RTMP 流媒体,并且如果想要在浏览器上播放,无法使用 HTML5 的 video 标签,只能使用 Flash 播放器。(通过使用 video.js@5.x 以下的版本可以做到用 video 标签进行播放,但仍然需要加载 Flash)。它的实时性在几种方案中是最好的 ,但是由于只能使用 Flash 的方案,所以在移动端就直接 GG 了,在 PC 端也是明日黄花。
由于下面的两种方法也需要用到 RTMP,所以这里就展示一下 RTSP 流如何转换成 RTMP,我们使用 ffmpeg+Nginx+nginx-rtmp-module 来做这件事:

# 在 http 同一层配置 rtmp 协议的相关字段
rtmp {
    server {
          # 端口
        listen 1935;
            # 路径
        application test {
            # 开启实时流模式
            live on;
            record off;
        }
    }
}
# bash 上执行 ffmpeg 把 rtsp 转成 rtmp,并推到 1935 这个端口上
ffmpeg -i "rtsp://xxx.xxx.xxx:xxx/1" -vcodec copy -acodec copy -f flv "rtmp://127.0.0.1:1935/live/"

这样我们就得到了一个 RTMP 的流,我们可以直接用 VLC 或者 IINA 来播放这个流。

HLS

HLS(HTTP Live Streaming)是苹果公司提出的基于 HTTP 协议的的流媒体网络传输协议,它的工作原理是把整个流分成一个个小的基于 HTTP 的文件来下载,每次只下载一些。HLS 具有跨平台性,支持 iOS/Android/ 浏览器,通用性强。但是它的实时性差:苹果官方建议是请求到 3 个片之后才开始播放。所以一般很少用 HLS 做为互联网直播的传输协议。假设列表里面的包含 5 个 ts 文件,每个 TS 文件包含 5 秒的视频内容,那么整体的延迟就是 25 秒。苹果官方推荐的小文件时长是 10s,所以这样就会有 30s(n x 10)的延迟。
下面是 HLS 实时流的整个链路:

从图中可以看出来我们需要一个服务端作为编码器和流分割器,接受流并不断输出成流片段(stream),然后前端再通过一个索引文件,去访问这些流片段。那么我们同样可以使用 nginx+ffmpeg 来做这件事情。

# 在 rtmp 的 server 下开启 hls
# 作为上图中的 Server,负责流的处理
application hls{
        live on;
        hls on;
         hls_path xxx/; #保存 hls 文件的文件夹
        hls_fragment 10s;
}
# 在 http 的 server 中添加 HLS 的配置:
# 作为上图中的 Distribution,负责分片文件和索引文件的输出
location /hls {
      # 提供 HLS 片段,声明类型
      types {
        application/vnd.apple.mpegurl m3u8;
        video/mp2t ts;
      }
      root /Users/mark/Desktop/hls; #访问切片文件保存的文件夹
      # Cache-Controll no-cache;
      expires -1;
}

然后同样使用 ffmpeg 推流到 hls 路径上:

ffmpeg -i "rtsp://xxx.xxx.xxx:xxx/1" -vcodec copy -acodec copy -f flv rtmp://127.0.0.1:1935/hls

这个时候可以看到文件夹里已经有许多流文件存在,且不停地更新:

然后我们可以使用 video.js+video.js-contrib-hls 来播放这个视频:

<html>
<head>
<title>video</title>
<!-- 引入 css -->
<link href="https://unpkg.com/video.js/dist/video-js.min.css" rel="stylesheet">


</head>
<body>
<div class="videoBox">
    <video id="video" class="video-js vjs-default-skin" controls>
        <source src="http://localhost:8080/hls/test.m3u8" type="application/x-mpegURL"> 
    </video>
</div>

</body>
</html>
<script src="https://unpkg.com/video.js/dist/video.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/videojs-contrib-hls/5.15.0/videojs-contrib-hls.min.js"></script>
<script>
videojs.options.flash.swf = "./videojs/video-js.swf"
        videojs('video', {"autoplay":true}).play();
</script>

在我的测试下,HLS 的延迟在 10-20 秒左右,我们可以通过调整切片的大小来减少延迟,但是由于架构的限制,延迟是一个不可忽视的问题。

HTTP-FLV

接下来就是重头戏 HTTP-FLV 了,它集合了 HLS 的通用性和 RTMP 的实时性,可以做到在浏览器上用 HTML5 的 video 标签,以较低的延时播放实时流。HTTP-FLV 依靠 MIME 的特性,根据协议中的 Content-Type 来选择相应的程序去处理相应的内容,使得流媒体可以通过 HTTP 传输。除此之外,它可以通过 HTTP 302 跳转灵活调度 / 负载均衡,支持使用 HTTPS 加密传输,也能够兼容支持 Android,iOS 等移动端。HTTP-FLV 本质上是将流转成 HTTP 协议下的 flv 文件,在 Nginx 上我们可以使用 nginx-http-flv-module 来将 RTMP 流转成 HTTP 流。

其实 flv 格式依然是 Adobe 家的格式,原生 Video 标签无法直接播放,但是好在我们有 bilibili 家的 flv.js,它可以将 FLV 文件流转码复用成 ISO BMFF(MP4 碎片)片段,然后通过 Media Source Extensions 将 MP4 片段喂进浏览器。

在支持浏览器的协议里,延迟排序是这样的:RTMP = HTTP-FLV = WebSocket-FLV < HLS
而性能排序是这样的:RTMP > HTTP-FLV = WebSocket-FLV > HLS

说了这么多,不如直接上手看看吧:

  1. 首先我们需要一个新的 nginx 插件:nginx-http-flv-module
  2. 在 nginx.conf 中进行一些新的配置:
# rtmp server
application myvideo {
      live on;
      gop_cache: on; #减少首屏等待时间
}

# http server
location /live {flv_live on;}
  1. 依然用 ffmpeg 来推流,使用上面 RTMP 的命令
  2. 前端 import flv.js,然后使用它来播放
// 前端使用 flv.js,开启实时模式,然后访问这个 nginx 地址下的路径即可
import flvJs from 'flv.js';

export function playVideo(elementId, src) {const videoElement = document.getElementById(elementId);
  const flvPlayer = flvJs.createPlayer({
    isLive: true,
    type: 'flv',
    url: src,
  });
  flvPlayer.attachMediaElement(videoElement);
  flvPlayer.load();}

playVideo('#video', 'http://localhost:8080/live?port=1985&app=myvideo&stream=streamname')

可以看到 flv.js 使用了 video/x-flv 这个 MIME 返回数据。

如果对延迟有更高的要求,可以尝试下面的操作:

  1. 可以配置 flv.js 的 enableStashBuffer 字段,它是 flv.js 用于控制缓存 buffer 的开关,关闭了之后可以做到最小延迟,但由于没有缓存,可能会看到网络抖动带来的视频卡顿。
  2. 可以尝试关闭 nginx 的 http 配置里的 gop_cachegop_cache 又称关键帧缓存,其意义是控制视频的关键帧之间的缓存是否开启。

这里引入了一个关键帧的概念:我们使用最广泛的 H.264 视频压缩格式,它采用了诸如帧内预测压缩 / 帧间预测压缩等压缩方案,最后得到了 BPI 三种帧:

  • I 帧:关键帧,采用帧内压缩技术。
  • P 帧:向前参考帧,在压缩时,只参考前面已经处理的帧,表示的是当前帧画面与前一帧(前一帧可能是 I 帧也可能是 P 帧)的差别。采用帧间压缩技术。
  • B 帧:双向参考帧,在压缩时,它即参考前面的帧,又参考它后面的帧。B 帧记录的是本帧与前后帧的差别。采用帧间压缩技术。

带有 I 帧、B 帧和 P 帧的典型视频序列。P 帧只需要参考前面的 I 帧或 P 帧,而 B 帧则需要同时参考前面和后面的 I 帧或 P 帧。由于 P/B 帧对于 I 帧都有直接或者间接的依赖关系,所以播放器要解码一个视频帧序列,并进行播放,必须首先解码出 I 帧。假设 GOP(就是视频流中两个 I 帧的时间距离)是 10 秒,也就是每隔 10 秒才有关键帧,如果用户在第 5 秒时开始播放,就无法拿到当前的关键帧了。这个时候 gop_cache 就起作用了:gop_cache 可以控制是否缓存最近的一个关键帧。开启 gop_cache 可以让客户端开始播放时,立即收到一个关键帧,显示出画面,当然,由于增加了对上一个帧的缓存,所以延时自然就变大了。如果对延时有更高的要求,而对于首屏时间 / 播放流畅度的要求没那么高的话,那么可以尝试关闭 gop_cache,来达到低延时的效果。

思考

延迟与卡顿

实时视频的延时与卡顿是视频质量中最重要的两项指标。然而,这两项指标从理论上来说,是一对矛盾的关系——需要更低的延时,则表明服务器端和播放端的缓冲区都必须更短,来自网络的异常抖动容易引起卡顿;业务可以接受较高的延时时,服务端和播放端都可以有较长的缓冲区,以应对来自网络的抖动,提供更流畅的体验。

直播厂商是怎么做的?

现在各个直播平台基本上都放弃了以上这些比较传统的方式,使用了云服务商提供的 CDN,但还是离不开前文所说的几种协议与方式。如下图是阿里云的直播服务图。可以看到其流程大概分为这几步:

  1. 采集视频流(主播端使用 RTMP 进行推流)
  2. 推流到 CDN 节点(上传流)
  3. CDN 节点转到直播中心,直播中心类似于强大的具有计算能力的中间源,可以提供额外服务诸如落存(录制 / 录制到云存储 / 点播),转码,审核,多种协议的输出等。
  4. 直播中间分发到 CDN 节点
  5. 播放(阿里云支持 RTMP、FLV 及 HLS 三种播流协议)

退出移动版