ExoPlayer 中的 Timeline、Period 和 Window

ExoPlayer 的 TimelineTimeline.PeriodTimeline.Window 是其核心组件,用于描述媒体内容的结构和时间信息,特别是在处理直播、点播和复杂播放列表(如多段内容或动态窗口)时。这些类共同定义了媒体的时间轴(Timeline),帮助 ExoPlayer 管理播放位置、导航和内容切换。


1. 概述

ExoPlayer 的 Timeline 系统设计用于抽象化媒体内容的组织方式,无论是点播视频、直播流还是多内容播放列表。Timeline 提供了一个统一的方式来描述内容的时间范围、结构和元数据,而 Timeline.PeriodTimeline.Window 分别是其更细粒度的子结构。

  • Timeline:表示整个媒体内容的全局时间轴,包含一个或多个窗口(Window)和周期(Period)。
  • Timeline.Window:表示时间轴上的一个逻辑窗口,通常对应一个播放内容的持续时间段(如直播流的当前可用窗口或点播视频的整个时长)。
  • Timeline.Period:表示窗口内的更小时间段,通常对应内容的逻辑分段(如 DASH 的周期或 HLS 的片段组)。

Timelines for various use cases

Single media file or on-demand stream

Playlist of media files or on-demand streams

Live stream with limited availability

Live stream with indefinite availability

Live stream with multiple periods

On-demand stream followed by live stream

On-demand stream with mid-roll ads

The figure below shows some of the information defined by a period, as well as how this information relates to a corresponding Window in the timeline.

这些类在 ExoPlayer 中广泛用于: - 确定当前播放位置和内容边界。 - 支持直播流的滑动窗口(动态更新)。 - 管理多内容播放列表(如广告插入或拼接视频)。 - 处理时间相关逻辑。


2. 详细讲解

2.1 Timeline

  • 定义
    • Timeline 是 ExoPlayer 的抽象基类,表示媒体内容的整体时间轴结构。
    • 它包含一组窗口(Window)和周期(Period),每个窗口可能包含一个或多个周期。
    • Timeline 可以是静态的(如点播视频,固定时长)或动态的(如直播流,窗口随时间滑动)。
  • 主要方法
    • getWindowCount():返回时间轴中的窗口数量。
    • getWindow(int windowIndex, Timeline.Window window):获取指定索引的窗口信息,填充到提供的 Timeline.Window 对象。
    • getPeriodCount():返回时间轴中的周期数量。
    • getPeriod(int periodIndex, Timeline.Period period):获取指定索引的周期信息。
    • getLastWindowIndex(boolean shuffleModeEnabled):返回最后一个窗口的索引(在直播中通常是当前可用窗口)。
    • getIndexOfPeriod(Object uid):根据周期的唯一 ID 查找其索引。
  • 作用
    • 提供媒体内容的全局视图,描述窗口和周期的组织方式。
    • 支持导航(如跳转到特定窗口或周期)。
    • 在直播场景中,Timeline 会动态更新以反映滑动窗口的变化。
  • 典型场景
    • 点播:一个 Timeline 包含一个窗口(整个视频)和一个或多个周期(章节或广告)。
    • 直播:一个 Timeline 包含一个动态窗口(当前可用的直播内容)和多个周期(流的分段)。
    • 播放列表:一个 Timeline 包含多个窗口(每个视频)和对应的周期。
  • 代码示例
    1
    2
    3
    4
    5
    6
    Timeline timeline = player.getCurrentTimeline();
    if (!timeline.isEmpty()) {
    Timeline.Window window = new Timeline.Window();
    timeline.getWindow(timeline.getLastWindowIndex(false), window);
    Log.d(TAG, "Window duration: " + window.getDurationMs());
    }

