mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
Test and document the MemberMedia track list
Unit tests for tracks$ and trackBySource$, an end-to-end check of isActive$ through a mute cycle (skipped on Chromium, whose fake microphone the SFU never hears), the architecture document brought in line with the track list, and the changed behaviour recorded in the plan. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
18e0065086
commit
63ccfa50be
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
Copyright 2026 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 { EventEmitter } from "events";
|
||||
import {
|
||||
ParticipantEvent,
|
||||
type RemoteParticipant,
|
||||
type Room as LivekitRoom,
|
||||
Track,
|
||||
type TrackPublication,
|
||||
} from "livekit-client";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { E2eeType } from "../encryption";
|
||||
import { testScope } from "../utils/test";
|
||||
import { createLivekitMemberMedia } from "./LivekitMemberMedia";
|
||||
|
||||
describe("createLivekitMemberMedia", () => {
|
||||
it("lists one stable track per publication, in publication order", () => {
|
||||
const { participant, publications, room, emitter } = fakes();
|
||||
const media = createLivekitMemberMedia(testScope(), participant, room, {
|
||||
kind: E2eeType.NONE,
|
||||
});
|
||||
expect(media.tracks$.value).toEqual([]);
|
||||
|
||||
const microphone = publish(publications, "TR_1", Track.Source.Microphone);
|
||||
const camera = publish(publications, "TR_2", Track.Source.Camera);
|
||||
emitter.emit(ParticipantEvent.TrackPublished, camera);
|
||||
const [first, second] = media.tracks$.value;
|
||||
expect(first.source).toBe("microphone");
|
||||
expect(second.source).toBe("camera");
|
||||
|
||||
// An unrelated media event keeps the same objects
|
||||
microphone.isMuted = true;
|
||||
emitter.emit(ParticipantEvent.TrackMuted, microphone);
|
||||
expect(media.tracks$.value[0]).toBe(first);
|
||||
expect(media.tracks$.value[1]).toBe(second);
|
||||
|
||||
publications.delete("TR_1");
|
||||
emitter.emit(ParticipantEvent.TrackUnpublished, microphone);
|
||||
expect(media.tracks$.value).toEqual([second]);
|
||||
});
|
||||
|
||||
it("includes a track published without a source", () => {
|
||||
const { participant, publications, room, emitter } = fakes();
|
||||
const media = createLivekitMemberMedia(testScope(), participant, room, {
|
||||
kind: E2eeType.NONE,
|
||||
});
|
||||
emitter.emit(
|
||||
ParticipantEvent.TrackPublished,
|
||||
publish(publications, "TR_1", Track.Source.Unknown),
|
||||
);
|
||||
expect(media.tracks$.value.map((t) => t.source)).toEqual(["unknown"]);
|
||||
});
|
||||
});
|
||||
|
||||
type FakePublication = { isMuted: boolean } & Omit<TrackPublication, "isMuted">;
|
||||
|
||||
function publish(
|
||||
publications: Map<string, TrackPublication>,
|
||||
trackSid: string,
|
||||
source: Track.Source,
|
||||
): FakePublication {
|
||||
const publication = Object.assign(new EventEmitter(), {
|
||||
trackSid,
|
||||
source,
|
||||
kind:
|
||||
source === Track.Source.Camera || source === Track.Source.ScreenShare
|
||||
? Track.Kind.Video
|
||||
: Track.Kind.Audio,
|
||||
isMuted: false,
|
||||
isEncrypted: true,
|
||||
track: undefined,
|
||||
}) as unknown as FakePublication;
|
||||
publications.set(trackSid, publication as unknown as TrackPublication);
|
||||
return publication;
|
||||
}
|
||||
|
||||
function fakes(): {
|
||||
participant: RemoteParticipant;
|
||||
publications: Map<string, TrackPublication>;
|
||||
room: LivekitRoom;
|
||||
emitter: EventEmitter;
|
||||
} {
|
||||
const emitter = new EventEmitter();
|
||||
const publications = new Map<string, TrackPublication>();
|
||||
const participant = Object.assign(emitter, {
|
||||
isLocal: false,
|
||||
isSpeaking: false,
|
||||
identity: "@alice:example.org:DEVICE",
|
||||
trackPublications: publications,
|
||||
getTrackPublication: (source: Track.Source) =>
|
||||
[...publications.values()].find((p) => p.source === source),
|
||||
}) as unknown as RemoteParticipant;
|
||||
const room = new EventEmitter() as unknown as LivekitRoom;
|
||||
return { participant, publications, room, emitter };
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2026 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 { BehaviorSubject } from "rxjs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { type MediaTrack } from "../api";
|
||||
import { testScope } from "./test";
|
||||
import { trackBySource$ } from "./tracks";
|
||||
|
||||
const camera = { source: "camera", kind: "video", id: "1" } as MediaTrack;
|
||||
const microphone = {
|
||||
source: "microphone",
|
||||
kind: "audio",
|
||||
id: "2",
|
||||
} as MediaTrack;
|
||||
|
||||
describe("trackBySource$", () => {
|
||||
it("picks the track of a source and follows the list", () => {
|
||||
const tracks$ = new BehaviorSubject<MediaTrack[]>([]);
|
||||
const camera$ = trackBySource$(testScope(), tracks$, "camera");
|
||||
expect(camera$.value).toBeUndefined();
|
||||
tracks$.next([microphone, camera]);
|
||||
expect(camera$.value).toBe(camera);
|
||||
tracks$.next([microphone]);
|
||||
expect(camera$.value).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not emit when the list changes elsewhere", () => {
|
||||
const tracks$ = new BehaviorSubject<MediaTrack[]>([camera]);
|
||||
const camera$ = trackBySource$(testScope(), tracks$, "camera");
|
||||
const emissions: unknown[] = [];
|
||||
camera$.subscribe((t) => emissions.push(t));
|
||||
tracks$.next([camera, microphone]);
|
||||
expect(emissions).toEqual([camera]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user