“加密”事件未触发,尽管使用 cenc mp4 数据提供源缓冲区

Posted

技术标签:

【中文标题】“加密”事件未触发,尽管使用 cenc mp4 数据提供源缓冲区【英文标题】:"encrypted" event not firing albeit feeding source buffer with cenc mp4 data 【发布时间】:2022-01-06 20:30:42 【问题描述】:

我正在尝试了解 DRM 系统的工作原理,因此我的旅程开始于尝试使用 Clear Key DRM 系统播放 cenc 加密的 mp4 视频,而不使用 dash.js 或 Shaka Player 等任何库。

我遇到的第一个问题是我并不总是收到“加密”事件。 我只在 Safari 上收到“加密”,但在 Google Chrome 和 Firefox 上都没有。

有趣的是,我确实在 Google Chrome 和 Safari 上收到了“waitingforkey”,但在 Firefox 上却没有。

这个事实最让我困惑,因为如果谷歌浏览器知道它需要一个密钥,我假设它必须知道媒体是加密的,那么为什么它不触发“加密”事件?

您可以在下面找到我使用的代码。我正在使用一些便利功能。我希望很清楚他们在做什么。如果没有,您会看到他们的定义here。另外我的示例是在线here 供您直接在浏览器中进行测试和调试。

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
        
    

我拥有的 cenc encrpyted mp4 文件来自 dash.js 示例页面,所以我认为这不是我的问题的根源。

总结一下我的主要问题是:为什么“加密”事件没有被触发,或者我认为应该触发它的假设是错误的?

我还认为我花哨的 util 函数可能是问题的原因。可悲的是,情况并非如此。您可以在没有 utils 文件 here 的情况下查看我的版本。它的行为与其他版本一样。

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() 
    // video urls are from here https://reference.dashif.org/dash.js/nightly/samples/drm/clearkey.html
    // and here https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest_ClearKey.mpd
    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()

【问题讨论】:

【参考方案1】:

我认为您看到的问题是其中一些事件处于“实验”状态,因此支持可能不一致 - 例如:

https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/onencrypted#browser_compatibility

根据经验,浏览器兼容性变化非常频繁,因此您可能必须像以前那样进行试验。

为了增加一点复杂性,实际上流或容器可能会在多个位置指示轨道甚至轨道的一部分已加密。

尤其是过去,不同的玩家会根据他们所看到的位置给出不同的结果。

例如,一个普通播放器过去只查看加密信息的清单,如果它在那里什么也没看到,它会认为它是未加密的,即使媒体流本身在 mp4 中有“原子”表明它是加密的 -这导致播放失败。

更具体地说,查看在支持加密媒体扩展的浏览器中播放加密媒体时触发的事件。从标准看,高层流程如下图:

可以看出,向应用程序报告加密事件是可选的。 'waiting for key' 事件很遗憾没有显示,但它包含在 EME 规范的详细信息中。

正如您所看到的,从我在这里也基于 DASH.js 示例进行的快速检查中,加密事件不会在 Chrome 上触发,而是在 Safari 上触发,并且两者都触发了等待键事件。在 chrome 上,您可以更详细地查看事件和消息,如果需要,使用 Chrome 扩展程序来查看 EME 消息 - 这将再次显示加密事件似乎没有发送到应用程序。

https://developers.google.com/web/updates/2015/09/eme-logger

不幸的是,浏览器实现在其中一些细节上有所不同,如果您查看开源播放器加密处理代码,您会看到这一点 - 例如对于 videojs:

initializeMediaKeys()

player.eme.initializeMediaKeys() 立即按需设置 MediaKey。

这对于在加载任何内容之前为 DRM 设置视频元素很有用。否则,为加密事件的 DRM 设置视频元素。 Safari 不支持此功能。

(https://github.com/videojs/videojs-contrib-eme#initializemediakeys)

播放器在看到可能存在于媒体流中标记内容已加密的几个指示之一之前可以执行此操作的原因是,它还可以读取清单文件中的信息,它在加载之前读取媒体流,指示用于媒体流的加密方案。

值得注意的是,CDM、进行实际解密的元素和(可选)显示器的输出通常也是每个浏览器或 DRM 专有的。

回到您最初的目标,即了解 EME 和加密的工作原理,我认为您的方法很好。您可能还会看到其他浏览器和 CDM 差异。解决方案有时确实发展得很快,尤其是在响应特定攻击或漏洞时,因此您确实需要注意这一点。同样,开源播放器问题和讨论列表是了解最新变化以及历史的重要资源。

【讨论】:

我怀疑 Chrome 或 Firefox 不支持此事件,因为即使它被标记为“实验性”,它也不是很新和/或特别。请参阅this 文章,该文章现在已经快 8 年了,正在谈论该事件。此外,所有浏览器都需要知道文件是加密的,因为它们会触发 waitingforkey 事件。我怀疑我更有可能做错了,浏览器对它的反应不同。但如果您有任何其他提示 Chrome 和 Firefox 不支持此事件,请告诉我。 @Blubberlase 一个快速说明 - 当我查看您的示例时,您的 'sourceBuffer.addEventListener' 行导致错误。你的意思是在这里也使用“等待”吗? 观察力不错。我修复了这个小错误(参见 diff here)。现在在我的在线示例中也已修复。可悲的是,这似乎不是我在问题中描述的奇怪行为的原因。 videojs 的引用让我有点困惑。它说 Safari 不支持加密事件,尽管我的实验清楚地表明它支持。 (也许报价已经过时了?) 图片很好地显示了加密事件是可选的。部分回答我的问题。但图片它是非规范的。我一直在查看规范,但找不到任何规范表明加密和/或 waitingforkey 事件是可选的。如果您能找到规范中清楚显示加密事件如何是可选的部分,那就太好了。我假设 waitingforkey 元素也是可选的,但我也无法在规范中找到提示。这也将描述我在 Firefox 上看到的行为并完全回答我的问题。【参考方案2】:

这个事实最让我困惑,因为如果谷歌浏览器知道它需要一个密钥,我假设它必须知道媒体是加密的,那么为什么它不触发“加密”事件?

我相信这是因为your initialization segment 不包含pssh 原子。当 PSSH 未嵌入媒体文件时,Chrome 似乎不会触发 encrypted 事件。

您可以使用https://gpac.github.io/mp4box.js/test/filereader.html 查看初始化段的 MP4 框和原子。

在您的情况下,PSSH 数据不包含在媒体文件中,而是包含在清单本身中 – https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest.mpd – 您可以从中提取 PSSH 信息,而完全不依赖 encrypted 事件触发,因为您已经有了你需要的初始化数据。

或者,您需要以生成 PSSH 原子的方式打包您的媒体。这就是我使用 mp4encrypt 加密单个分段 MP4(1 个视频和 1 个音轨)以与 cenc 加密和 clearkey "DRM" 一起使用的方法:

mp4encrypt --method MPEG-CENC --key 1:eecdb2b549f02a7c97ce50c17f494ca0:random --property 1:KID:c77fee35e51fd615a7b91afcb1091c5e --key 2:9abb7ab6cc4ad3b86c2193dadb1e786c:random --property 2:KID:045f7ecc35848ed7b3c012ea7614422f --global-option mpeg-cenc.eme-pssh:true source.mp4 target-encrypted.mp4

(但是,您正在使用带有 Widevine 的 DASH 源,因此这并不真正适用于您的情况。我将其包括在内只是为了获得灵感,也许对于使用 clearkey 和单个遇到相同问题的其他人文件播放。)

【讨论】:

以上是关于“加密”事件未触发,尽管使用 cenc mp4 数据提供源缓冲区的主要内容,如果未能解决你的问题,请参考以下文章

沙盒模式下未触发 PayPal Webhook 事件

WPF PropertyChanged 事件未触发/更新文本框

H264学习

为啥尽管输入的值确实发生了变化,但 jquery 更改事件却没有触发? [复制]

尽管正在执行DataLayer推送,但未触发Google跟踪代码管理器代码

SoundCloud 小部件事件未触发