Android Media Player는 어떻게 서버의 영상 URL을 재생할까?

easyhooon·2026년 7월 15일
post-thumbnail

부제: Media3/ExoPlayer의 HTTP Range, HLS 세그먼트, 인증 헤더와 버퍼링의 원리

서두

Android에서 서버의 영상을 재생할 때는 보통 API를 통해 영상 URL을 전달받는다.

{
  "videoUrl": "https://cdn.example.com/videos/42/master.m3u8"
}

.m3u8 확장자 파일은 영상 바이트가 직접 들어 있는 파일이 아니라 HLS에서 사용하는 재생 목록 파일이며, Player는 이 Playlist를 먼저 읽고 그 안에 적힌 화질별 Playlist와 Segment URL을 따라가며 실제 미디어 데이터를 요청한다.

그리고 전달받은 URL을 MediaItem으로 만들어 Player에 넘긴다.

val mediaItem = MediaItem.fromUri(videoUrl)

player.setMediaItem(mediaItem)
player.prepare()
player.play()

앱에서 작성하는 코드는 사실상 이게 전부다.

그런데 영상을 재생해보면 전체 파일이 다운로드될 때까지 기다리지 않아도 곧바로 재생이 시작된다.

네트워크 속도가 달라지면 화질이 바뀌기도 하고, 재생 바를 영상 중간으로 이동하면 해당 위치부터 다시 재생된다.

조금 생각해보면 이상하다.

Player는 URL 하나만 전달받았는데, 영상의 어느 부분을 받아야 하는지 어떻게 알고 있을까?

스트리밍은 파일을 한 번에 전부 다운로드하지 않고 작은 데이터 단위로 끊어서 받는다고 알고 있다.

그렇다면 데이터를 한 조각 받을 때마다 새로운 HTTP 요청이 발생하는 걸까?

각 요청에는 기존 REST API처럼 Authorization 헤더가 들어갈 수 있을까?

데이터 단위의 크기와 요청 주기는 클라이언트와 서버 중 누가 결정하는 걸까?

이 글에서는 Android Media3의 ExoPlayer가 서버의 영상 URL을 어떤 과정으로 읽고, 버퍼링하고, 재생하는지 하나씩 살펴본다.

본론

우선, 결론부터 말하면 모든 스트리밍이 같은 방식으로 데이터를 나누어 요청하는 것은 아니다.

일반 MP4 파일을 재생하는 Progressive 방식과 HLS/DASH 같은 Adaptive Streaming 방식은 서버에 요청하는 형태부터 다르다.

Progressive Streaming은 하나의 미디어 파일을 앞에서부터 읽거나 필요한 바이트 구간을 요청한다.

HLS/DASH는 Manifest에 기록된 여러 Media Segment를 개별 HTTP 요청으로 가져온다.

두 방식 모두 HTTP를 사용할 수 있지만, 무엇을 하나의 요청 단위로 보는지는 다르다.

전체 흐름을 먼저 정리하면 대략 이렇다.

앱
  ↓ 재생 정보 API 요청
Playback API
  ↓ 영상 URL, 만료 시간, DRM 정보 반환
Media3 Player
  ↓ URL과 미디어 형식을 분석
MediaSource
  ↓ 필요한 데이터 범위 또는 Segment 결정
DataSource
  ↓ HTTP 요청
CDN / Origin Server
  ↓ 미디어 바이트 응답
Player Buffer
  ↓ Extractor / Decoder
화면과 스피커로 재생

영상 URL도 엔드포인트다

먼저 URL에 대한 오해부터 정리해보자.

스트리밍에는 REST API와 같은 엔드포인트가 없고, 단순한 파일 경로만 존재한다고 생각할 수 있다.

하지만 Player에 전달하는 URL 역시 HTTP Resource를 가리키는 엔드포인트다.

https://cdn.example.com/videos/movie.mp4
https://cdn.example.com/videos/master.m3u8
https://cdn.example.com/videos/720p/segment-001.m4s

첫 번째 URL은 MP4 파일이라는 하나의 Resource를 가리킨다.

두 번째 URL은 HLS Playlist를, 세 번째 URL은 Playlist에 포함된 Media Segment를 가리킨다.

즉 스트리밍에 URL이 없는 것이 아니다.

