應用程式基礎

使用 Jetpack Media3 API 最佳化媒體播放體驗,並盡量提高觸及率

喇叭

關於

講座簡介

使用者在行動裝置上花費的時間,有很大一部分用於媒體消費,因此播放體驗必須完美無瑕、高效率,且不耗費電池續航力。此外,使用者也希望這些體驗能在手機、客廳電視等裝置之間流暢切換。

瞭解 Media3 和 ExoPlayer 的架構更新如何讓各種板型規格的裝置都能流暢播放內容。討論重點包括使用 Media3 Compose UI 工具包打造回應式介面、運用全新 PreloadManager 消除動態消息中的緩衝,以及透過 CastPlayer 和空間音訊,將應用程式延伸至客廳螢幕和 Android XR。

摘要

  • Media3 UI Compose 提供現成的播放器可組合項和可自訂的建構區塊。您可以覆寫特定控制項插槽,同時讓程式庫管理播放器狀態和轉換。
  • 全新改寫的 CastPlayer 將取代獨立的 Cast SDK。它會實作標準播放器介面,並自動在本機 ExoPlayer 執行個體和遠端 Cast 裝置之間委派播放作業。
  • 重建的編輯管道與 OpenGL 分離,並直接管理硬體緩衝區。這種零複製做法可降低記憶體負擔,而解耦則可讓開發人員整合 WebGPU 等現代圖形架構。
  • Media3 在 Android 17 以上版本中,新增對 Eclipsa Video 和 Eclipsa Audio 的現成支援。規範性 HDR 中繼資料會根據環境光線調整高亮度,而空間音訊則會處理多種混合呈現方式。
  • 內建最佳化功能 (例如 PCM 音訊卸載) 可將螢幕關閉時的耗電量降低多達 20%。即將推出的 CodecDB 會使用基準指標,選取最適合裝置的編碼器設定。

資源

Gemini Notebook

主題

  • jetpack-media3
  • exoplayer
  • jetpack-compose
  • google-cast
  • 影片編輯
  • 廣播
  • performance

轉錄稿

Jetpack Media3 簡介

  • 好的。

  • 大家好,歡迎各位的加入。

  • 我是 Toni。

  • 我是 Android 媒體團隊的工程師。

  • 接下來,我和 Kristina、Donovan 將一起介紹 Jetpack Media3 如何協助您滿足 Android 上的所有媒體需求。

  • Media3 是 Android 的支援程式庫,適用於所有媒體相關功能,無論是使用 ExoPlayer 播放、透過媒體工作階段和 Cast 分享媒體、使用 Transformer 編輯媒體,或檢查媒體 (例如擷取影格或中繼資料),都能派上用場。

  • Media3 則在單一程式庫群組中涵蓋整個空間。

  • 這個程式庫會做為支援程式庫,與應用程式一併提供。

  • 這表示我們能確保一致性,讓相同的程式碼在所有 OS 版本和裝置上執行。

  • 此外,這也有助於更新。

  • 我們修正的每項錯誤都會套用至 API 24,且不與任何 OS 版本週期綁定。

  • 此外,由於這是支援資料庫,因此我們能提供非常豐富的自訂選項,這在 Android 架構中是無法做到的。

  • 此外,我們在 GitHub 上提供程式庫,因此開放原始碼是我們的首要考量。

  • 也就是說,我們所做的每項變更都會立即在 GitHub 上生效。

  • 我們有活躍的 Issue Tracker,也接受提取要求。

  • 在所有這些用途中,我們最重視的是各個層級的細節,從高階 UI 元素到簡單的現成 API,以及深度自訂選項、對各種格式和標準的廣泛支援,以及支撐這一切的效能和效率。

  • Media3 涵蓋整個空間。

  • 今天,我們將深入探討其中幾項主題。

  • 我們將介紹全新的 Compose UI 模組、可更輕鬆與媒體工作階段共用的新 CastPlayer、具備全新進階效果的改版編輯管道、可擷取中繼資料和影格的新工具、一些新的串流技術 (特別是 Eclipsa 影片和音訊),以及多項可預先載入、拖曳和節約耗電量的體驗品質公用程式。

