转眼间,2023 年已进入下半场,在这样一个时间节点下,长视频平台如爱奇艺、优酷、腾讯视频等,以及短视频平台如抖音、快手等,对大家来说早已是司空见惯的事物。然而,在我们追剧、刷弹幕的时候,很少有人会去深入思考这些平台背后的技术奥秘。直到最近,我需要在前端项目中实现视频播放时,我终于意识到,这些视频不仅在格式上存在着差异,甚至连播放形式都各有不同。举个例子,当下最为火热的 “直播”,通常是指实时的视频播放。相对应地,非实时的视频播放则被称为 “点播”。如果你有接触过 Netflix,你或许还听说过 “流媒体” 这个词汇。为了更好地理解这些概念,我利用空闲时间整理了一个相对完整的技术体系,并以此为基础撰写了今天这篇文章。
从 HTML5 说起
好了,现在让我们从最简单的视频播放方案开始说起。在 HTML5 标准发布前,主流的视频播放方案是使用 Adobe 的 Flash Player 插件,国内的优酷、土豆等视频网站创立初期都经历过这个阶段。后来,随着乔布斯那封 “关于 Flash 的思考” 的公开信的发表,某种意义上加速了整个 Flash 技术的 “消亡”。再后来,随着 HTML5 标准发布,我们可以使用 video
或者 audio
标签在网页中呈现音/视频内容。如图所示,下面是 video
标签的基本用法:
<video controls>
<source src="myVideo.mp4" type="video/mp4">
<source src="myVideo.webm" type="video/webm">
<p>Your browser doesn't support HTML5 video. Here is
a <a href="myVideo.mp4">link to the video</a> instead.</p>
</video>
具体来讲,这个 video
标签可以支持 Ogg
、MPEG4
和 WebM
三种视频格式。可惜,并不是所有的浏览器都支持这些格式,因此,你可以在 video
标签内指定多个视频源,并且当这些视频源都不被支持的时候,你可以使用一个自定义的 HTML 结构来进行降级处理。需要注意的是,MPEG-4
即 MP4
格式,实际上是一组格式,因为在视频处理中通常还涉及到编码器的问题。可不幸的是,浏览器目前唯一支持的编码器是 H.264
,在这种情况下,可选择的视频格式将会变得非常有限。
综上所述,这种方案的缺点主要体现在:可选择的视频格式、以及这些视频格式的兼容性问题上。在大多数情况下,你可以使用一个静态文件服务来承载这些视频文件,这意味着每次浏览器都会下载完整的视频文件。
[HttpGet("download")]
public ActionResult Download([FromQuery] string fileName)
{
var filePath = Path.Combine(_hostEnvironment.ContentRootPath, "Assets", fileName);
if (!System.IO.File.Exists(filePath)) return NotFound();
var contentType = GetContentType(filePath);
var fileStream = System.IO.File.OpenRead(filePath);
return File(fileStream, contentType, enableRangeProcessing: true);
}
事实上,你还可以使用带 Range
请求头的请求方式,来实现类似断点续传或者分段下载的效果。如图所示,可以注意到,分段下载返回的状态码为 206
,客户端通过 Range
字段告知服务端自己希望下载文件的哪一部分,而服务器则通过 Content-Range
字段告知客户端当前返回是文件的哪一部分,以及整个文件的大小:
RTMP 还是 RTSP?
虽然,从易用性的角度来看,HTML5 中的 video
标签显得平易近人,可遗憾的是,我们到现在为止仅仅看到是视频播放领域的冰山一角。举一个简单的例子,当我们通过电视收看电视台的节目时,有时会遇到现场直播的场景。在此场景下,人们对视频播放有了实时性的要求。如下图所示,常见的流媒体协议,大体上可以分为三类:
- 传统的视频流协议:以 RTMP 和 RTSP 为代表
- 基于 HTTP 的自适应协议:以 HTTP-FLV、HLS、MPEG-DASH 为代表
- 新技术:以 WebRTC 和 SRT 为代表
在通常的认知中,直播使用 RTMP 或者 RTSP,点播使用 HTTP。当然,工程上的问题没有绝对,随着近年来直播行业的持续火爆,理论上上述协议中的任意一种都可以实现直播。不过,在此之前,我们最好还是来了解一下,目前应用最为广泛的 RTMP 和 RTSP 。在这个过程中,你或许会深刻地感受到技术更迭带来的那种沧桑感。
RTMP,即:Real Time Messaging Protocol,是一种以 TCP 协议作为底层协议的实时消息协议,采用了 H.264 视频编解码器 以及 ACC 音频编解码器。时间追溯到 2005 年,Macromedia 这家公司制定并开发了 RTMP 协议,其目的是为了在 RTMP 服务器和用户设备上的 Flash 播放器之间传输流式数据。后来,Adobe 收购了 Macromedia,Flash 技术得到了进一步的发展,相继诞生了 ActionScript、Adobe AIR 等产品。再后来,乔布斯那封 “关于 Flash 的思考” 的公开信,像一篇檄文一样直指 Flash 的种种缺点,冥冥中 “加速” 了 Flash 的消亡。当然,从 “事后诸葛亮” 的角度来看,我们可以说那是乔帮主的又一次的成功预言。可这世上的事情总是难以捉摸,就像 Flash 早已于 2020 年宣布死亡,可 RTMP 如今还活着,孰是孰非,有时候真的有没有那么重要吗?
如图所示,是一个 RTMP 的基本流程。可以注意到,其核心在于 “推流” 和 “拉流”,简单来说,我们可以通过 FFmpeg 或者 OBS 来向一个 RTMP 服务器推送视频流。此时,运行在用户设备上的 Flash 播放器或者流媒体播放器就可以从 RTMP 服务器上拉取视频流,进而实现视频播放的效果。相较于 HTTP 这种 “平民化” 的协议,RTMP 这种视频流协议明显要更复杂一点。幸运的是,我们可以借助 Nginx 及其第三方模块 nginx-http-flv-module、nginx-rtmp-module 快速搭建一个 RTMP 服务器,这里以 nginx-http-flv-module 为例来进行说明:
rtmp {
server {
listen 1935;
application pushLive {
live on;
gop_cache on;
}
}
}
首先,通过 rtmp
节点下的一系列配置,我们让 Nginx 摇身一变成为一个 RTMP 服务器,如前文所述,RTMP 服务器使用 1935 端口。在此基础上,我们增加一个名为 pushLive
的应用,这一步的目的是对外暴露一个推送视频流的路由。一个 RTMP 服务器下可以同时配置多个应用,其中,live on;
指令表示开启直播,gop_cache on;
指令表示开启 GOP 的缓存。那么,GOP 又是什么呢?通常情况下,它是视频流中两个关键帧之间的距离。考虑到解码器必须拿到 GOP 才能开始解码、播放,因此,缓存 GOP 一定程度上可以降低延迟。
http {
server {
location /pullLive {
flv_live on;
chunked_transfer_encoding on;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
add_header Cache-Control no-cache;
}
}
}
接下来,我们需要在 Nginx 服务器里配置一个拉取视频流的路由,这里拉取的是 FLV 格式的视频流。考虑到视频流的推送方和拉取方通常位于不同的域,因此,还需要为 Nginx 配置跨域所需的头部字段。至此,我们搭建出了一个简单的 RTMP 服务器。
现在,我们只需要分别在 OBS 和 VLC 配置推流、拉流的地址。这里的推流码以及 stream
字段,可以理解为直播的频道或者房间号。只要双方都在一个频道或者房间里,即可实现直播效果:
好的,介绍完 RTMP,现在再来介绍一下 RTSP,它表示的是实时流协议,即:Real Time Streaming Protocol。该协议于 1996 年问世,由 RealNetworks、Netscape和哥伦比亚大学的专家共同开发。RTSP 使用 TCP 和 UDP 作为底层协议,其中,TCP 负责传输控制指令,如播放和停止,而 UDP 负责传输音频、视频和其他数据。与 RTMP 类似,RTSP 采用了 ACC 作为音频编码器,采用 H.265 作为视频编码器。考虑到,RTMP 是Adobe 的专有协议,而 RTSP 是一种公共协议。因此,在传统的安防、监控等垂直领域中,人们更倾向于使用 RTSP ,通常在集成这类产品时,都需要集成厂商提供的 SDK 或安装相应的浏览器插件。
ffmpeg -re -rtsp_transport tcp -i <rtsp地址> -vcodec libx264 -vprofile baseline -acodec aac -ar 44100 -strict -2 -ac 1 -f flv -s 1280x720 -q 10 <rtmp推流地址>
我个人认为,RTSP 唯一的优势在于更低的延迟,如果让我来选的话,我还是会选择 RTMP。数学上有种重要的思维,将一个从未解决过的问题,转化为一个解决过的问题。此时,我们就可以使用上面的命令将一个 RTSP 视频流转换为 RTMP 视频流,这样,我们就可以完全不用理会厂商提供的 SDK 或者浏览器插件。毕竟,ActiveX、OLE 控件早就该扫进历史的垃圾堆啦。当然,除了 FFmpeg,你还可以使用 VLC 播放器进行推流转换,大家有兴趣的话可以自行尝试。
Flash 的折戟沉沙
对于喜欢怀旧的人来说,回顾历史时总不免一阵唏嘘。因为一同被扫进历史垃圾堆的,不单单有的 ActiveX、OLE 控件、Sliverlight、MFC、CGI、SOAP… ,还有一度被 Adobe 寄予厚望的 Flash。2020 年 12 月以后,Adobe Flash Player 将不再受到浏览器的支持,这大抵正式宣告了 Flash 的死亡。有趣的是,当年微软为了对抗 Flash 而推出的 Sliverlight,如今早已是 “王图霸业,血海深仇,尽归尘土”,唯有 HTML5 “桃花依旧笑春风”,这一幕,与金庸先生《天龙八部》里逍遥三老的经历何其相似耶!无独有偶,微软的 IE 浏览器与谷歌的 Chrome 浏览器相爱相杀多年,直到去年 6 月 IE 浏览器正式退役,微软打不过就加入的做法,不免让人担忧,“屠龙少年终成恶龙” 这一幕会再次上演。如果注定要这样的话,那么,我只能选择 Firefox🤦。
言归正传,Flash 的折戟沉沙,直接导致 RTMP 和 RTSP 这对难兄难弟失去了赖以生存的 “土壤”。对于 RTMP 来说,其缺点主要是 FLV 与 HTML5 的兼容性问题、带宽问题,人们依然可以使用那些支持 FLV 格式的播放器软件,例如 PotPlayer、KMPlayer、VLC 等等;可对于 RTSP 来说,其本身与 HTTP 不兼容,必须要借助第三方插件,例如 NPAPI/PPAPI、ActiveX 等等。事实上,这些插件在不同浏览上的兼容性并不能得到充分的保证,这样就造成了一个相对混乱的局面。我以为,最重要的原因是,浏览器或者说前端生态圈,缺少像 FFmpeg 一样强大的视频编/解码工具。不过,随着 WebAssembly 等技术的成熟,我相信这一切终有一天会得到改善!
苹果公司在成功 “驱逐” Flash 以后,转头就开始开发自家的流媒体协议,这便是后来的 HLS 协议,其全称为:HTTP Live Streaming,是一种基于 HTTP 的流媒体传输协议,主要用于实时音/视频流的传输。其基本原理是:首先,它会将整个流切割为无数个可以通过 HTTP 下载的小文件。然后,它会向客户端提供一个配套的媒体文件列表。此时,客户端只需要按照顺序拉取和播放媒体文件,即可实现看起来像是在播放一条完整流媒体的效果。也许,你此前从来没有听说过这些内容,可当你在移动设备上播放 m3u8
格式的视频时,我想告诉你,你已经同 HLS 见过面啦!实际上,这个说法并不太严谨,因为 m3u8
本身就只是一个播放列表,真正的媒体文件其实是那些 ts
文件。考虑到整个传输层是标准的 HTTP 协议,因此,这个方案可以非常方便地利用 CDN 进行分发、加速。
FLV 还是 HLS?
感谢苹果公司,感谢乔布斯,现在我们有了新的选择。类似地,我们可以使用 Nginx 及其第三方模块 nginx-rtmp-module 来搭建一个支持 HLS 协议的流媒体服务器。其基本思路是:通过 OBS 或者 FFmpeg 向 RTMP 服务器推送视频流,服务器端生成对应的 m3u8
以及 ts
文件,此时,只需要像访问静态资源一样访问 m3u8
文件即可。除此之外,你同样可以将事先生成好的 m3u8
以及 ts
文件放置在指定的目录。首先,我们来定义一个用于推送视频流的路由:
rtmp {
server {
listen 1935;
application pushLive {
live on;
hls on;
hls_path /tmp/hls;
hls_fragment 15s;
meta off;
}
}
}
接下来,我们需要定义用于访问 m3u8
资源的路由。其中:alias
指令需要和上文中的 hls_path
指令保持一致。否则,Nginx 可能会找不到这些 m3u8
资源:
http {
server {
location /hls {
types {
application/vnd.apple.mpegurl m3u8;
}
alias /tmp/hls;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
add_header Cache-Control no-cache;
}
}
}
显然,接下来的事情就顺理成章啦!我们通过 OBS 推流,再通过 VLC 播放即可,如下图所示。此时此刻,屏幕前的你,是不是觉得刘海柱更像奇异博士了呢?😺
为了证明我前面讲过的理论,这里放一张容器内 /tmp/hls/
目录下的内容截图,可以注意到,它的确生成了相应的 m3u8
以及 ts
文件:
事实上,关于 ts
文件,其正式的格式名称为 MPEG-TS,视频编码采用了 H.264 格式,音频编码则采用了 AAC、MP3、AC-3 或者 EC-3 格式,这意味着它同样可以支持纯音频格式,即 MPEG 格式的基本音频文件。整体而言,HLS 和上文中提到过的 MPEG-DASH 非常相似,均采用了这种 “切片播放” 的思想,由于每个切片文件都非常小,因此,它在客户端产生的缓存更小、起播更快、响应更快。HLS 最初主要在苹果的 iOS 中流行,后来,逐渐获得安卓平台系统层面的支持,整体来看,在移动端的兼容性相当不错。
曾几何时,乔帮主炮轰 Flash,主要的观点是:Flash 在移动设备上性能差影响电池续航;Flash 是为个人电脑和鼠标设计的,并不适用于触屏和手指。多年以后,回头来再看这段,当年乔布斯的确做了一个正确的决定,越来越多的原生应用被 Web 技术代替,React Native、Flutter、Electron、Taro…等技术的流行,大抵都在证明 Web/HTML5 是大势所趋、是不可违逆的历史潮流。如今,当我们用前端技术去构建 App 和小程序时,这些曾经游走在原生应用中的历史遗留问题,终于要再次浮出水面。
幸运的是,我们生活在一个技术高度发展的年代,作为 Flash 时代的遗产,浏览器本身是不支持 FLV 格式的,直到盛行二次元文化的 B 站开源了 flv.js,这让在前端播放 FLV 格式的视频成为可能,我们继续使用前面 RTMP 直播的示例来演示这部分功能。首先,我们准备一个简单的 HTML 结构,同时在页面中引入 flv.js :
<script src="https://cdn.bootcdn.net/ajax/libs/flv.js/1.5.0/flv.min.js"></script>
<video id="videoElement" controls autoplay width="100%" height="100%"></video>
感谢 flv.js,我们只需要下面几行代码就可以播放一个来自 RTMP 视频流:
<script>
if (flvjs.isSupported()) {
var videoElement = document.getElementById('videoElement');
var flvPlayer = flvjs.createPlayer({
type: 'flv',
isLive:true,
url: 'http://127.0.0.1:50001/pullLive?port=1935&app=pushLive&stream=test'
});
flvPlayer.attachMediaElement(videoElement);
flvPlayer.load();
flvPlayer.play();
}
</script>
视频流在网页中的播放效果,如下图所示,经过博主测试,延迟大概在 15 秒左右:
类似地,对于 HLS 协议的视频流,我们可以使用 hls.js 从前端承载一个实时的视频流:
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<video id="video"></video>
<script>
if (Hls.isSupported()) {
var video = document.getElementById('video');
hls.attachMedia(video);
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
hls.loadSource('http://127.0.0.1:50002/hls/test.m3u8');
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
console.log('manifest loaded, found ' + data.levels.length + ' quality level');
});
});
}
</script>
此时,视频流在网页中的播放效果,如下图所示,测试发现,延迟大概在 15 秒左右:
理论上,HLS 比 FLV 的延迟要高一点,从目前这个结果来看,两者似乎平分秋色,所以,这道题该怎么选呢?我的想法是:在 PC 端选 FLV,在移动端选 HLS。因为国内主流的视频网站,基本上都在用 FLV。诚然,这有些人云亦云,可工程上的解决方案,有哪一个不是一点点摸索出来的,最重要的是适合自己!
WebRTC 的期冀
行文至此,关于流媒体的前世今生,其实已经基本厘清,甚至我们还能从尘埃往事中读出历史惊人的相似性,正如苹果公司开发 HLS 协议是为了挣脱 Flash 的摆布,可即便有乔布斯那样极具远见的战略眼光,依然阻挡不了 HTML5 那如潮水一般涌来的 “大势所趋”。也许,后人有一天会像嫌弃 Flash 一样嫌弃 HLS。技术的进步,始终是一件掺杂着期许和遗憾的事情,甚至于连同此时此刻都永远不是终点。所以,接下来我想聊一聊实时音/视频通信的未来——WebRTC。
回想过去三年的疫情,催生出大量网课/在线教育、视频会议、远程办公……等方面的需求,而 WebRTC 这样一个支持实时音/视频通信的开放标准,天然地拟合着这些业务方向,如前文所述,虽然利用 Nginx 搭建一个流媒体服务器并不算太难,可如果想要在当下的网络环境中做到低延迟,还是需要投入大量的精力去做进一步的优化。关于 WebRTC 的细节,大家可以自行检索,这里推荐一个更成熟的流媒体服务器 SRS,它可以支持本文中提及的所有协议,最最最重要的是它支持 WebRTC 协议。所以,下面我们利用 SRS 来快速搭建一个 WebRTC 的示例,可以让大家提前感受一下 WebRTC 的魅力:
docker run --rm --env CANDIDATE=<Your_IP> -p 1935:1935 -p 8080:8080 -p 1985:1985 -p 8000:8000/udp registry.cn-hangzhou.aliyuncs.com/ossrs/srs:4 objs/srs -c conf/rtmp2rtc.conf
首先,参照官方文档,我们将运行一个支持从 RTMP 转换到 WebRTC 的 SRS 容器实例。顾名思义,它可以将接收到的 RTMP 视频流,以 WebRTC 的形式发布出来。此时,我们只需要在浏览器中建立 WebRTC 连接即可。
docker run --rm -it registry.cn-hangzhou.aliyuncs.com/ossrs/srs:encoder ffmpeg -stream_loop -1 -re -i doc/source.flv -c copy -f flv rtmp://host.docker.internal/live/livestream
接下来,我们同样参照官方示例,运行一个 FFmpeg 的容器示例,它负责将内置的一个 FLV 格式的视频文件推送到 RTMP 服务器上。这里的 RTMP 服务器,其实就是 SRS 容器实例。可以注意到,FFmpeg 确实指向了一个 rtmp://
开头的地址。当然,在掌握了这些原理以后,你可以继续使用 OBS 推送视频流。对程序员来说,不会/记不住命令行这件小事,实在是无伤大雅。因为在真理面前,一切不过都是工具罢了,甚至连你我都是,不是吗?
此时,我们打开浏览器,就可以在 WebRTC 播放器中看到对应的视频。因为主流的浏览器基本都支持 WebRTC ,所以,你不需要再像以前一样借助第三方库或者插件,就可以直接播放视频。从实际的效果来看,除了画质有点模糊以外,整体表现要比 FLV 和 HLS 出色。博主认为,前端值得探索的方向还有更多,譬如 WebAssembly、WebGL、IoT、跨平台/跨端技术等等,如果始终局限在写页面,则永远是没有出路的。最近这几年 “前端已死” 的声音不绝于耳,不知道屏幕前的诸位作何感想呢?
写文章写到凌晨的时候,我的疲倦早已所剩无几,起初选择这个方向的时候,我更多的是关注前端、关注我需要解决的问题。可等我梳理清这些知识脉络以后,我发现它的知识点远比我想象中的密集、庞杂。因此,整篇博客基本上就是在查资料和做实验交替进行的情况下写出来的。本文先后介绍了 HTML5 中的视频播放技术、RTMP/RTSP这类传统的视频流协议、HTTP-FLV/HLS这类基于 HTTP 的自适应协议,以及属于未来的新技术 WebRTC。在这个过程中,博主提到了 Flash 的衰落、HTML5 的崛起,即便你对于技术话题百分百无感,这些互联网世界里的奇闻佚事,或许还有机会成为一种怀念。而对于我说,如何在专业和活泼两者间完成取舍,同样是一件值得思考的事情,就像视频播放总要在带宽、画质、延迟、流量等因素上综合权衡一样。