diff --git a/src/pages/accounts/Account.tsx b/src/pages/accounts/Account.tsx index c46587b..3ff9dd6 100644 --- a/src/pages/accounts/Account.tsx +++ b/src/pages/accounts/Account.tsx @@ -24,6 +24,7 @@ import { type ExamListItem, } from "~/service/wk"; import { setUnauthorizedHandler } from "~/service/http"; +import { startSilentAudio, stopSilentAudio } from "~/service/silentAudio"; import { accountStore, type AccountItem } from "~/store/account"; import { getMergedHosts, settingsStore } from "~/store/settings"; import type { CourseType } from "~/types/Course"; @@ -722,6 +723,7 @@ const Account = () => { try { accountStore.getState().setAccountRunningStudy(account.id, true); touchStudyHeartbeat(account.id); + startSilentAudio(); appendStudyLog(`开始刷课:${course.name}`, account.id); await runStudyQueue({ accountId: account.id, @@ -734,6 +736,7 @@ const Account = () => { setIsRunningStudy: () => { accountStore.getState().setAccountRunningStudy(account.id, false); clearStudyHeartbeat(account.id); + stopSilentAudio(); }, onLog: (message: string, accoundID: string) => { touchStudyHeartbeat(accoundID); @@ -751,6 +754,7 @@ const Account = () => { } finally { accountStore.getState().setAccountRunningStudy(account.id, false); clearStudyHeartbeat(account.id); + stopSilentAudio(); } }; @@ -762,6 +766,7 @@ const Account = () => { accountStore.getState().setAccountRunningStudy(account.id, false); clearStudyHeartbeat(account.id); + stopSilentAudio(); appendStudyLog(`已发送停止刷课指令:${account.user.name}`, account.id); }; diff --git a/src/service/silentAudio.ts b/src/service/silentAudio.ts new file mode 100644 index 0000000..25fc18a --- /dev/null +++ b/src/service/silentAudio.ts @@ -0,0 +1,57 @@ +/** + * Silent audio playback to prevent browser tab throttling during long-running tasks. + * + * Uses the Web Audio API to produce a nearly inaudible signal that keeps the + * browser from suspending the tab's timers and network requests. + */ + +let audioContext: AudioContext | null = null; +let oscillatorNode: OscillatorNode | null = null; +let gainNode: GainNode | null = null; + +/** + * Start playing silent audio. Safe to call multiple times — duplicate calls + * are ignored if audio is already playing. + */ +export const startSilentAudio = () => { + if (oscillatorNode) { + return; + } + + try { + audioContext = new AudioContext(); + gainNode = audioContext.createGain(); + gainNode.gain.value = 0.001; // Nearly silent + + oscillatorNode = audioContext.createOscillator(); + oscillatorNode.type = "sine"; + oscillatorNode.frequency.value = 1; // Sub-bass, inaudible + oscillatorNode.connect(gainNode); + gainNode.connect(audioContext.destination); + oscillatorNode.start(); + } catch { + // AudioContext may be unavailable in some environments; degrade silently. + oscillatorNode = null; + gainNode = null; + audioContext = null; + } +}; + +/** + * Stop playing silent audio and release resources. + */ +export const stopSilentAudio = () => { + try { + oscillatorNode?.stop(); + } catch { + // Already stopped or never started + } + + oscillatorNode?.disconnect(); + gainNode?.disconnect(); + audioContext?.close(); + + oscillatorNode = null; + gainNode = null; + audioContext = null; +};