(RK3399)使用TextureView+MediaPlayer实现视频播放器

做~自己de王妃 提交于 2019-12-24 05:09:48

(RK3399)使用TextureView+MediaPlayer实现视频播放器

为何使用TextureView

原因是公司导师要求,原本RkVideoPlayer是用的SurfaceView,现在给我的任务就是使用TextureView来替换SurfaceView。原因是原本使用SurfaceView,在播放中按电源键休眠,再打开,视频会暂停,不会继续播放,所以替换成TextureView看看是不是能继续播放。那么就先来看一下TextureView和SurfaceView的区别吧。
SurfaceView和TextureView均继承于android.view.View,与其它View不同的是,两者都能在独立的线程中绘制和渲染,在专用的GPU线程中大大提高渲染的性能。
对于SurfaceView来说,1.不能加上动画、平移、缩放;2.两个SurfaceView不能相互覆盖;
而TextureView更像是一般的View,像TextView那样能被缩放、平移,也能加上动画,还有必须在开启了硬件加速的window里使用。
弄清楚了这俩的区别以后,就开始研究研究具体如何替换吧。

SurfaceView

public class VideoDisplayView extends SurfaceView implements VideoPlayerControl, OnClickListener

原本是让VideoDisplayView继承SurfaceView同时实现VideoPlayerControl, OnClickListener接口,再通过SurfaceView的几个回调函数来实现SurfaceView的创建。

SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback() {
        public void surfaceChanged(SurfaceHolder holder, int format,
                                   int w, int h) {
            LOG("<----------surfaceChanged---------> format=" + format + "w=" + w + " h=" + h);
            mSurfaceWidth = w;
            mSurfaceHeight = h;

            if (mMediaPlayer != null && mIsPrepared && mVideoWidth == w && mVideoHeight == h) {
                LOG("CASE 5 EXE");
                if (mSetScreebSizeClicked == true) {
                    return;
                }
            } else return;
        }

        public void surfaceCreated(SurfaceHolder holder) {
            LOG("<--------surfaceCreate---------->");
            mSurfaceHolder = holder;
            openVideo();
        }

        public void surfaceDestroyed(SurfaceHolder holder) {
            LOG("<-----------surfaceDestoryed-------->");
            // after we return from this we can't use the surface any more
            mSurfaceHolder = null;
            if (mVideoController != null) mVideoController.hide();
            if (mMediaPlayer != null) {
                mMediaPlayer.reset();
                mMediaPlayer.release();
                mMediaPlayer = null;
            }				
        }
    };

TextureView

现在进入正题,首先先修改VideoDisaplayView的继承,继承于TextureView,实现VideoPlayerControl, 再通过实现TextureView.SurfaceTextureListener接口来完成。这里有个小问题,TextureView得在sdk14以上才行,一开始我把AndroidManifest.xml里面的android:minSdkVersion删掉了,导致于一直报错。仔细看了一下,错误的信息才知道,TextureView是要在Android4.0以上的,所以得设置好minSdkVersion,不太细心,嘻嘻。
TextureView最重要的就是SurfaceTextureListener监听的几个回调函数,下面开始讲讲。
先来看看SurfaceTextureListener接口里的源码是怎么写的

public static interface SurfaceTextureListener {
        /**
         * Invoked when a {@link TextureView}'s SurfaceTexture is ready for use.
         *
         * @param surface The surface returned by
         *                {@link android.view.TextureView#getSurfaceTexture()}
         * @param width The width of the surface
         * @param height The height of the surface
         */
        public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height);

        /**
         * Invoked when the {@link SurfaceTexture}'s buffers size changed.
         *
         * @param surface The surface returned by
         *                {@link android.view.TextureView#getSurfaceTexture()}
         * @param width The new width of the surface
         * @param height The new height of the surface
         */
        public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height);

        /**
         * Invoked when the specified {@link SurfaceTexture} is about to be destroyed.
         * If returns true, no rendering should happen inside the surface texture after this method
         * is invoked. If returns false, the client needs to call {@link SurfaceTexture#release()}.
         * Most applications should return true.
         *
         * @param surface The surface about to be destroyed
         */
        public boolean onSurfaceTextureDestroyed(SurfaceTexture surface);

        /**
         * Invoked when the specified {@link SurfaceTexture} is updated through
         * {@link SurfaceTexture#updateTexImage()}.
         *
         * @param surface The surface just updated
         */
        public void onSurfaceTextureUpdated(SurfaceTexture surface);
    }

