From 0c0d2a0292fdcf0ed991320059020899e7b1e596 Mon Sep 17 00:00:00 2001 From: zhilv Date: Sun, 26 Apr 2026 17:55:27 +0800 Subject: [PATCH] feat: add silent audio playback to prevent browser tab throttling during study Play a nearly inaudible Web Audio API signal when study starts, stop it when study completes, is stopped, or fails. This prevents browsers from throttling timers and network requests in background tabs. Co-Authored-By: Claude Opus 4.6 --- src/pages/accounts/Account.tsx | 5 +++ src/service/silentAudio.ts | 57 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/service/silentAudio.ts 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; +};