블로그 목록
Backend15분 읽기

Cloud Recording 탐색 재생 장애 — moov, ENDLIST, DISCONTINUITY

녹화 파일을 중간 위치로 탐색할 때 멈추는 문제를 MP4 index, HLS playlist 종료 상태, timestamp discontinuity 관점에서 진단합니다. `ffprobe`, manifest 검사, ffmpeg remux가 각각 확인하거나 복구할 수 있는 범위를 구분하고, 원본 segment 누락처럼 후처리로 복구할 수 없는 경우도 명시합니다.

Cloud RecordingMP4HLSmoovfaststartENDLISTDISCONTINUITYFFmpeg트러블슈팅

"Cloud Recording에서 받은 영상 — 처음부터 재생은 잘 되는데 중간으로 점프하면 무한 로딩, 또는 아예 재생 안 됨". 다운로드가 끝났어도 컨테이너 인덱스, 타임스탬프, 키프레임, 플레이리스트 상태를 각각 확인해야 합니다.

이 글은 그 인덱스 불완전성의 3가지 패턴을 분류하고, 각각의 진단·처방을 정리합니다.


첫 가설 — 인덱스와 경계 정보부터 확인

영상을 점프하려면 "이 시점은 파일의 어느 위치"라는 매핑 정보가 필요합니다. 그 인덱스가 없거나 손상돼도 순차 재생은 가능합니다(처음부터 데이터를 따라가기만 하면 되니까). 점프할 때만 실패합니다.

세 가지 인덱스 문제 패턴:

패턴컨테이너증상 트리거
① moov atom이 파일 끝에 있음MP4HTTP progressive download 시작이 늦거나 Range 요청이 추가됨
② #EXT-X-ENDLIST 누락HLS (m3u8)플레이어가 갱신 가능한 플레이리스트로 처리할 수 있음
③ #EXT-X-DISCONTINUITY 누락HLS코덱/해상도 변경 지점에서 디코더 초기화 실패

패턴 ① MP4 moov atom이 끝에 있을 때

구조

MP4는 두 부분으로 구성됩니다.

정상 (스트리밍 친화):
[ftyp][moov: 인덱스][mdat: 영상 데이터 1GB]
       ↑
       앞에 있음 → 점프 즉시 가능

녹화 직후 (인덱스 끝):
[ftyp][mdat: 영상 데이터 1GB][moov: 인덱스]
                              ↑
                              파일 끝

녹화 도중엔 전체 길이를 모르므로 인덱스를 끝에 붙이는 게 자연스럽습니다. 하지만 그 상태로 플레이어에 던지면:

  • 로컬 파일: 플레이어가 moov를 한 번 읽은 뒤 인덱스를 이용
  • HTTP progressive download: 파일 끝의 moov를 받기 위해 추가 Range 요청이 필요할 수 있어 시작이 늦어짐

진단

# atom 순서를 직접 추적
ffprobe -v trace recording.mp4 2>&1 | grep -E "type:'(moov|mdat)'" | head -5

처방 — +faststart

ffmpeg -i recording.mp4 -c copy -movflags +faststart fixed.mp4
  • -c copy: 재인코딩 없이 컨테이너만 다시 작성
  • +faststart: moov를 파일 앞으로 옮김
  • 결과물: progressive download 시작 조건 개선. seek 성공 여부는 타임스탬프와 키프레임 상태에도 좌우됨

패턴 ② HLS m3u8의 ENDLIST 누락

정상 m3u8 구조

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD     ← VOD임을 명시
#EXTINF:10.0,
segment_001.ts
#EXTINF:10.0,
segment_002.ts
...
#EXT-X-ENDLIST                ← "끝났음"

누락 시 증상

#EXT-X-ENDLIST가 없으면 플레이어는 플레이리스트가 계속 갱신될 수 있다고 판단합니다.

  • 마지막 세그먼트에 도달한 뒤 플레이리스트를 다시 요청할 수 있음
  • seek 가능 범위는 현재 플레이리스트가 제공하는 미디어 시간 범위와 플레이어 구현에 따라 달라짐

녹화가 비정상 종료되면 종종 발생합니다. Agora Cloud Recording에서도 stop API 누락·서버 크래시·storage upload 실패 같은 케이스에서 ENDLIST가 안 붙을 수 있습니다.

