Merge branch 'livekit' into toger5/track-processor-blur

This commit is contained in:
Hugh Nimmo-Smith
2024-12-18 09:41:38 +00:00
71 changed files with 1337 additions and 1056 deletions

View File

@@ -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),
);
});

View File

@@ -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 }) =>

View File

@@ -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") {

View File

@@ -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],
),
);

View File

@@ -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", () => {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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$);
}