使用 Jetpack Compose 建立媒體 UI

  • 現在,我們直接進入第一個主題,瞭解 Media3 如何協助建構媒體 UI 元素。

  • 我們最近發布了兩個新模組:Media3 UI Compose 和 Media3 UI Compose Material 3。

  • 您可以在這裡看到高階播放器可組合函式,因為它是在 Media3 1.10 中推出,未經進一步修改。

    @Composable
    fun videoScreen() {
      Player(viewModel.exoPlayer)
    }
  • 而且可立即使用。

  • 只需要 Media3 播放器。

  • 在本例中,這是一個 ExoPlayer,通常會出現在檢視模型中。

  • 但也可以是其他項目,例如 CastPlayer、媒體控制器,或應用程式中的任何其他播放器。

  • 值得注意的是,這裡的 ExoPlayer 是可靠資料來源。

  • 其中包含所有資訊,而播放器可組合函式會負責轉譯 UI 並連結所有內容。

  • 但如果想自訂這個 UI、新按鈕、不同位置、不同樣式、文字疊加層,該怎麼做?

  • 播放器可組合項是由建構區塊組成。

  • 你可以更換。

  • 您可以自訂每個個別區塊,也可以在應用程式的其他地方單獨使用。

  • 舉例來說,您可以分別自訂頂端、中心和bottomControls。

  • 您也可以自訂快門的外觀 (在影片未播放時顯示),或錯誤重疊。

  • 但這只是第一層。

  • 預設中心和bottomControls也有更多可自訂的詳細版位。

  • 舉例來說,您可以更換中央按鈕,同時保留其他所有內容,或是在滑桿上方或下方新增元素。

  • Media3 Compose 提供所有必要建構區塊,可協助您完成這項作業。

  • 舉例來說,我們有許多按鈕,包括播放/暫停按鈕、重複播放模式、隨機播放模式、靜音按鈕等。

  • 接著是進度指標,例如畫面上顯示的滑桿和位置文字。

  • 此外,還有多個容器和疊加層。

  • 我展示的高階播放器可組合項就是一個例子,但還有更多例子,例如錯誤疊加層或字幕等。

  • 此外,我們還有其他實用元件,例如這個迷你控制器。

  • 您可以在使用者瀏覽應用程式時,將廣告放在底部功能表中。

  • 我們不會就此停下腳步。

  • 我們正積極擴充這個程式庫,例如即將推出剪輯滑桿等新編輯控制項。

  • 現在來看看這項自訂功能,瞭解如何使用這些構成元素。

    Player(viewModel.exoPlayer)
  • 假設我們想將目前位於滑桿旁邊的位置和時間長度文字,移到滑桿上方。

    Player(
      player = viewModel.exoPlayer,
      bottomControls = {
        PlayerDefaults.BottomControls(
          player,
          left = {}, right = {},
          above = {
            PositionAndDurationText(
              player, separator = " • ")
          },
        )
      },
    )
  • 因此,我們可以覆寫這些 bottomControls。

  • 我們仍可使用預設元件,但將左右兩側的位置設為空白,讓它們消失。

  • 我們改為將職位和任職時間文字放在上方的版位,並使用自訂分隔符,讓版面看起來更有趣。

  • 現在,假設我們想完全移除 centerControls 疊加層,改為在滑桿上方放置播放按鈕,並附帶速度控制和靜音按鈕。

    Player(
      player = viewModel.exoPlayer,
      centerControls = {},
      bottomControls = { ...
          above = {
            Row(Modifier.fillMaxWidth()) {
              PositionAndDurationText(...)
              Spacer(Modifier.weight(1f))
              PlaybackSpeedToggleButton(player)
              MuteButton(player)
              PlayPauseButton(player)
            }
          },
        )
      }
  • 這與上述概念完全相同。

  • 我們可以將 centerControls 設為空白,讓這些按鈕消失,並在滑桿上方的版位中加入一些額外空間和要顯示的三個按鈕。

  • 請注意,這些按鈕來自程式庫本身,因此您不必在其中加入任何額外程式碼。

  • 這樣就完成了。

  • 您也可以繼續使用這種方式設定這些元素的樣式。

  • 舉例來說,如果您想變更靜音按鈕圖示,但保留其功能,也可以採用相同方式。

    Player(
      ...
      bottomControls = { ...
          above = {
              ...
              MuteButton(player,
                painter = { painterResource(
                  if (showMuted)
                    R.drawable.muted
                  else
                    R.drawable.unmuted )})
          }
      },
    )
  • 如畫面所示,你可以為按鈕定義自訂繪圖器,並自行選擇圖示。

  • 這個元件會提供一些中繼資料,因此您只需要查看狀態 (例如本例中的 showMuted),即可挑選合適的圖示。

  • 這正是 Media3 Compose 的主要優點。

  • 您可以專注於應用程式的外觀和風格,其餘部分交由 Media3 處理。

  • 它會讀取播放器的狀態、與播放器互動,並處理所有轉換和動畫。

