버튼 수집상

[안드로이드] ExoPlayer 비디오 캐싱하기 본문

TIL - 안드로이드

[안드로이드] ExoPlayer 비디오 캐싱하기

cocokaribou 2023. 8. 9. 14:34

배경

무한재생되는 30초 내외 분량의 비디오에서 트래픽이 너무 쌓인다고 캐싱이 제대로 되고 있는지 확인 요청이 들어왔다.

ExoPlayer는 캐싱 처리를 따로 해줘야 하는데, 기존 코드에서 설정하고 있지 않았다.

확인하기

디버거를 연결한 다음 안드로이드 스튜디오 하단의 App Inspection > Network Inspector 를 켰다.

최초의 요청 이후로도 비디오가 반복되며 재생될 때마다 통신이 오고감을 알 수 있다.

빨간 박스가 그려진 데이터 사이즈는 영상에 따라 달랐다.

기존 코드는 아래와 같다.

// player 초기화
private fun initPlayer() {
    // 싱글턴 오브젝트
    ExoPlayerInfo.instance.apply {
        player = SimpleExoPlayer.Builder(this@MainActivity).build()
    }
}
// api에서 받아오는 비디오 링크
val playUrl : String?

// player 상태값 리스너
val playbackStateListener = object : Player.Listener {
    override fun onPlaybackStateChanged(playbackState: Int) { /*...*/ }
    override fun onPlayerError(error: PlaybackException) { /*...*/ }
}

// 뷰에 player 설정
private fun preparePlayer() {
    with(ExoPlayerInfo.instance) {
        player?.let {
            binding.playerView.player = it // 싱글턴 오브젝트 player를 뷰에 할당

            it.volume = .0f
            it.setMediaItem(MediaItem.fromUri(playUrl ?: "")) // MediaItem 설정
            it.playWhenReady = true
            it.repeatMode = Player.REPEAT_MODE_ONE
            it.addListener(playbackStateListener)
            it.prepare()
        }
    }
}

샘플 찾아보기

MediaItem은 ExoPlayer 코드 내부에서 MediaSource로 바뀐다.

공식문서에 cache를 검색하니 CacheDataSource.Factory 라는 클래스로 MediaSource를 초기화하라고 나왔다.

 

샘플소스를 찾아보는데 공통적으로 등장하는 몇 몇 클래스를 찾을 수가 없었다.

일단 라이브러리 버전을 올렸다.

2.12.0 -> 2.16.0 (최신버전은 2.19.0)

implementation 'com.google.android.exoplayer:exoplayer-core:2.16.0'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.16.0'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.16.0'

그리고 기존에 사용하던 SimpleExoPlayer가 deprecated되어 ExoPlayer로 교체했다.

소스적용

우선 SimpleCache 객체를 초기화한다.

val exoCacheDir = File("${BaseApp.context.cacheDir}/exo")
val evictor = LeastRecentlyUsedCacheEvictor(500 * 1024 * 1024)
val cache = SimpleCache(exoCacheDir, evictor, StandaloneDatabaseProvider(this@MainActivity))

캐시 디렉토리는 ApplicationContext에서 가져온다.

캐시 사이즈는 500MB로 잡았는데 사실 기준을 잘 모르겠다.

chatGPT에 물어보니 bitRate 2Mbps 30초 내외 길이 동영상이면 7에서 7.5MB 정도면 된다는데 확실하지 않다.

val httpDataSourceFactory = DefaultHttpDataSource.Factory().apply {
    // request header 등 설정
    setUserAgent(CommonConst.extraUserAgent)
}
val upstreamDataSourceFactory = DefaultDataSource.Factory(this@MainActivity, httpDataSourceFactory)

DefaultHttpDataSource.Factory에서는 http 통신에 필요한 설정을 넣는다.

이제 CacheDataSource.Factory를 초기화한다.

