在上一篇文章AndroidX Media3之ExoPlayer簡(jiǎn)單使用(1)中介紹了ExoPlayer的簡(jiǎn)單使用盒卸,運(yùn)用了media3-ui包中提供的關(guān)于ExoPlayer的UI組件和資源俄认。但是在日常開發(fā)中,播放器的界面會(huì)被要求為各式各樣的撼嗓,沒有辦法使用media3-ui包中提供的通用界面狭瞎。
在這篇文章將介紹如何自己實(shí)現(xiàn)一個(gè)簡(jiǎn)單的PlayerView细移。
demo下載
看一下最終的效果界面圖:
依賴項(xiàng)
在上一篇文章的依賴項(xiàng)基礎(chǔ)上,可以去除media3-ui的依賴熊锭,只需要一個(gè)androidx.media3:media3-exoplayer:1.0.0-beta02依賴即可弧轧。
基本要素
通過查看media3-ui庫(kù)中的PlayerView實(shí)現(xiàn),可以看到自定義播放界面需要的一些基本要素:
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:視頻緩沖時(shí)顯示。
errorMessageView:視頻播放異常時(shí)顯示仿粹。
controller:視頻播放控制界面搁吓,包括播放暫停操作原茅、播放進(jìn)度操作等。
播放事件的監(jiān)聽
通過player.addListener添加一個(gè)Player.Listener進(jìn)行播放事件的監(jiān)聽堕仔。Player.Listener有空的默認(rèn)方法擂橘,因此按需實(shí)現(xiàn)所需要的方法即可。
我們所需要實(shí)現(xiàn)的方法主要有以下幾個(gè):
- onVideoSizeChanged:獲取播放視頻的寬度和高度贮预,用于更新播放界面的寬高比例大小
- onPlayerError:播放異常情況的監(jiān)聽贝室,用于展示錯(cuò)誤界面
- 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
) {
//播放暫停會(huì)回調(diào)該方法,播放時(shí)playWhenReady為true
updateProgressState()
}
}
播放界面的顯示
media3-ui庫(kù)中的PlayerView對(duì)于視頻播放界面的顯示用了AspectRatioFrameLayout唤冈,我們可以直接復(fù)用這個(gè)布局峡迷,也可以自己簡(jiǎn)單的實(shí)現(xiàn)一個(gè)。
<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作為一個(gè)根布局你虹,利用constraintDimensionRatio屬性可以設(shè)置播放界面的寬高比例顯示绘搞。當(dāng)拿到視頻的寬度和高度之后,將播放界面的寬高比例更改為視頻自身的寬高比傅物。
private fun updateAspectRatio() {
player?.videoSize?.let { videoSize ->
if (videoSize.width > 0 && videoSize.height > 0) {
val width = videoSize.width
val height = videoSize.height
//更改比例夯辖,根據(jù)視頻自身寬高比展示
(binding.exoContentFrame.layoutParams as? LayoutParams)?.let {
it.dimensionRatio = "$width:$height"
binding.exoContentFrame.layoutParams = it
}
}
}
}
什么時(shí)候可以拿到視頻的寬度和高度呢?在上一篇文章中董饰,通過player.addListener添加一個(gè)Player.Listener進(jìn)行播放事件的監(jiān)聽蒿褂,可以監(jiān)聽到播放狀態(tài)、播放異常情況等卒暂,此外還有onVideoSizeChanged方法可以獲取到視頻的寬度和高度啄栓,在該方法中更新播放界面的寬高比例即可。
用于渲染視頻的Surface
我們需要一個(gè)SurfaceView用于渲染視頻:
- 創(chuàng)建一個(gè)SurfaceView:
private val surfaceView by lazy { SurfaceView(context) }
- 將SurfaceView添加到布局上:
binding.exoContentFrame.addView(surfaceView, 0)
- 渲染視頻:
player.setVideoSurfaceView(surfaceView)
視頻緩沖時(shí)顯示
視頻的播放會(huì)需要緩沖時(shí)間也祠,在上一篇文章中有介紹到播放監(jiān)聽onPlaybackStateChanged方法中會(huì)有視頻播放的四種播放狀態(tài):
- STATE_IDLE:初始狀態(tài)昙楚,此時(shí)播放器沒有可以播放的資源,播放器停止播放或者播放失敗后也會(huì)處于該狀態(tài)
- STATE_BUFFERING: 沒有足夠的數(shù)據(jù)可以加載播放诈嘿,此時(shí)無法立即播放
- STATE_READY : 播放器可以立即播放堪旧,是否播放取決于playWhenReady的值,該值表達(dá)了使用者的意愿永淌,為true崎场,將會(huì)開始播放,否則不播遂蛀。
- STATE_ENDED: 播放完了所有的資源后處于該狀態(tài)
在監(jiān)聽到STATE_READY狀態(tài)為止之前就是視頻的緩沖時(shí)間,進(jìn)行一個(gè)緩沖狀態(tài)的展示干厚,例如loading李滴,監(jiān)聽到STATE_READY狀態(tài)之后螃宙,隱藏緩沖狀態(tài)loading,展示視頻播放所坯。
視頻播放異常時(shí)顯示
在播放監(jiān)聽onPlayerError方法中可以監(jiān)聽到播放異常情況谆扎,此時(shí)需要展示播放異常界面,展示錯(cuò)誤信息和重新加載界面芹助,當(dāng)需要重新加載時(shí)調(diào)用player.prepare()方法堂湖。
視頻播放控制界面
視頻播放控制界面,包括播放暫停操作状土、播放進(jìn)度操作等跟用戶進(jìn)行互動(dòng)的操作无蜂。
播放暫停操作
根據(jù)播放器當(dāng)前狀態(tài)來進(jìn)行播放或者暫停的操作,同時(shí)更新操作界面展示蒙谓。
binding.tvPlay.setOnClickListener {
if (player.isPlaying) {
player.pause()
binding.tvPlay.text = "播放"
} else {
player.play()
binding.tvPlay.text = "暫停"
}
}
播放進(jìn)度操作
播放進(jìn)度是一個(gè)需要不斷自動(dòng)更新的狀態(tài)斥季,在播放監(jiān)聽中onPlaybackStateChanged方法和onPlayWhenReadyChanged方法中我們都需要調(diào)用更新播放進(jìn)度的方法。onPlaybackStateChanged是加載播放視頻到加載完成播放會(huì)回調(diào)該方法累驮,onPlayWhenReadyChanged是視頻播放或者暫停操作會(huì)回調(diào)該方法酣倾。
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
) {
//播放暫停會(huì)回調(diào)該方法,播放時(shí)playWhenReady為true
updateProgressState()
}
}
在更新播放進(jìn)度方法中谤专,我們通過handler不斷的發(fā)消息來進(jìn)行進(jìn)度的更新躁锡。當(dāng)視頻不是播放狀態(tài)時(shí)自然也是不需要更新播放進(jìn)度的。
/**
* 更新進(jìn)度條狀態(tài)
*/
private fun updateProgressState() {
if (player?.playbackState == Player.STATE_READY && (player?.isPlaying == true)) {
progressHandler.removeCallbacksAndMessages(null)
progressHandler.sendEmptyMessage(1)
} else {
//清空進(jìn)度
progressHandler.removeCallbacksAndMessages(null)
}
}
通過播放器相關(guān)方法可以獲取到視頻總時(shí)長(zhǎng)置侍、當(dāng)前加載時(shí)長(zhǎng)和當(dāng)前播放時(shí)長(zhǎng)映之,進(jìn)而進(jìn)行播放進(jìn)度的更新和展示。
private val progressHandler = object : Handler(Looper.myLooper()!!) {
override fun handleMessage(msg: Message) {
val currentPlayer = player
//獲取進(jìn)度并通知
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秒后自動(dòng)獲取進(jìn)度
sendEmptyMessageDelayed(1, 500)
}
}
}
如果播放進(jìn)度條可以進(jìn)行拖拽從而達(dá)到操作播放進(jìn)度墅垮,只要使用player.seekTo()方法就可以指定播放器播放進(jìn)度惕医。
資源釋放
當(dāng)不再需要播放器時(shí),記得釋放資源算色,由于使用了handler發(fā)消息來進(jìn)行播放進(jìn)度的更新抬伺,所以也需要對(duì)handler進(jìn)行資源釋放:
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
//釋放資源
try {
player?.setVideoSurfaceView(null)
player?.stop()
player?.release()
player?.removeListener(componentListener)
progressHandler.removeCallbacksAndMessages(null)
} catch (e: Exception) {
e.printStackTrace()
}
}
前后臺(tái)切換播放優(yōu)化
如果不采取任何操作,在進(jìn)行后臺(tái)切至前臺(tái)的操作時(shí)灾梦,會(huì)出現(xiàn)黑屏的情況峡钓,因此當(dāng)切至后臺(tái)時(shí),對(duì)播放狀態(tài)進(jìn)行一個(gè)保存若河,并暫停播放器能岩,當(dāng)切回前臺(tái)時(shí),恢復(fù)播放器之前的播放狀態(tài)萧福,可以在onWindowVisibilityChanged方法中進(jìn)行該優(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()
}
}
}