DRM 완벽 가이드 | Widevine·FairPlay·PlayReady·AES-128 총정리

DRM 완벽 가이드 | Widevine·FairPlay·PlayReady·AES-128 총정리

이 글의 핵심

DRM(Digital Rights Management) 기술의 모든 것. Widevine, FairPlay, PlayReady, AES-128 암호화, EME, CENC 표준부터 Netflix, YouTube, Spotify의 DRM 구현까지 실전 예제로 완벽 이해.

들어가며: DRM이 필요한 이유

Netflix, Disney+, Spotify 같은 스트리밍 서비스는 어떻게 콘텐츠를 보호할까요? 답은 DRM(Digital Rights Management)입니다. DRM은 디지털 콘텐츠의 불법 복제를 방지하고, 승인된 사용자만 재생할 수 있게 하는 기술입니다.

이 글에서 다룰 내용:

  • 주요 DRM 시스템 (Widevine, FairPlay, PlayReady)
  • DRM 동작 원리와 아키텍처
  • EME (Encrypted Media Extensions)
  • CENC (Common Encryption) 표준
  • 실전 구현 예제

목차

  1. DRM 기본 개념
  2. 주요 DRM 시스템
  3. Widevine (Google)
  4. FairPlay (Apple)
  5. PlayReady (Microsoft)
  6. EME와 CENC
  7. HLS AES-128 암호화
  8. 실전 구현
  9. DRM 우회 방지
  10. 비교 및 선택 가이드

1. DRM 기본 개념

DRM이란?

DRM(Digital Rights Management)은 디지털 콘텐츠의 암호화, 라이선스 관리, 접근 제어를 통해 저작권을 보호하는 기술입니다.

DRM 구성 요소

flowchart TB
    subgraph Content[콘텐츠 제공자]
        Video[원본 비디오]
        Encoder[인코더/패키저]
        KeyServer[키 서버]
    end
    
    subgraph CDN[CDN]
        Encrypted[암호화된<br/>콘텐츠]
    end
    
    subgraph Client[클라이언트]
        Player[비디오 플레이어]
        CDM[CDM<br/>Content Decryption Module]
        TEE[TEE/Hardware<br/>보안 영역]
    end
    
    subgraph License[라이선스 서버]
        Auth[인증]
        LicenseDB[라이선스 DB]
    end
    
    Video --> Encoder
    Encoder -->|암호화| Encrypted
    Encoder -->|키 저장| KeyServer
    
    Encrypted --> Player
    Player -->|라이선스 요청| Auth
    Auth -->|검증| LicenseDB
    LicenseDB -->|키 전달| CDM
    CDM -->|복호화| TEE
    TEE -->|재생| Player

DRM 동작 흐름

sequenceDiagram
    participant User as 사용자
    participant Player as 플레이어
    participant CDN
    participant License as 라이선스 서버
    participant CDM as CDM (보안 모듈)
    
    User->>Player: 비디오 재생 요청
    Player->>CDN: 암호화된 콘텐츠 요청
    CDN->>Player: 암호화된 세그먼트 전달
    
    Player->>License: 라이선스 요청<br/>(사용자 인증 토큰)
    License->>License: 권한 확인<br/>(구독, 지역, 기기)
    License->>Player: 라이선스 + 복호화 키
    
    Player->>CDM: 복호화 요청
    CDM->>CDM: 하드웨어 보안 영역에서<br/>복호화
    CDM->>Player: 복호화된 프레임
    Player->>User: 비디오 재생

2. 주요 DRM 시스템

DRM 생태계

flowchart TB
    subgraph Google
        Widevine[Widevine]
        Chrome[Chrome]
        Android[Android]
    end
    
    subgraph Apple
        FairPlay[FairPlay]
        Safari[Safari]
        iOS[iOS/tvOS]
    end
    
    subgraph Microsoft
        PlayReady[PlayReady]
        Edge[Edge]
        Xbox[Xbox]
    end
    
    subgraph Open
        ClearKey[ClearKey]
        AES128[HLS AES-128]
    end
    
    Widevine --> Chrome
    Widevine --> Android
    
    FairPlay --> Safari
    FairPlay --> iOS
    
    PlayReady --> Edge
    PlayReady --> Xbox

플랫폼별 DRM 지원

플랫폼WidevineFairPlayPlayReadyClearKey
Chrome✅ L1/L3
Safari
Edge
Firefox✅ L3
Android✅ L1/L3
iOS
Xbox

3. Widevine (Google)

Widevine이란?

Widevine은 Google이 개발한 DRM으로, Android와 Chrome에서 널리 사용됩니다. Netflix, YouTube Premium, Disney+가 사용합니다.

Widevine 보안 레벨

flowchart TB
    subgraph L1[Widevine L1<br/>Hardware-backed]
        L1_Desc[✅ TEE/Secure Processor<br/>✅ 4K/HDR 지원<br/>✅ 최고 보안]
    end
    
    subgraph L2[Widevine L2<br/>Software-backed]
        L2_Desc[⚠️ 소프트웨어 보안<br/>⚠️ 1080p 제한<br/>⚠️ 중간 보안]
    end
    
    subgraph L3[Widevine L3<br/>Software only]
        L3_Desc[❌ 기본 보안<br/>❌ 480p/SD 제한<br/>❌ 낮은 보안]
    end
    
    L1 --> Best[최고 화질]
    L2 --> Medium[중간 화질]
    L3 --> Low[낮은 화질]

Widevine 아키�ecture

flowchart TB
    subgraph App[애플리케이션]
        Player[Video Player]
        EME[EME API]
    end
    
    subgraph Browser[브라우저]
        CDM[Widevine CDM]
    end
    
    subgraph Hardware[하드웨어 (L1)]
        TEE[Trusted Execution<br/>Environment]
        SecureVideo[Secure Video Path]
    end
    
    subgraph Backend[백엔드]
        License[Widevine<br/>License Server]
        Content[암호화된<br/>콘텐츠]
    end
    
    Player --> EME
    EME --> CDM
    CDM --> TEE
    
    Player --> Content
    CDM --> License
    
    TEE --> SecureVideo
    SecureVideo --> Display[디스플레이]

Widevine 구현 예시

