用户在移动设备上花费的时间中,媒体消费占据了很大一部分,因此播放体验必须完美无瑕、高效且注重电池续航。此外,用户还希望这些体验能够在手机、Living Room 电视等各种设备之间无缝切换。
探索 Media3 和 ExoPlayer 中的架构更新如何助力在各种设备规格上实现流畅播放。讨论重点包括:使用 Media3 Compose UI 工具包打造自适应界面、利用全新 PreloadManager 消除信息流中的缓冲,以及通过 CastPlayer 和空间音频将应用扩展到客厅屏幕和 Android XR。
转写文稿
好的。
欢迎大家!
我叫 Toni。
我是 Android 媒体团队的一名工程师。
Kristina、Donovan 和我将一起探讨 Jetpack Media3 如何帮助您满足 Android 上的所有媒体需求。
Media3 是 Android 的支持库,涵盖所有媒体相关功能,无论是使用 ExoPlayer 播放、通过媒体会话和 Cast 共享媒体,还是使用 Transformer 编辑媒体,或者检查媒体(例如提取帧或元数据),都一应俱全。
Media3 在单个库组中涵盖了整个空间。
作为支持库,它会捆绑到您的应用中。
这意味着我们可以确保一致性,让相同的代码在所有操作系统版本和所有设备上运行。
它还有助于提高可更新性。
我们所做的每项问题修复都会向下兼容到 API 24,并且不与任何操作系统发布周期相关联。
作为支持库,我们可以提供非常丰富的自定义选项,而这在 Android 框架中是无法实现的。
作为 GitHub 上的一个库,我们始终坚持开源优先。
这意味着,我们所做的每项更改都会立即在 GitHub 上发布。
我们有一个活跃的问题跟踪器,并且接受拉取请求。
这里要强调的重点是,在所有这些应用场景中,我们都非常注重各个层面的细节,从高级界面元素到简单的开箱即用 API,再到深度自定义选项、对各种格式和标准的广泛支持,以及支撑这一切的性能和效率。
Media3 涵盖了整个领域。
今天,我们将深入探讨其中一些主题。
我们将介绍全新的 Compose UI 模块、让媒体会话共享更轻松的新 CastPlayer、具有全新高级效果的改进版编辑流水线、用于检索元数据和帧的新工具、一些新的流式传输技术(特别是 Eclipsa 视频和音频),以及许多用于预加载、拖动和耗电量低的体验质量实用程序。
现在,我们直接进入第一个主题:Media3 如何帮助构建媒体界面元素。
我们最近发布了两个新模块:Media3 UI Compose 和 Media3 UI Compose Material 3。
在这里,您可以看到一个高级播放器可组合项,它在 Media3 1.10 中发布,未做任何进一步修改。
@Composable
fun videoScreen() {
Player(viewModel.exoPlayer)
}可直接使用。
它只需要一个 Media3 播放器。
在此示例中,它是 ExoPlayer,通常位于视图模型中。
但也可以是其他播放器,例如 CastPlayer、媒体控制器或应用中的任何其他播放器。
值得强调的是,这里的 ExoPlayer 是可信来源。
它包含所有信息,而播放器可组合项负责渲染界面并连接所有内容。
但是,如果您想自定义此界面、新按钮、不同位置、不同样式、文本叠加层,该怎么办?
播放器可组合项由多个基础组件组成。
您可以替换这些内容。
您可以自定义每个单独的块,也可以在应用中的其他位置单独使用它们。
例如,您可以分别自定义顶部、中心和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。
它会向其他应用提供播放状态和元数据,并允许其他应用控制播放。
Gemini、系统界面媒体通知、Cast、Wear OS 或 Android Auto 配套应用就是通过这种方式与您的应用进行通信。
MediaSession 封装了 Media3 播放器,一旦有了播放器,创建 就变得非常简单。
MediaSession.Builder(context, player).build()同样,这可以是 ExoPlayer 或应用中的任何其他播放器,就像 Compose 界面一样。
如果您在后台播放,通常会将其托管在 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,该 SDK 现已进入维护模式。
相反,Media3 Cast 完全取代了它,因为 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 界面吗?
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 连接到了界面,以确保远程发生的任何情况都会反映在本地界面中。
接下来,我将请 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 开始,它与 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 尼特之间。
当然,随着环境越来越亮,屏幕也必须不断变亮,这样您才能看清内容。当您在阳光直射下查看手机时,屏幕可能需要达到 1,000、2,000 甚至更高的尼特,您才能看清内容。
传统 SDR 的亮度范围为 100 到 300 尼特。
当然,HDR 并非旨在全局调亮屏幕。
您希望扩大动态范围,以便为火焰和阳光等事物提供局部高光。
不过,智能手机让情况变得有些复杂,因为我们会随身携带手机,无论是在家里的暗室,还是在明亮的海滩。
再加上许多应用在混合信息流中将 SDR 界面元素与 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 音频分流之外,我们还引入了动态调度机制,使 ExoPlayer 的内部渲染循环唤醒与 VSYNC 和解码器缓冲区可用性保持一致。
我们还提供渲染器预热功能,让您可以预热辅助视频渲染器,从而实现即时播放列表过渡和更轻松的广告拼接。
我们还提供卡顿播放器检测和预缓存功能,可自动检测停滞的缓冲区和渲染器,并清晰地限制预缓存内存。
我们还提供另一种最佳模式,即拖动模式。
这适用于用户经常拖动进度条的应用。
因此,启用拖动模式后,拖动进度条会变得更加灵敏和流畅,如您所见。
顺便说一下,这是在关闭拖动模式的情况下。
右侧则显示拖动模式已开启。
谢谢。
谢谢。
好了
现在,我们来谈谈 Android 系统运行状况。
我们需要尊重共享的 RAM、CPU 和电池资源,因此,随着 Android 强化音频和前台服务规则,管理自己的原始服务和位图的应用往往会遇到一些错误。
您可能已经看到过一些异常,例如前台服务启动不允许异常或 binder 内存不足异常。
因此,我们通过多种方式强化了媒体服务会话和媒体通知管理器以及位图流水线,包括防止过时的前台服务 intent、限制艺术作品内存等。
但对您来说,关键在于,如果您使用的是 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 Mbps 还是 15 Mbps,而是告诉 CodecDB 您愿意使用哪种编解码器,并要求达到感知质量目标。
在本例中,我们使用的是“非常好”质量级别。
在后台,CodecDB 会查找该特定 SOC 的经验基准曲线,并返回理想的编解码器和编码器参数。
我们预计 CodecDB 很快就会在 Media3 中发布,届时它将使用原生 Media3 类型。
因此,将其与现有 Media3 代码集成应该会非常轻松。
好的。
今天我们看到了 Media3 的许多强大功能,现在让我们来回顾一下。
今天,您了解了我们全新的 Compose 界面模块、新的 CastPlayer、经过改进的编辑流水线,以及可在 Media3 中使用的许多新效果。
我们还看到了用于检索元数据和帧的新工具。
我们还探讨了新的流式传输技术支持、性能增强和实用程序。
Media3 是一个开源库,因此您可以在我们的 GitHub 代码库中查看我们是如何实现所有这些功能的。
截至今天,Media3 的最新稳定版本为 1.11.1。
您还可以查看 1.12.0,该版本目前处于 Alpha 版阶段。
请点击此短链接,前往我们的 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()