微信分享去sdk

This commit is contained in:
joywayer
2026-06-30 17:40:54 +08:00
parent 04e702a99b
commit c1e51d2183
2 changed files with 198 additions and 146 deletions
+49 -32
View File
@@ -7,22 +7,22 @@
// 契约:docs/H5-Native-Contract.md §3.1 [5]mediaTypeAudio + §3.2 [7][8]
// gameui_play_voice / gameui_stop_voice
//
// ## 与原项目的行为对齐
// ## 与原项目的行为差异(用户反馈驱动的修正)
//
// daoqi msext NewRootVC.m / gameController.m 把 voicePlayer / User_id 作
// 为 VC 成员变量;本类把同款状态封装为单例。关键行为:
// daoqi msext NewRootVC.m / gameController.m 把 voicePlayer / User_id 作为
// VC 成员变量,覆盖式播放时直接 `voicePlayer = [[AVAudioPlayer alloc]...]`
// 替换 ivar,没有触发任何停旧 player 的回调;自然结束时再用最新的
// `self.User_id`(已被新一条消息覆盖)调 stopradio → 发给 H5 的
// `gameui_stop_voice` payload 实际是**新一条消息的 user_id**,导致旧消息
// 对应的 H5 语音动画永远停不掉。
//
// - `lastUserId`:mediaTypeAudio 入参 user 字段(每次 mediaTypeAudio 进来
// 都更新,无论是否真的播放——对齐 daoqi gameController.m:438
// `self.User_id=userid;` 在 canbofang=NO 时也会执行)
// - `gameui_stop_voice` payload 用 `lastUserId`,不是 didFinishPlaying 传
// 入的 player 关联值(对齐 daoqi stopradio 用 self.User_id 的行为;多条
// 消息连发时 gameui_stop_voice 发最新 user,**这是 daoqi 的行为**)
// - 覆盖式 play:前一条还在播时收到第二条,直接覆盖 player(让旧的被
// ARC/ARC-like 释放);对齐 daoqi `voicePlayer = [[AVAudioPlayer alloc]...]`
// 赋值行为
// - 主动 stop **不**触发 onFinish(daoqi 没有主动 stop 路径,只有
// audioPlayerDidFinishPlaying 触发 stopradio)
// 本类把 user 与 player 一一绑定(`currentUser`):
// - 覆盖式 play:在替换 player 之前主动 stop 旧 player 并触发
// `onFinish(currentUser)`,让 H5 收到正确的 `gameui_stop_voice(被打断 user)`
// - 自然结束 / 解码失败:用 `currentUser` 而非任何后写入的"最近 user"
// 触发 onFinish,保证停的是真正结束那条
// - 主动 stop():不触发 onFinish(保留 daoqi 语义;本项目目前也没有外部
// 主动 stop 调用路径)
//
import AVFoundation
@@ -36,42 +36,57 @@ public final class VoicePlayer: NSObject, AVAudioPlayerDelegate {
private var player: AVAudioPlayer?
/// 最近一次 mediaTypeAudio 入参中的 user 字段。
/// 对齐 daoqi self.User_id 行为(成员变量,每次 mediaTypeAudio 进来都更新)。
public private(set) var lastUserId: String = ""
/// 当前正在播放的语音对应的 user。与 `player` 一一绑定:`player != nil` 时一定有值,
/// `player == nil` 时为 nil。覆盖式 play / didFinishPlaying / decodeError 都用这个
/// 值触发 onFinish,确保 `gameui_stop_voice` payload 与真正结束 / 被中断的语音对应。
private var currentUser: String?
/// 播放完成回调(AVAudioPlayer didFinishPlaying 触发,成功/失败都触发)。
/// mediaTypeAudio handler 注册此 closure 触发反向调用 gameui_stop_voice。
/// payload 是 `lastUserId`(不是 player 关联的 user,对齐 daoqi 行为)。
/// 播放完成 / 被中断 / 解码失败统一回调。
/// mediaTypeAudio handler 注册此 closure → 反向调 H5 `gameui_stop_voice`。
/// payload 始终是与刚结束 / 被打断 player 绑定的 user(不是入参中最新的 user)。
public var onFinish: ((String) -> Void)?
private override init() { super.init() }
/// 记录 mediaTypeAudio 入参的 user,**无论是否实际播放都要调**。
/// 对齐 daoqi gameController.m:438(voicePlaying=0 时仍执行 `self.User_id=userid`)。
public func recordIncomingUser(_ user: String) {
lastUserId = user
}
/// 播放 wav。若当前已有正在播放的 player,先 stop 旧 player 并触发
/// `onFinish(被打断的 user)`,让 H5 把旧动画停掉;再启动新 player。
/// - Throws: AVAudioPlayer 创建 / 启动失败
public func play(_ wav: URL, user: String) throws {
// 1. 若已有 player 正在播放,主动通知"被中断",确保 H5 把旧动画停掉。
// delegate 先置 nil 防止旧 player 的 stop / dealloc 后续异步触发
// didFinishPlaying 又再发一次 onFinish。
if let old = player {
old.delegate = nil
old.stop()
player = nil
if let interrupted = currentUser {
currentUser = nil
Self.log.debug("interrupted user=\(interrupted, privacy: .public)")
onFinish?(interrupted)
} else {
currentUser = nil
}
}
/// 播放 wav。若已有正在播放的 player,直接覆盖(对齐 daoqi 行为)。
/// 调用方应在调用前调 `recordIncomingUser(user)` 更新 lastUserId。
/// - Throws: AVAudioPlayer 创建/启动失败
public func play(_ wav: URL) throws {
// 2. 创建并启动新 player
let p = try AVAudioPlayer(contentsOf: wav)
p.delegate = self
guard p.prepareToPlay(), p.play() else {
throw PlayerError.startFailed
}
player = p
Self.log.debug("play \(wav.lastPathComponent, privacy: .public) user=\(self.lastUserId, privacy: .public)")
currentUser = user
Self.log.debug("play \(wav.lastPathComponent, privacy: .public) user=\(user, privacy: .public)")
}
/// 主动停止当前播放(不触发 onFinish)。
/// 外部清理使用(如 H5 BackGameData 退出子游戏时);
/// 业务正常路径下不会主动调,由 didFinishPlaying 自然完成。
public func stop() {
player?.delegate = nil
player?.stop()
player = nil
currentUser = nil
}
public enum PlayerError: Error, Sendable {
@@ -110,8 +125,10 @@ public final class VoicePlayer: NSObject, AVAudioPlayerDelegate {
/// 的 player 与当前 self.player 同身份时才清理 + 触发 onFinish。
private func handleFinish(playerIdentity: ObjectIdentifier, success: Bool) {
guard let current = player, ObjectIdentifier(current) == playerIdentity else { return }
let user = currentUser
player = nil
Self.log.debug("didFinish user=\(self.lastUserId, privacy: .public) success=\(success, privacy: .public)")
onFinish?(lastUserId)
currentUser = nil
Self.log.debug("didFinish user=\(user ?? "<nil>", privacy: .public) success=\(success, privacy: .public)")
if let user { onFinish?(user) }
}
}