專案記錄: Exoplayer備忘錄1
專案記錄: Exoplayer備忘錄1
一體式頭盔全景播放器開發是Unity-Android協作完成的.
Android端採用的Google的Exoplayer作為框架.
重點通過學習了以下幾篇部落格,對Exoplayer有了一個初步的認識.做一下記錄
1. Exoplayer簡介
ExoPlayer是建立在 Android low-level api之上的 app級開源播放器。開源專案包含 ExoPlayer庫和demo。 它用Java實現瞭解封裝,用MediaCodec實現 硬體解碼
。
-
ExoPlayer庫 - 類庫原始碼
-
Demo - 演示類庫的使用
ExoPlayer相較於MediaPlayer有很多優點:
- 支援 Dynamic Adaptive Streaming over HTTP (DASH) 和SmoothStreaming,更多支援請參閱支援的格式)詳細資訊頁面。
- 支援高階 HLS (HTTP Live Streaming)功能,如正確處理 #EXT-X-DISCONTINUITY的標籤。
- 能夠無縫融合,串聯和迴圈媒體資源。
- 支援定製和擴充套件,ExoPlayer是考慮到這一點而設計的,並允許許多部件與定製實現替換。
- 更新起來更方便
- 裝置通用性更強
- 支援在Android 4.3(API級別18)和更高的Widevine通用加密。
2. Library Overview
ExoPlayer庫的核心是ExoPlayer介面。
ExoPlayer 介面暴露了傳統的 high-level 播放器中的功能,如資源緩衝,播放,暫停和拖拽等。介面的實現類對媒體的播放型別、儲存位置和渲染方式做出假設,而不是籠統的載入和渲染。
Exoplayer 把播放型別、儲存位置和渲染方式等任務委派給不同的部件,然後在建立播放器或後臺播放的時候把這些部件注入。 這些部件包括:
- MediaSource - 負責裝載 media,裝載到MediaSource 的 media 可以被讀取,MediaSource 在呼叫 ExoPlayer.prepare 方法時被注入。
- Render S - 用於渲染 media 的部件,在建立播放器時被注入
- TrackSelector - 從MediaSource 中選出 media 提供給可用的 Render S 來渲染,在建立播放器時被注入。
- LoadControl - 控制 MediaSource 快取更多的 media,有多少 media 被緩衝。在建立播放器時被注入。
類庫提供這些部件在通常情況下的預設實現.一個 ExoPlayer 可以利用這些部件.
如果標準實現不能滿足需求,也可以使用自定義實現
舉例:
- ExoPlayer提供預設的音訊和視訊渲染器,利用了Android框架中的MediaCodec和AudioTrack類。這兩個都需要一個SampleSource物件中注入,用來實現媒體示例的播放。
- 元件的注入在當前ExoPlayer庫中是普遍存在的。
- 下圖展示了使用一個ExoPlayer來配置和播放MP4媒體流的高階物件模型。
- 預設的音訊和視訊渲染器已經被註解到ExoPlayer中。一個叫ExtractorSampleSource類的實現被註解到渲染器中用於提供簡單的媒體播放功能。DataSource和Extractor示例被註解到ExtractorSampleSource來支援載入媒體流和在被載入的資料中提取樣板。在這個示例中DefaultUriDataSource和Mp4Extractor被用於播放從URIs中匯入的MP4流。
3. Demo上層呼叫
上層呼叫方式基本為:
PlayerActivity -> DemoPlayer -> ExoPlayer
PlayerActivity -> RendererBuilder -> ExtractorRendererBuilder
類圖為:
- 其中,PlayerActivity是面向UI層的.一方面是控制播放器Demoplayer.一方面是選擇了Renderer.這裡的Renderer指定了資料來源的格式,解碼方式和緩衝區大小等等.
- 緩衝區的大小指的是
RollingSampleBuffer的大小
不會影響播放的速度,只會 影響快取資料的最大值. - Exoplayer則是媒體API的介面
- DemoPlayer中直接封裝了ExoPlayer和相關的回撥介面,負責播放器的邏輯控制和傳入
SurfaceView
等操作, 而非播放器內部的原理.
通過時序圖來說明Demo中幾個類的呼叫和封裝方式
4. 程式碼結構
- 以播放器本地視訊的程式碼結構展開
ExoPlayer ->ExoPlayerImpl -> ExoPlayerImplInternal -> TrackRenderer
MediaCodecVideoTrackRenderer & MediaCodecAudioTrackRenderer -> MediaCodecTrackRenderer -> SampleSourceTrackRenderer -> SampleSource,SampleSourceReader
ExtractorSampleSource -> DataSource & Extractor & Loader
- ExoPlayer為介面。ExoPlayerImpl為實現,實現的一些詳細步驟在ExoPlayerImplInternal中。後者用
Handler訊息機制
進行非同步通訊,必要時會阻塞。 TrackRenderer
是渲染器的介面MediaCodecTrackRenderer
中加入了MediaCodec
(Android硬解碼)。ExoPlayer用的是硬解,並且要求4.1以上Android系統。SampleSourceTrackRenderer
中呼叫了SampleSource
,SampleSourceReader
的介面.SampleSource
在這裡指的是解封裝後的媒體資料。ExtractorSampleSource
相當於一個核心控制器,它實現了SampleSource和SampleSourceReader介面。它通過實際的控制執行緒Loader
,把從某DataSource即資料來源中傳過來的原始資料,傳遞給某Extractor來解封裝。原始資料解析成SampleSource後,儲存在RollingSampleBuffer即環形緩衝區中。MediaCodecTrackRenderer
會間接通過ExtractorSampleSource
間接從RollingSampleBuffer
中讀取資料並渲染成畫面,顯示到SurfaceView中。
流程圖如下所示:
5. 程式碼原理
ExoPlayer -> ExoPlayerImpl -> ExoPlayerImplInternal
通過以下這段程式碼 ExoPlayerImpl的建構函式.可以看出,ExoplayerImpl中持有一個 ExoplayerImpl
物件來控制播放器 . 在建立播放器的時候,傳入了一個 eventHandler
物件,把底層的錯誤資訊和狀態資訊傳遞給上層.
// ExoPlayerImpl類中構造方法:
eventHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
ExoPlayerImpl.this.handleEvent(msg);
}
};
internalPlayer = new ExoPlayerImplInternal(eventHandler, playWhenReady, selectedTrackIndices,
minBufferMs, minRebufferMs);
具體的功能性程式碼,都在 ExoPlayerImplInternal
中實現.狀態改變資訊和錯誤資訊會通過eventHandler傳上來進行處理.
// ExoPlayerImpl類:
// Not private so it can be called from an inner class without going through
// a thunk method.
/* package */ void handleEvent(Message msg) {
switch (msg.what) {
case ExoPlayerImplInternal.MSG_PREPARED: {
System.arraycopy(msg.obj, 0, trackFormats, 0, trackFormats.length);
playbackState = msg.arg1;
for (Listener listener : listeners) {
listener.onPlayerStateChanged(playWhenReady, playbackState);
}
break;
}
case ExoPlayerImplInternal.MSG_STATE_CHANGED: {
playbackState = msg.arg1;
for (Listener listener : listeners) {
listener.onPlayerStateChanged(playWhenReady, playbackState);
}
break;
}
case ExoPlayerImplInternal.MSG_SET_PLAY_WHEN_READY_ACK: {
pendingPlayWhenReadyAcks--;
if (pendingPlayWhenReadyAcks == 0) {
for (Listener listener : listeners) {
listener.onPlayWhenReadyCommitted();
}
}
break;
}
case ExoPlayerImplInternal.MSG_ERROR: {
ExoPlaybackException exception = (ExoPlaybackException) msg.obj;
for (Listener listener : listeners) {
listener.onPlayerError(exception);
}
break;
}
}
}
- 這裡的listeners是一個CopyOnWriteArrayList,裡面的物件都是Listener,這裡用的是一個觀察者模式,用於給上層監聽回撥訊息。上層即DemoPlayer或是EventLogger都在這裡註冊或登出監聽。
ExoPlayerImplInternal -> TrackRenderer -> SampleSource,SampleSourceReader -> ExtractorSampleSource
1) ExoplayerImplInternal中的訊息機制
// ExoPlayerImplInternal類中構造方法:
internalPlaybackThread = new PriorityHandlerThread(getClass().getSimpleName() + ":Handler",
Process.THREAD_PRIORITY_AUDIO);
internalPlaybackThread.start();
handler = new Handler(internalPlaybackThread.getLooper(), this);
ExoplayerImplInternal 實現了 Handler.Callback
介面
// ExoPlayerImplInternal類: 訊息處理
@Override
public boolean handleMessage(Message msg) {
try {
switch (msg.what) {
case MSG_PREPARE: {
prepareInternal((TrackRenderer[]) msg.obj);
return true;
}
case MSG_INCREMENTAL_PREPARE: {
incrementalPrepareInternal();
return true;
}
case MSG_SET_PLAY_WHEN_READY: {
setPlayWhenReadyInternal(msg.arg1 != 0);
return true;
}
case MSG_DO_SOME_WORK: {
doSomeWork();
return true;
}
case MSG_SEEK_TO: {
seekToInternal(Util.getLong(msg.arg1, msg.arg2));
return true;
}
case MSG_STOP: {
stopInternal();
return true;
}
case MSG_RELEASE: {
releaseInternal();
return true;
}
case MSG_CUSTOM: {
sendMessageInternal(msg.arg1, msg.obj);
return true;
}
case MSG_SET_RENDERER_SELECTED_TRACK: {
setRendererSelectedTrackInternal(msg.arg1, msg.arg2);
return true;
}
default:
return false;
}
} catch (ExoPlaybackException e) {
Log.e(TAG, "Internal track renderer error.", e);
eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
stopInternal();
return true;
} catch (RuntimeException e) {
Log.e(TAG, "Internal runtime error.", e);
eventHandler.obtainMessage(MSG_ERROR, new ExoPlaybackException(e, true)).sendToTarget();
stopInternal();
return true;
}
}
- 通過這段程式碼,可以看出來,在
ExoPlayerImplInternal
內部是通過訊息來控制播放器邏輯(控制TrackRenderer
)。
2) doSomeWork分析及作用
-
doSomeWork方法是在播放器執行完prepare後執行的。是在準備動作都完成後,具體控制播放器開始渲染畫面的方法。
-
主要的動作有:
1. 更新positionUs(以及elapsedRealtimeUs) 2. renderer.doSomeWork 呼叫渲染器進行渲染 3. 把播放狀態回撥上層 4. 定時執行下一次doSomeWork (10ms?)
// ExoPlayerImplInternal類:doSomeWork
private void doSomeWork() throws ExoPlaybackException {
TraceUtil.beginSection("doSomeWork");
long operationStartTimeMs = SystemClock.elapsedRealtime();
long bufferedPositionUs = durationUs != TrackRenderer.UNKNOWN_TIME_US ? durationUs : Long.MAX_VALUE;
boolean allRenderersEnded = true;
boolean allRenderersReadyOrEnded = true;
updatePositionUs();// 筆記:更新positionUs
for (int i = 0; i < enabledRenderers.size(); i++) {
TrackRenderer renderer = enabledRenderers.get(i);
// TODO: Each renderer should return the maximum delay before which
// it wishes to be
// invoked again. The minimum of these values should then be used as
// the delay before the next
// invocation of this method.
// 筆記:這裡呼叫了renderer的doSomeWork方法並傳入了positionUs,
// elapsedRealtimeUs是個獨立的系統時間參考
renderer.doSomeWork(positionUs, elapsedRealtimeUs);
allRenderersEnded = allRenderersEnded && renderer.isEnded();
// Determine whether the renderer is ready (or ended). If it's not,
// throw an error that's
// preventing the renderer from making progress, if such an error
// exists.
boolean rendererReadyOrEnded = rendererReadyOrEnded(renderer);
if (!rendererReadyOrEnded) {
renderer.maybeThrowError();
}
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded;
if (bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) {
// We've already encountered a track for which the buffered
// position is unknown. Hence the
// media buffer position unknown regardless of the buffered
// position of this track.
} else {
long rendererDurationUs = renderer.getDurationUs();
long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
if (rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) {
bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US;
} else if (rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US
|| (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US
&& rendererDurationUs != TrackRenderer.MATCH_LONGEST_US
&& rendererBufferedPositionUs >= rendererDurationUs)) {
// This track is fully buffered.
} else {
bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);
}
}
}
// 筆記:更新緩衝位置,主要用於上層回撥
this.bufferedPositionUs = bufferedPositionUs;
// 筆記:根據durationUs和positionUs來判斷狀態和開關渲染器(Renderer)
if (allRenderersEnded && (durationUs == TrackRenderer.UNKNOWN_TIME_US || durationUs <= positionUs)) {
setState(ExoPlayer.STATE_ENDED);
stopRenderers();
} else if (state == ExoPlayer.STATE_BUFFERING && allRenderersReadyOrEnded) {
setState(ExoPlayer.STATE_READY);
if (playWhenReady) {
startRenderers();
}
} else if (state == ExoPlayer.STATE_READY && !allRenderersReadyOrEnded) {
rebuffering = playWhenReady;
setState(ExoPlayer.STATE_BUFFERING);
stopRenderers();
}
// 筆記:準備再次呼叫doSomework
handler.removeMessages(MSG_DO_SOME_WORK);
if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) {
scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, RENDERING_INTERVAL_MS);
} else if (!enabledRenderers.isEmpty()) {
scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, IDLE_INTERVAL_MS);
}
TraceUtil.endSection();
}
private void scheduleNextOperation(int operationType, long thisOperationStartTimeMs, long intervalMs) {
long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs;
long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime();
if (nextOperationDelayMs <= 0) {
handler.sendEmptyMessage(operationType);
} else {
handler.sendEmptyMessageDelayed(operationType, nextOperationDelayMs);
}
}
// 筆記:通過上層傳入的eventHandler把狀態改變資訊傳遞給上層
private void setState(int state) {
if (this.state != state) {
this.state = state;
eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget();
}
}
3) updataPositionUs和renderer.doSomeWork分析
- positionUs指的是實際渲染位置。
- 通過這段在ExoPlayerImplInternal類中的程式碼,看出,這有兩個分支,第一個分支主要是用於有音訊的情況下,音訊時間可以作為整體參考時間,來調整positionUs。第二個分支是沒有音訊的情況下,用系統獨立時鐘作為整體參考時間,來調整positionUs。
// ExoPlayerImplInternal類:updatePositionUs
private void updatePositionUs() {
if (rendererMediaClock != null && enabledRenderers.contains(rendererMediaClockSource)
&& !rendererMediaClockSource.isEnded()) {
positionUs = rendererMediaClock.getPositionUs();
standaloneMediaClock.setPositionUs(positionUs);
} else {
positionUs = standaloneMediaClock.getPositionUs();
}
elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
}
- positionUs傳遞給了drainOutputBuffer方法和feedInputBuffer方法。用於調整播放時間,和獲取緩衝幀。
MediaCodecTrackRenderer類:
@Override
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
// 筆記:判斷是否應該繼續緩衝
sourceState = continueBufferingSource(positionUs)
? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState) : SOURCE_STATE_NOT_READY;
// 筆記:判斷解碼是否連續,如果不連續,則重啟解碼器
checkForDiscontinuity(positionUs);
if (format == null) {
// 筆記:讀取格式
readFormat(positionUs);
}
if (codec == null && shouldInitCodec()) {
// 筆記:當有格式無解碼器時,開啟解碼器
maybeInitCodec();
}
if (codec != null) {
TraceUtil.beginSection("drainAndFeed");
// 筆記:如果解碼器中可以輸出緩衝,則會返回true,否則返回false
while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {
}
// 筆記:如果解碼器還可以輸入原始幀,則返回true,否則返回false,第二個引數代表是否首次執行
if (feedInputBuffer(positionUs, true)) {
while (feedInputBuffer(positionUs, false)) {
}
}
TraceUtil.endSection();
}
codecCounters.ensureUpdated();
}
drainOutputBuffer
方法呼叫到了processOutputBuffer
方法,這裡處理緩衝幀。這個方法在MediaCodecTrackRenderer
類中是個抽象方法,具體實現在MediaCodecVideoTrackRenderer
和MediaCodecAudioTrackRenderer
類中。
// MediaCodecVideoTrackRenderer類:
// 筆記:返回true意味著輸出的緩衝幀已經被渲染,false意味著尚未被渲染
@Override
protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, ByteBuffer buffer,
MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) {
if (shouldSkip) {
skipOutputBuffer(codec, bufferIndex);
return true;
}
if (!renderedFirstFrame) {
if (Util.SDK_INT >= 21) {
renderOutputBufferV21(codec, bufferIndex, System.nanoTime());
} else {
renderOutputBuffer(codec, bufferIndex);
}
return true;
}
if (getState() != TrackRenderer.STATE_STARTED) {
return false;
}
// Compute how many microseconds it is until the buffer's presentation
// time.
long elapsedSinceStartOfLoopUs = (SystemClock.elapsedRealtime() * 1000) - elapsedRealtimeUs;
long earlyUs = bufferInfo.presentationTimeUs - positionUs - elapsedSinceStartOfLoopUs;
// Compute the buffer's desired release time in nanoseconds.
long systemTimeNs = System.nanoTime();
long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000);
// Apply a timestamp adjustment, if there is one.
long adjustedReleaseTimeNs = frameReleaseTimeHelper.adjustReleaseTime(bufferInfo.presentationTimeUs,
unadjustedFrameReleaseTimeNs);
earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000;
// 筆記:以上是通過positionUs(實際渲染位置),elapsedRealtimeUs(獨立時鐘位置),
// bufferInfo.presentationTimeUs(緩衝幀位置)得出緩衝位置和播放位置之間的時間差值。
// 筆記:如果渲染位置在此緩衝幀位置後面30ms,則棄掉此幀
if (earlyUs < -30000) {
// We're more than 30ms late rendering the frame.
dropOutputBuffer(codec, bufferIndex);
return true;
}
if (Util.SDK_INT >= 21)