JSON 대신 미디어 파일이나 Manifest를 응답하고, 재생 중 여러 Resource를 연속해서 요청한다는 점이 일반적인 API 통신과 다를 뿐이다.

Player에 URL을 넘기면 어떤 컴포넌트가 요청할까?

MediaItem은 재생할 미디어에 대한 정보다.

val mediaItem = MediaItem.fromUri(videoUrl)

Player는 이 정보를 바탕으로 적절한 MediaSource를 만든다.

Media3의 DefaultMediaSourceFactory는 URL과 MIME type 등의 정보를 보고 일반 파일인지, HLS인지, DASH인지 판단한다.

Signed URL처럼 확장자만으로 형식을 판단하기 어렵다면 MediaItem에 MIME type을 명시해 MediaSource 선택을 도울 수 있다.

일반 MP4 / MP3 -> ProgressiveMediaSource
HLS            -> HlsMediaSource
DASH           -> DashMediaSource

실제 데이터를 읽는 역할은 DataSource가 담당한다.

HTTP URL이라면 HttpDataSource가 서버에 요청하고, 로컬 파일이나 content:// URI라면 각각 해당 scheme을 처리할 수 있는 다른 DataSource가 사용된다.

MediaItem
  ↓
MediaSource
  ↓ 어떤 데이터를 언제 로드할지 결정
DataSource
  ↓ 실제 HTTP 연결과 데이터 읽기
Server

즉 앱에서 Retrofit으로 영상 바이트를 직접 받아 Player에 반복해서 넣는 구조가 아니다.

앱은 재생 URL을 Player에 전달하고, 이후 미디어 데이터 요청은 Player 내부의 MediaSource와 DataSource가 수행한다.

Progressive Streaming은 파일을 어떻게 나누어 받을까?

먼저 MP4나 MP3 파일 하나를 직접 재생하는 경우를 살펴보자.

https://cdn.example.com/videos/movie.mp4

이 방식은 일반적으로 Progressive Streaming 또는 Progressive Download라고 부른다.

서버는 하나의 파일을 제공하고, Player는 파일의 앞부분부터 읽으면서 확보된 데이터로 재생을 시작한다.

여기서 중요한 점은 Player가 내부적으로 데이터를 여러 번 읽는다고 해서 매번 새로운 HTTP 요청이 발생하는 것은 아니라는 점이다.

하나의 HTTP 연결을 연 뒤 응답 Body를 계속 읽을 수 있다.

Player Buffer가 충분히 채워지면 추가 로딩을 잠시 멈추고, 재생으로 Buffer가 줄어들면 다시 데이터를 읽을 수 있다.

GET /videos/movie.mp4

200 OK
Content-Length: 50000000
Content-Type: video/mp4

[응답 Body를 순서대로 읽음]

그렇다면 영상 중간으로 Seek할 때는 어떻게 할까?

이때 사용할 수 있는 것이 HTTP의 Range 요청이다.

GET /videos/movie.mp4
Range: bytes=25000000-

서버가 Byte Range를 지원한다면 요청한 구간에 대해 206 Partial Content로 응답한다.

206 Partial Content
Accept-Ranges: bytes
Content-Range: bytes 25000000-49999999/50000000
Content-Length: 25000000

이 요청은 전체 50MB 파일 중 25MB 위치부터 마지막까지 보내달라는 뜻이다.

Player는 MP4 Container의 Seek 정보와 현재 재생 위치를 이용해 필요한 바이트 위치를 찾고, 해당 지점부터 새로운 연결을 열 수 있다.

사용자가 10분 위치로 Seek
  ↓
Extractor가 재생 시각에 대응하는 파일 위치 계산
  ↓
기존 로딩 취소 또는 연결 재구성
  ↓
Range: bytes=N-
  ↓
해당 위치부터 Buffer 채우기

다만 모든 최초 요청이 반드시 Range 요청인 것은 아니다.

처음부터 순서대로 재생한다면 일반 GET으로 응답을 읽을 수도 있고, Seek나 재연결처럼 파일의 특정 위치가 필요할 때 Range 요청이 사용될 수 있다.

또한 HTTP 표준상 서버는 Range 요청을 지원하지 않거나 무시할 수도 있다.

따라서 Progressive Streaming에서의 요청 횟수는 정해져 있지 않다.

