为什么我不认识视频的播放地址了?

不知道从何时起,各大主流网站的视频地址不再是我们想下载就能下载的mp4视频地址。

image.png
image.png

image.png

而是一个形如blob:<origin>/<uuid>的不知名地址,如:

blob:https://www.bilibili.com/2d2f8de5-0e42-4044-aeb0-6db9ff195550

当我们尝试使用新的标签页打开这个地址时,却只会得到如下的结果:

image.png

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 等。

视频播放器的工作原理

播放器播放视频主要会经历以下的几个流程:

  1. 解封装

首先我们需要从文件容器中分离音频和视频压缩编码数据。例如我们可以通过对 FLV 格式的文件进行解封装,得到 H.264 编码的视频数据和 AAC 编码的音频数据。

  1. 解码

然后我们需要将视频,音频压缩编码数据,还原成非压缩的视频,音频数据。例如我们可以对H.264编码压缩的视频数据进行解码得到非压缩的视频颜色数据如 YUV或RGB ,以及对AAC 编码的音频数据解码得到非压缩的音频数据 PCM 等。

  1. 渲染及音视频同步

最后,将解码出来的音频和视频数据分别送至系统声卡和显卡进行播放。同时还需要进行音视频同步。

流媒体播放原理 —— Media Source Extensions API

虽然浏览器自带的播放器<video>标签已经拥有解封装和解码的功能,我们只需要提供一个MP4或者是WEBM格式的视频地址给<video>标签,<video>标签就能播放这个视频。

但是<video>标签支持的媒体封装格式是十分有限的(W3C标准中只支持MP4),同时它也只能满足一次播放整个曲目的需要,无法实现拆分/合并数个缓冲文件。

这时候,MSE(Media Source Extensions)就应运而生了。

MSE 提供了将单个媒体文件的 src 值替换成引用 MediaSource 对象(一个包含即将播放的媒体文件的准备状态等信息的容器),以及引用多个 SourceBuffer 对象(代表多个组成整个串流的不同媒体块)的元素。

image.png

简单来说,就是开发者能够动态构造 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,已经附加到媒体元素上,并准备好将数据附加SourceBuffersourceBuffers
  • 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 兼容性

image.png

除了IE和safari(没错,safari也不支持,但他能够原生支持hls)之外,兼容性还是不错的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容