DASH 딥: SegmentTemplate, ContentProtection, 시점 전환 MPD 구조
MPD 입문 가이드에서 MPD가 무엇인지, 왜 MP4가 아니라 12KB XML이 튀어나오는지 설명했다. 이 글은 속편——MPD를 열어서 400줄의 <SegmentTemplate>, <SegmentTimeline>, <ContentProtection>, <AdaptationSet>을 보고, 진짜로 쓸만한 게 뭔지 알고 싶은 사람을 위한 것.
DASH 파서를 쓰고 있거나, 다운로더가 어떤 스트림은 되고 어떤 스트림은 안 되는 이유를 알고 싶다면, 이 글이 지도가 된다.
MPD의 계층
모든 MPD는 이런 중첩 구조:
MPD
├── Period # 콘텐츠의 1세그먼트 (본편, 광고 등)
│ ├── AdaptationSet # 교환 가능한 버전 그룹 (비디오, 오디오 등)
│ │ ├── Representation # 구체적인 1버전 (1080p, 720p 등)
│ │ │ ├── SegmentList # 세그먼트 URL을 명시적으로 나열
│ │ │ ├── SegmentTemplate # 세그먼트 URL을 템플릿화
│ │ │ └── SegmentBase # 단일 세그먼트 + 바이트 범위
│ │ └── ContentProtection # DRM 시그널링 (Widevine, PlayReady)
│ └── EventStream # 대역 외 이벤트 (ID3 같은 것)
전형적인 영화의 MPD는 Period 1개, AdaptationSet 2개(비디오+오디오), 각 셋에 Representation 5-10개. 광고 있는 라이브는 Period가 4개 이상. 멀티 카메라 스포츠 라이브는 각 카메라가 AdaptationSet 하나씩.
핵심: DASH는 모든 세그먼트 URL을 나열하지 않도록 설계되어 있다. HLS는 1개씩 나열. DASH는 템플릿 사용.
SegmentTemplate: 90%의 케이스
<SegmentTemplate
initialization="video/init.mp4"
media="video/$Number$.m4s"
startNumber="1"
timescale="1000"
duration="6000" />
해석:
- 초기화 세그먼트는
video/init.mp4(fMP4 초기화 박스) - 각 미디어 세그먼트는
video/$Number$.m4s.$Number$는 1부터 - 각 세그먼트는 6000밀리초 (
timescale=1000이므로 6초)
이 Representation을 다운로드하려면 $Number$를 치환해 URL을 생성:
const startNumber = 1
const duration = 6000 // ms
const timescale = 1000 // 1단위 = 1 ms
const totalDuration = 7200000 // ms——Representation의 @duration 속성에서
const segmentCount = Math.ceil(totalDuration * timescale / (duration * timescale))
// = 1200 세그먼트
const urls = []
for (let n = startNumber; n < startNumber + segmentCount; n++) {
urls.push(`video/${n}.m4s`)
}
HLS보다 훨씬 컴팩트——1200줄의 #EXTINF + URL이 1개의 <SegmentTemplate>으로. 대가는 파서가 직접 계산을 해야 하는 것이고, 엣지 케이스(마지막 세그먼트가 짧음, 세그먼트 길이 변화)에 주의.
$Number$ vs $Time$
$Number$는 세그먼트 인덱스(1, 2, 3……). $Time$은 세그먼트 시작 시각(timescale 단위). 둘 다 쓰는 것, 하나만 쓰는 것이 있다:
<SegmentTemplate
media="video/$Time$.m4s"
timescale="1000"
duration="6000" />
$Time$을 파싱하려면 세그먼트 인덱스에 길이를 곱한다:
const segmentTime = (n - startNumber) * duration
const url = `video/${segmentTime}.m4s`
timescale을 곱하는 걸 잊으면 URL이 1000배 어긋나서 전부 404. 이게 DASH 파싱에서 가장 흔한 버그.
SegmentTimeline: 세그먼트 길이가 고정이 아닐 때
라이브와 동적 콘텐츠는 <SegmentTimeline>을 쓴다:
<SegmentTemplate timescale="1000" initialization="init.mp4" media="$Number$.m4s">
<SegmentTimeline>
<S t="0" d="6000" />
<S t="6000" d="6000" />
<S t="12000" d="4000" />
<S t="16000" d="6000" r="3" /> <!-- r=3은 3회 반복: 총 4세그먼트 -->
<S t="40000" d="6000" />
</SegmentTimeline>
</SegmentTemplate>
t——시작 시각 (timescale 단위)d——길이 (timescale 단위)r——반복 횟수 (r+1개 세그먼트를 의미)
라이브의 timeline은 시간이 지나면서 자란다. 전형적 흐름:
- 클라이언트가 MPD 획득. 세그먼트 1-50 보임.
minimumUpdatePeriod는 2초 - 클라이언트는 1-50 재생
- 클라이언트는 MPD 재획득. 이제 1-52 (timeline이 늘어남)
MPD@endOfAvailability또는 스트림 종료까지 반복
다운로더의 선택지:
- 스트림이 완전히 끝나기를 기다린다 (VOD로 전환)——항상 가능한 건 아님
- 현재 timeline을 스냅샷하고, 이후 추가되는 콘텐츠는 포기
FlowPick은 스냅샷 방식: 현재 timeline을 잡고, 세그먼트를 다운로드, 병합. 스트림이 아직 살아있으면 사용자가 한 번 더 트리거해서 새 세그먼트를 잡을 수 있다.
ContentProtection: DRM 시그널링
<ContentProtection
schemeIdUri="urn:mpeg:dash:mp4protection:2011"
value="cenc"
cenc:default_KID="12345678-1234-1234-1234-123456789012" />
<ContentProtection
schemeIdUri="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"
value="Widevine">
<cenc:pssh>AAAA...</cenc:pssh>
</ContentProtection>
<ContentProtection
schemeIdUri="urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95"
value="PlayReady">
<cenc:pssh>BBBB...</cenc:pssh>
</ContentProtection>
UUID가 DRM 시스템을 식별:
edef8ba9-79d6-4ace-a3c8-27dcd51d21ed——Widevine (Google)9a04f079-9840-4286-ab92-e65be0885f95——PlayReady (Microsoft)f239e769-efa3-4850-9c16-a9036d3fe1c1——Adobe Primetime94ce86fb-07ff-4f43-adb8-93d2fa968ca2——FairPlay (Apple, DASH 맥락에서)
<cenc:pssh>는 Protection System Specific Header——base64 blob으로, key ID와 시스템 고유 데이터를 담는다. 브라우저는 이걸 EME(Encrypted Media Extensions)에 넘기고, EME가 시스템의 CDM(Content Decryption Module)과 대화한다.
여기서 선을 긋는다: FlowPick은 DRM 보호된 DASH 콘텐츠를 해독하지 않는다, 안 한다, 못 한다. 정당한 브라우저 확장도 오픈소스 툴도 못 한다. CDM은 블랙박스——암호화 버퍼를 주면 표시용 복호 프레임을 반환, 영원히 익스포트하지 않는다. 이를 우회하는 것은 반회피법(미국 DMCA §1201, 다른 지역도 동등)에 위반, CDM 계약에도 위반.
DASH 사양에는 "보호되지 않은" 경로가 있다: 어떤 AdaptationSet이 ContentProtection을 갖지 않으면, 세그먼트는 암호화되지 않고, 다운로더가 자유롭게 페치하고 병합할 수 있다. 그래서 FlowPick은 Bilibili, Udemy 무료 코스, 대부분의 기업 LMS를 처리할 수 있다. Netflix/Disney+/HBO Max는 못 한다.
"보호처럼 보이지만" DRM이 아닌 일반적 패턴
"다운로드 안 됨"이 전부 DRM인 건 아니다. 다운로드를 어렵게 하지만 실제로는 암호화하지 않는 패턴:
- 짧은 TTL의 토큰 URL——
?token=abc&expires=1234567890. 토큰이 몇 초 만에 만료되어서, 청단을 파싱하면 바로 세그먼트를 페치해야 한다. FlowPick은 그렇게 한다. "몇 시간 전에 URL을 파싱하고 지금 다운로드" 식의 배치 다운로더는 실패. - CDN의 CORS 제한——하지만 확장은 페이지 origin에서 요청을 보내므로 CORS에 묶이지 않아, 스니퍼를 사실상 막지 못한다.
<BaseURL>재작성——MPD의BaseURL요소는 문서 중간에 바뀔 수 있다. 각BaseURL은 부모에 대해 상대적으로 해석되어, URL 체인을 추적해야 한다. 이건 DASH판 HLS 상대 URI인데, 에러를 일으키기 더 쉽다.
멀티 Period MPD: 광고와 라이브 이벤트
<Period id="ad1" start="PT0S" duration="PT30S">...</Period>
<Period id="main" start="PT30S">...</Period>
<Period id="ad2" start="PT1800S" duration="PT30S">...</Period>
<Period id="main2" start="PT1830S">...</Period>
각 Period는 자체 AdaptationSet을 가진다. 전체 콘텐츠를 다운로드하려면:
@start로 Period 정렬- 각 Period에서 비디오+오디오 세그먼트 전부 획득
- Period 사이 결합——단 codec이 Period 사이에 변하는 경우 주의
광고 브레이크는 종종 다른 codec(광고는 H.264 baseline, 본편은 H.264 high). 무식하게 결합하면 본편은 정상 재생되다가 광고 부분에서 글리치.
수정:
- 광고 Period를 건너뛴다 (FlowPick의 접근——사용자는 광고를 원하지 않으므로)
- 광고 Period를 본편과 동일 codec으로 재인코딩 (느림, 트랜스코딩 필요)
- 더 똑똑한 멀티플렉서로 codec 전환 처리 (FFmpeg은 가능하지만 복잡)
시점 전환과 trick-mode AdaptationSet
<AdaptationSet id="1" contentType="video" group="1">
<!-- 메인 카메라 -->
</AdaptationSet>
<AdaptationSet id="2" contentType="video" group="1">
<!-- 백업 카메라, 같은 group -->
</AdaptationSet>
<AdaptationSet id="3" contentType="video" group="2">
<!-- Trick mode (시크바 썸네일) -->
</AdaptationSet>
group="1"은 같은 콘텐츠의 다른 시점을 의미. 플레이어는 사용자에게 카메라 전환을 제공. 다운로더는 하나를 선택——보통 첫 번째지만, 이상은 사용자에게 선택시킨다.
Trick-mode AdaptationSet(여기서 group=2)은 시크바에서 드래그 시 미리보기용 저비트레이트 저프레임레이트 버전. 이걸 다운로드해봤자 소용없다——파서가 이걸 "비디오"로 잘못 인식하지 않게.
프리페치와 병렬화
DASH 다운로드에서 가장 실용적인 최적화: 세그먼트를 병렬 페치.
async function downloadRepresentation(rep, concurrency = 6) {
const segmentUrls = computeSegmentUrls(rep)
const initBytes = await fetch(rep.initUrl).then(r => r.arrayBuffer())
const results = new Array(segmentUrls.length)
let nextIndex = 0
async function worker() {
while (nextIndex < segmentUrls.length) {
const i = nextIndex++
const resp = await fetch(segmentUrls[i])
results[i] = new Uint8Array(await resp.arrayBuffer())
}
}
await Promise.all(Array.from({ length: concurrency }, () => worker()))
// init + 세그먼트를 순서대로 결합
const total = initBytes.byteLength + results.reduce((s, b) => s + b.byteLength, 0)
const out = new Uint8Array(total)
let offset = 0
out.set(new Uint8Array(initBytes), offset); offset += initBytes.byteLength
for (const buf of results) {
out.set(buf, offset); offset += buf.byteLength
}
return out
}
6개 병렬 연결이 스위트스팟——그 이상은 CDN 레이트 리미트를 트리거, 그 이하는 대역 낭비. Promise.all의 worker 풀 패턴으로 세그먼트가 순서 없이 완료되어도 올바른 배열 위치에 떨어진다.
이게 FlowPick 내부에서 쓰는 패턴. 브라우저 내 병합 글에서 최종 버퍼를 어떻게 멀티플렉스하는지 설명.
밟은 함정
함정 1: BaseURL 무시. MPD가 https://cdn.example.com/v/manifest.mpd에 있고, 안에 <BaseURL>https://other-cdn.example.com/</BaseURL>이면, 이 섹션의 상대 URL은 other-cdn.example.com으로 해석. MPD의 origin이 아니다. 이걸 잊으면 "전부 404" 버그의 1등 원인.
함정 2: timescale 파싱 실수. duration="6000"이고 timescale="1000"이면 6초. timescale="90000"(비디오에서 흔함)이면 0.067초. timescale을 반드시 확인——같은 MPD 내에서 AdaptationSet마다 다를 수 있다.
함정 3: init 세그먼트 잊기. HLS(TS의 경우)는 각 세그먼트가 자기 기술적이지만, DASH의 fMP4 세그먼트는 init 세그먼트 없이는 쓸모없다. "다운로드 성공한 것처럼 보이는데" 재생 안 되는 파일은 initialization="init.mp4"를 잊은 경우가 많다.
함정 4: 라이브 MPD는 갱신된다. 한 번 파싱하고 스냅샷으로 다운로드하면, 이후 추가된 세그먼트를 놓친다. VOD는 문제없음. 라이브는 주기적으로 재획득하거나 스트림 종료를 기다린다.
참고 자료
- ISO/IEC 23009-1 — DASH 사양——공식 사양, 유료지만 권위
- DASH-IF 구현 가이드——DASH Industry Forum의 가이드, 무료, ISO 문서보다 실용적
- GPAC MP4Box 문서——레퍼런스급 MP4/DASH 도구. 파서 검증용
- Bento4 MP4 문서——오픈소스 MP4 툴킷, 컨테이너 포맷 이해에 도움
정리
DASH와 HLS는 같은 문제——HTTP 기반의 적응형 스트리밍——를 반대 접근으로 푼다. HLS는 각 세그먼트를 나열. DASH는 템플릿을 쓴다. HLS는 기본이 MPEG-TS. DASH는 fMP4. HLS의 DRM은 Apple 독점(FairPlay). DASH의 DRM은 멀티 벤더(Widevine, PlayReady, FairPlay)로 CENC를 통과.
다운로더 관점에서 DASH는 구조적으로 더 다루기 쉽다: fMP4 세그먼트는 직접 결합 가능, init 세그먼트가 codec 정보를 명시, $Number$ 치환은 청단을 추가 페치 없이 URL을 계산. 복잡도는 XML 파싱에 있다——하지만 SegmentTemplate, SegmentTimeline, BaseURL, ContentProtection을 다룰 수 있으면, 현실의 MPD의 95%를 커버.
다음은 fMP4의 결합이 왜 되는지, moof 박스에 뭐가 들었는지, 왜 "리먹스하고 트랜스코딩하지 않는"게 올바른 철학인지.
추천 글
- DASH 스트리밍이란?MPD 파일 해설——이 글의 입문판
- HLS 딥: 암호화, 멀티트랙, 기억해야 할 EXT-X 태그——HLS 쪽의 대응물
- 브라우저에서 비디오 리먹스: fMP4, ISOBMFF, 트랜스코딩하지 않는 이유——DASH 세그먼트를 다운로드한 후 무슨 일이 일어나는가
- 스트리밍 다운로드는 합법인가?기술과 법적 체크리스트——ContentProtection 태그가 왜 하드한 선인가
- FlowPick은 어떻게 브라우저에서 수백 개의 비디오 세그먼트를 하나의 MP4로 합치는가——여기서의 패턴이 FlowPick에서 어떻게 구현되는가
- WebCodecs + Web Workers + OPFS: 브라우저 비디오 처리 실전——FFmpeg WASM을 보완하는 더 저수준 브라우저 API