첫 연결을 얼마나 유지했는지, Seek가 발생했는지, 네트워크가 끊겼는지, 재시도가 발생했는지에 따라 달라진다.

HLS와 DASH는 진짜 Segment 단위로 요청한다

HLS와 DASH는 하나의 거대한 파일을 순서대로 읽는 방식과 다르다.

미디어를 몇 초 분량의 Media Segment로 미리 나누고, Segment의 위치와 품질 정보를 Manifest에 기록한다.

HLS에서는 보통 .m3u8 Playlist를 사용한다.

master.m3u8
  ├─ 360p/playlist.m3u8
  ├─ 720p/playlist.m3u8
  └─ 1080p/playlist.m3u8

Master Playlist에는 선택 가능한 화질과 Bitrate 정보가 들어간다.

#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360
360p/playlist.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720
720p/playlist.m3u8

각 품질의 Media Playlist에는 실제 Segment URL과 재생 시간이 기록된다.

#EXTM3U
#EXT-X-TARGETDURATION:6
#EXT-X-MAP:URI="init.mp4"

#EXTINF:6.0,
segment-001.m4s

#EXTINF:6.0,
segment-002.m4s

#EXTINF:6.0,
segment-003.m4s

Player의 요청 흐름은 대략 다음과 같다.

GET /master.m3u8
  ↓ 사용 가능한 품질 확인
GET /720p/playlist.m3u8
  ↓ Segment 목록 확인
GET /720p/segment-001.m4s
GET /720p/segment-002.m4s
GET /1080p/segment-003.m4s

네트워크 상태가 좋아지면 다음 Segment부터 더 높은 품질을 선택할 수 있다.

반대로 다운로드 속도가 느려져 Buffer가 부족해질 가능성이 커지면 낮은 품질의 Segment를 선택한다.

현재 대역폭 추정치
        +
현재 Buffer에 남은 재생 시간
        +
선택 가능한 품질별 Bitrate
        ↓
다음에 요청할 Track 선택

이것이 Adaptive Bitrate Streaming, 줄여서 ABR의 기본 원리다.

DASH도 XML 기반의 MPD를 사용한다는 차이가 있지만, Manifest를 읽고 여러 품질의 Media Segment를 선택한다는 큰 흐름은 비슷하다.

둘의 차이를 간단히 정리하면 다음과 같다.

구분HLSDASH
Manifest 형식.m3u8 Playlist.mpd MPD
Manifest 포맷텍스트 기반 PlaylistXML
대표 MediaSourceHlsMediaSourceDashMediaSource
Segment 표현Media Playlist에 Segment URI와 EXTINF 재생 시간 기록MPD의 Period, AdaptationSet, Representation, Segment 정보로 표현
품질 선택Variant Stream 중 다음 Segment 품질 선택Representation 중 다음 Segment 품질 선택
공통점Manifest를 읽고 Segment를 HTTP 요청으로 가져오며 ABR로 품질을 조정Manifest를 읽고 Segment를 HTTP 요청으로 가져오며 ABR로 품질을 조정

즉 HLS와 DASH는 Manifest 문법과 생태계는 다르지만, Player가 Manifest를 읽고 다음에 받을 Segment와 품질을 결정한다는 점에서는 같은 계열의 Adaptive Streaming 방식이다.

Segment가 6초라면 6초마다 요청할까?

여기서 처음의 질문으로 돌아가보자.

6초짜리 Segment로 구성된 영상은 6초마다 서버에 요청할까?

꼭 그렇지는 않다.

Segment의 재생 시간과 HTTP 요청 간격은 같은 값이 아니다.

예를 들어 6초 분량의 Segment 하나를 1초 만에 다운로드할 수 있다고 가정해보자.

재생을 시작할 때 Player Buffer가 비어 있으므로 Player는 여러 Segment를 연속으로 요청해 먼저 Buffer를 채울 수 있다.

0초: segment-001 다운로드
1초: segment-002 다운로드
2초: segment-003 다운로드
3초: segment-004 다운로드
...
Buffer 목표치 도달
추가 로딩 잠시 중단

이후 재생으로 Buffer가 줄어들면 다음 Segment를 요청한다.

Buffer가 부족함 -> 다음 Segment 로드
Buffer가 충분함 -> 로딩 대기

