
文章目录
-
- [🎯 本阶段学习目标](#🎯 本阶段学习目标)
- [📅 1. MediaPlayer基础](#📅 1. MediaPlayer基础)
-
- [🎵 什么是MediaPlayer?](#🎵 什么是MediaPlayer?)
-
- [🔍 MediaPlayer的状态机](#🔍 MediaPlayer的状态机)
- [💻 基础音频播放实现](#💻 基础音频播放实现)
- [📅 2. VideoView视频播放](#📅 2. VideoView视频播放)
-
- [📺 什么是VideoView?](#📺 什么是VideoView?)
-
- [💻 基础视频播放实现](#💻 基础视频播放实现)
- [📱 布局文件](#📱 布局文件)
- [📅 3. 音频焦点管理](#📅 3. 音频焦点管理)
-
- [🔊 什么是音频焦点?](#🔊 什么是音频焦点?)
-
- [💻 音频焦点管理实现](#💻 音频焦点管理实现)
- [📅 4. 生命周期整合](#📅 4. 生命周期整合)
-
- [🔄 为什么要管理生命周期?](#🔄 为什么要管理生命周期?)
-
- [💻 完整的生命周期管理](#💻 完整的生命周期管理)
- [🎓 第二阶段总结](#🎓 第二阶段总结)
-
- [✅ 你已经掌握的技能](#✅ 你已经掌握的技能)
从理论走向实践,开始编写真正的代码 📱💻
掌握Android平台的基础音视频API
🎯 本阶段学习目标
学完这个阶段,你将能够:
- 🎵 使用MediaPlayer播放音频:掌握基础音频播放
- 📺 使用VideoView播放视频:实现简单视频播放
- 🔊 管理音频焦点:处理多应用音频冲突
- 🔄 正确管理生命周期:避免内存泄漏和崩溃
📅 1. MediaPlayer基础
🎵 什么是MediaPlayer?
生活比喻 :MediaPlayer就像是一台简单的CD播放机
text
💿 音频文件 → 🎮 MediaPlayer → 🔊 扬声器播放
🔍 MediaPlayer的状态机
MediaPlayer有一个复杂的状态机,理解它很重要:
kotlin
/**
* 🎮 MediaPlayer状态机详解
*/
class MediaPlayerStates {
/**
* MediaPlayer就像一台有很多按钮的机器
* 每个按钮只能在特定状态下按,否则会出错
*/
enum class State(val description: String, val allowedOperations: List<String>) {
IDLE("空闲状态", listOf("setDataSource")),
INITIALIZED("已初始化", listOf("prepare", "prepareAsync")),
PREPARING("准备中", listOf("等待准备完成")),
PREPARED("准备完成", listOf("start", "seekTo", "setLooping")),
STARTED("播放中", listOf("pause", "stop", "seekTo")),
PAUSED("暂停中", listOf("start", "stop", "seekTo")),
STOPPED("已停止", listOf("prepare", "prepareAsync")),
PLAYBACK_COMPLETED("播放完成", listOf("start", "stop", "seekTo")),
ERROR("错误状态", listOf("reset")),
END("已释放", listOf("无操作"))
}
/**
* 🚨 常见错误:在错误的状态调用方法
*/
fun showCommonMistakes() {
println("❌ 常见错误示例:")
println("1. 在IDLE状态调用start() → 崩溃")
println("2. 在PREPARING状态调用pause() → 崩溃")
println("3. 忘记调用prepare() → 崩溃")
println("4. 忘记调用release() → 内存泄漏")
}
}
💻 基础音频播放实现
kotlin
/**
* 🎵 简单音频播放器
*/
class SimpleAudioPlayer {
private var mediaPlayer: MediaPlayer? = null
/**
* 播放本地音频文件
*
* @param context 上下文对象,用于访问应用资源
* @param rawResourceId 音频资源ID,位于res/raw目录下
*
* 使用场景:播放应用内置的音效、背景音乐等
* 优点:不需要网络,加载速度快
* 缺点:会增加APK大小
*/
fun playLocalAudio(context: Context, rawResourceId: Int) {
try {
// 第一步:创建MediaPlayer实例
// MediaPlayer.create()是便捷方法,会自动完成setDataSource()和prepare()
mediaPlayer = MediaPlayer.create(context, rawResourceId)
// 第二步:设置播放完成监听器
// 当音频播放到结尾时会触发此回调
mediaPlayer?.setOnCompletionListener { mp ->
println("🎵 播放完成")
releasePlayer() // 播放完成后释放资源,避免内存泄漏
}
// 设置错误监听器
// 当播放过程中发生错误时会触发此回调
// what: 错误类型,extra: 额外错误信息
mediaPlayer?.setOnErrorListener { mp, what, extra ->
println("❌ 播放错误: what=$what, extra=$extra")
releasePlayer() // 发生错误时也要释放资源
true // 返回true表示已处理错误,不会再抛出异常
}
// 第三步:开始播放
// 由于使用了MediaPlayer.create(),此时已经是PREPARED状态,可以直接start()
mediaPlayer?.start()
println("▶️ 开始播放音频")
} catch (e: Exception) {
println("❌ 播放失败: ${e.message}")
releasePlayer()
}
}
/**
* 播放网络音频文件
*
* @param url 网络音频URL,支持HTTP/HTTPS协议
*
* 使用场景:播放在线音乐、播客、网络电台等
* 注意事项:
* 1. 需要网络权限:<uses-permission android:name="android.permission.INTERNET" />
* 2. 需要处理网络异常情况
* 3. 建议使用异步准备,避免阻塞UI线程
*/
fun playNetworkAudio(url: String) {
try {
mediaPlayer = MediaPlayer().apply {
// 设置网络音频数据源
// 这会将MediaPlayer从IDLE状态转换到INITIALIZED状态
setDataSource(url)
// 异步准备音频数据(网络音频强烈推荐使用异步)
// 同步prepare()会阻塞UI线程,可能导致ANR
// 异步prepareAsync()不会阻塞,准备完成后会回调OnPreparedListener
prepareAsync()
// 设置准备完成监听器
// 只有在此回调中才能安全地调用start()方法
setOnPreparedListener { mp ->
println("✅ 准备完成,开始播放")
mp.start() // 从PREPARED状态转换到STARTED状态
}
// 设置播放完成监听器
setOnCompletionListener { mp ->
println("🎵 播放完成")
releasePlayer() // 自动释放资源
}
// 设置错误处理监听器
// 网络音频更容易出错:网络中断、URL无效、格式不支持等
setOnErrorListener { mp, what, extra ->
println("❌ 播放错误: what=$what, extra=$extra")
releasePlayer()
true // 返回true表示错误已被处理
}
}
} catch (e: Exception) {
println("❌ 播放失败: ${e.message}")
releasePlayer()
}
}
/**
* 暂停音频播放
*
* 只有在STARTED状态下才能调用pause()
* 暂停后MediaPlayer进入PAUSED状态,可以通过start()恢复播放
*/
fun pauseAudio() {
// 使用takeIf确保只有在播放状态下才暂停,避免IllegalStateException
mediaPlayer?.takeIf { it.isPlaying }?.pause()
println("⏸️ 暂停播放")
}
/**
* 恢复音频播放
*
* 从PAUSED状态恢复到STARTED状态
* 会从暂停的位置继续播放,而不是从头开始
*/
fun resumeAudio() {
// 确保MediaPlayer存在且当前不在播放状态
mediaPlayer?.takeIf { !it.isPlaying }?.start()
println("▶️ 继续播放")
}
/**
* 停止音频播放
*
* 停止后MediaPlayer进入STOPPED状态
* 如果要重新播放,需要重新调用prepare()或prepareAsync()
* 与pause()不同,stop()后无法直接start()恢复
*/
fun stopAudio() {
mediaPlayer?.stop()
println("⏹️ 停止播放")
}
/**
* 🧹 释放MediaPlayer资源(非常重要!)
*
* 为什么必须调用release():
* 1. MediaPlayer占用系统底层资源(音频解码器、缓冲区等)
* 2. 这些资源数量有限,不释放会导致其他应用无法播放音频
* 3. 可能导致内存泄漏和性能问题
*
* 调用时机:
* - Activity/Fragment的onDestroy()中
* - 播放完成后
* - 发生错误时
* - 不再需要播放器时
*/
fun releasePlayer() {
mediaPlayer?.release() // 释放底层资源,MediaPlayer进入END状态
mediaPlayer = null // 清空引用,帮助GC回收
println("🧹 播放器资源已释放")
}
}
📅 2. VideoView视频播放
📺 什么是VideoView?
生活比喻 :VideoView就像是一台简单的电视机
text
📹 视频文件 → 📺 VideoView → 🖥️ 屏幕显示
💻 基础视频播放实现
kotlin
/**
* 📺 简单视频播放器Activity
*/
class SimpleVideoActivity : AppCompatActivity() {
private lateinit var videoView: VideoView
private lateinit var mediaController: MediaController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_simple_video)
initVideoView()
playVideo()
}
/**
* 初始化VideoView组件和相关设置
*
* VideoView是基于MediaPlayer和SurfaceView的封装,提供了简单的视频播放功能
*/
private fun initVideoView() {
videoView = findViewById(R.id.video_view)
// 创建媒体控制器(提供播放、暂停、进度条等UI控件)
// MediaController会在用户点击屏幕时显示,几秒后自动隐藏
mediaController = MediaController(this)
mediaController.setAnchorView(videoView) // 设置控制器的附着视图
videoView.setMediaController(mediaController) // 将控制器绑定到VideoView
// 设置视频准备完成监听器
// 当视频数据准备完成后会触发此回调
videoView.setOnPreparedListener { mediaPlayer ->
println("✅ 视频准备完成")
// 设置视频缩放模式
// SCALE_TO_FIT: 保持宽高比,缩放到适合尺寸
// SCALE_TO_FIT_WITH_CROPPING: 填满屏幕,可能裁剪部分内容
mediaPlayer.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT)
}
// 设置视频播放完成监听器
videoView.setOnCompletionListener { mediaPlayer ->
println("🎬 视频播放完成")
// 可以在这里处理播放完成后的逻辑,比如跳转到下一个视频
}
// 设置视频播放错误监听器
videoView.setOnErrorListener { mediaPlayer, what, extra ->
println("❌ 视频播放错误: what=$what, extra=$extra")
// 常见错误:网络问题、文件格式不支持、文件损坏等
true // 返回true表示错误已被处理
}
}
/**
* 播放视频文件
*
* VideoView支持多种视频源:本地文件、网络文件、内容提供者等
*/
private fun playVideo() {
// 方式1:播放应用内置的本地视频文件
// 视频文件需要放在res/raw目录下
// android.resource://协议用于访问应用资源
val uri = Uri.parse("android.resource://$packageName/${R.raw.sample_video}")
videoView.setVideoURI(uri)
// 方式2:播放网络视频文件
// 需要网络权限和处理网络异常
// val networkUri = Uri.parse("https://example.com/video.mp4")
// videoView.setVideoURI(networkUri)
// 方式3:播放本地存储的视频文件
// val localFile = File(Environment.getExternalStorageDirectory(), "video.mp4")
// val fileUri = Uri.fromFile(localFile)
// videoView.setVideoURI(fileUri)
// 开始播放视频
// VideoView会自动调用prepare()和start()
videoView.start()
}
/**
* Activity暂停时的处理
*
* 当Activity进入后台或被其他Activity遮挡时会调用
* 需要暂停视频播放以节省资源和电量
*/
override fun onPause() {
super.onPause()
// 检查视频是否正在播放,如果是则暂停
// 这样可以避免在后台继续播放视频,浪费电量和流量
if (videoView.isPlaying) {
videoView.pause()
}
}
/**
* Activity销毁时的处理
*
* 当Activity被销毁时必须释放视频播放器资源
* 避免内存泄漏和资源泄漏
*/
override fun onDestroy() {
super.onDestroy()
// 停止视频播放并释放相关资源
// stopPlayback()会停止播放并释放底层MediaPlayer资源
videoView.stopPlayback()
}
}
📱 布局文件
xml
<!-- activity_simple_video.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<VideoView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
📅 3. 音频焦点管理
🔊 什么是音频焦点?
生活比喻 :音频焦点就像是话语权
text
🎵 音乐APP正在播放
📞 电话来了 → 抢夺话语权 → 音乐暂停
📞 电话结束 → 归还话语权 → 音乐继续
💻 音频焦点管理实现
kotlin
/**
* 🔊 音频焦点管理器
*/
class AudioFocusManager(private val context: Context) {
private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
private var mediaPlayer: MediaPlayer? = null
/**
* 音频焦点变化监听器
*
* 当系统音频焦点发生变化时会回调此监听器
* 应用需要根据不同的焦点变化类型做出相应的处理
*/
private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> {
// 获得音频焦点(完全获得或重新获得)
// 场景:应用启动播放、其他应用释放焦点后
println("🔊 获得音频焦点,恢复播放")
mediaPlayer?.start() // 开始或恢复播放
mediaPlayer?.setVolume(1.0f, 1.0f) // 恢复正常音量
}
AudioManager.AUDIOFOCUS_LOSS -> {
// 永久失去音频焦点
// 场景:用户打开了其他音乐应用、接听电话等
// 应该停止播放并释放资源
println("🔇 永久失去音频焦点,停止播放")
mediaPlayer?.stop() // 停止播放
releaseAudioFocus() // 释放音频焦点
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
// 暂时失去音频焦点
// 场景:通知声音、短信提示音、导航语音等
// 应该暂停播放,等待重新获得焦点
println("⏸️ 暂时失去音频焦点,暂停播放")
mediaPlayer?.pause() // 暂停播放,保持播放位置
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
// 暂时失去焦点,但可以降低音量继续播放("ducking")
// 场景:导航提示、语音助手等短暂的音频
// 可以降低音量继续播放,不需要完全停止
println("🔉 降低音量继续播放")
mediaPlayer?.setVolume(0.3f, 0.3f) // 降低到30%音量
}
}
}
/**
* 请求音频焦点
*
* @return Boolean 是否成功获得音频焦点
*
* 在开始播放音频前必须先请求音频焦点
* 这是Android音频系统的礼貌约定,确保不同应用间的音频不会冲突
*/
fun requestAudioFocus(): Boolean {
val result = audioManager.requestAudioFocus(
audioFocusChangeListener, // 焦点变化监听器
AudioManager.STREAM_MUSIC, // 音频流类型(音乐流)
AudioManager.AUDIOFOCUS_GAIN // 请求的焦点类型(完全获得)
)
// 检查是否成功获得音频焦点
// AUDIOFOCUS_REQUEST_GRANTED: 成功获得
// AUDIOFOCUS_REQUEST_FAILED: 获取失败
// AUDIOFOCUS_REQUEST_DELAYED: 延迟获得(API 26+)
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
}
/**
* 释放音频焦点
*
* 当不再需要播放音频时应该主动释放音频焦点
* 这样其他应用就可以获得音频焦点进行播放
*
* 调用时机:
* - 播放完成时
* - 用户主动停止播放时
* - Activity销毁时
* - 发生播放错误时
*/
fun releaseAudioFocus() {
// 放弃音频焦点,传入之前注册的监听器
audioManager.abandonAudioFocus(audioFocusChangeListener)
}
/**
* 带音频焦点管理的播放器
*
* @param audioUrl 音频文件URL
*
* 这是一个完整的音频播放流程示例:
* 1. 先请求音频焦点
* 2. 获得焦点后才开始播放
* 3. 播放过程中响应焦点变化
* 4. 出错时释放焦点
*/
fun playWithAudioFocus(audioUrl: String) {
// 第一步:请求音频焦点
// 只有获得焦点后才能开始播放,这是良好的用户体验
if (requestAudioFocus()) {
try {
// 第二步:创建并配置MediaPlayer
mediaPlayer = MediaPlayer().apply {
setDataSource(audioUrl)
prepareAsync() // 异步准备
// 准备完成后开始播放
setOnPreparedListener { mp ->
mp.start()
println("▶️ 开始播放(已获得音频焦点)")
}
// 播放完成后释放焦点
setOnCompletionListener { mp ->
releaseAudioFocus()
}
// 出错时也要释放焦点
setOnErrorListener { mp, what, extra ->
releaseAudioFocus()
true
}
}
} catch (e: Exception) {
println("❌ 播放失败: ${e.message}")
releaseAudioFocus() // 出错时释放焦点
}
} else {
println("❌ 无法获得音频焦点,播放失败")
// 可能的原因:其他应用正在播放重要音频(如通话)
}
}
}
📅 4. 生命周期整合
🔄 为什么要管理生命周期?
生活比喻 :就像离开房间要关灯一样
text
🏠 进入房间 → 💡 开灯 → 🚶♂️ 离开房间 → 💡 关灯
📱 进入页面 → ▶️ 开始播放 → 🔄 离开页面 → ⏹️ 停止播放
💻 完整的生命周期管理
kotlin
/**
* 🔄 生命周期感知的音视频播放器
*/
class LifecycleAwarePlayer : AppCompatActivity() {
private var mediaPlayer: MediaPlayer? = null
private var videoView: VideoView? = null
private lateinit var audioFocusManager: AudioFocusManager
// 记录播放状态
private var isPlaying = false
private var currentPosition = 0
/**
* Activity创建时的初始化工作
*
* 这是Activity的第一个生命周期方法,只会调用一次
* 在这里进行一次性的初始化工作
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_player)
// 初始化音频焦点管理器
audioFocusManager = AudioFocusManager(this)
// 初始化播放器组件(但不开始播放)
initPlayer()
println("🔄 onCreate: 初始化播放器")
}
/**
* Activity变为可见状态
*
* 当Activity从不可见变为可见时调用
* 可以在这里做一些准备工作,但不要开始播放
*/
override fun onStart() {
super.onStart()
println("🔄 onStart: 页面可见")
// 可以在这里做一些准备工作,比如检查网络状态
}
/**
* Activity获得焦点,变为活跃状态
*
* 这是用户可以与Activity交互的状态
* 在这里恢复播放是最合适的
*/
override fun onResume() {
super.onResume()
println("🔄 onResume: 页面获得焦点")
// 恢复之前的播放状态
// 只有在之前正在播放的情况下才恢复
if (isPlaying) {
resumePlayback()
}
}
/**
* Activity失去焦点,进入暂停状态
*
* 当其他Activity覆盖当前Activity或用户按下主页键时调用
* 必须在这里暂停播放,节省资源和电量
*/
override fun onPause() {
super.onPause()
println("🔄 onPause: 页面失去焦点")
// 第一步:保存当前播放状态和位置
// 这样在onResume()时可以恢复到相同的状态
savePlaybackState()
// 第二步:暂停播放
// 避免在后台继续播放,浪费电量和流量
pausePlayback()
}
/**
* Activity变为不可见状态
*
* 当Activity完全被其他Activity遮挡时调用
* 可以在这里停止一些不必要的后台任务
*/
override fun onStop() {
super.onStop()
println("🔄 onStop: 页面不可见")
// 可以在这里停止一些耗资源的操作,比如停止位置更新
}
/**
* Activity被销毁
*
* 这是Activity的最后一个生命周期方法
* 必须在这里释放所有资源,避免内存泄漏
*/
override fun onDestroy() {
super.onDestroy()
println("🔄 onDestroy: 页面销毁")
// 释放所有资源:MediaPlayer、VideoView、音频焦点等
// 这是防止内存泄漏的关键步骤
releaseAllResources()
}
/**
* 保存当前播放状态和位置
*
* 在onPause()中调用,保存播放状态以便在onResume()中恢复
* 这样可以提供更好的用户体验,用户回到应用时可以继续之前的播放
*/
private fun savePlaybackState() {
// 保存MediaPlayer的状态
mediaPlayer?.let { player ->
isPlaying = player.isPlaying // 是否正在播放
currentPosition = player.currentPosition // 当前播放位置(毫秒)
}
// 保存VideoView的状态
videoView?.let { video ->
if (video.isPlaying) {
isPlaying = true
currentPosition = video.currentPosition
}
}
}
/**
* 暂停当前播放
*
* 在onPause()中调用,暂停所有正在播放的媒体
* 暂停后可以通过resumePlayback()恢复播放
*/
private fun pausePlayback() {
// 暂停音频播放器
mediaPlayer?.pause()
// 暂停视频播放器
videoView?.pause()
}
/**
* 恢复之前的播放状态
*
* 在onResume()中调用,恢复到之前保存的播放位置和状态
* 只有在之前正在播放的情况下才会恢复播放
*/
private fun resumePlayback() {
// 恢复音频播放器
mediaPlayer?.let { player ->
player.seekTo(currentPosition) // 跳转到之前的播放位置
player.start() // 开始播放
}
// 恢复视频播放器
videoView?.let { video ->
video.seekTo(currentPosition) // 跳转到之前的播放位置
video.start() // 开始播放
}
}
/**
* 释放所有媒体播放相关资源
*
* 在onDestroy()中调用,确保所有资源都被正确释放
* 这是防止内存泄漏的关键步骤
*/
private fun releaseAllResources() {
// 释放MediaPlayer资源
mediaPlayer?.release() // 释放底层音频解码器资源
mediaPlayer = null // 清空引用
// 释放VideoView资源
videoView?.stopPlayback() // 停止播放并释放资源
videoView = null // 清空引用
// 释放音频焦点
audioFocusManager.releaseAudioFocus() // 让其他应用可以获得音频焦点
println("🧹 所有资源已释放")
}
}
🎓 第二阶段总结
✅ 你已经掌握的技能
🎵 MediaPlayer使用:
- 理解了MediaPlayer的状态机
- 能够播放本地和网络音频
- 掌握了基础的播放控制
📺 VideoView使用:
- 能够播放本地和网络视频
- 了解了MediaController的使用
- 掌握了基础的视频播放
🔊 音频焦点管理:
- 理解了音频焦点的概念
- 能够正确处理音频冲突
- 实现了礼貌的音频播放
🔄 生命周期管理:
- 理解了Android生命周期
- 能够正确管理播放器资源
- 避免了内存泄漏问题
💡 学习建议 :
一定要动手实践!理论再多,不如写一行代码。
每个示例都要运行一遍,遇到问题就是学习的机会! 🛠️