버튼 수집상

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

TIL - 안드로이드

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

cocokaribou 2023. 9. 21. 10:30

저번에 적은 1편에서 썼던 내용에 보충.

 

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

배경 무한재생되는 30초 내외 분량의 비디오에서 트래픽이 너무 쌓인다고 캐싱이 제대로 되고 있는지 확인 요청이 들어왔다. ExoPlayer는 캐싱 처리를 따로 해줘야 하는데, 기존 코드에서 설정하고

collectingbuttons.tistory.com

안드로이드 스튜디오 네트워크 인스펙터 확인하기

운영중인 앱에서 동일한 동영상이 반복재생될 때마다 트래픽이 발생했다.

무슨 데이터를 주고 받는지는 네트워크 인스펙터를 켜서 리퀘스트/리스폰스 정보를 보면 알 수 있다.

위 이미지에서 mp4 파일들이 일정 간격으로 다운받아지는 것을 볼 수 있다.

Status code 206은 데이터를 부분 다운받았다는 뜻이다.

혹시 동영상 파일이 아니라 동영상에 관련된 메타데이터가 아닐까 하는 의구심이 있었는데 해결됐다.

라이브러리 버전

지난 글에서는 ExoPlayer 라이브러리 버전을 2.12.0에서 2.16.0으로 업데이트하였다.

그런데 상부에서 라이브러리 버전을 올리기 전에 검토가 필요하다 하여(..) 2.12.0 버전 클래스로 코드를 다시 짰다.

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

현재 ExoPlayer의 최신버전은 2.19.1이니 참고 바란다.

소스적용

CustomDataSourceFactory.kt

class CustomDataSourceFactory : DataSource.Factory {
    companion object {
        // singleton
        // ❌deprecated constructor!
        val simpleCache : SimpleCache = SimpleCache(File(getCacheDir(), "exo_cache"), NoOpCacheEvictor())
    }

    private val bandwidthMeter = DefaultBandwidthMeter.Builder(BaseApp.context).build()
    private val defaultDataSourceFactory = DefaultDataSourceFactory(
        BaseApp.context, bandwidthMeter, DefaultHttpDataSource.Factory()
    )

    override fun createDataSource(): DataSource {
        return CacheDataSource(
            simpleCache,
            defaultDataSourceFactory.createDataSource(),
            FileDataSource(),
            CacheDataSink(simpleCache, DEFAULT_BUFFER_SIZE.toLong()),
            CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
            object: CacheDataSource.EventListener {
                override fun onCacheIgnored(reason: Int) {
                    Logger.v("onCachedIgnored")
                }

                override fun onCachedBytesRead(cacheSizeBytes: Long, cachedBytesRead: Long) {
                    Logger.v("onCachedBytesRead")
                }
            }
        )
    }
}

주석에 적었듯이

File, CacheEvictor 파라미터로 초기화되는 SimpleCache 생성자는 2.12.0 버전에서 deprecated 되었다.

(CacheEvictor는 캐시가 가득 찼을 때 어떤 파일을 삭제할지 사용빈도를 보고 결정한다.)

 

사용권장하는 SimpleCache 생성자는 세번째 인자에 DatabaseProvider를 넘겨야 한다.

// ExoPlayer 2.16.0+
val cache = SimpleCache(exoCacheDir, evictor, StandaloneDatabaseProvider(this@MainActivity))

ExoPlayer 2.12.0에서는 StandaloneDatabaseProvider 클래스가 없어서 직접 만들어줘야 한다.

아래는 커스텀 DatabaseProvider 클래스 예시이다.

// Generated by GPT-3.5

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper

class CustomDataBaseProvider(context: Context) : DatabaseProvider {

    private val dbHelper: CustomDbHelper = CustomDbHelper(context)

    override fun getWritableDatabase(): SQLiteDatabase {
        return dbHelper.writableDatabase
    }

    override fun getReadableDatabase(): SQLiteDatabase {
        return dbHelper.readableDatabase
    }

    // Define your custom database schema and version in this helper class
    private inner class CustomDbHelper(context: Context) :
        SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {

        override fun onCreate(db: SQLiteDatabase) {
            // Create your database tables here
            // Example: db.execSQL("CREATE TABLE IF NOT EXISTS your_table_name (column_name data_type);")
        }

        override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
            // Handle database schema upgrades here if needed
            // Example: db.execSQL("DROP TABLE IF EXISTS your_table_name;")
            // Then, recreate the table or make any necessary updates
            // onCreate(db)
        }

        companion object {
            private const val DATABASE_NAME = "custom_database.db"
            private const val DATABASE_VERSION = 1
        }
    }
}

 

이제 기존에 MediaItem을 세팅하던 자리에 CustomDataSourceFactory 클래스로 MediaSource를 생성하여 세팅하면 된다.

val playUrl : String?

private fun preparePlayer() {
    // 싱글턴 전역변수로 관리되는 플레이어 객체
    with(ExoPlayerInfo.instance) {
        player?.let {
            val mediaSource =
                ProgressiveMediaSource.Factory(CustomDataSourceFactory()).createMediaSource(MediaItem.fromUri(Uri.parse(playUrl)))
            // it.setMediaItem(MediaItem.fromUri(playUrl ?: ""))
            it.setMediaSource(mediaSource)
            it.addListener(playbackStateListener)
            it.prepare()
        }
    }
}

ExoPlayer에서 미디어는 MediaItem 으로 표현되지만, 코드 내부에서는 MediaSource 객체가 있어야 플레이할 수 있다.

디폴트로 MediaSource.Factory에서 만들던 MediaSource를 커스텀하게 생성했다.

여러 MediaSource 클래스 중에서 일반적인 미디어에 쓰이는 ProgressiveMediaSource 클래스를 사용했다. 출처

 

이러면 ExoPlayer 동영상이 자동 반복재생이 될 때 더이상 트래픽이 발생하지 않는다.

다른 방법은 없었나?

기존 코드에 동영상캐싱을 처리하는 코드가 벌써 있는 것 같다는 제보를 받았다.

코드는 아래와 같다.

private fun initView(videoReplay: VideoReplayData) {
    with (binding) {
        title.text = videoReplay.title
        recyclerView.apply {
            // getCache
            val config = Config.Builder()
                .cache(ExoPlayerUtils.getCache(itemView.context))
                .build()
            autoplayMode = PlayableItemsContainer.AutoplayMode.MULTIPLE_SIMULTANEOUSLY
            layoutManager = LinearLayoutManager(itemView.context, LinearLayoutManager.HORIZONTAL, false)
            val player = PlayerProviderImpl.getInstance(BaseApp.context).getPlayer(config, playable.getKey())
            // 이하 생략...
        }
    }
}

ExoPlayerUtils.getCache 함수에 주목한다.

그러게, 이걸 왜 발견을 못했지? 했는데, 다른 라이브러리에 구축된 유틸함수였다.

implementation "com.arthurivanets.arvi:arvi:1.3.0"
implementation "com.arthurivanets.arvi:arvi-utils:1.3.0"

Arvi는 ExoPlayer 위에 구축된 자동재생 비디오 리스트 UI 라이브러리이다.
스크롤 내리면 미리보기 동영상이 재생되는 유튜브 피드를 떠올리면 된다.

예시 이미지 (출처: https://hardikparmarj.medium.com/for-gods-sake-can-you-autoplay-video-in-list-ios-5ddc4ce1a3b3)

 

작은 비디오들이 자동재생되는 UI다보니 캐싱 설정이 필수일 것으로 생각된다.

ExoPlayerUtil.getCache는 ExoPlayer가 아닌 Arvi에서 제공하는 함수였다.

 

728x90