따라서 Player는 단순히 6초마다 요청 같은 Timer만 사용하지 않는다.

현재 확보한 재생 시간, 목표 Buffer량, 네트워크 속도, 재생 위치, Live Edge, 재시도 상태 등을 바탕으로 추가 로딩이 필요한지 결정한다.

Media3에서는 LoadControl이 언제 추가 데이터를 Buffer에 적재할지와 어느 정도까지 Buffer를 확보할지 결정하는 역할을 한다.

라이브 HLS라면 Segment뿐만 아니라 Playlist도 반복해서 갱신해야 한다.

아직 만들어지지 않은 미래의 Segment URL은 Playlist에 없기 때문이다.

Media Playlist 요청
  ↓ 현재 공개된 Segment 확인
새 Segment 다운로드
  ↓ 재생
Playlist 갱신
  ↓ 새로 추가된 Segment 확인

이 갱신 시점 역시 HLS의 Target Duration, Low-Latency HLS 기능, 서버가 제공하는 Playlist 정보와 Player 구현에 따라 달라진다.

Segment와 Chunk는 같은 말일까?

스트리밍을 이야기할 때 가장 헷갈리는 부분은 SegmentChunk를 같은 뜻처럼 섞어 쓰기 쉽다는 점이다.

둘 다 작은 조각처럼 들리지만, 실제로는 가리키는 계층이 다르다.

이 글에서는 HLS/DASH Manifest에 기록된 미디어 조각을 Segment라고 부르고, Chunk는 일반적인 조각이라는 표현이거나 HTTP/1.1 Chunked Transfer를 말할 때만 사용한다.

구분의미새로운 HTTP 요청인가?
HLS/DASH Media Segment몇 초 분량으로 나눈 미디어 Resource일반적으로 Segment마다 요청
HTTP Byte Range하나의 Resource에서 요청한 바이트 구간Range 요청마다 요청
HTTP/1.1 Chunked Transfer길이를 모르는 HTTP Body를 전송하기 위한 FramingChunk마다 새 요청이 아님
Player 내부 BufferPlayer가 메모리에 보관하고 읽는 데이터 단위Buffer read마다 새 요청이 아님

특히 HTTP/1.1의 Transfer-Encoding: chunked와 HLS Media Segment는 전혀 다른 개념이다.

HTTP/1.1 200 OK
Transfer-Encoding: chunked

Chunked Transfer Coding은 하나의 HTTP 응답 Body를 크기가 표시된 여러 조각으로 전송하는 Message Framing 방식이다.

HTTP Chunk 하나를 받을 때마다 클라이언트가 새로운 GET을 보내는 것이 아니다.

하나의 HTTP Request
  ↓
하나의 HTTP Response
  ├─ transfer chunk 1
  ├─ transfer chunk 2
  └─ transfer chunk 3

반면 HLS의 Media Segment는 Playlist에서 각각 별도의 URI로 표현되는 Resource다.

GET segment-001.m4s
GET segment-002.m4s
GET segment-003.m4s

따라서 스트리밍은 청크로 받는다는 표현만으로는 실제 네트워크 요청 방식을 설명하기 어렵다.

HLS/DASH의 Segment를 말하는지, MP4 파일의 Byte Range를 말하는지, HTTP 응답 내부의 transfer chunk를 말하는지 먼저 구분해야 한다.

Segment의 크기는 누가 정할까?

HLS/DASH의 Segment 경계는 일반적으로 서버의 Encoder와 Packager가 미리 정한다.

원본 영상
  ↓ Encoder
여러 Bitrate와 해상도로 인코딩
  ↓ Packager
일정한 재생 시간 단위로 Segment 생성
  ↓
Manifest + Media Segment 배포

Segment의 실제 바이트 크기는 고정되지 않을 수 있다.

재생 시간과 Bitrate로 대략적인 크기를 계산하면 다음과 같다.

Segment 크기 ≈ Bitrate × Segment 재생 시간 ÷ 8

예를 들어 4Mbps 영상의 6초 Segment라면 대략 3MB다.

4,000,000 bit/s × 6s ÷ 8
= 약 3,000,000 byte

다만 VBR 인코딩에서는 장면의 복잡도에 따라 같은 6초 Segment라도 실제 크기가 달라질 수 있다.

