修复 H5 音效完全不出声 + 子游戏音效路径错(srcIsloop)
真机诊断([SFX-DIAG])锁定两因: 1. AVPlayer.url 不接受 file:// 本地路径,报 `401 ... not fd:// or network address`——所有本地音效播放失败,故"完全没声"。改用 fdSrc(openSync 取 fd), 远程语音(mediaTypeAudio)仍走 url(网络地址可直接用)。 2. 子游戏音效路径写死 local.gamestart(=gamehall),而 sangelaok 等子游戏音效在 各自目录(.../sangelaok/assets/wav/),找不到文件 → 子游戏更是无声。对齐 Android webviewActivity(gamestart)/NewwebviewActivity(gamedirectory):按"本槽内容目录" 定位,经 CapabilityContext.gameContentDir 注入(大厅=gamestart、子游戏=其目录)。 并发模型重写(复刻 Android SoundPool 并发一次性 + MediaPlayer-per-src 循环): - 原单 AVPlayer 每次 release 再建,互相打断、还杀正在循环的 BGM。 - 改为:循环 BGM 每 src 一个播放器(loopPlayers map);一次性音效各自独立瞬时 播放器(oneShots,播完自释),互不打断。isloop<0 停止该 src 循环。 - fd 生命周期:随播放器释放(release 后)关闭,防泄漏;mute(voicePlaying)作用于全部播放器。 链路改动:CapabilityContext 增 gameContentDir;WebSlot.setup 透传; BridgeGameContainer setupLobby 传 gamestart、switchGame 记录子游戏目录供 setupSubgame 传入。 真机验证:大厅(dir=gamehall)与子游戏(dir=sangelaok)音效均 exists=true、无 401, 循环 BGM 与一次性音效正常播放。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d335a589b6
commit
a104394076
@@ -36,6 +36,8 @@ export struct BridgeGameContainer {
|
|||||||
private cancels: Array<() => void> = [];
|
private cancels: Array<() => void> = [];
|
||||||
/** 非空 → 子游戏 Web 显示(路径 A 切换)。 */
|
/** 非空 → 子游戏 Web 显示(路径 A 切换)。 */
|
||||||
@State private subgameUrl: string = '';
|
@State private subgameUrl: string = '';
|
||||||
|
/** 当前子游戏目录名(switchGame 记录,供 setupSubgame 给能力上下文定位资源,如音效)。 */
|
||||||
|
private currentSubgameDir: string = '';
|
||||||
/** 子游戏资源下载中(按需下载期间显示启动页同款进度条)。 */
|
/** 子游戏资源下载中(按需下载期间显示启动页同款进度条)。 */
|
||||||
@State private subgameDownloading: boolean = false;
|
@State private subgameDownloading: boolean = false;
|
||||||
/** 子游戏下载进度(0~100)。 */
|
/** 子游戏下载进度(0~100)。 */
|
||||||
@@ -97,12 +99,14 @@ export struct BridgeGameContainer {
|
|||||||
if (this.slotL === undefined) {
|
if (this.slotL === undefined) {
|
||||||
this.slotL = new WebSlot('lobby', this.controllerL, true, this.uploadServer);
|
this.slotL = new WebSlot('lobby', this.controllerL, true, this.uploadServer);
|
||||||
}
|
}
|
||||||
this.slotL.setup(this.hostCtx!, this.config!, this.kv!, this.resourceRoot());
|
// 大厅内容目录 = gamestart(gamehall):音效等按此定位 <urlpath>/gamehall/assets/wav/*
|
||||||
|
this.slotL.setup(this.hostCtx!, this.config!, this.kv!, this.resourceRoot(), this.config!.getLocal().gamestart);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupSubgame(): void {
|
private setupSubgame(): void {
|
||||||
this.ensureDeps();
|
this.ensureDeps();
|
||||||
this.slotS?.setup(this.hostCtx!, this.config!, this.kv!, this.resourceRoot());
|
// 子游戏内容目录 = 当前进入的子游戏目录(switchGame 时记录),音效定位 <urlpath>/<dir>/assets/wav/*
|
||||||
|
this.slotS?.setup(this.hostCtx!, this.config!, this.kv!, this.resourceRoot(), this.currentSubgameDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
private activeSlot(): WebSlot | undefined {
|
private activeSlot(): WebSlot | undefined {
|
||||||
@@ -186,6 +190,7 @@ export struct BridgeGameContainer {
|
|||||||
const values: AppDataValues = AppDataInjector.buildValues(cfg.getLocal(), '1');
|
const values: AppDataValues = AppDataInjector.buildValues(cfg.getLocal(), '1');
|
||||||
AppDataInjector.inject(subDir, values);
|
AppDataInjector.inject(subDir, values);
|
||||||
this.slotL?.deactivate();
|
this.slotL?.deactivate();
|
||||||
|
this.currentSubgameDir = d.dir; // 供 setupSubgame 注入能力上下文(音效资源定位)
|
||||||
this.slotS = new WebSlot('subgame', new webview.WebviewController(), true, this.uploadServer);
|
this.slotS = new WebSlot('subgame', new webview.WebviewController(), true, this.uploadServer);
|
||||||
this.applyOrientation(d.webtype !== '2'); // "3" 横 / "2" 竖
|
this.applyOrientation(d.webtype !== '2'); // "3" 横 / "2" 竖
|
||||||
this.subgamePainted = false; // 子游戏首帧前用启动图盖住,避免 下载完→子游戏上屏 间白屏
|
this.subgamePainted = false; // 子游戏首帧前用启动图盖住,避免 下载完→子游戏上屏 间白屏
|
||||||
|
|||||||
@@ -46,8 +46,10 @@ export class WebSlot {
|
|||||||
return this.active;
|
return this.active;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** onControllerAttached:建桥 + 经组装根注册能力(每槽独立一套)。 */
|
/** onControllerAttached:建桥 + 经组装根注册能力(每槽独立一套)。
|
||||||
setup(hostCtx: common.UIAbilityContext, config: ConfigManager, kv: KvStore, resourceRoot: string): void {
|
* gameContentDir:本槽加载内容目录名(大厅 gamestart / 子游戏目录),供音效等按槽定位资源。 */
|
||||||
|
setup(hostCtx: common.UIAbilityContext, config: ConfigManager, kv: KvStore, resourceRoot: string,
|
||||||
|
gameContentDir: string): void {
|
||||||
try {
|
try {
|
||||||
this.controller.setPathAllowingUniversalAccess([resourceRoot]);
|
this.controller.setPathAllowingUniversalAccess([resourceRoot]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -65,6 +67,7 @@ export class WebSlot {
|
|||||||
config,
|
config,
|
||||||
kv,
|
kv,
|
||||||
uploadServer: this.uploadServer,
|
uploadServer: this.uploadServer,
|
||||||
|
gameContentDir,
|
||||||
log: Logger.tag(`Cap-${this.role}`),
|
log: Logger.tag(`Cap-${this.role}`),
|
||||||
captureCanvas: (canvasId: string): Promise<string> => this.captureCanvas(canvasId),
|
captureCanvas: (canvasId: string): Promise<string> => this.captureCanvas(canvasId),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ export interface CapabilityContext {
|
|||||||
kv: KvStore;
|
kv: KvStore;
|
||||||
/** 本机上传端点(截图分享用,M3)。 */
|
/** 本机上传端点(截图分享用,M3)。 */
|
||||||
uploadServer: LocalUploadServer;
|
uploadServer: LocalUploadServer;
|
||||||
|
/**
|
||||||
|
* 本槽当前加载内容的目录名(urlpath 下的子目录):大厅槽 = gamestart(gamehall)、
|
||||||
|
* 子游戏槽 = 子游戏目录名。音效路径 `<urlpath>/<gameContentDir>/assets/wav/<src>` 依赖它
|
||||||
|
* (对齐 Android:webviewActivity 用 gamestart、NewwebviewActivity 用 gamedirectory)。
|
||||||
|
*/
|
||||||
|
gameContentDir: string;
|
||||||
/** 通用日志。 */
|
/** 通用日志。 */
|
||||||
log: Logger;
|
log: Logger;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
/**
|
/**
|
||||||
* 音频能力(契约 §8.7/§9/§10.6,T-M3-10)。@kit.MediaKit AVPlayer。
|
* 音频能力(契约 §8.7/§9/§10.6,T-M3-10)。@kit.MediaKit AVPlayer。
|
||||||
* - mediaTypeAudio:data={audiourl,type,user},播放语音消息 → gameui_play_voice/gameui_stop_voice(user 透传)。
|
* - mediaTypeAudio:data={audiourl,type,user},播放语音消息(远程URL) → gameui_play_voice/gameui_stop_voice(user 透传)。
|
||||||
* - srcIsloop:data={src,isloop},播放本地 wav 音效/背景乐(isloop 0一次/>0循环/<0停止)。
|
* - srcIsloop:data={src,isloop},播放本地音效/背景乐(isloop 0一次/>0循环/<0停止该 src)。
|
||||||
* - voicePlaying:data="1" 开 / 其他 静音(总开关)。
|
* - voicePlaying:data="1" 开 / 其他 静音(总开关)。
|
||||||
* - prepareaudio:按住说话录音 →(上传七牛)→ getaudiourl。
|
* - prepareaudio:按住说话录音 →(上传七牛)→ getaudiourl。
|
||||||
*
|
*
|
||||||
* ⚠ prepareaudio 录音为分档实现:录音上传链路用 platform QiniuUploader(token 走服务端,见其注释),
|
* 🔴 本地音效播放要点(对齐 Android webviewActivity/NewwebviewActivity.playwav 行为,内部实现自由):
|
||||||
* 但"按住说话"的原生触摸起止手势与麦克风权限需与容器触摸集成(原 Android 在 WebView 上做手势),
|
* - **路径按"本槽内容目录"**:`<urlpath>/<gameContentDir>/assets/wav/<src>`(大厅 gamehall / 子游戏其目录)。
|
||||||
* 且需七牛服务端 token 端点——此三者就绪前 prepareaudio 为安全空实现(不录、不卡死)。见 TODO。
|
* 源由 CapabilityContext.gameContentDir 注入(Android webviewActivity 用 gamestart、Newwebview 用 gamedirectory)。
|
||||||
|
* - **AVPlayer 不接受 `file://` 本地路径**(报 401 "not fd:// or network address"):本地音效必须用 `fdSrc`(打开 fd),
|
||||||
|
* 远程语音(mediaTypeAudio)是网络地址才可直接用 `url`。这是早期"H5 完全没声"的根因。
|
||||||
|
* - **并发不互斥**:循环 BGM 每个 src 一个播放器(loopPlayers map,复刻 Android map<src,MediaPlayer>);一次性音效
|
||||||
|
* 各自独立瞬时播放器(oneShots,播完自释)。互不打断、不杀 BGM(复刻 Android SoundPool 并发一次性 + MediaPlayer 循环)。
|
||||||
|
* - isloop<0:停止该 src 的循环(Android 语义"停止背景音乐")。
|
||||||
|
*
|
||||||
|
* ⚠ prepareaudio 录音为分档实现:录音上传链路用 platform QiniuUploader(token 走服务端),但"按住说话"的原生触摸
|
||||||
|
* 起止手势与麦克风权限需与容器触摸集成,且需七牛服务端 token 端点——三者就绪前为安全空实现(不录、不卡死)。
|
||||||
*/
|
*/
|
||||||
import { media } from '@kit.MediaKit';
|
import { media } from '@kit.MediaKit';
|
||||||
import { BusinessError } from '@kit.BasicServicesKit';
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
import { common } from '@kit.AbilityKit';
|
import { common } from '@kit.AbilityKit';
|
||||||
|
import { fileIo } from '@kit.CoreFileKit';
|
||||||
import { BridgeController } from 'feature_bridge';
|
import { BridgeController } from 'feature_bridge';
|
||||||
import { InboundHandlers, OutboundHandlers, MediaTypeAudioReq, SrcIsLoopReq } from 'contracts';
|
import { InboundHandlers, OutboundHandlers, MediaTypeAudioReq, SrcIsLoopReq } from 'contracts';
|
||||||
import { ConfigManager, ResourceManager } from 'domain_resource';
|
import { ConfigManager, ResourceManager } from 'domain_resource';
|
||||||
|
import { FileSystem } from 'platform';
|
||||||
import { Logger } from 'common';
|
import { Logger } from 'common';
|
||||||
import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider';
|
import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider';
|
||||||
|
|
||||||
@@ -24,8 +34,15 @@ export class AudioProvider implements CapabilityProvider {
|
|||||||
private bridge: BridgeController | undefined = undefined;
|
private bridge: BridgeController | undefined = undefined;
|
||||||
private context: common.UIAbilityContext | undefined = undefined;
|
private context: common.UIAbilityContext | undefined = undefined;
|
||||||
private config: ConfigManager | undefined = undefined;
|
private config: ConfigManager | undefined = undefined;
|
||||||
|
/** 本槽内容目录(大厅 gamehall / 子游戏目录),音效路径用。 */
|
||||||
|
private gameContentDir: string = '';
|
||||||
private voicePlayer: media.AVPlayer | undefined = undefined;
|
private voicePlayer: media.AVPlayer | undefined = undefined;
|
||||||
private sfxPlayer: media.AVPlayer | undefined = undefined;
|
/** 循环 BGM:src → 播放器(每 src 一个,复刻 Android map<src,MediaPlayer>)。 */
|
||||||
|
private loopPlayers: Map<string, media.AVPlayer> = new Map();
|
||||||
|
/** 一次性音效:各自独立瞬时播放器,播完自释(并发不互斥)。 */
|
||||||
|
private oneShots: Set<media.AVPlayer> = new Set();
|
||||||
|
/** 播放器 → 其打开的本地 fd(释放时关闭,防泄漏)。 */
|
||||||
|
private fds: Map<media.AVPlayer, number> = new Map();
|
||||||
private muted: boolean = false;
|
private muted: boolean = false;
|
||||||
private disposed: boolean = false;
|
private disposed: boolean = false;
|
||||||
|
|
||||||
@@ -33,6 +50,7 @@ export class AudioProvider implements CapabilityProvider {
|
|||||||
this.bridge = bridge;
|
this.bridge = bridge;
|
||||||
this.context = ctx.uiAbilityContext;
|
this.context = ctx.uiAbilityContext;
|
||||||
this.config = ctx.config;
|
this.config = ctx.config;
|
||||||
|
this.gameContentDir = ctx.gameContentDir;
|
||||||
bridge.registerHandler(InboundHandlers.MediaTypeAudio, (data: string, _cb: (resp: string) => void) => {
|
bridge.registerHandler(InboundHandlers.MediaTypeAudio, (data: string, _cb: (resp: string) => void) => {
|
||||||
this.playVoiceMessage(data);
|
this.playVoiceMessage(data);
|
||||||
});
|
});
|
||||||
@@ -49,19 +67,19 @@ export class AudioProvider implements CapabilityProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 槽去激活:停语音/音效播放(非激活端不出声,M5 §7.5)。H5 再激活后按需重触发。 */
|
/** 槽去激活:停语音 + 全部音效/背景乐(非激活端不出声,M5 §7.5)。H5 再激活后按需重触发。 */
|
||||||
onBackground(): void {
|
onBackground(): void {
|
||||||
this.releaseVoice();
|
this.releaseVoice();
|
||||||
this.releaseSfx();
|
this.releaseAllSfx();
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(): void {
|
onDestroy(): void {
|
||||||
this.disposed = true;
|
this.disposed = true;
|
||||||
this.releaseVoice();
|
this.releaseVoice();
|
||||||
this.releaseSfx();
|
this.releaseAllSfx();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 播放语音消息(远程 url),上报 gameui_play_voice/gameui_stop_voice。 */
|
/** 播放语音消息(远程 url,AVPlayer.url 接受网络地址),上报 gameui_play_voice/gameui_stop_voice。 */
|
||||||
private playVoiceMessage(data: string): void {
|
private playVoiceMessage(data: string): void {
|
||||||
let req: MediaTypeAudioReq;
|
let req: MediaTypeAudioReq;
|
||||||
try {
|
try {
|
||||||
@@ -103,7 +121,7 @@ export class AudioProvider implements CapabilityProvider {
|
|||||||
}).catch((e: BusinessError) => this.log.w(`createAVPlayer(voice) failed: ${e.message}`));
|
}).catch((e: BusinessError) => this.log.w(`createAVPlayer(voice) failed: ${e.message}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 播放本地 wav 音效/背景乐:<gameDir>/assets/wav/<src>。isloop<0 停止。 */
|
/** 播放本地音效/背景乐:<urlpath>/<gameContentDir>/assets/wav/<src>。isloop<0 停止该 src。 */
|
||||||
private playSfx(data: string): void {
|
private playSfx(data: string): void {
|
||||||
let req: SrcIsLoopReq;
|
let req: SrcIsLoopReq;
|
||||||
try {
|
try {
|
||||||
@@ -112,8 +130,14 @@ export class AudioProvider implements CapabilityProvider {
|
|||||||
this.log.w(`srcIsloop bad json: ${(e as Error).message}`);
|
this.log.w(`srcIsloop bad json: ${(e as Error).message}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (req.isloop < 0) {
|
const src: string = req.src;
|
||||||
this.releaseSfx();
|
if (src === undefined || src === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// isloop 归一(H5 可能传字符串/数字):<0 停止 / >0 循环 / 0 一次。
|
||||||
|
const isloop: number = Number(req.isloop);
|
||||||
|
if (isloop < 0) {
|
||||||
|
this.stopLoop(src);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ctx = this.context;
|
const ctx = this.context;
|
||||||
@@ -122,16 +146,38 @@ export class AudioProvider implements CapabilityProvider {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const local = cfg.getLocal();
|
const local = cfg.getLocal();
|
||||||
const wavPath: string =
|
const dir: string = this.gameContentDir !== '' ? this.gameContentDir : local.gamestart;
|
||||||
`${ResourceManager.resourceRoot(ctx)}/${local.gamedir}/${local.gamestart}/assets/wav/${req.src}`;
|
const wavPath: string = `${ResourceManager.resourceRoot(ctx)}/${local.gamedir}/${dir}/assets/wav/${src}`;
|
||||||
const loop: boolean = req.isloop > 0;
|
if (!FileSystem.exists(wavPath)) {
|
||||||
this.releaseSfx();
|
this.log.w(`sfx file missing: ${wavPath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isloop > 0) {
|
||||||
|
this.stopLoop(src); // 同 src 重复触发:先停旧的再起新循环(复刻 Android reset+restart)
|
||||||
|
this.spawn(wavPath, true, (p: media.AVPlayer) => this.loopPlayers.set(src, p));
|
||||||
|
} else {
|
||||||
|
this.spawn(wavPath, false, (p: media.AVPlayer) => this.oneShots.add(p));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用 fdSrc 起一个播放器(loop 决定循环),onCreated 把它登记到对应集合。 */
|
||||||
|
private spawn(wavPath: string, loop: boolean, onCreated: (p: media.AVPlayer) => void): void {
|
||||||
|
let file: fileIo.File;
|
||||||
|
try {
|
||||||
|
file = fileIo.openSync(wavPath, fileIo.OpenMode.READ_ONLY);
|
||||||
|
} catch (e) {
|
||||||
|
this.log.w(`open sfx failed: ${(e as Error).message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fd: number = file.fd;
|
||||||
media.createAVPlayer().then((player: media.AVPlayer) => {
|
media.createAVPlayer().then((player: media.AVPlayer) => {
|
||||||
if (this.disposed) {
|
if (this.disposed) {
|
||||||
player.release();
|
player.release();
|
||||||
|
this.safeCloseFd(fd);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.sfxPlayer = player;
|
this.fds.set(player, fd);
|
||||||
|
onCreated(player);
|
||||||
player.on('stateChange', (state: string) => {
|
player.on('stateChange', (state: string) => {
|
||||||
if (state === 'initialized') {
|
if (state === 'initialized') {
|
||||||
player.prepare();
|
player.prepare();
|
||||||
@@ -141,25 +187,38 @@ export class AudioProvider implements CapabilityProvider {
|
|||||||
player.play();
|
player.play();
|
||||||
} else if (state === 'completed') {
|
} else if (state === 'completed') {
|
||||||
if (!loop) {
|
if (!loop) {
|
||||||
this.releaseSfx();
|
this.disposePlayer(player); // 一次性音效播完即释(含关 fd、移出集合)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
player.on('error', (e: BusinessError) => {
|
player.on('error', (e: BusinessError) => {
|
||||||
this.log.w(`sfx play error: ${e.code} ${e.message}`);
|
this.log.w(`sfx play error: ${e.code} ${e.message}`);
|
||||||
this.releaseSfx();
|
this.disposePlayer(player);
|
||||||
});
|
});
|
||||||
player.url = `file://${wavPath}`;
|
player.fdSrc = { fd } as media.AVFileDescriptor;
|
||||||
}).catch((e: BusinessError) => this.log.w(`createAVPlayer(sfx) failed: ${e.message}`));
|
}).catch((e: BusinessError) => {
|
||||||
|
this.log.w(`createAVPlayer(sfx) failed: ${e.message}`);
|
||||||
|
this.safeCloseFd(fd);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 语音总开关:静音/取消静音,作用于当前两个播放器。 */
|
/** 停止并释放某 src 的循环 BGM。 */
|
||||||
|
private stopLoop(src: string): void {
|
||||||
|
const p = this.loopPlayers.get(src);
|
||||||
|
if (p !== undefined) {
|
||||||
|
this.loopPlayers.delete(src);
|
||||||
|
this.releasePlayer(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 语音总开关:静音/取消静音,作用于当前全部播放器。 */
|
||||||
private setMuted(muted: boolean): void {
|
private setMuted(muted: boolean): void {
|
||||||
this.muted = muted;
|
this.muted = muted;
|
||||||
const vol: number = muted ? 0 : 1;
|
const vol: number = muted ? 0 : 1;
|
||||||
try {
|
try {
|
||||||
this.voicePlayer?.setVolume(vol);
|
this.voicePlayer?.setVolume(vol);
|
||||||
this.sfxPlayer?.setVolume(vol);
|
this.loopPlayers.forEach((p: media.AVPlayer) => p.setVolume(vol));
|
||||||
|
this.oneShots.forEach((p: media.AVPlayer) => p.setVolume(vol));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.log.w(`setVolume failed: ${(e as Error).message}`);
|
this.log.w(`setVolume failed: ${(e as Error).message}`);
|
||||||
}
|
}
|
||||||
@@ -173,11 +232,49 @@ export class AudioProvider implements CapabilityProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private releaseSfx(): void {
|
/** 释放全部音效(循环 + 一次性)。 */
|
||||||
const p = this.sfxPlayer;
|
private releaseAllSfx(): void {
|
||||||
this.sfxPlayer = undefined;
|
this.loopPlayers.forEach((p: media.AVPlayer) => this.releasePlayer(p));
|
||||||
if (p !== undefined) {
|
this.loopPlayers.clear();
|
||||||
p.release().catch((e: BusinessError) => this.log.w(`release sfx failed: ${e.message}`));
|
this.oneShots.forEach((p: media.AVPlayer) => this.releasePlayer(p));
|
||||||
|
this.oneShots.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从集合移除并释放一个播放器(一次性播完/出错时用)。 */
|
||||||
|
private disposePlayer(player: media.AVPlayer): void {
|
||||||
|
let loopKey: string | undefined = undefined;
|
||||||
|
this.loopPlayers.forEach((p: media.AVPlayer, k: string) => {
|
||||||
|
if (p === player) {
|
||||||
|
loopKey = k;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (loopKey !== undefined) {
|
||||||
|
this.loopPlayers.delete(loopKey);
|
||||||
|
}
|
||||||
|
this.oneShots.delete(player);
|
||||||
|
this.releasePlayer(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 释放播放器并关闭其 fd(不动集合,调用方负责移除登记)。 */
|
||||||
|
private releasePlayer(player: media.AVPlayer): void {
|
||||||
|
const fd = this.fds.get(player);
|
||||||
|
this.fds.delete(player);
|
||||||
|
player.release()
|
||||||
|
.then(() => this.safeCloseFd(fd))
|
||||||
|
.catch((e: BusinessError) => {
|
||||||
|
this.log.w(`release sfx failed: ${e.message}`);
|
||||||
|
this.safeCloseFd(fd);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private safeCloseFd(fd: number | undefined): void {
|
||||||
|
if (fd === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
fileIo.closeSync(fd);
|
||||||
|
} catch (e) {
|
||||||
|
this.log.w(`close fd failed: ${(e as Error).message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user