// HTML5 Video + EME (Encrypted Media Extensions)
const video = document.querySelector('video');
const config = [{
  initDataTypes: ['cenc'],
  videoCapabilities: [{
    contentType: 'video/mp4; codecs="avc1.42E01E"',
    robustness: 'SW_SECURE_CRYPTO'  // L3
    // robustness: 'HW_SECURE_ALL'   // L1
  }],
  audioCapabilities: [{
    contentType: 'audio/mp4; codecs="mp4a.40.2"',
    robustness: 'SW_SECURE_CRYPTO'
  }]
}];

// MediaKeys 생성
navigator.requestMediaKeySystemAccess('com.widevine.alpha', config)
  .then(keySystemAccess => {
    return keySystemAccess.createMediaKeys();
  })
  .then(mediaKeys => {
    return video.setMediaKeys(mediaKeys);
  })
  .then(() => {
    console.log('✅ Widevine initialized');
    
    // 비디오 로드
    video.src = 'https://cdn.example.com/encrypted-video.mp4';
  })
  .catch(err => {
    console.error('❌ DRM error:', err);
  });

// 라이선스 요청 처리
video.addEventListener('encrypted', (event) => {
  const session = video.mediaKeys.createSession();
  
  session.addEventListener('message', (event) => {
    // 라이선스 서버에 요청
    fetch('https://license.example.com/widevine', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/octet-stream',
        'Authorization': 'Bearer ' + userToken
      },
      body: event.message
    })
    .then(response => response.arrayBuffer())
    .then(license => {
      // 라이선스 적용
      return session.update(license);
    })
    .then(() => {
      console.log('✅ License acquired');
    })
    .catch(err => {
      console.error('❌ License error:', err);
    });
  });
  
  // 세션 생성
  session.generateRequest(event.initDataType, event.initData);
});

Widevine 레벨 확인

// 브라우저에서 Widevine 레벨 확인
async function checkWidevineLevel() {
  const configs = [
    {
      label: 'L1 (Hardware)',
      config: [{
        initDataTypes: ['cenc'],
        videoCapabilities: [{
          contentType: 'video/mp4; codecs="avc1.42E01E"',
          robustness: 'HW_SECURE_ALL'
        }]
      }]
    },
    {
      label: 'L3 (Software)',
      config: [{
        initDataTypes: ['cenc'],
        videoCapabilities: [{
          contentType: 'video/mp4; codecs="avc1.42E01E"',
          robustness: 'SW_SECURE_CRYPTO'
        }]
      }]
    }
  ];
  
  for (const {label, config} of configs) {
    try {
      await navigator.requestMediaKeySystemAccess('com.widevine.alpha', config);
      console.log(`✅ ${label} supported`);
      return label;
    } catch (e) {
      console.log(`❌ ${label} not supported`);
    }
  }
  
  return 'Not supported';
}

checkWidevineLevel();

4. FairPlay (Apple)

FairPlay란?

FairPlay는 Apple이 개발한 DRM으로, iOS, macOS, tvOS, Safari에서 사용됩니다. HLS (HTTP Live Streaming)와 통합되어 있습니다.

FairPlay 동작 흐름

sequenceDiagram
    participant Player as AVPlayer
    participant App as 앱
    participant KSM as Key Server Module
    participant License as FairPlay License Server
    
    Player->>App: 암호화된 콘텐츠 감지
    App->>KSM: SPC 요청<br/>(Server Playback Context)
    KSM->>App: SPC 생성
    
    App->>License: SPC + 인증 토큰
    License->>License: 권한 검증
    License->>App: CKC<br/>(Content Key Context)
    
    App->>KSM: CKC 전달
    KSM->>Player: 복호화 키
    Player->>Player: 콘텐츠 재생

FairPlay 구현 (iOS/Swift)

import AVFoundation

class FairPlayManager: NSObject {
    var asset: AVURLAsset?
    var resourceLoaderDelegate: FairPlayResourceLoaderDelegate?
    
    func playVideo(url: URL, certificateURL: URL, licenseURL: URL) {
        // AVURLAsset 생성
        asset = AVURLAsset(url: url)
        
        // Resource Loader Delegate 설정
        resourceLoaderDelegate = FairPlayResourceLoaderDelegate(
            certificateURL: certificateURL,
            licenseURL: licenseURL
        )
        
        asset?.resourceLoader.setDelegate(
            resourceLoaderDelegate,
            queue: DispatchQueue.main
        )
        
        // AVPlayer로 재생
        let playerItem = AVPlayerItem(asset: asset!)
        let player = AVPlayer(playerItem: playerItem)
        player.play()
    }
}

class FairPlayResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate {
    let certificateURL: URL
    let licenseURL: URL
    var certificate: Data?
    
    init(certificateURL: URL, licenseURL: URL) {
        self.certificateURL = certificateURL
        self.licenseURL = licenseURL
        super.init()
        
        // 인증서 다운로드
        loadCertificate()
    }
    
    func loadCertificate() {
        URLSession.shared.dataTask(with: certificateURL) { data, _, error in
            if let data = data {
                self.certificate = data
                print("✅ FairPlay certificate loaded")
            }
        }.resume()
    }
    
    func resourceLoader(
        _ resourceLoader: AVAssetResourceLoader,
        shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest
    ) -> Bool {
        guard let url = loadingRequest.request.url,
              let certificate = certificate else {
            return false
        }
        
        // skd:// URL 처리
        guard url.scheme == "skd" else {
            return false
        }
        
        // SPC (Server Playback Context) 생성
        let contentId = url.host ?? ""
        let contentIdData = contentId.data(using: .utf8)!
        
        do {
            let spcData = try loadingRequest.streamingContentKeyRequestData(
                forApp: certificate,
                contentIdentifier: contentIdData,
                options: nil
            )
            
            // 라이선스 서버에 요청
            requestLicense(spc: spcData, contentId: contentId) { ckcData in
                if let ckc = ckcData {
                    // CKC (Content Key Context) 적용
                    loadingRequest.dataRequest?.respond(with: ckc)
                    loadingRequest.finishLoading()
                    print("✅ FairPlay license acquired")
                } else {
                    loadingRequest.finishLoading(with: NSError(
                        domain: "FairPlay",
                        code: -1,
                        userInfo: nil
                    ))
                }
            }
            
            return true
            
        } catch {
            print("❌ SPC generation failed: \(error)")
            return false
        }
    }
    
    func requestLicense(spc: Data, contentId: String, completion: @escaping (Data?) -> Void) {
        var request = URLRequest(url: licenseURL)
        request.httpMethod = "POST"
        request.httpBody = spc
        request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            if let data = data {
                completion(data)
            } else {
                completion(nil)
            }
        }.resume()
    }
}

