向SourceBuffer输入cenc加密mp4数据时encrypted事件未触发问题咨询
Clear Key DRM播放cenc加密MP4时encrypted事件触发异常问题
问题表现
- 仅在Safari浏览器上能触发
encrypted事件,Chrome和Firefox浏览器均无法触发该事件 - Chrome和Safari上能收到
waitingforkey事件,但Firefox上无法收到该事件 - 已确认加密片源来自dash.js官方示例,排除片源本身问题;已排除自定义工具函数的影响,无工具函数依赖的版本表现完全一致
核心疑问
Chrome能识别到媒体播放需要密钥,说明浏览器已经判断出媒体处于加密状态,为什么不会触发encrypted事件?
测试代码
带工具函数版本
async function playClearkeyVideoFromUrls(videoElement, initUrl, urls) { // for debugging purposes videoElement.addEventListener(`waitingforkey`, () => console.log(`Event: waitingforkey`)) videoElement.addEventListener(`encrypted`, () => console.log(`Event: encrypted`)) videoElement.addEventListener(`error`, function () { console.log(`Event: HTMLMediaElement.onerror`) console.log(this.error) }) // we create a MediaSource const mediaSource = new MediaSource() // attach the MediaSource to the Video tag, only then it will fire the "sourceopen" event videoElement.src = URL.createObjectURL(mediaSource) // add a SourceBuffer to the MediaSource, we need to specify the MIME type of the video we want to play const sourceBuffer = await mediaSource.asyncAddSourceBuffer(`video/mp4;codecs="avc1.64001f"`) // for debugging purposes sourceBuffer.addEventListener(`error`, e => { console.log(`Event: SourceBuffer.onerror`); console.log(e) }) // append the first (init) segment console.log(`Appending the first (init) segment`) await sourceBuffer.asyncAppendBuffer(await fetchArrayBuffer(initUrl), videoElement) // here I expect the "encrypted" AND "waitingforkey" event to fire // now append the rest of the segments for (let i = 0; i < urls.length; i++) { const url = urls[i] console.log(`Appending a segment ...`) if (!await sourceBuffer.asyncAppendBuffer(await fetchArrayBuffer(url), videoElement)) { console.log(`Canceling playback as an error has occurred.`) console.log(videoElement.error) break } } }
无工具函数依赖版本
let initUrl let urls let segmentIndex = 0 Number.prototype.toStringPadded = function(size) { let thisString = this.toString(); while (thisString.length < size) thisString = "0" + thisString; return thisString; } async function fetchArrayBuffer(url) { return await (await (await fetch(url)).blob()).arrayBuffer() } async function updateend() { console.log(`Event: updateend`) this.appendBuffer(await fetchArrayBuffer(urls[segmentIndex])) segmentIndex++ if (segmentIndex === urls.length) { this.removeEventListener(`updateend`, updateend) } console.log(`Appended segment with id ${segmentIndex}.`) } async function sourceopen() { console.log(`Event: sourceopen`) // add a SourceBuffer to the MediaSource, we need to specify the MIME type of the video we want to play const sourceBuffer = this.addSourceBuffer(`video/mp4;codecs="avc1.64001f"`) // for debugging purposes sourceBuffer.addEventListener(`error`, e => { console.log(`Event: SourceBuffer.onerror`); console.log(e) }) sourceBuffer.addEventListener(`updateend`, updateend) sourceBuffer.appendBuffer(await fetchArrayBuffer(initUrl)) } async function playClearkeyVideoFromUrls(videoElement) { // for debugging purposes videoElement.addEventListener(`waitingforkey`, (event) => { console.log(`Event: waitingforkey`) console.log(event) }) videoElement.addEventListener(`encrypted`, (mediaEncryptedEvent) => { console.log(`Event: encrypted`) console.log(mediaEncryptedEvent) }) videoElement.addEventListener(`error`, function () { console.log(`Event: HTMLMediaElement.onerror`) console.log(this.error) }) // we create a MediaSource const mediaSource = new MediaSource() mediaSource.addEventListener(`sourceopen`, sourceopen) // attach the MediaSource to the Video tag, only then it will fire the "sourceopen" event videoElement.src = URL.createObjectURL(mediaSource) } async function testPlayClearkeyVideoFromUrls() { const streamId = 1 initUrl = `https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/${streamId}/init.mp4` const videoUrlPrefix = `https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/${streamId}/` const videoUrlSuffix = `.m4s` const numberOfSegments = 4 // first we generate our urls we will download urls = [] for (let i = 0; i < numberOfSegments; i++) { const url = `${videoUrlPrefix}${(i + 1).toStringPadded(4)}${videoUrlSuffix}` urls.push(url) } const videoElement = document.querySelector(`video`) await playClearkeyVideoFromUrls(videoElement) } testPlayClearkeyVideoFromUrls()
问题原因及解决方案
触发异常的核心原因
Chrome、Firefox的EME(加密媒体扩展)实现有明确的前置要求:必须提前为video元素绑定MediaKeys实例,浏览器才会触发encrypted事件。Safari的EME实现没有这个前置限制,所以仅在Safari上能正常收到事件。
另外Firefox的waitingforkey事件触发要求MIME类型中明确声明加密方式,缺失声明的情况下会忽略该事件。
修复步骤
- 初始化阶段提前调用
navigator.requestMediaKeySystemAccess获取Clear Key的访问权限,创建MediaKeys实例后通过videoElement.setMediaKeys()方法绑定到video元素上,不要等待事件触发后再创建实例。 - 补充SourceBuffer的MIME类型声明,将原MIME类型修改为
video/mp4;codecs="avc1.64001f";encryption=cenc,明确告知浏览器当前流采用cenc加密。 - Firefox环境下确认
about:config中的media.eme.enabled配置项为开启状态,默认状态为开启,部分定制版浏览器可能会关闭该配置。
完成以上修改后即可在三大浏览器上稳定触发encrypted和waitingforkey事件。
内容的提问来源于stack exchange,提问作者Blubberlase
相关产品推荐
相关产品推荐