2.2 Timeline.Window

  • 定义
    • Timeline.Window 表示时间轴上的一个逻辑时间窗口,通常对应一个连续的播放内容段。
    • 每个窗口有自己的时间范围(开始时间、持续时间)和元数据(如是否动态、是否可寻址)。
  • 关键字段
    • long presentationStartTimeMs:窗口内容的“呈现”开始时间(以毫秒为单位),通常是内容的逻辑开始时间(例如,直播流的节目开始时间)。如果未知,设为 C.TIME_UNSET
    • long windowStartTimeMs:窗口在时间轴上的实际开始时间(以毫秒为单位),通常用于直播流的时间计算。如果未知,设为 C.TIME_UNSET
    • long durationUs:窗口的持续时间(以微秒为单位)。如果是直播流,可能是当前窗口的可用时长。
    • boolean isDynamic:指示窗口是否动态(即内容可能随时间更新,如直播流的滑动窗口)。
    • boolean isSeekable:指示窗口是否支持寻址(seek),通常点播为 true,直播可能为 false
    • long defaultPositionUs:窗口的默认播放起始位置(以微秒为单位),用于初始化播放。
  • 作用
    • 描述媒体内容的宏观时间范围(如直播流的当前窗口或点播视频的总时长)。
    • 提供时间基准,用于计算播放位置(如你的 currentPositionMs)。
    • 支持直播流的动态更新(isDynamic = true 时,窗口内容随时间变化)。
  • 典型场景
    • 直播:窗口表示当前可用的直播内容(滑动窗口),windowStartTimeMsdurationUs 定义其时间范围。
    • 点播:窗口表示整个视频,durationUs 是视频总时长。
    • 广告:窗口可能表示主内容或广告段。
  • 代码示例
    1
    2
    3
    4
    5
    Timeline.Window window = new Timeline.Window();
    timeline.getWindow(timeline.getLastWindowIndex(false), window);
    if (window.isDynamic) {
    Log.d(TAG, "Live window, duration: " + C.usToMs(window.durationUs) + "ms");
    }

2.3 Timeline.Period

  • 定义
    • Timeline.Period 表示窗口内的一个逻辑分段,通常对应媒体内容的子部分(如 DASH 的一个 Period 或 HLS 的一个片段组)。
    • 周期是窗口的更细粒度划分,可能包含多个媒体片段(chunk)。
  • 关键字段
    • Object id:周期的唯一标识符,用于区分不同的周期。
    • long durationUs:周期的持续时间(以微秒为单位)。如果未知,设为 C.TIME_UNSET
    • long positionInWindowUs:周期相对于窗口开始的偏移量(以微秒为单位)。
    • int windowIndex:周期所属的窗口索引。
    • boolean isAd:指示周期是否为广告内容。
  • 作用
    • 描述窗口内的内容分段,允许 ExoPlayer 精确导航到特定周期。
    • 提供周期级别的元数据(如广告标记或内容切换点)。
    • 在直播场景中,周期通常对应流的分段,动态添加或移除。
  • 典型场景
    • 直播:周期表示 HLS 或 DASH 流中的一个时间段(例如,10 秒的媒体片段)。
    • 点播:周期可能表示视频的章节或广告插入点。
    • 播放列表:每个窗口的子内容可能由多个周期组成。
  • 代码示例
    1
    2
    3
    Timeline.Period period = new Timeline.Period();
    timeline.getPeriod(timeline.getCurrentPeriodIndex(), period);
    Log.d(TAG, "Period duration: " + C.usToMs(period.durationUs) + "ms");

3. 关系与工作流程

TimelineTimeline.WindowTimeline.Period 的关系可以用以下结构表示:

