Phase 3.A:srcIsloop 本地音频 + 3.B/3.C/3.D handler stub

3.A 本地音频完整落地(与 msext NewRootVC initJSdata 等价):

  Source/Audio/AudioPlayer.swift(@MainActor 状态机):
    - playOnce(url) 单次按钮音;buttonPlayers 数组保活池避免多次点击
      互相打断(duration + 0.5s 后清理)
    - loopBackground(url, type:) 循环背景音;numberOfLoops = -1 +
      backgroundType 记录用于同名停止
    - stopBackground(type:) 同名校验后才停止(msext 同款语义)

  Source/Bridge/Handlers/LocalAudioHandler.swift:
    - 注册 srcIsloop handler(@MainActor enum)
    - 入参 src + isloop,按 0/1/-1 分支调 AudioPlayer.shared 对应方法
    - audioFileURL 路径:{Caches}/{gamedir}/{gamestart}/assets/wav/{src}
      与 msext NewRootVC 路径约定 1:1(H5 zip 内 assets/wav 子目录)
    - audioFileURL 标 nonisolated 因 SandboxPaths 是 nonisolated 静态
    - cb 字面 "Response from srcIsloop" 严格保持

  Source/Bridge/Handlers/RemoteAudioHandler.swift(3.B/3.C/3.D stub):
    - prepareaudio + mediaTypeAudio 仅 cb 维持契约,无真实业务
    - 待 Phase 3.B(opencore-amr .a 接入)+ 3.C(七牛 SPM + token)+
      3.D(远端播放)完整后升级
    - 按 CLAUDE.md 原则 A:H5 调啥就得有人接,即使是 stub

WebContainerViewController.registerBridgeHandlers 接入:
  - LocalAudioHandler.register(完整实现)
  - RemoteAudioHandler.register(stub)

Plan §5 Phase 3.1 勾选;3.2 真机验证留待 H5 提供音频文件后做。

Verification-Checklist.md 新增 Phase 3 章节:
  - H. 本地音频(E.14-E.16 srcIsloop 三个 isloop 值)
  - I. prepareaudio / mediaTypeAudio stub(E.17 E.18)

BuildProject 通过

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
joywayer
2026-06-22 19:11:34 +08:00
co-authored by Claude Opus 4.7
parent b2ee1b38eb
commit 0602b9d4b6
6 changed files with 210 additions and 9 deletions
@@ -0,0 +1,57 @@
//
// LocalAudioHandler.swift
// ylgamehall
//
// H5 → Native handler:srcIsloop — 本地音频播放(背景循环 / 按钮单次)
// 契约:docs/H5-Native-Contract.md §3.1 [3]
// 原项目:daoqi/msext NewRootVC.m initJSdata 关联 buttunPlayer / backgroundPlayer
//
// 本地音频文件路径:`{Caches}/{gamedir}/{gamestart}/assets/wav/{src}`
// 与 SandboxPaths.lobbyIndex 同一根目录下 assets/wav 子目录
//
import Foundation
@MainActor
public enum LocalAudioHandler {
public static func register(on bridge: any BridgeProtocol) {
// 【3】srcIsloop — 本地音频播放
// 入参 src : string 音频文件名(位于 lobbyAssets/assets/wav/{src})
// 入参 isloop : int 0 单次 / 1 循环背景 / -1 停同名循环
// cb: "Response from srcIsloop"
bridge.register("srcIsloop") { data, callback in
defer { callback?(.string("Response from srcIsloop")) }
guard let obj = data?.asObject,
let src = obj["src"]?.asString,
let isloop = obj["isloop"]?.asInt
else {
return
}
let url = audioFileURL(src: src)
switch isloop {
case 0:
await AudioPlayer.shared.playOnce(url)
case 1:
await AudioPlayer.shared.loopBackground(url, type: src)
case -1:
await AudioPlayer.shared.stopBackground(type: src)
default:
break
}
}
}
/// `{lobbyAssets}/assets/wav/{src}` —— 与 msext NewRootVC 路径约定一致。
/// H5 业务调用前会先用 zip 升级(gamehall.zip 内含 assets/wav/*.wav 音频文件)。
nonisolated private static func audioFileURL(src: String) -> URL {
SandboxPaths.lobbyIndex
.deletingLastPathComponent() // {Caches}/{gamedir}/{gamestart}/
.appendingPathComponent("assets")
.appendingPathComponent("wav")
.appendingPathComponent(src)
}
}
@@ -0,0 +1,40 @@
//
// RemoteAudioHandler.swift
// ylgamehall
//
// H5 → Native handler stub:prepareaudio / mediaTypeAudio
// 契约:docs/H5-Native-Contract.md §3.1 [4][5] + §3.2 [7][8][9]
//
// Phase 3.A 阶段为 stub(仅 cb 维持契约,让 H5 启动不报 "no handler");
// Phase 3.B AMR 转码 + Phase 3.C 录音上传 + Phase 3.D mediaTypeAudio 真实播放
// 完整接入后升级为真实现。
//
// 按 CLAUDE.md 原则 A 第一准则:H5 调啥就得有人接,即使没真实业务也要注册。
//
import Foundation
public enum RemoteAudioHandler {
public static func register(on bridge: any BridgeProtocol) {
// 【4】prepareaudio — 启动麦克风录音
// 入参忽略;cb: "Response from prepareaudio"
// 完整实现(Phase 3.C):权限检查 + AVAudioRecorder + WAV→AMR + 七牛上传 +
// 反向 callback getaudiourl({audiourl, time}) + 子游戏额外 recordSuccess
// stub 仅 cb 维持契约
bridge.register("prepareaudio") { _, callback in
callback?(.string("Response from prepareaudio"))
}
// 【5】mediaTypeAudio — 远程语音回放
// 入参 audiourl: string;user: string
// cb: "Response from mediaTypeAudio"
// 完整实现(Phase 3.D):voicePlaying 总开关短路 + 下载 AMR + AMR→WAV +
// AVAudioPlayer 播放 + 反向 callback gameui_play_voice(user) /
// gameui_stop_voice(user)
// stub 仅 cb 维持契约(不下载、不播放)
bridge.register("mediaTypeAudio") { _, callback in
callback?(.string("Response from mediaTypeAudio"))
}
}
}