클라이언트가 서버에 이번 Segment는 500KB로 잘라서 보내줘라고 요청하는 구조는 일반적인 HLS/DASH 방식이 아니다.

클라이언트는 대신 다음 Segment의 낮은 Bitrate Track을 선택해 다운로드할 데이터 크기를 간접적으로 줄일 수 있다.

서버와 클라이언트의 설정 범위를 정리하면 다음과 같다.

항목주로 결정하는 쪽
Segment 재생 시간서버 Encoder / Packager
제공할 해상도와 Bitrate서버
Keyframe과 Segment 경계서버 Encoder / Packager
Range 요청 지원서버 / CDN
다음에 받을 품질클라이언트 ABR
목표 Buffer량클라이언트 LoadControl
Timeout과 재시도 정책클라이언트
Cache 정책클라이언트와 서버 / CDN
인증 Header 또는 Signed URL클라이언트와 서버 / CDN

여기서 Player Buffer를 크게 설정한다고 서버의 Segment 크기가 커지는 것은 아니다.

Segment는 전송할 미디어 Resource의 경계이고, Buffer는 클라이언트가 미리 보관할 데이터의 양이다.

미디어 요청에도 Authorization 헤더를 넣을 수 있을까?

넣을 수 있다.

미디어 요청도 HTTP 요청이므로 Authorization, Cookie와 서비스 전용 Header를 사용할 수 있다.

Media3에서는 HttpDataSource.Factory에 기본 Request Header를 설정할 수 있다.

val httpDataSourceFactory =
    DefaultHttpDataSource.Factory()
        .setDefaultRequestProperties(
            mapOf(
                "Authorization" to "Bearer $authToken",
            )
        )

val mediaSourceFactory =
    DefaultMediaSourceFactory(context)
        .setDataSourceFactory(httpDataSourceFactory)

val player =
    ExoPlayer.Builder(context)
        .setMediaSourceFactory(mediaSourceFactory)
        .build()

이렇게 설정한 Header는 해당 DataSource가 만드는 HTTP 요청에 포함된다.

HLS라면 Master Playlist, Media Playlist, Segment 등 재생 과정에서 발생하는 여러 요청에 Header가 전달될 수 있다.

GET /master.m3u8
Authorization: Bearer ...

GET /720p/playlist.m3u8
Authorization: Bearer ...

GET /720p/segment-001.m4s
Authorization: Bearer ...

다만 재생 시간이 긴 영상에서는 토큰 만료도 생각해야 한다.

Player를 생성할 때 넣은 토큰이 재생 도중 만료되면, 이후 새 Segment나 Range 요청에서 401 Unauthorized가 발생할 수 있다.

요청 직전에 최신 Header를 계산해야 한다면 ResolvingDataSource를 사용할 수 있다.

val dataSourceFactory =
    ResolvingDataSource.Factory(httpDataSourceFactory) { dataSpec ->
        dataSpec.withRequestHeaders(
            mapOf(
                "Authorization" to "Bearer ${tokenProvider.currentToken()}",
            )
        )
    }

이 방식은 각 HTTP Source와 상호작용하기 직전에 현재 URI와 최신 토큰을 기준으로 Header를 구성할 수 있다.

실무에서는 Bearer Token을 모든 요청에 넣는 방식만 사용하는 것은 아니다.

Playback API가 짧은 만료 시간을 가진 Signed URL을 반환하거나, CDN에서 검증할 수 있는 Signed Cookie를 사용할 수도 있다.

{
  "playbackUrl": "https://cdn.example.com/master.m3u8?expires=...&signature=...",
  "expiresAt": "2026-07-15T13:00:00Z"
}
Bearer Header
  -> 각 요청의 Authorization을 Origin 또는 CDN이 검증

Signed URL
  -> URL의 만료 시간과 서명을 CDN이 검증

Signed Cookie
  -> Cookie에 포함된 권한 정보를 CDN이 검증

어떤 방식이 적합한지는 CDN 구성, Cache 전략, URL 공유 방지, 토큰 갱신 정책에 따라 달라진다.

DRM을 사용하는 영상이라면 미디어 요청 인증과 DRM License 요청도 구분해야 한다.

Segment를 다운로드할 권한이 있다고 해서 Widevine License까지 자동으로 발급되는 것은 아니다.

