ExoPlayer 漫谈之架构剖析

3,982 阅读10分钟

ExoPlayer 是google封装的一个极其优秀的播放框架,目前youtube等很多应用都在使用它来播放视频,ExoPlayer的更新非常快,基本上保持着半个月更新一个版本的节奏; ExoPlayer本质上是使用MediaCodec来解码视频,但是其中的流程非常复杂,所有我们由浅入深的讲解一下,很多地方也是刚开始看,看得不详细,向大家汇报一下吧。

概要

ExoPlayer旨在对正在播放的媒体类型,媒体的存储方式和存储方式以及呈现方式做出很少的假设(并因此而施加了一些限制)。 ExoPlayer实现不是直接实现媒体的加载和渲染,而是将这项工作委托给在创建播放器或准备播放时注入的组件。所有ExoPlayer实现共同的组件是:

  • 一个MediaSource,它定义要播放的媒体,加载媒体,并从中读取加载的媒体。在播放开始时通过prepare(MediaSource)注入MediaSource。库模块为渐进式媒体文件(ProgressiveMediaSource),DASH(DashMediaSource),SmoothStreaming(SsMediaSource)和HLS(HlsMediaSource)提供了默认实现,该实现是用于加载单个媒体样本(SingleSampleMediaSource)的一种实现,该样本最常用于侧面加载的字幕文件,以及用于从更简单的媒体源构建更复杂的媒体源的实现(MergingMediaSource,ConcatenatingMediaSource,LoopingMediaSource和ClippingMediaSource)。
  • 渲染媒体的各个组成部分的渲染器。该库提供了常见媒体类型(MediaCodecVideoRenderer,MediaCodecAudioRenderer,TextRenderer和MetadataRenderer)的默认实现。渲染器使用正在播放的MediaSource中的媒体。创建播放器时将注入渲染器。
  • 一个TrackSelector,它选择MediaSource提供的轨道以供每个可用的渲染器使用。该库提供适合大多数使用情况的默认实现(DefaultTrackSelector)。创建播放器时会注入TrackSelector。
  • 一个LoadControl,它控制MediaSource何时缓冲更多媒体,以及缓冲多少媒体。该库提供适合大多数使用情况的默认实现(DefaultLoadControl)。创建播放器时将注入LoadControl。

可以使用库提供的默认组件来构建ExoPlayer,但是如果需要非标准行为,也可以使用自定义实现来构建。例如,可以注入自定义LoadControl来更改播放器的缓冲策略,或者可以注入自定义Renderer来添加对Android原生不支持的视频编解码器的支持。

在整个库中都存在注入实现播放器功能的组件的概念。上面列出的默认组件实现将工作委托给其他注入的组件。这允许将许多子组件分别替换为自定义实现。例如,默认的MediaSource实现需要通过其构造函数注入一个或多个DataSource工厂。通过提供自定义工厂,可以从非标准来源或通过其他网络堆栈加载数据。

下面是ExoPlayer的线程模型: exoplayer线程模型

  • 必须从单个应用程序线程访问ExoPlayer实例。在绝大多数情况下,这应该是应用程序的主线程。使用ExoPlayer的UI组件或IMA扩展时,也需要使用应用程序的主线程。创建播放器时,可以通过传递“ Looper”来明确指定必须访问ExoPlayer实例的线程。如果未指定“ Looper”,则使用创建播放器的线程的“ Looper”,或者如果该线程不具有“ Looper”,则使用应用程序主线程的“ Looper”。在所有情况下,都可以使用Player.getApplicationLooper()查询必须从中访问播放器的线程的“ Looper”。
  • 在与Player.getApplicationLooper()关联的线程上调用已注册的侦听器。请注意,这意味着已注册的侦听器在必须用于访问播放器的同一线程上调用。
  • 内部回放线程负责回放。播放器在此线程上调用注入的播放器组件,例如Renderer,MediaSources,TrackSelectors和LoadControls。
  • 当应用程序在播放器上执行操作(例如搜索)时,消息会通过消息队列传递到内部回放线程。内部回放线程使用队列中的消息并执行相应的操作。类似地,当内部回放线程上发生回放事件时,一条消息将通过第二个消息队列传递到应用程序线程。应用程序线程使用队列中的消息,更新应用程序的可见状态并调用相应的侦听器方法。
  • 注入的播放器组件可能会使用其他后台线程。例如,MediaSource可以使用后台线程来加载数据。

