ExoPlayer之PlayerView源码分析

项目从MediaPlayer迁移到ExoPlayer也有一段时间了.之前看过VideoView的源码,就是对MediaPlayer的一层封装.网上对该类的分析太多了,所以不多赘述了.

当前ExoPlayer已经更新到2.8.1,在此之前,简单播放视频使用SimpleExoPlayerView, 类似于内置的VideoView.不过当前该类已经废弃,改为PlayerView.所以我们基于ExoPlayer V2.8.1对该类进行分析.

public class PlayerView extends FrameLayout {

  private static final int SURFACE_TYPE_NONE = 0;
  private static final int SURFACE_TYPE_SURFACE_VIEW = 1;
  private static final int SURFACE_TYPE_TEXTURE_VIEW = 2;

  **private final AspectRatioFrameLayout contentFrame;**
  private final View shutterView;
  **private final View surfaceView;**
  private final ImageView artworkView;
  private final SubtitleView subtitleView;
  private final @Nullable View bufferingView;
  private final @Nullable TextView errorMessageView;
  **private final PlayerControlView controller;**
  **private final ComponentListener componentListener;**
  private final FrameLayout overlayFrameLayout;

  **private Player player;**
  private boolean useController;
  private boolean useArtwork;
  private Bitmap defaultArtwork;
  private boolean showBuffering;
  private boolean keepContentOnPlayerReset;
  private @Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
  private @Nullable CharSequence customErrorMessage;
  private int controllerShowTimeoutMs;
  private boolean controllerAutoShow;
  private boolean controllerHideDuringAds;
  private boolean controllerHideOnTouch;
  private int textureViewRotation;
}

通过成员变量可以知道,该类主要的有5个成员变量.分别是AspectRatioFrameLayout, surfaceView, PlayerControlView,
ComponentListener, Player;

1.首先是AspectRatioFrameLayout.
可以查看到源码

/**
   * Sets the aspect ratio that this view should satisfy.
   * 设置视频宽高比例,然后重新布局
   * @param widthHeightRatio The width to height ratio.
   */
  public void setAspectRatio(float widthHeightRatio) {
    if (this.videoAspectRatio != widthHeightRatio) {
      this.videoAspectRatio = widthHeightRatio;
      requestLayout();
    }
  }


@Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    if (videoAspectRatio <= 0) {
      // Aspect ratio not set.
      return;
    }

    int width = getMeasuredWidth();
    int height = getMeasuredHeight();
    float viewAspectRatio = (float) width / height;
    float aspectDeformation = videoAspectRatio / viewAspectRatio - 1;
    //宽高比和目标的宽高比小于0.01f,不用刷新,主要是有一些视频,可能默认是1280 x 720 这样,但是某些机型获取到的回是1280 x 719这样的奇怪宽高
    if (Math.abs(aspectDeformation) <= MAX_ASPECT_RATIO_DEFORMATION_FRACTION) {
      // We're within the allowed tolerance.
      aspectRatioUpdateDispatcher.scheduleUpdate(videoAspectRatio, viewAspectRatio, false);
      return;
    }

    switch (resizeMode) {
      case RESIZE_MODE_FIXED_WIDTH:
        height = (int) (width / videoAspectRatio);
        break;
      case RESIZE_MODE_FIXED_HEIGHT:
        width = (int) (height * videoAspectRatio);
        break;
      case RESIZE_MODE_ZOOM:
        if (aspectDeformation > 0) {
          width = (int) (height * videoAspectRatio);
        } else {
          height = (int) (width / videoAspectRatio);
        }
        break;
      case RESIZE_MODE_FIT:
        if (aspectDeformation > 0) {
          height = (int) (width / videoAspectRatio);
        } else {
          width = (int) (height * videoAspectRatio);
        }
        break;
      case RESIZE_MODE_FILL:
      default:
        // Ignore target aspect ratio
        break;
    }
    aspectRatioUpdateDispatcher.scheduleUpdate(videoAspectRatio, viewAspectRatio, true);
    super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
        MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
  }

2.surfaceView
我们可以看到

