✅作者简介:大家好,我是 Meteors., 向往着更加简洁高效的代码写法与编程方式,持续分享Java技术内容。
🍎个人主页:Meteors.的博客
💞当前专栏:知识分享
✨特色专栏:知识分享
🥭本文内容:安卓进阶------多媒体
📚 ** ps ** :阅读文章如果有问题或者疑惑,欢迎在评论区提问或指出。
目录
[一. MediaPlayer](#一. MediaPlayer)
[(二)基本使用步骤(以播放 Raw 资源为例)](#(二)基本使用步骤(以播放 Raw 资源为例))
[1. 创建 MediaPlayer 实例](#1. 创建 MediaPlayer 实例)
[2. 设置数据源](#2. 设置数据源)
[3. 准备播放器](#3. 准备播放器)
[4. 开始播放](#4. 开始播放)
[5. 管理播放状态](#5. 管理播放状态)
[6. 释放资源(非常重要!)](#6. 释放资源(非常重要!))
[(三)MediaPlayer 的生命周期](#(三)MediaPlayer 的生命周期)
[二. SoundPool](#二. SoundPool)
[1. 创建 SoundPool 对象](#1. 创建 SoundPool 对象)
[2. 加载音频资源](#2. 加载音频资源)
[3. 设置加载完成监听](#3. 设置加载完成监听)
[4. 播放音频](#4. 播放音频)
[5. 控制播放](#5. 控制播放)
[6. 资源释放](#6. 资源释放)
[(四) 注意事项](#(四) 注意事项)
[三. VideoView](#三. VideoView)
[1. 布局文件中添加 VideoView](#1. 布局文件中添加 VideoView)
[2. 基本配置和播放控制](#2. 基本配置和播放控制)
[1. 自定义视频播放器](#1. 自定义视频播放器)
[2. 完整的视频播放器 Activity](#2. 完整的视频播放器 Activity)
[3. 对应的布局文件](#3. 对应的布局文件)
[1. 添加网络权限](#1. 添加网络权限)
[2. 处理网络视频缓冲](#2. 处理网络视频缓冲)
[1. 权限管理](#1. 权限管理)
[2. 格式支持:](#2. 格式支持:)
[3. 内存管理](#3. 内存管理)
[4. 网络视频](#4. 网络视频)
[5. 生命周期](#5. 生命周期)
[四. MediaRecorder](#四. MediaRecorder)
[1. 权限配置](#1. 权限配置)
[2. 音频录制基础实现](#2. 音频录制基础实现)
[3. 视频录制基础实现](#3. 视频录制基础实现)
[1. 完整的录制管理器](#1. 完整的录制管理器)
[2. 完整的录制 Activity 实现](#2. 完整的录制 Activity 实现)
[五. AudioRecord](#五. AudioRecord)
[1. 音频参数说明](#1. 音频参数说明)
[2. 缓冲区计算](#2. 缓冲区计算)
[1. 权限配置](#1. 权限配置)
[2. 基础 AudioRecord 实现](#2. 基础 AudioRecord 实现)
[1. 完整的音频录制管理器](#1. 完整的音频录制管理器)
[2. 实时音频分析示例](#2. 实时音频分析示例)
[3. 完整的录音 Activity](#3. 完整的录音 Activity)
[六. AudioTrack](#六. AudioTrack)
[1. 基本配置](#1. 基本配置)
[2. 播放音频数据](#2. 播放音频数据)
[3. 流模式实时播放](#3. 流模式实时播放)
[4. 静态模式播放(适合短音效)](#4. 静态模式播放(适合短音效))
[5. 音量控制](#5. 音量控制)
[七. MediaRouter](#七. MediaRouter)
[1. 添加依赖](#1. 添加依赖)
[2. 基本配置](#2. 基本配置)
[3. 设备选择和播放控制](#3. 设备选择和播放控制)
[4. 设备切换对话框](#4. 设备切换对话框)
[5. Chromecast 集成](#5. Chromecast 集成)
[6. 音频焦点管理](#6. 音频焦点管理)
一. MediaPlayer
(一)介绍
MediaPlayer是 Android 系统提供的一个用于播放音频和视频文件的核心类。它功能强大,可以处理多种媒体源,包括:
本地资源 :如
res/raw目录下的文件。本地文件系统:如设备内部存储或 SD 卡中的文件。
网络流媒体:通过 HTTP/HTTPS 协议在线播放音频/视频。
应用资源:通过 URI 指定的内容。
支持的格式:常见格式如 MP3, MP4, 3GP, OGG, WAV, AAC 等。但具体支持情况因设备硬件和系统版本而异。
(二)基本使用步骤(以播放 Raw 资源为例)
播放一个媒体文件最直接的流程如下:
1. 创建 MediaPlayer 实例
Kotlin// 或者使用 create 快捷方法(对于资源文件) MediaPlayer mediaPlayer = new MediaPlayer(); // MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.my_sound);2. 设置数据源
这是最关键的一步,告诉 MediaPlayer 要播放什么。
Kotlin// 示例1:播放 res/raw 下的音频 AssetFileDescriptor afd = context.getResources().openRawResourceFd(R.raw.my_sound); mediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); afd.close(); // 示例2:播放网络音频(需要声明网络权限) mediaPlayer.setDataSource("https://example.com/audio.mp3"); // 示例3:播放本地文件 mediaPlayer.setDataSource("/sdcard/Music/song.mp3");注意 :使用网络源必须要在
AndroidManifest.xml中添加<uses-permission android:name="android.permission.INTERNET"/>权限。3. 准备播放器
在设置好数据源后,需要让 MediaPlayer 进入准备状态。有两种方式:
(1)同步准备 :
prepare()这是一个阻塞调用,会一直等待直到媒体准备完毕。不能在主线程(UI线程)中调用 ,否则会引发
NetworkOnMainThreadException(对于网络源)或导致界面卡顿。
KotlinmediaPlayer.prepare(); // 必须在后台线程执行 (2)异步准备 :
prepareAsync()这是推荐的方式。它会立即返回,并在后台准备媒体。你需要设置一个监听器来接收准备完成的通知。
KotlinmediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { // 媒体准备就绪,可以开始播放了 mediaPlayer.start(); } }); mediaPlayer.prepareAsync(); // 非阻塞,安全在主线程调用4. 开始播放
KotlinmediaPlayer.start(); // 开始或恢复播放5. 管理播放状态
KotlinmediaPlayer.pause(); // 暂停播放 mediaPlayer.stop(); // 停止播放,之后需要重新 prepare 才能再次 start mediaPlayer.seekTo(10000); // 跳转到第10秒(单位:毫秒)6. 释放资源(非常重要!)
MediaPlayer 会占用系统稀缺的资源(如编解码器)。当不再使用时,必须释放它。
KotlinmediaPlayer.release(); mediaPlayer = null;通常这个操作在 Activity 的
onDestroy()方法中执行。
(三)MediaPlayer 的生命周期
理解 MediaPlayer 的状态机至关重要,可以避免很多错误(例如在错误的状态下调用方法)。其生命周期可以简化为以下核心状态:
Idle(空闲) : 通过
new创建后的状态。此时尚未设置数据源。Initialized(已初始化) : 调用
setDataSource()后的状态。Preparing / Prepared(准备中/已就绪):
调用
prepareAsync()后进入 Preparing 状态。准备完成后(
onPrepared被调用)进入 Prepared 状态。此时可以调用start()。Started(已开始) : 调用
start()后,媒体正在播放。可以调用isPlaying()来检查。Paused(已暂停) : 调用
pause()后的状态。可以从这里恢复播放。Stopped(已停止) : 调用
stop()后的状态。要重新播放,必须再次调用prepare()或prepareAsync()。PlaybackCompleted(播放完成) : 媒体播放完毕。可以设置循环播放(
setLooping(true))或通过OnCompletionListener来监听。Error(错误) : 发生错误时进入此状态。应该设置
OnErrorListener来处理错误。End(结束) : 调用
release()后进入此状态。MediaPlayer 不再可用。关键点 :从
Stopped状态重新开始,必须再次prepare。调用reset()方法可以将 MediaPlayer 重置到Idle状态。
(三)完整代码示例
Kotlinpublic class MainActivity extends AppCompatActivity { private MediaPlayer mediaPlayer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button playButton = findViewById(R.id.play_button); Button pauseButton = findViewById(R.id.pause_button); // 1. 使用 create 方法创建,它已经同步准备好了资源,无需再调用 prepare() mediaPlayer = MediaPlayer.create(this, R.raw.my_sound); // 设置播放完成监听 mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { // 播放结束,释放资源 releaseMediaPlayer(); Toast.makeText(MainActivity.this, "播放完成", Toast.LENGTH_SHORT).show(); } }); playButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mediaPlayer != null && !mediaPlayer.isPlaying()) { mediaPlayer.start(); } } }); pauseButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mediaPlayer != null && mediaPlayer.isPlaying()) { mediaPlayer.pause(); } } }); } @Override protected void onDestroy() { super.onDestroy(); // 确保在 Activity 销毁时释放 MediaPlayer releaseMediaPlayer(); } /** * 释放 MediaPlayer 资源的辅助方法 */ private void releaseMediaPlayer() { if (mediaPlayer != null) { mediaPlayer.release(); mediaPlayer = null; } } }
(四)最佳实践和注意事项
及时释放资源 :这是最重要的原则。在 Activity/Fragment 的
onPause()或onDestroy()中释放 MediaPlayer。否则会导致内存泄漏和资源占用。使用异步准备 :对于本地大文件或网络流,始终使用
prepareAsync()和OnPreparedListener,避免阻塞 UI 线程。处理音频焦点(Audio Focus) :在播放音频前,申请音频焦点。当有来电或其他应用播放声音时,根据音频焦点的变化做出相应处理(如暂停播放、降低音量),这是良好的用户体验。这需要用到
AudioManager。处理错误 :务必设置
OnErrorListener,以便在媒体播放出错时能做出响应(如给用户提示)。
KotlinmediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { @Override public boolean onError(MediaPlayer mp, int what, int extra) { // 'what' 和 'extra' 是错误代码 Log.e("MediaPlayer", "Error: " + what + ", " + extra); releaseMediaPlayer(); return true; // 返回 true 表示错误已处理 } });使用
reset() :如果你想用同一个 MediaPlayer 实例播放另一个数据源,先调用reset()将其重置到Idle状态,然后再设置新的数据源并prepare。
二. SoundPool
(一)介绍
SoundPool 是 Android 中用于播放短音频片段的类,特别适合播放需要低延迟、快速响应的音效(如游戏音效、按键声等)。
(二)特点
低延迟播放:适合短音效(通常不超过 5 秒)
同时播放多个音频:可管理多个音频流
预加载机制:提前加载音频到内存,减少播放延迟
资源高效:适合频繁播放的小音频
(三)基本使用步骤
1. 创建 SoundPool 对象
java// 使用 SoundPool.Builder AudioAttributes attributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_GAME) // 使用场景 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) // 内容类型 .build(); SoundPool soundPool = new SoundPool.Builder() .setAudioAttributes(attributes) .setMaxStreams(10) // 最大同时播放流数 .build();2. 加载音频资源
java// 从资源文件加载 int soundId1 = soundPool.load(this, R.raw.beep, 1); // 从文件路径加载 int soundId2 = soundPool.load("/sdcard/sound/click.wav", 1); // 从Asset加载 try { AssetFileDescriptor afd = getAssets().openFd("sounds/explosion.wav"); int soundId3 = soundPool.load(afd, 1); } catch (IOException e) { e.printStackTrace(); }3. 设置加载完成监听
javasoundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() { @Override public void onLoadComplete(SoundPool soundPool, int sampleId, int status) { if (status == 0) { // 加载成功,sampleId 是对应音频的ID Log.d("SoundPool", "音频加载成功,ID: " + sampleId); } else { Log.e("SoundPool", "音频加载失败"); } } });4. 播放音频
java// 播放音频 int streamId = soundPool.play( soundId, // 音频ID 1.0f, // 左声道音量 (0.0 - 1.0) 1.0f, // 右声道音量 1, // 优先级 (0 = 最低) 0, // 循环次数 (0 = 不循环, -1 = 无限循环) 1.0f // 播放速率 (0.5 - 2.0) );5. 控制播放
java// 暂停播放 soundPool.pause(streamId); // 恢复播放 soundPool.resume(streamId); // 停止播放 soundPool.stop(streamId); // 设置循环(-1无限循环,0不循环,n循环n次) soundPool.setLoop(streamId, 2); // 设置音量 soundPool.setVolume(streamId, 0.5f, 0.8f); // 设置播放速率 soundPool.setRate(streamId, 1.5f); // 1.5倍速6. 资源释放
java// 释放单个音频 soundPool.unload(soundId); // 释放所有资源 soundPool.release(); soundPool = null;
(四) 注意事项
**音频格式限制:**建议使用 OGG 或 WAV 格式,避免使用 MP3(可能有兼容性问题)
音频长度: 适合短音频(1-5秒),长音频建议使用 MediaPlayer
内存管理: 不要加载过多或过大的音频文件
生命周期管理: 在 Activity 的 onDestroy() 中释放资源
**异步加载:**音频加载是异步的,播放前确保已加载完成
三. VideoView
(一)介绍
VideoView 是 Android 提供的用于播放视频的视图组件,它封装了 MediaPlayer 和 SurfaceView,提供了简单的视频播放功能。
(二)特点
简单易用:无需复杂配置即可实现视频播放
内置控制界面:支持播放/暂停、进度条等控制功能
支持多种格式:可播放本地和网络视频
生命周期管理:可与 Activity/Fragment 生命周期绑定
(三)基本使用步骤
1. 布局文件中添加 VideoView
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/videoView" android:layout_width="match_parent" android:layout_height="300dp" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:id="@+id/btnPlay" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="播放" /> <Button android:id="@+id/btnPause" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="暂停" /> <Button android:id="@+id/btnReplay" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="重播" /> </LinearLayout> </LinearLayout>2. 基本配置和播放控制
javapublic class VideoPlayerActivity extends AppCompatActivity { private VideoView videoView; private MediaController mediaController; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_video_player); initVideoView(); setupControls(); } private void initVideoView() { videoView = findViewById(R.id.videoView); // 设置媒体控制器(内置控制界面) mediaController = new MediaController(this); videoView.setMediaController(mediaController); // 设置视频路径 String videoPath = "android.resource://" + getPackageName() + "/" + R.raw.sample_video; Uri uri = Uri.parse(videoPath); videoView.setVideoURI(uri); // 或者从文件路径设置 // String filePath = Environment.getExternalStorageDirectory() + "/Movies/sample.mp4"; // videoView.setVideoPath(filePath); // 或者从网络URL设置 // String videoUrl = "https://example.com/sample.mp4"; // videoView.setVideoURI(Uri.parse(videoUrl)); } private void setupControls() { Button btnPlay = findViewById(R.id.btnPlay); Button btnPause = findViewById(R.id.btnPause); Button btnReplay = findViewById(R.id.btnReplay); btnPlay.setOnClickListener(v -> videoView.start()); btnPause.setOnClickListener(v -> videoView.pause()); btnReplay.setOnClickListener(v -> { videoView.resume(); videoView.start(); }); } }
(四)高级功能实现
1. 自定义视频播放器
javapublic class CustomVideoPlayer { private VideoView videoView; private SeekBar seekBar; private TextView currentTime, totalTime; private Handler handler = new Handler(); public CustomVideoPlayer(VideoView videoView, SeekBar seekBar, TextView currentTime, TextView totalTime) { this.videoView = videoView; this.seekBar = seekBar; this.currentTime = currentTime; this.totalTime = totalTime; setupSeekBar(); setupListeners(); } private void setupSeekBar() { // 更新进度条的Runnable Runnable updateProgress = new Runnable() { @Override public void run() { if (videoView.isPlaying()) { int current = videoView.getCurrentPosition(); int duration = videoView.getDuration(); seekBar.setProgress(current); seekBar.setMax(duration); currentTime.setText(formatTime(current)); totalTime.setText(formatTime(duration)); } handler.postDelayed(this, 1000); } }; handler.post(updateProgress); // SeekBar变化监听 seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser) { videoView.seekTo(progress); } } @Override public void onStartTrackingTouch(SeekBar seekBar) {} @Override public void onStopTrackingTouch(SeekBar seekBar) {} }); } private void setupListeners() { // 准备完成监听 videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { // 视频准备完成,可以开始播放 seekBar.setMax(videoView.getDuration()); totalTime.setText(formatTime(videoView.getDuration())); } }); // 播放完成监听 videoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { // 播放完成后的操作 videoView.seekTo(0); seekBar.setProgress(0); } }); // 错误监听 videoView.setOnErrorListener(new MediaPlayer.OnErrorListener() { @Override public boolean onError(MediaPlayer mp, int what, int extra) { // 处理播放错误 Log.e("VideoView", "播放错误: " + what + ", " + extra); return true; } }); } private String formatTime(int milliseconds) { int seconds = milliseconds / 1000; int minutes = seconds / 60; seconds = seconds % 60; return String.format("%02d:%02d", minutes, seconds); } public void release() { handler.removeCallbacksAndMessages(null); } }2. 完整的视频播放器 Activity
javapublic class AdvancedVideoPlayerActivity extends AppCompatActivity { private VideoView videoView; private SeekBar seekBar; private TextView currentTime, totalTime; private Button btnPlayPause; private CustomVideoPlayer customPlayer; private boolean isPlaying = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_advanced_video_player); initViews(); setupVideoPlayer(); } private void initViews() { videoView = findViewById(R.id.videoView); seekBar = findViewById(R.id.seekBar); currentTime = findViewById(R.id.currentTime); totalTime = findViewById(R.id.totalTime); btnPlayPause = findViewById(R.id.btnPlayPause); // 隐藏默认控制器 videoView.setMediaController(null); } private void setupVideoPlayer() { // 初始化自定义播放器 customPlayer = new CustomVideoPlayer(videoView, seekBar, currentTime, totalTime); // 设置视频源 String videoUrl = getIntent().getStringExtra("video_url"); if (videoUrl != null) { videoView.setVideoURI(Uri.parse(videoUrl)); } else { // 默认视频 Uri uri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.sample_video); videoView.setVideoURI(uri); } // 播放/暂停按钮 btnPlayPause.setOnClickListener(v -> togglePlayPause()); // 全屏按钮 findViewById(R.id.btnFullscreen).setOnClickListener(v -> toggleFullscreen()); } private void togglePlayPause() { if (isPlaying) { videoView.pause(); btnPlayPause.setText("播放"); } else { videoView.start(); btnPlayPause.setText("暂停"); } isPlaying = !isPlaying; } private void toggleFullscreen() { if (getSupportActionBar() != null) { if (getSupportActionBar().isShowing()) { getSupportActionBar().hide(); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); } else { getSupportActionBar().show(); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); } } } @Override protected void onPause() { super.onPause(); if (videoView.isPlaying()) { videoView.pause(); } } @Override protected void onResume() { super.onResume(); if (isPlaying) { videoView.start(); } } @Override protected void onDestroy() { super.onDestroy(); if (customPlayer != null) { customPlayer.release(); } videoView.stopPlayback(); } }3. 对应的布局文件
XML<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <VideoView android:id="@+id/videoView" android:layout_width="match_parent" android:layout_height="300dp" android:layout_alignParentTop="true" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/videoView" android:orientation="vertical" android:padding="16dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:id="@+id/currentTime" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="00:00" /> <SeekBar android:id="@+id/seekBar" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:layout_marginHorizontal="8dp" /> <TextView android:id="@+id/totalTime" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="00:00" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_marginTop="16dp"> <Button android:id="@+id/btnPlayPause" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="播放" /> <Button android:id="@+id/btnFullscreen" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="全屏" /> </LinearLayout> </LinearLayout> </RelativeLayout>(五)网络视频播放配置
1. 添加网络权限
XML<uses-permission android:name="android.permission.INTERNET" />2. 处理网络视频缓冲
javavideoView.setOnInfoListener(new MediaPlayer.OnInfoListener() { @Override public boolean onInfo(MediaPlayer mp, int what, int extra) { switch (what) { case MediaPlayer.MEDIA_INFO_BUFFERING_START: // 显示加载提示 showLoading(); return true; case MediaPlayer.MEDIA_INFO_BUFFERING_END: // 隐藏加载提示 hideLoading(); return true; case MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START: // 视频开始渲染 return true; } return false; } });(六)注意事项
1. 权限管理
网络视频需要
INTERNET权限本地文件需要存储权限
2. 格式支持:
支持 MP4、3GP 等常见格式
不同设备支持的编码格式可能不同
3. 内存管理
及时释放资源,避免内存泄漏
在 onPause() 中暂停播放,onDestroy() 中释放资源
4. 网络视频
使用 HTTPS 确保安全性
处理网络异常情况
5. 生命周期
正确处理 Activity 生命周期
配置变化时保存播放状态
四. MediaRecorder
(一)介绍
MediaRecorder 是 Android 中用于录制音频和视频的核心类,它提供了完整的媒体录制功能,支持音频、视频的采集、编码和保存。
(二)特点
多功能录制:支持音频、视频录制
格式丰富:支持多种编码格式和输出格式
灵活配置:可自定义录制参数(分辨率、比特率、帧率等)
状态机管理:严格的状态转换控制
(三)状态机
MediaRecorder 遵循严格的状态转换流程:
Initial → Initialized → DataSourceConfigured → Prepared → Recording → Released
(四)基本使用步骤
1. 权限配置
XML<!-- 音频录制权限 --> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <!-- 视频录制权限 --> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <!-- 存储权限 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- 如果针对 Android 10+ 使用媒体存储 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />2. 音频录制基础实现
javapublic class AudioRecorder { private MediaRecorder mediaRecorder; private String outputFile; private boolean isRecording = false; public AudioRecorder(Context context) { mediaRecorder = new MediaRecorder(); // 设置输出文件路径 outputFile = getOutputFilePath(context, "audio"); } private String getOutputFilePath(Context context, String type) { File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES); String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); String fileName = type + "_" + timeStamp; if (type.equals("audio")) { return storageDir.getAbsolutePath() + "/" + fileName + ".3gp"; } else { return storageDir.getAbsolutePath() + "/" + fileName + ".mp4"; } } public void startAudioRecording() { try { // 重置 MediaRecorder(如果是重复使用) mediaRecorder.reset(); // 设置音频源(麦克风) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); // 设置输出格式 mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); // 设置音频编码器 mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); // 设置输出文件 mediaRecorder.setOutputFile(outputFile); // 准备录制 mediaRecorder.prepare(); // 开始录制 mediaRecorder.start(); isRecording = true; Log.d("AudioRecorder", "开始录制音频: " + outputFile); } catch (IOException e) { Log.e("AudioRecorder", "录制准备失败: " + e.getMessage()); e.printStackTrace(); } catch (IllegalStateException e) { Log.e("AudioRecorder", "状态异常: " + e.getMessage()); e.printStackTrace(); } } public void stopAudioRecording() { if (isRecording) { try { mediaRecorder.stop(); mediaRecorder.reset(); // 重置到初始状态 isRecording = false; Log.d("AudioRecorder", "停止录制,文件保存至: " + outputFile); } catch (RuntimeException e) { Log.e("AudioRecorder", "停止录制失败: " + e.getMessage()); // 可能是录制时间太短导致的异常 } } } public void release() { if (mediaRecorder != null) { mediaRecorder.release(); mediaRecorder = null; } } public boolean isRecording() { return isRecording; } public String getOutputFile() { return outputFile; } }3. 视频录制基础实现
javapublic class VideoRecorder { private MediaRecorder mediaRecorder; private Camera camera; private String outputFile; private boolean isRecording = false; private SurfaceHolder surfaceHolder; public VideoRecorder(Context context, SurfaceHolder holder) { this.surfaceHolder = holder; mediaRecorder = new MediaRecorder(); outputFile = getOutputFilePath(context, "video"); } public void startVideoRecording() { try { // 释放相机(如果已占用) if (camera != null) { camera.unlock(); } mediaRecorder.reset(); // 设置视频源(相机) mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); // 设置音频源 mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); // 设置输出格式 mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); // 设置视频编码器 mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); // 设置音频编码器 mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); // 设置视频参数 mediaRecorder.setVideoSize(1280, 720); // 分辨率 mediaRecorder.setVideoFrameRate(30); // 帧率 mediaRecorder.setVideoEncodingBitRate(3000000); // 比特率 3Mbps // 设置预览显示 mediaRecorder.setPreviewDisplay(surfaceHolder.getSurface()); // 设置输出文件 mediaRecorder.setOutputFile(outputFile); // 设置方向提示(如果需要) mediaRecorder.setOrientationHint(90); // 竖屏90度 mediaRecorder.prepare(); mediaRecorder.start(); isRecording = true; Log.d("VideoRecorder", "开始录制视频: " + outputFile); } catch (IOException e) { Log.e("VideoRecorder", "录制准备失败: " + e.getMessage()); e.printStackTrace(); } catch (IllegalStateException e) { Log.e("VideoRecorder", "状态异常: " + e.getMessage()); e.printStackTrace(); } } public void stopVideoRecording() { if (isRecording) { try { mediaRecorder.stop(); mediaRecorder.reset(); isRecording = false; // 重新锁定相机(如果使用) if (camera != null) { camera.lock(); } Log.d("VideoRecorder", "停止录制,文件保存至: " + outputFile); } catch (RuntimeException e) { Log.e("VideoRecorder", "停止录制失败: " + e.getMessage()); } } } public void setCamera(Camera camera) { this.camera = camera; } public void release() { if (mediaRecorder != null) { mediaRecorder.release(); mediaRecorder = null; } if (camera != null) { camera.release(); camera = null; } } public boolean isRecording() { return isRecording; } private String getOutputFilePath(Context context, String type) { // 同上... } }
(五)高级功能实现
1. 完整的录制管理器
javapublic class MediaRecorderManager { private MediaRecorder mediaRecorder; private Camera camera; private String outputFile; private boolean isRecording = false; private RecorderType currentType; private MediaRecorderCallback callback; public enum RecorderType { AUDIO, VIDEO } public interface MediaRecorderCallback { void onRecordingStarted(); void onRecordingStopped(String filePath); void onError(String error); void onRecordingTimeUpdate(long milliseconds); } public MediaRecorderManager() { mediaRecorder = new MediaRecorder(); } public void setupAudioRecorder(Context context) { currentType = RecorderType.AUDIO; outputFile = getOutputFilePath(context, "audio"); try { mediaRecorder.reset(); mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); mediaRecorder.setAudioEncodingBitRate(128000); // 128kbps mediaRecorder.setAudioSamplingRate(44100); // 44.1kHz mediaRecorder.setOutputFile(outputFile); mediaRecorder.prepare(); } catch (Exception e) { if (callback != null) { callback.onError("音频录制设置失败: " + e.getMessage()); } } } public void setupVideoRecorder(Context context, Camera camera, SurfaceHolder holder) { currentType = RecorderType.VIDEO; this.camera = camera; outputFile = getOutputFilePath(context, "video"); try { if (camera != null) { camera.unlock(); } mediaRecorder.reset(); mediaRecorder.setCamera(camera); mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); // 高质量视频设置 mediaRecorder.setVideoSize(1920, 1080); mediaRecorder.setVideoFrameRate(30); mediaRecorder.setVideoEncodingBitRate(6000000); // 6Mbps mediaRecorder.setAudioEncodingBitRate(128000); // 预览设置 if (holder != null) { mediaRecorder.setPreviewDisplay(holder.getSurface()); } mediaRecorder.setOutputFile(outputFile); mediaRecorder.setOrientationHint(90); mediaRecorder.prepare(); } catch (Exception e) { if (callback != null) { callback.onError("视频录制设置失败: " + e.getMessage()); } } } public void startRecording() { if (isRecording) { return; } try { mediaRecorder.start(); isRecording = true; if (callback != null) { callback.onRecordingStarted(); } // 开始计时(可选) startTimer(); } catch (IllegalStateException e) { if (callback != null) { callback.onError("开始录制失败: " + e.getMessage()); } } } public void stopRecording() { if (!isRecording) { return; } try { mediaRecorder.stop(); isRecording = false; stopTimer(); if (callback != null) { callback.onRecordingStopped(outputFile); } // 重置以备下次使用 mediaRecorder.reset(); if (camera != null) { camera.lock(); } } catch (RuntimeException e) { if (callback != null) { callback.onError("停止录制失败: " + e.getMessage()); } } } private Handler timerHandler = new Handler(); private long startTime = 0; private Runnable timerRunnable = new Runnable() { @Override public void run() { if (isRecording && callback != null) { long elapsed = System.currentTimeMillis() - startTime; callback.onRecordingTimeUpdate(elapsed); timerHandler.postDelayed(this, 1000); } } }; private void startTimer() { startTime = System.currentTimeMillis(); timerHandler.postDelayed(timerRunnable, 1000); } private void stopTimer() { timerHandler.removeCallbacks(timerRunnable); } public void setCallback(MediaRecorderCallback callback) { this.callback = callback; } public void release() { stopRecording(); if (mediaRecorder != null) { mediaRecorder.release(); mediaRecorder = null; } if (camera != null) { camera.release(); camera = null; } stopTimer(); } // 其他getter方法... private String getOutputFilePath(Context context, String type) { // 实现同上... } }2. 完整的录制 Activity 实现
javapublic class RecordingActivity extends AppCompatActivity { private MediaRecorderManager recorderManager; private SurfaceView surfaceView; private Camera camera; private Button btnRecord; private TextView tvTimer; private boolean hasCameraPermission = false; private boolean hasAudioPermission = false; private static final int PERMISSION_REQUEST_CODE = 100; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_recording); initViews(); checkPermissions(); initRecorderManager(); } private void initViews() { surfaceView = findViewById(R.id.surfaceView); btnRecord = findViewById(R.id.btnRecord); tvTimer = findViewById(R.id.tvTimer); btnRecord.setOnClickListener(v -> toggleRecording()); // 设置SurfaceHolder SurfaceHolder holder = surfaceView.getHolder(); holder.addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { initCamera(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} @Override public void surfaceDestroyed(SurfaceHolder holder) { releaseCamera(); } }); } private void initCamera() { if (hasCameraPermission) { try { camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK); Camera.Parameters params = camera.getParameters(); // 设置相机参数... camera.setDisplayOrientation(90); camera.setPreviewDisplay(surfaceView.getHolder()); camera.startPreview(); } catch (Exception e) { Log.e("RecordingActivity", "相机初始化失败: " + e.getMessage()); } } } private void initRecorderManager() { recorderManager = new MediaRecorderManager(); recorderManager.setCallback(new MediaRecorderManager.MediaRecorderCallback() { @Override public void onRecordingStarted() { runOnUiThread(() -> { btnRecord.setText("停止录制"); btnRecord.setBackgroundColor(Color.RED); }); } @Override public void onRecordingStopped(String filePath) { runOnUiThread(() -> { btnRecord.setText("开始录制"); btnRecord.setBackgroundColor(Color.GREEN); tvTimer.setText("00:00"); // 显示录制完成提示 Toast.makeText(RecordingActivity.this, "录制完成: " + filePath, Toast.LENGTH_LONG).show(); }); } @Override public void onError(String error) { runOnUiThread(() -> { Toast.makeText(RecordingActivity.this, error, Toast.LENGTH_LONG).show(); }); } @Override public void onRecordingTimeUpdate(long milliseconds) { runOnUiThread(() -> { tvTimer.setText(formatTime(milliseconds)); }); } }); } private void toggleRecording() { if (recorderManager.isRecording()) { recorderManager.stopRecording(); } else { if (hasCameraPermission && hasAudioPermission) { // 设置视频录制 recorderManager.setupVideoRecorder(this, camera, surfaceView.getHolder()); recorderManager.startRecording(); } } } private String formatTime(long milliseconds) { long seconds = milliseconds / 1000; long minutes = seconds / 60; seconds = seconds % 60; return String.format("%02d:%02d", minutes, seconds); } private void checkPermissions() { String[] permissions = { Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE }; if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE); } else { hasCameraPermission = true; hasAudioPermission = true; initCamera(); } } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == PERMISSION_REQUEST_CODE) { hasCameraPermission = grantResults[0] == PackageManager.PERMISSION_GRANTED; hasAudioPermission = grantResults[1] == PackageManager.PERMISSION_GRANTED; if (hasCameraPermission) { initCamera(); } else { Toast.makeText(this, "需要相机权限才能录制视频", Toast.LENGTH_LONG).show(); } } } private void releaseCamera() { if (camera != null) { camera.stopPreview(); camera.release(); camera = null; } } @Override protected void onPause() { super.onPause(); if (recorderManager != null && recorderManager.isRecording()) { recorderManager.stopRecording(); } releaseCamera(); } @Override protected void onDestroy() { super.onDestroy(); if (recorderManager != null) { recorderManager.release(); } } }
(六)对应布局文件
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"> <!-- 预览画面 --> <SurfaceView android:id="@+id/surfaceView" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <!-- 控制区域 --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="16dp" android:gravity="center"> <TextView android:id="@+id/tvTimer" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="00:00" android:textSize="18sp" android:textStyle="bold" android:gravity="center" /> <Button android:id="@+id/btnRecord" android:layout_width="120dp" android:layout_height="120dp" android:text="开始录制" android:textSize="16sp" android:background="@drawable/record_button_shape" /> <View android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" /> </LinearLayout> </LinearLayout>
(七)录制按钮样式
XML<!-- res/drawable/record_button_shape.xml --> <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="#4CAF50" /> <stroke android:width="2dp" android:color="#FFFFFF" /> <size android:width="120dp" android:height="120dp" /> </shape>
(八)注意事项
- 权限管理:确保在运行时请求必要权限
- 状态管理:严格遵守 MediaRecorder 的状态转换顺序
- 异常处理:妥善处理各种异常情况
- 资源释放:及时释放 MediaRecorder 和 Camera 资源
- 文件管理:使用适当的文件路径和命名规则
- 性能优化:根据设备能力调整录制参数
- Android 10+ 适配:使用分区存储或 MediaStore API
五. AudioRecord
(一)介绍
AudioRecord 是 Android 中用于直接录制原始音频数据的类,它提供了比 MediaRecorder 更底层的音频录制控制,适合需要实时处理音频数据的场景。
(二)特点
原始音频数据:直接获取 PCM 音频数据
实时处理:适合音频分析、实时处理等场景
低延迟:提供更低的音频录制延迟
灵活控制:可自定义音频参数和数据处理逻辑
(三)核心参数
1. 音频参数说明
采样率(Sample Rate):8000Hz、16000Hz、44100Hz 等
声道配置(Channel Config):单声道(MONO)、立体声(STEREO)
音频格式(Audio Format):8bit、16bit PCM
2. 缓冲区计算
javaint bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
(四)基本使用步骤
1. 权限配置
XML<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />2. 基础 AudioRecord 实现
javapublic class BasicAudioRecorder { private static final String TAG = "BasicAudioRecorder"; // 音频参数 private static final int SAMPLE_RATE = 44100; // 44.1kHz private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO; // 单声道 private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT; // 16bit PCM private AudioRecord audioRecord; private int bufferSize; private boolean isRecording = false; private Thread recordingThread; public boolean initAudioRecorder() { // 获取最小缓冲区大小 bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT); if (bufferSize == AudioRecord.ERROR || bufferSize == AudioRecord.ERROR_BAD_VALUE) { Log.e(TAG, "无法获取有效的缓冲区大小"); return false; } try { // 创建 AudioRecord 实例 audioRecord = new AudioRecord( MediaRecorder.AudioSource.MIC, // 音频源(麦克风) SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, bufferSize * 2 // 缓冲区大小(最小值的2倍) ); if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { Log.e(TAG, "AudioRecord 初始化失败"); return false; } return true; } catch (SecurityException e) { Log.e(TAG, "没有录音权限: " + e.getMessage()); return false; } } public void startRecording() { if (isRecording || audioRecord == null) { return; } try { audioRecord.startRecording(); isRecording = true; // 创建录音线程 recordingThread = new Thread(new RecordingRunnable(), "AudioRecorder Thread"); recordingThread.start(); Log.d(TAG, "开始录制音频"); } catch (IllegalStateException e) { Log.e(TAG, "启动录制失败: " + e.getMessage()); } } public void stopRecording() { if (!isRecording || audioRecord == null) { return; } isRecording = false; try { if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { audioRecord.stop(); } // 等待录音线程结束 if (recordingThread != null) { recordingThread.join(1000); } Log.d(TAG, "停止录制音频"); } catch (InterruptedException e) { Log.e(TAG, "停止录制时线程中断: " + e.getMessage()); Thread.currentThread().interrupt(); } } public void release() { stopRecording(); if (audioRecord != null) { audioRecord.release(); audioRecord = null; } } private class RecordingRunnable implements Runnable { @Override public void run() { // 创建缓冲区 byte[] buffer = new byte[bufferSize]; while (isRecording && audioRecord != null) { // 读取音频数据 int bytesRead = audioRecord.read(buffer, 0, buffer.length); if (bytesRead > 0) { // 处理音频数据(这里只是示例) processAudioData(buffer, bytesRead); } else { Log.e(TAG, "读取音频数据错误: " + bytesRead); break; } } } private void processAudioData(byte[] data, int length) { // 这里可以添加音频数据处理逻辑 // 例如:实时分析、保存到文件、网络传输等 Log.d(TAG, "处理音频数据长度: " + length + " 字节"); } } public boolean isRecording() { return isRecording; } public int getAudioSessionId() { return audioRecord != null ? audioRecord.getAudioSessionId() : -1; } }
(五)高级功能实现
1. 完整的音频录制管理器
javapublic class AdvancedAudioRecorder { private static final String TAG = "AdvancedAudioRecorder"; // 支持的音频参数 public enum AudioQuality { LOW(8000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT), MEDIUM(16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT), HIGH(44100, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT); final int sampleRate; final int channelConfig; final int audioFormat; AudioQuality(int sampleRate, int channelConfig, int audioFormat) { this.sampleRate = sampleRate; this.channelConfig = channelConfig; this.audioFormat = audioFormat; } } private AudioRecord audioRecord; private int bufferSize; private boolean isRecording = false; private Thread recordingThread; private AudioQuality currentQuality = AudioQuality.HIGH; private AudioRecorderCallback callback; private File outputFile; private FileOutputStream fileOutputStream; public interface AudioRecorderCallback { void onRecordingStarted(); void onRecordingStopped(File outputFile); void onError(String error); void onAudioDataAvailable(byte[] data, int size); void onRecordingProgress(long duration); } public AdvancedAudioRecorder(AudioQuality quality) { this.currentQuality = quality; } public boolean initialize() { // 计算缓冲区大小 bufferSize = AudioRecord.getMinBufferSize( currentQuality.sampleRate, currentQuality.channelConfig, currentQuality.audioFormat ); if (bufferSize <= 0) { notifyError("无效的缓冲区大小: " + bufferSize); return false; } try { audioRecord = new AudioRecord( MediaRecorder.AudioSource.MIC, currentQuality.sampleRate, currentQuality.channelConfig, currentQuality.audioFormat, bufferSize * 2 ); if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { notifyError("AudioRecord 初始化失败"); return false; } Log.d(TAG, String.format("AudioRecord 初始化成功 - 采样率: %d, 声道: %s, 格式: %s", currentQuality.sampleRate, getChannelConfigName(currentQuality.channelConfig), getAudioFormatName(currentQuality.audioFormat))); return true; } catch (Exception e) { notifyError("初始化异常: " + e.getMessage()); return false; } } public void startRecording(Context context) { if (isRecording || audioRecord == null) { return; } try { // 创建输出文件 outputFile = createOutputFile(context, "pcm"); fileOutputStream = new FileOutputStream(outputFile); audioRecord.startRecording(); isRecording = true; // 开始录音线程 recordingThread = new Thread(new RecordingRunnable(), "AdvancedAudioRecorder"); recordingThread.start(); notifyRecordingStarted(); Log.d(TAG, "开始录制音频到: " + outputFile.getAbsolutePath()); } catch (Exception e) { notifyError("开始录制失败: " + e.getMessage()); } } public void stopRecording() { if (!isRecording) { return; } isRecording = false; try { if (audioRecord != null && audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { audioRecord.stop(); } // 等待录音线程结束 if (recordingThread != null) { recordingThread.join(2000); } // 关闭文件流 if (fileOutputStream != null) { fileOutputStream.close(); fileOutputStream = null; } notifyRecordingStopped(); Log.d(TAG, "音频录制完成: " + outputFile.getAbsolutePath()); } catch (Exception e) { notifyError("停止录制异常: " + e.getMessage()); } } private class RecordingRunnable implements Runnable { private long startTime = 0; @Override public void run() { android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO); byte[] buffer = new byte[bufferSize]; startTime = System.currentTimeMillis(); while (isRecording && audioRecord != null) { int bytesRead = audioRecord.read(buffer, 0, buffer.length); if (bytesRead > 0) { // 通知数据可用 notifyAudioDataAvailable(buffer, bytesRead); // 写入文件 writeToFile(buffer, bytesRead); // 更新进度 notifyProgress(System.currentTimeMillis() - startTime); } else if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) { notifyError("无效操作错误"); break; } else if (bytesRead == AudioRecord.ERROR_BAD_VALUE) { notifyError("参数错误"); break; } else { notifyError("读取音频数据失败: " + bytesRead); break; } } } private void writeToFile(byte[] data, int size) { if (fileOutputStream != null) { try { fileOutputStream.write(data, 0, size); } catch (IOException e) { Log.e(TAG, "写入文件失败: " + e.getMessage()); } } } } private File createOutputFile(Context context, String extension) throws IOException { File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_MUSIC); String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); String fileName = "recording_" + timeStamp + "." + extension; return new File(storageDir, fileName); } // 转换PCM为WAV文件 public File convertToWav(Context context) { if (outputFile == null || !outputFile.exists()) { return null; } try { File wavFile = createOutputFile(context, "wav"); convertPcmToWav(outputFile, wavFile, currentQuality.sampleRate, getChannelCount(currentQuality.channelConfig), 16); return wavFile; } catch (IOException e) { Log.e(TAG, "转换WAV失败: " + e.getMessage()); return null; } } private void convertPcmToWav(File pcmFile, File wavFile, int sampleRate, int channels, int bitsPerSample) throws IOException { try (FileInputStream fis = new FileInputStream(pcmFile); FileOutputStream fos = new FileOutputStream(wavFile)) { // PCM数据长度 long pcmDataLength = pcmFile.length(); // WAV文件头 byte[] header = createWavHeader(pcmDataLength, sampleRate, channels, bitsPerSample); fos.write(header); // 写入PCM数据 byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { fos.write(buffer, 0, bytesRead); } } } private byte[] createWavHeader(long pcmDataLength, int sampleRate, int channels, int bitsPerSample) { byte[] header = new byte[44]; long byteRate = sampleRate * channels * bitsPerSample / 8; long totalDataLength = pcmDataLength + 36; // RIFF header header[0] = 'R'; header[1] = 'I'; header[2] = 'F'; header[3] = 'F'; writeInt(header, 4, (int) totalDataLength); header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E'; // fmt chunk header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' '; writeInt(header, 16, 16); // PCM格式 writeShort(header, 20, (short) 1); // 音频格式(PCM) writeShort(header, 22, (short) channels); writeInt(header, 24, sampleRate); writeInt(header, 28, (int) byteRate); writeShort(header, 32, (short) (channels * bitsPerSample / 8)); // 块对齐 writeShort(header, 34, (short) bitsPerSample); // data chunk header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a'; writeInt(header, 40, (int) pcmDataLength); return header; } private void writeShort(byte[] array, int offset, short value) { array[offset] = (byte) (value & 0xff); array[offset + 1] = (byte) ((value >> 8) & 0xff); } private void writeInt(byte[] array, int offset, int value) { array[offset] = (byte) (value & 0xff); array[offset + 1] = (byte) ((value >> 8) & 0xff); array[offset + 2] = (byte) ((value >> 16) & 0xff); array[offset + 3] = (byte) ((value >> 24) & 0xff); } private int getChannelCount(int channelConfig) { switch (channelConfig) { case AudioFormat.CHANNEL_IN_MONO: return 1; case AudioFormat.CHANNEL_IN_STEREO: return 2; default: return 1; } } private String getChannelConfigName(int channelConfig) { switch (channelConfig) { case AudioFormat.CHANNEL_IN_MONO: return "MONO"; case AudioFormat.CHANNEL_IN_STEREO: return "STEREO"; default: return "UNKNOWN"; } } private String getAudioFormatName(int audioFormat) { switch (audioFormat) { case AudioFormat.ENCODING_PCM_8BIT: return "PCM_8BIT"; case AudioFormat.ENCODING_PCM_16BIT: return "PCM_16BIT"; case AudioFormat.ENCODING_PCM_FLOAT: return "PCM_FLOAT"; default: return "UNKNOWN"; } } // 回调通知方法 private void notifyRecordingStarted() { if (callback != null) { new Handler(Looper.getMainLooper()).post(() -> callback.onRecordingStarted()); } } private void notifyRecordingStopped() { if (callback != null) { new Handler(Looper.getMainLooper()).post(() -> callback.onRecordingStopped(outputFile)); } } private void notifyError(String error) { if (callback != null) { new Handler(Looper.getMainLooper()).post(() -> callback.onError(error)); } } private void notifyAudioDataAvailable(byte[] data, int size) { if (callback != null) { // 注意:这里可能会频繁回调,需要确保回调处理效率 byte[] copy = Arrays.copyOf(data, size); new Handler(Looper.getMainLooper()).post(() -> callback.onAudioDataAvailable(copy, size)); } } private void notifyProgress(long duration) { if (callback != null && duration % 1000 < 50) { // 约每秒通知一次 new Handler(Looper.getMainLooper()).post(() -> callback.onRecordingProgress(duration)); } } public void setCallback(AudioRecorderCallback callback) { this.callback = callback; } public void release() { stopRecording(); if (audioRecord != null) { audioRecord.release(); audioRecord = null; } } public boolean isRecording() { return isRecording; } public AudioQuality getCurrentQuality() { return currentQuality; } }2. 实时音频分析示例
javapublic class AudioAnalyzer { private AdvancedAudioRecorder recorder; private boolean isAnalyzing = false; public interface AudioAnalysisCallback { void onVolumeChanged(double volume); void onFrequencyDetected(double frequency); void onSilenceDetected(); } public void startAnalysis(Context context, AudioAnalysisCallback callback) { if (isAnalyzing) return; recorder = new AdvancedAudioRecorder(AdvancedAudioRecorder.AudioQuality.MEDIUM); recorder.setCallback(new AdvancedAudioRecorder.AudioRecorderCallback() { @Override public void onRecordingStarted() {} @Override public void onRecordingStopped(File outputFile) {} @Override public void onError(String error) {} @Override public void onAudioDataAvailable(byte[] data, int size) { analyzeAudioData(data, size, callback); } @Override public void onRecordingProgress(long duration) {} }); if (recorder.initialize()) { recorder.startRecording(context); isAnalyzing = true; } } private void analyzeAudioData(byte[] data, int size, AudioAnalysisCallback callback) { // 计算音量(RMS) double volume = calculateVolume(data, size); if (callback != null) { callback.onVolumeChanged(volume); } // 检测静音 if (volume < 0.01) { // 阈值可根据需要调整 if (callback != null) { callback.onSilenceDetected(); } } // 简单频率分析(FFT实现较复杂,这里用简化的过零率) double zeroCrossingRate = calculateZeroCrossingRate(data, size); // 可根据过零率估算基频(简化处理) } private double calculateVolume(byte[] data, int size) { long sum = 0; for (int i = 0; i < size - 1; i += 2) { // 将2个byte转换为16bit的short short audioSample = (short) ((data[i + 1] << 8) | (data[i] & 0xFF)); sum += audioSample * audioSample; } double meanSquare = (double) sum / (size / 2); return Math.sqrt(meanSquare) / 32768.0; // 归一化到[0,1] } private double calculateZeroCrossingRate(byte[] data, int size) { int zeroCrossings = 0; short previousSample = 0; for (int i = 0; i < size - 1; i += 2) { short currentSample = (short) ((data[i + 1] << 8) | (data[i] & 0xFF)); if ((previousSample >= 0 && currentSample < 0) || (previousSample < 0 && currentSample >= 0)) { zeroCrossings++; } previousSample = currentSample; } return (double) zeroCrossings / (size / 2); } public void stopAnalysis() { if (recorder != null) { recorder.stopRecording(); recorder.release(); } isAnalyzing = false; } }3. 完整的录音 Activity
javapublic class AudioRecordingActivity extends AppCompatActivity { private AdvancedAudioRecorder audioRecorder; private AudioAnalyzer audioAnalyzer; private Button btnRecord; private TextView tvStatus, tvVolume; private ProgressBar volumeBar; private boolean isRecording = false; private static final int PERMISSION_REQUEST_CODE = 200; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_audio_recording); initViews(); checkPermissions(); } private void initViews() { btnRecord = findViewById(R.id.btnRecord); tvStatus = findViewById(R.id.tvStatus); tvVolume = findViewById(R.id.tvVolume); volumeBar = findViewById(R.id.volumeBar); btnRecord.setOnClickListener(v -> toggleRecording()); // 初始化音频分析器(用于实时显示音量) audioAnalyzer = new AudioAnalyzer(); } private void toggleRecording() { if (isRecording) { stopRecording(); } else { startRecording(); } } private void startRecording() { if (audioRecorder == null) { audioRecorder = new AdvancedAudioRecorder(AdvancedAudioRecorder.AudioQuality.HIGH); audioRecorder.setCallback(createRecorderCallback()); } if (audioRecorder.initialize()) { audioRecorder.startRecording(this); isRecording = true; updateUI(); } } private void stopRecording() { if (audioRecorder != null) { audioRecorder.stopRecording(); // 转换为WAV格式 File wavFile = audioRecorder.convertToWav(this); if (wavFile != null) { Toast.makeText(this, "音频已保存: " + wavFile.getName(), Toast.LENGTH_LONG).show(); } audioRecorder.release(); audioRecorder = null; } isRecording = false; updateUI(); } private AdvancedAudioRecorder.AudioRecorderCallback createRecorderCallback() { return new AdvancedAudioRecorder.AudioRecorderCallback() { @Override public void onRecordingStarted() { runOnUiThread(() -> { tvStatus.setText("录制中..."); tvStatus.setTextColor(Color.GREEN); }); } @Override public void onRecordingStopped(File outputFile) { runOnUiThread(() -> { tvStatus.setText("已停止"); tvStatus.setTextColor(Color.RED); }); } @Override public void onError(String error) { runOnUiThread(() -> { tvStatus.setText("错误: " + error); tvStatus.setTextColor(Color.RED); Toast.makeText(AudioRecordingActivity.this, error, Toast.LENGTH_LONG).show(); }); } @Override public void onAudioDataAvailable(byte[] data, int size) { // 实时分析音频数据 analyzeRealTimeData(data, size); } @Override public void onRecordingProgress(long duration) { runOnUiThread(() -> { long seconds = duration / 1000; long minutes = seconds / 60; seconds = seconds % 60; tvStatus.setText(String.format("录制中 %02d:%02d", minutes, seconds)); }); } }; } private void analyzeRealTimeData(byte[] data, int size) { // 计算实时音量 double volume = calculateSimpleVolume(data, size); runOnUiThread(() -> { int progress = (int) (volume * 100); volumeBar.setProgress(Math.min(progress, 100)); tvVolume.setText(String.format("音量: %d%%", progress)); }); } private double calculateSimpleVolume(byte[] data, int size) { long sum = 0; for (int i = 0; i < size - 1; i += 2) { short sample = (short) ((data[i + 1] << 8) | (data[i] & 0xFF)); sum += Math.abs(sample); } return (double) sum / (size / 2) / 32768.0; } private void updateUI() { runOnUiThread(() -> { if (isRecording) { btnRecord.setText("停止录制"); btnRecord.setBackgroundColor(Color.RED); } else { btnRecord.setText("开始录制"); btnRecord.setBackgroundColor(Color.GREEN); volumeBar.setProgress(0); tvVolume.setText("音量: 0%"); } }); } private void checkPermissions() { String[] permissions = { Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE }; if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE); } } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == PERMISSION_REQUEST_CODE) { if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED) { Toast.makeText(this, "需要录音权限才能使用此功能", Toast.LENGTH_LONG).show(); finish(); } } } @Override protected void onPause() { super.onPause(); if (isRecording) { stopRecording(); } } @Override protected void onDestroy() { super.onDestroy(); if (audioRecorder != null) { audioRecorder.release(); } } }
(六)对应布局文件
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" android:padding="16dp"> <TextView android:id="@+id/tvStatus" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="准备录制" android:textSize="24sp" android:textStyle="bold" android:gravity="center" android:layout_marginBottom="32dp" /> <ProgressBar android:id="@+id/volumeBar" style="@android:style/Widget.ProgressBar.Horizontal" android:layout_width="match_parent" android:layout_height="20dp" android:max="100" android:progress="0" android:layout_marginBottom="16dp" /> <TextView android:id="@+id/tvVolume" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="音量: 0%" android:textSize="16sp" android:gravity="center" android:layout_marginBottom="32dp" /> <Button android:id="@+id/btnRecord" android:layout_width="120dp" android:layout_height="120dp" android:text="开始录制" android:textSize="18sp" android:layout_gravity="center" android:background="@drawable/record_button_shape" /> </LinearLayout>
(七)注意事项
权限管理:确保在运行时请求录音权限
线程安全:AudioRecord 操作需要在非UI线程进行
缓冲区管理:合理设置缓冲区大小,避免数据丢失
性能优化:实时处理时注意算法效率
资源释放:及时释放 AudioRecord 资源
异常处理:妥善处理各种异常情况
(八)常见应用场景
语音录制:高质量音频录制
实时分析:音量检测、语音识别
音频处理:实时滤镜、特效处理
通信应用:VoIP、语音聊天
音乐应用:乐器调音、节拍器
六. AudioTrack
(一)介绍
AudioTrack 是 Android 中用于播放原始 PCM 音频数据的底层类,提供比 MediaPlayer 更底层的音频控制。
(二)核心特点
原始 PCM 数据播放
低延迟音频输出
实时音频流处理
灵活的音量控制
**(三)**使用
1. 基本配置
javapublic class SimpleAudioPlayer { private AudioTrack audioTrack; private int sampleRate = 44100; private int channelConfig = AudioFormat.CHANNEL_OUT_STEREO; private int audioFormat = AudioFormat.ENCODING_PCM_16BIT; private int bufferSize; public boolean initialize() { // 计算最小缓冲区大小 bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat); if (bufferSize == AudioTrack.ERROR_BAD_VALUE) { return false; } // 创建 AudioTrack(流模式) audioTrack = new AudioTrack( AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, bufferSize, AudioTrack.MODE_STREAM ); return audioTrack.getState() == AudioTrack.STATE_INITIALIZED; } }2. 播放音频数据
javapublic class AudioPlayer { private AudioTrack audioTrack; public void playPcmData(byte[] pcmData) { if (audioTrack == null || audioTrack.getState() != AudioTrack.STATE_INITIALIZED) { return; } // 开始播放 audioTrack.play(); // 写入音频数据 int written = audioTrack.write(pcmData, 0, pcmData.length); if (written != pcmData.length) { Log.e("AudioTrack", "数据写入不完整"); } } public void stop() { if (audioTrack != null) { audioTrack.stop(); audioTrack.flush(); // 清空缓冲区 } } public void release() { if (audioTrack != null) { audioTrack.release(); audioTrack = null; } } }3. 流模式实时播放
javapublic class StreamAudioPlayer { private AudioTrack audioTrack; private Thread playbackThread; private boolean isPlaying = false; public void startStreaming() { audioTrack = new AudioTrack( AudioManager.STREAM_MUSIC, 44100, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT, AudioTrack.getMinBufferSize(44100, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT), AudioTrack.MODE_STREAM ); audioTrack.play(); isPlaying = true; playbackThread = new Thread(() -> { byte[] buffer = new byte[1024]; while (isPlaying) { // 从网络或实时源获取音频数据 int bytesRead = getAudioData(buffer); if (bytesRead > 0) { audioTrack.write(buffer, 0, bytesRead); } } }); playbackThread.start(); } private int getAudioData(byte[] buffer) { // 实现音频数据获取逻辑 // 例如:从网络流、实时录音等获取 return 1024; // 示例 } }4. 静态模式播放(适合短音效)
javapublic class StaticAudioPlayer { public void playShortSound(byte[] pcmData) { AudioTrack audioTrack = new AudioTrack( AudioManager.STREAM_MUSIC, 44100, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT, pcmData.length, AudioTrack.MODE_STATIC ); // 一次性写入所有数据 audioTrack.write(pcmData, 0, pcmData.length); audioTrack.play(); // 播放完成后自动释放 audioTrack.setNotificationMarkerPosition(pcmData.length / 2); audioTrack.setPlaybackPositionUpdateListener(new AudioTrack.OnPlaybackPositionUpdateListener() { @Override public void onMarkerReached(AudioTrack track) { track.release(); } @Override public void onPeriodicNotification(AudioTrack track) {} }); } }5. 音量控制
javapublic class VolumeControl { private AudioTrack audioTrack; public void setupVolume() { // 设置音量(0.0 - 1.0) float volume = 0.8f; int result = audioTrack.setVolume(volume); if (result != AudioTrack.SUCCESS) { Log.e("VolumeControl", "音量设置失败"); } // 立体声音量分别控制 audioTrack.setStereoVolume(0.7f, 1.0f); // 左声道70%,右声道100% } public void setPlaybackRate(int sampleRate) { // 动态调整播放速率 audioTrack.setPlaybackRate(sampleRate); } }
(四)使用示例
java// 播放 raw 资源中的 PCM 文件 public void playRawResource(Context context, int resId) { try { InputStream inputStream = context.getResources().openRawResource(resId); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] data = new byte[1024]; int bytesRead; while ((bytesRead = inputStream.read(data)) != -1) { buffer.write(data, 0, bytesRead); } byte[] pcmData = buffer.toByteArray(); playPcmData(pcmData); } catch (IOException e) { e.printStackTrace(); } }
(五)关键参数说明
参数 说明 常用值 streamType 音频流类型 STREAM_MUSIC, STREAM_VOICE_CALL sampleRate 采样率 8000, 16000, 44100 Hz channelConfig 声道配置 CHANNEL_OUT_MONO, CHANNEL_OUT_STEREO audioFormat 音频格式 ENCODING_PCM_8BIT, ENCODING_PCM_16BIT bufferSize 缓冲区大小 getMinBufferSize() 计算 mode 播放模式 MODE_STREAM, MODE_STATIC
七. MediaRouter
(一)介绍
MediaRouter 用于管理媒体输出路由,实现音频/视频在不同设备间的切换(如手机扬声器、蓝牙设备、Chromecast等)。
(二)核心功能
设备发现:自动检测可用输出设备
路由管理:在不同设备间切换媒体播放
回调通知:监听设备连接状态变化
(三)使用
1. 添加依赖
javadependencies { implementation 'androidx.mediarouter:mediarouter:1.4.0' implementation 'com.google.android.gms:play-services-cast-framework:21.3.0' }2. 基本配置
javapublic class MediaRouterHelper { private MediaRouter mediaRouter; private MediaRouter.Callback callback; private MediaRouter.RouteInfo selectedRoute; public void initialize(Context context) { mediaRouter = MediaRouter.getInstance(context); // 创建回调 callback = new MediaRouter.Callback() { @Override public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) { Log.d("MediaRouter", "设备添加: " + route.getName()); } @Override public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) { selectedRoute = route; Log.d("MediaRouter", "设备选择: " + route.getName()); } @Override public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route) { Log.d("MediaRouter", "设备取消选择: " + route.getName()); } }; // 注册回调 mediaRouter.addCallback(MediaRouter.ROUTE_TYPE_LIVE_AUDIO, callback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY); } }3. 设备选择和播放控制
javapublic class MediaRouterPlayer { private MediaRouter mediaRouter; private MediaController mediaController; // 选择输出设备 public void selectDevice(MediaRouter.RouteInfo route) { MediaRouter.RouteInfo currentRoute = mediaRouter.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_AUDIO); if (!route.equals(currentRoute)) { mediaRouter.selectRoute(route); } } // 获取所有可用设备 public List<MediaRouter.RouteInfo> getAvailableDevices() { List<MediaRouter.RouteInfo> devices = new ArrayList<>(); for (MediaRouter.RouteInfo route : mediaRouter.getRoutes()) { if (route.isEnabled()) { devices.add(route); } } return devices; } // 播放媒体到选定设备 public void playMedia(Uri mediaUri) { MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_AUDIO); if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) { // 远程播放(如Chromecast) playRemotely(route, mediaUri); } else { // 本地播放 playLocally(mediaUri); } } private void playRemotely(MediaRouter.RouteInfo route, Uri mediaUri) { // 远程播放实现 MediaControlIntent intent = new MediaControlIntent.Builder(mediaUri) .setTitle("媒体标题") .build(); route.sendControlRequest(intent, null); } }4. 设备切换对话框
javapublic class DeviceSelectionHelper { // 显示设备选择对话框 public void showDevicePicker(FragmentManager fragmentManager) { MediaRouteButton mediaRouteButton = new MediaRouteButton(context); MediaRouteDialogFactory factory = MediaRouteDialogFactory.getDefault(); Dialog dialog = factory.onCreateDialog(MediaRouteDialogFactory.DIALOG_TYPE_DEFAULT); dialog.show(); } // 或者在布局中添加MediaRouteButton // <androidx.mediarouter.app.MediaRouteButton // android:id="@+id/media_route_button" // android:layout_width="wrap_content" // android:layout_height="wrap_content" /> }5. Chromecast 集成
javapublic class ChromecastHelper { private CastContext castContext; private CastSession castSession; private SessionManagerListener<CastSession> sessionManagerListener; public void initializeCast(Context context) { castContext = CastContext.getSharedInstance(context); sessionManagerListener = new SessionManagerListener<CastSession>() { @Override public void onSessionStarted(CastSession session, String sessionId) { castSession = session; // Chromecast连接成功 } @Override public void onSessionEnded(CastSession session, int error) { // Chromecast断开连接 castSession = null; } }; castContext.getSessionManager().addSessionManagerListener(sessionManagerListener); } public void castMedia(Uri mediaUri, String title) { if (castSession != null && castSession.isConnected()) { RemoteMediaClient client = castSession.getRemoteMediaClient(); MediaInfo mediaInfo = new MediaInfo.Builder(mediaUri.toString()) .setContentType("video/mp4") .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setMetadata(new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE) .putString(MediaMetadata.KEY_TITLE, title)) .build(); client.load(mediaInfo, true, 0); } } }6. 音频焦点管理
javapublic class AudioFocusHelper { private AudioManager audioManager; private AudioManager.OnAudioFocusChangeListener focusListener; public void setupAudioFocus(Context context) { audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); focusListener = new AudioManager.OnAudioFocusChangeListener() { @Override public void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_GAIN: // 重新获得焦点,恢复播放 break; case AudioManager.AUDIOFOCUS_LOSS: // 失去焦点,停止播放 break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: // 暂时失去焦点,暂停播放 break; } } }; // 请求音频焦点 int result = audioManager.requestAudioFocus(focusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { // 可以开始播放 } } }
(四)完整使用示例
javapublic class MediaPlayerActivity extends AppCompatActivity { private MediaRouter mediaRouter; private MediaRouterHelper routerHelper; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_media_player); initializeMediaRouter(); setupMediaControls(); } private void initializeMediaRouter() { routerHelper = new MediaRouterHelper(); routerHelper.initialize(this); // 设置MediaRouteButton MediaRouteButton routeButton = findViewById(R.id.media_route_button); MediaRouteSelector selector = new MediaRouteSelector.Builder() .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO) .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO) .build(); } private void setupMediaControls() { Button playButton = findViewById(R.id.play_button); playButton.setOnClickListener(v -> { Uri mediaUri = Uri.parse("https://example.com/media.mp4"); routerHelper.playMedia(mediaUri); }); } @Override protected void onDestroy() { super.onDestroy(); if (routerHelper != null) { routerHelper.cleanup(); } } }
(五)关键类说明
类/接口 用途 MediaRouter媒体路由管理器 RouteInfo表示一个输出设备 MediaRouteProvider扩展支持新设备类型 MediaControlIntent远程播放控制
(六)常用路由类型
ROUTE_TYPE_LIVE_AUDIO- 音频输出
ROUTE_TYPE_LIVE_VIDEO- 视频输出
ROUTE_TYPE_USER- 用户自定义
(七)注意事项
权限检查:蓝牙等设备需要相应权限
生命周期管理:及时移除回调监听
错误处理:设备连接可能失败
用户体验:提供清晰的设备切换界面