FairPlay HLS 매니페스트

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0

# FairPlay 키 정보
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://content-id-12345",KEYFORMAT="com.apple.streamingkeydelivery",KEYFORMATVERSIONS="1"

# 세그먼트
#EXTINF:10.0,
segment-0.m4s
#EXTINF:10.0,
segment-1.m4s
#EXTINF:10.0,
segment-2.m4s

5. PlayReady (Microsoft)

PlayReady란?

PlayReady는 Microsoft가 개발한 DRM으로, Windows, Xbox, Edge에서 사용됩니다.

PlayReady 아키텍처

flowchart TB
    subgraph Client[클라이언트]
        App[애플리케이션]
        PRT[PlayReady Runtime]
        TEE[Trusted Execution<br/>Environment]
    end
    
    subgraph Server[서버]
        Content[암호화된 콘텐츠]
        License[PlayReady<br/>License Server]
    end
    
    App --> PRT
    PRT --> TEE
    
    App --> Content
    PRT --> License
    
    License -->|라이선스| PRT
    Content -->|암호화된 데이터| PRT
    PRT -->|복호화| TEE

PlayReady 구현 (C#)

using Windows.Media.Protection;
using Windows.Media.Protection.PlayReady;

public class PlayReadyManager
{
    private MediaProtectionManager protectionManager;
    
    public void Initialize()
    {
        // MediaProtectionManager 생성
        protectionManager = new MediaProtectionManager();
        
        // PlayReady 서비스 요청 처리
        protectionManager.ServiceRequested += OnServiceRequested;
        
        // 컴포넌트 로드 실패 처리
        protectionManager.ComponentLoadFailed += OnComponentLoadFailed;
        
        // PlayReady 속성 설정
        var props = new Windows.Foundation.Collections.PropertySet();
        props.Add(
            "{F4637010-03C3-42CD-B932-B48ADF3A6A54}",
            "Windows.Media.Protection.PlayReady.PlayReadyWinRTTrustedInput"
        );
        protectionManager.Properties.Add("Windows.Media.Protection.MediaProtectionSystemId", 
            "{F4637010-03C3-42CD-B932-B48ADF3A6A54}");
        protectionManager.Properties.Add("Windows.Media.Protection.MediaProtectionSystemIdMapping", 
            props);
    }
    
    private async void OnServiceRequested(
        MediaProtectionManager sender,
        ServiceRequestedEventArgs args)
    {
        if (args.Request is PlayReadyIndividualizationServiceRequest)
        {
            // 개별화 요청
            var request = args.Request as PlayReadyIndividualizationServiceRequest;
            await request.BeginServiceRequest();
        }
        else if (args.Request is PlayReadyLicenseAcquisitionServiceRequest)
        {
            // 라이선스 요청
            var request = args.Request as PlayReadyLicenseAcquisitionServiceRequest;
            
            // 커스텀 헤더 추가 (인증 토큰 등)
            request.ChallengeCustomData = GetAuthToken();
            
            await request.BeginServiceRequest();
            Console.WriteLine("✅ PlayReady license acquired");
        }
    }
    
    private void OnComponentLoadFailed(
        MediaProtectionManager sender,
        ComponentLoadFailedEventArgs args)
    {
        Console.WriteLine($"❌ Component load failed: {args.Information.Items}");
    }
    
    private string GetAuthToken()
    {
        // 사용자 인증 토큰 생성
        return "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
    }
}

6. EME와 CENC

EME (Encrypted Media Extensions)

EME는 W3C 표준으로, 브라우저에서 DRM 콘텐츠를 재생하기 위한 JavaScript API입니다.

// EME 지원 확인
if ('mediaKeys' in HTMLMediaElement.prototype) {
  console.log('✅ EME supported');
} else {
  console.log('❌ EME not supported');
}

// 지원되는 DRM 시스템 확인
const drmSystems = [
  'com.widevine.alpha',           // Widevine
  'com.apple.fps.1_0',            // FairPlay
  'com.microsoft.playready',      // PlayReady
  'org.w3.clearkey'               // ClearKey
];

for (const system of drmSystems) {
  try {
    await navigator.requestMediaKeySystemAccess(system, [{}]);
    console.log(`✅ ${system} supported`);
  } catch (e) {
    console.log(`❌ ${system} not supported`);
  }
}

CENC (Common Encryption)

CENC는 하나의 암호화된 파일을 여러 DRM 시스템에서 사용할 수 있게 하는 표준입니다.

flowchart TB
    Original[원본 비디오] --> Encoder[인코더]
    
    Encoder --> CENC[CENC 암호화<br/>단일 파일]
    
    CENC --> Chrome[Chrome<br/>Widevine]
    CENC --> Safari[Safari<br/>FairPlay]
    CENC --> Edge[Edge<br/>PlayReady]
    
    Chrome --> Play1[재생]
    Safari --> Play2[재생]
    Edge --> Play3[재생]

CENC 암호화 (Shaka Packager)

# Shaka Packager로 CENC 암호화
packager \
  in=input.mp4,stream=video,output=video_encrypted.mp4 \
  in=input.mp4,stream=audio,output=audio_encrypted.mp4 \
  --enable_raw_key_encryption \
  --keys label=:key_id=<KEY_ID>:key=<KEY> \
  --protection_scheme cenc \
  --clear_lead 3 \
  --mpd_output manifest.mpd

# 여러 DRM 시스템 지원
packager \
  in=input.mp4,stream=video,output=video.mp4 \
  --enable_widevine_encryption \
  --key_server_url "https://license.widevine.com/cenc/getcontentkey" \
  --content_id "content-123" \
  --signer "widevine_test" \
  --enable_playready_encryption \
  --playready_server_url "https://playready.example.com" \
  --enable_fairplay_encryption \
  --fairplay_key_uri "skd://fairplay-key-123"

7. HLS AES-128 암호화

HLS AES-128이란?

HLS AES-128은 Apple의 HTTP Live Streaming에서 사용하는 간단한 암호화 방식입니다. 완전한 DRM은 아니지만, 기본적인 콘텐츠 보호를 제공합니다.

HLS 암호화 매니페스트

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0

# AES-128 암호화 키 정보
#EXT-X-KEY:METHOD=AES-128,URI="https://example.com/key.bin",IV=0x12345678901234567890123456789012

# 세그먼트
#EXTINF:10.0,
segment-0.ts
#EXTINF:10.0,
segment-1.ts
#EXTINF:10.0,
segment-2.ts

HLS 암호화 생성 (FFmpeg)

# 1. 암호화 키 생성 (16바이트)
openssl rand 16 > enc.key

# 2. IV 생성 (16바이트)
openssl rand -hex 16 > enc.iv

# 3. 키 정보 파일 생성
echo "https://example.com/key.bin" > enc.keyinfo
cat enc.key >> enc.keyinfo
cat enc.iv >> enc.keyinfo

# 4. FFmpeg로 암호화
ffmpeg -i input.mp4 \
  -c:v libx264 -c:a aac \
  -hls_time 10 \
  -hls_key_info_file enc.keyinfo \
  -hls_playlist_type vod \
  -hls_segment_filename "segment_%03d.ts" \
  playlist.m3u8

# 생성된 파일:
# - playlist.m3u8 (매니페스트)
# - segment_000.ts, segment_001.ts, ... (암호화된 세그먼트)

HLS 복호화 (Python)

from Crypto.Cipher import AES
import requests

def decrypt_hls_segment(segment_url, key_url, iv):
    """HLS 세그먼트 복호화"""
    # 암호화 키 다운로드
    key_response = requests.get(key_url)
    key = key_response.content
    
    # 암호화된 세그먼트 다운로드
    segment_response = requests.get(segment_url)
    encrypted_data = segment_response.content
    
    # AES-128 CBC 복호화
    cipher = AES.new(key, AES.MODE_CBC, iv)
    decrypted_data = cipher.decrypt(encrypted_data)
    
    # PKCS7 패딩 제거
    padding_length = decrypted_data[-1]
    decrypted_data = decrypted_data[:-padding_length]
    
    return decrypted_data

# 사용
iv = bytes.fromhex('12345678901234567890123456789012')
decrypted = decrypt_hls_segment(
    'https://cdn.example.com/segment_000.ts',
    'https://example.com/key.bin',
    iv
)

with open('decrypted_segment.ts', 'wb') as f:
    f.write(decrypted)

8. 실전 구현

멀티 DRM 플레이어

class MultiDRMPlayer {
  constructor(videoElement) {
    this.video = videoElement;
    this.drmSystem = null;
  }
  
  async initialize(manifestUrl, licenseUrl, authToken) {
    // DRM 시스템 감지
    this.drmSystem = await this.detectDRM();
    console.log(`🔒 DRM System: ${this.drmSystem}`);
    
    // DRM 설정
    await this.setupDRM(licenseUrl, authToken);
    
    // 비디오 로드
    this.video.src = manifestUrl;
    await this.video.play();
  }
  
  async detectDRM() {
    // 플랫폼별 DRM 감지
    const ua = navigator.userAgent;
    
    if (/iPhone|iPad|iPod/.test(ua)) {
      return 'fairplay';
    } else if (/Android/.test(ua)) {
      return 'widevine';
    } else if (/Windows/.test(ua)) {
      // Edge는 PlayReady와 Widevine 모두 지원
      return 'widevine';  // 또는 'playready'
    } else if (/Mac/.test(ua)) {
      return 'fairplay';
    }
    
    return 'widevine';  // 기본값
  }
  
  async setupDRM(licenseUrl, authToken) {
    if (this.drmSystem === 'fairplay') {
      await this.setupFairPlay(licenseUrl, authToken);
    } else if (this.drmSystem === 'widevine') {
      await this.setupWidevine(licenseUrl, authToken);
    } else if (this.drmSystem === 'playready') {
      await this.setupPlayReady(licenseUrl, authToken);
    }
  }
  
  async setupWidevine(licenseUrl, authToken) {
    const config = [{
      initDataTypes: ['cenc'],
      videoCapabilities: [{
        contentType: 'video/mp4; codecs="avc1.42E01E"',
        robustness: 'SW_SECURE_CRYPTO'
      }],
      audioCapabilities: [{
        contentType: 'audio/mp4; codecs="mp4a.40.2"',
        robustness: 'SW_SECURE_CRYPTO'
      }]
    }];
    
    const keySystemAccess = await navigator.requestMediaKeySystemAccess(
      'com.widevine.alpha',
      config
    );
    
    const mediaKeys = await keySystemAccess.createMediaKeys();
    await this.video.setMediaKeys(mediaKeys);
    
    // 라이선스 요청 처리
    this.video.addEventListener('encrypted', (event) => {
      const session = mediaKeys.createSession();
      
      session.addEventListener('message', async (event) => {
        const response = await fetch(licenseUrl, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/octet-stream',
            'Authorization': `Bearer ${authToken}`
          },
          body: event.message
        });
        
        const license = await response.arrayBuffer();
        await session.update(license);
      });
      
      session.generateRequest(event.initDataType, event.initData);
    });
    
    console.log('✅ Widevine initialized');
  }
  
  async setupFairPlay(licenseUrl, authToken) {
    // FairPlay는 네이티브 구현 필요
    console.log('ℹ️  FairPlay requires native implementation');
  }
  
  async setupPlayReady(licenseUrl, authToken) {
    const config = [{
      initDataTypes: ['cenc'],
      videoCapabilities: [{
        contentType: 'video/mp4; codecs="avc1.42E01E"'
      }]
    }];
    
    const keySystemAccess = await navigator.requestMediaKeySystemAccess(
      'com.microsoft.playready',
      config
    );
    
    const mediaKeys = await keySystemAccess.createMediaKeys();
    await this.video.setMediaKeys(mediaKeys);
    
    console.log('✅ PlayReady initialized');
  }
}