// Create a surface view and insert it into the content frame, if there is one.
    if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) {
      ViewGroup.LayoutParams params =
          new ViewGroup.LayoutParams(
              ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
      surfaceView =
          surfaceType == SURFACE_TYPE_TEXTURE_VIEW
              ? new TextureView(context)
              : new SurfaceView(context);
      surfaceView.setLayoutParams(params);
      contentFrame.addView(surfaceView, 0);
    } else {
      surfaceView = null;
    }

surfaceview在该类里面有两种类型,一种是Surfaceview(GLSurfaceView);一种是TextureView.默认情况下,会使用TextureView.主要是因为TextureView可以做一些矩阵变化之类的操作,而SurfaceView不具备类似的操作.这样的好处在于播放视频衔接的地方,会有

@Override
    public void onVideoSizeChanged(
        int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
      if (contentFrame == null) {
        return;
      }
      float videoAspectRatio =
          (height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height;

      if (surfaceView instanceof TextureView) {
        // Try to apply rotation transformation when our surface is a TextureView.
        if (unappliedRotationDegrees == 90 || unappliedRotationDegrees == 270) {
          // We will apply a rotation 90/270 degree to the output texture of the TextureView.
          // In this case, the output video's width and height will be swapped.
          videoAspectRatio = 1 / videoAspectRatio;
        }
        if (textureViewRotation != 0) {
          surfaceView.removeOnLayoutChangeListener(this);
        }
        textureViewRotation = unappliedRotationDegrees;
        if (textureViewRotation != 0) {
          // The texture view's dimensions might be changed after layout step.
          // So add an OnLayoutChangeListener to apply rotation after layout step.
          surfaceView.addOnLayoutChangeListener(this);
        }
        applyTextureViewRotation((TextureView) surfaceView, textureViewRotation);
      }

      contentFrame.setAspectRatio(videoAspectRatio);
    }

我们知道,ExoPlayer支持视频无缝衔接,这也是当初项目之所以选择ExoPlayer的一个原因.在安卓5.0以上的机器,ExoPlayer在解码的时候,会设置视频的角度,那么解码出来的数据已经旋转好角度,我们无需自己再进行翻转.但是对于5.0一下的机器.我们可以看到,对于TextureView.在视频衔接的时候,会默认对视频进行翻转,所以我们无需适配视频的角度问题.但是对于surfaceview,我们就需要自己处理了.更加具体的解释可以看一下issus:

https://github.com/google/ExoPlayer/issues/91

3.PlayerControlView
这只是一个简单的布局,用于控制视频的播放,暂停,快进之类. 我们可以通过在xml的局部文件配置exo_controller.修改控制器的局部文件,但是对应的id不能修改

<declare-styleable name="PlayerControlView">
    <attr name="show_timeout"/>   //控制器展示时间
    <attr name="rewind_increment"/>   //回退时间
    <attr name="fastforward_increment"/>   //快进时间
    <attr name="repeat_toggle_modes"/>   //播放模式(循环播放/播放单个视频/播放到结尾结束)
    <attr name="show_shuffle_button"/>    //是否需要自摸
    <attr name="controller_layout_id"/>    
  </declare-styleable>

4.ComponentListener
我们主要关注以下

private final class ComponentListener extends Player.DefaultEventListener
      implements TimeBar.OnScrubListener, OnClickListener {

    //开始滑动进度条
    @Override
    public void onScrubStart(TimeBar timeBar, long position) {
      removeCallbacks(hideAction);
      scrubbing = true;
    }

    //滑动进度条
    @Override
    public void onScrubMove(TimeBar timeBar, long position) {
      if (positionView != null) {
        positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
      }
    }

    //停止滑动进度条
    @Override
    public void onScrubStop(TimeBar timeBar, long position, boolean canceled) {
      scrubbing = false;
      if (!canceled && player != null) {
        seekToTimeBarPosition(position);
      }
      hideAfterTimeout();
    }

    /**视频状态改变的回调,分别有一下几种状态
        //还未初始化的状态
        case Player.STATE_IDLE:
        //初始化结束,在缓冲,还不能直接播放
        case Player.STATE_BUFFERING:
        //准备好了,可以直接播放
        case Player.STATE_READY:
        //播放到视频流结尾
        case Player.STATE_ENDED:
    **/
    @Override
    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
      updatePlayPauseButton();
      updateProgress();
    }

    /**
    播放模式(循环播放/播放单个视频/播放到结尾结束)改变
    **/
    @Override
    public void onRepeatModeChanged(int repeatMode) {
      updateRepeatModeButton();
      updateNavigation();
    }

    //字幕是否可以改变
    @Override
    public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
      updateShuffleButton();
      updateNavigation();
    }

    /**
    视频的播放位置发生改变的回调
    //Seek adjustment due to being unable to seek to the requested position or because the seek was permitted to be inexact.
            case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
                break;
            //Seek within the current period or to another period.
            case DISCONTINUITY_REASON_SEEK:
                break;
            //Discontinuity to or from an ad within one period in the timeline.
            case DISCONTINUITY_REASON_AD_INSERTION:
                break;
            //Discontinuity introduced internally by the source.
            case DISCONTINUITY_REASON_INTERNAL:
                break;
            //Automatic playback transition from one period in the timeline to the next.
            case DISCONTINUITY_REASON_PERIOD_TRANSITION:
            DISCONTINUITY_REASON_PERIOD_TRANSITION是在视频连播的状态下,从A视频播放到B视频的时候的回调,这里我们可以处理一些自己的业务.其他状态可以根据自身需要去处理
    **/
    @Override
    public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
      updateNavigation();
      updateProgress();
    }

    //时间轴改变
    @Override
    public void onTimelineChanged(
        Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) {
      updateNavigation();
      updateTimeBarMode();
      updateProgress();
    }

