系列文章目录
ExoPlayer架构详解与源码剖析(1)——前语
ExoPlayer架构详解与源码剖析(2)——Player
ExoPlayer架构详解与源码剖析(3)——Timeline
ExoPlayer架构详解与源码剖析(4)——整体架构
前语
假如播映器便是一只火箭,那么火箭发射就有必要要有一个根据时序的发射方案,火箭在运转过程中经过获取当时时刻点的发射方案就会知道当时的时序状况,以及决定下一步该干什么,如需要在什么时候焚烧、发动机什么时候停机、各个阶段的姿态调整等等。
所以规划了播映器还是不够的,还需要描绘出媒体的时序结构,但是播映器播映的媒体种类有许多,可所以一个播映列表、一个mp4文件、一个网络的url,一段视频的流,横竖千奇百怪。如何规划一个数据结构能够灵敏的表明出上面各种的媒体在不同时刻点的时序结构呢。ExoPlayer给出的答案是 Timeline(时刻线)。Timeline贯穿在整个的Exoplayer源码中,后续系列文章提到的Player、MediaSource、LoadControl、TrackSelector等等都会运用到Timeline,所以有必要将Timeline提前了解下。
Timeline
Timeline 是媒体时序结构的灵敏表明。由于仅仅用来获取状况,所以Timeline是一个不可变的对象,一切的特点都是不可变的(final),这样规划也保证了多线程下的数据安全。关于动态媒体(例如直播流),Timeline 表明是当时状况的快照。 Timeline 由 一个或多个Window(窗口) 和 Period(时段) 组成。
-
Window :通常对应一个播映列表的子项。 它或许会跨多个 Period 而且界说了这些 Period 中可播映的区域。Window 还包括一些其他信息,如当时Window 是否能够Seek,能够开端播映的默许方位等。
上图中window1横跨了2个period,Window包括以下特点:
- uid Window的仅有标识符。单Window有必要运用 Window.SINGLE_WINDOW_UID。
- firstPeriodIndex 记载横跨的第一个Period的索引。
- lastPeriodIndex 记载横跨的最终一个Period的索引。
- durationUs 表明Window的时长。
- defaultPositionUs 默许开端播映方位(图中黑点)相关于Window开端时刻的差值。
- positionInFirstPeriodUs Window开端时刻相关于第一个Period开端时刻的方位的差值,由于Window开端时刻肯定大于等于第一个Period的开端时刻,这个值一定是大于0的。
- mediaItem 与窗口相关的mediaItem,mediaItem 是在Player setMediaItem时,创立Timeline并设置到其间的Window里。相同Player在获取当时mediaItem时首先获取当时的Timeline,然后在Timeline里获取当时的Window,最终在Window里获取mediaItem。
- manifest Window的清单,播映单个文件的时候为null,当在播映HLS流时,会将HLS流的索引文件相关信息设置进去。
- windowStartTimeMs Window的开端时刻。
- isSeekable 是否能够Seek。
- isDynamic 是否是动态的Window,当Timeline 跟着时刻改变时,Window是否也会跟着改变。
- isLive 是否为直播流。
- liveConfiguration 直播流的相关装备。
- elapsedRealtimeEpochOffsetMs 本地时刻和服务器时刻偏差,用于Timeline.getCurrentUnixTimeMs获取当时实践时刻。
- isPlaceholder用于符号当时Window是否为占位的Window,由于当时正式数据还未加载,因此只包括初始值的占位信息,其间mediaItem在播映前已知,PlaceholderTimeline会将此值设置为true。
-
Period:界说了媒体的单个逻辑块,如一个视频文件。它还能够界说刺进到视频里的广告组,还记载这些广告是否已经加载和播映。
上图包括了2个Period,指向同一个Window,Period包括以下特点:
- uid Period的仅有标识符。
- windowIndex Period 所属Window的索引。
- durationUs 当时Period的时长,Period完毕时刻相关于Period开端时刻的差值,关于直播流(图中period2)就没有值。
- positionInWindowUs 该Period的开端时刻相关于其所属Window开端时刻的差值。假如该Period的开端时刻在Window左侧,则为负值,如上图的period1,比照Window的图能够看到他们都是指示的同一线段仅仅方向纷歧致,也便是这个值的绝对值是和Window的positionInFirstPeriodUs是相等的。
- adPlaybackState 刺进到Period中的广告相关信息。
Timeline是不可变的,是当时播映的一个静态快照,从这个视点比照火箭发射(播映器播映),发射火箭的时段或许是接连的几天(时段),但是能够发射(能够播映)的窗口期或许就在这一天中的某1个小时,具体在这个小时的哪个时刻点发射(播映)就对应Window的defaultPositionUs(小黑点),而这个窗口期或许正好在23:30-1:00,跨过2天(时段)。
下面列举出各种媒体 Timeline 的表明
单文件或许点播流媒体
这类媒体包括一个Period和一个Window。 Window和 Period 一样长,Window的默许播映方位就在Period 起点。这个很好了解,当你播映本地的一个视频文件时,由所以单个文件能够了解为只要一个文件的播映列表,这个文件能够从头播映播映到完毕,由于文件只要一个所以 Period 只要一段。像单个视频文件或许点播类的HLS便是用的这种方法笼统的 Timeline,一个文件或许点播流就对应一个 Period。
文件播映列表或许点播流列表
这类媒体包括多个Window和多个Period,每个Period 都有一个自己的Window与之对应,Window默许播映方位就在每个Period的开端,这类媒体能够想象成将上面的单个文件添加到一个播映列表。这类媒体只要在列表里播映到相应的项才干获取到Window和Period。ExoPlayer 针对这种结构,其实是经过将上面的单个Timeline组合起来,笼统出一个新的ListTimeline来完成的,也便是上图相当于3个Timeline。
有限可播的直播流
由所以直播内容是实时产生的,跟着时刻不断增多,所以Period总时长是不知道的。由所以有限的,仍然可播映内容时刻只占 Period 的一段,所以Window就界说了这段可播映规模,开端播映播映方位也纷歧定在Window的最初。此刻Window的 isLive=true,当Window改动时isDynamic将被设置为true。这类媒体的默许播映方位一般在Window的边缘,接近于当时时刻,如上图的黑点。像直播类的DASH或许HLS都属于这类。举个例子,当你看一个直播时,你能够回看2分钟之前到现在的视频,这个2分钟到现在便是一个Window,跟着时刻的推移Window也在向右平移,那么这个Window便是动态的,isDynamic=true,而打开这个直播默许的播映方位往往是最接近当时时刻的点,同时也在Window的右侧边缘。
无限可播的直播流
和上面有限可播的直播流相似,仅有不同的是Window的起点固定在Period的最初,也便是能够播映之前已播的一切直播内容。
有多个Period的直播流
这类将直播流分成了多个Period,和有限可播的直播流相似,仅仅Window或许跨一个或多个Period。
先点播后直播流
这类将点播流和直播流结合,当点播流播映完毕的时候直播流将在Window靠近当时时刻的一侧开端播映。这种能够当作将点播文件和直播文件放到一个播映列表里。
带有插播广告的点播流
这类在单个点播流中刺进了广告(上图灰色)。经过查询当时的Period能够获取广告组或许广告的信息。
关于一些动态的媒体,比如说播映一个直播流,跟着时刻的推移不同时刻点的Timeline(播映快照)对应Period的时长或许数量是不断增加的,不同时刻点的Timeline对应的Window是不断改动的,其间包括Window的 开端时刻、完毕时刻、时长等等都在不断的改变,而非直播流这些又是相对固定的。
小结下特点
- Timeline 里或许包括多个Period或许Window
- 多个Period都是接连的,而多个Window或许是不接连的
- Window时长小于等于一切Period的时长和
- 一个Window或许跨域多个Peroid
- Window开端时刻大于等于第一个Period的开端时刻
- Window的默许播映方位不是固定的
- Period能够跟着时刻没有右鸿沟,但Window是一定有右鸿沟的,也便是有确认的durationUs
- 关于静态媒体。在不同时刻获取的Timelin对应的Period和Window是相对固定的。
- 关于动态媒体。在不同时刻获取的Timelin对应的Period和Window是相对改变的。
Timeline的完成
说完结构规划,看下代码具体是怎么完成上述规划的,先看下整体架构
ExoPlayer 播映各种媒体时,首要经过这几个完成类来描绘Timeline 来看下各自的效果
Timeline
这儿没有界说任何特点,首要界说完成了以下几个功用
- 查询Window和Period
//运用指定Window索引的数据填充Window
public final Window getWindow(int windowIndex, Window window)
public abstract int getWindowCount();
//获取下个Window,媒体列表的循环模式最终便是在这儿完成,这儿其实也是填充Window容器
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled));
public final Period getPeriod(int periodIndex, Period period);
- 经过已知的Window的方位,查找到对应的Period的方位,这儿要点看下,加深下Winodw和Period之间联系的了解
这个函数效果是获得Window中黑点(windowPositionUs)对应的Period红点(periodPositionUs)方位,这个方位是相关于Period开端方位的差值。
- 关于动态媒体periodPositionUs = windowPositionUs-period2.positionInWindowUs
- 关于静态媒体period.positionInWindowUs = 0,periodPositionUs = windowPositionUs
看下源码完成
public final Pair<Object, Long> getPeriodPositionUs(
Window window,
Period period,
int windowIndex,
long windowPositionUs,
long defaultPositionProjectionUs) {
Assertions.checkIndex(windowIndex, 0, getWindowCount());
getWindow(windowIndex, window, defaultPositionProjectionUs);//获取当时的Window
if (windowPositionUs == C.TIME_UNSET) {
windowPositionUs = window.getDefaultPositionUs();//windowPositionUs 没有设置,获取默许开端播映方位
if (windowPositionUs == C.TIME_UNSET) {//没有设置则返回
return null;
}
}
int periodIndex = window.firstPeriodIndex;
getPeriod(periodIndex, period);
while (periodIndex < window.lastPeriodIndex//从第一个period开端查找到最终一个
&& period.positionInWindowUs != windowPositionUs//查找到第一个开端时刻=Window方位或许完毕时刻(下一个period开端时刻)>Window方位的period
&& getPeriod(periodIndex + 1, period).positionInWindowUs <= windowPositionUs) {
periodIndex++;
}
getPeriod(periodIndex, period, /* setIds= */ true);
long periodPositionUs = windowPositionUs - period.positionInWindowUs;//用Window当时方位减去period开端方位(这2个方位都是相关于Window开端时刻的),成果便是相关于当时period开端方位的period当时方位,参阅上图
// The period positions must be less than the period duration, if it is known.
if (period.durationUs != C.TIME_UNSET) {
periodPositionUs = min(periodPositionUs, period.durationUs - 1);//保证不要超出period 总时长
}
// Period positions cannot be negative.
periodPositionUs = max(0, periodPositionUs);
return Pair.create(Assertions.checkNotNull(period.uid), periodPositionUs);
}
Timeline 完成类中其实并不包括Window和Period成员特点,而是保存了能够组装出这2个对象的数据,经过界说获取办法对传入的Window和Period对象填充来获取这个2个对象。简略说Window和Period就相当于一个获取数据容器,用于盛放数据,向上层供给数据。
SinglePeriodTimeline
这是一个只包括一个Period和一个静态Window的Timeline完成。
- 包括了用于组装Window和Period的必要数据
private final long presentationStartTimeMs;//用于媒体裁剪,能够先不必管
private final long windowStartTimeMs;//对应Window特点
private final long elapsedRealtimeEpochOffsetMs;//对应Window特点取当时实践时刻
private final long periodDurationUs;//对应Period特点
private final long windowDurationUs;//对应Window特点
private final long windowPositionInPeriodUs;//对应Window positionInFirstPeriodUs特点,由于这个值是以Window为起点核算的,所以取负数便是以Period为起点核算,-positionInFirstPeriodUs则对应Period的positionInWindowUs特点,
private final long windowDefaultStartPositionUs;//对应Window特点
private final boolean isSeekable;//对应Window特点
private final boolean isDynamic;//对应Window特点
private final boolean suppressPositionProjection;
private final Object manifest;//对应Window特点
private final MediaItem mediaItem;//对应Window特点
private final MediaItem.LiveConfiguration liveConfiguration;//对应Window特点
- 完成了单个Window和Period的填充办法,也便是Timeline里界说的虚函数getWindow和getPeriod,这儿完成就很简略由于只要一个Window和Period,直接将界说的特点设置进去
@Override
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
Assertions.checkIndex(windowIndex, 0, 1);
long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs;
if (isDynamic && !suppressPositionProjection && defaultPositionProjectionUs != 0) {
if (windowDurationUs == C.TIME_UNSET) {
// Don't allow projection into a window that has an unknown duration.
windowDefaultStartPositionUs = C.TIME_UNSET;
} else {
windowDefaultStartPositionUs += defaultPositionProjectionUs;
if (windowDefaultStartPositionUs > windowDurationUs) {
// The projection takes us beyond the end of the window.
windowDefaultStartPositionUs = C.TIME_UNSET;
}
}
}
return window.set(
Window.SINGLE_WINDOW_UID,
mediaItem,
manifest,
presentationStartTimeMs,
windowStartTimeMs,
elapsedRealtimeEpochOffsetMs,
isSeekable,
isDynamic,
liveConfiguration,
windowDefaultStartPositionUs,
windowDurationUs,
/* firstPeriodIndex= */ 0,
/* lastPeriodIndex= */ 0,
windowPositionInPeriodUs);//正值
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
Assertions.checkIndex(periodIndex, 0, 1);
@Nullable Object uid = setIds ? UID : null;
return period.set(/* id= */ null, uid, 0, periodDurationUs, -windowPositionInPeriodUs);//取负值设置Period
}
PlaceholderTimeline
一个占位的Timeline,通常用于播映器perpared前占位,由于在播准备前Window和Period都是动态不确认的,只要mediaItem是确认的
ForwardingTimeline
这个类就很简略了,直接结构的时候传入一个Timeline,直接将有办法转发给这个Timeline
MaskingTimeline
继承自ForwardingTimeline,在已有Timeline掩盖一层,首要服务于MaskingMediaSource,在MaskingMediaSource创立时假如没有Timeline则在占位的PlaceholderTimeline上掩盖一层。
- 在播映器媒体没有prepared前,创立MaskingTimeline掩盖在PlaceholderTimeline上来占位。
public static MaskingTimeline createWithPlaceholderTimeline(MediaItem mediaItem) {
return new MaskingTimeline(
new PlaceholderTimeline(mediaItem),
Window.SINGLE_WINDOW_UID,
MASKING_EXTERNAL_PERIOD_UID);
}
- 在prepared后,创立MaskingTimeline时传入需要被替换成原始ID的PeriodUid和WindowUid,当获取这些指定的ID时将返回原始ID(Window.SINGLE_WINDOW_UID或许MASKING_EXTERNAL_PERIOD_UID)。
private final Object replacedInternalWindowUid;
private final Object replacedInternalPeriodUid;
public static MaskingTimeline createWithRealTimeline(
Timeline timeline, @Nullable Object firstWindowUid, @Nullable Object firstPeriodUid) {
return new MaskingTimeline(timeline, firstWindowUid, firstPeriodUid);
}
@Override
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
timeline.getWindow(windowIndex, window, defaultPositionProjectionUs);
if (Util.areEqual(window.uid, replacedInternalWindowUid)) {
window.uid = Window.SINGLE_WINDOW_UID;
}
return window;
}
- 创立MaskingTimeline的过程首要在创立MaskingMediaSource时完成。
public MaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation) {
super(mediaSource);
this.useLazyPreparation = useLazyPreparation && mediaSource.isSingleWindow();
window = new Timeline.Window();
period = new Timeline.Period();
@Nullable Timeline initialTimeline = mediaSource.getInitialTimeline();
if (initialTimeline != null) {
timeline =
MaskingTimeline.createWithRealTimeline(
initialTimeline, /* firstWindowUid= */ null, /* firstPeriodUid= */ null);
hasRealTimeline = true;
} else {
timeline = MaskingTimeline.createWithPlaceholderTimeline(mediaSource.getMediaItem());
}
}
AbstractConcatenatedTimeline
将一个或多个Timeline按照一定次第串联成一个新的Timeline的笼统基类。
- 界说了一个播映次第的分发器用于支撑按照指定次第播映列表,和一个符号当时一切的子Timeline列表否是原子的,不可拆分的,有必要作为一个整体进行重复播映,而且不支撑指定次第。
private final ShuffleOrder shuffleOrder;
private final boolean isAtomic;
@Override
public int getNextWindowIndex(
int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
if (isAtomic) {
// Adapt repeat and shuffle mode to atomic concatenation.
repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode;//有必要作为一个整体进行重复播映
shuffleModeEnabled = false;//不支撑指定次第
}
.....
}
private int getNextChildIndex(int childIndex, boolean shuffleModeEnabled) {
return shuffleModeEnabled
? shuffleOrder.getNextIndex(childIndex)//运用指定次第代替列表次第
: childIndex < childCount - 1 ? childIndex + 1 : C.INDEX_UNSET;
}
- 由于包括了多个Timeline,完成或许重写了本来Timeline里关于Window和Period获取的相关办法。大致流程便是先去查询Timeline列表获取当时的Timeline(这部分Timeline的管理是虚函数由子类完成),然后从当时的Timeline里获取指定的Window或许Period。
@Override
public final Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
int childIndex = getChildIndexByWindowIndex(windowIndex);//经过windowIndex获取子Timeline的索引
int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);//获取子Timeline的第一个Window在一切Timeline中的索引
int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex);//获取子Timeline的第一个Period在一切Timeline中的索引
getTimelineByChildIndex(childIndex)//获取子Timeline
.getWindow(windowIndex - firstWindowIndexInChild, window, defaultPositionProjectionUs);//用当时windowIndex-firstWindowIndexInChild获取指定Window索引在子Timeline里的索引
Object childUid = getChildUidByChildIndex(childIndex);//获取子Timeline的UID
//假如当时Window的UID为SINGLE_WINDOW_UID,则直接运用Timeline的UID,由于此刻的Timeline里只包括这一个Window
window.uid =
Window.SINGLE_WINDOW_UID.equals(window.uid)
? childUid
: getConcatenatedUid(childUid, window.uid);//否则用子Timeline的UID和当时Window的UID组成一个新的UID
window.firstPeriodIndex += firstPeriodIndexInChild;//更新第一个Period的索引
window.lastPeriodIndex += firstPeriodIndexInChild;
return window;
}
PlaylistTimeline
继承了AbstractConcatenatedTimeline,实践管理了多个Timeline,获取指定索引的子Timeline。ExoPlayer默许会将一切的媒体都封装成PlaylistTimeline。 具体数据结构参阅下图 这儿每个Window或许Priod都有2个Index,一个是在PalylistTimeline中的索引,一个是在当时Timeline中的索引,firstPeriodInChildIndices记载了每个子Timeline中第一个Window或许Period在PlaylistTimeline中的索引,firstWindowInChildIndices相似,看下代码完成
private final int windowCount;//Window总数
private final int periodCount;//PeriodCount总数
private final int[] firstPeriodInChildIndices;
private final int[] firstWindowInChildIndices;
private final Timeline[] timelines;//一切Timeline的数组
private final Object[] uids;//能够了解成Timeline的UID数组
private final HashMap<Object, Integer> childIndexByUid;//包括UID对应的子Timeline索引的MAP
private PlaylistTimeline(Timeline[] timelines, Object[] uids, ShuffleOrder shuffleOrder) {
super(/* isAtomic= */ false, shuffleOrder);//这儿播映列表不具有原子性
int childCount = timelines.length;
this.timelines = timelines;
firstPeriodInChildIndices = new int[childCount];
firstWindowInChildIndices = new int[childCount];
this.uids = uids;
childIndexByUid = new HashMap<>();
int index = 0;
int windowCount = 0;
int periodCount = 0;
for (Timeline timeline : timelines) {
this.timelines[index] = timeline;
firstWindowInChildIndices[index] = windowCount;//保存
firstPeriodInChildIndices[index] = periodCount;
windowCount += this.timelines[index].getWindowCount();//累加索引
periodCount += this.timelines[index].getPeriodCount();
childIndexByUid.put(uids[index], index++);
}
this.windowCount = windowCount;
this.periodCount = periodCount;
}
总结
- Timeline仅仅对媒体播映状况的一种描绘方法,方便播映器查询当时的播映状况的快照,用于辅导播映器播映。
- Timeline里并不包括用来给播映渲染音视频的媒体数据,mediaItem也仅仅保存了媒体的描绘。
- 实践在播映不同媒体结构时,Timeline的结构并不是上面某个单一的数据结构,而是上面这些类型结构的组合,后续系列文章会提到,如播映单文件Timeline结构,播映列表Timeline结构。
版权声明
本文为作者山雨楼原创文章
转载请注明出处
原创不易,觉得有用的话,保藏转发点赞支撑