AndroidX Media3之ExoPlayer簡單使用(2)

在上一篇文章AndroidX Media3之ExoPlayer簡單使用(1)中介紹了ExoPlayer的簡單使用,運用了media3-ui包中提供的關于ExoPlayer的UI組件和資源。但是在日常開發(fā)中,播放器的界面會被要求為各式各樣的,沒有辦法使用media3-ui包中提供的通用界面。
在這篇文章將介紹如何自己實現一個簡單的PlayerView。
demo下載
看一下最終的效果界面圖:

image.png

依賴項

在上一篇文章的依賴項基礎上,可以去除media3-ui的依賴,只需要一個androidx.media3:media3-exoplayer:1.0.0-beta02依賴即可。

基本要素

通過查看media3-ui庫中的PlayerView實現,可以看到自定義播放界面需要的一些基本要素:

  private final ComponentListener componentListener;
  @Nullable private final AspectRatioFrameLayout contentFrame;
  @Nullable private final View shutterView;
  @Nullable private final View surfaceView;
  private final boolean surfaceViewIgnoresVideoAspectRatio;
  @Nullable private final ImageView artworkView;
  @Nullable private final SubtitleView subtitleView;
  @Nullable private final View bufferingView;
  @Nullable private final TextView errorMessageView;
  @Nullable private final PlayerControlView controller;

componentListener:用于播放事件的監(jiān)聽,包括有播放狀態(tài)、播放異常情況等。
contentFrame:播放界面的寬高比例大小等。
surfaceView:用于渲染視頻的Surface。
bufferingView:視頻緩沖時顯示。
errorMessageView:視頻播放異常時顯示。
controller:視頻播放控制界面,包括播放暫停操作、播放進度操作等。

播放事件的監(jiān)聽

通過player.addListener添加一個Player.Listener進行播放事件的監(jiān)聽。Player.Listener有空的默認方法,因此按需實現所需要的方法即可。
我們所需要實現的方法主要有以下幾個:

  • onVideoSizeChanged:獲取播放視頻的寬度和高度,用于更新播放界面的寬高比例大小
  • onPlayerError:播放異常情況的監(jiān)聽,用于展示錯誤界面
  • onPlaybackStateChanged:播放狀態(tài)的監(jiān)聽,用于視頻緩沖界面展示、視頻播放控制界面
  • onPlayWhenReadyChanged:播放暫停操作的監(jiān)聽,用于視頻播放控制界面
    private inner class ComponentListener : Player.Listener {

        override fun onVideoSizeChanged(videoSize: VideoSize) {
            updateAspectRatio()
        }

        override fun onPlayerError(error: PlaybackException) {
            updateErrorMessage()
        }

        override fun onPlaybackStateChanged(playbackState: @State Int) {
            if (playbackState == Player.STATE_READY && visibility != View.VISIBLE) {
                visibility = View.VISIBLE
            }
            updateProgressState()
        }

        override fun onPlayWhenReadyChanged(
            playWhenReady: Boolean, reason: @Player.PlayWhenReadyChangeReason Int
        ) {
            //播放暫停會回調該方法,播放時playWhenReady為true
            updateProgressState()
        }
    }

播放界面的顯示

media3-ui庫中的PlayerView對于視頻播放界面的顯示用了AspectRatioFrameLayout,我們可以直接復用這個布局,也可以自己簡單的實現一個。

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <FrameLayout
        android:id="@+id/exo_content_frame"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintDimensionRatio="2:1"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/exo_error_message"
        android:layout_width="wrap_content"
        android:layout_height="32dp"
        android:layout_gravity="center"
        android:gravity="center"
        android:textColor="@color/white"
        android:visibility="gone"
        android:text="刷新"
        app:layout_constraintBottom_toBottomOf="@id/exo_content_frame"
        app:layout_constraintEnd_toEndOf="@id/exo_content_frame"
        app:layout_constraintStart_toStartOf="@id/exo_content_frame"
        app:layout_constraintTop_toTopOf="@id/exo_content_frame" />
</androidx.constraintlayout.widget.ConstraintLayout>

使用ConstraintLayout作為一個根布局,利用constraintDimensionRatio屬性可以設置播放界面的寬高比例顯示。當拿到視頻的寬度和高度之后,將播放界面的寬高比例更改為視頻自身的寬高比。

    private fun updateAspectRatio() {
        player?.videoSize?.let { videoSize ->
            if (videoSize.width > 0 && videoSize.height > 0) {
                val width = videoSize.width
                val height = videoSize.height
                //更改比例,根據視頻自身寬高比展示
                (binding.exoContentFrame.layoutParams as? LayoutParams)?.let {
                    it.dimensionRatio = "$width:$height"
                    binding.exoContentFrame.layoutParams = it
                }
            }
        }
    }

什么時候可以拿到視頻的寬度和高度呢?在上一篇文章中,通過player.addListener添加一個Player.Listener進行播放事件的監(jiān)聽,可以監(jiān)聽到播放狀態(tài)、播放異常情況等,此外還有onVideoSizeChanged方法可以獲取到視頻的寬度和高度,在該方法中更新播放界面的寬高比例即可。

用于渲染視頻的Surface

我們需要一個SurfaceView用于渲染視頻:

  • 創(chuàng)建一個SurfaceView:
private val surfaceView by lazy { SurfaceView(context) }
  • 將SurfaceView添加到布局上:
binding.exoContentFrame.addView(surfaceView, 0)
  • 渲染視頻:
