(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一定是你的代码写错了!
来源:CSDN
作者:PresidentChao
链接:https://blog.csdn.net/weixin_38737818/article/details/103586700