BetterAudioPlayer: Dont Use Observer
This commit is contained in:
parent
c507c762b6
commit
12c9b21d67
1 changed files with 49 additions and 116 deletions
|
|
@ -13,12 +13,11 @@ import definePlugin, { OptionType } from "@utils/types";
|
||||||
import { ColorUtils, React, showToast, Toasts } from "@webpack/common";
|
import { ColorUtils, React, showToast, Toasts } from "@webpack/common";
|
||||||
|
|
||||||
const cl = classNameFactory("vc-better-audio-player-");
|
const cl = classNameFactory("vc-better-audio-player-");
|
||||||
const CORS_PROXY = "https://cors.keiran0.workers.dev?url=";
|
|
||||||
const MAX_FILE_SIZE = 12e6;
|
|
||||||
|
|
||||||
interface PlayerInstance {
|
interface PlayerInstance {
|
||||||
mediaRef: React.RefObject<HTMLAudioElement>;
|
mediaRef: React.RefObject<HTMLAudioElement | null>;
|
||||||
props: { src: string; type: string; };
|
props: { type: string; };
|
||||||
|
state: { playing: boolean; };
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateColor(value: string, key: string, fallback: string) {
|
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> {
|
function Visualizer({ mediaRef, playing }: { mediaRef: PlayerInstance["mediaRef"]; playing: boolean; }) {
|
||||||
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; }) {
|
|
||||||
const canvasRef = React.useRef<HTMLCanvasElement>(null);
|
const canvasRef = React.useRef<HTMLCanvasElement>(null);
|
||||||
const audioCtxRef = React.useRef<AudioContext | null>(null);
|
const graphRef = React.useRef<{ ctx: AudioContext; analyser: AnalyserNode; } | 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);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const audio = playerRef.current;
|
const audio = mediaRef.current;
|
||||||
const canvas = canvasRef.current;
|
const canvasCtx = canvasRef.current?.getContext("2d");
|
||||||
if (!audio || !canvas) return () => { };
|
if (!playing || !audio || !canvasCtx) return;
|
||||||
|
|
||||||
let cancelled = false;
|
if (!graphRef.current) {
|
||||||
|
const ctx = new AudioContext();
|
||||||
const init = async () => {
|
const analyser = ctx.createAnalyser();
|
||||||
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();
|
|
||||||
analyser.fftSize = 2048;
|
analyser.fftSize = 2048;
|
||||||
const source = audioCtx.createMediaElementSource(audio);
|
ctx.createMediaElementSource(audio).connect(analyser);
|
||||||
source.connect(analyser);
|
analyser.connect(ctx.destination);
|
||||||
analyser.connect(audioCtx.destination);
|
graphRef.current = { ctx, analyser };
|
||||||
audioCtxRef.current = audioCtx;
|
}
|
||||||
analyserRef.current = analyser;
|
|
||||||
setupDoneRef.current = true;
|
|
||||||
|
|
||||||
if (wasPlaying) {
|
const { ctx, analyser } = graphRef.current;
|
||||||
audio.play().catch(() => { });
|
ctx.resume();
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const canvasCtx = canvas.getContext("2d");
|
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
||||||
let dataArray: Uint8Array<ArrayBuffer> | null = null;
|
const frequencyData = new Uint8Array(analyser.frequencyBinCount);
|
||||||
let frequencyData: Uint8Array<ArrayBuffer> | null = null;
|
let frame = 0;
|
||||||
|
|
||||||
const draw = () => {
|
const draw = () => {
|
||||||
const analyser = analyserRef.current;
|
frame = requestAnimationFrame(draw);
|
||||||
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);
|
|
||||||
|
|
||||||
analyser.getByteTimeDomainData(dataArray);
|
analyser.getByteTimeDomainData(dataArray);
|
||||||
analyser.getByteFrequencyData(frequencyData);
|
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);
|
canvasCtx.clearRect(0, 0, width, height);
|
||||||
if (settings.store.oscilloscope) drawOscilloscope(canvasCtx, width, height, dataArray, dataArray.length);
|
if (settings.store.oscilloscope) drawOscilloscope(canvasCtx, width, height, dataArray, dataArray.length);
|
||||||
if (settings.store.spectrograph) drawSpectrograph(canvasCtx, width, height, frequencyData, frequencyData.length);
|
if (settings.store.spectrograph) drawSpectrograph(canvasCtx, width, height, frequencyData, frequencyData.length);
|
||||||
};
|
};
|
||||||
|
draw();
|
||||||
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();
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelAnimationFrame(frame);
|
||||||
audio.removeEventListener("play", onPlay);
|
ctx.suspend();
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [playerRef]);
|
}, [playing]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => () => void graphRef.current?.ctx.close(), []);
|
||||||
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();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<canvas
|
<canvas
|
||||||
|
|
@ -281,15 +208,21 @@ export default definePlugin({
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
find: "}renderPlayIcon(){",
|
find: "}renderPlayIcon(){",
|
||||||
replacement: {
|
replacement: [
|
||||||
match: /this\.renderAudio\(\):this\.renderVideo\(\)/,
|
{
|
||||||
replace: "$&,$self.renderVisualizer(this)",
|
match: /"audio",\{(?=className:)/,
|
||||||
},
|
replace: '$&crossOrigin:"anonymous",',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /this\.renderAudio\(\):this\.renderVideo\(\)/,
|
||||||
|
replace: "$&,$self.renderVisualizer(this)",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
renderVisualizer(player: PlayerInstance) {
|
renderVisualizer(player: PlayerInstance) {
|
||||||
if (player.props.type !== "AUDIO") return null;
|
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} />;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue