Engineering/OS

[Architecture] 미디어 플레이어의 구조에 대한 고찰

MB Kyle K. 2025. 11. 17. 13:54
반응형

Background

처리해야 할 데이터가 많은 경우에는 멀티 코어 프로세서의 환경을 적극 활용하여, 최대한 가용한 리소스를 극한까지 사용해야 할 필요가 있다. 미디어 처리를 최대한 빠르게 처리하여 사용자에게 seemless 한 사용자 경험을 제공하는 것이다. 이를 위해서 여러 가지의 방법이 동원되게 된다. 그리고 소프트웨어 구조에 대해서 얘기하자면, 소프트웨어 구조는 확장성이 가능하도록 구현되어야 한다. 기본적으로 응집도를 높이고 결합도를 낮추라는 말을 많이 들었을 것이다.
 
예전부터 나는 이에 대해 어마무시하게 집중을 했다. 그리고 실제로 많이 중요하다. 실제로 회사에서 소속되어 개발을 하다 보면, 이를 무시하고 거대한 코드 뭉치를 만들어서 손도 못 대도록 구현할 때가 있다. 그리고 본인이 만든 코드 뭉치 또한 맘에 안 들어서 바꾸고 싶어 하면서도 해본 적 없던 코드 개선 작업에 엄두를 못 내는 사람들도 있다. 하지만 본인만의 레퍼런스와 경험이 있다면, 코드 개선은 생각보다 수월해질 수 있다.
 
기존 비즈니스 로직의 영향도를 고려하여 진행해야겠지만, 이에 대해서 조심만 하면 코드는 놀라울 정도의 확장성과 명백한 책임의 분리 구조를 갖게 된다. 무엇보다 중요한 것은 내가 어떤 아키텍처를 따르느냐보다는 내가 적절한 아키텍처를 선택하였느냐일 것이다. 여기서는 미디어 플레이어에 구현을 위한 구조에 대해서 말하고 싶다.
 

미디어 플레이어

Concept

미디어 플레이어는 사용자가 원하는 영상 혹은 오디오를 저장을 위한 압축 포맷에서 끊임없이 변환하여 재생을 위한 형태로 변환/제공해야한다. 이를 위해서 시스템에서 가용한 모든 리소스를 분배하여 극한까지 성능을 발휘해야 한다. 그리고 미디어를 변환하여 제공하는 측과 이를 재생하여 소비하는 측의 결합도를 낮춰야 한다. 이러한 콘셉트는 생각해 보면 단순하지만, 코드로 구현할 때에 고려하기 쉽지 않을 수 있다. 생각보다 비동기와 동기화에 대해서 어려워하는 사람도 있을뿐더러, 이를 깊이 있게 응용해 본 경험이 없으면 어려울 수 있다.
 
위에서 말했듯이 우리는 우선 제공자(Provider) / 소비자(Consumer) 패턴을 기반으로 구조를 만들어 나가면 된다. 네트워크에서 데이터를 생산(produce) 하는 컴포넌트(다운로더/컨버터)와 재생(디코딩/렌더링)하는 컴포넌트(플레이어)를 공유 버퍼(큐)로 분리해 독립적으로 동작시키고, 버퍼 상태에 따라 생산 속도를 조절(백프레셔) 하는 패턴입니다.
 

  • Downloader / Converter (Producer)
    • 네트워크에서 비디오/오디오를 조각(chunk) 단위로 요청/수신.
    • 네트워크 전송 및 저장을 위한 압축 포맷에서 재생을 위한 포맷으로 변환 (Co-Dec).
    • 수신한 조각을 변환 및 복호화 등을 거쳐서 버퍼에 푸시.
    • 각 조각의 변환 및 복호화 과정은 병렬 프로세싱으로 처리하여 하드웨어 리소스를 최대한 활용.
    • 실패/재시도, 우선순위(near-future segments 우선) 관리.
  • Buffer (Shared Queue / Storage)
    • 메모리 큐 또는 임시 파일(디스크)로 구현.
    • 큐는 순차 접근(FIFO). 읽기 인덱스와 쓰기 인덱스 유지.
    • 세마포어와 모니터 등으로 Producer와 Consumer의 접근 관리.
    • 상태: available, low (언더플로우 위험), high (오버플로우 위험).
  • Player (Consumer)
    • 버퍼에서 다음 조각을 꺼내 디코딩 -> 렌더링.
    • 재생 시점에 필요한 최소한의 프리버퍼(pre-roll) 기준을 가짐.
    • Gapless 등의 기술을 활용하여 프리버퍼 최소화 가능
  • Controller / Flow Controller
    • 버퍼 수위(level)를 모니터링하고, Downloader / Converter에게 일시정지/재개/속도조절(브레이킹)을 지시.
    • 네트워크 상태, 플레이어 재생 상태에 따라 정책 적용.
  • Cache Layer
    • 디스크 캐시: 재방문 시 재다운로드 방지.
    • Eviction policy: LRU 등.

 

