fix: Fix Safari volume bug, #34797 (#34929)

This commit is contained in:
diondiondion 2025-06-04 16:35:29 +02:00 committed by GitHub
parent e9f197740d
commit 6637ecb460
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 117 additions and 71 deletions

View file

@ -17,6 +17,7 @@ import { Blurhash } from 'mastodon/components/blurhash';
import { Icon } from 'mastodon/components/icon';
import { SpoilerButton } from 'mastodon/components/spoiler_button';
import { formatTime, getPointerPosition } from 'mastodon/features/video';
import { useAudioContext } from 'mastodon/hooks/useAudioContext';
import { useAudioVisualizer } from 'mastodon/hooks/useAudioVisualizer';
import {
displayMedia,
@ -119,12 +120,17 @@ export const Audio: React.FC<{
const seekRef = useRef<HTMLDivElement>(null);
const volumeRef = useRef<HTMLDivElement>(null);
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>();
const [resumeAudio, suspendAudio, frequencyBands] = useAudioVisualizer(
audioRef,
3,
);
const accessibilityId = useId();
const { audioContextRef, sourceRef, gainNodeRef, playAudio, pauseAudio } =
useAudioContext({ audioElementRef: audioRef });
const frequencyBands = useAudioVisualizer({
audioContextRef,
sourceRef,
numBands: 3,
});
const [style, spring] = useSpring(() => ({
progress: '0%',
buffer: '0%',
@ -152,6 +158,9 @@ export const Audio: React.FC<{
restoreVolume(audioRef.current);
setVolume(audioRef.current.volume);
setMuted(audioRef.current.muted);
if (gainNodeRef.current) {
gainNodeRef.current.gain.value = audioRef.current.volume;
}
void spring.start({
volume: `${audioRef.current.volume * 100}%`,
immediate: reduceMotion,
@ -159,15 +168,14 @@ export const Audio: React.FC<{
}
},
[
spring,
setVolume,
setMuted,
deployPictureInPicture,
src,
poster,
backgroundColor,
accentColor,
foregroundColor,
deployPictureInPicture,
accentColor,
gainNodeRef,
spring,
],
);
@ -178,7 +186,11 @@ export const Audio: React.FC<{
audioRef.current.volume = volume;
audioRef.current.muted = muted;
}, [volume, muted]);
if (gainNodeRef.current) {
gainNodeRef.current.gain.value = muted ? 0 : volume;
}
}, [volume, muted, gainNodeRef]);
useEffect(() => {
if (typeof visible !== 'undefined') {
@ -192,11 +204,10 @@ export const Audio: React.FC<{
}, [visible, sensitive]);
useEffect(() => {
if (!revealed && audioRef.current) {
audioRef.current.pause();
suspendAudio();
if (!revealed) {
pauseAudio();
}
}, [suspendAudio, revealed]);
}, [pauseAudio, revealed]);
useEffect(() => {
let nextFrame: ReturnType<typeof requestAnimationFrame>;
@ -228,13 +239,11 @@ export const Audio: React.FC<{
}
if (audioRef.current.paused) {
resumeAudio();
void audioRef.current.play();
playAudio();
} else {
audioRef.current.pause();
suspendAudio();
pauseAudio();
}
}, [resumeAudio, suspendAudio]);
}, [playAudio, pauseAudio]);
const handlePlay = useCallback(() => {
setPaused(false);
@ -349,8 +358,7 @@ export const Audio: React.FC<{
document.removeEventListener('mouseup', handleSeekMouseUp, true);
setDragging(false);
resumeAudio();
void audioRef.current?.play();
playAudio();
};
const handleSeekMouseMove = (e: MouseEvent) => {
@ -377,7 +385,7 @@ export const Audio: React.FC<{
e.preventDefault();
e.stopPropagation();
},
[setDragging, spring, resumeAudio],
[playAudio, spring],
);
const handleMouseEnter = useCallback(() => {
@ -446,10 +454,9 @@ export const Audio: React.FC<{
const handleCanPlayThrough = useCallback(() => {
if (startPlaying) {
resumeAudio();
void audioRef.current?.play();
playAudio();
}
}, [startPlaying, resumeAudio]);
}, [startPlaying, playAudio]);
const seekBy = (time: number) => {
if (!audioRef.current) {
@ -492,7 +499,7 @@ export const Audio: React.FC<{
return;
}
const newVolume = audioRef.current.volume + step;
const newVolume = Math.max(0, audioRef.current.volume + step);
if (!isNaN(newVolume)) {
audioRef.current.volume = newVolume;

View file

@ -0,0 +1,62 @@
import { useCallback, useEffect, useRef } from 'react';
interface AudioContextOptions {
audioElementRef: React.MutableRefObject<HTMLAudioElement | null>;
}
/**
* Create and return an audio context instance for a given audio element [0].
* Also returns an associated audio source, a gain node, and play and pause actions
* which should be used instead of `audioElementRef.current.play/pause()`.
*
* [0] https://developer.mozilla.org/en-US/docs/Web/API/AudioContext
*/
export const useAudioContext = ({ audioElementRef }: AudioContextOptions) => {
const audioContextRef = useRef<AudioContext>();
const sourceRef = useRef<MediaElementAudioSourceNode>();
const gainNodeRef = useRef<GainNode>();
useEffect(() => {
if (!audioElementRef.current) {
return;
}
const context = audioContextRef.current ?? new AudioContext();
const source =
sourceRef.current ??
context.createMediaElementSource(audioElementRef.current);
const gainNode = context.createGain();
gainNode.connect(context.destination);
source.connect(gainNode);
audioContextRef.current = context;
gainNodeRef.current = gainNode;
sourceRef.current = source;
return () => {
if (context.state !== 'closed') {
void context.close();
}
};
}, [audioElementRef]);
const playAudio = useCallback(() => {
void audioElementRef.current?.play();
void audioContextRef.current?.resume();
}, [audioElementRef]);
const pauseAudio = useCallback(() => {
audioElementRef.current?.pause();
void audioContextRef.current?.suspend();
}, [audioElementRef]);
return {
audioContextRef,
sourceRef,
gainNodeRef,
playAudio,
pauseAudio,
};
};

View file

@ -1,4 +1,4 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useState, useEffect, useRef } from 'react';
const normalizeFrequencies = (arr: Float32Array): number[] => {
return new Array(...arr).map((value: number) => {
@ -10,12 +10,17 @@ const normalizeFrequencies = (arr: Float32Array): number[] => {
});
};
export const useAudioVisualizer = (
ref: React.MutableRefObject<HTMLAudioElement | null>,
numBands: number,
) => {
const audioContextRef = useRef<AudioContext>();
const sourceRef = useRef<MediaElementAudioSourceNode>();
interface AudioVisualiserOptions {
audioContextRef: React.MutableRefObject<AudioContext | undefined>;
sourceRef: React.MutableRefObject<MediaElementAudioSourceNode | undefined>;
numBands: number;
}
export const useAudioVisualizer = ({
audioContextRef,
sourceRef,
numBands,
}: AudioVisualiserOptions) => {
const analyzerRef = useRef<AnalyserNode>();
const [frequencyBands, setFrequencyBands] = useState<number[]>(
@ -23,47 +28,31 @@ export const useAudioVisualizer = (
);
useEffect(() => {
if (!audioContextRef.current) {
audioContextRef.current = new AudioContext();
if (audioContextRef.current) {
analyzerRef.current = audioContextRef.current.createAnalyser();
analyzerRef.current.smoothingTimeConstant = 0.6;
analyzerRef.current.fftSize = 2048;
}
return () => {
if (audioContextRef.current) {
void audioContextRef.current.close();
}
};
}, []);
}, [audioContextRef]);
useEffect(() => {
if (
audioContextRef.current &&
analyzerRef.current &&
!sourceRef.current &&
ref.current
) {
sourceRef.current = audioContextRef.current.createMediaElementSource(
ref.current,
);
if (analyzerRef.current && sourceRef.current) {
sourceRef.current.connect(analyzerRef.current);
sourceRef.current.connect(audioContextRef.current.destination);
}
const currentSource = sourceRef.current;
return () => {
if (sourceRef.current) {
sourceRef.current.disconnect();
if (currentSource && analyzerRef.current) {
currentSource.disconnect(analyzerRef.current);
}
};
}, [ref]);
}, [audioContextRef, sourceRef]);
useEffect(() => {
const source = sourceRef.current;
const analyzer = analyzerRef.current;
const context = audioContextRef.current;
if (!source || !analyzer || !context) {
if (!analyzer || !context) {
return;
}
@ -94,19 +83,7 @@ export const useAudioVisualizer = (
return () => {
clearInterval(updateInterval);
};
}, [numBands]);
}, [numBands, audioContextRef]);
const resume = useCallback(() => {
if (audioContextRef.current) {
void audioContextRef.current.resume();
}
}, []);
const suspend = useCallback(() => {
if (audioContextRef.current) {
void audioContextRef.current.suspend();
}
}, []);
return [resume, suspend, frequencyBands] as const;
return frequencyBands;
};