Android 开源库分析——ExoPlayer的Timeline

本文链接:https://rainmonth.github.io/posts/A191130.html

摘要

刚开始分析ExoPlayer源码时,对于Timeline的概念不是很理解,这几天重新看了下,这里对Timeline及其相关概念做个简单分析

Timeline

Timeline是一种数据结构,可以灵活的代表各种媒体的结构,Timeline的实例不可修改,对于直播流媒体,Timeline提供的是当前状态的快照。

Timeline由Timeline.Period和Timeline.Window组成。Period是Media逻辑上的代表(可以是单个的媒体文件、也可以是包含掺入广告的播放信息的插入广告的媒体文件)。Window可以横跨多个Period,定义了默认的播放开始位置,所在Period可以播放的范围。

几种常见场景下Period和Window的对应关系:

  • case 单个文件或单个点播流

    一对一关系,Window横跨这个Period,表示媒体文件的所有部分都可播,默认开始播放位置在Period开始的位置;
  • case 多个媒体文件或多个点播流

    一对一关系,每个Window都横跨与之对应的Period,具体表现同上;
  • case 受限的直播流

    Window可能开始于一个非0时间点,同时 Timeline.Window.isDynamic将被置为true,默认开始播放位置通常Period(直播流的快照)的边缘。
  • case 不受限的直播流

    除了Window开始于Period开始的位置外,其他与受限直播流相同
  • case 带多个Period的直播流

    通常是因为直播流被切分成了多个Period,除了Window可能会横跨多个Period外,其他同受限直播流的情形相同。
  • case 点播流后接直播流

    即单个文件和带多个Period的直播流两种case的组合,表现结果就是两者表现结果的组合
  • case 中间插入广告的点播流

    表现同 case 单个文件或单个点播流,只不过Period可能包含一组广告,并且可以通过Period来访问广告的信息。

Timeline主要提供了获取Period、Window个数、索引的方法。Period和Window都是Timeline的内部类。

实现

Timeline有如下实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
Timeline (com.google.android.exoplayer2)
DashTimeline in DashMediaSource (com.google.android.exoplayer2.source.dash)
DummyTimeline in ConcatenatingMediaSource (com.google.android.exoplayer2.source)
ForwardingTimeline (com.google.android.exoplayer2.source)
InfinitelyLoopingTimeline in LoopingMediaSource (com.google.android.exoplayer2.source)
SinglePeriodAdTimeline (com.google.android.exoplayer2.source.ads)
ClippingTimeline in ClippingMediaSource (com.google.android.exoplayer2.source)
DeferredTimeline in ConcatenatingMediaSource (com.google.android.exoplayer2.source)
AbstractConcatenatedTimeline (com.google.android.exoplayer2.source)
ConcatenatedTimeline in ConcatenatingMediaSource (com.google.android.exoplayer2.source)
LoopingTimeline in LoopingMediaSource (com.google.android.exoplayer2.source)
SinglePeriodTimeline (com.google.android.exoplayer2.source)


从上面的继承结构可见,每种类型的MediaSource都会有一种与之对应的Timeline与之对应。

之前的文章中简单介绍了下MediaSource,MediaSource的职责:

  • 提供媒体资源对应的Timeline,并在媒体资源改变时提供新的Timeline;
  • 为媒体资源对应的Timeline的Period提供对应的MediaPeriod,MediaPeriod与Timeline的Period之间是对应的;

现在看看MeiiaPeriod这个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public interface MediaPeriod extends SequenceableLoader {

/**
* 用来处理事件通知的回调(比如MediaPeriod准备就绪)
*/
interface Callback extends SequenceableLoader.Callback<MediaPeriod> {

/**
* 准备就绪好调用该方法
*
* 该方法在播放线程中调用,调用后可调用selectTracks方法来初始化轨道选择信息。
*
* @param mediaPeriod 已准备就绪的MediaPeriod
*/
void onPrepared(MediaPeriod mediaPeriod);
}

/**
* 异步准备MediaPeriod
*
* 当准备完成时,上面回调接口的onPrepared方法将被调用,准备失败时,maybeThrowPrepareError将被调用,抛出错误。
*
* 如果准备完成后导致Timeline发生改变,会先调用MediaSource.SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline, Object),
* 再调用回调接口的onPrepared方法。
*
* @param callback 上面的Callback
* @param positionUs 从哪里开始prepare
*/
void prepare(Callback callback, long positionUs);

/**
* 抛出导致prepare失败的错误信息
*
* 只在prepare完成前调用
*/
void maybeThrowPrepareError() throws IOException;

/**
* 获取当前MediaPeriod包含的TrackGroupArray
*
* 在prepare完成后调用
*/
TrackGroupArray getTrackGroups();

/*
* 获取用来进行媒体资源过滤的StreamKey列表以实现按需加载
* <p>This method is only called after the period has been prepared.
*/
default List<StreamKey> getStreamKeys(List<TrackSelection> trackSelections) {
return Collections.emptyList();
}

/**
* 进行轨道选择
*/
long selectTracks(
@NullableType TrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
long positionUs);

/**
* 丢弃指定位置前的缓冲
*
* @param positionUs The position in microseconds.
* @param toKeyframe 是否对齐到关键帧
*/
void discardBuffer(long positionUs, boolean toKeyframe);

/**
* 不连续读取
*
* 返回的值不为C#TIME_UNSET时,所有的SampleStream都将从关键帧开始
*
* @return If a discontinuity was read then the playback position in microseconds after the
* discontinuity. Else {@link C#TIME_UNSET}.
*/
long readDiscontinuity();

/**
* 拖动至指定位置
*/
long seekToUs(long positionUs);

/**
* 获取修正后的拖动位置
*/
long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters);
}

看看ExoPlayer中 MediaPeriod的继承关系:
1
2
3
4
5
6
7
8
9
10
ClippingMediaPeriod (com.google.android.exoplayer2.source)
SsMediaPeriod (com.google.android.exoplayer2.source.smoothstreaming)
HlsMediaPeriod (com.google.android.exoplayer2.source.hls)
SilenceMediaPeriod in SilenceMediaSource (com.google.android.exoplayer2.source)
DeferredMediaPeriod (com.google.android.exoplayer2.source)
DashMediaPeriod (com.google.android.exoplayer2.source.dash)
MergingMediaPeriod (com.google.android.exoplayer2.source)
ProgressiveMediaPeriod (com.google.android.exoplayer2.source)
SingleSampleMediaPeriod (com.google.android.exoplayer2.source)


几乎每一种MediaSource都有与之对应的MediaPeriod,这也是实现MediaSource职责所必须的。

MediaPeriodSequenceableLoader的子接口,SequenceableLoader主要提供了一下几个方法:

  • Callback<T extends SequenceableLoader>,内部接口
  • getBufferedPositionUs,返回当前已经缓冲的位置;
  • getNextLoadPositionUs,获取下载加载的位置;
  • continueLoading,是否需要继续加载,如果getNextLoadPositionUs返回的值与上次不一样,就返回true;
  • reevaluateBuffer,从当前位置重新缓存,可以用来在播放过程中切换媒体源的清晰度;

总结

Timeline是ExoPlayer对媒体源的一个抽象,在创建或修改MediaSource时会对应的创建和更新Timeline,Timeline的内部类Period有一个与之对应MediaPeriod,每种不同的MediaSource都有对应的MediaPeriod实现,MediaPeriod使Media变的可读。