当前位置: 首页 > news >正文

安卓进阶——多媒体

✅作者简介:大家好,我是 Meteors., 向往着更加简洁高效的代码写法与编程方式,持续分享Java技术内容。
🍎个人主页:Meteors.的博客
💞当前专栏:知识分享
✨特色专栏:知识分享
🥭本文内容:安卓进阶——多媒体
📚 ** ps **  :阅读文章如果有问题或者疑惑,欢迎在评论区提问或指出。


目录

一. MediaPlayer

(一)介绍

(二)基本使用步骤(以播放 Raw 资源为例)

1. 创建 MediaPlayer 实例​​

​​2. 设置数据源​​

3. 准备播放器​​

​​(1)同步准备​​:prepare()

​​​​(2)异步准备​​:prepareAsync()

4. 开始播放​​

5. ​​管理播放状态​​

6. 释放资源(非常重要!)​​

(三)MediaPlayer 的生命周期

(三)完整代码示例

(四)最佳实践和注意事项

二. SoundPool

(一)介绍

(二)特点

(三)基本使用步骤

1. 创建 SoundPool 对象

2. 加载音频资源

3. 设置加载完成监听

4. 播放音频

5. 控制播放

6. 资源释放

(四) 注意事项

三. VideoView

(一)介绍

(二)特点

(三)基本使用步骤

1. 布局文件中添加 VideoView

2. 基本配置和播放控制

(四)高级功能实现

1. 自定义视频播放器

2. 完整的视频播放器 Activity

3. 对应的布局文件

(五)网络视频播放配置

1. 添加网络权限

2. 处理网络视频缓冲

(六)注意事项

1. 权限管理

2. 格式支持:

3. 内存管理

4. 网络视频

5. 生命周期

四. MediaRecorder

(一)介绍

(二)特点

(三)状态机

(四)基本使用步骤

1. 权限配置

2. 音频录制基础实现

3. 视频录制基础实现

(五)高级功能实现

1. 完整的录制管理器

2. 完整的录制 Activity 实现

(六)对应布局文件

(七)录制按钮样式

(八)注意事项

五. AudioRecord

(一)介绍

(二)特点

(三)核心参数

1. 音频参数说明

2. 缓冲区计算

(四)基本使用步骤

1. 权限配置

2. 基础 AudioRecord 实现

(五)高级功能实现

1. 完整的音频录制管理器

2. 实时音频分析示例

3. 完整的录音 Activity

(六)对应布局文件

(七)注意事项

(八)常见应用场景

六. AudioTrack

(一)介绍

(二)核心特点

(三)使用

1. 基本配置

2. 播放音频数据

3. 流模式实时播放

4. 静态模式播放(适合短音效)

5. 音量控制

(四)使用示例

(五)关键参数说明

七. MediaRouter

(一)介绍

(二)核心功能

(三)使用

1. 添加依赖

2. 基本配置

3. 设备选择和播放控制

4. 设备切换对话框

5. Chromecast 集成

6. 音频焦点管理

(四)完整使用示例

(五)关键类说明

(六)常用路由类型

(七)注意事项


一. MediaPlayer

(一)介绍

MediaPlayer是 Android 系统提供的一个用于播放音频和视频文件的核心类。它功能强大,可以处理多种媒体源,包括:

  • ​本地资源​​:如 res/raw目录下的文件。

  • ​本地文件系统​​:如设备内部存储或 SD 卡中的文件。

  • ​网络流媒体​​:通过 HTTP/HTTPS 协议在线播放音频/视频。

  • ​应用资源​​:通过 URI 指定的内容。

​支持的格式​​:常见格式如 MP3, MP4, 3GP, OGG, WAV, AAC 等。但具体支持情况因设备硬件和系统版本而异。


(二)基本使用步骤(以播放 Raw 资源为例)

播放一个媒体文件最直接的流程如下:

1. 创建 MediaPlayer 实例​

// 或者使用 create 快捷方法(对于资源文件) 
MediaPlayer mediaPlayer = new MediaPlayer(); 
// MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.my_sound);

​2. 设置数据源​

这是最关键的一步,告诉 MediaPlayer 要播放什么。

// 示例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(对于网络源)或导致界面卡顿。

mediaPlayer.prepare(); // 必须在后台线程执行
​(2)异步准备​​:prepareAsync()

这是推荐的方式。它会立即返回,并在后台准备媒体。你需要设置一个监听器来接收准备完成的通知。

mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {@Overridepublic void onPrepared(MediaPlayer mp) {// 媒体准备就绪,可以开始播放了mediaPlayer.start();}
});
mediaPlayer.prepareAsync(); // 非阻塞,安全在主线程调用

