1. 程式人生 > >專案記錄: Exoplayer備忘錄1

專案記錄: 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流。

img

3. Demo上層呼叫

上層呼叫方式基本為:

PlayerActivity -> DemoPlayer -> ExoPlayer 

PlayerActivity -> RendererBuilder -> ExtractorRendererBuilder 

類圖為:

这里写图片描述

  • 其中,PlayerActivity是面向UI層的.一方面是控制播放器Demoplayer.一方面是選擇了Renderer.這裡的Renderer指定了資料來源的格式,解碼方式和緩衝區大小等等.
  • 緩衝區的大小指的是 RollingSampleBuffer的大小 不會影響播放的速度,只會 影響快取資料的最大值.
  • Exoplayer則是媒體API的介面
  • DemoPlayer中直接封裝了ExoPlayer和相關的回撥介面,負責播放器的邏輯控制和傳入 SurfaceView 等操作, 而非播放器內部的原理.

通過時序圖來說明Demo中幾個類的呼叫和封裝方式

img

4. 程式碼結構

  • 以播放器本地視訊的程式碼結構展開
ExoPlayer ->ExoPlayerImpl -> ExoPlayerImplInternal -> TrackRenderer 

MediaCodecVideoTrackRenderer & MediaCodecAudioTrackRenderer ->  MediaCodecTrackRenderer -> SampleSourceTrackRenderer -> SampleSource,SampleSourceReader 

ExtractorSampleSource -> DataSource & Extractor & Loader 

img

  • 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

img

通過以下這段程式碼 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

img

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 類中是個抽象方法,具體實現在 MediaCodecVideoTrackRendererMediaCodecAudioTrackRenderer 類中。
// 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)