video视频src是blob:https://探究

自从HTML5提供了video标签,在网页中播放视频已经变成一个非常简单的事,只要一个video标签,src属性设置为视频的地址就完事了。由于src指向真实的视频网络地址,在早期一般网站资源文件不怎么通过referer设置防盗链,当我们拿到视频的地址后可以随意的下载或使用(每次放假回家,就会有亲戚找我帮忙从一些视频网站上下东西)。

目前的云存储服务商大部分都支持referer防盗链。其原理就是在访问资源时,请求头会带上发起请求的页面地址,判断其不存在(表示直接访问图片地址)或不在白名单内,即为盗链。

可是从某个时间开始我们打开调试工具去看各大视频网站的视频src会发现,它们统统变成了这样的形式。

拿b站的一个视频来看,红框中的视频地址,这个blob是个什么东西?

其实这个Blob URL也不是什么新技术,国内外出来都有一阵子了,但是网上的相关的文章不多也不是很详细,今天就和大家一起分享学习一下。

Blob和ArrayBuffer

最早是数据库直接用Blob来存储二进制数据对象,这样就不用关注存储数据的格式了。在web领域,Blob对象表示一个只读原始数据的类文件对象,虽然是二进制原始数据但是类似文件的对象,因此可以像操作文件对象一样操作Blob对象。

ArrayBuffer对象用来表示通用的、固定长度的原始二进制数据缓冲区。我们可以通过new ArrayBuffer(length)来获得一片连续的内存空间,它不能直接读写,但可根据需要将其传递到TypedArray视图或 DataView 对象来解释原始缓冲区。实际上视图只是给你提供了一个某种类型的读写接口,让你可以操作ArrayBuffer里的数据。TypedArray需指定一个数组类型来保证数组成员都是同一个数据类型,而DataView数组成员可以是不同的数据类型。

TypedArray视图的类型数组对象有以下几个:

  • Int8Array:8位有符号整数,长度1个字节。
  • Uint8Array:8位无符号整数,长度1个字节。
  • Uint8ClampedArray:8位无符号整数,长度1个字节,溢出处理不同。
  • Int16Array:16位有符号整数,长度2个字节。
  • Uint16Array:16位无符号整数,长度2个字节。
  • Int32Array:32位有符号整数,长度4个字节。
  • Uint32Array:32位无符号整数,长度4个字节。
  • Float32Array:32位浮点数,长度4个字节。
  • Float64Array:64位浮点数,长度8个字节。

Blob与ArrayBuffer的区别是,除了原始字节以外它还提供了mime type作为元数据,Blob和ArrayBuffer之间可以进行转换。

File对象其实继承自Blob对象,并提供了提供了name , lastModifiedDate, size ,type 等基础元数据。

创建Blob对象并转换成ArrayBuffer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//创建一个以二进制数据存储的html文件
const text = "<div>hello world</div>";
const blob = new Blob([text], { type: "text/html" }); // Blob {size: 22, type: "text/html"}
//以文本读取
const textReader = new FileReader();
textReader.readAsText(blob);
textReader.onload = function() {
console.log(textReader.result); // <div>hello world</div>
};
//以ArrayBuffer形式读取
const bufReader = new FileReader();
bufReader.readAsArrayBuffer(blob);
bufReader.onload = function() {
console.log(new Uint8Array(bufReader.result)); // Uint8Array(22) [60, 100, 105, 118, 62, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 60, 47, 100, 105, 118, 62]
};

创建一个相同数据的ArrayBuffer,并转换成Blob:

1
2
3
4
5
6
7
8
9
//我们直接创建一个Uint8Array并填入上面的数据
const u8Buf = new Uint8Array([60, 100, 105, 118, 62, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 60, 47, 100, 105, 118, 62]);
const u8Blob = new Blob([u8Buf], { type: "text/html" }); // Blob {size: 22, type: "text/html"}
const textReader = new FileReader();

textReader.readAsText(u8Blob);
textReader.onload = function() {
console.log(textReader.result); // 同样得到div>hello world</div>
};

更多Blob和ArrayBuffer的相关内容可以参看下面的资料:

URL.createObjectURL

video标签,audio标签还是img标签的src属性,不管是相对路径,绝对路径,或者一个网络地址,归根结底都是指向一个文件资源的地址。既然我们知道了Blob其实是一个可以当作文件用的二进制数据,那么只要我们可以生成一个指向Blob的地址,是不是就可以用在这些标签的src属性上,答案肯定是可以的,这里我们要用到的就是URL.createObjectURL()。

1
2
3
const objectURL = URL.createObjectURL(object); 

