基於IJKPlayer的簡易視訊播放器
寫在前面
PS:沒錯,這就是那篇躺在草稿箱裡好幾個月的殭屍部落格,直到現在(2017年1月中旬)才打算寫完,簡單總結一下知識點,以備不時之需。
現在的專案是一個電影預告的APP,必然得有個視訊播放器,之前是用VideoView寫的,並且所有功能寫在一個Activity中,都沒有針對播放器單獨做一下封裝,程式碼有一千兩百來行,暈,程式碼的格式,變數的命名慘不忍睹,所以後期的功能新增和改動可以用大工程三個字來形容,並且老闆也對這個播放器提過很多意見,以上各種原因,打算徹底拋棄這個播放器,重新規劃。百度了好久,最後在Github上發現了一個視屏播放器(https://github.com/lipangit/JieCaoVideoPlayer
),介面也挺不錯,但是最終發現不太適用於現在的專案,看了下原始碼,覺得貌似不難,可以自己寫一個,要是以後加功能或改需求,我也好有個心裡準備!
Begin
開始的時候基於MediaPlayer來寫,寫好之後才發現,MediaPlayer真的是從入門到放棄,監聽介面的呼叫非常詭異,比如在視訊剛開始播時MediaPlayer.OnErrorListener居然被呼叫等等,不過這些問題可以寫一堆的Boolean變數來規避,最讓人受不了的就是當播放器從後臺返回當前頁面時的一些列問題:奔潰,畫面空白,長時間的重新緩衝......,最後發現,最終的實現效果很不好,並且自己已經被這一堆的Boolean變數搞暈了!後來才知道有IJKPlayer這麼個東西,介面相比於MediaPlayer就多個一個字母i,介面呼叫很規律,前後臺的切換也無前面那些問題,完美!先實現一堆的介面:
- IMediaPlayer.OnInfoListener 當前視屏播放狀態,如正在開始快取,或緩衝結束開始播放
- IMediaPlayer.OnPreparedListener MediaPlayer的初始化,可以做一些初始化操作
- IMediaPlayer.OnCompletionListener ,IMediaPlayer.OnErrorListener 看名字都知道是幹毛用的
- IMediaPlayer.OnBufferingUpdateListener 網路視屏的緩衝進度監聽
- SurfaceHolder.Callback 用於監聽SurfaceView的狀態
- 再實現一堆的用於更新介面的介面:如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行左右,整體來說難度不大,但是需要花費不少時間和精力去完善介面與邏輯。