val cacheFactory = CacheDataSource.Factory()
    .setCache(cache)
    .setCacheWriteDataSinkFactory(CacheDataSink.Factory().setCache(cache))
    .setCacheReadDataSourceFactory(FileDataSource.Factory())
    .setUpstreamDataSourceFactory(upstreamDataSourceFactory)
    .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
    .setEventListener(object: CacheDataSource.EventListener {
        override fun onCachedBytesRead(cacheSizeBytes: Long, cachedBytesRead: Long) {
            Log.v("exoPlayer", "onCachedBytesRead. cacheSizeBytes:$cacheSizeBytes, cachedBytesRead:$cachedBytesRead")
        }

        override fun onCacheIgnored(reason: Int) {
            Log.v("exoPlayer", "onCacheIgnored. reason:$reason")
        }
    })

ExoPlayerInfo.instance.apply {
    // player = SimpleExoPlayer.Builder(this@MainActivity).build()
    player = ExoPlayer.Builder(this@MainActivity).build()
    cacheDataSourceFactory = cacheFactory // 싱글턴 오브젝트에 필드 추가
}

이제 url을 받아오고 CacheSourceDataFactory를 이용해서 MediaSource로 만들어주겠다.

private fun preparePlayer() {
    val mediaItem = MediaItem.fromUri(playUrl ?: "")
    val mediaSource by lazy {
        val dataSourceFactory = ExoPlayerInfo.instance.cacheDataSourceFactory // 싱글턴 오브젝트 필드
        ProgressiveMediaSource.Factory(dataSourceFactory)
            .createMediaSource(mediaItem)
    }
    with(ExoPlayerInfo.instance) {
        player?.let {
            binding.playerView.player = it

            it.volume = .0f
            // it.setMediaItem(MediaItem.fromUri(playUrl ?: ""))
            it.setMediaSource(mediaSource, true)
            it.playWhenReady = true
            it.repeatMode = Player.REPEAT_MODE_ONE
            it.addListener(playbackStateListener)
            it.prepare()
        }
    }
}

위 코드에서 ProgressiveMediaSource 대신 HlsMediaSource를 사용했었는데,

Input does not start with the #EXTM3U header 라는 에러가 계속 났었다.

https://github.com/google/ExoPlayer/issues/5424

 

HLS Streaming - Input does not start with the #EXTM3U header · Issue #5424 · google/ExoPlayer

Seached Issues - #4141 , #4150 ,#4066 I am using HLS encrypted Stream for exoplayer. URL - https://mediadev.camtrack.com/dev/temp/bunny.m3u8?Expires=2072197799&Signature=PqNjmCoIRo~lSurGLVtY6FLBNGQ...

github.com

 

ProgressiveMediaSource로 바꾸면 위와 같은 에러가 나지 않는다.

다시 확인하기

CacheDataSource.Factory 를 설정하니 더이상 반복재생될때마다 통신을 주고받지 않았다.

어떤 사이드이펙트가 있는지, 어떤 부분을 누락했는지 놓친 게 있다면 후속글로 다루겠다.

 

참고

https://medium.com/@thanductaimgt/pre-caching-adaptive-video-stream-in-a-playlist-with-exoplayer-api-5ef92097d94e

 

Pre-caching adaptive video stream in a playlist with ExoPlayer API

Recently, I have been working on improving video playlist performance for my company Android project. The playlist is a video list that can…

medium.com

https://exoplayer.dev/customization.html

 

Customization - ExoPlayer

 

exoplayer.dev

https://tourspace.tistory.com/527

 

[ExoPlayer] HTTP GET, POST, Cache 및 JSON body 적용

ExoPlayer는 다양한 방법으로 재생을 지원합니다. 단말에 저장하고 있는 동영상 파일이라던가, 앱의 resource로 가지고 있는 파일, 서버에 존재하여 uri로 (http로 다운받아) 재생하기, 게다가 DRM 걸린

tourspace.tistory.com

https://proandroiddev.com/lets-dive-into-exo-player-part-iv-caching-video-7ac2dc430dbf

 

Lets Dive into Exo-Player (Part IV): Caching Video

Streaming is the continuous transmission of audio or video files from a server to a client (the process that makes watching videos online…

proandroiddev.com

 

 

728x90