prettier !
This commit is contained in:
@@ -24,10 +24,9 @@ import {
|
|||||||
mockMatrixRoomMember,
|
mockMatrixRoomMember,
|
||||||
mockMediaDevices,
|
mockMediaDevices,
|
||||||
mockRtcMembership,
|
mockRtcMembership,
|
||||||
mockTrack
|
mockTrack,
|
||||||
} from "../utils/test";
|
} from "../utils/test";
|
||||||
|
|
||||||
|
|
||||||
export const TestAudioContextConstructor = vi.fn(() => testAudioContext);
|
export const TestAudioContextConstructor = vi.fn(() => testAudioContext);
|
||||||
|
|
||||||
const MediaDevicesProvider = MediaDevicesContext.MediaDevicesContext.Provider;
|
const MediaDevicesProvider = MediaDevicesContext.MediaDevicesContext.Provider;
|
||||||
@@ -65,8 +64,8 @@ it("should render for member", () => {
|
|||||||
const p = {
|
const p = {
|
||||||
id: "test:123",
|
id: "test:123",
|
||||||
participant: undefined,
|
participant: undefined,
|
||||||
member: carol
|
member: carol,
|
||||||
}
|
};
|
||||||
const livekitRoom = mockLivekitRoom(
|
const livekitRoom = mockLivekitRoom(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
@@ -95,8 +94,8 @@ it("should not render without member", () => {
|
|||||||
const p = {
|
const p = {
|
||||||
id: "test:123",
|
id: "test:123",
|
||||||
participant: undefined,
|
participant: undefined,
|
||||||
member: carol
|
member: carol,
|
||||||
}
|
};
|
||||||
const livekitRoom = mockLivekitRoom(
|
const livekitRoom = mockLivekitRoom(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
@@ -122,8 +121,8 @@ it("should not setup audioContext gain and pan if there is no need to.", () => {
|
|||||||
const p = {
|
const p = {
|
||||||
id: "test:123",
|
id: "test:123",
|
||||||
participant: undefined,
|
participant: undefined,
|
||||||
member: carol
|
member: carol,
|
||||||
}
|
};
|
||||||
const livekitRoom = mockLivekitRoom(
|
const livekitRoom = mockLivekitRoom(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
@@ -160,8 +159,8 @@ it("should setup audioContext gain and pan", () => {
|
|||||||
const p = {
|
const p = {
|
||||||
id: "test:123",
|
id: "test:123",
|
||||||
participant: undefined,
|
participant: undefined,
|
||||||
member: carol
|
member: carol,
|
||||||
}
|
};
|
||||||
const livekitRoom = mockLivekitRoom(
|
const livekitRoom = mockLivekitRoom(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
@@ -173,7 +172,8 @@ it("should setup audioContext gain and pan", () => {
|
|||||||
<LivekitRoomAudioRenderer
|
<LivekitRoomAudioRenderer
|
||||||
participants={[p]}
|
participants={[p]}
|
||||||
url={""}
|
url={""}
|
||||||
livekitRoom={livekitRoom} />
|
livekitRoom={livekitRoom}
|
||||||
|
/>
|
||||||
</MediaDevicesProvider>,
|
</MediaDevicesProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ function createGroupCallView(
|
|||||||
muteStates={muteState}
|
muteStates={muteState}
|
||||||
widget={widget}
|
widget={widget}
|
||||||
joined={true}
|
joined={true}
|
||||||
setJoined={function(value: boolean): void { }}
|
setJoined={function (value: boolean): void {}}
|
||||||
/>
|
/>
|
||||||
</ProcessorProvider>
|
</ProcessorProvider>
|
||||||
</MediaDevicesContext>
|
</MediaDevicesContext>
|
||||||
|
|||||||
@@ -116,20 +116,22 @@ export const RoomPage: FC = () => {
|
|||||||
const groupCallView = (): ReactNode => {
|
const groupCallView = (): ReactNode => {
|
||||||
switch (groupCallState.kind) {
|
switch (groupCallState.kind) {
|
||||||
case "loaded":
|
case "loaded":
|
||||||
return muteStates && (
|
return (
|
||||||
<GroupCallView
|
muteStates && (
|
||||||
widget={widget}
|
<GroupCallView
|
||||||
client={client!}
|
widget={widget}
|
||||||
rtcSession={groupCallState.rtcSession}
|
client={client!}
|
||||||
joined={joined}
|
rtcSession={groupCallState.rtcSession}
|
||||||
setJoined={setJoined}
|
joined={joined}
|
||||||
isPasswordlessUser={passwordlessUser}
|
setJoined={setJoined}
|
||||||
confineToRoom={confineToRoom}
|
isPasswordlessUser={passwordlessUser}
|
||||||
preload={preload}
|
confineToRoom={confineToRoom}
|
||||||
skipLobby={skipLobby || wasInWaitForInviteState.current}
|
preload={preload}
|
||||||
header={header}
|
skipLobby={skipLobby || wasInWaitForInviteState.current}
|
||||||
muteStates={muteStates}
|
header={header}
|
||||||
/>
|
muteStates={muteStates}
|
||||||
|
/>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
case "waitForInvite":
|
case "waitForInvite":
|
||||||
case "canKnock": {
|
case "canKnock": {
|
||||||
@@ -148,31 +150,35 @@ export const RoomPage: FC = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
muteStates && <LobbyView
|
muteStates && (
|
||||||
client={client!}
|
<LobbyView
|
||||||
matrixInfo={{
|
client={client!}
|
||||||
userId: client!.getUserId() ?? "",
|
matrixInfo={{
|
||||||
displayName: userDisplayName ?? "",
|
userId: client!.getUserId() ?? "",
|
||||||
avatarUrl: avatarUrl ?? "",
|
displayName: userDisplayName ?? "",
|
||||||
roomAlias: null,
|
avatarUrl: avatarUrl ?? "",
|
||||||
roomId: groupCallState.roomSummary.room_id,
|
roomAlias: null,
|
||||||
roomName: groupCallState.roomSummary.name ?? "",
|
roomId: groupCallState.roomSummary.room_id,
|
||||||
roomAvatar: groupCallState.roomSummary.avatar_url ?? null,
|
roomName: groupCallState.roomSummary.name ?? "",
|
||||||
e2eeSystem: {
|
roomAvatar: groupCallState.roomSummary.avatar_url ?? null,
|
||||||
kind: groupCallState.roomSummary["im.nheko.summary.encryption"]
|
e2eeSystem: {
|
||||||
? E2eeType.PER_PARTICIPANT
|
kind: groupCallState.roomSummary[
|
||||||
: E2eeType.NONE,
|
"im.nheko.summary.encryption"
|
||||||
},
|
]
|
||||||
}}
|
? E2eeType.PER_PARTICIPANT
|
||||||
onEnter={(): void => knock?.()}
|
: E2eeType.NONE,
|
||||||
enterLabel={label}
|
},
|
||||||
waitingForInvite={groupCallState.kind === "waitForInvite"}
|
}}
|
||||||
confineToRoom={confineToRoom}
|
onEnter={(): void => knock?.()}
|
||||||
hideHeader={header !== "standard"}
|
enterLabel={label}
|
||||||
participantCount={null}
|
waitingForInvite={groupCallState.kind === "waitForInvite"}
|
||||||
muteStates={muteStates}
|
confineToRoom={confineToRoom}
|
||||||
onShareClick={null}
|
hideHeader={header !== "standard"}
|
||||||
/>
|
participantCount={null}
|
||||||
|
muteStates={muteStates}
|
||||||
|
onShareClick={null}
|
||||||
|
/>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case "loading":
|
case "loading":
|
||||||
|
|||||||
@@ -23,38 +23,37 @@ vi.mock("./widget", () => ({
|
|||||||
...actualWidget,
|
...actualWidget,
|
||||||
widget: {
|
widget: {
|
||||||
api: {
|
api: {
|
||||||
setAlwaysOnScreen: (): void => {
|
setAlwaysOnScreen: (): void => {},
|
||||||
},
|
transport: { send: vi.fn(), reply: vi.fn(), stop: vi.fn() },
|
||||||
transport: { send: vi.fn(), reply: vi.fn(), stop: vi.fn() }
|
|
||||||
},
|
},
|
||||||
lazyActions: new EventEmitter()
|
lazyActions: new EventEmitter(),
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
test("It joins the correct Session", async () => {
|
test("It joins the correct Session", async () => {
|
||||||
const focusFromOlderMembership = {
|
const focusFromOlderMembership = {
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
livekit_service_url: "http://my-oldest-member-service-url.com",
|
livekit_service_url: "http://my-oldest-member-service-url.com",
|
||||||
livekit_alias: "my-oldest-member-service-alias"
|
livekit_alias: "my-oldest-member-service-alias",
|
||||||
};
|
};
|
||||||
|
|
||||||
const focusConfigFromWellKnown = {
|
const focusConfigFromWellKnown = {
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
livekit_service_url: "http://my-well-known-service-url.com"
|
livekit_service_url: "http://my-well-known-service-url.com",
|
||||||
};
|
};
|
||||||
const focusConfigFromWellKnown2 = {
|
const focusConfigFromWellKnown2 = {
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
livekit_service_url: "http://my-well-known-service-url2.com"
|
livekit_service_url: "http://my-well-known-service-url2.com",
|
||||||
};
|
};
|
||||||
const clientWellKnown = {
|
const clientWellKnown = {
|
||||||
"org.matrix.msc4143.rtc_foci": [
|
"org.matrix.msc4143.rtc_foci": [
|
||||||
focusConfigFromWellKnown,
|
focusConfigFromWellKnown,
|
||||||
focusConfigFromWellKnown2
|
focusConfigFromWellKnown2,
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
mockConfig({
|
mockConfig({
|
||||||
livekit: { livekit_service_url: "http://my-default-service-url.com" }
|
livekit: { livekit_service_url: "http://my-default-service-url.com" },
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.spyOn(AutoDiscovery, "getRawClientConfig").mockImplementation(
|
vi.spyOn(AutoDiscovery, "getRawClientConfig").mockImplementation(
|
||||||
@@ -63,7 +62,7 @@ test("It joins the correct Session", async () => {
|
|||||||
return Promise.resolve(clientWellKnown);
|
return Promise.resolve(clientWellKnown);
|
||||||
}
|
}
|
||||||
return Promise.resolve({});
|
return Promise.resolve({});
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const mockedSession = vi.mocked({
|
const mockedSession = vi.mocked({
|
||||||
@@ -75,64 +74,67 @@ test("It joins the correct Session", async () => {
|
|||||||
access_token: "ACCCESS_TOKEN",
|
access_token: "ACCCESS_TOKEN",
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
matrix_server_name: "localhost",
|
matrix_server_name: "localhost",
|
||||||
expires_in: 10000
|
expires_in: 10000,
|
||||||
})
|
}),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
memberships: [],
|
memberships: [],
|
||||||
getFocusInUse: vi.fn().mockReturnValue(focusFromOlderMembership),
|
getFocusInUse: vi.fn().mockReturnValue(focusFromOlderMembership),
|
||||||
getOldestMembership: vi.fn().mockReturnValue({
|
getOldestMembership: vi.fn().mockReturnValue({
|
||||||
getPreferredFoci: vi.fn().mockReturnValue([focusFromOlderMembership])
|
getPreferredFoci: vi.fn().mockReturnValue([focusFromOlderMembership]),
|
||||||
}),
|
}),
|
||||||
joinRoomSession: vi.fn()
|
joinRoomSession: vi.fn(),
|
||||||
}) as unknown as MatrixRTCSession;
|
}) as unknown as MatrixRTCSession;
|
||||||
|
|
||||||
await enterRTCSession(mockedSession, {
|
await enterRTCSession(
|
||||||
|
mockedSession,
|
||||||
|
{
|
||||||
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",
|
||||||
},
|
},
|
||||||
true);
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
expect(mockedSession.joinRoomSession).toHaveBeenLastCalledWith(
|
expect(mockedSession.joinRoomSession).toHaveBeenLastCalledWith(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
livekit_alias: "my-oldest-member-service-alias",
|
livekit_alias: "my-oldest-member-service-alias",
|
||||||
livekit_service_url: "http://my-oldest-member-service-url.com",
|
livekit_service_url: "http://my-oldest-member-service-url.com",
|
||||||
type: "livekit"
|
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_alias: "roomId",
|
||||||
livekit_service_url: "http://my-well-known-service-url2.com",
|
livekit_service_url: "http://my-well-known-service-url2.com",
|
||||||
type: "livekit"
|
type: "livekit",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
livekit_alias: "roomId",
|
livekit_alias: "roomId",
|
||||||
livekit_service_url: "http://my-default-service-url.com",
|
livekit_service_url: "http://my-default-service-url.com",
|
||||||
type: "livekit"
|
type: "livekit",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
focus_selection: "oldest_membership",
|
focus_selection: "oldest_membership",
|
||||||
type: "livekit"
|
type: "livekit",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
manageMediaKeys: false,
|
manageMediaKeys: false,
|
||||||
useLegacyMemberEvents: false,
|
useLegacyMemberEvents: false,
|
||||||
useNewMembershipManager: true,
|
useNewMembershipManager: true,
|
||||||
useExperimentalToDeviceTransport: false
|
useExperimentalToDeviceTransport: false,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function testLeaveRTCSession(
|
async function testLeaveRTCSession(
|
||||||
cause: "user" | "error",
|
cause: "user" | "error",
|
||||||
expectClose: boolean
|
expectClose: boolean,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
const session = { leaveRoomSession: vi.fn() } as unknown as MatrixRTCSession;
|
const session = { leaveRoomSession: vi.fn() } as unknown as MatrixRTCSession;
|
||||||
@@ -140,18 +142,18 @@ async function testLeaveRTCSession(
|
|||||||
expect(session.leaveRoomSession).toHaveBeenCalled();
|
expect(session.leaveRoomSession).toHaveBeenCalled();
|
||||||
expect(widget!.api.transport.send).toHaveBeenCalledWith(
|
expect(widget!.api.transport.send).toHaveBeenCalledWith(
|
||||||
ElementWidgetActions.HangupCall,
|
ElementWidgetActions.HangupCall,
|
||||||
expect.anything()
|
expect.anything(),
|
||||||
);
|
);
|
||||||
if (expectClose) {
|
if (expectClose) {
|
||||||
expect(widget!.api.transport.send).toHaveBeenCalledWith(
|
expect(widget!.api.transport.send).toHaveBeenCalledWith(
|
||||||
ElementWidgetActions.Close,
|
ElementWidgetActions.Close,
|
||||||
expect.anything()
|
expect.anything(),
|
||||||
);
|
);
|
||||||
expect(widget!.api.transport.stop).toHaveBeenCalled();
|
expect(widget!.api.transport.stop).toHaveBeenCalled();
|
||||||
} else {
|
} else {
|
||||||
expect(widget!.api.transport.send).not.toHaveBeenCalledWith(
|
expect(widget!.api.transport.send).not.toHaveBeenCalledWith(
|
||||||
ElementWidgetActions.Close,
|
ElementWidgetActions.Close,
|
||||||
expect.anything()
|
expect.anything(),
|
||||||
);
|
);
|
||||||
expect(widget!.api.transport.stop).not.toHaveBeenCalled();
|
expect(widget!.api.transport.stop).not.toHaveBeenCalled();
|
||||||
}
|
}
|
||||||
@@ -179,24 +181,26 @@ test("It fails with configuration error if no live kit url config is set in fall
|
|||||||
room: {
|
room: {
|
||||||
roomId: "roomId",
|
roomId: "roomId",
|
||||||
client: {
|
client: {
|
||||||
getDomain: vi.fn().mockReturnValue("example.org")
|
getDomain: vi.fn().mockReturnValue("example.org"),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
memberships: [],
|
memberships: [],
|
||||||
getFocusInUse: vi.fn(),
|
getFocusInUse: vi.fn(),
|
||||||
joinRoomSession: vi.fn()
|
joinRoomSession: vi.fn(),
|
||||||
}) as unknown as MatrixRTCSession;
|
}) as unknown as MatrixRTCSession;
|
||||||
|
|
||||||
await expect(enterRTCSession(
|
await expect(
|
||||||
mockedSession,
|
enterRTCSession(
|
||||||
{
|
mockedSession,
|
||||||
livekit_alias: "roomId",
|
{
|
||||||
livekit_service_url: "http://my-well-known-service-url.com",
|
livekit_alias: "roomId",
|
||||||
type: "livekit"
|
livekit_service_url: "http://my-well-known-service-url.com",
|
||||||
},
|
type: "livekit",
|
||||||
true
|
},
|
||||||
)).rejects.toThrowError(
|
true,
|
||||||
expect.objectContaining({ code: ErrorCode.MISSING_MATRIX_RTC_TRANSPORT })
|
),
|
||||||
|
).rejects.toThrowError(
|
||||||
|
expect.objectContaining({ code: ErrorCode.MISSING_MATRIX_RTC_TRANSPORT }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -206,9 +210,9 @@ test("It should not fail with configuration error if homeserver config has livek
|
|||||||
"org.matrix.msc4143.rtc_foci": [
|
"org.matrix.msc4143.rtc_foci": [
|
||||||
{
|
{
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
livekit_service_url: "http://my-well-known-service-url.com"
|
livekit_service_url: "http://my-well-known-service-url.com",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockedSession = vi.mocked({
|
const mockedSession = vi.mocked({
|
||||||
@@ -220,19 +224,22 @@ test("It should not fail with configuration error if homeserver config has livek
|
|||||||
access_token: "ACCCESS_TOKEN",
|
access_token: "ACCCESS_TOKEN",
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
matrix_server_name: "localhost",
|
matrix_server_name: "localhost",
|
||||||
expires_in: 10000
|
expires_in: 10000,
|
||||||
})
|
}),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
memberships: [],
|
memberships: [],
|
||||||
getFocusInUse: vi.fn(),
|
getFocusInUse: vi.fn(),
|
||||||
joinRoomSession: vi.fn()
|
joinRoomSession: vi.fn(),
|
||||||
}) as unknown as MatrixRTCSession;
|
}) as unknown as MatrixRTCSession;
|
||||||
|
|
||||||
await enterRTCSession(mockedSession, {
|
await enterRTCSession(
|
||||||
|
mockedSession,
|
||||||
|
{
|
||||||
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",
|
||||||
},
|
},
|
||||||
true);
|
true,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,14 +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 {
|
import { catchError, from, map, type Observable, of, startWith } from "rxjs";
|
||||||
catchError,
|
|
||||||
from,
|
|
||||||
map,
|
|
||||||
type Observable,
|
|
||||||
of,
|
|
||||||
startWith
|
|
||||||
} from "rxjs";
|
|
||||||
|
|
||||||
// TODO where are all the comments? ::cry::
|
// TODO where are all the comments? ::cry::
|
||||||
// There used to be an unitialized state!, a state might not start in loading
|
// There used to be an unitialized state!, a state might not start in loading
|
||||||
@@ -34,13 +27,15 @@ export function async$<A>(promise: Promise<A>): Observable<Async<A>> {
|
|||||||
return from(promise).pipe(
|
return from(promise).pipe(
|
||||||
map(ready),
|
map(ready),
|
||||||
startWith(loading),
|
startWith(loading),
|
||||||
catchError((e: unknown) => of(error(e as Error ?? new Error("Unknown error")))),
|
catchError((e: unknown) =>
|
||||||
|
of(error((e as Error) ?? new Error("Unknown error"))),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapAsync<A, B>(
|
export function mapAsync<A, B>(
|
||||||
async: Async<A>,
|
async: Async<A>,
|
||||||
project: (value: A) => B
|
project: (value: A) => B,
|
||||||
): Async<B> {
|
): Async<B> {
|
||||||
return async.state === "ready" ? ready(project(async.value)) : async;
|
return async.state === "ready" ? ready(project(async.value)) : async;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -352,7 +352,10 @@ function withCallViewModel(
|
|||||||
options,
|
options,
|
||||||
raisedHands$,
|
raisedHands$,
|
||||||
reactions$,
|
reactions$,
|
||||||
new BehaviorSubject<ProcessorState>({ processor: undefined, supported: undefined }),
|
new BehaviorSubject<ProcessorState>({
|
||||||
|
processor: undefined,
|
||||||
|
supported: undefined,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
onTestFinished(() => {
|
onTestFinished(() => {
|
||||||
|
|||||||
@@ -126,7 +126,11 @@ import {
|
|||||||
} from "../rtcSessionHelpers";
|
} from "../rtcSessionHelpers";
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider";
|
import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider";
|
||||||
import { type Connection, type ConnectionOpts, RemoteConnection } from "./Connection";
|
import {
|
||||||
|
type Connection,
|
||||||
|
type ConnectionOpts,
|
||||||
|
RemoteConnection,
|
||||||
|
} from "./Connection";
|
||||||
import { type MuteStates } from "./MuteStates";
|
import { type MuteStates } from "./MuteStates";
|
||||||
import { getUrlParams } from "../UrlParams";
|
import { getUrlParams } from "../UrlParams";
|
||||||
import { type ProcessorState } from "../livekit/TrackProcessorContext";
|
import { type ProcessorState } from "../livekit/TrackProcessorContext";
|
||||||
@@ -485,7 +489,6 @@ export class CallViewModel extends ViewModel {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The MatrixRTC session participants.
|
* The MatrixRTC session participants.
|
||||||
*/
|
*/
|
||||||
@@ -574,7 +577,6 @@ export class CallViewModel extends ViewModel {
|
|||||||
(transport) =>
|
(transport) =>
|
||||||
transport &&
|
transport &&
|
||||||
mapAsync(transport, (transport) => {
|
mapAsync(transport, (transport) => {
|
||||||
|
|
||||||
const opts: ConnectionOpts = {
|
const opts: ConnectionOpts = {
|
||||||
transport,
|
transport,
|
||||||
client: this.matrixRTCSession.room.client,
|
client: this.matrixRTCSession.room.client,
|
||||||
@@ -582,15 +584,16 @@ export class CallViewModel extends ViewModel {
|
|||||||
remoteTransports$: this.remoteTransports$,
|
remoteTransports$: this.remoteTransports$,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
connection: new PublishConnection(
|
connection: new PublishConnection(
|
||||||
opts,
|
opts,
|
||||||
this.mediaDevices,
|
this.mediaDevices,
|
||||||
this.muteStates,
|
this.muteStates,
|
||||||
this.e2eeLivekitOptions(),
|
this.e2eeLivekitOptions(),
|
||||||
this.scope.behavior(this.trackProcessorState$),
|
this.scope.behavior(this.trackProcessorState$),
|
||||||
),
|
),
|
||||||
transport,
|
transport,
|
||||||
}}),
|
};
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -605,14 +608,14 @@ export class CallViewModel extends ViewModel {
|
|||||||
this.localConnection$.pipe(
|
this.localConnection$.pipe(
|
||||||
switchMap((c) =>
|
switchMap((c) =>
|
||||||
c?.state === "ready"
|
c?.state === "ready"
|
||||||
// TODO mapping to ConnectionState for compatibility, but we should use the full state?
|
? // TODO mapping to ConnectionState for compatibility, but we should use the full state?
|
||||||
? c.value.focusedConnectionState$.pipe(
|
c.value.focusedConnectionState$.pipe(
|
||||||
map((s) => {
|
map((s) => {
|
||||||
if (s.state === "ConnectedToLkRoom") return s.connectionState;
|
if (s.state === "ConnectedToLkRoom") return s.connectionState;
|
||||||
return ConnectionState.Disconnected
|
return ConnectionState.Disconnected;
|
||||||
}),
|
}),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
)
|
)
|
||||||
: of(ConnectionState.Disconnected),
|
: of(ConnectionState.Disconnected),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -659,8 +662,11 @@ export class CallViewModel extends ViewModel {
|
|||||||
client: this.matrixRTCSession.room.client,
|
client: this.matrixRTCSession.room.client,
|
||||||
scope: this.scope,
|
scope: this.scope,
|
||||||
remoteTransports$: this.remoteTransports$,
|
remoteTransports$: this.remoteTransports$,
|
||||||
}
|
};
|
||||||
nextConnection = new RemoteConnection(args, this.e2eeLivekitOptions());
|
nextConnection = new RemoteConnection(
|
||||||
|
args,
|
||||||
|
this.e2eeLivekitOptions(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.log(
|
logger.log(
|
||||||
"SFU remoteConnections$ use prev connection: ",
|
"SFU remoteConnections$ use prev connection: ",
|
||||||
@@ -1952,16 +1958,20 @@ export class CallViewModel extends ViewModel {
|
|||||||
.pipe(this.scope.bind())
|
.pipe(this.scope.bind())
|
||||||
.subscribe(({ start, stop }) => {
|
.subscribe(({ start, stop }) => {
|
||||||
for (const c of stop) {
|
for (const c of stop) {
|
||||||
logger.info(`Disconnecting from ${c.localTransport.livekit_service_url}`);
|
logger.info(
|
||||||
|
`Disconnecting from ${c.localTransport.livekit_service_url}`,
|
||||||
|
);
|
||||||
c.stop().catch((err) => {
|
c.stop().catch((err) => {
|
||||||
// TODO: better error handling
|
// TODO: better error handling
|
||||||
logger.error("MuteState: handler error", err);
|
logger.error("MuteState: handler error", err);
|
||||||
});;
|
});
|
||||||
}
|
}
|
||||||
for (const c of start) {
|
for (const c of start) {
|
||||||
c.start().then(
|
c.start().then(
|
||||||
() =>
|
() =>
|
||||||
logger.info(`Connected to ${c.localTransport.livekit_service_url}`),
|
logger.info(
|
||||||
|
`Connected to ${c.localTransport.livekit_service_url}`,
|
||||||
|
),
|
||||||
(e) =>
|
(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}`,
|
||||||
|
|||||||
@@ -5,21 +5,37 @@ 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 { afterEach, describe, expect, it, type Mock, type MockedObject, vi } from "vitest";
|
import {
|
||||||
|
afterEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
type Mock,
|
||||||
|
type MockedObject,
|
||||||
|
vi,
|
||||||
|
} from "vitest";
|
||||||
import { BehaviorSubject, of } from "rxjs";
|
import { BehaviorSubject, of } from "rxjs";
|
||||||
import {
|
import {
|
||||||
ConnectionState,
|
ConnectionState,
|
||||||
type LocalParticipant,
|
type LocalParticipant,
|
||||||
type RemoteParticipant,
|
type RemoteParticipant,
|
||||||
type Room as LivekitRoom,
|
type Room as LivekitRoom,
|
||||||
RoomEvent, type RoomOptions
|
RoomEvent,
|
||||||
|
type RoomOptions,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
import fetchMock from "fetch-mock";
|
import fetchMock from "fetch-mock";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import { type IOpenIDToken } from "matrix-js-sdk";
|
import { type IOpenIDToken } from "matrix-js-sdk";
|
||||||
|
|
||||||
import type { CallMembership, LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
import type {
|
||||||
import { type ConnectionOpts, type FocusConnectionState, RemoteConnection } from "./Connection.ts";
|
CallMembership,
|
||||||
|
LivekitTransport,
|
||||||
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
|
import {
|
||||||
|
type ConnectionOpts,
|
||||||
|
type FocusConnectionState,
|
||||||
|
RemoteConnection,
|
||||||
|
} from "./Connection.ts";
|
||||||
import { ObservableScope } from "./ObservableScope.ts";
|
import { ObservableScope } from "./ObservableScope.ts";
|
||||||
import { type OpenIDClientParts } from "../livekit/openIDSFU.ts";
|
import { type OpenIDClientParts } from "../livekit/openIDSFU.ts";
|
||||||
import { FailToGetOpenIdToken } from "../utils/errors.ts";
|
import { FailToGetOpenIdToken } from "../utils/errors.ts";
|
||||||
@@ -38,28 +54,30 @@ let localParticipantEventEmiter: EventEmitter;
|
|||||||
let fakeLocalParticipant: MockedObject<LocalParticipant>;
|
let fakeLocalParticipant: MockedObject<LocalParticipant>;
|
||||||
|
|
||||||
let fakeRoomEventEmiter: EventEmitter;
|
let fakeRoomEventEmiter: EventEmitter;
|
||||||
let fakeMembershipsFocusMap$: BehaviorSubject<{ membership: CallMembership; transport: LivekitTransport }[]>;
|
let fakeMembershipsFocusMap$: BehaviorSubject<
|
||||||
|
{ membership: CallMembership; transport: LivekitTransport }[]
|
||||||
|
>;
|
||||||
|
|
||||||
const livekitFocus: LivekitTransport = {
|
const livekitFocus: LivekitTransport = {
|
||||||
livekit_alias: "!roomID:example.org",
|
livekit_alias: "!roomID:example.org",
|
||||||
livekit_service_url: "https://matrix-rtc.example.org/livekit/jwt",
|
livekit_service_url: "https://matrix-rtc.example.org/livekit/jwt",
|
||||||
type: "livekit"
|
type: "livekit",
|
||||||
};
|
};
|
||||||
|
|
||||||
function setupTest(): void {
|
function setupTest(): void {
|
||||||
testScope = new ObservableScope();
|
testScope = new ObservableScope();
|
||||||
client = vi.mocked<OpenIDClientParts>({
|
client = vi.mocked<OpenIDClientParts>({
|
||||||
getOpenIdToken: vi.fn().mockResolvedValue(
|
getOpenIdToken: vi.fn().mockResolvedValue({
|
||||||
{
|
access_token: "rYsmGUEwNjKgJYyeNUkZseJN",
|
||||||
"access_token": "rYsmGUEwNjKgJYyeNUkZseJN",
|
token_type: "Bearer",
|
||||||
"token_type": "Bearer",
|
matrix_server_name: "example.org",
|
||||||
"matrix_server_name": "example.org",
|
expires_in: 3600,
|
||||||
"expires_in": 3600
|
}),
|
||||||
}
|
getDeviceId: vi.fn().mockReturnValue("ABCDEF"),
|
||||||
),
|
|
||||||
getDeviceId: vi.fn().mockReturnValue("ABCDEF")
|
|
||||||
} as unknown as OpenIDClientParts);
|
} as unknown as OpenIDClientParts);
|
||||||
fakeMembershipsFocusMap$ = new BehaviorSubject<{ membership: CallMembership; transport: LivekitTransport }[]>([]);
|
fakeMembershipsFocusMap$ = new BehaviorSubject<
|
||||||
|
{ membership: CallMembership; transport: LivekitTransport }[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
localParticipantEventEmiter = new EventEmitter();
|
localParticipantEventEmiter = new EventEmitter();
|
||||||
|
|
||||||
@@ -69,9 +87,15 @@ function setupTest(): void {
|
|||||||
getTrackPublication: vi.fn().mockReturnValue(undefined),
|
getTrackPublication: vi.fn().mockReturnValue(undefined),
|
||||||
on: localParticipantEventEmiter.on.bind(localParticipantEventEmiter),
|
on: localParticipantEventEmiter.on.bind(localParticipantEventEmiter),
|
||||||
off: localParticipantEventEmiter.off.bind(localParticipantEventEmiter),
|
off: localParticipantEventEmiter.off.bind(localParticipantEventEmiter),
|
||||||
addListener: localParticipantEventEmiter.addListener.bind(localParticipantEventEmiter),
|
addListener: localParticipantEventEmiter.addListener.bind(
|
||||||
removeListener: localParticipantEventEmiter.removeListener.bind(localParticipantEventEmiter),
|
localParticipantEventEmiter,
|
||||||
removeAllListeners: localParticipantEventEmiter.removeAllListeners.bind(localParticipantEventEmiter)
|
),
|
||||||
|
removeListener: localParticipantEventEmiter.removeListener.bind(
|
||||||
|
localParticipantEventEmiter,
|
||||||
|
),
|
||||||
|
removeAllListeners: localParticipantEventEmiter.removeAllListeners.bind(
|
||||||
|
localParticipantEventEmiter,
|
||||||
|
),
|
||||||
} as unknown as LocalParticipant);
|
} as unknown as LocalParticipant);
|
||||||
fakeRoomEventEmiter = new EventEmitter();
|
fakeRoomEventEmiter = new EventEmitter();
|
||||||
|
|
||||||
@@ -84,56 +108,45 @@ function setupTest(): void {
|
|||||||
on: fakeRoomEventEmiter.on.bind(fakeRoomEventEmiter),
|
on: fakeRoomEventEmiter.on.bind(fakeRoomEventEmiter),
|
||||||
off: fakeRoomEventEmiter.off.bind(fakeRoomEventEmiter),
|
off: fakeRoomEventEmiter.off.bind(fakeRoomEventEmiter),
|
||||||
addListener: fakeRoomEventEmiter.addListener.bind(fakeRoomEventEmiter),
|
addListener: fakeRoomEventEmiter.addListener.bind(fakeRoomEventEmiter),
|
||||||
removeListener: fakeRoomEventEmiter.removeListener.bind(fakeRoomEventEmiter),
|
removeListener:
|
||||||
removeAllListeners: fakeRoomEventEmiter.removeAllListeners.bind(fakeRoomEventEmiter),
|
fakeRoomEventEmiter.removeListener.bind(fakeRoomEventEmiter),
|
||||||
setE2EEEnabled: vi.fn().mockResolvedValue(undefined)
|
removeAllListeners:
|
||||||
|
fakeRoomEventEmiter.removeAllListeners.bind(fakeRoomEventEmiter),
|
||||||
|
setE2EEEnabled: vi.fn().mockResolvedValue(undefined),
|
||||||
} as unknown as LivekitRoom);
|
} as unknown as LivekitRoom);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupRemoteConnection(): RemoteConnection {
|
function setupRemoteConnection(): RemoteConnection {
|
||||||
|
|
||||||
const opts: ConnectionOpts = {
|
const opts: ConnectionOpts = {
|
||||||
client: client,
|
client: client,
|
||||||
transport: livekitFocus,
|
transport: livekitFocus,
|
||||||
remoteTransports$: fakeMembershipsFocusMap$,
|
remoteTransports$: fakeMembershipsFocusMap$,
|
||||||
scope: testScope,
|
scope: testScope,
|
||||||
livekitRoomFactory: () => fakeLivekitRoom
|
livekitRoomFactory: () => fakeLivekitRoom,
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMock.post(`${livekitFocus.livekit_service_url}/sfu/get`,
|
fetchMock.post(`${livekitFocus.livekit_service_url}/sfu/get`, () => {
|
||||||
() => {
|
return {
|
||||||
return {
|
status: 200,
|
||||||
status: 200,
|
body: {
|
||||||
body:
|
url: "wss://matrix-rtc.m.localhost/livekit/sfu",
|
||||||
{
|
jwt: "ATOKEN",
|
||||||
"url": "wss://matrix-rtc.m.localhost/livekit/sfu",
|
},
|
||||||
"jwt": "ATOKEN"
|
};
|
||||||
}
|
});
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
fakeLivekitRoom
|
fakeLivekitRoom.connect.mockResolvedValue(undefined);
|
||||||
.connect
|
|
||||||
.mockResolvedValue(undefined);
|
|
||||||
|
|
||||||
return new RemoteConnection(
|
return new RemoteConnection(opts, undefined);
|
||||||
opts,
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe("Start connection states", () => {
|
describe("Start connection states", () => {
|
||||||
|
|
||||||
it("start in initialized state", () => {
|
it("start in initialized state", () => {
|
||||||
setupTest();
|
setupTest();
|
||||||
|
|
||||||
@@ -142,15 +155,13 @@ describe("Start connection states", () => {
|
|||||||
transport: livekitFocus,
|
transport: livekitFocus,
|
||||||
remoteTransports$: fakeMembershipsFocusMap$,
|
remoteTransports$: fakeMembershipsFocusMap$,
|
||||||
scope: testScope,
|
scope: testScope,
|
||||||
livekitRoomFactory: () => fakeLivekitRoom
|
livekitRoomFactory: () => fakeLivekitRoom,
|
||||||
};
|
};
|
||||||
const connection = new RemoteConnection(
|
const connection = new RemoteConnection(opts, undefined);
|
||||||
opts,
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(connection.focusedConnectionState$.getValue().state)
|
expect(connection.focusedConnectionState$.getValue().state).toEqual(
|
||||||
.toEqual("Initialized");
|
"Initialized",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fail to getOpenId token then error state", async () => {
|
it("fail to getOpenId token then error state", async () => {
|
||||||
@@ -162,31 +173,27 @@ describe("Start connection states", () => {
|
|||||||
transport: livekitFocus,
|
transport: livekitFocus,
|
||||||
remoteTransports$: fakeMembershipsFocusMap$,
|
remoteTransports$: fakeMembershipsFocusMap$,
|
||||||
scope: testScope,
|
scope: testScope,
|
||||||
livekitRoomFactory: () => fakeLivekitRoom
|
livekitRoomFactory: () => fakeLivekitRoom,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const connection = new RemoteConnection(opts, undefined);
|
||||||
const connection = new RemoteConnection(
|
|
||||||
opts,
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const capturedStates: FocusConnectionState[] = [];
|
const capturedStates: FocusConnectionState[] = [];
|
||||||
connection.focusedConnectionState$.subscribe((value) => {
|
connection.focusedConnectionState$.subscribe((value) => {
|
||||||
capturedStates.push(value);
|
capturedStates.push(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const deferred = Promise.withResolvers<IOpenIDToken>();
|
const deferred = Promise.withResolvers<IOpenIDToken>();
|
||||||
|
|
||||||
client.getOpenIdToken.mockImplementation(async (): Promise<IOpenIDToken> => {
|
client.getOpenIdToken.mockImplementation(
|
||||||
return await deferred.promise;
|
async (): Promise<IOpenIDToken> => {
|
||||||
});
|
return await deferred.promise;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
connection.start()
|
connection.start().catch(() => {
|
||||||
.catch(() => {
|
// expected to throw
|
||||||
// expected to throw
|
});
|
||||||
});
|
|
||||||
|
|
||||||
let capturedState = capturedStates.pop();
|
let capturedState = capturedStates.pop();
|
||||||
expect(capturedState).toBeDefined();
|
expect(capturedState).toBeDefined();
|
||||||
@@ -199,11 +206,14 @@ describe("Start connection states", () => {
|
|||||||
capturedState = capturedStates.pop();
|
capturedState = capturedStates.pop();
|
||||||
if (capturedState!.state === "FailedToStart") {
|
if (capturedState!.state === "FailedToStart") {
|
||||||
expect(capturedState!.error.message).toEqual("Something went wrong");
|
expect(capturedState!.error.message).toEqual("Something went wrong");
|
||||||
expect(capturedState!.focus.livekit_alias).toEqual(livekitFocus.livekit_alias);
|
expect(capturedState!.focus.livekit_alias).toEqual(
|
||||||
|
livekitFocus.livekit_alias,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
expect.fail("Expected FailedToStart state but got " + capturedState?.state);
|
expect.fail(
|
||||||
|
"Expected FailedToStart state but got " + capturedState?.state,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fail to get JWT token and error state", async () => {
|
it("fail to get JWT token and error state", async () => {
|
||||||
@@ -215,13 +225,10 @@ describe("Start connection states", () => {
|
|||||||
transport: livekitFocus,
|
transport: livekitFocus,
|
||||||
remoteTransports$: fakeMembershipsFocusMap$,
|
remoteTransports$: fakeMembershipsFocusMap$,
|
||||||
scope: testScope,
|
scope: testScope,
|
||||||
livekitRoomFactory: () => fakeLivekitRoom
|
livekitRoomFactory: () => fakeLivekitRoom,
|
||||||
};
|
};
|
||||||
|
|
||||||
const connection = new RemoteConnection(
|
const connection = new RemoteConnection(opts, undefined);
|
||||||
opts,
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const capturedStates: FocusConnectionState[] = [];
|
const capturedStates: FocusConnectionState[] = [];
|
||||||
connection.focusedConnectionState$.subscribe((value) => {
|
connection.focusedConnectionState$.subscribe((value) => {
|
||||||
@@ -230,21 +237,17 @@ describe("Start connection states", () => {
|
|||||||
|
|
||||||
const deferredSFU = Promise.withResolvers<void>();
|
const deferredSFU = Promise.withResolvers<void>();
|
||||||
// mock the /sfu/get call
|
// mock the /sfu/get call
|
||||||
fetchMock.post(`${livekitFocus.livekit_service_url}/sfu/get`,
|
fetchMock.post(`${livekitFocus.livekit_service_url}/sfu/get`, async () => {
|
||||||
async () => {
|
await deferredSFU.promise;
|
||||||
await deferredSFU.promise;
|
return {
|
||||||
return {
|
status: 500,
|
||||||
status: 500,
|
body: "Internal Server Error",
|
||||||
body: "Internal Server Error"
|
};
|
||||||
};
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
connection.start().catch(() => {
|
||||||
connection.start()
|
// expected to throw
|
||||||
.catch(() => {
|
});
|
||||||
// expected to throw
|
|
||||||
});
|
|
||||||
|
|
||||||
let capturedState = capturedStates.pop();
|
let capturedState = capturedStates.pop();
|
||||||
expect(capturedState).toBeDefined();
|
expect(capturedState).toBeDefined();
|
||||||
@@ -256,15 +259,19 @@ describe("Start connection states", () => {
|
|||||||
capturedState = capturedStates.pop();
|
capturedState = capturedStates.pop();
|
||||||
|
|
||||||
if (capturedState?.state === "FailedToStart") {
|
if (capturedState?.state === "FailedToStart") {
|
||||||
expect(capturedState?.error.message).toContain("SFU Config fetch failed with exception Error");
|
expect(capturedState?.error.message).toContain(
|
||||||
expect(capturedState?.focus.livekit_alias).toEqual(livekitFocus.livekit_alias);
|
"SFU Config fetch failed with exception Error",
|
||||||
|
);
|
||||||
|
expect(capturedState?.focus.livekit_alias).toEqual(
|
||||||
|
livekitFocus.livekit_alias,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
expect.fail("Expected FailedToStart state but got " + capturedState?.state);
|
expect.fail(
|
||||||
|
"Expected FailedToStart state but got " + capturedState?.state,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it("fail to connect to livekit error state", async () => {
|
it("fail to connect to livekit error state", async () => {
|
||||||
setupTest();
|
setupTest();
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
@@ -274,46 +281,36 @@ describe("Start connection states", () => {
|
|||||||
transport: livekitFocus,
|
transport: livekitFocus,
|
||||||
remoteTransports$: fakeMembershipsFocusMap$,
|
remoteTransports$: fakeMembershipsFocusMap$,
|
||||||
scope: testScope,
|
scope: testScope,
|
||||||
livekitRoomFactory: () => fakeLivekitRoom
|
livekitRoomFactory: () => fakeLivekitRoom,
|
||||||
};
|
};
|
||||||
|
|
||||||
const connection = new RemoteConnection(
|
const connection = new RemoteConnection(opts, undefined);
|
||||||
opts,
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const capturedStates: FocusConnectionState[] = [];
|
const capturedStates: FocusConnectionState[] = [];
|
||||||
connection.focusedConnectionState$.subscribe((value) => {
|
connection.focusedConnectionState$.subscribe((value) => {
|
||||||
capturedStates.push(value);
|
capturedStates.push(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const deferredSFU = Promise.withResolvers<void>();
|
const deferredSFU = Promise.withResolvers<void>();
|
||||||
// mock the /sfu/get call
|
// mock the /sfu/get call
|
||||||
fetchMock.post(`${livekitFocus.livekit_service_url}/sfu/get`,
|
fetchMock.post(`${livekitFocus.livekit_service_url}/sfu/get`, () => {
|
||||||
() => {
|
return {
|
||||||
return {
|
status: 200,
|
||||||
status: 200,
|
body: {
|
||||||
body:
|
url: "wss://matrix-rtc.m.localhost/livekit/sfu",
|
||||||
{
|
jwt: "ATOKEN",
|
||||||
"url": "wss://matrix-rtc.m.localhost/livekit/sfu",
|
},
|
||||||
"jwt": "ATOKEN"
|
};
|
||||||
}
|
});
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
fakeLivekitRoom
|
fakeLivekitRoom.connect.mockImplementation(async () => {
|
||||||
.connect
|
await deferredSFU.promise;
|
||||||
.mockImplementation(async () => {
|
throw new Error("Failed to connect to livekit");
|
||||||
await deferredSFU.promise;
|
});
|
||||||
throw new Error("Failed to connect to livekit");
|
|
||||||
});
|
|
||||||
|
|
||||||
connection.start()
|
connection.start().catch(() => {
|
||||||
.catch(() => {
|
// expected to throw
|
||||||
// expected to throw
|
});
|
||||||
});
|
|
||||||
|
|
||||||
let capturedState = capturedStates.pop();
|
let capturedState = capturedStates.pop();
|
||||||
expect(capturedState).toBeDefined();
|
expect(capturedState).toBeDefined();
|
||||||
@@ -326,12 +323,17 @@ describe("Start connection states", () => {
|
|||||||
capturedState = capturedStates.pop();
|
capturedState = capturedStates.pop();
|
||||||
|
|
||||||
if (capturedState && capturedState?.state === "FailedToStart") {
|
if (capturedState && capturedState?.state === "FailedToStart") {
|
||||||
expect(capturedState.error.message).toContain("Failed to connect to livekit");
|
expect(capturedState.error.message).toContain(
|
||||||
expect(capturedState.focus.livekit_alias).toEqual(livekitFocus.livekit_alias);
|
"Failed to connect to livekit",
|
||||||
|
);
|
||||||
|
expect(capturedState.focus.livekit_alias).toEqual(
|
||||||
|
livekitFocus.livekit_alias,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
expect.fail("Expected FailedToStart state but got " + JSON.stringify(capturedState));
|
expect.fail(
|
||||||
|
"Expected FailedToStart state but got " + JSON.stringify(capturedState),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("connection states happy path", async () => {
|
it("connection states happy path", async () => {
|
||||||
@@ -356,7 +358,6 @@ describe("Start connection states", () => {
|
|||||||
expect(connectingState?.state).toEqual("ConnectingToLkRoom");
|
expect(connectingState?.state).toEqual("ConnectingToLkRoom");
|
||||||
const connectedState = capturedState.shift();
|
const connectedState = capturedState.shift();
|
||||||
expect(connectedState?.state).toEqual("ConnectedToLkRoom");
|
expect(connectedState?.state).toEqual("ConnectedToLkRoom");
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should relay livekit events once connected", async () => {
|
it("should relay livekit events once connected", async () => {
|
||||||
@@ -378,7 +379,7 @@ describe("Start connection states", () => {
|
|||||||
ConnectionState.SignalReconnecting,
|
ConnectionState.SignalReconnecting,
|
||||||
ConnectionState.Connecting,
|
ConnectionState.Connecting,
|
||||||
ConnectionState.Connected,
|
ConnectionState.Connected,
|
||||||
ConnectionState.Reconnecting
|
ConnectionState.Reconnecting,
|
||||||
];
|
];
|
||||||
for (const state of states) {
|
for (const state of states) {
|
||||||
fakeRoomEventEmiter.emit(RoomEvent.ConnectionStateChanged, state);
|
fakeRoomEventEmiter.emit(RoomEvent.ConnectionStateChanged, state);
|
||||||
@@ -387,12 +388,18 @@ describe("Start connection states", () => {
|
|||||||
for (const state of states) {
|
for (const state of states) {
|
||||||
const s = capturedState.shift();
|
const s = capturedState.shift();
|
||||||
expect(s?.state).toEqual("ConnectedToLkRoom");
|
expect(s?.state).toEqual("ConnectedToLkRoom");
|
||||||
const connectedState = s as FocusConnectionState & { state: "ConnectedToLkRoom" };
|
const connectedState = s as FocusConnectionState & {
|
||||||
|
state: "ConnectedToLkRoom";
|
||||||
|
};
|
||||||
expect(connectedState.connectionState).toEqual(state);
|
expect(connectedState.connectionState).toEqual(state);
|
||||||
|
|
||||||
// should always have the focus info
|
// should always have the focus info
|
||||||
expect(connectedState.focus.livekit_alias).toEqual(livekitFocus.livekit_alias);
|
expect(connectedState.focus.livekit_alias).toEqual(
|
||||||
expect(connectedState.focus.livekit_service_url).toEqual(livekitFocus.livekit_service_url);
|
livekitFocus.livekit_alias,
|
||||||
|
);
|
||||||
|
expect(connectedState.focus.livekit_service_url).toEqual(
|
||||||
|
livekitFocus.livekit_service_url,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the state is not ConnectedToLkRoom, no events should be relayed anymore
|
// If the state is not ConnectedToLkRoom, no events should be relayed anymore
|
||||||
@@ -403,10 +410,8 @@ describe("Start connection states", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(capturedState.length).toEqual(0);
|
expect(capturedState.length).toEqual(0);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it("shutting down the scope should stop the connection", async () => {
|
it("shutting down the scope should stop the connection", async () => {
|
||||||
setupTest();
|
setupTest();
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
@@ -423,7 +428,6 @@ describe("Start connection states", () => {
|
|||||||
const stopSpy = vi.spyOn(connection, "stop");
|
const stopSpy = vi.spyOn(connection, "stop");
|
||||||
testScope.end();
|
testScope.end();
|
||||||
|
|
||||||
|
|
||||||
expect(stopSpy).toHaveBeenCalled();
|
expect(stopSpy).toHaveBeenCalled();
|
||||||
expect(fakeLivekitRoom.disconnect).toHaveBeenCalled();
|
expect(fakeLivekitRoom.disconnect).toHaveBeenCalled();
|
||||||
|
|
||||||
@@ -437,26 +441,22 @@ describe("Start connection states", () => {
|
|||||||
|
|
||||||
expect(capturedState.length).toEqual(0);
|
expect(capturedState.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function fakeRemoteLivekitParticipant(id: string): RemoteParticipant {
|
function fakeRemoteLivekitParticipant(id: string): RemoteParticipant {
|
||||||
return vi.mocked<RemoteParticipant>({
|
return vi.mocked<RemoteParticipant>({
|
||||||
identity: id
|
identity: id,
|
||||||
} as unknown as RemoteParticipant);
|
} as unknown as RemoteParticipant);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fakeRtcMemberShip(userId: string, deviceId: string): CallMembership {
|
function fakeRtcMemberShip(userId: string, deviceId: string): CallMembership {
|
||||||
return vi.mocked<CallMembership>({
|
return vi.mocked<CallMembership>({
|
||||||
sender: userId,
|
sender: userId,
|
||||||
deviceId: deviceId
|
deviceId: deviceId,
|
||||||
} as unknown as CallMembership);
|
} as unknown as CallMembership);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Publishing participants observations", () => {
|
describe("Publishing participants observations", () => {
|
||||||
|
|
||||||
|
|
||||||
it("should emit the list of publishing participants", async () => {
|
it("should emit the list of publishing participants", async () => {
|
||||||
setupTest();
|
setupTest();
|
||||||
|
|
||||||
@@ -464,13 +464,24 @@ describe("Publishing participants observations", () => {
|
|||||||
|
|
||||||
const bobIsAPublisher = Promise.withResolvers<void>();
|
const bobIsAPublisher = Promise.withResolvers<void>();
|
||||||
const danIsAPublisher = Promise.withResolvers<void>();
|
const danIsAPublisher = Promise.withResolvers<void>();
|
||||||
const observedPublishers: { participant: RemoteParticipant; membership: CallMembership }[][] = [];
|
const observedPublishers: {
|
||||||
|
participant: RemoteParticipant;
|
||||||
|
membership: CallMembership;
|
||||||
|
}[][] = [];
|
||||||
connection.publishingParticipants$.subscribe((publishers) => {
|
connection.publishingParticipants$.subscribe((publishers) => {
|
||||||
observedPublishers.push(publishers);
|
observedPublishers.push(publishers);
|
||||||
if (publishers.some((p) => p.participant.identity === "@bob:example.org:DEV111")) {
|
if (
|
||||||
|
publishers.some(
|
||||||
|
(p) => p.participant.identity === "@bob:example.org:DEV111",
|
||||||
|
)
|
||||||
|
) {
|
||||||
bobIsAPublisher.resolve();
|
bobIsAPublisher.resolve();
|
||||||
}
|
}
|
||||||
if (publishers.some((p) => p.participant.identity === "@dan:example.org:DEV333")) {
|
if (
|
||||||
|
publishers.some(
|
||||||
|
(p) => p.participant.identity === "@dan:example.org:DEV333",
|
||||||
|
)
|
||||||
|
) {
|
||||||
danIsAPublisher.resolve();
|
danIsAPublisher.resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -482,14 +493,13 @@ describe("Publishing participants observations", () => {
|
|||||||
fakeRemoteLivekitParticipant("@alice:example.org:DEV000"),
|
fakeRemoteLivekitParticipant("@alice:example.org:DEV000"),
|
||||||
fakeRemoteLivekitParticipant("@bob:example.org:DEV111"),
|
fakeRemoteLivekitParticipant("@bob:example.org:DEV111"),
|
||||||
fakeRemoteLivekitParticipant("@carol:example.org:DEV222"),
|
fakeRemoteLivekitParticipant("@carol:example.org:DEV222"),
|
||||||
fakeRemoteLivekitParticipant("@dan:example.org:DEV333")
|
fakeRemoteLivekitParticipant("@dan:example.org:DEV333"),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Let's simulate 3 members on the livekitRoom
|
// Let's simulate 3 members on the livekitRoom
|
||||||
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get")
|
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get").mockReturnValue(
|
||||||
.mockReturnValue(
|
new Map(participants.map((p) => [p.identity, p])),
|
||||||
new Map(participants.map((p) => [p.identity, p]))
|
);
|
||||||
);
|
|
||||||
|
|
||||||
for (const participant of participants) {
|
for (const participant of participants) {
|
||||||
fakeRoomEventEmiter.emit(RoomEvent.ParticipantConnected, participant);
|
fakeRoomEventEmiter.emit(RoomEvent.ParticipantConnected, participant);
|
||||||
@@ -498,20 +508,27 @@ describe("Publishing participants observations", () => {
|
|||||||
// At this point there should be no publishers
|
// At this point there should be no publishers
|
||||||
expect(observedPublishers.pop()!.length).toEqual(0);
|
expect(observedPublishers.pop()!.length).toEqual(0);
|
||||||
|
|
||||||
|
|
||||||
const otherFocus: LivekitTransport = {
|
const otherFocus: LivekitTransport = {
|
||||||
livekit_alias: "!roomID:example.org",
|
livekit_alias: "!roomID:example.org",
|
||||||
livekit_service_url: "https://other-matrix-rtc.example.org/livekit/jwt",
|
livekit_service_url: "https://other-matrix-rtc.example.org/livekit/jwt",
|
||||||
type: "livekit"
|
type: "livekit",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const rtcMemberships = [
|
const rtcMemberships = [
|
||||||
// Say bob is on the same focus
|
// Say bob is on the same focus
|
||||||
{ membership: fakeRtcMemberShip("@bob:example.org", "DEV111"), transport: livekitFocus },
|
{
|
||||||
|
membership: fakeRtcMemberShip("@bob:example.org", "DEV111"),
|
||||||
|
transport: livekitFocus,
|
||||||
|
},
|
||||||
// Alice and carol is on a different focus
|
// Alice and carol is on a different focus
|
||||||
{ membership: fakeRtcMemberShip("@alice:example.org", "DEV000"), transport: otherFocus },
|
{
|
||||||
{ membership: fakeRtcMemberShip("@carol:example.org", "DEV222"), transport: otherFocus }
|
membership: fakeRtcMemberShip("@alice:example.org", "DEV000"),
|
||||||
|
transport: otherFocus,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
membership: fakeRtcMemberShip("@carol:example.org", "DEV222"),
|
||||||
|
transport: otherFocus,
|
||||||
|
},
|
||||||
// NO DAVE YET
|
// NO DAVE YET
|
||||||
];
|
];
|
||||||
// signal this change in rtc memberships
|
// signal this change in rtc memberships
|
||||||
@@ -521,53 +538,74 @@ describe("Publishing participants observations", () => {
|
|||||||
await bobIsAPublisher.promise;
|
await bobIsAPublisher.promise;
|
||||||
const publishers = observedPublishers.pop();
|
const publishers = observedPublishers.pop();
|
||||||
expect(publishers?.length).toEqual(1);
|
expect(publishers?.length).toEqual(1);
|
||||||
expect(publishers?.[0].participant.identity).toEqual("@bob:example.org:DEV111");
|
expect(publishers?.[0].participant.identity).toEqual(
|
||||||
|
"@bob:example.org:DEV111",
|
||||||
|
);
|
||||||
|
|
||||||
// Now let's make dan join the rtc memberships
|
// Now let's make dan join the rtc memberships
|
||||||
rtcMemberships
|
rtcMemberships.push({
|
||||||
.push({ membership: fakeRtcMemberShip("@dan:example.org", "DEV333"), transport: livekitFocus });
|
membership: fakeRtcMemberShip("@dan:example.org", "DEV333"),
|
||||||
|
transport: livekitFocus,
|
||||||
|
});
|
||||||
fakeMembershipsFocusMap$.next(rtcMemberships);
|
fakeMembershipsFocusMap$.next(rtcMemberships);
|
||||||
|
|
||||||
// We should have bob and dan has publishers now
|
// We should have bob and dan has publishers now
|
||||||
await danIsAPublisher.promise;
|
await danIsAPublisher.promise;
|
||||||
const twoPublishers = observedPublishers.pop();
|
const twoPublishers = observedPublishers.pop();
|
||||||
expect(twoPublishers?.length).toEqual(2);
|
expect(twoPublishers?.length).toEqual(2);
|
||||||
expect(twoPublishers?.some((p) => p.participant.identity === "@bob:example.org:DEV111")).toBeTruthy();
|
expect(
|
||||||
expect(twoPublishers?.some((p) => p.participant.identity === "@dan:example.org:DEV333")).toBeTruthy();
|
twoPublishers?.some(
|
||||||
|
(p) => p.participant.identity === "@bob:example.org:DEV111",
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
twoPublishers?.some(
|
||||||
|
(p) => p.participant.identity === "@dan:example.org:DEV333",
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
// Now let's make bob leave the livekit room
|
// Now let's make bob leave the livekit room
|
||||||
participants = participants.filter((p) => p.identity !== "@bob:example.org:DEV111");
|
participants = participants.filter(
|
||||||
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get")
|
(p) => p.identity !== "@bob:example.org:DEV111",
|
||||||
.mockReturnValue(
|
);
|
||||||
new Map(participants.map((p) => [p.identity, p]))
|
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get").mockReturnValue(
|
||||||
);
|
new Map(participants.map((p) => [p.identity, p])),
|
||||||
fakeRoomEventEmiter.emit(RoomEvent.ParticipantDisconnected, fakeRemoteLivekitParticipant("@bob:example.org:DEV111"));
|
);
|
||||||
|
fakeRoomEventEmiter.emit(
|
||||||
|
RoomEvent.ParticipantDisconnected,
|
||||||
|
fakeRemoteLivekitParticipant("@bob:example.org:DEV111"),
|
||||||
|
);
|
||||||
|
|
||||||
const updatedPublishers = observedPublishers.pop();
|
const updatedPublishers = observedPublishers.pop();
|
||||||
expect(updatedPublishers?.length).toEqual(1);
|
expect(updatedPublishers?.length).toEqual(1);
|
||||||
expect(updatedPublishers?.some((p) => p.participant.identity === "@dan:example.org:DEV333")).toBeTruthy();
|
expect(
|
||||||
|
updatedPublishers?.some(
|
||||||
|
(p) => p.participant.identity === "@dan:example.org:DEV333",
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it("should be scoped to parent scope", (): void => {
|
it("should be scoped to parent scope", (): void => {
|
||||||
setupTest();
|
setupTest();
|
||||||
|
|
||||||
const connection = setupRemoteConnection();
|
const connection = setupRemoteConnection();
|
||||||
|
|
||||||
let observedPublishers: { participant: RemoteParticipant; membership: CallMembership }[][] = [];
|
let observedPublishers: {
|
||||||
|
participant: RemoteParticipant;
|
||||||
|
membership: CallMembership;
|
||||||
|
}[][] = [];
|
||||||
connection.publishingParticipants$.subscribe((publishers) => {
|
connection.publishingParticipants$.subscribe((publishers) => {
|
||||||
observedPublishers.push(publishers);
|
observedPublishers.push(publishers);
|
||||||
});
|
});
|
||||||
|
|
||||||
let participants: RemoteParticipant[] = [
|
let participants: RemoteParticipant[] = [
|
||||||
fakeRemoteLivekitParticipant("@bob:example.org:DEV111")
|
fakeRemoteLivekitParticipant("@bob:example.org:DEV111"),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Let's simulate 3 members on the livekitRoom
|
// Let's simulate 3 members on the livekitRoom
|
||||||
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get")
|
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get").mockReturnValue(
|
||||||
.mockReturnValue(
|
new Map(participants.map((p) => [p.identity, p])),
|
||||||
new Map(participants.map((p) => [p.identity, p]))
|
);
|
||||||
);
|
|
||||||
|
|
||||||
for (const participant of participants) {
|
for (const participant of participants) {
|
||||||
fakeRoomEventEmiter.emit(RoomEvent.ParticipantConnected, participant);
|
fakeRoomEventEmiter.emit(RoomEvent.ParticipantConnected, participant);
|
||||||
@@ -578,7 +616,10 @@ describe("Publishing participants observations", () => {
|
|||||||
|
|
||||||
const rtcMemberships = [
|
const rtcMemberships = [
|
||||||
// Say bob is on the same focus
|
// Say bob is on the same focus
|
||||||
{ membership: fakeRtcMemberShip("@bob:example.org", "DEV111"), transport: livekitFocus }
|
{
|
||||||
|
membership: fakeRtcMemberShip("@bob:example.org", "DEV111"),
|
||||||
|
transport: livekitFocus,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
// signal this change in rtc memberships
|
// signal this change in rtc memberships
|
||||||
fakeMembershipsFocusMap$.next(rtcMemberships);
|
fakeMembershipsFocusMap$.next(rtcMemberships);
|
||||||
@@ -586,27 +627,31 @@ describe("Publishing participants observations", () => {
|
|||||||
// We should have bob has a publisher now
|
// We should have bob has a publisher now
|
||||||
const publishers = observedPublishers.pop();
|
const publishers = observedPublishers.pop();
|
||||||
expect(publishers?.length).toEqual(1);
|
expect(publishers?.length).toEqual(1);
|
||||||
expect(publishers?.[0].participant.identity).toEqual("@bob:example.org:DEV111");
|
expect(publishers?.[0].participant.identity).toEqual(
|
||||||
|
"@bob:example.org:DEV111",
|
||||||
|
);
|
||||||
|
|
||||||
// end the parent scope
|
// end the parent scope
|
||||||
testScope.end();
|
testScope.end();
|
||||||
observedPublishers = [];
|
observedPublishers = [];
|
||||||
|
|
||||||
// SHOULD NOT emit any more publishers as the scope is ended
|
// SHOULD NOT emit any more publishers as the scope is ended
|
||||||
participants = participants.filter((p) => p.identity !== "@bob:example.org:DEV111");
|
participants = participants.filter(
|
||||||
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get")
|
(p) => p.identity !== "@bob:example.org:DEV111",
|
||||||
.mockReturnValue(
|
);
|
||||||
new Map(participants.map((p) => [p.identity, p]))
|
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get").mockReturnValue(
|
||||||
);
|
new Map(participants.map((p) => [p.identity, p])),
|
||||||
fakeRoomEventEmiter.emit(RoomEvent.ParticipantDisconnected, fakeRemoteLivekitParticipant("@bob:example.org:DEV111"));
|
);
|
||||||
|
fakeRoomEventEmiter.emit(
|
||||||
|
RoomEvent.ParticipantDisconnected,
|
||||||
|
fakeRemoteLivekitParticipant("@bob:example.org:DEV111"),
|
||||||
|
);
|
||||||
|
|
||||||
expect(observedPublishers.length).toEqual(0);
|
expect(observedPublishers.length).toEqual(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe("PublishConnection", () => {
|
describe("PublishConnection", () => {
|
||||||
|
|
||||||
// let fakeBlurProcessor: ProcessorWrapper<BackgroundOptions>;
|
// let fakeBlurProcessor: ProcessorWrapper<BackgroundOptions>;
|
||||||
let roomFactoryMock: Mock<() => LivekitRoom>;
|
let roomFactoryMock: Mock<() => LivekitRoom>;
|
||||||
let muteStates: MockedObject<MuteStates>;
|
let muteStates: MockedObject<MuteStates>;
|
||||||
@@ -616,7 +661,6 @@ describe("PublishConnection", () => {
|
|||||||
|
|
||||||
roomFactoryMock = vi.fn().mockReturnValue(fakeLivekitRoom);
|
roomFactoryMock = vi.fn().mockReturnValue(fakeLivekitRoom);
|
||||||
|
|
||||||
|
|
||||||
muteStates = mockMuteStates();
|
muteStates = mockMuteStates();
|
||||||
|
|
||||||
// fakeBlurProcessor = vi.mocked<ProcessorWrapper<BackgroundOptions>>({
|
// fakeBlurProcessor = vi.mocked<ProcessorWrapper<BackgroundOptions>>({
|
||||||
@@ -626,20 +670,15 @@ describe("PublishConnection", () => {
|
|||||||
// getOptions: vi.fn().mockReturnValue({ strength: 0.5 }),
|
// getOptions: vi.fn().mockReturnValue({ strength: 0.5 }),
|
||||||
// isRunning: vi.fn().mockReturnValue(false)
|
// isRunning: vi.fn().mockReturnValue(false)
|
||||||
// });
|
// });
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
describe("Livekit room creation", () => {
|
describe("Livekit room creation", () => {
|
||||||
|
|
||||||
|
|
||||||
function createSetup(): void {
|
function createSetup(): void {
|
||||||
setUpPublishConnection();
|
setUpPublishConnection();
|
||||||
|
|
||||||
const fakeTrackProcessorSubject$ = new BehaviorSubject<ProcessorState>({
|
const fakeTrackProcessorSubject$ = new BehaviorSubject<ProcessorState>({
|
||||||
supported: true,
|
supported: true,
|
||||||
processor: undefined
|
processor: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const opts: ConnectionOpts = {
|
const opts: ConnectionOpts = {
|
||||||
@@ -647,28 +686,25 @@ describe("PublishConnection", () => {
|
|||||||
transport: livekitFocus,
|
transport: livekitFocus,
|
||||||
remoteTransports$: fakeMembershipsFocusMap$,
|
remoteTransports$: fakeMembershipsFocusMap$,
|
||||||
scope: testScope,
|
scope: testScope,
|
||||||
livekitRoomFactory: roomFactoryMock
|
livekitRoomFactory: roomFactoryMock,
|
||||||
};
|
};
|
||||||
|
|
||||||
const audioInput = {
|
const audioInput = {
|
||||||
available$: of(new Map([["mic1", { id: "mic1" }]])),
|
available$: of(new Map([["mic1", { id: "mic1" }]])),
|
||||||
selected$: new BehaviorSubject({ id: "mic1" }),
|
selected$: new BehaviorSubject({ id: "mic1" }),
|
||||||
select(): void {
|
select(): void {},
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const videoInput = {
|
const videoInput = {
|
||||||
available$: of(new Map([["cam1", { id: "cam1" }]])),
|
available$: of(new Map([["cam1", { id: "cam1" }]])),
|
||||||
selected$: new BehaviorSubject({ id: "cam1" }),
|
selected$: new BehaviorSubject({ id: "cam1" }),
|
||||||
select(): void {
|
select(): void {},
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const audioOutput = {
|
const audioOutput = {
|
||||||
available$: of(new Map([["speaker", { id: "speaker" }]])),
|
available$: of(new Map([["speaker", { id: "speaker" }]])),
|
||||||
selected$: new BehaviorSubject({ id: "speaker" }),
|
selected$: new BehaviorSubject({ id: "speaker" }),
|
||||||
select(): void {
|
select(): void {},
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO understand what is wrong with our mocking that requires ts-expect-error
|
// TODO understand what is wrong with our mocking that requires ts-expect-error
|
||||||
@@ -678,7 +714,7 @@ describe("PublishConnection", () => {
|
|||||||
// @ts-expect-error Mocking only
|
// @ts-expect-error Mocking only
|
||||||
videoInput,
|
videoInput,
|
||||||
// @ts-expect-error Mocking only
|
// @ts-expect-error Mocking only
|
||||||
audioOutput
|
audioOutput,
|
||||||
});
|
});
|
||||||
|
|
||||||
new PublishConnection(
|
new PublishConnection(
|
||||||
@@ -686,18 +722,17 @@ describe("PublishConnection", () => {
|
|||||||
fakeDevices,
|
fakeDevices,
|
||||||
muteStates,
|
muteStates,
|
||||||
undefined,
|
undefined,
|
||||||
fakeTrackProcessorSubject$
|
fakeTrackProcessorSubject$,
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it("should create room with proper initial audio and video settings", () => {
|
it("should create room with proper initial audio and video settings", () => {
|
||||||
|
|
||||||
createSetup();
|
createSetup();
|
||||||
|
|
||||||
expect(roomFactoryMock).toHaveBeenCalled();
|
expect(roomFactoryMock).toHaveBeenCalled();
|
||||||
|
|
||||||
const lastCallArgs = roomFactoryMock.mock.calls[roomFactoryMock.mock.calls.length - 1];
|
const lastCallArgs =
|
||||||
|
roomFactoryMock.mock.calls[roomFactoryMock.mock.calls.length - 1];
|
||||||
|
|
||||||
const roomOptions = lastCallArgs.pop() as unknown as RoomOptions;
|
const roomOptions = lastCallArgs.pop() as unknown as RoomOptions;
|
||||||
expect(roomOptions).toBeDefined();
|
expect(roomOptions).toBeDefined();
|
||||||
@@ -705,7 +740,6 @@ describe("PublishConnection", () => {
|
|||||||
expect(roomOptions!.videoCaptureDefaults?.deviceId).toEqual("cam1");
|
expect(roomOptions!.videoCaptureDefaults?.deviceId).toEqual("cam1");
|
||||||
expect(roomOptions!.audioCaptureDefaults?.deviceId).toEqual("mic1");
|
expect(roomOptions!.audioCaptureDefaults?.deviceId).toEqual("mic1");
|
||||||
expect(roomOptions!.audioOutput?.deviceId).toEqual("speaker");
|
expect(roomOptions!.audioOutput?.deviceId).toEqual("speaker");
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("respect controlledAudioDevices", () => {
|
it("respect controlledAudioDevices", () => {
|
||||||
@@ -719,7 +753,6 @@ describe("PublishConnection", () => {
|
|||||||
// })
|
// })
|
||||||
// };
|
// };
|
||||||
// });
|
// });
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,12 +5,27 @@ 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 { connectedParticipantsObserver, connectionStateObserver } from "@livekit/components-core";
|
import {
|
||||||
import { type ConnectionState, type E2EEOptions, Room as LivekitRoom, type RoomOptions } from "livekit-client";
|
connectedParticipantsObserver,
|
||||||
import { type CallMembership, type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
connectionStateObserver,
|
||||||
|
} from "@livekit/components-core";
|
||||||
|
import {
|
||||||
|
type ConnectionState,
|
||||||
|
type E2EEOptions,
|
||||||
|
Room as LivekitRoom,
|
||||||
|
type RoomOptions,
|
||||||
|
} from "livekit-client";
|
||||||
|
import {
|
||||||
|
type CallMembership,
|
||||||
|
type LivekitTransport,
|
||||||
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { BehaviorSubject, combineLatest } from "rxjs";
|
import { BehaviorSubject, combineLatest } from "rxjs";
|
||||||
|
|
||||||
import { getSFUConfigWithOpenID, type OpenIDClientParts, type SFUConfig } from "../livekit/openIDSFU";
|
import {
|
||||||
|
getSFUConfigWithOpenID,
|
||||||
|
type OpenIDClientParts,
|
||||||
|
type SFUConfig,
|
||||||
|
} from "../livekit/openIDSFU";
|
||||||
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";
|
||||||
@@ -23,20 +38,26 @@ export interface ConnectionOpts {
|
|||||||
/** The observable scope to use for this connection. */
|
/** The observable scope to use for this connection. */
|
||||||
scope: ObservableScope;
|
scope: ObservableScope;
|
||||||
/** An observable of the current RTC call memberships and their associated focus. */
|
/** An observable of the current RTC call memberships and their associated focus. */
|
||||||
remoteTransports$: Behavior<{ membership: CallMembership; transport: LivekitTransport }[]>;
|
remoteTransports$: Behavior<
|
||||||
|
{ membership: CallMembership; transport: LivekitTransport }[]
|
||||||
|
>;
|
||||||
|
|
||||||
/** Optional factory to create the Livekit room, mainly for testing purposes. */
|
/** Optional factory to create the Livekit room, mainly for testing purposes. */
|
||||||
livekitRoomFactory?: (options?: RoomOptions) => LivekitRoom;
|
livekitRoomFactory?: (options?: RoomOptions) => LivekitRoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FocusConnectionState =
|
export type FocusConnectionState =
|
||||||
| { state: 'Initialized' }
|
| { state: "Initialized" }
|
||||||
| { state: 'FetchingConfig', focus: LivekitTransport }
|
| { state: "FetchingConfig"; focus: LivekitTransport }
|
||||||
| { state: 'ConnectingToLkRoom', focus: LivekitTransport }
|
| { state: "ConnectingToLkRoom"; focus: LivekitTransport }
|
||||||
| { state: 'PublishingTracks', focus: LivekitTransport }
|
| { state: "PublishingTracks"; focus: LivekitTransport }
|
||||||
| { state: 'FailedToStart', error: Error, focus: LivekitTransport }
|
| { state: "FailedToStart"; error: Error; focus: LivekitTransport }
|
||||||
| { state: 'ConnectedToLkRoom', connectionState: ConnectionState, focus: LivekitTransport }
|
| {
|
||||||
| { state: 'Stopped', focus: LivekitTransport };
|
state: "ConnectedToLkRoom";
|
||||||
|
connectionState: ConnectionState;
|
||||||
|
focus: LivekitTransport;
|
||||||
|
}
|
||||||
|
| { state: "Stopped"; focus: LivekitTransport };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A connection to a Matrix RTC LiveKit backend.
|
* A connection to a Matrix RTC LiveKit backend.
|
||||||
@@ -44,10 +65,9 @@ export type FocusConnectionState =
|
|||||||
* Expose observables for participants and connection state.
|
* Expose observables for participants and connection state.
|
||||||
*/
|
*/
|
||||||
export class Connection {
|
export class Connection {
|
||||||
|
|
||||||
// Private Behavior
|
// Private Behavior
|
||||||
private readonly _focusedConnectionState$
|
private readonly _focusedConnectionState$ =
|
||||||
= new BehaviorSubject<FocusConnectionState>({ state: 'Initialized' });
|
new BehaviorSubject<FocusConnectionState>({ state: "Initialized" });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current state of the connection to the focus server.
|
* The current state of the connection to the focus server.
|
||||||
@@ -71,31 +91,44 @@ export class Connection {
|
|||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
this.stopped = false;
|
this.stopped = false;
|
||||||
try {
|
try {
|
||||||
this._focusedConnectionState$.next({ state: 'FetchingConfig', focus: this.localTransport });
|
this._focusedConnectionState$.next({
|
||||||
|
state: "FetchingConfig",
|
||||||
|
focus: this.localTransport,
|
||||||
|
});
|
||||||
// TODO could this be loaded earlier to save time?
|
// TODO could this be loaded earlier to save time?
|
||||||
const { url, jwt } = await this.getSFUConfigWithOpenID();
|
const { url, jwt } = await this.getSFUConfigWithOpenID();
|
||||||
// If we were stopped while fetching the config, don't proceed to connect
|
// If we were stopped while fetching the config, don't proceed to connect
|
||||||
if (this.stopped) return;
|
if (this.stopped) return;
|
||||||
|
|
||||||
this._focusedConnectionState$.next({ state: 'ConnectingToLkRoom', focus: this.localTransport });
|
this._focusedConnectionState$.next({
|
||||||
|
state: "ConnectingToLkRoom",
|
||||||
|
focus: this.localTransport,
|
||||||
|
});
|
||||||
await this.livekitRoom.connect(url, jwt);
|
await this.livekitRoom.connect(url, jwt);
|
||||||
// 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;
|
||||||
|
|
||||||
this._focusedConnectionState$.next({ state: 'ConnectedToLkRoom', focus: this.localTransport, connectionState: this.livekitRoom.state });
|
this._focusedConnectionState$.next({
|
||||||
|
state: "ConnectedToLkRoom",
|
||||||
|
focus: this.localTransport,
|
||||||
|
connectionState: this.livekitRoom.state,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this._focusedConnectionState$.next({ state: 'FailedToStart', error: error instanceof Error ? error : new Error(`${error}`), focus: this.localTransport });
|
this._focusedConnectionState$.next({
|
||||||
|
state: "FailedToStart",
|
||||||
|
error: error instanceof Error ? error : new Error(`${error}`),
|
||||||
|
focus: this.localTransport,
|
||||||
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected async getSFUConfigWithOpenID(): Promise<SFUConfig> {
|
protected async getSFUConfigWithOpenID(): Promise<SFUConfig> {
|
||||||
return await getSFUConfigWithOpenID(
|
return await getSFUConfigWithOpenID(
|
||||||
this.client,
|
this.client,
|
||||||
this.localTransport.livekit_service_url,
|
this.localTransport.livekit_service_url,
|
||||||
this.localTransport.livekit_alias
|
this.localTransport.livekit_alias,
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Stops the connection.
|
* Stops the connection.
|
||||||
@@ -106,11 +139,13 @@ export class Connection {
|
|||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
if (this.stopped) return;
|
if (this.stopped) return;
|
||||||
await this.livekitRoom.disconnect();
|
await this.livekitRoom.disconnect();
|
||||||
this._focusedConnectionState$.next({ state: 'Stopped', focus: this.localTransport });
|
this._focusedConnectionState$.next({
|
||||||
|
state: "Stopped",
|
||||||
|
focus: this.localTransport,
|
||||||
|
});
|
||||||
this.stopped = true;
|
this.stopped = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An observable of the participants that are publishing on this connection.
|
* An observable of the participants that are publishing on this connection.
|
||||||
* This is derived from `participantsIncludingSubscribers$` and `membershipsFocusMap$`.
|
* This is derived from `participantsIncludingSubscribers$` and `membershipsFocusMap$`.
|
||||||
@@ -135,20 +170,20 @@ export class Connection {
|
|||||||
public readonly livekitRoom: LivekitRoom,
|
public readonly livekitRoom: LivekitRoom,
|
||||||
opts: ConnectionOpts,
|
opts: ConnectionOpts,
|
||||||
) {
|
) {
|
||||||
const { transport, client, scope, remoteTransports$ } =
|
const { transport, client, scope, remoteTransports$ } = opts;
|
||||||
opts;
|
|
||||||
|
|
||||||
this.livekitRoom = livekitRoom
|
this.livekitRoom = livekitRoom;
|
||||||
this.localTransport = transport;
|
this.localTransport = transport;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
|
||||||
this.focusedConnectionState$ = scope.behavior(
|
this.focusedConnectionState$ = scope.behavior(
|
||||||
this._focusedConnectionState$, { state: 'Initialized' }
|
this._focusedConnectionState$,
|
||||||
|
{ state: "Initialized" },
|
||||||
);
|
);
|
||||||
|
|
||||||
const participantsIncludingSubscribers$ = scope.behavior(
|
const participantsIncludingSubscribers$ = scope.behavior(
|
||||||
connectedParticipantsObserver(this.livekitRoom),
|
connectedParticipantsObserver(this.livekitRoom),
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
this.publishingParticipants$ = scope.behavior(
|
this.publishingParticipants$ = scope.behavior(
|
||||||
@@ -161,7 +196,7 @@ export class Connection {
|
|||||||
transport.livekit_service_url ===
|
transport.livekit_service_url ===
|
||||||
this.localTransport.livekit_service_url
|
this.localTransport.livekit_service_url
|
||||||
? [membership]
|
? [membership]
|
||||||
: []
|
: [],
|
||||||
)
|
)
|
||||||
// Pair with their associated LiveKit participant (if any)
|
// Pair with their associated LiveKit participant (if any)
|
||||||
// Uses flatMap to filter out memberships with no associated rtc participant ([])
|
// Uses flatMap to filter out memberships with no associated rtc participant ([])
|
||||||
@@ -171,18 +206,22 @@ export class Connection {
|
|||||||
return participant ? [{ participant, membership }] : [];
|
return participant ? [{ participant, membership }] : [];
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
scope.behavior<ConnectionState>(
|
scope
|
||||||
connectionStateObserver(this.livekitRoom)
|
.behavior<ConnectionState>(connectionStateObserver(this.livekitRoom))
|
||||||
).subscribe((connectionState) => {
|
.subscribe((connectionState) => {
|
||||||
const current = this._focusedConnectionState$.value;
|
const current = this._focusedConnectionState$.value;
|
||||||
// Only update the state if we are already connected to the LiveKit room.
|
// Only update the state if we are already connected to the LiveKit room.
|
||||||
if (current.state === 'ConnectedToLkRoom') {
|
if (current.state === "ConnectedToLkRoom") {
|
||||||
this._focusedConnectionState$.next({ state: 'ConnectedToLkRoom', connectionState, focus: current.focus });
|
this._focusedConnectionState$.next({
|
||||||
}
|
state: "ConnectedToLkRoom",
|
||||||
});
|
connectionState,
|
||||||
|
focus: current.focus,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
scope.onEnd(() => void this.stop());
|
scope.onEnd(() => void this.stop());
|
||||||
}
|
}
|
||||||
@@ -195,17 +234,21 @@ export class Connection {
|
|||||||
* It does not publish any local tracks.
|
* It does not publish any local tracks.
|
||||||
*/
|
*/
|
||||||
export class RemoteConnection extends Connection {
|
export class RemoteConnection extends Connection {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new remote connection to a matrix RTC LiveKit backend.
|
* Creates a new remote connection to a matrix RTC LiveKit backend.
|
||||||
* @param opts
|
* @param opts
|
||||||
* @param sharedE2eeOption - The shared E2EE options to use for the connection.
|
* @param sharedE2eeOption - The shared E2EE options to use for the connection.
|
||||||
*/
|
*/
|
||||||
public constructor(opts: ConnectionOpts, sharedE2eeOption: E2EEOptions | undefined) {
|
public constructor(
|
||||||
const factory = opts.livekitRoomFactory ?? ((options: RoomOptions): LivekitRoom => new LivekitRoom(options));
|
opts: ConnectionOpts,
|
||||||
|
sharedE2eeOption: E2EEOptions | undefined,
|
||||||
|
) {
|
||||||
|
const factory =
|
||||||
|
opts.livekitRoomFactory ??
|
||||||
|
((options: RoomOptions): LivekitRoom => new LivekitRoom(options));
|
||||||
const livekitRoom = factory({
|
const livekitRoom = factory({
|
||||||
...defaultLiveKitOptions,
|
...defaultLiveKitOptions,
|
||||||
e2ee: sharedE2eeOption
|
e2ee: sharedE2eeOption,
|
||||||
});
|
});
|
||||||
super(livekitRoom, opts);
|
super(livekitRoom, opts);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,24 @@ import {
|
|||||||
LocalVideoTrack,
|
LocalVideoTrack,
|
||||||
Room as LivekitRoom,
|
Room as LivekitRoom,
|
||||||
type RoomOptions,
|
type RoomOptions,
|
||||||
Track
|
Track,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
import { map, NEVER, type Observable, type Subscription, switchMap } from "rxjs";
|
import {
|
||||||
|
map,
|
||||||
|
NEVER,
|
||||||
|
type Observable,
|
||||||
|
type Subscription,
|
||||||
|
switchMap,
|
||||||
|
} from "rxjs";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
import type { Behavior } from "./Behavior.ts";
|
import type { Behavior } from "./Behavior.ts";
|
||||||
import type { MediaDevices, SelectedDevice } from "./MediaDevices.ts";
|
import type { MediaDevices, SelectedDevice } from "./MediaDevices.ts";
|
||||||
import type { MuteStates } from "./MuteStates.ts";
|
import type { MuteStates } from "./MuteStates.ts";
|
||||||
import { type ProcessorState, trackProcessorSync } from "../livekit/TrackProcessorContext.tsx";
|
import {
|
||||||
|
type ProcessorState,
|
||||||
|
trackProcessorSync,
|
||||||
|
} from "../livekit/TrackProcessorContext.tsx";
|
||||||
import { getUrlParams } from "../UrlParams.ts";
|
import { getUrlParams } from "../UrlParams.ts";
|
||||||
import { defaultLiveKitOptions } from "../livekit/options.ts";
|
import { defaultLiveKitOptions } from "../livekit/options.ts";
|
||||||
import { getValue } from "../utils/observable.ts";
|
import { getValue } from "../utils/observable.ts";
|
||||||
@@ -31,7 +40,6 @@ import { type ObservableScope } from "./ObservableScope.ts";
|
|||||||
* This connection will publish the local user's audio and video tracks.
|
* This connection will publish the local user's audio and video tracks.
|
||||||
*/
|
*/
|
||||||
export class PublishConnection extends Connection {
|
export class PublishConnection extends Connection {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new PublishConnection.
|
* Creates a new PublishConnection.
|
||||||
* @param args - The connection options. {@link ConnectionOpts}
|
* @param args - The connection options. {@link ConnectionOpts}
|
||||||
@@ -45,15 +53,22 @@ export class PublishConnection extends Connection {
|
|||||||
devices: MediaDevices,
|
devices: MediaDevices,
|
||||||
private readonly muteStates: MuteStates,
|
private readonly muteStates: MuteStates,
|
||||||
e2eeLivekitOptions: E2EEOptions | undefined,
|
e2eeLivekitOptions: E2EEOptions | undefined,
|
||||||
trackerProcessorState$: Behavior<ProcessorState>
|
trackerProcessorState$: Behavior<ProcessorState>,
|
||||||
) {
|
) {
|
||||||
const { scope } = args;
|
const { scope } = args;
|
||||||
logger.info("[LivekitRoom] Create LiveKit room");
|
logger.info("[LivekitRoom] Create LiveKit room");
|
||||||
const { controlledAudioDevices } = getUrlParams();
|
const { controlledAudioDevices } = getUrlParams();
|
||||||
|
|
||||||
const factory = args.livekitRoomFactory ?? ((options: RoomOptions): LivekitRoom => new LivekitRoom(options));
|
const factory =
|
||||||
|
args.livekitRoomFactory ??
|
||||||
|
((options: RoomOptions): LivekitRoom => new LivekitRoom(options));
|
||||||
const room = factory(
|
const room = factory(
|
||||||
generateRoomOption(devices, trackerProcessorState$.value, controlledAudioDevices, e2eeLivekitOptions)
|
generateRoomOption(
|
||||||
|
devices,
|
||||||
|
trackerProcessorState$.value,
|
||||||
|
controlledAudioDevices,
|
||||||
|
e2eeLivekitOptions,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
room.setE2EEEnabled(e2eeLivekitOptions !== undefined).catch((e) => {
|
room.setE2EEEnabled(e2eeLivekitOptions !== undefined).catch((e) => {
|
||||||
logger.error("Failed to set E2EE enabled on room", e);
|
logger.error("Failed to set E2EE enabled on room", e);
|
||||||
@@ -83,14 +98,14 @@ export class PublishConnection extends Connection {
|
|||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
this.stopped = false;
|
this.stopped = false;
|
||||||
|
|
||||||
await super.start()
|
await super.start();
|
||||||
|
|
||||||
if (this.stopped) return;
|
if (this.stopped) return;
|
||||||
|
|
||||||
// TODO this can throw errors? It will also prompt for permissions if not already granted
|
// TODO this can throw errors? It will also prompt for permissions if not already granted
|
||||||
const tracks = await this.livekitRoom.localParticipant.createTracks({
|
const tracks = await this.livekitRoom.localParticipant.createTracks({
|
||||||
audio: this.muteStates.audio.enabled$.value,
|
audio: this.muteStates.audio.enabled$.value,
|
||||||
video: this.muteStates.video.enabled$.value
|
video: this.muteStates.video.enabled$.value,
|
||||||
});
|
});
|
||||||
if (this.stopped) return;
|
if (this.stopped) return;
|
||||||
for (const track of tracks) {
|
for (const track of tracks) {
|
||||||
@@ -100,7 +115,7 @@ export class PublishConnection extends Connection {
|
|||||||
if (this.stopped) return;
|
if (this.stopped) return;
|
||||||
// TODO: check if the connection is still active? and break the loop if not?
|
// TODO: check if the connection is still active? and break the loop if not?
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
/// Private methods
|
/// Private methods
|
||||||
|
|
||||||
@@ -112,17 +127,19 @@ export class PublishConnection extends Connection {
|
|||||||
// in the LocalParticipant object for the track object and there's not a nice
|
// in the LocalParticipant object for the track object and there's not a nice
|
||||||
// way to do that generically. There is usually no OS-level default video capture
|
// way to do that generically. There is usually no OS-level default video capture
|
||||||
// device anyway, and audio outputs work differently.
|
// device anyway, and audio outputs work differently.
|
||||||
private workaroundRestartAudioInputTrackChrome(devices: MediaDevices, scope: ObservableScope): void {
|
private workaroundRestartAudioInputTrackChrome(
|
||||||
|
devices: MediaDevices,
|
||||||
|
scope: ObservableScope,
|
||||||
|
): void {
|
||||||
devices.audioInput.selected$
|
devices.audioInput.selected$
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((device) => device?.hardwareDeviceChange$ ?? NEVER),
|
switchMap((device) => device?.hardwareDeviceChange$ ?? NEVER),
|
||||||
scope.bind()
|
scope.bind(),
|
||||||
)
|
)
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
if (this.livekitRoom.state != ConnectionState.Connected) return;
|
if (this.livekitRoom.state != ConnectionState.Connected) return;
|
||||||
const activeMicTrack = Array.from(
|
const activeMicTrack = Array.from(
|
||||||
this.livekitRoom.localParticipant.audioTrackPublications.values()
|
this.livekitRoom.localParticipant.audioTrackPublications.values(),
|
||||||
).find((d) => d.source === Track.Source.Microphone)?.track;
|
).find((d) => d.source === Track.Source.Microphone)?.track;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -147,11 +164,15 @@ export class PublishConnection extends Connection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe changes in the selected media devices and update the LiveKit room accordingly.
|
// Observe changes in the selected media devices and update the LiveKit room accordingly.
|
||||||
private observeMediaDevices(scope: ObservableScope, devices: MediaDevices, controlledAudioDevices: boolean):void {
|
private observeMediaDevices(
|
||||||
|
scope: ObservableScope,
|
||||||
|
devices: MediaDevices,
|
||||||
|
controlledAudioDevices: boolean,
|
||||||
|
): void {
|
||||||
const syncDevice = (
|
const syncDevice = (
|
||||||
kind: MediaDeviceKind,
|
kind: MediaDeviceKind,
|
||||||
selected$: Observable<SelectedDevice | undefined>
|
selected$: Observable<SelectedDevice | undefined>,
|
||||||
): Subscription =>
|
): Subscription =>
|
||||||
selected$.pipe(scope.bind()).subscribe((device) => {
|
selected$.pipe(scope.bind()).subscribe((device) => {
|
||||||
if (this.livekitRoom.state != ConnectionState.Connected) return;
|
if (this.livekitRoom.state != ConnectionState.Connected) return;
|
||||||
@@ -160,7 +181,7 @@ export class PublishConnection extends Connection {
|
|||||||
"[LivekitRoom] syncDevice room.getActiveDevice(kind) !== d.id :",
|
"[LivekitRoom] syncDevice room.getActiveDevice(kind) !== d.id :",
|
||||||
this.livekitRoom.getActiveDevice(kind),
|
this.livekitRoom.getActiveDevice(kind),
|
||||||
" !== ",
|
" !== ",
|
||||||
device?.id
|
device?.id,
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
device !== undefined &&
|
device !== undefined &&
|
||||||
@@ -169,7 +190,7 @@ export class PublishConnection extends Connection {
|
|||||||
this.livekitRoom
|
this.livekitRoom
|
||||||
.switchActiveDevice(kind, device.id)
|
.switchActiveDevice(kind, device.id)
|
||||||
.catch((e) =>
|
.catch((e) =>
|
||||||
logger.error(`Failed to sync ${kind} device with LiveKit`, e)
|
logger.error(`Failed to sync ${kind} device with LiveKit`, e),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -208,21 +229,23 @@ export class PublishConnection extends Connection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private observeTrackProcessors(scope: ObservableScope, room: LivekitRoom, trackerProcessorState$: Behavior<ProcessorState>): void {
|
private observeTrackProcessors(
|
||||||
|
scope: ObservableScope,
|
||||||
|
room: LivekitRoom,
|
||||||
|
trackerProcessorState$: Behavior<ProcessorState>,
|
||||||
|
): void {
|
||||||
const track$ = scope.behavior(
|
const track$ = scope.behavior(
|
||||||
observeTrackReference$(room.localParticipant, Track.Source.Camera).pipe(
|
observeTrackReference$(room.localParticipant, Track.Source.Camera).pipe(
|
||||||
map((trackRef) => {
|
map((trackRef) => {
|
||||||
const track = trackRef?.publication?.track;
|
const track = trackRef?.publication?.track;
|
||||||
return track instanceof LocalVideoTrack ? track : null;
|
return track instanceof LocalVideoTrack ? track : null;
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
trackProcessorSync(track$, trackerProcessorState$);
|
trackProcessorSync(track$, trackerProcessorState$);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Generate the initial LiveKit RoomOptions based on the current media devices and processor state.
|
// Generate the initial LiveKit RoomOptions based on the current media devices and processor state.
|
||||||
function generateRoomOption(
|
function generateRoomOption(
|
||||||
devices: MediaDevices,
|
devices: MediaDevices,
|
||||||
@@ -235,11 +258,11 @@ function generateRoomOption(
|
|||||||
videoCaptureDefaults: {
|
videoCaptureDefaults: {
|
||||||
...defaultLiveKitOptions.videoCaptureDefaults,
|
...defaultLiveKitOptions.videoCaptureDefaults,
|
||||||
deviceId: devices.videoInput.selected$.value?.id,
|
deviceId: devices.videoInput.selected$.value?.id,
|
||||||
processor: processorState.processor
|
processor: processorState.processor,
|
||||||
},
|
},
|
||||||
audioCaptureDefaults: {
|
audioCaptureDefaults: {
|
||||||
...defaultLiveKitOptions.audioCaptureDefaults,
|
...defaultLiveKitOptions.audioCaptureDefaults,
|
||||||
deviceId: devices.audioInput.selected$.value?.id
|
deviceId: devices.audioInput.selected$.value?.id,
|
||||||
},
|
},
|
||||||
audioOutput: {
|
audioOutput: {
|
||||||
// When using controlled audio devices, we don't want to set the
|
// When using controlled audio devices, we don't want to set the
|
||||||
@@ -247,8 +270,8 @@ function generateRoomOption(
|
|||||||
// (also the id does not need to match a browser device id)
|
// (also the id does not need to match a browser device id)
|
||||||
deviceId: controlledAudioDevices
|
deviceId: controlledAudioDevices
|
||||||
? undefined
|
? undefined
|
||||||
: getValue(devices.audioOutput.selected$)?.id
|
: getValue(devices.audioOutput.selected$)?.id,
|
||||||
},
|
},
|
||||||
e2ee: e2eeLivekitOptions
|
e2ee: e2eeLivekitOptions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ import { type ComponentProps } from "react";
|
|||||||
|
|
||||||
import { MediaView } from "./MediaView";
|
import { MediaView } from "./MediaView";
|
||||||
import { EncryptionStatus } from "../state/MediaViewModel";
|
import { EncryptionStatus } from "../state/MediaViewModel";
|
||||||
import { mockLocalParticipant, mockMatrixRoomMember, mockRtcMembership } from "../utils/test";
|
import {
|
||||||
|
mockLocalParticipant,
|
||||||
|
mockMatrixRoomMember,
|
||||||
|
mockRtcMembership,
|
||||||
|
} from "../utils/test";
|
||||||
|
|
||||||
describe("MediaView", () => {
|
describe("MediaView", () => {
|
||||||
const participant = mockLocalParticipant({});
|
const participant = mockLocalParticipant({});
|
||||||
|
|||||||
@@ -419,7 +419,9 @@ export const deviceStub = {
|
|||||||
select(): void {},
|
select(): void {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function mockMediaDevices(data: Partial<MediaDevices>): MockedObject<MediaDevices> {
|
export function mockMediaDevices(
|
||||||
|
data: Partial<MediaDevices>,
|
||||||
|
): MockedObject<MediaDevices> {
|
||||||
return vi.mocked<MediaDevices>({
|
return vi.mocked<MediaDevices>({
|
||||||
audioInput: deviceStub,
|
audioInput: deviceStub,
|
||||||
audioOutput: deviceStub,
|
audioOutput: deviceStub,
|
||||||
|
|||||||
Reference in New Issue
Block a user