AndroidX Media3之ExoPlayer简单使用(2)

在上一篇文章AndroidX Media3之ExoPlayer简单使用(1)中介绍了ExoPlayer的简单使用,运用了media3-ui包中提供的关于ExoPlayer的UI组件和资源。但是在日常开发中,播放器的界面会被要求为各式各样的,没有办法使用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:用于播放事件的监听,包括有播放状态、播放异常情况等。
contentFrame:播放界面的宽高比例大小等。
surfaceView:用于渲染视频的Surface。
bufferingView:视频缓冲时显示。
errorMessageView:视频播放异常时显示。
controller:视频播放控制界面,包括播放暂停操作、播放进度操作等。

播放事件的监听

通过player.addListener添加一个Player.Listener进行播放事件的监听。Player.Listener有空的默认方法,因此按需实现所需要的方法即可。
我们所需要实现的方法主要有以下几个:

  • onVideoSizeChanged:获取播放视频的宽度和高度,用于更新播放界面的宽高比例大小
  • onPlayerError:播放异常情况的监听,用于展示错误界面
  • 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()
        }
    }

播放界面的显示

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进行播放事件的监听,可以监听到播放状态、播放异常情况等,此外还有onVideoSizeChanged方法可以获取到视频的宽度和高度,在该方法中更新播放界面的宽高比例即可。

用于渲染视频的Surface

我们需要一个SurfaceView用于渲染视频:

  • 创建一个SurfaceView:
private val surfaceView by lazy { SurfaceView(context) }
  • 将SurfaceView添加到布局上:
binding.exoContentFrame.addView(surfaceView, 0)
  • 渲染视频:
player.setVideoSurfaceView(surfaceView)

视频缓冲时显示

视频的播放会需要缓冲时间,在上一篇文章中有介绍到播放监听onPlaybackStateChanged方法中会有视频播放的四种播放状态:

  • STATE_IDLE:初始状态,此时播放器没有可以播放的资源,播放器停止播放或者播放失败后也会处于该状态
  • STATE_BUFFERING: 没有足够的数据可以加载播放,此时无法立即播放
  • STATE_READY : 播放器可以立即播放,是否播放取决于playWhenReady的值,该值表达了使用者的意愿,为true,将会开始播放,否则不播。
  • STATE_ENDED: 播放完了所有的资源后处于该状态

在监听到STATE_READY状态为止之前就是视频的缓冲时间,进行一个缓冲状态的展示,例如loading,监听到STATE_READY状态之后,隐藏缓冲状态loading,展示视频播放。

视频播放异常时显示

在播放监听onPlayerError方法中可以监听到播放异常情况,此时需要展示播放异常界面,展示错误信息和重新加载界面,当需要重新加载时调用player.prepare()方法。

视频播放控制界面

视频播放控制界面,包括播放暂停操作、播放进度操作等跟用户进行互动的操作。

播放暂停操作

根据播放器当前状态来进行播放或者暂停的操作,同时更新操作界面展示。

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

播放进度操作

播放进度是一个需要不断自动更新的状态,在播放监听中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不断的发消息来进行进度的更新。当视频不是播放状态时自然也是不需要更新播放进度的。

    /**
     * 更新进度条状态
     */
    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发消息来进行播放进度的更新,所以也需要对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()
        }
    }

前后台切换播放优化

如果不采取任何操作,在进行后台切至前台的操作时,会出现黑屏的情况,因此当切至后台时,对播放状态进行一个保存,并暂停播放器,当切回前台时,恢复播放器之前的播放状态,可以在onWindowVisibilityChanged方法中进行该优化操作:

    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()
            }
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,392评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,258评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,417评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,992评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,930评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,199评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,652评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,327评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,463评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,382评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,432评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,118评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,704评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,787评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,999评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,476评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,057评论 2 341

推荐阅读更多精彩内容