mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-10 23:25:52 +00:00
Extract the spotlight and PiP choice into a pure function
This commit is contained in:
@@ -104,7 +104,7 @@ import {
|
|||||||
type SpotlightPortraitLayoutMedia,
|
type SpotlightPortraitLayoutMedia,
|
||||||
type WindowMode,
|
type WindowMode,
|
||||||
} from "../layout-types.ts";
|
} from "../layout-types.ts";
|
||||||
import { chooseSpotlightSpeaker } from "../layoutMedia.ts";
|
import { chooseSpotlightSpeaker, computeSpotlight } from "../layoutMedia.ts";
|
||||||
import { ElementCallError, UnknownCallError } from "../../utils/errors.ts";
|
import { ElementCallError, UnknownCallError } from "../../utils/errors.ts";
|
||||||
import { type Epoch, type ObservableScope } from "../ObservableScope.ts";
|
import { type Epoch, type ObservableScope } from "../ObservableScope.ts";
|
||||||
import { createHomeserverConnected$ } from "./localMember/HomeserverConnected.ts";
|
import { createHomeserverConnected$ } from "./localMember/HomeserverConnected.ts";
|
||||||
@@ -1029,33 +1029,12 @@ export function createCallViewModel$(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const spotlightAndPip$ = scope.behavior<{
|
const spotlightAndPip$ = scope.behavior(
|
||||||
spotlight: MediaViewModel[];
|
combineLatest(
|
||||||
pip$: Observable<UserMediaViewModel | undefined>;
|
[ringingMedia$, screenShares$, spotlightSpeaker$, localUserMediaForPip$],
|
||||||
}>(
|
(ringing, screenShares, speaker, localPip) =>
|
||||||
ringingMedia$.pipe(
|
computeSpotlight({ ringing, screenShares, speaker, localPip }),
|
||||||
switchMap((ringingMedia) => {
|
).pipe(distinctUntilChanged(layoutShallowEquals)),
|
||||||
if (ringingMedia !== null)
|
|
||||||
return of({ spotlight: [ringingMedia], pip$: localUserMediaForPip$ });
|
|
||||||
|
|
||||||
return screenShares$.pipe(
|
|
||||||
switchMap((screenShares) => {
|
|
||||||
if (screenShares.length > 0)
|
|
||||||
return of({ 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)),
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const spotlight$ = scope.behavior<MediaViewModel[]>(
|
const spotlight$ = scope.behavior<MediaViewModel[]>(
|
||||||
@@ -1147,16 +1126,12 @@ export function createCallViewModel$(
|
|||||||
edgeToEdge: boolean,
|
edgeToEdge: boolean,
|
||||||
): Observable<SpotlightExpandedLayoutMedia> =>
|
): Observable<SpotlightExpandedLayoutMedia> =>
|
||||||
spotlightAndPip$.pipe(
|
spotlightAndPip$.pipe(
|
||||||
switchMap(({ spotlight, pip$ }) =>
|
map(({ spotlight, pip }) => ({
|
||||||
pip$.pipe(
|
type: "spotlight-expanded" as const,
|
||||||
map((pip) => ({
|
edgeToEdge,
|
||||||
type: "spotlight-expanded" as const,
|
spotlight,
|
||||||
edgeToEdge,
|
pip,
|
||||||
spotlight,
|
})),
|
||||||
pip: pip ?? undefined,
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const oneOnOneLayoutMedia$: Behavior<{
|
const oneOnOneLayoutMedia$: Behavior<{
|
||||||
|
|||||||
@@ -7,15 +7,22 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { chooseSpotlightSpeaker } from "./layoutMedia";
|
import { chooseSpotlightSpeaker, computeSpotlight } from "./layoutMedia";
|
||||||
import { type LocalUserMediaViewModel } from "./media/LocalUserMediaViewModel";
|
import { type LocalUserMediaViewModel } from "./media/LocalUserMediaViewModel";
|
||||||
import { type RemoteUserMediaViewModel } from "./media/RemoteUserMediaViewModel";
|
import { type RemoteUserMediaViewModel } from "./media/RemoteUserMediaViewModel";
|
||||||
|
import { type RingingMediaViewModel } from "./media/RingingMediaViewModel";
|
||||||
|
import { type ScreenShareViewModel } from "./media/ScreenShareViewModel";
|
||||||
|
|
||||||
// The layout functions only look at `type` and `local`, so stubs suffice
|
// The layout functions only look at `type` and `local`, so stubs suffice
|
||||||
const local = { type: "user", local: true } as LocalUserMediaViewModel;
|
const local = { type: "user", local: true } as LocalUserMediaViewModel;
|
||||||
const alice = { type: "user", local: false } as RemoteUserMediaViewModel;
|
const alice = { type: "user", local: false } as RemoteUserMediaViewModel;
|
||||||
const bob = { type: "user", local: false } as RemoteUserMediaViewModel;
|
const bob = { type: "user", local: false } as RemoteUserMediaViewModel;
|
||||||
const carol = { type: "user", local: false } as RemoteUserMediaViewModel;
|
const carol = { type: "user", local: false } as RemoteUserMediaViewModel;
|
||||||
|
const screenShare = {
|
||||||
|
type: "screen share",
|
||||||
|
local: false,
|
||||||
|
} as ScreenShareViewModel;
|
||||||
|
const ringing = { type: "ringing" } as RingingMediaViewModel;
|
||||||
|
|
||||||
describe("chooseSpotlightSpeaker", () => {
|
describe("chooseSpotlightSpeaker", () => {
|
||||||
const items = (
|
const items = (
|
||||||
@@ -60,3 +67,46 @@ describe("chooseSpotlightSpeaker", () => {
|
|||||||
expect(chooseSpotlightSpeaker(local, items())).toBe(alice);
|
expect(chooseSpotlightSpeaker(local, items())).toBe(alice);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("computeSpotlight", () => {
|
||||||
|
it("spotlights ringing media with the local user in the PiP", () => {
|
||||||
|
expect(
|
||||||
|
computeSpotlight({
|
||||||
|
ringing,
|
||||||
|
screenShares: [screenShare],
|
||||||
|
speaker: alice,
|
||||||
|
localPip: local,
|
||||||
|
}),
|
||||||
|
).toEqual({ spotlight: [ringing], pip: local });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("spotlights screen shares with the speaker in the PiP", () => {
|
||||||
|
expect(
|
||||||
|
computeSpotlight({
|
||||||
|
ringing: null,
|
||||||
|
screenShares: [screenShare],
|
||||||
|
speaker: alice,
|
||||||
|
localPip: local,
|
||||||
|
}),
|
||||||
|
).toEqual({ spotlight: [screenShare], pip: alice });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("otherwise spotlights the speaker and hides a redundant PiP", () => {
|
||||||
|
expect(
|
||||||
|
computeSpotlight({
|
||||||
|
ringing: null,
|
||||||
|
screenShares: [],
|
||||||
|
speaker: alice,
|
||||||
|
localPip: local,
|
||||||
|
}),
|
||||||
|
).toEqual({ spotlight: [alice], pip: local });
|
||||||
|
expect(
|
||||||
|
computeSpotlight({
|
||||||
|
ringing: null,
|
||||||
|
screenShares: [],
|
||||||
|
speaker: local,
|
||||||
|
localPip: local,
|
||||||
|
}),
|
||||||
|
).toEqual({ spotlight: [local], pip: undefined });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { type MediaViewModel } from "./media/MediaViewModel.ts";
|
||||||
|
import { type RingingMediaViewModel } from "./media/RingingMediaViewModel.ts";
|
||||||
|
import { type ScreenShareViewModel } from "./media/ScreenShareViewModel.ts";
|
||||||
|
import { type UserMediaViewModel } from "./media/UserMediaViewModel.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Picks who to spotlight based on who is speaking, preferring the previous
|
* Picks who to spotlight based on who is speaking, preferring the previous
|
||||||
* pick so that the spotlight doesn't flip eagerly between speakers.
|
* pick so that the spotlight doesn't flip eagerly between speakers.
|
||||||
@@ -31,3 +36,33 @@ export function chooseSpotlightSpeaker<M extends { local: boolean }>(
|
|||||||
items.find((i) => i.media.local)?.media
|
items.find((i) => i.media.local)?.media
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SpotlightInputs {
|
||||||
|
ringing: RingingMediaViewModel | null;
|
||||||
|
screenShares: ScreenShareViewModel[];
|
||||||
|
speaker: UserMediaViewModel | undefined;
|
||||||
|
/** Local user media suitable for showing in a PiP, if any. */
|
||||||
|
localPip: UserMediaViewModel | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decides what goes in the spotlight, and what goes in the picture-in-picture
|
||||||
|
* tile of the expanded spotlight layout.
|
||||||
|
*/
|
||||||
|
export function computeSpotlight({
|
||||||
|
ringing,
|
||||||
|
screenShares,
|
||||||
|
speaker,
|
||||||
|
localPip,
|
||||||
|
}: SpotlightInputs): {
|
||||||
|
spotlight: MediaViewModel[];
|
||||||
|
pip: UserMediaViewModel | undefined;
|
||||||
|
} {
|
||||||
|
if (ringing !== null) return { spotlight: [ringing], pip: localPip };
|
||||||
|
if (screenShares.length > 0) return { spotlight: screenShares, pip: speaker };
|
||||||
|
return {
|
||||||
|
spotlight: speaker ? [speaker] : [],
|
||||||
|
// Hide PiP if redundant (i.e. if local user is already in spotlight)
|
||||||
|
pip: localPip === speaker ? undefined : localPip,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user