# 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 的兼容性。