首頁>技術>

出處:https://juejin.cn/post/6917058201833701384

ExoPlayer 是google封裝的一個極其優秀的播放框架,目前youtube等很多應用都在使用它來播放影片,ExoPlayer的更新非常快,基本上保持著半個月更新一個版本的節奏; ExoPlayer本質上是使用MediaCodec來解碼影片,但是其中的流程非常複雜,所有我們由淺入深的講解一下,很多地方也是剛開始看,看得不詳細,向大家彙報一下吧。

概要

ExoPlayer旨在對正在播放的媒體型別,媒體的儲存方式和儲存方式以及呈現方式做出很少的假設(並因此而施加了一些限制)。 ExoPlayer實現不是直接實現媒體的載入和渲染,而是將這項工作委託給在建立播放器或準備播放時注入的元件。所有ExoPlayer實現共同的元件是:

一個MediaSource,它定義要播放的媒體,載入媒體,並從中讀取載入的媒體。在播放開始時透過prepare(MediaSource)注入MediaSource。庫模組為漸進式媒體檔案(ProgressiveMediaSource),DASH(DashMediaSource),SmoothStreaming(SsMediaSource)和HLS(HlsMediaSource)提供了預設實現,該實現是用於載入單個媒體樣本(SingleSampleMediaSource)的一種實現,該樣本最常用於側面載入的字幕檔案,以及用於從更簡單的媒體源構建更復雜的媒體源的實現(MergingMediaSource,ConcatenatingMediaSource,LoopingMediaSource和ClippingMediaSource)。渲染媒體的各個組成部分的渲染器。該庫提供了常見媒體型別(MediaCodecVideoRenderer,MediaCodecAudioRenderer,TextRenderer和MetadataRenderer)的預設實現。渲染器使用正在播放的MediaSource中的媒體。建立播放器時將注入渲染器。一個TrackSelector,它選擇MediaSource提供的軌道以供每個可用的渲染器使用。該庫提供適合大多數使用情況的預設實現(DefaultTrackSelector)。建立播放器時會注入TrackSelector。一個LoadControl,它控制MediaSource何時緩衝更多媒體,以及緩衝多少媒體。該庫提供適合大多數使用情況的預設實現(DefaultLoadControl)。建立播放器時將注入LoadControl。

可以使用庫提供的預設元件來構建ExoPlayer,但是如果需要非標準行為,也可以使用自定義實現來構建。例如,可以注入自定義LoadControl來更改播放器的緩衝策略,或者可以注入自定義Renderer來新增對Android原生不支援的影片編解碼器的支援。

在整個庫中都存在注入實現播放器功能的元件的概念。上面列出的預設元件實現將工作委託給其他注入的元件。這允許將許多子元件分別替換為自定義實現。例如,預設的MediaSource實現需要透過其建構函式注入一個或多個DataSource工廠。透過提供自定義工廠,可以從非標準來源或透過其他網路堆疊載入資料。

下面是ExoPlayer的執行緒模型:

必須從單個應用程式執行緒訪問ExoPlayer例項。在絕大多數情況下,這應該是應用程式的主執行緒。使用ExoPlayer的UI元件或IMA擴充套件時,也需要使用應用程式的主執行緒。建立播放器時,可以透過傳遞“ Looper”來明確指定必須訪問ExoPlayer例項的執行緒。如果未指定“ Looper”,則使用建立播放器的執行緒的“ Looper”,或者如果該執行緒不具有“ Looper”,則使用應用程式主執行緒的“ Looper”。在所有情況下,都可以使用Player.getApplicationLooper()查詢必須從中訪問播放器的執行緒的“ Looper”。在與Player.getApplicationLooper()關聯的執行緒上呼叫已註冊的偵聽器。請注意,這意味著已註冊的偵聽器在必須用於訪問播放器的同一執行緒上呼叫。內部回放執行緒負責回放。播放器在此執行緒上呼叫注入的播放器元件,例如Renderer,MediaSources,TrackSelectors和LoadControls。當應用程式在播放器上執行操作(例如搜尋)時,訊息會透過訊息佇列傳遞到內部回放執行緒。內部回放執行緒使用佇列中的訊息並執行相應的操作。類似地,當內部回放執行緒上發生回放事件時,一條訊息將透過第二個訊息佇列傳遞到應用程式執行緒。應用程式執行緒使用佇列中的訊息,更新應用程式的可見狀態並呼叫相應的偵聽器方法。注入的播放器元件可能會使用其他後臺執行緒。例如,MediaSource可以使用後臺執行緒來載入資料。

