Port reactionaudiorenderer to useAudioContext

This commit is contained in:
Half-Shot
2024-12-03 14:36:40 +00:00
parent fc0bc1a3d7
commit 719e6e7977
3 changed files with 39 additions and 50 deletions

View File

@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
*/ */
import { ReactNode, useEffect } from "react"; import { ReactNode, useEffect } from "react";
import { debounce, filter, interval, skip, throttle } from "rxjs"; import { debounce, filter, interval, throttle } from "rxjs";
import { CallViewModel } from "../state/CallViewModel"; import { CallViewModel } from "../state/CallViewModel";
import joinCallSoundMp3 from "../sound/join_call.mp3"; import joinCallSoundMp3 from "../sound/join_call.mp3";
import joinCallSoundOgg from "../sound/join_call.ogg"; import joinCallSoundOgg from "../sound/join_call.ogg";

View File

@@ -5,70 +5,56 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { ReactNode, useEffect, useRef } from "react"; import { ReactNode, useDeferredValue, useEffect } from "react";
import { useReactions } from "../useReactions"; import { useReactions } from "../useReactions";
import { import { playReactionsSound, useSetting } from "../settings/settings";
playReactionsSound, import { ReactionSet } from "../reactions";
soundEffectVolumeSetting as effectSoundVolumeSetting, import { prefetchSounds, useAudioContext } from "../useAudioContext";
useSetting,
} from "../settings/settings"; const SoundMap = Object.fromEntries(
import { GenericReaction, ReactionSet } from "../reactions"; ReactionSet.filter((v) => v.sound !== undefined).map((v) => [
v.name,
v.sound!,
]),
);
const Sounds = prefetchSounds(SoundMap);
export function ReactionsAudioRenderer(): ReactNode { export function ReactionsAudioRenderer(): ReactNode {
const { reactions } = useReactions(); const { reactions } = useReactions();
const [shouldPlay] = useSetting(playReactionsSound); const [shouldPlay] = useSetting(playReactionsSound);
const [effectSoundVolume] = useSetting(effectSoundVolumeSetting); const audioEngineCtx = useAudioContext({
const audioElements = useRef<Record<string, HTMLAudioElement | null>>({}); sounds: Sounds,
latencyHint: "interactive",
});
const oldReactions = useDeferredValue(reactions);
useEffect(() => { useEffect(() => {
if (!audioElements.current) { if (!audioEngineCtx) {
return; return;
} }
if (!shouldPlay) { if (!shouldPlay) {
return; return;
} }
const oldReactionSet = new Set(
Object.values(oldReactions).map((r) => r.name),
);
for (const reactionName of new Set( for (const reactionName of new Set(
Object.values(reactions).map((r) => r.name), Object.values(reactions).map((r) => r.name),
)) { )) {
const audioElement = if (oldReactionSet.has(reactionName)) {
audioElements.current[reactionName] ?? audioElements.current.generic; // Don't replay old reactions
if (audioElement?.paused) { return;
audioElement.volume = effectSoundVolume; }
void audioElement.play(); if (SoundMap[reactionName]) {
audioEngineCtx.playSound(reactionName);
} else {
// Fallback sounds.
audioEngineCtx.playSound("generic");
} }
} }
}, [audioElements, shouldPlay, reactions, effectSoundVolume]); }, [shouldPlay, oldReactions, reactions]);
return <></>;
// Do not render any audio elements if playback is disabled. Will save
// audio file fetches.
if (!shouldPlay) {
return null;
}
// NOTE: We load all audio elements ahead of time to allow the cache
// to be populated, rather than risk a cache miss and have the audio
// be delayed.
return (
<>
{[GenericReaction, ...ReactionSet].map(
(r) =>
r.sound && (
<audio
ref={(el) => (audioElements.current[r.name] = el)}
data-testid={r.name}
key={r.name}
preload="auto"
hidden
>
<source src={r.sound.ogg} type="audio/ogg; codecs=vorbis" />
{r.sound.mp3 ? (
<source src={r.sound.mp3} type="audio/mpeg" />
) : null}
</audio>
),
)}
</>
);
} }

View File

@@ -6,7 +6,7 @@ import {
} from "./settings/settings"; } from "./settings/settings";
import { useMediaDevices } from "./livekit/MediaDevicesContext"; import { useMediaDevices } from "./livekit/MediaDevicesContext";
type SoundDefinition = { mp3: string; ogg: string }; type SoundDefinition = { mp3?: string; ogg: string };
async function fetchBuffer(filename: string) { async function fetchBuffer(filename: string) {
// Load an audio file // Load an audio file
@@ -64,7 +64,10 @@ export async function prefetchSounds<S extends string>(
await Promise.all( await Promise.all(
Object.entries(sounds).map(async ([name, file]) => { Object.entries(sounds).map(async ([name, file]) => {
const { mp3, ogg } = file as SoundDefinition; const { mp3, ogg } = file as SoundDefinition;
buffers[name] = await fetchBuffer(PreferredFormat === "ogg" ? ogg : mp3); // Use preferred format, fallback to ogg if no mp3 is provided.
buffers[name] = await fetchBuffer(
PreferredFormat === "ogg" ? ogg : (mp3 ?? ogg),
);
}), }),
); );
return buffers as Record<S, ArrayBuffer>; return buffers as Record<S, ArrayBuffer>;