구조

Producer (Downloader/Converter) - Consumer (Player) 구조도
[Player 요청 재생시점] 
    ↓
Controller: Downloader에 데이터 요청 시작
    ↓
Downloader / Converter (Producer) ──> Buffer (enqueue chunks)
    ↓                                   ↑
Player (Consumer)  <────────────────────┘ (dequeue 및 디코드/렌더)
  • 초기: pre-roll (예: 최소 2~3초 분량) 확보 후 재생 시작
  • 재생 중: Controller가 버퍼 수준을 모니터링하여 지속적으로 다운로더가 미래의 조각을 채우게 함
  • 네트워크 저하: Downloader는 재시도, 비트레이트 낮추기(ABR), 또는 더 큰 프리패치 시도 가능

 

버퍼 관리 전략

  • High water mark / Low water mark
    • highThreshold: 버퍼가 이 수준 이상이면 downloader 일시정지 (또는 속도 감소)
    • lowThreshold: 이 수준 이하이면 downloader 재개(또는 속도 증가)
  • 정책 예시
    • preRoll = 2s, low = 1s, high = 10s
    • 재생 시간 기준(초)이나 조각 개수 기준 사용
  • 우선순위 / 슬라이딩 윈도우
    • 플레이어가 곧 필요로 하는 구간(near-future)을 우선 다운로드
    • 멀티 해상도/멀티 비트레이트: 현재 네트워크에 맞춰 적절한 품질(segment) 선택 (ABR)

 

Conclusion

Downloader / Converter

다운로더는 네트워크로부터 영상 데이터를 작은 단위(chunk 또는 segment)로 받아오는 역할을 한다. 다운로더는 영상 파일 전체를 한 번에 다운로드하지 않고, 플레이어가 필요할 것으로 예상되는 이후 구간을 미리 앞서서 일정량 다운로드해 둔다. 다운로더는 영상 조각을 받을 때마다 해당 데이터를 변환 및 복호화 등을 거쳐서 즉시 버퍼에 넣는다.
 
이 과정에서 중요한 점은 다운로더가 네트워크 환경 변화에 대응하면서 언제 다운로드를 빠르게 진행할지 또는 언제 다운로드를 잠시 멈춰야 할지를 조절해야 한다는 것이다. 이를 위해 다운로더는 시스템의 “컨트롤러” 역할을 하는 상위 레이어로부터 신호를 받는다.
 
예를 들어:

  • 버퍼가 거의 가득 찼을 때는 다운로드를 멈추고
  • 버퍼가 바닥나기 시작하면 다시 다운로드를 재개한다.

이렇게 함으로써 다운로더는 버퍼 오버플로우(메모리 낭비)언더플로우(재생 끊김)를 모두 방지한다.
 

Buffer

버퍼는 다운로더와 플레이어를 연결하는 중간 저장소이자 여러 변동을 흡수하는 완충 장치의 역할을 한다. 이 버퍼는 일정 크기의 큐(queue) 형태로 구현되며, Producer는 데이터를 뒤에 추가하고, Consumer는 앞에서부터 데이터를 꺼내가는 구조를 가진다.
 
