Dev/Android

[Android] HLS in Android

ragabys 2024. 7. 20. 19:59



서론

 

 

 

 

HLS란?

 

 

HLS(HTTP Live Streaming)은 비디오 스트리밍 프로토콜로, 오디오나 동영상을 여러 개의 작은 세그먼트로 분할하고,

 

 

이러한 세그먼트를 HTTP 기반의 웹 서버를 통해 전송한다.

 

 

주로 Twitch 같은 라이브 스트리밍 플랫폼이나, OTT에서 사용하는 방식이다.

 

 

HLS 사용 이유

 

 

HLS를 사용하는 가장 큰 이유는 적응형 비트레이트 스트리밍이라는 장점 때문이다.

 

 

HLS는 네트워크 상태에 따라 스트리밍 품질을 동적으로 조정하여 버퍼링을 최소화하고,

 

 

여러 품질 수준으로 인코딩된 동일한 콘텐츠를 제공하고, 클라이언트는 현재 네트워크 상태에 가장 적합한 비트레이트를 선택한다.

 

 

또한 HTTP를 기반으로 전송하기 때문에, 기존의 HTTP 인프라를 그대로 사용할 수 있다.

 

 

Android HLS 사용 방식

 

 

Android의 많은 미디어 플레이어 중, Exoplayer를 통한 방식이 간단하여 Exoplayer를 활용한 방식을 소개하고자 한다.

 

 

프로젝트를 진행하며 Compose를 사용했기 때문에, 기존의 xml을 활용하던 Android 코드와는 다를 수 있음을 미리 말한다.

 

 

우선 exoplayer와 HLS를 사용하기 위한 dependency를 추가해준다.

 

 

dependencies {
    implementation("androidx.media3:media3-exoplayer:1.3.0")
    implementation("androidx.media3:media3-exoplayer-hls:1.3.0")
}

 

 

이후 exoplayer를 실행할 player와 미디어 데이터를 불러오기 위한 dataSourceFactory를 선언해준다.

 

 

//Compose 기반 코드
val context = LocalContext.current
val player = remember { ExoPlayer.Builder(context).build() }
val dataSourceFactory: DataSource.Factory = DefaultHttpDataSource.Factory()

 

 

이후 HlsMediaSource에 미디어 데이터를 담아 player에 연결한다.

 

 

    val hlsMediaSource =
        HlsMediaSource.Factory(dataSourceFactory)
            .createMediaSource(
                MediaItem.fromUri(streamingURL)
            )
    player.setMediaSource(hlsMediaSource)
    player.prepare()

 

 

이 때 MediaItem.fromUri에 포함되는 streamingURL은 .m3u8로 끝나야 한다.

 

 

m3u8은 텍스트 파일로, 주석과 메타데이터, 미디어 세그먼트의 URL로 구성되는 HLS 프로토콜에 사용되는 플레이리스트 파일이다.

 

 

이와 같은 방식대로 파일을 추가하면, HLS 프로토콜을 활용한 미디어 재생이 가능하다.

 

 

물론 Exoplayer를 활용한 방식이기 때문에, 아래의 예시와 같이 Player.Listener 인터페이스를 추가하여 재생을 관리할 수 있다.

 

 

player.addListener(object : Player.Listener {
    @Deprecated("Deprecated in Java")
    override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
        if (playbackState == Player.STATE_BUFFERING) {
            startBuffering()
        }

        if (playbackState == Player.STATE_READY) {
            player.play()
            audioDuration.intValue = player.duration.toInt()
            playerLoad.value = false
            endBuffering()
        }

        if (playbackState == Player.STATE_IDLE) {
            audioDuration.intValue = 0
            endBuffering()
        }

        if (playbackState == Player.STATE_ENDED) {
            audioDuration.intValue = 0
            endBuffering()
        }
    }

    private fun startBuffering() {
        bufferingJob?.cancel()
        bufferingJob = CoroutineScope(Dispatchers.IO).launch {
            repeat(10) {
                delay(1000)
                bufferingTime.intValue += 1
            }
        }
    }

    private fun endBuffering() {
        bufferingJob?.cancel()
        bufferingJob = null
    }
})