18、Media3:ExoPlayer升级到Media3、Media3与Compose集成、自适应流媒体

各位同学,今天我们来聊聊 Android 媒体播放领域的一次重要升级——从 ExoPlayer 迁移到 Media3。说实话,这个变化我一开始也觉得有点折腾,但用下来之后,确实能感受到 Google 的良苦用心。

Media3 不是简单的版本号更新,它是一次架构层面的重构。它把视频播放、音频播放、字幕、DRM 这些能力统一到了一个全新的库中。说白了,就是 Google 想把媒体播放这件事做得更干净、更模块化。

为什么要有 Media3?

ExoPlayer 从 1.0 到 2.x,功能越来越强大,但问题也随之而来。我记得有一次在项目中集成 ExoPlayer 和 MediaSession,发现两者的生命周期管理完全是两套逻辑,调试起来非常痛苦。

Media3 的核心目标就是解决这个痛点。它把 Player、MediaSession、MediaController 这些组件统一到了同一个架构下。你想想看,以前你要维护 ExoPlayer 实例、还要单独维护一个 MediaSession,现在它们天然就是一家人了。

核心变化一览:

  • 包名从 com.google.android.exoplayer2 变为 androidx.media3
  • Player 接口统一,ExoPlayer 成为 Media3 的一个实现
  • MediaSession 和 MediaController 内建支持,无需额外集成
  • Compose 集成成为一等公民

从 ExoPlayer 升级到 Media3

升级过程其实比我想象中要平滑。我在一个老项目上做过迁移,大概花了半天时间。这里我给大家总结一下关键步骤。

第一步:替换依赖

把 build.gradle 里的 ExoPlayer 依赖全部替换成 Media3。注意,Media3 的模块划分更细了。

// 旧的 ExoPlayer 依赖
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'

// 新的 Media3 依赖
implementation 'androidx.media3:media3-exoplayer:1.2.0'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1' // 注意:UI 组件还在旧包下

嗯,这里有个坑。Media3 的 UI 组件(比如 PlayerView)目前还是放在旧包名下的。Google 说后续会迁移到 Media3 的 UI 模块,但目前我们还得混着用。

第二步:修改导入语句

把所有 com.google.android.exoplayer2 开头的 import 改成 androidx.media3。这一步可以用 IDE 的全局替换,但要注意有些类名也变了。

旧类名 新类名
SimpleExoPlayer ExoPlayer
ExoPlayerFactory ExoPlayer.Builder
Player.Listener Listener
MediaSource MediaItem

第三步:调整 API 调用

Media3 的 API 设计更简洁了。比如创建播放器的方式变了:

// 旧方式
SimpleExoPlayer player = ExoPlayerFactory.newSimpleInstance(context);

// 新方式
ExoPlayer player = new ExoPlayer.Builder(context).build();

设置媒体源的方式也变了:

// 旧方式
MediaSource mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory)
    .createMediaSource(Uri.parse(url));
player.prepare(mediaSource);

// 新方式
MediaItem mediaItem = MediaItem.fromUri(url);
player.setMediaItem(mediaItem);
player.prepare();

我的经验:升级时最容易出问题的是自定义的 Renderer 和 MediaSource。如果你有自定义的组件,建议先跑一遍官方的迁移指南,对照着改。我曾经因为一个自定义的音频渲染器没改对,导致播放时只有画面没有声音,排查了半天。

Media3 与 Compose 集成

Compose 现在是 Android UI 的主流方向,Media3 自然也提供了原生支持。说实话,我第一次在 Compose 里集成播放器时,感觉比 XML 方式清爽太多了。

Media3 提供了 AndroidView 的方式来嵌入 PlayerView,但更推荐的做法是使用 rememberDisposableEffect 来管理播放器的生命周期。

@Composable
fun VideoPlayer(
    url: String,
    modifier: Modifier = Modifier
) {
    val context = LocalContext.current
    val player = remember {
        ExoPlayer.Builder(context).build()
    }

    // 自动管理生命周期
    DisposableEffect(Unit) {
        val mediaItem = MediaItem.fromUri(url)
        player.setMediaItem(mediaItem)
        player.prepare()
        player.playWhenReady = true

        onDispose {
            player.release()
        }
    }

    // 嵌入 PlayerView
    AndroidView(
        modifier = modifier,
        factory = { ctx ->
            PlayerView(ctx).apply {
                this.player = player
                useController = true
            }
        }
    )
}

