自定义

ExoPlayer 库的核心是 Player 接口。Player 公开了传统的高级媒体播放器功能,例如缓冲媒体、播放、暂停和跳转。默认实现 ExoPlayer 旨在对所播放媒体的类型、存储方式和位置以及呈现方式做出尽可能少的假设(从而施加尽可能少的限制)。ExoPlayer 实现不会直接实现媒体的加载和渲染,而是将此工作委托给在创建播放器或将新媒体来源传递给播放器时注入的组件。所有 ExoPlayer 实现共有的组件包括:

  • MediaSource 实例,用于定义要播放的媒体、加载媒体,以及从中读取加载的媒体。MediaSource 实例由播放器内的 MediaSource.Factory 从 MediaItem 创建。也可以使用基于媒体来源的播放列表 API 直接传递给播放器。
  • 一个 MediaSource.Factory 实例,用于将 MediaItem 转换为 MediaSource。MediaSource.Factory 会在创建播放器时注入。
  • Renderer 实例,用于呈现媒体的各个组件。这些是在创建播放器时注入的。
  • TrackSelector,用于选择 MediaSource 提供的轨道,以供每个可用的 Renderer 使用。创建播放器时会注入 TrackSelector。
  • 一个 LoadControl,用于控制 MediaSource 何时缓冲更多媒体以及缓冲多少媒体。创建播放器时会注入 LoadControl。
  • 一个 LivePlaybackSpeedControl,用于控制直播播放期间的播放速度,以便播放器保持接近配置的直播偏移量。创建播放器时会注入 LivePlaybackSpeedControl。

注入实现播放器功能片段的组件这一概念贯穿整个库。某些组件的默认实现会将工作委托给进一步注入的组件。这样,许多子组件都可以单独替换为以自定义方式配置的实现。

播放器自定义

以下部分介绍了一些通过注入组件来自定义播放器的常见示例。

配置网络栈

我们有一个页面专门介绍如何自定义 ExoPlayer 使用的网络栈。

缓存从网络加载的数据

请参阅临时动态缓存和下载媒体指南。

自定义服务器互动

某些应用可能需要拦截 HTTP 请求和响应。您可能需要注入自定义请求标头、读取服务器的响应标头、修改请求的 URI 等。例如,您的应用在请求媒体片段时,可以通过注入令牌作为标头来进行身份验证。

以下示例演示了如何通过将自定义 DataSource.Factory 注入 DefaultMediaSourceFactory 来实现这些行为:

Kotlin

val dataSourceFactory = DataSource.Factory {
  val dataSource = httpDataSourceFactory.createDataSource()
  // Set a custom authentication request header.
  dataSource.setRequestProperty("Header", "Value")
  dataSource
}
val player =
  ExoPlayer.Builder(context)
    .setMediaSourceFactory(
      DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory)
    )
    .build()

Java

DataSource.Factory dataSourceFactory =
    () -> {
      HttpDataSource dataSource = httpDataSourceFactory.createDataSource();
      // Set a custom authentication request header.
      dataSource.setRequestProperty("Header", "Value");
      return dataSource;
    };

ExoPlayer player =
    new ExoPlayer.Builder(context)
        .setMediaSourceFactory(
            new DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory))
        .build();

在前面的代码段中,注入的 HttpDataSource 在每个 HTTP 请求中都包含标头 "Header: Value"。对于与 HTTP 来源的每次互动,此行为都是固定的。

如需采用更精细的方法,您可以使用 ResolvingDataSource 注入即时行为。以下代码段展示了如何在与 HTTP 来源交互之前注入请求标头:

Kotlin

val dataSourceFactory: DataSource.Factory =
  ResolvingDataSource.Factory(httpDataSourceFactory) { dataSpec: DataSpec ->
    // Provide just-in-time request headers.
    dataSpec.withRequestHeaders(getCustomHeaders(dataSpec.uri))
  }

Java

DataSource.Factory dataSourceFactory =
    new ResolvingDataSource.Factory(
        httpDataSourceFactory,
        // Provide just-in-time request headers.
        dataSpec -> dataSpec.withRequestHeaders(getCustomHeaders(dataSpec.uri)));

您还可以使用 ResolvingDataSource 对 URI 进行即时修改,如以下代码段所示:

Kotlin

val dataSourceFactory: DataSource.Factory =
  ResolvingDataSource.Factory(httpDataSourceFactory) { dataSpec: DataSpec ->
    // Provide just-in-time URI resolution logic.
    dataSpec.withUri(resolveUri(dataSpec.uri))
  }

Java

DataSource.Factory dataSourceFactory =
    new ResolvingDataSource.Factory(
        httpDataSourceFactory,
        // Provide just-in-time URI resolution logic.
        dataSpec -> dataSpec.withUri(resolveUri(dataSpec.uri)));

自定义错误处理

通过实现自定义 LoadErrorHandlingPolicy,应用可以自定义 ExoPlayer 对加载错误的响应方式。例如,应用可能希望快速失败,而不是多次重试,或者可能希望自定义退避逻辑,以控制玩家在每次重试之间等待的时间。以下代码段展示了如何实现自定义退避逻辑:

Kotlin

val loadErrorHandlingPolicy: LoadErrorHandlingPolicy =
  object : DefaultLoadErrorHandlingPolicy() {
    override fun getRetryDelayMsFor(
      loadErrorInfo: LoadErrorHandlingPolicy.LoadErrorInfo
    ): Long {
      // Implement custom back-off logic here.
      return 0
    }
  }
val player =
  ExoPlayer.Builder(context)
    .setMediaSourceFactory(
      DefaultMediaSourceFactory(context).setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
    )
    .build()

Java

LoadErrorHandlingPolicy loadErrorHandlingPolicy =
    new DefaultLoadErrorHandlingPolicy() {
      @Override
      public long getRetryDelayMsFor(LoadErrorHandlingPolicy.LoadErrorInfo loadErrorInfo) {
        // Implement custom back-off logic here.
        return 0;
      }
    };

ExoPlayer player =
    new ExoPlayer.Builder(context)
        .setMediaSourceFactory(
            new DefaultMediaSourceFactory(context)
                .setLoadErrorHandlingPolicy(loadErrorHandlingPolicy))
        .build();

LoadErrorInfo 参数包含有关加载失败的更多信息,以便根据错误类型或失败的请求自定义逻辑。

自定义提取器标志

提取器标志可用于自定义如何从渐进式媒体中提取各个格式。您可以在提供给 DefaultMediaSourceFactory 的 DefaultExtractorsFactory 上设置这些属性。以下示例传递了一个标志,该标志可为 MP3 流启用基于索引的跳转。

Kotlin

val extractorsFactory =
  DefaultExtractorsFactory().setMp3ExtractorFlags(Mp3Extractor.FLAG_ENABLE_INDEX_SEEKING)
val player =
  ExoPlayer.Builder(context)
    .setMediaSourceFactory(DefaultMediaSourceFactory(context, extractorsFactory))
    .build()

Java

DefaultExtractorsFactory extractorsFactory =
    new DefaultExtractorsFactory().setMp3ExtractorFlags(Mp3Extractor.FLAG_ENABLE_INDEX_SEEKING);

ExoPlayer player =
    new ExoPlayer.Builder(context)
        .setMediaSourceFactory(new DefaultMediaSourceFactory(context, extractorsFactory))
        .build();

启用恒定比特率跳转

对于 MP3、ADTS 和 AMR 流,您可以使用 FLAG_ENABLE_CONSTANT_BITRATE_SEEKING 标志,通过恒定比特率假设启用近似跳转。 如前所述,您可以使用各个 DefaultExtractorsFactory.setXyzExtractorFlags 方法为各个提取器设置这些标志。如需为所有支持恒定比特率跳转的提取器启用该功能,请使用 DefaultExtractorsFactory.setConstantBitrateSeekingEnabled。

Kotlin

val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)

Java

DefaultExtractorsFactory extractorsFactory =
    new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);

然后,可以使用 DefaultMediaSourceFactory 注入 ExtractorsFactory,如前面自定义提取器标志部分所述。

启用异步缓冲区队列

异步缓冲区队列是 ExoPlayer 渲染流水线中的一项增强功能,它在异步模式下运行 MediaCodec 实例,并使用额外的线程来安排数据的解码和渲染。启用此功能可以减少丢帧和音频欠载。

在搭载 Android 12(API 级别 31)或更高版本的设备上,异步缓冲区排队默认处于启用状态,从 Android 6.0(API 级别 23)开始,可以手动启用。如果您发现特定设备上出现丢帧或音频欠载问题,尤其是在播放受 DRM 保护的内容或高帧速率内容时,请考虑为这些设备启用该功能。

在最简单的情况下,您需要向播放器注入 DefaultRenderersFactory,如下所示:

Kotlin

val renderersFactory =
  DefaultRenderersFactory(context).forceEnableMediaCodecAsynchronousQueueing()
val exoPlayer = ExoPlayer.Builder(context, renderersFactory).build()

Java

DefaultRenderersFactory renderersFactory =
    new DefaultRenderersFactory(context).forceEnableMediaCodecAsynchronousQueueing();
ExoPlayer exoPlayer = new ExoPlayer.Builder(context, renderersFactory).build();

如果您要直接实例化渲染程序,请将 new DefaultMediaCodecAdapter.Factory(context).forceEnableAsynchronous() 传递给 MediaCodecVideoRenderer 和 MediaCodecAudioRenderer 构造函数。

使用 ForwardingSimpleBasePlayer 自定义操作

您可以通过将 Player 实例封装在 ForwardingSimpleBasePlayer 的子类中,自定义该实例的某些行为。此类可让您拦截特定的“操作”,而无需直接实现 Player 方法。这可确保 play()、pause() 和 setPlayWhenReady(boolean) 等的行为保持一致。它还可确保所有状态更改都正确传播到已注册的 Player.Listener 实例。对于大多数自定义用例,由于这些一致性保证,ForwardingSimpleBasePlayer 应优先于更容易出错的 ForwardingPlayer。

