音視頻開發之旅(44)-ExoPlayer介紹及簡單使用

目錄

  1. ExoPlayer基本介紹
  2. ExoPlayer的基本使用
  3. 遇到的問題
  4. 資料
  5. 收穫

從這篇開始我們進入階段五 —— 一些音視頻開源項目的學習使用分析,今天我們進入ExoPlayer部分的學習實踐

一、ExoPlayer基本介紹

1.1 ExoPlayer優缺點
ExoPlayer是谷歌開源的一個應用級的音視頻播放器。ExoPlayer 支持基於 HTTP 的動態自適應流 (DASH)、SmoothStreaming 和通用加密、以及可以很好的支持播放隊列、播放源的無縫切換等功能。它採用易於自定義和擴展的設計。
內部的實現也是調用了低層API,比如:MediaCodec、AudioTrack等

畫張表格來對比下ExoPlayer和MediaPlayer,更直觀的瞭解

ExoPlayer的代碼倉庫地址是* https://github.com/google/ExoPlayer*

紅色框框起來的,核心部分加ui的library也是我們這個系列學習使用重點。

1.2 ExoPlayer架構設計
ExoPlayer的核心是ExoPlayer的接口,其中定義了包涵傳統播放器的功能(緩衝音視頻、播放、暫停、seek等)。ExoPlayer沒有設定可以播放的媒體類型、存儲方式以及渲染方式,也沒有直接實現加載和播放。而是在播放器被創建或者準備播放時將這些工作代理給註冊的組件來實現。下面是一些常見ExoPlayer的組件實現:

  1. MediaSource 加載媒體,通過ExoPlayer.prepare註冊
  2. TrackSelector:音/視軌提取器,從MediaSource中提取出軌道的數據
  3. Render:對TrackSelector提取出來的數據進行渲染,AudioTrack播放音頻、Surface渲染視頻
  4. LoadControl:對MediaSource進行控制(什麼時候開始緩衝、緩衝多少等)
    ExoPlayer爲這些組件提供了默認的實現,如果需要定製可以自定義組件來擴展實現。

通過ExoPlayer的架構圖,我們也可以看到其組件模塊化的設計,這個架構設計值得學習,也是好的組件/SDK的一個重要要求。在我們的日常項目開發中,開發一個組件 從易用性和以擴展性方面考慮,既要保證使用者很容易上手使用(提供一套默認實現),又要有方便使用者根據自己的場景進行方便的擴展的能力。

1.3 狀態機
在看ExoPlayer的狀態機之前,我們先看下MeidaPlayer的狀態機

可以看到MediaPlayer的狀態比較多,使用時如果在不當的位置觸發了不匹配的操作,直接回崩潰。
相比MediaPlayer,ExoPlayer的狀態少了些,也更容易使用區分,不像MediaPlayer在沒有prepared之前都不可以進行播放相關操作,ExoPlayer很多listener以及isplaying的API監聽狀態的變化。ExoPlayer的四種狀態如下

 /**
   * 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:初始狀態,此時播放器沒有可以播放的資源,播放器停止播放或者播放失敗後也會處於該狀態
STATE_BUFFERING: 沒有足夠的數據可以加載播放,此時無法立即播放
STATE_READY : 播放器可以立即播放,是否播放取決於playWhenReady的值,該值表達了使用者的意願,爲true,將會開始播放,否則不播。
STATE_ENDED: 播放完了所有的資源後處於改狀態

二、ExoPlayer的簡單使用

這一小節我們學習實踐ExoPlayer的使用

2.1 AS中引入library
ExoPlayer有很好的擴展性和可定製性,可以根據項目需要進行選擇對應的模塊,也可以全部包含。

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.

我們根據需要來添加library

    implementation 'com.google.android.exoplayer:exoplayer-core:2.13.3'
    implementation 'com.google.android.exoplayer:exoplayer-ui: 2.13.3'

接下來出創建一個容器PlayerView以及ExoPlayerView進行播放

2.2 創建播放器、綁定播放器容器、設置數據源、prepare

 //1. 創建播放器
        player = SimpleExoPlayer.Builder(this).build()
        printCurPlaybackState("init")  //  此時處於STATE_IDLE = 1;

        //2. 播放器和播放器容器綁定
        playerView.player = player

        //3. 設置數據源
        //音頻
        val mediaItem = MediaItem.fromUri(" https://storage.googleapis.com/exoplayer-test-media-0/play.mp3")
        player.setMediaItem(mediaItem)
    
    //4.當Player處於STATE_READY狀態時,進行播放
        player.playWhenReady = true

    //5. 調用prepare開始加載準備數據,該方法時異步方法,不會阻塞ui線程
        player.prepare()
        printCurPlaybackState("prepare") //  此時處於 STATE_BUFFERING = 2;

2.3 播放監聽
當前是否在播放中

public final boolean isPlaying() {
    return getPlaybackState() == Player.STATE_READY
        && getPlayWhenReady()
        && getPlaybackSuppressionReason() == PLAYBACK_SUPPRESSION_REASON_NONE;
  }

播放狀態改變的listener、音頻相關的listener、視頻相關的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(會輸出更詳細的信息)

   //通過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前多了一個空格,這個太….,細節有時候不注意就好浪費不少時間。

問題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)

沒有加網絡權限的原因,Mainfest中靜態註冊後,在requesetPermission中動態的請求下。通過這個崩潰堆棧,我們可以看到ExoPlayer加載網絡視頻使用的是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模擬器播放時偶爾會閃退,真機正常。機型設備的適配問題始終是一個大問題

四、資料

  1. Media streaming with ExoPlayer
  2. ExoPlayer blog
  3. ExoPlayer developer guide
  4. ExoPlayer播放音視頻的使用介紹

五、 收穫

通過本次學習實踐收穫如下:

  1. 瞭解ExoPlayer的背景以及相比MediaPlayer的優缺點
  2. 瞭解ExoPlayer的基本功能
  3. 簡單實踐

感謝你的閱讀

下一篇我們繼續學習實踐ExoPlayer,實現一個簡單的音頻播放器,歡迎關注公衆號“音視頻開發之旅”,一起學習成長。

歡迎交流

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章