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)落地,语音功能完整对齐原项目。
This commit is contained in:
@@ -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}需要访问您的麦克风...")
|
||||
|
||||
Reference in New Issue
Block a user