一种优雅的视频分享方法

起因

笔者平时也会玩一下摄影和视频剪辑, 所以有时候会想给朋友分享一下. 以前都会通过微信群或者是上传到视频平台去分享, 但是呢, 用微信发, 会被压缩, 也有时间限制, 有时候又不太愿意发到视频平台.

后来有一段时间, 笔者会把整个视频文件放到服务器上, 再直接用NginX代理, 那朋友们就可以直接在浏览器上打开我分享的视频了. 美中不足的是, 当拖动播放条的时候, 需要通过网络重新加载Mp4的文件头, 再重新完成文件寻址和缓存这些动作, 遇到时间比较长码率较大的文件的时候, 播放会有明显的卡顿. 除此之外, 当播放设备处于弱网络下, 也同样会影响播放的流畅性.

所以, 笔者决定, 换一个优雅一点的方案.

方案介绍

我们会使用ffmpeg把视频文件(以H264编码为主)转码成HLS文件, 最后编写出m3u8索引并托管到HTTP服务中, 客户端就可以通过互联网访问我们的m3u8文件, 进而播放视频.

architecture

HLS(HTTP Live Streaming)

针对在线播放这个场景, HLS协议会把源视频文件渲染成多个不同码率的版本, 并对每个版本都分割成多个小文件, 使客户端可以因应不同的网络带宽自动选择合适的播放码率, 实现流畅播放. 此外, 因为视频已经被分割成多个小文件, 当拖动视频进度条的时候, 服务器可以根据进度条上的时间快速找到正确的视频内容, 减少客户端上的等待时间.

adaptive-bitrate-streaming

m3u8

顺应HLS的思路, 当视频文件被分割成多个片段后, 我们在播放的时候自然就需要一个索引去记录每个小文件对应哪个时间段.

在视频直播领域中, 有一种被广泛使用方案, 相信也有不少人见过了.

比如说, 有一个播放链接: https://rondochen.com/m3u8/xihongshi/playlist.m3u8

在2022年的当下, 安卓手机和iphone都可以直接在浏览器上播放, 至于PC平台, 则可能需要浏览器插件的支持.

使用ffmpeg制作m3u8文件

ffmpeg是一个被广泛使用的开源的音视频工具, 具体可以在ffmpeg的官网了解更多.

https://ffmpeg.org/ffmpeg.html

示例文件

笔者在自己的电影服务器随便挑了一部电影, 文件示例信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
rondo@jellyfin:~$ ffmpeg -hide_banner -i /mnt/movie_disk/movie/西虹市首富.2018/西虹市首富.2018.1080p.AAC.mp4  
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/mnt/movie_disk/movie/西虹市首富.2018/西虹市首富.2018.1080p.AAC.mp4':
Metadata:
major_brand : isom
minor_version : 1
compatible_brands: isomavc1
creation_time : 2019-04-13T15:02:15.000000Z
Duration: 01:58:07.94, start: 0.000000, bitrate: 3457 kb/s
Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1920x1080 [SAR 1:1 DAR 16:9], 3357 kb/s, 24 fps, 24 tbr, 24k tbn, 48 tbc (default)
Metadata:
creation_time : 2019-04-13T15:02:15.000000Z
Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 96 kb/s (default)
Metadata:
creation_time : 2019-04-13T13:58:02.000000Z

很明显, 这是一个mp4文件, 视频编码使用h264, 帧率24, 视频码率3357 kb/s, 音频编码AAC, 音频码率96kb/s, 具体还有其他的信息, 这里不细说.

mp4转码成HLS

我们使用下面的命令:

1
2
3
4
ffmpeg -i /mnt/movie_disk/movie/西虹市首富.2018/西虹市首富.2018.1080p.AAC.mp4 \
-vf scale=w=1280:h=720:force_original_aspect_ratio=decrease -c:a aac -ar 48000 -b:a 96k \
-c:v h264 -profile:v main -crf 20 -g 48 -keyint_min 48 -sc_threshold 0 -b:v 2500k -maxrate 2675k \
-bufsize 3750k -hls_time 4 -hls_playlist_type vod -hls_segment_filename /tmp/xihongshi/720p_%03d.ts /tmp/xihongshi/720p.m3u8

部分注释如下:

参数 作用
-i 指定输入的源文件
-vf "scale=w=1280:h=720:force_original_aspect_ratio=decrease" 设置视频的分辨率
-c:a aac -ar 48000 -b:a 96k 设置音频编码, 采样率和码率
-c:v h264 使用H264编码, 而H264也是HLS的标准编码类型
-profile:v main H264编码方案会进一步细分为多种预设, 这里为了保障最广泛的兼容性, 显式地使用main预设
-crf 20 Constant Rate Factor, 取值范围是0-51, 默认是23, 当取值为0时画质损失最少
-g 48 -keyint_min 48 重要, 设置每48帧取关键帧
-b:v 2500k -maxrate 2675k -bufsize 3750k 设置视频的码率
-hls_time 4 设置视频分片的间隔, 每4秒设置一个分片, 注意这个选项受关键帧的设置约束
-hls_segment_filename 指定分片的视频文件存放路径
/tmp/xihongshi/720p.m3u8 指定最终生成的m3u8文件的存放路径

最后, 我们会得到大量的ts文件, 以及一个m3u8目录:

1
2
3
4
5
6
7
8
9
10
11
rondo@jellyfin:/$ tree /tmp/xihongshi 
├── 720p_000.ts
├── 720p_001.ts
├── 720p_002.ts
├── 720p_003.ts
...省略...
├── 720p_027.ts
├── 720p_028.ts
├── 720p_029.ts
...省略...
└── 720p.m3u8

