mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-10 23:25:52 +00:00
Build the layout media from one snapshot of its inputs
This commit is contained in:
@@ -30,7 +30,6 @@ import {
|
|||||||
scan,
|
scan,
|
||||||
startWith,
|
startWith,
|
||||||
Subject,
|
Subject,
|
||||||
switchAll,
|
|
||||||
switchMap,
|
switchMap,
|
||||||
switchScan,
|
switchScan,
|
||||||
take,
|
take,
|
||||||
@@ -148,6 +147,7 @@ import {
|
|||||||
import { Publisher } from "./localMember/Publisher.ts";
|
import { Publisher } from "./localMember/Publisher.ts";
|
||||||
import { type Connection } from "./remoteMembers/Connection.ts";
|
import { type Connection } from "./remoteMembers/Connection.ts";
|
||||||
import {
|
import {
|
||||||
|
type LayoutMode,
|
||||||
type LayoutSwitchViewModel,
|
type LayoutSwitchViewModel,
|
||||||
createLayoutSwitchViewModel,
|
createLayoutSwitchViewModel,
|
||||||
} from "../LayoutSwitchViewModel.ts";
|
} from "../LayoutSwitchViewModel.ts";
|
||||||
@@ -1092,52 +1092,7 @@ export function createCallViewModel$(
|
|||||||
hasRemoteScreenShares$,
|
hasRemoteScreenShares$,
|
||||||
);
|
);
|
||||||
|
|
||||||
const gridLayoutMedia$: Observable<GridLayoutMedia> = combineLatest(
|
const oneOnOneLayoutMedia$: Behavior<OneOnOneMedia | null> = scope.behavior(
|
||||||
[grid$, spotlight$],
|
|
||||||
(grid, spotlight) => ({
|
|
||||||
type: "grid",
|
|
||||||
edgeToEdge: false,
|
|
||||||
spotlight: spotlight.some((vm) => vm.type === "screen share")
|
|
||||||
? spotlight
|
|
||||||
: undefined,
|
|
||||||
grid,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const spotlightLandscapeLayoutMedia$ = (
|
|
||||||
edgeToEdge: boolean,
|
|
||||||
): Observable<SpotlightLandscapeLayoutMedia> =>
|
|
||||||
combineLatest([grid$, spotlight$], (grid, spotlight) => ({
|
|
||||||
type: "spotlight-landscape",
|
|
||||||
edgeToEdge,
|
|
||||||
spotlight,
|
|
||||||
grid,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const spotlightPortraitLayoutMedia$: Observable<SpotlightPortraitLayoutMedia> =
|
|
||||||
combineLatest([grid$, spotlight$], (grid, spotlight) => ({
|
|
||||||
type: "spotlight-portrait",
|
|
||||||
edgeToEdge: false,
|
|
||||||
spotlight,
|
|
||||||
grid,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const spotlightExpandedLayoutMedia$ = (
|
|
||||||
edgeToEdge: boolean,
|
|
||||||
): Observable<SpotlightExpandedLayoutMedia> =>
|
|
||||||
spotlightAndPip$.pipe(
|
|
||||||
map(({ spotlight, pip }) => ({
|
|
||||||
type: "spotlight-expanded" as const,
|
|
||||||
edgeToEdge,
|
|
||||||
spotlight,
|
|
||||||
pip,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
const oneOnOneLayoutMedia$: Behavior<{
|
|
||||||
local: LocalUserMediaViewModel;
|
|
||||||
remote: UserMediaViewModel | RingingMediaViewModel;
|
|
||||||
} | null> = scope.behavior(
|
|
||||||
combineLatest([userMedia$, screenShares$]).pipe(
|
combineLatest([userMedia$, screenShares$]).pipe(
|
||||||
switchMap(([userMedia, screenShares]) => {
|
switchMap(([userMedia, screenShares]) => {
|
||||||
// One-on-one layout only supports 2 user media, no screen shares
|
// One-on-one layout only supports 2 user media, no screen shares
|
||||||
@@ -1175,47 +1130,12 @@ export function createCallViewModel$(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const oneOnOneDesktopLayoutMedia$: Observable<OneOnOneDesktopLayoutMedia | null> =
|
const localVideoEnabled$ = scope.behavior<boolean>(
|
||||||
oneOnOneLayoutMedia$.pipe(
|
oneOnOneLayoutMedia$.pipe(
|
||||||
map((media) => {
|
switchMap((media) =>
|
||||||
if (media === null) return null;
|
media === null ? of(false) : media.local.videoEnabled$,
|
||||||
return media.remote.type === "ringing"
|
),
|
||||||
? {
|
),
|
||||||
type: "one-on-one-desktop" as const,
|
|
||||||
edgeToEdge: false,
|
|
||||||
spotlight: media.local,
|
|
||||||
pip: media.remote,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
type: "one-on-one-desktop" as const,
|
|
||||||
edgeToEdge: false,
|
|
||||||
spotlight: media.remote,
|
|
||||||
pip: media.local,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const oneOnOneMobileLayoutMedia$: Observable<OneOnOneMobileLayoutMedia | null> =
|
|
||||||
oneOnOneLayoutMedia$.pipe(
|
|
||||||
switchMap((media) => {
|
|
||||||
if (media === null) return of(null);
|
|
||||||
return media.local.videoEnabled$.pipe(
|
|
||||||
map((videoEnabled) => ({
|
|
||||||
type: "one-on-one-mobile" as const,
|
|
||||||
edgeToEdge: true as const,
|
|
||||||
spotlight: media.remote,
|
|
||||||
pip: videoEnabled ? media.local : undefined,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const pipLayoutMedia$: Observable<LayoutMedia> = spotlight$.pipe(
|
|
||||||
map((spotlight) => ({
|
|
||||||
type: "pip",
|
|
||||||
edgeToEdge: platform !== "desktop",
|
|
||||||
spotlight,
|
|
||||||
})),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
spotlight$
|
spotlight$
|
||||||
@@ -1250,66 +1170,28 @@ export function createCallViewModel$(
|
|||||||
* The media to be used to produce a layout.
|
* The media to be used to produce a layout.
|
||||||
*/
|
*/
|
||||||
const layoutMedia$ = scope.behavior<LayoutMedia>(
|
const layoutMedia$ = scope.behavior<LayoutMedia>(
|
||||||
windowMode$.pipe(
|
merge(
|
||||||
switchMap((windowMode) => {
|
windowMode$,
|
||||||
switch (windowMode) {
|
layoutSwitchVm.layout$,
|
||||||
case "normal":
|
spotlightExpanded$,
|
||||||
return layoutSwitchVm.layout$.pipe(
|
oneOnOneLayoutMedia$,
|
||||||
switchMap((layout) => {
|
localVideoEnabled$,
|
||||||
switch (layout) {
|
spotlightAndPip$,
|
||||||
case "grid":
|
grid$,
|
||||||
return oneOnOneDesktopLayoutMedia$.pipe(
|
).pipe(
|
||||||
switchMap((oneOnOne) =>
|
map(() =>
|
||||||
oneOnOne === null ? gridLayoutMedia$ : of(oneOnOne),
|
computeLayoutMedia({
|
||||||
),
|
windowMode: windowMode$.value,
|
||||||
);
|
layoutMode: layoutSwitchVm.layout$.value,
|
||||||
case "spotlight":
|
spotlightExpanded: spotlightExpanded$.value,
|
||||||
return spotlightExpanded$.pipe(
|
oneOnOne: oneOnOneLayoutMedia$.value,
|
||||||
switchMap((expanded) =>
|
localVideoEnabled: localVideoEnabled$.value,
|
||||||
expanded
|
...spotlightAndPip$.value,
|
||||||
? spotlightExpandedLayoutMedia$(false)
|
grid: grid$.value,
|
||||||
: spotlightLandscapeLayoutMedia$(false),
|
desktop: platform === "desktop",
|
||||||
),
|
}),
|
||||||
);
|
),
|
||||||
}
|
distinctUntilChanged(layoutShallowEquals),
|
||||||
}),
|
|
||||||
);
|
|
||||||
case "narrow":
|
|
||||||
return oneOnOneMobileLayoutMedia$.pipe(
|
|
||||||
switchMap((oneOnOne) =>
|
|
||||||
oneOnOne === null
|
|
||||||
? combineLatest([grid$, spotlight$], (grid, spotlight) =>
|
|
||||||
grid.length > smallMobileCallThreshold ||
|
|
||||||
spotlight.some((vm) => vm.type === "screen share")
|
|
||||||
? spotlightPortraitLayoutMedia$
|
|
||||||
: gridLayoutMedia$,
|
|
||||||
).pipe(switchAll())
|
|
||||||
: of(oneOnOne),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
case "flat":
|
|
||||||
return oneOnOneMobileLayoutMedia$.pipe(
|
|
||||||
switchMap((oneOnOne) =>
|
|
||||||
oneOnOne === null
|
|
||||||
? layoutSwitchVm.layout$.pipe(
|
|
||||||
switchMap((layout) => {
|
|
||||||
switch (layout) {
|
|
||||||
case "grid":
|
|
||||||
// Yes, grid mode actually gets you a "spotlight" layout in
|
|
||||||
// this window mode.
|
|
||||||
return spotlightLandscapeLayoutMedia$(true);
|
|
||||||
case "spotlight":
|
|
||||||
return spotlightExpandedLayoutMedia$(true);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
: of(oneOnOne),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
case "pip":
|
|
||||||
return pipLayoutMedia$;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1891,3 +1773,133 @@ function getE2eeKeyProvider(
|
|||||||
return keyProvider;
|
return keyProvider;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OneOnOneMedia {
|
||||||
|
local: LocalUserMediaViewModel;
|
||||||
|
remote: UserMediaViewModel | RingingMediaViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LayoutMediaInputs {
|
||||||
|
windowMode: WindowMode;
|
||||||
|
/** The layout chosen with the layout switch. */
|
||||||
|
layoutMode: LayoutMode;
|
||||||
|
spotlightExpanded: boolean;
|
||||||
|
/** Set when the call qualifies for a one-on-one layout. */
|
||||||
|
oneOnOne: OneOnOneMedia | null;
|
||||||
|
localVideoEnabled: boolean;
|
||||||
|
spotlight: MediaViewModel[];
|
||||||
|
grid: UserMediaViewModel[];
|
||||||
|
pip: UserMediaViewModel | undefined;
|
||||||
|
desktop: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decides which layout to use and which media goes where in it.
|
||||||
|
*/
|
||||||
|
function computeLayoutMedia({
|
||||||
|
windowMode,
|
||||||
|
layoutMode,
|
||||||
|
spotlightExpanded,
|
||||||
|
oneOnOne,
|
||||||
|
localVideoEnabled,
|
||||||
|
spotlight,
|
||||||
|
grid,
|
||||||
|
pip,
|
||||||
|
desktop,
|
||||||
|
}: LayoutMediaInputs): LayoutMedia {
|
||||||
|
switch (windowMode) {
|
||||||
|
case "normal":
|
||||||
|
if (layoutMode === "grid")
|
||||||
|
return oneOnOne === null
|
||||||
|
? gridLayoutMedia(spotlight, grid)
|
||||||
|
: oneOnOneDesktopLayoutMedia(oneOnOne);
|
||||||
|
return spotlightExpanded
|
||||||
|
? spotlightExpandedLayoutMedia(false, spotlight, pip)
|
||||||
|
: spotlightLandscapeLayoutMedia(false, spotlight, grid);
|
||||||
|
case "narrow":
|
||||||
|
if (oneOnOne !== null)
|
||||||
|
return oneOnOneMobileLayoutMedia(oneOnOne, localVideoEnabled);
|
||||||
|
return grid.length > smallMobileCallThreshold || hasScreenShare(spotlight)
|
||||||
|
? spotlightPortraitLayoutMedia(spotlight, grid)
|
||||||
|
: gridLayoutMedia(spotlight, grid);
|
||||||
|
case "flat":
|
||||||
|
if (oneOnOne !== null)
|
||||||
|
return oneOnOneMobileLayoutMedia(oneOnOne, localVideoEnabled);
|
||||||
|
// Yes, grid mode actually gets you a "spotlight" layout in this window
|
||||||
|
// mode.
|
||||||
|
return layoutMode === "grid"
|
||||||
|
? spotlightLandscapeLayoutMedia(true, spotlight, grid)
|
||||||
|
: spotlightExpandedLayoutMedia(true, spotlight, pip);
|
||||||
|
case "pip":
|
||||||
|
return { type: "pip", edgeToEdge: !desktop, spotlight };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasScreenShare(spotlight: MediaViewModel[]): boolean {
|
||||||
|
return spotlight.some((vm) => vm.type === "screen share");
|
||||||
|
}
|
||||||
|
|
||||||
|
function gridLayoutMedia(
|
||||||
|
spotlight: MediaViewModel[],
|
||||||
|
grid: UserMediaViewModel[],
|
||||||
|
): GridLayoutMedia {
|
||||||
|
return {
|
||||||
|
type: "grid",
|
||||||
|
edgeToEdge: false,
|
||||||
|
spotlight: hasScreenShare(spotlight) ? spotlight : undefined,
|
||||||
|
grid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function oneOnOneDesktopLayoutMedia(
|
||||||
|
media: OneOnOneMedia,
|
||||||
|
): OneOnOneDesktopLayoutMedia {
|
||||||
|
return media.remote.type === "ringing"
|
||||||
|
? {
|
||||||
|
type: "one-on-one-desktop",
|
||||||
|
edgeToEdge: false,
|
||||||
|
spotlight: media.local,
|
||||||
|
pip: media.remote,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: "one-on-one-desktop",
|
||||||
|
edgeToEdge: false,
|
||||||
|
spotlight: media.remote,
|
||||||
|
pip: media.local,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function spotlightExpandedLayoutMedia(
|
||||||
|
edgeToEdge: boolean,
|
||||||
|
spotlight: MediaViewModel[],
|
||||||
|
pip: UserMediaViewModel | undefined,
|
||||||
|
): SpotlightExpandedLayoutMedia {
|
||||||
|
return { type: "spotlight-expanded", edgeToEdge, spotlight, pip };
|
||||||
|
}
|
||||||
|
|
||||||
|
function spotlightLandscapeLayoutMedia(
|
||||||
|
edgeToEdge: boolean,
|
||||||
|
spotlight: MediaViewModel[],
|
||||||
|
grid: UserMediaViewModel[],
|
||||||
|
): SpotlightLandscapeLayoutMedia {
|
||||||
|
return { type: "spotlight-landscape", edgeToEdge, spotlight, grid };
|
||||||
|
}
|
||||||
|
|
||||||
|
function oneOnOneMobileLayoutMedia(
|
||||||
|
media: OneOnOneMedia,
|
||||||
|
localVideoEnabled: boolean,
|
||||||
|
): OneOnOneMobileLayoutMedia {
|
||||||
|
return {
|
||||||
|
type: "one-on-one-mobile",
|
||||||
|
edgeToEdge: true,
|
||||||
|
spotlight: media.remote,
|
||||||
|
pip: localVideoEnabled ? media.local : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function spotlightPortraitLayoutMedia(
|
||||||
|
spotlight: MediaViewModel[],
|
||||||
|
grid: UserMediaViewModel[],
|
||||||
|
): SpotlightPortraitLayoutMedia {
|
||||||
|
return { type: "spotlight-portrait", edgeToEdge: false, spotlight, grid };
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user