Files
youle_app_ios_v2/ylgamehall/Source/Audio/AudioPlayer.swift
T
joywayerandClaude Opus 4.7 5f3a435180 Phase 6 commit C:backgameData pop + getWebdata 反向 callback
- 新增 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>
2026-06-22 21:05:23 +08:00

97 lines
3.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// AudioPlayer.swift
// ylgamehall
//
// 本地音频播放(srcIsloop 支撑):背景音循环 + 单次按钮音。
//
// 契约:docs/H5-Native-Contract.md §3.1 3srcIsloop
// 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))")
}
}