버퍼는 다음과 같은 상태값을 지속적으로 변화시키며 시스템 전체에 중요한 신호를 전달한다.

  • 버퍼가 비어 가는 경우: 플레이어는 곧 재생할 데이터가 부족해짐 → 다운로더는 즉시 다운로드를 실행해야 한다.
  • 버퍼가 너무 가득 차는 경우: 메모리 낭비 → 다운로더는 일시정지하거나 속도를 줄인다.
  • 버퍼가 안정적일 때: 다운로더와 플레이어는 자신의 속도대로 독립적으로 동작한다.

이 버퍼가 있기 때문에, 네트워크가 잠시 느려져도 당장 재생이 끊기지 않으며, 반대로 네트워크가 급격히 빨라져도 재생 속도보다 다운로드가 훨씬 빠르게 진행되는 문제를 방지할 수 있다.
 

Player

플레이어는 버퍼에서 데이터를 가져와 디코딩 → 렌더링 과정을 수행하여 실제 화면에 영상을 출력한다. 플레이어는 버퍼에서 데이터를 순차적으로 꺼내기 때문에, 버퍼에 일정량의 데이터가 확보되어 있다면 네트워크 상태와 상관없이 일정한 프레임을 유지하며 재생할 수 있다. 플레이어는 또한 재생 과정에서 현재 버퍼 잔량을 모니터링하여, 특정 임계값 이하로 떨어지면 “버퍼링 상태”로 전환할 수 있다.
 
이 과정에서 영상 재생을 잠시 중단하고, 다운로더가 다시 충분한 데이터를 채워 넣을 때까지 기다린다. 이 로직 덕분에 갑작스러운 끊김보다는 짧은 버퍼링으로 시청 경험을 개선할 수 있다.
 

Contoller

컨트롤러는 Producer와 Consumer 사이에서 정책을 수행하는 두뇌 역할을 한다.
컨트롤러는 다음과 같은 판단을 내린다.

  • 버퍼가 충분하면 다운로드를 늦춘다.
  • 버퍼가 부족하면 다운로드를 가속한다.
  • 필요에 따라 비트레이트를 낮춰 더 작은 조각을 받아오도록 한다(ABR: Adaptive Bitrate).
  • 다운로더나 플레이어의 상태를 관찰하여 오류나 지연을 감지한다.

컨트롤러가 있기 때문에 전체 시스템은 하나의 유기적인 단위로 동작하며, 네트워크 변화나 디바이스 성능 변화에 유연하게 대응할 수 있다.
 

Summary

전체 동작을 요약하면 다음과 같은 흐름으로 이해할 수 있다.

  1. 다운로더가 네트워크로부터 조각 단위 데이터를 받아 버퍼에 채운다.
  2. 버퍼는 Producer가 넣은 데이터와 Consumer가 소비하는 데이터를 연결하는 완충지 역할을 한다.
  3. 플레이어는 버퍼에서 데이터를 가져와 디코딩·재생한다.
  4. 컨트롤러는 버퍼 상태를 감시해 다운로드 속도나 재생 동작을 조절한다.

이 구조 덕분에 다운로드와 재생이 서로 독립적으로 동작하면서도, 서로의 상태를 고려해 안정적으로 영상을 재생할 수 있게 된다.
 
이 구조의 특징을 다시 정리하자면:

  • 네트워크가 순간적으로 느려져도 재생이 끊기지 않는다.
  • 다운로드 속도가 재생보다 빠를 때 불필요한 리소스 사용을 막는다.
  • 모바일 환경의 폭발적인 네트워크 변동에도 안정적인 재생 경험을 제공한다.
  • 영상 품질을 동적으로 조절하는 ABR 시스템과 쉽게 결합된다.
  • 디코더나 네트워크가 일시적으로 멈추어도 시스템 전체가 흔들리지 않는다.

즉, Producer–Consumer + 버퍼링 구조는 스트리밍 서비스의 기본 뼈대이며, 대부분의 미디어를 처리하기 위해서 사용되는 구조이다.

반응형

'Engineering > OS' 카테고리의 다른 글

[OS : Open webOS] webOS is opened!!  (0) 2012.09.02