使用 MediaSession 分享播放

  • 接下來,我們從 UI 轉到分享媒體。

  • 您可以與同一部裝置上的其他應用程式,或與連結至遠端裝置的應用程式分享播放狀態。

  • 發布持續播放內容的主要方式是使用 MediaSession。

  • 可讓其他人查看播放狀態和中繼資料,並控制播放。

  • Gemini、系統 UI 媒體通知、Cast、Wear OS 或 Android Auto 隨附應用程式,都是透過這種方式與您的應用程式通訊。

  • MediaSession 會包裝 Media3 播放器,只要有播放器,就能輕鬆建立。

    MediaSession.Builder(context, player).build()
  • 同樣地,這可以是 ExoPlayer 或應用程式中的任何其他播放器,就像 Compose UI 一樣。

  • 如要在背景播放,通常會將其託管在 MediaSessionService 或 MediaLibraryService 中,這兩者也會自動處理所有生命週期和通知等事項。

    class PlaybackService : MediaSessionService() {
    
      private lateinit var mediaSession:
            MediaSession
    
      override fun onGetSession(
            info: ControllerInfo) : MediaSession?
        = mediaSession
    }
  • 不過,如果您有媒體應用程式,可能已經知道這些資訊。

  • 我想向各位介紹一項令人期待的新功能,那就是全新改寫的 CastPlayer。

  • Google Cast 可將本機播放內容轉移到遠端裝置,例如 Android TV、Chromecast 或智慧音箱。

  • 我們過去曾發布獨立的 Cast SDK,但現在已進入維護模式。

  • Media3 Cast 則可完全取代,因為它提供更簡單、更流暢的整合方式。

  • 這裡最重要的元素是 CastPlayer,它會實作與 ExoPlayer 相同的標準播放器介面。

  • ExoPlayer 適用於本機播放,CastPlayer 則是委派執行個體,可透過 ExoPlayer 在本機播放,或透過 Cast 通訊協定從遠端播放。

  • 並處理所有相關事宜。

  • 它會監聽輸出裝置的變更、建立連線、同步處理狀態,並在需要時轉移播放。

  • 因此,將 CastPlayer 整合到應用程式中非常簡單。

    val player = ExoPlayer.Builder(context).build()
    
    mediaSession = MediaSession.Builder(context,player)
    .build()
  • 您不必建立 ExoPlayer 並與 MediaSession 共用,只要改用 CastPlayer 即可。

    Cast.getSingletonInstance(context).initialize()
    
    val player = CastPlayer.Builder(context).build()
    
    mediaSession = MediaSession.Builder(context,player)
    .build()
  • 您只需要在應用程式中使用 Cast 系統前,先初始化該系統。

  • 請注意,這個 CastPlayer 會在內部建構 ExoPlayer 進行本機播放,因此如要進一步自訂本機播放器,您也可以建構自己的 ExoPlayer,並將其做為本機播放器傳遞至這個 CastPlayer。

    Cast.getSingletonInstance(context).initialize()
    val exoPlayer = ExoPlayer.Builder(context).build()
    val player = CastPlayer.Builder(context)
                    .setLocalPlayer(exoPlayer).build()
    mediaSession = MediaSession.Builder(context,player)
    .build()
  • 這同樣是 Media3 的原則。

  • 我們提供簡單、立即可用的 API 建構模塊,以及所有必要的自訂選項,方便您進一步自訂。

  • 您可能會想知道,使用者在這種設定下,如何切換本機和遠端播放。

  • 媒體通知中的系統輸出切換器對話方塊,可立即使用。

  • 就是角落的小按鈕。

  • 但如果想在播放畫面中顯示投放按鈕,讓使用者更容易找到,該怎麼做?

  • 還記得我們幾分鐘前建立的這個自訂 Compose UI 嗎?

    Player(
      exoPlayer,
      centerControls = {}
      bottomControls = { /* customized */ },
    )
  • Media3 Cast 也提供現成按鈕,可供您放置在該處。

  • 舉例來說,只要將現成的 MediaRouteButton 新增至 topControls 即可,不需要設定任何其他參數,就是這麼簡單。

    Player(
      castPlayer,
      centerControls = {}
      bottomControls = { /* customized */ },
      topControls = {
        PlayerDefaults.TopControls(castPlayer) {
          MediaRouteButton(
            Modifier.align(Alignment.TopEnd))
        }
      },
    )
  • 點選這個按鈕後,系統會開啟相同的系統輸出切換器對話方塊,然後 CastPlayer 會為您轉移播放作業。

  • 您也會注意到,我將 CastPlayer 連線至 UI,確保遠端發生的任何事件都會反映在本機 UI 中。

  • 接著,我將交給 Kristina 介紹編輯用途。

