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}需要访问您的麦克风...")