0Pricing
Android Academy · 课时

播放音频与视频

在 Android 中实现简单的媒体播放

播放音频与视频 是 CoddyKit 上的免费 Android Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Android Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Android Academy 课程共包含 4 节课。

超越图片:媒体播放

Coil 可以出色地显示图片,但应用还需要播放声音和视频:通知提示音、播客、背景视频片段或视频信息流。Android 为媒体提供了专用 API。

目前推荐使用的现代化库是 Media3 (ExoPlayer)。与旧版 MediaPlayer 相比,它在流式传输、格式处理、缓冲和播放控制方面表现更好。本课将介绍如何在 Compose 应用中播放简单的音频和视频。

添加 Media3 依赖

Media3 被拆分为多个模块。对于大多数应用,您需要 media3-exoplayer(播放器引擎)和 media3-ui(现成的播放器视图)。对于 Compose 视频,您还可以通过 AndroidView 使用 PlayerView。

// build.gradle.kts (module level)
dependencies {
    implementation("androidx.media3:media3-exoplayer:1.5.0")
    implementation("androidx.media3:media3-ui:1.5.0")
}

创建 ExoPlayer

ExoPlayer 根据 Context 创建。您为它提供一个 MediaItem(URL 或本地文件),调用 prepare(),然后开始播放。它会在自己的线程中运行,因此不会阻塞界面。

需要特别注意的是,播放器会占用系统资源,因此必须释放;稍后会详细介绍。

import android.content.Context
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer

fun createPlayer(context: Context, url: String): ExoPlayer {
    val player = ExoPlayer.Builder(context).build()
    player.setMediaItem(MediaItem.fromUri(url))
    player.prepare()
    player.playWhenReady = true
    return player
}

播放音频

仅播放音频时,设置方式完全相同,只需让 MediaItem 指向音频 URL(mp3、aac 等)。由于没有需要显示的视图,您可以通过播放器对象控制它:play()、pause()、seekTo()。

这些功能足以构建播客或音乐播放器。

import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer

fun playPodcast(player: ExoPlayer, audioUrl: String) {
    player.setMediaItem(MediaItem.fromUri(audioUrl))
    player.prepare()
    player.play()
}

fun togglePause(player: ExoPlayer) {
    if (player.isPlaying) player.pause() else player.play()
}

在 Compose 中记住播放器

在 Compose 中,您应该只创建一次播放器,并让它在多次重组之间保持不变。请使用 remember,避免每次重组都创建新的播放器。

请将其与正确的清理操作结合使用(下一节会介绍),以免泄漏资源。

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer

@Composable
fun rememberPlayer(url: String): ExoPlayer {
    val context = LocalContext.current
    return remember {
        ExoPlayer.Builder(context).build().apply {
            setMediaItem(MediaItem.fromUri(url))
            prepare()
        }
    }
}

释放播放器

播放器会持续占用编解码器、缓冲区和音频焦点。如果忘记释放它,就会造成内存泄漏,甚至可能在屏幕消失后仍继续播放。在 Compose 中,当可组合项离开组合时,DisposableEffect 会执行清理操作。

请务必在那里调用 player.release()。

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.media3.exoplayer.ExoPlayer

@Composable
fun ManagedPlayer(player: ExoPlayer) {
    DisposableEffect(player) {
        onDispose {
            player.release()
        }
    }
    // ... use player here
}

使用 PlayerView 显示视频

Media3 提供了带控制功能的现成 PlayerView。Compose 没有原生的视频可组合项,因此您需要使用 AndroidView 嵌入视图并附加播放器。

这样就可以直接获得播放/暂停、进度条和全屏功能。

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView

@Composable
fun VideoPlayer(player: ExoPlayer, modifier: Modifier = Modifier) {
    AndroidView(
        factory = { context ->
            PlayerView(context).apply {
                this.player = player
            }
        },
        modifier = modifier
    )
}

完整的视频屏幕

让我们把这些内容整合起来:创建播放器、记住播放器、将其附加到 PlayerView,并在释放时销毁它。这就是一个可以重复使用的独立视频屏幕。

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView

@Composable
fun VideoScreen(videoUrl: String) {
    val context = LocalContext.current
    val player = remember {
        ExoPlayer.Builder(context).build().apply {
            setMediaItem(MediaItem.fromUri(videoUrl))
            prepare()
            playWhenReady = true
        }
    }
    DisposableEffect(Unit) { onDispose { player.release() } }

    AndroidView(
        factory = { PlayerView(it).apply { this.player = player } },
        modifier = Modifier.fillMaxWidth().height(220.dp)
    )
}

跟踪播放状态

要更新界面(例如播放/暂停图标或进度条),您可以通过 Player.Listener 监听播放器。它会报告缓冲、准备就绪和播放结束等状态变化,以及当前是否正在播放。

在屏幕出现时添加监听器,并在释放时移除它。

import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer

fun observe(player: ExoPlayer, onPlaying: (Boolean) -> Unit): Player.Listener {
    val listener = object : Player.Listener {
        override fun onIsPlayingChanged(isPlaying: Boolean) {
            onPlaying(isPlaying)
        }
        override fun onPlaybackStateChanged(state: Int) {
            when (state) {
                Player.STATE_BUFFERING -> { /* show spinner */ }
                Player.STATE_READY -> { /* ready */ }
                Player.STATE_ENDED -> { /* finished */ }
            }
        }
    }
    player.addListener(listener)
    return listener
}

应用进入后台时暂停

对于内嵌视频(而非音乐应用),通常希望用户离开屏幕时暂停,返回时继续播放。您可以通过观察 Lifecycle 将播放与 Compose 生命周期关联起来,并在 ON_PAUSE 时暂停。

这样可以避免播放用户看不到的视频,同时节省电量。

import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.media3.exoplayer.ExoPlayer

@Composable
fun PauseWithLifecycle(player: ExoPlayer) {
    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_PAUSE -> player.pause()
                Lifecycle.Event.ON_RESUME -> player.play()
                else -> {}
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
    }
}

使用 MediaSession 播放后台音频

对于应用进入后台后仍应继续播放的音频(音乐、播客),请将播放功能封装在 MediaSessionService 中。这样可以与系统控制、锁屏和蓝牙按钮集成。

实现过程更复杂一些,但核心思路是通过 MediaSession 对外提供播放器。

import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService

class PlaybackService : MediaSessionService() {
    private var session: MediaSession? = null

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

    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = session

    override fun onDestroy() {
        session?.run { player.release(); release() }
        session = null
        super.onDestroy()
    }
}

快速检查

在包含 ExoPlayer 的 Compose 屏幕中,当屏幕离开组合时,应使用哪个 Compose API 来释放播放器?

回顾

您已经添加了音频和视频播放功能:

  • 使用现代化的 Media3 ExoPlayer,它是 MediaPlayer 的替代方案。
  • 创建播放器,设置 MediaItem,调用 prepare(),然后调用 play()。
  • 在 Compose 中使用 remember 记住播放器,并在 DisposableEffect 的 onDispose 中释放它。
  • 通过使用 AndroidView 嵌入 PlayerView 来显示视频。
  • 使用 Player.Listener 跟踪状态;使用 MediaSessionService 实现后台音频。

至此,您已经完成了“使用 Coil 加载图片和媒体”课程。现在,您可以流畅地加载图片,并以现代 Android 方式播放媒体。

常见问题解答

「播放音频与视频」课时是免费的吗?

是的 — 「播放音频与视频」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Android Academy 课程的其余内容,请升级到 CoddyKit PRO。 Android Academy 课程共包含 4 节课。

「播放音频与视频」这节课中我会学到什么?

在 Android 中实现简单的媒体播放 你通过在浏览器中直接运行的动手代码来练习 Android Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Android Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Android Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「播放音频与视频」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Android Academy 课中编写并运行代码吗?

能。每节 Android Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 使用 Coil 加载图片
  2. 占位符与错误状态
  3. 缓存与性能
  4. 播放音频与视频
← 返回 Android Academy