JetPack知识点实战系列十:ExoPlayer进行视频播放的实现
本节教程我们将来介绍下ExoPlayer的视频播放功能。
我们在本节将主要介绍以下知识点:
- ExoPlayer高级自定义的实现
- 视频的全屏播放和退出全屏播放
- ExoPlayer在RecyclerView中的复用
ExoPlayer介绍
MediaPlayer和ExoPlayer是Google官方支持的两种播放器,但是ExoPlayer比MediaPlayer多了支持基于 HTTP 的动态自适应流 (DASH)、SmoothStreaming 和通用加密等功能。
并且重要的是它独立于Android代码框架,以一个开源代码库的形式存在,所以在自定义上更有优势。
ExoPlayer简单的使用方法
- 引入依赖库
implementation 'com.google.android.exoplayer:exoplayer:2.12.0'
- 布局中引入PlayerView
播放视频我们需要使用PlayerView,我们简单来看下PlayerView的源码,其继承于FrameLayout,其中有三个重要的属性,
public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider {@Nullable private final View surfaceView;@Nullable private final PlayerControlView controller;private Player player;
}
surfaceView
是呈现视频的View,可以是TextureView,SurfaceView, 默认是SurfaceView。controller
是播放控制的View,上面提供一些控件可以控制视频的播放,暂停,显示当前进度等。默认是PlayerControlView。player
是视频的播放器,在构造函数初始化的时候没有赋值,需要单独设置。
总结:PlayerView通过
player
播放视频显示在surfaceView
上,用户可以通过提供的controller
进行播放的控制。
介绍了基本的知识点后,我们在布局文件中引入PlayerView:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".DefaultViewActivity"><com.google.android.exoplayer2.ui.PlayerViewandroid:id="@+id/video_player"android:layout_width="match_parent"android:layout_height="match_parent"app:show_buffering="always"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent"/></androidx.constraintlayout.widget.ConstraintLayout>
- 设置播放器
val player: SimpleExoPlayer = SimpleExoPlayer.Builder(this@MainActivity).build().also { it.playWhenReady = true }
video_player.player = player
我们前面提到PlayerView的两个属性在构造函数调用时赋值了,但是player没有,需要主动设置。这里我们设置成SimpleExoPlayer对象。
SimpleExoPlayer是库中提供的播放器,可以直接使用。
- 设置播放源
// play item
val uri = Uri.parse("https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4")
val dataSourceFactory = DefaultHttpDataSourceFactory()
val videoSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri)
// prepare
player.prepare(videoSource)
- 监听播放器的状态
我们可以监听播放器的状态,代码如下:
player.addListener(object: Player.EventListener {override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {Log.d("JJMusic","playWhenReady: $playWhenReady playbackState: $playbackState")when (playbackState) {Player.STATE_BUFFERING ->Log.d("JJMusic","加载中")Player.STATE_READY ->Log.d("JJMusic","准备完毕")Player.STATE_ENDED ->Log.d("JJMusic","播放完成")}}override fun onPlayerError(error: ExoPlaybackException) {Log.e("JJMusic","ExoPlaybackException: $error")}
})
最后得到的效果如下所示:
ExoPlayer简单自定义
我们目前使用的是默认的播放控制布局文件,我们可以修改播放的布局文件达到自定义效果。
- 自定义播放控制的布局文件
假设我们把布局文件设计如下所示:
<!-- layout_video_simple.xml -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/constraint"android:layout_width="match_parent"android:layout_height="match_parent"><ImageViewandroid:id="@+id/exo_play"android:layout_width="wrap_content"android:layout_height="wrap_content"android:contentDescription="@null"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:srcCompat="@mipmap/exo_btn_play" /><ImageViewandroid:id="@+id/exo_pause"android:layout_width="wrap_content"android:layout_height="wrap_content"android:contentDescription="@null"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:srcCompat="@mipmap/exo_btn_pause" /><TextViewandroid:id="@+id/exo_position"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginStart="6dp"android:layout_marginBottom="12dp"android:contentDescription="@null"android:text="1"android:textColor="@color/colorPrimary"android:textSize="12sp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toStartOf="parent" /><TextViewandroid:id="@+id/splash_tv"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginStart="1dp"android:layout_marginBottom="12dp"android:contentDescription="@null"android:text="/"android:textColor="@color/colorPrimary"android:textSize="12sp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toEndOf="@+id/exo_position"tools:text="/" /><TextViewandroid:id="@+id/exo_duration"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginStart="1dp"android:layout_marginBottom="12dp"android:contentDescription="@null"android:text="1"android:textColor="@color/colorPrimary"android:textSize="12sp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toEndOf="@+id/splash_tv" /><com.google.android.exoplayer2.ui.DefaultTimeBarandroid:id="@+id/exo_progress"android:layout_width="0dp"android:layout_height="15dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:bar_height="2dp"app:unplayed_color="@color/exo_gray_ripple"app:played_color="@color/colorAccent"app:scrubber_color="@color/colorAccent"app:buffered_color="@color/colorPrimary"/></androidx.constraintlayout.widget.ConstraintLayout>
- id为
exo_play
的按钮和id为exo_pause
的按钮在屏幕正中间位置 - id为
exo_position
的文本和id为exo_duration
的文本在左下角 - id为
exo_progress
的进度条在最底部。进度条的类是DefaultTimeBar,可以设置一些属性。譬如上面的bar_height
(进度条的高度),unplayed_color
(未缓冲部分的颜色),played_color
(已播放部分的颜色)和buffered_color
(已缓冲完部分的颜色)等等。
注意:这些id是PlayerControlView源代码中能找到的id,否则是没有效果的。
- 修改PlayerView布局文件
<com.google.android.exoplayer2.ui.PlayerView...app:controller_layout_id="@layout/layout_video_simple"/>
其他的和前面的类似,只是加了个属性controller_layout_id
,值为我们刚才设计的布局文件layout_video_simple。
简单自定义得到的效果如下所示:
ExoPlayer高级自定义
简单的自定义我们只是更改了PlayerControlView的布局文件,复用了其中的id
,能修改的很有限,没有涉及到源代码的修改。
高级自定义就需要修改源代码了。其实就是修改PlayerView,PlayerControlView,甚至是TimeBar的源代码。
接下来我们就用高级自定义来实现下网易云音乐的全屏播放功能,需要的效果如下:
- 修改PlayerControlView
新建一个JJPlayerControlView类,然后将PlayerControlView所有源代码拷贝在这个类中。
public class JJPlayerControlView extends FrameLayout {// PlayerControlView内容
}
接下来在JJPlayerControlView中加入一个全屏按钮属性。
public class JJPlayerControlView extends FrameLayout {// 全屏按钮private final ImageButton maxButton;// PlayerControlView内容public JJPlayerControlView(Context context,@Nullable AttributeSet attrs,int defStyleAttr,@Nullable AttributeSet playbackAttrs) {...maxButton = findViewById(R.id.exo_max_btn);if (maxButton != null) {maxButton.setOnClickListener(componentListener);}...}
}
- 修改PlayerView
新建一个JJPlayerView类,然后将PlayerView所有源代码拷贝在这个类中。
public class JJPlayerView extends FrameLayout implements AdsLoader.AdViewProvider {
// PlayerView的内容
}
将JJPlayerView的controller
指定为JJPlayerControlView,即:
public class JJPlayerView extends FrameLayout implements AdsLoader.AdViewProvider {@Nullable private final JJPlayerControlView controller;// PlayerView的其他内容
}
- 修改TimeBar
如果需要修改进度条,新建一个JJTimeBar类,然后将DefaultTimeBar所有源代码拷贝在这个类中。
public class JJTimeBar extends View implements TimeBar {...
}
当然修改将JJPlayerControlView中的timeBar
改为JJTimeBar类。
public class JJPlayerControlView extends FrameLayout {// 全屏按钮private final ImageButton maxButton;// 自定义进度条@Nullable private JJTimeBar timeBar;// PlayerControlView内容public JJPlayerControlView(Context context,@Nullable AttributeSet attrs,int defStyleAttr,@Nullable AttributeSet playbackAttrs) {...maxButton = findViewById(R.id.exo_max_btn);if (maxButton != null) {maxButton.setOnClickListener(componentListener);}...}
}
- 修改JJPlayerControlView布局文件
layout_video_recyclerview.xml相对前面,我们多添加了一个id
为exo_max_btn的按钮。
为了看的更加明显,我把其他的按钮或者文本的id都改了,不再使用默认的id,这时候为了找到对应的控件,就需要修改对应的源代码了。譬如我把播放按钮的id改为了exo_play_btn
。
public class JJPlayerControlView extends FrameLayout {// 代码修改playButton = findViewById(R.id.exo_play_btn);if (playButton != null) {playButton.setOnClickListener(componentListener);}
}
- JJPlayerView布局文件
JJPlayerView使用JJPlayerControlView自定义的布局文件
<com.johnny.jjmusic.exoplayer.JJPlayerViewxmlns: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="match_parent"app:show_buffering="always"app:controller_layout_id="@layout/layout_video_recyclerview"></com.johnny.jjmusic.exoplayer.JJPlayerView>
- 全屏和退出全屏的实现逻辑
我们先来看一张图就能很清晰的了解全屏和退出全屏的逻辑了:
全屏的时候JJPlayerView放在Activity的R.id.content上,隐藏ActionBar,切换成横屏显示,退出全屏的时候就重新放在RecyclerView的ItemView上,显示ActionBar,切换成竖屏显示。
所以最后很简单,只要处理maxButton点击事件时实现这个功能就可以了。
进入全屏播放
fun enterFullScreen() {// 横竖屏状态判断if (viewModel.playMode == VideoPlayMode.MODE_FULL_SCREEN) return// 隐藏ActionBarplayerView.context.hideActionBar()// 旋转屏幕playerView.context.activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE// 将JJPlayerView从RecyclerView移除,加入Activity的R.id.content下playerView.context.activity?.let {val contentView = it.findViewById<ViewGroup>(android.R.id.content)// removeremovePlayerView()viewModel.isVideoViewAdded = true// addval params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)contentView.addView(playerView, params)val frameParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)playerView.controller?.timeBarContainer?.addView(timeBar, frameParams)viewModel.playMode = VideoPlayMode.MODE_FULL_SCREEN}}
退出全屏播放
/* 退出全屏 */fun exitFullScreen() {// 横竖屏状态判断if (viewModel.playMode == VideoPlayMode.MODE_NORMAL) return// 显示ActionBarplayerView.context.showActionBar()// 旋转屏幕playerView.context.activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT// 将JJPlayerView从Activity的R.id.content移除,加入RecyclerView的ItemView下playerView.context.activity?.let {// removeval contentView = it.findViewById<ViewGroup>(android.R.id.content)contentView.removeView(playerView)playerView.controller?.timeBarContainer?.removeView(timeBar)// addviewModel.viewModelScope.launch {delay(100)addPlayerView()}viewModel.playMode = VideoPlayMode.MODE_NORMAL}
}
上面代码中涉及到的几个扩展方法,也一同贴出来:
//----------Activity----------
val Context.activity: Activity?get() {return when (this) {is Activity -> {this}is ContextWrapper -> {this.baseContext.activity}else -> {null}}}val Context.appCompActivity: AppCompatActivity?get() {return when (this) {is AppCompatActivity -> {this}is ContextThemeWrapper -> {this.baseContext.appCompActivity}else -> {null}}}//---------- ActionBar ----------
@SuppressLint("RestrictedApi")
fun Context.showActionBar() {this.appCompActivity?.supportActionBar?.let {it.setShowHideAnimationEnabled(false)it.show()}this.activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
}@SuppressLint("RestrictedApi")
fun Context.hideActionBar() {this.appCompActivity?.supportActionBar?.let {it.setShowHideAnimationEnabled(false)it.hide()}this.activity?.window?.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
}
至此ExoPlayer的高级自定义就到此为止了。
由于可以修改源码,所以进行高度自定义就变得可实现了。当然是在熟悉源码的前提下进行修改。
ExoPlayer在RecyclerView中的复用
上面的实现效果中,我们点击RecyclerView不同的Item,都能播放视频,如果每个ItemView都有一个PlayerView那是非常不合适的。对PlayerView复用是一个非常合适的解决方案。
其实这个解决方案和全屏的方案也非常相似,就是将PlayerView在不同的Item中移除和加入。然后播放新的视频。
其中有一些细节需要处理,譬如播放的进度需要记录下来,下次再点击的时候从上次停止的地方进行播放。还譬如需要监听RecyclerView.OnChildAttachStateChangeListener,当执行onChildViewDetachedFromWindow时候,如果在播放需要将播放器停止。等等
有了思路,解决起来也就很简单了。这里不再贴代码了。
JetPack知识点实战系列十:ExoPlayer进行视频播放的实现相关推荐
- JetPack知识点实战系列十一:MotionLayout让动画如此简单
MotionLayout是ConstraintLayout的子类,所以它是一种布局类型,但是它能够为布局属性添加动画效果,是开发者实现动画效果的另一个新的选择. MotionLayout基础 让动画跑 ...
- 【Youtobe trydjango】Django2.2教程和React实战系列十【动态路由、app内部路由】
[Youtobe trydjango]Django2.2教程和React实战系列十[动态路由.app内部路由] 1. 动态路由示例 1.1 动态路由 1.2 处理DoesNotExist不存在 2. ...
- xen是服务器虚拟化,xen虚拟化实战系列(十二)之xen虚拟机高可用之在线迁移
xen虚拟化实战系列文章列表 xen虚拟化实战系列(十三)之xen虚拟机集中管理之convirt 1. 方案背景概述 本文是有对我们一个xen虚拟化生产环境将要改造的一个方案而来,在项目上线初期,没有 ...
- 云计算实战系列十五(SQL I)
一.MySQL数据库表操作 MySQL表的基本概念 在windows中有个程序叫做excel. 而Excel文件中存在了如sheet1.sheet2.sheet3的表, 所有的sheet都存储在这个E ...
- 云计算实战系列十四(MySQL基础)
一.Mysql开篇 1.1.MySQL数据库介绍 什么是数据库DB? 数据库无处不在 DB的全称是database,即数据库的意思.数据库实际上就是一个文件集合,是一个存储数据的仓库,数据库是按照特定 ...
- 云计算实战系列十(文件查找及包管理)
文件查找 知识点 grep: 文件内容过滤 find : 文件查找,针对文件名 xargs 文件打包及压缩 gzip bzip2 xz unzip(了解) 1.1 命令文件 # which ls // ...
- openstack运维实战系列(十)之nova指定compute节点和IP地址
1. 背景需求 在openstack中,nova负责openstack虚拟机的生命周期的管理,neutron则负责虚拟机的网络管理工作,默认情况下,创建一台虚拟机,nova会根据nova-schedu ...
- 云计算实战系列十六(SQL II)
1.3 MySQL数据操作DML 在MySQL管理软件中,可以通过SQL语句中的DML语言来实现数据的操作,包括使用INSERT实现数据 的插入.DELETE实现数据的删除以及UPDATE实现数据的更 ...
- 云计算实战系列十二(Linux系统优化)
Linux高级系统优化 uptime 命令 [root@newrain ~]# uptime 14:01:51 up 1 day, 20:11, 3 users, load average: 0.13 ...
- xen虚拟化实战系列(六)之xen虚拟机破解密码
xen虚拟化实战系列文章列表 xen虚拟化实战系列(一)之xen虚拟化环境安装 xen虚拟化实战系列(二)之xen虚拟机安装 xen虚拟化实战系列(三)之xen虚拟机复制 xen虚拟化实战系列(四)之 ...
最新文章
- git : 依赖: liberror-perl 但无法安装它
- 获取指定日期之间的各个周和月
- java excel导出 jxl_java使用JXL导出Excel及合并单元格
- keras回调监控函数
- 连接虚拟机mysql无法访问_连接虚拟机mysql无法访问,报错编号1130的解决方法
- PowerDesigner导入SQL脚本
- 技术交流论坛_天气预报|“第一届国家建筑工程与材料测试技术论坛”暨“第七届全国建筑材料测试技术”交流会...
- 关于linux系统端口查看和占用的解决方案
- 计算机专业课程项目教学教学设计,高职旅游管理专业计算机课程项目化教学设计...
- java程序设计俄罗斯方块_Java俄罗斯方块实现代码
- 想学制作外挂的新手看过来
- Python——第一天的Suger Rush
- android 多媒体列表,android – 使用Exoplayer的流媒体视频列表
- KafkaController机制(六):Zookeeper Listener之TopicDeletionManager与DeleteTopicsListener
- 支持DDR5,超频更简单,小雕够给力,技嘉B760M小雕WIFI主板上手
- JS轮播图(网易云轮播图)
- Java航班预订统计leetcode_1109
- Python爬虫编程思想(48):项目实战:抓取起点中文网的小说信息
- 网络对抗技术---实验一
- 程序员老黄历Java源码实现