不知道从何时起,各大主流网站的视频地址不再是我们想下载就能下载的mp4视频地址。
而是一个形如
blob:<origin>/<uuid>
的不知名地址,如:
blob:https://www.bilibili.com/2d2f8de5-0e42-4044-aeb0-6db9ff195550
当我们尝试使用新的标签页打开这个地址时,却只会得到如下的结果:
URL.createObjectURL()
如果遇到过预览本地图片的需求的话,那么你对 URL.createObjectURL()
一定不陌生。他的使用方式如下:
const objectURL = URL.createObjectURL(object);
URL.createObjectURL()
会接收一个object对象参数,这个object可以是File
对象、Blob
对象或者 MediaSource
对象。然后他会返回一个形如blob:<origin>/<uuid>
的URL。这个URL指向了我们传入的object对象参数,同时这个 URL 的生命周期和创建它的窗口中的 document
绑定。
图片预览功能:
<input id="selectInput" type="file" ></input>
<img id="previewImg" />
$("#selectInput").on("change",function(){
$("#previewImg").attr("src",URL.createObjectURL($(this)[0].files[0]));
});
需要小心的是,在每次调用 createObjectURL()
方法时,都会创建一个新的 URL,而这个URL指向的对象会驻留在内存当中。我们前面有提到 URL 的生命周期和创建它的窗口中的 document是绑定的,不做任何处理的话,浏览器只有在document卸载时,才会释放这个对象,这很容易会造成内存泄漏的问题。
针对这个问题,我们需要调用URL.revokeObjectURL(url)
,URL.revokeObjectURL(url)
会从内部映射中删除这个对象的引用,当这个对象没有其他引用时,浏览器就能释放内存。
看到这里,我们大概能猜到这些blob url的来源了。
难道?他们是把mp4文件下载下来然后再生成blob???(狗头
当然不是,在介绍URL.createObjectURL()
接收的参数时,我们说过,这个参数可以是File对象、Blob对象或者 MediaSource
对象。
这些视频的blob url其实都指向了一个 MediaSource
对象。
在介绍Media Source Extensions API之前,我们先来了解一些概念。
编码
我们知道,原始的媒体文件的数据量都是十分庞大的,而为了便于传输和存储,我们通常会编码来对媒体文件进行压缩,这里就涉及到了编码格式
的概念。
我们常见的视频编码格式有 H.264、MPEG-4、MPEG-2,VC-1 等,而常见的音频编解码格式有 AAC,MP3,AC-3 等。
媒体封装格式
一个视频内除了包含编码压缩后视频数据之外,还能够包含音频、字幕等数据,把这些数据都包装到一个文件容器内,就是容器封装的过程。我们常用的封装格式有: MP4,MOV,TS,FLV,MKV 等。
视频播放器的工作原理
播放器播放视频主要会经历以下的几个流程:
- 解封装
首先我们需要从文件容器中分离音频和视频压缩编码数据。例如我们可以通过对 FLV 格式的文件进行解封装,得到 H.264 编码的视频数据和 AAC 编码的音频数据。
- 解码
然后我们需要将视频,音频压缩编码数据,还原成非压缩的视频,音频数据。例如我们可以对H.264编码压缩的视频数据进行解码得到非压缩的视频颜色数据如 YUV或RGB ,以及对AAC 编码的音频数据解码得到非压缩的音频数据 PCM 等。
- 渲染及音视频同步
最后,将解码出来的音频和视频数据分别送至系统声卡和显卡进行播放。同时还需要进行音视频同步。
流媒体播放原理 —— Media Source Extensions API
虽然浏览器自带的播放器<video>标签已经拥有解封装和解码的功能,我们只需要提供一个MP4或者是WEBM格式的视频地址给<video>标签,<video>标签就能播放这个视频。
但是<video>标签支持的媒体封装格式是十分有限的(W3C标准中只支持MP4),同时它也只能满足一次播放整个曲目的需要,无法实现拆分/合并数个缓冲文件。
这时候,MSE(Media Source Extensions)就应运而生了。
MSE 提供了将单个媒体文件的 src 值替换成引用 MediaSource 对象(一个包含即将播放的媒体文件的准备状态等信息的容器),以及引用多个 SourceBuffer 对象(代表多个组成整个串流的不同媒体块)的元素。
简单来说,就是开发者能够动态构造 MediaSource 对象,然后通过 URL.createObjectURL()
喂给<video>和<audio>标签。
这就给我们带来了巨大的想象空间了。比如说:
- 我们能够将原本播放器不支持的封装格式实时转封装为它能够支持的封装格式再喂给播放器。(flv.js原理)
- 我们还能让服务端提供多份码率的媒体流,根据当前用户的网络条件选择合适的媒体流喂给播放器,实现码率自适应。(hls.js、dash.js自适应原理)
等等等等。
MediaSource
interface MediaSource : EventTarget {
constructor();
readonly attribute SourceBufferList sourceBuffers;
readonly attribute SourceBufferList activeSourceBuffers;
readonly attribute ReadyState readyState;
attribute unrestricted double duration;
attribute EventHandler onsourceopen;
attribute EventHandler onsourceended;
attribute EventHandler onsourceclose;
static readonly attribute boolean canConstructInDedicatedWorker;
SourceBuffer addSourceBuffer (DOMString type);
undefined removeSourceBuffer (SourceBuffer sourceBuffer);
undefined endOfStream (optional EndOfStreamError error);
undefined setLiveSeekableRange (double start, double end);
undefined clearLiveSeekableRange ();
static boolean isTypeSupported (DOMString type);
};
MediaSource 属性
ReadyState
表示 MediaSource 的当前状态,可选值有:
- closed,表示当前还没有附加到媒体元素上。
- open,已经附加到媒体元素上,并准备好将数据附加
SourceBuffer
到sourceBuffers
。 - ended,仍附加在媒体元素,但已调用了endOfStream()结束当前流。
sourceBuffers
只读属性,返回当前 MediaSource 包含的的 SourceBuffer 的对象列表。
activeSourceBuffers
只读属性,返回当前 MediaSource.sourceBuffers 中的 SourceBuffer 子集的对象,这个对象包含当前被选中的视频轨(video track),启用的音频轨(audio tracks)以及显示/隐藏的字幕轨(text tracks)的对象列表。
duration
获取和设置当前正在推流媒体的持续时间。
在获取时,如果readyState
属性是closed
,则返回 NaN 。
在设置时,如果设置的值是负数或 NaN,则抛出TypeError
异常;如果readyState
属性不是 open
,则抛出InvalidStateError
异常;如果updating
属性等于 true,则抛出InvalidStateError
异常;
MediaSource 方法
addSourceBuffer(mineCodes)
根据传入的mineCodes,创建一个新的 SourceBuffer 并添加到 MediaSource 的 SourceBuffers 列表。
removeSourceBuffer(sourceBuffer)
从 SourceBuffers 列表中移除指定的 SourceBuffer。
endOfStream(endOfStreamError?)
向 MediaSource 发送结束信号,可接收 endOfStreamError 参数,表示到达流末尾时将抛出的错误。
enum EndOfStreamError {
"network", // 终止播放并发出网络错误信号。
"decode" // 终止播放并发出解码错误信号。
};
MediaSource事件
sourceopen
readyState从 closed 变成 open 或从 ended 变成 open 时触发 。
sourceended
readyState从 open 变为 ended 时触发。
sourceclose
readyState从 open 变为 closed 或从 ended 变为 closed 时触发
MediaSource 静态方法
isTypeSupported(mineCodes)
返回一个 Boolean 值表明当前浏览器播放器是否支持给定的 mineCodes 类型。为 true 是表示可能支持,但不一定支持。
SourceBuffer
interface SourceBuffer : EventTarget {
attribute AppendMode mode;
readonly attribute boolean updating;
readonly attribute TimeRanges buffered;
attribute double timestampOffset;
readonly attribute AudioTrackList audioTracks;
readonly attribute VideoTrackList videoTracks;
readonly attribute TextTrackList textTracks;
attribute double appendWindowStart;
attribute unrestricted double appendWindowEnd;
attribute EventHandler onupdatestart;
attribute EventHandler onupdate;
attribute EventHandler onupdateend;
attribute EventHandler onerror;
attribute EventHandler onabort;
undefined appendBuffer (BufferSource data);
undefined abort ();
undefined changeType (DOMString type);
undefined remove (double start, unrestricted double end);
};
SourceBuffer 属性
mode
mode 可以被设置为segments或者sequence。在不同的mode值下,SourceBuffer会用不同的方式去处理添加进来的数据。
segments: 媒体片段中的时间戳决定了各个媒体片段的播放顺序。可以按任何顺序附加媒体片段,但播放顺序只会只会依赖时间戳。
sequence:媒体片段添加顺序决定了播放顺序,播放顺序不受媒体片段的时间戳影响。
该属性的初始值在调用 addSourceBuffer() 时设置,如果媒体片段有时间戳设置为 segments,否则 sequence。,可以通过 changeType() 或设置该属性进行更新。
updating
只读属性,用于说明调用的 appendBuffer() 或 remove() 是否仍在处理中。
buffered
只读属性,用于表示 SourceBuffer 缓冲了哪些 TimeRanges。
timestampOffset
用于控制媒体片段的时间戳偏移量,默认是 0。
audioTracks
只读属性,返回当前包含的 AudioTrack 的 AudioTrackList 对象。
videoTracks
只读属性,返回当前包含的 VideoTrack 的 VideoTrackList 对象。
textTracks
只读属性,返回当前包含的 TextTrack 的 TextTrackList 对象。
appendWindowStart
设置或获取 append window 的开始时间戳
appendWindowEnd
设置或获取 append window 的结束时间戳
Append Window 使用appendWindowStart 和 appendWindowEnd来表示一个时间范围,在追加编码帧时,编码帧的时间戳在这个时间范围内,那么这个编码帧就能附加到 SourceBuffer中,否则将被过滤掉
SourceBuffer 方法
appendBuffer(source)
添加媒体数据片段(ArrayBuffer 或 ArrayBufferView)到 SourceBuffer。
abort
中止对当前媒体片段数据的操作,并重置解析器。调用后 updating 属性回重置为 false。
changeType
更改当前关联的 MIME 类型。
remove(start, end)
移除指定范围的媒体数据。
SourceBuffer 事件
updatestart
当updating 从 false 变为 true 时触发。
update
appendBuffer 或 remove 的操作已经成功完成,updating 从 true 变为 false 时触发。
updateend
appendBuffer 或 remove 的操作已经结束,在 update 事件之后触发。
error
执行 appendBuffer 时发生了错误,updating 从 true 变为 false 时触发。
abort
appendBuffer 或 remove 被 abort() 方法中断,updating 从 true 变为 false 时触发。
使用示例
var video = document.querySelector('#mse-video');
var mineCodes = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
if (window.MediaSource && MediaSource.isTypeSupported(mineCodes)) {
// 检测当前环境是否支持 MediaSource API以及是否支持此mineCodes
var mediaSource = new MediaSource();
// 使用 mediaSource对象创建blob url,并赋给video.src
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
console.log("The Media Source Extensions API is not supported.")
}
function sourceOpen(e) {
// URL.revokeObjectURL 主动释放引用
URL.revokeObjectURL(video.src);
var mediaSource = e.target;
// addSourceBuffer根据传入的mineCodes,创建一个新的 SourceBuffer 并添加到 MediaSource 的 SourceBuffers 列表
var sourceBuffer = mediaSource.addSourceBuffer(mineCodes);
var videoUrl = 'video.mp4';
fetch(videoUrl)
.then(function(response) {
return response.arrayBuffer();
})
.then(function(arrayBuffer) {
sourceBuffer.addEventListener('updateend', function(e) {
if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
// 数据添加完毕后,调用endOfStream结束当前流
mediaSource.endOfStream();
}
});
// 将媒体数据添加到sourceBuffer中
sourceBuffer.appendBuffer(arrayBuffer);
});
}
当 MediaSource.readyState 的值是 ended 时,再次去调用 appendBuffer() 和 remove() 或设置 mode 和 timestampOffset 时,都将会让 readyState 变为 open,并触发 sourceopen 事件。若不需要监听后续操作 SourceBuffer 导致的 sourceopen 事件的话,应只监听首次 sourceopen 事件,然后移除对 sourceopen 事件的监听
MSE 兼容性
除了IE和safari(没错,safari也不支持,但他能够原生支持hls)之外,兼容性还是不错的。