mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-16 06:17:10 +00:00
Merge branch 'livekit' into toger5/track-processor-blur
This commit is contained in:
@@ -13,11 +13,11 @@ import {
|
||||
type MockedFunction,
|
||||
test,
|
||||
vitest,
|
||||
afterEach,
|
||||
} from "vitest";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { ConnectionState } from "livekit-client";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
import { afterEach } from "node:test";
|
||||
import { act, type ReactNode } from "react";
|
||||
import {
|
||||
type CallMembership,
|
||||
@@ -100,13 +100,13 @@ function getMockEnv(
|
||||
): {
|
||||
vm: CallViewModel;
|
||||
session: MockRTCSession;
|
||||
remoteRtcMemberships: BehaviorSubject<CallMembership[]>;
|
||||
remoteRtcMemberships$: BehaviorSubject<CallMembership[]>;
|
||||
} {
|
||||
const matrixRoomMembers = new Map(members.map((p) => [p.userId, p]));
|
||||
const remoteParticipants = of([aliceParticipant]);
|
||||
const remoteParticipants$ = of([aliceParticipant]);
|
||||
const liveKitRoom = mockLivekitRoom(
|
||||
{ localParticipant },
|
||||
{ remoteParticipants },
|
||||
{ remoteParticipants$ },
|
||||
);
|
||||
const matrixRoom = mockMatrixRoom({
|
||||
client: {
|
||||
@@ -118,14 +118,14 @@ function getMockEnv(
|
||||
getMember: (userId) => matrixRoomMembers.get(userId) ?? null,
|
||||
});
|
||||
|
||||
const remoteRtcMemberships = new BehaviorSubject<CallMembership[]>(
|
||||
const remoteRtcMemberships$ = new BehaviorSubject<CallMembership[]>(
|
||||
initialRemoteRtcMemberships,
|
||||
);
|
||||
|
||||
const session = new MockRTCSession(
|
||||
matrixRoom,
|
||||
localRtcMember,
|
||||
).withMemberships(remoteRtcMemberships);
|
||||
).withMemberships(remoteRtcMemberships$);
|
||||
|
||||
const vm = new CallViewModel(
|
||||
session as unknown as MatrixRTCSession,
|
||||
@@ -135,7 +135,7 @@ function getMockEnv(
|
||||
},
|
||||
of(ConnectionState.Connected),
|
||||
);
|
||||
return { vm, session, remoteRtcMemberships };
|
||||
return { vm, session, remoteRtcMemberships$ };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,33 +146,33 @@ function getMockEnv(
|
||||
* a noise every time.
|
||||
*/
|
||||
test("plays one sound when entering a call", () => {
|
||||
const { session, vm, remoteRtcMemberships } = getMockEnv([local, alice]);
|
||||
const { session, vm, remoteRtcMemberships$ } = getMockEnv([local, alice]);
|
||||
render(<TestComponent rtcSession={session} vm={vm} />);
|
||||
// Joining a call usually means remote participants are added later.
|
||||
act(() => {
|
||||
remoteRtcMemberships.next([aliceRtcMember, bobRtcMember]);
|
||||
remoteRtcMemberships$.next([aliceRtcMember, bobRtcMember]);
|
||||
});
|
||||
expect(playSound).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
// TODO: Same test?
|
||||
test("plays a sound when a user joins", () => {
|
||||
const { session, vm, remoteRtcMemberships } = getMockEnv([local, alice]);
|
||||
const { session, vm, remoteRtcMemberships$ } = getMockEnv([local, alice]);
|
||||
render(<TestComponent rtcSession={session} vm={vm} />);
|
||||
|
||||
act(() => {
|
||||
remoteRtcMemberships.next([aliceRtcMember, bobRtcMember]);
|
||||
remoteRtcMemberships$.next([aliceRtcMember, bobRtcMember]);
|
||||
});
|
||||
// Play a sound when joining a call.
|
||||
expect(playSound).toBeCalledWith("join");
|
||||
});
|
||||
|
||||
test("plays a sound when a user leaves", () => {
|
||||
const { session, vm, remoteRtcMemberships } = getMockEnv([local, alice]);
|
||||
const { session, vm, remoteRtcMemberships$ } = getMockEnv([local, alice]);
|
||||
render(<TestComponent rtcSession={session} vm={vm} />);
|
||||
|
||||
act(() => {
|
||||
remoteRtcMemberships.next([]);
|
||||
remoteRtcMemberships$.next([]);
|
||||
});
|
||||
expect(playSound).toBeCalledWith("left");
|
||||
});
|
||||
@@ -185,7 +185,7 @@ test("plays no sound when the participant list is more than the maximum size", (
|
||||
);
|
||||
}
|
||||
|
||||
const { session, vm, remoteRtcMemberships } = getMockEnv(
|
||||
const { session, vm, remoteRtcMemberships$ } = getMockEnv(
|
||||
[local, alice],
|
||||
mockRtcMemberships,
|
||||
);
|
||||
@@ -193,7 +193,7 @@ test("plays no sound when the participant list is more than the maximum size", (
|
||||
render(<TestComponent rtcSession={session} vm={vm} />);
|
||||
expect(playSound).not.toBeCalled();
|
||||
act(() => {
|
||||
remoteRtcMemberships.next(
|
||||
remoteRtcMemberships$.next(
|
||||
mockRtcMemberships.slice(0, MAX_PARTICIPANT_COUNT_FOR_SOUND - 1),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -65,7 +65,7 @@ export function CallEventAudioRenderer({
|
||||
}, [audioEngineRef, previousRaisedHandCount, raisedHandCount]);
|
||||
|
||||
useEffect(() => {
|
||||
const joinSub = vm.memberChanges
|
||||
const joinSub = vm.memberChanges$
|
||||
.pipe(
|
||||
filter(
|
||||
({ joined, ids }) =>
|
||||
@@ -77,7 +77,7 @@ export function CallEventAudioRenderer({
|
||||
void audioEngineRef.current?.playSound("join");
|
||||
});
|
||||
|
||||
const leftSub = vm.memberChanges
|
||||
const leftSub = vm.memberChanges$
|
||||
.pipe(
|
||||
filter(
|
||||
({ ids, left }) =>
|
||||
|
||||
@@ -110,8 +110,8 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
sfuConfig,
|
||||
props.e2eeSystem,
|
||||
);
|
||||
const connStateObservable = useObservable(
|
||||
(inputs) => inputs.pipe(map(([connState]) => connState)),
|
||||
const connStateObservable$ = useObservable(
|
||||
(inputs$) => inputs$.pipe(map(([connState]) => connState)),
|
||||
[connState],
|
||||
);
|
||||
const [vm, setVm] = useState<CallViewModel | null>(null);
|
||||
@@ -131,12 +131,12 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
props.rtcSession,
|
||||
livekitRoom,
|
||||
props.e2eeSystem,
|
||||
connStateObservable,
|
||||
connStateObservable$,
|
||||
);
|
||||
setVm(vm);
|
||||
return (): void => vm.destroy();
|
||||
}
|
||||
}, [props.rtcSession, livekitRoom, props.e2eeSystem, connStateObservable]);
|
||||
}, [props.rtcSession, livekitRoom, props.e2eeSystem, connStateObservable$]);
|
||||
|
||||
if (livekitRoom === undefined || vm === null) return null;
|
||||
|
||||
@@ -225,14 +225,14 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
() => void toggleRaisedHand(),
|
||||
);
|
||||
|
||||
const windowMode = useObservableEagerState(vm.windowMode);
|
||||
const layout = useObservableEagerState(vm.layout);
|
||||
const tileStoreGeneration = useObservableEagerState(vm.tileStoreGeneration);
|
||||
const windowMode = useObservableEagerState(vm.windowMode$);
|
||||
const layout = useObservableEagerState(vm.layout$);
|
||||
const tileStoreGeneration = useObservableEagerState(vm.tileStoreGeneration$);
|
||||
const [debugTileLayout] = useSetting(debugTileLayoutSetting);
|
||||
const gridMode = useObservableEagerState(vm.gridMode);
|
||||
const showHeader = useObservableEagerState(vm.showHeader);
|
||||
const showFooter = useObservableEagerState(vm.showFooter);
|
||||
const switchCamera = useSwitchCamera(vm.localVideo);
|
||||
const gridMode = useObservableEagerState(vm.gridMode$);
|
||||
const showHeader = useObservableEagerState(vm.showHeader$);
|
||||
const showFooter = useObservableEagerState(vm.showFooter$);
|
||||
const switchCamera = useSwitchCamera(vm.localVideo$);
|
||||
|
||||
// Ideally we could detect taps by listening for click events and checking
|
||||
// that the pointerType of the event is "touch", but this isn't yet supported
|
||||
@@ -317,15 +317,15 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
windowMode,
|
||||
],
|
||||
);
|
||||
const gridBoundsObservable = useObservable(
|
||||
(inputs) => inputs.pipe(map(([gridBounds]) => gridBounds)),
|
||||
const gridBoundsObservable$ = useObservable(
|
||||
(inputs$) => inputs$.pipe(map(([gridBounds]) => gridBounds)),
|
||||
[gridBounds],
|
||||
);
|
||||
|
||||
const spotlightAlignment = useInitial(
|
||||
const spotlightAlignment$ = useInitial(
|
||||
() => new BehaviorSubject(defaultSpotlightAlignment),
|
||||
);
|
||||
const pipAlignment = useInitial(
|
||||
const pipAlignment$ = useInitial(
|
||||
() => new BehaviorSubject(defaultPipAlignment),
|
||||
);
|
||||
|
||||
@@ -383,15 +383,17 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
{ className, style, targetWidth, targetHeight, model },
|
||||
ref,
|
||||
) {
|
||||
const spotlightExpanded = useObservableEagerState(vm.spotlightExpanded);
|
||||
const spotlightExpanded = useObservableEagerState(
|
||||
vm.spotlightExpanded$,
|
||||
);
|
||||
const onToggleExpanded = useObservableEagerState(
|
||||
vm.toggleSpotlightExpanded,
|
||||
vm.toggleSpotlightExpanded$,
|
||||
);
|
||||
const showSpeakingIndicatorsValue = useObservableEagerState(
|
||||
vm.showSpeakingIndicators,
|
||||
vm.showSpeakingIndicators$,
|
||||
);
|
||||
const showSpotlightIndicatorsValue = useObservableEagerState(
|
||||
vm.showSpotlightIndicators,
|
||||
vm.showSpotlightIndicators$,
|
||||
);
|
||||
|
||||
return model instanceof GridTileViewModel ? (
|
||||
@@ -424,9 +426,9 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
|
||||
const layouts = useMemo(() => {
|
||||
const inputs = {
|
||||
minBounds: gridBoundsObservable,
|
||||
spotlightAlignment,
|
||||
pipAlignment,
|
||||
minBounds$: gridBoundsObservable$,
|
||||
spotlightAlignment$,
|
||||
pipAlignment$,
|
||||
};
|
||||
return {
|
||||
grid: makeGridLayout(inputs),
|
||||
@@ -435,7 +437,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
"spotlight-expanded": makeSpotlightExpandedLayout(inputs),
|
||||
"one-on-one": makeOneOnOneLayout(inputs),
|
||||
};
|
||||
}, [gridBoundsObservable, spotlightAlignment, pipAlignment]);
|
||||
}, [gridBoundsObservable$, spotlightAlignment$, pipAlignment$]);
|
||||
|
||||
const renderContent = (): JSX.Element => {
|
||||
if (layout.type === "pip") {
|
||||
|
||||
@@ -159,7 +159,7 @@ export const LobbyView: FC<Props> = ({
|
||||
useTrackProcessorSync(videoTrack);
|
||||
const showSwitchCamera = useShowSwitchCamera(
|
||||
useObservable(
|
||||
(inputs) => inputs.pipe(map(([video]) => video)),
|
||||
(inputs$) => inputs$.pipe(map(([video]) => video)),
|
||||
[videoTrack],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -6,13 +6,13 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
||||
import React, { type ReactNode } from "react";
|
||||
import { beforeEach } from "vitest";
|
||||
import { type ReactNode } from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
|
||||
import { useMuteStates } from "./MuteStates";
|
||||
import {
|
||||
type DeviceLabel,
|
||||
type MediaDevice,
|
||||
type MediaDevices,
|
||||
MediaDevicesContext,
|
||||
@@ -63,10 +63,11 @@ const mockCamera: MediaDeviceInfo = {
|
||||
},
|
||||
};
|
||||
|
||||
function mockDevices(available: MediaDeviceInfo[]): MediaDevice {
|
||||
function mockDevices(available: Map<string, DeviceLabel>): MediaDevice {
|
||||
return {
|
||||
available,
|
||||
selectedId: "",
|
||||
selectedGroupId: "",
|
||||
select: (): void => {},
|
||||
};
|
||||
}
|
||||
@@ -83,25 +84,29 @@ function mockMediaDevices(
|
||||
} = { microphone: true, speaker: true, camera: true },
|
||||
): MediaDevices {
|
||||
return {
|
||||
audioInput: mockDevices(microphone ? [mockMicrophone] : []),
|
||||
audioOutput: mockDevices(speaker ? [mockSpeaker] : []),
|
||||
videoInput: mockDevices(camera ? [mockCamera] : []),
|
||||
audioInput: mockDevices(
|
||||
microphone
|
||||
? new Map([[mockMicrophone.deviceId, mockMicrophone]])
|
||||
: new Map(),
|
||||
),
|
||||
audioOutput: mockDevices(
|
||||
speaker ? new Map([[mockSpeaker.deviceId, mockSpeaker]]) : new Map(),
|
||||
),
|
||||
videoInput: mockDevices(
|
||||
camera ? new Map([[mockCamera.deviceId, mockCamera]]) : new Map(),
|
||||
),
|
||||
startUsingDeviceNames: (): void => {},
|
||||
stopUsingDeviceNames: (): void => {},
|
||||
};
|
||||
}
|
||||
|
||||
describe("useMuteStates", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(React, "useContext").mockReturnValue({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("disabled when no input devices", () => {
|
||||
|
||||
@@ -58,12 +58,12 @@ function useMuteState(
|
||||
): MuteState {
|
||||
const [enabled, setEnabled] = useReactiveState<boolean | undefined>(
|
||||
(prev) =>
|
||||
device.available.length > 0 ? (prev ?? enabledByDefault()) : undefined,
|
||||
device.available.size > 0 ? (prev ?? enabledByDefault()) : undefined,
|
||||
[device],
|
||||
);
|
||||
return useMemo(
|
||||
() =>
|
||||
device.available.length === 0
|
||||
device.available.size === 0
|
||||
? deviceUnavailable
|
||||
: {
|
||||
enabled: enabled ?? false,
|
||||
|
||||
@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
|
||||
import { render } from "@testing-library/react";
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeEach,
|
||||
expect,
|
||||
test,
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
} from "vitest";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { act, type ReactNode } from "react";
|
||||
import { afterEach } from "node:test";
|
||||
|
||||
import {
|
||||
MockRoom,
|
||||
|
||||
@@ -6,10 +6,9 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { expect, test } from "vitest";
|
||||
import { expect, test, afterEach } from "vitest";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
import { act, type ReactNode } from "react";
|
||||
import { afterEach } from "node:test";
|
||||
|
||||
import {
|
||||
MockRoom,
|
||||
|
||||
@@ -31,17 +31,17 @@ import { useLatest } from "../useLatest";
|
||||
* producing a callback if so.
|
||||
*/
|
||||
export function useSwitchCamera(
|
||||
video: Observable<LocalVideoTrack | null>,
|
||||
video$: Observable<LocalVideoTrack | null>,
|
||||
): (() => void) | null {
|
||||
const mediaDevices = useMediaDevices();
|
||||
const setVideoInput = useLatest(mediaDevices.videoInput.select);
|
||||
|
||||
// Produce an observable like the input 'video' observable, except make it
|
||||
// emit whenever the track is muted or the device changes
|
||||
const videoTrack: Observable<LocalVideoTrack | null> = useObservable(
|
||||
(inputs) =>
|
||||
inputs.pipe(
|
||||
switchMap(([video]) => video),
|
||||
const videoTrack$: Observable<LocalVideoTrack | null> = useObservable(
|
||||
(inputs$) =>
|
||||
inputs$.pipe(
|
||||
switchMap(([video$]) => video$),
|
||||
switchMap((video) => {
|
||||
if (video === null) return of(null);
|
||||
return merge(
|
||||
@@ -53,15 +53,15 @@ export function useSwitchCamera(
|
||||
);
|
||||
}),
|
||||
),
|
||||
[video],
|
||||
[video$],
|
||||
);
|
||||
|
||||
const switchCamera: Observable<(() => void) | null> = useObservable(
|
||||
(inputs) =>
|
||||
const switchCamera$: Observable<(() => void) | null> = useObservable(
|
||||
(inputs$) =>
|
||||
platform === "desktop"
|
||||
? of(null)
|
||||
: inputs.pipe(
|
||||
switchMap(([track]) => track),
|
||||
: inputs$.pipe(
|
||||
switchMap(([track$]) => track$),
|
||||
map((track) => {
|
||||
if (track === null) return null;
|
||||
const facingMode = facingModeFromLocalTrack(track).facingMode;
|
||||
@@ -86,8 +86,8 @@ export function useSwitchCamera(
|
||||
);
|
||||
}),
|
||||
),
|
||||
[videoTrack],
|
||||
[videoTrack$],
|
||||
);
|
||||
|
||||
return useObservableEagerState(switchCamera);
|
||||
return useObservableEagerState(switchCamera$);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user