// 사용
const player = new MultiDRMPlayer(document.querySelector('video'));
player.initialize(
  'https://cdn.example.com/manifest.mpd',
  'https://license.example.com/widevine',
  'user-auth-token-123'
);

9. DRM 우회 방지

보안 레이어

flowchart TB
    subgraph Layer1[레이어 1: 암호화]
        Encrypt[AES-128/256<br/>콘텐츠 암호화]
    end
    
    subgraph Layer2[레이어 2: 키 관리]
        KeyRotation[키 로테이션]
        KeyDerivation[키 파생]
    end
    
    subgraph Layer3[레이어 3: 하드웨어 보안]
        TEE[Trusted Execution<br/>Environment]
        SecurePath[Secure Video Path]
    end
    
    subgraph Layer4[레이어 4: 애플리케이션]
        Obfuscation[코드 난독화]
        AntiDebug[디버깅 방지]
        Watermark[워터마크]
    end
    
    subgraph Layer5[레이어 5: 네트워크]
        TokenAuth[토큰 인증]
        GeoBlock[지역 차단]
        RateLimit[재생 횟수 제한]
    end
    
    Encrypt --> KeyRotation
    KeyRotation --> TEE
    TEE --> Obfuscation
    Obfuscation --> TokenAuth

워터마킹

