使用者在行動裝置上花費的時間,有很大一部分用於媒體消費,因此播放體驗必須完美無瑕、高效率,且不耗費電池續航力。此外,使用者也希望這些體驗能在手機、客廳電視等裝置之間流暢切換。
瞭解 Media3 和 ExoPlayer 的架構更新如何讓各種板型規格的裝置都能流暢播放內容。討論重點包括使用 Media3 Compose UI 工具包打造回應式介面、運用全新 PreloadManager 消除動態消息中的緩衝,以及透過 CastPlayer 和空間音訊,將應用程式延伸至客廳螢幕和 Android XR。
轉錄稿
好的。
大家好,歡迎各位的加入。
我是 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 影片和音訊),以及多項可預先載入、拖曳和節約耗電量的體驗品質公用程式。
現在,我們直接進入第一個主題,瞭解 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 處理。
它會讀取播放器的狀態、與播放器互動,並處理所有轉換和動畫。
接下來,我們從 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 包含多個元件。
首先,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 接下來的活動中收穫滿滿。
@Composable
fun videoScreen() {
Player(viewModel.exoPlayer)
}Player(viewModel.exoPlayer)Player(
player = viewModel.exoPlayer,
bottomControls = {
PlayerDefaults.BottomControls(
player,
left = {}, right = {},
above = {
PositionAndDurationText(
player, separator = " • ")
},
)
},
)Player(
player = viewModel.exoPlayer,
centerControls = {},
bottomControls = { ...
above = {
Row(Modifier.fillMaxWidth()) {
PositionAndDurationText(...)
Spacer(Modifier.weight(1f))
PlaybackSpeedToggleButton(player)
MuteButton(player)
PlayPauseButton(player)
}
},
)
}Player(
...
bottomControls = { ...
above = {
...
MuteButton(player,
painter = { painterResource(
if (showMuted)
R.drawable.muted
else
R.drawable.unmuted )})
}
},
)MediaSession.Builder(context, player).build()class PlaybackService : MediaSessionService() {
private lateinit var mediaSession:
MediaSession
override fun onGetSession(
info: ControllerInfo) : MediaSession?
= mediaSession
}val player = ExoPlayer.Builder(context).build()
mediaSession = MediaSession.Builder(context,player)
.build()Cast.getSingletonInstance(context).initialize()
val player = CastPlayer.Builder(context).build()
mediaSession = MediaSession.Builder(context,player)
.build()Cast.getSingletonInstance(context).initialize()
val exoPlayer = ExoPlayer.Builder(context).build()
val player = CastPlayer.Builder(context)
.setLocalPlayer(exoPlayer).build()
mediaSession = MediaSession.Builder(context,player)
.build()Player(
exoPlayer,
centerControls = {}
bottomControls = { /* customized */ },
)Player(
castPlayer,
centerControls = {}
bottomControls = { /* customized */ },
topControls = {
PlayerDefaults.TopControls(castPlayer) {
MediaRouteButton(
Modifier.align(Alignment.TopEnd))
}
},
)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();
}val frameProcessorFactory = EnhancementSessionDecorator.Builder(/* context = */ this, baseGlFactory)
.setTonemappingEnabled(true)
.setDeblurAndDenoiseEnabled(true)
.setUpscaleVideoEnabled(true)
.build()
val transformerBuilder = Transformer.Builder(/* context = */ this)
.setFrameProcessorFactory(frameProcessorFactory)val editedMediaItem =
EditedMediaItem.Builder(MediaItem.fromUri(media.uri))
.setEffects(
Effects(
listOf(
soundSeparationAudioProcessor
),
emptyList(), // video effects
)
)
.build()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))
}
}
},
)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()
}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()
}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()