Android JiaoZiVideoPlayer原始碼分析

weixin_34138377發表於2017-11-11

序言

最近接手專案中用到了視訊播放的功能,使用了用的比較多的一個開源專案JiaoZiVideo可以迅速的讓我們實現視訊播放的相關功能。

ZJ播放器實現效果圖
ZJ播放器實現效果圖

jz播放器簡單使用

JZVideoPlayerStandard jzVideoPlayerStandard = (JZVideoPlayerStandard) findViewById(R.id.jz_vedio);
//設定播放視訊連結和視訊標題
jzVideoPlayerStandard.setUp(VEDIO_URL
                , JZVideoPlayer.SCREEN_WINDOW_NORMAL, "餃子閉眼睛");
//為播放視訊設定封面圖
jzVideoPlayerStandard.thumbImageView.setImageResource(R.mipmap.ic_launcher);複製程式碼

Jc播放器的簡單使用,只需要在佈局檔案中引入該檔案,然後為其設定待播放視訊的連結和播放視訊的封面圖即可。其它的播放相關的無需我們關心。

程式碼結構分析

核心類結構
核心類結構

該播放器的核心實現類為以上幾個。

  • JZVideoPlayer為繼承自FrameLayout實現的一個組合自定義View來實現了視訊播放器的View相關的內容。
  • JZVideoPlayerStandard則是繼承自JZVideoPlayer實現了一些自身的功能。
  • JZMediaManager是用來對於MediaPlayer的管理,對於MediaPlayer的一些監聽器方法的回撥和TextrueView的相關回撥處理。
  • JZVideoPlayerManager管理JZVideoPlayer

View實現

播放器的View實現通過一個組合自定義View的方式,最下層有一個用來放置播放視訊的View,然後是在上層一些裝飾控制元件和相關的提示View等。

播放器View實現
播放器View實現

  • 0:最底層View為視訊播放預留(TextureView)的容器
  • 1:視訊標題和返回鍵
  • 2:電量顯示和時間
  • 3:播放按鈕,在視訊播放出問題時的提示View區域
  • 4:視訊視窗最大化和最小化控制
  • 5:視訊播放進度條(SeekBar)

播放流程

播放初始化的入口也是通過開始按鈕點選所觸發的,因此對於原始碼的分析,從start點選事件處理處分析。對於開始按鈕的點選處理,這裡涉及到很多種情況,播放中,未播放,播放網路檔案在何種網路情況下,當前是全屏還是小屏等等。這裡不再貼出原始碼,只是對於相關判斷流程給予梳理。

播放前初始化流程
播放前初始化流程

這裡我們首先分析的是對於播放的情況下,這個時候會呼叫startVedio方法。

public void startVideo() {
   //結束當前的播放狀態
   JZVideoPlayerManager.completeAll();

   //初始化新增用來視訊播放的TextureView
   initTextureView();
   addTextureView();

  //設定音訊管理
   AudioManager mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
   mAudioManager.requestAudioFocus(onAudioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);

  //設定螢幕常亮
  JZUtils.scanForActivity(getContext()).getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

  //為MediaManager設定播放相關的配置資訊
   JZMediaManager.CURRENT_PLAYING_URL = JZUtils.getCurrentUrlFromMap(urlMap, currentUrlMapIndex);
   JZMediaManager.CURRENT_PLING_LOOP = loop;
   JZMediaManager.MAP_HEADER_DATA = headData;

   //開始播放狀態準備
   onStatePreparing();

   JZVideoPlayerManager.setFirstFloor(this);
   JZMediaManager.instance().positionInList = positionInList;
 }複製程式碼
  • onStatePreParing
    public void onStatePreparing() {
       currentState = CURRENT_STATE_PREPARING;
       resetProgressAndTime();
    }複製程式碼
    設定當前狀態,同時將進度和時間進行重置。在startVedio方法中,我們沒有看到具體的開啟播放的呼叫,原始碼的閱讀過程中,也是開始比較好奇的一點,這裡的開啟播放的流程是在TextureView的相應回撥中。
public void initTextureView() {
    removeTextureView();
    JZMediaManager.textureView = new JZResizeTextureView(getContext());
   JZMediaManager.textureView.setSurfaceTextureListener(JZMediaManager.instance());
}複製程式碼

