0

我试图了解 DRM 系统是如何工作的,所以我的旅程开始于尝试使用 Clear Key DRM 系统播放 cenc 加密的 mp4 视频,而不使用 dash.js 或 Shaka Player 等任何库。

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

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

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

您可以在下面找到我使用的代码。我正在使用一些便利功能。我希望很清楚他们做了什么。如果不是,您可以在此处查看它们的定义。我的示例也在这里在线供您在浏览器中进行测试和调试。

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 文件的我的版本。它的行为就像其他版本一样。

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()
4

2 回答 2

0

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

我相信那是因为您的初始化段不包含pssh原子。encrypted当 PSSH 未嵌入媒体文件时,Chrome 似乎不会触发事件。

您可以使用https://gpac.github.io/mp4box.js/test/filereader.html查看 init 段的 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 和单个文件播放遇到相同问题的其他人。 )

于 2022-01-22T04:51:28.777 回答
0

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

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

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

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

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

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

在此处输入图像描述

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

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

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

初始化媒体键()

player.eme.initializeMediaKeys() 会根据需要立即设置 MediaKeys。

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

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

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

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

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

于 2021-12-02T18:00:58.767 回答