ExoPlayer的使用與解析(官方文件翻譯)

耳朵朵發表於2019-04-28
  • 簡介 ExoPlayer是一個Android應用層的媒體播放器,它提供了一套可替換Android MediaPlayer的API,可以播放本地或者是線上的音視訊資源。ExoPlayer支援一些Android MediaPlayer不支援的特性,比如適配DASH和SmoothStreaming 的播放。和MediaPlayer不同的是,ExoPlayer很容易自定義和擴充套件,並且它可以通過應用商店的應用程式更新來直接更新。

    現在在Android裝置上播放視訊和音樂的應用是一個很熱門的應用,Android框架提供的MediaPlayer可以使用很少的程式碼量快速的實現播放音視訊的功能,它也提供了底層的API比如MediaCodec、AudioTrack和MediaDrm,它們同樣可以建立自定義媒體播放器,而ExoPlayer是建立在底層音視訊API之上的開源的應用級媒體播放器。

  • 優缺點 對於Android內建的MediaPlayer來說,ExoPlayer有以下幾個優點:

    1. 支援DASH和SmoothStreaming這兩種資料格式的資源,而MediaPlayer對這兩種資料格式都不支援。它還支援其它格式的資料資源,比如MP4, M4A, FMP4, WebM, MKV, MP3, Ogg, WAV, MPEG-TS, MPEG-PS, FLV and ADTS (AAC)等
    2. 支援高階的HLS特性,比如能正確的處理#EXT-X-DISCONTINUITY標籤
    3. 無縫連線,合併和迴圈播放多媒體的能力
    4. 和應用一起更新播放器(ExoPlayer),因為ExoPlayer是一個整合到應用APK裡面的庫,你可以決定你所想使用的ExoPlayer版本,並且可以隨著應用的更新把ExoPlayer更新到一個最新的版本。
    5. 較少的關於裝置的特殊問題,並且在不同的Android版本和裝置上很少會有不同的表現。
    6. 在Android4.4(API level 19)以及更高的版本上支援Widevine通用加密
    7. 為了符合你的開發需求,播放器支援自定義和擴充套件。其實ExoPlayer為此專門做了設計,並且允許很多元件可以被自定義的實現類替換。
    8. 使用官方的擴充套件功能可以很快的整合一些第三方的庫,比如IMA擴充套件功能通過使用互動媒體廣告SDK可以很容易地將視訊內容貨幣化(變現)

    同樣很重要的是ExoPlayer也有一些不足之處

    1. 比如音訊在Android裝置上的播放,ExoPlayer會比MediaPlayer消耗更多的電量。更多細節請參考Battery consumption page
  • 開始使用 使用ExoPlayer包括以下幾步

    1. 把ExoPlayer作為一個依賴新增到你的專案中去
    2. 建立一個SimPleExoPlayer物件
    3. 給View繫結一個player,這個View是用來播放視訊和使用者互動的
    4. 為播放器準備播放的資源,開始播放
    5. 播放完畢,釋放播放器

    下面將詳細的介紹這些步驟,完整示例請參考main demo app裡面的PlayerActivity

  • 把ExoPlayer作為一個依賴新增到工程中去 新增倉庫 第一步就是確保你在工程根目錄的build.gradle檔案裡新增了Google和JCenter倉庫

     repositories {
         google()
         jcenter()
     }
    複製程式碼

    新增ExoPlayer模組 在你的app module 裡面的build.gradle資料夾裡新增一個ExoPlayer依賴,下面展示了怎麼新增了一個ExoPlayer的全量包依賴

     implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
    複製程式碼

    2.x.x是你選擇的版本

    你也可以只依賴你想要的模組,來代替全量包。比如當你的app想要播放DASH格式的內容的時候,可以只依賴Core,DASH和UI模組的庫。

     implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
     implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X'
     implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X'
    複製程式碼

    下面列舉了可使用的模組庫,新增一個ExoPlayer全量的依賴庫等同於把下面所有的依賴庫都分別新增進去。

    1. exoplayer-core: 核心功能(必須的).
    2. exoplayer-dash: 支援DASH內容.
    3. exoplayer-hls: 支援HLS內容.
    4. exoplayer-smoothstreaming: 支援SmoothStreaming內容.
    5. exoplayer-ui: ExoPlayer所使用的UI元件和資源.

    除了這些模組庫之外,ExoPlayer還有很多可以提供額外功能的依賴於第三方庫的擴充套件模組,可以參考extensions directory瞭解更多資訊

  • 開啟對java 8的支援 如果還沒有設定支援java8,那麼你需要在所有依賴ExoPlayer的build.gradle檔案裡開啟對JAVA8的支援,通過在Android域中新增以下程式碼即可

     compileOptions {
       targetCompatibility JavaVersion.VERSION_1_8
     }
    複製程式碼

    記住如果你想在你的程式碼裡用java8的特性,你需要新增下面額外的設定

     // For Java compilers:
     compileOptions {
       sourceCompatibility JavaVersion.VERSION_1_8
     }
     // For Kotlin compilers:
     kotlinOptions {
       jvmTarget = JavaVersion.VERSION_1_8
     }
    複製程式碼
  • 建立一個播放器 你可以使用ExoPlayerFactory建立一個ExoPlayer物件。為了不同的需求,這個工廠類提供了一系列方法來建立ExoPlayer例項,但是在大多數情況下,使用ExoPlayerFactory.newSimpleInstance方法就可以了。這些方法會返回SimpleExoPlayer型別的物件,它繼承自ExoPlayer,並且新增了一些額外的高階的播放器功能。下面的程式碼展示了怎麼建立一個SimpleExoPlayer物件的

     SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(context);
    複製程式碼

    應用裡面的某個執行緒一定可以訪問ExoPlayer物件,在大多數情況下它一般是應用的主執行緒,並且只有在應用的主執行緒裡才能使用ExoPlayer的UI元件和IMA擴充套件。

    能夠訪問ExoPlayer物件的執行緒可以通過建立播放器例項的時候傳入一個Looper被明確的指定,如果沒有指定Looper,那麼建立player的執行緒的Looper會被使用,或者這個執行緒也沒有Looper,那麼應用的主執行緒的Looper會被使用。在所有的情況下,能夠訪問player的執行緒的Looer能夠通過Player.getApplicationLooper獲取到。

  • 把這個播放器例項附著到一個View上 ExoPlayer庫提供了一個PlayerView,它封裝了一個PlayerControlView和一個能夠渲染視訊的Surface。一個PlayerView可以被加入到應用的佈局檔案中去。可以像這樣把一個player繫結到一個View上

     // Bind the player to the view.
     playerView.setPlayer(player);
    複製程式碼

    如果你需要更加精確的控制播放器和渲染視訊的Surface,你可以使用SimpleExoPlayer的setVideoSurfaceView、setVideoTextureView、setVideoSurfaceHolder和setVideoSurface方法分別的設定播放器的屬性SurfaceView、TextureView、SurfaceHolder和Surface。你還可以把PlayerControlView來當成一個單獨的元件使用或者實現自定義的播放控制類來和播放器進行直接互動。在播放的時候,setTextOutput和setId3Output可以被用來接收字幕和ID3後設資料輸出。

  • 準備播放器資源 在ExoPlayer裡每一種媒體資源都是被MediaSource來代表的。如果想播放一種媒體資源,你首先要為它建立相應的MediaSource物件,然後把這個物件傳遞給ExoPlayer.prepare方法。ExoPlayer庫提供了多種MediaSource的實現類,比如代表DASH資源的DashMediaSource,代表SmoothStreaming資源的SsMediaSource,代表HLS資源的HlsMediaSource和代表一般的多媒體檔案的ExtractorMediaSource。下面的程式碼展示瞭如何為播放MP4檔案的播放器準備適合的MediaSource。

     // Produces DataSource instances through which media data is loaded.
     DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context,
         Util.getUserAgent(context, "yourApplicationName"));
     // This is the MediaSource representing the media to be played.
     MediaSource videoSource = new ExtractorMediaSource.Factory(dataSourceFactory)
         .createMediaSource(mp4VideoUri);
     // Prepare the player with the source.
     player.prepare(videoSource);
    複製程式碼
  • 控制播放器 一旦播放器準備就緒,就可以呼叫player的方法進行播放了,例如呼叫setPlayWhenReady可以開始和暫停播放。不同的seekTo方法可以在媒體資源裡進行搜尋,setRepeatMode控制了多媒體如何迴圈播放,setShuffleModeEnabled控制了播放列表的打亂方式,setPlaybackParameters用來調整播放的速度和音調的。

    如果player和PlayerView或者是PlayerControlView進行繫結了,那麼使用者和這些控制元件的互動將會呼叫player相對應的方法。

  • 監聽播放器事件 改變播放狀態或者是播放錯誤的事件將會被註冊的Player.EventListener物件報告出來,很容易我們就可以註冊一個接收這種事件的監聽。

     // Add a listener to receive events from the player.
     player.addListener(eventListener);
    複製程式碼

    如果你只是對一部分事件感興趣,那麼你可以繼承Player.DefaultEventListener而不是實現Player.EventListener,這樣會讓你只實現你想要的方法。

    當使用SimpleExoPlayer的時候,也可以給player設定一些額外的監聽。比如addVideoListener方法允許你獲取到視訊渲染相關的事件,它可以幫助你調整UI佈局(渲染視訊的Surface的長寬比)。addAnalyticsListener方法允許你或者更加詳細的事件,它有助於你分析一些東西。

  • 釋放播放器 當不在需要播放的時候釋放掉播放器是非常重要的,以便釋放掉有限的資源比如視訊解碼器供其它應用使用。釋放掉播放器可以通過呼叫ExoPlayer.release執行。

  • MediaSource(播放資源) 在ExoPlayer裡每一種媒體資源都是被MediaSource來代表的。ExoPlayer庫提供了多種MediaSource的實現類,比如代表DASH資源的DashMediaSource,代表SmoothStreaming資源的SsMediaSource,代表HLS資源的HlsMediaSource和代表一般的多媒體檔案的ExtractorMediaSource。你可以參考main demo app的PlayerActivity類看一下怎麼例項化這四種MediaSource。

    除了上面所描述的MediaSource實現類之外,ExoPlayer也提供了ConcatenatingMediaSource,ClippingMediaSource,LoopingMediaSource和MergingMediaSource。通過組合這些MediaSource的實現類可以實現更加複雜的播放功能。一些常用的使用功能會在下面描述。需要注意的是下面描述的是以視訊播放為示例的,但是它們同樣適用於音訊的播放,以及任何所支援的媒體型別的播放。

  • Playlists(播放列表) 使用ConcatenatingMediaSource支援Playlists,它可以連續的播放多種MediaSource資源。下面的例子展示了由兩個videos組成的playlists。

     MediaSource firstSource =
         new ExtractorMediaSource.Factory(...).createMediaSource(firstVideoUri);
     MediaSource secondSource =
         new ExtractorMediaSource.Factory(...).createMediaSource(secondVideoUri);
     // Plays the first video, then the second video.
     ConcatenatingMediaSource concatenatedSource =
         new ConcatenatingMediaSource(firstSource, secondSource);
    複製程式碼

    連線資源的轉換是無縫的。這種連線不要求是相同格式的資源(例如可以把包含480P H264視訊檔案和包含720P VP9的視訊檔案很好地連線在一起)。它們甚至可以是不同的型別(比如可以將一個視訊和一個純音訊流很好地連線在一起)。並且在一個連線裡一個型別的MediaSource可以被多次使用。

    在一個ConcatenatingMediaSource裡可以動態地修改播放列表通過新增,刪除和移動MediaSource。可以通過呼叫相應的ConcatenatingMediaSource方法動態修改播放列表在播放視訊之前或者是正在播放的過程中。播放器會正確地自動處理這些修改。例如正在播放的MediaSource被移動了,播放不會中斷並且播放完成後會自動播放它後面的一個MediaSource資源。如果正在播放的MediaSource別刪除了,播放器會自動移動到第一個存在的後繼者去播放,如果沒有後繼者的話,播放器將會轉到結束的狀態。

  • Clipping a video(剪輯視訊) ClippingMediaSource可以被用來剪輯視訊,這樣可以只播放它的一部分。下面的例子展示了怎麼剪輯了一個視訊,從第5s開始播放,第10s結束播放。

     MediaSource videoSource =
         new ExtractorMediaSource.Factory(...).createMediaSource(videoUri);
     // Clip to start at 5 seconds and end at 10 seconds.
     ClippingMediaSource clippingSource =
         new ClippingMediaSource(
             videoSource,
             /* startPositionUs= */ 5_000_000,
             /* endPositionUs= */ 10_000_000);
    複製程式碼

    如果只是從資源的開始進行剪輯,那麼結束的位置可以被設定為C.TIME_END_OF_SOURCE。為了只剪輯到特定的持續時間,有一個建構函式可以接收一個durationUs引數。

    當從一個視訊檔案的開始進行剪輯的時候,如果可能的話,儘量把開始位置和關鍵幀對齊。如果開始位置沒有和關鍵幀對齊,那麼在開始播放之前播放器需要解碼然後丟棄掉前一個關鍵幀到開始位置之間的資料,這樣使 在開始播放的時候,這將會產生一小段延遲,包括當播放器轉到播放作為一個迴圈播放列表的一部分的ClippingMediaSource的時候。

  • Looping a video(視訊迴圈播放) 如果想無限迴圈播放,最好使用ExoPlayer.setRepeatMode而不是LoopingMediaSource。

    使用LoopingMediaSource一個視訊可以被無縫的迴圈一定次數。下面的例子展示了怎麼播放一個視訊兩次

     MediaSource source =
         new ExtractorMediaSource.Factory(...).createMediaSource(videoUri);
     // Plays the video twice.
     LoopingMediaSource loopingSource = new LoopingMediaSource(source, 2);
    複製程式碼
  • Side-loading a subtitle file(側載一個字幕檔案) 給一個視訊檔案和一個分開的字幕檔案,MergingMediaSource可以用來把它們合併成一個單獨的資源來播放。

     // Build the video MediaSource.
     MediaSource videoSource =
         new ExtractorMediaSource.Factory(...).createMediaSource(videoUri);
     // Build the subtitle MediaSource.
     Format subtitleFormat = Format.createTextSampleFormat(
         id, // An identifier for the track. May be null.
         MimeTypes.APPLICATION_SUBRIP, // The mime type. Must be set correctly.
         selectionFlags, // Selection flags for the track.
         language); // The subtitle language. May be null.
     MediaSource subtitleSource =
         new SingleSampleMediaSource.Factory(...)
             .createMediaSource(subtitleUri, subtitleFormat, C.TIME_UNSET);
     // Plays the video with the sideloaded subtitle.
     MergingMediaSource mergedSource =
         new MergingMediaSource(videoSource, subtitleSource);
    複製程式碼
  • 高階組合 為了更多不一般的用例有可能更進一步地合併組合的MediaSource。假如有兩個視訊A和B,下面的例子展示了怎麼一起使用LoopingMediaSource和ConcatenatingMediaSource來播放A、A、B序列。

     MediaSource firstSource =
         new ExtractorMediaSource.Factory(...).createMediaSource(firstVideoUri);
     MediaSource secondSource =
         new ExtractorMediaSource.Factory(...).createMediaSource(secondVideoUri);
     // Plays the first video twice.
     LoopingMediaSource firstSourceTwice = new LoopingMediaSource(firstSource, 2);
     // Plays the first video twice, then the second video.
     ConcatenatingMediaSource concatenatedSource =
         new ConcatenatingMediaSource(firstSourceTwice, secondSource);
    複製程式碼

    下面這個例子也可以實現這個效果,這說明了不止有一種方法來實現相同的效果。

     MediaSource firstSource =
         new ExtractorMediaSource.Builder(firstVideoUri, ...).build();
     MediaSource secondSource =
         new ExtractorMediaSource.Builder(secondVideoUri, ...).build();
     // Plays the first video twice, then the second video.
     ConcatenatingMediaSource concatenatedSource =
         new ConcatenatingMediaSource(firstSource, firstSource, secondSource);
    複製程式碼
  • 軌道選擇 軌道選擇決定了哪一個可用的媒體軌道可以被播放器的渲染器播放。軌道選擇由TrackSelector負責,無論什麼時候建立一個ExoPlayer例項,都要給它提供一個TrackSelector物件。

     DefaultTrackSelector trackSelector = new DefaultTrackSelector();
     SimpleExoPlayer player =
         ExoPlayerFactory.newSimpleInstance(context, trackSelector);
    複製程式碼

    DefaultTrackSelector是一個靈活的TrackSelector,適合更多使用場景。當使用一個DefaultTrackSelector的時候,通過修改它的引數可以控制哪一個tracks被它選擇,這種選擇可以在播放前完成。例如下面的程式碼告訴選擇器將視訊軌道限制為SD,並且如果音訊軌道只有一個就選擇一個德語的音訊軌道。

     trackSelector.setParameters(
         trackSelector
             .buildUponParameters()
             .setMaxVideoSizeSd()
             .setPreferredAudioLanguage("deu"));
    複製程式碼

    這是一個基於約束的軌道選擇的例子,在這個例子中,在不知道實際可用軌道的情況下指定約束。可以使用引數指定許多不同型別的約束。引數還可以用來從可用的軌道中選擇特定的軌道。有關詳細資訊,請參閱DefaultTrackSelectorParameters和ParametersBuilderParametersBuilder文件。

  • 傳送訊息給元件 可以向ExoPlayer元件傳送訊息。這些訊息可以使用createMessage建立,然後使用PlayerMessage.send傳送。預設情況下,訊息會盡快在播放執行緒上傳遞,但是這是可以自定義的通過設定另一個回撥執行緒(使用PlayerMessage.setHandler)或指定一個傳遞訊息的播放位置(使用PlayerMessage.setPosition)。通過ExoPlayer傳送訊息可以確保操作的執行順序與在播放器上執行的任何其他操作一致。

    大多數ExoPlayer的開箱即用渲染器都支援在播放期間更改配置的訊息。例如,音訊渲染器接受訊息來設定音量,而視訊渲染器接受訊息來設定Surface。這些訊息應當在播放執行緒上傳遞,以確保執行緒安全。

  • 自定義 與Android的MediaPlayer相比,ExoPlayer的主要優勢之一是能夠定製和擴充套件播放器,以更好地適應開發人員的用例。ExoPlayer庫是專門為此而設計的,它定義了許多介面和抽象基類,使應用程式開發人員能夠輕鬆替換庫提供的預設實現。下面是一些用於構建自定義元件的用例:

    渲染器——您可能想要實現一個自定義渲染器來處理庫提供的預設實現不支援的媒體型別。

    TrackSelector——實現自定義TrackSelector允許應用程式開發人員更改MediaSource暴露tracks的方式。它會被每個可用的渲染器選擇使用。

    LoadControl—實現自定義LoadControl允許應用程式開發人員更改播放器的緩衝策略。

    Extractor——如果您需要支援目前該庫不支援的容器格式,請考慮實現一個定製的Extractor類,然後可以將其與ExtractorMediaSource一起用於播放該型別的媒體。

    MediaSource——如果您希望以自定義的方式獲取媒體樣本以提供給渲染程式,或者希望實現自定義的MediaSource組合行為,那麼實現自定義的MediaSource類可能是最好的選擇。

    DataSource——ExoPlayer的upstream包已經包含了許多不同用例的DataSource實現。您可能希望實現自己的DataSource,以另一種方式載入資料,例如通過自定義協議、使用自定義HTTP堆疊或從自定義持久快取載入資料。

    在構建自定義元件時,我們建議如下:

    如果自定義元件需要嚮應用程式報告事件,我們建議使用與現有ExoPlayer元件相同的模型,其中事件監聽器和Handler一起傳遞給元件的建構函式。

    我們建議自定義元件使用與現有ExoPlayer元件相同的模型,以允許應用程式在播放期間進行重新配置,如Sending messages to components所說的。為此,您應該實現一個ExoPlayerComponent並在其handleMessage方法中接收配置更改。您的應用程式應該通過呼叫外部播放器的sendMessages和blockingSendMessages方法來傳遞配置更改。

相關文章