Skip to content

Adaptive Bitrate Streaming

ABR(Adaptive Bitrate Streaming)은 네트워크 상태에 따라 동영상의 비트레이트(화질)를 실시간으로 조절하는 스트리밍 기술이다.

플레이어는 동영상을 하나의 연속된 파일로 받는 것이 아니라, 짧은 세그먼트 단위로 나눠서 받는다. 매 세그먼트마다 “지금 네트워크 상태에서 어떤 화질을 받아야 끊김 없이 최고 화질을 유지할 수 있는가”를 판단하는 것이 ABR의 핵심이다.

기본 구조

ABR 시스템은 서버 측 준비와 클라이언트 측 결정으로 나뉜다.

서버는 하나의 영상을 여러 비트레이트(예: 400kbps, 800kbps, 1.5Mbps, 3Mbps, 6Mbps)로 인코딩하고, 각 버전을 짧은 세그먼트(보통 2~6초)로 분할해 둔다. 그리고 이 정보를 매니페스트 파일로 제공한다.

  • HLS (HTTP Live Streaming): Apple이 개발한 방식으로 .m3u8 매니페스트를 사용한다. 마스터 플레이리스트가 각 비트레이트별 미디어 플레이리스트를 가리키는 2단 구조이다
  • DASH (Dynamic Adaptive Streaming over HTTP): 국제 표준(ISO 23009-1)으로 .mpd 매니페스트를 사용한다. AdaptationSet 안에 여러 Representation(비트레이트 레벨)이 정의된다

클라이언트(플레이어)는 매니페스트를 받아 사용 가능한 비트레이트 목록, 각 세그먼트의 URL, 코덱과 해상도 정보를 파악한다. 이 목록을 “비트레이트 래더(ladder)“라고 부르며, 매 세그먼트마다 이 래더에서 하나를 고르는 것이 플레이어의 역할이다.

세그먼트 길이는 ABR 성능에 직접적인 영향을 준다. 짧으면(2초) 비트레이트 전환 반응이 빠르지만 요청 오버헤드가 늘어나고, 길면(10초) 네트워크 변화에 대한 반응이 느려진다. 대부분의 서비스는 4~6초를 절충점으로 사용한다.

플레이어 내부 파이프라인

플레이어 내부는 크게 세 모듈이 순환하며 동작한다.

  1. Bandwidth Estimator — 과거 다운로드 이력으로 가용 대역폭을 추정한다
  2. ABR Controller — 추정값과 버퍼 상태를 보고 다음 세그먼트의 비트레이트를 결정한다
  3. Scheduling Engine — 언제 다음 세그먼트를 요청할지 타이밍을 제어한다

매 세그먼트 다운로드가 끝날 때마다 이 루프가 한 번 돈다. 각 모듈의 판단이 서로 영향을 주기 때문에 세밀한 튜닝이 필요하다. Bandwidth Estimator가 과대추정하면 ABR Controller가 높은 비트레이트를 선택하고, 다운로드가 느려지면 버퍼가 줄어들며, Scheduling Engine이 더 급하게 다음 요청을 보내는 연쇄 효과가 발생한다.

대역폭 추정 (Bandwidth Estimation)

다음 세그먼트의 비트레이트를 결정하려면 현재 네트워크가 얼마나 빠른지 알아야 한다. 네트워크 대역폭은 직접 측정할 수 없고, 과거 다운로드 결과로부터 추정할 수밖에 없다.

기본 throughput 측정

세그먼트 하나를 다운로드한 뒤 실측 throughput을 계산한다.

throughput = segment_size_bits / download_time_seconds

download_time에는 TCP slow start 구간이 포함되므로 실제 가용 대역폭보다 낮게 측정될 수 있다. 세그먼트가 클수록 slow start의 영향이 줄어들어 추정이 정확해진다.

EWMA (Exponentially Weighted Moving Average)

가장 널리 쓰이는 smoothing 방식이다. 단순 이동평균과 달리 최근 값에 더 큰 가중치를 부여한다.

B_est = α × B_last + (1 - α) × B_est_prev
  • B_last = 직전 세그먼트의 실측 throughput
  • α = 가중치 (보통 0.4~0.6). 클수록 최근 값에 민감하다

구체적인 예시로 살펴보자. α=0.5일 때:

세그먼트 1: 4.0 Mbps → B_est = 4.0 (초기값)
세그먼트 2: 3.0 Mbps → B_est = 0.5 × 3.0 + 0.5 × 4.0 = 3.5
세그먼트 3: 5.0 Mbps → B_est = 0.5 × 5.0 + 0.5 × 3.5 = 4.25
세그먼트 4: 1.0 Mbps → B_est = 0.5 × 1.0 + 0.5 × 4.25 = 2.625
세그먼트 5: 1.5 Mbps → B_est = 0.5 × 1.5 + 0.5 × 2.625 = 2.0625

세그먼트 4에서 대역폭이 급락했지만, EWMA는 2.625로 완충해준다. 직전 값만 썼다면 1.0Mbps로 과도하게 반응하고, 단순 평균을 썼다면 3.25로 과대추정했을 것이다.

Dual EWMA

dash.js 등에서는 반응 속도가 다른 두 추정기를 병행한다.

  • Fast EWMA (α=0.5): 빠른 변화 감지용
  • Slow EWMA (α=0.9): 안정적 추세 파악용
  • 두 값 중 더 작은 쪽을 채택해 보수적으로 추정한다

왜 보수적으로 잡을까? 대역폭을 과대추정하면 고비트레이트 세그먼트를 요청하게 되고, 다운로드가 재생 속도를 못 따라가서 버퍼가 고갈(rebuffering)된다. 사용자 경험 연구에 따르면 rebuffering 1회는 평균 화질을 한 단계 낮추는 것보다 체감 만족도에 훨씬 나쁜 영향을 준다. 약간 낮은 화질로 안정적으로 재생하는 것이 가끔 높은 화질을 보여주다가 끊기는 것보다 낫다.

비트레이트 결정 알고리즘

이 부분이 ABR의 핵심이다. 크게 세 가지 계열이 있다.

Throughput-based (처리량 기반)

가장 직관적인 방식이다. 추정된 대역폭에 안전 마진을 곱하고, 그 이하인 가장 높은 비트레이트를 선택한다.

선택 비트레이트 = max(R_i) where R_i ≤ B_est × safety_factor
  • safety_factor는 보통 0.7~0.9이다
  • 예를 들어 래더가 [400k, 800k, 1.5M, 3M, 6M]이고, 추정 대역폭이 5Mbps, safety_factor가 0.85이면 임계값은 4.25Mbps이다. 4.25 이하인 가장 높은 레벨인 3Mbps를 선택한다

화질 진동(oscillation)을 방지하기 위해 switch-up 규칙을 두는 경우가 많다. 비트레이트를 올릴 때는 연속 N개 세그먼트 동안 상위 레벨의 대역폭을 유지해야만 올리고(보수적), 내릴 때는 즉시 내린다(공격적).

단점은 대역폭 추정이 부정확하면 바로 잘못된 선택을 한다는 것이다.

Buffer-based (버퍼 기반) — BOLA 알고리즘

대역폭을 추정하지 않고, 현재 버퍼 수위만으로 결정하는 방식이다. 핵심 아이디어는 버퍼 수위가 “지금까지의 네트워크 상태를 종합한 결과물”이라는 점이다. 대역폭이 좋으면 버퍼가 자연스럽게 쌓이고, 나쁘면 줄어든다.

구체적으로 두 임계값을 설정한다.

  • buffer_low (예: 10초) — 이 이하면 최저 비트레이트
  • buffer_high (예: 30초) — 이 이상이면 최고 비트레이트
  • 그 사이에서는 선형 보간(linear interpolation)으로 비트레이트를 매핑한다

장점은 대역폭 추정이라는 불확실한 과정을 건너뛴다는 것이다 (Robust해짐).