在初始化TextureView 的時候為其設定了SurfaceTexture的監聽器回撥。在繼續介紹播放流程之前,先對TextureView做一個簡單的介紹。

應用程式的視訊或者opengl內容往往是顯示在一個特別的UI控制元件中:SurfaceView。SurfaceView的工作方式是建立一個置於應用視窗之後的新視窗。這種方式的效率非常高,因為SurfaceView視窗重新整理的時候不需要重繪應用程式的視窗(android普通視窗的檢視繪製機制是一層一層的,任何一個子元素或者是區域性的重新整理都會導致整個檢視結構全部重繪一次,因此效率非常低下,不過滿足普通應用介面的需求還是綽綽有餘),但是SurfaceView也有一些非常不便的限制。

因為SurfaceView的內容不在應用視窗上,所以不能使用變換(平移、縮放、旋轉等)。也難以放在ListView或者ScrollView中,不能使用UI控制元件的一些特性比如View.setAlpha()。與SurfaceView相比,TextureView並沒有建立一個單獨的Surface用來繪製,這使得它可以像一般的View一樣執行一些變換操作,設定透明度等。另外,Textureview必須在硬體加速開啟的視窗中。為了解決這個問題 Android 4.0中引入了TextureView。當TextureView被attach到當前Window之後,onSurfaceTextureAvailable方法將會被回撥。

 @Override
 public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {
    if (savedSurfaceTexture == null) {
        savedSurfaceTexture = surfaceTexture;
        prepare();
    } else {
        textureView.setSurfaceTexture(savedSurfaceTexture);
    }
 }複製程式碼

這裡對於SurfaceTexture做了快取,當判斷快取為空的時候,會為原來的快取設定新值,然後呼叫perpare方法。

public void prepare() {
    releaseMediaPlayer();
    Message msg = new Message();
    msg.what = HANDLER_PREPARE;
    mMediaHandler.sendMessage(msg);
}複製程式碼

首先對於釋放原有的相關播放資源,然後傳送HANDLER_PREPARE訊息,在JZMediaManager中建立了一個HandlerThread,接收該訊息後進行處理,以下為相關處理邏輯。

 currentVideoWidth = 0;
 currentVideoHeight = 0;

 //釋放原有的MediaPlayer,建立新的MediaPlayer
 mediaPlayer.release();
 mediaPlayer = new MediaPlayer();

//為MediaPlayer設定相關的屬性和監聽器
 mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
 mediaPlayer.setLooping(CURRENT_PLING_LOOP);
 mediaPlayer.setOnPreparedListener(JZMediaManager.this);
 mediaPlayer.setOnCompletionListener(JZMediaManager.this);                  
 mediaPlayer.setOnBufferingUpdateListener(JZMediaManager.this);
 mediaPlayer.setScreenOnWhilePlaying(true);
 mediaPlayer.setOnSeekCompleteListener(JZMediaManager.this);
 mediaPlayer.setOnErrorListener(JZMediaManager.this);
 mediaPlayer.setOnInfoListener(JZMediaManager.this);           
 mediaPlayer.setOnVideoSizeChangedListener(JZMediaManager.this);

 //通過反射的方式呼叫MediaPlayer為其設定播放源
 Class<MediaPlayer> clazz = MediaPlayer.class;
 Method method = clazz.getDeclaredMethod("setDataSource", String.class, Map.class);
 method.invoke(mediaPlayer, CURRENT_PLAYING_URL, MAP_HEADER_DATA);

 //非阻塞,有資料就會返回
 mediaPlayer.prepareAsync();
 if (surface != null) {
    surface.release();
 }

//為MediaPlayer設定surface,用來顯示解碼後的視訊
 surface = new Surface(savedSurfaceTexture);
 mediaPlayer.setSurface(surface);複製程式碼

這裡建立MediaPlayer例項,為其設定相關的監聽器,通過反射的方式為其設定了資料來源。

