共计 4903 个字符,预计需要花费 13 分钟才能阅读完成。
作为一个从未接触过实时流(直播流)的人,我之前对实时视频一直没有概念,而最近参与的项目刚好有视频监控的需求,在参与技术选型之前,我对前端实时流的展示进行了一下摸底。
概览
实时视频与视频文件不同,有一个流的概念,这里很好理解,因为视频是实时的,需要有一个地方不停地输出视频出来,所以整个视频可以用流来称呼。那么视频可否直接输出到前端页面上呢?可惜,如果可以的话,就没有我这篇文章了。现在摄像头的实时视频流普遍采用的是 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
说了这么多,不如直接上手看看吧:
- 首先我们需要一个新的 nginx 插件:nginx-http-flv-module
- 在 nginx.conf 中进行一些新的配置:
# rtmp server
application myvideo {
live on;
gop_cache: on; #减少首屏等待时间
}
# http server
location /live {flv_live on;}
- 依然用 ffmpeg 来推流,使用上面 RTMP 的命令
- 前端 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 返回数据。
如果对延迟有更高的要求,可以尝试下面的操作:
- 可以配置 flv.js 的
enableStashBuffer
字段,它是 flv.js 用于控制缓存 buffer 的开关,关闭了之后可以做到最小延迟,但由于没有缓存,可能会看到网络抖动带来的视频卡顿。 - 可以尝试关闭 nginx 的 http 配置里的
gop_cache
。gop_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,但还是离不开前文所说的几种协议与方式。如下图是阿里云的直播服务图。可以看到其流程大概分为这几步:
- 采集视频流(主播端使用 RTMP 进行推流)
- 推流到 CDN 节点(上传流)
- CDN 节点转到直播中心,直播中心类似于强大的具有计算能力的中间源,可以提供额外服务诸如落存(录制 / 录制到云存储 / 点播),转码,审核,多种协议的输出等。
- 直播中间分发到 CDN 节点
- 播放(阿里云支持 RTMP、FLV 及 HLS 三种播流协议)