본문 바로가기
Android/Kotlin

Android ExoPlayer 로 앱에서 동영상 재생하기

by wannagohome97 2024. 1. 5.

ExoPlayer 란?

Exoplayer 는 안드로이드에서 자주 사용되는 Media 라이브러리인데 상세한 설명은 안드로이드 개발자 공식을 참고하자

출처 : https://developer.android.com/guide/topics/media/exoplayer?hl=ko

 

ExoPlayer  |  Android 개발자  |  Android Developers

ExoPlayer 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. ExoPlayer는 Android 프레임워크에 속하지 않고 Android SDK에서 별도로 배포되는 오픈소스 프로젝트입니다.

developer.android.com

 

 

기본적으로 우리가 흔히 아는 동영상의 컨테이너 포맷(MP4, AVI, WMV 등등..) 은 거의 지원한다고 볼 수 있고

설령 지원하지 않는 포맷이 있어도 FFMPEG 에 원하는 포맷을 넣고 빌드하면 정말 범용성이 높다.

FFmepg 는 추후 별도로 글을 작성할 것이고 오늘은 Exoplayer 로 동영상을 재생하는 간단한 코드만 볼 것이다.

 

    implementation 'com.google.android.exoplayer:exoplayer-common:2.19.1'
    implementation 'com.google.android.exoplayer:exoplayer:2.19.1'

  

글을 쓰는 2023년 기준으로 Exoplayer 2 의 최신 버전은 2.19.1 이다.

참고사항이라 하면 공식적으로 Exoplayer 2 자체는 Deprecated 되었다.. (Media 3 에 통합되었으니 업데이트 받고싶으면 마이그레이션 해라! 라고 하던데 아직 2022년에 Exoplayer 1.5xx 버전을 써서 잘만 구현해내던 개발자도 본 나로써는.. 모르겠다)

 

이제 액티비티가 실행되면 동영상이 재생되는 형태로 구현해 볼 것이다.

일단 동영상이 재생될 View 는 PlayerView 라는 Exoplayer 에서 지원하는 View 인데 이것을 레이아웃에 원하는 위치에 배치해보자.

    <com.google.android.exoplayer2.ui.StyledPlayerView
        android:id="@+id/player_view"
        android:layout_width="@dimen/player_width"
        android:layout_height="@dimen/player_height"
        app:use_controller="false"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        />

 

이런 식으로 화면에 보여질 레이아웃에 PlayerView 를 배치해주고

 

class MainActivity : AppCompatActivity() {

    private var mPlayerView: StyledPlayerView? = null
    private var mPlayer: ExoPlayer? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mPlayerView = findViewById(R.id.player_view)
    }

    override fun onStart() {
        super.onStart()
        initPlayer()
    }

    override fun onStop() {
        super.onStop()
        releasePlayer()
    }

    private fun initPlayer(){
        val factory = DefaultRenderersFactory(this)
        factory.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
        val parameters = DefaultTrackSelector.ParametersBuilder()
            .build()
        val trackSelector = DefaultTrackSelector(this, parameters)
        /*
        val loadControl = DefaultLoadControl.Builder()
            .setBufferDurationsMs(10000,
                20000,
                DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
                DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
            .setTargetBufferBytes(DEFAULT_BUFFER_SIZE*DEFAULT_BUFFER_SIZE)
            .build()
        */
        /*
        val bandwidthMeter = DefaultBandwidthMeter.Builder(this).build()
        */
        val builder = ExoPlayer.Builder(this)
        mPlayer = builder.setTrackSelector(trackSelector)
            .setRenderersFactory(factory)
            .build()
        mPlayer?.addListener(mListener)
        mPlayerView?.player = mPlayer
    }


    private fun releasePlayer(){
        mPlayer?.stop()
        mPlayer?.release()
        mPlayer = null
    }

    private val mListener = object : Player.Listener{
        override fun onPlayerError(error: PlaybackException) {
        }
        override fun onPlaybackStateChanged(playbackState: Int) {
            super.onPlaybackStateChanged(playbackState)
            when (playbackState) {
                Player.STATE_READY -> {
                }
                Player.STATE_BUFFERING -> {
                }
                Player.STATE_ENDED -> {
                }
                Player.STATE_IDLE -> {
                }
            }
        }
    }
}

 

