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>
58 lines
2.0 KiB
Swift
58 lines
2.0 KiB
Swift
//
|
||
// 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)
|
||
}
|
||
}
|