Media 요청
  -> Manifest와 암호화된 Segment 다운로드

DRM License 요청
  -> 복호화에 필요한 License 획득

두 요청은 서로 다른 서버와 인증 Header를 사용할 수 있다.

그래서 HTTP 통신이 맞을까?

이 글의 예시처럼 Player에 전달하는 URL이 https://cdn.example.com/videos/42/master.m3u8 같은 http 또는 https URI라면, 실제 미디어 요청은 HTTP 기반 통신으로 보는 것이 맞다.

확인 절차를 나누어 보면 다음과 같다.

첫째, URI의 scheme을 확인한다.

https://cdn.example.com/videos/42/master.m3u8

https://는 HTTP 메시지를 TLS 위에서 주고받는 방식이다.

즉 보안 연결을 사용하더라도 요청과 응답의 의미는 HTTP의 Method, Header, Status Code, Body를 따른다.

실제 전송에 사용되는 HTTP 버전은 서버, CDN, 클라이언트 구현, TLS 협상 결과에 따라 HTTP/1.1, HTTP/2, HTTP/3 중 하나가 될 수 있다.

둘째, Media3가 해당 URL을 어떤 MediaSource로 해석하는지 확인한다.

Media3의 DefaultMediaSourceFactory는 URI가 .m3u8로 끝나거나 MIME type이 APPLICATION_M3U8로 지정되어 있으면 HlsMediaSource를 사용한다.

일반 미디어 파일이라면 ProgressiveMediaSource가 사용될 수 있다.

https://.../master.m3u8 -> HlsMediaSource
https://.../movie.mp4   -> ProgressiveMediaSource

셋째, HLS 규격이 Playlist와 Segment를 어떻게 정의하는지 확인한다.

HLS에서는 하나의 재생 대상이 Playlist URI로 표현되고, Media Playlist 안에는 Media Segment URI들이 들어간다.

따라서 HLS 재생은 보통 다음과 같은 HTTP 요청 흐름으로 나타난다.

GET /videos/42/master.m3u8

GET /videos/42/720p/playlist.m3u8

GET /videos/42/720p/segment-001.m4s

넷째, Progressive 재생에서 중간 위치부터 다시 읽는 경우에는 HTTP Range 요청인지 확인한다.

GET /videos/movie.mp4
Range: bytes=25000000-

서버가 Range 요청을 지원하고 요청 범위가 유효하면 206 Partial Content로 응답할 수 있다.

정리하면 https://로 전달된 MP4, HLS, DASH 재생은 HTTP(S) 기반 통신이다.

다만 모든 Media3 재생이 HTTP인 것은 아니다.

file://, content://, rtsp:// 같은 URI를 전달하면 각각 파일, Android Content Provider, RTSP처럼 다른 데이터 소스나 프로토콜이 사용될 수 있다.

따라서 정확한 표현은 다음에 가깝다.

Media3 재생 자체가 항상 HTTP인 것은 아니지만, http 또는 https URL로 전달된 Progressive/HLS/DASH 미디어는 HTTP 요청과 응답을 통해 Manifest, Segment, Range 데이터를 가져온다.

기존 REST API와 무엇이 다를까?

여기서 또 하나의 질문이 생긴다.

스트리밍 통신은 기존 REST API 통신과 완전히 다른 방식일까?

그렇지는 않다.

REST는 별도의 네트워크 프로토콜 이름이라기보다 HTTP Resource를 어떤 방식으로 설계하고 다룰지에 대한 아키텍처 스타일이다.

HTTP 기반 스트리밍 역시 URL, GET, Status Code, Header, Cache를 사용한다.

GET /api/videos/42
GET /videos/master.m3u8
GET /videos/720p/segment-001.m4s

차이는 HTTP를 사용하느냐가 아니라, 전달하는 데이터와 요청 패턴에 있다.

구분일반적인 REST APIHTTP 기반 스트리밍
주요 데이터JSON 등 구조화된 데이터Manifest와 미디어 바이트
요청 형태짧은 요청과 응답긴 응답 또는 여러 Segment 요청
핵심 관심사Resource 상태와 비즈니스 동작Buffer, Bitrate, Seek, 재생 연속성
캐시API 정책에 따라 사용CDN Cache가 매우 중요
재시도요청 단위Segment, Range, Manifest 단위
품질 선택보통 없음ABR로 Track 선택 가능

