本节教程我们将来介绍下ExoPlayer的视频播放功能。

我们在本节将主要介绍以下知识点:

  1. ExoPlayer高级自定义的实现
  2. 视频的全屏播放和退出全屏播放
  3. ExoPlayer在RecyclerView中的复用

ExoPlayer介绍

MediaPlayerExoPlayer是Google官方支持的两种播放器,但是ExoPlayerMediaPlayer多了支持基于 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;
}
  1. surfaceView是呈现视频的View,可以是TextureViewSurfaceView, 默认是SurfaceView
  2. controller是播放控制的View,上面提供一些控件可以控制视频的播放,暂停,显示当前进度等。默认是PlayerControlView
  3. 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>
  1. idexo_play的按钮和idexo_pause的按钮在屏幕正中间位置
  2. idexo_position的文本和idexo_duration的文本在左下角
  3. idexo_progress的进度条在最底部。进度条的类是DefaultTimeBar,可以设置一些属性。譬如上面的bar_height(进度条的高度),unplayed_color(未缓冲部分的颜色),played_color(已播放部分的颜色)和buffered_color(已缓冲完部分的颜色)等等。

注意:这些idPlayerControlView源代码中能找到的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,能修改的很有限,没有涉及到源代码的修改。

高级自定义就需要修改源代码了。其实就是修改PlayerViewPlayerControlView,甚至是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的内容
}

JJPlayerViewcontroller指定为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相对前面,我们多添加了一个idexo_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放在ActivityR.id.content上,隐藏ActionBar,切换成横屏显示,退出全屏的时候就重新放在RecyclerViewItemView上,显示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进行视频播放的实现相关推荐

  1. JetPack知识点实战系列十一:MotionLayout让动画如此简单

    MotionLayout是ConstraintLayout的子类,所以它是一种布局类型,但是它能够为布局属性添加动画效果,是开发者实现动画效果的另一个新的选择. MotionLayout基础 让动画跑 ...

  2. 【Youtobe trydjango】Django2.2教程和React实战系列十【动态路由、app内部路由】

    [Youtobe trydjango]Django2.2教程和React实战系列十[动态路由.app内部路由] 1. 动态路由示例 1.1 动态路由 1.2 处理DoesNotExist不存在 2. ...

  3. xen是服务器虚拟化,xen虚拟化实战系列(十二)之xen虚拟机高可用之在线迁移

    xen虚拟化实战系列文章列表 xen虚拟化实战系列(十三)之xen虚拟机集中管理之convirt 1. 方案背景概述 本文是有对我们一个xen虚拟化生产环境将要改造的一个方案而来,在项目上线初期,没有 ...

  4. 云计算实战系列十五(SQL I)

    一.MySQL数据库表操作 MySQL表的基本概念 在windows中有个程序叫做excel. 而Excel文件中存在了如sheet1.sheet2.sheet3的表, 所有的sheet都存储在这个E ...

  5. 云计算实战系列十四(MySQL基础)

    一.Mysql开篇 1.1.MySQL数据库介绍 什么是数据库DB? 数据库无处不在 DB的全称是database,即数据库的意思.数据库实际上就是一个文件集合,是一个存储数据的仓库,数据库是按照特定 ...

  6. 云计算实战系列十(文件查找及包管理)

    文件查找 知识点 grep: 文件内容过滤 find : 文件查找,针对文件名 xargs 文件打包及压缩 gzip bzip2 xz unzip(了解) 1.1 命令文件 # which ls // ...

  7. openstack运维实战系列(十)之nova指定compute节点和IP地址

    1. 背景需求 在openstack中,nova负责openstack虚拟机的生命周期的管理,neutron则负责虚拟机的网络管理工作,默认情况下,创建一台虚拟机,nova会根据nova-schedu ...

  8. 云计算实战系列十六(SQL II)

    1.3 MySQL数据操作DML 在MySQL管理软件中,可以通过SQL语句中的DML语言来实现数据的操作,包括使用INSERT实现数据 的插入.DELETE实现数据的删除以及UPDATE实现数据的更 ...

  9. 云计算实战系列十二(Linux系统优化)

    Linux高级系统优化 uptime 命令 [root@newrain ~]# uptime 14:01:51 up 1 day, 20:11, 3 users, load average: 0.13 ...

  10. xen虚拟化实战系列(六)之xen虚拟机破解密码

    xen虚拟化实战系列文章列表 xen虚拟化实战系列(一)之xen虚拟化环境安装 xen虚拟化实战系列(二)之xen虚拟机安装 xen虚拟化实战系列(三)之xen虚拟机复制 xen虚拟化实战系列(四)之 ...

最新文章

  1. git : 依赖: liberror-perl 但无法安装它
  2. 获取指定日期之间的各个周和月
  3. java excel导出 jxl_java使用JXL导出Excel及合并单元格
  4. keras回调监控函数
  5. 连接虚拟机mysql无法访问_连接虚拟机mysql无法访问,报错编号1130的解决方法
  6. PowerDesigner导入SQL脚本
  7. 技术交流论坛_天气预报|“第一届国家建筑工程与材料测试技术论坛”暨“第七届全国建筑材料测试技术”交流会...
  8. 关于linux系统端口查看和占用的解决方案
  9. 计算机专业课程项目教学教学设计,高职旅游管理专业计算机课程项目化教学设计...
  10. java程序设计俄罗斯方块_Java俄罗斯方块实现代码
  11. 想学制作外挂的新手看过来
  12. Python——第一天的Suger Rush
  13. android 多媒体列表,android – 使用Exoplayer的流媒体视频列表
  14. KafkaController机制(六):Zookeeper Listener之TopicDeletionManager与DeleteTopicsListener
  15. 支持DDR5,超频更简单,小雕够给力,技嘉B760M小雕WIFI主板上手
  16. JS轮播图(网易云轮播图)
  17. Java航班预订统计leetcode_1109
  18. Python爬虫编程思想(48):项目实战:抓取起点中文网的小说信息
  19. 网络对抗技术---实验一
  20. 程序员老黄历Java源码实现

热门文章

  1. 软件工程作业——《人件》读书笔记
  2. Recoil 的使用
  3. win7+linux双系统安装
  4. DM数据库全面调优指南之Linux操作系统
  5. vue中你真的理解v-modle基础理解和使用吗?
  6. html超链接字体颜色怎么改DW,如何利用Dreamweaver设计彩色文字链接
  7. 企业微信之发送应用消息案例
  8. PS图片中字体或图像的颜色替换
  9. idear配置工具上传Jar包到服务器并运行
  10. 手把手带你免费打嘉立创pcb板