下面是写一个ExoPlayer demo的基本调用流程,其中的关系还是比较简单,我们可以从这张图展开,全面分析一下ExoPlayer的工作机制。 ExoPlayer调用时序图

一、MediaSource关系梳理

MediaSource是ExoPlayer中非常重要的结构,加载啊的视频资源要经过MediaSource封装,MediaSource里面还有解析这些视频资源的逻辑;下面是MediaSource类结构关系图; MediaSource类图.png

1.1 DashMediaSource

DashMediaSource专门用来解析Dash格式的视频,这里所说的格式都是封装格式,不是编解码格式,下面也是一样的; Dynamic Adaptive Streaming over HTTP,基于HTTP的动态自适应流,DASH是一项自适性流技术,其将多媒体文件分割为一个或多个片段,并使用超文本传输协议传递给客户端;

1.2 HlsMediaSource

HlsMediaSource专门用来解析HLS格式的视频,HLS---> Http Live Streaming技术,是Apple首先倡导的,目前已经成为直播流的基本技术构成;HLS的格式区分非常细,感兴趣可以取了解一下HLS协议规范;

1.3 SsMediaSource

SsMediaSource 是微软提出的针对SmoothStreaming技术的解决方案,其和DASH和HLS有很多共通之处;

1.4 ProgressiveMediaSource

这是针对一般视频的MediaSource封装方案,所谓一般,就是非DASH、HLS、SS类型的视频; 下面是ProgressiveMediaSource支持的视频的封装格式;

Container formatSupportedComment
MP4YES 
M4AYES 
FMP4YES 
WebMYES 
MatroskaYES 
MP3YESSome streams only seekable using constant bitrate seeking**
OggYESContaining Vorbis, Opus and FLAC
WAVYES 
MPEG-TSYES 
MPEG-PSYES 
FLVYESNot seekable*
ADTS (AAC)YESOnly seekable using constant bitrate seeking**
FLACYESFLAC extension only
AMRYESOnly seekable using constant bitrate seeking**

小结:目前DASH和SS已经用的相对较少,我们主要分析HLS和ProgressiveMediaSource两种类型;

1.5 MediaSource 判断规则

  private MediaSource createLeafMediaSource(
      Uri uri, String extension, DrmSessionManager<ExoMediaCrypto> drmSessionManager) {
    @ContentType int type = Util.inferContentType(uri, extension);
    switch (type) {
      case C.TYPE_DASH:
        return new DashMediaSource.Factory(dataSourceFactory)
            .setDrmSessionManager(drmSessionManager)
            .createMediaSource(uri);
      case C.TYPE_SS:
        return new SsMediaSource.Factory(dataSourceFactory)
            .setDrmSessionManager(drmSessionManager)
            .createMediaSource(uri);
      case C.TYPE_HLS:
        return new HlsMediaSource.Factory(dataSourceFactory)
            .setDrmSessionManager(drmSessionManager)
            .createMediaSource(uri);
      case C.TYPE_OTHER:
        return new ProgressiveMediaSource.Factory(dataSourceFactory)
            .setDrmSessionManager(drmSessionManager)
            .createMediaSource(uri);
      default:
        throw new IllegalStateException("Unsupported type: " + type);
    }
  }

各种MediaSource类型是根据一定的规则来使用的,上面是判断的逻辑,提取的关键是根据uri和extension得到的type类型;

  public static int inferContentType(String fileName) {
    fileName = toLowerInvariant(fileName);
    if (fileName.endsWith(".mpd")) {
      return C.TYPE_DASH;
    } else if (fileName.endsWith(".m3u8")) {
      return C.TYPE_HLS;
    } else if (fileName.matches(".*\\.ism(l)?(/manifest(\\(.+\\))?)?")) {
      return C.TYPE_SS;
    } else {
      return C.TYPE_OTHER;
    }
  }

这儿的判断逻辑就非常简单,直接判断一下uri的fileName,这样针对HLS的判断不是很准确,很多HLS类型的视频会被判断成非HLS类型,之后的HLS格式分析会讲到;

