Skip to content

DRM:HTML5

DRM으로 보호된 동영상 파일을 HTML5(즉 웹브라우저)로 재생하는 방법에 대한 정리.

ChatGPT 답변

<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);
              }
            });
          });
      });
  });

위의 코드는 다음과 같은 과정을 거쳐 동작합니다.

  • HTML5 Video 요소를 생성합니다.
  • 동영상 파일을 가져와서 ArrayBuffer 형태로 변환합니다.
  • CENC 암호화를 사용하여 보호된 동영상을 재생하기 위한 MediaKeySystemAccess 객체를 생성합니다.
  • MediaKeySession 객체를 생성하고, CENC 요청을 생성하여 서버로 전송합니다.
  • 서버에서는 요청을 받아 암호화된 키를 생성하고, ArrayBuffer 형태로 변환하여 응답합니다.
  • MediaKeySession 객체를 사용하여 키를 업데이트합니다.
  • MediaSource 객체를 생성하고, 비디오 스트림을 복호화하여 SourceBuffer에 추가합니다.
  • SourceBuffer에 복호화된 비디오 스트림을 추가하여 동영상을 재생합니다.

m3u8 버전

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