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 维持契约)
|
// 【5】mediaTypeAudio — 完整实现(Phase 3.F)
|
||||||
// 完整实现(Phase 3.F):voicePlaying 总开关短路 + 下载 AMR + AMR→WAV +
|
// cb: "Response from mediaTypeAudio"(在下载+转码完成后触发,对齐 daoqi
|
||||||
// VoicePlayer 播放 + 反向 callback gameui_play_voice(user) /
|
// 同步阻塞下载语义;下载失败时不触发 cb)
|
||||||
// gameui_stop_voice(user)
|
bridge.register("mediaTypeAudio") { data, callback in
|
||||||
bridge.register("mediaTypeAudio") { _, callback in
|
Task { @MainActor in
|
||||||
callback?(.string("Response from mediaTypeAudio"))
|
await handleMediaTypeAudio(
|
||||||
|
data: data,
|
||||||
|
bridge: bridge,
|
||||||
|
callback: callback
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +208,105 @@ public enum RemoteAudioHandler {
|
|||||||
return "\(timestamp)\(separator)\(random).amr"
|
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
|
@MainActor
|
||||||
private static func showMicrophonePermissionAlert(on hostVC: UIViewController?) {
|
private static func showMicrophonePermissionAlert(on hostVC: UIViewController?) {
|
||||||
// 对齐 daoqi NewRootVC.m:316 文案("{gamehallname}需要访问您的麦克风...")
|
// 对齐 daoqi NewRootVC.m:316 文案("{gamehallname}需要访问您的麦克风...")
|
||||||
|
|||||||
Reference in New Issue
Block a user