Files
youle_app_ohos/docs/superpowers/specs/2026-06-27-录音流程-design.md
T
lanterngamescnandClaude Opus 4.8 3a224fd7e9 docs(录音): 录音流程设计文档(prepareaudio→AMR→七牛端侧token→getaudiourl)
对齐 Android webviewActivity 录音流程;手势走方案A(Web父层并行手势)、
AMR-NB(CFT_AMR+AUDIO_AMR_NB)、七牛token端侧生成(对齐原工程不走服务端)、
分层静音(录音暂停全部原生声音+gameui_stop_voice,取消/太短/发送都恢复)。
对应契约 §8.7/§9/§10.6,无需改契约。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 16:33:30 +08:00

160 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 录音流程设计(prepareaudio → AMR 录音 → 七牛上传 → getaudiourl
- 日期:2026-06-27
- 契约依据:§8.7 / §9 / §10.6`InboundHandlers.PrepareAudio='prepareaudio'``OutboundHandlers.GetAudioUrl='getaudiourl'``GameUiStopVoice='gameui_stop_voice'`、DTO `GetAudioUrlResp{audiourl,time,filepath?}`(均已存在,**无需改契约**
- 对照基准:原 Android `webviewActivity.java``prepareAudio()``onTouch``upload()``reset()`+ `com.nickming.view.AudioManager`
## 1. 背景与现状
当前 HarmonyOS 侧 `AudioProvider.PrepareAudio` 是空桩(只打日志),录音三段(录音 / 手势 / 上传回传)全缺。本设计落地完整录音流程,**对 H5 边界行为 100% 对齐 Android**,原生内部按 HarmonyOS 最佳实践自由实现。
原 Android 录音是 **H5 触发 + 原生手势** 的混合流程:
- H5 按钮 `touchstart` → 桥 `prepareaudio`(H5 只调这一次;契约**无**"松手/移动/取消"入站接口)
- 原生 WebView `onTouch`return false 不吞)跟踪 DOWN/MOVE/UP:上滑取消、抬手发送、`mTime<0.8s` 太短
- 正常结束 → 七牛上传(端侧硬编码 AK/SK)→ 回投 `getaudiourl{audiourl,time,filepath}`
- 取消/太短 → 删文件 → 回投 `getaudiourl{audiourl:"",time:0}`
## 2. 关键决策(已与用户确认)
| # | 决策 | 结论 |
|---|---|---|
| D1 | 录音格式 | **必须 AMR**(跨端 App 互通)。HarmonyOS 支持:`CFT_AMR` 容器 + `AUDIO_AMR_NB` 编码(API 18+AVCodec 格式表明确支持) |
| D2 | 七牛 token | **对齐原工程、不走服务端**:端侧用 Android 同款 AK/SK 生成 uploadToken。⚠️ 刻意违背 CLAUDE.md 加固红线,代码注释标注 |
| D3 | 手势接入 | **方案 A**:Web 外层绑并行手势观察触摸(对齐 Android `onTouch`)。真机证伪不过则切方案 C(注入 JS shim) |
| D4 | 暂停声音范围 | **全部原生在播声音**(语音消息+BGM+音效)静音;并对正在播的语音消息回投 `gameui_stop_voice(user)` 通知 H5 |
| D5 | 恢复声音时机 | **取消/太短/正常发送 三种结束都恢复**(恢复到 H5 `voicePlaying` 原本静音态,不盖 H5 意图) |
| D6 | UI 波形 | **装饰动画**(计时器驱动),非真实振幅——AVRecorder 不暴露振幅 API,用户已接受 |
## 3. 架构
套用现成的 Share 协作范式(`ShareProvider` emit EventBus → 容器弹叠加 UI → 一次性回投)。
| 角色 | 归属 | 职责 |
|---|---|---|
| **AudioProvider** | feature_capabilities | 注册 `prepareaudio`;持有 `AmrRecorder`;端侧生成七牛 token、上传、回投 `getaudiourl`;声音暂停/恢复(分层静音) |
| **AmrRecorder**(新) | feature_capabilities | 封装 `@kit.MediaKit` AVRecorderAMR-NB),录到 cache 目录 `UUID.amr`,无 UIstart/stop/cancel(删文件) |
| **BridgeGameContainer** | entry/pages | Web 外层绑并行手势观察 DOWN/MOVE/UP + UI 计时器 + 太短判定;挂 `RecordingOverlay`;经 `AudioEvents` 与 Provider 协作 |
| **RecordingOverlay**(新) | entry/components | 纯展示浮层,由容器手势状态驱动(复刻 Android `DialogManager` 只画不管逻辑) |
| **AudioEvents**(新) | common/event | Provider↔容器 事件通道(仿 `ShareEvents` |
| **QiniuToken**(新) | platform | 端侧 HMAC-SHA1 生成七牛 uploadToken |
依赖方向不变(单向向下):entry(①②) → feature_capabilities(④) / platform(⑥) / common(横切)。桥核心零感知不变。
## 4. 数据流(职责切分对齐 Android)
```
H5 touchstart → prepareaudio ──► AudioProvider.onPrepareAudio()
│ 1. recordMuted=true(静音全部原生在播声音)
│ 2. 若有在播语音消息 → callHandler(gameui_stop_voice, user) + releaseVoice
│ 3. 申请 MICROPHONE 权限(首次)
│ 4. AmrRecorder.start() ──成功──► emit AudioEvents.SHOW_RECORDING{resultEvent}
│ ──失败──► recordMuted=false 恢复,静默放弃(不弹浮层)
容器收到 SHOW_RECORDING:显示 RecordingOverlay + 激活手势观察 + 启动 UI 计时器(mTime: 每100ms+0.1)
· MOVE 上滑 > 阈值 → 想取消态(红,"松开取消");否则 录音中态
· UP
- mTime < 0.8s → 太短态显示 1.3s → emit resultEvent{action:'tooShort'}
- 上滑(想取消) → emit resultEvent{action:'cancel'}
- 正常 → emit resultEvent{action:'send', timeSec: mTime}
· 撤浮层 + 解除手势观察
AudioProvider 收到 resultEvent{action}
· send → AmrRecorder.stop() → 端侧 token → QiniuUploader.upload(UUID.amr)
├ 成功 → callHandler(getaudiourl, {audiourl:url, time:timeSec, filepath:url})
└ 失败 → callHandler(getaudiourl, {audiourl:"", time:0}) + toast
· cancel/tooShort → AmrRecorder.cancel()(停+删) → callHandler(getaudiourl, {audiourl:"", time:0})
· 任何 action 结束 → recordMuted=false(恢复声音)
```
**计时归属**:UI 计时器在容器(复刻 Android `mTime``mGetVoiceLevelRunnable` 每 100ms +0.1,非录音器时长)。`send` 时传回的 `timeSec` 即此 UI 计时值。
**事件协议(AudioEvents**
- Provider → 容器:`SHOW_RECORDING { resultEvent: string }`
- 容器 → Provideremit `resultEvent` 携带 `{ action: 'send'|'cancel'|'tooShort', timeSec?: number }`
- 复用 Share 既有的"一次性 once 回投 + 悬挂保护"约定(新请求来时先按 cancel 回投旧请求)
## 5. 手势接入(方案 A,对齐 Android onTouch
H5 按下时手指已落在 Web 上,原生须跟踪**进行中**的同一次触摸。
- 在包裹激活 Web 的 Stack 上绑 `parallelGesture(PanGesture)` + `onTouch`,与 Web 内部事件**并行识别、不吞**触摸。
- 仅在录音激活期(收到 SHOW_RECORDING 到 UP 之间)消费这些回调;非录音期忽略,绝不影响 H5 正常触摸/滚动。
- 上滑取消阈值对齐 Android `wantToCancel`:位移 > 100vp(按 Android 的 100px 量级取等价 vp)。
- 浮层纯展示,状态机:`录音中 / 想取消 / 太短`,完全由手势回调驱动。
**兜底方案 C**(A 真机证伪不过时):prepareaudio 后原生注入 JS 给 `document``touchmove/touchend`,坐标经 `yy://` 回报,结束移除。对 H5 源码零改动(注入在原生侧)。
## 6. AMR 录音参数(对齐 Android RAW_AMR + AMR_NB
`media.AVRecorderProfile`
- `audioSourceType = media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC`
- `fileFormat = media.ContainerFormatType.CFT_AMR`
- `audioCodec = media.CodecMimeType.AUDIO_AMR_NB`
- `audioSampleRate = 8000`AMR-NB 标准)
- `audioChannels = 1`
- `audioBitrate = 12200`AMR-NB 最高档)
输出:cache 目录下 `UUID.amr``util.generateRandomUUID()`),AVRecorder 用 fd`fileIo.open` 后取 `fd://<fd>`)。AVRecorder 状态机:`prepare → start → stop → release`stop 后取文件路径上传。
## 7. 七牛 token 端侧生成(D2,对齐原工程)
- AK/SKAndroid 同款(`ngN3rFW1j8dn7ZGpATKl7mreaNmp2Ei_l9AIhkIf` / `6VXav9eqUCORJTYvTFOeTVRonAyk4Hrs-PIZ8jmZ`),bucket/scope `gameauio`,回放域名 `gameaudio.daoqi88.cn`
- token 算法:`AK : urlsafeBase64(HMAC_SHA1(SK, encodedPolicy)) : encodedPolicy`,其中 `encodedPolicy = urlsafeBase64(JSON({scope:"gameauio", deadline: nowSec+3600}))`
- HMAC-SHA1 经 `@kit.CryptoArchitectureKit` cryptoFramework`HMAC` + `SHA1`
- ⚠️ lint `@security/no-unsafe-mac` 会报 SHA1 → **行内 `// eslint-disable` + 注释说明七牛协议强制 SHA1、且 AK/SK 端侧为刻意对齐原工程**
- key=`UUID.amr` → 回放 `http://gameaudio.daoqi88.cn/UUID.amr`
- 复用现成 `platform/QiniuUploader.upload(ctx, localPath, key, tokenProvider)``tokenProvider = () => Promise.resolve(token)`
## 8. 声音暂停/恢复(D4/D5,分层静音)
- `AudioProvider` 现有 `muted`H5 经 `voicePlaying` 设定)之上加 `recordMuted: boolean`
- **有效静音 = muted || recordMuted**;重构 `setVolume` 调用点统一走有效静音
- 录音开始:`recordMuted=true` + 应用;任何结束:`recordMuted=false` + 应用 → 恢复到 `muted` 原态(不解掉 H5 主动静音)
- 录音开始时若 `voicePlayer` 在播:取其 `user`(新增 `currentVoiceUser` 跟踪)→ `callHandler(gameui_stop_voice, currentVoiceUser)` + `releaseVoice()`(对齐 Android
## 9. 权限
- `module.json5` 声明 `ohos.permission.MICROPHONE`reason 文案 + abilities 内 `when:"inuse"`
- 首次 `prepareaudio` 运行时 `abilityAccessCtrl.requestPermissionsFromUser`
- 拒绝 → 不录、`recordMuted=false` 恢复、静默放弃(不回 `getaudiourl`,对齐 Android `resetPrepared`
- `gameaudio.daoqi88.cn` 明文 HTTP:确认/补充到 `module.json5` 网络安全 cleartext 放行清单
## 10. UI 浮层(D6,现代微信式)
- 居中圆角深色半透卡(约 160×160vp):顶部麦克风图标 + **装饰音量波形/跳动竖条**(计时器驱动随机高度)+ 底部提示
- `录音中`:提示"手指上滑,取消发送"
- `想取消`:卡片转红、图标→取消/叉、提示"松开手指,取消发送"
- `太短`:图标→感叹号、文案"说话时间太短",显示 ~1.3s 后自动消失
- 叠加层级与 SharePanel 同级(容器 Stack 顶层),不抢系统安全区
- 资源:麦克风/取消图标若 `docs/Res` 无现成,用 ArkUI symbol/矢量或简单绘制
## 11. 错误处理
| 场景 | 处理 |
|---|---|
| AVRecorder create/prepare/start 失败 | 不弹浮层;`recordMuted=false` 恢复;可选 toast;不回 `getaudiourl` |
| 上传失败 | `getaudiourl{audiourl:"",time:0}` + toast"语音发送失败";恢复声音 |
| 录音中切后台/容器销毁 | `AmrRecorder.cancel()`(停+删)+ 恢复声音 + 撤浮层 |
| 并发:录音中再来 prepareaudio | 忽略(`isRecording` 守卫,仿 Android 1s 防抖) |
| 权限拒绝 | 见 §9 |
## 12. 受影响文件
- **新增**`common/.../event/AudioEvents.ets``platform/.../upload/QiniuToken.ets``feature_capabilities/.../providers/AmrRecorder.ets``entry/.../components/RecordingOverlay.ets`
- **修改**`feature_capabilities/.../providers/AudioProvider.ets`(实现 prepareaudio + 分层静音 + currentVoiceUser)、`entry/.../pages/BridgeGameContainer.ets`(手势观察 + overlay + AudioEvents 接线)、`entry/.../di/AppModule.ets`(如需注入七牛参数)、`module.json5`MICROPHONE + cleartext 域名)
- **无需改**contractsDTO/枚举齐全)、桥核心
## 13. 真机阻塞验证关(实施第一步,仿 M1/M2)
1. **手势**:真机证伪方案 A——Web 父层并行手势能否稳定收到 DOWN/MOVE/UP。不过 → 切方案 C。
2. **AMR 编码器**:真机(TLR-AL00 / API 23)验证 `CFT_AMR`+`AUDIO_AMR_NB` AVRecorder 能创建/录出有效 `.amr`("编码能力设备强相关")。
3. **端到端**:录音 → 端侧 token → 七牛上传 → `getaudiourl` → H5 回放 `.amr`(经现有 `mediaTypeAudio` 远程 URL 播放链路验证可放)。
## 14. 验收口径
- H5 长按按钮 → 现代录音浮层出现、波形跳动、声音被静音
- 上滑 → 转红"松开取消";松开 → 不发送、声音恢复、`getaudiourl{"",0}`
- 正常松手且 ≥0.8s → 上传成功、`getaudiourl{audiourl,time,filepath}`、声音恢复、H5 能回放该语音
- <0.8s 松手 → "说话时间太短"、不上传、`getaudiourl{"",0}`、声音恢复
- 全程 H5 一行不改