用MediaPlayer+TextureView封装一个完美实现全屏、小窗口的视频播放器
ksgc0018
7年前
<h3>为什么使用TextureView</h3> <p>在Android总播放视频可以直接使用 VideoView , VideoView 是通过继承自 SurfaceView 来实现的。 SurfaceView 的大概原理就是在现有 View 的位置上创建一个新的 Window ,内容的显示和渲染都在新的 Window 中。这使得 SurfaceView 的绘制和刷新可以在单独的线程中进行,从而大大提高效率。但是呢,由于 SurfaceView 的内容没有显示在 View 中而是显示在新建的 Window 中, 使得 SurfaceView 的显示不受 View 的属性控制,不能进行平移,缩放等变换,也不能放在其它 RecyclerView 或 ScrollView 中,一些 View 中的特性也无法使用。</p> <p>TextureView 是在4.0(API level 14)引入的,与 SurfaceView 相比,它不会创建新的窗口来显示内容。它是将内容流直接投放到 View 中,并且可以和其它普通 View 一样进行移动,旋转,缩放,动画等变化。 TextureView 必须在硬件加速的窗口中使用。</p> <p>TextureView 被创建后不能直接使用,必须要在它被它添加到 ViewGroup 后,待 SurfaceTexture 准备就绪才能起作用(看 TextureView 的源码, TextureView 是在绘制的时候创建的内部 SurfaceTexture )。通常需要给 TextureView 设置监听器 SurfaceTextuListener :</p> <pre> <code class="language-java">mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { // SurfaceTexture准备就绪 } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { // SurfaceTexture缓冲大小变化 } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { // SurfaceTexture即将被销毁 return false; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { // SurfaceTexture通过updateImage更新 } });</code></pre> <p>SurfaceTexture 的准备就绪、大小变化、销毁、更新等状态变化时都会回调相对应的方法。当 TextureView 内部创建好 SurfaceTexture 后,在监听器的 onSurfaceTextureAvailable 方法中,用 SurfaceTexture 来关联 MediaPlayer ,作为播放视频的图像数据来源。</p> <p>SurfaceTexture 作为数据通道,把从数据源( MediaPlayer )中获取到的图像帧数据转为GL外部纹理,交给 TextureVeiw 作为 View heirachy 中的一个硬件加速层来显示,从而实现视频播放功能。</p> <h3>MediaPlayer介绍</h3> <p>MediaPlayer 是Android原生的多媒体播放器,可以用它来实现本地或者在线音视频的播放,同时它支持https和rtsp。</p> <p>MediaPlayer 定义了各种状态,可以理解为是它的生命周期。</p> <p><img src="https://simg.open-open.com/show/d2b18b0f77f187f3b2bb568fa43b6cf5.png"></p> <p>MediaPlayer状态图(生命周期)</p> <p>这个状态图描述了 MediaPlayer 的各种状态,以及主要方法调用后的状态变化。</p> <p>MediaPlayer的相关方法及监听接口:</p> <table> <thead> <tr> <th>方法</th> <th>介绍</th> <th>状态</th> </tr> </thead> <tbody> <tr> <td>setDataSource</td> <td>设置数据源</td> <td>Initialized</td> <td> </td> </tr> <tr> <td>prepare</td> <td>准备播放,同步</td> <td>Preparing —> Prepared</td> <td> </td> </tr> <tr> <td>prepareAsync</td> <td>准备播放,异步</td> <td>Preparing —> Prepared</td> <td> </td> </tr> <tr> <td>start</td> <td>开始或恢复播放</td> <td>Started</td> <td> </td> </tr> <tr> <td>pause</td> <td>暂停</td> <td>Paused</td> <td> </td> </tr> <tr> <td>stop</td> <td>停止</td> <td>Stopped</td> <td> </td> </tr> <tr> <td>seekTo</td> <td>到指定时间点位置</td> <td>PrePared/Started</td> <td> </td> </tr> <tr> <td>reset</td> <td>重置播放器</td> <td>Idle</td> <td> </td> </tr> <tr> <td>setAudioStreamType</td> <td>设置音频流类型</td> <td>--</td> <td> </td> </tr> <tr> <td>setDisplay</td> <td>设置播放视频的Surface</td> <td>--</td> <td> </td> </tr> <tr> <td>setVolume</td> <td>设置声音</td> <td>--</td> <td> </td> </tr> <tr> <td>getBufferPercentage</td> <td>获取缓冲半分比</td> <td>--</td> <td> </td> </tr> <tr> <td>getCurrentPosition</td> <td>获取当前播放位置</td> <td>--</td> <td> </td> </tr> <tr> <td>getDuration</td> <td>获取播放文件总时间</td> <td>--</td> <td> </td> </tr> </tbody> </table> <table> <thead> <tr> <th>内部回调接口</th> <th>介绍</th> <th>状态</th> </tr> </thead> <tbody> <tr> <td>OnPreparedListener</td> <td>准备监听</td> <td>Preparing ——>Prepared</td> <td> </td> </tr> <tr> <td>OnVideoSizeChangedListener</td> <td>视频尺寸变化监听</td> <td>--</td> <td> </td> </tr> <tr> <td>OnInfoListener</td> <td>指示信息和警告信息监听</td> <td>--</td> <td> </td> </tr> <tr> <td>OnCompletionListener</td> <td>播放完成监听</td> <td>PlaybackCompleted</td> <td> </td> </tr> <tr> <td>OnErrorListener</td> <td>播放错误监听</td> <td>Error</td> <td> </td> </tr> <tr> <td>OnBufferingUpdateListener</td> <td>缓冲更新监听</td> <td>--</td> <td> </td> </tr> </tbody> </table> <p>MediaPlayer 在直接new出来之后就进入了Idle状态,此时可以调用多个重载的 setDataSource() 方法从idle状态进入Initialized状态(如果调用 setDataSource() 方法的时候, MediaPlayer 对象不是出于Idle状态,会抛异常,可以调用 reset() 方法回到Idle状态)。</p> <p>调用 prepared() 方法和 preparedAsync() 方法进入Prepared状态,prepared()方法直接进入Parpared状态,preparedAsync()方法会先进入PreParing状态,播放引擎准备完毕后会通过 OnPreparedListener.onPrepared() 回调方法通知Prepared状态。</p> <p>在Prepared状态下就可以调用start()方法进行播放了,此时进入started()状态,如果播放的是网络资源,Started状态下也会自动调用客户端注册的 OnBufferingUpdateListener.OnBufferingUpdate() 回调方法,对流播放缓冲的状态进行追踪。</p> <p>pause() 方法和 start() 方法是对应的,调用 pause() 方法会进入Paused状态,调用 start() 方法重新进入Started状态,继续播放。</p> <p>stop() 方法会使 MdiaPlayer 从Started、Paused、Prepared、PlaybackCompleted等状态进入到Stoped状态,播放停止。</p> <p>当资源播放完毕时,如果调用了 setLooping(boolean) 方法,会自动进入Started状态重新播放,如果没有调用则会自动调用客户端播放器注册的 OnCompletionListener.OnCompletion() 方法,此时 MediaPlayer 进入PlaybackCompleted状态,在此状态里可以调用 start() 方法重新进入Started状态。</p> <h3>封装考虑</h3> <p>MediaPlayer 的方法和接口比较多,不同的状态调用各个方法后状态变化情况也比较复杂。播放相关的逻辑只与 MediaPlayer 的播放状态和调用方法相关,而界面展示和UI操作很多时候都需要根据自己项目来定制。参考原生的 VideoView ,为了解耦和方便定制,把 MediaPlayer 的播放逻辑和UI界面展示及操作相关的逻辑分离。我是把 MediaPlayer 直接封装到 NiceVideoPlayer 中,各种UI状态和操作反馈都封装到 NiceVideoPlayerController 里面。如果需要根据不同的项目需求来修改播放器的功能,就只重写 NiceVideoPlayerController 就可以了。</p> <h3>NiceVideoPlayer</h3> <p>首先,需要一个 FrameLayout 容器 mContainer ,里面有两层内容,第一层就是展示播放视频内容的 TextureView ,第二层就是播放器控制器 mController 。那么自定义一个 NiceVideoPlayer 继承自 FrameLayout ,将 mContainer 添加到当前控件:</p> <pre> <code class="language-java">public class NiceVideoPlayer extends FrameLayout{ private Context mContext; private NiceVideoController mController; private FrameLayout mContainer; public NiceVideoPlayer(Context context) { this(context, null); } public NiceVideoPlayer(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; init(); } private void init() { mContainer = new FrameLayout(mContext); mContainer.setBackgroundColor(Color.BLACK); LayoutParams params = new LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); this.addView(mContainer, params); } }</code></pre> <p>添加 setUp 方法来配置播放的视频资源路径(本地/网络资源):</p> <pre> <code class="language-java">public void setUp(String url, Map<String, String> headers) { mUrl = url; mHeaders = headers; }</code></pre> <p>用户要在 mController 中操作才能播放,因此需要在播放之前设置好 mController :</p> <pre> <code class="language-java">public void setController(NiceVideoPlayerController controller) { mController = controller; mController.setNiceVideoPlayer(this); LayoutParams params = new LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); mContainer.addView(mController, params); }</code></pre> <p>用户在自定义好自己的控制器后通过 setController 这个方法设置给播放器进行关联。</p> <p>触发播放时, NiceVideoPlayer 将展示视频图像内容的 mTextureView 添加到 mContainer 中(在 mController 的下层),同时初始化 mMediaPlayer ,待 mTextureView 的数据通道 SurfaceTexture 准备就绪后就可以打开播放器:</p> <pre> <code class="language-java">public void start() { initMediaPlayer(); // 初始化播放器 initTextureView(); // 初始化展示视频内容的TextureView addTextureView(); // 将TextureView添加到容器中 } private void initTextureView() { if (mTextureView == null) { mTextureView = new TextureView(mContext); mTextureView.setSurfaceTextureListener(this); } } private void addTextureView() { mContainer.removeView(mTextureView); LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); mContainer.addView(mTextureView, 0, params); } private void initMediaPlayer() { if (mMediaPlayer == null) { mMediaPlayer = new MediaPlayer(); mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mMediaPlayer.setScreenOnWhilePlaying(true); mMediaPlayer.setOnPreparedListener(mOnPreparedListener); mMediaPlayer.setOnVideoSizeChangedListener(mOnVideoSizeChangedListener); mMediaPlayer.setOnCompletionListener(mOnCompletionListener); mMediaPlayer.setOnErrorListener(mOnErrorListener); mMediaPlayer.setOnInfoListener(mOnInfoListener); mMediaPlayer.setOnBufferingUpdateListener(mOnBufferingUpdateListener); } } @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { // surfaceTexture数据通道准备就绪,打开播放器 openMediaPlayer(surface); } private void openMediaPlayer(SurfaceTexture surface) { try { mMediaPlayer.setDataSource(mContext.getApplicationContext(), Uri.parse(mUrl), mHeaders); mMediaPlayer.setSurface(new Surface(surface)); mMediaPlayer.prepareAsync(); } catch (IOException e) { e.printStackTrace(); } } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return false; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { }</code></pre> <p>打开播放器调用 prepareAsync() 方法后, mMediaPlayer 进入准备状态,准备就绪后就可以开始:</p> <pre> <code class="language-java">private MediaPlayer.OnPreparedListener mOnPreparedListener = new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { mp.start(); } };</code></pre> <p>NiceVideoPlayer 的这些逻辑已经实现视频播放了,操作相关以及UI展示的逻辑需要在控制器 NiceVideoPlayerController 中来实现。但是呢,UI的展示和反馈都需要依据播放器当前的播放状态,所以需要给播放器定义一些常量来表示它的播放状态:</p> <pre> <code class="language-java">public static final int STATE_ERROR = -1; // 播放错误 public static final int STATE_IDLE = 0; // 播放未开始 public static final int STATE_PREPARING = 1; // 播放准备中 public static final int STATE_PREPARED = 2; // 播放准备就绪 public static final int STATE_PLAYING = 3; // 正在播放 public static final int STATE_PAUSED = 4; // 暂停播放 // 正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,缓冲区数据足够后恢复播放) public static final int STATE_BUFFERING_PLAYING = 5; // 正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,此时暂停播放器,继续缓冲,缓冲区数据足够后恢复暂停) public static final int STATE_BUFFERING_PAUSED = 6; public static final int STATE_COMPLETED = 7; // 播放完成</code></pre> <p>播放视频时, mMediaPlayer 准备就绪( Prepared )后没有马上进入播放状态,中间有一个时间延迟时间段,然后开始渲染图像。所以将Prepared——>“开始渲染”中间这个时间段定义为 STATE_PREPARED 。</p> <p>如果是播放网络视频,在播放过程中,缓冲区数据不足时 mMediaPlayer 内部会停留在某一帧画面以进行缓冲。正在缓冲时, mMediaPlayer 可能是在正在播放也可能是暂停状态,因为在缓冲时如果用户主动点击了暂停,就是处于 STATE_BUFFERING_PAUSED ,所以缓冲有 STATE_BUFFERING_PLAYING 和 STATE_BUFFERING_PAUSED 两种状态,缓冲结束后,恢复播放或暂停。</p> <pre> <code class="language-java">private MediaPlayer.OnPreparedListener mOnPreparedListener = new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { mp.start(); mCurrentState = STATE_PREPARED; mController.setControllerState(mPlayerState, mCurrentState); LogUtil.d("onPrepared ——> STATE_PREPARED"); } }; private MediaPlayer.OnVideoSizeChangedListener mOnVideoSizeChangedListener = new MediaPlayer.OnVideoSizeChangedListener() { @Override public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { LogUtil.d("onVideoSizeChanged ——> width:" + width + ",height:" + height); } }; private MediaPlayer.OnCompletionListener mOnCompletionListener = new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { mCurrentState = STATE_COMPLETED; mController.setControllerState(mPlayerState, mCurrentState); LogUtil.d("onCompletion ——> STATE_COMPLETED"); } }; private MediaPlayer.OnErrorListener mOnErrorListener = new MediaPlayer.OnErrorListener() { @Override public boolean onError(MediaPlayer mp, int what, int extra) { mCurrentState = STATE_ERROR; mController.setControllerState(mPlayerState, mCurrentState); LogUtil.d("onError ——> STATE_ERROR ———— what:" + what); return false; } }; private MediaPlayer.OnInfoListener mOnInfoListener = new MediaPlayer.OnInfoListener() { @Override public boolean onInfo(MediaPlayer mp, int what, int extra) { if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) { // 播放器渲染第一帧 mCurrentState = STATE_PLAYING; mController.setControllerState(mPlayerState, mCurrentState); LogUtil.d("onInfo ——> MEDIA_INFO_VIDEO_RENDERING_START:STATE_PLAYING"); } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) { // MediaPlayer暂时不播放,以缓冲更多的数据 if (mCurrentState == STATE_PAUSED || mCurrentState == STATE_BUFFERING_PAUSED) { mCurrentState = STATE_BUFFERING_PAUSED; LogUtil.d("onInfo ——> MEDIA_INFO_BUFFERING_START:STATE_BUFFERING_PAUSED"); } else { mCurrentState = STATE_BUFFERING_PLAYING; LogUtil.d("onInfo ——> MEDIA_INFO_BUFFERING_START:STATE_BUFFERING_PLAYING"); } mController.setControllerState(mPlayerState, mCurrentState); } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) { // 填充缓冲区后,MediaPlayer恢复播放/暂停 if (mCurrentState == STATE_BUFFERING_PLAYING) { mCurrentState = STATE_PLAYING; mController.setControllerState(mPlayerState, mCurrentState); LogUtil.d("onInfo ——> MEDIA_INFO_BUFFERING_END: STATE_PLAYING"); } if (mCurrentState == STATE_BUFFERING_PAUSED) { mCurrentState = STATE_PAUSED; mController.setControllerState(mPlayerState, mCurrentState); LogUtil.d("onInfo ——> MEDIA_INFO_BUFFERING_END: STATE_PAUSED"); } } else { LogUtil.d("onInfo ——> what:" + what); } return true; } }; private MediaPlayer.OnBufferingUpdateListener mOnBufferingUpdateListener = new MediaPlayer.OnBufferingUpdateListener() { @Override public void onBufferingUpdate(MediaPlayer mp, int percent) { mBufferPercentage = percent; } };</code></pre> <p>mController.setControllerState(mPlayerState, mCurrentState) , mCurrentState 表示当前播放状态, mPlayerState 表示播放器的全屏、小窗口,正常三种状态。</p> <pre> <code class="language-java">public static final int PLAYER_NORMAL = 10; // 普通播放器 public static final int PLAYER_FULL_SCREEN = 11; // 全屏播放器 public static final int PLAYER_TINY_WINDOW = 12; // 小窗口播放器</code></pre> <p>定义好播放状态后,开始暂停等操作逻辑也需要根据播放状态调整:</p> <pre> <code class="language-java">@Override public void start() { if (mCurrentState == STATE_IDLE || mCurrentState == STATE_ERROR || mCurrentState == STATE_COMPLETED) { initMediaPlayer(); initTextureView(); addTextureView(); } } @Override public void restart() { if (mCurrentState == STATE_PAUSED) { mMediaPlayer.start(); mCurrentState = STATE_PLAYING; mController.setControllerState(mPlayerState, mCurrentState); LogUtil.d("STATE_PLAYING"); } if (mCurrentState == STATE_BUFFERING_PAUSED) { mMediaPlayer.start(); mCurrentState = STATE_BUFFERING_PLAYING; mController.setControllerState(mPlayerState, mCurrentState); LogUtil.d("STATE_BUFFERING_PLAYING"); } } @Override public void pause() { if (mCurrentState == STATE_PLAYING) { mMediaPlayer.pause(); mCurrentState = STATE_PAUSED; mController.setControllerState(mPlayerState, mCurrentState); LogUtil.d("STATE_PAUSED"); } if (mCurrentState == STATE_BUFFERING_PLAYING) { mMediaPlayer.pause(); mCurrentState = STATE_BUFFERING_PAUSED; mController.setControllerState(mPlayerState, mCurrentState); LogUtil.d("STATE_BUFFERING_PAUSED"); } }</code></pre> <p>reStart() 方法是暂停时继续播放调用。</p> <h3>全屏、小窗口播放的实现</h3> <p>可能最能想到实现全屏的方式就是把当前播放器的宽高给放大到屏幕大小,同时隐藏除播放器以外的其他所有UI,并设置成横屏模式。但是这种方式有很多问题,比如在列表( ListView或RecyclerView )中,除了放大隐藏外,还需要去计算滑动多少距离才刚好让播放器与屏幕边缘重合,退出全屏的时候还需要滑动到之前的位置,这样实现逻辑不但繁琐,而且和外部UI偶合严重,后面改动维护起来非常困难(我曾经就用这种方式被坑了无数道)。</p> <p>分析能不能有其他更好的实现方式呢?</p> <p>整个播放器由 mMediaPalyer + mTexutureView + mController 组成,要实现全屏或小窗口播放,我们只需要挪动播放器的展示界面 mTexutureView 和控制界面 mController 即可。并且呢我们在上面定义播放器时,已经把 mTexutureView 和 mController 一起添加到 mContainer 中了,所以只需要将 mContainer 从当前视图中移除,并添加到全屏和小窗口的目标视图中即可。</p> <p>那么怎么确定全屏和小窗口的目标视图呢?</p> <p>我们知道每个 Activity 里面都有一个 android.R.content ,它是一个 FrameLayout ,里面包含了我们 setContentView 的所有控件。既然它是一个 FrameLayout ,我们就可以将它作为全屏和小窗口的目标视图。</p> <p>我们把从当前视图移除的 mContainer 重新添加到 android.R.content 中,并且设置成横屏。这个时候还需要注意 android.R.content 是不包括 ActionBar 和状态栏的,所以要将 Activity 设置成全屏模式,同时隐藏 ActionBar 。</p> <pre> <code class="language-java">@Override public void enterFullScreen() { if (mPlayerState == PLAYER_FULL_SCREEN) return; // 隐藏ActionBar、状态栏,并横屏 NiceUtil.hideActionBar(mContext); NiceUtil.scanForActivity(mContext) .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); this.removeView(mContainer); ViewGroup contentView = (ViewGroup) NiceUtil.scanForActivity(mContext) .findViewById(android.R.id.content); contentView.addView(mContainer); mPlayerState = PLAYER_FULL_SCREEN; mController.setControllerState(mPlayerState, mCurrentState); LogUtil.d("PLAYER_FULL_SCREEN"); }</code></pre> <p>退出全屏也就很简单了,将 mContainer 从 android.R.content 中移除,重新添加到当前视图,并恢复 ActionBar 、清除全屏模式就行了。</p> <pre> <code class="language-java">@Override public boolean exitFullScreen() { if (mPlayerState == PLAYER_FULL_SCREEN) { NiceUtil.showActionBar(mContext); NiceUtil.scanForActivity(mContext) .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); ViewGroup contentView = (ViewGroup) NiceUtil.scanForActivity(mContext) .findViewById(android.R.id.content); contentView.removeView(mContainer); this.addView(mContainer); mPlayerState = PLAYER_NORMAL; mController.setControllerState(mPlayerState, mCurrentState); LogUtil.d("PLAYER_NORMAL"); return true; } return false; }</code></pre> <p>进入小窗口播放和退出小窗口的实现原理就和全屏功能一样了,只需要修改它的宽高参数:</p> <pre> <code class="language-java">@Override public void enterTinyWindow() { if (mPlayerState == PLAYER_TINY_WINDOW) return; this.removeView(mContainer); ViewGroup contentView = (ViewGroup) NiceUtil.scanForActivity(mContext) .findViewById(android.R.id.content); // 小窗口的宽度为屏幕宽度的60%,长宽比默认为16:9,右边距、下边距为8dp。 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( (int) (NiceUtil.getScreenWidth(mContext) * 0.6f), (int) (NiceUtil.getScreenWidth(mContext) * 0.6f * 9f / 16f)); params.gravity = Gravity.BOTTOM | Gravity.END; params.rightMargin = NiceUtil.dp2px(mContext, 8f); params.bottomMargin = NiceUtil.dp2px(mContext, 8f); contentView.addView(mContainer, params); mPlayerState = PLAYER_TINY_WINDOW; mController.setControllerState(mPlayerState, mCurrentState); LogUtil.d("PLAYER_TINY_WINDOW"); } @Override public boolean exitTinyWindow() { if (mPlayerState == PLAYER_TINY_WINDOW) { ViewGroup contentView = (ViewGroup) NiceUtil.scanForActivity(mContext) .findViewById(android.R.id.content); contentView.removeView(mContainer); this.addView(mContainer); mPlayerState = PLAYER_NORMAL; mController.setControllerState(mPlayerState, mCurrentState); LogUtil.d("PLAYER_NORMAL"); return true; } return false; }</code></pre> <p>这里有个特别需要注意的一点:</p> <p>当 mContainer 移除重新添加后, mContainer 及其内部的 mTextureView 和 mController 都会重绘, mTextureView 重绘后,会重新 new 一个 SurfaceTexture ,并重新回调 onSurfaceTextureAvailable 方法,这样 mTextureView 的数据通道 SurfaceTexture 发生了变化,但是 mMediaPlayer 还是持有原先的 mSurfaceTexut ,所以在切换全屏之前要保存之前的 mSufaceTexture ,当切换到全屏后重新调用 onSurfaceTextureAvailable 时,将之前的 mSufaceTexture 重新设置给 mTexutureView 。这样讲保证了切换时视频播放的无缝衔接。</p> <pre> <code class="language-java">@Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { if (mSurfaceTexture == null) { mSurfaceTexture = surfaceTexture; openMediaPlayer(); } else { mTextureView.setSurfaceTexture(mSurfaceTexture); } }</code></pre> <h3>NiceVideoPlayerControl</h3> <p>为了解除 NiceVideoPlayer 和 NiceVideoPlayerController 的耦合,把 NiceVideoPlayer 的一些功能性和判断性方法抽象到 NiceVideoPlayerControl 接口中。</p> <pre> <code class="language-java">public interface NiceVideoPlayerControl { void start(); void restart(); void pause(); void seekTo(int pos); boolean isIdle(); boolean isPreparing(); boolean isPrepared(); boolean isBufferingPlaying(); boolean isBufferingPaused(); boolean isPlaying(); boolean isPaused(); boolean isError(); boolean isCompleted(); boolean isFullScreen(); boolean isTinyWindow(); boolean isNormal(); int getDuration(); int getCurrentPosition(); int getBufferPercentage(); void enterFullScreen(); boolean exitFullScreen(); void enterTinyWindow(); boolean exitTinyWindow(); void release(); }</code></pre> <p>NiceVideoPlayer 实现这个接口即可。</p> <h3>NiceVideoPlayerManager</h3> <p>同一界面上有多个视频,或者视频放在 ReclerView 或者 ListView 的容器中,要保证同一时刻只有一个视频在播放,其他的都是初始状体,所以需要一个 NiceVideoPlayerManager 来管理播放器,主要功能是保存当前已经开始了的播放器。</p> <pre> <code class="language-java">public class NiceVideoPlayerManager { private NiceVideoPlayer mVideoPlayer; private NiceVideoPlayerManager() { } private static NiceVideoPlayerManager sInstance; public static synchronized NiceVideoPlayerManager instance() { if (sInstance == null) { sInstance = new NiceVideoPlayerManager(); } return sInstance; } public void setCurrentNiceVideoPlayer(NiceVideoPlayer videoPlayer) { mVideoPlayer = videoPlayer; } public void releaseNiceVideoPlayer() { if (mVideoPlayer != null) { mVideoPlayer.release(); mVideoPlayer = null; } } public boolean onBackPressd() { if (mVideoPlayer != null) { if (mVideoPlayer.isFullScreen()) { return mVideoPlayer.exitFullScreen(); } else if (mVideoPlayer.isTinyWindow()) { return mVideoPlayer.exitTinyWindow(); } else { mVideoPlayer.release(); return false; } } return false; } }</code></pre> <p>采用单例,同时, onBackPressed 供 Activity 中用户按返回键时调用。</p> <p>NiceVideoPlayer 的 start 方法以及 onCompleted 需要修改一下,保证开始播放一个视频时要先释放掉之前的播放器;同时自己播放完毕,要将 NiceVideoPlayerManager 中的 mNiceVideoPlayer 实例置空,避免内存泄露。</p> <pre> <code class="language-java">// NiceVideoPlayer的start()方法。 @Override public void start() { NiceVideoPlayerManager.instance().releaseNiceVideoPlayer(); NiceVideoPlayerManager.instance().setCurrentNiceVideoPlayer(this); if (mCurrentState == STATE_IDLE || mCurrentState == STATE_ERROR || mCurrentState == STATE_COMPLETED) { initMediaPlayer(); initTextureView(); addTextureView(); } } // NiceVideoPlayer中的onCompleted监听。 private MediaPlayer.OnCompletionListener mOnCompletionListener = new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { mCurrentState = STATE_COMPLETED; mController.setControllerState(mPlayerState, mCurrentState); LogUtil.d("onCompletion ——> STATE_COMPLETED"); NiceVideoPlayerManager.instance().setCurrentNiceVideoPlayer(null); } };</code></pre> <h3>NiceVideoPlayerController</h3> <p>播放控制界面上,播放、暂停、播放进度、缓冲动画、全屏/小屏等触发都是直接调用播放器对应的操作的。需要注意的就是调用之前要判断当前的播放状态,因为有些状态下调用播放器的操作可能引起错误(比如播放器还没准备就绪,就去获取当前的播放位置)。</p> <p>播放器在触发相应功能的时候都会调用 NiceVideoPlayerController 的 setControllerState(int playerState, int playState) 这个方法来让用户修改UI。</p> <p>不同项目都可能定制不同的控制器(播放操作界面),这里我就不详细分析实现逻辑了,大致功能就类似腾讯视频的热点列表中的播放器。其中全屏模式下横向滑动改变播放进度、左侧上下滑动改变亮度,右侧上下滑动改变亮度等功能代码中并未实现,有需要的可以直接参考 <a href="/misc/goto?guid=4959748979465563563" rel="nofollow,noindex">节操播放器</a> ,只需要在 Controller 的 onInterceptTouchEvent 中处理就行了(后续会添加上去)。</p> <p>代码有点长,就不贴了,需要的直接 下载源码 。</p> <h3>使用</h3> <pre> <code class="language-java">mNiceVideoPlayer.setUp(url, null); NiceVideoPlayerController controller = new NiceVideoPlayerController(this); controller.setTitle(title); controller.setImage(imageUrl); mNiceVideoPlayer.setController(controller);</code></pre> <p>在 RecyclerView 或者 ListView 中使用时,需要监听 itemView 的 detached :</p> <pre> <code class="language-java">mRecyclerView.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() { @Override public void onChildViewAttachedToWindow(View view) { } @Override public void onChildViewDetachedFromWindow(View view) { NiceVideoPlayer niceVideoPlayer = (NiceVideoPlayer) view.findViewById(R.id.nice_video_player); if (niceVideoPlayer != null) { niceVideoPlayer.release(); } } });</code></pre> <p>在 ItemView detach窗口时,需要释放掉 itemView 内部的播放器。</p> <h3>效果图</h3> <p><img src="https://simg.open-open.com/show/e9b6435c6493f383b6f107a18f255236.png"></p> <p><img src="https://simg.open-open.com/show/baa3c083bd5ac26653d3a4a6cbcde28f.png"></p> <p><img src="https://simg.open-open.com/show/dbccf38ed9489bb2e84d9ebde7ec10f1.png"></p> <p><img src="https://simg.open-open.com/show/14d54b5a313385ed2b557bc5056b7e90.png"></p> <h3>最后</h3> <p>整个功能有参考 <a href="/misc/goto?guid=4959717425203868313">节操播放器</a> ,但是自己这样封装和节操播放器还是有很大差异:一是分离了播放功能和控制界面,定制只需修改控制器即可。二是全屏/小窗口没有新建一个播放器,只是挪动了播放界面和控制器,不用每个视频都需要新建两个播放器,也不用同步状态。</p> <p>MediaPlayer有很多格式不支持,后面会考虑用IjkPlayer或者ExoPlayer封装。</p> <p>如果有错误和更好的建议都请提出,源码已上传GitHub,欢迎Star,谢谢!。</p>