4. 开始播放​

mediaPlayer.start(); // 开始或恢复播放

5. ​​管理播放状态​

mediaPlayer.pause();   // 暂停播放
mediaPlayer.stop();    // 停止播放,之后需要重新 prepare 才能再次 start
mediaPlayer.seekTo(10000); // 跳转到第10秒(单位:毫秒)

6. 释放资源(非常重要!)​

MediaPlayer 会占用系统稀缺的资源(如编解码器)。当不再使用时,​​必须​​释放它。

mediaPlayer.release();
mediaPlayer = null;

通常这个操作在 Activity 的 onDestroy()方法中执行。


(三)MediaPlayer 的生命周期

理解 MediaPlayer 的状态机至关重要,可以避免很多错误(例如在错误的状态下调用方法)。其生命周期可以简化为以下核心状态:

  1. ​Idle(空闲)​​: 通过 new创建后的状态。此时尚未设置数据源。

  2. ​Initialized(已初始化)​​: 调用 setDataSource()后的状态。

  3. ​Preparing / Prepared(准备中/已就绪)​​:

    • 调用 prepareAsync()后进入 ​​Preparing​​ 状态。

    • 准备完成后(onPrepared被调用)进入 ​​Prepared​​ 状态。此时可以调用 start()

  4. ​Started(已开始)​​: 调用 start()后,媒体正在播放。可以调用 isPlaying()来检查。

  5. ​Paused(已暂停)​​: 调用 pause()后的状态。可以从这里恢复播放。

  6. ​Stopped(已停止)​​: 调用 stop()后的状态。要重新播放,必须再次调用 prepare()或 prepareAsync()

  7. ​PlaybackCompleted(播放完成)​​: 媒体播放完毕。可以设置循环播放(setLooping(true))或通过 OnCompletionListener来监听。

  8. ​Error(错误)​​: 发生错误时进入此状态。应该设置 OnErrorListener来处理错误。

  9. ​End(结束)​​: 调用 release()后进入此状态。MediaPlayer 不再可用。

​关键点​​:从 Stopped状态重新开始,必须再次 prepare。调用 reset()方法可以将 MediaPlayer 重置到 Idle状态。


(三)完整代码示例

public class MainActivity extends AppCompatActivity {private MediaPlayer mediaPlayer;@Overrideprotected 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() {@Overridepublic void onCompletion(MediaPlayer mp) {// 播放结束,释放资源releaseMediaPlayer();Toast.makeText(MainActivity.this, "播放完成", Toast.LENGTH_SHORT).show();}});playButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {if (mediaPlayer != null && !mediaPlayer.isPlaying()) {mediaPlayer.start();}}});pauseButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {if (mediaPlayer != null && mediaPlayer.isPlaying()) {mediaPlayer.pause();}}});}@Overrideprotected void onDestroy() {super.onDestroy();// 确保在 Activity 销毁时释放 MediaPlayerreleaseMediaPlayer();}/*** 释放 MediaPlayer 资源的辅助方法*/private void releaseMediaPlayer() {if (mediaPlayer != null) {mediaPlayer.release();mediaPlayer = null;}}
}

(四)最佳实践和注意事项

  1. ​及时释放资源​​:这是最重要的原则。在 Activity/Fragment 的 onPause()或 onDestroy()中释放 MediaPlayer。否则会导致内存泄漏和资源占用。

  2. ​使用异步准备​​:对于本地大文件或网络流,始终使用 prepareAsync()和 OnPreparedListener,避免阻塞 UI 线程。

  3. ​处理音频焦点(Audio Focus)​​:在播放音频前,申请音频焦点。当有来电或其他应用播放声音时,根据音频焦点的变化做出相应处理(如暂停播放、降低音量),这是良好的用户体验。这需要用到 AudioManager

  4. ​处理错误​​:务必设置 OnErrorListener,以便在媒体播放出错时能做出响应(如给用户提示)。

    mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {@Overridepublic boolean onError(MediaPlayer mp, int what, int extra) {// 'what' 和 'extra' 是错误代码Log.e("MediaPlayer", "Error: " + what + ", " + extra);releaseMediaPlayer();return true; // 返回 true 表示错误已处理}
    });
  5. ​使用 reset()​:如果你想用同一个 MediaPlayer 实例播放另一个数据源,先调用 reset()将其重置到 Idle状态,然后再设置新的数据源并 prepare


二. SoundPool

(一)介绍

SoundPool 是 Android 中用于播放短音频片段的类,特别适合播放需要低延迟、快速响应的音效(如游戏音效、按键声等)。


(二)特点