有这么四个回调函数,其中最为重要的便是onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height)及onSurfaceTextureDestroyed(SurfaceTexture surface)。而onSurfaceTextureAvailable是在什么时候回调执行的呢?
说到这就得再提一个东西,setSurfaceTextureListener(),在初始化VideoDisplayView的时候注册了这么一个监听,当SurfaceTexture准备就绪时,便会回调onSurfaceTextureAvailable,用SurfaceTexture来关联MediaPlayer,作为播放视频的图像数据来源。SurfaceTexture作为数据通道,把从数据源(MediaPlayer)中获取到的图像帧数据转为GL外部纹理,交给TextureVeiw作为View heirachy中的一个硬件加速层来显示,从而实现视频播放功能。
既然提到了MediaPlayer,也来讲讲他吧。于我的理解,他是Android原生是多媒体播放器,用他可以实现本地或者在线的播放。而MediaPlayer最重要的就是他的几种状态,下面用一张老图来展示吧。
这张图包含了MediaPlayer的全部状态,也就是他的生命周期,因为本文重点不在于此,就不细说了。挑几个我印象深刻的讲讲,Idle状态在MediaPlayer被new创建出来时进入,mMediaPlayer = new MediaPlayer(),在这个状态下执行setDataSource()方法设置数据源,则会转为Initialized状态,这时调用preparedAsync()方法先进入PreParing状态,然后通过OnPreparedListener.onPrepared()回调方法通知Prepared状态,此时就可以start()进行播放了。
讲完onSurfaceTextureAvailable,再讲讲onSurfaceTextureDestroyed,就像字面上的一样,是SurfaceTexture即将被销毁时调用,看看源码的介绍
Invoked when the specified {@link SurfaceTexture} is about to be destroyed.
* If returns true, no rendering should happen inside the surface texture after this method
* is invoked. If returns false, the client needs to call {@link SurfaceTexture#release()}.
* Most applications should return true.
大多数都是返回true,当返回为false时,得调用release()方法释放。

遇到的问题

搞清楚了TextureView.SurfaceTextureListener的几个回调函数后,就可以替换代码了。但是在替换后,遇到了几个问题。

1.选择好播放文件点击播放时,进入播放器但未播放视频,而是处于黑屏状态,再点击退出,或者小窗,或者旋转屏幕时则开始播放。
一开始遇到这个bug让我十分不解,为什么一开始不播放,反而点击退出时会开始播放呢?
于是我开始观察运行期间的log,发现了问题所在。当点击播放进入播放器时,log打印了Enter the openVideo(),说明进入了播放的方法没错,但是底下的方法的log一概没有打印,而当点击返回时,再次打印了Enter the openVideo()(说明回调onSurfaceTextureAvailable方法起作用了,但是当时我并没有理解这个问题),底下的方法正常执行,视频开始播放。
那问题出在哪呢?
仔细阅读了代码,发现问题了,在openVideo方法中有这么一段代码。

if (mUri == null || mSurfaceHolder == null) {
            // not ready for playback just yet, will try again later
            return;
        }

判断mUri和mSurfaceHolder是否为空,为空直接返回。那是不是就是因为mSurfaceHolder为空导致直接return所以不播放视频呢?经过log打印的测试验证了我的猜想,那么此时为何mSurfaceHolder会为空呢,我又看了下onSurfaceTextureAvailable方法,其中有一行这样的代码。

mSurfaceHolder = surface;

说明要在回调onSurfaceTextureAvailable方法时mSurfaceHolder才有指向。所以openVideo()中就不需要判断mSurfaceHolder是否为空然后return,则修改判断。onSurfaceTextureAvailable方法如下:

 @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        LOG("<-----------SurfaceTexture is Available-------------->");
        if (mSurfaceHolder == null) {
            mSurfaceHolder = surface;
            openVideo();
        }else{
            mMediaPlayer.setSurface(new Surface(mSurfaceHolder));
        }
    }

