Android 實現視屏播放器、邊播邊緩存功能、外加鏟屎(IJKPlayer)(轉載)

轉載自:《Android 實現視屏播放器、邊播邊緩存功能、外加鏟屎(IJKPlayer)》

 hello,大家好,我就是那個會掀桌子的話嘮,剛剛結束兩篇關於音頻播放與錄製的文章,舊坑未埋就挖新坑,還望多多關照。最近累趴了,週末果斷休假。
快看,用力戳它:https://github.com/CarGuo/GSYVideoPlayer 。項目是翻改至JieCaoVideoPlayer,本文特長,看官請耐心,妹子會有的。

效果


開源播放器選擇

 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事件,根據ViewgetId判斷觸摸的是進度條還是界面,如果是界面判斷是左右滑動就顯示DialogseekTo,如果是上下就根據屏幕的左邊還是右邊來選擇是調節音量還是亮度
···
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);

3、列表全屏邏輯 :Window層級的全屏、單例邏輯播放器的全屏ListVideoUtil。


效果GIF(比較大):

1)、Window層級的

 
 傳聞每一個Activity都有一個com.android.internal.R.id.content,它默默的包含了各種你塞進去的物體,而且是一個FrameLayout,谷歌有太多它的傳說了,我們用它是就是。

 既然是FrameLayout,那麼我們往他裏面塞東西就好了,這裏我們可以在GSYVideoPlayer裏面寫一個方法,在點擊全屏按鈕的時候:

  • 隱藏狀態欄,清除當前TextureView。
  • 然後新創建一個GSYVideoPlayer2,只有把這個G2添加到window下FrameLayout
  • 設置它的播放狀態和當前列表這個邏輯播放器一致。
  • 最後把G2告知Manager承接畫面,這樣是就實現了無縫的列表到全屏啦,返回只需要倒着做就好了。

 
 在切換的時候可以做一些位移動畫,讓播放器的全屏更加友好,下面長代碼來襲((/- -)/。深夜碼字不易,不知道爲什麼每次這個時候老婆的意見很大啊。

Constructor<GSYBaseVideoPlayer> constructor = (Constructor<GSYBaseVideoPlayer>) GSYBaseVideoPlayer.this.getClass().getConstructor(Context.class);
final GSYBaseVideoPlayer gsyVideoPlayer = constructor.newInstance(getContext());
//記錄新創建的這個video的id,在返回的時候通過它銷燬
gsyVideoPlayer.setId(FULLSCREEN_ID);
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
final int w = wm.getDefaultDisplay().getWidth();
final int h = wm.getDefaultDisplay().getHeight();
//設置黑色背景,自動充滿全屏
FrameLayout.LayoutParams lpParent = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
FrameLayout frameLayout = new FrameLayout(context);
frameLayout.setBackgroundColor(Color.BLACK);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    //如果5.0的話,先讓播放器出現的位置和列表中一直,再樣式一會執行到屏幕中間的過度動畫效果
    FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(getWidth(), getHeight());
    lp.setMargins(mListItemRect[0], mListItemRect[1], 0, 0);
    frameLayout.addView(gsyVideoPlayer, lp);
    vp.addView(frameLayout, lpParent);
    mHandler.postDelayed(new Runnable() {
        @Override
        public void run() {
            TransitionManager.beginDelayedTransition(vp);
            resolveFullVideoShow(context, gsyVideoPlayer, h, w);
        }
    }, 300);
} else {
    //5.0一下直接顯示
    FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(getWidth(), getHeight());
    frameLayout.addView(gsyVideoPlayer, lp);
    vp.addView(frameLayout, lpParent);
    resolveFullVideoShow(context, gsyVideoPlayer, h, w);
}
//設置全屏邏輯播放器的狀態,動態及添加播放view
gsyVideoPlayer.setUp(mUrl, mCache, mObjects);
gsyVideoPlayer.setStateAndUi(mCurrentState);
gsyVideoPlayer.addTextureView();
//添加監聽
GSYVideoManager.instance().setLastListener(this);
GSYVideoManager.instance().setListener(gsyVideoPlayer);
2)、ListVideoUtil的單例模式

 這裏利用另外一種實現思路,列表的邏輯播放器只用一個,因爲普通的list在滑動的時候會有複用和銷燬,這會導致視頻被釋放而停止了,如果你是和今日黃(tou)條一樣的視頻列表播放效果,滑出屏幕就停止那無所謂。

 如果你需要無論怎麼滑動,視頻都在原來的位置播放的話,那麼ListVideoUtil適合你,,內部它已經帶了全屏,防錯位,旋轉的各種邏輯,直接上代碼,有興趣的看DEMO。

listVideoUtil = new ListVideoUtil(this);
//設置列表最外層的佈局用於全屏,空FrameLayout
listVideoUtil.setFullViewContainer(videoFullContainer);
//全屏隱藏狀態欄,如果有的話
listVideoUtil.setHideStatusBar(true);

···
//在列表中吧列表位置,封面,哪個列表的TAG,列表視頻的承載ViewGroup,播放按鍵傳入到Utils中
listVideoUtil.addVideoPlayer(position, imageView, TAG, holder.videoContainer, holder.playerBtn);
holder.playerBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //每次播放都要更新列表讓其他的item恢復狀態
        notifyDataSetChanged();
        //設置播放的tag和位置,防止錯位
        listVideoUtil.setPlayPositionAndTag(position, TAG);
        //開始播放
        final String url = "http://baobab.wdjcdn.com/14564977406580.mp4";
        listVideoUtil.startPlay(url);
    }
});

4、OrientationUtils 重力旋轉的工具類

