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的生命周期和状态
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<Video>) {
/*数据源获取成功*/
}
override fun onDataNotAvailable() {
/*数据源获取失败*/
}
})
}
- 关于
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
参考文章:
