joywayer
|
7d89a9d3ee
|
3.E 修复:换用 iOS 17+ AVAudioApplication.requestRecordPermission 避免 EXC_BREAKPOINT
## 问题
第一次调用 prepareaudio 弹出系统权限对话框,用户点允许/拒绝都触发
Thread 10 EXC_BREAKPOINT 崩溃。
## 根因猜测
deprecated 的 AVAudioSession.requestRecordPermission(_:) 在
iOS 26.5 SDK 上内部桥接到 AVAudioApplication 时,与
withCheckedContinuation 的 Swift Concurrency 状态机交互异常。
具体表现:completion 在 Thread 10 触发 cont.resume(returning:),
runtime 检测到某种 actor / continuation 状态违规,触发 trap。
## 修复
新增 `requestMicrophonePermission()` 私有助手:
```swift
if #available(iOS 17.0, *) {
return await AVAudioApplication.requestRecordPermission() // 原生 async
} else {
return await withCheckedContinuation { ... } // 旧 SDK 兼容
}
```
iOS 17+ 走原生 async API,不经 Continuation 状态机,避开 corner case;
iOS 15.6 / 16 走 deprecated API 兼容路径。
## 验证
BuildProject 通过 0 错误。真机首次启动 → 按录音按钮 → 系统权限对话框
出现 → 点允许/拒绝 → 应无崩溃。
|
2026-06-24 02:05:25 +08:00 |
|
joywayer
|
6f10d231ad
|
3.E 优化:权限被拒绝时的 Alert 增加"去授权"跳转按钮
iOS 行为约束:用户在系统权限对话框选择"拒绝"后,再次调
requestRecordPermission 不会再弹对话框、直接返回 false。唯一恢复路径
是用户手动去「设置 → app → 麦克风」开启。
原 Alert 只有"确定"按钮(对齐 daoqi),用户被拒绝后需要自己摸索路径
去设置。优化为两按钮:
- "取消":关闭 Alert
- "去授权":UIApplication.open(openSettingsURLString) 直接跳本 app
的设置页
文案保留 daoqi NewRootVC.m:316 的引导语("需要访问您的麦克风,请启用
麦克风-设置/隐私/麦克风!")。
属于 UX 改进,对 H5 不可见,不影响契约(按钮属于 native 内部 UI,
原则 B 范畴)。
|
2026-06-24 01:58:02 +08:00 |
|
joywayer
|
530309c258
|
3.E 修复:notDetermined 路径触发 cb 让 H5 释放业务锁
## 问题
用户首次点击录音 → 弹权限对话框 → 用户允许 → H5 整体无法再点击。
## 根因
我上次修复让 .notDetermined 分支 return 时不触发 cb,违反 daoqi 行为:
daoqi NewRootVC.m:322 在 ifauth==1(NotDetermined)路径下走录音流程并
**立即触发 responseCallback**。
H5 业务在 callHandler('prepareaudio') 调用后通常会上"录音中"业务锁
(加触摸遮罩、禁用按钮等),等收到 cb 才解锁。系统权限对话框已经把
H5 的 in-flight touch 打断为 touchcancel,H5 业务的 touchcancel 处理
可能不完善 → 业务锁不释放 → 整个 H5 后续无法点击。
## 修复
.notDetermined 分支权限请求完成后**仍然触发 cb**:
```swift
case .notDetermined:
_ = await requestRecordPermission()
callback?(.string("Response from prepareaudio")) // ← 触发 cb
return
```
H5 收到 cb 释放业务锁,整个 H5 恢复可点击。本次实际未录音 → 无后续
getaudiourl,H5 按"无 getaudiourl 的失败路径"走(与 daoqi 转码失败
silent 路径等价)。用户第二次按录音按钮走 .authorized 分支正常录音。
## 验证
BuildProject 通过 0 错误。
|
2026-06-24 01:56:52 +08:00 |
|
joywayer
|
97749ac177
|
3.E 修复:首次启动权限请求与浮层 touchObserver 死锁
## 问题
首次启动 app,H5 按住麦克风按钮触发 prepareaudio:
1. 主动调 AVAudioSession.requestRecordPermission 弹系统权限对话框
2. 用户为了点"允许"必须松手
3. 松手被 RecordingAwareWindow.touchObserver 立刻捕获 → recorder.stop()
4. 浮层瞬间关闭,录音失败
5. 用户感受:"按一次没反应,再点也没用"
## 根因
我之前用 `requestRecordPermission` 主动请求权限(会同步弹系统对话框),
与 daoqi `FuncPublic.ifauth`(只读 `AVCaptureDevice.authorizationStatus`,
不主动请求)行为不一致。主动请求 + 按住状态 + touchObserver 全局监听
三者形成死锁。
## 修复
改用只读状态 + 三分支处理:
- `.denied / .restricted`:弹 Alert + return(对齐 daoqi)
- `.notDetermined`(首次):**主动弹权限对话框但不启动录音浮层**。
用户做完选择后 return,不触发 cb。第二次按录音按钮时走 .authorized
分支正常录音
- `.authorized`:直接启动录音浮层
## 与 daoqi 行为对比
daoqi 在 .notDetermined 时 ifauth 返回 1(继续启动录音),但实测此路径
也有体验问题——浮层弹出 + 系统对话框遮挡 + 用户松手立刻触发 touchEnded
→ 录音空文件 → 转码失败 → silent return。最终用户体验也是"按一次没
反应"。
新外壳的修复让首次行为更明确(不启动录音 = 不会有空文件上传),与 daoqi
真实终态(不触发 getaudiourl)等价,UX 等价(首次启动需按两次)。
## H5 契约影响
无。"不触发 cb" 与 daoqi 转码失败 silent return 等价;H5 收不到
getaudiourl 反向 callback 时本来就要兼容(daoqi 各类失败路径都不触发 cb)。
## 验证
BuildProject 通过 0 错误。
|
2026-06-24 01:50:04 +08:00 |
|
joywayer
|
df2a099fb5
|
3.F:mediaTypeAudio 完整实现(远端语音下载→转码→播放→反向 callback)
替换 mediaTypeAudio handler 的 stub 为完整链路。至此 Phase 3 录音 + 远端
语音播放整套流程接通;prepareaudio / mediaTypeAudio / VoicePlaying / 反向
callback 全部对齐 daoqi msext 行为。
## 完整链路
1. H5 调 mediaTypeAudio({audiourl, user})
2. 解析入参;参数不全 → log + return(不触发 cb,对齐 daoqi 入参空时
走到 file 不存在 return 的语义)
3. `VoicePlayer.shared.recordIncomingUser(user)` 更新 lastUserId
⚠️ 无论后续是否实际播放都更新(对齐 daoqi gameController.m:438
`self.User_id=userid;` 总是执行的行为)
4. 注册 VoicePlayer.onFinish 绑定当前 bridge([weak bridge])→ 播完触发
`gameui_stop_voice(lastUserId)` 反向 callback
5. `URLSession.shared.data(from:)` 异步下载 AMR
- 下载失败 → log + return,**不触发 cb**
(对齐 daoqi NewRootVC.m:335-337 file 不存在 return)
6. 写入沙盒 .amr 临时文件
7. `AMRCodec.amrToWav` 转码到 `{tempName}_AmrToWav.wav`
(文件名约定对齐 daoqi NewRootVC.m:341)
- 转码失败 → log,**但 cb 仍触发**(对齐 daoqi NewRootVC.m:355-357
amr转wav失败 NSLog 后仍触发 responseCallback)
8. `await VoiceCenter.shared.isEnabled`(canbofang 等价)+ 实际播放
- 仅转码成功 + 开关 enabled 时 play + 触发 `gameui_play_voice(user)`
对齐 daoqi NewRootVC.m:343-353 if (canbofang) { play + callHandler }
- `gameui_play_voice` data 是 user 字符串(非 dict)
9. 触发 `cb("Response from mediaTypeAudio")` —— **总是**触发
(除下载失败 return 路径外)
## VoicePlayer.onFinish 绑定时机
每次 mediaTypeAudio 调用时更新 onFinish 闭包,绑定当前 bridge。
[weak bridge] 防止 bridge 释放后 onFinish 触发野指针。
## 关键契约点(已验证字面字符串)
- handler 名:"mediaTypeAudio"
- 入参字段:"audiourl" / "user"
- 回包字面:"Response from mediaTypeAudio"
- 反向 callback:
- "gameui_play_voice" data = user String
- "gameui_stop_voice" data = lastUserId String
## 验证
BuildProject 通过 0 错误。至此 Phase 3 全部子任务(3.A / 3.B / 3.C /
3.D / 3.E / 3.F)落地,语音功能完整对齐原项目。
|
2026-06-24 01:38:17 +08:00 |
|
joywayer
|
f2b456cf00
|
3.E:prepareaudio 完整实现 + 大厅/子游戏分流接入
替换 RemoteAudioHandler 中 prepareaudio 的 stub 为完整链路:权限检查 →
录音浮层 → 转码 → 七牛上传 → 反向 callback getaudiourl + 子游戏额外
recordSuccess。mediaTypeAudio 仍为 stub(Phase 3.F 处理)。
## 完整链路
1. H5 调 prepareaudio
2. `AVAudioSession.requestRecordPermission`
- 拒绝:弹 Alert("需要访问您的麦克风..."),**不触发 cb**
(对齐 daoqi NewRootVC.m:314-319)
3. 同意 → `RecordingPresenter.start(on: hostVC, fileName:)`
- 启动失败:log + return,**不触发 cb**
- 启动成功:立即触发 `cb("Response from prepareaudio")`,对齐 daoqi
NewRootVC.m:322 `[recorderVC beginRecordByFileName:..]` 之后 cb 行为
4. 用户松手 / 60s 自动停 → `processCompletedRecording` 后台 Task
- 用 AVAudioPlayer 读 wav `duration` → `round` 取整秒(对齐 daoqi
NewRootVC.m:1891-1893)
- 生成 amr 文件名:
- 大厅 `yyyyMMddHHmmss%08X.amr`(无下划线,NewRootVC.m:1908)
- 子游戏 `yyyyMMddHHmmss_%08X.amr`(有下划线,gameController.m:2269)
- `AMRCodec.wavToAmr`
- `QiniuUploader.upload`(key = 本地文件名,对齐 daoqi 上传命名)
5. 反向 callback(MainActor):
- `getaudiourl({audiourl: String, time: String})`
⚠️ time 是字符串(%ld 整秒),不是 number — 对齐 daoqi
NewRootVC.m:1923 / gameController.m:2305
- **仅子游戏**额外 `recordSuccess({fileUrl, fileName, fileKey})`
对齐 daoqi gameController.m:2310;大厅 NewRootVC 无此 callHandler
## register 签名扩展
新增参数:
- `hostVC: @escaping @MainActor @Sendable () -> UIViewController?`
—— 每次录音重新求值,避免捕获过时引用
- `isSubGame: Bool` —— 决定是否触发 recordSuccess
WebContainerViewController(大厅)传 `isSubGame: false`;
SubGameViewController(子游戏)传 `isSubGame: true`。
## QiniuUploader 改动
key 生成方式:UUID → 本地文件名(lastPathComponent)。让调用方完全控制
key 命名,对齐 daoqi 录音文件命名规则。H5 通过 audiourl 转发给对端,对端
按文件名规律假设,必须严格对齐。
## 错误处理
启动失败 / 转码失败 / 上传失败:log + return,不触发反向 callback。
对齐 daoqi 同款 silent return 行为(mediaTypeAudio 下载失败、转码失败
也是同样 silent return 模式)。
## 验证
BuildProject 通过 0 错误。
|
2026-06-24 01:36:25 +08:00 |
|
 joywayerandClaude Opus 4.7
|
0602b9d4b6
|
Phase 3.A:srcIsloop 本地音频 + 3.B/3.C/3.D handler stub
3.A 本地音频完整落地(与 msext NewRootVC initJSdata 等价):
Source/Audio/AudioPlayer.swift(@MainActor 状态机):
- playOnce(url) 单次按钮音;buttonPlayers 数组保活池避免多次点击
互相打断(duration + 0.5s 后清理)
- loopBackground(url, type:) 循环背景音;numberOfLoops = -1 +
backgroundType 记录用于同名停止
- stopBackground(type:) 同名校验后才停止(msext 同款语义)
Source/Bridge/Handlers/LocalAudioHandler.swift:
- 注册 srcIsloop handler(@MainActor enum)
- 入参 src + isloop,按 0/1/-1 分支调 AudioPlayer.shared 对应方法
- audioFileURL 路径:{Caches}/{gamedir}/{gamestart}/assets/wav/{src}
与 msext NewRootVC 路径约定 1:1(H5 zip 内 assets/wav 子目录)
- audioFileURL 标 nonisolated 因 SandboxPaths 是 nonisolated 静态
- cb 字面 "Response from srcIsloop" 严格保持
Source/Bridge/Handlers/RemoteAudioHandler.swift(3.B/3.C/3.D stub):
- prepareaudio + mediaTypeAudio 仅 cb 维持契约,无真实业务
- 待 Phase 3.B(opencore-amr .a 接入)+ 3.C(七牛 SPM + token)+
3.D(远端播放)完整后升级
- 按 CLAUDE.md 原则 A:H5 调啥就得有人接,即使是 stub
WebContainerViewController.registerBridgeHandlers 接入:
- LocalAudioHandler.register(完整实现)
- RemoteAudioHandler.register(stub)
Plan §5 Phase 3.1 勾选;3.2 真机验证留待 H5 提供音频文件后做。
Verification-Checklist.md 新增 Phase 3 章节:
- H. 本地音频(E.14-E.16 srcIsloop 三个 isloop 值)
- I. prepareaudio / mediaTypeAudio stub(E.17 E.18)
BuildProject 通过
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
2026-06-22 19:11:34 +08:00 |
|