from PIL import Image, ImageDraw, ImageFont
import hashlib

def add_forensic_watermark(video_frame, user_id, timestamp):
    """포렌식 워터마크 추가"""
    img = Image.fromarray(video_frame)
    draw = ImageDraw.Draw(img)
    
    # 사용자 식별 정보 해시
    watermark_data = f"{user_id}:{timestamp}"
    watermark_hash = hashlib.sha256(watermark_data.encode()).hexdigest()[:16]
    
    # 보이지 않는 워터마크 (LSB 스테가노그래피)
    pixels = img.load()
    width, height = img.size
    
    for i, bit in enumerate(watermark_hash):
        if i >= width * height:
            break
        
        x = i % width
        y = i // width
        
        r, g, b = pixels[x, y]
        # LSB에 워터마크 비트 삽입
        r = (r & 0xFE) | (ord(bit) & 0x01)
        pixels[x, y] = (r, g, b)
    
    return img

# 워터마크 추출
def extract_watermark(video_frame):
    """워터마크 추출"""
    img = Image.fromarray(video_frame)
    pixels = img.load()
    width, height = img.size
    
    watermark_bits = []
    for i in range(16):  # 16자 해시
        x = i % width
        y = i // width
        r, g, b = pixels[x, y]
        watermark_bits.append(chr(r & 0x01))
    
    return ''.join(watermark_bits)

화면 캡처 방지

// 화면 캡처 감지 (완벽하지 않음)
document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    // 화면 캡처 의심
    console.warn('⚠️ Screen capture suspected');
    video.pause();
  }
});

// 개발자 도구 감지
setInterval(() => {
  const before = new Date();
  debugger;  // 개발자 도구 열려있으면 멈춤
  const after = new Date();
  
  if (after - before > 100) {
    console.warn('⚠️ DevTools detected');
    video.pause();
  }
}, 1000);

// 우클릭 방지
video.addEventListener('contextmenu', (e) => {
  e.preventDefault();
});

// 드래그 방지
video.addEventListener('dragstart', (e) => {
  e.preventDefault();
});

실전 시나리오

시나리오 1: Netflix 스타일 DRM

from flask import Flask, request, jsonify
import jwt
import time
import hashlib

app = Flask(__name__)

class DRMLicenseServer:
    def __init__(self):
        self.content_keys = {}  # content_id -> key
        self.user_licenses = {}  # user_id -> [content_ids]
    
    def generate_content_key(self, content_id):
        """콘텐츠 암호화 키 생성"""
        key = hashlib.sha256(f"{content_id}:{time.time()}".encode()).digest()[:16]
        self.content_keys[content_id] = key
        return key
    
    def verify_user_access(self, user_id, content_id):
        """사용자 접근 권한 확인"""
        # 구독 상태 확인
        subscription = get_user_subscription(user_id)
        if not subscription or not subscription.is_active():
            return False
        
        # 지역 제한 확인
        user_country = get_user_country(user_id)
        content_regions = get_content_regions(content_id)
        if user_country not in content_regions:
            return False
        
        # 동시 재생 제한 확인
        active_sessions = get_active_sessions(user_id)
        if len(active_sessions) >= subscription.max_streams:
            return False
        
        return True
    
    def issue_license(self, user_id, content_id, device_id):
        """라이선스 발급"""
        if not self.verify_user_access(user_id, content_id):
            return None
        
        # 라이선스 생성
        license_data = {
            'user_id': user_id,
            'content_id': content_id,
            'device_id': device_id,
            'issued_at': int(time.time()),
            'expires_at': int(time.time()) + 86400,  # 24시간
            'max_resolution': '1080p',
            'offline_allowed': False
        }
        
        # JWT로 서명
        license_token = jwt.encode(
            license_data,
            'secret-key',
            algorithm='HS256'
        )
        
        # 콘텐츠 키 (암호화되어 전달)
        content_key = self.content_keys.get(content_id)
        
        return {
            'license': license_token,
            'key': content_key.hex(),
            'expires_in': 86400
        }

@app.route('/license/widevine', methods=['POST'])
def widevine_license():
    # Widevine 라이선스 요청 처리
    challenge = request.data  # Widevine challenge
    auth_token = request.headers.get('Authorization')
    
    # 토큰 검증
    try:
        payload = jwt.decode(auth_token.replace('Bearer ', ''), 'secret-key', algorithms=['HS256'])
        user_id = payload['user_id']
        content_id = payload['content_id']
    except:
        return jsonify({'error': 'Invalid token'}), 401
    
    # 라이선스 발급
    drm = DRMLicenseServer()
    license_data = drm.issue_license(user_id, content_id, request.remote_addr)
    
    if not license_data:
        return jsonify({'error': 'Access denied'}), 403
    
    # Widevine 라이선스 응답 (실제로는 Widevine SDK 사용)
    return license_data['key'], 200, {'Content-Type': 'application/octet-stream'}

시나리오 2: 오프라인 재생 (다운로드)

