yumi-flutter/docs/android-room-music-execution-plan.md
2026-04-29 14:09:34 +08:00

18 KiB
Raw Permalink Blame History

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/ 已有音乐相关图标:播放、暂停、上一首、下一首、音量、空态、播放模式、删除、添加、音乐入口等。
  • 多语言中已有 musicmyMusiclocalMusicadddeletesearcheditconfirmcancel 等基础文案。
  • local_packages/on_audio_query-2.9.0local_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 页面”的拆分方式。

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.dartAndroid 本地 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 增加:

on_audio_query:
  path: ./local_packages/on_audio_query-2.9.0

如果代码中直接使用 package:path/path.dart 做文件夹路径处理,建议把 path 显式加入依赖,避免依赖传递包:

path: ^1.9.0

4.2 AndroidManifest

现有 Manifest 已有:

  • READ_EXTERNAL_STORAGE
  • WRITE_EXTERNAL_STORAGE

需要补充 Android 13+ 音频权限,并建议限制旧权限版本:

<uses-permission
    android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32" />
<uses-permission
    android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

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,兼容旧字段读取:

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.uriAgora 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))
  • 读取旧数据时要兼容当前模型里的 titleartistdurationlocalPath

5.2 文件夹模型

新增 SCMusicFolderMode

class SCMusicFolderMode {
  final String id;
  final String name;
  final String path;
  final int musicCount;
  final int addedCount;
  final List<SCMusicMode> songs;
}

文件夹统计规则:

  • 使用 p.dirname(song.localPath) 作为文件夹路径。
  • 单个文件夹只统计该文件夹当前层级直接包含的 MP3。
  • 子文件夹内 MP3 归入子文件夹,不累计到父文件夹。
  • 文件夹列表按名称或路径稳定排序。

6. 播放器方案

6.1 播放方式

统一使用 Agora startAudioMixing,原因是麦上需要把音乐推给远端。

麦下播放:

engine.startAudioMixing(
  filePath: playPath,
  loopback: true,
  cycle: 1,
  startPos: currentPositionMs,
);

麦上播放:

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 播放模式

定义:

enum RoomMusicPlayMode {
  list,
  shuffle,
  single,
}

规则:

  • 列表顺序播放:播放完成后按当前排序下一首,末尾回到第一首或停止,建议按原型做循环到第一首。
  • 随机播放:播放完成后随机选择非当前歌曲;只有一首时播放自身。
  • 单曲循环:播放完成后继续当前歌曲。

切换 Toast

  • 列表顺序播放
  • 随机播放
  • 单曲循环播放

6.5 播放完成

监听 onAudioMixingStateChanged

  • audioMixingStatePlaying:更新为播放中。
  • audioMixingStatePaused:更新为暂停。
  • audioMixingStateStopped + audioMixingReasonAllLoopsCompleted:按播放模式自动下一首。
  • audioMixingStateStopped + audioMixingReasonStoppedByUser:停止态。
  • audioMixingStateFailedToast 提示并停止当前播放,常见原因是文件被删除、路径无权限、格式不支持。

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 添加流程

流程:

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.dartMultiProvider 中注册。
  3. 封装播放、暂停、恢复、停止、上下首。
  4. 封装进度轮询,间隔建议 500-1000ms。
  5. 封装 seek 与音量调节。
  6. 实现三种播放模式。
  7. 处理 onAudioMixingStateChanged 自动切歌与失败兜底。
  8. 在上麦/下麦/离房流程接入播放路由同步和清理。

阶段 5UI 细节与真机验证

预计 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_querySongModel.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 的兼容性。