自己有个需求,有时候希望能直接下载网站上的视频。以前另存一下就行,但后来发现很多视频网站都不能用这种方法下载了。仔细观察了一下,发现它们都有一个共同点,就是将视频分成大量切片,这种片段的格式也不是常见的, 其中在一种的 URL 都以.ts
结尾,并且有个m3u8
的链接跟这些.ts
的 URL 有一定关系。于是就这样我就又开始愉快地玩了起来。
生成 ts 及 m3u8 文件
经过查阅一些资料,发现这些.ts
文件是可以通过 mp4 等视频格式生成的。以下是ffmpeg
生成.ts
文件以及相关m3u8
播放列表的方法,没错,m3u8
本质上就是一个播放列表。ffmpeg
的安装步骤就不多说了。
$ ffmpeg -i file.ext -c:v libx264 -strict -2 ./test.mp4 # 如果视频为 mp4,可省略该步骤。如果 file.ext 视频不是 mp4,则转为 mp4 格式
$ ffmpeg -y -i test.mp4 -vcodec copy -acodec copy -vbsf h264_mp4toannexb test.ts # 将 mp4 转为 ts 文件(转格式)
$ mkdir segments
$ ffmpeg -i test.ts -c copy -map 0 -f segment -segment_list segments/index.m3u8 -segment_time 10 segments/nxb-%04d.ts # 将 test.ts 文件进行切片,将这些切片文件存放到 segments 目录中,并以 nxb-%04d.ts 的格式命名,再将所有文件名存放到 segments 目录下的 index.m3u8 文件中。-segment_time 10 表示每个切片的时长为 10 秒
在用户上传完视频后,后台执行以上命令行,就生成了视频文件的多个切片nxb-%04d.ts
以及这些切片的播放列表index.m3u8
。在前端播放这个视频时,只需要请求index.m3u8
即可。然后再根据index.m3u8
的列表获取各个切片。以切片的方式获取视频,降低了网络延迟感,不需要等待完整的视频文件传输完毕后才能播放;转而根据视频的播放进度获取相应的片段,大大地提升了用户体验。
index.m3u8
内容格式如下。
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:16
#EXTINF:15.875000,
nxb-0000.ts
#EXTINF:6.125000,
nxb-0001.ts
#EXTINF:10.291667,
nxb-0002.ts
#EXTINF:12.541667,
nxb-0003.ts
#EXTINF:7.000000,
nxb-0004.ts
#EXTINF:12.291667,
nxb-0005.ts
#EXTINF:5.708333,
nxb-0006.ts
#EXT-X-ENDLIST
前端获取 m3u8 视频
下面是前端使用hls.js
请求m3u8
视频资源的示例。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/hls.js@0.13.1/dist/hls.min.js"></script>
</head>
<body>
<video id="v" controls></video>
<script>
var video = document.getElementById('v');
if (Hls.isSupported()) {
var hls = new Hls();
hls.loadSource('http://localhost/segments/index.m3u8');
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function () {
// video.play();
});
}
// hls.js is not supported on platforms that do not have Media Source Extensions (MSE) enabled.
// When the browser has built-in HLS support (check using `canPlayType`), we can provide an HLS manifest (i.e. .m3u8 URL) directly to the video element through the `src` property.
// This is using the built-in support of the plain video element, without using hls.js.
// Note: it would be more normal to wait on the 'canplay' event below however on Safari (where you are most likely to find built-in HLS support) the video.src URL must be on the user-driven
// white-list before a 'canplay' event will be emitted; the last video event that can be reliably listened-for when the URL is not on the white-list is 'loadedmetadata'.
else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = 'http://localhost/segments/index.m3u8';
video.addEventListener('loadedmetadata', function () {
// video.play();
});
}
</script>
</body>
</html>
至于hls.js
还没时间深入研究,但其基本原理也不难理解。但目前还是有以下问题还没想通:
- 在拖动视频进条时,不知道是不是本地的原因,不需要再次请求 ts,缓存?在视频网站貌似会重复请求的;
- video 的 src 的 blob 地址在播放时是固定的,不清楚是用哪个对象生成的 blob;
下载 m3u8 视频
下载 m3u8 视频同样用到 ffmpeg,大概的原理就是根据 m3u8 列表下载,最后合并多个 ts 文件。命令如下:
$ ffmpeg -i http://example.com/index.m3u8 "file.mp4"
以上命令可以将 index.m3u8 文件中的视频列表合并到 file.mp4 文件中。至于具体的解码、编码细节,就不是我熟悉的领域了。