// 오프라인 DRM 콘텐츠 저장
class OfflineDRMPlayer {
  async downloadContent(manifestUrl, licenseUrl, authToken) {
    // Persistent License 요청
    const config = [{
      initDataTypes: ['cenc'],
      videoCapabilities: [{
        contentType: 'video/mp4; codecs="avc1.42E01E"'
      }],
      persistentState: 'required',  // 오프라인 지원
      sessionTypes: ['persistent-license']
    }];
    
    const keySystemAccess = await navigator.requestMediaKeySystemAccess(
      'com.widevine.alpha',
      config
    );
    
    const mediaKeys = await keySystemAccess.createMediaKeys();
    
    // Persistent Session 생성
    const session = mediaKeys.createSession('persistent-license');
    
    // 라이선스 요청
    session.addEventListener('message', async (event) => {
      const response = await fetch(licenseUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/octet-stream',
          'Authorization': `Bearer ${authToken}`
        },
        body: event.message
      });
      
      const license = await response.arrayBuffer();
      await session.update(license);
      
      // Session ID 저장 (나중에 재사용)
      const sessionId = session.sessionId;
      localStorage.setItem('drm-session-' + manifestUrl, sessionId);
      
      console.log('✅ Persistent license stored');
    });
    
    // 콘텐츠 다운로드 (Service Worker 사용)
    await this.downloadSegments(manifestUrl);
  }
  
  async playOffline(manifestUrl) {
    // 저장된 Session ID 로드
    const sessionId = localStorage.getItem('drm-session-' + manifestUrl);
    
    if (!sessionId) {
      throw new Error('No offline license found');
    }
    
    // MediaKeys 복원
    const keySystemAccess = await navigator.requestMediaKeySystemAccess(
      'com.widevine.alpha',
      [{ persistentState: 'required' }]
    );
    
    const mediaKeys = await keySystemAccess.createMediaKeys();
    await this.video.setMediaKeys(mediaKeys);
    
    // Session 로드
    const session = mediaKeys.createSession('persistent-license');
    await session.load(sessionId);
    
    // 오프라인 콘텐츠 재생
    this.video.src = 'offline://' + manifestUrl;
    await this.video.play();
    
    console.log('✅ Playing offline content');
  }
  
  async downloadSegments(manifestUrl) {
    // Service Worker에서 세그먼트 다운로드 및 캐싱
    const registration = await navigator.serviceWorker.ready;
    await registration.sync.register('download-' + manifestUrl);
  }
}

DRM 콘텐츠 패키징

Shaka Packager로 멀티 DRM

#!/bin/bash

INPUT="input.mp4"
OUTPUT_DIR="output"

# Widevine + PlayReady + FairPlay 동시 지원
packager \
  in=$INPUT,stream=video,output=$OUTPUT_DIR/video.mp4 \
  in=$INPUT,stream=audio,output=$OUTPUT_DIR/audio.mp4 \
  --mpd_output $OUTPUT_DIR/manifest.mpd \
  --hls_master_playlist_output $OUTPUT_DIR/master.m3u8 \
  \
  --enable_widevine_encryption \
  --key_server_url "https://license.widevine.com/cenc/getcontentkey/widevine_test" \
  --content_id "content-123" \
  --signer "widevine_test" \
  --aes_signing_key "1ae8ccd0e7985cc0b6203a55855a1034afc252980e970ca90e5202689f947ab9" \
  --aes_signing_iv "d58ce954203b7c9a9a9d467f59839249" \
  \
  --enable_playready_encryption \
  --playready_server_url "https://playready.example.com/rightsmanager.asmx" \
  --playready_key_id "10000000100010001000100000000001" \
  --playready_key "3a2a1b68dd2b9d8b8d8d8d8d8d8d8d8d" \
  \
  --enable_fairplay_encryption \
  --fairplay_key_uri "skd://fairplay-key-123" \
  \
  --protection_scheme cenc \
  --clear_lead 3

echo "✅ Multi-DRM packaging complete"

DASH 매니페스트 (CENC)

<?xml version="1.0"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011">
  <Period>
    <AdaptationSet mimeType="video/mp4" codecs="avc1.42E01E">
      <!-- DRM 정보 -->
      <ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc"/>
      
      <!-- Widevine -->
      <ContentProtection schemeIdUri="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed">
        <cenc:pssh>AAAAA...</cenc:pssh>
      </ContentProtection>
      
      <!-- PlayReady -->
      <ContentProtection schemeIdUri="urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95">
        <cenc:pssh>BBBBB...</cenc:pssh>
      </ContentProtection>
      
      <Representation bandwidth="1000000" width="1280" height="720">
        <SegmentTemplate media="video_$Number$.m4s" initialization="video_init.mp4"/>
      </Representation>
    </AdaptationSet>
  </Period>
</MPD>

라이선스 서버 구현

Node.js 라이선스 서버

const express = require('express');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

const app = express();
app.use(express.raw({ type: 'application/octet-stream', limit: '10mb' }));

// 콘텐츠 키 저장소
const contentKeys = new Map();

// 라이선스 발급
app.post('/license/:drm', async (req, res) => {
  const drmType = req.params.drm;  // widevine, fairplay, playready
  const challenge = req.body;
  const authToken = req.headers.authorization?.replace('Bearer ', '');
  
  try {
    // 토큰 검증
    const payload = jwt.verify(authToken, process.env.JWT_SECRET);
    const { userId, contentId, deviceId } = payload;
    
    // 권한 확인
    const hasAccess = await verifyUserAccess(userId, contentId);
    if (!hasAccess) {
      return res.status(403).json({ error: 'Access denied' });
    }
    
    // 콘텐츠 키 조회
    let contentKey = contentKeys.get(contentId);
    if (!contentKey) {
      // 새 키 생성
      contentKey = crypto.randomBytes(16);
      contentKeys.set(contentId, contentKey);
    }
    
    // DRM별 라이선스 생성
    let license;
    if (drmType === 'widevine') {
      license = await generateWidevineLicense(challenge, contentKey, userId);
    } else if (drmType === 'fairplay') {
      license = await generateFairPlayLicense(challenge, contentKey, userId);
    } else if (drmType === 'playready') {
      license = await generatePlayReadyLicense(challenge, contentKey, userId);
    }
    
    // 재생 로그
    await logPlayback(userId, contentId, deviceId, drmType);
    
    res.set('Content-Type', 'application/octet-stream');
    res.send(license);
    
    console.log(`✅ License issued: ${drmType} for user ${userId}`);
    
  } catch (err) {
    console.error('❌ License error:', err);
    res.status(401).json({ error: 'Invalid token' });
  }
});

async function verifyUserAccess(userId, contentId) {
  // 구독 확인
  const subscription = await getSubscription(userId);
  if (!subscription || !subscription.active) {
    return false;
  }
  
  // 지역 제한 확인
  const userCountry = await getUserCountry(userId);
  const contentRegions = await getContentRegions(contentId);
  if (!contentRegions.includes(userCountry)) {
    return false;
  }
  
  // 동시 재생 제한
  const activeSessions = await getActiveSessions(userId);
  if (activeSessions.length >= subscription.maxStreams) {
    return false;
  }
  
  return true;
}

async function generateWidevineLicense(challenge, contentKey, userId) {
  // 실제로는 Widevine SDK 사용
  // 여기서는 간소화된 예시
  
  // 1. Challenge 파싱
  // 2. 콘텐츠 키로 라이선스 생성
  // 3. 사용자별 제한 사항 추가 (해상도, 기간 등)
  
  const license = Buffer.from('widevine-license-data');
  return license;
}

