video 为什么有的视频地址是blob开头?

492 阅读7分钟

我们经常在很多直播网站去看视频,你有没有注意到他们是用的什么流?我们去B站随便找一个直播或者视频打开控制台查看播放器,你会发现 video 标签的 src 居然是 blob 开头的一个 url 。

image.png

我找到的是一个普通视频,并不是直播(直播一般是flv或者hls的地址)。拿到 video中的src blob 后面的地址,去浏览器中单独访问是无法访问的。为什么呢?它又是怎么实现的播放的呢?

其实这里用到了 MSE Media Source Extensions技术。通过MSE的方式我们可以处理视频流或者音频流,一般用 MSE 处理做的播放器,我们叫做流式播放器。

Media Source Extensions

媒体源扩展(Media Source Extensions,缩写MSE)是一项W3C规范,MSE允许Javascript为audio标签和video标签动态地构造媒体源。

借助MSE的能力,将接收到的实时流通过 blob url 往video标签中灌入二进制数据(如fmp4格式流),或者使用 canvas 来实现直播。

1、是否支持MediaSource

const supportMediaSource = window.MediaSource && 
typeof window.MediaSource.isTypeSupported === 'function' &&
window.MediaSource.isTypeSupported('video/mp4; codecs="avc1.42c01f,mp4a.40.2"');

if (supportMediaSource) {
    // 支持
}

2、往video blob url 灌入二进制数据

const assetURL = "xxx.MP4";
const mimeCodec = 'video/mp4; codecs="mp4a.40.2,avc1.64002a"';

const video = document.createElement("video");
video.width = 200;
video.controls = true;
document.body.appendChild(video);

const supportMediaSource =
  window.MediaSource &&
  typeof window.MediaSource.isTypeSupported === "function" &&
  window.MediaSource.isTypeSupported(mimeCodec);
console.log(supportMediaSource);

const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener("sourceopen", sourceOpen);

function sourceOpen() {
  // 新建一个 sourceBuffer
  const sourceBuffer = mediaSource.addSourceBuffer(mime);
  // 加载一段 chunk,然后 append 到 sourceBuffer 中
  fetchBuffer(assetURL, (buffer) => {
    sourceBuffer.addEventListener("updateend", function (_) {
      if (!sourceBuffer.updating && mediaSource.readyState === "open") {
        mediaSource.endOfStream();
        video.play();
      }
      console.log(mediaSource.readyState); // ended
    });
    sourceBuffer.appendBuffer(buffer);
  });
}

function fetchBuffer(url, callback) {
  const xhr = new XMLHttpRequest();
  xhr.open("get", url);
  xhr.responseType = "arraybuffer";
  xhr.onload = function () {
    callback(xhr.response);
  };
  xhr.send();
}

大家也可以看官方的demo

3、H.265视频播放

H.265是ITU-T VCEG继H.264之后所制定的新的视频编码标准。H.265标准围绕着现有的视频编码标准H.264,保留原来的某些技术,同时对一些相关的技术加以改进。新技术使用先进的技术用以改善码流、编码质量、延时和算法复杂度之间的关系,达到最优化设置。 但是由于浏览器不支持H265格式的流,所以我们无法直接播放。这时候可以使用MSE的方式在 sourceBuffer.appendBuffer(evt.data) 前将 evt.data 使用libde265.js 等转码库转码后给到sourceBuffer。

必看知识点

1、mime 字符串?

const mimeCodec = 'video/mp4; codecs="avc1.42E01E,mp4a.40.2"';
const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
// 是否支持mp4
window.MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"');

// 是否支持webm 
window.MediaSource.isTypeSupported('video/webm;codecs="vorbis,vp8"');

// 是否支持ts
window.MediaSource.isTypeSupported('video/mp2t;codes="avc1.42E01E,mp4a.40.2"')

filereader 可以查看 Mime 类型。

video/mp4

video/mp4 代表这是一段 mp4 格式封装的视频,同理也存在类似 video/webm、audio/mpeg、audio/mp4 这样的 mime 格式。一般情况下,可以通过 canPlayType 这个方法来判断浏览器是否支持当前格式。

codecs

codecs="avc1.42E01E,mp4a.40.2"' 以逗号相隔,分为两段。

codecs 第一段

codecs 第一段, 'avc1.42E01E' ,即它用于告诉浏览器关于视频编解码的一些重要信息,诸如编码方式、分辨率、帧率、码率以及对解码器解码能力的要求。

在这个例子中,'avc1' 代表视频采用 H.264 编码,随后是一个分隔点,之后是 3 个两位的十六进制的数,这 3 个十六进制数分别代表:

  • AVCProfileIndication(42):(标识 H.264 的 profile)
  • profile_compability(E0):(用于标识视频对于解码器的要求)
  • AVCLevelIndication(1E):(用于标识视频对于解码器的要求)

如果是 mp4 视频,可以使用 mp4file (mac 安装 brew install mp4v2)命令行工具查看:

mp4file --dump xxx.mp4

找到 avcC Box 后,就可以看到这三个值:

type avcC (moov.trak.mdia.minf.stbl.stsd.avc1.avcC)
configurationVersion = 1 (0x01)
AVCProfileIndication = 100 (0x64)
profile_compatibility = 0 (0x00)
AVCLevelIndication = 42 (0x2a)

注意,后面两个值(profile_compability、AVCLevelIndication)只是浏览器用于判断自身的解码能力能否满足需求,所以不需要和视频完全对应,更高也是可以的。