1
2
3
4
5
6
7
8
9
Timeline
├── Window 0
│ ├── Period 0
│ ├── Period 1
│ └── ...
├── Window 1
│ ├── Period 0
│ └── ...
└── ...
  • 层级关系
    • 一个 Timeline 包含多个 Window,每个 Window 包含一个或多个 Period
    • Window 定义宏观时间范围(如直播流的当前窗口),Period 定义窗口内的细粒度分段(如流的一个片段)。
  • 时间映射
    • WindowwindowStartTimeMsdurationUs 定义全局时间范围。
    • PeriodpositionInWindowUs 表示其在窗口中的相对位置。
    • 播放位置(positionUs)通常相对于某个周期,通过 Period 映射到窗口时间,再通过 Window 映射到全局时间。
  • 动态更新
    • 在直播场景中,Timeline 是动态的(isDynamic = true),新窗口或周期会随时间添加,旧周期可能被移除(滑动窗口)。
    • ExoPlayer 通过 Player.EventListeneronTimelineChanged 事件通知 Timeline 更新。
  • 工作流程
    1. ExoPlayer 从 MediaSource(如 DashMediaSourceHlsMediaSource)加载 Timeline
    2. Timeline 提供窗口和周期信息,ExoPlayer 使用它们确定播放位置(player.getCurrentPosition())和内容边界。
    3. 直播流的 Timeline 定期更新,反映新的窗口或周期(例如,HLS 播放列表刷新)。
    4. 播放器通过 Timeline 导航内容(如 seekTo(windowIndex, positionMs))。

4. 典型用法与注意事项

4.1 典型用法

  • 获取当前播放位置

    1
    2
    3
    4
    5
    6
    7
    Timeline timeline = player.getCurrentTimeline();
    Timeline.Window window = new Timeline.Window();
    Timeline.Period period = new Timeline.Period();
    timeline.getWindow(player.getCurrentWindowIndex(), window);
    timeline.getPeriod(player.getCurrentPeriodIndex(), period);
    long positionMs = player.getCurrentPosition();
    Log.d(TAG, "Window: " + window.windowStartTimeMs + ", Period: " + period.positionInWindowUs + ", Position: " + positionMs);

  • 处理直播滑动窗口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    player.addListener(new Player.Listener() {
    @Override
    public void onTimelineChanged(Timeline timeline, int reason) {
    if (timeline.isEmpty()) return;
    Timeline.Window window = new Timeline.Window();
    timeline.getWindow(timeline.getLastWindowIndex(false), window);
    if (window.isDynamic) {
    Log.d(TAG, "Live window updated, duration: " + C.usToMs(window.durationUs));
    }
    }
    });

  • 跳转到直播边缘

    1
    2
    3
    4
    Timeline timeline = player.getCurrentTimeline();
    Timeline.Window window = new Timeline.Window();
    timeline.getWindow(timeline.getLastWindowIndex(false), window);
    player.seekTo(window.windowIndex, window.getDefaultPositionUs());

4.2 注意事项

  • 动态更新
    • 直播流的 Timeline 是动态的,需通过 onTimelineChanged 监听更新,避免使用过时的窗口或周期数据。
  • 时间戳准确性
    • windowStartTimeMspresentationStartTimeMs 依赖直播流的元数据(如 DASH MPD 或 HLS 播放列表),可能因流提供商错误导致不准确。
    • 在你的代码中,负值 currentLatencyMs 可能与时间戳超前有关,需验证元数据。
  • 周期与窗口映射
    • 确保 periodPositionUsToWindowPositionMs 正确处理周期切换,特别是在直播流中周期频繁更新时。
  • 性能考虑
    • 频繁调用 getWindowgetPeriod 可能影响性能,建议缓存 Timeline 数据或在必要时更新。
  • 错误处理
    • 检查 C.TIME_UNSET 的情况,确保代码对未知时间戳有回退逻辑(如你的代码中对 presentationStartTimeMswindowStartTimeMs 的检查)。

5. 总结

  • Timeline:全局时间轴,管理窗口和周期,提供媒体内容的结构化视图。
  • Timeline.Window:表示一个播放窗口,定义时间范围(windowStartTimeMsdurationUs)和动态性(isDynamic),在直播中表示滑动窗口。
  • Timeline.Period:窗口内的细粒度分段,定义周期时间(durationUs)和位置(positionInWindowUs),用于精确导航。
  • 关系Timeline 包含多个 Window,每个 Window 包含多个 Period,共同映射播放位置和时间。

6. 参考链接

Timeline

Timeline.Window

Timeline.Period