진단

# ENDLIST 있는지
grep -c "EXT-X-ENDLIST" recording.m3u8
# 1 → 정상
# 0 → 누락

# PLAYLIST-TYPE도 함께
grep "EXT-X-PLAYLIST-TYPE" recording.m3u8
# #EXT-X-PLAYLIST-TYPE:VOD  ← 이상적
# EVENT면 항목을 삭제할 수 없고 끝날 때 ENDLIST를 추가

처방

녹화가 완전히 종료됐음을 확인한 뒤 — 한 줄 추가:

echo "#EXT-X-ENDLIST" >> recording.m3u8

진행 중인 EVENT/라이브 플레이리스트에 임의로 추가하면 이후 세그먼트를 잘라낸 것으로 보일 수 있습니다. 녹화 종료와 업로드 완료를 먼저 확인하세요.

근본 — mp4로 변환:

ffmpeg -i recording.m3u8 -c copy -movflags +faststart final.mp4

ffmpeg이 현재 플레이리스트에 나열된 세그먼트를 읽어 단일 MP4로 정리합니다. 누락된 세그먼트나 잘못된 타임스탬프까지 자동 복구하는 것은 아닙니다.


패턴 ③ DISCONTINUITY 태그 누락

개념

HLS에서 파일 형식, 트랙 구성·식별자, 타임스탬프 시퀀스가 바뀌면 #EXT-X-DISCONTINUITY가 필요합니다. 인코딩 파라미터나 인코딩 시퀀스가 바뀌는 지점에도 이 태그 사용이 권고됩니다.

#EXTINF:10.0,
segment_005.ts          ← 1280x720 H.264
#EXT-X-DISCONTINUITY     ← 경계
#EXTINF:10.0,
segment_006.ts          ← 1920x1080 H.264 (해상도 변경)

이 태그 없이 segment 특성만 바뀌면:

  • 처음부터 재생: segment 1→2→3 순차 진행이라 디코더가 알아서 재초기화 시도
  • 점프: 특성 다른 segment로 바로 갔는데 m3u8엔 변경 표시 없음 → 디코더가 이전 segment 기준으로 가정 → 초기화 실패 → 무한 로딩

발생 시나리오 (Cloud Recording 맥락)

트리거무엇이 바뀌나
실제 출력 트랙 구성이 바뀜audio-only ↔ audio+video 등
누군가 해상도 변경width/height/bitrate
합성 출력의 인코딩 파라미터가 바뀜코덱·해상도·타임스탬프 시퀀스 등
Cloud Recording의 High Availability mechanism 작동녹화 서버 자동 전환으로 m3u8이 둘로 쪼개짐 (bak0, bak1 … prefix)

진단

# discontinuity 태그가 있는지
grep -c "EXT-X-DISCONTINUITY" recording.m3u8

# bak 파일이 있는지 (HA mechanism 작동 흔적)
ls bak*.m3u8 2>/dev/null

# segment들의 실제 특성 비교 — 다 같아야 정상
for f in segment_001.ts segment_010.ts segment_020.ts; do
  echo "=== $f ==="
  ffprobe -v error -select_streams v:0 \
    -show_entries stream=width,height,codec_name,r_frame_rate \
    -of compact "$f"
done

각 segment의 width/height/codec/framerate가 다르면 그 사이에 DISCONTINUITY가 있어야 합니다.

처방

가장 확실 — 재인코딩으로 정규화:

ffmpeg -i recording.m3u8 -c:v libx264 -c:a aac -movflags +faststart final.mp4
  • 재인코딩하면 모든 frame이 균일한 코덱/해상도로 통일됨 → discontinuity 자체가 사라짐
  • 처리 시간은 하드웨어·코덱·프리셋에 따라 측정
  • seek 간격을 일정하게 하려면 프레임레이트에 맞춰 IDR 간격과 scene-cut 정책도 함께 설정
ffmpeg -i recording.m3u8 \
  -c:v libx264 -preset fast -crf 23 \
  -c:a aac \
  -g 60 -keyint_min 60 \
  -movflags +faststart \
  final.mp4