Media3 編輯 API

  • 感謝您!

  • 大家好,現在來介紹編輯和進階特效。

  • Media3 API 包含多個元件。

  • 首先,Transformer 是編輯 API 的主要進入點。

  • Transformer 會處理、轉碼媒體,並將媒體匯出至檔案。

  • 接下來是 CompositionPlayer,可即時預覽媒體組成。

  • 這樣一來,你就能在最終匯出前預覽及微調編輯內容。

  • 接著是特效。

  • 無論是套用簡單的視覺效果,還是新增圖像疊加層,都能完全掌控逐格處理。

  • 最後是 Muxer。

  • 這是管道的最終輸出階段,會接收處理過的音訊、影片和中繼資料軌,並寫入標準化 MP4 容器。

  • 過去一年,我們重新思考並從頭打造影片編輯流程,今天很高興能與大家分享重大強化功能。

  • 為什麼要重建管道?

  • 因此,舊版架構帶來了幾項挑戰。

  • 首先,它無法在所有處理步驟中傳播中繼資料。

  • 它與 OpenGL 深度耦合,限制了現代圖形工具的彈性。

  • 此外,複雜的 API 也讓開發人員在自訂和維護方面面臨重重阻礙。

  • 為解決這些難題,我們建構了全新的處理管道。

  • 首先,現在要組合複雜的多重資產陣容變得更簡單。

  • 第二,與 OpenGL 分離後,您就能自由使用 WebGPU 等現代圖形架構。

  • 最後,Media3 現在會直接管理解碼器和編碼器,確保所有裝置都能提供更一致、更有效率的體驗。

  • 接著就來深入瞭解這個管道的運作方式。

  • 解碼器會直接將影格輸出至 Media3 管理的硬體緩衝區。

  • 影格匯總器會協調多個序列的影格,讓自訂影格處理器直接存取及處理這些緩衝區,再傳送至編碼器。

  • FrameProcessor 介面是應用程式層級處理作業的主要擴充點。

    public interface FrameProcessor
        extends AutoCloseable {
    
      interface Listener {
        void onWakeup();
        void onError(
             VideoFrameProcessingException exception);
        void onFrameProcessed(
             Frame frame,
             @Nullable SyncFenceWrapper onCompleteFence);
      }
    
      interface Factory {
        FrameProcessor create(
             FrameWriter output,
             Executor listenerExecutor,
             Listener listener);
      }
    
      boolean queue(List<AsyncFrame> frames);
    
      void signalEndOfStream();
    }
  • 應用程式會透過事件監聽器介面,在處理完影格時通知 Media3,並傳遞同步處理柵欄,讓 Media3 確切知道何時可以安全地重複使用緩衝區。

  • 關於統一記憶體的注意事項:Media3 取得硬體緩衝區的擁有權後,應用程式就能直接在原地讀取及修改資料。

  • 這種零複製方法可減少記憶體負擔,並保持超快的處理速度。

  • Adobe 是其中一家善用新版編輯 API 的開發商。

  • Adobe Premiere 團隊以 Media3 編輯 API 為基礎,建構媒體編輯基礎架構,確保與 Android 生態系統有更高的相容性,同時保有使用自訂管道和 WebGPU 所需的彈性。

  • 接下來,很高興宣布我們將在 Media3 1.12 版本中推出新效果。

  • 首先是影片強化功能。

  • 在幕後,我們的架構仰賴模組化設定。

  • 應用程式整合了 Media3 效果 Play 服務,該服務會與 Play 服務強化模組互動。

  • 影片強化模組包含色調對應、去噪、去模糊和超高解析度功能。

  • 只要幾行程式碼,就能將 Video Enhance 整合至管道。

    val frameProcessorFactory = EnhancementSessionDecorator.Builder(/* context = */ this, baseGlFactory)
        .setTonemappingEnabled(true)
        .setDeblurAndDenoiseEnabled(true)
        .setUpscaleVideoEnabled(true)
        .build()
    
    val transformerBuilder = Transformer.Builder(/* context = */ this)
        .setFrameProcessorFactory(frameProcessorFactory)
  • 使用 EnhancementSessionDecorator.Builder,您可以選擇性啟用色調對應、去模糊或升級等功能,並將產生的 frameProcessorFactory 和硬體緩衝區輔助程式傳遞至 Transformer.Builder。

  • Media3 1.12 即將推出的另一項精彩效果是錄音室音效。

  • Studio Sound 會自動分析影片音軌,並分成語音、環境噪音和音樂等個別音源。

  • 創作者可以輕鬆降低風聲或人群噪音,同時凸顯清晰的對話和音樂。

  • 如要整合 Studio Sound,請在效果物件中傳遞 Studio Sound 音訊處理器。

    val editedMediaItem =
         EditedMediaItem.Builder(MediaItem.fromUri(media.uri))
           .setEffects(
             Effects(
               listOf(
                 soundSeparationAudioProcessor
               ),
               emptyList(), // video effects
             )
           )
           .build()
  • 這會將音訊處理器附加至媒體項目。

  • 建立 Studio Sound 音訊處理器時,請附加事件監聽器回呼,透過 onResult 接收經過分析的音訊聲道對應。

    val soundSeparationAudioProcessor =
          SoundSeparationAudioProcessor(
            getApplication(),
            mainExecutor,
            object : SoundSeparationAudioProcessor.Listener {
              override fun onResult(channelToAudioType: SparseIntArray) {
                for (i in 0 until channelToAudioType.size()) {
                  val channelIndex = channelToAudioType.keyAt(i)
                  val audioType = channelToAudioType.valueAt(i)
                  val label = toAudioTypeName(audioType)
                  detectedChannelVolumes.add(ChannelVolume(channelIndex, audioType, label))
                }
              }
            },
          )
  • 作業結果可用時,疊代每個聲道,擷取聲道索引、識別的音訊類型和可讀標籤。

  • 接著來聽聽錄音室效果的實際表現。

  • 首先是第一個效果。寶貝,請別哭。

  • 我們正在為你準備晚餐。

  • 你可以聽見背景傳來嬰兒哭聲,現在已套用效果。

  • 沒錯,如你所聽,多虧了錄音室效果,背景中沒有嬰兒哭聲。

  • 現在,我們來深入瞭解中繼資料擷取。

  • 我們將探討 Media3 如何協助您輕鬆讀取容器中繼資料及擷取影格。

  • 從 MetadataRetriever 開始,Room 與 Kotlin 協同程式完美整合,可提供簡潔、非阻斷的程式碼。

  • 其設計更簡潔,且每個媒體項目使用一個不可變的擷取器,因此較不易出錯。

  • 在所有 API 版本和裝置上都能穩定運作。

  • 而且支援多種媒體格式。

  • 以下是使用 MetadataRetriever 的實際範例。

    MetadataRetriever.Builder(context, mediaItem).build().use { retriever ->
        // 1. Duration (in microseconds)
        val durationUs = retriever.retrieveDurationUs().await()
        // 2. Track formats & container metadata (reuses the same preparation pass)
        val trackGroups = retriever.retrieveTrackGroups().await()
        for (i in 0 until trackGroups.length) {
            val format = trackGroups[i].getFormat(0)
            // Video/Audio properties: format.width, format.height, format.colorInfo …
            // Rich metadata: SlowMotionData, MotionPhotoMetadata, Mp4TimestampData …
            val metadata = format.metadata
        }
        // 3. Stream structure & manifest info
        val timeline = retriever.retrieveTimeline().await()
    }
  • 使用 Kotlin 協同程式,您可以在單一簡潔的區塊中,以非同步方式擷取時間長度、檢查音軌格式和中繼資料。

  • 接著來談談FrameExtractor。

  • 可快速高效地從媒體項目中提取解碼的影片影格。

  • 這項工具會自動管理解碼器等基礎資源,讓您套用視覺效果或 HDR 色調映射,並提供精細的跳轉控制項,方便您取得所需影格。

  • 以下是影格擷取的程式碼。

    FrameExtractor.Builder(context, mediaItem)
        .setSeekParameters(SeekParameters.CLOSEST_SYNC) // Fast keyframe seeking
        .setEffects(listOf(Presentation.createForHeight(360))) // GPU downscaling
        .setExtractHdrFrames(true) // Optional: HLG HDR or UltraHDR
        .build()
        .use { extractor ->
            // Representative thumbnail
            val thumbnail = extractor.getThumbnail().await()
            // Frame at specific timestamp (bitmap + actual presentationTimeMs)
            val frame = extractor.getFrame(positionMs = 5_000L).await()
        }
  • 您可以使用跳轉參數、GPU 縮小、效果和 HDR 支援等選項建構例項,然後使用乾淨的暫停呼叫,以非同步方式等待縮圖和特定影格。

  • 現在,我們來看看 Donovan。