MediaPlayer要播放的檔案主要包括3個來源:

  • 使用者在應用中事先自帶的resource資源
    MediaPlayer.create(this, R.raw.test);複製程式碼
  • 儲存在SD卡或其他檔案路徑下的媒體檔案
    mp.setDataSource("/sdcard/test.mp3");複製程式碼
  • 網路上的媒體檔案
    mp.setDataSource("http://www.citynorth.cn/music/confucius.mp3");複製程式碼

為其設定了播放源之後呼叫了prepareAsync方法,該方法為native方法,在播放視訊前,我們可以呼叫prepare或者prepareAsync方法,第一個是阻塞的,第二個是非阻塞的,這裡無需等待,至此,我們的播放流程完成了,當我們的視訊資料來後,就可以進行播放。
對於視訊的播放這裡採用的是通過MediaPlayer做解碼操作,然後將解碼後的資料交給TextureView進行渲染顯示。(對於TextureView和繪製渲染相關的問題在接下來的原始碼分析文章中,將會展開分析)

全屏播放實現

if (currentState == CURRENT_STATE_AUTO_COMPLETE)
   return;
if (currentScreen == SCREEN_WINDOW_FULLSCREEN) {
    //quit fullscreen
    backPress();
 } else {
    onEvent(JZUserAction.ON_ENTER_FULLSCREEN);
    startWindowFullscreen();
 }複製程式碼

當點選全屏播放完成,直接返回,如果當前已經為全屏,則呼叫backPress方法回退到之前的小屏,否則呼叫startWindowFullscreen方法來開啟全屏狀態。

 public static void startFullscreen(Context context, Class _class, String url, Object... objects) {
    LinkedHashMap map = new LinkedHashMap();
    map.put(URL_KEY_DEFAULT, url);
    startFullscreen(context, _class, map, 0, objects);
 }複製程式碼

呼叫startFullscreen方法來實現全屏播放

 public static void startFullscreen(Context context, Class _class, LinkedHashMap urlMap, int defaultUrlMapIndex, Object... objects) {
    //隱藏ActionBar
    hideSupportActionBar(context);

    //獲取當前視窗的contentView,如果當前有全屏顯示視訊View,移除該View
    JZUtils.setRequestedOrientation(context, FULLSCREEN_ORIENTATION);
    ViewGroup vp = (JZUtils.scanForActivity(context))//.getWindow().getDecorView();
                .findViewById(Window.ID_ANDROID_CONTENT);
     View old = vp.findViewById(R.id.jz_fullscreen_id);
     if (old != null) {
         vp.removeView(old);
     }

    //建立一個JZVideoPlayer例項,然後新增到contentView中
     try {
        Constructor<JZVideoPlayer> constructor = _class.getConstructor(Context.class);
        final JZVideoPlayer jzVideoPlayer = constructor.newInstance(context);
        jzVideoPlayer.setId(R.id.jz_fullscreen_id);
        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        vp.addView(jzVideoPlayer, lp);
        jzVideoPlayer.setUp(urlMap, defaultUrlMapIndex, JZVideoPlayerStandard.SCREEN_WINDOW_FULLSCREEN, objects);
        CLICK_QUIT_FULLSCREEN_TIME = System.currentTimeMillis();
        //觸發開始按鈕
        jzVideoPlayer.startButton.performClick();
      } catch (InstantiationException e) {
           e.printStackTrace();
      } catch (Exception e) {
           e.printStackTrace();
       }
   }複製程式碼

開啟全屏播放的原理拿到當前頁面的contentView,然後建立一個JZVideoPlayer,設定為佈局屬性寬高為matchParent後新增到contentView之中,這個時候之前的頁面就會被覆蓋掉,看起來似乎是新開了一個頁面來做播放,此時還有一個問題就是進度的更新問題,這兩個TextureView的播放進度是如何保持同步的,這個時候之前的start點選事件再次被回撥。之前對於startVedio方法的分析,主要側重於啟動播放的流程,這裡將側重對於前一個視訊播放的處理。在startVedio中首先呼叫了

JZVideoPlayerManager.completeAll();複製程式碼

這個方法將會呼叫之前我們設定的JZVideoPlayer的onComplete方法。該方法的目的就是儲存當前播放進度,釋放掉之前播放所持有的一些資源。同時對於現有的View進行一系列的修改。

