Skip to content

Commit

Permalink
Merge branch 'livekit' into hughns/connection-stats
Browse files Browse the repository at this point in the history
  • Loading branch information
hughns committed Dec 18, 2024
2 parents 2b5766c + ba5da7e commit 730a9de
Show file tree
Hide file tree
Showing 19 changed files with 335 additions and 70 deletions.
57 changes: 29 additions & 28 deletions docs/url-params.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@
"version": "{{productName}} version: {{version}}",
"video_tile": {
"always_show": "Always show",
"camera_starting": "Video loading...",
"change_fit_contain": "Fit to frame",
"collapse": "Collapse",
"expand": "Expand",
Expand Down
2 changes: 1 addition & 1 deletion src/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const sizes = new Map([
[Size.XL, 90],
]);

interface Props {
export interface Props {
id: string;
name: string;
className?: string;
Expand Down
3 changes: 1 addition & 2 deletions src/Modal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import { expect, test } from "vitest";
import { expect, test, afterEach } from "vitest";
import { render } from "@testing-library/react";
import { type ReactNode, useState } from "react";
import { afterEach } from "node:test";
import userEvent from "@testing-library/user-event";

import { Modal } from "./Modal";
Expand Down
50 changes: 49 additions & 1 deletion src/UrlParams.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ Please see LICENSE in the repository root for full details.

import { describe, expect, it } from "vitest";

import { getRoomIdentifierFromUrl, getUrlParams } from "../src/UrlParams";
import {
getRoomIdentifierFromUrl,
getUrlParams,
UserIntent,
} from "../src/UrlParams";

const ROOM_NAME = "roomNameHere";
const ROOM_ID = "!d45f138fsd";
Expand Down Expand Up @@ -195,4 +199,48 @@ describe("UrlParams", () => {
expect(getUrlParams("?homeserver=asd").homeserver).toBe("asd");
});
});

describe("intent", () => {
it("defaults to unknown", () => {
expect(getUrlParams().intent).toBe(UserIntent.Unknown);
});

it("ignores intent if it is not a valid value", () => {
expect(getUrlParams("?intent=foo").intent).toBe(UserIntent.Unknown);
});

it("accepts start_call", () => {
expect(getUrlParams("?intent=start_call").intent).toBe(
UserIntent.StartNewCall,
);
});

it("accepts join_existing", () => {
expect(getUrlParams("?intent=join_existing").intent).toBe(
UserIntent.JoinExistingCall,
);
});
});

describe("skipLobby", () => {
it("defaults to false", () => {
expect(getUrlParams().skipLobby).toBe(false);
});

it("defaults to false if intent is start_call in SPA mode", () => {
expect(getUrlParams("?intent=start_call").skipLobby).toBe(false);
});

it("defaults to true if intent is start_call in widget mode", () => {
expect(
getUrlParams(
"?intent=start_call&widgetId=12345&parentUrl=https%3A%2F%2Flocalhost%2Ffoo",
).skipLobby,
).toBe(true);
});

it("default to false if intent is join_existing", () => {
expect(getUrlParams("?intent=join_existing").skipLobby).toBe(false);
});
});
});
23 changes: 22 additions & 1 deletion src/UrlParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ interface RoomIdentifier {
viaServers: string[];
}

export enum UserIntent {
StartNewCall = "start_call",
JoinExistingCall = "join_existing",
Unknown = "unknown",
}

// If you need to add a new flag to this interface, prefer a name that describes
// a specific behavior (such as 'confineToRoom'), rather than one that describes
// the situations that call for this behavior ('isEmbedded'). This makes it
Expand Down Expand Up @@ -142,6 +148,13 @@ export interface UrlParams {
* creating a spa link.
*/
homeserver: string | null;

/**
* The user's intent with respect to the call.
* e.g. if they clicked a Start Call button, this would be `start_call`.
* If it was a Join Call button, it would be `join_existing`.
*/
intent: string | null;
}

// This is here as a stopgap, but what would be far nicer is a function that
Expand Down Expand Up @@ -211,6 +224,10 @@ export const getUrlParams = (

const fontScale = parseFloat(parser.getParam("fontScale") ?? "");

let intent = parser.getParam("intent");
if (!intent || !Object.values(UserIntent).includes(intent as UserIntent)) {
intent = UserIntent.Unknown;
}
const widgetId = parser.getParam("widgetId");
const parentUrl = parser.getParam("parentUrl");
const isWidget = !!widgetId && !!parentUrl;
Expand Down Expand Up @@ -243,11 +260,15 @@ export const getUrlParams = (
analyticsID: parser.getParam("analyticsID"),
allowIceFallback: parser.getFlagParam("allowIceFallback"),
perParticipantE2EE: parser.getFlagParam("perParticipantE2EE"),
skipLobby: parser.getFlagParam("skipLobby"),
skipLobby: parser.getFlagParam(
"skipLobby",
isWidget && intent === UserIntent.StartNewCall,
),
returnToLobby: isWidget ? parser.getFlagParam("returnToLobby") : true,
theme: parser.getParam("theme"),
viaServers: !isWidget ? parser.getParam("viaServers") : null,
homeserver: !isWidget ? parser.getParam("homeserver") : null,
intent,
};
};

Expand Down
2 changes: 1 addition & 1 deletion src/room/CallEventAudioRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/room/ReactionAudioRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions src/room/ReactionsOverlay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions src/room/VideoPreview.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ video.mirror {
transform: scaleX(-1);
}

