Shaka Player로 보는 라이브 스트리밍 개론

들어가며 — VOD와 라이브, 무엇이 다른가

웹에서 영상을 재생할 때, 우리가 보는 콘텐츠는 크게 VOD(주문형 비디오)와 라이브로 나뉩니다. 둘은 같은 플레이어, 같은 재생 파이프라인을 쓰면서도 전혀 다른 방식으로 동작합니다. 그 차이의 뿌리는 "매니페스트가 끝나 있는가, 지금도 자라고 있는가"에 있습니다.

VOD와 라이브의 차이는 한 문장으로 요약됩니다.

VOD의 타임라인은 처음부터 끝까지 고정되어 있지만, 라이브의 타임라인은 시간이 흐를수록 앞으로 자라나고 뒤는 잘려나간다.

VOD 매니페스트는 한 번 받으면 그 안에 전체 세그먼트 목록이 들어 있습니다. 0초부터 끝까지 어디든 탐색할 수 있고, 매니페스트를 다시 받을 이유도 없습니다. 반면 라이브는 방송이 지금도 진행 중이기 때문에, 매니페스트를 받는 그 순간에도 새로운 세그먼트가 인코더에서 만들어지고 있습니다. 그래서 라이브 재생에는 VOD에 없는 세 가지 개념이 등장합니다.

  • 매니페스트 갱신(refresh): 새 세그먼트를 알아내기 위해 플레이어가 주기적으로 매니페스트를 다시 받아야 한다.
  • 슬라이딩 윈도우(sliding window): 서버는 보통 최근 N초 분량의 세그먼트만 유지하고 오래된 것은 버린다. 그래서 탐색 가능한 구간이 시간에 따라 통째로 미끄러진다.
  • 라이브 엣지(live edge)와 지연(latency): "지금 이 순간"에 해당하는 재생 위치가 따로 있고, 플레이어는 안정적인 재생을 위해 그보다 일정 시간 뒤에서 재생한다.

이 글에서는 Shaka Player가 이 세 가지를 어떻게 다루는지, HLS 라이브 매니페스트는 실제로 어떻게 생겼는지, 그리고 라이브 재생 품질을 좌우하는 설정들이 무엇인지를 정리합니다.

Shaka Player는 라이브를 어떻게 재생하는가

Shaka Player 내부에서 시간 축을 관리하는 핵심 객체는 Presentation Timeline입니다. VOD든 라이브든 모든 재생은 이 타임라인 위에서 일어나며, 라이브에서는 이 타임라인이 실시간 시계와 연동되어 계속 움직인다는 점이 다릅니다.

매니페스트 갱신 루프