串流標準和新媒體格式

  • 謝謝,Kristina。

  • 接下來,我想談談我們在 Media3 中新增的一些基礎串流技術支援,包括支援 Eclipsa Video 和 Eclipsa Audio 等格式。

  • 因此,Media3 的核心工作是領先串流標準,這樣每次有新規格推出時,您就不必重寫媒體管道。

  • 以下是我們最近推出的幾項功能。

  • 我們有內容導向功能,可在串流播放期間動態切換後端 CDN 路徑。

  • 我們支援伺服器導向廣告插播,可順暢拼接個人化廣告。

  • 我們也為多個進入點導入 AV1 支援,包括完整的 AV1 HDR 支援。

  • 我們支援 Android 專業影片,可進行高保真擷取和編輯。

  • 我們也將支援 JPEG 和 HEIC 格式的動態相片。

  • 當然,我們也將支援 Eclipsa Video 和 Eclipsa Audio,稍後會進一步說明。

  • 好消息是,隨著裝置支援功能推出,系統會自動支援所有這些功能。

  • 接下來,我們來看看 HDR 的下一個演進階段:Eclipsa Video,以及這項技術要解決的核心問題。

  • 因此,先前的 HDR 格式提供描述性中繼資料,因此他們決定場景的外觀,但並未實際告知顯示器如何轉譯。

  • 首先,我們來奠定基礎。

  • 在不同環境下,我們會建議不同的平均螢幕亮度,讓你能清楚舒適地查看內容。

  • 因此,在昏暗的房間中,這個範圍會介於 100 到 300 nit 之間。

  • 當然,隨著環境越來越亮,螢幕也必須跟著調亮,才能讓使用者看清楚內容。在陽光直射下看手機時,螢幕可能需要調到 1,000、2,000 甚至更高的 nit,才能看清楚內容。

  • 傳統 SDR 的運作範圍為 100 到 300。

  • 當然,HDR 的目的並非全面調亮螢幕。

  • 您想擴大動態範圍,讓火光和陽光等局部區域更明亮。

  • 不過,智慧型手機會讓情況變得更複雜,因為我們會隨身攜帶手機,無論是在家中的暗房或明亮的海灘。

  • 此外,許多應用程式會在混合動態消息中,同時顯示 SDR UI 元素、SDR 內容和 HDR 內容,這可能會導致刺眼、令人不適的效果,尤其是在深夜捲動畫面時。

  • 現在你應該明白 HDR 的價值了吧?

  • 讓內容在更多環境中都能完美呈現。

  • 我們甚至發現,開啟 HDR 後,使用者參與度有所提升。

  • 但問題是,現有的 HDR 格式並未考量到這些環境。

  • 這正是 Eclipsa Video 的強項。

  • (等觀眾笑完)

  • 感謝您!

  • 感謝您!

  • 因此,Eclipsa Video 提供的是規範性中繼資料,而非描述性中繼資料。

  • 這項服務主要奠基於三大要素。

  • 第一項要素是參考白色錨點。

  • 因此,這項功能會將標準亮度鎖定在螢幕根據環境調整的 SDR 白點,避免亮度突然飆高。

  • 第二個要素是創作者 SDR 基準。

  • 這樣一來,創作者的色調映射曲線就能精確保留,並向下轉換為 SDR。

  • 第三,我們有餘裕自適應增益曲線,可確定性地縮放高光,以符合顯示器的即時 HDR 餘裕,讓內容符合觀看環境。

  • 在搭載 Android 17 以上版本的裝置上,Media3 1.11 的 ExoPlayer 內建這項功能。

  • Eclipsa Video 採用開放式標準,可跨產業相容。

  • 因此,現在正是支援 HDR 的最佳時機。

  • 音訊方面,我們推出 Eclipsa Audio,這是一種開放原始碼、免權利金的 3D 空間音訊格式。

  • 這項技術是以開放媒體聯盟 (Alliance for Open Media) 的 IAMF 規格為基礎。

  • 這項技術結合了場景式 Ambisonics 和聲道式音訊,可將多種混合呈現方式整合至單一串流。

  • 因此,這項技術可進行類似物件的操作,例如強化對話,而且適用於立體聲耳機到環場音效設定等所有裝置。

  • Media3 實際上提供兩種播放路徑。

  • 在 Android 17 以上版本,ExoPlayer 和 Android 音訊架構會自動轉譯 Eclipsa Audio。

  • Android 16 以下版本仍可選擇是否啟用。

  • 您可以透過選用的 Media3 外掛程式庫啟用 IAMF 擴充功能。

  • 在創作方面,您可以使用 Media3 多工器編寫 IAMF 音軌。