下面是寫一個ExoPlayer demo的基本呼叫流程,其中的關係還是比較簡單,我們可以從這張圖展開,全面分析一下ExoPlayer的工作機制。

一、MediaSource關係梳理

MediaSource是ExoPlayer中非常重要的結構,載入啊的影片資源要經過MediaSource封裝,MediaSource裡面還有解析這些影片資源的邏輯;下面是MediaSource類結構關係圖;

1.1 DashMediaSource

DashMediaSource專門用來解析Dash格式的影片,這裡所說的格式都是封裝格式,不是編解碼格式,下面也是一樣的; Dynamic Adaptive Streaming over HTTP,基於HTTP的動態自適應流,DASH是一項自適性流技術,其將多媒體檔案分割為一個或多個片段,並使用超文字傳輸協議傳遞給客戶端;

1.2 HlsMediaSource

HlsMediaSource專門用來解析HLS格式的影片,HLS---> Http Live Streaming技術,是Apple首先倡導的,目前已經成為直播流的基本技術構成;HLS的格式區分非常細,感興趣可以取了解一下HLS協議規範;

1.3 SsMediaSource

SsMediaSource 是微軟提出的針對SmoothStreaming技術的解決方案,其和DASH和HLS有很多共通之處;

1.4 ProgressiveMediaSource

這是針對一般影片的MediaSource封裝方案,所謂一般,就是非DASH、HLS、SS型別的影片; 下面是ProgressiveMediaSource支援的影片的封裝格式;

小結:目前DASH和SS已經用的相對較少,我們主要分析HLS和ProgressiveMediaSource兩種型別;

1.5 MediaSource 判斷規則
private MediaSource createLeafMediaSource(      Uri uri, String extension, DrmSessionManager<ExoMediaCrypto> drmSessionManager) {    @ContentType int type = Util.inferContentType(uri, extension);    switch (type) {      case C.TYPE_DASH:        return new DashMediaSource.Factory(dataSourceFactory)            .setDrmSessionManager(drmSessionManager)            .createMediaSource(uri);      case C.TYPE_SS:        return new SsMediaSource.Factory(dataSourceFactory)            .setDrmSessionManager(drmSessionManager)            .createMediaSource(uri);      case C.TYPE_HLS:        return new HlsMediaSource.Factory(dataSourceFactory)            .setDrmSessionManager(drmSessionManager)            .createMediaSource(uri);      case C.TYPE_OTHER:        return new ProgressiveMediaSource.Factory(dataSourceFactory)            .setDrmSessionManager(drmSessionManager)            .createMediaSource(uri);      default:        throw new IllegalStateException("Unsupported type: " + type);    }  }

各種MediaSource型別是根據一定的規則來使用的,上面是判斷的邏輯,提取的關鍵是根據uri和extension得到的type型別;

public static int inferContentType(String fileName) {    fileName = toLowerInvariant(fileName);    if (fileName.endsWith(".mpd")) {      return C.TYPE_DASH;    } else if (fileName.endsWith(".m3u8")) {      return C.TYPE_HLS;    } else if (fileName.matches(".*\\.ism(l)?(/manifest(\\(.+\\))?)?")) {      return C.TYPE_SS;    } else {      return C.TYPE_OTHER;    }  }

這兒的判斷邏輯就非常簡單,直接判斷一下uri的fileName,這樣針對HLS的判斷不是很準確,很多HLS型別的影片會被判斷成非HLS型別,之後的HLS格式分析會講到;