app.listen(3000, () => {
  console.log('🔒 DRM License Server running on port 3000');
});

DRM 비교

기술적 비교

DRM개발사플랫폼보안 레벨최대 화질오프라인
Widevine L1GoogleAndroid, Chrome하드웨어4K
Widevine L3GoogleChrome, Firefox소프트웨어480p
FairPlayAppleiOS, Safari하드웨어4K
PlayReadyMicrosoftWindows, Xbox하드웨어4K
HLS AES-128Apple모든 플랫폼기본제한 없음
ClearKeyW3C모든 플랫폼없음제한 없음

서비스별 DRM 사용

flowchart TB
    subgraph Netflix
        N_W[Widevine L1/L3]
        N_F[FairPlay]
        N_P[PlayReady]
    end
    
    subgraph YouTube_Premium
        Y_W[Widevine L3]
        Y_F[FairPlay]
    end
    
    subgraph Disney_Plus
        D_W[Widevine L1]
        D_F[FairPlay]
        D_P[PlayReady]
    end
    
    subgraph Spotify
        S_W[Widevine L3]
        S_F[FairPlay]
    end

DRM 우회 기법과 대응

일반적인 우회 시도

flowchart TB
    Attack[DRM 우회 시도]
    
    A1[화면 캡처]
    A2[메모리 덤프]
    A3[디버거 사용]
    A4[CDM 추출]
    A5[HDMI 캡처]
    
    Attack --> A1
    Attack --> A2
    Attack --> A3
    Attack --> A4
    Attack --> A5
    
    A1 --> D1[대응: 워터마크]
    A2 --> D2[대응: 메모리 암호화]
    A3 --> D3[대응: 안티 디버깅]
    A4 --> D4[대응: TEE/하드웨어]
    A5 --> D5[대응: HDCP]

HDCP (High-bandwidth Digital Content Protection)

HDCP: 디지털 비디오 인터페이스 보호

HDMI/DisplayPort 연결에서 콘텐츠를 암호화하여
캡처 장비로 녹화를 방지

DRM L1 + HDCP → 완전한 보호 경로

안티 디버깅

// 디버거 감지
(function() {
  let devtoolsOpen = false;
  const threshold = 160;
  
  setInterval(() => {
    if (window.outerWidth - window.innerWidth > threshold ||
        window.outerHeight - window.innerHeight > threshold) {
      if (!devtoolsOpen) {
        devtoolsOpen = true;
        console.log('⚠️ DevTools detected, pausing playback');
        document.querySelector('video')?.pause();
      }
    } else {
      devtoolsOpen = false;
    }
  }, 500);
})();

// 코드 난독화 (webpack)
module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          mangle: true,
          compress: {
            drop_console: true,
            drop_debugger: true
          }
        }
      })
    ]
  }
};

실전 플레이어 라이브러리

Shaka Player (Google)

// Shaka Player로 멀티 DRM 지원
import shaka from 'shaka-player';

const video = document.querySelector('video');
const player = new shaka.Player(video);

// DRM 설정
player.configure({
  drm: {
    servers: {
      'com.widevine.alpha': 'https://license.example.com/widevine',
      'com.microsoft.playready': 'https://license.example.com/playready'
    },
    advanced: {
      'com.widevine.alpha': {
        'videoRobustness': 'SW_SECURE_CRYPTO',
        'audioRobustness': 'SW_SECURE_CRYPTO'
      }
    }
  }
});

// 라이선스 요청 인터셉터
player.getNetworkingEngine().registerRequestFilter((type, request) => {
  if (type === shaka.net.NetworkingEngine.RequestType.LICENSE) {
    // 인증 헤더 추가
    request.headers['Authorization'] = 'Bearer ' + getAuthToken();
  }
});

// 비디오 로드
player.load('https://cdn.example.com/manifest.mpd')
  .then(() => {
    console.log('✅ Video loaded');
    video.play();
  })
  .catch(err => {
    console.error('❌ Load error:', err);
  });

Video.js + videojs-contrib-eme

import videojs from 'video.js';
import 'videojs-contrib-eme';

const player = videojs('my-video', {
  plugins: {
    eme: {
      keySystems: {
        'com.widevine.alpha': {
          url: 'https://license.example.com/widevine',
          licenseHeaders: {
            'Authorization': 'Bearer ' + authToken
          },
          videoRobustness: 'SW_SECURE_CRYPTO',
          audioRobustness: 'SW_SECURE_CRYPTO'
        },
        'com.apple.fps.1_0': {
          certificateUri: 'https://example.com/fairplay.cer',
          licenseUri: 'https://license.example.com/fairplay',
          licenseHeaders: {
            'Authorization': 'Bearer ' + authToken
          }
        }
      }
    }
  }
});

player.src({
  src: 'https://cdn.example.com/manifest.mpd',
  type: 'application/dash+xml'
});

player.play();

DRM 테스트

Widevine 테스트 콘텐츠

// Google의 공개 테스트 스트림
const testStreams = {
  widevine: {
    manifest: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine/dash.mpd',
    license: 'https://cwip-shaka-proxy.appspot.com/no_auth'
  },
  clearKey: {
    manifest: 'https://storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd',
    license: 'https://cwip-shaka-proxy.appspot.com/clearkey'
  }
};

// Shaka Player로 테스트
async function testDRM() {
  const player = new shaka.Player(video);
  
  player.configure({
    drm: {
      servers: {
        'com.widevine.alpha': testStreams.widevine.license
      }
    }
  });
  
  try {
    await player.load(testStreams.widevine.manifest);
    console.log('✅ Widevine test successful');
  } catch (err) {
    console.error('❌ Widevine test failed:', err);
  }
}

브라우저 DRM 지원 확인

async function checkDRMSupport() {
  const drmSystems = [
    { name: 'Widevine', keySystem: 'com.widevine.alpha' },
    { name: 'FairPlay', keySystem: 'com.apple.fps.1_0' },
    { name: 'PlayReady', keySystem: 'com.microsoft.playready' },
    { name: 'ClearKey', keySystem: 'org.w3.clearkey' }
  ];
  
  const results = {};
  
  for (const drm of drmSystems) {
    try {
      const config = [{
        initDataTypes: ['cenc'],
        videoCapabilities: [{
          contentType: 'video/mp4; codecs="avc1.42E01E"'
        }]
      }];
      
      await navigator.requestMediaKeySystemAccess(drm.keySystem, config);
      results[drm.name] = '✅ Supported';
    } catch (e) {
      results[drm.name] = '❌ Not supported';
    }
  }
  
  console.table(results);
  return results;
}

