feat/optimization-and-audio #1
@@ -24,6 +24,7 @@ import {
|
|||||||
type ExamListItem,
|
type ExamListItem,
|
||||||
} from "~/service/wk";
|
} from "~/service/wk";
|
||||||
import { setUnauthorizedHandler } from "~/service/http";
|
import { setUnauthorizedHandler } from "~/service/http";
|
||||||
|
import { startSilentAudio, stopSilentAudio } from "~/service/silentAudio";
|
||||||
import { accountStore, type AccountItem } from "~/store/account";
|
import { accountStore, type AccountItem } from "~/store/account";
|
||||||
import { getMergedHosts, settingsStore } from "~/store/settings";
|
import { getMergedHosts, settingsStore } from "~/store/settings";
|
||||||
import type { CourseType } from "~/types/Course";
|
import type { CourseType } from "~/types/Course";
|
||||||
@@ -722,6 +723,7 @@ const Account = () => {
|
|||||||
try {
|
try {
|
||||||
accountStore.getState().setAccountRunningStudy(account.id, true);
|
accountStore.getState().setAccountRunningStudy(account.id, true);
|
||||||
touchStudyHeartbeat(account.id);
|
touchStudyHeartbeat(account.id);
|
||||||
|
startSilentAudio();
|
||||||
appendStudyLog(`开始刷课:${course.name}`, account.id);
|
appendStudyLog(`开始刷课:${course.name}`, account.id);
|
||||||
await runStudyQueue({
|
await runStudyQueue({
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
@@ -734,6 +736,7 @@ const Account = () => {
|
|||||||
setIsRunningStudy: () => {
|
setIsRunningStudy: () => {
|
||||||
accountStore.getState().setAccountRunningStudy(account.id, false);
|
accountStore.getState().setAccountRunningStudy(account.id, false);
|
||||||
clearStudyHeartbeat(account.id);
|
clearStudyHeartbeat(account.id);
|
||||||
|
stopSilentAudio();
|
||||||
},
|
},
|
||||||
onLog: (message: string, accoundID: string) => {
|
onLog: (message: string, accoundID: string) => {
|
||||||
touchStudyHeartbeat(accoundID);
|
touchStudyHeartbeat(accoundID);
|
||||||
@@ -751,6 +754,7 @@ const Account = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
accountStore.getState().setAccountRunningStudy(account.id, false);
|
accountStore.getState().setAccountRunningStudy(account.id, false);
|
||||||
clearStudyHeartbeat(account.id);
|
clearStudyHeartbeat(account.id);
|
||||||
|
stopSilentAudio();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -762,6 +766,7 @@ const Account = () => {
|
|||||||
|
|
||||||
accountStore.getState().setAccountRunningStudy(account.id, false);
|
accountStore.getState().setAccountRunningStudy(account.id, false);
|
||||||
clearStudyHeartbeat(account.id);
|
clearStudyHeartbeat(account.id);
|
||||||
|
stopSilentAudio();
|
||||||
appendStudyLog(`已发送停止刷课指令:${account.user.name}`, account.id);
|
appendStudyLog(`已发送停止刷课指令:${account.user.name}`, account.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
57
src/service/silentAudio.ts
Normal file
57
src/service/silentAudio.ts
Normal 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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user