一、簡介
ExoPlayer是一個Android應用層的媒體播放器,它提供了一套可替換Android MediaPlayer的API,可以播放本地或者是線上的音視頻資源。ExoPlayer支持一些Android MediaPlayer不支持的特性,比如適配DASH和SmoothStreaming的播放。和MediaPlayer不同的是,ExoPlayer很容易自定義和擴展,並且它可以通過應用商店的應用程序更新來直接更新。
現在在Android設備上播放視頻和音樂的應用是一個很熱門的應用,Android框架提供的MediaPlayer可以使用很少的代碼量快速的實現播放音視頻的功能,而且它也提供了底層的API比如MediaCodec、AudioTrack和MediaDrm,它們同樣可以創建自定義媒體播放器,而ExoPlayer是建立在底層音視頻API之上的開源的應用級媒體播放器。
項目地址:https://github.com/google/ExoPlayer
ExoPlayer系列文章:https://medium.com/google-exoplayer
ExoPlayer開發文檔:https://exoplayer.dev/
優點
對於Android內置的MediaPlayer來說,ExoPlayer有以下幾個優點:
- 支持DASH和SmoothStreaming這兩種數據格式的資源,而MediaPlayer對這兩種數據格式都不支持。它還支持其它格式的數據資源,比如MP4, M4A, FMP4, WebM, MKV, MP3, Ogg, WAV, MPEG-TS, MPEG-PS, FLV and ADTS (AAC)等
- 支持高級的HLS特性,比如能正確的處理#EXT-X-DISCONTINUITY標簽
- 無縫連接,合並和循環播放多媒體的能力
- 和應用一起更新播放器(ExoPlayer),因為ExoPlayer是一個集成到應用APK里面的庫,你可以決定你所想使用的ExoPlayer版本,並且可以隨着應用的更新把ExoPlayer更新到一個最新的版本。
- 較少的關於設備的特殊問題,並且在不同的Android版本和設備上很少會有不同的表現。
- 在Android4.4(API level 19)以及更高的版本上支持Widevine通用加密
- 為了符合你的開發需求,播放器支持自定義和擴展。其實ExoPlayer為此專門做了設計,並且允許很多組件可以被自定義的實現類替換。
- 使用官方的擴展功能可以很快的集成一些第三方的庫,比如IMA擴展功能通過使用互動媒體廣告SDK可以很容易地將視頻內容貨幣化(變現)
缺點
- 比如音頻在Android設備上的播放,ExoPlayer會比MediaPlayer消耗更多的電量。更多細節請參考文章:Battery consumption page
二、ExoPlayer 使用
1.把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全量的依賴庫等同於把下面所有的依賴庫都分別添加進去。
- exoplayer-core: 核心功能(必須的).
- exoplayer-dash: 支持DASH內容.
- exoplayer-hls: 支持HLS內容.
- exoplayer-smoothstreaming: 支持SmoothStreaming內容.
- exoplayer-ui: ExoPlayer所使用的UI組件和資源.
除了這些模塊庫之外,ExoPlayer還有很多可以提供額外功能的依賴於第三方庫的擴展模塊,可以參考extensions directory來了解更多信息
2. 打開對 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
}
3. 創建一個播放器
SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(context);
應用里面的某個線程一定可以訪問ExoPlayer對象,在大多數情況下它一般是應用的主線程,並且只有在應用的主線程里才能使用ExoPlayer的UI組件和IMA擴展。
能夠訪問ExoPlayer對象的線程可以通過創建播放器實例的時候傳入一個Looper被明確的指定,如果沒有指定Looper,那么創建player的線程的Looper會被使用,或者這個線程也沒有Looper,那么應用的主線程的Looper會被使用。在所有的情況下,能夠訪問播放器的線程的Looer能夠通過Player.getApplicationLooper獲取到。
4. 把這個播放器實例附着到一個View上
// 綁定播放器到View上
playerView.setPlayer(player);
5. 准備播放器資源
在ExoPlayer里每一種媒體資源都是被MediaSource來代表的。如果想播放一種媒體資源,你首先要為它創建相應的MediaSource對象,然后把這個對象傳遞給ExoPlayer.prepare方法。ExoPlayer庫提供了多種MediaSource的實現類,比如代表DASH資源的DashMediaSource,代表SmoothStreaming資源的SsMediaSource,代表HLS資源的HlsMediaSource和代表一般的多媒體文件的ExtractorMediaSource。下面的代碼展示了如何為播放MP4文件的播放器准備適合的MediaSource。
//創建一個DataSource對象,通過它來下載多媒體數據
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context,
Util.getUserAgent(context, "yourApplicationName"));
//這是一個代表將要被播放的媒體的MediaSource
MediaSource videoSource = new ExtractorMediaSource.Factory(dataSourceFactory)
.createMediaSource(mp4VideoUri);
//使用資源准備播放器
player.prepare(videoSource);
6. 控制播放器
一旦播放器准備就緒,就可以調用player的方法進行播放了,例如調用setPlayWhenReady可以開始和暫停播放。不同的seekTo方法可以在媒體資源里進行搜索,setRepeatMode控制了多媒體如何循環播放,setShuffleModeEnabled控制了是否打亂播放列表,setPlaybackParameters用來調整播放的速度和音調。
如果player和PlayerView或者是PlayerControlView進行綁定了,那么用戶和這些控件的交互將會調用player相對應的方法。
7. 監聽播放器事件
改變播放狀態或者是播放錯誤的事件將會被注冊的Player.EventListener對象接收,很容易我們就可以注冊一個接收這種事件的監聽。
// 添加一個監聽來接收播放器事件.
player.addListener(eventListener);
如果你只是對一部分事件感興趣,那么你可以繼承Player.DefaultEventListener而不是實現Player.EventListener,這樣會讓你只實現你想要的方法。
當使用SimpleExoPlayer的時候,也可以給player設置一些額外的監聽。比如addVideoListener方法允許你獲取到視頻渲染相關的事件,它可以幫助你調整UI布局(渲染視頻的Surface的長寬比)。addAnalyticsListener方法允許你接收更加詳細的事件,它有助於你分析一些東西。
8. 釋放播放器
當不在需要播放的時候釋放掉播放器是非常重要的,以便釋放掉有限的資源比如視頻解碼器供其它應用使用。釋放掉播放器可以通過調用ExoPlayer.release實現。
9. MediaSource(播放資源)
在ExoPlayer里每一種媒體資源都是被MediaSource來代表的。ExoPlayer庫提供了多種MediaSource的實現類,比如代表DASH資源的DashMediaSource,代表SmoothStreaming資源的SsMediaSource,代表HLS資源的HlsMediaSource和代表一般的多媒體文件的ExtractorMediaSource。你可以參考main demo app的PlayerActivity類看一下怎么實例化這四種MediaSource。
除了上面所描述的MediaSource實現類之外,ExoPlayer也提供了ConcatenatingMediaSource,ClippingMediaSource,LoopingMediaSource和MergingMediaSource。通過組合這些MediaSource的實現類可以實現更加復雜的播放功能。一些常用的使用功能會在下面描述。需要注意的是下面描述的是以視頻播放為示例的,但是它們同樣適用於音頻的播放,以及適用任何所支持的媒體類型的播放。
10. Playlists(播放列表)
使用ConcatenatingMediaSource支持播放列表,它可以連續的播放多種MediaSource資源。下面的例子展示了怎么實現由兩個videos組成的playlists。
MediaSource firstSource = new ExtractorMediaSource.Factory(...).createMediaSource(firstVideoUri);
MediaSource secondSource = new ExtractorMediaSource.Factory(...).createMediaSource(secondVideoUri);
//先播放第一個視頻,再播放第二個視頻
ConcatenatingMediaSource concatenatedSource = new ConcatenatingMediaSource(firstSource, secondSource);
連接資源的轉換是無縫的。這種連接不要求是相同格式的資源(例如可以把包含480P H264視頻文件和包含720P VP9的視頻文件很好地連接在一起)。它們甚至可以是不同的類型(比如可以將一個視頻和一個純音頻流很好地連接在一起)。並且在一個連接里一個類型的MediaSource可以被多次使用。
在一個ConcatenatingMediaSource里可以通過添加,刪除和移動MediaSource動態地修改播放列表。同樣在播放視頻之前或者是正在播放的過程中可以通過調用相應的ConcatenatingMediaSource方法動態修改播放列表。播放器會正確地自動處理這些動態修改。例如正在播放的MediaSource被移動了,播放不會中斷並且播放完成后會自動播放它后面的一個MediaSource資源。如果正在播放的MediaSource被刪除了,播放器會自動移動到第一個存在的后繼者去播放,如果沒有后繼者的話,播放器將會轉到結束的狀態。
11. Clipping a video(剪輯視頻)
ClippingMediaSource可以被用來剪輯視頻,這樣可以只播放它的一部分。下面的例子展示了怎么剪輯了一個視頻,從第5s開始播放,到第10s結束播放。
MediaSource videoSource = new ExtractorMediaSource.Factory(...).createMediaSource(videoUri);
// 從第5s開始剪輯到第10s
ClippingMediaSource clippingSource = new ClippingMediaSource(videoSource, /* startPositionUs= */ 5_000_000, /* endPositionUs= */ 10_000_000);
如果只是從資源的開始進行剪輯,那么結束的位置可以被設置為C.TIME_END_OF_SOURCE。為了只剪輯到特定的持續時間,有一個構造函數可以接收一個durationUs參數。
當從一個視頻文件的開始進行剪輯的時候,如果可能的話,盡量把開始位置和關鍵幀對齊。如果開始位置沒有和關鍵幀對齊,那么在開始播放之前播放器需要解碼然后丟棄掉前一個關鍵幀到開始位置之間的數據,這樣使 在開始播放的時候,這將會產生一小段延遲,包括當播放器將ClippingMediaSource作為播放列表的一部分播放或循環播放時。
12. Looping a video(視頻循環播放)
如果想無限循環播放,最好使用ExoPlayer.setRepeatMode而不是LoopingMediaSource。
使用LoopingMediaSource一個視頻可以被無縫的循環一定次數。下面的例子展示了怎么播放一個視頻兩次:
MediaSource source = new ExtractorMediaSource.Factory(...).createMediaSource(videoUri);
// Plays the video twice.
LoopingMediaSource loopingSource = new LoopingMediaSource(source, 2);
13. Side-loading a subtitle file(側載一個字幕文件)
給一個視頻文件和一個分開的字幕文件,MergingMediaSource可以用來把它們合並成一個單獨的資源來播放。
// 創建一個視頻的 MediaSource.
MediaSource videoSource = new ExtractorMediaSource.Factory(...).createMediaSource(videoUri);
// 創建一個字幕的 MediaSource.
Format subtitleFormat = Format.createTextSampleFormat(id, // 一個軌道的標志,可以為空
MimeTypes.APPLICATION_SUBRIP, // The mime type. Must be set correctly.
selectionFlags, // 軌道的選擇標志
language); // 字幕的語言,可以為空
MediaSource subtitleSource = new SingleSampleMediaSource.Factory(...) .createMediaSource(subtitleUri, subtitleFormat, C.TIME_UNSET);
// 播放帶有字幕的視頻
MergingMediaSource mergedSource = new MergingMediaSource(videoSource, subtitleSource);
14. 高級組合
為了更多高級的功能有可能更進一步地合並組合的MediaSource。假如有兩個視頻A和B,下面的例子展示了怎么一起使用LoopingMediaSource和ConcatenatingMediaSource來播放A、A、B序列。
MediaSource firstSource = new ExtractorMediaSource.Factory(...).createMediaSource(firstVideoUri);
MediaSource secondSource = new ExtractorMediaSource.Factory(...).createMediaSource(secondVideoUri);
// 播放第一個視頻兩次
LoopingMediaSource firstSourceTwice = new LoopingMediaSource(firstSource, 2);
// 播放第一個視頻兩次,然后再播放第二個視頻
ConcatenatingMediaSource concatenatedSource = new ConcatenatingMediaSource(firstSourceTwice, secondSource);
下面這個例子也可以實現這個效果,這說明了不止有一種方法來實現相同的效果。
MediaSource firstSource = new ExtractorMediaSource.Builder(firstVideoUri, ...).build();
MediaSource secondSource = new ExtractorMediaSource.Builder(secondVideoUri, ...).build();
// 播放第一個視頻兩次,然后再播放第二個視頻
ConcatenatingMediaSource concatenatedSource = new ConcatenatingMediaSource(firstSource, firstSource, secondSource);
15. 軌道選擇
軌道選擇決定了哪一個可用的媒體軌道可以被播放器的渲染器播放。軌道選擇由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"));
這是一個基於約束的軌道選擇的例子,在這個例子中,在不知道實際可用軌道的情況下指定約束。可以使用參數指定許多不同類型的約束。參數還可以用來從可用的軌道中選擇特定的軌道。有關詳細信息,請參閱DefaultTrackSelector、Parameters和ParametersBuilderParametersBuilder文檔。
16. 發送消息給組件
可以向ExoPlayer組件發送消息。這些消息可以使用createMessage創建,然后使用PlayerMessage.send發送。默認情況下,消息會盡快在播放線程上傳遞,但是這是可以自定義的通過設置另一個回調線程(使用PlayerMessage.setHandler)或指定一個傳遞消息的播放位置(使用PlayerMessage.setPosition)。通過ExoPlayer發送消息可以確保操作的執行順序與在播放器上執行的任何其他操作一致。
大多數ExoPlayer的開箱即用渲染器都支持在播放期間更改配置的消息。例如,音頻渲染器接受消息來設置音量,而視頻渲染器接受消息來設置Surface。這些消息應當在播放線程上傳遞,以確保線程安全。
17.自定義播放器
與Android的MediaPlayer相比,ExoPlayer的主要優勢之一是能夠自定義和擴展播放器,以更好地適應開發人員的用例。ExoPlayer庫是專門為此而設計的,它定義了許多接口和抽象基類,可以使應用程序開發人員能夠輕松替換庫提供的默認實現。下面是一些用於構建自定義組件的用例:
Renderer——您可能想要實現一個自定義Renderer來處理庫默認不支持的媒體類型。
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方法來傳遞配置更改。