//blob:http://localhost:1234/abcedfgh-1234-1234-1234-abcdefghijkl

这里的object参数是用于创建URL的File对象、Blob 对象或者 MediaSource 对象,生成的链接就是以blob:开头的一段地址,表示指向的是一个二进制数据。

其中localhost:1234是当前网页的主机名称和端口号,也就是location.host,而且这个Blob URL是可以直接访问的。需要注意的是,即使是同样的二进制数据,每调用一次URL.createObjectURL方法,就会得到一个不一样的Blob URL。这个URL的存在时间,等同于网页的存在时间,一旦网页刷新或卸载,这个Blob URL就失效。

通过URL.revokeObjectURL(objectURL) 释放一个之前已经存在的、通过调用 URL.createObjectURL() 创建的 URL 对象。当你结束使用某个 URL 对象之后,应该通过调用这个方法来让浏览器知道不用在内存中继续保留对这个文件的引用了,允许平台在合适的时机进行垃圾收集。

如果是以文件协议打开的html文件(即url为file://开头),则地址中http://localhost:1234会变成null,而且此时这个Blob URL是无法直接访问的。

实战一:上传图片预览

有时我们通过input上传图片文件之前,会希望可以预览一下图片,这个时候就可以通过前面所学到的东西实现,而且非常简单。

1
<inputid="upload"type="file" /><imgid="preview"src=""alt="预览"/>
1
2
3
4
5
6
7
8
const upload = document.querySelector("#upload");
const preview = document.querySelector("#preview");

upload.onchange = function() {
const file = upload.files[0]; //File对象
const src = URL.createObjectURL(file);
preview.src = src;
};

这样一个图片上传预览就实现了,同样这个方法也适用于上传视频的预览。

实战二:以Blob URL加载网络视频

现在我们有一个网络视频的地址,怎么能将这个视频地址变成Blob URL是形式呢,思路肯定是先要拿到存储这个视频原始数据的Blob对象,但是不同于input上传可以直接拿到File对象,我们只有一个网络地址。

我们知道平时请求接口我们可以使用xhr(jquery里的ajax和axios就是封装的这个)或fetch,请求一个服务端地址可以返回我们相应的数据,那如果我们用xhr或者fetch去请求一个图片或视频地址会返回什么呢?当然是返回图片和视频的数据,只不过要设置正确responseType才能拿到我们想要的格式数据。

1
2
3
4
5
6
7
8
9
functionajax(url, cb) {
const xhr = new XMLHttpRequest();
xhr.open("get", url);
xhr.responseType = "blob"; // ""|"text"-字符串 "blob"-Blob对象 "arraybuffer"-ArrayBuffer对象
xhr.onload = function() {
cb(xhr.response);
};
xhr.send();
}

注意XMLHttpRequest和Fetch API请求会有跨域问题,可以通过跨域资源共享(CORS)解决。

看到responseType可以设置blob和arraybuffer我们应该就有谱了,请求返回一个Blob对象,或者返回ArrayBuffer对象转换成Blob对象,然后通过createObjectURL生成地址赋值给视频的src属性就可以了,这里我们直接请求一个Blob对象。

1
2
3
4
ajax('video.mp4', function(res){
const src = URL.createObjectURL(res);
video.src = src;
})

用调试工具查看视频标签的src属性已经变成一个Blob URL,表面上看起来是不是和各大视频网站形式一致了,但是考虑一个问题,这种形式要等到请求完全部视频数据才能播放,小视频还好说,要是视频资源大一点岂不爆炸,显然各大视频网站不可能这么干。

解决这个问题的方法就是流媒体,其带给我们最直观体验就是使媒体文件可以边下边播(像我这样的90后男性最早体会到流媒体好处的应该是源于那款快子头的播放器),web端如果要使用流媒体,有多个流媒体协议可以供我们选择。

HLS和MPEG DASH

HLS (HTTP Live Streaming), 是由 Apple 公司实现的基于 HTTP 的媒体流传输协议。HLS以ts为传输格式,m3u8为索引文件(文件中包含了所要用到的ts文件名称,时长等信息,可以用播放器播放,也可以用vscode之类的编辑器打开查看),在移动端大部分浏览器都支持,也就是说你可以用video标签直接加载一个m3u8文件播放视频或者直播,但是在pc端,除了苹果的Safari,需要引入库来支持。

用到此方案的视频网站比如优酷,可以在视频播放时通过调试查看Network里的xhr请求,会发现一个m3u8文件,和每隔一段时间请求几个ts文件。

但是除了HLS,还有Adobe的HDS,微软的MSS,方案一多就要有个标准点的东西,于是就有了MPEG DASH。

DASH(Dynamic Adaptive Streaming over HTTP) ,是一种在互联网上传送动态码率的Video Streaming技术,类似于苹果的HLS,DASH会通过media presentation description (MPD)将视频内容切片成一个很短的文件片段,每个切片都有多个不同的码率,DASH Client可以根据网络的情况选择一个码率进行播放,支持在不同码率之间无缝切换。

Youtube,B站都是用的这个方案。这个方案索引文件通常是mpd文件(类似HLS的m3u8文件功能),传输格式推荐的是fmp4(Fragmented MP4),文件扩展名通常为.m4s或直接用.mp4。所以用调试查看b站视频播放时的网络请求,会发现每隔一段时间有几个m4s文件请求。

不管是HLS还是DASH们,都有对应的库甚至是高级的播放器方便我们使用,但我们其实是想要学习一点实现。其实抛开掉索引文件的解析拿到实际媒体文件的传输地址,摆在我们面前的只有一个如何将多个视频数据合并让video标签可以无缝播放。

与之相关的一篇B站文章推荐给感兴趣的朋友:我们为什么使用DASH

MediaSource

video标签src指向一个视频地址,视频播完了再将src修改为下一段的视频地址然后播放,这显然不符合我们无缝播放的要求。其实有了我们前面Blob URL的学习,我们可能就会想到一个思路,用Blob URL指向一个视频二进制数据,然后不断将下一段视频的二进制数据添加拼接进去。这样就可以在不影响播放的情况下,不断的更新视频内容并播放下去,想想是不是有点流的意思出来了。

要实现这个功能我们要通过MediaSource来实现,MediaSource接口功能也很纯粹,作为一个媒体数据容器可以和HTMLMediaElement进行绑定。基本流程就是通过URL.createObjectURL创建容器的BLob URL,设置到video标签的src上,在播放过程中,我们仍然可以通过MediaSource.appendBuffer方法往容器里添加数据,达到更新视频内容的目的。

实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const video = document.querySelector('video');
//视频资源存放路径,假设下面有5个分段视频 video1.mp4 ~ video5.mp4,第一个段为初始化视频init.mp4
const assetURL = "http://www.demo.com";
//视频格式和编码信息,主要为判断浏览器是否支持视频格式,但如果信息和视频不符可能会报错
const mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
if ('MediaSource' in window && MediaSource.isTypeSupported(mimeCodec)) {
const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource); //将video与MediaSource绑定,此处生成一个Blob URL
mediaSource.addEventListener('sourceopen', sourceOpen); //可以理解为容器打开
} else {
//浏览器不支持该视频格式
console.error('Unsupported MIME type or codec: ', mimeCodec);
}

