续 androidx ViewPager2 实现横向、纵向滑动播放短视频(一)
播放器的选择
上篇已经写到,用 androidx Viewpager2 基本实现了横向、纵向滑动展现视图数据的基本需求,接下来要做的就是播放短视频,期初用的播放器最终被放弃了,最终更换成ExoPlayer,ExoPalyer Github完成项目 ,ExoPalyer doc。
ExoPlayer 功能强大,使用方便灵活,按照项目说明 配置即可使用,遇到的一些问题:
我使用的是当前最新版本2.12.0,刚开始无法播放m3u8视频流,是因为我没引入exoplayer-hls,后来无法播放rtmp直播流,是因为我没引入extension-rtmp,注意extension-的版本要和exoplayer-的版本保持一致,关于extension- 看下图红色,点击进去就能看到支持的所有扩展
根据实际项目需要,去引入各个依赖,关于HLS,RTMP,DASH 这些流媒体协议可以去查询学习了解。
// ExoPlayer
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.0'
// implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.0'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.0'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.0'
implementation 'com.google.android.exoplayer:extension-rtmp:2.12.0'
Java和kotlin 混合开发时增加kotlin的配置
compileOptions {
targetCompatibility = 1.8
sourceCompatibility = 1.8
}
kotlinOptions {
jvmTarget = '1.8'
}
播放器的使用
参考ExoPalyer doc使用,抽离出来的简单大概代码如下:
一、播放器的初始化,包括根据需求添加监听器
/**
* 初始化播放器
*/
private fun initExoPlayer() {
exoPlayer = SimpleExoPlayer.Builder(this.activity!!.applicationContext).build()
exoPlayer?.repeatMode = Player.REPEAT_MODE_ALL
exoPlayer?.addVideoListener(object : VideoListener {
override fun onVideoSizeChanged(width: Int, height: Int, unappliedRotationDegrees: Int, pixelWidthHeightRatio: Float) {
super.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio)
if (currentVideoHolder == null) return
if (width < height) {
currentVideoHolder!!.playerView!!.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM
} else {
currentVideoHolder!!.playerView!!.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
}
}
})
exoPlayer?.addListener(object : Player.EventListener {
override fun onPlayerError(error: ExoPlaybackException) {
super.onPlayerError(error)
ToastUtils.showCommonToast(activity!!.applicationContext, "播放出错!")
//LogUtils.
}
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
if (currentVideoHolder == null) return
if (playWhenReady) {
currentVideoHolder!!.pauseIv?.visibility = View.GONE
} else {
when (reason) {
Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST -> {
currentVideoHolder!!.pauseIv?.visibility = View.VISIBLE
}
}
}
}
override fun onPlaybackStateChanged(state: Int) {
super.onPlaybackStateChanged(state)
when (state) {
Player.STATE_IDLE -> { //the state when the player is stopped, and when playback failed
LogUtils.e("Player.STATE_IDLE:index=" + currentVideoHolder!!.layoutPosition)
currentVideoHolder!!.shortVideoCoverView?.visibility = View.VISIBLE
}
Player.STATE_BUFFERING -> { LogUtils.e("Player.STATE_BUFFERING:startTime=${System.currentTimeMillis()};index=${currentVideoHolder!!.layoutPosition}")
// currentVideoHolder!!.shortVideoCoverView?.visibility = View.VISIBLE
currentVideoHolder!!.shortVideoLoadingView?.visibility = View.VISIBLE
}
Player.STATE_READY -> { LogUtils.e("Player.STATE_READY:endTime=${System.currentTimeMillis()};index=${currentVideoHolder!!.layoutPosition}")
currentVideoHolder!!.shortVideoCoverView?.visibility = View.GONE
currentVideoHolder!!.shortVideoLoadingView?.visibility = View.GONE
}
Player.STATE_ENDED -> {
LogUtils.e("Player.STATE_ENDED:index=" + currentVideoHolder!!.layoutPosition)
}
}
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
if (isPlaying) {// Active playback.
currentVideoHolder!!.pauseIv?.visibility = View.GONE
} else {
// Not playing because playback is paused, ended, suppressed, or the player
// is buffering, stopped or failed. Check player.getPlayWhenReady,
// player.getPlaybackState, player.getPlaybackSuppressionReason and
// player.getPlaybackError for details.
}
}
})
}
exoPlayer?.repeatMode = Player.REPEAT_MODE_ALL 控制循环播放短视频;添加addVideoListener,重写onVideoSizeChanged方法,适配横竖屏的短视频;添加addListener,重写onPlayerError方法处理播放错误,重写onPlayWhenReadyChanged方法,处理playWhenReady的改变,具体reason值,可以点到源码看一下,重写onPlaybackStateChanged方法,处理的状态很多,每个状态值都能从源码里详细看到。
二、播放器的播放控制
由于是横向、纵向滑动播放短视频,必须控制好播放器的暂停以及恢复,无论是Fragment还是Activity,维护一个ExoPlayer就可以了。
纵向滑动时,当前RecyclerView.Adapter 缓存下视图的Holder—currentVideoHolder,拿视图Holder得先拿到当前ViewPager2的RecyclerView:
recyclerView = videoPlayPager!!.getChildAt(0) as RecyclerView
然后就可以缓存当前的视图Holder
currentVideoHolder = recyclerView!!.findViewHolderForLayoutPosition(position) as ShortVideoExoPlayAdapter.RecyclerHolder
把ExoPlayer赋值给当前holderd的playerView的player 就可以了。
具体的播放、暂停时机,我刚开始处理是通过RecyclerView.Adapter的以下两个方法:
override fun onViewAttachedToWindow(holder: ShortVideoExoPlayAdapter.RecyclerHolder) {
}
override fun onViewDetachedFromWindow(holder: ShortVideoExoPlayAdapter.RecyclerHolder) {
}
后来发现不靠谱,改为通过ViewPager2的registerOnPageChangeCallback 重写onPageSelected方法控制播放和暂停,这样当你慢慢滑动界面,只要没有完成当前Page选择,就不会影响你的播放、暂停逻辑,大致代码简单如下:
videoPlayPager!!.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
onHandlePageSelected(position)
}
})
/**
* 处理ViewPager选择
*/
private fun onHandlePageSelected(position: Int) {
if (currentVideoHolder != null) {// 界面当前为活动状态
stopCurrentStartNextPlay(position)
} else {
currentVideoHolder = recyclerView!!.findViewHolderForLayoutPosition(position) as ShortVideoExoPlayAdapter.RecyclerHolder
currentVideoHolder!!.playerView!!.player = exoPlayer
exoPlayer?.setMediaItem(currentVideoHolder!!.mediaItem!!)
exoPlayer?.prepare()
exoPlayer?.play()
if (!this.isResumed) {
exoPlayer?.pause()
}
}
if (position == shortVideoPlayListAdapter!!.data.size - 1) {//加载视频列表
}
// 视频播放上报
}
/**
* 停止当前 播放下一个
*/
private fun stopCurrentStartNextPlay(position: Int) {
exoPlayer?.stop(true)
currentVideoHolder!!.playerView!!.player = null
currentVideoHolder = recyclerView!!.findViewHolderForLayoutPosition(position) as ShortVideoExoPlayAdapter.RecyclerHolder
currentVideoHolder!!.playerView!!.player = exoPlayer
exoPlayer?.setMediaItem(currentVideoHolder!!.mediaItem!!)
exoPlayer?.prepare()
exoPlayer?.play()
}
以上为纵向滑动播放短视频的大概控制,不要担心Anr哦,滑动选择到某一个,先通过currentVideoHolder判断当前是否存在播放,存在则先停止再去播放选择的;这里可能还会遇到跳转定位播放的问题,比如当前数据列表20,点击第12个跳转到播放界面,需要延迟处理一下,因为RecyclerView.Adapter Holder的创造机制,当前Holder 为null呢:
Handler().postDelayed({
currentVideoHolder = recyclerView!!.findViewHolderForLayoutPosition(position) as ShortVideoExoPlayAdapter.RecyclerHolder
currentVideoHolder!!.playerView!!.player = exoPlayer
exoPlayer?.setMediaItem(currentVideoHolder!!.mediaItem!!)
exoPlayer?.prepare()
exoPlayer?.play()
}, 150)
播放、暂停通过属性playWhenReady控制:
override fun onPause() {
super.onPause()
exoPlayer?.playWhenReady = false
}
override fun onResume() {
super.onResume()
exoPlayer?.playWhenReady = true
//检查用户关注状态
}
override fun onDestroy() {
super.onDestroy()
exoPlayer?.stop(true)
exoPlayer?.release()
}
横向滑动的时候,当前Fragment的播放器同样用onPause()和onResume()控制,这里如果横向慢慢滑动的时候,容易同时播放两个类别的短视频,所以在onHandlePageSelected方法里增加以下代码,如果Fragment的状态不是resumed,pause一下即可,当然了也可以不是resumed不去播放。
if (!this.isResumed) {
exoPlayer?.pause()
}
最外层视图如果是Fragment被主界面通过show()、hide()控制,可以重写onHiddenChanged方法控制播放暂停:
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if (hidden){
videoTypeFragmentAdapter?.onPausePlayVideo(videoType!!)
}else{
videoTypeFragmentAdapter?.onResumePlayVideo(videoType!!)
}
}
到这里横向、纵向滑动播放短视频的控制基本完成,阿门!!!