【Android(安卓)前沿技术】用MediaPlayer+TextureView封装好的视频播放器,可直接使用(附demo)
一、引言
在经过将近半个月的调研开发,我终于算是对“Android短视频开发”入门了。为了给自己一个总结,也为了把自己潜心研究的成果分享出来,我决定写下这篇blog。
播放器已经封装好,结构很清晰,直接调用即可。这里附上效果图和demo(点击下载)!
其中,图一为全屏播放,图二为播放中,图三为暂停,图四为上滑展示小窗口,图五为网络异常提示。
二、方案选择
1、方案一:videoView+mediaPlayer
videoView继承自SurfaceView。surfaceView是在现有View上创建一个新的Window,内容显示和渲染是在新的Window中,这使得SurfaceView的绘制和刷新可以在单独的线程中进行。由于SurfaceView的内容是在新建的Window中,这使得SurfaceView不能放在RecyclerView或ScrollView中,一些View中的特性也无法使用。
2、方案二:textureView+mediaPlayer
textureView不会创建新的窗口,它的使用跟其他普通View一样。
考虑到以后的可扩展性,最终采用方案二。
三、TextureView介绍
1、TextureView被创建后不能直接使用,必须将其添加到ViewGroup中。
2、TextureView必须要等SurfaceTexture准备就绪才能起作用,这里通常需要给TextureView设置监听器SurfaceTextureListener。等待onSurfaceTextureAvailable回调后,才能使用。
@Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) { //SurfaceTexture准备就绪 if (mSurfaceTexture == null) { mSurfaceTexture = surfaceTexture; openMediaPlayer(); } else { mTextureView.setSurfaceTexture(mSurfaceTexture); } } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) { //SurfaceTexture缓冲大小变化 } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { //SurfaceTexture即将被销毁 return mSurfaceTexture == null; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { //SurfaceTexture通过updateImage更新 }
SurfaceTexture的各个变化都有相应的回调方法。其中最重要的是onSurfaceTextureAvailable方法,该方法在surfaceTexture创建好后回调。主要在该方法中关联MediaPlayer,作为视频数据的图像来源。
四、MediaPlayer介绍
1、几种重要的状态
idle:空闲状态。当mediaPlayer没有prepareAsync之前,就是处于idle状态。
prepared:准备好状态。想要让mediaPlayer开始播放,不能直接start,必须要先prepareSync。这期间mediaPlayer会一直在准备preparing,直到进入prepared状态。
started:当mediaPlayer准备好,就可以调用mediaPlayer的start方法进入started状态。
paused:当调用pause方法,进入paused状态。
completed:播放完成,进入completed状态。
error:播放错误。
2、几个重要的方法
prepareAsync:要想使用mediaPlayer,必须先调用prepareAsync。这是第一步。
start:开始
pause:暂停
reset:播放完成后,如想重新开始,调用该方法。
3、几个重要的回调
onSurfaceTextureAvailable:开始关联mediaPlayer
onPrepared:此处开始调用mediaPlayer.start()
onInfo:播放开始后,视频到底状态如何,就是在onInfo中处理
@Override public boolean onInfo(MediaPlayer mp, int what, int extra) { if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) { // 播放器渲染第一帧 mCurrentState = STATE_PLAYING; mController.onPlayStateChanged(mCurrentState); } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) { // MediaPlayer暂时不播放,以缓冲更多的数据 if (mCurrentState == STATE_PAUSED || mCurrentState == STATE_BUFFERING_PAUSED) { mCurrentState = STATE_BUFFERING_PAUSED; } else { mCurrentState = STATE_BUFFERING_PLAYING; } mController.onPlayStateChanged(mCurrentState); } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) { // 填充缓冲区后,MediaPlayer恢复播放/暂停 if (mCurrentState == STATE_BUFFERING_PLAYING) { mCurrentState = STATE_PLAYING; mController.onPlayStateChanged(mCurrentState); } if (mCurrentState == STATE_BUFFERING_PAUSED) { mCurrentState = STATE_PAUSED; mController.onPlayStateChanged(mCurrentState); } } else { LogUtil.d("onInfo ——> what:" + what); } return true; }
五、封装自己的短视频播放器
以上是短视频开发的知识点,如果是初识视频开发,可能会不清楚讲的是什么,可以接着看下面的视频播放器的封装。这里会详细地介绍如何利用TextureView和MediaPlayer封装一个属于自己的视频播放器。由于MediaPlayer的方法和状态比较多,播放暂停操作在不同状态下应调用不同方法。
一个短视频播放控件应该包含两层:顶层是播放器的控制器mController,底层是播放视频内容的TextureView。这里将这两层封装在一个容器FrameLayout中。
public TourVideoPlayer(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); mContext = context; if (mNetworkChangeReceiver == null) { mNetworkChangeReceiver = new NetworkChangeReceiver(this); } allow4GFlag = false; 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); }
private void addTextureView() { mContainer.removeView(mTextureView); LayoutParams params = new LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER); mContainer.addView(mTextureView, 0, params); }
public void setController(IVideoController controller) { mContainer.removeView(mController); mController = controller; mController.reset(); mController.setVideoPlayer(this); LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); mContainer.addView(mController, params); }
触发播放是,将TextureView、MediaPlayer、Controller进行初始化。待TextureView的数据通道SurfaceTexture准备就绪后,打开播放器。
private void openMediaPlayer() { // 屏幕常亮 mContainer.setKeepScreenOn(true); // 设置监听 mMediaPlayer.setOnPreparedListener(this); mMediaPlayer.setOnVideoSizeChangedListener(this); mMediaPlayer.setOnCompletionListener(this); mMediaPlayer.setOnErrorListener(this); mMediaPlayer.setOnInfoListener(this); mMediaPlayer.setOnBufferingUpdateListener(this); mCurrentNetworkState = NetworkChangeReceiver.getNetworkStatus(CtripBaseApplication.getInstance()); // TODO: 2018/1/4 待确定 mNetworkChangeReceiver.registerNetworkChangeBroadcast(); // 设置dataSource try { mMediaPlayer.setDataSource(mUrl); if (mSurface == null) { mSurface = new Surface(mSurfaceTexture); } mMediaPlayer.setSurface(mSurface); mMediaPlayer.prepareAsync(); mCurrentState = STATE_PREPARING; mController.onPlayStateChanged(mCurrentState); } catch (IOException e) { e.printStackTrace(); LogUtil.e("打开播放器发生错误", e); } }
private void initMediaPlayer() { if (mMediaPlayer == null) { mMediaPlayer = new MediaPlayer(); mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); } } private void initTextureView() { if (mTextureView == null) { mTextureView = new TourTextureView(mContext); mTextureView.setSurfaceTextureListener(this);//此时回调onSurfaceTextureAvailable } }
@Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) { if (mSurfaceTexture == null) { mSurfaceTexture = surfaceTexture; openMediaPlayer(); } else { mTextureView.setSurfaceTexture(mSurfaceTexture); } }
播放逻辑写完之后,具体UI展示逻辑在VideoPlayerController中。根据不同的状态,VideoPlayerController展示不同UI。
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; //播放完成 public static final int STATE_NOTE_4G = 8; //提示4G public static final int STATE_NOTE_DISCONNECT = 9; //提示断网 public static final int MODE_NORMAL = 10; //普通模式 public static final int MODE_FULL_SCREEN = 11; //全屏模式 public static final int MODE_TINY_WINDOW = 13; //小窗口模式
全屏、小窗口播放的实现
如何实现全屏?
将mContainer移除,并添加到android.R.content中,并设置成横屏
@Override public void enterFullScreen() { if (mCurrentMode == MODE_FULL_SCREEN) return; // 隐藏ActionBar、状态栏,并横屏 TourVideoUtil.hideActionBar(mContext); TourVideoUtil.scanForActivity(mContext) .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); new Handler().post(new Runnable() { @Override public void run() { ViewGroup contentView = (ViewGroup) TourVideoUtil.scanForActivity(mContext) .findViewById(android.R.id.content); if (mCurrentMode == MODE_TINY_WINDOW) { contentView.removeView(mContainer); } else { TourVideoPlayer.this.removeView(mContainer); } LayoutParams params = new LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); contentView.addView(mContainer, params); } }); mCurrentMode = MODE_FULL_SCREEN; mController.onPlayModeChanged(mCurrentMode); }
如何实现小窗口?
将mContainer移除,添加到android.R.content中,并设置宽高
@Override public void enterTinyWindow() { if (mCurrentMode == MODE_TINY_WINDOW) return; this.removeView(mContainer); new Handler().post(new Runnable() { @Override public void run() { ViewGroup contentView = (ViewGroup) TourVideoUtil.scanForActivity(mContext) .findViewById(android.R.id.content); // 小窗口的宽度为屏幕宽度的60%,长宽比默认为16:9,右边距、下边距为8dp。 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( (int) (CommonUtil.getScreenWidth(mContext) * 0.6f), (int) (CommonUtil.getScreenWidth(mContext) * 0.6f * 9f / 16f)); params.gravity = Gravity.TOP | Gravity.START; params.topMargin = CommonUtil.dp2px(mContext, 48f); contentView.addView(mContainer, params); } }); mCurrentMode = MODE_TINY_WINDOW; mController.onPlayModeChanged(mCurrentMode); }
六、最后
由于日常工作繁忙,这里博客写的可能没有很详细,不过demo写的非常详细,应该会是你想要的~~~~
待日后有时间时,我会进一步优化一下。
有任何问题,欢迎留言或邮件联系zhshan@ctrip.com
最后再次附上demo。
更多相关文章
- android vold初始化及sd卡挂载流程
- Android(安卓)转屏那些事儿
- 关于Android(安卓)7.0系统通知声音不能播放
- Android获取View的宽度和高度
- Android(安卓)6.0以上权限拒绝打开权限设置界面的解决方法
- 大厂面试,居然还问这些问题!
- Android(安卓)字体国际化适配方法以及源码解析
- Android(安卓)Binder 应用层调用过程分析
- IOS各项生命周期