실무에서는 재생 정보를 얻는 통신과 실제 미디어를 전달하는 통신을 분리하는 경우가 많다.

[Control Plane]
App
  ↓ Authorization Header
Playback API
  ↓ URL, 만료 시간, DRM 정보
App

[Data Plane]
Media3 Player
  ↓ Signed URL 또는 Authorization Header
CDN / Origin
  ↓ Manifest와 Media Segment
Player Buffer

Playback API는 사용자가 해당 영상을 재생할 권한이 있는지 확인하고, Player가 사용할 재생 정보를 반환한다.

이후 실제 대용량 미디어 데이터는 앱 서버가 아니라 CDN에서 제공할 수 있다.

따라서 앱 관점에서는 같은 영상을 재생하더라도 두 종류의 네트워크 클라이언트가 관여할 수 있다.

Retrofit / Ktor Client
  -> 재생 정보 API 요청

Media3 HttpDataSource
  -> Manifest, Segment, Range 요청

둘은 같은 토큰 저장소를 사용할 수 있지만, Header 설정과 Timeout, Cache, 재시도 정책은 별도로 구성해야 할 수 있다.

결론

처음 질문으로 돌아가보자.

Android Media Player는 서버에서 전달받은 영상 URL을 어떻게 스트리밍할까?

정확히는 URL 하나를 보고 무조건 같은 크기의 데이터 조각을 일정한 주기로 요청하는 것이 아니다.

Player는 URL과 미디어 형식을 바탕으로 적절한 MediaSource를 선택한다.

일반 MP4 같은 Progressive Media라면 하나의 HTTP 응답을 순서대로 읽거나, Seek와 재연결이 필요할 때 Range 요청으로 특정 바이트 구간부터 다시 읽을 수 있다.

Progressive Media
URL -> GET 또는 Range GET -> 하나의 파일을 필요한 위치부터 읽음

HLS와 DASH라면 Manifest를 먼저 읽고, 그 안에 기록된 여러 품질의 Media Segment를 개별 HTTP 요청으로 가져온다.

Adaptive Streaming
Manifest -> Track 선택 -> Segment 요청 -> Buffer -> 다음 Track 선택

요청 주기는 Segment Duration 하나로 결정되지 않는다.

Player는 현재 Buffer량, 네트워크 속도, 재생 위치, 선택한 Bitrate, Live Edge를 기준으로 추가 데이터가 필요한 시점을 판단한다.

Segment의 길이와 품질 목록은 주로 서버의 Encoder와 Packager가 정하고, 다음에 어떤 품질을 받을지와 어느 정도 Buffer를 유지할지는 클라이언트가 결정한다.

인증도 일반 HTTP API와 크게 다르지 않다.

Manifest Request -> Authorization Header 가능
Segment Request  -> Authorization Header 가능
Range Request    -> Authorization Header 가능
DRM Request      -> 별도 인증 가능

Bearer Token뿐만 아니라 Signed URL과 Signed Cookie를 사용할 수 있고, 긴 재생에서는 토큰 만료와 재요청 시점까지 고려해야 한다.

결국 HTTP 기반 스트리밍은 REST API와 완전히 분리된 별도 프로토콜이라기보다, 같은 HTTP 위에 미디어 재생을 위한 규칙과 전략이 추가된 형태에 가깝다.

그 위에서 Range, Manifest, Segment, Buffer, ABR, CDN 같은 요소들이 재생이 끊기지 않도록 함께 동작한다.

App -> 재생 권한과 URL 획득
Player -> 재생 형식 분석
MediaSource -> 다음에 필요한 데이터 결정
DataSource -> HTTP 요청과 인증
Server / CDN -> 미디어 데이터 응답
Buffer -> 재생이 끊기지 않도록 데이터 보관
Decoder -> 화면과 소리로 변환

MediaItem.fromUri() 한 줄 뒤에서는 Player가 현재 재생 위치와 Buffer 상태를 계속 확인하고, 필요한 Resource와 바이트 구간, 품질을 선택하고 있었다.

너무 자연스럽게 재생되어 그 과정이 보이지 않았을 뿐이다.

Reference

profile
실력은 고통의 총합이다. Android Developer

0개의 댓글