mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(app): make the media volume slider track loudness (#1847)
This commit is contained in:
1 parent
5cb893245f
commit
7c5cab2144
1 file changed
+116
-37
@@ -6,7 +6,22 @@ import {
|
||||
MUTE_STORAGE_KEY,
|
||||
VOLUME_STORAGE_KEY,
|
||||
} from '@app/features/voice/components/media_player/utils/MediaConstants';
|
||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {useCallback, useEffect, useRef, useState, useSyncExternalStore} from 'react';
|
||||
|
||||
const VOLUME_CURVE_EXPONENT = 2.8;
|
||||
const VOLUME_SETTING_EPSILON = 1e-6;
|
||||
|
||||
export function mediaGainFromVolumeSetting(setting: number): number {
|
||||
if (!Number.isFinite(setting) || setting <= 0) return 0;
|
||||
if (setting >= 1) return 1;
|
||||
return setting ** VOLUME_CURVE_EXPONENT;
|
||||
}
|
||||
|
||||
export function volumeSettingFromMediaGain(gain: number): number {
|
||||
if (!Number.isFinite(gain) || gain <= 0) return 0;
|
||||
if (gain >= 1) return 1;
|
||||
return gain ** (1 / VOLUME_CURVE_EXPONENT);
|
||||
}
|
||||
|
||||
interface UseMediaVolumeOptions {
|
||||
mediaRef: React.RefObject<HTMLMediaElement | null>;
|
||||
@@ -34,7 +49,7 @@ function getStoredVolume(): number {
|
||||
if (stored !== null) {
|
||||
const value = parseFloat(stored);
|
||||
if (Number.isFinite(value) && value >= 0 && value <= 1) {
|
||||
return value;
|
||||
return volumeSettingFromMediaGain(value);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
@@ -49,9 +64,9 @@ function getStoredMuted(): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function storeVolume(volume: number): void {
|
||||
function storeVolume(setting: number): void {
|
||||
try {
|
||||
AppStorage.setItem(VOLUME_STORAGE_KEY, volume.toString());
|
||||
AppStorage.setItem(VOLUME_STORAGE_KEY, mediaGainFromVolumeSetting(setting).toString());
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -61,65 +76,129 @@ function storeMuted(muted: boolean): void {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
interface StoredVolumeSetting {
|
||||
volume: number;
|
||||
muted: boolean;
|
||||
}
|
||||
|
||||
let storedVolumeSetting: StoredVolumeSetting | null = null;
|
||||
const storedVolumeListeners = new Set<() => void>();
|
||||
|
||||
function readStoredVolumeSetting(): StoredVolumeSetting {
|
||||
storedVolumeSetting ??= {volume: getStoredVolume(), muted: getStoredMuted()};
|
||||
return storedVolumeSetting;
|
||||
}
|
||||
|
||||
function subscribeStoredVolumeSetting(listener: () => void): () => void {
|
||||
storedVolumeListeners.add(listener);
|
||||
return () => {
|
||||
storedVolumeListeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
function publishStoredVolumeSetting(next: StoredVolumeSetting): void {
|
||||
const current = readStoredVolumeSetting();
|
||||
if (Math.abs(current.volume - next.volume) < VOLUME_SETTING_EPSILON && current.muted === next.muted) {
|
||||
return;
|
||||
}
|
||||
storedVolumeSetting = next;
|
||||
storeVolume(next.volume);
|
||||
storeMuted(next.muted);
|
||||
for (const listener of Array.from(storedVolumeListeners)) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
export function resetStoredVolumeSettingForTests(): void {
|
||||
storedVolumeSetting = null;
|
||||
storedVolumeListeners.clear();
|
||||
}
|
||||
|
||||
export function useMediaVolume(options: UseMediaVolumeOptions): UseMediaVolumeReturn {
|
||||
const {mediaRef, initialVolume, initialMuted, persist = true, onVolumeChange, onMuteChange} = options;
|
||||
const [volume, setVolumeState] = useState(() => initialVolume ?? (persist ? getStoredVolume() : DEFAULT_VOLUME));
|
||||
const [isMuted, setIsMutedState] = useState(() => initialMuted ?? (persist ? getStoredMuted() : false));
|
||||
const tracksSharedSetting = persist && initialVolume === undefined && initialMuted === undefined;
|
||||
const sharedSetting = useSyncExternalStore(
|
||||
subscribeStoredVolumeSetting,
|
||||
readStoredVolumeSetting,
|
||||
readStoredVolumeSetting,
|
||||
);
|
||||
const [ownVolume, setOwnVolume] = useState(() => initialVolume ?? (persist ? getStoredVolume() : DEFAULT_VOLUME));
|
||||
const [ownMuted, setOwnMuted] = useState(() => initialMuted ?? (persist ? getStoredMuted() : false));
|
||||
const volume = tracksSharedSetting ? sharedSetting.volume : ownVolume;
|
||||
const isMuted = tracksSharedSetting ? sharedSetting.muted : ownMuted;
|
||||
const previousVolumeRef = useRef(volume > 0 ? volume : DEFAULT_VOLUME);
|
||||
const settingRef = useRef({volume, isMuted});
|
||||
settingRef.current = {volume, isMuted};
|
||||
const commitSetting = useCallback(
|
||||
(nextVolume: number, nextMuted: boolean) => {
|
||||
if (tracksSharedSetting) {
|
||||
publishStoredVolumeSetting({volume: nextVolume, muted: nextMuted});
|
||||
return;
|
||||
}
|
||||
setOwnVolume(nextVolume);
|
||||
setOwnMuted(nextMuted);
|
||||
if (persist) {
|
||||
storeVolume(nextVolume);
|
||||
storeMuted(nextMuted);
|
||||
}
|
||||
},
|
||||
[persist, tracksSharedSetting],
|
||||
);
|
||||
useEffect(() => {
|
||||
const media = mediaRef.current;
|
||||
if (!media) return;
|
||||
media.volume = volume;
|
||||
media.volume = mediaGainFromVolumeSetting(volume);
|
||||
media.muted = isMuted;
|
||||
const handleVolumeChange = () => {
|
||||
const newVolume = media.volume;
|
||||
const newMuted = media.muted;
|
||||
setVolumeState(newVolume);
|
||||
setIsMutedState(newMuted);
|
||||
if (newVolume > 0) {
|
||||
previousVolumeRef.current = newVolume;
|
||||
const setting = volumeSettingFromMediaGain(media.volume);
|
||||
if (setting > 0) {
|
||||
previousVolumeRef.current = setting;
|
||||
}
|
||||
const current = settingRef.current;
|
||||
if (Math.abs(current.volume - setting) < VOLUME_SETTING_EPSILON && current.isMuted === media.muted) {
|
||||
return;
|
||||
}
|
||||
commitSetting(setting, media.muted);
|
||||
};
|
||||
media.addEventListener('volumechange', handleVolumeChange);
|
||||
return () => {
|
||||
media.removeEventListener('volumechange', handleVolumeChange);
|
||||
};
|
||||
}, [mediaRef, volume, isMuted]);
|
||||
}, [mediaRef, volume, isMuted, commitSetting]);
|
||||
const setMuted = useCallback(
|
||||
(muted: boolean) => {
|
||||
const media = mediaRef.current;
|
||||
let nextVolume = settingRef.current.volume;
|
||||
if (!muted && nextVolume === 0) {
|
||||
nextVolume = previousVolumeRef.current;
|
||||
}
|
||||
if (media) {
|
||||
media.muted = muted;
|
||||
media.volume = mediaGainFromVolumeSetting(nextVolume);
|
||||
}
|
||||
commitSetting(nextVolume, muted);
|
||||
onMuteChange?.(muted);
|
||||
},
|
||||
[mediaRef, commitSetting, onMuteChange],
|
||||
);
|
||||
const setVolume = useCallback(
|
||||
(newVolume: number) => {
|
||||
const media = mediaRef.current;
|
||||
const clampedVolume = Math.max(0, Math.min(1, newVolume));
|
||||
if (media) {
|
||||
media.volume = clampedVolume;
|
||||
media.volume = mediaGainFromVolumeSetting(clampedVolume);
|
||||
}
|
||||
setVolumeState(clampedVolume);
|
||||
if (clampedVolume > 0) {
|
||||
previousVolumeRef.current = clampedVolume;
|
||||
}
|
||||
if (persist) {
|
||||
storeVolume(clampedVolume);
|
||||
const nextMuted = clampedVolume === 0;
|
||||
if (media) {
|
||||
media.muted = nextMuted;
|
||||
}
|
||||
commitSetting(clampedVolume, nextMuted);
|
||||
onVolumeChange?.(clampedVolume);
|
||||
},
|
||||
[mediaRef, persist, onVolumeChange],
|
||||
);
|
||||
const setMuted = useCallback(
|
||||
(muted: boolean) => {
|
||||
const media = mediaRef.current;
|
||||
if (media) {
|
||||
media.muted = muted;
|
||||
if (!muted && media.volume === 0) {
|
||||
media.volume = previousVolumeRef.current;
|
||||
setVolumeState(previousVolumeRef.current);
|
||||
}
|
||||
}
|
||||
setIsMutedState(muted);
|
||||
if (persist) {
|
||||
storeMuted(muted);
|
||||
}
|
||||
onMuteChange?.(muted);
|
||||
},
|
||||
[mediaRef, persist, onMuteChange],
|
||||
[mediaRef, commitSetting, onVolumeChange],
|
||||
);
|
||||
const toggleMute = useCallback(() => {
|
||||
setMuted(!isMuted);
|
||||
|
||||
Reference in new issue
Block a user