ExoPlayer视频播放流程简析(二)

ExoPlayer播放视频的流程是什么样的?在整个视频播放的过程中,它是如何进行的?

ExoPlayer在设计视频播放时定义了很多视频相关的接口,以使该框架灵活性和扩展性更好,在主要的PlayerExoPlayer中都定义了哪些通用的接口和方法呢?

源码参考r2.8.1版本。

Player定义的功能:

  1. 视频事件监听接口EventListener
    • 加载状态监听;
    • 播放状态监听;
    • 时间轴变化;
    • 轨道变化;
    • 视频参数变化;
    • seek状态变化;
    • 重放模式监听;
    • 播放异常信息监听;
  2. 视频组件;
  3. 字幕组件;
  4. 视频播放控制(开始,暂停,停止);
  5. 视频参数设置;
  6. 广告相关;

ExoPlayer定义的功能:

ExoPlayer实现了Player,在Player功能的基础上增加了如下特性:

  1. 其它线程和播放线程的通信;
  2. 媒体资源的准备接口;

Player中定义的都是和视频本身事件相关的接口;而在ExoPlayer中定义了和上层通信的接口以及为上层视频播放提供资源的接口;ExoPlayer为上层提供了交互的桥梁;

ExoPlayerImpl功能实现:

ExoPlayerImplExoPlayer的实现,但它仅提供了简单实现:

  1. 对外提供视频播放的基本信息;
  2. ExoPlayer的真正实现ExoPlayerImplInternal的封装;

Q: 为什么不由ExoPlayerImpl直接提供对ExoPlayer的实现呢?反而交由ExoPlayerImplInternal来完成。

A: 在ExoPlayerImplInternal的内部实现中,是通过handler机制将视频播放的事件发送到独立的线程中运行。由ExoPlayerImplInternal来完全处理播放相关事件的发送和实现;通过该方式达到业务的实现状态(提供给上层的接口)分离。接口的使用者根本不用关心其内部是如何实现的,并且也不应该将其实现暴漏给接口调用者。后续若有需求/实现的变更,仅修改对应的部分即可,互不影响。

ExoPlayerImplInternal部分实现

  1. 在构建ExoPlayerImplInternal对象时,传入了渲染器轨道选择器加载控制器等参数用于为后续的视频播放提供支持;
  2. 通过prepare方法为视频播放提供播放的视频源,内部实现是通过handler将该操作发送到播放线程执行,
1
2
3
4
5
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
handler
.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource)
.sendToTarget();
}

在播放线程中接收消息:

1
2
3
4
5
6
7
8
9
10
11
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_PREPARE:
prepareInternal(
(MediaSource) msg.obj,
/* resetPosition= */ msg.arg1 != 0,
/* resetState= */ msg.arg2 != 0);
break;
...
}
}

处理消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
// 标示进入就绪状态的个数;
// 若该值>0,则认为针对新的MediaSource的timeline还没有初始化完成,部分操作将进入就绪状态。
// 当timeline更新或者plyer stop时会被重置。
pendingPrepareCount++;
// 重置(清除MSG_DO_SOME_WORK;停止渲染并创建新的渲染器;清除就绪队列;重置状态;重置mediasource等信息)
resetInternal(/* releaseMediaSource= */ true, resetPosition, resetState);
// 通知重置加载控制器
loadControl.onPrepared();
this.mediaSource = mediaSource;
// 由于有了新的MediaSource,需要重置状态;
setState(Player.STATE_BUFFERING);
// 通知MediaSource的实现准备数据源;
mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener= */ this);
// 通知播放线程开始工作。
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}

准备工作完成后,后续的工作交给了MSG_DO_SOME_WORK,最终实现为doSomeWork:

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
91
private void doSomeWork() throws ExoPlaybackException, IOException {
long operationStartTimeMs = clock.uptimeMillis();
// 更新缓冲/播放时间
updatePeriods();
// 若队列中没有需要播放的资源,则在间隔PREPARING_SOURCE_INTERVAL_MS时间后重复检测。
if (!queue.hasPlayingPeriod()) {
// We're still waiting for the first period to be prepared.
maybeThrowPeriodPrepareError();
scheduleNextWork(operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS);
return;
}
MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();

TraceUtil.beginSection("doSomeWork");
// 更新播放(渲染)/缓冲位置;
updatePlaybackPositions();
long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
// 丢弃指定位置/关键帧之前的缓冲;
playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs - backBufferDurationUs,retainBackBufferFromKeyframe);

boolean renderersEnded = true;
boolean renderersReadyOrEnded = true;
// 此处检查渲染是否就绪,及渲染是否结束;
for (Renderer renderer : enabledRenderers) {
// TODO: Each renderer should return the maximum delay before which it wishes to be called
// again. The minimum of these values should then be used as the delay before the next
// invocation of this method.
renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);
renderersEnded = renderersEnded && renderer.isEnded();
// Determine whether the renderer is ready (or ended). We override to assume the renderer is
// ready if it needs the next sample stream. This is necessary to avoid getting stuck if
// tracks in the current period have uneven durations. See:
// https://github.com/google/ExoPlayer/issues/1874
boolean rendererReadyOrEnded = renderer.isReady() || renderer.isEnded()
|| rendererWaitingForNextStream(renderer);
if (!rendererReadyOrEnded) {
renderer.maybeThrowStreamError();
}
renderersReadyOrEnded = renderersReadyOrEnded && rendererReadyOrEnded;
}
if (!renderersReadyOrEnded) {
maybeThrowPeriodPrepareError();
}