public void onCompletion() {
   if (currentState == CURRENT_STATE_PLAYING || currentState == CURRENT_STATE_PAUSE) {
        //獲取當前進度,儲存當前播放視訊的進度
       int position = getCurrentPositionWhenPlaying();
       JZUtils.saveProgress(getContext(), JZUtils.getCurrentUrlFromMap(urlMap, currentUrlMapIndex), position);
    }
    //取消當前播放進度的計算
    cancelProgressTimer();
    onStateNormal();

    //從當前View中移除當前的textureView
    textureViewContainer.removeView(JZMediaManager.textureView);
    JZMediaManager.instance().currentVideoWidth = 0;
    JZMediaManager.instance().currentVideoHeight = 0;

   //停止音訊的播放
   AudioManager mAudioManager = (AudioManager) 
   getContext().getSystemService(Context.AUDIO_SERVICE);
 mAudioManager.abandonAudioFocus(onAudioFocusChangeListener);

  // 清理掉全屏狀態下的View
  JZUtils.scanForActivity(getContext()).getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
  clearFullscreenLayout();
  JZUtils.setRequestedOrientation(getContext(), NORMAL_ORIENTATION);

  //釋放掉當前繫結的textureView和surfaceTexture
   if (JZMediaManager.surface != null) JZMediaManager.surface.release();
       JZMediaManager.textureView = null;
       JZMediaManager.savedSurfaceTexture = null;
  }複製程式碼

這裡並沒有發現進度快取相關的內容,這裡再回到startVedio方法區,這裡我們可以看到我們在播放的時候,建立了一個新的MediaPlayer物件,然後為其設定了多個監聽器,其中有個

 mediaPlayer.setOnPreparedListener(JZMediaManager.this);複製程式碼

其回撥函式如下,這裡開啟了視訊的播放,同時呼叫了播放器的onPrepared方法。

@Override
public void onPrepared(MediaPlayer mp) {
    mediaPlayer.start();
    mainThreadHandler.post(new Runnable() {
    @Override
     public void run() {
         if (JZVideoPlayerManager.getCurrentJzvd() != null) {
             JZVideoPlayerManager.getCurrentJzvd().onPrepared();
         }
     }
   });
 }複製程式碼
public void onPrepared() {
    if (JZUtils.getCurrentUrlFromMap(urlMap, currentUrlMapIndex).toLowerCase().contains("mp3")) {
            onStatePrepared();
            onStatePlaying();
     }
 }複製程式碼

該方法會將獲得我們之前的播放進度,然後將當前的MediaPlayer調節到當前進度。

public void onStatePrepared() {
    if (seekToInAdvance != 0) {
       JZMediaManager.instance().mediaPlayer.seekTo(seekToInAdvance);
       seekToInAdvance = 0;
    } else {
        int position = JZUtils.getSavedProgress(getContext(), JZUtils.getCurrentUrlFromMap(urlMap, currentUrlMapIndex));
         if (position != 0) {
              JZMediaManager.instance().mediaPlayer.seekTo(position);
         }
   }
}複製程式碼

開始進度條的計時。

public void onStatePlaying() {
    currentState = CURRENT_STATE_PLAYING;
    startProgressTimer();
}複製程式碼

這裡可以看到在prepared的時候,只是對於mp3型別的進行了進度的改變,但是對於視訊型別並沒有做處理,而是在註冊的OnInfo監聽器的onInfo方法中進行了回撥。

  public void onInfo(int what, int extra) {
        if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
           onStatePrepared();
           onStatePlaying();
       }
  }複製程式碼

當返回的資訊為開始渲染播放,則呼叫onStatePrepared和onStatePlaying方法,設定我們之前儲存進度,同時開啟計時機制。相比之前的onPrepared回撥,這個可以保證當我們的視訊開始顯示的時候才會去做進度的調整。

該播放器還支援右下角小視窗的播放,播放原理和正常播放到全屏的實現也是類似。對於原始碼的分析這裡只是在該播放器的相關業務的實現上,具體的核心都是在Mediaplayer和TextureView中,接下來將會針對這兩塊的原始碼進行一個梳理和分析。

參考

Android TextureView簡易教程

Android MediaPlayer 播放各種來源的音訊

相關文章