시리즈: 실시간 통화, 어떻게 녹화하는가4/6
- 1.실시간 통화, 왜 녹화해야 하는가
- 2.내 서버에서 직접 녹화한다면
- 3.녹화 모드 해부
- 4.M3U8과 TS 파일의 모든 것 ← 현재 글
- 5.FFmpeg, 미디어의 스위스 아미 나이프
- 6.FFmpeg 실전 파이프라인
M3U8과 TS 파일의 모든 것 — HLS Deep Dive
MP4의 moov atom 한계, TS 패킷 188바이트 구조, I/P/B-frame 슬라이싱 규칙, Agora 전용 M3U8 태그까지 HLS를 프로토콜 수준에서 해부합니다.
녹화 파일을 열어보면 두 가지가 나옵니다. .m3u8 파일과 .ts 파일들. 처음 보면 생소합니다. MP4나 WebM을 예상했는데 왜 이런 구조인가?
이유는 하나입니다. 실시간 녹화에서 장애 내성(Fault Tolerance)이 최우선이기 때문입니다.
"서버가 비정상 종료되어도 지금까지 녹화된 내용은 살아남아야 한다" — 이 요구사항을 충족하려면 MP4 단일 파일 구조는 근본적으로 적합하지 않습니다. 왜 그런지부터 시작합니다.
MP4의 치명적 약점 — moov atom
MP4 파일을 열면 내부는 이렇게 생겼습니다.
moov atom은 MP4에서 사실상 목차(index)입니다. 트랙 정보, 타임스탬프, 각 샘플의 바이트 오프셋 — 플레이어가 영상을 재생하려면 이 정보가 반드시 필요합니다. 문제는 이 moov가 파일의 맨 끝에 위치한다는 것입니다.
녹화가 정상적으로 끝나면 moov가 파일 끝에 기록됩니다. 하지만 서버 크래시, 네트워크 중단, 강제 종료 — 이런 상황에서는 mdat만 쓰여진 채 moov가 없는 파일이 남습니다. 플레이어는 이 파일을 열지 못합니다. 1시간짜리 상담 녹화가 통째로 날아가는 겁니다.
ffmpeg -i input.mp4 -movflags faststart output.mp4로 moov를 파일 앞으로 옮길 수 있습니다. 하지만 이 작업은 녹화가 완전히 끝난 후에만 가능합니다. 진행 중인 녹화에는 적용할 수 없습니다.
HLS — HTTP Live Streaming
Apple이 2009년에 만든 스트리밍 프로토콜입니다. 핵심 아이디어는 단순합니다. 긴 영상을 통째로 다루는 대신, 짧은 조각들로 쪼개서 HTTP로 전달한다.
M3U8은 목차(playlist) 역할을 하는 텍스트 파일이고, TS(Transport Stream)는 실제 미디어가 담긴 조각들입니다. 플레이어는 M3U8을 읽고, 필요한 TS 세그먼트를 순서대로 다운로드해서 재생합니다.
녹화 관점에서 장점이 명확합니다. 세그먼트 단위로 파일이 완성되기 때문에, 중간에 서버가 죽어도 지금까지 완성된 세그먼트들은 온전히 살아있습니다. MP4처럼 마지막에 한 번에 메타데이터를 쓰는 구조가 아닙니다. 녹화 파일이 S3에 저장되고 업로드되는 흐름은 녹화 파일이 S3에 저장되는 흐름에서 다뤘습니다.
TS (Transport Stream) 패킷 구조
TS는 원래 위성방송과 디지털 방송을 위해 설계된 컨테이너 포맷입니다. 핵심 설계 철학은 **손실 허용(loss-tolerant)**입니다. 신뢰할 수 없는 전송 채널을 가정하고 만들었습니다.
모든 패킷이 고정 188 bytes입니다. 첫 번째 바이트는 항상 0x47 (sync byte)입니다. 패킷 경계를 찾기 위해 파일 전체를 파싱할 필요가 없습니다. 파일 오프셋 mod 188 == 0인 위치를 찾으면 됩니다.
**PID (Packet Identifier)**는 13-bit 값으로 어느 스트림인지를 식별합니다. PMT(Program Map Table)에 오디오 PID와 비디오 PID가 정의되어 있고, 플레이어는 이를 참고해 패킷을 분류합니다. 패킷 하나가 손상되어도 해당 패킷만 스킵하면 됩니다. 파일 전체가 망가지지 않습니다.
| MP4 | TS | |
|---|---|---|
| 구조 | moov atom이 파일 끝 | 각 패킷이 자기완결적 |
| 중간 손상 | 파일 전체 재생 불가 | 손상된 패킷만 스킵 |
| 실시간 녹화 | 비정상 종료 시 깨짐 | 쓴 세그먼트는 안전 |
| 스트리밍 | 전체 다운로드 필요 | 세그먼트 단위 즉시 재생 |
| 파일 크기 | 효율적 | 헤더 오버헤드 (~2%) |
M3U8 파일 해부
M3U8은 그냥 텍스트 파일입니다. #으로 시작하는 태그와 파일 경로로 구성됩니다. Agora Cloud Recording이 실제로 생성하는 M3U8은 이렇게 생겼습니다.
표준 HLS 태그:
#EXTM3U— M3U8 파일임을 선언하는 시작 태그. 반드시 첫 줄에 있어야 합니다.#EXT-X-VERSION:3— HLS 스펙 버전. Agora는 v3를 사용합니다.#EXT-X-TARGETDURATION:15— 세그먼트 최대 길이(초). 실제 세그먼트는 이보다 짧을 수 있지만, 길어서는 안 됩니다.#EXTINF:15.0,— 바로 다음에 오는 TS 파일의 실제 길이(초). 쉼표로 끝납니다.#EXT-X-ENDLIST— 녹화 완료를 나타냅니다. 이 태그가 없으면 플레이어는 세그먼트가 계속 추가될 것으로 기대합니다 (라이브 스트리밍 모드).
Agora 전용 확장 태그:
#EXT-X-AGORA-TRACK-EVENT—EVENT=START또는EVENT=STOP,TRACK_TYPE=AUDIO또는VIDEO,TIME=UTC초. 스트림이 언제 시작하고 중단됐는지를 기록합니다. 네트워크 중단으로 스트림이 잠깐 끊겼다 재개된 경우 START/STOP 쌍이 여러 번 나타납니다.#EXT-X-AGORA-ROTATE—WIDTH,HEIGHT,ROTATE(0/90/180/270). 해상도와 화면 회전 정보. 모바일에서 카메라 방향이 바뀌면 이 태그가 추가됩니다.
마지막 세그먼트의 길이가 12.3초인 것에 주목하세요. 15초 경계에서 녹화가 끝나지 않았기 때문입니다. 실제 녹화 데이터가 12.3초이면 그 길이 그대로 기록됩니다.
Agora 모드별 출력 파일 트리
모드에 따라 생성되는 파일 구조가 다릅니다.
Individual 모드 — 참여자별 분리 저장
UID마다, 트랙(audio/video)마다 별도의 M3U8과 TS 세트가 생성됩니다. 참여자가 10명이면 최대 20개의 M3U8 파일이 나옵니다.
Mix (Composite) 모드 — 합성 후 단일 파일
서버 사이드에서 모든 참여자 영상을 믹싱한 결과물입니다. 파일이 훨씬 단순합니다.
파일명 컨벤션 해부:
타임스탬프가 파일명에 들어가 있어서 파일 시스템 정렬만으로 시간순 순서를 알 수 있습니다.
avFileType 설정
Cloud Recording API 요청 시 출력 포맷을 지정할 수 있습니다.
MP4만 단독으로 지정하면 에러가 납니다. 이유는 Agora의 내부 파이프라인 때문입니다. MP4는 HLS 녹화 결과물(TS 세그먼트)을 후처리로 합쳐서 만드는 방식입니다. HLS 없이 MP4를 만들 수 없습니다. MP4는 HLS에 의존합니다.
"hls", "mp4" 조합을 쓰면 녹화가 끝난 후 Agora가 TS 세그먼트들을 MP4로 합쳐서 추가로 업로드합니다. 즉시 재생 가능한 단일 MP4가 생기지만, 녹화 완료 후 추가 시간이 걸립니다.
I-frame / P-frame / B-frame — 세그먼트를 어디서 자르는가
TS 세그먼트를 임의로 자를 수는 없습니다. 비디오 압축의 기본 개념이 개입합니다.
- I-frame (Key Frame): 완전한 이미지 정보를 담습니다. 앞 프레임 없이도 독립 재생이 가능합니다. JPEG 한 장과 유사한 정보량을 가집니다.
- P-frame: 이전 I-frame 또는 P-frame과의 차이(delta)만 저장합니다. I-frame 없이는 재생할 수 없습니다.
- B-frame: 앞뒤 프레임 모두와의 차이를 저장합니다. 가장 압축률이 높지만 의존성이 가장 큽니다.
TS 세그먼트는 반드시 I-frame에서 시작해야 합니다. P-frame이나 B-frame에서 세그먼트가 시작되면, 참조할 이전 프레임이 없어서 화면이 깨집니다. 플레이어가 세그먼트를 독립적으로 재생할 수 없게 됩니다.
TS 슬라이싱 규칙
Agora가 TS 세그먼트를 자르는 조건은 다음과 같습니다.
비디오 슬라이싱 트리거:
오디오 슬라이싱:
오디오는 단순합니다. 비디오 슬라이싱과 동기화되거나, 독립적으로 매 15초마다 일정하게 잘립니다.
비디오와 오디오의 타임스탬프가 어긋나는 경우, Individual 모드에서는 M3U8의 #EXTINF 값을 통해 플레이어가 싱크를 맞춥니다. 나중에 FFmpeg으로 합칠 때 이 타임스탬프 정렬이 중요해집니다.
M3U8 파싱 스크립트
Agora M3U8을 파싱해서 유용한 정보를 빠르게 추출하는 Python 스크립트입니다.
녹화 파일을 받았을 때 가장 먼저 실행할 만한 스크립트입니다. 총 길이와 스트림 중단 여부를 빠르게 확인할 수 있습니다. 세그먼트 수가 예상보다 적거나, START/STOP 쌍이 여러 번 나타나면 네트워크 중단이 있었음을 의미합니다.
핵심 요약
- MP4가 실시간 녹화에 부적합한 이유는 moov atom 구조 때문입니다. 비정상 종료 시 메타데이터가 기록되지 않아 파일 전체를 잃습니다.
- HLS는 영상을 짧은 TS 세그먼트로 쪼개 저장합니다. 완성된 세그먼트는 서버가 죽어도 안전합니다.
- TS 패킷은 고정 188 bytes로 자기완결적입니다. 중간 손상이 해당 패킷에만 영향을 미칩니다.
- M3U8은 플레인 텍스트 목차입니다. Agora 전용 태그(
#EXT-X-AGORA-TRACK-EVENT,#EXT-X-AGORA-ROTATE)를 통해 스트림 이벤트와 해상도 변경을 기록합니다. - TS 세그먼트는 반드시 I-frame에서 시작해야 합니다. Agora는 I-frame 도착 후 15초를 기준으로 슬라이싱하며, 코덱/해상도 변경, 스트림 중단, 강제 타임아웃 조건에서도 즉시 자릅니다.
- MP4 출력은 HLS에 의존합니다.
avFileType: ["mp4"]단독은 에러이며,["hls", "mp4"]로 지정하면 녹화 완료 후 TS → MP4 변환이 추가로 수행됩니다.
시리즈 네비게이션
실시간 통화, 어떻게 녹화하는가 시리즈