long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;
// 所有的渲染已经结束则停止渲染(@todo playingPeriodDurationUs <= playbackInfo.positionUs ?)
if (renderersEnded
&& (playingPeriodDurationUs == C.TIME_UNSET
|| playingPeriodDurationUs <= playbackInfo.positionUs)
&& playingPeriodHolder.info.isFinal) {
setState(Player.STATE_ENDED);
stopRenderers();
// 若之前是缓冲状态,并且可以进入ready状态,则开始渲染。
} else if (playbackInfo.playbackState == Player.STATE_BUFFERING
&& shouldTransitionToReadyState(renderersReadyOrEnded)) {
setState(Player.STATE_READY);
if (playWhenReady) {
startRenderers();
}
// 已经进入播放状态,但timeline/渲染器没有准备就绪,则进入缓冲状态,并停止渲染器;
// 根据官方关于Player.STATE_READY的描述(The player is able to immediately play from its current position.),既然可直接播放,应该是渲染已经就绪了,renderersReadyOrEnded应该为true才对,有点矛盾呀。在官方的commit记录中有如下描述:
// - Renderers becoming ready is asynchronous, so the change wasn't well thought through :(.
// - This will bring back the possibility of getting stuck in the buffering-but-not-loading anything state. This will need to be addressed in a future CL.
} else if (playbackInfo.playbackState == Player.STATE_READY
&& !(enabledRenderers.length == 0 ? isTimelineReady() : renderersReadyOrEnded)) {
rebuffering = playWhenReady;
setState(Player.STATE_BUFFERING);
stopRenderers();
}
// 检测渲染器在渲染的过程中是否有异常,若有则中断渲染。
// 在每次轮询时,为什么每次都要执行渲染异常检测呢?其commit如下:
// On an edge case, player may get stuck when the renderers are ready but the buffer doesn't get full enough because of a fatal error in data source. An example state can be created by starting a live DASH stream and switching between normal and slow network connections.
if (playbackInfo.playbackState == Player.STATE_BUFFERING) {
for (Renderer renderer : enabledRenderers) {
renderer.maybeThrowStreamError();
}
}
// 若播放器已准备就绪/正在缓冲中,则间隔RENDERING_INTERVAL_MS时间后轮询执行doSomeWork()。
if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY)
|| playbackInfo.playbackState == Player.STATE_BUFFERING) {
scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS);
} else if (enabledRenderers.length != 0 && playbackInfo.playbackState != Player.STATE_ENDED) {
// 若播放未结束,则间隔IDLE_INTERVAL_MS时间后执行doSomeWork();可能播放器还未准备就绪/或者playWhenReady = false。此时轮询等待。
scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
} else {
// 停止轮询检测。
handler.removeMessages(MSG_DO_SOME_WORK);
}

TraceUtil.endSection();
}

在以上的过程中执行如下操作:

  1. 更新播放的位置;
  2. 更新缓冲的位置;
  3. 更新播放器当前状态;
  4. 渲染视频/音频;
  5. 判断播放器当前状态,若播放器在缓冲中,或者已准备就绪(缓冲完毕,可以播放),则轮询执行,一直等到播放器播放结束;
  6. 在轮询的过程中检测渲染器是否有异常,若有异常则终止轮询。

官方的描述中:

Once the player has been prepared, playback can be controlled by calling methods on the player. For example setPlayWhenReady can be used to start and pause playback

为什么setPlayWhenReady可以用于播放的暂停和开始呢?

1
2
3
public void setPlayWhenReady(boolean playWhenReady) {
handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget();
}

其详细实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {
rebuffering = false;
this.playWhenReady = playWhenReady;
// 停止渲染视频。
if (!playWhenReady) {
stopRenderers();
updatePlaybackPositions();
} else {
// 若视频渲染已经就绪,可以立即播放,则开始渲染,并轮询。
if (playbackInfo.playbackState == Player.STATE_READY) {
startRenderers();
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
// 若视频正在缓冲中,则开始轮询检测。
} else if (playbackInfo.playbackState == Player.STATE_BUFFERING) {
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
}
}

播放的开始和暂停如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void startRenderers() throws ExoPlaybackException {
rebuffering = false;
mediaClock.start();
for (Renderer renderer : enabledRenderers) {
renderer.start();
}
}

private void stopRenderers() throws ExoPlaybackException {
mediaClock.stop();
for (Renderer renderer : enabledRenderers) {
ensureStopped(renderer);
}
}

视频的停止通过如下方式实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void stopInternal(boolean reset, boolean acknowledgeStop) {
// 重置(清除MSG_DO_SOME_WORK;停止渲染并创建新的渲染器;清除就绪队列;重置状态;重置mediasource等信息)
resetInternal(
/* releaseMediaSource= */ true, /* resetPosition= */ reset, /* resetState= */ reset);
// 将标志位-1,若标志位位0,则ExoPlayerImpl会重置状态位。
playbackInfoUpdate.incrementPendingOperationAcks(
pendingPrepareCount + (acknowledgeStop ? 1 : 0));
pendingPrepareCount = 0;
// 通知加载控制器停止;
loadControl.onStopped();
// 将播放器状态置为空闲(没有视频源要播放)
setState(Player.STATE_IDLE);
}

视频播放的简单流程即如上所示,简单的处理过程为:

数据源准备->媒体渲染->显示->停止渲染->终止(资源释放)。

初步了解ExoPlayer,理解应该有偏差,以上仅是ExoPlayer处理视频播放的一个大致过程,关于视频源如何加载,轨道选择,渲染,缓冲等等,还需要不断深入了解。

0%