function sourceOpen () {
const mediaSource = this;
const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
function getNextVideo(url) {
//ajax代码实现翻看上文,数据请求类型为arraybuffer
ajax(url, function(buf) {
//往容器中添加请求到的数据,不会影响当下的视频播放。
sourceBuffer.appendBuffer(buf);
});
}
//每次appendBuffer数据更新完之后就会触发
sourceBuffer.addEventListener("updateend", function() {
if (i === 1) {
//第一个初始化视频加载完就开始播放
video.play();
}
if (i < 6) {
//一段视频加载完成后,请求下一段视频
getNextVideo(`${assetURL}/video${i}.mp4`);
}
if (i === 6) {
//全部视频片段加载完关闭容器
mediaSource.endOfStream();
URL.revokeObjectURL(video.src); //Blob URL已经使用并加载,不需要再次使用的话可以释放掉。
}
i++;
});
//加载初始视频
getNextVideo(`${assetURL}/init.mp4`);
};

这段代码修改自MDN的MediaSource词条中的示例代码,原例子中只有加载一段视频,我修改为了多段视频,代码里面很多地方还可以优化精简,这里没做就当是为了方便我们看逻辑。

此时我们已经基本实现了一个简易的流媒体播放功能,如果愿意可以再加入m3u8或mpd文件的解析,设计一下UI界面,就可以实现一个流媒体播放器了。

最后提一下一个坑,很多人跑了MDN的MediaSource示例代码,可能会发现使用官方提供的视频是没问题的,但是用了自己的mp4视频就会报错,这是因为fmp4文件扩展名通常为.m4s或直接用.mp4,但却是特殊的mp4文件。

Fragmented MP4

