Android JiaoZiVideoPlayer源碼分析

序言

最近接手項目中用到了視頻播放的功能,使用了用的比較多的一個開源項目JiaoZiVideo能夠迅速的讓咱們實現視頻播放的相關功能。html

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播放器的簡單使用,只須要在佈局文件中引入該文件,而後爲其設置待播放視頻的連接和播放視頻的封面圖便可。其它的播放相關的無需咱們關心。android

代碼結構分析

核心類結構

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

  • JZVideoPlayer爲繼承自FrameLayout實現的一個組合自定義View來實現了視頻播放器的View相關的內容。github

  • JZVideoPlayerStandard則是繼承自JZVideoPlayer實現了一些自身的功能。緩存

  • JZMediaManager是用來對於MediaPlayer的管理,對於MediaPlayer的一些監聽器方法的回調和TextrueView的相關回調處理。網絡

  • JZVideoPlayerManager管理JZVideoPlayeride

View實現

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

播放器View實現

  • 0:最底層View爲視頻播放預留(TextureView)的容器oop

  • 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 播放各類來源的音頻

相關文章
相關標籤/搜索