[2026] 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) 표준
- 실전 구현 예제
실무에서 마주한 현실
개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.
목차
- DRM 기본 개념
- 주요 DRM 시스템
- Widevine (Google)
- FairPlay (Apple)
- PlayReady (Microsoft)
- EME와 CENC
- HLS AES-128 암호화
- 실전 구현
- DRM 우회 방지
- 비교 및 선택 가이드
1. DRM 기본 개념
DRM이란?
DRM(Digital Rights Management)은 디지털 콘텐츠의 암호화, 라이선스 관리, 접근 제어를 통해 저작권을 보호하는 기술입니다.
DRM 구성 요소
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TB
subgraph Content[콘텐츠 제공자]
Video[원본 비디오]
Encoder[인코더/패키저]
KeyServer[키 서버]
end
subgraph CDN[CDN]
Encrypted["암호화된\n콘텐츠"]
end
subgraph Client[클라이언트]
Player[비디오 플레이어]
CDM["CDM\nContent Decryption Module"]
TEE["TEE/Hardware\n보안 영역"]
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 동작 흐름
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
sequenceDiagram
participant User as 사용자
participant Player as 플레이어
participant CDN
participant License as 라이선스 서버
participant CDM as CDM (보안 모듈)
User->>Player: 비디오 재생 요청
Player->>CDN: 암호화된 콘텐츠 요청
CDN->>Player: 암호화된 세그먼트 전달
Player->>License: 라이선스 요청\n(사용자 인증 토큰)
License->>License: 권한 확인\n(구독, 지역, 기기)
License->>Player: 라이선스 + 복호화 키
Player->>CDM: 복호화 요청
CDM->>CDM: 하드웨어 보안 영역에서\n복호화
CDM->>Player: 복호화된 프레임
Player->>User: 비디오 재생
2. 주요 DRM 시스템
DRM 생태계
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 지원
| 플랫폼 | Widevine | FairPlay | PlayReady | ClearKey |
|---|---|---|---|---|
| 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 보안 레벨
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TB
subgraph L1["Widevine L1\nHardware-backed"]
L1_Desc["✅ TEE/Secure Processor\n✅ 4K/HDR 지원\n✅ 최고 보안"]
end
subgraph L2["Widevine L2\nSoftware-backed"]
L2_Desc["⚠️ 소프트웨어 보안\n⚠️ 1080p 제한\n⚠️ 중간 보안"]
end
subgraph L3["Widevine L3\nSoftware only"]
L3_Desc["❌ 기본 보안\n❌ 480p/SD 제한\n❌ 낮은 보안"]
end
L1 --> Best[최고 화질]
L2 --> Medium[중간 화질]
L3 --> Low[낮은 화질]
Widevine 아키�ecture
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TB
subgraph App[애플리케이션]
Player[Video Player]
EME[EME API]
end
subgraph Browser[브라우저]
CDM[Widevine CDM]
end
subgraph Hardware["하드웨어 (L1)"]
TEE["Trusted Execution\nEnvironment"]
SecureVideo[Secure Video Path]
end
subgraph Backend[백엔드]
License["Widevine\nLicense Server"]
Content["암호화된\n콘텐츠"]
end
Player --> EME
EME --> CDM
CDM --> TEE
Player --> Content
CDM --> License
TEE --> SecureVideo
SecureVideo --> Display[디스플레이]
Widevine 구현 예시
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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 레벨 확인
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 브라우저에서 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 동작 흐름
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 요청\n(Server Playback Context)
KSM->>App: SPC 생성
App->>License: SPC + 인증 토큰
License->>License: 권한 검증
License->>App: CKC\n(Content Key Context)
App->>KSM: CKC 전달
KSM->>Player: 복호화 키
Player->>Player: 콘텐츠 재생
FairPlay 구현 (iOS/Swift)
다음은 swift를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 매니페스트
다음은 m3u8를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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 아키텍처
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TB
subgraph Client[클라이언트]
App[애플리케이션]
PRT[PlayReady Runtime]
TEE["Trusted Execution\nEnvironment"]
end
subgraph Server[서버]
Content[암호화된 콘텐츠]
License["PlayReady\nLicense Server"]
end
App --> PRT
PRT --> TEE
App --> Content
PRT --> License
License -->|라이선스| PRT
Content -->|암호화된 데이터| PRT
PRT -->|복호화| TEE
PlayReady 구현 (C#)
다음은 csharp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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입니다. 다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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 시스템에서 사용할 수 있게 하는 표준입니다. 아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TB
Original[원본 비디오] --> Encoder[인코더]
Encoder --> CENC["CENC 암호화\n단일 파일"]
CENC --> Chrome["Chrome\nWidevine"]
CENC --> Safari["Safari\nFairPlay"]
CENC --> Edge["Edge\nPlayReady"]
Chrome --> Play1[재생]
Safari --> Play2[재생]
Edge --> Play3[재생]
CENC 암호화 (Shaka Packager)
다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# 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 암호화 매니페스트
다음은 m3u8를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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)
다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# 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)
다음은 python를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 플레이어
다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 우회 방지
보안 레이어
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TB
subgraph Layer1["레이어 1: 암호화"]
Encrypt["AES-128/256\n콘텐츠 암호화"]
end
subgraph Layer2["레이어 2: 키 관리"]
KeyRotation[키 로테이션]
KeyDerivation[키 파생]
end
subgraph Layer3["레이어 3: 하드웨어 보안"]
TEE["Trusted Execution\nEnvironment"]
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
워터마킹
다음은 python를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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)
화면 캡처 방지
다음은 javascript를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 화면 캡처 감지 (완벽하지 않음)
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
다음은 python를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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: 오프라인 재생 (다운로드)
다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 오프라인 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
다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#!/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를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
<?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 라이선스 서버
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 L1 | Android, Chrome | 하드웨어 | 4K | ✅ | |
| Widevine L3 | Chrome, Firefox | 소프트웨어 | 480p | ❌ | |
| FairPlay | Apple | iOS, Safari | 하드웨어 | 4K | ✅ |
| PlayReady | Microsoft | Windows, Xbox | 하드웨어 | 4K | ✅ |
| HLS AES-128 | Apple | 모든 플랫폼 | 기본 | 제한 없음 | ✅ |
| ClearKey | W3C | 모든 플랫폼 | 없음 | 제한 없음 | ✅ |
서비스별 DRM 사용
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 우회 기법과 대응
일반적인 우회 시도
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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)
아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
HDCP: 디지털 비디오 인터페이스 보호
HDMI/DisplayPort 연결에서 콘텐츠를 암호화하여
캡처 장비로 녹화를 방지
DRM L1 + HDCP → 완전한 보호 경로
안티 디버깅
다음은 javascript를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 디버거 감지
(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)
다음은 javascript를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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
다음은 javascript를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 테스트 콘텐츠
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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 지원 확인
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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();
성능 최적화
라이선스 캐싱
다음은 javascript를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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();
프리로딩
다음은 javascript를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 라이선스 미리 요청
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 선택 가이드
아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TD
Start[DRM 선택] --> Q1{플랫폼은?}
Q1 -->|iOS/Safari| FairPlay[FairPlay]
Q1 -->|Android/Chrome| Widevine[Widevine]
Q1 -->|Windows/Xbox| PlayReady[PlayReady]
Q1 -->|멀티 플랫폼| Multi["멀티 DRM\nCENC"]
Multi --> Q2{보안 수준은?}
Q2 -->|최고| L1["Widevine L1\n+ FairPlay\n+ PlayReady"]
Q2 -->|중간| L3["Widevine L3\n+ FairPlay"]
Q2 -->|기본| AES[HLS AES-128]
구현 복잡도
다음은 code를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 실행 예제
복잡도: FairPlay > PlayReady > Widevine > HLS AES-128
FairPlay:
- 인증서 발급 필요
- 네이티브 구현 필수
- Apple 승인 필요
Widevine:
- 웹 표준 EME 사용
- JavaScript로 구현 가능
- Google 계약 필요
PlayReady:
- Windows 플랫폼 통합
- C# 구현 권장
- Microsoft 계약 필요
HLS AES-128:
- 간단한 구현
- FFmpeg로 생성 가능
- 계약 불필요
비용 비교
다음은 code를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
라이선스 비용 (대략적):
Widevine:
- 무료 (테스트)
- 상용: 기기당 과금
FairPlay:
- Apple Developer Program: $99/년
- 추가 비용 없음
PlayReady:
- 기기당 라이선스 비용
- 볼륨 할인 가능
HLS AES-128:
- 완전 무료
- 오픈소스
베스트 프랙티스
1. 멀티 DRM 전략
다음은 javascript를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 플랫폼 감지 후 적절한 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. 에러 처리
다음은 javascript를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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. 보안 체크리스트
다음은 python를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# DRM 구현 체크리스트
# ✅ 콘텐츠 암호화
# - AES-128 이상 사용
# - 키 로테이션 구현
# ✅ 라이선스 서버
# - HTTPS 필수
# - 토큰 기반 인증
# - 권한 검증 (구독, 지역, 기기)
# ✅ 클라이언트
# - EME API 사용
# - 에러 처리
# - 폴백 전략
# ✅ 모니터링
# - 재생 로그
# - 이상 패턴 감지
# - 라이선스 요청 빈도
# ✅ 추가 보호
# - 워터마크
# - 화면 캡처 방지
# - 안티 디버깅
참고 자료
- Widevine DRM
- Apple FairPlay Streaming
- Microsoft PlayReady
- W3C Encrypted Media Extensions
- MPEG CENC Specification
- Shaka Player
- Shaka Packager 한 줄 요약: 멀티 플랫폼 지원을 위해 Widevine, FairPlay, PlayReady를 모두 지원하는 CENC 방식을 사용하고, 라이선스 서버에서 철저한 권한 검증을 수행하여 콘텐츠를 보호하세요.