5.Player
Player自然是指我们外部传进去的ExoPlayer了

分析完,我们会发现,其实PlayerView的逻辑挺简单,即是包装了一层自适应layout的播放器,外部通过设置视频的展示模式(Fit, Full之类)控制视频的展示大小.对应的通过设置PlayerControlView即达到修改ui的效果.但是局限性也比较大.如果需要自身拓展视频的播放的话(例如,添加滤镜之类),不能直接用该类.但是其中的设计思想我们可以借鉴.

最后,欢迎大家给我的项目点一波star,或者关注我一下

https://github.com/RuijiePan

https://github.com/RuijiePan/FileManager

要将 ExoPlayer 部署到您的项目中,您可以按照以下步骤进行操作: 1. 添加依赖项:在您的项目的 build.gradle 文件中,添加 ExoPlayer 的依赖项。请根据您的需求选择适当的版本。示例代码如下: ```groovy implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X' implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X' // 如果需要支持 DASH implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X' // 如果需要使用 ExoPlayer 的 UI 组件 ``` 2. 初始化 ExoPlayer:在您的代码中,创建一个 ExoPlayer 实例并进行初始化。您可以使用 `SimpleExoPlayer` 类来快速开始。示例代码如下: ```java SimpleExoPlayer player = new SimpleExoPlayer.Builder(context).build(); ``` 3. 准备媒体源:创建一个 `MediaSource` 对象来指定要播放的媒体源。ExoPlayer 支持多种媒体类型,包括常见的视频和音频格式,以及 DASH、SmoothStreaming 和 HLS 流。示例代码如下: ```java MediaSource mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory) .createMediaSource(Uri.parse("https://example.com/video.mp4")); ``` 4. 设置播放器和媒体源:将媒体源分配给播放器,并将播放器与播放视图关联。如果您使用 ExoPlayer 的 UI 组件,可以使用 `PlayerView` 来显示视频。示例代码如下: ```java player.prepare(mediaSource); player.setPlayWhenReady(true); // 自动播放 playerView.setPlayer(player); ``` 这些是将 ExoPlayer 部署到您的 Android 项目中的基本步骤。您还可以根据需要进行更多的自定义和配置,例如添加事件监听器、设置播放器控制器等。请参阅 ExoPlayer 的官方文档以获取更详细的信息和示例代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值