player.setVideoSurfaceView(surfaceView)

視頻緩沖時顯示

視頻的播放會需要緩沖時間,在上一篇文章中有介紹到播放監(jiān)聽onPlaybackStateChanged方法中會有視頻播放的四種播放狀態(tài):

  • STATE_IDLE:初始狀態(tài),此時播放器沒有可以播放的資源,播放器停止播放或者播放失敗后也會處于該狀態(tài)
  • STATE_BUFFERING: 沒有足夠的數據可以加載播放,此時無法立即播放
  • STATE_READY : 播放器可以立即播放,是否播放取決于playWhenReady的值,該值表達了使用者的意愿,為true,將會開始播放,否則不播。
  • STATE_ENDED: 播放完了所有的資源后處于該狀態(tài)

在監(jiān)聽到STATE_READY狀態(tài)為止之前就是視頻的緩沖時間,進行一個緩沖狀態(tài)的展示,例如loading,監(jiān)聽到STATE_READY狀態(tài)之后,隱藏緩沖狀態(tài)loading,展示視頻播放。

視頻播放異常時顯示

在播放監(jiān)聽onPlayerError方法中可以監(jiān)聽到播放異常情況,此時需要展示播放異常界面,展示錯誤信息和重新加載界面,當需要重新加載時調用player.prepare()方法。

視頻播放控制界面

視頻播放控制界面,包括播放暫停操作、播放進度操作等跟用戶進行互動的操作。

播放暫停操作

根據播放器當前狀態(tài)來進行播放或者暫停的操作,同時更新操作界面展示。

        binding.tvPlay.setOnClickListener {
            if (player.isPlaying) {
                player.pause()
                binding.tvPlay.text = "播放"
            } else {
                player.play()
                binding.tvPlay.text = "暫停"
            }
        }

播放進度操作

播放進度是一個需要不斷自動更新的狀態(tài),在播放監(jiān)聽中onPlaybackStateChanged方法和onPlayWhenReadyChanged方法中我們都需要調用更新播放進度的方法。onPlaybackStateChanged是加載播放視頻到加載完成播放會回調該方法,onPlayWhenReadyChanged是視頻播放或者暫停操作會回調該方法。

    private inner class ComponentListener : Player.Listener {

        override fun onVideoSizeChanged(videoSize: VideoSize) {
            updateAspectRatio()
        }

        override fun onPlayerError(error: PlaybackException) {
            updateErrorMessage()
        }

        override fun onPlaybackStateChanged(playbackState: @State Int) {
            if (playbackState == Player.STATE_READY && visibility != View.VISIBLE) {
                visibility = View.VISIBLE
            }
            updateProgressState()
        }

        override fun onPlayWhenReadyChanged(
            playWhenReady: Boolean, reason: @Player.PlayWhenReadyChangeReason Int
        ) {
            //播放暫停會回調該方法,播放時playWhenReady為true
            updateProgressState()
        }
    }

在更新播放進度方法中,我們通過handler不斷的發(fā)消息來進行進度的更新。當視頻不是播放狀態(tài)時自然也是不需要更新播放進度的。

    /**
     * 更新進度條狀態(tài)
     */
    private fun updateProgressState() {
        if (player?.playbackState == Player.STATE_READY && (player?.isPlaying == true)) {
            progressHandler.removeCallbacksAndMessages(null)
            progressHandler.sendEmptyMessage(1)
        } else {
            //清空進度
            progressHandler.removeCallbacksAndMessages(null)
        }
    }

通過播放器相關方法可以獲取到視頻總時長、當前加載時長和當前播放時長,進而進行播放進度的更新和展示。

    private val progressHandler = object : Handler(Looper.myLooper()!!) {
        override fun handleMessage(msg: Message) {
            val currentPlayer = player
            //獲取進度并通知
            if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) {
                val currentPosition = currentPlayer.currentPosition.toInt()
                val bufferedPosition = currentPlayer.bufferedPosition.toInt()
                val duration = currentPlayer.duration.toInt()
                progressChangeList.forEach {
                    it.onProgressChanged(currentPosition, bufferedPosition, duration)
                }
                //0.5秒后自動獲取進度
                sendEmptyMessageDelayed(1, 500)
            }
        }
    }

如果播放進度條可以進行拖拽從而達到操作播放進度,只要使用player.seekTo()方法就可以指定播放器播放進度。

資源釋放

當不再需要播放器時,記得釋放資源,由于使用了handler發(fā)消息來進行播放進度的更新,所以也需要對handler進行資源釋放:

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        //釋放資源
        try {
            player?.setVideoSurfaceView(null)
            player?.stop()
            player?.release()
            player?.removeListener(componentListener)
            progressHandler.removeCallbacksAndMessages(null)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

前后臺切換播放優(yōu)化

如果不采取任何操作,在進行后臺切至前臺的操作時,會出現黑屏的情況,因此當切至后臺時,對播放狀態(tài)進行一個保存,并暫停播放器,當切回前臺時,恢復播放器之前的播放狀態(tài),可以在onWindowVisibilityChanged方法中進行該優(yōu)化操作:

    override fun onWindowVisibilityChanged(visibility: Int) {
        super.onWindowVisibilityChanged(visibility)
        if (visibility == View.VISIBLE) {
            if (playState) {
                player?.play()
            }
        } else {
            playState = (player?.isPlaying == true)
            if (player?.isPlaying == true) {
                player?.pause()
            }
        }
    }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容