通常我们使用的mp4文件是嵌套结构的,客户端必须要从头加载一个 MP4 文件,才能够完整播放,不能从中间一段开始播放。而Fragmented MP4(简称fmp4),就如它的名字碎片mp4,是由一系列的片段组成,如果服务器支持 byte-range 请求,那么,这些片段可以独立的进行请求到客户端进行播放,而不需要加载整个文件。

我们可以通过这个网站判断一个mp4文件是否为Fragmented MP4,网站地址

我们通过FFmpegBento4的mp4fragment来将普通mp4转换为Fragmented MP4,两个工具都是命令行工具,按照各自系统下载下来对应的压缩包,解压后设置环境变量指向文件夹中的bin目录,就可以使用相关命令了。

Bento4的mp4fragment,没有太多参数,命令如下:

1
mp4fragment video.mp4 video-fragmented.mp4

FFmpeg会需要设置一些参数,命令如下:

1
ffmpeg -i video.mp4 -movflags empty_moov+default_base_moof+frag_keyframe video-fragmented.mp4

Tips:网上大部分的资料中转换时是不带default_base_moof这个参数的,虽然可以转换成功,但是经测试如果不添加此参数网页中MediaSource处理视频时会报错。

视频的切割分段可以使用Bento4的mp4slipt,命令如下:

1
mp4split video.mp4 --media-segment video-%llu.mp4 --pattern-parameters N

二进制文件下载

  • 有这样需求,需要下载一个 excel 表格。
  • 主要是后端渲染一个 excel 格式的二进制文件,接口返回一个 ArrayBuff 类型的文件,然后前端提供用户下载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
functionrequest () {
const req = new XMLHttpRequest();
req.open('POST', '<接口地址>', true);
req.responseType = 'blob';
req.setRequestHeader('Content-Type', 'application/json');
req.onload = function() {
const data = req.response;
const a = document.createElement('a');
const blob = new Blob([data]);
const blobUrl = window.URL.createObjectURL(blob);
download(blobUrl) ;
};
req.send('<请求参数:json字符串>');
};

function download(blobUrl) {
const a = document.createElement('a');
a.style.display = 'none';
a.download = '<文件名>';
a.href = blobUrl;
a.click();
document.body.removeChild(a);
}

request();

先了解一下URL.createObjectURL(object)方法 和 blob 对象

createObjectURL

objectURL = URL.createObjectURL(object);

  • 用于创建 URL 的 File 对象、Blob 对象或者 MediaSource 对象。

blob

1
var aBlob = new Blob( array, options );
  • array 是一个由ArrayBuffer, ArrayBufferView, Blob, DOMString 等对象构成的 Array ,或者其他类似对象的混合体,它将会被放进 Blob。DOMStrings会被编码为UTF-8。

  • options 是一个可选的BlobPropertyBag字典,它可能会指定如下两个属性:
    type,默认值为 “”,它代表了将会被放入到blob中的数组内容的MIME类型。
    endings,默认值为”transparent”,用于指定包含行结束符\n的字符串如何被写入。 它是以下两个值中的一个: “native”,代表行结束符会被更改为适合宿主操作系统文件系统的换行符,或者 “transparent”,代表会保持blob中保存的结束符不变

1
2
3
4
5
var debug = {hello: "world"};
var blob = new Blob([JSON.stringify(debug, null, 2)],
{type : 'application/json'});

// 得到一个 blob

downloadFile 函数

1
<buttononclick="downloadFile(API.getExcel(),{ excel_name: '数据表单' })">点击下载</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* @param {*} apiUrl // api 地址
* @param {*} params // 配置项
* @param {*} blobType // 文件类型,你可以和后端对接传入合理的 type
* @param {*} fileSuffix // 文件后缀,具体的后缀视需求而定
*/function downloadFile(apiUrl, params = { excel_name: 'data' }, blobType = 'application/ynd.ms-excel;charset=UTF-8',excelTypeSuffix:string = 'xls') {

if (!apiUrl) {
console.error('please input API URL!');

return;
}

// 这里示例使用 axios,apiUrl 为后端提供的相关接口return axios
.get(apiUrl, {
params,
responseType: 'arraybuffer'
})
.then(
resp => {
// 先把后端返回的 resp 包装成一个 Blob 类型let localHref = window.URL.createObjectURL(
new Blob([resp], { type: blobType })
);

// 创建一个 a 标签用来下载文件let ele = document.createElement('a');

ele.href = localHref;

// download 属性决定下载的文件的名字
ele.download = `${params.excel_name}.${excelTypeSuffix}`;

document.querySelectorAll('body')[0].appendChild(ele);

ele.click();

// 主动移除这个 a 标签,来释放内存
setTimeout(() => {
ele.remove();
}, 1000);
},
err => {
console.error(err.error_msg);
}
)
.finally(() => {
console.log('end')
});
}