二、MediaSource解析

這裡有很多Factory類,這是ExoPlayer將各個模組的工作統一封裝在Factory類中,然後由Factory統一管理;

ExoPlayer中有DefaultHttpDataSourceFactory ---> DefaultHttpDataSource 來實現對HLS資源的解析工作;

HlsMediaSource.Factory建構函式中會傳入一個DataSource.Factory物件,這個物件就是DefaultHttpDataSourceFactory 物件,這個物件負責http請求;

public Factory(DataSource.Factory dataSourceFactory) {      this(new DefaultHlsDataSourceFactory(dataSourceFactory));    }

ExoPlayer中很多Factory類,基本上每個模組都有相應的Factory類管理這個模組的相應工作; DefaultHttpDataSource中有針對相應的file path解析的工作:

public long open(DataSpec dataSpec) throws IOException {    Assertions.checkState(dataSource == null);    // Choose the correct source for the scheme.    String scheme = dataSpec.uri.getScheme();    if (Util.isLocalFileUri(dataSpec.uri)) {      String uriPath = dataSpec.uri.getPath();      if (uriPath != null && uriPath.startsWith("/android_asset/")) {        dataSource = getAssetDataSource();      } else {        dataSource = getFileDataSource();      }    } else if (SCHEME_ASSET.equals(scheme)) {      dataSource = getAssetDataSource();    } else if (SCHEME_CONTENT.equals(scheme)) {      dataSource = getContentDataSource();    } else if (SCHEME_RTMP.equals(scheme)) {      dataSource = getRtmpDataSource();    } else if (SCHEME_UDP.equals(scheme)) {      dataSource = getUdpDataSource();    } else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) {      dataSource = getDataSchemeDataSource();    } else if (SCHEME_RAW.equals(scheme)) {      dataSource = getRawResourceDataSource();    } else {      dataSource = baseDataSource;    }    // Open the source and return.    return dataSource.open(dataSpec);  }

最關鍵的是http的dataSource,這是在DefaultHttpDataSourceFactory 中createDataSource 建立的,最終發起和處理http請求的地方在DefaultHttpDataSource類中;

上面類圖關係可以看出ExoPlayer中DataSource之間的結構分佈; 常用的Http請求的主要處理類是DefaultHttpDataSource,OkHttpDataSource和CronetDataSource是擴充套件的類,方便開發者接入外部的網路載入庫;

三、Renderer

Renderer從SampleStream讀取的媒體。 在內部,Renderer的生命週期由擁有的ExoPlayer管理。隨著整體播放狀態和啟用的軌道發生變化,Renderer會透過各種狀態進行轉換。有效狀態轉換如下所示,並標有每次轉換期間呼叫的方法。

還有一些可供擴充套件的Renderer類,如:FfmpegAudioRenderer ,這兒沒有列出;

3.1 Renderer 初始化

在構造SimpleExoPlayer物件的時候,傳入了Renderer list物件,這個Renderer list物件,就是將支援的所有Renderer 渲染器傳進來;

public Renderer[] createRenderers(      Handler eventHandler,      VideoRendererEventListener videoRendererEventListener,      AudioRendererEventListener audioRendererEventListener,      TextOutput textRendererOutput,      MetadataOutput metadataRendererOutput,      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {    if (drmSessionManager == null) {      drmSessionManager = this.drmSessionManager;    }    ArrayList<Renderer> renderersList = new ArrayList<>();    buildVideoRenderers(        context,        extensionRendererMode,        mediaCodecSelector,        drmSessionManager,        playClearSamplesWithoutKeys,        enableDecoderFallback,        eventHandler,        videoRendererEventListener,        allowedVideoJoiningTimeMs,        renderersList);    buildAudioRenderers(        context,        extensionRendererMode,        mediaCodecSelector,        drmSessionManager,        playClearSamplesWithoutKeys,        enableDecoderFallback,        buildAudioProcessors(),        eventHandler,        audioRendererEventListener,        renderersList);    buildTextRenderers(context, textRendererOutput, eventHandler.getLooper(),        extensionRendererMode, renderersList);    buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(),        extensionRendererMode, renderersList);    buildCameraMotionRenderers(context, extensionRendererMode, renderersList);    buildMiscellaneousRenderers(context, eventHandler, extensionRendererMode, renderersList);    return renderersList.toArray(new Renderer[0]);  }
buildVideoRenderers buildAudioRenderers buildTextRenderers buildMetadataRenderers buildCameraMotionRenderers buildMiscellaneousRenderers 追蹤VideoRenderer和AudioRenderer的工作流程,對我們理解影片的解碼流程有較大的幫助;
protected void buildVideoRenderers(      Context context,      @ExtensionRendererMode int extensionRendererMode,      MediaCodecSelector mediaCodecSelector,      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,      boolean playClearSamplesWithoutKeys,      boolean enableDecoderFallback,      Handler eventHandler,      VideoRendererEventListener eventListener,      long allowedVideoJoiningTimeMs,      ArrayList<Renderer> out) {    out.add(        new MediaCodecVideoRenderer(            context,            mediaCodecSelector,            allowedVideoJoiningTimeMs,            drmSessionManager,            playClearSamplesWithoutKeys,            enableDecoderFallback,            eventHandler,            eventListener,            MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));    if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {      return;    }    int extensionRendererIndex = out.size();    if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {      extensionRendererIndex--;    }    try {      // Full class names used for constructor args so the LINT rule triggers if any of them move.      // LINT.IfChange      Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer");      Constructor<?> constructor =          clazz.getConstructor(              long.class,              android.os.Handler.class,              com.google.android.exoplayer2.video.VideoRendererEventListener.class,              int.class);      // LINT.ThenChange(../../../../../../../proguard-rules.txt)      Renderer renderer =          (Renderer)              constructor.newInstance(                  allowedVideoJoiningTimeMs,                  eventHandler,                  eventListener,                  MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);      out.add(extensionRendererIndex++, renderer);      Log.i(TAG, "Loaded LibvpxVideoRenderer.");    } catch (ClassNotFoundException e) {      // Expected if the app was built without the extension.    } catch (Exception e) {      // The extension is present, but instantiation failed.      throw new RuntimeException("Error instantiating VP9 extension", e);    }    try {      // Full class names used for constructor args so the LINT rule triggers if any of them move.      // LINT.IfChange      Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.av1.Libgav1VideoRenderer");      Constructor<?> constructor =          clazz.getConstructor(              long.class,              android.os.Handler.class,              com.google.android.exoplayer2.video.VideoRendererEventListener.class,              int.class);      // LINT.ThenChange(../../../../../../../proguard-rules.txt)      Renderer renderer =          (Renderer)              constructor.newInstance(                  allowedVideoJoiningTimeMs,                  eventHandler,                  eventListener,                  MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);      out.add(extensionRendererIndex++, renderer);      Log.i(TAG, "Loaded Libgav1VideoRenderer.");    } catch (ClassNotFoundException e) {      // Expected if the app was built without the extension.    } catch (Exception e) {      // The extension is present, but instantiation failed.      throw new RuntimeException("Error instantiating AV1 extension", e);    }  }

主要是MediaCodecVideoRenderer ,下面都是可擴充套件的軟解碼的Renderer引擎;

protected void buildAudioRenderers(      Context context,      @ExtensionRendererMode int extensionRendererMode,      MediaCodecSelector mediaCodecSelector,      @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,      boolean playClearSamplesWithoutKeys,      boolean enableDecoderFallback,      AudioProcessor[] audioProcessors,      Handler eventHandler,      AudioRendererEventListener eventListener,      ArrayList<Renderer> out) {    out.add(        new MediaCodecAudioRenderer(            context,            mediaCodecSelector,            drmSessionManager,            playClearSamplesWithoutKeys,            enableDecoderFallback,            eventHandler,            eventListener,            new DefaultAudioSink(AudioCapabilities.getCapabilities(context), audioProcessors)));    if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {      return;    }    int extensionRendererIndex = out.size();    if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {      extensionRendererIndex--;    }    try {      // Full class names used for constructor args so the LINT rule triggers if any of them move.      // LINT.IfChange      Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer");      Constructor<?> constructor =          clazz.getConstructor(              android.os.Handler.class,              com.google.android.exoplayer2.audio.AudioRendererEventListener.class,              com.google.android.exoplayer2.audio.AudioProcessor[].class);      // LINT.ThenChange(../../../../../../../proguard-rules.txt)      Renderer renderer =          (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors);      out.add(extensionRendererIndex++, renderer);      Log.i(TAG, "Loaded LibopusAudioRenderer.");    } catch (ClassNotFoundException e) {      // Expected if the app was built without the extension.    } catch (Exception e) {      // The extension is present, but instantiation failed.      throw new RuntimeException("Error instantiating Opus extension", e);    }    try {      // Full class names used for constructor args so the LINT rule triggers if any of them move.      // LINT.IfChange      Class<?> clazz = Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer");      Constructor<?> constructor =          clazz.getConstructor(              android.os.Handler.class,              com.google.android.exoplayer2.audio.AudioRendererEventListener.class,              com.google.android.exoplayer2.audio.AudioProcessor[].class);      // LINT.ThenChange(../../../../../../../proguard-rules.txt)      Renderer renderer =          (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors);      out.add(extensionRendererIndex++, renderer);      Log.i(TAG, "Loaded LibflacAudioRenderer.");    } catch (ClassNotFoundException e) {      // Expected if the app was built without the extension.    } catch (Exception e) {      // The extension is present, but instantiation failed.      throw new RuntimeException("Error instantiating FLAC extension", e);    }    try {      // Full class names used for constructor args so the LINT rule triggers if any of them move.      // LINT.IfChange      Class<?> clazz =          Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer");      Constructor<?> constructor =          clazz.getConstructor(              android.os.Handler.class,              com.google.android.exoplayer2.audio.AudioRendererEventListener.class,              com.google.android.exoplayer2.audio.AudioProcessor[].class);      // LINT.ThenChange(../../../../../../../proguard-rules.txt)      Renderer renderer =          (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors);      out.add(extensionRendererIndex++, renderer);      Log.i(TAG, "Loaded FfmpegAudioRenderer.");    } catch (ClassNotFoundException e) {      // Expected if the app was built without the extension.    } catch (Exception e) {      // The extension is present, but instantiation failed.      throw new RuntimeException("Error instantiating FFmpeg extension", e);    }  }

MediaCodecAudioRenderer是音訊的Renderer引擎; 下面我覺得應該從初始化的地方如何呼叫Renderer來闡述一下這些Renderer是如何工作的?

3.2 Renderer工作機制

Renderer有很多型別 ,我們選擇MediaCodecRenderer來闡述一下Renderer的工作機制;

這兒的流程涉及到很多MediaCodec的知識,我會專門講解一下MediaCodec的解碼流程,這兒我就不展開講了; 有幾個知識點需要關注下:

1.音影片同步如何實現;2.字幕如何解析;3.MediaCodec可以複用嗎?
public interface VideoRendererEventListener {  default void onVideoEnabled(DecoderCounters counters) {}  default void onVideoDecoderInitialized(      String decoderName, long initializedTimestampMs, long initializationDurationMs) {}  default void onVideoInputFormatChanged(Format format) {}  default void onDroppedFrames(int count, long elapsedMs) {}  default void onVideoSizeChanged(      int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {}  default void onRenderedFirstFrame(@Nullable Surface surface) {}  default void onVideoDisabled(DecoderCounters counters) {}}

對我們來說比較重要的是onRenderedFirstFrame 回撥,表明依次渲染過程中surface首次出現數據的情況;

出處:https://juejin.cn/post/6917058201833701384

21
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • Python安裝不用愁,Python安裝教程來了(2021)