前一篇文章 使用 Media Source Extensions 播放视频 我们大致写了 Media Source Extensions, MSE 的基本介绍和使用这篇文章是在之前基础上完成的, 文章将实现如何借助 MSE 来播放流文件比如 m3u8 或者 dash
自己之前在知乎上回答过这个问题
有支持 M3U8 格式的 html5 播放器吗?
有简单说一些基本实现思路, 但是没有贴实现的代码, 因为已经有很多前端开源的播放器了比如 hls.js, 不过今天这篇文章会贴出一些基本的代码来实现这块逻辑;
了解 m3u8 文件
HLS, HTTP Live Streaming 苹果公司针对 iPhoneiPodiTouch 和 iPad 等移动设备而开发的基于 HTTP 协议的流媒体解决方案在 App Store 中的视频相关的应用, 基本都是应用的此种技术该技术基本原理是将视频文件或视频流切分成小片 (ts) 并建立索引文件(m3u8)
参考上图, HLS 的架构基本都是会将一个完整的视频分割成不同的小视频, 然后通过索引文件 m3u8 建立起联系;
我们可以看下 自己使用 ffmpeg 手动转换的 文件 index.m3u8
- #EXTM3U
- #EXT-X-VERSION:3
- #EXT-X-TARGETDURATION:17
- #EXT-X-MEDIA-SEQUENCE:0
- #EXTINF:11.910889,
- index0.ts
- #EXTINF:16.601022,
- index1.ts
- #EXTINF:5.088756,
- index2.ts
- #EXTINF:9.051311,
- index3.ts
- #EXTINF:7.466289,
- index4.ts
- #EXTINF:14.724022,
- index5.ts
- #EXTINF:13.848089,
- index6.ts
- #EXTINF:3.462022,
- index7.ts
- #EXTINF:14.306911,
- index8.ts
- #EXTINF:10.844889,
- index9.ts
- #EXTINF:6.131533,
- index10.ts
- #EXTINF:7.508000,
- index11.ts
- #EXTINF:10.052378,
- index12.ts
- #EXT-X-ENDLIST
标签说明
标签 | 含义 |
---|---|
EXTM3U | 每个 M3U 文件第一行必须是这个 tag |
EXT-X-TARGETDURATION | 指定最大的媒体段时间长(秒)。所以 #EXTINF 中指定的时间长度必须小于或是等于这个最大值 |
EXTINF | 指定每个媒体段 (ts) 的持续时间(秒),仅对其后面的 URI 有效 |
EXT-X-STREAM-INF | 指定一个包含多媒体信息的 media URI 作为 PlayList,一般做 M3U8 的嵌套使用,它只对紧跟后面的 URI 有效,格式如下: BANDWIDTH:带宽,必须有; PROGRAM-ID:该值是一个十进制整数,惟一地标识一个在 PlayList 文件范围内的特定的描述。一个 PlayList 文件中可能包含多个有相同 ID 的此 tag; CODECS:视频编码格式,不是必须的; RESOLUTION:分辨率; AUDIO:这个值必须和 AUDIO 类别的 “EXT-X-MEDIA” 标签中 “GROUP-ID” 属性值相匹配。 VIDEO:同上 |
EXT-X-ENDLIST | 表示 PlayList 的末尾了,它可以在 PlayList 中任意位置出现,但是只能出现一个 |
EXT-X-MEDIA | 被用来在 PlayList 中表示相同内容的不用语种 / 译文的版本,比如可以通过使用 3 个这种 tag 表示 3 中不用语音的音频,或者用 2 个这个 tag 表示不同角度的 video 在 PlayLists 中。这个标签是独立存在的,属性包含: URI:如果没有,则表示这个 tag 描述的可选择版本在主 PlayList 的 EXT-X-STREAM-INF 中存在; TYPE: AUDIO and VIDEO; GROUP-ID: 具有相同 ID 的 MEDIAtag,组成一组样式; LANGUAGE:确定使用的主要语言; NAME:人类可读的语言的翻译; DEFAULT:YES 或是 NO,默认是 No,如果是 YES,则客户端会以这种选项来播放,除非用户自己进行选择; AUTOSELECT:YES 或是 NO,默认是 No,如果是 YES,则客户端会根据当前播放环境来进行选择(用户没有根据自己偏好进行选择的前提下) |
了解 关于 m3u8 格式 中标记的含义
前端在解析 m3u8 的时候主要是通过正则表达式, 然后获取基本的信息这里不做具体的介绍了, 我们可以使用类库 m3u8-parser
- var playManifest = {};
- function fetchM3u8() {
- var parser = new m3u8Parser.Parser();
- var m3u8url = './video/index.m3u8';
- fetch(m3u8url, {
- })
- .then(function(response) {
- return response.text();
- }).then(function(data) {
- parser.push(data);
- parser.end();
- playManifest = parser.manifest;
- })
- }
这样我们就可以拿到 m3u8 文件的基本信息了
解析 .ts 文件
前面我们之前已经能够读取到我们的 m3u8 文件, 那么也就是我们可以确切的拿到我们媒体资源, 但是我们必须要解决播放 .ts 的文件 这里写过一篇 使用 mux.js 播放 .ts 文件 , 这里我们依旧需要 引入 mux.js 来实现前端的编码工作
首先我们需要在给 video 绑定 mse 对象的时候
- var index = 0;
- // create a transmuxer:
- var transmuxer = new muxjs.mp4.Transmuxer();
- var remuxedSegs = [];
- var remuxedBytesLength = 0;
- var remuxedInitSegment = null;
- var createInitSegment = true;
- var sourceBuffer;
- var video = document.querySelector('.js-player-m3u8');
- if (window.MediaSource) {
- var mediaSource = new MediaSource();
- video.src = URL.createObjectURL(mediaSource);
- mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });
- // 监听 transmuxer 数据添加
- transmuxer.on('data', function (segment) {
- remuxedSegs.push(segment);
- remuxedBytesLength = segment.data.byteLength;
- if (!remuxedInitSegment) {
- remuxedInitSegment = segment.initSegment;
- }
- appendBuffer();
- });
- } else {
- console.log("The Media Source Extensions API is not supported.")
- }
在绑定 video 后, MSE 会触发 open 事件:
- function sourceOpen(e) {
- URL.revokeObjectURL(video.src);
- var mime = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
- var mediaSource = e.target;
- sourceBuffer = mediaSource.addSourceBuffer(mime);
- sourceBuffer.addEventListener('updateend', updateEnd);
- var videoUrl = './video/' + playManifest.segments[index]['uri'];
- log('.js-log-m3u8', 'Fetch Segment ~' + videoUrl);
- fetch(videoUrl, {
- })
- .then(function(response) {
- return response.arrayBuffer();
- })
- .then(function(arrayBuffer) {
- // data events signal a new fMP4 segment is ready:
- transmuxer.push(new Uint8Array(arrayBuffer));
- transmuxer.flush();
- });
- }
我们在前面看到这段代码;
- remuxedSegs.push(segment);
- remuxedBytesLength = segment.data.byteLength;
- if (!remuxedInitSegment) {
- remuxedInitSegment = segment.initSegment;
- }
- appendBuffer();
这个是 transmuxer 中队数据流的监听, 我们其实就是需要将数据进行重新修改, 让它能够在浏览器播放具体细节
接下来就是需要将数据往 MSE 里面填充了:
- var offset = 0;
- function appendBuffer() {
- var bytes = null;
- if (createInitSegment) {
- bytes = new Uint8Array(remuxedInitSegment.byteLength + remuxedBytesLength)
- bytes.set(remuxedInitSegment, offset);
- offset += remuxedInitSegment.byteLength;
- createInitSegment = false;
- } else {
- bytes = new Uint8Array(remuxedBytesLength);
- }
- var i = offset;
- bytes.set(remuxedSegs[index].data, i);
- offset += remuxedSegs[index].byteLength;
- remuxedBytesLength = 0;
- // var sourceBuffer = mediaSource.sourceBuffers[index];
- sourceBuffer.appendBuffer(bytes);
- }
在 MSE 添加完 buffer 后, 我们在触发的 updateend 事件中, 绑定函数, 定义 fetchNextSegment 进行下一个一个分片的请求
- // fetchNextSegment() {...}
- var url = './video/' + playManifest.segments[index]['uri'];
- fetch(url, { headers: { } })
- .then(response => response.arrayBuffer())
- .then(data => {
- // transmuxer.flush();
- transmuxer.push(new Uint8Array(data));
- transmuxer.flush();
- // var sourceBuffer = mediaSource.sourceBuffers[0];
- // sourceBuffer.appendBuffer(data);
- });
同时我们通过监听 index 来判断是否完成媒体资源的加载完成, 触发 Video 播放
- function updateEnd() {
- if (!sourceBuffer.updating && mediaSource.readyState === 'open'
- && index == playManifest.segments.length - 1) {
- mediaSource.endOfStream();
- video.play();
- return;
- }
- // Fetch the next segment of video when user starts playing the video.
- fetchNextSegment();
- }
- Demo
- Github Code
- M3U
- How to Encode Video for HLS Delivery
- m3u8-parser
- Media Source Extensions
来源: https://juejin.im/entry/5aa64acb6fb9a028b6172adf