基于IJKPlayer的简易视频播放器

写在前面


PS没错,这就是那篇躺在草稿箱里好几个月的僵尸博客,直到现在(2017年1月中旬)才打算写完,简单总结一下知识点,以备不时之需。


现在的项目是一个电影预告的APP,必然得有个视频播放器,之前是用VideoView写的,并且所有功能写在一个Activity中,都没有针对播放器单独做一下封装,代码有一千两百来行,晕,代码的格式,变量的命名惨不忍睹,所以后期的功能添加和改动可以用大工程三个字来形容,并且老板也对这个播放器提过很多意见,以上各种原因,打算彻底抛弃这个播放器,重新规划。百度了好久,最后在Github上发现了一个视屏播放器(https://github.com/lipangit/JieCaoVideoPlayer),界面也挺不错,但是最终发现不太适用于现在的项目,看了下源码,觉得貌似不难,可以自己写一个,要是以后加功能或改需求,我也好有个心里准备!


Begin


开始的时候基于MediaPlayer来写,写好之后才发现,MediaPlayer真的是从入门到放弃,监听接口的调用非常诡异,比如在视频刚开始播时MediaPlayer.OnErrorListener居然被调用等等,不过这些问题可以写一堆的Boolean变量来规避,最让人受不了的就是当播放器从后台返回当前页面时的一些列问题:奔溃,画面空白,长时间的重新缓冲......,最后发现,最终的实现效果很不好,并且自己已经被这一堆的Boolean变量搞晕了!后来才知道有IJKPlayer这么个东西,接口相比于MediaPlayer就多个一个字母i,接口调用很规律,前后台的切换也无前面那些问题,完美!

先实现一堆的接口:

    1. IMediaPlayer.OnInfoListener 当前视屏播放状态,如正在开始缓存,或缓冲结束开始播放
    2. IMediaPlayer.OnPreparedListener MediaPlayer的初始化,可以做一些初始化操作
    3. IMediaPlayer.OnCompletionListener ,IMediaPlayer.OnErrorListener 看名字都知道是干毛用的
    4. IMediaPlayer.OnBufferingUpdateListener 网络视屏的缓冲进度监听
    5. SurfaceHolder.Callback  用于监听SurfaceView的状态
    6. 再实现一堆的用于更新界面的接口:如OnClickListener, OnTouchListener, SeekBar.OnSeekBarChangeListener OnAudioFocusChangeListener.....

接着就是播放器的初始化,使用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行左右,整体来说难度不大,但是需要花费不少时间和精力去完善界面与逻辑。




你可能感兴趣的:(Android)