前端流媒体:MSE入门
HTML5 规范允许我们直接在网页中嵌入视频,
github参考代码:https://github.com/shushushv/Front-End-Media/tree/master
<video src="demo.mp4"></video>
但 src 指定的资源地址必须是一个完整的媒体文件,如何在 Web 做到流式的媒体资源播放?MSE
提供了这样的可能性,先看下 MDN 对它对描述:
媒体源扩展 API(MSE) 提供了实现无插件且基于 Web 的流媒体的功能。使用 MSE,媒体串流能够通过 创建,并且能通过使用
<audio>
和<video>
元素进行播放。
MSE
让我们可以通过 JS 创建媒体资源,使用起来也十分方便:
const mediaSource = new MediaSource();
const video = document.querySelector('video');
video.src = URL.createObjectURL(mediaSource);
媒体资源对象创建完毕,接下来就是喂给它视频数据(片段),代码看上去就像是:
mediaSource.addEventListener('sourceopen', () => {
const mime = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
const sourceBuffer = mediaSource.addSourceBuffer(mime);
const data = new ArrayBuffer([...]); // 视频数据
sourceBuffer.appendBuffer(data);
});
此时,视频就可以正常播放了。要想做到流式播放,只需要不停的调用 appendBuffer
喂音视频数据就行了…但不禁有疑问, 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'
这段字符串什么意思?音视频数据又要从哪来的?🤔
MIME TYPE
// webm MIME-type
'video/webm;codecs="vp8,vorbis"'
// fmp4 MIME-type
'video/mp4;codecs="avc1.42E01E,mp4a.40.2"'
这段字符串描述了视频的相关参数,如封装格式、音/视频编码格式以及其他重要信息。以上面 mp4 这段为例,以 ;
分为两部分:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vECFqlDF-1647063901325)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1646381626961.png)]
但音视频格式多种多样,前端有什么方法直接取到视频的 MIME TYPE
呢?
对于 mp4 格式的可以使用:🌟🌟🌟 mp4box 🌟🌟🌟,获取方式如下:
// utils.ts
// 添加库
// yarn add mp4box
import MP4Box from 'mp4box';
export function getMimeType (buffer: ArrayBuffer) {
return new Promise<string>((resolve, reject) => {
const mp4boxfile = MP4Box.createFile();
```
mp4boxfile.onReady = (info: any) => resolve(info.mime);
mp4boxfile.onError = () => reject();
(buffer as any).fileStart = 0;
mp4boxfile.appendBuffer(buffer);
```
});
}
MIME TYPE
获取到后,可以通过 MSE 的静态方法 MediaSource.isTypeSupported() 检测当前浏览器是否支持该媒体格式。
import { getMimeType } from './utils';
...
const mime = await getMimeType(buffer);
if (!MediaSource.isTypeSupported(mime)) {
throw new Error('mimetype not supported');
}
Media Segment
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wiYPZ23M-1647063901341)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1646383156690.png)]
理论都了解了,实操呢?如何从已有的媒体文件生成上述的 媒体片段 呢?
这里我们用到的是🌟🌟🌟 FFmpeg 🌟🌟🌟,用起来很方便,只需要一行命令:
ffmpeg -i xxx -c copy -f dash index.mpd
xxx
是你本地的媒体文件,我这边分别用 lol.mp4
和 big-buck-bunny.webm
两个文件进行测试:
👉 ffmpeg -i lol.mp4 -c copy -f dash index.mpd
FMP4(fragmented mp4) 分段MP4
MSE(Media Source Extensions)介绍
**MSE(Media Source Extensions),即媒体源扩展,可以理解为一种API,其提供了实现无插件且基于 Web 的流媒体的功能。**通过 MSE,媒体串流能够通过 JavaScript 创建,并且可以使用 HTML5 的 和 标签进行播放。
以前,用户在通过Web浏览器浏览网页内容,尤其是视频内容时,需要使用诸如 Adobe Flash 或是微软的 Silverlight 等类似的插件,才能播放视音频内容,这些插件对于Web浏览器来说,扮演着媒体播放器角色。但是,在浏览器中使用插件是不便捷并且不安全的(不法分子会在插件上动手脚),因此,最新的HTML5标准中,定义了一系列新的元素来避免使用插件,其中就包含了大名鼎鼎的 标签。
http://simpl.info/mse
whyMSE
有了 标签,支持 HTML5 的Web浏览器就能够实现无需插件播放媒体内容了,但是 HTML5 的 标签对媒体的格式有所限制(在 Windows 电脑上只支持 mp4、webm、ogg 等格式)。这里简单介绍一下媒体格式,媒体的格式分为“编码格式”和“封装格式”,原视频数据通过编码来压缩视频数据大小,这里用到的格式为编码格式(如常见的H.264编码),再通过封装将压缩后的视音频、字幕组合到一个容器中,这里用到的格式为封装格式(如常见的mp4格式)。
我们可以把 标签看做是浏览器自带的、具有解封和解码功能的视频播放器。但是,随着视频点播、直播等视频业务的兴起,视频数据会通过流媒体传输协议(目前常用的 MPEG-DASH 和 Apple 的 HLS)从服务端分发给客户端,在这种情况下,媒体内容就被包含在传输协议中了,此时 HTML5 的 标签就无法识别并播放媒体内容了。以 HLS 协议为例。HLS 将视频源文件的内容分散地封装到一个个TS文件中,此时仅靠 标签是无法识别这些TS文件的,所以,我们就需要引入了 MSE 来帮助Web浏览器识别并处理TS文件,将其变回Web浏览器可识别的媒体容器格式(mp4格式),经过 MSE 的处理, 标签就可以识别并播放视频源文件了。例如,hls.js 中就利用 MSE 将视频分片内容进行合流,并组成 HTML5 的 标签可播放的媒体资源文件。再比如,flv.js 通过 MSE 播放 HTTP-FLV 协议的视频直播流。简单总结一下,引入 MSE 之后,支持 HTML5 的Web浏览器就变成了能够解析流协议的播放器了。
从另外一个角度来说,通过引入 MSE,HTML5 的 标签不仅可以直接播放其默认支持的 mp4、m3u8、webm、ogg 等格式,还可以支持能够被 (具备MSE功能的)JS 处理的视频流格式。如此一来,我们就可以通过 (具备MSE功能的)JS,把一些 标签原本不支持的视频流格式,转化为其支持的格式(如 H.264 的 mp4)。B站开源的 flv.js 就是此技术的一个典型应用场景,B站的HTML5播放器,通过使用 MSE 技术,将FLV源用 JS(flv.js) 实时转码成 HTML5 支持的视频流编码格式,提供给 HTML5 播放器播放。
EME-加密媒体扩展
加密媒体扩展提供了一个 API,使 Web 应用程序能够与内容保护系统交互,以允许播放加密的音频和视频。
EME 旨在使相同的应用程序和加密文件能够在任何浏览器中使用,而不管底层保护系统如何。前者是通过标准化的 API 和流程实现的,而后者是通过Common Encryption的概念实现的。
EME (加密媒体扩展)是HTMLMediaElement
规范的扩展——因此得名。作为“扩展”意味着浏览器对 EME 的支持是可选的:如果浏览器不支持加密媒体,它将无法播放加密媒体,但 HTML 规范合规性不需要 EME。
EME 实现使用以下外部组件:
-
**密钥系统:**一种内容保护 (DRM) 机制。EME 本身并没有定义 Key Systems,除了 Clear Key(更多内容见下文)。
-
**内容解密模块 (CDM):**支持加密媒体播放的客户端软件或硬件机制。与 Key Systems 一样,EME 没有定义任何 CDM,但为应用程序提供了一个接口,以与可用的 CDM 进行交互。
-
**许可证(密钥)服务器:**与 CDM 交互以提供密钥来解密媒体。与许可证服务器协商是应用程序的责任。
-
**打包服务:**对媒体进行编码和加密以供分发/消费。
Clear Key
尽管 EME 没有定义 DRM 功能,但该规范目前要求所有支持 EME 的浏览器都必须实现 Clear Key。**使用该Clear Key系统,媒体可以使用密钥加密,然后只需提供该密钥即可播放。**Clear Key 可以内置到浏览器中:它不需要使用单独的解密模块。
虽然不太可能用于多种类型的商业内容,但 Clear Key 可在所有支持 EME 的浏览器之间完全互操作。它还可以方便地测试 EME 实现和使用 EME 的应用程序,而无需从许可证服务器请求内容密钥。simpl.info/ck有一个简单的 Clear Key 示例。下面是代码的演练,它与上述步骤类似,但没有许可证服务器交互。
clearkey测试js代码示例
// Define a key: hardcoded in this example
// – this corresponds to the key used for encryption
var KEY = new Uint8Array([
0xeb, 0xdd, 0x62, 0xf1, 0x68, 0x14, 0xd2, 0x7b,
0x68, 0xef, 0x12, 0x2a, 0xfc, 0xe4, 0xae, 0x3c
]);
var config = [{
initDataTypes: ['webm'],
videoCapabilities: [{
contentType: 'video/webm; codecs="vp8"'
}]
}];
var video = document.querySelector('video');
video.addEventListener('encrypted', handleEncrypted, false);
navigator.requestMediaKeySystemAccess('org.w3.clearkey', config).then(
function(keySystemAccess) {
return keySystemAccess.createMediaKeys();
}
).then(
function(createdMediaKeys) {
return video.setMediaKeys(createdMediaKeys);
}
).catch(
function(error) {
console.error('Failed to set up MediaKeys', error);
}
);
function handleEncrypted(event) {
var session = video.mediaKeys.createSession();
session.addEventListener('message', handleMessage, false);
session.generateRequest(event.initDataType, event.initData).catch(
function(error) {
console.error('Failed to generate a license request', error);
}
);
}
function handleMessage(event) {
// If you had a license server, you would make an asynchronous XMLHttpRequest
// with event.message as the body. The response from the server, as a
// Uint8Array, would then be passed to session.update().
// Instead, we will generate the license synchronously on the client, using
// the hard-coded KEY at the top.
var license = generateLicense(event.message);
var session = event.target;
session.update(license).catch(
function(error) {
console.error('Failed to update the session', error);
}
);
}
// Convert Uint8Array into base64 using base64url alphabet, without padding.
function toBase64(u8arr) {
return btoa(String.fromCharCode.apply(null, u8arr)).
replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/, '');
}
// This takes the place of a license server.
// kids is an array of base64-encoded key IDs
// keys is an array of base64-encoded keys
function generateLicense(message) {
// Parse the clearkey license request.
var request = JSON.parse(new TextDecoder().decode(message));
// We only know one key, so there should only be one key ID.
// A real license server could easily serve multiple keys.
console.assert(request.kids.length === 1);
var keyObj = {
kty: 'oct',
alg: 'A128KW',
kid: request.kids[0],
k: toBase64(KEY)
};
return new TextEncoder().encode(JSON.stringify({
keys: [keyObj]
}));
}
要测试此代码,您需要播放加密视频。
Dynamic Adaptive Streaming over HTTP (DASH)
多设备、多平台、移动——不管你怎么称呼它,网络通常是在多变的连接条件下体验的。动态、自适应交付对于应对多设备世界中的带宽限制和可变性至关重要。
DASH(又名 MPEG-DASH)旨在在片状世界中实现最佳的媒体交付,用于流式传输和下载。其他几种技术也有类似的功能——例如 Apple 的HTTP Live Streaming (HLS) 和 Microsoft 的Smooth Streaming——但 DASH 是唯一一种基于开放标准的通过 HTTP 自适应比特率流的方法。DASH 已被 YouTube 等网站使用。
这与 EME 和 MSE 有什么关系?基于 MSE 的 DASH 实现可以解析清单,以适当的比特率下载视频片段,并在视频元素饥饿时将它们提供给视频元素——使用现有的 HTTP 基础设施。
换句话说,DASH 使商业内容提供商能够对受保护内容进行自适应流式传输。
- **动态:**响应不断变化的条件。
- **自适应:**适应提供适当的音频或视频比特率。
- **流式传输:**允许流式传输和下载。
- **HTTP:**利用 HTTP 的优势实现内容交付,而没有传统流媒体服务器的缺点。
Dynamic Adaptive Streaming over HTTP (DASH)
DASH
(即 MPEG-DASH
)设计用来最大限度的满足在实际环境中的媒体内容流播放及下载需求。很多其他技术也在做着类似的事情 —— 例如苹果的 HTTP Live Streaming(HLS) 和微软的 Smooth Streaming —— 但 DASH 是唯一的一个基于开放标准的使用 HTTP 提供可变比特率流技术。DASH 已经应用在 YouTube 等网站上。
这与 EME 和 MSE 有什么关系?基于 MSE 的 DASH 实现能够解析清单(mpd
文件),下载恰当比特率的视频片段,并将其提供给 <video>
元素,这些都是在现有的 HTTP 之上完成的。换句话说,DASH 使商用内容提供商能够提供可变比特率的受保护内容。
用 Clear Key 加密 MP4 并在浏览器中播放
总体方案
- 加密内容的创建:包括对原始视频的转码以获得
MP4(H.264/AAC)
格式视频;视频内容加密;DASH 分割及打包。 - 内容播放:在浏览器中播放加密并 DASH 视频
所需工具
- ffmpeg:视频转码,将源视频格式转换为 MP4(H.264/AAC)。
- mse-eme:Cable Labs 提供的相关工具集合,使用其中的 Clear Key 加密文件生成器来生成加密用文件。
- MP4Box:GPAC 项目中的 MP4Box 工具可用来对 MP4 视频进行加密及 DASH。
- dash.js:用来在浏览器中播放 DASH 视频。
上述工具除 dash.js
外,基本都需要安装,可以自行按照官网的说明来安装,也可以直接使用我组装好的这个 docker 镜像,使用方式为:
# 拉取镜像
$ docker pull alphahinex/try-docker:vc
# 交互模式运行镜像,并将容器命名为 vc
$ docker run --name vc -v /local/folder:/docker/folder -t -i alphahinex/try-docker:vc /bin/bash
# 查看 ffmpeg 信息
root@57ec3690605c:/usr/local# ffmpeg -version
# 查看 MP4Box 信息
root@57ec3690605c:/usr/local# MP4Box -version
# 查看 clearkey 加密文件生成工具信息
root@57ec3690605c:/usr/local# java -jar mse-eme/create/encrypt/clearkey/cryptgen/clearkey.jar -help
# 退出容器
root@57ec3690605c:/usr/local# exit
各个工具的具体参数请参考其帮助手册。好,让我们找个视频来试一下。
操作步骤
引用 html5rocks 上的这个 WebM 视频 作为源视频,将其转换为 MP4
并使用 Clear Key
加密,之后使用 HTML5 播放。
# 交互模式启动之前创建的容器 vc
$ docker start -i vc
# 假定源文件在容器中的路径为 /usr/local/video/devstories.webm
root@57ec3690605c:/usr/local/video# ffmpeg -i devstories.webm -codec:v libx264 -x264opts keyint=48:min-keyint=48:no-scenecut -codec:a aac -strict -2 devstories.mp4
# 可通过 ffprobe 或 MP4Box 查看转换后的视频信息
root@57ec3690605c:/usr/local/video# ffprobe -i devstories.mp4
root@57ec3690605c:/usr/local/video# MP4Box -info devstories.mp4
# 生成加密文件,key id 和 key 随便写,都是 16 位的 16 进制数,需要记住其转换成的 Base64 字符串
root@57ec3690605c:/usr/local/video# java -jar /usr/local/mse-eme/create/encrypt/clearkey/cryptgen/clearkey.jar 1:20212223-2425-2627-2829-2A2B2C2D2E2F=15161718191A1B1C1D1E1F2021222324 2:12131415-1617-1819-1A1B-1C1D1E1F2021=25262728292A2B2C2D2E2F3031323334 -out devstories_drm.xml
Ensure the following keys are available to the client:
202122232425262728292a2b2c2d2e2f : 15161718191a1b1c1d1e1f2021222324 (ICEiIyQlJicoKSorLC0uLw : FRYXGBkaGxwdHh8gISIjJA)
12131415161718191a1b1c1d1e1f2021 : 25262728292a2b2c2d2e2f3031323334 (EhMUFRYXGBkaGxwdHh8gIQ : JSYnKCkqKywtLi8wMTIzNA)
# 使用上一步生成的 devstories_drm.xml 加密 MP4
root@57ec3690605c:/usr/local/video# MP4Box -crypt devstories_drm.xml devstories.mp4 -out devstories_enc.mp4
# 可再次通过 MP4Box -info devstories_enc.mp4 看到两个轨道都已被加密
# dash
root@57ec3690605c:/usr/local/video# MP4Box -dash 10000 -profile onDemand -out devstories_enc.mpd devstories_enc.mp4#video devstories_enc.mp4#audio
完成上述步骤后,会得到如下三个文件,从 mpd
中可以看到视频清单和加密使用的 key id
。
devstories_enc.mpd
devstories_enc_track1_dashinit.mp4
devstories_enc_track2_dashinit.mp4
将上述文件部署至服务器,并在页面中增加如下代码:
<head>
<script src="dash.all-1.5.1.js"></script>
<script>
function init() {
var video, context, player;
video = document.querySelector('video');
context = new Dash.di.DashContext()
player = new MediaPlayer(context);
player.startup();
player.attachView(video);
player.setAutoPlay(true);
player.attachSource('devstories_enc.mpd', null, {
'org.w3.clearkey': {
// 例子中是将解密视频所需的 key id 和 key 都硬编码在 js 中
'clearkeys': {
'ICEiIyQlJicoKSorLC0uLw': 'FRYXGBkaGxwdHh8gISIjJA',
'EhMUFRYXGBkaGxwdHh8gIQ': 'JSYnKCkqKywtLi8wMTIzNA'
}
// 还可以使用授权服务器在播放视频时根据 key id 请求解密所需 key
// 'serverURL': 'http://license-server-host:port'
}
});
}
</script>
</head>
<body onload="init()">
<video width="640" height="360" controls="true"></video>
</body>
实际例子
实际例子与上述代码片段略有不同。由于无法在 onload
中触发 init
函数,需点击下面 初始化并播放
按钮初始化视频后方可播放。
为了显示效果,为视频增加了一个封面图片。
目前 dash.js
版本已更新至 1.6.0
,但实际测试时发现播放加密视频时会有解码错误或无法完整播放的情况,还有在视频播放完后无法重新播放等问题,故例子中使用 1.5.1
版本。
据不完全测试,该段视频只能在 Chrome45+ 中播放。
Clear Key License Server
如上所述,除了直接将解密所需 key 硬编码进 js 之外,还可从授权服务器中根据 key id
获取 key。下面的 server.js
是一个简单的 node.js
server,能够根据请求中发送过来的 key id 响应所需的 key。用法为:
$ node server.js
之后将上面代码片段中 serverURL
属性替换为服务发布的 ip 和端口并解注,注释掉 clearkeys
属性即可。
server.js代码
var http = require('http');
var url = require('url');
console.log('');
console.log('Clear Key License Server Demo');
console.log('');
keys = {
'ICEiIyQlJicoKSorLC0uLw': 'FRYXGBkaGxwdHh8gISIjJA',
'EhMUFRYXGBkaGxwdHh8gIQ': 'JSYnKCkqKywtLi8wMTIzNA'
};
var addCORSHeaders = function(res, length) {
res.writeHeader(200, {
'Content-Length': length,
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, PUT, POST, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, Content-Length, X-Requested-Width'});
};
var onReq = function(req, res) {
console.log('Request URL:' + req.url);
var parsed_url = url.parse(req.url, true);
var query = parsed_url.query;
console.log('Received key request! Query = %j', query);
// Validate query string
if (query === undefined) {
console.error('Illegal request!');
res.writeHeader(400, 'Illegal query string');
res.end();
return;
}
var keyIDs = [];
for (var p in query) {
keyIDs.push(p);
}
var keyarray = [];
for (var i = 0; i < keyIDs.length; i++) {
var keyID = keyIDs[i];
if (!keys.hasOwnProperty(keyID)) {
console.warn('KeyID %s not registered in our lookup table!', keyID);
continue;
}
var keypair = {
kid: keyIDs[i],
k: keys[keyIDs[i]]
};
keyarray.push(keypair);
}
var response = {
keys: keyarray
};
console.log('Returning key array: %j', response);
var json_str_response = JSON.stringify(response);
addCORSHeaders(res, json_str_response.length);
res.write(json_str_response);
res.end();
};
http.createServer(onReq).listen(8584);
流媒体相关资料链接
学习使用ffmpeg Dash视频加密 | wakasann Croner Encrypted Media Extensions EME, CDM, AES, CENC, and Keys - The Essential Building Blocks of DRM - OTTVerse Dash-Industry-Forum/ClearKey-Content-Protection: The repository holds a description and an issue tracker for how ClearKey-based content protection should be used with MPEG DASH. Bitmovin Docs - Encoding Tutorials | How to create MPEG-CENC ClearKey content MP4文件处理工具包Bento4 - 简书 前端流媒体播放从入门到入坑 - 掘金 FFmpeg 视频处理入门教程 - 阮一峰的网络日志 FFmpeg最全教程 - 云+社区 - 腾讯云 ffmpeg详细安装教程,亲测有效! - 知乎 Download FFmpeg Builds - CODEX FFMPEG @ gyan.dev 在 Ubuntu 和其他 Linux 中安装和使用 ffmpeg [完整指南] (255条消息) 红帽Linux安装ffmpeg_MachineEye-CSDN博客_redhat安装ffmpeg (255条消息) Linux编译安装ffmpeg redhat /cenos_zhangqifeng专栏-CSDN博客 小文章,小技巧:如何在CentOS7上安装使用ffmpeg,入门第一课 - 云+社区 - 腾讯云 音视频处理工具FFmpeg与Java结合的简单使用 - 知乎 使用 Clear Key 加密 MP4 视频并播放 - Alpha Hinex's Blog EME WTF?: An introduction to Encrypted Media Extensions - HTML5 Rocks simpl/mse at gh-pages · samdutton/simpl Media Source Extensions™ 媒体源扩展™ media - What exactly is Fragmented mp4(fMP4)? How is it different from normal mp4? - Stack Overflow 前端流媒体:MSE入门 - SegmentFault 思否 Front-End-Media/MSE/Introduction at master · shushushv/Front-End-Media