codecs 第二段

codecs 的第二段 'mp4a.40.2',这一段信息是关于音频部分的,代表视频的音频部分采用了 AAC LC 标准。

  • mp4a 代表此视频的音频部分采用 MPEG-4 压缩编码。
  • 一个十六进制数(40),ObjectTypeIndication,40 对应的是 Audio ISO/IEC 14496-3 标准。(不同的值具有不同的含义,详细可以参考官方文档
  • 一个十进制数(2),2 这是 MPEG-4 Audio Object Type,是一种 H.264 视频中常用的音频编码规范。

这一整段 codecs 都有完善的官方文档,可以参考: The 'Codecs' and 'Profiles' Parameters for "Bucket" Media Types

2、不是所有mp4都支持 MSE 流式传输

只有 fragmented mp4支持MSE的播放形式。一开始用的是自己本地随便找的一个视频文件,结果报错:

Uncaught DOMException: Failed to execute ‘endOfStream’ on ‘MediaSource’: The MediaSource’s readyState is not ‘open’.

也可能是loding完一整个黑屏状态:

image.png image.png

这里就要注意:不是所有mp4文件都可以流式传输的,需要将普通MP4转为fmp4格式 fragmented mp4。这里提供一个转化工具。 Bento4 MP4 & DASH Class Library, SDK and Tools工具用这个 handbrake.fr

MSE(Media Source Extensions)内提供的一个fragament mp4文件。 当然你也可以通过 ffmpeg自己生成你想要的视频。

3、如何转码出符合标准的视频?

开源的视频处理工具,比如 FFMPEG、HandBrake进行切割转码。

转码其实很简单,HandBrake 打开后,加入想要处理的视频(mp4 格式),窗口下半部分 video 标签,H.264 Profile 选择 "Baseline",level 选择 "3.0";audio 标签,选择 Encoder 为 "AAC"。

image.png

4、向 SourceBuffer 中添加多个 chunk?

以上代码只是请求了一段 chunk 然后加入到播放器里,如果视频很长,存在多个chunk 的话,就需要不停地向 SourceBuffer 中加入新的 chunk。

这里就需要注意一个问题了,即 appendBuffer 是异步执行的,在完成前,不能 append 新的 chunk:

Buffer.appendBuffer(buffer1)
// 如果再接一个 appendBuffer
Buffer.appendBuffer(buffer2)
// 此时就会报错
Uncaught DOMException: Failed to set the 'timestampOffset' property on 'SourceBuffer' ": This SourceBuffer " is "still processing an" 'appendBuffer' or 'remove'

应该监听 SourceBuffer 上的 updateend 事件,确定空闲后,再加入新的 chunk:

sourceBuffer. addEventListener('updateend',() =>{ 
    //这个时候才能加入新 chunk
    // 先设定新chunk加入的位置,比如第20秒处
    sourceBuffer.timestampoffset = 20
    // 然后加入
    sourceBuffer.append (newBuffer)
})

5、blob url 里面的地址怎么来的?

// video中的url blob:https://xxxx.xxx.xxx/50365ee4-37aa-43d9-bb5d-f6438d22a2fb
video.src = URL.createObjectURL(mediaSource);

这个生成的地址就是 blob:https://xxxx.xxx.xxx/50365ee4-37aa-43d9-bb5d-f6438d22a2fb

blob:https并不是一种协议,而是html5中blob对象在赋给video标签后生成的一串标记,blob对象包含的数据,浏览器内部会解析。

6、如何下载 blob 视频?

如果是fmp4的视频,在network中是可以找到真实的视频地址的。 image.png

如果是hls协议m3u8的视频,视频会被分解成很多个小片段,这个链接下载的是一个包含多个小视频(ts格式的视频)的链接集合,可以拿到https://xxxx.m3u8地址,然后用 ffmpeg 工具进行下载。

7、注意video支持的类型

在浏览器播放视频,可以使用HTML5原生的video标签。但其播放的格式有一定限制的,目前video只支持三种格式WebM、Ogg、MP4。

  • WebM:WebM 文件使用 VP8 视频编解码器和 Vorbis 音频编解码器
  • Ogg :Ogg 文件使用 Theora 视频编解码器和 Vorbis音频编解码器
  • MP4:MPEG 4文件使用 H264 视频编解码器和AAC音频编解码器

8、码率自适应算法难点

对于随时变化的网络情况,我们会根据情况加载不同码率的视频,这里就需要一些控制算法决定当前加载哪个码率的视频。

很容易就能想到一种简单的算法:上一段 chunk 加载完后,计算出加载速度,从而决定下一个 chunk 的码率。

但这种朴素的算法有很严重的问题,即它假设网络是相当稳定的,我们可以根据当前的信息预测出未来的网速。但这已经被大量的统计数据证明是不现实的,换句话说,我们没办法预测未来的网络环境。

所以学术界提出了一系列新的算法,比如最常见的 Buffer-Based 算法,即根据当前缓冲区里视频的长度,决定下一个 chunk 的码率。如果有很长的缓冲,那么就加载高码率的视频。这在相当于一个积分控制器,把时刻变化的无法预测的网络环境在时间维度上积分,以获得一个更平缓更能预测的函数。

但是 Buffer-Based 算法依然有问题,在视频起步阶段,缓冲区里的视频很短,导致无论网络环境如何,起步阶段的码率都是很低的。所以 Buffer-Based 算法只适用于视频 startup 后的稳态阶段。在起步阶段,依然有很多优化的空间

好文推荐