OrientationUtils使用的是OrientationEventListener,通過手機的角度判斷需要旋轉到哪個位置。爲什麼用它?因爲谷歌到的時候剛好看到,緣分啊懂嗎。

這裏需要個關注的是手動點擊和自動旋轉之間的衝突,主要看代碼吧,老婆開始催我了 (ノಠ益ಠ)ノ彡┻━┻。

//判斷系統是否開了旋轉,是的,這貨不需要系統旋轉是否開啓
boolean autoRotateOn = (android.provider.Settings.System.getInt(activity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 1);
        if (!autoRotateOn) {
            if (mIsLand == 0) {
                return;
            }
        }
        // 設置豎屏
        if (((rotation >= 0) && (rotation <= 30)) || (rotation >= 330)) {
            //是否點擊導致的
            if (mClick) {
                if (mIsLand > 0 && !mClickLand) {
                    return;
                } else {
                    //清除狀態
                    mClickPort = true;
                    mClick = false;
                    mIsLand = 0;
                }
            } else {
                //自動旋轉
                if (mIsLand > 0) {
                    screenType = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
                    activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
                    gsyVideoPlayer.getFullscreenButton().setImageResource(R.drawable.video_enlarge);
                    mIsLand = 0;
                    mClick = false;
                }
            }
        }
        // 設置橫屏
        else if (((rotation >= 230) && (rotation <= 310))) {
            if (mClick) {
                if (!(mIsLand == 1) && !mClickPort) {
                    return;
                } else {
                    mClickLand = true;
                    mClick = false;
                    mIsLand = 1;
                }
            } else {
                if (!(mIsLand == 1)) {
                    screenType = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
                    activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
                    gsyVideoPlayer.getFullscreenButton().setImageResource(R.drawable.video_shrink);
                    mIsLand = 1;
                    mClick = false;
                }
            }
        }
        // 設置反向橫屏
        else if (rotation > 30 && rotation < 95) {
            if (mClick) {
                if (!(mIsLand == 2) && !mClickPort) {
                    return;
                } else {
                    mClickLand = true;
                    mClick = false;
                    mIsLand = 2;
                }
            } else if (!(mIsLand == 2)) {
                screenType = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
                activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
                gsyVideoPlayer.getFullscreenButton().setImageResource(R.drawable.video_shrink);
                mIsLand = 2;
                mClick = false;
            }
        }
    }
};
orientationEventListener.enable();

6、邊播邊緩存

 好吧,老婆睡了,我偷偷起來了(。・・)ノ
 這個需求曾經讓我徹夜難眠,因爲IJKPlayer不支持,好吧,沒見過哪個播放器支持的,和產品爭(tuo)論(yan)需(shi)求(jian)之後,最終還是github大法好:AndroidVideoCache

 接入簡單,使用簡單,你可以趾高氣揚的和產品說,這個so easy了。

HttpProxyCacheServer proxy = getProxy();
//注意不能傳入本地路徑,本地的你還傳進來幹嘛。
String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
videoView.setVideoPath(proxyUrl);

 該項目的原理其實就是將url鏈接轉化爲本地鏈接 h t t p://127.0.0.1:LocalPort/url,然後它開一個服務器一邊下載緩存視頻,一邊把緩存的數據正常返回給你的播放器,如果已經緩存過的這裏會返回一個本地文件路徑。Σ( ° △ °|||)︴曾經的我真的是too young too smiple。

5、一些坑和說明

  • 1、IJKPLAY的後臺播放和回到前臺恢復畫面的速度之快是其他播放器(我坐井觀天)無法比擬的,真的好快,而且適合你,因爲你什麼都不用做。

  • 2、IJKPLAY有一個問題,我也提過ISSUSE了 #2104,不過目前還未解決,就是某些短小的視頻會無法seekTo,說是FFMEPG的問題,然後就太監了。

  • 3、IJKPLAY庫裏還封裝了exoplayer谷歌乾兒子,用法也基本一致,這個播放器自己內部判斷旋轉,不會有上面的seekto問題,可是後臺或者onPause之後的畫面恢復速度堪憂啊,各位遇到過嗎?

  • 4、千萬別開硬解碼,不然會這樣。 ( ‵o′)凸

  • 5、拖動進度條,需要在停止拖動的時候,判斷視頻是不是已經播放完了被釋放了。

  • 6、如果橫屏全屏的話,恢復到正常畫面是最好有一個延時,這樣畫面纔不會出現背景抖動的問題,還有最關鍵的,Maifest文件。

//不要忘記配置activity,所有背景的activity

android:configChanges="orientation|keyboardHidden|screenSize"
  • 7、普通列表中播放視頻在快速移動可能出現的錯位問題
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    int lastVisibleItem = firstVisibleItem + visibleItemCount;
    //大於0說明有播放
    if (GSYVideoManager.instance().getPlayPosition() >= 0) {
        //當前播放的位置
        int position = GSYVideoManager.instance().getPlayPosition();
        //對應的播放列表TAG
        if (GSYVideoManager.instance().getPlayTag().equals(ListNormalAdapter.TAG)
                && (position < firstVisibleItem || position > lastVisibleItem)) {
            //如果滑出去了上面和下面就是否,和今日頭條一樣
            GSYVideoPlayer.releaseAllVideos();
            listNormalAdapter.notifyDataSetChanged();
        }
    }
}

到底了呢(^o^)/。

下面的的看到了嗎 ?<( ̄︶ ̄)>

點我點我上60級:https://github.com/CarGuo/GSYVideoPlayer


能看到這裏都是真愛啊,我最後問兩句,你們會覺得文章太長閱讀起來比較費勁嗎?

 
相關文章: Android 列表視頻的全屏、自動小窗口優化實踐
 
友情鏈接:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章