플레이어가 라이브 매니페스트를 처음 파싱하면, 파서는 그것이 라이브라는 사실(HLS라면 #EXT-X-ENDLIST의 부재, DASH라면 type="dynamic")을 인지하고 갱신 타이머를 겁니다. 이 타이머가 만료될 때마다 파서는 매니페스트를 다시 내려받아, 그사이 새로 추가된 세그먼트를 타임라인에 반영하고 사라진 세그먼트를 제거합니다.

갱신 주기는 매니페스트가 스스로 알려줍니다. HLS에서는 일반적으로 #EXT-X-TARGETDURATION(세그먼트 1개의 최대 길이)을 기준으로 다음 갱신 시점을 잡습니다. 즉 세그먼트가 6초 단위라면, 대략 그 주기에 맞춰 플레이어가 "새 세그먼트 나왔나요?"를 물어보는 셈입니다.

INFO

이 갱신 루프는 재생(playback)과는 독립적인 타이머로 돌아갑니다. 사용자가 일시정지를 누르고 있어도 매니페스트는 계속 갱신될 수 있고, 그래서 잠깐 멈췄다가 풀면 라이브 엣지가 이미 저만치 앞서 있는 상황이 생깁니다.

Live Edge와 탐색 가능한 구간

라이브에서 가장 중요한 좌표는 라이브 엣지(live edge) 입니다. 라이브 엣지는 "지금 방송되고 있는 가장 최신 지점"을 의미하며, Presentation Timeline은 실시간으로 이 값을 갱신합니다.

하지만 플레이어가 라이브 엣지에 딱 붙어서 재생하지는 않습니다. 가장 최신 세그먼트는 방금 막 만들어졌거나 아직 완성되지 않았을 수 있어서, 그 지점을 재생하려 하면 버퍼가 비어 끊김(stall)이 발생하기 쉽습니다. 그래서 플레이어는 라이브 엣지에서 일정 시간만큼 뒤로 물러난 지점을 실제 재생 시작점으로 삼습니다. 이 간격이 곧 라이브 지연(latency) 입니다.

탐색(seek) 가능한 구간도 VOD와 다릅니다. video.seekable이 가리키는 범위는 고정되어 있지 않고, 슬라이딩 윈도우를 따라 양 끝이 함께 앞으로 이동합니다.

1시각 T:        [=========== seekable ===========]
2              ^seekable.start            ^live edge
3                                    ^실제 재생 위치(지연만큼 뒤)
4
5시각 T+6초:          [=========== seekable ===========]
6                    ^오래된 세그먼트 제거    ^새 세그먼트 추가

윈도우의 시작점이 앞으로 미끄러지기 때문에, 플레이어가 너무 뒤처져 재생하면 재생 위치가 윈도우 시작점 밖으로 밀려날 수 있습니다. 이렇게 되면 세그먼트가 이미 서버에서 사라진 상태라 다운로드가 실패하고, 결국 라이브 엣지 쪽으로 강제로 점프하게 됩니다.

지연은 어디에서 생기나

라이브의 "체감 지연"은 한 군데에서 오는 게 아니라 파이프라인 전체에 누적됩니다.

  1. 인코딩·패키징: 인코더가 세그먼트를 만들고 CDN에 올리는 데 걸리는 시간.
  2. 세그먼트 길이: 세그먼트가 6초라면, 플레이어는 최소 한 세그먼트가 완성될 때까지 기다려야 한다.
  3. 플레이어 버퍼: 끊김을 막기 위해 플레이어가 미리 쌓아두는 버퍼만큼 재생이 뒤로 밀린다.
  4. 라이브 지연 타깃: 플레이어가 의도적으로 라이브 엣지에서 떨어뜨려 둔 간격.

저지연(low latency) 라이브를 만든다는 것은 결국 이 네 가지를 동시에 줄이는 일입니다. 그리고 그중 플레이어 단에서 직접 조정할 수 있는 것이 3번과 4번이며, 뒤에서 다룰 Shaka 설정이 바로 여기에 개입합니다.

HLS 라이브 매니페스트 뜯어보기

이제 실제 매니페스트를 봅시다. HLS는 마스터 플레이리스트(master playlist)미디어 플레이리스트(media playlist) 라는 2단 구조로 되어 있습니다.

마스터 플레이리스트

마스터 플레이리스트는 화질·코덱별 변형(variant) 목록을 담은 진입점입니다. 라이브와 VOD에서 구조 자체는 크게 다르지 않습니다.

1#EXTM3U
2#EXT-X-VERSION:7
3#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2"
4720p/index.m3u8
5#EXT-X-STREAM-INF:BANDWIDTH=5120000,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2"
61080p/index.m3u8

#EXT-X-STREAM-INF 한 줄이 하나의 화질이며, 그 아래 URL이 실제 세그먼트 목록을 담은 미디어 플레이리스트를 가리킵니다. Shaka는 이 목록을 ABR(적응형 비트레이트)의 후보로 삼아 네트워크 상황에 맞춰 화질을 오갑니다.

미디어 플레이리스트 — 라이브의 핵심

라이브의 본질이 드러나는 곳은 미디어 플레이리스트입니다. 아래는 6초 세그먼트를 3개만 유지하는 라이브 플레이리스트의 예시입니다.

1#EXTM3U
2#EXT-X-VERSION:7
3#EXT-X-TARGETDURATION:6
4#EXT-X-MEDIA-SEQUENCE:142
5#EXT-X-PROGRAM-DATE-TIME:2026-06-24T12:00:00.000Z
6#EXTINF:6.000,
7seg142.m4s
8#EXTINF:6.000,
9seg143.m4s
10#EXTINF:6.000,
11seg144.m4s

각 태그가 의미하는 바를 짚어보겠습니다.

  • #EXT-X-TARGETDURATION:6 — 세그먼트 1개의 최대 길이가 6초임을 선언합니다. 플레이어는 이 값을 매니페스트 갱신 주기의 기준으로 삼습니다.
  • #EXT-X-MEDIA-SEQUENCE:142 — 이 플레이리스트의 첫 번째 세그먼트가 전체 방송에서 142번째임을 뜻합니다. 이 값이 슬라이딩 윈도우의 핵심입니다.
  • #EXT-X-PROGRAM-DATE-TIME — 세그먼트를 실제 벽시계 시각(wall-clock)에 매핑합니다. 라이브 엣지를 절대 시각으로 환산하거나, 멀티 트랙 간 동기화·자막 싱크를 맞출 때 사용됩니다.
  • #EXTINF:6.000 — 바로 뒤 세그먼트의 실제 재생 길이입니다.

여기서 결정적인 점은, 이 플레이리스트에 #EXT-X-ENDLIST 태그가 없다는 것입니다.

TIP

#EXT-X-ENDLIST는 "이 플레이리스트는 더 이상 변하지 않는다(= VOD)"는 종료 표식입니다. 라이브 플레이리스트에는 이 태그가 없으며, Shaka를 비롯한 모든 HLS 플레이어는 이 태그의 유무로 라이브/VOD를 판별합니다. 방송이 끝나면 인코더가 마지막 갱신에서 #EXT-X-ENDLIST를 붙여 라이브가 종료되었음을 알립니다.

슬라이딩 윈도우의 동작

이 플레이리스트가 6초 뒤에 어떻게 바뀌는지 보면 슬라이딩 윈도우가 한눈에 들어옵니다.

1# 6초 후 — 새 세그먼트 추가, 가장 오래된 것 제거
2#EXTM3U
3#EXT-X-VERSION:7
4#EXT-X-TARGETDURATION:6
5#EXT-X-MEDIA-SEQUENCE:143          ← 142 → 143으로 증가
6#EXT-X-PROGRAM-DATE-TIME:2026-06-24T12:00:06.000Z
7#EXTINF:6.000,
8seg143.m4s                         ← seg142가 사라짐
9#EXTINF:6.000,
10seg144.m4s
11#EXTINF:6.000,
12seg145.m4s                         ← seg145가 새로 등장

seg142가 목록에서 빠지고 #EXT-X-MEDIA-SEQUENCE가 143으로 올라갔습니다. 즉 목록 길이는 그대로지만 창(window)이 통째로 한 칸 앞으로 미끄러진 것입니다. Shaka의 파서는 갱신 때마다 이 차이를 계산해, 새 세그먼트는 타임라인에 추가하고 사라진 세그먼트는 더 이상 탐색 대상에서 제외합니다.

LL-HLS — 저지연을 위한 확장

일반 HLS는 세그먼트 단위(6초 등)로 움직이기 때문에 구조적으로 지연이 큽니다. 이를 줄이기 위한 것이 LL-HLS(Low-Latency HLS) 이며, 핵심은 세그먼트를 더 작은 부분(Partial Segment) 으로 쪼개는 것입니다.

1#EXT-X-PART-INF:PART-TARGET=1.0
2...
3#EXT-X-PART:DURATION=1.000,URI="seg145.0.m4s"
4#EXT-X-PART:DURATION=1.000,URI="seg145.1.m4s"
5#EXT-X-PART:DURATION=1.000,URI="seg145.2.m4s"
6#EXT-X-PRELOAD-HINT:TYPE=PART,URI="seg145.3.m4s"
  • #EXT-X-PART — 완성된 세그먼트를 기다리지 않고, 1초 단위 조각을 먼저 받아 재생할 수 있게 합니다.
  • #EXT-X-PRELOAD-HINT — 아직 만들어지지 않은 다음 조각의 URL을 미리 알려주어, 플레이어가 선제적으로 요청을 걸어둘 수 있게 합니다.
  • 블로킹 리로드(Blocking Playlist Reload) — 플레이어가 "다음 조각이 준비되면 응답해 달라"는 쿼리(_HLS_msn, _HLS_part)를 붙여 매니페스트를 요청하고, 서버는 준비될 때까지 응답을 보류합니다. 짧은 주기로 폴링하는 대신, 새 조각이 나오는 즉시 응답을 받는 구조입니다.

이 방식으로 LL-HLS는 일반 HLS의 수~십수 초 지연을 2초 안팎까지 끌어내릴 수 있습니다. Shaka Player에서 이 동작을 켜는 것이 뒤에서 볼 lowLatencyMode 설정입니다.

Shaka가 매니페스트 갱신을 처리하는 방식

매니페스트 구조를 봤으니, Shaka의 HLS 파서가 이를 실제로 어떻게 소화하는지 정리해 보겠습니다. 라이브 파싱은 크게 세 가지 일을 반복합니다.

  1. 주기적 갱신 스케줄링 — 첫 파싱에서 라이브임을 확인하면(#EXT-X-ENDLIST 부재), #EXT-X-TARGETDURATION 등을 기준으로 다음 갱신 타이머를 건다. 갱신이 끝나면 다시 타이머를 거는 식으로 루프가 이어진다.
  2. 세그먼트 추가·제거(evict) — 갱신된 플레이리스트의 #EXT-X-MEDIA-SEQUENCE를 기준으로, 이전에 없던 세그먼트는 타임라인에 추가하고 윈도우에서 빠진 세그먼트는 제거한다. 이때 Presentation Timeline의 availability window(탐색 가능 구간)가 함께 갱신된다.
  3. 라이브 엣지 갱신 — 가장 최신 세그먼트를 기준으로 라이브 엣지를 다시 계산해, seekable 범위와 재생 위치 보정의 기준점을 최신 상태로 유지한다.
WARNING

매니페스트 갱신은 네트워크 요청이므로 실패할 수 있습니다. 갱신이 지연되거나 실패하면 플레이어가 알고 있는 세그먼트 목록이 실제 방송보다 뒤처지고, 그 상태로 재생을 이어가면 윈도우 밖으로 밀려나 끊김으로 이어집니다. 그래서 라이브에서는 갱신 주기와 버퍼·지연 설정의 균형이 VOD보다 훨씬 민감하게 작동합니다.

라이브에 유용한 Shaka Player 설정

이제 실전입니다. Shaka Player의 설정은 player.configure({ ... })로 주입하며, 라이브와 관련된 주요 항목은 streamingmanifest 네임스페이스에 모여 있습니다.

저지연 모드

1player.configure({
2  streaming: {
3    lowLatencyMode: true,
4  },
5});

streaming.lowLatencyMode를 켜면 LL-HLS의 부분 세그먼트(#EXT-X-PART)와 블로킹 리로드를 활용해 라이브 엣지에 더 가깝게 붙어 재생합니다. 단, 이 모드는 콘텐츠(서버)가 LL-HLS를 지원할 때만 효과가 있으며, 라이브 엣지에 바짝 붙는 만큼 네트워크가 불안정하면 끊김에 취약해집니다.

라이브 동기화(Live Sync)

라이브 엣지와의 간격을 일정하게 유지하려면 Live Sync를 사용합니다.

1player.configure({
2  streaming: {
3    liveSync: {
4      enabled: true,
5      targetLatency: 3,        // 목표 지연(초)
6      maxPlaybackRate: 1.1,    // 따라잡을 때 최대 배속
7      minPlaybackRate: 0.95,   // 너무 앞설 때 최소 배속
8    },
9  },
10});

플레이어가 목표 지연보다 뒤처지면 재생 속도를 미세하게 올려(예: 1.1배) 라이브 엣지를 따라잡고, 너무 앞서면 속도를 살짝 낮춰 간격을 맞춥니다. 사용자가 체감하기 어려운 수준의 배속 조정으로 지연을 일정하게 수렴시키는 것이 핵심입니다.

INFO

Shaka의 버전에 따라 liveSync 관련 옵션의 형태가 단순 불리언에서 객체 형태로 확장되어 왔습니다. 실제 적용 전에 사용 중인 버전의 기본 설정(defaultConfig)을 확인하는 것을 권장합니다.

버퍼링 목표

1player.configure({
2  streaming: {
3    bufferingGoal: 10,      // 앞으로 채워둘 목표 버퍼(초)
4    rebufferingGoal: 2,     // 재생 시작/재개에 필요한 최소 버퍼(초)
5    bufferBehind: 30,       // 재생 위치 뒤로 유지할 버퍼(초)
6  },
7});
  • bufferingGoal — 재생 위치 앞으로 미리 쌓아둘 버퍼 길이입니다. 크게 잡으면 끊김에 강하지만 그만큼 라이브 엣지에서 멀어져 지연이 늘어납니다. 저지연을 원하면 줄이고, 안정성을 원하면 늘리는 직접적인 트레이드오프 지점입니다.
  • rebufferingGoal — 재생을 (재)시작하기 위해 최소한 확보해야 하는 버퍼입니다.
  • bufferBehind — 재생 위치 뒤로 얼마나 보관할지로, 라이브에서 살짝 되감기를 허용하는 폭에 영향을 줍니다.

매니페스트·지연 관련 설정

1player.configure({
2  manifest: {
3    defaultPresentationDelay: 0,         // 라이브 시작 지연 기본값(초)
4    availabilityWindowOverride: NaN,     // 탐색 가능 윈도우 강제 지정(초)
5  },
6  streaming: {
7    inaccurateManifestTolerance: 2,      // 매니페스트 시간 오차 허용폭(초)
8  },
9});
  • manifest.defaultPresentationDelay — 매니페스트가 별도 지연 값을 명시하지 않았을 때 적용할 라이브 시작 지연입니다. 0이면 플레이어가 세그먼트 길이 등을 바탕으로 합리적인 값을 추정합니다.
  • manifest.availabilityWindowOverride — 탐색 가능한 윈도우 길이를 매니페스트 값과 무관하게 강제로 덮어씁니다. 서버가 실제로는 더 긴 구간을 보관하는데 매니페스트에 짧게 표기된 경우 등에 사용합니다.
  • streaming.inaccurateManifestTolerance — 매니페스트가 알려주는 세그먼트 시각과 실제 미디어 사이의 오차를 어느 정도까지 허용할지를 정합니다. 저지연 환경에서 불필요한 보정 점프를 줄이려면 이 값을 조정하기도 합니다.

트레이드오프 한눈에 보기

라이브 설정은 결국 지연(latency) ↔ 안정성(stability) 사이의 줄다리기입니다.

설정지연을 줄이려면안정성을 높이려면
lowLatencyModetrue (LL-HLS 지원 시)false
bufferingGoal작게크게
liveSync.targetLatency작게크게
defaultPresentationDelay작게크게

정답은 콘텐츠와 시청 환경에 따라 다릅니다. 스포츠 중계처럼 실시간성이 중요하면 지연을 줄이는 쪽으로, 네트워크가 불안정한 환경에서 끊김 없는 시청이 우선이면 안정성 쪽으로 무게를 옮기면 됩니다.

마무리

라이브 스트리밍은 결국 끝나지 않는 타임라인을 다루는 일이었습니다. 핵심을 정리하면 다음과 같습니다.

  • 라이브는 매니페스트가 주기적으로 갱신되며, 세그먼트 윈도우가 슬라이딩한다. #EXT-X-ENDLIST의 부재가 라이브임을 선언한다.
  • HLS 미디어 플레이리스트의 #EXT-X-MEDIA-SEQUENCE 가 슬라이딩 윈도우의 기준이며, #EXT-X-PROGRAM-DATE-TIME 이 라이브를 벽시계 시각에 잇는다. LL-HLS의 #EXT-X-PART 는 세그먼트를 잘게 쪼개 지연을 줄인다.
  • Shaka의 파서는 갱신마다 세그먼트를 추가·제거하고 라이브 엣지availability window를 갱신한다.
  • 라이브 재생 품질은 lowLatencyMode, liveSync, bufferingGoal, defaultPresentationDelay 등의 설정으로 조정하며, 그 본질은 지연과 안정성의 트레이드오프다.

한 줄로 요약하면 다음과 같습니다.

"라이브 플레이어는 끊임없이 갱신되는 매니페스트를 따라 슬라이딩 윈도우 위를 달리며, 라이브 엣지에 얼마나 가까이 붙을지를 설정으로 조율한다."

라이브 재생은 결국 "끝나지 않는 타임라인을 따라가는 일"이고, 그 위에서 지연과 안정성을 어떻게 저울질하느냐가 시청 경험을 가릅니다. 매니페스트가 어떻게 갱신되고 윈도우가 어떻게 미끄러지는지를 이해하고 나면, 라이브에서 마주치는 끊김이나 지연 문제도 더 이상 블랙박스로 느껴지지 않을 것입니다.