mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-31 07:00:26 +00:00
Preload reaction sounds to prevent delays.
This commit is contained in:
@@ -659,7 +659,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
))}
|
||||
<RoomAudioRenderer />
|
||||
{renderContent()}
|
||||
<audio ref={handRaisePlayer} hidden>
|
||||
<audio ref={handRaisePlayer} preload="auto" hidden>
|
||||
<source src={handSoundOgg} type="audio/ogg; codecs=vorbis" />
|
||||
<source src={handSoundMp3} type="audio/mpeg" />
|
||||
</audio>
|
||||
|
||||
@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { act, render } from "@testing-library/react";
|
||||
import { expect, test } from "vitest";
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "../utils/testReactions";
|
||||
import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
||||
import { ReactionSet } from "../reactions";
|
||||
import { playReactionsSound } from "../settings/settings";
|
||||
|
||||
const memberUserIdAlice = "@alice:example.org";
|
||||
const memberUserIdBob = "@bob:example.org";
|
||||
@@ -45,7 +46,26 @@ function TestComponent({
|
||||
);
|
||||
}
|
||||
|
||||
test("defaults to no audio elements", () => {
|
||||
const originalPlayFn = window.HTMLMediaElement.prototype.play;
|
||||
afterAll(() => {
|
||||
playReactionsSound.setValue(playReactionsSound.defaultValue);
|
||||
window.HTMLMediaElement.prototype.play = originalPlayFn;
|
||||
});
|
||||
|
||||
test("preloads all audio elements", () => {
|
||||
playReactionsSound.setValue(true);
|
||||
const rtcSession = new MockRTCSession(
|
||||
new MockRoom(memberUserIdAlice),
|
||||
membership,
|
||||
);
|
||||
const { container } = render(<TestComponent rtcSession={rtcSession} />);
|
||||
expect(container.getElementsByTagName("audio")).toHaveLength(
|
||||
ReactionSet.filter((r) => r.sound).length,
|
||||
);
|
||||
});
|
||||
|
||||
test("loads no audio elements when disabled in settings", () => {
|
||||
playReactionsSound.setValue(false);
|
||||
const rtcSession = new MockRTCSession(
|
||||
new MockRoom(memberUserIdAlice),
|
||||
membership,
|
||||
@@ -55,9 +75,15 @@ test("defaults to no audio elements", () => {
|
||||
});
|
||||
|
||||
test("will play an audio sound when there is a reaction", () => {
|
||||
const audioIsPlaying: string[] = [];
|
||||
window.HTMLMediaElement.prototype.play = async function (): Promise<void> {
|
||||
audioIsPlaying.push((this.children[0] as HTMLSourceElement).src);
|
||||
return Promise.resolve();
|
||||
};
|
||||
playReactionsSound.setValue(true);
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const { container } = render(<TestComponent rtcSession={rtcSession} />);
|
||||
render(<TestComponent rtcSession={rtcSession} />);
|
||||
|
||||
// Find the first reaction with a sound effect
|
||||
const chosenReaction = ReactionSet.find((r) => !!r.sound);
|
||||
@@ -69,24 +95,21 @@ test("will play an audio sound when there is a reaction", () => {
|
||||
act(() => {
|
||||
room.testSendReaction(memberEventAlice, chosenReaction, membership);
|
||||
});
|
||||
const elements = container.getElementsByTagName("audio");
|
||||
expect(elements).toHaveLength(1);
|
||||
const audioElement = elements[0];
|
||||
|
||||
expect(audioElement.autoplay).toBe(true);
|
||||
|
||||
const sources = audioElement.getElementsByTagName("source");
|
||||
expect(sources).toHaveLength(2);
|
||||
|
||||
// The element will be the full URL, whereas the chosenReaction will have the path.
|
||||
expect(sources[0].src).toContain(chosenReaction.sound?.ogg);
|
||||
expect(sources[1].src).toContain(chosenReaction.sound?.mp3);
|
||||
expect(audioIsPlaying).toHaveLength(1);
|
||||
expect(audioIsPlaying[0]).toContain(chosenReaction.sound?.ogg);
|
||||
});
|
||||
|
||||
test("will play multiple audio sounds when there are multiple different reactions", () => {
|
||||
const audioIsPlaying: string[] = [];
|
||||
window.HTMLMediaElement.prototype.play = async function (): Promise<void> {
|
||||
audioIsPlaying.push((this.children[0] as HTMLSourceElement).src);
|
||||
return Promise.resolve();
|
||||
};
|
||||
playReactionsSound.setValue(true);
|
||||
|
||||
const room = new MockRoom(memberUserIdAlice);
|
||||
const rtcSession = new MockRTCSession(room, membership);
|
||||
const { container } = render(<TestComponent rtcSession={rtcSession} />);
|
||||
render(<TestComponent rtcSession={rtcSession} />);
|
||||
|
||||
// Find the first reaction with a sound effect
|
||||
const [reaction1, reaction2] = ReactionSet.filter((r) => !!r.sound);
|
||||
@@ -100,7 +123,7 @@ test("will play multiple audio sounds when there are multiple different reaction
|
||||
room.testSendReaction(memberEventBob, reaction2, membership);
|
||||
room.testSendReaction(memberEventCharlie, reaction1, membership);
|
||||
});
|
||||
const elements = container.getElementsByTagName("audio");
|
||||
// Do not play the same reaction twice.
|
||||
expect(elements).toHaveLength(2);
|
||||
expect(audioIsPlaying).toHaveLength(2);
|
||||
expect(audioIsPlaying[0]).toContain(reaction1.sound?.ogg);
|
||||
expect(audioIsPlaying[1]).toContain(reaction2.sound?.ogg);
|
||||
});
|
||||
|
||||
@@ -5,35 +5,56 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { ReactNode, useMemo } from "react";
|
||||
import { ReactNode, useEffect, useRef } from "react";
|
||||
|
||||
import { useReactions } from "../useReactions";
|
||||
import { playReactionsSound, useSetting } from "../settings/settings";
|
||||
import { ReactionSet } from "../reactions";
|
||||
|
||||
export function ReactionsAudioRenderer(): ReactNode {
|
||||
const { reactions } = useReactions();
|
||||
const [shouldPlay] = useSetting(playReactionsSound);
|
||||
const audioElements = useRef<Record<string, HTMLAudioElement | null>>({});
|
||||
|
||||
const expectedReactions = useMemo(() => {
|
||||
if (!shouldPlay) {
|
||||
return [];
|
||||
useEffect(() => {
|
||||
if (!audioElements.current) {
|
||||
return;
|
||||
}
|
||||
const reactionsToPlayNames = new Set();
|
||||
return Object.values(reactions).filter((r) => {
|
||||
if (reactionsToPlayNames.has(r.name)) {
|
||||
return false;
|
||||
}
|
||||
reactionsToPlayNames.add(r.name);
|
||||
return true;
|
||||
});
|
||||
}, [shouldPlay, reactions]);
|
||||
|
||||
if (!shouldPlay) {
|
||||
return;
|
||||
}
|
||||
for (const reactionName of new Set(
|
||||
Object.values(reactions).map((r) => r.name),
|
||||
)) {
|
||||
const audioElement = audioElements.current[reactionName];
|
||||
if (audioElement?.paused) {
|
||||
void audioElement.play();
|
||||
}
|
||||
}
|
||||
}, [audioElements, shouldPlay, reactions]);
|
||||
|
||||
// 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 (
|
||||
<>
|
||||
{expectedReactions.map(
|
||||
{ReactionSet.map(
|
||||
(r) =>
|
||||
r.sound && (
|
||||
<audio key={r.name} autoPlay hidden>
|
||||
<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" />
|
||||
|
||||
@@ -12,7 +12,10 @@ import { useObservableEagerState } from "observable-hooks";
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
|
||||
export class Setting<T> {
|
||||
public constructor(key: string, defaultValue: T) {
|
||||
public constructor(
|
||||
key: string,
|
||||
public readonly defaultValue: T,
|
||||
) {
|
||||
this.key = `matrix-setting-${key}`;
|
||||
|
||||
const storedValue = localStorage.getItem(this.key);
|
||||
|
||||
Reference in New Issue
Block a user