발행된 HLS 매니페스트가 사후에 바뀌는 in-place 수정 문제
같은 MEDIA-SEQUENCE 번호에 처음엔 라이브 세그먼트가, 4초 뒤엔 광고 세그먼트가 들어 있었습니다. HLS에서 이건 해서는 안 되는 일입니다.
먼저 두 스냅샷을 나란히 놓고 봅니다
광고 삽입을 검증하면서 매니페스트를 주기적으로 내려받아 저장하고 있었습니다. OUT 마커 직후 구간을 두 번 폴링한 결과가 이렇게 나왔습니다.
Poll 1 (04:21:13.0Z)
#EXT-X-MEDIA-SEQUENCE:10482
#EXT-X-DATERANGE:ID="<ad-break-id>",START-DATE="2026-03-11T04:21:12.900Z",PLANNED-DURATION=508.000,SCTE35-OUT=0xFC302F00...
#EXTINF:4.000,
segment_<channel-id>_10482.ts <- 라이브 세그먼트
#EXTINF:4.000,
segment_<channel-id>_10483.tsPoll 2 (04:21:17.0Z, 4초 후)
#EXT-X-MEDIA-SEQUENCE:10482
#EXT-X-DATERANGE:ID="<ad-break-id>",START-DATE="2026-03-11T04:21:12.900Z",PLANNED-DURATION=508.000,SCTE35-OUT=0xFC302F00...
#EXT-X-DISCONTINUITY
#EXTINF:15.000,
ad_<creative-id>_001.ts <- 광고 세그먼트로 바뀌었다
#EXTINF:15.000,
ad_<creative-id>_002.tsEXT-X-MEDIA-SEQUENCE가 10482로 동일합니다. 즉 두 스냅샷은 같은 시퀀스 번호에서 시작하는 같은 플레이리스트입니다. 그런데 첫 번째 엔트리의 내용이 라이브 세그먼트에서 광고 세그먼트로 통째로 교체됐습니다. DISCONTINUITY 태그도 없던 게 생겼습니다.
diff로 보면 이렇습니다.
#EXT-X-MEDIA-SEQUENCE:10482
#EXT-X-DATERANGE:ID="<ad-break-id>",START-DATE="...",SCTE35-OUT=...
+ #EXT-X-DISCONTINUITY
- #EXTINF:4.000,
- segment_<channel-id>_10482.ts
+ #EXTINF:15.000,
+ ad_<creative-id>_001.ts추가만 일어난 게 아닙니다. 이미 발행했던 줄이 삭제되고 다른 줄로 대체됐습니다. 이게 문제의 핵심입니다.
이게 왜 규격 위반인가
HLS 사양(RFC 8216)이 live 플레이리스트에 요구하는 건 단순합니다. 서버는 이미 발행한 내용을 되돌릴 수 없습니다. 허용되는 변경은 사실상 세 가지뿐입니다.
| 허용되는 변경 | 설명 |
|---|---|
| 뒤에 줄을 덧붙이기 | 새 segment를 append |
| 앞에서 segment를 제거 | sliding window가 앞으로 밀리면서 오래된 것 삭제 |
| MEDIA-SEQUENCE 증가 | 앞에서 제거한 개수만큼 증가 |
여기에 "중간에 있는 segment의 내용을 바꾸기"는 없습니다. 한 번 특정 시퀀스 번호로 발행한 segment는 그 번호를 유지하는 동안 같은 것이어야 합니다.
주의: 플레이어는 이미 파싱한 플레이리스트를 캐시하고, 시퀀스 번호로 "여기부터는 새 것"을 판단합니다. 같은 번호의 내용이 바뀌면 플레이어는 그걸 감지할 방법이 없습니다. 이미 다운로드를 시작했거나 버퍼에 넣어둔 segment와 서버의 현재 상태가 갈라집니다.
그래서 플레이어에는 뭐가 보이나
증상은 플레이어가 어느 타이밍에 폴링했는지에 따라 갈립니다.
| 플레이어의 폴링 타이밍 | 결과 |
|---|---|
| Poll 1 시점에만 받고 다음 갱신까지 재생 | 광고 시작 부분에 라이브 화면이 4초 남는다 |
| Poll 1을 받고 segment 다운로드 중에 Poll 2 반영 | 광고 첫 소재를 건너뛰거나 중복 재생 |
| Poll 2 이후부터 진입 | 정상. 아무 문제 없다 |
즉 항상 깨지는 게 아니고, 특정 타이밍에 걸린 세션만 이상하게 봅니다. 이런 종류의 문제가 재현이 어렵고 제보가 애매한 이유입니다. "가끔 광고 앞에 방송이 좀 나와요" 같은 형태로 들어옵니다.
원인: 광고 소재의 첫 트랜스코딩 순간
원인을 좁히는 데 도움이 된 관찰이 하나 있습니다. 같은 광고 소재로 두 번째, 세 번째 브레이크를 돌리면 재현이 안 됐습니다. 오직 그 소재를 처음 사용하는 브레이크에서만 나왔습니다.
광고 소재는 원본 스트림의 코덱, 해상도, GOP 구조에 맞춰 트랜스코딩한 뒤 붙습니다. 이 트랜스코딩은 처음 필요해진 순간에 수행되고, 결과는 재사용됩니다.
여기에 두 번째 조건이 겹칩니다. 서로 다른 bitrate의 sub-stream(렌디션)들이 짧은 시간 안에 동기화된 상태로 반복 요청을 보냅니다. 광고 구간이 시작되면 모든 렌디션이 거의 동시에 같은 광고 소재를 필요로 하기 때문입니다.
문제의 구조가 보입니다. 마커 정보와 실제 광고 데이터가 서로 다른 속도로 준비되는데, 매니페스트 발행은 둘을 기다려주지 않습니다. 마커는 계산만 하면 되니 즉시 나가고, 광고 segment는 트랜스코딩이 끝나야 나옵니다. 그 사이에 폴링이 끼면 "마커는 있는데 광고는 없는" 중간 상태가 외부로 노출됩니다.
그리고 일단 노출된 뒤에 데이터가 준비되면, 파이프라인은 그 자리를 채우는 게 맞다고 판단해서 덮어씁니다. 의도는 이해가 되지만, 이미 발행한 걸 고치는 순간 규격을 벗어납니다.
참고: 이런 계열의 버그는 대개 "준비 안 된 상태를 발행했다"가 아니라 "발행한 뒤에 고쳤다"가 진짜 문제입니다. 중간 상태를 내보내는 것 자체는 마커만 있고 광고가 없는 형태로 넘어갈 수 있습니다. 되돌리는 게 문제입니다.
다른 지연 현상과는 어떻게 다른가
헷갈리기 쉬운 지점이라 구분해 둡니다.
| 현상 | 층 | 원인 |
|---|---|---|
| START-DATE가 3초에서 8초 늦다 | 마커 계산 층 | splice 지점을 segment 경계로 밀면서 |
| 발행된 매니페스트가 나중에 바뀐다 | 매니페스트 발행 층 | 광고 데이터 준비 완료 시점과 발행 시점의 경합 |
둘 다 "광고 시작 부분에 라이브 화면이 조금 보인다"는 비슷한 증상을 만들 수 있습니다. 그래서 증상만으로는 구분이 안 됩니다. 구분하는 방법은 폴 스냅샷을 비교해 보는 것입니다.
- 스냅샷들 사이에 같은 시퀀스 번호의 내용이 다르면: 이 글에서 다룬 소급 수정 문제
- 스냅샷은 전부 일관된데
START-DATE가 splice 시점과 다르면: 마커 계산 지연 문제
이 구분법이 있으면 제보 하나를 받고 어느 쪽인지 바로 판정할 수 있습니다.
폴 스냅샷 비교를 어떻게 자동화했나
수동으로 curl을 두 번 치는 방식으로는 이 문제를 잡을 수 없습니다. 4초 창을 노려서 눌러야 하기 때문입니다. 그래서 검증을 스냅샷 기반으로 바꿨습니다.
1) 짧은 주기로 매니페스트를 계속 받아서 타임스탬프 붙여 저장
GET https://edge.example.com/live/<channel-id>/index.m3u8
-> snapshots/2026-03-11T04-21-13.000Z.m3u8
-> snapshots/2026-03-11T04-21-17.000Z.m3u8
2) 각 스냅샷을 (MEDIA-SEQUENCE 번호 -> segment URI) 맵으로 파싱
3) 연속한 스냅샷 쌍에서 겹치는 시퀀스 번호를 찾아 URI 비교
4) 같은 번호에 다른 URI 가 있으면 위반으로 기록핵심은 3번입니다. 매니페스트 텍스트를 통째로 diff하면 정상적인 append와 sliding까지 다 걸려서 노이즈가 됩니다. 시퀀스 번호를 키로 잡고 겹치는 구간만 비교해야 진짜 위반만 남습니다.
검출 로직
for (a, b) in 연속한_스냅샷_쌍:
공통 = a.시퀀스집합 & b.시퀀스집합
for seq in 공통:
if a[seq] != b[seq]:
위반 기록 (seq, a[seq], b[seq], a.시각, b.시각)이렇게 만들어 두면 장시간 돌려놓고 나중에 확인할 수 있습니다. 사람이 타이밍을 맞춰 관측할 필요가 없어집니다.
팁: live 매니페스트를 검증할 때 "지금 상태가 맞는가"만 보면 절반만 보는 것입니다. "시간에 따라 변한 방식이 규격에 맞는가"를 봐야 이 계열 문제가 잡힙니다. 스냅샷을 버리지 말고 쌓아두면 그게 가능해집니다.
좁은 영향 범위
정직하게 말하면 이 문제의 실제 영향은 제한적입니다.
| 항목 | 내용 |
|---|---|
| 발생 조건 | 광고 소재의 최초 트랜스코딩 시점. 여러 렌디션이 동시에 요청하는 상황 |
| 빈도 | 정상적인 장기 운영 테스트에서는 재현되지 않았다 |
| 지속성 | 해당 소재가 한 번 트랜스코딩되면 이후 브레이크에서는 발생하지 않는다 |
| 심각도 | 규격 위반이지만 영향받는 세션이 좁고, 재생이 완전히 멈추지는 않는다 |
그래도 이걸 기록해 두는 이유는 두 가지입니다. 첫째, 규격 위반은 빈도와 별개로 언젠가 특정 플레이어 구현에서 크게 터집니다. 둘째, PDT 갭 같은 좌표계 어긋남과 겹치면 증상이 훨씬 복잡해집니다. PDT 갭이 있는 상태에서 매니페스트가 소급 수정되면, 어느 쪽이 원인인지 로그만 보고는 분리할 수 없습니다.
해결 방법
방향은 "발행한 걸 고치지 않는다"로 정해집니다.
| 접근 | 내용 | 트레이드오프 |
|---|---|---|
| 광고 준비를 선행시킨다 | 마커를 받으면 트랜스코딩을 먼저 끝내고 매니페스트를 발행 | 광고 시작이 늦어질 수 있다 |
| 중간 상태를 발행하지 않는다 | 광고 segment가 준비될 때까지 해당 위치를 발행 보류 | 발행 지연. window가 짧으면 위험 |
| 준비 안 된 자리는 라이브로 확정한다 | 라이브로 내보냈으면 그대로 두고, 광고는 다음 위치부터 | 광고 앞부분 손실을 감수 |
세 번째가 규격 준수 관점에서는 가장 안전합니다. 이미 내보낸 건 진실로 확정하고, 못 넣은 광고는 포기하는 것입니다. 실제로는 첫 번째와 조합해서 트랜스코딩 선행으로 중간 상태 자체를 없애는 쪽이 현실적입니다.
정리
- OUT 마커 직후 구간에서 같은
EXT-X-MEDIA-SEQUENCE번호의 segment가 라이브에서 광고로 소급 변경되는 현상을 확인했습니다. RFC 8216이 금지하는 in-place 수정입니다. - 원인은 마커 계산과 광고 데이터 준비가 다른 속도로 진행되는 경합입니다. 마커는 즉시 나가지만 광고 segment는 트랜스코딩을 기다려야 합니다.
- 광고 소재의 최초 트랜스코딩 시점에만, 여러 렌디션이 동시에 요청할 때만 발생합니다. 소재가 캐시된 이후에는 재현되지 않습니다.
- 마커 계산 지연과 증상이 비슷해서 헷갈립니다. 폴 스냅샷 사이에 같은 시퀀스 번호의 내용이 다른지 보면 구분됩니다.
- live 매니페스트 검증은 단일 시점이 아니라 스냅샷 시계열로 해야 합니다. 겹치는 시퀀스 번호만 비교하면 정상적인 append와 sliding을 노이즈로 걸러낼 수 있습니다.