/* 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, TrackEvent, type TrackPublication, } from "livekit-client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ObservableScope } from "../../../src/state/ObservableScope"; import { type AudioMediaTrack } from "../api"; import { createLivekitMediaTrack } from "./LivekitMediaTrack"; describe("createLivekitMediaTrack", () => { let scope: ObservableScope; beforeEach(() => { scope = new ObservableScope(); }); afterEach(() => scope.end()); it("describes the publication", () => { const { participant, publication, room } = fakes(); const track = createLivekitMediaTrack( scope, participant, publication as unknown as TrackPublication, room, "camera", ); expect(track.kind).toBe("video"); expect(track.source).toBe("camera"); expect(track.id).toBe("TR_1"); expect(track.encrypted$.value).toBe(true); }); it("attaches elements to the track, even one that arrives later", () => { const { participant, publication, room, emitter } = fakes({ track: undefined, }); const element = document.createElement("video"); const track = createLivekitMediaTrack( scope, participant, publication as unknown as TrackPublication, room, "camera", ); track.attach(element); track.attach(element); const livekitTrack = fakeTrack(); publication.track = livekitTrack; emitter.emit(TrackEvent.Subscribed, livekitTrack); expect(livekitTrack.attach).toHaveBeenCalledTimes(1); expect(livekitTrack.attach).toHaveBeenCalledWith(element); track.detach(element); expect(livekitTrack.detach).toHaveBeenCalledWith(element); }); it("detaches everything when its scope ends", () => { const { participant, publication, room } = fakes(); const element = document.createElement("video"); const scope = new ObservableScope(); const track = createLivekitMediaTrack( scope, participant, publication as unknown as TrackPublication, room, "camera", ); track.attach(element); scope.end(); expect(publication.track!.detach).toHaveBeenCalledWith(element); }); it("follows the mute state", () => { const { participant, publication, room, emitter } = fakes(); const track = createLivekitMediaTrack( scope, participant, publication as unknown as TrackPublication, room, "camera", ); expect(track.muted$.value).toBe(false); publication.isMuted = true; emitter.emit(ParticipantEvent.TrackMuted, publication); expect(track.muted$.value).toBe(true); }); it("scales a remote member's volume on the participant", () => { const { participant, publication, room } = fakes({ kind: Track.Kind.Audio, }); const track = createLivekitMediaTrack( scope, participant, publication as unknown as TrackPublication, room, "microphone", ) as AudioMediaTrack; track.setVolume(0.5); expect(participant.setVolume).toHaveBeenCalledWith( 0.5, Track.Source.Microphone, ); }); }); type FakeTrack = Track & { attach: ReturnType; detach: ReturnType; }; type FakePublication = Omit & { track: FakeTrack | undefined; isMuted: boolean; }; function fakeTrack(): FakeTrack { return { attach: vi.fn(), detach: vi.fn() } as unknown as FakeTrack; } function fakes({ track = fakeTrack(), kind = Track.Kind.Video, }: { track?: FakeTrack | undefined; kind?: Track.Kind } = {}): { participant: RemoteParticipant & { setVolume: ReturnType }; publication: FakePublication; room: LivekitRoom; emitter: EventEmitter; } { const emitter = new EventEmitter(); const participant = Object.assign(emitter, { isLocal: false, identity: "@alice:example.org:DEVICE", setVolume: vi.fn(), getTrackPublication: (): TrackPublication => publication as unknown as TrackPublication, }) as unknown as RemoteParticipant & { setVolume: ReturnType }; const publication = Object.assign(emitter, { kind, trackSid: "TR_1", isMuted: false, isEncrypted: true, track, }) as unknown as FakePublication; const room = new EventEmitter() as unknown as LivekitRoom; return { participant, publication, room, emitter }; }