二、MediaSource解析

MediaSource解析流程 这里有很多Factory类,这是ExoPlayer将各个模块的工作统一封装在Factory类中,然后由Factory统一管理;

ExoPlayer中有DefaultHttpDataSourceFactory ---> DefaultHttpDataSource 来实现对HLS资源的解析工作;

HlsMediaSource.Factory构造函数中会传入一个DataSource.Factory对象,这个对象就是DefaultHttpDataSourceFactory 对象,这个对象负责http请求;

    public Factory(DataSource.Factory dataSourceFactory) {
      this(new DefaultHlsDataSourceFactory(dataSourceFactory));
    }

ExoPlayer中很多Factory类,基本上每个模块都有相应的Factory类管理这个模块的相应工作; DefaultHttpDataSource中有针对相应的file path解析的工作:

  public long open(DataSpec dataSpec) throws IOException {
    Assertions.checkState(dataSource == null);
    // Choose the correct source for the scheme.
    String scheme = dataSpec.uri.getScheme();
    if (Util.isLocalFileUri(dataSpec.uri)) {
      String uriPath = dataSpec.uri.getPath();
      if (uriPath != null && uriPath.startsWith("/android_asset/")) {
        dataSource = getAssetDataSource();
      } else {
        dataSource = getFileDataSource();
      }
    } else if (SCHEME_ASSET.equals(scheme)) {
      dataSource = getAssetDataSource();
    } else if (SCHEME_CONTENT.equals(scheme)) {
      dataSource = getContentDataSource();
    } else if (SCHEME_RTMP.equals(scheme)) {
      dataSource = getRtmpDataSource();
    } else if (SCHEME_UDP.equals(scheme)) {
      dataSource = getUdpDataSource();
    } else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) {
      dataSource = getDataSchemeDataSource();
    } else if (SCHEME_RAW.equals(scheme)) {
      dataSource = getRawResourceDataSource();
    } else {
      dataSource = baseDataSource;
    }
    // Open the source and return.
    return dataSource.open(dataSpec);
  }

最关键的是http的dataSource,这是在DefaultHttpDataSourceFactory 中createDataSource 创建的,最终发起和处理http请求的地方在DefaultHttpDataSource类中; DataSource类结构图 上面类图关系可以看出ExoPlayer中DataSource之间的结构分布; 常用的Http请求的主要处理类是DefaultHttpDataSource,OkHttpDataSource和CronetDataSource是扩展的类,方便开发者接入外部的网络加载库;

三、Renderer

Renderer从SampleStream读取的媒体。 在内部,Renderer的生命周期由拥有的ExoPlayer管理。随着整体播放状态和启用的轨道发生变化,Renderer会通过各种状态进行转换。有效状态转换如下所示,并标有每次转换期间调用的方法。 renderer状态图 Renderer结构图

还有一些可供扩展的Renderer类,如:FfmpegAudioRenderer ,这儿没有列出;

3.1 Renderer 初始化

在构造SimpleExoPlayer对象的时候,传入了Renderer list对象,这个Renderer list对象,就是将支持的所有Renderer 渲染器传进来;

  public Renderer[] createRenderers(
      Handler eventHandler,
      VideoRendererEventListener videoRendererEventListener,
      AudioRendererEventListener audioRendererEventListener,
      TextOutput textRendererOutput,
      MetadataOutput metadataRendererOutput,
      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
    if (drmSessionManager == null) {
      drmSessionManager = this.drmSessionManager;
    }
    ArrayList<Renderer> renderersList = new ArrayList<>();
    buildVideoRenderers(
        context,
        extensionRendererMode,
        mediaCodecSelector,
        drmSessionManager,
        playClearSamplesWithoutKeys,
        enableDecoderFallback,
        eventHandler,
        videoRendererEventListener,
        allowedVideoJoiningTimeMs,
        renderersList);
    buildAudioRenderers(
        context,
        extensionRendererMode,
        mediaCodecSelector,
        drmSessionManager,
        playClearSamplesWithoutKeys,
        enableDecoderFallback,
        buildAudioProcessors(),
        eventHandler,
        audioRendererEventListener,
        renderersList);
    buildTextRenderers(context, textRendererOutput, eventHandler.getLooper(),
        extensionRendererMode, renderersList);
    buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(),
        extensionRendererMode, renderersList);
    buildCameraMotionRenderers(context, extensionRendererMode, renderersList);
    buildMiscellaneousRenderers(context, eventHandler, extensionRendererMode, renderersList);
    return renderersList.toArray(new Renderer[0]);
  }

