Fix a couple of CallViewModel tests.
This commit is contained in:
@@ -93,6 +93,9 @@ import {
|
|||||||
MatrixRTCTransportMissingError,
|
MatrixRTCTransportMissingError,
|
||||||
} from "../../utils/errors.ts";
|
} from "../../utils/errors.ts";
|
||||||
|
|
||||||
|
mockConfig({
|
||||||
|
livekit: { livekit_service_url: "http://my-default-service-url.com" },
|
||||||
|
});
|
||||||
vi.mock("rxjs", async (importOriginal) => ({
|
vi.mock("rxjs", async (importOriginal) => ({
|
||||||
...(await importOriginal()),
|
...(await importOriginal()),
|
||||||
// Disable interval Observables for the following tests since the test
|
// Disable interval Observables for the following tests since the test
|
||||||
@@ -384,9 +387,10 @@ export function withCallViewModel(
|
|||||||
continuation(vm, rtcSession, { raisedHands$: raisedHands$ }, setSyncState);
|
continuation(vm, rtcSession, { raisedHands$: raisedHands$ }, setSyncState);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Restore this test. It requires makeTransport to not be mocked, unlike
|
describe("CallViewModel", () => {
|
||||||
// the rest of the tests in this file… what do we do?
|
// TODO: Restore this test. It requires makeTransport to not be mocked, unlike
|
||||||
test.skip("test missing RTC config error", async () => {
|
// the rest of the tests in this file… what do we do?
|
||||||
|
test.skip("test missing RTC config error", async () => {
|
||||||
const rtcMemberships$ = new BehaviorSubject<CallMembership[]>([]);
|
const rtcMemberships$ = new BehaviorSubject<CallMembership[]>([]);
|
||||||
const emitter = new EventEmitter();
|
const emitter = new EventEmitter();
|
||||||
const client = vi.mocked<MatrixClient>({
|
const client = vi.mocked<MatrixClient>({
|
||||||
@@ -414,6 +418,7 @@ test.skip("test missing RTC config error", async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
mockConfig({});
|
mockConfig({});
|
||||||
|
|
||||||
vi.spyOn(AutoDiscovery, "getRawClientConfig").mockResolvedValue({});
|
vi.spyOn(AutoDiscovery, "getRawClientConfig").mockResolvedValue({});
|
||||||
|
|
||||||
const callVM = new CallViewModel(
|
const callVM = new CallViewModel(
|
||||||
@@ -446,9 +451,9 @@ test.skip("test missing RTC config error", async () => {
|
|||||||
|
|
||||||
const error = await failPromise.promise;
|
const error = await failPromise.promise;
|
||||||
expect(error).toBeInstanceOf(MatrixRTCTransportMissingError);
|
expect(error).toBeInstanceOf(MatrixRTCTransportMissingError);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("participants are retained during a focus switch", () => {
|
test("participants are retained during a focus switch", () => {
|
||||||
withTestScheduler(({ behavior, expectObservable }) => {
|
withTestScheduler(({ behavior, expectObservable }) => {
|
||||||
// Participants disappear on frame 2 and come back on frame 3
|
// Participants disappear on frame 2 and come back on frame 3
|
||||||
const participantInputMarbles = "a-ba";
|
const participantInputMarbles = "a-ba";
|
||||||
@@ -483,9 +488,9 @@ test("participants are retained during a focus switch", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("screen sharing activates spotlight layout", () => {
|
test("screen sharing activates spotlight layout", () => {
|
||||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||||
// Start with no screen shares, then have Alice and Bob share their screens,
|
// Start with no screen shares, then have Alice and Bob share their screens,
|
||||||
// then return to no screen shares, then have just Alice share for a bit
|
// then return to no screen shares, then have just Alice share for a bit
|
||||||
@@ -564,9 +569,9 @@ test("screen sharing activates spotlight layout", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("participants stay in the same order unless to appear/disappear", () => {
|
test("participants stay in the same order unless to appear/disappear", () => {
|
||||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||||
const visibilityInputMarbles = "a";
|
const visibilityInputMarbles = "a";
|
||||||
// First Bob speaks, then Dave, then Alice
|
// First Bob speaks, then Dave, then Alice
|
||||||
@@ -647,9 +652,9 @@ test("participants stay in the same order unless to appear/disappear", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("participants adjust order when space becomes constrained", () => {
|
test("participants adjust order when space becomes constrained", () => {
|
||||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||||
// Start with all tiles on screen then shrink to 3
|
// Start with all tiles on screen then shrink to 3
|
||||||
const visibilityInputMarbles = "a-b";
|
const visibilityInputMarbles = "a-b";
|
||||||
@@ -682,7 +687,8 @@ test("participants adjust order when space becomes constrained", () => {
|
|||||||
(vm) => {
|
(vm) => {
|
||||||
let setVisibleTiles: ((value: number) => void) | null = null;
|
let setVisibleTiles: ((value: number) => void) | null = null;
|
||||||
vm.layout$.subscribe((layout) => {
|
vm.layout$.subscribe((layout) => {
|
||||||
if (layout.type === "grid") setVisibleTiles = layout.setVisibleTiles;
|
if (layout.type === "grid")
|
||||||
|
setVisibleTiles = layout.setVisibleTiles;
|
||||||
});
|
});
|
||||||
schedule(visibilityInputMarbles, {
|
schedule(visibilityInputMarbles, {
|
||||||
a: () => setVisibleTiles!(Infinity),
|
a: () => setVisibleTiles!(Infinity),
|
||||||
@@ -717,9 +723,9 @@ test("participants adjust order when space becomes constrained", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("spotlight speakers swap places", () => {
|
test("spotlight speakers swap places", () => {
|
||||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||||
// Go immediately into spotlight mode for the test
|
// Go immediately into spotlight mode for the test
|
||||||
const modeInputMarbles = " s";
|
const modeInputMarbles = " s";
|
||||||
@@ -798,9 +804,9 @@ test("spotlight speakers swap places", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("layout enters picture-in-picture mode when requested", () => {
|
test("layout enters picture-in-picture mode when requested", () => {
|
||||||
withTestScheduler(({ schedule, expectObservable }) => {
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
// Enable then disable picture-in-picture
|
// Enable then disable picture-in-picture
|
||||||
const pipControlInputMarbles = "-ed";
|
const pipControlInputMarbles = "-ed";
|
||||||
@@ -835,9 +841,9 @@ test("layout enters picture-in-picture mode when requested", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("PiP tile in expanded spotlight layout switches speakers without layout shifts", () => {
|
test("PiP tile in expanded spotlight layout switches speakers without layout shifts", () => {
|
||||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||||
// Switch to spotlight immediately
|
// Switch to spotlight immediately
|
||||||
const modeInputMarbles = " s";
|
const modeInputMarbles = " s";
|
||||||
@@ -913,9 +919,9 @@ test("PiP tile in expanded spotlight layout switches speakers without layout shi
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("spotlight remembers whether it's expanded", () => {
|
test("spotlight remembers whether it's expanded", () => {
|
||||||
withTestScheduler(({ schedule, expectObservable }) => {
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
// Start in spotlight mode, then switch to grid and back to spotlight a
|
// Start in spotlight mode, then switch to grid and back to spotlight a
|
||||||
// couple times
|
// couple times
|
||||||
@@ -968,9 +974,9 @@ test("spotlight remembers whether it's expanded", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("participants must have a MatrixRTCSession to be visible", () => {
|
test("participants must have a MatrixRTCSession to be visible", () => {
|
||||||
withTestScheduler(({ behavior, expectObservable }) => {
|
withTestScheduler(({ behavior, expectObservable }) => {
|
||||||
// iterate through a number of combinations of participants and MatrixRTC memberships
|
// iterate through a number of combinations of participants and MatrixRTC memberships
|
||||||
// Bob never has an MatrixRTC membership
|
// Bob never has an MatrixRTC membership
|
||||||
@@ -1020,9 +1026,9 @@ test("participants must have a MatrixRTCSession to be visible", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show at least one tile per MatrixRTCSession", () => {
|
it("should show at least one tile per MatrixRTCSession", () => {
|
||||||
withTestScheduler(({ behavior, expectObservable }) => {
|
withTestScheduler(({ behavior, expectObservable }) => {
|
||||||
// iterate through some combinations of MatrixRTC memberships
|
// iterate through some combinations of MatrixRTC memberships
|
||||||
const scenarioInputMarbles = " abcd";
|
const scenarioInputMarbles = " abcd";
|
||||||
@@ -1068,9 +1074,9 @@ it("should show at least one tile per MatrixRTCSession", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should rank raised hands above video feeds and below speakers and presenters", () => {
|
it("should rank raised hands above video feeds and below speakers and presenters", () => {
|
||||||
withTestScheduler(({ schedule, expectObservable }) => {
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
// There should always be one tile for each MatrixRTCSession
|
// There should always be one tile for each MatrixRTCSession
|
||||||
const expectedLayoutMarbles = "ab";
|
const expectedLayoutMarbles = "ab";
|
||||||
@@ -1127,48 +1133,48 @@ it("should rank raised hands above video feeds and below speakers and presenters
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function nooneEverThere$<T>(
|
function nooneEverThere$<T>(
|
||||||
behavior: (marbles: string, values: Record<string, T[]>) => Behavior<T[]>,
|
behavior: (marbles: string, values: Record<string, T[]>) => Behavior<T[]>,
|
||||||
): Behavior<T[]> {
|
): Behavior<T[]> {
|
||||||
return behavior("a-b-c-d", {
|
return behavior("a-b-c-d", {
|
||||||
a: [], // Start empty
|
a: [], // Start empty
|
||||||
b: [], // Alice joins
|
b: [], // Alice joins
|
||||||
c: [], // Alice still there
|
c: [], // Alice still there
|
||||||
d: [], // Alice leaves
|
d: [], // Alice leaves
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function participantJoinLeave$(
|
function participantJoinLeave$(
|
||||||
behavior: (
|
behavior: (
|
||||||
marbles: string,
|
marbles: string,
|
||||||
values: Record<string, RemoteParticipant[]>,
|
values: Record<string, RemoteParticipant[]>,
|
||||||
) => Behavior<RemoteParticipant[]>,
|
) => Behavior<RemoteParticipant[]>,
|
||||||
): Behavior<RemoteParticipant[]> {
|
): Behavior<RemoteParticipant[]> {
|
||||||
return behavior("a-b-c-d", {
|
return behavior("a-b-c-d", {
|
||||||
a: [], // Start empty
|
a: [], // Start empty
|
||||||
b: [aliceParticipant], // Alice joins
|
b: [aliceParticipant], // Alice joins
|
||||||
c: [aliceParticipant], // Alice still there
|
c: [aliceParticipant], // Alice still there
|
||||||
d: [], // Alice leaves
|
d: [], // Alice leaves
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function rtcMemberJoinLeave$(
|
function rtcMemberJoinLeave$(
|
||||||
behavior: (
|
behavior: (
|
||||||
marbles: string,
|
marbles: string,
|
||||||
values: Record<string, CallMembership[]>,
|
values: Record<string, CallMembership[]>,
|
||||||
) => Behavior<CallMembership[]>,
|
) => Behavior<CallMembership[]>,
|
||||||
): Behavior<CallMembership[]> {
|
): Behavior<CallMembership[]> {
|
||||||
return behavior("a-b-c-d", {
|
return behavior("a-b-c-d", {
|
||||||
a: [localRtcMember], // Start empty
|
a: [localRtcMember], // Start empty
|
||||||
b: [localRtcMember, aliceRtcMember], // Alice joins
|
b: [localRtcMember, aliceRtcMember], // Alice joins
|
||||||
c: [localRtcMember, aliceRtcMember], // Alice still there
|
c: [localRtcMember, aliceRtcMember], // Alice still there
|
||||||
d: [localRtcMember], // Alice leaves
|
d: [localRtcMember], // Alice leaves
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test("autoLeave$ emits only when autoLeaveWhenOthersLeft option is enabled", () => {
|
test("autoLeave$ emits only when autoLeaveWhenOthersLeft option is enabled", () => {
|
||||||
withTestScheduler(({ behavior, expectObservable }) => {
|
withTestScheduler(({ behavior, expectObservable }) => {
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
{
|
{
|
||||||
@@ -1186,9 +1192,9 @@ test("autoLeave$ emits only when autoLeaveWhenOthersLeft option is enabled", ()
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("autoLeave$ never emits autoLeaveWhenOthersLeft option is enabled but no-one is there", () => {
|
test("autoLeave$ never emits autoLeaveWhenOthersLeft option is enabled but no-one is there", () => {
|
||||||
withTestScheduler(({ behavior, expectObservable }) => {
|
withTestScheduler(({ behavior, expectObservable }) => {
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
{
|
{
|
||||||
@@ -1204,9 +1210,9 @@ test("autoLeave$ never emits autoLeaveWhenOthersLeft option is enabled but no-on
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("autoLeave$ doesn't emit when autoLeaveWhenOthersLeft option is disabled and all others left", () => {
|
test("autoLeave$ doesn't emit when autoLeaveWhenOthersLeft option is disabled and all others left", () => {
|
||||||
withTestScheduler(({ behavior, expectObservable }) => {
|
withTestScheduler(({ behavior, expectObservable }) => {
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
{
|
{
|
||||||
@@ -1222,9 +1228,9 @@ test("autoLeave$ doesn't emit when autoLeaveWhenOthersLeft option is disabled an
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("autoLeave$ emits when autoLeaveWhenOthersLeft option is enabled and all others left", () => {
|
test("autoLeave$ emits when autoLeaveWhenOthersLeft option is enabled and all others left", () => {
|
||||||
withTestScheduler(({ behavior, expectObservable }) => {
|
withTestScheduler(({ behavior, expectObservable }) => {
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
{
|
{
|
||||||
@@ -1252,9 +1258,9 @@ test("autoLeave$ emits when autoLeaveWhenOthersLeft option is enabled and all ot
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("waitForCallPickup$", () => {
|
describe("waitForCallPickup$", () => {
|
||||||
test("unknown -> ringing -> timeout when notified and nobody joins", () => {
|
test("unknown -> ringing -> timeout when notified and nobody joins", () => {
|
||||||
withTestScheduler(({ schedule, expectObservable }) => {
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
// No one ever joins (only local user)
|
// No one ever joins (only local user)
|
||||||
@@ -1571,7 +1577,10 @@ describe("waitForCallPickup$", () => {
|
|||||||
d: () => {
|
d: () => {
|
||||||
rtcSession.room.emit(
|
rtcSession.room.emit(
|
||||||
MatrixRoomEvent.Timeline,
|
MatrixRoomEvent.Timeline,
|
||||||
new MatrixEvent({ event_id: "$decl2", type: "m.rtc.decline" }),
|
new MatrixEvent({
|
||||||
|
event_id: "$decl2",
|
||||||
|
type: "m.rtc.decline",
|
||||||
|
}),
|
||||||
rtcSession.room,
|
rtcSession.room,
|
||||||
undefined,
|
undefined,
|
||||||
false,
|
false,
|
||||||
@@ -1647,12 +1656,14 @@ describe("waitForCallPickup$", () => {
|
|||||||
sender: alice.userId,
|
sender: alice.userId,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("audio output changes when toggling earpiece mode", () => {
|
test("audio output changes when toggling earpiece mode", () => {
|
||||||
withTestScheduler(({ schedule, expectObservable }) => {
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
getUrlParams.mockReturnValue({ controlledAudioDevices: true });
|
getUrlParams.mockReturnValue({ controlledAudioDevices: true });
|
||||||
vi.mocked(ComponentsCore.createMediaDeviceObserver).mockReturnValue(of([]));
|
vi.mocked(ComponentsCore.createMediaDeviceObserver).mockReturnValue(
|
||||||
|
of([]),
|
||||||
|
);
|
||||||
|
|
||||||
const devices = new MediaDevices(testScope());
|
const devices = new MediaDevices(testScope());
|
||||||
|
|
||||||
@@ -1676,13 +1687,15 @@ test("audio output changes when toggling earpiece mode", () => {
|
|||||||
yesNo,
|
yesNo,
|
||||||
);
|
);
|
||||||
expectObservable(
|
expectObservable(
|
||||||
vm.audioOutputSwitcher$.pipe(map((switcher) => switcher?.targetOutput)),
|
vm.audioOutputSwitcher$.pipe(
|
||||||
|
map((switcher) => switcher?.targetOutput),
|
||||||
|
),
|
||||||
).toBe(expectedTargetStateMarbles, { s: "speaker", e: "earpiece" });
|
).toBe(expectedTargetStateMarbles, { s: "speaker", e: "earpiece" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("media tracks are paused while reconnecting to MatrixRTC", () => {
|
test("media tracks are paused while reconnecting to MatrixRTC", () => {
|
||||||
withTestScheduler(({ schedule, expectObservable }) => {
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
const trackRunning$ = new BehaviorSubject(true);
|
const trackRunning$ = new BehaviorSubject(true);
|
||||||
const originalPublications = localParticipant.trackPublications;
|
const originalPublications = localParticipant.trackPublications;
|
||||||
@@ -1755,4 +1768,5 @@ test("media tracks are paused while reconnecting to MatrixRTC", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -182,6 +182,21 @@ export class CallViewModel {
|
|||||||
this.matrixRTCSession,
|
this.matrixRTCSession,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Each hbar seperates a block of input variables required for the CallViewModel to function.
|
||||||
|
// The outputs of this block is written under the hbar.
|
||||||
|
//
|
||||||
|
// For mocking purposes it is recommended to only mock the functions creating those outputs.
|
||||||
|
// All other fields are just temp computations for the mentioned output.
|
||||||
|
// The class does not need anything except the values underneath the bar.
|
||||||
|
// The creation of the values under the bar are all tested independently and testing the callViewModel Should
|
||||||
|
// not test their cretation. Call view model only needs:
|
||||||
|
// - memberships$ via createMemberships$
|
||||||
|
// - localMembership via createLocalMembership$
|
||||||
|
// - callLifecycle via createCallNotificationLifecycle$
|
||||||
|
// - matrixMemberMetadataStore via createMatrixMemberMetadata$
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// memberships$
|
||||||
private memberships$ = createMemberships$(this.scope, this.matrixRTCSession);
|
private memberships$ = createMemberships$(this.scope, this.matrixRTCSession);
|
||||||
|
|
||||||
private membershipsAndTransports = membershipsAndTransports$(
|
private membershipsAndTransports = membershipsAndTransports$(
|
||||||
@@ -189,6 +204,9 @@ export class CallViewModel {
|
|||||||
this.memberships$,
|
this.memberships$,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
// matrixLivekitMembers$ AND localMembership
|
||||||
|
|
||||||
private localTransport$ = createLocalTransport$({
|
private localTransport$ = createLocalTransport$({
|
||||||
scope: this.scope,
|
scope: this.scope,
|
||||||
memberships$: this.memberships$,
|
memberships$: this.memberships$,
|
||||||
@@ -199,18 +217,19 @@ export class CallViewModel {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private connectionFactory = new ECConnectionFactory(
|
private connectionFactory = new ECConnectionFactory(
|
||||||
this.matrixRoom.client,
|
this.matrixRoom.client,
|
||||||
this.mediaDevices,
|
this.mediaDevices,
|
||||||
this.trackProcessorState$,
|
this.trackProcessorState$,
|
||||||
this.livekitKeyProvider,
|
this.livekitKeyProvider,
|
||||||
getUrlParams().controlledAudioDevices,
|
getUrlParams().controlledAudioDevices,
|
||||||
|
this.options.livekitRoomFactory,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Can contain duplicates. The connection manager will take care of this.
|
private connectionManager = createConnectionManager$({
|
||||||
private allTransports$ = this.scope.behavior(
|
scope: this.scope,
|
||||||
|
connectionFactory: this.connectionFactory,
|
||||||
|
inputTransports$: this.scope.behavior(
|
||||||
combineLatest(
|
combineLatest(
|
||||||
[this.localTransport$, this.membershipsAndTransports.transports$],
|
[this.localTransport$, this.membershipsAndTransports.transports$],
|
||||||
(localTransport, transports) => {
|
(localTransport, transports) => {
|
||||||
@@ -221,16 +240,9 @@ export class CallViewModel {
|
|||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
|
|
||||||
private connectionManager = createConnectionManager$({
|
|
||||||
scope: this.scope,
|
|
||||||
connectionFactory: this.connectionFactory,
|
|
||||||
inputTransports$: this.allTransports$,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private matrixLivekitMembers$ = createMatrixLivekitMembers$({
|
private matrixLivekitMembers$ = createMatrixLivekitMembers$({
|
||||||
scope: this.scope,
|
scope: this.scope,
|
||||||
membershipsWithTransport$:
|
membershipsWithTransport$:
|
||||||
@@ -273,6 +285,7 @@ export class CallViewModel {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
private localMatrixLivekitMemberUninitialized = {
|
private localMatrixLivekitMemberUninitialized = {
|
||||||
membership$: this.localRtcMembership$,
|
membership$: this.localRtcMembership$,
|
||||||
participant$: this.localMembership.participant$,
|
participant$: this.localMembership.participant$,
|
||||||
@@ -294,6 +307,7 @@ export class CallViewModel {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
|
// callLifecycle
|
||||||
|
|
||||||
private callLifecycle = createCallNotificationLifecycle$({
|
private callLifecycle = createCallNotificationLifecycle$({
|
||||||
scope: this.scope,
|
scope: this.scope,
|
||||||
@@ -309,6 +323,13 @@ export class CallViewModel {
|
|||||||
public autoLeave$ = this.callLifecycle.autoLeave$;
|
public autoLeave$ = this.callLifecycle.autoLeave$;
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
|
// matrixMemberMetadataStore
|
||||||
|
|
||||||
|
private matrixMemberMetadataStore = createMatrixMemberMetadata$(
|
||||||
|
this.scope,
|
||||||
|
this.scope.behavior(this.memberships$.pipe(map((mems) => mems.value))),
|
||||||
|
createRoomMembers$(this.scope, this.matrixRoom),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If there is a configuration error with the call (e.g. misconfigured E2EE).
|
* If there is a configuration error with the call (e.g. misconfigured E2EE).
|
||||||
@@ -402,12 +423,6 @@ export class CallViewModel {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
private matrixMemberMetadataStore = createMatrixMemberMetadata$(
|
|
||||||
this.scope,
|
|
||||||
this.scope.behavior(this.memberships$.pipe(map((mems) => mems.value))),
|
|
||||||
createRoomMembers$(this.scope, this.matrixRoom),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of user media (camera feeds) that we want tiles for.
|
* List of user media (camera feeds) that we want tiles for.
|
||||||
*/
|
*/
|
||||||
@@ -426,20 +441,23 @@ export class CallViewModel {
|
|||||||
{ value: matrixLivekitMembers },
|
{ value: matrixLivekitMembers },
|
||||||
duplicateTiles,
|
duplicateTiles,
|
||||||
]) {
|
]) {
|
||||||
|
let localParticipantId = undefined;
|
||||||
// add local member if available
|
// add local member if available
|
||||||
if (localMatrixLivekitMember) {
|
if (localMatrixLivekitMember) {
|
||||||
const {
|
const { userId, participant$, connection$, membership$ } =
|
||||||
|
localMatrixLivekitMember;
|
||||||
|
localParticipantId = `${userId}:${membership$.value.deviceId}`; // should be membership$.value.membershipID which is not optional
|
||||||
|
// const participantId = membership$.value.membershipID;
|
||||||
|
if (localParticipantId) {
|
||||||
|
for (let dup = 0; dup < 1 + duplicateTiles; dup++) {
|
||||||
|
yield {
|
||||||
|
keys: [
|
||||||
|
dup,
|
||||||
|
localParticipantId,
|
||||||
userId,
|
userId,
|
||||||
participant$,
|
participant$,
|
||||||
connection$,
|
connection$,
|
||||||
// membership$,
|
],
|
||||||
} = localMatrixLivekitMember;
|
|
||||||
const participantId = participant$.value?.identity; // should be membership$.value.membershipID which is not optional
|
|
||||||
// const participantId = membership$.value.membershipID;
|
|
||||||
if (participantId) {
|
|
||||||
for (let dup = 0; dup < 1 + duplicateTiles; dup++) {
|
|
||||||
yield {
|
|
||||||
keys: [dup, participantId, userId, participant$, connection$],
|
|
||||||
data: undefined,
|
data: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -450,11 +468,11 @@ export class CallViewModel {
|
|||||||
userId,
|
userId,
|
||||||
participant$,
|
participant$,
|
||||||
connection$,
|
connection$,
|
||||||
// membership$
|
membership$,
|
||||||
} of matrixLivekitMembers) {
|
} of matrixLivekitMembers) {
|
||||||
const participantId = participant$.value?.identity;
|
const participantId = `${userId}:${membership$.value.deviceId}`;
|
||||||
|
if (participantId === localParticipantId) continue;
|
||||||
// const participantId = membership$.value?.identity;
|
// const participantId = membership$.value?.identity;
|
||||||
if (!participantId) continue;
|
|
||||||
for (let dup = 0; dup < 1 + duplicateTiles; dup++) {
|
for (let dup = 0; dup < 1 + duplicateTiles; dup++) {
|
||||||
yield {
|
yield {
|
||||||
keys: [dup, participantId, userId, participant$, connection$],
|
keys: [dup, participantId, userId, participant$, connection$],
|
||||||
@@ -550,9 +568,8 @@ export class CallViewModel {
|
|||||||
* - There can be multiple participants for one Matrix user if they join from
|
* - There can be multiple participants for one Matrix user if they join from
|
||||||
* multiple devices.
|
* multiple devices.
|
||||||
*/
|
*/
|
||||||
// TODO KEEP THIS!! and adapt it to what our membershipManger returns
|
|
||||||
public readonly participantCount$ = this.scope.behavior(
|
public readonly participantCount$ = this.scope.behavior(
|
||||||
this.memberships$.pipe(map((ms) => ms.value.length)),
|
this.matrixLivekitMembers$.pipe(map((ms) => ms.value.length)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// only public to expose to the view.
|
// only public to expose to the view.
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
startWith,
|
startWith,
|
||||||
switchMap,
|
switchMap,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
import { type Behavior } from "../../Behavior";
|
import { type Behavior } from "../../Behavior";
|
||||||
import { type IConnectionManager } from "../remoteMembers/ConnectionManager";
|
import { type IConnectionManager } from "../remoteMembers/ConnectionManager";
|
||||||
@@ -52,7 +52,7 @@ import { PosthogAnalytics } from "../../../analytics/PosthogAnalytics.ts";
|
|||||||
import { MatrixRTCMode } from "../../../settings/settings.ts";
|
import { MatrixRTCMode } from "../../../settings/settings.ts";
|
||||||
import { Config } from "../../../config/Config.ts";
|
import { Config } from "../../../config/Config.ts";
|
||||||
import { type Connection } from "../remoteMembers/Connection.ts";
|
import { type Connection } from "../remoteMembers/Connection.ts";
|
||||||
|
const logger = rootLogger.getChild("[LocalMembership]");
|
||||||
export enum LivekitState {
|
export enum LivekitState {
|
||||||
Uninitialized = "uninitialized",
|
Uninitialized = "uninitialized",
|
||||||
Connecting = "connecting",
|
Connecting = "connecting",
|
||||||
@@ -323,10 +323,19 @@ export const createLocalMembership$ = ({
|
|||||||
!connectRequested ||
|
!connectRequested ||
|
||||||
state.matrix$.value.state !== MatrixState.Disconnected
|
state.matrix$.value.state !== MatrixState.Disconnected
|
||||||
) {
|
) {
|
||||||
logger.info("Waiting for transport to enter rtc session");
|
logger.info(
|
||||||
|
"Not yet connecting because: ",
|
||||||
|
"transport === null:",
|
||||||
|
transport === null,
|
||||||
|
"!connectRequested:",
|
||||||
|
!connectRequested,
|
||||||
|
"state.matrix$.value.state !== MatrixState.Disconnected:",
|
||||||
|
state.matrix$.value.state !== MatrixState.Disconnected,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.matrix$.next({ state: MatrixState.Connecting });
|
state.matrix$.next({ state: MatrixState.Connecting });
|
||||||
|
logger.info("Matrix State connecting");
|
||||||
enterRTCSession(matrixRTCSession, transport, options.value).catch(
|
enterRTCSession(matrixRTCSession, transport, options.value).catch(
|
||||||
(error) => {
|
(error) => {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
@@ -376,7 +385,9 @@ export const createLocalMembership$ = ({
|
|||||||
for (const p of publications) {
|
for (const p of publications) {
|
||||||
if (p.track?.isUpstreamPaused === true) {
|
if (p.track?.isUpstreamPaused === true) {
|
||||||
const kind = p.track.kind;
|
const kind = p.track.kind;
|
||||||
logger.log(`Resuming ${kind} track (MatrixRTC connection present)`);
|
logger.info(
|
||||||
|
`Resuming ${kind} track (MatrixRTC connection present)`,
|
||||||
|
);
|
||||||
p.track
|
p.track
|
||||||
.resumeUpstream()
|
.resumeUpstream()
|
||||||
.catch((e) =>
|
.catch((e) =>
|
||||||
@@ -391,7 +402,7 @@ export const createLocalMembership$ = ({
|
|||||||
for (const p of publications) {
|
for (const p of publications) {
|
||||||
if (p.track?.isUpstreamPaused === false) {
|
if (p.track?.isUpstreamPaused === false) {
|
||||||
const kind = p.track.kind;
|
const kind = p.track.kind;
|
||||||
logger.log(
|
logger.info(
|
||||||
`Pausing ${kind} track (uncertain MatrixRTC connection)`,
|
`Pausing ${kind} track (uncertain MatrixRTC connection)`,
|
||||||
);
|
);
|
||||||
p.track
|
p.track
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export function createConnectionManager$({
|
|||||||
connectionFactory,
|
connectionFactory,
|
||||||
inputTransports$,
|
inputTransports$,
|
||||||
}: Props): IConnectionManager {
|
}: Props): IConnectionManager {
|
||||||
const logger = rootLogger.getChild("ConnectionManager");
|
const logger = rootLogger.getChild("[ConnectionManager]");
|
||||||
|
|
||||||
const running$ = new BehaviorSubject(true);
|
const running$ = new BehaviorSubject(true);
|
||||||
scope.onEnd(() => running$.next(false));
|
scope.onEnd(() => running$.next(false));
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { Epoch, type ObservableScope } from "../../ObservableScope";
|
|||||||
import { type Connection } from "./Connection";
|
import { type Connection } from "./Connection";
|
||||||
import { generateItemsWithEpoch } from "../../../utils/observable";
|
import { generateItemsWithEpoch } from "../../../utils/observable";
|
||||||
|
|
||||||
const logger = rootLogger.getChild("MatrixLivekitMembers");
|
const logger = rootLogger.getChild("[MatrixLivekitMembers]");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a Matrix call member and their associated LiveKit participation.
|
* Represents a Matrix call member and their associated LiveKit participation.
|
||||||
|
|||||||
@@ -74,15 +74,7 @@ export class ObservableScope {
|
|||||||
// they will no longer re-emit their current value upon subscription. We want
|
// they will no longer re-emit their current value upon subscription. We want
|
||||||
// to support Observables that complete (for example `of({})`), so we have to
|
// to support Observables that complete (for example `of({})`), so we have to
|
||||||
// take care to not propagate the completion event.
|
// take care to not propagate the completion event.
|
||||||
setValue$
|
setValue$.pipe(this.bind(), distinctUntilChanged()).subscribe({
|
||||||
.pipe(
|
|
||||||
this.bind(),
|
|
||||||
distinctUntilChanged((a, b) => {
|
|
||||||
logger.log("distinctUntilChanged", a, b);
|
|
||||||
return a === b;
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.subscribe({
|
|
||||||
next(value) {
|
next(value) {
|
||||||
subject$.next(value);
|
subject$.next(value);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -76,6 +76,6 @@ export const createMemberships$ = (
|
|||||||
MatrixRTCSessionEvent.MembershipsChanged,
|
MatrixRTCSessionEvent.MembershipsChanged,
|
||||||
(_, memberships: CallMembership[]) => memberships,
|
(_, memberships: CallMembership[]) => memberships,
|
||||||
).pipe(trackEpoch()),
|
).pipe(trackEpoch()),
|
||||||
new Epoch([]),
|
new Epoch(matrixRTCSession.memberships),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import {
|
|||||||
mockRemoteParticipant,
|
mockRemoteParticipant,
|
||||||
} from "./test";
|
} from "./test";
|
||||||
|
|
||||||
export const localRtcMember = mockRtcMembership("@carol:example.org", "1111");
|
export const localRtcMember = mockRtcMembership("@local:example.org", "1111");
|
||||||
export const localRtcMemberDevice2 = mockRtcMembership(
|
export const localRtcMemberDevice2 = mockRtcMembership(
|
||||||
"@carol:example.org",
|
"@local:example.org",
|
||||||
"2222",
|
"2222",
|
||||||
);
|
);
|
||||||
export const local = mockMatrixRoomMember(localRtcMember);
|
export const local = mockMatrixRoomMember(localRtcMember);
|
||||||
|
|||||||
@@ -6,7 +6,14 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
import { map, type Observable, of, type SchedulerLike } from "rxjs";
|
import { map, type Observable, of, type SchedulerLike } from "rxjs";
|
||||||
import { type RunHelpers, TestScheduler } from "rxjs/testing";
|
import { type RunHelpers, TestScheduler } from "rxjs/testing";
|
||||||
import { expect, type MockedObject, onTestFinished, vi, vitest } from "vitest";
|
import {
|
||||||
|
expect,
|
||||||
|
type MockedObject,
|
||||||
|
type MockInstance,
|
||||||
|
onTestFinished,
|
||||||
|
vi,
|
||||||
|
vitest,
|
||||||
|
} from "vitest";
|
||||||
import {
|
import {
|
||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
type Room as MatrixRoom,
|
type Room as MatrixRoom,
|
||||||
@@ -269,6 +276,7 @@ export function mockLivekitRoom(
|
|||||||
}: { remoteParticipants$?: Observable<RemoteParticipant[]> } = {},
|
}: { remoteParticipants$?: Observable<RemoteParticipant[]> } = {},
|
||||||
): LivekitRoom {
|
): LivekitRoom {
|
||||||
const livekitRoom = {
|
const livekitRoom = {
|
||||||
|
options: {},
|
||||||
...mockEmitter(),
|
...mockEmitter(),
|
||||||
...room,
|
...room,
|
||||||
} as Partial<LivekitRoom> as LivekitRoom;
|
} as Partial<LivekitRoom> as LivekitRoom;
|
||||||
@@ -291,6 +299,7 @@ export function mockLocalParticipant(
|
|||||||
return {
|
return {
|
||||||
isLocal: true,
|
isLocal: true,
|
||||||
trackPublications: new Map(),
|
trackPublications: new Map(),
|
||||||
|
unpublishTracks: async () => Promise.resolve(),
|
||||||
getTrackPublication: () =>
|
getTrackPublication: () =>
|
||||||
({}) as Partial<LocalTrackPublication> as LocalTrackPublication,
|
({}) as Partial<LocalTrackPublication> as LocalTrackPublication,
|
||||||
...mockEmitter(),
|
...mockEmitter(),
|
||||||
@@ -331,6 +340,8 @@ export function mockRemoteParticipant(
|
|||||||
setVolume() {},
|
setVolume() {},
|
||||||
getTrackPublication: () =>
|
getTrackPublication: () =>
|
||||||
({}) as Partial<RemoteTrackPublication> as RemoteTrackPublication,
|
({}) as Partial<RemoteTrackPublication> as RemoteTrackPublication,
|
||||||
|
// this will only get used for `getTrackPublications().length`
|
||||||
|
getTrackPublications: () => [0],
|
||||||
...mockEmitter(),
|
...mockEmitter(),
|
||||||
...participant,
|
...participant,
|
||||||
} as RemoteParticipant;
|
} as RemoteParticipant;
|
||||||
@@ -363,13 +374,16 @@ export function createRemoteMedia(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mockConfig(config: Partial<ResolvedConfigOptions> = {}): void {
|
export function mockConfig(
|
||||||
vi.spyOn(Config, "get").mockReturnValue({
|
config: Partial<ResolvedConfigOptions> = {},
|
||||||
|
): MockInstance<() => ResolvedConfigOptions> {
|
||||||
|
const spy = vi.spyOn(Config, "get").mockReturnValue({
|
||||||
...DEFAULT_CONFIG,
|
...DEFAULT_CONFIG,
|
||||||
...config,
|
...config,
|
||||||
});
|
});
|
||||||
// simulate loading the config
|
// simulate loading the config
|
||||||
vi.spyOn(Config, "init").mockResolvedValue(void 0);
|
vi.spyOn(Config, "init").mockResolvedValue(void 0);
|
||||||
|
return spy;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MockRTCSession extends TypedEventEmitter<
|
export class MockRTCSession extends TypedEventEmitter<
|
||||||
|
|||||||
Reference in New Issue
Block a user