PS:没错,这就是那篇躺在草稿箱里好几个月的僵尸博客,直到现在(2017年1月中旬)才打算写完,简单总结一下知识点,以备不时之需。
现在的项目是一个电影预告的APP,必然得有个视频播放器,之前是用VideoView写的,并且所有功能写在一个Activity中,都没有针对播放器单独做一下封装,代码有一千两百来行,晕,代码的格式,变量的命名惨不忍睹,所以后期的功能添加和改动可以用大工程三个字来形容,并且老板也对这个播放器提过很多意见,以上各种原因,打算彻底抛弃这个播放器,重新规划。百度了好久,最后在Github上发现了一个视屏播放器(https://github.com/lipangit/JieCaoVideoPlayer),界面也挺不错,但是最终发现不太适用于现在的项目,看了下源码,觉得貌似不难,可以自己写一个,要是以后加功能或改需求,我也好有个心里准备!
开始的时候基于MediaPlayer来写,写好之后才发现,MediaPlayer真的是从入门到放弃,监听接口的调用非常诡异,比如在视频刚开始播时MediaPlayer.OnErrorListener居然被调用等等,不过这些问题可以写一堆的Boolean变量来规避,最让人受不了的就是当播放器从后台返回当前页面时的一些列问题:奔溃,画面空白,长时间的重新缓冲......,最后发现,最终的实现效果很不好,并且自己已经被这一堆的Boolean变量搞晕了!后来才知道有IJKPlayer这么个东西,接口相比于MediaPlayer就多个一个字母i,接口调用很规律,前后台的切换也无前面那些问题,完美!
先实现一堆的接口:
接着就是播放器的初始化,使用SurfaceView播放视频
SurfaceHolder holder= mSurface.getHolder(); holder.addCallback(this); IjkMediaPlayer player = new IjkMediaPlayer(); player.reset(); try { //设置视频url mPlayer.setDataSource(getContext(), Uri.parse(url)); } catch (IOException e) { e.printStackTrace(); } @Override public void surfaceCreated(SurfaceHolder holder) { //指定MediaPlayer在当前的Surface中进行播放 mPlayer.setDisplay(mHolder); }
处理音频相关的操作:
mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE); audioFocusListener = new AudioManager.OnAudioFocusChangeListener() { @Override public void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_GAIN: break; case AudioManager.AUDIOFOCUS_LOSS: // 长久的失去音频焦点,释放MediaPlayer //stop(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: // 暂时失去音频焦点,暂停播放等待重新获得音频焦点 pause(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: break; } } }; //申请音频焦点 mAudioManager.requestAudioFocus(audioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
至于视屏的填充样式,参考Windows的背景做了四种,分别为:适应,填充,拉伸,居中。如果不做任何处理就是拉伸效果,项目中默认使用适应效果,其实这个项目根本用不到填充和居中,我就是写着玩的。
switch (mode) { case FILL_MODE_ADAPT://适应 if ((float) vWidth / vHeight > (float) width / height) { //视屏的高不足以填充屏幕,宽度填充,计算合适的高度 params.width = width; params.height = width * vHeight / vWidth; } else { //视屏的宽不足以填充屏幕,高度填充,计算合适的宽度 params.width = height * vWidth / vHeight; params.height = height; } break; case FILL_MODE_FILL://填充 if ((float) vWidth / vHeight > (float) width / height) { //视屏的高不足以填充屏幕,宽度填充,舍弃部分宽度,高度填充 params.width = height * vWidth / vHeight; params.height = height; } else { //视屏的宽不足以填充屏幕,高度填充,舍弃部分高度,宽度度填充 params.width = width; params.height = width * vHeight / vWidth; } break; case FILL_MODE_STRETCH://拉伸 //不做任何处理就是拉伸 break; case FILL_MODE_CENTER://居中 params.width = vWidth; params.height = vHeight; break; }
视频的进度调节和音量调节,使用Touch时间去处理,当Touch事件为MotionEvent.ACTION_UP时,使用IjkMediaPlayer.seekTo(long var1)方法定位视频的播放位置,参数为要定为的视频时间点,而视频的总时间可以通过IjkMediaPlayer.getDuration()方法获取,下面是手势处理中的视频进度调节Dialog
/** * 手势视屏进度条 * * @param dx 当前手指所在的点相对于初始点在X轴上划过的距离 */ private void showVideoProgressDialog(float dx) { if (mVideoProgressDialog == null) { View view = LayoutInflater.from(getContext()).inflate(R.layout.player_video_progress, this, false); mProgressProgress = (ProgressBar) view.findViewById(R.id.player_progress_progress); mProgressTips = (ImageView) view.findViewById(R.id.player_progress_tips); mProgressTime = (TextView) view.findViewById(R.id.player_progress_time); mVideoProgressDialog = new Dialog(getContext(), R.style.style_dialog_progress); mVideoProgressDialog.setContentView(view); mVideoProgressDialog.getWindow().setLayout(dip2px(190), dip2px(100)); } if (!mVideoProgressDialog.isShowing()) { //初始化进度框的大小及位置 WindowManager.LayoutParams localLayoutParams = mVideoProgressDialog.getWindow().getAttributes(); localLayoutParams.gravity = (Gravity.CENTER_HORIZONTAL | Gravity.TOP); localLayoutParams.y = (getHeight() - dip2px(100)) / 2; mVideoProgressDialog.getWindow().setAttributes(localLayoutParams); mVideoProgressDialog.show(); tempProgress = currentProgress; tempVideoPosition = mPlayer.getCurrentPosition(); } //根据屏宽与手指相对于初始点在X轴上划过的距离计算进度条该显示的百分比 // slideProgress 的值也就是手势灵敏度 float slideProgress = (dx - minSideDistance) * 1.0f / getSWidth(); int progress = (int) (tempProgress + slideProgress * 100); mProgressProgress.setProgress(progress); //进度时间 mProgressTime.setText(String.format("%s/%s", stringForTime((int) ((mPlayer.getDuration() * slideProgress) + tempVideoPosition)), stringForTime((int) mPlayer.getDuration()))); if (Math.abs(dx - lastX) > minSideDistance) { if (dx > oldDx) {//右滑 mProgressTips.setImageResource(R.mipmap.forward_icon); } else {//左滑 mProgressTips.setImageResource(R.mipmap.backward_icon); } lastX = dx; } }
通过IMediaPlayer.OnInfoListener的接口可以获取当前视频的播放状态信息,这里我只用了以下几个,还有很多状态,感兴趣可以自己去看官方文档
@Override public boolean onInfo(IMediaPlayer iMediaPlayer, int what, int extra) { Log.d("xxx", "-----onInfo---- what: " + what); switch (what) { case IMediaPlayer.MEDIA_INFO_BUFFERING_START://网络不好,视屏卡住了 701 updatePlayMark(PLAYER_MARK_BUFFERING_START);//显示缓冲图标 isBuffering = true; break; case IMediaPlayer.MEDIA_INFO_BUFFERING_END://网络良好,视屏开始播放了 702 isBuffering = false; updatePlayMark(PLAYER_MARK_BUFFERING_END);//隐藏缓冲图标 break; case IMediaPlayer.MEDIA_INFO_AUDIO_RENDERING_START://每准备一次调用一次 1002 mSurface.setBackgroundColor(Color.TRANSPARENT); isStartPlay = true; isBuffering = false; updatePlayMark(PLAYER_MARK_FIRST_PLAY);//首次播放 break; } return false; }
然后就可以播放了:
private void startPlay() { if (isStartPlay) { mPlayer.start(); updatePlayMark(PLAYER_MARK_PLAY); } else { mPlayer.prepareAsync();//准备并开始播放,这里与MediaPlayer不同 updatePlayMark(PLAYER_MARK_BUFFERING_START); isBuffering = true; if (!isPlayNext) { delayHideTopBottom(); } } }
那么播放过程中的播放进度改怎么监听呢?没错就是用IMediaPlayer.OnBufferingUpdateListener,但这只是监听缓冲进度而已,而播放进度可以通过在视频准备播放时使用Handler和Runnable形成一个每隔固定时间段的循环来实现,在这个循环中通过IjkMediaplayer.getCurrentPosition()来计算相应的进度信息
@Override public void onPrepared(IMediaPlayer iMediaPlayer) { ... mHandler.post(mRunnable); }
//每隔0.5秒更新视屏界面信息,如进度条,当前播放时间点等等 mRunnable = new Runnable() { @Override public void run() { float position = mPlayer.getCurrentPosition(); currentProgress = (int) ((position / mPlayer.getDuration()) * 100); mSeekBar.setProgress(currentProgress); mTipsProgress.setProgress(currentProgress); mCurrentTime.setText(stringForTime((int) position)); mHandler.postDelayed(mRunnable, 500); } };
看看最终的实现效果:
至于上下信息栏的显示与隐藏,可以用属性动画来实现。当播放下一个视频时需要注意将IjkMediaPlayer重置,还有那一堆的界面UI和用于判断的Boolean变量。
/** * 播放下一视屏 */ public void playNext(String videoTitle, String videoUrl) { isStartPlay = false; isPlayNext = true; mPlayer.reset(); mPlayer.setDisplay(mHolder); setVideoMsg(videoTitle, videoUrl); start(); }
视频播放结束时,释放IjkMediaPlayer资源,释放音频焦点,移除Handler的循环。
/** * 停止播放视屏,释放资源 */ public void stop() { if (mPlayer.isPlaying()) { mPlayer.stop(); } currentPlayerColum--; if (currentPlayerColum <= 0) { mPlayer.release(); currentPlayerColum = 0; } mHandler.removeCallbacks(mRunnable); tbHandler.removeCallbacks(tbRunnable); //释放音频焦点 mAudioManager.abandonAudioFocus(audioFocusListener); screenOrientationSwitcher.disable(); }
那么最后如何处理屏幕的旋转呢,我们项目中使用的方法,在当前界面完成,旋转屏幕时通过监听屏幕的旋转,重新设置播放器界面的大小,并处理其他UI。其实从8月份到现在github上也有很多优秀的基于IjkPlayer的播放器开源,有的是采用另外一个activity中实现全屏播放。另外默认使用Gradle导入的IjkPlayer默认是不支持HTTPS的,恰巧我们项目中用的就是https,但幸运的是我去掉s视频也是可以播放的,如果需要支持https的话需要自己编译IjkPlayer了,或者你可以直接用github上的别人编译好的。还有一个问题就是当时在做的时候发现,如果项目含有.so文件时,在部分手机上会奔溃,比如说同事的华为和老板的一个锤子手机上,奔溃信息显示来自IjkPlayer的底层,好几个月过去当时截屏的奔溃信息也丢了......
End
通过写这个播放器也学到了不少东西,起码对视频播放有了个大致的了解。播放器代码总共1000行左右,整体来说难度不大,但是需要花费不少时间和精力去完善界面与逻辑。