buildVideoRenderers buildAudioRenderers buildTextRenderers buildMetadataRenderers buildCameraMotionRenderers buildMiscellaneousRenderers 追踪VideoRenderer和AudioRenderer的工作流程,对我们理解视频的解码流程有较大的帮助;



  protected void buildVideoRenderers(
      Context context,
      @ExtensionRendererMode int extensionRendererMode,
      MediaCodecSelector mediaCodecSelector,
      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
      boolean playClearSamplesWithoutKeys,
      boolean enableDecoderFallback,
      Handler eventHandler,
      VideoRendererEventListener eventListener,
      long allowedVideoJoiningTimeMs,
      ArrayList<Renderer> out) {
    out.add(
        new MediaCodecVideoRenderer(
            context,
            mediaCodecSelector,
            allowedVideoJoiningTimeMs,
            drmSessionManager,
            playClearSamplesWithoutKeys,
            enableDecoderFallback,
            eventHandler,
            eventListener,
            MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));

    if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
      return;
    }
    int extensionRendererIndex = out.size();
    if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {
      extensionRendererIndex--;
    }

    try {
      // Full class names used for constructor args so the LINT rule triggers if any of them move.
      // LINT.IfChange
      Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer");
      Constructor<?> constructor =
          clazz.getConstructor(
              long.class,
              android.os.Handler.class,
              com.google.android.exoplayer2.video.VideoRendererEventListener.class,
              int.class);
      // LINT.ThenChange(../../../../../../../proguard-rules.txt)
      Renderer renderer =
          (Renderer)
              constructor.newInstance(
                  allowedVideoJoiningTimeMs,
                  eventHandler,
                  eventListener,
                  MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
      out.add(extensionRendererIndex++, renderer);
      Log.i(TAG, "Loaded LibvpxVideoRenderer.");
    } catch (ClassNotFoundException e) {
      // Expected if the app was built without the extension.
    } catch (Exception e) {
      // The extension is present, but instantiation failed.
      throw new RuntimeException("Error instantiating VP9 extension", e);
    }

    try {
      // Full class names used for constructor args so the LINT rule triggers if any of them move.
      // LINT.IfChange
      Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.av1.Libgav1VideoRenderer");
      Constructor<?> constructor =
          clazz.getConstructor(
              long.class,
              android.os.Handler.class,
              com.google.android.exoplayer2.video.VideoRendererEventListener.class,
              int.class);
      // LINT.ThenChange(../../../../../../../proguard-rules.txt)
      Renderer renderer =
          (Renderer)
              constructor.newInstance(
                  allowedVideoJoiningTimeMs,
                  eventHandler,
                  eventListener,
                  MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
      out.add(extensionRendererIndex++, renderer);
      Log.i(TAG, "Loaded Libgav1VideoRenderer.");
    } catch (ClassNotFoundException e) {
      // Expected if the app was built without the extension.
    } catch (Exception e) {
      // The extension is present, but instantiation failed.
      throw new RuntimeException("Error instantiating AV1 extension", e);
    }
  }

主要是MediaCodecVideoRenderer ,下面都是可扩展的软解码的Renderer引擎;




  protected void buildAudioRenderers(
      Context context,
      @ExtensionRendererMode int extensionRendererMode,
      MediaCodecSelector mediaCodecSelector,
      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
      boolean playClearSamplesWithoutKeys,
      boolean enableDecoderFallback,
      AudioProcessor[] audioProcessors,
      Handler eventHandler,
      AudioRendererEventListener eventListener,
      ArrayList<Renderer> out) {
    out.add(
        new MediaCodecAudioRenderer(
            context,
            mediaCodecSelector,
            drmSessionManager,
            playClearSamplesWithoutKeys,
            enableDecoderFallback,
            eventHandler,
            eventListener,
            new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors)));

    if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
      return;
    }
    int extensionRendererIndex = out.size();
    if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {
      extensionRendererIndex--;
    }

    try {
      // Full class names used for constructor args so the LINT rule triggers if any of them move.
      // LINT.IfChange
      Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer");
      Constructor<?> constructor =
          clazz.getConstructor(
              android.os.Handler.class,
              com.google.android.exoplayer2.audio.AudioRendererEventListener.class,
              com.google.android.exoplayer2.audio.AudioProcessor[].class);
      // LINT.ThenChange(../../../../../../../proguard-rules.txt)
      Renderer renderer =
          (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors);
      out.add(extensionRendererIndex++, renderer);
      Log.i(TAG, "Loaded LibopusAudioRenderer.");
    } catch (ClassNotFoundException e) {
      // Expected if the app was built without the extension.
    } catch (Exception e) {
      // The extension is present, but instantiation failed.
      throw new RuntimeException("Error instantiating Opus extension", e);
    }

    try {
      // Full class names used for constructor args so the LINT rule triggers if any of them move.
      // LINT.IfChange
      Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer");
      Constructor<?> constructor =
          clazz.getConstructor(
              android.os.Handler.class,
              com.google.android.exoplayer2.audio.AudioRendererEventListener.class,
              com.google.android.exoplayer2.audio.AudioProcessor[].class);
      // LINT.ThenChange(../../../../../../../proguard-rules.txt)
      Renderer renderer =
          (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors);
      out.add(extensionRendererIndex++, renderer);
      Log.i(TAG, "Loaded LibflacAudioRenderer.");
    } catch (ClassNotFoundException e) {
      // Expected if the app was built without the extension.
    } catch (Exception e) {
      // The extension is present, but instantiation failed.
      throw new RuntimeException("Error instantiating FLAC extension", e);
    }

    try {
      // Full class names used for constructor args so the LINT rule triggers if any of them move.
      // LINT.IfChange
      Class<?> clazz =
          Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer");
      Constructor<?> constructor =
          clazz.getConstructor(
              android.os.Handler.class,
              com.google.android.exoplayer2.audio.AudioRendererEventListener.class,
              com.google.android.exoplayer2.audio.AudioProcessor[].class);
      // LINT.ThenChange(../../../../../../../proguard-rules.txt)
      Renderer renderer =
          (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors);
      out.add(extensionRendererIndex++, renderer);
      Log.i(TAG, "Loaded FfmpegAudioRenderer.");
    } catch (ClassNotFoundException e) {
      // Expected if the app was built without the extension.
    } catch (Exception e) {
      // The extension is present, but instantiation failed.
      throw new RuntimeException("Error instantiating FFmpeg extension", e);
    }
  }

