지난 DrmEngine 편의 정리에서 우리는 두 문장을 짧게 적고 넘어갔습니다.
- 매니페스트 + 앱 설정: 콘텐츠가 주는
keySystem·initData에, 앱이 정한licenseServerUri·robustness가 더해져야currentDrmInfo_가 완성됩니다.- 현대 EME 경로:
mediaCapabilities.decodingInfo가 채운keySystemAccess를 재사용해, 코덱 지원성과 키 시스템 협상을 한 번에 처리합니다.
이번 편은 그 두 문장을 펼치는 글입니다. DrmEngine 편이 "전체 수명주기를 위에서 훑는" 지도였다면, 이번엔 그 안에서 가장 까다로운 네 가지 — ① 라이선스 협상은 실제로 어떻게 이루어지는가, ② robustness란 무엇인가, ③ 협상으로 받은 키는 무엇으로 이루어졌고 어떻게 쓰이는가, ④ decodingInfo로 코덱과 키 시스템을 어떻게 한 번에 묻는가 — 를 코드와 함께 깊게 파고듭니다.
미리 말해두면, 이 네 주제는 따로 노는 토막이 아니라 하나의 사슬입니다. decodingInfo로 협상해 keySystemAccess를 얻고 → 세션을 열어 챌린지-응답으로 라이선스를 받고 → 라이선스에 담긴 키가 → 재생 가능한 트랙을 가릅니다. 그리고 robustness는 이 사슬 전체에 그림자를 드리웁니다.
DrmEngine 편에서 getDecodingInfosForVariants가 queryMediaKeys_보다 먼저 와야 한다고 했습니다. 그 안을 열어 보면, Shaka는 variant마다 MediaDecodingConfiguration 객체를 만들어 navigator.mediaCapabilities.decodingInfo()에 넘깁니다.
이 한 덩이의 객체에 두 질문이 함께 담깁니다.
video/audio 필드 → "이 코덱·해상도·프레임레이트·HDR을 디코딩할 수 있나?"keySystemConfiguration 필드 → "이 키 시스템을, 이 robustness로, 이 암호화 방식으로 쓸 수 있나?"| 영역 | 주요 필드 | 의미 |
|---|---|---|
type | 'media-source' / 'file' | MSE 재생인지 src= 재생인지 |
video | contentType, width, height, bitrate, framerate, transferFunction(HDR) | 비디오 디코딩 가능성 질의 |
audio | contentType, channels, samplerate, spatialRendering | 오디오 디코딩 가능성 질의 |
keySystemConfiguration | keySystem, initDataType, robustness, encryptionScheme, sessionTypes | 키 시스템·보안 등급 협상 |
그리고 돌아오는 결과가 MediaCapabilitiesDecodingInfo입니다.
supported이면서 keySystemAccess가 있으면, 그 variant는 재생도 되고 복호화도 된다는 뜻입니다. 그리고 DrmEngine 편에서 본 queryMediaKeys_는 이 keySystemAccess를 그대로 재사용해 createMediaKeys()를 호출합니다. 다시 협상하지 않습니다.
과거에는 navigator.requestMediaKeySystemAccess로 키 시스템만 따로 물었습니다. 하지만 그러면 "코덱은 되는데 그 코덱을 이 DRM 조합으로는 못 쓰는" 경우를 놓칠 수 있습니다. 현대 경로인 decodingInfo는 코덱 협상과 키 시스템 협상을 하나로 합쳐, 디코딩 가능성과 복호화 가능성을 한 번에 판정합니다. 모바일 일부 환경에서는 이 호출이 영영 끝나지 않는 버그가 있어, Shaka는 5초 타임아웃(promiseWithTimeout)으로 감쌉니다.
위 keySystemConfiguration에 슬쩍 들어 있던 robustness가 두 번째 주제입니다.
robustness는 콘텐츠 디코더와 CDM이 키와 복호화된 평문 미디어를 얼마나 안전하게 다루는지를 나타내는 등급입니다. 값 체계는 키 시스템마다 다릅니다. Widevine을 예로 들면 낮은 등급부터 다음과 같습니다.
SW_SECURE_CRYPTO — 소프트웨어, 복호화 연산만 보호SW_SECURE_DECODE — 소프트웨어, 디코딩 단계까지 보호HW_SECURE_CRYPTO — 하드웨어 기반 복호화 보호HW_SECURE_DECODE — 하드웨어 기반 디코딩 보호HW_SECURE_ALL — 복호화·디코딩·렌더링 전 구간을 TEE(보안 영역)에서 처리PlayReady는 SL150/SL2000/SL3000, FairPlay는 또 다른 체계를 씁니다. Shaka는 매니페스트에 robustness가 비어 있고 Widevine이면 다음 기본값을 채워 넣습니다.
이 값이 keySystemConfiguration.video/audio.robustness로 실려 decodingInfo 협상에 들어갑니다. 즉 robustness를 협상한다는 것은 "이 키 시스템을 이 보안 등급으로 쓸 수 있느냐"를 브라우저·플랫폼에 묻는 일입니다. HW_SECURE_* 같은 높은 등급은 하드웨어 보안 모듈이 있는 기기에서만 supported: true가 됩니다.
여기에 실무적으로 중요한 함의가 있습니다. 스튜디오의 라이선스 정책상 HD·UHD·4K 같은 고화질 콘텐츠는 하드웨어 robustness(보통 HW_SECURE_DECODE 이상)를 요구하는 경우가 많습니다. robustness가 낮은 기기에서는 고화질 트랙의 라이선스가 거부되거나, 뒤에서 볼 output-restricted 상태로 다운스케일됩니다. 그래서 robustness는 사실상 "이 기기에서 재생 가능한 최고 화질"을 가르는 손잡이입니다.
협상으로 keySystemAccess를 얻고 MediaKeys·세션까지 만들었다면, 이제 실제 키를 받아 올 차례입니다. EME의 라이선스 협상은 챌린지-응답(challenge-response) 모델을 따릅니다.
흐름을 순서대로 보면 이렇습니다.
generateRequest(initDataType, initData) — CDM에게 "이 init data(PSSH 등 보호된 키 ID 정보)로 라이선스 요청을 만들어라"라고 지시합니다. 이 initData는 매니페스트(DASH의 PSSH / HLS의 EXT-X-KEY)나 세그먼트(parseInbandPssh)에서 옵니다.message 이벤트 — CDM이 챌린지(라이선스 요청 blob)를 내놓습니다. onSessionMessage_를 거쳐 sendLicenseRequest_로 넘어갑니다.session.update(response.data) — 받은 라이선스를 세션에 적용합니다. 이때 CDM이 라이선스를 풀어 콘텐츠 키를 자기 보안 영역에 저장합니다.요청이 RequestType.LICENSE로 NetworkingEngine을 거친다는 점은 DrmEngine 편에서도 강조했습니다. 덕분에 앱이 등록한 요청·응답 필터가 그대로 적용되어, 인증 토큰 헤더를 끼우거나 서버가 감싸 보낸 응답을 풀어내는 일을 이 지점에서 처리할 수 있습니다.
한 가지 분명히 해둘 점. 콘텐츠 키 자체는 자바스크립트가 절대 볼 수 없습니다. 챌린지와 라이선스는 CDM과 라이선스 서버 사이에서만 의미를 갖는 암호화된 봉투이고, Shaka(자바스크립트)는 그 봉투를 네트워크로 중계할 뿐입니다. 실제 키는 robustness가 보장하는 CDM의 보안 영역 안에서만 존재합니다. robustness가 높을수록 그 영역이 더 견고하다는 뜻이죠.
그렇다면 세 번째 질문, 라이선스로 받은 "키"는 무엇으로 이루어졌을까요? session.update가 적용되면 CDM이 keystatuseschange 이벤트를 쏘고, 그 안의 session.keyStatuses가 답을 줍니다.
즉 라이선스로 받은 키는 자바스크립트 입장에서 (keyId, status)의 묶음입니다.
keyId — 어떤 콘텐츠 키인지 식별하는 16바이트 ID입니다. 매니페스트의 default_KID(DASH)나 EXT-X-KEY의 키 ID(HLS)와 매칭됩니다.status — 그 키의 현재 사용 가능 상태입니다.상태 값은 여러 가지가 있고, Shaka는 그에 따라 다르게 반응합니다.
| status | 의미 | Shaka의 처리 |
|---|---|---|
usable | 정상 사용 가능 | 해당 키로 보호된 트랙 재생 허용 |
status-pending | 아직 확정 안 됨 | 대기 |
output-restricted | 보호 경로(HDCP 등) 부족 | restrictedStatuses에 걸려 트랙 제한 |
expired | 만료됨 | 세션 닫기, 모두 만료면 EXPIRED 에러 |
internal-error | CDM 내부 오류 | restrictedStatuses에 걸려 트랙 제한 |
실제 콘텐츠 키의 비트는 여기 어디에도 없습니다. 우리가 손에 쥘 수 있는 것은 "어떤 ID의 키가 지금 쓸 수 있는 상태인가"라는 메타데이터뿐입니다.
마지막 질문, 이 키가 어떻게 활용되느냐입니다. 핵심은 keyId가 매니페스트의 트랙과 라이선스의 키 상태를 잇는 핀이라는 데 있습니다.
매니페스트를 파싱할 때, 각 스트림에는 그 트랙을 보호하는 키 ID들이 stream.keyIds(Set)로 채워졌습니다. 재생 가능 여부를 판정할 때 Shaka는 이 stream.keyIds를 방금 만든 키 상태 맵과 대조합니다.
restrictedStatuses의 정체는 단출합니다.
스트림의 keyId에 해당하는 키가 output-restricted거나 internal-error면, 그 스트림이 포함된 variant는 재생 불가로 표시됩니다(allowedByKeySystem = false). 그 결과 재생 가능한 variant가 하나도 남지 않으면 RESTRICTIONS_CANNOT_BE_MET 에러가 발생합니다.
여기서 앞의 robustness 이야기가 되돌아옵니다. output-restricted는 robustness나 HDCP가 부족해 고화질 출력이 막히는 전형적인 케이스입니다. 보안 등급이 낮은 기기에서 4K 트랙의 키가 output-restricted로 내려오면, 그 트랙은 자동으로 후보에서 빠지고 Shaka는 재생 가능한 더 낮은 화질로 떨어집니다. robustness가 협상 단계(supported 여부)뿐 아니라 재생 단계(키 상태)에서도 화질을 가른다는 뜻이죠.
그리고 이 판정은 한 번으로 끝나지 않습니다. 라이브에서 키가 로테이션되거나 라이선스가 만료되면 keystatuseschange가 다시 발생하고, 그때마다 재생 가능 트랙이 실시간으로 다시 계산됩니다. DrmEngine 편에서 본 pollExpiration_/triggerRenewal_이 이 과정을 떠받칩니다.
DrmEngine 편의 정리에 적었던 두 문장을, 네 개의 주제로 펼쳐 따라왔습니다. 이 네 가지는 결국 하나의 사슬이었습니다.
핵심을 다시 모으면 다음과 같습니다.
decodingInfo는 코덱 디코딩 가능성과 키 시스템·robustness 협상을 한 객체에 담아, supported와 keySystemAccess를 함께 돌려줍니다.supported 여부와 재생 단계의 output-restricted 여부 양쪽에서 재생 가능 화질을 가릅니다.stream.keyIds가 라이선스가 준 keyStatuses와 만나, 어떤 트랙을 재생할 수 있는지를 실시간으로 결정합니다.한 줄로 요약하면 다음과 같습니다.
"decodingInfo로 코덱과 키 시스템을 함께 협상하고, 챌린지-응답으로 라이선스를 받아오면, 그 안의 (keyId, 상태)가 매니페스트의 트랙과 맞물려 무엇을 어떤 화질로 재생할 수 있는지를 가른다. robustness는 그 모든 단계에 드리운 보안의 그림자다."
이로써 DRM 이야기는 마무리됩니다. PreloadManager가 시동을 걸고(initForPlayback), DrmEngine이 수명주기를 끌고 가며, 그 안에서 협상·라이선스·키가 어떻게 맞물리는지까지 모두 따라왔습니다. 다음 편부터는 드디어 이 모든 준비를 실제 재생으로 바꾸는 구동 단계 — 세그먼트를 내려받아 MediaSource로 흘려보내는 StreamingEngine과 대역폭을 추정해 화질을 갈아 끼우는 AbrManager — 로 넘어가겠습니다.