BetterAudioPlayer: Dont Use Observer

This commit is contained in:
thororen 2026-09-26 21:13:15 -04:00
parent c507c762b6
commit 12c9b21d67
No known key found for this signature in database

View file

@ -13,12 +13,11 @@ import definePlugin, { OptionType } from "@utils/types";
import { ColorUtils, React, showToast, Toasts } from "@webpack/common";
const cl = classNameFactory("vc-better-audio-player-");
const CORS_PROXY = "https://cors.keiran0.workers.dev?url=";
const MAX_FILE_SIZE = 12e6;
interface PlayerInstance {
mediaRef: React.RefObject<HTMLAudioElement>;
props: { src: string; type: string; };
mediaRef: React.RefObject<HTMLAudioElement | null>;
props: { type: string; };
state: { playing: boolean; };
}
function validateColor(value: string, key: string, fallback: string) {
@ -99,134 +98,62 @@ function drawSpectrograph(ctx: CanvasRenderingContext2D, w: number, h: number, f
}
}
async function fetchAudioBlob(src: string): Promise<string | null> {
const url = new URL(src);
url.searchParams.set("t", Date.now().toString());
const response = await fetch(CORS_PROXY + encodeURIComponent(url.href));
const contentLength = response.headers.get("content-length");
if (contentLength && Number(contentLength) > MAX_FILE_SIZE) return null;
const blob = await response.blob();
return URL.createObjectURL(blob);
}
function Visualizer({ playerRef, src }: { playerRef: React.RefObject<HTMLAudioElement>; src: string; }) {
function Visualizer({ mediaRef, playing }: { mediaRef: PlayerInstance["mediaRef"]; playing: boolean; }) {
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const audioCtxRef = React.useRef<AudioContext | null>(null);
const analyserRef = React.useRef<AnalyserNode | null>(null);
const animFrameRef = React.useRef(0);
const setupDoneRef = React.useRef(false);
const blobUrlRef = React.useRef<string | null>(null);
const graphRef = React.useRef<{ ctx: AudioContext; analyser: AnalyserNode; } | null>(null);
React.useEffect(() => {
const audio = playerRef.current;
const canvas = canvasRef.current;
if (!audio || !canvas) return () => { };
const audio = mediaRef.current;
const canvasCtx = canvasRef.current?.getContext("2d");
if (!playing || !audio || !canvasCtx) return;
let cancelled = false;
const init = async () => {
const blobUrl = await fetchAudioBlob(src).catch(() => null);
if (cancelled || !blobUrl) return;
blobUrlRef.current = blobUrl;
const wasPlaying = !audio.paused;
const { currentTime } = audio;
audio.src = blobUrl;
audio.currentTime = currentTime;
const audioCtx = new AudioContext();
const analyser = audioCtx.createAnalyser();
if (!graphRef.current) {
const ctx = new AudioContext();
const analyser = ctx.createAnalyser();
analyser.fftSize = 2048;
const source = audioCtx.createMediaElementSource(audio);
source.connect(analyser);
analyser.connect(audioCtx.destination);
audioCtxRef.current = audioCtx;
analyserRef.current = analyser;
setupDoneRef.current = true;
ctx.createMediaElementSource(audio).connect(analyser);
analyser.connect(ctx.destination);
graphRef.current = { ctx, analyser };
}
if (wasPlaying) {
audio.play().catch(() => { });
}
};
const { ctx, analyser } = graphRef.current;
ctx.resume();
const canvasCtx = canvas.getContext("2d");
let dataArray: Uint8Array<ArrayBuffer> | null = null;
let frequencyData: Uint8Array<ArrayBuffer> | null = null;
const dataArray = new Uint8Array(analyser.frequencyBinCount);
const frequencyData = new Uint8Array(analyser.frequencyBinCount);
let frame = 0;
const draw = () => {
const analyser = analyserRef.current;
if (!canvasCtx || !analyser) return;
if (!dataArray || !frequencyData) {
const bufferLength = analyser.frequencyBinCount;
dataArray = new Uint8Array(bufferLength);
frequencyData = new Uint8Array(bufferLength);
}
if (!audio.paused) animFrameRef.current = requestAnimationFrame(draw);
frame = requestAnimationFrame(draw);
analyser.getByteTimeDomainData(dataArray);
analyser.getByteFrequencyData(frequencyData);
const { width, height } = canvas.getBoundingClientRect();
const { canvas } = canvasCtx;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const dpr = window.devicePixelRatio;
const pixelWidth = Math.round(width * dpr);
const pixelHeight = Math.round(height * dpr);
if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
canvas.width = pixelWidth;
canvas.height = pixelHeight;
canvasCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
canvasCtx.clearRect(0, 0, width, height);
if (settings.store.oscilloscope) drawOscilloscope(canvasCtx, width, height, dataArray, dataArray.length);
if (settings.store.spectrograph) drawSpectrograph(canvasCtx, width, height, frequencyData, frequencyData.length);
};
const onPlay = () => {
if (!setupDoneRef.current) return;
if (audioCtxRef.current?.state === "suspended") {
audioCtxRef.current.resume();
}
draw();
};
const onPause = () => {
audioCtxRef.current?.suspend();
cancelAnimationFrame(animFrameRef.current);
};
audio.addEventListener("play", onPlay);
audio.addEventListener("pause", onPause);
init();
draw();
return () => {
cancelled = true;
audio.removeEventListener("play", onPlay);
audio.removeEventListener("pause", onPause);
cancelAnimationFrame(animFrameRef.current);
audioCtxRef.current?.close();
audioCtxRef.current = null;
analyserRef.current = null;
setupDoneRef.current = false;
if (blobUrlRef.current) {
URL.revokeObjectURL(blobUrlRef.current);
blobUrlRef.current = null;
}
cancelAnimationFrame(frame);
ctx.suspend();
};
}, [playerRef]);
}, [playing]);
React.useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return () => { };
const resize = () => {
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * window.devicePixelRatio;
canvas.height = rect.height * window.devicePixelRatio;
const ctx = canvas.getContext("2d");
ctx?.scale(window.devicePixelRatio, window.devicePixelRatio);
};
resize();
const observer = new ResizeObserver(resize);
observer.observe(canvas);
return () => observer.disconnect();
}, []);
React.useEffect(() => () => void graphRef.current?.ctx.close(), []);
return (
<canvas
@ -281,15 +208,21 @@ export default definePlugin({
patches: [
{
find: "}renderPlayIcon(){",
replacement: {
match: /this\.renderAudio\(\):this\.renderVideo\(\)/,
replace: "$&,$self.renderVisualizer(this)",
},
replacement: [
{
match: /"audio",\{(?=className:)/,
replace: '$&crossOrigin:"anonymous",',
},
{
match: /this\.renderAudio\(\):this\.renderVideo\(\)/,
replace: "$&,$self.renderVisualizer(this)",
},
],
},
],
renderVisualizer(player: PlayerInstance) {
if (player.props.type !== "AUDIO") return null;
return <Visualizer playerRef={player.mediaRef} src={player.props.src} key={player.props.src} />;
return <Visualizer mediaRef={player.mediaRef} playing={player.state.playing} />;
},
});