你看,代码非常简洁。播放器的创建、准备、释放都在 Compose 的生命周期管理下,不会出现内存泄漏的问题。

注意:在 Compose 中使用 AndroidView 时,一定要处理好配置变更(比如屏幕旋转)的情况。我建议把播放器的状态提升到 ViewModel 中管理,这样即使 Activity 重建,播放进度也不会丢失。

如果你需要更精细的控制,比如自定义播放控制 UI,Media3 也提供了 Compose 风格的 API:

@Composable
fun CustomPlayerControls(player: Player) {
    val isPlaying by player.isPlayingAsState()
    val currentPosition by player.currentPositionAsState()
    val duration by player.durationAsState()

    Column {
        // 播放/暂停按钮
        IconButton(onClick = {
            if (isPlaying) player.pause() else player.play()
        }) {
            Icon(
                imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
                contentDescription = null
            )
        }

        // 进度条
        Slider(
            value = currentPosition.toFloat(),
            onValueChange = { player.seekTo(it.toLong()) },
            valueRange = 0f..duration.toFloat()
        )
    }
}

这里用到了 Media3 提供的 Compose 扩展函数,比如 isPlayingAsState(),它们会把播放器的状态转换成 Compose 可观察的状态,UI 会自动更新。

自适应流媒体

自适应流媒体,说白了就是根据网络状况动态调整视频质量。用户在地铁里信号不好时自动降画质,到了 Wi-Fi 环境自动升画质。这个功能在 ExoPlayer 时代就已经很成熟了,Media3 进一步优化了它。

Media3 支持主流的自适应流媒体协议:

  • DASH:Dynamic Adaptive Streaming over HTTP,最通用的标准
  • HLS:Apple 的 HTTP Live Streaming,iOS 生态必备
  • SS:Smooth Streaming,微软的方案,现在用得少了

在 Media3 中,使用自适应流媒体非常简单。你只需要提供一个自适应流媒体的 URL,Media3 会自动处理一切:

// DASH 流
val dashMediaItem = MediaItem.fromUri("https://example.com/manifest.mpd")

// HLS 流
val hlsMediaItem = MediaItem.fromUri("https://example.com/playlist.m3u8")

player.setMediaItem(dashMediaItem)
player.prepare()

Media3 内部会根据网络带宽、设备性能、屏幕尺寸等因素,自动选择最合适的码率。你不需要手动干预。

但如果你需要更精细的控制,比如限制最大码率,Media3 也提供了 API:

val trackSelector = DefaultTrackSelector(context).apply {
    setParameters(
        buildUponParameters {
            setMaxVideoSizeSd() // 限制为 SD 画质
            // 或者直接限制码率
            setMaxVideoBitrate(2_000_000) // 2 Mbps
        }
    )
}

val player = ExoPlayer.Builder(context)
    .setTrackSelector(trackSelector)
    .build()

避坑指南:我曾经在一个项目中遇到自适应流播放卡顿的问题。排查后发现是 CDN 配置的问题——服务器没有正确返回 206 Partial Content 响应。如果你遇到类似问题,建议先用 ExoPlayer 的日志功能查看网络请求详情:player.addAnalyticsListener(EventLogger())

知识体系总览

下面这张图展示了 Media3 的核心架构和本章涉及的知识点:

Media3 架构与知识体系 Media3 核心库 ExoPlayer 升级 Compose 集成 自适应流媒体 升级要点 • 包名迁移:exoplayer2 → media3 • API 简化:ExoPlayer.Builder • MediaItem 替代 MediaSource 集成方式 • AndroidView 嵌入 • 生命周期管理 • 自定义控制 UI 协议支持 • DASH (.mpd) / HLS (.m3u8) • 自动码率选择 • TrackSelector 精细控制 统一架构 · 模块化 · 声明式 UI

从这张图可以看出,Media3 的核心思想就是统一。它把播放器、UI 集成、流媒体协议支持整合到了一个清晰的架构中。你不需要再像以前那样拼凑各种库,一个 Media3 就能搞定大部分需求。

好了,关于 Media3 的核心内容就讲到这里。从 ExoPlayer 升级到 Media3 其实没有想象中那么复杂,关键是理解它的设计理念。Compose 集成让 UI 开发更符合现代 Android 开发的习惯,而自适应流媒体则是提升用户体验的关键技术。

我个人建议,新项目直接上 Media3,老项目也尽快迁移。毕竟 Google 已经明确表示 ExoPlayer 2.x 只会做 bug 修复,新功能都会在 Media3 上开发。早迁移早省心。