주석 처리해둔 부분은 initPlayer 가 사실 프로젝트에서 긁어온 코드인데 저 부분은 통상적으론 없어도 큰 지장이 없는 부분이어서 세부 값은 Default 로 바꾼 채로 주석처리 해두었다.

 

설명을 추가하자면

  • LoadControl 
    데이터(스트림) 을 로드하고 버퍼링하는 부분을 제어하는 인터페이스인데 그냥 DefaultLoadControl 로 써도 큰 지장 없다.
    나 같은 경우는 테스트 케이스 중에 서버 자체적인 오디오 코덱 이슈랑 맞물려서 스트림(데이터)이 타겟 버퍼만큼 들어오질 않아 버퍼링이 무한으로 걸리던 상황이 있었어서 타겟 버퍼를 조금 내려주었다.

  • BandwidthMeter 
    네트워크에서 받은 데이터의 양이나 속도를 기반으로 네트워크의 대역폭을 알 수 있고 재생 품질을 조정할 수 있다.
    이전에 그냥 Default 로 두고 넘어갔다가 위에 LoadControl 쪽에서 있던 그 이슈때 이 쪽도 인터페이스를 별도로 구현하고 구현한 김에 UI 에 동영상 품질 느낌으로 띄웠었다.

  • Listener
    현재 재생 상태가 변할 때 라던지, 스트림이 끊기거나 잘못되서 에러가 발생하면 Listener 에 state 가 Int 형태로 전달된다. 에러가 났을 때 다시 재접속 시도를 한다던가.. 소스 자체에 문제가 생겼을 때 유저에게 노티를 한다던가.. 
    유용한 기능이다.

아무튼 위에 작성해둔 코드대로 작성하면 PlayerView 에 Exoplayer 가 붙는다.

이렇게 붙여둔 플레이어에 Media 를 MediaSource 라는 형태로 바꿔서 올려주고 실행시키면 동영상이 재생된다.

아래 코드는 url 을 기준으로 작성되었다.

private fun setupMediaSource(streamUrl: String) {
    val factory = DefaultHttpDataSource.Factory()
    val upstreamFactory: DataSource.Factory =
        DefaultDataSource.Factory(this, factory)
    val mediaItem = MediaItem.fromUri(Uri.parse(streamUrl))
    val progressiveFactory = ProgressiveMediaSource.Factory(upstreamFactory)
    val mediaSource = progressiveFactory.createMediaSource(mediaItem)
    /*
    // 만약 동영상의 프로토콜이 HLS 면 위의 두 줄 대신 아래 코드를 사용하자.
    val m3u8Factory = HlsMediaSource.Factory(upstreamFactory)
    val mediaSource = m3u8Factory.createMediaSource(mediaItem)
    */
    mPlayer?.setMediaSource(mediaSource)
    mPlayer?.playWhenReady = true
    mPlayer?.prepare()

}

 

 

ExoPlayer 2 를 기준으로 ProgressiveMediaSource 에서 꽤 많은 포맷을 지원하기 때문에 구현하기 굉장히 편하다.

ProgressiveMediaSource 에서 지원이 안되는 m3u8 같은 HLS 프로토콜 역시 HlsMediaSource 클래스를 지원해주기 때문에 쉽게 구현 가능하다.

위의 메서드 setupMediaSource 를 동영상을 재생시키는 트리거에 붙여주면 된다.

 

추가로, File 이면 

MediaItem.fromUri(Uri.parse(streamUrl))

 

이 부분을

MediaItem.fromUri(Uri.fromFile(file))

 

이렇게 바꿔주면 된다.