为什么视频网站的视频链接地址是 blob?
Category(分类): Other Status: 已读
原文:为什么视频网站的视频链接地址是blob?(CSDN,作者 wangzy2019,掘金同步版 juejin.im/post/5d1ea7a8e51d454fd8057bea)。 本文为整理版本,保留全部原理讲解与代码。
从防盗链说起
HTML5 的 video 标签让网页播放视频变得非常简单——src 指向真实视频地址即可。但这也意味着拿到地址就能随意下载(早期网站资源不怎么设防盗链)。
目前的云存储服务商大部分都支持 referer 防盗链:访问资源时请求头带上发起页面地址,判断其不存在(直接访问资源地址)或不在白名单内,即为盗链。
可是从某个时间开始,用调试工具看各大视频网站的 video src,它们统统变成了 blob:https://…… 的形式。这个 blob 是什么?
Blob 和 ArrayBuffer
最早是数据库用 Blob 存储二进制数据对象(不用关注存储格式)。在 Web 领域,Blob 对象表示一个只读原始数据的类文件对象——虽然是二进制数据但类似文件,可以像操作文件对象一样操作它。
ArrayBuffer 表示通用的、固定长度的原始二进制数据缓冲区:new ArrayBuffer(length) 分配一片连续内存,不能直接读写,需通过 TypedArray 或 DataView 视图来操作。TypedArray 常见类型:Int8/Uint8/Int16/Uint16/Int32/Uint32/Float32/Float64Array 等。
Blob 与 ArrayBuffer 的区别:除了原始字节以外,Blob 还提供了 mime type 作为元数据,两者可以互相转换。File 对象继承自 Blob,额外提供 name、lastModifiedDate 等元数据。
// Blob → 文本 / ArrayBuffer
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>
};
const bufReader = new FileReader();
bufReader.readAsArrayBuffer(blob);
bufReader.onload = function () {
console.log(new Uint8Array(bufReader.result)); // Uint8Array(22) [60, 100, 105, ...]
};
// ArrayBuffer → Blob
const u8Buf = new Uint8Array([60, 100, 105, 118, ...]);
const u8Blob = new Blob([u8Buf], { type: "text/html" }); // 同样得到那个 Blob
URL.createObjectURL
video/audio/img 的 src,归根结底都是指向一个文件资源的地址。既然 Blob 可以当作文件用,那只要能生成一个指向 Blob 的地址,就可以用在 src 上——这就是 URL.createObjectURL():
const objectURL = URL.createObjectURL(object);
// blob:http://localhost:1234/abcedfgh-1234-1234-1234-abcdefghijkl
object 参数是 File、Blob 或 MediaSource 对象,生成以 blob: 开头的地址。要点:
localhost:1234是当前页面的 location.host;- 即使是同样的二进制数据,每调用一次就得到一个不一样的 Blob URL;
- URL 的存在时间等同于网页的存在时间,刷新或卸载即失效;
- 用
URL.revokeObjectURL(objectURL)主动释放,让浏览器可以垃圾回收; - 以
file://协议打开的 html,地址中的 origin 会变成 null 且无法直接访问。
实战一:上传图片预览
<input id="upload" type="file" />
<img id="preview" src="" alt="预览"/>
upload.onchange = function () {
const file = upload.files[0]; // File 对象
const src = URL.createObjectURL(file);
preview.src = src;
};
同样适用于上传视频的预览。
实战二:以 Blob URL 加载网络视频
用 XHR/fetch 请求网络视频地址,把 responseType 设为 "blob",拿到 Blob 后生成 Blob URL:
function ajax(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();
}
ajax('video.mp4', function (res) {
video.src = URL.createObjectURL(res);
});
表面上看和各大视频网站形式一致了。但问题在于:这种形式要等全部视频数据请求完才能播放——小视频还行,大视频就爆炸了,视频网站不可能这么干。解决方案就是流媒体。
HLS 和 MPEG DASH
- HLS(HTTP Live Streaming):Apple 实现的基于 HTTP 的媒体流传输协议。以 ts 为传输格式、m3u8 为索引文件(包含所用 ts 文件名、时长等信息)。移动端大部分浏览器支持(video 直接加载 m3u8),PC 端除 Safari 外需引入库。优酷等用此方案——调试工具 Network 里能看到 m3u8 文件和每隔一段时间的 ts 请求;
- MPEG DASH(Dynamic Adaptive Streaming over HTTP):类似 HLS 的动态码率流媒体技术,通过 MPD 将视频内容切片,每个切片有多个码率,客户端按网络情况选择并支持无缝切换。YouTube、B 站用此方案——索引文件是 mpd,传输格式推荐 fmp4(扩展名 .m4s 或 .mp4),所以 B 站播放时网络请求里每隔一段时间有几个 m4s 文件。
相关阅读:B 站官方文章《我们为什么使用DASH》(bilibili.com/read/cv855111)。
MediaSource:边下边播的核心
不管 HLS 还是 DASH,抛开索引文件解析,核心问题只有一个:如何把多段视频数据合并、让 video 标签无缝播放。
改 src 切换视频显然不能无缝。基于 Blob URL 的思路:用 Blob URL 指向一个"视频数据容器",不断把下一段视频的二进制数据追加进去——这就是 MediaSource:一个媒体数据容器,可与 HTMLMediaElement 绑定。通过 URL.createObjectURL(mediaSource) 生成 Blob URL 设置到 video.src,播放过程中用 sourceBuffer.appendBuffer() 往容器里追加数据。
const video = document.querySelector('video');
// 假设有 init.mp4 + video1.mp4 ~ video5.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); // 生成 Blob URL 绑定 video
mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
console.error('Unsupported MIME type or codec: ', mimeCodec);
}
function sourceOpen() {
const mediaSource = this;
const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
let i = 1;
function getNextVideo(url) {
// ajax 代码见上文,responseType 为 arraybuffer
ajax(url, function (buf) {
sourceBuffer.appendBuffer(buf); // 追加数据,不影响当前播放
});
}
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`);
}
至此就实现了一个简易的流媒体播放功能。再加上 m3u8/mpd 解析和 UI,就是一个流媒体播放器。
Fragmented MP4(fmp4)
一个常见的坑:跑 MDN 的 MediaSource 示例,用官方视频没问题,换自己的 mp4 就报错——因为 MediaSource 需要的是 fmp4(Fragmented MP4),是特殊的 mp4 文件。
- 普通 mp4 是嵌套结构,必须从头加载才能完整播放,不能从中间一段开始;
- fmp4 由一系列片段组成,服务器支持 byte-range 请求时,片段可独立请求播放,无需加载整个文件。
可用这个网页判断 mp4 是否为 fmp4:http://nickdesaulniers.github.io/mp4info/
转换与切割(FFmpeg 或 Bento4,均为命令行工具):
# Bento4 mp4fragment
mp4fragment video.mp4 video-fragmented.mp4
# FFmpeg(注意 default_base_moof 参数:网上大部分资料不带它,虽能转换成功,
# 但经测试不加此参数网页中 MediaSource 处理视频会报错)
ffmpeg -i video.mp4 -movflags empty_moov+default_base_moof+frag_keyframe video-fragmented.mp4
# Bento4 切割分段
mp4split video.mp4 --media-segment video-%llu.mp4 --pattern-parameters N
总结
视频网站的 blob: 链接 = URL.createObjectURL(MediaSource):MSE 作为可追加数据的容器生成 Blob URL 给 video.src,配合 HLS/DASH 流媒体协议(m3u8/mpd 索引 + ts/fmp4 分片)实现边下边播,同时隐藏了真实视频分片地址。防盗链 + 流式播放 + 防直接下载,一举三得。
整理者注
本文与本分类《你不知道的 Blob》互为补充:那篇讲 Blob 本身的 API 与通用场景,本文讲 Blob URL + MediaSource 在视频领域的深度应用。另见《如何下载视频资源以blob:http开头的资源》——从"下载者"视角理解同一套机制。