ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Exoplayer2 사용하기
    개발/안드로이드 2020. 4. 12. 15:02

     

    0. ExoPlayer란?

     

    안드로이드에서 영상 재생을 위해 사용하는 플레이어로 기본 내장 라이브러리인 MediaPlayer가 있었는데 스트리밍 서비스가 주류를 이루면서 구글에서 DASH와 SmoothStreaming을 지원하는 ExoPlayer 라이브러리를 도입했다. 유튜브, 네이버 동영상 프레임들도 Exoplayer를 사용하고 있다고 하니 앞으로 안드로이드 동영상 플레이어는 ExoPlayer가 주류를 이룰 것 같은 예감이다. 아니면 이미 그런지도 모르겠고.

     

    ExoPlayer는 MediaPlayer에서 이미 지원하는 기능에서 새로운 기능을 추가한 것이기 때문에 로컬/인터넷 동영상 파일 재생은 당연히 가능하고 Android Media Codec 기반으로 작업을 해서 Media Codec가 도입되기 시작한 안드로이드 기기 (API16 이상)에선 대부분 문제 없이 동작 한다. 물론 일부 기능은 더 높은 API 버전이 필요하긴 하지만 이는 거의 특수한 경우인 것 같다. 이번 포스트에서는 ExoPlayer를 사용하는 방법을 간단히 다룰 예정이다.

     

    1. Components 

     

    ExoPlayer: ExoPlayer의 라이브러리중 Renderer, 즉 화면에 뿌려주는 역할을 하는 컴포넌트다. ExoPlayer 인터페이스로 커스텀하게 만들 수 있으며 SimpleExoPlayer는 ExoPlayer에서 제공하는 컴포넌트다. 특별히 커스터마이즈 할 것이 아니면 이걸 그냥 가져다 쓰는게 좋다. 아래 함수를 통해 만들 수 있다.

     

    val simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(requireContext(), trackSelector)

     

    TrackSelector: SimpleExoPlayer 생성 과정에서 두번째 인자로 전달된 클래스는 영상의 Track 정보를 세팅하는 역할을 한다. 이 정보라면 예를 들면 선호하는 오디오 언어는 어떤 것인지, 비디오 사이즈는 무엇인지, 비디오 bitrate는 어떤 것으로 할지 등등 이런 것들을 말한다. 이것도 Renderer와 동일하게 따로 커스터마이즈 할 수 있긴 하나 특별한 이유가 없다면 라이브러리에서 기본으로 만들어 둔 것을 쓰는게 가장 좋다.

     

    아래 코드는 TrackSelector를 만들 때 AdaptiveTrackSelection 팩토리를 사용한 예시다. AdaptiveTrackSelection 팩토리 클래스는 현재 bandwidth 정보를 이용해 현재 선택된 track에서 최상의 퀄리티를 제공하는 역할을 한다고 한다. 더 자세한 내용은 라이브러리 내부 주석을 살펴보는 것이 좋을 것 같다. Streaming 서비스를 한다면 이쪽 클래스를 주요하게 보게될 것 같다.

     

    val bandwidthMeter = DefaultBandwidthMeter()
    val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory(bandwidthMeter)
    val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory)

     

    MediaSource: 영상에 출력할 미디어 정보를 가져오는 클래스다. ExtractorFactory 클래스를 통해 만드는데 이 클래스는 DataSource 클래스를 주입해서 만든다. 여기서 사용한 DefaultDataSource도 다른 라이브러리처럼 ExoPlayer에서 uri 형태로된 데이터를 읽어오기 위해 기본적으로 제공하는 라이브러리다. 특별한 형태의 DataSource 클래스를 사용하고 싶다면 커스터마이즈가 가능하다. 

     

    val extractorFactory = ExtractorMediaSource.Factory(DefaultDataSourceFactory(context, Util.getUserAgent(context, context!!.applicationInfo.packageName)))
    val mediaSource = extractorFactory.createMediaSource(Uri.parse(mediaPath))

     

    Player: 영상 재생을 위해선 미디어를 읽어오는 작업뿐만 아니라 영상을 UI 상에 뿌려줄 수 있는 뷰어가 필요한데 ExoPlayer용 뷰어가 따로 있다. 아래 코드를 XML에 넣으면 된다. 재생바, 앞으로 당기기기 같은 기본적인 UI 기능도 지원한다.

     

    <com.google.android.exoplayer2.ui.PlayerView
            android:id="@+id/fr_main_player"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#000"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>

     

    2. Play Video 

     

    앞서 소개한 컴포넌트들을 하나의 코드로 조합하면 재생이 가능하다. 코드의 순서는 설명한 순서와 조금 다른데 이는 클래스 생성 후에 주입하기 위함이다. exoPlayer.prepare(mediaSource)는 영상 정보를 가져오는 작업이고 fr_main_player.player.playWhenReady는 준비되면 영상을 시작하는 함수다.

     

    val mediaPath = "http://somewhere...."
    val bandwidthMeter = DefaultBandwidthMeter()
    val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory(bandwidthMeter)
    val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory)
    val exoPlayer = ExoPlayerFactory.newSimpleInstance(requireContext(), trackSelector)
    val extractorFactory = ExtractorMediaSource.Factory(
        DefaultDataSourceFactory(
            context,
            Util.getUserAgent(context, context!!.applicationInfo.packageName)
        )
    )
    val mediaSource = extractorFactory.createMediaSource(Uri.parse(mediaPath))
    
    fr_main_player.player = exoPlayer
    exoPlayer.prepare(mediaSource)
    fr_main_player.player.playWhenReady = true

     

    3. Extension 

     

    Player.Listener: 영상 재생중 로딩에 실패하거나 Track 속성이 바뀌거나 혹은 영상 재생이 완료된 경우에 대해서 리스너를 등록해줄 수 있는데 이 경우들은 뷰어에 리스너를 등록해서 구현이 가능하다. 아래 코드를 통해 어떤 경우에 대해서 콜백 호출이 가능한지 확인 해볼 수 있다. 추가로 아래 코드에선 영상 재생이 완료된 경우 다시 재생하도록 구현했다.

     

    fr_main_player.player.addListener(object: Player.EventListener{
        override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters?) {}
        override fun onSeekProcessed() {}
        override fun onTracksChanged(trackGroups: TrackGroupArray?, trackSelections: TrackSelectionArray?) {}
        override fun onPlayerError(error: ExoPlaybackException?) {}
        override fun onLoadingChanged(isLoading: Boolean) {}
        override fun onPositionDiscontinuity(reason: Int) {}
        override fun onRepeatModeChanged(repeatMode: Int) {}
        override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {}
        override fun onTimelineChanged(timeline: Timeline?, manifest: Any?, reason: Int) {}
        override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
            if (playbackState == Player.STATE_ENDED) {
                fr_main_player.player.seekTo(0)
                fr_main_player.player.playWhenReady = true
            }
        }
    })

     

    CacheDataSource: 인터넷으로 영상을 받는 경우 여러번 재생을 할 때 마다 동일한 데이터를 계속 인터넷으로 불러오게돼 데이터를 낭비할 수도 있는 문제가 있다. ExoPlayer에서는 이 문제점을 해결하고자 별도의 미디어 데이터 저장 공간으로 Cache를 뒀다. 이것도 다양하게 커스터마이즈 할 수 있으나 가장 기본적인 사용 방법은 아래 코드와 같다. 

     

    ExtractorMediaSource.Factory 함수에서 호출 할 수 있도록 임의의 클래스를 DataSource.Factory의 인터페이스를 구현한 형태로 만든다. 리턴 값으로는 CacheDataSource가 되는데 여기서 생성자에서 캐시가 가져야할 정보를 입력하게 된다. 아래 코드 보면 캐시의 크기도 설정 할 수 있도 플래그를 넣을 수 있는 것도 확인 할 수 있다.

     

    private class CacheDataSourceFactory internal constructor(
        private val context: Context,
        private val defaultDataSourceFactory: com.google.android.exoplayer2.upstream.DataSource.Factory,
        private val maxCacheSize: Long,
        private val maxFileSize: Long,
        private val url: String
    ) : com.google.android.exoplayer2.upstream.DataSource.Factory {
        override fun createDataSource(): com.google.android.exoplayer2.upstream.DataSource {
            val evictor = LeastRecentlyUsedCacheEvictor(maxCacheSize)
            val simpleCache = SimpleCache(File(context.cacheDir, "media"), evictor)
            return CacheDataSource(
                simpleCache,
                defaultDataSourceFactory.createDataSource(),
                FileDataSource(),
                CacheDataSink(simpleCache, maxFileSize),
                CacheDataSource.FLAG_BLOCK_ON_CACHE or CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
                null
            )
        }
    }

     

    위 코드를 이용한 호출부는 다음과 같다. 앞서 설명한 CacheDataSource 팩토리 클래스에서 두번째 인자로 DataSourceFactory를 넣었는데 아래 구현부 코드를 확인해보면 이전에 만든 DefaultDataSourceFactory 클래스를 넣는 것을 볼 수 있다. 외부 데이터를 불러오는 작업은 기존 데이터 클래스를 따라 간다는 뜻이다.

     

    val extractorCacheFactory = ExtractorMediaSource.Factory(
        CacheDataSourceFactory(requireContext(), DefaultDataSourceFactory(
            context,
            Util.getUserAgent(context, context!!.applicationInfo.packageName)
        ), MAX_CACHE_SIZE, MIN_CACHE_SIZE, mediaPath)
    )
    
    val mediaSource = extractorCacheFactory.createMediaSource(Uri.parse(mediaPath))

     

     

    '개발 > 안드로이드' 카테고리의 다른 글

    Kotlin - Coroutine  (0) 2020.04.15
    Kotlin으로 깔끔한 Builder를 만들어보자  (1) 2020.04.14
    FragmentManagers Android  (1) 2020.04.06
    ViewModelProviders.of deprecated  (0) 2020.04.06
    Exoplayer에 stetho 적용하기  (0) 2020.03.16

    댓글

Designed by Tistory.