- 新增 Source/Bridge/Handlers/BackGameDataHandler.swift(仅 SubGameViewController 注册):
- 调 AudioPlayer.stopAllBackground 停背景音
- AppCoordinator.popSubGame(returningData:) → popViewController + 发 .subGameDidReturn
- data 兼容 string / object(非字符串走 JSONSerialization 序列化透传)
- 字面 cb "backgameData"(msext gameController.m:691-709 等价,
WXApi 清理跳过 — 微信 SDK 待 Phase 4.E)
- AudioPlayer.stopAllBackground:无条件停背景音(msext 不看 type 直接 nil 行为)
- SubGameViewController.registerBridgeHandlers:挂上 BackGameDataHandler,
注释更新 exitRoom/getVideoinfo/createRoom 推迟到 Phase 8
- WebContainerViewController:在 setupExternalSubscriptions 挂 .subGameDidReturn
观察者 → bridge.call("getWebdata", .string(data)),teardown 时 removeObserver
(生命周期与 battery/network/appservice 同一对,子游戏栈顶时大厅已 teardown
避免双发)
- Plan §5.6.3 / 6.5 / 6.6 / §8 进度已勾选
至此 SwitchOverGameData → push 子游戏 → backgameData → pop + getWebdata
完整链路接通;子游戏视频房间(exitRoom/getVideoinfo/createRoom)留待 Phase 8。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
97 lines
3.9 KiB
Swift
97 lines
3.9 KiB
Swift
//
|
||
// AudioPlayer.swift
|
||
// ylgamehall
|
||
//
|
||
// 本地音频播放(srcIsloop 支撑):背景音循环 + 单次按钮音。
|
||
//
|
||
// 契约:docs/H5-Native-Contract.md §3.1 [3]srcIsloop
|
||
// Design:§8.1 AudioKit
|
||
//
|
||
|
||
import AVFoundation
|
||
import os.log
|
||
|
||
/// 本地音频播放器(@MainActor 隔离的状态机)。
|
||
///
|
||
/// 三类音频通道:
|
||
/// - `background`:循环背景音,记录 backgroundType 用于 isloop=-1 时同名停止
|
||
/// - `button`:单次按钮音(短促音效),互不抢占(每次调 playOnce 新建 player)
|
||
///
|
||
/// 与原 msext NewRootVC 的 `buttunPlayer` / `backgroundPlayer` /
|
||
/// `backgroundType` 静态变量等价,封装到实例避免全局可变状态。
|
||
@MainActor
|
||
public final class AudioPlayer {
|
||
|
||
public static let shared = AudioPlayer()
|
||
|
||
private static let log = Logger(subsystem: "ylgamehall", category: "AudioPlayer")
|
||
|
||
private var backgroundPlayer: AVAudioPlayer?
|
||
private var backgroundType: String?
|
||
/// 单次按钮音的 player 保活池:同时点多个按钮也不会互相打断
|
||
private var buttonPlayers: [AVAudioPlayer] = []
|
||
|
||
public init() {}
|
||
|
||
// MARK: - srcIsloop 入口
|
||
|
||
/// 单次按钮音播放(msext `isloop == 0`)。
|
||
public func playOnce(_ url: URL) {
|
||
do {
|
||
let player = try AVAudioPlayer(contentsOf: url)
|
||
player.numberOfLoops = 0
|
||
player.prepareToPlay()
|
||
player.play()
|
||
buttonPlayers.append(player)
|
||
// 短促音播放完就清理(依靠 AVAudioPlayer 自身的 duration + delay 释放)
|
||
let duration = player.duration
|
||
Task { @MainActor [weak self] in
|
||
try? await Task.sleep(nanoseconds: UInt64((duration + 0.5) * 1_000_000_000))
|
||
self?.buttonPlayers.removeAll { $0 === player }
|
||
}
|
||
Self.log.debug("playOnce \(url.lastPathComponent, privacy: .public) duration=\(duration, privacy: .public)s")
|
||
} catch {
|
||
Self.log.error("playOnce failed: \(error.localizedDescription, privacy: .public) url=\(url.path, privacy: .public)")
|
||
}
|
||
}
|
||
|
||
/// 循环背景音(msext `isloop == 1`)。同名再次启动会替换上一份。
|
||
public func loopBackground(_ url: URL, type: String) {
|
||
do {
|
||
let player = try AVAudioPlayer(contentsOf: url)
|
||
player.numberOfLoops = -1 // 无限循环
|
||
player.prepareToPlay()
|
||
player.play()
|
||
backgroundPlayer?.stop()
|
||
backgroundPlayer = player
|
||
backgroundType = type
|
||
Self.log.debug("loopBackground type=\(type, privacy: .public) url=\(url.lastPathComponent, privacy: .public)")
|
||
} catch {
|
||
Self.log.error("loopBackground failed: \(error.localizedDescription, privacy: .public) url=\(url.path, privacy: .public)")
|
||
}
|
||
}
|
||
|
||
/// 停止同名背景音(msext `isloop == -1`)。仅 type 与当前 backgroundType 匹配时停止。
|
||
public func stopBackground(type: String) {
|
||
guard backgroundType == type else {
|
||
Self.log.debug("stopBackground noop: requested type=\(type, privacy: .public) but current=\(self.backgroundType ?? "nil", privacy: .public)")
|
||
return
|
||
}
|
||
backgroundPlayer?.stop()
|
||
backgroundPlayer = nil
|
||
backgroundType = nil
|
||
Self.log.debug("stopBackground done type=\(type, privacy: .public)")
|
||
}
|
||
|
||
/// 无条件停止当前背景音。子游戏 `backgameData` 退出时统一调一次
|
||
/// (msext gameController.m:693-697 行为:不看 type,直接 stop + nil)。
|
||
public func stopAllBackground() {
|
||
guard backgroundPlayer != nil else { return }
|
||
let oldType = backgroundType ?? "nil"
|
||
backgroundPlayer?.stop()
|
||
backgroundPlayer = nil
|
||
backgroundType = nil
|
||
Self.log.debug("stopAllBackground done (was type=\(oldType, privacy: .public))")
|
||
}
|
||
}
|