DRM:HTML5
lastmod 2023-04-22created 2023-04-222 edits
Notes on how to play DRM-protected video files with HTML5 (i.e., in a web browser).
ChatGPT answer
const videoElement = document.getElementById('encryptedVideo');
const videoSource = 'https://example.com/encrypted-video.mp4';
fetch(videoSource)
.then(response => response.arrayBuffer())
.then(videoData => {
const videoContent = new Uint8Array(videoData);
const videoConfig = [{
initDataTypes: ['cenc'],
videoCapabilities: [{contentType: 'video/mp4; codecs="avc1.42E01E"'}],
audioCapabilities: [{contentType: 'audio/mp4; codecs="mp4a.40.2"'}]
}];
return navigator.requestMediaKeySystemAccess('org.w3.clearkey', videoConfig)
.then(keySystemAccess => {
const keySystem = keySystemAccess.keySystem;
const keySession = keySystem.createSession('persistent-license');
keySession.generateRequest('cenc', videoContent).then(request => {
return fetch(videoSource + '.key', {
method: 'POST',
body: request,
headers: new Headers({
'Content-Type': 'application/octet-stream'
})
});
}).then(response => response.arrayBuffer())
.then(keyData => keySession.update(keyData))
.then(() => {
const decryptedStream = new MediaSource();
videoElement.src = URL.createObjectURL(decryptedStream);
const decryptedSourceBuffer = decryptedStream.addSourceBuffer('video/mp4; codecs="avc1.42E01E"');
const video = new Uint8Array(videoContent);
decryptedSourceBuffer.addEventListener('updateend', () => {
if (!decryptedSourceBuffer.updating && decryptedStream.readyState === 'open') {
decryptedSourceBuffer.appendBuffer(video);
}
});
});
});
});
The code above works through the following steps.
- Creates an HTML5 Video element.
- Fetches the video file and converts it to an ArrayBuffer.
- Creates a MediaKeySystemAccess object to play video protected with CENC encryption.
- Creates a MediaKeySession object, generates a CENC request, and sends it to the server.
- The server receives the request, generates an encrypted key, converts it to an ArrayBuffer, and responds.
- Updates the key using the MediaKeySession object.
- Creates a MediaSource object, decrypts the video stream, and adds it to the SourceBuffer.
- Adds the decrypted video stream to the SourceBuffer to play the video.
m3u8 version
const videoElement = document.getElementById('encryptedVideo');
const manifestUrl = 'https://example.com/manifest.m3u8';
fetch(manifestUrl)
.then(response => response.text())
.then(manifestData => {
const manifestLines = manifestData.trim().split('\n');
const manifestBaseUrl = manifestUrl.substring(0, manifestUrl.lastIndexOf('/') + 1);
const variantLines = manifestLines.filter(line => line.startsWith('#EXT-X-STREAM-INF'));
const variantUrls = variantLines.map(line => line.split('\n')[1].trim());
const keySystemConfig = [{
initDataTypes: ['cenc'],
videoCapabilities: [{contentType: 'video/mp4; codecs="avc1.42E01E"'}],
audioCapabilities: [{contentType: 'audio/mp4; codecs="mp4a.40.2"'}]
}];
Promise.all(variantUrls.map(variantUrl => {
return fetch(manifestBaseUrl + variantUrl)
.then(response => response.text())
.then(variantData => {
const variantLines = variantData.trim().split('\n');
const mediaSegmentLines = variantLines.filter(line => line.startsWith('#EXTINF'));
const mediaSegmentUrls = mediaSegmentLines.map((line, index) => variantLines[index * 2 + 1]);
return Promise.all(mediaSegmentUrls.map(segmentUrl => {
return fetch(manifestBaseUrl + segmentUrl)
.then(response => response.arrayBuffer())
.then(segmentData => {
const segmentContent = new Uint8Array(segmentData);
return navigator.requestMediaKeySystemAccess('org.w3.clearkey', keySystemConfig)
.then(keySystemAccess => {
const keySystem = keySystemAccess.keySystem;
const keySession = keySystem.createSession('persistent-license');
keySession.generateRequest('cenc', segmentContent).then(request => {
return fetch(manifestBaseUrl + segmentUrl + '.key', {
method: 'POST',
body: request,
headers: new Headers({
'Content-Type': 'application/octet-stream'
})
});
}).then(response => response.arrayBuffer())
.then(keyData => keySession.update(keyData))
.then(() => {
return segmentData;
});
});
});
}));
})
})).then(mediaSegments => {
const decryptedStreams = mediaSegments.map(segmentData => {
const segmentContent = new Uint8Array(segmentData);
const decryptedStream = new MediaSource();
const decryptedUrl = URL.createObjectURL(decryptedStream);
videoElement.src = decryptedUrl;
const decryptedSourceBuffer = decryptedStream.addSourceBuffer('video/mp4; codecs="avc1.42E01E"');
decryptedSourceBuffer.mode = 'segments';
decryptedSourceBuffer.appendBuffer(segmentContent);
return decryptedStream;
});
// Switch between media streams
let currentStreamIndex = 0;
const switchMediaStream = () => {
currentStreamIndex = (currentStreamIndex + 1) % decryptedStreams.length;
const decryptedStream = decryptedStreams[currentStreamIndex];
if (videoElement.src !== decryptedStream.url) {
videoElement.src = decryptedStream.url;
}
};
setInterval(switchMediaStream, 5000);
});
});
See also
- DRM
- HTML5
- Shaka Packager
- Shaka Player
- Widevine
- PlayReady
- Marlin
- CENC
- FairPlay Streaming
- AES Crypt
- requestMediaKeySystemAccess