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

Android 實現視屏播放器與邊播邊緩存功能外加蹲坑鏟屎(IJKPlayer)android

 hello,你們好,我就是那個會掀桌子的話嘮,剛剛結束兩篇關於音頻播放與錄製的文章,舊坑未埋就挖新坑,還望多多關照。最近累趴了,週末果斷休假。

快看,用力戳它:github.com/CarGuo/GSYV… 。項目是翻改至JieCaoVideoPlayer,本文特長,看官請耐心,妹子會有的。

效果
git

開源播放器選擇

 Android上最爲人熟知的MediaPlayer,對,就是這貨,在上兩篇音頻文章中頻頻露臉的傢伙,此次又有它的身影,然而仍是此次不講他,就連他的封裝類VideoView也不講<( ̄︶ ̄)>,呸呸呸,又扯了一堆沒用的。 github

  • ijkplayer,此次要推薦的是它,鼎鼎大名的BILIBILI開源的播放器。基於FFMPEG,支持Android與IOS,還封裝了谷歌親兒子MediaPlayer與乾兒子EXOPlayer(爲何要用EXO),支持直播流,Star-9000多與fork-3000的視頻播放器你支持安利。(issues 600多算活躍嗎┑( ̄Д  ̄)┍)

 集成工做仍是有定的工做量的,它的DEMO確定知足不了慾求不滿的設計獅和產品汪的,這裏咱們不跑分,不打廣告,不講原理,只求站在巨人的肩膀上學(cao)習(xi),快速集成。api

  • 定義一個單例的視頻內核播放管理器。
  • 自定義一個知足你上下其手的TextureView
  • 定義一個UI層級邏輯播放器
  • 重力旋轉的相關邏輯處理
  • 列表邏輯的相關處理
  • 列表到全屏相關的邏輯處理
  • 視頻緩存邏輯

一、播放管理器: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拿到視頻寬和高,這是咱們後續正常顯示視頻的依靠之一。ide

 另一個就是setOnInfoListener,這裏咱們主要是獲取到視頻相關的元信息裏視頻旋轉角度!還記得那時候對視頻播放不熟悉,和產品還有QA力爭「這個視頻原本就是轉了90度的,我就不改,你咬我嗎···」這樣的黑歷史。Σ( ° △ °|||)工具

 特別是Android拍攝的豎屏視頻,旋轉不是視頻自己的圖像,而是增長了旋轉信息,而這個時候你須要作的就是識別它,而後轉了它丫的。另外,由於Android自己的MediaPlaer和VideoView自身就處理好因此不須要你旋轉。((ノO益O)ノ彡┻━┻親生的啊)佈局

 這裏的接口主要是把當前播放的視頻狀態和信息到返回到邏輯播放器中。post

@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);
    }
}複製代碼

二、自定義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;
        }
    }
}複製代碼

三、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);複製代碼

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


效果GIF(比較大):

1)、Window層級的

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

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

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

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

Constructor
  
  
  

 
  
  constructor = (Constructor 
 
  
    ) 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);
    }
});複製代碼

四、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)="" (!(misland="=" 1)="" &&="" !mclickport)="" return;="" }="" else="" mclickland="true;" mclick="false;" misland="1;" 1))="" screentype="ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;" activity.setrequestedorientation(activityinfo.screen_orientation_landscape);="" gsyvideoplayer.getfullscreenbutton().setimageresource(r.drawable.video_shrink);="" 設置反向橫屏="" (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();
  
  
  

 
  
  
  

 複製代碼

六、邊播邊緩存

 好吧,老婆睡了,我偷偷起來了(。・・)ノ
 這個需求曾經讓我徹夜難眠,由於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。

五、一些坑和說明

  • 一、IJKPLAY的後臺播放和回到前臺恢復畫面的速度之快是其餘播放器(我坐井觀天)沒法比擬的,真的好快,並且適合你,由於你什麼都不用作。

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

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

  • 四、千萬別開硬解碼,否則會這樣。 ( ‵o′)凸

  • 五、拖動進度條,須要在中止拖動的時候,判斷視頻是否是已經播放完了被釋放了。

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

//不要忘記配置activity,全部背景的activity

android:configChanges="orientation|keyboardHidden|screenSize"複製代碼
  • 七、普通列表中播放視頻在快速移動可能出現的錯位問題
@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級:github.com/CarGuo/GSYV…


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

友情連接:

相關文章
相關標籤/搜索