블로그 목록
Backend15분 읽기

Cloud Recording 점프 재생 무한 로딩 — moov / ENDLIST / DISCONTINUITY 트러블슈팅

다운로드는 다 끝났는데 영상을 중간으로 점프하면 무한 로딩이 걸리거나 아예 재생이 안 됩니다. 알고 보면 데이터가 손상된 게 아니라 인덱스가 불완전한 경우가 대부분입니다. 이 글은 MP4의 moov atom 위치, HLS의 ENDLIST 누락, 그리고 DISCONTINUITY 누락이라는 세 가지 패턴을 진단하는 법과 ffmpeg으로 고치는 처방을 정리하고, Agora Cloud Recording 결과물에 바로 쓸 수 있는 후처리 파이프라인까지 안내합니다.

Cloud RecordingMP4HLSmoovfaststartENDLISTDISCONTINUITYFFmpeg트러블슈팅

"Cloud Recording에서 받은 영상 — 처음부터 재생은 잘 되는데 중간으로 점프하면 무한 로딩, 또는 아예 재생 안 됨". 다운로드는 다 끝났는데도 그렇습니다. 이건 데이터가 손상된 게 아니라 인덱스가 불완전한 것이 원인일 가능성이 높습니다.

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


핵심 가설 — 데이터는 멀쩡, 인덱스만 깨짐

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

세 가지 인덱스 문제 패턴:

패턴컨테이너증상 트리거
① moov atom이 파일 끝에 있음MP4점프할 때마다 파일 끝까지 다녀와서 느림/멈춤
#EXT-X-ENDLIST 누락HLS (m3u8)플레이어가 "라이브로 인식" → 점프 시 다음 segment 무한 대기
#EXT-X-DISCONTINUITY 누락HLS코덱/해상도 변경 지점에서 디코더 초기화 실패

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

구조

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

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

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

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

  • 첫 재생: 파일 끝까지 가서 moov 읽고 → 처음으로 돌아와서 재생
  • 점프: 매번 인덱스 참조하느라 디스크/파일 안에서 왔다갔다 → 느림 또는 멈춤

진단

# 빠른 확인 — 파일 시작 1KB 안에 moov가 있나?
python3 -c "
with open('recording.mp4', 'rb') as f:
    head = f.read(8192)
    print('moov in head' if b'moov' in head else 'moov NOT in head — at end')
"

# 또는 ffprobe 추적 (느리면 moov가 뒤에 있다는 신호)
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: 재인코딩 없음 → 2시간 영상도 수십 초
  • +faststart: moov를 파일 앞으로 옮김
  • 결과물: 어디로 점프해도 즉시 반응

YouTube가 업로드된 영상에 자동으로 적용하는 후처리가 이것입니다.


패턴 ② 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가 없으면 플레이어는 "라이브 스트리밍인가?" 판단합니다.

  • 처음부터 재생: 첫 segment부터 순차 진행, 어느 시점에 마지막 segment 도달 → 다음 segment 기다림 → 멈춤 (이건 사용자가 끝까지 본 경우라 잘 안 보임)
  • 점프: 그 위치 segment 받고 → 다음 segment 받으려고 m3u8 새로고침 시도 → 라이브로 인식했으니 "곧 새 segment 올 거야"라며 무한 대기

녹화가 비정상 종료되면 종종 발생합니다. 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면 VOD seek 동작 보장 X)

처방

응급 — 한 줄 추가:

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

>>는 파일 끝에 줄 추가. 정말 단순하지만 효과적인 응급 처치입니다.

근본 — mp4로 변환:

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

ffmpeg이 m3u8을 읽어 단일 mp4로 정리. ENDLIST 누락도 자동으로 처리됩니다.


패턴 ③ DISCONTINUITY 태그 누락

개념

HLS의 segment들은 보통 같은 코덱·해상도·프레임레이트로 매끄럽게 이어집니다. 중간에 특성이 바뀌면 #EXT-X-DISCONTINUITY 태그를 넣어 "여기서 stream이 바뀜"을 디코더에 알려야 합니다.

#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 맥락)

트리거무엇이 바뀌나
사용자가 카메라/마이크를 껐다 켬stream 구성 변경 (audio-only ↔ audio+video)
누군가 해상도 변경width/height/bitrate
채널에 새 사용자 입장/퇴장 (composite mode)합쳐진 mixed stream의 구성
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 자체가 사라짐
  • 시간: 영상 길이의 0.3~0.5배 정도
  • 점프 성능을 더 보장하려면 keyframe 간격도 강제: -g 60 -keyint_min 60
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 -i in.m3u8 -c copy \
    -movflags +faststart \
    -fflags +genpts \
    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 +genptspts 깨진 경우 ffmpeg이 새로 생성
-g 60 -keyint_min 6060프레임마다 keyframe 강제 (#30 참고)

실무 팁: Cloud Recording의 recordingFileConfig.avFileType: ["hls", "mp4"]로 mp4를 동시에 받도록 설정해도, 그 mp4의 moov 위치까지 보장되는 건 아닙니다. 한 번 후처리를 거치는 게 안전합니다.


빠른 체크리스트

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

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

# 2-1. mp4면 — moov 위치
ffprobe -v error -show_entries format=duration recording.mp4
# 즉시 답하면 OK, 한참 걸리면 문제

# 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 -i recording.m3u8 -c copy -movflags +faststart -fflags +genpts 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.