生活的道路一旦选定,就要勇敢地走到底,决不回头。

发掘积累过程的快感

首页 » BIBLE模型 » Javascript » 浏览器播放m3u8文件

浏览器播放m3u8文件


有简单说一些基本实现思路,但是没有贴实现的代码,因为已经有很多前端开源的播放器了比如 hls.js, 不过今天这篇文章会贴出一些基本的代码来实现这块逻辑;
img

了解 m3u8 文件

HLS, HTTP Live Streaming 苹果公司针对 iPhone、iPod、iTouch 和 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();
}
互联网信息太多太杂,各互联网公司不断推送娱乐花边新闻,SNS,微博不断转移我们的注意力。但是,我们的时间和精力却是有限的。这里是互联网浩瀚的海洋中的一座宁静与美丽的小岛,供开发者歇息与静心潜心修炼。 “Bible”是圣经,有权威的书,我们的本意就是为开发者提供真正有用的的资料。 我的电子邮件 1217179982@qq.com,您在开发过程中遇到任何问题,欢迎与我联系。
Copyright © 2024. All rights reserved. 本站由 Helay 纯手工打造. 蜀ICP备15017444号