MediaCodecAudioRenderer是音频的Renderer引擎; 下面我觉得应该从初始化的地方如何调用Renderer来阐述一下这些Renderer是如何工作的?

3.2 Renderer工作机制

Renderer有很多类型 ,我们选择MediaCodecRenderer来阐述一下Renderer的工作机制; MediaCodecRenderer创建流程 这儿的流程涉及到很多MediaCodec的知识,我会专门讲解一下MediaCodec的解码流程,这儿我就不展开讲了; 有几个知识点需要关注下:

  • 1.音视频同步如何实现;
  • 2.字幕如何解析;
  • 3.MediaCodec可以复用吗?
public interface VideoRendererEventListener {
  default void onVideoEnabled(DecoderCounters counters) {}

  default void onVideoDecoderInitialized(
      String decoderName, long initializedTimestampMs, long initializationDurationMs) {}

  default void onVideoInputFormatChanged(Format format) {}

  default void onDroppedFrames(int count, long elapsedMs) {}

  default void onVideoSizeChanged(
      int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {}

  default void onRenderedFirstFrame(@Nullable Surface surface) {}

  default void onVideoDisabled(DecoderCounters counters) {}
}

对我们来说比较重要的是onRenderedFirstFrame 回调,表明依次渲染过程中surface首次出现数据的情况;

ExoPlayer 还有一些重要的知识点,限于文章篇幅,还是另外开文章写了。