效能和電池效能最佳化

  • 不過,支援高畫質格式只是其中一環。

  • 我們必須有效率地提供這種品質。

  • 接下來,我想談談 Media3 如何加快管道速度並提高效率。

  • 如同我們今天討論過的許多內容,只要將 Media3 版本保持在最新狀態,就能直接享有大部分的效能提升。

  • 舉例來說,我們提供 PCM 音訊卸載功能,可將音訊播放轉送至低功耗處理單元。

  • 根據初步分析,在螢幕關閉時播放音訊,最多可節省 20% 的電力。

  • 除了 PCM 音訊卸載外,我們還提供動態排程功能,可根據 VSYNC 和解碼器緩衝區可用性,調整 ExoPlayer 的內部算繪迴圈喚醒。

  • 我們也提供轉譯器預熱功能,可預熱次要影片轉譯器,讓播放清單能立即切換,並更輕鬆地插入廣告。

  • 我們也提供卡住播放器偵測和預先快取功能,可自動偵測停滯的緩衝區和轉譯器,並清楚繫結預先快取的記憶體。

  • 另一個最佳模式是拖曳模式。

  • 這適用於使用者經常拖曳內容的應用程式。

  • 因此,啟用拖曳模式後,拖曳跳轉列時會更加靈敏流暢,如這裡所示。

  • 順帶一提,這是在關閉拖曳模式的情況下。

  • 右側則顯示拖曳模式已開啟。

  • 感謝您!

  • 感謝您!

  • 現在我們來做個小測驗

  • 接著來談談 Android 系統健康。

  • 我們必須尊重共用的 RAM、CPU 和電池資源,因此隨著 Android 強化音訊和前景服務的規則,管理自身原始服務和點陣圖的應用程式,往往會發生一些錯誤。

  • 您可能看過前景服務啟動不允許例外狀況,或繫結器記憶體不足例外狀況。

  • 因此,我們透過多種方式強化媒體服務工作階段、媒體通知管理工具和點陣圖管道,包括防止過時的前景服務意圖,以及限制圖片記憶體等。

  • 不過,如果您使用 Media3 並遵循標準指南,系統會自動處理所有系統健康狀態限制,不會造成任何影響。

  • 最後,我想談談 Media3 即將推出的功能。

  • 編碼或匯出影片時,硬體多樣性可能是最大的挑戰之一。

  • 許多應用程式都採用一體適用的轉碼器設定,但這會導致品質和效能不佳。

  • 因此,我們打造了 CodecDB。

  • CodecDB 是以廣泛的實際影片測量結果為基礎,並在 Android 裝置上使用標準化品質指標和硬體基準測試。

  • 一起來看看實際的運作方式吧。

  • 左側是我們用手機拍攝的影片影格,其中只使用了一般影片設定,這類設定可能適用於許多裝置。

  • 右側則使用從 CodecDB 取得的裝置層級最佳設定。

  • 那麼,兩者有何差異?

  • 在細節豐富的區域,畫質差異最明顯。

  • 請觀察樹木和灌木枝葉的清晰度,或建築物邊緣的銳利度。

  • 因此,我們能利用 CodecDB 充分發揮特定裝置的功能,提供更優質的觀看體驗。

  • 在程式碼中使用 CodecDB 相當簡單。

    val config = CodecDb.onDevice(context)
       .forEncodingFallingBackTo {
               constraints -> createFallbackConfig(constraints)
       }
       .findOptimalSettings()
       .forEditing()
       .withResolution(1920, 1080, /* frameRate= */ 30.0)
       .withAnyCodec()
       .targetingQuality(VideoEncoder.QUALITY_LEVEL_VERY_GOOD)
       .get()
  • 您將使用流暢的階段建構工具模式,引導您完成每個必要參數。

  • 因此您需要指定用途。

  • 這裡我們用來編輯。

  • 這會告知 CodecDB,您使用的是 Media3 匯出或轉碼管道。

  • 您也可以設定其他用途,例如搭配相機拍攝功能。

  • 您也需要傳入目標解析度和影格速率。

  • 接著,您不必猜測 1080p 影片需要每秒 8 MB 還是 15 MB,只要告訴 CodecDB 您願意使用哪種轉碼器,並要求感知品質目標即可。

  • 在本範例中,我們使用「非常好」品質等級。

  • CodecDB 會在背景查詢該 SOC 的經驗基準曲線,並傳回理想的轉碼器和編碼器參數。

  • 我們預計很快就會在 Media3 中推出 CodecDB,屆時將使用原生 Media3 類型。

  • 因此,將其與現有的 Media3 程式碼整合應該非常簡單。

