Administrator
发布于 2026-08-25 / 24 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 43 篇:Android Media3:ExoPlayer 的继任者

上一篇聊了 CameraX,这篇继续媒体方向。视频播放是很多 App 的核心功能——短视频、直播、视频教程、在线课程。Google 在 2023 年推出了 Media3,统一了以前散乱的 ExoPlayer、MediaSession 和播放器 API。到底 Media3 是什么?跟 ExoPlayer 什么关系?怎么迁移?

Media3 是什么

Media3 是 Jetpack 的媒体库系列(androidx.media3),替代了以前的几个独立库:

  • ExoPlayer 2.x → Media3 ExoPlayer

  • MediaSessionCompat → Media3 Session

  • Cast framework → Media3 Cast(实验性)

  • Transformer → Media3 Transformer(视频编辑/转码)

核心变化是把播放器、会话、UI 统一到一个 artifact 体系下,API 设计一致,版本同步发布。

为什么不直接用 MediaPlayer

Android 原生的 MediaPlayer 有不少硬伤:

  1. 只支持系统解码器,自定义解码器不行

  2. 状态机不透明,错误信息几乎没用

  3. 不支持 DASH、HLS 自适应流

  4. 缓冲策略不可控

  5. 多实例管理和释放容易泄漏

ExoPlayer/Media3 解决了这些,还提供了:ABR(自适应码率)、DRM 支持、广告插播、预加载等高级能力。

基础用法

// 依赖
// implementation("androidx.media3:media3-exoplayer:1.4.1")
// implementation("androidx.media3:media3-ui:1.4.1")
// implementation("androidx.media3:media3-common:1.4.1")

// 布局
// <androidx.media3.ui.PlayerView android:id="@+id/playerView" />

class VideoFragment : Fragment() {
    private var player: ExoPlayer? = null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val playerView = view.findViewById<PlayerView>(R.id.playerView)

        player = ExoPlayer.Builder(requireContext()).build().also {
            playerView.player = it
            it.setMediaItem(MediaItem.fromUri("https://example.com/video.mp4"))
            it.prepare()
            it.playWhenReady = true
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        player?.release()
        player = null
    }
}

注意:release() 必须在生命周期结束时调用,否则解码器资源泄漏。

与 ExoPlayer 2.x 的区别

维度

ExoPlayer 2.x

Media3

包名

com.google.android.exoplayer2

androidx.media3

Player 接口

ExoPlayer

ExoPlayer(继承自 Player)

MediaItem

com.google.android.exoplayer2.MediaItem

androidx.media3.common.MediaItem

UI 组件

PlayerView(exoplayer-ui)

PlayerView(media3-ui)

MediaSession

MediaSessionCompat(独立库)

MediaSession(media3-session)

迁移工作量不大但改动面广——主要是包名替换。

迁移工具

Google 提供了官方迁移脚本:

# 在项目根目录执行
./gradlew :app:migrateToMedia3

脚本会自动把 com.google.android.exoplayer2 的 import 替换为 androidx.media3,并处理 API 差异。但有些手动改动:

// ExoPlayer 2.x
val dataSourceFactory = DefaultDataSourceFactory(context, userAgent)
val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory)
    .createMediaSource(uri)
player.prepare(mediaSource)

// Media3
player.setMediaItem(MediaItem.fromUri(uri))
player.prepare()

Media3 把 MediaSource 的概念简化了,大部分场景直接用 MediaItem 就行。

自适应流(HLS/DASH)

val mediaItem = MediaItem.Builder()
    .setUri("https://example.com/stream.m3u8")
    .setMimeType(MimeTypes.APPLICATION_M3U8)
    .build()

player.setMediaItem(mediaItem)
player.prepare()

Media3 自动处理 HLS/DASH 的码率切换。默认策略是根据网络带宽选择最高可用码率。

自定义 ABR 策略:

val trackSelector = DefaultTrackSelector(context).apply {
    setParameters(
        buildUponParameters()
            .setMaxVideoSize(1280, 720) // 限制最高分辨率
            .setForceHighestSupportedBitrate(false)
    )
}

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

MediaSession:后台播放

如果你的 App 需要在后台播放音频(如音乐播放器),需要 MediaSession:

// 依赖
// implementation("androidx.media3:media3-session:1.4.1")

val mediaSession = MediaSession.Builder(context, player)
    .build()

// 在 Service 中使用
class PlaybackService : MediaSessionService() {
    private var mediaSession: MediaSession? = null

    override fun onCreate() {
        super.onCreate()
        val player = ExoPlayer.Builder(this).build()
        mediaSession = MediaSession.Builder(this, player).build()
    }

    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
        return mediaSession
    }

    override fun onDestroy() {
        mediaSession?.run {
            player.release()
            release()
        }
        super.onDestroy()
    }
}

注册 Service 后,系统通知栏会自动显示播放控制,蓝牙耳机的媒体键也能控制播放。

预加载

短视频场景需要预加载下一段视频,减少切换延迟:

val mediaSource = ProgressiveMediaSource.Factory(cacheDataSourceFactory)
    .createMediaSource(MediaItem.fromUri(nextVideoUrl))

// 用 CacheWriter 预加载
val cache = SimpleCache(
    File(context.cacheDir, "media"),
    LeastRecentlyUsedCacheEvictor(100 * 1024 * 1024) // 100MB
)

预加载是短视频流畅度的关键。但要注意缓存大小管理,定期清理。

常见坑

  1. 音频焦点。播放音频时要请求音频焦点,不然跟其他播放器同时出声:

player.setHandleAudioBecomingNoisy(true) // 拔耳机时暂停
val audioAttributes = AudioAttributes.Builder()
    .setUsage(C.USAGE_MEDIA)
    .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
    .build()
player.setAudioAttributes(audioAttributes, true) // 自动管理焦点
  1. 画面比例。PlayerView 默认 resize 模式是 RESIZE_MODE_FIT(保持比例留黑边)。全屏播放用 RESIZE_MODE_FILL 或 RESIZE_MODE_ZOOM。

  2. Codec 不支持。某些老设备不支持 H.265/AV1。用 MediaCodecUtil.getCodecInfoForMime() 检查,必要时降级到 H.264。

小结

Media3 是当前 Android 媒体播放的标准方案,新项目直接上 Media3,存量 ExoPlayer 2.x 项目用迁移工具。核心是理解 MediaItem 简化了媒体源管理,MediaSession 统一了后台播放。短视频场景别忘了预加载和缓存。

下一篇聊聊 Compose 自定义布局与 Modifier 原理。


评论