Skip to content
Search

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

<video id="encryptedVideo" controls></video>
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

Favorite site

author Zer0 sitemap