摘要和結論

  • 好的。

  • 今天我們介紹了 Media3 的許多精彩功能,現在來複習一下。

  • 今天,您瞭解了全新的 Compose UI 模組、新版 CastPlayer、經過翻新的編輯管道,以及許多可在 Media3 中使用的新效果。

  • 我們也發現了擷取中繼資料和影格的新工具。

  • 我們也探討了新的串流技術支援、效能強化和公用程式。

  • Media3 是開放原始碼程式庫,因此您可以在 GitHub 存放區中,查看我們如何實作所有這些功能。

  • 截至目前,Media3 的最新穩定版本為 1.11.1。

  • 您也可以查看目前處於 Alpha 版的 1.12.0。

  • 點選這個短連結,即可前往 Media3 的開發人員說明文件。

  • 非常感謝,祝您在 ADS 接下來的活動中收穫滿滿。

程式碼

程式碼片段

「使用 Jetpack Compose 建立媒體 UI」影片 02:31 處的 Kotlin 程式碼片段
@Composable
fun videoScreen() {
  Player(viewModel.exoPlayer)
}
「使用 Jetpack Compose 建立媒體 UI」影片 04:44 處的 Kotlin 程式碼片段
Player(viewModel.exoPlayer)
「使用 Jetpack Compose 建立媒體 UI」影片 04:48 處的 Kotlin 程式碼片段
Player(
  player = viewModel.exoPlayer,
  bottomControls = {
    PlayerDefaults.BottomControls(
      player,
      left = {}, right = {},
      above = {
        PositionAndDurationText(
          player, separator = " • ")
      },
    )
  },
)
「使用 Jetpack Compose 建立媒體 UI」影片 05:13 處的 Kotlin 程式碼片段
Player(
  player = viewModel.exoPlayer,
  centerControls = {},
  bottomControls = { ...
      above = {
        Row(Modifier.fillMaxWidth()) {
          PositionAndDurationText(...)
          Spacer(Modifier.weight(1f))
          PlaybackSpeedToggleButton(player)
          MuteButton(player)
          PlayPauseButton(player)
        }
      },
    )
  }
「使用 Jetpack Compose 建立媒體 UI」影片 05:51 處的 Kotlin 程式碼片段
Player(
  ...
  bottomControls = { ...
      above = {
          ...
          MuteButton(player,
            painter = { painterResource(
              if (showMuted)
                R.drawable.muted
              else
                R.drawable.unmuted )})
      }
  },
)
「Sharing playback with MediaSession」影片 07:01 處的 Kotlin 程式碼片段
MediaSession.Builder(context, player).build()
「透過 MediaSession 分享播放內容」影片 07:17 處的 Kotlin 程式碼片段
class PlaybackService : MediaSessionService() {

  private lateinit var mediaSession:
        MediaSession

  override fun onGetSession(
        info: ControllerInfo) : MediaSession?
    = mediaSession
}
「Sharing playback with MediaSession」影片 08:25 處的 Kotlin 程式碼片段
val player = ExoPlayer.Builder(context).build()

mediaSession = MediaSession.Builder(context,player)
.build()
「透過 MediaSession 分享播放內容」影片 08:29 處的 Kotlin 程式碼片段
Cast.getSingletonInstance(context).initialize()

val player = CastPlayer.Builder(context).build()

