探索 ExoPlayer 音视频播放技术(2):播放器事件监听

这个系列文章我们来介绍一位海外工程师如何探索 ExoPlayer 音视频播放技术,对于想要开始学习音视频技术的朋友,这些文章是份不错的入门资料,这是第 2 篇:ExoPlayer 播放器事件监听。

—— 来自公众号关键帧Keyframe的分享

1、监听播放事件

诸如播放状态变化和播放错误等事件,会报告给已注册的 Player.Listener 实例。若要注册一个监听器以接收此类事件:

1.1、Kotlin

// 添加一个监听器以接收播放器的事件。
player.addListener(listener)

1.2、Java

// 添加一个监听器以接收播放器的事件。
player.addListener(listener);

Player.Listener 的默认方法均为空实现,因此你只需实现自己感兴趣的那些方法。可参阅 Javadoc 以获取方法的完整描述以及它们被调用的时机。下面对一些最重要的方法进行了更详细的说明。

监听器可以选择实现单独的事件回调,或者实现一个通用的 onEvents 回调,该回调会在一个或多个事件一起发生后被调用。关于不同使用场景下应优先选择哪种方式,可参阅 单独回调与 onEvents 部分的解释。

1.3、播放状态变化

播放器状态的变化可以通过在已注册的 Player.Listener 中实现 onPlaybackStateChanged(@State int state) 来接收。播放器共有四种播放状态:

  • Player.STATE_IDLE:这是初始状态,也是播放器停止或播放失败时的状态。在此状态下,播放器仅保留有限的资源。
  • Player.STATE_BUFFERING:播放器无法从当前位置立即播放。这通常是因为需要加载更多数据。
  • Player.STATE_READY:播放器能够从当前位置立即播放。
  • Player.STATE_ENDED:播放器已播放完所有媒体。

除了这些状态外,播放器还有一个 playWhenReady 标志,用于表示用户希望播放的意图。可以通过实现 onPlayWhenReadyChanged(playWhenReady, @PlayWhenReadyChangeReason int reason) 来接收此标志的变化。

当同时满足以下三个条件时,播放器处于播放状态(即其位置在推进且媒体正在呈现给用户):

  • 播放器处于 Player.STATE_READY 状态
  • playWhenReady 为 true
  • 播放未因 Player.getPlaybackSuppressionReason 返回的原因而被抑制

与其逐一检查这些属性,不如直接调用 Player.isPlaying。通过实现 onIsPlayingChanged(boolean isPlaying) 可以接收此状态的变化:

1.4、Kotlin

player.addListener(
  object : Player.Listener {
    override fun onIsPlayingChanged(isPlaying: Boolean) {
      if (isPlaying) {
        // 正在播放。
      } else {
        // 未播放,因为播放已暂停、结束、被抑制,或者播放器正在缓冲、停止或失败。请检查 player.playWhenReady、player.playbackState、player.playbackSuppressionReason 和 player.playerError 以获取详细信息。
      }
    }
  }
)

1.5、Java

player.addListener(
    new Player.Listener() {
      @Override
      public void onIsPlayingChanged(boolean isPlaying) {
        if (isPlaying) {
          // 正在播放。
        } else {
          // 未播放,因为播放已暂停、结束、被抑制,或者播放器正在缓冲、停止或失败。请检查 player.getPlayWhenReady()、player.getPlaybackState()、player.getPlaybackSuppressionReason() 和 player.getPlaybackError() 以获取详细信息。
        }
      }
    });

1.6、播放错误

导致播放失败的错误可以通过在已注册的 Player.Listener 中实现 onPlayerError(PlaybackException error) 来接收。当发生失败时,此方法将在播放状态转换为 Player.STATE_IDLE 之前立即被调用。可以通过调用 ExoPlayer.prepare 来重试失败或停止的播放。

需注意,某些 Player 实现会将 PlaybackException 的子类实例传递给监听器,以提供有关失败的更多详细信息。例如,ExoPlayer 会传递 ExoPlaybackException,其中包含 typerendererIndex 以及其他特定于 ExoPlayer 的字段。

以下示例展示了如何检测由于 HTTP 网络问题导致的播放失败:

1.7、Kotlin

player.addListener(
  object : Player.Listener {
    override fun onPlayerError(error: PlaybackException) {
      val cause = error.cause
      if (cause is HttpDataSourceException) {
        // 发生了 HTTP 错误。
        val httpError = cause
        // 既可以通过强制转换,也可以通过查询 cause 来获取更多有关错误的信息。
        if (httpError is InvalidResponseCodeException) {
          // 强制转换为 InvalidResponseCodeException 并获取响应代码、消息和标题。
        } else {
          // 尝试调用 httpError.getCause() 来获取根本原因,但请注意它可能为 null。
        }
      }
    }
  }
)

1.8、Java

player.addListener(
    new Player.Listener() {
      @Override
      public void onPlayerError(PlaybackException error) {
        @Nullable Throwable cause = error.getCause();
        if (cause instanceof HttpDataSourceException) {
          // 发生了 HTTP 错误。
          HttpDataSourceException httpError = (HttpDataSourceException) cause;
          // 既可以通过强制转换,也可以通过查询 cause 来获取更多有关错误的信息。
          if (httpError instanceof HttpDataSource.InvalidResponseCodeException) {
            // 强制转换为 InvalidResponseCodeException 并获取响应代码、消息和标题。
          } else {
            // 尝试调用 httpError.getCause() 来获取根本原因,但请注意它可能为 null。
          }
        }
      }
    });

