From df2a099fb5ed8544542a6acfca4bdd8b7b6e7348 Mon Sep 17 00:00:00 2001 From: joywayer Date: Wed, 24 Jun 2026 01:38:17 +0800 Subject: [PATCH] =?UTF-8?q?3.F=EF=BC=9AmediaTypeAudio=20=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=EF=BC=88=E8=BF=9C=E7=AB=AF=E8=AF=AD=E9=9F=B3?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E2=86=92=E8=BD=AC=E7=A0=81=E2=86=92=E6=92=AD?= =?UTF-8?q?=E6=94=BE=E2=86=92=E5=8F=8D=E5=90=91=20callback=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 替换 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)落地,语音功能完整对齐原项目。 --- .../Bridge/Handlers/RemoteAudioHandler.swift | 116 +++++++++++++++++- 1 file changed, 110 insertions(+), 6 deletions(-) diff --git a/ylgamehall/Source/Bridge/Handlers/RemoteAudioHandler.swift b/ylgamehall/Source/Bridge/Handlers/RemoteAudioHandler.swift index da46ae8..ac0605e 100644 --- a/ylgamehall/Source/Bridge/Handlers/RemoteAudioHandler.swift +++ b/ylgamehall/Source/Bridge/Handlers/RemoteAudioHandler.swift @@ -62,12 +62,17 @@ public enum RemoteAudioHandler { } } - // 【5】mediaTypeAudio — Phase 3.F stub(仅 cb 维持契约) - // 完整实现(Phase 3.F):voicePlaying 总开关短路 + 下载 AMR + AMR→WAV + - // VoicePlayer 播放 + 反向 callback gameui_play_voice(user) / - // gameui_stop_voice(user) - bridge.register("mediaTypeAudio") { _, callback in - callback?(.string("Response from mediaTypeAudio")) + // 【5】mediaTypeAudio — 完整实现(Phase 3.F) + // cb: "Response from mediaTypeAudio"(在下载+转码完成后触发,对齐 daoqi + // 同步阻塞下载语义;下载失败时不触发 cb) + bridge.register("mediaTypeAudio") { data, callback in + Task { @MainActor in + await handleMediaTypeAudio( + data: data, + bridge: bridge, + callback: callback + ) + } } } @@ -203,6 +208,105 @@ public enum RemoteAudioHandler { return "\(timestamp)\(separator)\(random).amr" } + // MARK: - mediaTypeAudio 处理(MainActor) + + @MainActor + private static func handleMediaTypeAudio( + data: BridgeData?, + bridge: any BridgeProtocol, + callback: BridgeCallback? + ) async { + // 1. 解析入参(只有 audiourl + user 两个字段,对齐 daoqi + // NewRootVC.m:326-328 / gameController.m:435-437) + guard let obj = data?.asObject, + let urlString = obj["audiourl"]?.asString, + let user = obj["user"]?.asString, + let downloadURL = URL(string: urlString) + else { + log.error("mediaTypeAudio: bad params \(String(describing: data), privacy: .public)") + return // 参数不全 → 不触发 cb(对齐 daoqi 入参空时走到 file 不存在 return) + } + + // 2. 记录 lastUserId(无论是否实际播放都更新,对齐 daoqi + // `self.User_id = userid;` 总是执行的行为) + VoicePlayer.shared.recordIncomingUser(user) + + // 3. 注册 VoicePlayer.onFinish(每次 mediaTypeAudio 更新,绑定当前 bridge; + // [weak bridge] 让 bridge 释放后 onFinish 自动失效) + VoicePlayer.shared.onFinish = { [weak bridge] finishedUser in + Task { @MainActor in + bridge?.call( + "gameui_stop_voice", + data: .string(finishedUser), + callback: nil + ) + } + } + + // 4. 异步下载 AMR + let amrData: Data + do { + let (d, _) = try await URLSession.shared.data(from: downloadURL) + amrData = d + } catch { + log.error("mediaTypeAudio download failed: \(error.localizedDescription, privacy: .public)") + return // 下载失败 → 不触发 cb(对齐 daoqi NewRootVC.m:335-337 + // `if (![[NSFileManager defaultManager] fileExistsAtPath:filename]) return;`) + } + + // 5. 写到沙盒 .amr 临时文件 + let tempName = VoiceRecorder.currentTimeString() + let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + let amrLocal = docs.appendingPathComponent(tempName).appendingPathExtension("amr") + try? FileManager.default.removeItem(at: amrLocal) + do { + try amrData.write(to: amrLocal) + } catch { + log.error("mediaTypeAudio write amr failed: \(error.localizedDescription, privacy: .public)") + return + } + + // 6. AMR → WAV 转码 + // daoqi 文件名约定:tempName + "_AmrToWav.wav"(NewRootVC.m:341) + let wavLocal = docs + .appendingPathComponent("\(tempName)_AmrToWav") + .appendingPathExtension("wav") + try? FileManager.default.removeItem(at: wavLocal) + let transcoded: Bool + do { + try AMRCodec.amrToWav(amrLocal, to: wavLocal) + transcoded = true + } catch { + // 转码失败:log,**但 cb 仍触发**(对齐 daoqi NewRootVC.m:355-357 + // 失败仅 NSLog 不影响 responseCallback 触发) + log.error("mediaTypeAudio amr→wav failed: \(error.localizedDescription, privacy: .public)") + transcoded = false + } + + // 7. voicePlaying 总开关 + 实际播放 + gameui_play_voice + // (仅转码成功 + 开关 enabled 时触发,对齐 daoqi + // NewRootVC.m:343-353 if (canbofang) { play + callHandler }) + let canPlay = await VoiceCenter.shared.isEnabled + if transcoded, canPlay { + do { + try VoicePlayer.shared.play(wavLocal) + // gameui_play_voice 数据是 user 字符串(非 dict) + bridge.call( + "gameui_play_voice", + data: .string(user), + callback: nil + ) + log.debug("gameui_play_voice user=\(user, privacy: .public)") + } catch { + log.error("VoicePlayer.play failed: \(error.localizedDescription, privacy: .public)") + } + } + + // 8. 触发 mediaTypeAudio cb(对齐 daoqi NewRootVC.m:357,**总是**触发 + // 即使转码失败 / canbofang=NO 都触发;仅下载失败的 return 路径不触发) + callback?(.string("Response from mediaTypeAudio")) + } + @MainActor private static func showMicrophonePermissionAlert(on hostVC: UIViewController?) { // 对齐 daoqi NewRootVC.m:316 文案("{gamehallname}需要访问您的麦克风...")