Throughput-based 방식은 “대역폭이 5Mbps일 것이다”라고 추정하고 그에 맞는 비트레이트를 고르는데, TCP competing flow나 일시적 지연 등으로 추정이 틀리면 바로 문제가 생긴다. 반면 BOLA는 버퍼 수위라는 직접 관측 가능한 값만 보고 판단한다. 대역폭이 떨어지면 버퍼가 자연스럽게 줄어들고, 줄어든 버퍼를 보고 비트레이트를 낮추므로 추정 오류가 끼어들 여지가 없다.

단점은 startup 단계에서 버퍼가 아직 충분히 쌓이지 않아 불필요하게 낮은 비트레이트에 머무를 수 있다는 점이다. 이 때문에 실제 구현(BOLA-E)에서는 startup 구간에서만 throughput-based를 쓰고, 버퍼가 일정 수준 이상 쌓이면 BOLA로 전환한다.

Hybrid (혼합형) — MPC (Model Predictive Control)

실제 프로덕션 플레이어 대부분이 이 방식을 사용한다. 향후 N개 세그먼트에 대해 최적 비트레이트 시퀀스를 탐색한다.

score = Σ quality(R_i) ← 화질 최대화
- μ × Σ |quality(R_i) - quality(R_{i-1})| ← 화질 급변 방지
- λ × Σ rebuffer_time(R_i) ← 버퍼링 최소화

이 식은 세 가지 목표의 합산 점수이다. 가능한 모든 비트레이트 조합에 대해 이 점수를 계산하고, 가장 높은 조합을 선택한다.

  • Σ quality(R_i): 선택한 비트레이트들의 화질 합이다. quality는 보통 log(R_i)를 쓴다. 로그를 쓰는 이유는 인간의 화질 인식이 비트레이트에 대해 로그적이기 때문이다 (1→2Mbps의 체감 차이가 5→6Mbps보다 크다). 이 항만 있으면 무조건 최고 비트레이트를 고른다
  • μ × Σ |quality(R_i) - quality(R_{i-1})|: 연속된 세그먼트 간 화질 차이의 합이다. 3M→6M→1.5M처럼 급변하면 이 값이 커져서 전체 점수를 깎는다. μ가 클수록 안정적인 화질을 선호한다
  • λ × Σ rebuffer_time(R_i): 재생이 멈추는 시간의 합이다. 세그먼트 다운로드 시간(segment_size / B_est)이 현재 버퍼 잔량보다 길면, 그 초과분만큼 rebuffering이 발생한다. λ가 클수록 끊김 방지를 우선시한다

예시로 이해해보자. 추정 대역폭 4Mbps, 버퍼 12초, 세그먼트 4초일 때 (μ=1, λ=3):

조합 A [3M, 3M, 3M]: 화질 3.30, 변동 0, rebuffer 0 → score 3.30
조합 B [6M, 6M, 6M]: 화질 5.37, 변동 0, rebuffer 0 → score 5.37
조합 C [3M, 6M, 1.5M]: 화질 3.30, 변동 2.08, rebuffer 0 → score 1.22

조합 B가 최고 점수이다. 하지만 대역폭이 3Mbps로 떨어지면 6M 세그먼트의 다운로드 시간이 8초로 늘어나 버퍼가 고갈되고, rebuffer 패널티가 커지면서 조합 A가 역전한다. 이처럼 MPC는 상황에 따라 균형점이 자동으로 달라진다

lookahead window는 보통 5개 세그먼트이고, 비트레이트 레벨이 5개라면 탐색 공간은 5^5 = 3,125으로 실시간에 풀 수 있다. μ, λ는 서비스 특성에 맞게 조정하며, 라이브 중계처럼 끊김이 치명적인 경우 λ를 크게, 영화 스트리밍처럼 화질이 중요한 경우 μ를 작게 설정한다.