1.9、播放列表切换

每当播放器在播放列表中切换到新的媒体项时,已注册的 Player.Listener 对象的 onMediaItemTransition(MediaItem mediaItem, @MediaItemTransitionReason int reason) 方法会被调用。reason 参数表明这是自动切换、寻址(例如调用 player.next() 后)、重复播放同一项,还是由于播放列表更改(例如当前播放项被移除)导致的。

1.10、元数据

由 player.getCurrentMediaMetadata() 返回的元数据可能会因多种原因而变化:播放列表切换、流内元数据更新,或在播放过程中更新当前的 MediaItem

如果您对元数据变化感兴趣,例如为了更新显示当前标题的 UI,可以监听 onMediaMetadataChanged

1.11、寻址

调用 Player.seekTo 方法会导致一系列回调被发送到已注册的 Player.Listener 实例:

  1. onPositionDiscontinuity,其 reason 参数为 DISCONTINUITY_REASON_SEEK。这是调用 Player.seekTo 的直接结果。回调包含寻址前后的位置信息。
  2. onPlaybackStateChanged,其带有与寻址相关的任何即时状态变化。需注意,可能并无此类变化。

1.12、单独回调与 onEvents

监听器可以选择实现单独的回调(如 onIsPlayingChanged(boolean isPlaying))或通用的 onEvents(Player player, Events events) 回调。通用回调提供了对 Player 对象的访问,并指定了同时发生的事件集。此回调总是在对应于各个事件的回调之后被调用。

1.13、Kotlin

override fun onEvents(player: Player, events: Player.Events) {
  if (
    events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) ||
      events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)
  ) {
    uiModule.updateUi(player)
  }
}

1.14、Java

@Override
public void onEvents(Player player, Events events) {
  if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)
      || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
    uiModule.updateUi(player);
  }
}

在以下情况下应优先使用单独的事件:

  • 监听器对变化的原因感兴趣。例如,onPlayWhenReadyChanged 或 onMediaItemTransition 提供的原因。
  • 监听器仅根据回调参数中的新值采取行动,或触发不依赖于回调参数的其他操作。
  • 监听器实现更倾向于在方法名称中明确指示触发事件的可读性。
  • 监听器向分析系统报告需要了解所有单独事件和状态变化的情况。

在以下情况下应优先使用通用的 onEvents(Player player, Events events)

  • 监听器希望对多个事件触发相同的逻辑。例如,为 onPlaybackStateChanged 和 onPlayWhenReadyChanged 更新 UI。
  • 监听器需要访问 Player 对象以触发进一步的事件,例如在媒体项切换后进行寻址。
  • 监听器打算一起使用多个状态值,这些值通过单独的回调报告,或与 Player 的 getter 方法结合使用。例如,在 onTimelineChanged 中使用 Player.getCurrentWindowIndex() 与 Timeline 一起使用,这在 onEvents 回调内部才是安全的。
  • 监听器对事件是否逻辑上同时发生感兴趣。例如,由于媒体项切换导致的 onPlaybackStateChanged 到 STATE_BUFFERING

在某些情况下,监听器可能需要将单独的回调与通用的 onEvents 回调结合使用,例如记录 onMediaItemTransition 的媒体项变化原因,但仅在所有状态变化可以一起在 onEvents 中使用时才采取行动。

2、使用 AnalyticsListener

当使用 ExoPlayer 时,可以通过调用 addAnalyticsListener 向播放器注册 AnalyticsListenerAnalyticsListener 实现能够监听对于分析和日志记录目的可能有用的详细事件。请参阅分析页面以获取更多详细信息。

2.1、使用 EventLogger

EventLogger 是库直接提供的一个 AnalyticsListener,用于日志记录目的。通过一行代码将 EventLogger 添加到 ExoPlayer,即可启用有用的额外日志记录:

2.2、Kotlin

player.addAnalyticsListener(EventLogger())

2.3、Java

player.addAnalyticsListener(new EventLogger());

请参阅调试日志页面以获取更多详细信息。

3、在指定播放位置触发事件

某些使用场景需要在指定的播放位置触发事件。这可以通过 PlayerMessage 来实现。PlayerMessage 可以使用 ExoPlayer.createMessage 创建。可以使用 PlayerMessage.setPosition 设置其应执行的播放位置。默认情况下,消息在播放线程上执行,但可以使用 PlayerMessage.setLooper 自定义。PlayerMessage.setDeleteAfterDelivery 可用于控制消息是否在每次遇到指定的播放位置时执行(由于寻址和重复模式,这可能会发生多次),或者仅第一次遇到时执行。一旦 PlayerMessage 配置完成,可以使用 PlayerMessage.send 进行调度。

3.1、Kotlin

player
  .createMessage { messageType: Int, payload: Any? -> }
  .setLooper(Looper.getMainLooper())
  .setPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 120000)
  .setPayload(customPayloadData)
  .setDeleteAfterDelivery(false)
  .send()

3.2、Java

player
    .createMessage(
        (messageType, payload) -> {
          // 在指定的播放位置执行操作。
        })
    .setLooper(Looper.getMainLooper())
    .setPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 120_000)
    .setPayload(customPayloadData)
    .setDeleteAfterDelivery(false)
    .send();

音视频方向学习、求职,欢迎加入我们的星球

丰富的音视频知识、面试题、技术方案干货分享,还可以进行面试辅导

探索 ExoPlayer 音视频播放技术(2):播放器事件监听

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论