Android 实现视屏播放器与边播边缓存功能外加蹲坑铲屎(IJKPlayer).视频播放器,支持基本的拖动,声音、亮度调节,支持边播边缓存,支持视频本身自带rotation的旋转(90,270之类),重力旋转与手动旋转的同步支持,支持列表播放 ,直接添加控件为封面,列表全屏动画,列表小窗口支持拖动,5.0的过场效果,其他一些小动画效果。
开源播放器选择
Android上最为人熟知的MediaPlayer,对,就是这货,在上两篇音频文章中频频露脸的家伙,这次又有它的身影,然而还是这次不讲他,就连他的封装类VideoView也不讲<( ̄︶ ̄)>,呸呸呸,又扯了一堆没用的。
ijkplayer,这次要推荐的是它,鼎鼎大名的BILIBILI开源的播放器。基于FFMPEG,支持Android与IOS,还封装了谷歌亲儿子MediaPlayer与干儿子EXOPlayer(为什么要用EXO),支持直播流,Star-9000多与fork-3000的视频播放器你支持安利。(issues 600多算活跃吗┑( ̄Д  ̄)┍)
集成工作还是有定的工作量的,它的DEMO肯定满足不了欲求不满的设计狮和产品汪的,这里我们不跑分,不打广告,不讲原理,只求站在巨人的肩膀上学(cao)习(xi),快速集成。
定义一个单例的视频内核播放管理器。
自定义一个满足你上下其手的TextureView
定义一个UI层级逻辑播放器
重力旋转的相关逻辑处理
列表逻辑的相关处理
列表到全屏相关的逻辑处理
视频缓存逻辑
1、播放管理器:GSYVideoManager
单例,没得商量,它需要负责真正的播放请求与显示逻辑,集成了IjkMediaPlayer,BILIBLI的开源小组还是很有心的,它的封装和接口使用基本和MediaPlayer没有什么区别,只需要用起来就好了。‘
这里我们要实现IjkMediaPlayer的播放接口,监听IjkMediaPlayer的相关状态回调然后封发到各个逻辑播放器中。从下方代码可以看到,真的和MediaPlayer好像。
mediaPlayer = new IjkMediaPlayer(); //音频类型 mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); //数据源 mediaPlayer.setDataSource(((GSYModel) msg.obj).getUrl(), ((GSYModel) msg.obj).getMapHeadData()); //播放完成 mediaPlayer.setOnCompletionListener(GSYVideoManager.this); //缓冲 mediaPlayer.setOnBufferingUpdateListener(GSYVideoManager.this); //常亮 mediaPlayer.setScreenOnWhilePlaying(true); //加载完毕 mediaPlayer.setOnPreparedListener(GSYVideoManager.this); //拖动 mediaPlayer.setOnSeekCompleteListener(GSYVideoManager.this); //失败 mediaPlayer.setOnErrorListener(GSYVideoManager.this); //视频相关信息-重要 mediaPlayer.setOnInfoListener(GSYVideoManager.this); //视频大小 mediaPlayer.setOnVideoSizeChangedListener(GSYVideoManager.this);】 //开始加载 mediaPlayer.prepareAsync();
监听的回调接口里,大部分大家都耳目能详吧,没听过也没关系,都写上就对了,但是最主要需要关注的两个,一个是通过setOnVideoSizeChangedListener拿到视频宽和高,这是我们后续正常显示视频的依靠之一。
另外一个就是setOnInfoListener,这里我们主要是获取到视频相关的元信息里视频旋转角度!还记得那时候对视频播放不熟悉,和产品还有QA力争“这个视频本来就是转了90度的,我就不改,你咬我吗···”这样的黑历史。Σ( ° △ °|||)
特别是Android拍摄的竖屏视频,旋转不是视频本身的图像,而是增加了旋转信息,而这个时候你需要做的就是识别它,然后转了它丫的。另外,因为Android本身的MediaPlaer和VideoView自身就处理好所以不需要你旋转。((ノO益O)ノ彡┻━┻亲生的啊)
这里的接口主要是把当前播放的视频状态和信息到返回到逻辑播放器中。
@Override public void onInfo(int what, int extra) { if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) { BACKUP_PLAYING_BUFFERING_STATE = mCurrentState; setStateAndUi(CURRENT_STATE_PLAYING_BUFFERING_START); } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) { if (BACKUP_PLAYING_BUFFERING_STATE != -1) { setStateAndUi(BACKUP_PLAYING_BUFFERING_STATE); BACKUP_PLAYING_BUFFERING_STATE = -1; } } else if (what == IMediaPlayer.MEDIA_INFO_VIDEO_ROTATION_CHANGED) { //这里返回了视频旋转的角度,根据角度旋转视频到正确的画面 mRotate = extra; if (mTextureView != null) mTextureView.setRotation(mRotate); } }
2、自定义TextureView:GSYTextureView
为什么不用SurfaceView?因为TextureView很可爱啊。这里我们主要针对视频的大小和旋转角度设置TextureView的大小,详细就不多说了(不是懒),挑其中一类讲讲,因为主要也是这个。
例如根据视频的长宽比和屏幕的长宽比判断,如果视频宽与屏幕宽之比小于高之比,那么就需要按理比压缩宽度,然后高度适应屏幕。
例如根据旋转信息,判断TextureView界面的比例是横的还是竖的,如果View是竖的,而视频也是竖的,那么因为旋转了90度,那么让视频的高显示为屏幕的宽度,从新计算旋转后的宽度。
觉得看起来有点绕口?没关系,用着用着就习惯了····
width = widthSpecSize; height = heightSpecSize; ··· if (videoWidth * height < width * videoHeight) { width = height * videoWidth / videoHeight; } else if (videoWidth * height > width * videoHeight) { height = width * videoHeight / videoWidth; } ··· if (getRotation() != 0 && getRotation() % 90 == 0) { if (widthS < heightS) { if (width > height) { width = (int) (width * (float) widthS / height); height = widthS; } else { height = (int) (height * (float) width / widthS); width = widthS; } } else { if (width > height) { height = (int) (height * (float) width / widthS); width = widthS; } else { width = (int) (width * (float) widthS / height); height = widthS; } } }
3、UI层级逻辑播放器 GSYVideoPlayer
所有的UI逻辑基本都可以写到这里,目前继承了 FrameLayout,View.OnClickListener, View.OnTouchListener, SeekBar.OnSeekBarChangeListener, TextureView.SurfaceTextureListener和GSYMediaPlayerListener。
逻辑播放器实现的内容太多了,这里主要说几个地方,好吧,我承认我懒╮(╯_╰)╭ ,但是写太多了也没人看啊,所以这里主要是说一些关键的点,有需要留言再开个坑聊一聊,反正有DEMO。
记录界面的播放状态,把播放管理器GSYVideoManager的状态记录下来,如果有别的逻辑播放器点击播放了,就把原本的逻辑播放器状态清空,所有逻辑播放器的整个界面的UI都是根据这个State来决定的。
在逻辑播放器中统一分发各种状态,把被播放的manager状态同步到这里,之后你想要在哪个逻辑播放器里播放只需要对应的设置状态后把manager的监听同步过来。
switch (mCurrentState) { //正常初始化状态 case CURRENT_STATE_NORMAL: if (isCurrentMediaListener()) { cancelProgressTimer(); GSYVideoManager.instance().releaseMediaPlayer(); } break; //loading中 case CURRENT_STATE_PREPAREING: resetProgressAndTime(); break; //播放中 case CURRENT_STATE_PLAYING: startProgressTimer(); break; //暂停 case CURRENT_STATE_PAUSE: startProgressTimer(); break; //错误-需要判断是否切换了逻辑播放器 case CURRENT_STATE_ERROR: if (isCurrentMediaListener()) { GSYVideoManager.instance().releaseMediaPlayer(); } break; //结束 case CURRENT_STATE_AUTO_COMPLETE: cancelProgressTimer(); mProgressBar.setProgress(100); mCurrentTimeTextView.setText(mTotalTimeTextView.getText()); break; }
增加界面的onTouch事件,根据View的getId判断触摸的是进度条还是界面,如果是界面判断是左右滑动就显示Dialog并seekTo,如果是上下就根据屏幕的左边还是右边来选择是调节音量还是亮度。
··· case MotionEvent.ACTION_MOVE: float deltaX = x - mDownX; float deltaY = y - mDownY; float absDeltaX = Math.abs(deltaX); float absDeltaY = Math.abs(deltaY); //是全屏还是设置了可以触摸 if (mIfCurrentIsFullscreen || mIsTouchWiget) { //之前是否已经符合了触摸逻辑条件 if (!mChangePosition && !mChangeVolume && !mBrightness) { //如果手指动了超过一定距离就可以判断是滑动,防止点击的误判的 if (absDeltaX > mThreshold || absDeltaY > mThreshold) { cancelProgressTimer(); //如果是左右的就是进度 if (absDeltaX >= mThreshold) { mChangePosition = true; mDownPosition = getCurrentPositionWhenPlaying(); if (mVideoAllCallBack != null && isCurrentMediaListener()) { mVideoAllCallBack.onTouchScreenSeekPosition(mUrl, mObjects); } } else { //如果是上下的判断是左边还是右边 if (mFirstTouch) { mBrightness = mDownX < mScreenWidth * 0.5f; mFirstTouch = false; } if (!mBrightness) { mChangeVolume = true; mGestureDownVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); if (mVideoAllCallBack != null && isCurrentMediaListener()) { mVideoAllCallBack.onTouchScreenSeekVolume(mUrl, mObjects); } } } } } } ··· //根据flag执行逻辑
要监听TextureView.setSurfaceTextureListener来通知画面的创建和销毁,比如回到后台,onPause等。
这里有一个是TextureView的动态添加,动态添加的好处是你可以在不停止视频的情况下载不同的逻辑播放器中切换视频播放,比如列表全屏。
protected void addTextureView() { if (mTextureViewContainer.getChildCount() > 0) { mTextureViewContainer.removeAllViews(); } mTextureView = null; mTextureView = new GSYTextureView(getContext()); mTextureView.setSurfaceTextureListener(this); mTextureView.setRotation(mRotate); RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT); mTextureViewContainer.addView(mTextureView, layoutParams); } ··· //把Surface丢给视频播放管理 @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { mSurface = new Surface(surface); GSYVideoManager.instance().setDisplay(mSurface); } //告诉视频播放渲染画面销毁了 @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { GSYVideoManager.instance().setDisplay(null); surface.release(); return true; }
每次播放都要把Manager的player的监听移到当前播放的逻辑播放器,这样才能够正确的监听视频的播放状态。
//这里其实就有播放管理器的监听分发保存的逻辑需要注意 GSYVideoManager.instance().setLastListener(this); GSYVideoManager.instance().setListener(gsyVideoPlayer);
更多介绍:详见:Android 实现视屏播放器、边播边缓存功能、外加铲屎(IJKPlayer) http://bbs.jiandaima.com/thread-759-1-1.html
..