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 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 17:55:27 +08:00
parent 5d4e0f493c
commit 0c0d2a0292
2 changed files with 62 additions and 0 deletions

View File

@@ -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);
};

View File

@@ -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;
};