.preview .cameraStarting {
position: absolute;
top: var(--cpd-space-10x);
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
color: var(--cpd-color-text-secondary);
}

.avatarContainer {
position: absolute;
top: 0;
Expand Down
73 changes: 73 additions & 0 deletions src/room/VideoPreview.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import { expect, describe, it, vi, beforeAll } from "vitest";
import { render } from "@testing-library/react";

import { type MatrixInfo, VideoPreview } from "./VideoPreview";
import { type MuteStates } from "./MuteStates";
import { E2eeType } from "../e2ee/e2eeType";

function mockMuteStates({ audio = true, video = true } = {}): MuteStates {
return {
audio: { enabled: audio, setEnabled: vi.fn() },
video: { enabled: video, setEnabled: vi.fn() },
};
}

describe("VideoPreview", () => {
const matrixInfo: MatrixInfo = {
userId: "@a:example.org",
displayName: "Alice",
avatarUrl: "",
roomId: "",
roomName: "",
e2eeSystem: { kind: E2eeType.NONE },
roomAlias: null,
roomAvatar: null,
};

beforeAll(() => {
window.ResizeObserver = class ResizeObserver {
public observe(): void {
// do nothing
}
public unobserve(): void {
// do nothing
}
public disconnect(): void {
// do nothing
}
};
});

it("shows avatar with video disabled", () => {
const { queryByRole } = render(
<VideoPreview
matrixInfo={matrixInfo}
muteStates={mockMuteStates({ video: false })}
videoTrack={null}
children={<></>}
/>,
);
expect(queryByRole("img", { name: "@a:example.org" })).toBeVisible();
});

it("shows loading status with video enabled but no track", () => {
const { queryByRole } = render(
<VideoPreview
matrixInfo={matrixInfo}
muteStates={mockMuteStates({ video: true })}
videoTrack={null}
children={<></>}
/>,
);
expect(queryByRole("status")).toHaveTextContent(
"video_tile.camera_starting",
);
});
});
37 changes: 26 additions & 11 deletions src/room/VideoPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/

import { useEffect, useRef, type FC, type ReactNode } from "react";
import { useEffect, useMemo, useRef, type FC, type ReactNode } from "react";
import useMeasure from "react-use-measure";
import { facingModeFromLocalTrack, type LocalVideoTrack } from "livekit-client";
import classNames from "classnames";
import { useTranslation } from "react-i18next";

import { Avatar } from "../Avatar";
import { TileAvatar } from "../tile/TileAvatar";
import styles from "./VideoPreview.module.css";
import { type MuteStates } from "./MuteStates";
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
Expand Down Expand Up @@ -39,6 +40,7 @@ export const VideoPreview: FC<Props> = ({
videoTrack,
children,
}) => {
const { t } = useTranslation();
const [previewRef, previewBounds] = useMeasure();

const videoEl = useRef<HTMLVideoElement | null>(null);
Expand All @@ -53,6 +55,11 @@ export const VideoPreview: FC<Props> = ({
};
}, [videoTrack]);

const cameraIsStarting = useMemo(
() => muteStates.video.enabled && !videoTrack,
[muteStates.video.enabled, videoTrack],
);

return (
<div className={classNames(styles.preview)} ref={previewRef}>
<video
Expand All @@ -69,15 +76,23 @@ export const VideoPreview: FC<Props> = ({
tabIndex={-1}
disablePictureInPicture
/>
{!muteStates.video.enabled && (
<div className={styles.avatarContainer}>
<Avatar
id={matrixInfo.userId}
name={matrixInfo.displayName}
size={Math.min(previewBounds.width, previewBounds.height) / 2}
src={matrixInfo.avatarUrl}
/>
</div>
{(!muteStates.video.enabled || cameraIsStarting) && (
<>
<div className={styles.avatarContainer}>
{cameraIsStarting && (
<div className={styles.cameraStarting} role="status">
{t("video_tile.camera_starting")}
</div>
)}
<TileAvatar
id={matrixInfo.userId}
name={matrixInfo.displayName}
size={Math.min(previewBounds.width, previewBounds.height) / 2}
src={matrixInfo.avatarUrl}
loading={cameraIsStarting}
/>
</div>
</>
)}
<div className={styles.buttonBar}>{children}</div>
</div>
Expand Down
13 changes: 12 additions & 1 deletion src/rtcSessionHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Please see LICENSE in the repository root for full details.

import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { expect, test, vi } from "vitest";
import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery";

import { enterRTCSession } from "../src/rtcSessionHelpers";
import { mockConfig } from "./utils/test";
Expand Down Expand Up @@ -36,11 +37,21 @@ test("It joins the correct Session", async () => {
mockConfig({
livekit: { livekit_service_url: "http://my-default-service-url.com" },
});

vi.spyOn(AutoDiscovery, "getRawClientConfig").mockImplementation(
async (domain) => {
if (domain === "example.org") {
return Promise.resolve(clientWellKnown);
}
return Promise.resolve({});
},
);

const mockedSession = vi.mocked({
room: {
roomId: "roomId",
client: {
getClientWellKnown: vi.fn().mockReturnValue(clientWellKnown),
getDomain: vi.fn().mockReturnValue("example.org"),
},
},
memberships: [],
Expand Down
Loading

0 comments on commit 730a9de

Please sign in to comment.