ErrorHandling: publish connection error handling
This commit is contained in:
@@ -141,8 +141,8 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
|
|||||||
.filter(roomIsJoinable);
|
.filter(roomIsJoinable);
|
||||||
const sortedRooms = sortRooms(client, rooms);
|
const sortedRooms = sortRooms(client, rooms);
|
||||||
Promise.all(
|
Promise.all(
|
||||||
sortedRooms.map(async (room) => {
|
sortedRooms.map((room) => {
|
||||||
const session = await client.matrixRTC.getRoomSession(room);
|
const session = client.matrixRTC.getRoomSession(room);
|
||||||
return {
|
return {
|
||||||
roomAlias: room.getCanonicalAlias() ?? undefined,
|
roomAlias: room.getCanonicalAlias() ?? undefined,
|
||||||
roomName: room.name,
|
roomName: room.name,
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ import { type MuteStates } from "../state/MuteStates";
|
|||||||
import { type MatrixInfo } from "./VideoPreview";
|
import { type MatrixInfo } from "./VideoPreview";
|
||||||
import { InviteButton } from "../button/InviteButton";
|
import { InviteButton } from "../button/InviteButton";
|
||||||
import { LayoutToggle } from "./LayoutToggle";
|
import { LayoutToggle } from "./LayoutToggle";
|
||||||
import { CallViewModel, GridMode } from "../state/CallViewModel";
|
import { CallViewModel, type GridMode } from "../state/CallViewModel";
|
||||||
import { Grid, type TileProps } from "../grid/Grid";
|
import { Grid, type TileProps } from "../grid/Grid";
|
||||||
import { useInitial } from "../useInitial";
|
import { useInitial } from "../useInitial";
|
||||||
import { SpotlightTile } from "../tile/SpotlightTile";
|
import { SpotlightTile } from "../tile/SpotlightTile";
|
||||||
@@ -109,7 +109,7 @@ import { useAudioContext } from "../useAudioContext";
|
|||||||
import ringtoneMp3 from "../sound/ringtone.mp3?url";
|
import ringtoneMp3 from "../sound/ringtone.mp3?url";
|
||||||
import ringtoneOgg from "../sound/ringtone.ogg?url";
|
import ringtoneOgg from "../sound/ringtone.ogg?url";
|
||||||
import { useTrackProcessorObservable$ } from "../livekit/TrackProcessorContext.tsx";
|
import { useTrackProcessorObservable$ } from "../livekit/TrackProcessorContext.tsx";
|
||||||
import { Layout } from "../state/layout-types.ts";
|
import { type Layout } from "../state/layout-types.ts";
|
||||||
|
|
||||||
const maxTapDurationMs = 400;
|
const maxTapDurationMs = 400;
|
||||||
|
|
||||||
@@ -297,6 +297,10 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$);
|
const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$);
|
||||||
const sharingScreen = useBehavior(vm.sharingScreen$);
|
const sharingScreen = useBehavior(vm.sharingScreen$);
|
||||||
|
|
||||||
|
const fatalCallError = useBehavior(vm.configError$);
|
||||||
|
// Stop the rendering and throw for the error boundary
|
||||||
|
if (fatalCallError) throw fatalCallError;
|
||||||
|
|
||||||
// We need to set the proper timings on the animation based upon the sound length.
|
// We need to set the proper timings on the animation based upon the sound length.
|
||||||
const ringDuration = pickupPhaseAudio?.soundDuration["waiting"] ?? 1;
|
const ringDuration = pickupPhaseAudio?.soundDuration["waiting"] ?? 1;
|
||||||
useEffect((): (() => void) => {
|
useEffect((): (() => void) => {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import EventEmitter from "events";
|
|||||||
import { enterRTCSession, leaveRTCSession } from "../src/rtcSessionHelpers";
|
import { enterRTCSession, leaveRTCSession } from "../src/rtcSessionHelpers";
|
||||||
import { mockConfig } from "./utils/test";
|
import { mockConfig } from "./utils/test";
|
||||||
import { ElementWidgetActions, widget } from "./widget";
|
import { ElementWidgetActions, widget } from "./widget";
|
||||||
import { ErrorCode } from "./utils/errors.ts";
|
|
||||||
|
|
||||||
const USE_MUTI_SFU = false;
|
const USE_MUTI_SFU = false;
|
||||||
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
|
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
|
||||||
@@ -97,38 +96,20 @@ test("It joins the correct Session", async () => {
|
|||||||
{
|
{
|
||||||
encryptMedia: true,
|
encryptMedia: true,
|
||||||
useMultiSfu: USE_MUTI_SFU,
|
useMultiSfu: USE_MUTI_SFU,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockedSession.joinRoomSession).toHaveBeenLastCalledWith(
|
expect(mockedSession.joinRoomSession).toHaveBeenLastCalledWith(
|
||||||
[
|
[
|
||||||
{
|
|
||||||
livekit_alias: "my-oldest-member-service-alias",
|
|
||||||
livekit_service_url: "http://my-oldest-member-service-url.com",
|
|
||||||
type: "livekit",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
livekit_alias: "roomId",
|
livekit_alias: "roomId",
|
||||||
livekit_service_url: "http://my-well-known-service-url.com",
|
livekit_service_url: "http://my-well-known-service-url.com",
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
livekit_alias: "roomId",
|
|
||||||
livekit_service_url: "http://my-well-known-service-url2.com",
|
|
||||||
type: "livekit",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
livekit_alias: "roomId",
|
|
||||||
livekit_service_url: "http://my-default-service-url.com",
|
|
||||||
type: "livekit",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
{
|
undefined,
|
||||||
focus_selection: "oldest_membership",
|
|
||||||
type: "livekit",
|
|
||||||
},
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
manageMediaKeys: false,
|
manageMediaKeys: true,
|
||||||
useLegacyMemberEvents: false,
|
useLegacyMemberEvents: false,
|
||||||
useNewMembershipManager: true,
|
useNewMembershipManager: true,
|
||||||
useExperimentalToDeviceTransport: false,
|
useExperimentalToDeviceTransport: false,
|
||||||
@@ -177,40 +158,6 @@ test("leaveRTCSession doesn't close the widget when returning to lobby", async (
|
|||||||
await testLeaveRTCSession("user", false);
|
await testLeaveRTCSession("user", false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("It fails with configuration error if no live kit url config is set in fallback", async () => {
|
|
||||||
mockConfig({});
|
|
||||||
vi.spyOn(AutoDiscovery, "getRawClientConfig").mockResolvedValue({});
|
|
||||||
|
|
||||||
const mockedSession = vi.mocked({
|
|
||||||
room: {
|
|
||||||
roomId: "roomId",
|
|
||||||
client: {
|
|
||||||
getDomain: vi.fn().mockReturnValue("example.org"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
memberships: [],
|
|
||||||
getFocusInUse: vi.fn(),
|
|
||||||
joinRoomSession: vi.fn(),
|
|
||||||
}) as unknown as MatrixRTCSession;
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
enterRTCSession(
|
|
||||||
mockedSession,
|
|
||||||
{
|
|
||||||
livekit_alias: "roomId",
|
|
||||||
livekit_service_url: "http://my-well-known-service-url.com",
|
|
||||||
type: "livekit",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
encryptMedia: true,
|
|
||||||
useMultiSfu: USE_MUTI_SFU,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
).rejects.toThrowError(
|
|
||||||
expect.objectContaining({ code: ErrorCode.MISSING_MATRIX_RTC_TRANSPORT }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("It should not fail with configuration error if homeserver config has livekit url but not fallback", async () => {
|
test("It should not fail with configuration error if homeserver config has livekit url but not fallback", async () => {
|
||||||
mockConfig({});
|
mockConfig({});
|
||||||
vi.spyOn(AutoDiscovery, "getRawClientConfig").mockResolvedValue({
|
vi.spyOn(AutoDiscovery, "getRawClientConfig").mockResolvedValue({
|
||||||
@@ -250,6 +197,6 @@ test("It should not fail with configuration error if homeserver config has livek
|
|||||||
{
|
{
|
||||||
encryptMedia: true,
|
encryptMedia: true,
|
||||||
useMultiSfu: USE_MUTI_SFU,
|
useMultiSfu: USE_MUTI_SFU,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -123,7 +123,6 @@ export async function enterRTCSession(
|
|||||||
useMultiSfu: true,
|
useMultiSfu: true,
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
encryptMedia,
|
encryptMedia,
|
||||||
useNewMembershipManager = true,
|
useNewMembershipManager = true,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, vi, onTestFinished, it, describe } from "vitest";
|
import { test, vi, onTestFinished, it, describe, expect } from "vitest";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
@@ -45,6 +45,7 @@ import {
|
|||||||
MatrixRTCSessionEvent,
|
MatrixRTCSessionEvent,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { deepCompare } from "matrix-js-sdk/lib/utils";
|
import { deepCompare } from "matrix-js-sdk/lib/utils";
|
||||||
|
import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery";
|
||||||
|
|
||||||
import { CallViewModel, type CallViewModelOptions } from "./CallViewModel";
|
import { CallViewModel, type CallViewModelOptions } from "./CallViewModel";
|
||||||
import { type Layout } from "./layout-types";
|
import { type Layout } from "./layout-types";
|
||||||
@@ -58,6 +59,7 @@ import {
|
|||||||
MockRTCSession,
|
MockRTCSession,
|
||||||
mockMediaDevices,
|
mockMediaDevices,
|
||||||
mockMuteStates,
|
mockMuteStates,
|
||||||
|
mockConfig,
|
||||||
} from "../utils/test";
|
} from "../utils/test";
|
||||||
import {
|
import {
|
||||||
ECAddonConnectionState,
|
ECAddonConnectionState,
|
||||||
@@ -92,6 +94,10 @@ import { MediaDevices } from "./MediaDevices";
|
|||||||
import { getValue } from "../utils/observable";
|
import { getValue } from "../utils/observable";
|
||||||
import { type Behavior, constant } from "./Behavior";
|
import { type Behavior, constant } from "./Behavior";
|
||||||
import type { ProcessorState } from "../livekit/TrackProcessorContext.tsx";
|
import type { ProcessorState } from "../livekit/TrackProcessorContext.tsx";
|
||||||
|
import {
|
||||||
|
type ElementCallError,
|
||||||
|
MatrixRTCTransportMissingError,
|
||||||
|
} from "../utils/errors.ts";
|
||||||
|
|
||||||
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
|
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
|
||||||
vi.mock("../UrlParams", () => ({ getUrlParams }));
|
vi.mock("../UrlParams", () => ({ getUrlParams }));
|
||||||
@@ -365,6 +371,61 @@ function withCallViewModel(
|
|||||||
continuation(vm, rtcSession, { raisedHands$: raisedHands$ }, setSyncState);
|
continuation(vm, rtcSession, { raisedHands$: raisedHands$ }, setSyncState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("test missing RTC config error", async () => {
|
||||||
|
const rtcMemberships$ = new BehaviorSubject<CallMembership[]>([]);
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
const client = vi.mocked<MatrixClient>({
|
||||||
|
on: emitter.on.bind(emitter),
|
||||||
|
off: emitter.off.bind(emitter),
|
||||||
|
getSyncState: vi.fn().mockReturnValue(SyncState.Syncing),
|
||||||
|
getUserId: vi.fn().mockReturnValue("@user:localhost"),
|
||||||
|
getUser: vi.fn().mockReturnValue(null),
|
||||||
|
getDeviceId: vi.fn().mockReturnValue("DEVICE"),
|
||||||
|
credentials: {
|
||||||
|
userId: "@user:localhost",
|
||||||
|
},
|
||||||
|
getCrypto: vi.fn().mockReturnValue(undefined),
|
||||||
|
getDomain: vi.fn().mockReturnValue("example.org"),
|
||||||
|
} as unknown as MatrixClient);
|
||||||
|
|
||||||
|
const matrixRoom = mockMatrixRoom({
|
||||||
|
roomId: "!myRoomId:example.com",
|
||||||
|
client,
|
||||||
|
getMember: vi.fn().mockReturnValue(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
const fakeRtcSession = new MockRTCSession(matrixRoom).withMemberships(
|
||||||
|
rtcMemberships$,
|
||||||
|
);
|
||||||
|
|
||||||
|
mockConfig({});
|
||||||
|
vi.spyOn(AutoDiscovery, "getRawClientConfig").mockResolvedValue({});
|
||||||
|
|
||||||
|
const callVM = new CallViewModel(
|
||||||
|
fakeRtcSession.asMockedSession(),
|
||||||
|
matrixRoom,
|
||||||
|
mockMediaDevices({}),
|
||||||
|
mockMuteStates(),
|
||||||
|
{
|
||||||
|
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
|
||||||
|
autoLeaveWhenOthersLeft: false,
|
||||||
|
},
|
||||||
|
new BehaviorSubject({} as Record<string, RaisedHandInfo>),
|
||||||
|
new BehaviorSubject({} as Record<string, ReactionInfo>),
|
||||||
|
of({ processor: undefined, supported: false }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const failPromise = Promise.withResolvers<ElementCallError>();
|
||||||
|
callVM.configError$.subscribe((error) => {
|
||||||
|
if (error) {
|
||||||
|
failPromise.resolve(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = await failPromise.promise;
|
||||||
|
expect(error).toBeInstanceOf(MatrixRTCTransportMissingError);
|
||||||
|
});
|
||||||
|
|
||||||
test("participants are retained during a focus switch", () => {
|
test("participants are retained during a focus switch", () => {
|
||||||
withTestScheduler(({ behavior, expectObservable }) => {
|
withTestScheduler(({ behavior, expectObservable }) => {
|
||||||
// Participants disappear on frame 2 and come back on frame 3
|
// Participants disappear on frame 2 and come back on frame 3
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
} from "matrix-js-sdk";
|
} from "matrix-js-sdk";
|
||||||
import { deepCompare } from "matrix-js-sdk/lib/utils";
|
import { deepCompare } from "matrix-js-sdk/lib/utils";
|
||||||
import {
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
concat,
|
concat,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
@@ -76,7 +77,7 @@ import { ViewModel } from "./ViewModel";
|
|||||||
import {
|
import {
|
||||||
LocalUserMediaViewModel,
|
LocalUserMediaViewModel,
|
||||||
type MediaViewModel,
|
type MediaViewModel,
|
||||||
RemoteUserMediaViewModel,
|
type RemoteUserMediaViewModel,
|
||||||
ScreenShareViewModel,
|
ScreenShareViewModel,
|
||||||
type UserMediaViewModel,
|
type UserMediaViewModel,
|
||||||
} from "./MediaViewModel";
|
} from "./MediaViewModel";
|
||||||
@@ -130,14 +131,15 @@ import { type Async, async$, mapAsync, ready } from "./Async";
|
|||||||
import { sharingScreen$, UserMedia } from "./UserMedia.ts";
|
import { sharingScreen$, UserMedia } from "./UserMedia.ts";
|
||||||
import { ScreenShare } from "./ScreenShare.ts";
|
import { ScreenShare } from "./ScreenShare.ts";
|
||||||
import {
|
import {
|
||||||
GridLayoutMedia,
|
type GridLayoutMedia,
|
||||||
Layout,
|
type Layout,
|
||||||
LayoutMedia,
|
type LayoutMedia,
|
||||||
OneOnOneLayoutMedia,
|
type OneOnOneLayoutMedia,
|
||||||
SpotlightExpandedLayoutMedia,
|
type SpotlightExpandedLayoutMedia,
|
||||||
SpotlightLandscapeLayoutMedia,
|
type SpotlightLandscapeLayoutMedia,
|
||||||
SpotlightPortraitLayoutMedia,
|
type SpotlightPortraitLayoutMedia,
|
||||||
} from "./layout-types.ts";
|
} from "./layout-types.ts";
|
||||||
|
import { ElementCallError, UnknownCallError } from "../utils/errors.ts";
|
||||||
|
|
||||||
export interface CallViewModelOptions {
|
export interface CallViewModelOptions {
|
||||||
encryptionSystem: EncryptionSystem;
|
encryptionSystem: EncryptionSystem;
|
||||||
@@ -224,6 +226,19 @@ export class CallViewModel extends ViewModel {
|
|||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
private readonly _configError$ = new BehaviorSubject<ElementCallError | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there is a configuration error with the call (e.g. misconfigured E2EE).
|
||||||
|
* This is a fatal error that prevents the call from being created/joined.
|
||||||
|
* Should render a blocking error screen.
|
||||||
|
*/
|
||||||
|
public get configError$(): Behavior<ElementCallError | null> {
|
||||||
|
return this._configError$;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly join$ = new Subject<void>();
|
private readonly join$ = new Subject<void>();
|
||||||
|
|
||||||
public join(): void {
|
public join(): void {
|
||||||
@@ -273,7 +288,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
* The transport that we would personally prefer to publish on (if not for the
|
* The transport that we would personally prefer to publish on (if not for the
|
||||||
* transport preferences of others, perhaps).
|
* transport preferences of others, perhaps).
|
||||||
*/
|
*/
|
||||||
private readonly preferredTransport = makeTransport(this.matrixRTCSession);
|
private readonly preferredTransport$: Observable<Async<LivekitTransport>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists the transports used by ourselves, plus all other MatrixRTC session
|
* Lists the transports used by ourselves, plus all other MatrixRTC session
|
||||||
@@ -287,11 +302,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
switchMap((joined) =>
|
switchMap((joined) =>
|
||||||
joined
|
joined
|
||||||
? combineLatest(
|
? combineLatest(
|
||||||
[
|
[this.preferredTransport$, this.memberships$, multiSfu.value$],
|
||||||
async$(this.preferredTransport),
|
|
||||||
this.memberships$,
|
|
||||||
multiSfu.value$,
|
|
||||||
],
|
|
||||||
(preferred, memberships, multiSfu) => {
|
(preferred, memberships, multiSfu) => {
|
||||||
const oldestMembership =
|
const oldestMembership =
|
||||||
this.matrixRTCSession.getOldestMembership();
|
this.matrixRTCSession.getOldestMembership();
|
||||||
@@ -313,6 +324,13 @@ export class CallViewModel extends ViewModel {
|
|||||||
local = ready(selection);
|
local = ready(selection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (local.state === "error") {
|
||||||
|
this._configError$.next(
|
||||||
|
local.value instanceof ElementCallError
|
||||||
|
? local.value
|
||||||
|
: new UnknownCallError(local.value),
|
||||||
|
);
|
||||||
|
}
|
||||||
return { local, remote };
|
return { local, remote };
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -1743,6 +1761,10 @@ export class CallViewModel extends ViewModel {
|
|||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.preferredTransport$ = async$(
|
||||||
|
makeTransport(this.matrixRTCSession),
|
||||||
|
).pipe(this.scope.bind());
|
||||||
|
|
||||||
// Start and stop local and remote connections as needed
|
// Start and stop local and remote connections as needed
|
||||||
this.connectionInstructions$
|
this.connectionInstructions$
|
||||||
.pipe(this.scope.bind())
|
.pipe(this.scope.bind())
|
||||||
@@ -1765,11 +1787,21 @@ export class CallViewModel extends ViewModel {
|
|||||||
logger.info(
|
logger.info(
|
||||||
`Connected to ${c.localTransport.livekit_service_url}`,
|
`Connected to ${c.localTransport.livekit_service_url}`,
|
||||||
),
|
),
|
||||||
(e) =>
|
(e) => {
|
||||||
|
// We only want to report fatal errors `_configError$` for the publish connection.
|
||||||
|
// If there is an error with another connection, it will not terminate the call and will be displayed
|
||||||
|
// on eacn tile.
|
||||||
|
if (
|
||||||
|
c instanceof PublishConnection &&
|
||||||
|
e instanceof ElementCallError
|
||||||
|
) {
|
||||||
|
this._configError$.next(e);
|
||||||
|
}
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to start connection to ${c.localTransport.livekit_service_url}`,
|
`Failed to start connection to ${c.localTransport.livekit_service_url}`,
|
||||||
e,
|
e,
|
||||||
),
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1778,6 +1810,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
this.scope.reconcile(this.localTransport$, async (localTransport) => {
|
this.scope.reconcile(this.localTransport$, async (localTransport) => {
|
||||||
if (localTransport?.state === "ready") {
|
if (localTransport?.state === "ready") {
|
||||||
try {
|
try {
|
||||||
|
this._configError$.next(null);
|
||||||
await enterRTCSession(this.matrixRTCSession, localTransport.value, {
|
await enterRTCSession(this.matrixRTCSession, localTransport.value, {
|
||||||
encryptMedia: this.options.encryptionSystem.kind !== E2eeType.NONE,
|
encryptMedia: this.options.encryptionSystem.kind !== E2eeType.NONE,
|
||||||
useExperimentalToDeviceTransport: true,
|
useExperimentalToDeviceTransport: true,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
connectionStateObserver,
|
connectionStateObserver,
|
||||||
} from "@livekit/components-core";
|
} from "@livekit/components-core";
|
||||||
import {
|
import {
|
||||||
|
ConnectionError,
|
||||||
type ConnectionState,
|
type ConnectionState,
|
||||||
type E2EEOptions,
|
type E2EEOptions,
|
||||||
Room as LivekitRoom,
|
Room as LivekitRoom,
|
||||||
@@ -29,6 +30,10 @@ import {
|
|||||||
import { type Behavior } from "./Behavior";
|
import { type Behavior } from "./Behavior";
|
||||||
import { type ObservableScope } from "./ObservableScope";
|
import { type ObservableScope } from "./ObservableScope";
|
||||||
import { defaultLiveKitOptions } from "../livekit/options";
|
import { defaultLiveKitOptions } from "../livekit/options";
|
||||||
|
import {
|
||||||
|
InsufficientCapacityError,
|
||||||
|
SFURoomCreationRestrictedError,
|
||||||
|
} from "../utils/errors.ts";
|
||||||
|
|
||||||
export interface ConnectionOpts {
|
export interface ConnectionOpts {
|
||||||
/** The focus server to connect to. */
|
/** The focus server to connect to. */
|
||||||
@@ -88,6 +93,9 @@ export class Connection {
|
|||||||
* 1. Request an OpenId token `request_token` (allows matrix users to verify their identity with a third-party service.)
|
* 1. Request an OpenId token `request_token` (allows matrix users to verify their identity with a third-party service.)
|
||||||
* 2. Use this token to request the SFU config to the MatrixRtc authentication service.
|
* 2. Use this token to request the SFU config to the MatrixRtc authentication service.
|
||||||
* 3. Connect to the configured LiveKit room.
|
* 3. Connect to the configured LiveKit room.
|
||||||
|
*
|
||||||
|
* @throws {InsufficientCapacityError} if the LiveKit server indicates that it has insufficient capacity to accept the connection.
|
||||||
|
* @throws {SFURoomCreationRestrictedError} if the LiveKit server indicates that the room does not exist and cannot be created.
|
||||||
*/
|
*/
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
this.stopped = false;
|
this.stopped = false;
|
||||||
@@ -105,7 +113,30 @@ export class Connection {
|
|||||||
state: "ConnectingToLkRoom",
|
state: "ConnectingToLkRoom",
|
||||||
focus: this.localTransport,
|
focus: this.localTransport,
|
||||||
});
|
});
|
||||||
await this.livekitRoom.connect(url, jwt);
|
try {
|
||||||
|
await this.livekitRoom.connect(url, jwt);
|
||||||
|
} catch (e) {
|
||||||
|
// LiveKit uses 503 to indicate that the server has hit its track limits.
|
||||||
|
// https://github.com/livekit/livekit/blob/fcb05e97c5a31812ecf0ca6f7efa57c485cea9fb/pkg/service/rtcservice.go#L171
|
||||||
|
// It also errors with a status code of 200 (yes, really) for room
|
||||||
|
// participant limits.
|
||||||
|
// LiveKit Cloud uses 429 for connection limits.
|
||||||
|
// Either way, all these errors can be explained as "insufficient capacity".
|
||||||
|
if (e instanceof ConnectionError) {
|
||||||
|
if (e.status === 503 || e.status === 200 || e.status === 429) {
|
||||||
|
throw new InsufficientCapacityError();
|
||||||
|
}
|
||||||
|
if (e.status === 404) {
|
||||||
|
// error msg is "Could not establish signal connection: requested room does not exist"
|
||||||
|
// The room does not exist. There are two different modes of operation for the SFU:
|
||||||
|
// - the room is created on the fly when connecting (livekit `auto_create` option)
|
||||||
|
// - Only authorized users can create rooms, so the room must exist before connecting (done by the auth jwt service)
|
||||||
|
// In the first case there will not be a 404, so we are in the second case.
|
||||||
|
throw new SFURoomCreationRestrictedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
// If we were stopped while connecting, don't proceed to update state.
|
// If we were stopped while connecting, don't proceed to update state.
|
||||||
if (this.stopped) return;
|
if (this.stopped) return;
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ export class PublishConnection extends Connection {
|
|||||||
* 2. Use this token to request the SFU config to the MatrixRtc authentication service.
|
* 2. Use this token to request the SFU config to the MatrixRtc authentication service.
|
||||||
* 3. Connect to the configured LiveKit room.
|
* 3. Connect to the configured LiveKit room.
|
||||||
* 4. Create local audio and video tracks based on the current mute states and publish them to the room.
|
* 4. Create local audio and video tracks based on the current mute states and publish them to the room.
|
||||||
|
*
|
||||||
|
* @throws {InsufficientCapacityError} if the LiveKit server indicates that it has insufficient capacity to accept the connection.
|
||||||
|
* @throws {SFURoomCreationRestrictedError} if the LiveKit server indicates that the room does not exist and cannot be created.
|
||||||
*/
|
*/
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
this.stopped = false;
|
this.stopped = false;
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ Copyright 2025 New Vector Ltd.
|
|||||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
import { ObservableScope } from "./ObservableScope.ts";
|
|
||||||
import { ScreenShareViewModel } from "./MediaViewModel.ts";
|
|
||||||
import { BehaviorSubject, type Observable } from "rxjs";
|
import { BehaviorSubject, type Observable } from "rxjs";
|
||||||
import {
|
import {
|
||||||
LocalParticipant,
|
type LocalParticipant,
|
||||||
RemoteParticipant,
|
type RemoteParticipant,
|
||||||
type Room as LivekitRoom,
|
type Room as LivekitRoom,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
|
|
||||||
|
import { ObservableScope } from "./ObservableScope.ts";
|
||||||
|
import { ScreenShareViewModel } from "./MediaViewModel.ts";
|
||||||
import type { RoomMember } from "matrix-js-sdk";
|
import type { RoomMember } from "matrix-js-sdk";
|
||||||
import type { EncryptionSystem } from "../e2ee/sharedKeyManagement.ts";
|
import type { EncryptionSystem } from "../e2ee/sharedKeyManagement.ts";
|
||||||
import type { Behavior } from "./Behavior.ts";
|
import type { Behavior } from "./Behavior.ts";
|
||||||
|
|||||||
@@ -5,27 +5,28 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { BehaviorSubject, map, type Observable, of, switchMap } from "rxjs";
|
||||||
|
import {
|
||||||
|
type LocalParticipant,
|
||||||
|
type Participant,
|
||||||
|
ParticipantEvent,
|
||||||
|
type RemoteParticipant,
|
||||||
|
type Room as LivekitRoom,
|
||||||
|
} from "livekit-client";
|
||||||
|
import { observeParticipantEvents } from "@livekit/components-core";
|
||||||
|
|
||||||
import { ObservableScope } from "./ObservableScope.ts";
|
import { ObservableScope } from "./ObservableScope.ts";
|
||||||
import {
|
import {
|
||||||
LocalUserMediaViewModel,
|
LocalUserMediaViewModel,
|
||||||
RemoteUserMediaViewModel,
|
RemoteUserMediaViewModel,
|
||||||
UserMediaViewModel,
|
type UserMediaViewModel,
|
||||||
} from "./MediaViewModel.ts";
|
} from "./MediaViewModel.ts";
|
||||||
import { BehaviorSubject, map, type Observable, of, switchMap } from "rxjs";
|
|
||||||
import {
|
|
||||||
LocalParticipant,
|
|
||||||
Participant,
|
|
||||||
ParticipantEvent,
|
|
||||||
RemoteParticipant,
|
|
||||||
type Room as LivekitRoom,
|
|
||||||
} from "livekit-client";
|
|
||||||
import type { Behavior } from "./Behavior.ts";
|
import type { Behavior } from "./Behavior.ts";
|
||||||
import type { RoomMember } from "matrix-js-sdk";
|
import type { RoomMember } from "matrix-js-sdk";
|
||||||
import type { EncryptionSystem } from "../e2ee/sharedKeyManagement.ts";
|
import type { EncryptionSystem } from "../e2ee/sharedKeyManagement.ts";
|
||||||
import type { MediaDevices } from "./MediaDevices.ts";
|
import type { MediaDevices } from "./MediaDevices.ts";
|
||||||
import type { ReactionOption } from "../reactions";
|
import type { ReactionOption } from "../reactions";
|
||||||
import { observeSpeaker$ } from "./observeSpeaker.ts";
|
import { observeSpeaker$ } from "./observeSpeaker.ts";
|
||||||
import { observeParticipantEvents } from "@livekit/components-core";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO Document this
|
* TODO Document this
|
||||||
|
|||||||
@@ -5,8 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { GridTileViewModel, SpotlightTileViewModel } from "./TileViewModel.ts";
|
import {
|
||||||
import { MediaViewModel, UserMediaViewModel } from "./MediaViewModel.ts";
|
type GridTileViewModel,
|
||||||
|
type SpotlightTileViewModel,
|
||||||
|
} from "./TileViewModel.ts";
|
||||||
|
import {
|
||||||
|
type MediaViewModel,
|
||||||
|
type UserMediaViewModel,
|
||||||
|
} from "./MediaViewModel.ts";
|
||||||
|
|
||||||
export interface GridLayoutMedia {
|
export interface GridLayoutMedia {
|
||||||
type: "grid";
|
type: "grid";
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export function mockRtcMembership(
|
|||||||
content: data,
|
content: data,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cms = new CallMembership(event);
|
const cms = new CallMembership(event, data);
|
||||||
vi.mocked(cms).getTransport = vi.fn().mockReturnValue(fociPreferred[0]);
|
vi.mocked(cms).getTransport = vi.fn().mockReturnValue(fociPreferred[0]);
|
||||||
return cms;
|
return cms;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user