Agora HA bak 파일 합치기: HA mechanism이 작동하면 fault processing center가 90초 이내에 새 녹화 서버로 전환하고, 그 시점부터의 인덱스를 담은 새 m3u8(bak0, bak1 …)을 생성합니다. Agora는 이 원본 m3u8과 bak m3u8을 단일 mp4로 합쳐주는 자체 transcoder script를 제공합니다(composite/mix mode 한정). Manage Recorded Files 문서의 다운로드 링크 참조. 단순 ffmpeg concat이 아니라 경계에 discontinuity를 적절히 처리해줍니다.


통합 진단 흐름

   [점프 시 무한 로딩 / 재생 실패]
                │
                ▼
       [어떤 파일을 받았나?]
        │             │
       MP4         M3U8 + TS
        │             │
        │             │
        ▼             ▼
  ① moov 위치    ② ENDLIST 있나?
   확인              │
        │       ┌────┴────┐
   끝에 있음    있음     없음
        │        │        │
   faststart    │   echo로 추가
        │        │   또는 mp4 변환
        │        │        │
        │        ▼
        │   ③ segment 특성
        │      균일한가?
        │       │      │
        │      예      아니오
        │       │      │
        │       │   재인코딩
        │       │   (libx264)
        │       ▼
        ▼   [점프 정상]
    [점프 정상]

Agora Cloud Recording 후처리 권장 파이프라인

매번 클라이언트 쪽에서 이 문제를 만나는 게 싫다면, 결과물을 그대로 전달하지 말고 표준 후처리 파이프라인을 한 번 거치는 걸 권장합니다.

[Cloud Recording 종료 webhook]
        │
        ▼
[원본 m3u8 + ts (또는 mp4) 다운로드]
        │
        ▼
[1단계: 빠른 재컨테이닝]
  ffmpeg -fflags +genpts -i in.m3u8 -c copy \
    -movflags +faststart \
    out.mp4
        │
        ▼
[2단계 (옵션): 점프 테스트 후 문제 있으면 재인코딩]
  ffmpeg -i in.m3u8 \
    -c:v libx264 -c:a aac \
    -g 60 -keyint_min 60 \
    -movflags +faststart \
    out.mp4
        │
        ▼
[검증된 mp4를 클라이언트에 전달]

핵심 옵션 의미:

옵션의미
-c copy재인코딩 없이 컨테이너만 변경 (빠름, 화질 손실 없음)
-movflags +faststartmoov atom을 앞으로
-fflags +genptsDTS가 있을 때 누락된 PTS 생성. 입력 옵션이므로 -i 앞에 배치
-g 60 -keyint_min 60최대 GOP와 최소 keyint 설정. 정확한 60프레임 주기는 frame rate·scene-cut 설정도 필요 (#30 참고)

실무 팁: Agora 문서상 composite recording은 recordingFileConfig.avFileType: ["hls", "mp4"]로 MP4를 함께 생성할 수 있습니다. 전달 전 실제 파일의 atom 순서와 seek 동작을 검사하고, 필요한 경우에만 +faststart 후처리를 적용하세요.


빠른 체크리스트

받은 결과물에서 점프 이슈가 있을 때, 5분 안에 원인을 좁히는 순서:

# 1. 무엇을 받았는지
ls -la
# recording.m3u8 + segment_*.ts 인지, 아니면 단일 .mp4 인지

# 2-1. mp4면 — moov 위치
ffprobe -v trace recording.mp4 2>&1 | grep -E "type:'(moov|mdat)'" | head -5

# 2-2. m3u8이면 — ENDLIST와 DISCONTINUITY
grep -c "EXT-X-ENDLIST" recording.m3u8
grep -c "EXT-X-DISCONTINUITY" recording.m3u8
ls bak*.m3u8 2>/dev/null

# 3. 재컨테이닝을 별도 출력 파일로 시험
ffmpeg -fflags +genpts -i recording.m3u8 -c copy -movflags +faststart step1.mp4
# 이 결과물이 잘 되면 → 컨테이너 문제였음
# 이것도 안 되면 → 재인코딩

ffmpeg -i recording.m3u8 -c:v libx264 -c:a aac -movflags +faststart step2.mp4

한 줄 결론

점프 실패는 한 가지 원인으로 단정하지 않습니다. moov 위치, ENDLIST, DISCONTINUITY를 먼저 확인하고, 이어서 타임스탬프·키프레임·누락 세그먼트를 검사합니다.


관련 글

참고 자료

© 2026 Frank Kim. All rights reserved.