MediaPlayer+TextureView实现视频播放器

Android中实现视频播放器的途径有两种:

  • 使用VideoView
  • 通过MediaPlayer + SurfaceView/ TextureView
1. VideoView

VideoView使用比较简单,配合MediaController可以达到控制播放、暂停、快进、快退、切换视频、进度条显示等,具体使用在这里不在赘述了。

2. MediaPlayer + SurfaceView / TextureView

实现一个相对完善的视频播放器,可以使用 MediaPlayer+SurfaceView /MediaPlayer+TextureView

这里为什么有两种方式呢?然后哪种方式更适合用在项目中呢?接下来我们对比一下。

SurfaceViewSurfaceView提供了嵌入视图层次结构内部的专用绘图面板,可以控制此面板的格式,也可以控制其大小,而且它提供了一个辅助线程用以渲染到屏幕中。

TextureViewTextureView可用于显示内容流,这样的内容流可以是视频或OpenGL场景、可以是来自本地数据源或者是远程数据源。TextureView只能在有硬件加速的窗口中使用,当软件渲染时,TextureView将不绘制任何内容。与SurfaceView不同,TextureView不会创建单独的窗口,而是充当常规View。所以允许用户对TextureView进行移动,转换,设置动画等操作。

如果项目需求简单,当然可以选择SurfaceView,但是在这里选择了后者,原因是TextureView更适合视频流,以及具备普通View的特性,可以在项目中达到想要的变化效果。选好了显示的View,接下来看看如何使用。

TextureView如何使用
可以通过调用getSurfaceTexture()来获取TextureViewSurfaceTexture。重要的是只有在将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之后的操作。

  • MediaPlayerPrepared状态之后可以调用start()使它进入Started状态,并开始播放视频,isPlaying()可以测试MediaPlayer对象是否处于Started状态。在Started状态时,通过setOnBufferingUpdateListener()可以在其OnBufferingUpdateListener.onBufferingUpdate()回调中获取流式传输 音频/视频 时跟踪缓冲状态。

  • 播放可以暂停和停止,并且可以调整当前播放位置。
    可以通过pause()暂停播放。当调用 pause()返回时,MediaPlayer对象将进入Pause状态。注意在播放器引擎中,从Strated 状态到Pause状态的转换是异步的,可能要花一些时间。调用start()会重新变为Started状态并开始播放。
    可以通过stop()停止播放,这时,StartedPausedPreparedPlaybackCompleted状态的MediaPlayer进入Stopped状态。处于Stopped状态,就无法开始播放,直到调用prepare()prepareAsync()MediaPlayer对象重新设置为Prepared状态。

  • 调整播放位置可以通过seekTo()方法,由于seekTo()是异步的,实际上查找需要一定时间才能完成,实际的查找位置完成时会走setOnSeekCompleteListener()OnSeekComplete.onSeekComplete()回调。
    seekTo()PreparedPausedPlaybackCompleted 状态下执行仍然会保持当前的状态。
    实际的当前播放位置可以通过getCurrentPosition()获取。

  • 当视频播放完成之后默认会走setOnCompletionListener()中的OnCompletion.onCompletion()回调,在回调后处于PlaybackCompleted状态。
    如果设置setLooping()true时,会在播放完成后重新变为Started状态并重新播放视频。
    PlaybackCompleted 状态下,调用start()也可以从音频/视频源的开头重新开始播放。

开始实现视频播放器
这里简单地画了下播放器的流程图(有点丑...)。

流程图

项目结构的话借鉴Google官方MVP模式,能实现视频播放器解耦,功能操作和UI逻辑分离,使用契约类实现PresenterViewsPresenter中实现各种功能逻辑,Views负责UI展示。下面是项目的结构:

JVideoView项目结构

  • 契约类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中持有JVideoViewVideoRepository(数据仓库)的引用,里面实现具体的播放功能。
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的设置
  1. 设置播放器的幕布以及播放的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)
  1. 在设置完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 ->
        //这里是视频的原始尺寸大小
    }
  1. 幕布准备完毕也就是前面的onSurfaceTextureAvailable()回调中让MediaPlayer进入Prepared状态。在这里用异步的方式prepareAsync,在进入setOnPreparedListener(预加载完成)之前都设为STATE_PREPARING
    //预加载监听
    mPlayer?.setOnPreparedListener {
        mPlayState = PlayState.STATE_PREPARED
        //预加载后播放
        mPlayer?.start()
    }
    //异步的方式装载流媒体文件
     prepareAsync()
  1. 视频播放中会有缓冲,当缓冲区不足时会停止播放并进行缓冲加载,缓冲加载中如果用户未暂停则设为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总进度和视频的总长度一致,拖动时在SeekBaronStopTrackingTouch中通知播放器设置进度。
    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
参考文章:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容