MPC은 향후 대역폭이 일정하다고 가정한다는 한계가 있다. (MIT에서 개발한 Pensieve는 강화학습으로 대역폭 패턴을 학습해 이 한계를 극복한다.)

버퍼 관리와 스케줄링

Startup Phase (초기 구간)

재생 시작 시에는 버퍼가 비어 있으므로 특별한 전략을 쓴다.

  • 최저 비트레이트로 시작해 빠르게 첫 세그먼트를 가져온다
  • 버퍼가 startup_threshold (보통 2~4초)에 도달하면 재생을 시작한다
  • 이후 급속히 비트레이트를 올린다 (한 번에 최대 1~2단계씩)
segment 1: 400kbps (즉시 요청)
segment 2: 800kbps (대역폭 추정 시작)
segment 3: 1.5Mbps (추정값 반영)
segment 4: 3Mbps (안정화)

세그먼트 1~2개의 throughput만으로는 대역폭 추정이 불안정하기 때문에, ramp-up 속도에 제한을 두는 것이 중요하다. 비트레이트가 ceiling에 도달하거나 대역폭 제한에 걸리면 startup을 종료하고 steady state로 전환한다.

Steady State (정상 구간)

  • 세그먼트 다운로드가 끝나면 즉시 다음 세그먼트를 요청한다
  • 단, 버퍼가 max_buffer (보통 30~60초)에 도달하면 요청을 지연시킨다
  • 사용자가 seek하면 버퍼가 무효화되므로 과도한 선행 버퍼링은 낭비이다

seek이 발생하면 기존 버퍼를 폐기하고 seek된 위치부터 다시 요청한다. 이미 대역폭 추정값이 있으므로 최저 비트레이트부터 시작하지 않고, 현재 추정값 기반으로 비트레이트를 선택하되 첫 1~2개 세그먼트는 한 단계 낮게 보수적으로 시작한다.

Rebuffer 방어

버퍼가 위험 수준으로 떨어지면 비상 모드에 진입한다.

  • panic_threshold (예: 5초) 이하면 ABR 알고리즘 판단을 무시하고 즉시 최저 비트레이트로 전환한다
  • 일부 플레이어는 재생 속도를 미세하게 올린다 (1.02~1.05x). 4초 세그먼트를 1.03x로 재생하면 매 세그먼트마다 약 0.12초의 버퍼 여유를 얻는다
  • 다운로드 중인 고비트레이트 세그먼트를 abort하고 낮은 비트레이트로 재요청할 수도 있다

rebuffering이 실제로 발생하면 버퍼가 recovery_threshold (보통 5~10초)까지 쌓일 때까지 재생을 멈추고, recovery 후에도 한동안 보수적인 비트레이트를 유지하다가 점진적으로 올린다.

세그먼트 Prefetch 최적화

단순히 순서대로 가져오는 것 외에도 여러 최적화 기법이 있다.

  • Parallel downloading: HTTP/2 환경에서 다음 2~3개 세그먼트를 동시에 요청한다. 네트워크 유휴 시간을 줄이지만, 비트레이트를 미리 결정해야 하므로 중간에 네트워크가 변해도 대응할 수 없는 트레이드오프가 있다
  • Partial segment abort: 다운로드 중에 대역폭이 급락하면, 진행 중인 고비트레이트 세그먼트를 취소하고 낮은 비트레이트로 재요청한다. 예를 들어 4초 세그먼트 다운로드가 8초 이상 걸릴 것으로 예측되면 abort한다
  • CMAF low-latency chunked transfer: 세그먼트를 chunk 단위(200~500ms)로 나눠서 전송한다. 세그먼트가 서버에서 완전히 생성되기 전에 받기 시작할 수 있어 라이브 스트리밍 지연을 세그먼트 길이(6초)에서 chunk 길이(0.5초)까지 줄인다
  • Bandwidth probing: 버퍼가 충분히 쌓여 있을 때, 의도적으로 한 단계 높은 비트레이트를 요청해 본다. 성공하면 비트레이트를 올리고, 실패하면 복귀한다

참고