2

序言

最近接手项目中用到了视频播放的功能,使用了用的比较多的一个开源项目JiaoZiVideo可以迅速的让我们实现视频播放的相关功能。

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实现

  • 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 播放各种来源的音频


Jensen95
2.9k 声望534 粉丝

连续创业者。