add more test for publisher
This commit is contained in:
@@ -79,7 +79,7 @@ test("Should show error screen if call creation is restricted", async ({
|
|||||||
}) => {
|
}) => {
|
||||||
test.skip(
|
test.skip(
|
||||||
browserName === "firefox",
|
browserName === "firefox",
|
||||||
"The test to check the video visibility is not working in Firefox CI environment. looks like video is disabled?",
|
"The is test is not working on firefox CI environment.",
|
||||||
);
|
);
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
|
|||||||
138
src/state/CallViewModel/localMember/Publisher.test.ts
Normal file
138
src/state/CallViewModel/localMember/Publisher.test.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
afterEach,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
type Mock,
|
||||||
|
vi,
|
||||||
|
} from "vitest";
|
||||||
|
import { ConnectionState as LivekitConenctionState } from "livekit-client";
|
||||||
|
import { type BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
|
import { ObservableScope } from "../../ObservableScope";
|
||||||
|
import { constant } from "../../Behavior";
|
||||||
|
import {
|
||||||
|
mockLivekitRoom,
|
||||||
|
mockLocalParticipant,
|
||||||
|
mockMediaDevices,
|
||||||
|
} from "../../../utils/test";
|
||||||
|
import { Publisher } from "./Publisher";
|
||||||
|
import {
|
||||||
|
type Connection,
|
||||||
|
type ConnectionState,
|
||||||
|
} from "../remoteMembers/Connection";
|
||||||
|
import { type MuteStates } from "../../MuteStates";
|
||||||
|
import { FailToStartLivekitConnection } from "../../../utils/errors";
|
||||||
|
|
||||||
|
describe("Publisher", () => {
|
||||||
|
let scope: ObservableScope;
|
||||||
|
let connection: Connection;
|
||||||
|
let muteStates: MuteStates;
|
||||||
|
beforeEach(() => {
|
||||||
|
muteStates = {
|
||||||
|
audio: {
|
||||||
|
enabled$: constant(false),
|
||||||
|
unsetHandler: vi.fn(),
|
||||||
|
setHandler: vi.fn(),
|
||||||
|
},
|
||||||
|
video: {
|
||||||
|
enabled$: constant(false),
|
||||||
|
unsetHandler: vi.fn(),
|
||||||
|
setHandler: vi.fn(),
|
||||||
|
},
|
||||||
|
} as unknown as MuteStates;
|
||||||
|
scope = new ObservableScope();
|
||||||
|
connection = {
|
||||||
|
state$: constant({
|
||||||
|
state: "ConnectedToLkRoom",
|
||||||
|
livekitConnectionState$: constant(LivekitConenctionState.Connected),
|
||||||
|
}),
|
||||||
|
livekitRoom: mockLivekitRoom({
|
||||||
|
localParticipant: mockLocalParticipant({}),
|
||||||
|
}),
|
||||||
|
} as unknown as Connection;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => scope.end());
|
||||||
|
|
||||||
|
it("throws if livekit room could not publish", async () => {
|
||||||
|
const publisher = new Publisher(
|
||||||
|
scope,
|
||||||
|
connection,
|
||||||
|
mockMediaDevices({}),
|
||||||
|
muteStates,
|
||||||
|
constant({ supported: false, processor: undefined }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// should do nothing if no tracks have been created yet.
|
||||||
|
await publisher.startPublishing();
|
||||||
|
expect(
|
||||||
|
connection.livekitRoom.localParticipant.publishTrack,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await expect(publisher.createAndSetupTracks()).rejects.toThrow(
|
||||||
|
Error("audio and video is false"),
|
||||||
|
);
|
||||||
|
|
||||||
|
(muteStates.audio.enabled$ as BehaviorSubject<boolean>).next(true);
|
||||||
|
|
||||||
|
(
|
||||||
|
connection.livekitRoom.localParticipant.createTracks as Mock
|
||||||
|
).mockResolvedValue([{}, {}]);
|
||||||
|
|
||||||
|
await expect(publisher.createAndSetupTracks()).resolves.not.toThrow();
|
||||||
|
expect(
|
||||||
|
connection.livekitRoom.localParticipant.createTracks,
|
||||||
|
).toHaveBeenCalledOnce();
|
||||||
|
|
||||||
|
// failiour due to localParticipant.publishTrack
|
||||||
|
(
|
||||||
|
connection.livekitRoom.localParticipant.publishTrack as Mock
|
||||||
|
).mockRejectedValue(Error("testError"));
|
||||||
|
|
||||||
|
await expect(publisher.startPublishing()).rejects.toThrow(
|
||||||
|
new FailToStartLivekitConnection("testError"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// does not try other conenction after the first one failed
|
||||||
|
expect(
|
||||||
|
connection.livekitRoom.localParticipant.publishTrack,
|
||||||
|
).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// failiour due to connection.state$
|
||||||
|
const beforeState = connection.state$.value;
|
||||||
|
(connection.state$ as BehaviorSubject<ConnectionState>).next({
|
||||||
|
state: "FailedToStart",
|
||||||
|
error: Error("testStartError"),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(publisher.startPublishing()).rejects.toThrow(
|
||||||
|
new FailToStartLivekitConnection("testStartError"),
|
||||||
|
);
|
||||||
|
(connection.state$ as BehaviorSubject<ConnectionState>).next(beforeState);
|
||||||
|
|
||||||
|
// does not try other conenction after the first one failed
|
||||||
|
expect(
|
||||||
|
connection.livekitRoom.localParticipant.publishTrack,
|
||||||
|
).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// success case
|
||||||
|
(
|
||||||
|
connection.livekitRoom.localParticipant.publishTrack as Mock
|
||||||
|
).mockResolvedValue({});
|
||||||
|
|
||||||
|
await expect(publisher.startPublishing()).resolves.not.toThrow();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
connection.livekitRoom.localParticipant.publishTrack,
|
||||||
|
).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -56,7 +56,7 @@ export class Publisher {
|
|||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
private scope: ObservableScope,
|
private scope: ObservableScope,
|
||||||
private connection: Connection,
|
private connection: Pick<Connection, "livekitRoom" | "state$">, //setE2EEEnabled,
|
||||||
devices: MediaDevices,
|
devices: MediaDevices,
|
||||||
private readonly muteStates: MuteStates,
|
private readonly muteStates: MuteStates,
|
||||||
trackerProcessorState$: Behavior<ProcessorState>,
|
trackerProcessorState$: Behavior<ProcessorState>,
|
||||||
@@ -160,7 +160,7 @@ export class Publisher {
|
|||||||
reject(
|
reject(
|
||||||
s.error instanceof ElementCallError
|
s.error instanceof ElementCallError
|
||||||
? s.error
|
? s.error
|
||||||
: new FailToStartLivekitConnection(),
|
: new FailToStartLivekitConnection(s.error.message),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -180,17 +180,16 @@ export class Publisher {
|
|||||||
// with a timeout.
|
// with a timeout.
|
||||||
await lkRoom.localParticipant.publishTrack(track).catch((error) => {
|
await lkRoom.localParticipant.publishTrack(track).catch((error) => {
|
||||||
this.logger?.error("Failed to publish track", error);
|
this.logger?.error("Failed to publish track", error);
|
||||||
|
throw new FailToStartLivekitConnection(
|
||||||
|
error instanceof Error ? error.message : error,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: check if the connection is still active? and break the loop if not?
|
|
||||||
}
|
}
|
||||||
this._publishing$.next(true);
|
this._publishing$.next(true);
|
||||||
return this.tracks$.value;
|
return this.tracks$.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stopPublishing(): Promise<void> {
|
public async stopPublishing(): Promise<void> {
|
||||||
// TODO-MULTI-SFU: Move these calls back to ObservableScope.onEnd once scope
|
|
||||||
// actually has the right lifetime
|
|
||||||
this.muteStates.audio.unsetHandler();
|
this.muteStates.audio.unsetHandler();
|
||||||
this.muteStates.video.unsetHandler();
|
this.muteStates.video.unsetHandler();
|
||||||
|
|
||||||
@@ -246,6 +245,9 @@ export class Publisher {
|
|||||||
// the process of being restarted.
|
// the process of being restarted.
|
||||||
activeMicTrack.mediaStreamTrack.readyState !== "ended"
|
activeMicTrack.mediaStreamTrack.readyState !== "ended"
|
||||||
) {
|
) {
|
||||||
|
this.logger?.info(
|
||||||
|
"Restarting audio device track due to active media device changed (workaroundRestartAudioInputTrackChrome)",
|
||||||
|
);
|
||||||
// Restart the track, which will cause Livekit to do another
|
// Restart the track, which will cause Livekit to do another
|
||||||
// getUserMedia() call with deviceId: default to get the *new* default device.
|
// getUserMedia() call with deviceId: default to get the *new* default device.
|
||||||
// Note that room.switchActiveDevice() won't work: Livekit will ignore it because
|
// Note that room.switchActiveDevice() won't work: Livekit will ignore it because
|
||||||
|
|||||||
@@ -136,12 +136,12 @@ export class FailToGetOpenIdToken extends ElementCallError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class FailToStartLivekitConnection extends ElementCallError {
|
export class FailToStartLivekitConnection extends ElementCallError {
|
||||||
public constructor() {
|
public constructor(e?: string) {
|
||||||
super(
|
super(
|
||||||
t("error.failed_to_start_livekit"),
|
t("error.failed_to_start_livekit"),
|
||||||
ErrorCode.FAILED_TO_START_LIVEKIT,
|
ErrorCode.FAILED_TO_START_LIVEKIT,
|
||||||
ErrorCategory.NETWORK_CONNECTIVITY,
|
ErrorCategory.NETWORK_CONNECTIVITY,
|
||||||
undefined,
|
e,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,6 +284,8 @@ export function mockLivekitRoom(
|
|||||||
): LivekitRoom {
|
): LivekitRoom {
|
||||||
const livekitRoom = {
|
const livekitRoom = {
|
||||||
options: {},
|
options: {},
|
||||||
|
setE2EEEnabled: vi.fn(),
|
||||||
|
|
||||||
...mockEmitter(),
|
...mockEmitter(),
|
||||||
...room,
|
...room,
|
||||||
} as Partial<LivekitRoom> as LivekitRoom;
|
} as Partial<LivekitRoom> as LivekitRoom;
|
||||||
@@ -306,7 +308,9 @@ export function mockLocalParticipant(
|
|||||||
return {
|
return {
|
||||||
isLocal: true,
|
isLocal: true,
|
||||||
trackPublications: new Map(),
|
trackPublications: new Map(),
|
||||||
unpublishTracks: async () => Promise.resolve(),
|
publishTrack: vi.fn(),
|
||||||
|
unpublishTracks: vi.fn(),
|
||||||
|
createTracks: vi.fn(),
|
||||||
getTrackPublication: () =>
|
getTrackPublication: () =>
|
||||||
({}) as Partial<LocalTrackPublication> as LocalTrackPublication,
|
({}) as Partial<LocalTrackPublication> as LocalTrackPublication,
|
||||||
...mockEmitter(),
|
...mockEmitter(),
|
||||||
|
|||||||
Reference in New Issue
Block a user