1. 程式人生 > >基於IJKPlayer的簡易視訊播放器

基於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行左右,整體來說難度不大,但是需要花費不少時間和精力去完善介面與邏輯。