// 실행
checkDRMSupport();

성능 최적화

라이선스 캐싱

class LicenseCache {
  constructor() {
    this.cache = new Map();
    this.maxAge = 3600 * 1000;  // 1시간
  }
  
  async getLicense(contentId, licenseUrl, authToken) {
    const cacheKey = `${contentId}:${authToken}`;
    const cached = this.cache.get(cacheKey);
    
    // 캐시 확인
    if (cached && Date.now() - cached.timestamp < this.maxAge) {
      console.log('✅ License from cache');
      return cached.license;
    }
    
    // 라이선스 요청
    const response = await fetch(licenseUrl, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${authToken}`
      }
    });
    
    const license = await response.arrayBuffer();
    
    // 캐시 저장
    this.cache.set(cacheKey, {
      license,
      timestamp: Date.now()
    });
    
    console.log('✅ License from server');
    return license;
  }
  
  clear() {
    this.cache.clear();
  }
}

const licenseCache = new LicenseCache();

프리로딩

// 라이선스 미리 요청
async function preloadLicense(contentId, licenseUrl, authToken) {
  const config = [{
    initDataTypes: ['cenc'],
    videoCapabilities: [{
      contentType: 'video/mp4; codecs="avc1.42E01E"'
    }]
  }];
  
  const keySystemAccess = await navigator.requestMediaKeySystemAccess(
    'com.widevine.alpha',
    config
  );
  
  const mediaKeys = await keySystemAccess.createMediaKeys();
  const session = mediaKeys.createSession();
  
  // 미리 라이선스 요청
  session.addEventListener('message', async (event) => {
    const response = await fetch(licenseUrl, {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${authToken}` },
      body: event.message
    });
    
    const license = await response.arrayBuffer();
    await session.update(license);
    
    console.log('✅ License preloaded');
  });
  
  // 더미 init data로 세션 생성
  const initData = new Uint8Array([/* PSSH box */]);
  await session.generateRequest('cenc', initData);
}

정리

DRM 선택 가이드

flowchart TD
    Start[DRM 선택] --> Q1{플랫폼은?}
    
    Q1 -->|iOS/Safari| FairPlay[FairPlay]
    Q1 -->|Android/Chrome| Widevine[Widevine]
    Q1 -->|Windows/Xbox| PlayReady[PlayReady]
    Q1 -->|멀티 플랫폼| Multi[멀티 DRM<br/>CENC]
    
    Multi --> Q2{보안 수준은?}
    Q2 -->|최고| L1[Widevine L1<br/>+ FairPlay<br/>+ PlayReady]
    Q2 -->|중간| L3[Widevine L3<br/>+ FairPlay]
    Q2 -->|기본| AES[HLS AES-128]

구현 복잡도

복잡도: FairPlay > PlayReady > Widevine > HLS AES-128

FairPlay:
  - 인증서 발급 필요
  - 네이티브 구현 필수
  - Apple 승인 필요

Widevine:
  - 웹 표준 EME 사용
  - JavaScript로 구현 가능
  - Google 계약 필요

PlayReady:
  - Windows 플랫폼 통합
  - C# 구현 권장
  - Microsoft 계약 필요

HLS AES-128:
  - 간단한 구현
  - FFmpeg로 생성 가능
  - 계약 불필요

비용 비교

라이선스 비용 (대략적):

Widevine:
  - 무료 (테스트)
  - 상용: 기기당 과금

FairPlay:
  - Apple Developer Program: $99/년
  - 추가 비용 없음

PlayReady:
  - 기기당 라이선스 비용
  - 볼륨 할인 가능

HLS AES-128:
  - 완전 무료
  - 오픈소스

베스트 프랙티스

1. 멀티 DRM 전략

// 플랫폼 감지 후 적절한 DRM 선택
function selectDRM() {
  const ua = navigator.userAgent;
  
  if (/iPhone|iPad|iPod/.test(ua)) {
    return {
      system: 'com.apple.fps.1_0',
      licenseUrl: 'https://license.example.com/fairplay'
    };
  } else if (/Android/.test(ua)) {
    return {
      system: 'com.widevine.alpha',
      licenseUrl: 'https://license.example.com/widevine'
    };
  } else if (/Windows/.test(ua)) {
    // PlayReady 또는 Widevine
    return {
      system: 'com.widevine.alpha',
      licenseUrl: 'https://license.example.com/widevine'
    };
  }
  
  return {
    system: 'com.widevine.alpha',
    licenseUrl: 'https://license.example.com/widevine'
  };
}

2. 에러 처리

video.addEventListener('error', (event) => {
  const error = video.error;
  
  if (error.code === MediaError.MEDIA_ERR_ENCRYPTED) {
    console.error('❌ DRM error:', error.message);
    
    // 사용자에게 친화적인 메시지
    showError('이 콘텐츠를 재생할 수 없습니다. 브라우저를 업데이트하거나 다른 기기를 사용해주세요.');
  }
});

// MediaKeySession 에러
session.addEventListener('keystatuseschange', () => {
  session.keyStatuses.forEach((status, keyId) => {
    console.log(`Key ${keyId}: ${status}`);
    
    if (status === 'expired') {
      console.warn('⚠️ License expired, requesting new license');
      requestNewLicense();
    } else if (status === 'internal-error') {
      console.error('❌ DRM internal error');
    }
  });
});

3. 보안 체크리스트

# DRM 구현 체크리스트

# ✅ 콘텐츠 암호화
# - AES-128 이상 사용
# - 키 로테이션 구현

# ✅ 라이선스 서버
# - HTTPS 필수
# - 토큰 기반 인증
# - 권한 검증 (구독, 지역, 기기)

# ✅ 클라이언트
# - EME API 사용
# - 에러 처리
# - 폴백 전략

# ✅ 모니터링
# - 재생 로그
# - 이상 패턴 감지
# - 라이선스 요청 빈도

# ✅ 추가 보호
# - 워터마크
# - 화면 캡처 방지
# - 안티 디버깅

참고 자료

한 줄 요약: 멀티 플랫폼 지원을 위해 Widevine, FairPlay, PlayReady를 모두 지원하는 CENC 방식을 사용하고, 라이선스 서버에서 철저한 권한 검증을 수행하여 콘텐츠를 보호하세요.

---
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3