例如,如需在播放开始或停止时添加一些自定义逻辑,请执行以下操作:

Kotlin

class PlayerWithCustomPlay(player: Player) : ForwardingSimpleBasePlayer(player) {
  override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> {
    // Add custom logic
    return super.handleSetPlayWhenReady(playWhenReady)
  }
}

Java

public static final class PlayerWithCustomPlay extends ForwardingSimpleBasePlayer {

  public PlayerWithCustomPlay(Player player) {
    super(player);
  }

  @Override
  protected ListenableFuture<?> handleSetPlayWhenReady(boolean playWhenReady) {
    // Add custom logic
    return super.handleSetPlayWhenReady(playWhenReady);
  }
}

或者,如需禁止 SEEK_TO_NEXT 命令(并确保 Player.seekToNext 为空操作),请运行以下命令:

Kotlin

class PlayerWithoutSeekToNext(player: Player) : ForwardingSimpleBasePlayer(player) {
  override fun getState(): State {
    val state = super.getState()
    return state
      .buildUpon()
      .setAvailableCommands(
        state.availableCommands.buildUpon().remove(COMMAND_SEEK_TO_NEXT).build()
      )
      .build()
  }

  // We don't need to override handleSeek, because it is guaranteed not to be called for
  // COMMAND_SEEK_TO_NEXT since we've marked that command unavailable.
}

Java

public static final class PlayerWithoutSeekToNext extends ForwardingSimpleBasePlayer {

  public PlayerWithoutSeekToNext(Player player) {
    super(player);
  }

  @Override
  protected State getState() {
    State state = super.getState();
    return state
        .buildUpon()
        .setAvailableCommands(
            state.availableCommands.buildUpon().remove(COMMAND_SEEK_TO_NEXT).build())
        .build();
  }

  // We don't need to override handleSeek, because it is guaranteed not to be called for
  // COMMAND_SEEK_TO_NEXT since we've marked that command unavailable.
}

MediaSource 自定义

上述示例会注入自定义组件,以便在播放传递给播放器的所有 MediaItem 对象期间使用。如果需要进行精细的自定义,也可以将自定义组件注入各个 MediaSource 实例,这些实例可以直接传递给播放器。以下示例展示了如何自定义 ProgressiveMediaSource 以使用自定义 DataSource.Factory、ExtractorsFactory 和 LoadErrorHandlingPolicy:

Kotlin

val mediaSource =
  ProgressiveMediaSource.Factory(customDataSourceFactory, customExtractorsFactory)
    .setLoadErrorHandlingPolicy(customLoadErrorHandlingPolicy)
    .createMediaSource(MediaItem.fromUri(streamUri))

Java

ProgressiveMediaSource mediaSource =
    new ProgressiveMediaSource.Factory(customDataSourceFactory, customExtractorsFactory)
        .setLoadErrorHandlingPolicy(customLoadErrorHandlingPolicy)
        .createMediaSource(MediaItem.fromUri(streamUri));

创建自定义组件

该库为常见用例提供了本页顶部所列组件的默认实现。ExoPlayer 可以使用这些组件,但如果需要非标准行为,也可以构建为使用自定义实现。自定义实现的一些应用场景如下:

  • Renderer - 您可能需要实现自定义 Renderer,以处理库提供的默认实现不支持的媒体类型。
  • TrackSelector - 通过实现自定义 TrackSelector,应用开发者可以更改 MediaSource 公开的轨道,以便每个可用的 Renderer 选择使用。
  • LoadControl - 实现自定义 LoadControl 可让应用开发者更改播放器的缓冲政策。
  • Extractor - 如果您需要支持该库目前不支持的容器格式,请考虑实现自定义 Extractor 类。
  • MediaSource - 如果您想以自定义方式获取媒体样本并将其馈送到渲染器,或者想实现自定义 MediaSource 合成行为,则可能适合实现自定义 MediaSource 类。
  • MediaSource.Factory - 实现自定义 MediaSource.Factory 后,应用可以自定义从 MediaItem 创建 MediaSource 的方式。
  • DataSource - ExoPlayer 的上游软件包已包含许多针对不同应用场景的 DataSource 实现。您可能希望实现自己的 DataSource 类,以通过其他方式加载数据,例如通过自定义协议、使用自定义 HTTP 堆栈或从自定义永久性缓存加载。

构建自定义组件时,我们建议您遵循以下做法:

  • 如果自定义组件需要向应用报告事件,我们建议您使用与现有 ExoPlayer 组件相同的模型来执行此操作,例如使用 EventDispatcher 类或将 Handler 与监听器一起传递给组件的构造函数。
  • 我们建议自定义组件使用与现有 ExoPlayer 组件相同的模型,以便应用在播放期间进行重新配置。为此,自定义组件应实现 PlayerMessage.Target 并在 handleMessage 方法中接收配置更改。应用代码应通过以下方式传递配置更改:调用 ExoPlayer 的 createMessage 方法,配置消息,然后使用 PlayerMessage.send 将消息发送到组件。发送要在播放线程上递送的消息可确保这些消息与播放器上执行的任何其他操作按顺序执行。