mediaSession = MediaSession.Builder(context,player)
.build()
「Sharing playback with MediaSession」影片 08:45 處的 Kotlin 程式碼片段
Cast.getSingletonInstance(context).initialize()
val exoPlayer = ExoPlayer.Builder(context).build()
val player = CastPlayer.Builder(context)
                .setLocalPlayer(exoPlayer).build()
mediaSession = MediaSession.Builder(context,player)
.build()
「透過 MediaSession 分享播放內容」影片 09:32 處的 Kotlin 程式碼片段
Player(
  exoPlayer,
  centerControls = {}
  bottomControls = { /* customized */ },
)
「Sharing playback with MediaSession」影片 09:40 處的 Kotlin 程式碼片段
Player(
  castPlayer,
  centerControls = {}
  bottomControls = { /* customized */ },
  topControls = {
    PlayerDefaults.TopControls(castPlayer) {
      MediaRouteButton(
        Modifier.align(Alignment.TopEnd))
    }
  },
)
「Media3 編輯 API」影片 13:02 處的 Java 程式碼片段
public interface FrameProcessor
    extends AutoCloseable {

  interface Listener {
    void onWakeup();
    void onError(
         VideoFrameProcessingException exception);
    void onFrameProcessed(
         Frame frame,
         @Nullable SyncFenceWrapper onCompleteFence);
  }

  interface Factory {
    FrameProcessor create(
         FrameWriter output,
         Executor listenerExecutor,
         Listener listener);
  }

  boolean queue(List<AsyncFrame> frames);

  void signalEndOfStream();
}
「Media3 editing APIs」影片 14:45 處的 Kotlin 程式碼片段
val frameProcessorFactory = EnhancementSessionDecorator.Builder(/* context = */ this, baseGlFactory)
    .setTonemappingEnabled(true)
    .setDeblurAndDenoiseEnabled(true)
    .setUpscaleVideoEnabled(true)
    .build()

val transformerBuilder = Transformer.Builder(/* context = */ this)
    .setFrameProcessorFactory(frameProcessorFactory)
「Media3 editing APIs」影片 15:36 處的 Kotlin 程式碼片段
val editedMediaItem =
     EditedMediaItem.Builder(MediaItem.fromUri(media.uri))
       .setEffects(
         Effects(
           listOf(
             soundSeparationAudioProcessor
           ),
           emptyList(), // video effects
         )
       )
       .build()
「Media3 編輯 API」影片 15:50 處的 Kotlin 程式碼片段
val soundSeparationAudioProcessor =
      SoundSeparationAudioProcessor(
        getApplication(),
        mainExecutor,
        object : SoundSeparationAudioProcessor.Listener {
          override fun onResult(channelToAudioType: SparseIntArray) {
            for (i in 0 until channelToAudioType.size()) {
              val channelIndex = channelToAudioType.keyAt(i)
              val audioType = channelToAudioType.valueAt(i)
              val label = toAudioTypeName(audioType)
              detectedChannelVolumes.add(ChannelVolume(channelIndex, audioType, label))
            }
          }
        },
      )
「Media3 編輯 API」中 17:29 的 Kotlin 程式碼片段
MetadataRetriever.Builder(context, mediaItem).build().use { retriever ->
    // 1. Duration (in microseconds)
    val durationUs = retriever.retrieveDurationUs().await()
    // 2. Track formats & container metadata (reuses the same preparation pass)
    val trackGroups = retriever.retrieveTrackGroups().await()
    for (i in 0 until trackGroups.length) {
        val format = trackGroups[i].getFormat(0)
        // Video/Audio properties: format.width, format.height, format.colorInfo …
        // Rich metadata: SlowMotionData, MotionPhotoMetadata, Mp4TimestampData …
        val metadata = format.metadata
    }
    // 3. Stream structure & manifest info
    val timeline = retriever.retrieveTimeline().await()
}
「Media3 editing APIs」影片 18:06 處的 Kotlin 程式碼片段
FrameExtractor.Builder(context, mediaItem)
    .setSeekParameters(SeekParameters.CLOSEST_SYNC) // Fast keyframe seeking
    .setEffects(listOf(Presentation.createForHeight(360))) // GPU downscaling
    .setExtractHdrFrames(true) // Optional: HLG HDR or UltraHDR
    .build()
    .use { extractor ->
        // Representative thumbnail
        val thumbnail = extractor.getThumbnail().await()
        // Frame at specific timestamp (bitmap + actual presentationTimeMs)
        val frame = extractor.getFrame(positionMs = 5_000L).await()
    }
「效能和電池最佳化」中 28:26 的 Kotlin 程式碼片段
val config = CodecDb.onDevice(context)
   .forEncodingFallingBackTo {
           constraints -> createFallbackConfig(constraints)
   }
   .findOptimalSettings()
   .forEditing()
   .withResolution(1920, 1080, /* frameRate= */ 30.0)
   .withAnyCodec()
   .targetingQuality(VideoEncoder.QUALITY_LEVEL_VERY_GOOD)
   .get()