  • 低延迟播放:适合短音效(通常不超过 5 秒)

  • 同时播放多个音频:可管理多个音频流

  • 预加载机制:提前加载音频到内存,减少播放延迟

  • 资源高效:适合频繁播放的小音频


(三)基本使用步骤

1. 创建 SoundPool 对象

// 使用 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. 加载音频资源

// 从资源文件加载
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. 设置加载完成监听

soundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() {@Overridepublic void onLoadComplete(SoundPool soundPool, int sampleId, int status) {if (status == 0) {// 加载成功,sampleId 是对应音频的IDLog.d("SoundPool", "音频加载成功,ID: " + sampleId);} else {Log.e("SoundPool", "音频加载失败");}}
});

4. 播放音频

// 播放音频
int streamId = soundPool.play(soundId,    // 音频ID1.0f,       // 左声道音量 (0.0 - 1.0)1.0f,       // 右声道音量1,          // 优先级 (0 = 最低)0,          // 循环次数 (0 = 不循环, -1 = 无限循环)1.0f        // 播放速率 (0.5 - 2.0)
);

5. 控制播放

// 暂停播放
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. 资源释放

// 释放单个音频
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 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"><VideoViewandroid:id="@+id/videoView"android:layout_width="match_parent"android:layout_height="300dp" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"><Buttonandroid:id="@+id/btnPlay"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="播放" /><Buttonandroid:id="@+id/btnPause"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="暂停" /><Buttonandroid:id="@+id/btnReplay"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="重播" /></LinearLayout></LinearLayout>

2. 基本配置和播放控制

public class VideoPlayerActivity extends AppCompatActivity {private VideoView videoView;private MediaController mediaController;@Overrideprotected 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. 自定义视频播放器

public 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() {// 更新进度条的RunnableRunnable updateProgress = new Runnable() {@Overridepublic 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() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {if (fromUser) {videoView.seekTo(progress);}}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {}});}private void setupListeners() {// 准备完成监听videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {@Overridepublic void onPrepared(MediaPlayer mp) {// 视频准备完成,可以开始播放seekBar.setMax(videoView.getDuration());totalTime.setText(formatTime(videoView.getDuration()));}});// 播放完成监听videoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {@Overridepublic void onCompletion(MediaPlayer mp) {// 播放完成后的操作videoView.seekTo(0);seekBar.setProgress(0);}});// 错误监听videoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {@Overridepublic 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

public class AdvancedVideoPlayerActivity extends AppCompatActivity {private VideoView videoView;private SeekBar seekBar;private TextView currentTime, totalTime;private Button btnPlayPause;private CustomVideoPlayer customPlayer;private boolean isPlaying = false;@Overrideprotected 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);}}}@Overrideprotected void onPause() {super.onPause();if (videoView.isPlaying()) {videoView.pause();}}@Overrideprotected void onResume() {super.onResume();if (isPlaying) {videoView.start();}}@Overrideprotected void onDestroy() {super.onDestroy();if (customPlayer != null) {customPlayer.release();}videoView.stopPlayback();}
}

3. 对应的布局文件

<?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"><VideoViewandroid:id="@+id/videoView"android:layout_width="match_parent"android:layout_height="300dp"android:layout_alignParentTop="true" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_below="@id/videoView"android:orientation="vertical"android:padding="16dp"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"><TextViewandroid:id="@+id/currentTime"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="00:00" /><SeekBarandroid:id="@+id/seekBar"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:layout_marginHorizontal="8dp" /><TextViewandroid:id="@+id/totalTime"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="00:00" /></LinearLayout><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:layout_marginTop="16dp"><Buttonandroid:id="@+id/btnPlayPause"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="播放" /><Buttonandroid:id="@+id/btnFullscreen"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="全屏" /></LinearLayout></LinearLayout></RelativeLayout>

(五)网络视频播放配置

1. 添加网络权限

<uses-permission android:name="android.permission.INTERNET" />

2. 处理网络视频缓冲

videoView.setOnInfoListener(new MediaPlayer.OnInfoListener() {@Overridepublic 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. 权限配置

<!-- 音频录制权限 -->
<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. 音频录制基础实现

public 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. 视频录制基础实现

public 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. 完整的录制管理器

public 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); // 128kbpsmediaRecorder.setAudioSamplingRate(44100);     // 44.1kHzmediaRecorder.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); // 6MbpsmediaRecorder.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() {@Overridepublic 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 实现

public 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;@Overrideprotected 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());// 设置SurfaceHolderSurfaceHolder holder = surfaceView.getHolder();holder.addCallback(new SurfaceHolder.Callback() {@Overridepublic void surfaceCreated(SurfaceHolder holder) {initCamera();}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}@Overridepublic 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() {@Overridepublic void onRecordingStarted() {runOnUiThread(() -> {btnRecord.setText("停止录制");btnRecord.setBackgroundColor(Color.RED);});}@Overridepublic void onRecordingStopped(String filePath) {runOnUiThread(() -> {btnRecord.setText("开始录制");btnRecord.setBackgroundColor(Color.GREEN);tvTimer.setText("00:00");// 显示录制完成提示Toast.makeText(RecordingActivity.this, "录制完成: " + filePath, Toast.LENGTH_LONG).show();});}@Overridepublic void onError(String error) {runOnUiThread(() -> {Toast.makeText(RecordingActivity.this, error, Toast.LENGTH_LONG).show();});}@Overridepublic 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();}}@Overridepublic 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;}}@Overrideprotected void onPause() {super.onPause();if (recorderManager != null && recorderManager.isRecording()) {recorderManager.stopRecording();}releaseCamera();}@Overrideprotected void onDestroy() {super.onDestroy();if (recorderManager != null) {recorderManager.release();}}
}

