目錄
- ExoPlayer基本介紹
- ExoPlayer的基本使用
- 遇到的問題
- 資料
- 收獲
從這篇開始我們進(jìn)入階段五 —— 一些音視頻開源項目的學(xué)習(xí)使用分析栗恩,今天我們進(jìn)入ExoPlayer部分的學(xué)習(xí)實踐
一、ExoPlayer基本介紹
1.1 ExoPlayer優(yōu)缺點
ExoPlayer是谷歌開源的一個應(yīng)用級的音視頻播放器洪燥。ExoPlayer 支持基于 HTTP 的動態(tài)自適應(yīng)流 (DASH)磕秤、SmoothStreaming 和通用加密乳乌、以及可以很好的支持播放隊列、播放源的無縫切換等功能市咆。它采用易于自定義和擴展的設(shè)計汉操。
內(nèi)部的實現(xiàn)也是調(diào)用了低層API,比如:MediaCodec蒙兰、AudioTrack等
畫張表格來對比下ExoPlayer和MediaPlayer磷瘤,更直觀的了解
ExoPlayer的代碼倉庫地址是* https://github.com/google/ExoPlayer*
紅色框框起來的,核心部分加ui的library也是我們這個系列學(xué)習(xí)使用重點癞己。
1.2 ExoPlayer架構(gòu)設(shè)計
ExoPlayer的核心是ExoPlayer的接口,其中定義了包涵傳統(tǒng)播放器的功能(緩沖音視頻梭伐、播放痹雅、暫停、seek等)糊识。ExoPlayer沒有設(shè)定可以播放的媒體類型绩社、存儲方式以及渲染方式,也沒有直接實現(xiàn)加載和播放赂苗。而是在播放器被創(chuàng)建或者準(zhǔn)備播放時將這些工作代理給注冊的組件來實現(xiàn)愉耙。下面是一些常見ExoPlayer的組件實現(xiàn):
- MediaSource 加載媒體,通過ExoPlayer.prepare注冊
- TrackSelector:音/視軌提取器拌滋,從MediaSource中提取出軌道的數(shù)據(jù)
- Render:對TrackSelector提取出來的數(shù)據(jù)進(jìn)行渲染朴沿,AudioTrack播放音頻、Surface渲染視頻
- LoadControl:對MediaSource進(jìn)行控制(什么時候開始緩沖败砂、緩沖多少等)
ExoPlayer為這些組件提供了默認(rèn)的實現(xiàn)赌渣,如果需要定制可以自定義組件來擴展實現(xiàn)。
通過ExoPlayer的架構(gòu)圖昌犹,我們也可以看到其組件模塊化的設(shè)計坚芜,這個架構(gòu)設(shè)計值得學(xué)習(xí),也是好的組件/SDK的一個重要要求斜姥。在我們的日常項目開發(fā)中鸿竖,開發(fā)一個組件 從易用性和以擴展性方面考慮,既要保證使用者很容易上手使用(提供一套默認(rèn)實現(xiàn))铸敏,又要有方便使用者根據(jù)自己的場景進(jìn)行方便的擴展的能力缚忧。
1.3 狀態(tài)機
在看ExoPlayer的狀態(tài)機之前,我們先看下MeidaPlayer的狀態(tài)機
可以看到MediaPlayer的狀態(tài)比較多杈笔,使用時如果在不當(dāng)?shù)奈恢糜|發(fā)了不匹配的操作搔谴,直接回崩潰。
相比MediaPlayer桩撮,ExoPlayer的狀態(tài)少了些敦第,也更容易使用區(qū)分峰弹,不像MediaPlayer在沒有prepared之前都不可以進(jìn)行播放相關(guān)操作,ExoPlayer很多l(xiāng)istener以及isplaying的API監(jiān)聽狀態(tài)的變化芜果。ExoPlayer的四種狀態(tài)如下
/**
* Playback state. One of {@link #STATE_IDLE}, {@link #STATE_BUFFERING}, {@link #STATE_READY} or
* {@link #STATE_ENDED}.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED})
@interface State {}
/** The player does not have any media to play. */
int STATE_IDLE = 1;
/**
* The player is not able to immediately play from its current position. This state typically
* occurs when more data needs to be loaded.
*/
int STATE_BUFFERING = 2;
/**
* The player is able to immediately play from its current position. The player will be playing if
* {@link #getPlayWhenReady()} is true, and paused otherwise.
*/
int STATE_READY = 3;
/** The player has finished playing the media. */
int STATE_ENDED = 4;
STATE_IDLE:初始狀態(tài)鞠呈,此時播放器沒有可以播放的資源,播放器停止播放或者播放失敗后也會處于該狀態(tài)
STATE_BUFFERING: 沒有足夠的數(shù)據(jù)可以加載播放右钾,此時無法立即播放
STATE_READY : 播放器可以立即播放蚁吝,是否播放取決于playWhenReady的值,該值表達(dá)了使用者的意愿舀射,為true窘茁,將會開始播放,否則不播脆烟。
STATE_ENDED: 播放完了所有的資源后處于改狀態(tài)
二山林、ExoPlayer的簡單使用
這一小節(jié)我們學(xué)習(xí)實踐ExoPlayer的使用
2.1 AS中引入library
ExoPlayer有很好的擴展性和可定制性,可以根據(jù)項目需要進(jìn)行選擇對應(yīng)的模塊邢羔,也可以全部包含驼抹。
exoplayer-core: Core functionality (required).
exoplayer-dash: Support for DASH content.
exoplayer-hls: Support for HLS content.
exoplayer-smoothstreaming: Support for SmoothStreaming content.
exoplayer-ui: UI components and resources for use with ExoPlayer.
我們根據(jù)需要來添加library
implementation 'com.google.android.exoplayer:exoplayer-core:2.13.3'
implementation 'com.google.android.exoplayer:exoplayer-ui: 2.13.3'
接下來出創(chuàng)建一個容器PlayerView以及ExoPlayerView進(jìn)行播放
2.2 創(chuàng)建播放器、綁定播放器容器拜鹤、設(shè)置數(shù)據(jù)源框冀、prepare
//1. 創(chuàng)建播放器
player = SimpleExoPlayer.Builder(this).build()
printCurPlaybackState("init") // 此時處于STATE_IDLE = 1;
//2. 播放器和播放器容器綁定
playerView.player = player
//3. 設(shè)置數(shù)據(jù)源
//音頻
val mediaItem = MediaItem.fromUri(" https://storage.googleapis.com/exoplayer-test-media-0/play.mp3")
player.setMediaItem(mediaItem)
//4.當(dāng)Player處于STATE_READY狀態(tài)時,進(jìn)行播放
player.playWhenReady = true
//5. 調(diào)用prepare開始加載準(zhǔn)備數(shù)據(jù)敏簿,該方法時異步方法明也,不會阻塞ui線程
player.prepare()
printCurPlaybackState("prepare") // 此時處于 STATE_BUFFERING = 2;
2.3 播放監(jiān)聽
當(dāng)前是否在播放中
public final boolean isPlaying() {
return getPlaybackState() == Player.STATE_READY
&& getPlayWhenReady()
&& getPlaybackSuppressionReason() == PLAYBACK_SUPPRESSION_REASON_NONE;
}
播放狀態(tài)改變的listener、音頻相關(guān)的listener惯裕、視頻相關(guān)的listener
playbackListener = PlaybackListener()
player.addListener(playbackListener)
player.addAudioListener(playbackListener)
player.addVideoListener(playbackListener)
class PlaybackListener : Player.EventListener, AudioListener, VideoListener {
override fun onPlaybackStateChanged(playbackState: Int) {
val stateString: String
stateString = when (playbackState) {
ExoPlayer.STATE_IDLE -> "ExoPlayer.STATE_IDLE -"
ExoPlayer.STATE_BUFFERING -> "ExoPlayer.STATE_BUFFERING -"
ExoPlayer.STATE_READY -> "ExoPlayer.STATE_READY -"
ExoPlayer.STATE_ENDED -> "ExoPlayer.STATE_ENDED -" //播放列表存在時播放最后一個播放完成才會回掉該方法
else -> "UNKNOWN_STATE -"
}
Log.d("ExoBaseUserActivity", "changed state to $stateString")
}
override fun onAudioSessionIdChanged(audioSessionId: Int) {
Log.d("ExoBaseUserActivity", "onAudioSessionIdChanged--sessionId=" + audioSessionId)
}
override fun onAudioAttributesChanged(audioAttributes: AudioAttributes) {
Log.d("ExoBaseUserActivity", "onAudioAttributesChanged--audioAttributes=" + audioAttributes.toString())
}
override fun onVolumeChanged(volume: Float) {
Log.d("ExoBaseUserActivity", "onVolumeChanged--volume=" + volume)
}
override fun onSkipSilenceEnabledChanged(skipSilenceEnabled: Boolean) {
Log.d("ExoBaseUserActivity", "onSkipSilenceEnabledChanged--skipSilenceEnabled=" + skipSilenceEnabled)
}
override fun onVideoSizeChanged(width: Int, height: Int, unappliedRotationDegrees: Int, pixelWidthHeightRatio: Float) {
Log.d("ExoBaseUserActivity", "onVideoSizeChanged--width=" + width + " height=" + height + " unappliedRotationDegrees=" + unappliedRotationDegrees + " pixelWidthHeightRatio=" + pixelWidthHeightRatio)
}
override fun onSurfaceSizeChanged(width: Int, height: Int) {
Log.d("ExoBaseUserActivity", "onSurfaceSizeChanged--width=" + width + " height=" + height)
}
override fun onRenderedFirstFrame() {
Log.d("ExoBaseUserActivity", "onRenderedFirstFrame")
}
}
用于分析用的listener(會輸出更詳細(xì)的信息)
//通過AnalyticsListener可以輸出更多信息
analyticsListener = EventLogger(DefaultTrackSelector())
player.addAnalyticsListener(analyticsListener)
2.4 釋放資源
在頁面不可見/銷毀(看是否需要后臺播放)時要釋放資源
override fun onDestroy() {
super.onDestroy()
player.removeAnalyticsListener(analyticsListener)
player.removeListener(playbackListener)
player.removeAudioListener(playbackListener)
player.removeVideoListener(playbackListener)
player.release()
}
完整代碼已上傳至 github https://github.com/ayyb1988/mediajourney
三诡右、遇到的問題
問題1
Failed to resolve: com.google.android.exoplayer:exoplayer: 2.13.3
2.13.3前多了一個空格,這個太….轻猖,細(xì)節(jié)有時候不注意就好浪費不少時間帆吻。
問題2
java.lang.SecurityException: Permission denied (missing INTERNET permission?)
at java.net.Inet6AddressImpl.lookupHostByName(Inet6AddressImpl.java:150)
at java.net.Inet6AddressImpl.lookupAllHostAddr(Inet6AddressImpl.java:103)
at java.net.InetAddress.getAllByName(InetAddress.java:1152)
at com.android.okhttp.Dns$1.lookup(Dns.java:41)
at com.android.okhttp.internal.http.RouteSelector.resetNextInetSocketAddress(RouteSelector.java:178)
at com.android.okhttp.internal.http.RouteSelector.nextProxy(RouteSelector.java:144)
at com.android.okhttp.internal.http.RouteSelector.next(RouteSelector.java:86)
at com.android.okhttp.internal.http.StreamAllocation.findConnection(StreamAllocation.java:176)
at com.android.okhttp.internal.http.StreamAllocation.findHealthyConnection(StreamAllocation.java:128)
at com.android.okhttp.internal.http.StreamAllocation.newStream(StreamAllocation.java:97)
at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:289)
at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:232)
at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:465)
at com.android.okhttp.internal.huc.HttpURLConnectionImpl.connect(HttpURLConnectionImpl.java:131)
at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.connect(DelegatingHttpsURLConnection.java:90)
at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:30)
at com.google.android.exoplayer2.upstream.DefaultHttpDataSource.makeConnection(DefaultHttpDataSource.java:641)
at com.google.android.exoplayer2.upstream.DefaultHttpDataSource.makeConnection(DefaultHttpDataSource.java:528)
at com.google.android.exoplayer2.upstream.DefaultHttpDataSource.open(DefaultHttpDataSource.java:349)
at com.google.android.exoplayer2.upstream.DefaultDataSource.open(DefaultDataSource.java:201)
at com.google.android.exoplayer2.upstream.StatsDataSource.open(StatsDataSource.java:84)
at com.google.android.exoplayer2.source.ProgressiveMediaPeriod$ExtractingLoadable.load(ProgressiveMediaPeriod.java:1015)
at com.google.android.exoplayer2.upstream.Loader$LoadTask.run(Loader.java:415)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
沒有加網(wǎng)絡(luò)權(quán)限的原因,Mainfest中靜態(tài)注冊后咙边,在requesetPermission中動態(tài)的請求下猜煮。通過這個崩潰堆棧,我們可以看到ExoPlayer加載網(wǎng)絡(luò)視頻使用的是Okhttp
問題3
2021-05-15 18:41:17.414 11144-11144/? I/av.mediajourne: Not late-enabling -Xcheck:jni (already on)
2021-05-15 18:41:17.487 11144-11144/? E/av.mediajourne: Unknown bits set in runtime_flags: 0x8000
2021-05-15 18:41:17.489 11144-11144/? W/av.mediajourne: Unexpected CPU variant for X86 using defaults: x86
X86模擬器播放時偶爾會閃退败许,真機正常王带。機型設(shè)備的適配問題始終是一個大問題
四、資料
五市殷、 收獲
通過本次學(xué)習(xí)實踐收獲如下:
- 了解ExoPlayer的背景以及相比MediaPlayer的優(yōu)缺點
- 了解ExoPlayer的基本功能
- 簡單實踐
感謝你的閱讀
下一篇我們繼續(xù)學(xué)習(xí)實踐ExoPlayer愕撰,實現(xiàn)一個簡單的音頻播放器,歡迎關(guān)注公眾號“音視頻開發(fā)之旅”,一起學(xué)習(xí)成長搞挣。
歡迎交流