I am trying to understand how DRM systems work so my journey begins by trying to play a cenc encrypted mp4 video using the Clear Key DRM system without using any library like dash.js or Shaka Player.
The first problem I encountered is that I do not always receive the "encrypted" event. I receive the "encrypted" only on Safari, but not on Google Chrome nor on Firefox.
Interestingly I do receive the "waitingforkey" on Google Chrome and Safari, but not on Firefox.
This fact confuses me the most, because if Google Chrome knows it needs a key, I assume it has to know that the media is encrypted, so why does it not fire the "encrypted" event?
Below you can find the code I use. I am using some convenience functions. I hope it is clear what they do. If not you see their definitions here. Also my example is online here for you to test and debug right in the browser.
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
}
}
}
The cenc encrpyted mp4 files I have are from the dash.js examples page, so I assume that this is not the root of my problem.
To sum up my main question is: Why is the "encrypted" event not fired or is my assumption wrong that it should be fired?
I also thought that my fancy util functions could be the cause of the problem. Sadly this is not the case. You can check out my version without the utils file here. It behaves just like the other version.
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()