/* Copyright 2026 Lotus Guild SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ import { combineLatest, map, type Observable, of, switchMap } from "rxjs"; import { type UserMediaViewModel } from "../state/media/UserMediaViewModel"; import { type ScreenShareViewModel } from "../state/media/ScreenShareViewModel"; import { type MediaViewModel } from "../state/media/MediaViewModel"; import { type LocalUserMediaViewModel } from "../state/media/LocalUserMediaViewModel"; interface SpotlightAndPip { spotlight: MediaViewModel[]; pip$: Observable; } /** * [lotus #4] Manual spotlight override. * * Wraps upstream's auto-selected spotlight speaker so a host can pin a specific * participant (via the `io.lotus.focus_participant` widget action). Kept as a * pure function OUTSIDE CallViewModel so CallViewModel stays byte-close to * upstream and rebases cleanly: CallViewModel keeps its original * `spotlightSpeaker$` auto-selection unchanged and just routes the * screenshare/spotlight computation through this wrapper at one call point. * * Behaviour is IDENTICAL to upstream whenever `manualSpotlightUserId$` stays * `null` (the default). When it names a participant that is still present: * - with no screenshare, that participant is spotlighted instead of the * auto-selected active speaker; * - during a screenshare, that participant's camera is surfaced in the * spotlight ALONGSIDE the shared screen (#4 / A5 "focus camera during * screenshare"), and the redundant PiP is hidden. * * @param autoSpotlightSpeaker$ upstream's speaker-follows auto selection * @param manualSpotlightUserId$ host-pinned userId, or `null` for auto (default) * @param screenShares$ current screen-share view models * @param localUserMediaForPip$ local media suitable for the PiP * @param userMedia$ all user media in the call (to resolve the pinned userId) */ export function overrideSpotlight$( autoSpotlightSpeaker$: Observable, manualSpotlightUserId$: Observable, screenShares$: Observable, localUserMediaForPip$: Observable, userMedia$: Observable, ): Observable { // The effective spotlight speaker: the host-pinned participant when set and // still present, otherwise upstream's auto-selected speaker. const spotlightSpeaker$ = combineLatest([ autoSpotlightSpeaker$, manualSpotlightUserId$, userMedia$, ]).pipe( map(([auto, manualUserId, mediaItems]) => { if (manualUserId !== null) { const pinned = mediaItems.find((m) => m.userId === manualUserId); if (pinned) return pinned; } return auto; }), ); return screenShares$.pipe( switchMap((screenShares) => { if (screenShares.length > 0) // During a screenshare, if the host has explicitly pinned a // participant, surface that camera in the spotlight alongside the // shared screen (the whole point of "focus camera during screenshare"). // With no manual pin this is unchanged: the screenshare alone is // spotlighted. return combineLatest([manualSpotlightUserId$, userMedia$]).pipe( map(([manualUserId, mediaItems]) => { const pinned = manualUserId !== null ? mediaItems.find((m) => m.userId === manualUserId) : undefined; return pinned ? { spotlight: [...screenShares, pinned], pip$: of(undefined) } : { spotlight: screenShares, pip$: spotlightSpeaker$ }; }), ); return spotlightSpeaker$.pipe( map((speaker) => ({ spotlight: speaker ? [speaker] : [], // Hide PiP if redundant (i.e. if local user is already in spotlight) pip$: localUserMediaForPip$.pipe( map((m) => (m === speaker ? undefined : m)), ), })), ); }), ); }