Android中实现视频播放器的途径有两种:
- 使用
VideoView
- 通过
MediaPlayer
+SurfaceView
/TextureView
1. VideoView
VideoView
使用比较简单,配合MediaController
可以达到控制播放、暂停、快进、快退、切换视频、进度条显示等,具体使用在这里不在赘述了。
2. MediaPlayer + SurfaceView / TextureView
实现一个相对完善的视频播放器,可以使用 MediaPlayer+SurfaceView
/MediaPlayer+TextureView
。
这里为什么有两种方式呢?然后哪种方式更适合用在项目中呢?接下来我们对比一下。
SurfaceView:SurfaceView
提供了嵌入视图层次结构内部的专用绘图面板,可以控制此面板的格式,也可以控制其大小,而且它提供了一个辅助线程用以渲染到屏幕中。
TextureView:TextureView
可用于显示内容流,这样的内容流可以是视频或OpenGL
场景、可以是来自本地数据源或者是远程数据源。TextureView
只能在有硬件加速的窗口中使用,当软件渲染时,TextureView
将不绘制任何内容。与SurfaceView
不同,TextureView
不会创建单独的窗口,而是充当常规View
。所以允许用户对TextureView
进行移动,转换,设置动画等操作。
如果项目需求简单,当然可以选择SurfaceView
,但是在这里选择了后者,原因是TextureView
更适合视频流,以及具备普通View
的特性,可以在项目中达到想要的变化效果。选好了显示的View
,接下来看看如何使用。
TextureView如何使用
可以通过调用getSurfaceTexture()
来获取TextureView
的SurfaceTexture
。重要的是只有在将TextureView
附加到窗口(并onAttachedToWindow()
已被调用)后,SurfaceTexture
才可用。因此,在SurfaceTexture
可用时通过实现TextureView.SurfaceTextureListener
来获取状态的通知。要注意,只有一个生产者可以使用TextureView
。例如,如果使用TextureView
显示相机预览,则lockCanvas()
无法同时在TextureView
上绘制。
SurfaceTextureListener
的回调。
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture?, width: Int, height: Int) {
//SurfaceTexture缓冲区大小更改时调用(这里的width、height是改变后的画布大小)
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {
//SurfaceTexture通过更新指定的值 时调用
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean {
//当指定的SurfaceTexture对象即将被销毁时调用。返回true,则调用此方法后,
//表面纹理内不应进行任何渲染。如果返回false,则客户端需要SurfaceTexture.release()。
return false
}
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
//当TextureView准备使用SurfaceTexture时调用(这里的width、height是原始画布大小)
}
MediaPlaye如何使用
MediaPlayer可用于控制音频/视频文件和流的播放。
在官网给出的图中,显示了受支持的播放控制操作驱动的MediaPlayer
对象的生命周期和状态。
- 椭圆形表示
MediaPlayer
对象可能驻留的状态。 -
弧形表示驱动对象状态转换的回放控制操作,有两种类型的弧,具有单箭头的弧表示同步方法调用,而具有双箭头的弧表示异步方法调用。
MediaPlayer的生命周期:
当新建对象或者在创建之后调用
reset()
时,MediaPlayer
对象处于Idle状态。在调用release()
之后处于End状态。在这两种状态之间是MediaPlayer
对象的生命周期。一旦不再使用MediaPlayer
对象,立即调用release()
释放资源。MediaPlayer
对象处于End状态,就无法再使用它,也无法将其恢复为其他任何状态。某些回放控制操作可能由于各种原因而失败,例如,不支持的音频/视频格式,音频/视频的交错差,分辨率太高,流式传输超时等。在所有这些错误条件下,如果已经
setOnErrorListener()
,则内部播放器引擎将调用用户提供的OnErrorListener.onError()
方法。setDataSource()
用于设置视频资源,只能在Idle状态下调用,其他状态下会抛出IllegalStateException
,调用后处于Initialized状态。MediaPlayer
需要Initialized状态下才能进行准备工作,MediaPlayer
提供了两个方法prepare()
(同步)、prepareAsync()
(异步)使其进入Prepared状态,异步调用可以在OnPreparedListener
接口的回调方法onPrepared()
中进行Prepared之后的操作。当
MediaPlayer
在Prepared
状态之后可以调用start()
使它进入Started状态,并开始播放视频,isPlaying()
可以测试MediaPlayer
对象是否处于Started状态。在Started状态时,通过setOnBufferingUpdateListener()
可以在其OnBufferingUpdateListener.onBufferingUpdate()
回调中获取流式传输 音频/视频 时跟踪缓冲状态。播放可以暂停和停止,并且可以调整当前播放位置。
可以通过pause()
暂停播放。当调用pause()
返回时,MediaPlayer
对象将进入Pause状态。注意在播放器引擎中,从Strated 状态到Pause状态的转换是异步的,可能要花一些时间。调用start()
会重新变为Started状态并开始播放。
可以通过stop()
停止播放,这时,Started,Paused,Prepared或PlaybackCompleted状态的MediaPlayer
进入Stopped状态。处于Stopped状态,就无法开始播放,直到调用prepare()
或prepareAsync()
将MediaPlayer
对象重新设置为Prepared状态。调整播放位置可以通过
seekTo()
方法,由于seekTo()
是异步的,实际上查找需要一定时间才能完成,实际的查找位置完成时会走setOnSeekCompleteListener()
的OnSeekComplete.onSeekComplete()
回调。
seekTo()
在Prepared,Paused和PlaybackCompleted 状态下执行仍然会保持当前的状态。
实际的当前播放位置可以通过getCurrentPosition()
获取。当视频播放完成之后默认会走
setOnCompletionListener()
中的OnCompletion.onCompletion()
回调,在回调后处于PlaybackCompleted状态。
如果设置setLooping()
为true
时,会在播放完成后重新变为Started状态并重新播放视频。
在PlaybackCompleted 状态下,调用start()
也可以从音频/视频源的开头重新开始播放。
开始实现视频播放器
这里简单地画了下播放器的流程图(有点丑...)。
项目结构的话借鉴Google官方MVP
模式,能实现视频播放器解耦,功能操作和UI逻辑分离,使用契约类实现Presenter
和Views
,Presenter
中实现各种功能逻辑,Views
负责UI展示。下面是项目的结构:
- 契约类
JVideoViewContract
的实现,分工明确,在Presenter
中实现播放器功能逻辑。
interface JVideoViewContract {
interface Views {
//设置presenter
fun setPresenter(presenter: Presenter)
//设置播放标题
fun setTitle(title:String)
//缓冲中
fun buffering(percent:Int)
/**
* 其他状态下需实现的UI,例如:暂停、加载中、播放结束等
*/
}
interface Presenter {
//实现订阅关系
fun subscribe()
//移除订阅关系
fun unSubscribe()
//开始播放
fun startPlay(position: Int = 0)
//暂停播放
fun pausePlay()
/**
* 其他改变播放状态或者播放器参数的功能,比如初始化播放器、开始播放,滑动快进、音量调节等
*/
}
}
- 播放器的播放状态和播放模式等都写成常量封装在状态类
JVideoState
中,避免使用时过于混乱。
class JVideoState {
//播放器状态
class PlayState{
}
// 播放模式
class PlayMode{
}
//调节模式
class PlayAdjust{
}
}
- 播放器状态常量
PlayState
。
/**
* 播放器状态
*/
class PlayState{
companion object{
//播放错误
const val STATE_ERROR = -1
//播放未开始
const val STATE_IDLE = 0
//播放准备中
const val STATE_PREPARING = 1
// 播放准备就绪
const val STATE_PREPARED = 2
// 开始播放
const val STATE_START = 3
//正在播放
const val STATE_PLAYING = 4
// 暂停播放
const val STATE_PAUSED = 5
//正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,缓冲区数据足够后恢复播放)
const val STATE_BUFFERING_PLAYING = 6
// 正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,此时暂停播放器,继续缓冲,缓冲区
//数据足够后恢复暂停
const val STATE_BUFFERING_PAUSED = 7
// 播放完成
const val STATE_COMPLETED = 8
}
}
- 考虑到后期能运用在其他项目中,
JVideoView
继承LinearLayout
、实现JVideoViewContract.Views
,TextureView.SurfaceTextureListener
接口,同时布局由控制器界面和播放界面组成。
class JVideoView : LinearLayout, JVideoViewContract.Views, TextureView.SurfaceTextureListener {
/*具体逻辑*/
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture?, width: Int, height: Int) {
//SurfaceTexture缓冲区大小改变时
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {
// SurfaceTexture更新时
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean {
//幕布销毁时释放资源
return false
}
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
//幕布准备完毕,这里是原始画布大小
}
}
- 在
JVideoView
中实现对UI的显示控制。
override fun setPresenter(presenter: JVideoViewContract.Presenter) {
mPresenter = presenter
}
override fun setTitle(title: String) {
}
override fun preparedVideo(videoTime: String, max: Int) {
}
override fun startVideo(position: Int) {
}
override fun buffering(percent: Int) {
}
override fun continueVideo() {
}
override fun pauseVideo() {
}
override fun playing(videoTime: String, position: Int) {
}
override fun completedVideo() {
}
override fun showLoading(isShow: Boolean, text: String) {
}
- 在
JVideoViewPresenter
中持有JVideoView
和VideoRepository
(数据仓库)的引用,里面实现具体的播放功能。
class JVideoViewPresenter(
private val mContext: Context,
private val mView: JVideoViewContract.Views,
private val mVideoRepository: VideoRepository
) : JVideoViewContract.Presenter {
init {
mView.setPresenter(this)
}
/*其他操作*/
}
-
JVideoViewPresenter
中实现数据的获取以及功能实现后调用JVideoViewContract.Views
的方法。
override fun subscribe() {
}
override fun unSubscribe() {
}
override fun startPlay(position: Int) {
//开始播放
}
override fun pausePlay() {
//暂停播放视频
}
override fun continuePlay() {
//继续暂停播放视频
}
override fun onPause() {
//onPause,可在此处暂停播放视频
}
override fun onResume() {
//onResume,可在此处恢复播放视频
}
override fun getDuration(): Int {
/*总进度获取*/
}
override fun getPosition(): Int {
/*当前进度获取*/
}
override fun getBufferPercent(): Int {
/*获取缓冲*/
}
override fun releasePlay(destroyUi: Boolean) {
/*结束播放时释放资源,如对一些强引用的置空等*/
}
- 在
JVideoViewPresenter
中实现数据源的获取。
//获取数据源
private fun loadVideosData() {
mVideoRepository.getVideos(object : VideoDataSource.LoadVideosCallback {
override fun onVideosLoaded(videos: List
- 关于
MediaPlayer
的设置
- 设置播放器的幕布以及播放的Url以及相关的音频参数。
//设置视频的url,本地或者网络
mPlayer?.setDataSource(video.videoUrl)
//设置渲染画板
mPlayer?.setSurface(surface)
//设置是否循环播放,默认可不写
isLooping = false
//设置播放类型、音频参数
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val attributes = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setFlags(AudioAttributes.FLAG_LOW_LATENCY)
.setUsage(AudioAttributes.USAGE_MEDIA)
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
.build()
mPlayer?. setAudioAttributes(attributes)
} else {
mPlayer?.setAudioStreamType(AudioManager.STREAM_MUSIC)
}
//设置是否保持屏幕常亮
setScreenOnWhilePlaying(true)
- 在设置完
MediaPlayer
的基本参数之后设置各种监听setOnCompletionListener
(播放完毕),setOnSeekCompleteListener
(seekTo查找完成),setOnPreparedListener
(预加载完成),setOnBufferingUpdateListener
(缓冲变化),setOnErrorListener
(播放错误),setOnInfoListener
(播放器信息),setOnVideoSizeChangedListener
(视频尺寸修改),并修改相应的状态表示。
//播放完成监听
mPlayer?.setOnCompletionListener {
}
//seekTo()调用并实际查找完成之后
mPlayer?.setOnSeekCompleteListener {
}
//预加载监听
mPlayer?.setOnPreparedListener {
}
//相当于缓存进度条
mPlayer?.setOnBufferingUpdateListener { mp, percent ->
}
//播放错误监听
mPlayer?.setOnErrorListener { mp, what, extra ->
true
}
//播放信息监听
mPlayer?. setOnInfoListener { mp, what, extra ->
true
}
//播放尺寸
mPlayer?.setOnVideoSizeChangedListener { mp, width, height ->
//这里是视频的原始尺寸大小
}
- 幕布准备完毕也就是前面的
onSurfaceTextureAvailable()
回调中让MediaPlayer
进入Prepared状态。在这里用异步的方式prepareAsync
,在进入setOnPreparedListener
(预加载完成)之前都设为STATE_PREPARING
。
//预加载监听
mPlayer?.setOnPreparedListener {
mPlayState = PlayState.STATE_PREPARED
//预加载后播放
mPlayer?.start()
}
//异步的方式装载流媒体文件
prepareAsync()
- 视频播放中会有缓冲,当缓冲区不足时会停止播放并进行缓冲加载,缓冲加载中如果用户未暂停则设为
STATE_BUFFERING_PLAYING
状态,如果暂停则设为STATE_BUFFERING_PAUSED
状态,缓冲完成之后恢复到原来的播放状态。
//播放信息监听
mPlayer?. setOnInfoListener { mp, what, extra ->
when (what) {
MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START -> {
// 播放器开始渲染
mPlayState = PlayState.STATE_PLAYING
}
MediaPlayer.MEDIA_INFO_BUFFERING_START -> {
// MediaPlayer暂时不播放,以缓冲更多的数据
mPlayState = if (mPlayState == PlayState.STATE_PAUSED || mPlayState == PlayState.STATE_BUFFERING_PAUSED) {
PlayState.STATE_BUFFERING_PAUSED
} else {
PlayState.STATE_BUFFERING_PLAYING
}
}
MediaPlayer.MEDIA_INFO_BUFFERING_END -> {
// 填充缓冲区后,MediaPlayer恢复播放/暂停
if (mPlayState == PlayState.STATE_BUFFERING_PLAYING) {
mPlayState = PlayState.STATE_PLAYING
}
if (mPlayState == PlayState.STATE_BUFFERING_PAUSED) {
mPlayState = PlayState.STATE_PAUSED
}
}
MediaPlayer.MEDIA_INFO_NOT_SEEKABLE -> {
//无法seekTo
}
}
}
- 到这里一个简单的播放器就完成了。但是~ ~,简单是不行的,单纯靠简单直接使用
VideoView
不就行了,所以还要实现拖动改变进度,全屏播放,滑动改变进度、音量和亮度调节。
播放器的各种调节控制的实现
- 拖动改变进度的实现,拖动主要是在控制器底部写一个
SeekBar
,仿照平时看剧的App就行,设置SeekBar
总进度和视频的总长度一致,拖动时在SeekBar
的onStopTrackingTouch
中通知播放器设置进度。
override fun onStopTrackingTouch(seekBar: SeekBar) {
//seekBar滑动中的回调
mPlayer?.seekTo(seekBar.progress)
}
- 全屏播放的实现,由于播放器是继承
LinearLayout
,通过设置layoutParams
达到全屏效果,记得一开始需要保存原始的params
,用于再次恢复正常模式。
//进入全屏模式
mPlayMode = PlayMode.MODE_FULL_SCREEN
// 隐藏ActionBar、状态栏,并横屏
(mContext as AppCompatActivity).supportActionBar?.hide()
mContext.window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
mContext.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
//设置为充满父布局
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
//隐藏虚拟按键,并且全屏
mContext.window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_FULLSCREEN
//设置播放器为全屏
(mView as LinearLayout).layoutParams = params
- 滑动改变进度、音量和亮度调节的实现,通过对整个播放器的根部局的
setOnTouchListener
进行滑动监听,在不同的位置滑动对播放器进行控制即可。AudioManager
的使用可以百度,具体代码可以移步项目地址,这里不再赘述。
//亮度调节
val params = (mContext as AppCompatActivity).window.attributes
params.screenBrightness = light / 255f
mContext.window.attributes = params
// 音量调节
mAudioManager?.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0)
实现的界面
至此,一个简单的有功能的播放器实现了,可能或多或少有待改进的地方,后续仍然会进行优化,欢迎批评指正。
项目地址:Jvideoview
参考文章:
- 用MediaPlayer+TextureView封装一个完美实现全屏、小窗口的视频播放器
- Android 多媒体:TextureView+MediaPlayer实现视频播放器