(六)对应布局文件

<?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"><!-- 预览画面 --><SurfaceViewandroid:id="@+id/surfaceView"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1" /><!-- 控制区域 --><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"android:padding="16dp"android:gravity="center"><TextViewandroid: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" /><Buttonandroid:id="@+id/btnRecord"android:layout_width="120dp"android:layout_height="120dp"android:text="开始录制"android:textSize="16sp"android:background="@drawable/record_button_shape" /><Viewandroid:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1" /></LinearLayout></LinearLayout>

(七)录制按钮样式

<!-- 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" /><strokeandroid:width="2dp"android:color="#FFFFFF" /><sizeandroid: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. 缓冲区计算

int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);

(四)基本使用步骤

1. 权限配置

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

2. 基础 AudioRecord 实现

public class BasicAudioRecorder {private static final String TAG = "BasicAudioRecorder";// 音频参数private static final int SAMPLE_RATE = 44100;        // 44.1kHzprivate static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO; // 单声道private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT; // 16bit PCMprivate 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 {@Overridepublic 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. 完整的音频录制管理器

public 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;@Overridepublic 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 headerheader[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 chunkheader[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 chunkheader[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. 实时音频分析示例

public 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() {@Overridepublic void onRecordingStarted() {}@Overridepublic void onRecordingStopped(File outputFile) {}@Overridepublic void onError(String error) {}@Overridepublic void onAudioDataAvailable(byte[] data, int size) {analyzeAudioData(data, size, callback);}@Overridepublic 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的shortshort 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

public 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;@Overrideprotected 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() {@Overridepublic void onRecordingStarted() {runOnUiThread(() -> {tvStatus.setText("录制中...");tvStatus.setTextColor(Color.GREEN);});}@Overridepublic void onRecordingStopped(File outputFile) {runOnUiThread(() -> {tvStatus.setText("已停止");tvStatus.setTextColor(Color.RED);});}@Overridepublic void onError(String error) {runOnUiThread(() -> {tvStatus.setText("错误: " + error);tvStatus.setTextColor(Color.RED);Toast.makeText(AudioRecordingActivity.this, error, Toast.LENGTH_LONG).show();});}@Overridepublic void onAudioDataAvailable(byte[] data, int size) {// 实时分析音频数据analyzeRealTimeData(data, size);}@Overridepublic 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);}}@Overridepublic 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();}}}@Overrideprotected void onPause() {super.onPause();if (isRecording) {stopRecording();}}@Overrideprotected void onDestroy() {super.onDestroy();if (audioRecorder != null) {audioRecorder.release();}}
}

(六)对应布局文件

<?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"><TextViewandroid: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" /><ProgressBarandroid: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" /><TextViewandroid: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" /><Buttonandroid: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>

(七)注意事项

  1. 权限管理:确保在运行时请求录音权限

  2. 线程安全:AudioRecord 操作需要在非UI线程进行

  3. 缓冲区管理:合理设置缓冲区大小,避免数据丢失

  4. 性能优化:实时处理时注意算法效率

  5. 资源释放:及时释放 AudioRecord 资源

  6. 异常处理:妥善处理各种异常情况


(八)常见应用场景

  1. 语音录制:高质量音频录制

  2. 实时分析:音量检测、语音识别

  3. 音频处理:实时滤镜、特效处理

  4. 通信应用:VoIP、语音聊天

  5. 音乐应用:乐器调音、节拍器


六. AudioTrack

(一)介绍

AudioTrack 是 Android 中用于播放原始 PCM 音频数据的底层类,提供比 MediaPlayer 更底层的音频控制。


(二)核心特点

  • 原始 PCM 数据播放

  • 低延迟音频输出

  • 实时音频流处理

  • 灵活的音量控制


(三)使用

1. 基本配置

public 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. 播放音频数据

public 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. 流模式实时播放

public 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. 静态模式播放(适合短音效)

public 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() {@Overridepublic void onMarkerReached(AudioTrack track) {track.release();}@Overridepublic void onPeriodicNotification(AudioTrack track) {}});}
}

5. 音量控制

public 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);}
}