此时再重新测试,发现点击播放后会开始开始播放了,再看看log,果然此时进入了openVideo()方法后打印<-----------SurfaceTexture is Available-------------->,说明进入了回调,执行 mSurfaceHolder = surface,再执行了如下代码:

mMediaPlayer = new MediaPlayer();
            LOG("mMediaPlayer = " + mMediaPlayer);
            mIsPrepared = false;

            mMediaPlayer.setOnPreparedListener(mPreparedListener);
            LOG("reset duration to -1 in openVideo");
            mDuration = -1;
            mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
            mMediaPlayer.setOnCompletionListener(mCompletionListener);
            mMediaPlayer.setOnErrorListener(mErrorListener);
            mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
            mMediaPlayer.setOnSeekCompleteListener(mOnSeekCompleteListener);
            mMediaPlayer.setSurface(new Surface(mSurfaceHolder));
            
            mCurrentBufferPercentage = 0;
            LOG("-------------setDataSource-------------mUri:" + mUri);
            mMediaPlayer.setDataSource(mContext, mUri, null);

            //mMediaPlayer.setDisplay(mSurfaceHolder);
            mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            mMediaPlayer.setScreenOnWhilePlaying(true);

            mMediaPlayer.prepareAsync();
            attachMediaController();

播放问题解决!

2.还有一个问题,正常播放以后点击退出时,程序会报错
在这里插入图片描述
有了上面的经验,我猜测应该也是回调方法那边出了问题。
也是,先观察日志,发现报了这么一个错误。
Attempt to invoke virtual method ‘void android.media.MediaPlayer.release()’ on a null object reference
所以很有可能就是onSurfaceTextureDestroyed回调方法出了问题,代码如下:

 @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        mSurfaceHolder = null;
        mMediaPlayer.release();
        return true;
    }

说明直接使用mMediaPlayer.release()不对,我找到之前的surfaceDestroyed方法,直接偷过来改造改造。

 public void surfaceDestroyed(SurfaceHolder holder) {
            LOG("<-----------surfaceDestoryed-------->");
            // after we return from this we can't use the surface any more
            mSurfaceHolder = null;
            if (mVideoController != null) mVideoController.hide();
            if (mMediaPlayer != null) {
                mMediaPlayer.reset();
                mMediaPlayer.release();
                mMediaPlayer = null;
            }
				/*if(mActivity != null )
				{
					mActivity.Finish();
				}*/
        }
@Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        LOG("<-----------SurfaceTexture is Destroyed-------------->");
        mSurfaceHolder = null;
        if (mVideoController != null) mVideoController.hide();
        if (mMediaPlayer != null) {
            mMediaPlayer.reset();
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
        return true;
    }

再重新测试一遍,嗨,成了!
点击播放,视频正常播放,点击退出,程序不再报错,查看log,也是打印正常退出的信息。点击小窗播放,也不再报错。
成功!

测试

再完整的测试一遍,打开video,选择播放文件,点击播放,视频正常播放,音量亮度标记等等控制功能正常,点击退出,正常退出。
在播放中按电源键休眠,再进入,视频不暂停,继续播放。
这下真的成功了,完成需求!

总结

修改代码其实比自己写更麻烦,因为你得去看懂别人写的逻辑,替换其中的代码又会关联到很多别的方法,一开始需要有耐心把整个的代码逻辑给看懂,这是首先要做到的事情。
其次看源码也很重要,看源码上的注释介绍,其实讲的很明白。再结合一些现成的例子,要把方法调用的逻辑给弄懂,这块这回我吃了大亏,一开始并没有把逻辑给理解了。
最后再调试测试的时候,学会看日志是非常重要的,通过日志基本能帮你把逻辑给梳理清楚。
要相信没有很奇怪的bug,只有你没弄懂的代码,有bug一定是你的代码写错了!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!