目前几种视频流的简单对比:

国内常见的直播协议有几个:RTMP、HLS、HTTP-FLV,下面我们来一一介绍。

  • RTMP,全称 Real Time Messaging Protocol,即实时消息传送协议。Adobe 公司为 Flash 播放器和服务器之间音视频数据传输开发的私有协议。工作在 TCP 之上的明文协议,默认使用端口 1935。协议中的基本数据单元成为消息(Message),传输的过程中消息会被拆分为更小的消息块(Chunk)单元。最后将分割后的消息块通过 TCP 协议传输,接收端再反解接收的消息块恢复成流媒体数据。

RTMP 主要有以下几个优点:RTMP 是专为流媒体开发的协议,对底层的优化比其它协议更加优秀,同时它 Adobe Flash 支持好,基本上所有的编码器(摄像头之类)都支持 RTMP 输出。现在 PC 市场巨大,PC 主要是 Windows,Windows 的浏览器基本上都支持 Flash。另外RTMP适合长时间播放,曾经有过测试,联系 100 万秒,即 10 天多连续播放没有出现问题。最后 RTMP 的延迟相对较低,一般延时在 1-3s 之间,一般的视频会议,互动式直播,完全是够用的。

当然 RTMP 并没有尽善尽美,它也有不足的地方。一方面是它是基于 TCP 传输,非公共端口,可能会被防火墙阻拦;另一方面,也是比较坑的一方面是 RTMP 为 Adobe 私有协议,很多设备无法播放,特别是在 iOS 端,需要使用第三方解码器才能播放。

  • FLV (Flash Video) 是 Adobe 公司推出的另一种视频格式,是一种在网络上传输的流媒体数据存储容器格式。其格式相对简单轻量,不需要很大的媒体头部信息。整个 FLV 由 The FLV Header, The FLV Body 以及其它 Tag 组成。因此加载速度极快。采用 FLV 格式封装的文件后缀为 .flv。

而我们所说的 HTTP-FLV 即将流媒体数据封装成 FLV 格式,然后通过 HTTP 协议传输给客户端。

HTTP-FLV 依靠 MIME 的特性,根据协议中的 Content-Type 来选择相应的程序去处理相应的内容,使得流媒体可以通过 HTTP 传输。相较于 RTMP 协议,HTTP-FLV 能够好的穿透防火墙,它是基于 HTTP/80 传输,有效避免被防火墙拦截。除此之外,它可以通过 HTTP 302 跳转灵活调度/负载均衡,支持使用 HTTPS 加密传输,也能够兼容支持 Android,iOS 的移动端。

说了这么多优点,也来顺便说下 HTTP-FLV 的缺点,由于它的传输特性,会让流媒体资源缓存在本地客户端,在保密性方面不够好。因为网络流量较大,它也不适合做拉流协议。

  • HLS (HTTP Live Streaming) 则是苹果公司基于 HTTP 的流媒体传输协议。主要应用于 iOS 设备,包含(iPhone, iPad, iPod touch) 以及 Mac OSX 提供音视频直播服务和录制内容(点播)等服务。

相对于常见的流媒体协议,HLS 最大的不同在于它并不是一下请求完整的数据流。它会在服务器端将流媒体数据切割成连续的时长较短的 ts 小文件,并通过 M3U8 索引文件按序访问 ts 文件。客户端只要不停的按序播放从服务器获取到的文件,从而实现播放音视频。

相较 RTMP 而言,使用 HLS 在 HTML5 页面上实现播放非常简单:<video src='test.m3u8'></video>

HLS 的优势:

  • Apple 的全系列产品支持:由于 HLS 是苹果提出的,所以在 Apple 的全系列产品包括 iPhone、 iPad、safari 都不需要安装任何插件就可以原生支持播放 HLS, 现在 Android 也加入了对 HLS 的支持。
  • 穿透防火墙。基于 HTTP/80 传输,有效避免防火墙拦截
  • 性能高。通过 HTTP 传输, 支持网络分发,CDN 支持良好,且自带多码率自适应,Apple 在提出 HLS 时,就已经考虑了码流自适应的问题。

HLS 的劣势:

  • 实时性差,延迟高。HLS 的延迟基本在 10s+ 以上
  • 文件碎片。特性的双刃剑,ts 切片较小,会造成海量小文件,对存储和缓存都有一定的挑战

image

文章参考:为什么视频网站的视频链接地址是blob?