(四)使用示例

// 播放 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. 添加依赖

dependencies {implementation 'androidx.mediarouter:mediarouter:1.4.0'implementation 'com.google.android.gms:play-services-cast-framework:21.3.0'
}

2. 基本配置

public 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() {@Overridepublic void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) {Log.d("MediaRouter", "设备添加: " + route.getName());}@Overridepublic void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {selectedRoute = route;Log.d("MediaRouter", "设备选择: " + route.getName());}@Overridepublic 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. 设备选择和播放控制

public 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. 设备切换对话框

public 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 集成

public 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>() {@Overridepublic void onSessionStarted(CastSession session, String sessionId) {castSession = session;// Chromecast连接成功}@Overridepublic 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. 音频焦点管理

public 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() {@Overridepublic 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) {// 可以开始播放}}
}

(四)完整使用示例

public class MediaPlayerActivity extends AppCompatActivity {private MediaRouter mediaRouter;private MediaRouterHelper routerHelper;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_media_player);initializeMediaRouter();setupMediaControls();}private void initializeMediaRouter() {routerHelper = new MediaRouterHelper();routerHelper.initialize(this);// 设置MediaRouteButtonMediaRouteButton 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);});}@Overrideprotected void onDestroy() {super.onDestroy();if (routerHelper != null) {routerHelper.cleanup();}}
}

(五)关键类说明

类/接口

用途

MediaRouter

媒体路由管理器

RouteInfo

表示一个输出设备

MediaRouteProvider

扩展支持新设备类型

MediaControlIntent

远程播放控制


(六)常用路由类型

  • ROUTE_TYPE_LIVE_AUDIO- 音频输出

  • ROUTE_TYPE_LIVE_VIDEO- 视频输出

  • ROUTE_TYPE_USER- 用户自定义


(七)注意事项

  1. 权限检查:蓝牙等设备需要相应权限

  2. 生命周期管理:及时移除回调监听

  3. 错误处理:设备连接可能失败

  4. 用户体验:提供清晰的设备切换界面

http://www.dtcms.com/a/574284.html

相关文章:

  • Spring Boot3零基础教程,响应式编程的模型,笔记109
  • 解读IEC 60086-4 2025
  • 学做网站论坛 可以吗360建筑工程网
  • 济南旅游团购网站建设动态电子商务网站建设报告
  • 企业做网站用dedeCMS免费吗在线资源搜索引擎
  • 什么是离子注入的注入剂量?
  • 静态网站 挂马北京网站设计的公司价格
  • 厦门建设局网站商品房免费免费网站模板
  • 怎么做谷歌这样的网站刷赞网站推广免费链接
  • 5、foc控制系统——电流环设计
  • 代码随想录打卡day25:56.合并区间
  • 【C++】C++11新特性 (上)
  • vue3+ts element-plus动态Icon图标统一注册
  • 用户组管理指令大全
  • 网站建设经费保障青海网站开发多少钱
  • 跨越协议鸿沟:RS485转ETHERCAT网关在电力电动机保护中的破局之道
  • 【开题答辩全过程】以 扶贫农产品销售平台APP为例,包含答辩的问题和答案
  • 专业做网站建设公司有哪些o2o网站建设行业现状
  • 自己做网站服务器多少钱网站域名怎么修改吗
  • 黑马JAVAWeb-05 JDBC入门-预编译SQL-Mybatis入门-Mybatis日志输出-数据库连接池-增删改查-XML映射配置
  • 济南高端网站asp.net怎样做网站登录
  • 北京平台网站建设哪家好有口碑的镇江网站优化
  • 抖音来客如何实现自动回复
  • 网站设计入门闸北品牌网站建设
  • PhotoShop网页版(在线ps)在快速修复老照片,在线修旧如新
  • 网站怎么做评估wordpress获取当前子分类
  • 《P2585 [ZJOI2006] 三色二叉树》
  • 无需人类标注!Meta提出SPICE:让AI在文档语料库中自我对弈,推理能力持续进化
  • 手机网站制作套餐查企业不要钱的软件
  • 学做吃的网站有哪些php 网站开发流程