# Android 语言房音乐功能执行方案
整理时间:2026-04-29
适用范围:Android 单端,语言房内 MP3 背景音乐添加、管理与播放
参考文档:`/Users/nigger/Desktop/Android音乐添加流程.md`
## 1. 目标
在语言房内补齐一套可上线的 Android 音乐能力:
- 从房间操作面板进入音乐功能。
- 首次为空时引导扫描手机 MP3。
- 支持 Android 音频权限申请与本地 MP3 扫描。
- 按文件夹展示扫描结果,支持选择音乐添加到房间音乐列表。
- 支持已添加音乐的搜索、删除、排序、二次添加、取消已添加。
- 支持播放器:播放、暂停、停止、上一首、下一首、播放模式、音量、进度条拖动。
- 支持麦下仅自己听,麦上推流给房间其他用户听。
- 离房、切房、删除当前播放歌曲时正确停止并清理播放状态。
不纳入本次范围:
- iOS 权限和 iOS 媒体库适配。
- 在线音乐、歌词、专辑封面精修。
- 后台常驻播放通知栏。
- 服务端同步歌单。当前按用户本地持久化处理。
## 2. 项目现状
已有基础:
- `agora_rtc_engine: ^6.5.2` 已接入,实际锁定版本为 6.5.3。
- `lib/services/audio/rtc_manager.dart` 已有 `startAudioMixing(String localPath, int cycle)` 雏形。
- `rtc_manager.dart` 已监听 `onAudioMixingStateChanged`,但只维护了 `isMusicPlaying`。
- `lib/shared/data_sources/models/sc_music_mode.dart` 已有基础音乐模型。
- `lib/shared/data_sources/sources/local/data_persistence.dart` 已有 `getUserRoomMusic()` / `setUserRoomMusic()`。
- `sc_images/room/` 已有音乐相关图标:播放、暂停、上一首、下一首、音量、空态、播放模式、删除、添加、音乐入口等。
- 多语言中已有 `music`、`myMusic`、`localMusic`、`add`、`delete`、`search`、`edit`、`confirm`、`cancel` 等基础文案。
- `local_packages/on_audio_query-2.9.0` 和 `local_packages/on_audio_query_android-1.1.0` 已存在,可用于 Android MediaStore 音频扫描。
需要补齐:
- `pubspec.yaml` 尚未声明 `on_audio_query` 依赖。
- AndroidManifest 缺少 Android 13+ 的 `READ_MEDIA_AUDIO`。
- `SCMusicMode` 字段不足,缺少稳定 id、content uri、文件夹路径、排序、添加时间等。
- 音乐入口在 `lib/ui_kit/widgets/room/room_menu_dialog.dart` 中曾经存在,但当前被注释。
- 没有独立音乐状态管理器,播放器状态不应继续堆进已经很长的 `RtcProvider`。
- Agora 音频混流只封装了开始播放,缺少暂停、恢复、停止、音量、进度、seek、播放完成切歌、麦上/麦下路由切换。
## 3. 总体技术方案
采用“扫描服务 + 歌单状态管理 + Agora 播放适配 + UI 页面”的拆分方式。
```mermaid
flowchart LR
A["房间菜单 音乐入口"] --> B["RoomMusicPage"]
B --> C["RoomMusicManager"]
C --> D["RoomMusicRepository"]
D --> E["DataPersistence"]
C --> F["LocalMusicScanner"]
F --> G["on_audio_query / MediaStore"]
C --> H["RoomMusicPlayer"]
H --> I["RtcProvider / Agora RtcEngine"]
```
推荐新增目录:
- `lib/services/music/`
- `lib/modules/room/music/`
- `lib/ui_kit/widgets/room/music/`
推荐新增核心文件:
- `lib/shared/data_sources/models/sc_music_mode.dart`:扩展现有模型。
- `lib/shared/data_sources/models/sc_music_folder_mode.dart`:新增本地扫描文件夹模型。
- `lib/services/music/local_music_scanner.dart`:Android 本地 MP3 扫描。
- `lib/services/music/room_music_repository.dart`:已添加歌单本地持久化。
- `lib/services/music/room_music_manager.dart`:歌单、选择、播放状态总控。
- `lib/modules/room/music/room_music_page.dart`:音乐列表页。
- `lib/modules/room/music/room_music_folder_page.dart`:扫描后的文件夹列表页。
- `lib/modules/room/music/room_music_select_page.dart`:文件夹内音乐选择页。
- `lib/ui_kit/widgets/room/music/room_music_player_bar.dart`:播放器控制条。
- `lib/ui_kit/widgets/room/music/room_music_list_item.dart`:音乐列表项。
- `lib/ui_kit/widgets/room/music/room_music_empty_state.dart`:空状态。
## 4. 依赖与权限
### 4.1 pubspec
在 `pubspec.yaml` 增加:
```yaml
on_audio_query:
path: ./local_packages/on_audio_query-2.9.0
```
如果代码中直接使用 `package:path/path.dart` 做文件夹路径处理,建议把 `path` 显式加入依赖,避免依赖传递包:
```yaml
path: ^1.9.0
```
### 4.2 AndroidManifest
现有 Manifest 已有:
- `READ_EXTERNAL_STORAGE`
- `WRITE_EXTERNAL_STORAGE`
需要补充 Android 13+ 音频权限,并建议限制旧权限版本:
```xml
```
### 4.3 权限工具
在 `lib/shared/tools/sc_permission_utils.dart` 增加 `checkAudioPermission()`:
- Android 13+:请求 `Permission.audio`。
- Android 12 及以下:请求 `Permission.storage`。
- 需要使用项目已有 `device_info_plus` 判断 SDK 版本。
- 权限拒绝时不进入扫描结果页,只保留在当前流程;后续可补“去设置开启”引导。
## 5. 数据模型
### 5.1 音乐项
扩展 `SCMusicMode`,兼容旧字段读取:
```dart
class SCMusicMode {
final String id; // 优先用 MediaStore id;兜底用 localPath hash
final String title;
final String artist;
final int durationMs;
final String localPath; // SongModel.data
final String contentUri; // SongModel.uri,Agora Android 推荐优先使用
final String folderPath;
final String fileName;
final int size;
final int sortIndex;
final int addedAt;
}
```
关键点:
- 扫描只保留 `fileExtension == mp3` 或文件名 `.mp3` 的音频。
- 播放时优先使用 `contentUri`,为空再用 `localPath`。
- 持久化仍走 `DataPersistence.setUserRoomMusic(jsonEncode(list))`。
- 读取旧数据时要兼容当前模型里的 `title`、`artist`、`duration`、`localPath`。
### 5.2 文件夹模型
新增 `SCMusicFolderMode`:
```dart
class SCMusicFolderMode {
final String id;
final String name;
final String path;
final int musicCount;
final int addedCount;
final List songs;
}
```
文件夹统计规则:
- 使用 `p.dirname(song.localPath)` 作为文件夹路径。
- 单个文件夹只统计该文件夹当前层级直接包含的 MP3。
- 子文件夹内 MP3 归入子文件夹,不累计到父文件夹。
- 文件夹列表按名称或路径稳定排序。
## 6. 播放器方案
### 6.1 播放方式
统一使用 Agora `startAudioMixing`,原因是麦上需要把音乐推给远端。
麦下播放:
```dart
engine.startAudioMixing(
filePath: playPath,
loopback: true,
cycle: 1,
startPos: currentPositionMs,
);
```
麦上播放:
```dart
engine.startAudioMixing(
filePath: playPath,
loopback: false,
cycle: 1,
startPos: currentPositionMs,
);
```
这里的 `loopback` 是关键:
- `true`:只本地播放,只有自己听到。
- `false`:本地和远端都能听到。
### 6.2 麦上/麦下同步
当用户播放中上麦或下麦时,需要切换播放路由:
1. 记录当前播放位置:`getAudioMixingCurrentPosition()`。
2. `stopAudioMixing()`。
3. 根据 `RtcProvider.isOnMai()` 重新 `startAudioMixing(loopback: !isOnMai, startPos: position)`。
4. 恢复播放态和进度轮询。
接入点建议:
- 在 `RtcProvider` 上麦成功、下麦成功、角色切换成功后调用 `RoomMusicManager.syncPlaybackRouting(rtcProvider)`。
- 离房清理流程调用 `RoomMusicManager.stopAndReset()`.
### 6.3 控制能力
`RoomMusicManager` 需要封装:
- `play(SCMusicMode item)`
- `pause()`
- `resume()`
- `stop()`
- `previous()`
- `next({bool fromAutoComplete = false})`
- `seek(Duration position)`
- `setVolume(int percent)`
- `switchPlayMode()`
- `deleteMusic(SCMusicMode item)`
- `reorder(int oldIndex, int newIndex)`
Agora 对应 API:
- `startAudioMixing`
- `pauseAudioMixing`
- `resumeAudioMixing`
- `stopAudioMixing`
- `adjustAudioMixingVolume`
- `adjustAudioMixingPlayoutVolume`
- `adjustAudioMixingPublishVolume`
- `getAudioMixingDuration`
- `getAudioMixingCurrentPosition`
- `setAudioMixingPosition`
音量策略:
- UI 展示 0-100%。
- 默认同时调用 `adjustAudioMixingPlayoutVolume(volume)` 和 `adjustAudioMixingPublishVolume(volume)`。
- 如果后续产品区分“自己听到的音量”和“房间听到的音量”,再拆成两个滑杆。
### 6.4 播放模式
定义:
```dart
enum RoomMusicPlayMode {
list,
shuffle,
single,
}
```
规则:
- 列表顺序播放:播放完成后按当前排序下一首,末尾回到第一首或停止,建议按原型做循环到第一首。
- 随机播放:播放完成后随机选择非当前歌曲;只有一首时播放自身。
- 单曲循环:播放完成后继续当前歌曲。
切换 Toast:
- `列表顺序播放`
- `随机播放`
- `单曲循环播放`
### 6.5 播放完成
监听 `onAudioMixingStateChanged`:
- `audioMixingStatePlaying`:更新为播放中。
- `audioMixingStatePaused`:更新为暂停。
- `audioMixingStateStopped` + `audioMixingReasonAllLoopsCompleted`:按播放模式自动下一首。
- `audioMixingStateStopped` + `audioMixingReasonStoppedByUser`:停止态。
- `audioMixingStateFailed`:Toast 提示并停止当前播放,常见原因是文件被删除、路径无权限、格式不支持。
## 7. 页面与交互落地
### 7.1 房间操作面板入口
修改 `lib/ui_kit/widgets/room/room_menu_dialog.dart`:
- 恢复 `音乐` 入口。
- 点击后关闭菜单,打开 `RoomMusicPage`。
- 建议房主/管理员/普通用户都能看到入口;是否允许播放由房间配置 `allowMusic` 决定。如果产品要求强制限制,则判断 `currenRoom.roomProfile.roomSetting.allowMusic`。
### 7.2 音乐列表页
`RoomMusicPage` 状态:
- 空列表:展示空态图标、`没有音乐`、`从手机扫描添加`。
- 有列表:顶部 `添加`、搜索入口、编辑入口、总数、歌曲列表。
- 播放中:当前播放项高亮,底部显示播放器条。
- 编辑态:支持拖拽排序;也可保留左滑删除。
列表能力:
- 搜索按 `title` / `fileName` 包含匹配。
- 左滑删除,弹窗确认 `确定删除音乐?`。
- 删除当前播放歌曲时,先 `stop()`,再删除并持久化。
- 拖拽排序后立即更新 `sortIndex` 并持久化。
### 7.3 添加流程
流程:
```mermaid
flowchart TD
A["点击 添加/从手机扫描添加"] --> B["检查音频权限"]
B -->|"拒绝"| C["停留当前页"]
B -->|"允许"| D["扫描 MediaStore MP3"]
D -->|"无结果"| E["Toast 未找到音乐"]
D -->|"有结果"| F["文件夹列表页"]
F --> G["选择文件夹"]
G --> H["音乐选择页"]
H --> I["勾选/取消勾选"]
I --> J["点击 添加"]
J --> K["更新已添加列表并返回音乐列表"]
```
扫描规则:
- 每次进入添加流程都重新调用 `querySongs()`。
- 过滤 MP3。
- 过滤 `duration <= 0` 的异常项。
- 文件不存在或 `localPath` 为空的项不进入结果。
- 对同一个 MediaStore id 或同一路径去重。
选择规则:
- 已添加歌曲显示灰色勾选。
- 本次新选歌曲显示高亮勾选。
- 用户点击灰色勾选可取消已添加,提交后从列表移除。
- 只有存在“新增歌曲”时,添加按钮高亮;如果只有取消已添加,建议按钮文案仍为 `添加` 但可点击,否则无法完成取消动作。这里与原型“无新增则置灰”有轻微冲突,建议和产品确认;若严格按原型,则取消已添加后也需要允许提交,否则规则无法闭环。
## 8. 具体实施步骤
### 阶段 1:基础依赖与数据层
预计 4-6 小时。
1. 修改 `pubspec.yaml`,接入 `on_audio_query`。
2. 修改 `android/app/src/main/AndroidManifest.xml`,补 `READ_MEDIA_AUDIO`。
3. 扩展 `SCPermissionUtils.checkAudioPermission()`。
4. 扩展 `SCMusicMode`,新增 `SCMusicFolderMode`。
5. 新增 `RoomMusicRepository`,封装读取、保存、去重、排序。
6. 运行 `flutter pub get` 和基础静态检查。
### 阶段 2:扫描与添加流程
预计 6-8 小时。
1. 新增 `LocalMusicScanner`。
2. 使用 `OnAudioQuery.checkAndRequest()` 或项目权限工具 + `querySongs()` 扫描。
3. 实现 MP3 过滤、文件夹分组、已添加数量统计。
4. 实现文件夹列表页。
5. 实现音乐选择页、全选、新增勾选、已添加灰色勾选、取消已添加。
6. 添加成功后更新本地持久化并返回音乐列表。
### 阶段 3:音乐列表与 CRUD
预计 6-8 小时。
1. 实现 `RoomMusicPage` 空态、有数据态。
2. 接入搜索。
3. 接入左滑删除和二次确认。
4. 删除当前播放歌曲时停止播放。
5. 接入拖拽排序并持久化。
6. 接入口入口菜单。
### 阶段 4:播放器与 Agora 混流
预计 8-12 小时。
1. 新增 `RoomMusicManager extends ChangeNotifier`。
2. 在 `main.dart` 的 `MultiProvider` 中注册。
3. 封装播放、暂停、恢复、停止、上下首。
4. 封装进度轮询,间隔建议 500-1000ms。
5. 封装 `seek` 与音量调节。
6. 实现三种播放模式。
7. 处理 `onAudioMixingStateChanged` 自动切歌与失败兜底。
8. 在上麦/下麦/离房流程接入播放路由同步和清理。
### 阶段 5:UI 细节与真机验证
预计 6-10 小时。
1. 调整播放器底部高度,避免遮挡列表。
2. 接入播放中音乐 icon 旋转动画。
3. 补充多语言 key:没有音乐、从手机扫描添加、未找到音乐、确定删除音乐、音乐已删除、总计歌曲、播放模式 Toast 等。
4. Android 12、Android 13+ 真机验证权限。
5. 真机验证麦上/麦下播放、闭麦不影响音乐输出。
6. 回归语言房退出、切房、最小化、重新进入房间。
## 9. 建议工时
按 Android 单端、GPT-5.5 + Codex 协作速度估算:
| 模块 | 预计耗时 |
|---|---:|
| 依赖、权限、数据模型、持久化 | 4-6 小时 |
| MP3 扫描、文件夹分组、添加选择 | 6-8 小时 |
| 已添加列表、搜索、删除、排序 | 6-8 小时 |
| Agora 播放器、进度、音量、模式、上下首 | 8-12 小时 |
| 房间入口、上麦/下麦同步、离房清理 | 4-6 小时 |
| Android 真机测试与修复 | 6-10 小时 |
总计建议预留:34-50 小时。
如果先做 MVP:
- 扫描 MP3
- 添加到列表
- 播放/暂停/停止
- 上下首
- 音量
- 进度条
- 删除
- 麦下本地播放、麦上推流播放
预计:22-32 小时。
完整对齐原型并做稳定上线版本,建议按 40 小时左右排期。
## 10. 风险与处理
### 10.1 Android 10+ 路径访问
Agora 文档建议 Android 播放本地文件优先使用 `content://` URI。`on_audio_query` 的 `SongModel.uri` 可作为 `contentUri` 保存。
处理:
- 播放路径优先级:`contentUri` > `localPath`。
- 如果 `audioMixingReasonCanNotOpen`,提示文件无法播放,并可从列表移除失效文件。
### 10.2 添加按钮规则冲突
原型写到“无新增音乐时添加按钮置灰”,同时又写到“点击置灰已添加可取消,点击添加后移除”。如果只取消已添加但没有新增,按钮置灰会导致无法提交移除。
建议:
- 只要本次选择结果和原已添加状态有差异,按钮即可点击。
- 如果产品坚持按钮必须叫 `添加`,仍然允许提交取消动作。
### 10.3 播放状态跨麦位变化
用户麦下播放时是 `loopback: true`,上麦后需要切到 `loopback: false`。Agora 没有直接切换 loopback 的接口,需要 stop/start。
处理:
- 切换前读取进度。
- 用 `startPos` 恢复。
- 加 500ms 节流,避免连续上麦状态变化触发频繁 start。
### 10.4 当前播放歌曲被删除
处理:
- 删除前判断 `current?.id == target.id`。
- 是当前歌曲则先 `stopAudioMixing()`。
- 清空当前播放项和进度。
- 再持久化删除。
### 10.5 房间生命周期
处理:
- 离房、RTC disconnected 清理、App termination 释放前调用音乐停止。
- 切房时不保留上一房间播放状态。
- 最小化播放器仅影响 UI,不影响播放。
## 11. 验收清单
权限与扫描:
- Android 13+ 首次添加触发音频权限弹窗。
- Android 12 及以下可正常读取本地音乐。
- 拒绝权限后不会进入扫描结果页。
- 无 MP3 时 Toast `未找到音乐`。
- 扫描结果只包含 MP3。
- 文件夹数量符合“只统计当前层级直接音乐”的规则。
添加与列表:
- 空列表展示空态。
- 添加后返回音乐列表并展示总数。
- 二次添加能展示已添加数量。
- 已添加歌曲灰色勾选,新选择歌曲高亮勾选。
- 取消已添加并提交后,音乐列表移除对应歌曲。
- 搜索能按名称过滤。
- 左滑删除有确认弹窗。
- 删除成功 Toast `音乐已删除`。
- 拖拽排序后重新进入页面仍保持顺序。
播放:
- 点击歌曲开始播放。
- 播放/暂停/恢复正常。
- 上一首/下一首正常。
- 进度条自动推进。
- 拖动进度条能 seek。
- 音量调节生效。
- 列表顺序、随机、单曲循环三种模式正常。
- 播放完成后按当前模式切歌。
- 删除当前播放歌曲后立即停止。
房间场景:
- 麦下播放只有自己听到。
- 播放中上麦后,其他用户能听到音乐。
- 播放中下麦后,其他用户不再听到音乐,本地继续播放。
- 闭麦不影响音乐推流。
- 离房后音乐停止。
- 切房后不会继续上一房间音乐。
## 12. 推荐落地顺序
优先顺序:
1. 数据模型、持久化、权限、扫描。
2. 文件夹页和选择页。
3. 音乐列表页和入口。
4. 基础播放、暂停、停止。
5. 上下首、进度、音量、模式。
6. 麦上/麦下同步。
7. 删除、排序、搜索和细节回归。
这样可以尽早在真机上验证最不确定的两件事:Android MediaStore 扫描结果,以及 Agora 使用 `contentUri` 播放本地 MP3 的兼容性。