m3u8文件内容类似下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ head -n 15 720p.m3u8 
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:4.000000,
720p_000.ts
#EXTINF:4.000000,
720p_001.ts
#EXTINF:4.000000,
720p_002.ts
#EXTINF:4.000000,
720p_003.ts
#EXTINF:4.000000,
720p_004.ts

同时进行多个分辨率的转码任务

ffmpeg支持多个输入和输出命令, 所以我们可以使用一条长命令同时指定多个不同码率的任务. 但是在编写命令的时候, 最好注意对齐以方便核对参数和检查:

1
2
3
4
5
ffmpeg -hide_banner -y -i /mnt/movie_disk/movie/西虹市首富.2018/西虹市首富.2018.1080p.AAC.mp4 \
-vf scale=w=640:h=360:force_original_aspect_ratio=decrease -c:a aac -ar 48000 -c:v h264 -profile:v main -crf 20 -sc_threshold 0 -g 48 -keyint_min 48 -hls_time 4 -hls_playlist_type vod -b:v 800k -maxrate 856k -bufsize 1200k -b:a 96k -hls_segment_filename /tmp/xihongshi/360p_%03d.ts /tmp/xihongshi/360p.m3u8 \
-vf scale=w=842:h=480:force_original_aspect_ratio=decrease -c:a aac -ar 48000 -c:v h264 -profile:v main -crf 20 -sc_threshold 0 -g 48 -keyint_min 48 -hls_time 4 -hls_playlist_type vod -b:v 1400k -maxrate 1498k -bufsize 2100k -b:a 128k -hls_segment_filename /tmp/xihongshi/480p_%03d.ts /tmp/xihongshi/480p.m3u8 \
-vf scale=w=1280:h=720:force_original_aspect_ratio=decrease -c:a aac -ar 48000 -c:v h264 -profile:v main -crf 20 -sc_threshold 0 -g 48 -keyint_min 48 -hls_time 4 -hls_playlist_type vod -b:v 2800k -maxrate 2996k -bufsize 4200k -b:a 128k -hls_segment_filename /tmp/xihongshi/720p_%03d.ts /tmp/xihongshi/720p.m3u8 \
-vf scale=w=1920:h=1080:force_original_aspect_ratio=decrease -c:a aac -ar 48000 -c:v h264 -profile:v main -crf 20 -sc_threshold 0 -g 48 -keyint_min 48 -hls_time 4 -hls_playlist_type vod -b:v 3000k -maxrate 3350k -bufsize 6500k -b:a 192k -hls_segment_filename /tmp/xihongshi/1080p_%03d.ts /tmp/xihongshi/1080p.m3u8

编写master播放列表

在上面的步骤生成每一个m3u8文件都可以正常播放, 但是为了实现码率和分辨率的自适应, 我们最好再编写一个master播放列表, 保存为playlist.m3u8:

1
2
3
4
5
6
7
8
9
10
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360
360p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1400000,RESOLUTION=842x480
480p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720
720p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3000000,RESOLUTION=1920x1080
1080p.m3u8

最后, 我们会得到5个m3u8文件:

1
2
$ ls *m3u8
1080p.m3u8 360p.m3u8 480p.m3u8 720p.m3u8 playlist.m3u8

使用NginX代理视频文件

对于大多数熟手运维来说, 可能这是最简单的一个步骤了(笑). 简单来说, 只需要两个步骤:

  1. 把视频文件复制到http服务器的目录下, 比方说: /data/m3u8, 那播放列表存放的路径就是: /data/m3u8/xihongshi/playlist.m3u8.

  2. 编写NginX的配置文件, 考虑到不想把文章弄得太新手向, 所以就简单说说关键的配置内容算了:

    1
    2
    3
    4
    5
    6
    7
    location /m3u8 {
    types {
    application/vnd.apple.mpegurl m3u8;
    video/mp2t ts;
    }
    root /data/;
    }
  1. 至于NginX配置上其他关于安全和性能的, 就不在这里唠叨了.

配置完成后, 访问URL http://servername/m3u8/xihongshi/playlist.m3u8即可在浏览器上播放视频了.

拾遗

按照笔者的经历, 配置好了这个m3u8之后, 实现了播放的可行性之后, 接着就考虑到带宽还有流量费用的问题了(笑). 当初为了省钱, 买的是特价的云服务器, 每月流量限额40G, 虽说也够用, 但是带宽却只有5Mbps. 又正好笔者家里的宽带是有公网地址的, 所以解决起来也意外地简单, 直接在云服务器上的NginX写一个301跳转到家里的服务器就可以了, 带宽变大了, 流量还免费.

说回捣鼓HLS的初衷, 就是为了可以兼顾便捷和流畅性, 这个m3u8播放链接的方案也确实够用了, 起码能完全满足这个场景的需求. 至于这个以HLS为基础的方案有什么局限, 以及性能上要如何优化, 拿着关键字在搜索引擎上也能搜到许多, 我也不唠叨了.

拓展阅读

https://support.huaweicloud.com/intl/en-us/mpc_faq/mpc_08_0027.html

https://docs.peer5.com/guides/production-ready-hls-vod/

https://www.zype.com/blog/hls-vs-dash-vs-mp4-vs-mpeg-ts-linear-vs-rtmp-for-streaming

https://ffmpeg.org/ffmpeg.html