接入宿主生命周期暂停背景音乐

useBackgroundMusic 订阅 app.lifecycle 统一事件

宿主进后台或桌面失焦时暂停音频循环并 suspend WebAudio

宿主恢复 active 后按用户原音乐状态继续播放

更新测试和原生壳架构文档
This commit is contained in:
2026-06-18 03:39:00 +08:00
parent d261cc8d0b
commit 8d30cf65bb
5 changed files with 220 additions and 15 deletions
+74 -12
View File
@@ -1,5 +1,7 @@
import { useCallback, useEffect, useRef } from 'react';
import { subscribeHostAppLifecycle } from '../services/host-bridge/hostBridge';
type AudioWindow = Window & {
webkitAudioContext?: typeof AudioContext;
};
@@ -88,6 +90,7 @@ export function useBackgroundMusic({
const stepRef = useRef(0);
const activeRef = useRef(active);
const volumeRef = useRef(volume);
const hostLifecycleActiveRef = useRef(true);
const stopLoop = useCallback(() => {
if (loopTimerRef.current !== null) {
@@ -126,7 +129,12 @@ export function useBackgroundMusic({
const scheduleLoop = useCallback(() => {
const graph = ensureAudioGraph();
if (!graph || !activeRef.current || volumeRef.current <= 0) {
if (
!graph ||
!activeRef.current ||
!hostLifecycleActiveRef.current ||
volumeRef.current <= 0
) {
stopLoop();
return;
}
@@ -162,21 +170,46 @@ export function useBackgroundMusic({
loopTimerRef.current = window.setTimeout(scheduleLoop, 2200);
}, [ensureAudioGraph, stopLoop]);
const updateMasterVolume = useCallback((graph?: { context: AudioContext; masterGain: GainNode } | null) => {
const audioGraph = graph ?? ensureAudioGraph();
if (!audioGraph) return;
const updateMasterVolume = useCallback(
(graph?: { context: AudioContext; masterGain: GainNode } | null) => {
const audioGraph =
graph ??
(contextRef.current && masterGainRef.current
? {
context: contextRef.current,
masterGain: masterGainRef.current,
}
: null);
if (!audioGraph) return;
const targetGain = activeRef.current && volumeRef.current > 0
? Math.max(0.0001, volumeRef.current * 0.18)
: 0.0001;
const targetGain =
activeRef.current &&
hostLifecycleActiveRef.current &&
volumeRef.current > 0
? Math.max(0.0001, volumeRef.current * 0.18)
: 0.0001;
audioGraph.masterGain.gain.cancelScheduledValues(audioGraph.context.currentTime);
audioGraph.masterGain.gain.linearRampToValueAtTime(targetGain, audioGraph.context.currentTime + 0.24);
}, [ensureAudioGraph]);
audioGraph.masterGain.gain.cancelScheduledValues(
audioGraph.context.currentTime,
);
audioGraph.masterGain.gain.linearRampToValueAtTime(
targetGain,
audioGraph.context.currentTime + 0.24,
);
},
[],
);
const startPlayback = useCallback(async () => {
const graph = ensureAudioGraph();
if (!graph || !activeRef.current || volumeRef.current <= 0) return;
if (
!graph ||
!activeRef.current ||
!hostLifecycleActiveRef.current ||
volumeRef.current <= 0
) {
return;
}
if (graph.context.state === 'suspended') {
await graph.context.resume();
@@ -189,11 +222,21 @@ export function useBackgroundMusic({
}
}, [ensureAudioGraph, scheduleLoop, updateMasterVolume]);
const suspendPlaybackForHostLifecycle = useCallback(() => {
updateMasterVolume();
stopLoop();
const context = contextRef.current;
if (context?.state === 'running') {
void context.suspend().catch(() => undefined);
}
}, [stopLoop, updateMasterVolume]);
useEffect(() => {
activeRef.current = active;
volumeRef.current = volume;
if (!active || volume <= 0) {
if (!active || !hostLifecycleActiveRef.current || volume <= 0) {
updateMasterVolume();
stopLoop();
return;
@@ -214,6 +257,25 @@ export function useBackgroundMusic({
};
}, [active, startPlayback, stopLoop, updateMasterVolume, volume]);
useEffect(
() =>
subscribeHostAppLifecycle((payload) => {
const isHostActive = payload.state === 'active' && payload.focused;
if (hostLifecycleActiveRef.current === isHostActive) {
return;
}
hostLifecycleActiveRef.current = isHostActive;
if (!isHostActive) {
suspendPlaybackForHostLifecycle();
return;
}
void startPlayback();
}),
[startPlayback, suspendPlaybackForHostLifecycle],
);
useEffect(() => () => {
stopLoop();