Move sorting bin calculation into UserMedia
This commit is contained in:
@@ -168,40 +168,6 @@ export type GridMode = "grid" | "spotlight";
|
|||||||
|
|
||||||
export type WindowMode = "normal" | "narrow" | "flat" | "pip";
|
export type WindowMode = "normal" | "narrow" | "flat" | "pip";
|
||||||
|
|
||||||
/**
|
|
||||||
* Sorting bins defining the order in which media tiles appear in the layout.
|
|
||||||
*/
|
|
||||||
enum SortingBin {
|
|
||||||
/**
|
|
||||||
* Yourself, when the "always show self" option is on.
|
|
||||||
*/
|
|
||||||
SelfAlwaysShown,
|
|
||||||
/**
|
|
||||||
* Participants that are sharing their screen.
|
|
||||||
*/
|
|
||||||
Presenters,
|
|
||||||
/**
|
|
||||||
* Participants that have been speaking recently.
|
|
||||||
*/
|
|
||||||
Speakers,
|
|
||||||
/**
|
|
||||||
* Participants that have their hand raised.
|
|
||||||
*/
|
|
||||||
HandRaised,
|
|
||||||
/**
|
|
||||||
* Participants with video.
|
|
||||||
*/
|
|
||||||
Video,
|
|
||||||
/**
|
|
||||||
* Participants not sharing any video.
|
|
||||||
*/
|
|
||||||
NoVideo,
|
|
||||||
/**
|
|
||||||
* Yourself, when the "always show self" option is off.
|
|
||||||
*/
|
|
||||||
SelfNotAlwaysShown,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LayoutScanState {
|
interface LayoutScanState {
|
||||||
layout: Layout | null;
|
layout: Layout | null;
|
||||||
tiles: TileStore;
|
tiles: TileStore;
|
||||||
@@ -549,6 +515,9 @@ export class CallViewModel extends ViewModel {
|
|||||||
private readonly userId = this.matrixRoom.client.getUserId()!;
|
private readonly userId = this.matrixRoom.client.getUserId()!;
|
||||||
private readonly deviceId = this.matrixRoom.client.getDeviceId()!;
|
private readonly deviceId = this.matrixRoom.client.getDeviceId()!;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether we are connected to the MatrixRTC session.
|
||||||
|
*/
|
||||||
private readonly matrixConnected$ = this.scope.behavior(
|
private readonly matrixConnected$ = this.scope.behavior(
|
||||||
// To consider ourselves connected to MatrixRTC, we check the following:
|
// To consider ourselves connected to MatrixRTC, we check the following:
|
||||||
and$(
|
and$(
|
||||||
@@ -583,6 +552,10 @@ export class CallViewModel extends ViewModel {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether we are "fully" connected to the call. Accounts for both the
|
||||||
|
* connection to the MatrixRTC session and the LiveKit publish connection.
|
||||||
|
*/
|
||||||
private readonly connected$ = this.scope.behavior(
|
private readonly connected$ = this.scope.behavior(
|
||||||
and$(
|
and$(
|
||||||
this.matrixConnected$,
|
this.matrixConnected$,
|
||||||
@@ -600,7 +573,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
// We are reconnecting if we previously had some successful initial
|
// We are reconnecting if we previously had some successful initial
|
||||||
// connection but are now disconnected
|
// connection but are now disconnected
|
||||||
scan(
|
scan(
|
||||||
({ connectedPreviously, reconnecting }, connectedNow) => ({
|
({ connectedPreviously }, connectedNow) => ({
|
||||||
connectedPreviously: connectedPreviously || connectedNow,
|
connectedPreviously: connectedPreviously || connectedNow,
|
||||||
reconnecting: connectedPreviously && !connectedNow,
|
reconnecting: connectedPreviously && !connectedNow,
|
||||||
}),
|
}),
|
||||||
@@ -627,7 +600,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
private readonly participantsByRoom$ = this.scope.behavior<
|
private readonly participantsByRoom$ = this.scope.behavior<
|
||||||
{
|
{
|
||||||
livekitRoom: LivekitRoom;
|
livekitRoom: LivekitRoom;
|
||||||
url: string;
|
url: string; // Included for use as a React key
|
||||||
participants: {
|
participants: {
|
||||||
id: string;
|
id: string;
|
||||||
participant: LocalParticipant | RemoteParticipant | undefined;
|
participant: LocalParticipant | RemoteParticipant | undefined;
|
||||||
@@ -1114,31 +1087,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
this.userMedia$.pipe(
|
this.userMedia$.pipe(
|
||||||
switchMap((mediaItems) => {
|
switchMap((mediaItems) => {
|
||||||
const bins = mediaItems.map((m) =>
|
const bins = mediaItems.map((m) =>
|
||||||
combineLatest(
|
m.bin$.pipe(map((bin) => [m, bin] as const)),
|
||||||
[
|
|
||||||
m.speaker$,
|
|
||||||
m.presenter$,
|
|
||||||
m.vm.videoEnabled$,
|
|
||||||
m.vm.handRaised$,
|
|
||||||
m.vm instanceof LocalUserMediaViewModel
|
|
||||||
? m.vm.alwaysShow$
|
|
||||||
: of(false),
|
|
||||||
],
|
|
||||||
(speaker, presenter, video, handRaised, alwaysShow) => {
|
|
||||||
let bin: SortingBin;
|
|
||||||
if (m.vm.local)
|
|
||||||
bin = alwaysShow
|
|
||||||
? SortingBin.SelfAlwaysShown
|
|
||||||
: SortingBin.SelfNotAlwaysShown;
|
|
||||||
else if (presenter) bin = SortingBin.Presenters;
|
|
||||||
else if (speaker) bin = SortingBin.Speakers;
|
|
||||||
else if (handRaised) bin = SortingBin.HandRaised;
|
|
||||||
else if (video) bin = SortingBin.Video;
|
|
||||||
else bin = SortingBin.NoVideo;
|
|
||||||
|
|
||||||
return [m, bin] as const;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
// Sort the media by bin order and generate a tile for each one
|
// Sort the media by bin order and generate a tile for each one
|
||||||
return bins.length === 0
|
return bins.length === 0
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Copyright 2025 New Vector Ltd.
|
|||||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
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 { BehaviorSubject, type Observable } from "rxjs";
|
import { of, type Observable } from "rxjs";
|
||||||
import {
|
import {
|
||||||
type LocalParticipant,
|
type LocalParticipant,
|
||||||
type RemoteParticipant,
|
type RemoteParticipant,
|
||||||
@@ -17,13 +17,14 @@ import type { RoomMember } from "matrix-js-sdk";
|
|||||||
import type { EncryptionSystem } from "../e2ee/sharedKeyManagement.ts";
|
import type { EncryptionSystem } from "../e2ee/sharedKeyManagement.ts";
|
||||||
import type { Behavior } from "./Behavior.ts";
|
import type { Behavior } from "./Behavior.ts";
|
||||||
|
|
||||||
// TODO Document this
|
/**
|
||||||
|
* A screen share media item to be presented in a tile. This is a thin wrapper
|
||||||
|
* around ScreenShareViewModel which essentially just establishes an
|
||||||
|
* ObservableScope for behaviors that the view model depends on.
|
||||||
|
*/
|
||||||
export class ScreenShare {
|
export class ScreenShare {
|
||||||
private readonly scope = new ObservableScope();
|
private readonly scope = new ObservableScope();
|
||||||
public readonly vm: ScreenShareViewModel;
|
public readonly vm: ScreenShareViewModel;
|
||||||
private readonly participant$: BehaviorSubject<
|
|
||||||
LocalParticipant | RemoteParticipant
|
|
||||||
>;
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
id: string,
|
id: string,
|
||||||
@@ -35,12 +36,10 @@ export class ScreenShare {
|
|||||||
pretendToBeDisconnected$: Behavior<boolean>,
|
pretendToBeDisconnected$: Behavior<boolean>,
|
||||||
displayName$: Observable<string>,
|
displayName$: Observable<string>,
|
||||||
) {
|
) {
|
||||||
this.participant$ = new BehaviorSubject(participant);
|
|
||||||
|
|
||||||
this.vm = new ScreenShareViewModel(
|
this.vm = new ScreenShareViewModel(
|
||||||
id,
|
id,
|
||||||
member,
|
member,
|
||||||
this.participant$.asObservable(),
|
of(participant),
|
||||||
encryptionSystem,
|
encryptionSystem,
|
||||||
livekitRoom,
|
livekitRoom,
|
||||||
focusUrl,
|
focusUrl,
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ 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 { BehaviorSubject, map, type Observable, of, switchMap } from "rxjs";
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
combineLatest,
|
||||||
|
map,
|
||||||
|
type Observable,
|
||||||
|
of,
|
||||||
|
switchMap,
|
||||||
|
} from "rxjs";
|
||||||
import {
|
import {
|
||||||
type LocalParticipant,
|
type LocalParticipant,
|
||||||
type Participant,
|
type Participant,
|
||||||
@@ -29,71 +36,129 @@ import type { ReactionOption } from "../reactions";
|
|||||||
import { observeSpeaker$ } from "./observeSpeaker.ts";
|
import { observeSpeaker$ } from "./observeSpeaker.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO Document this
|
* Sorting bins defining the order in which media tiles appear in the layout.
|
||||||
|
*/
|
||||||
|
enum SortingBin {
|
||||||
|
/**
|
||||||
|
* Yourself, when the "always show self" option is on.
|
||||||
|
*/
|
||||||
|
SelfAlwaysShown,
|
||||||
|
/**
|
||||||
|
* Participants that are sharing their screen.
|
||||||
|
*/
|
||||||
|
Presenters,
|
||||||
|
/**
|
||||||
|
* Participants that have been speaking recently.
|
||||||
|
*/
|
||||||
|
Speakers,
|
||||||
|
/**
|
||||||
|
* Participants that have their hand raised.
|
||||||
|
*/
|
||||||
|
HandRaised,
|
||||||
|
/**
|
||||||
|
* Participants with video.
|
||||||
|
*/
|
||||||
|
Video,
|
||||||
|
/**
|
||||||
|
* Participants not sharing any video.
|
||||||
|
*/
|
||||||
|
NoVideo,
|
||||||
|
/**
|
||||||
|
* Yourself, when the "always show self" option is off.
|
||||||
|
*/
|
||||||
|
SelfNotAlwaysShown,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A user media item to be presented in a tile. This is a thin wrapper around
|
||||||
|
* UserMediaViewModel which additionally determines the media item's sorting bin
|
||||||
|
* for inclusion in the call layout.
|
||||||
*/
|
*/
|
||||||
export class UserMedia {
|
export class UserMedia {
|
||||||
private readonly scope = new ObservableScope();
|
private readonly scope = new ObservableScope();
|
||||||
public readonly vm: UserMediaViewModel;
|
private readonly participant$ = new BehaviorSubject(this.initialParticipant);
|
||||||
private readonly participant$: BehaviorSubject<
|
|
||||||
LocalParticipant | RemoteParticipant | undefined
|
|
||||||
>;
|
|
||||||
|
|
||||||
public readonly speaker$: Behavior<boolean>;
|
public readonly vm: UserMediaViewModel = this.participant$.value?.isLocal
|
||||||
public readonly presenter$: Behavior<boolean>;
|
? new LocalUserMediaViewModel(
|
||||||
|
this.id,
|
||||||
|
this.member,
|
||||||
|
this.participant$ as Behavior<LocalParticipant>,
|
||||||
|
this.encryptionSystem,
|
||||||
|
this.livekitRoom,
|
||||||
|
this.focusURL,
|
||||||
|
this.mediaDevices,
|
||||||
|
this.scope.behavior(this.displayname$),
|
||||||
|
this.scope.behavior(this.handRaised$),
|
||||||
|
this.scope.behavior(this.reaction$),
|
||||||
|
)
|
||||||
|
: new RemoteUserMediaViewModel(
|
||||||
|
this.id,
|
||||||
|
this.member,
|
||||||
|
this.participant$ as Observable<RemoteParticipant | undefined>,
|
||||||
|
this.encryptionSystem,
|
||||||
|
this.livekitRoom,
|
||||||
|
this.focusURL,
|
||||||
|
this.pretendToBeDisconnected$,
|
||||||
|
this.scope.behavior(this.displayname$),
|
||||||
|
this.scope.behavior(this.handRaised$),
|
||||||
|
this.scope.behavior(this.reaction$),
|
||||||
|
);
|
||||||
|
|
||||||
|
private readonly speaker$ = this.scope.behavior(
|
||||||
|
observeSpeaker$(this.vm.speaking$),
|
||||||
|
);
|
||||||
|
|
||||||
|
private readonly presenter$ = this.scope.behavior(
|
||||||
|
this.participant$.pipe(
|
||||||
|
switchMap((p) => (p === undefined ? of(false) : sharingScreen$(p))),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which sorting bin the media item should be placed in.
|
||||||
|
*/
|
||||||
|
// This is exposed here rather than by UserMediaViewModel because it's only
|
||||||
|
// relevant to the layout algorithms; the MediaView component should be
|
||||||
|
// ignorant of this value.
|
||||||
|
public readonly bin$ = combineLatest(
|
||||||
|
[
|
||||||
|
this.speaker$,
|
||||||
|
this.presenter$,
|
||||||
|
this.vm.videoEnabled$,
|
||||||
|
this.vm.handRaised$,
|
||||||
|
this.vm instanceof LocalUserMediaViewModel
|
||||||
|
? this.vm.alwaysShow$
|
||||||
|
: of(false),
|
||||||
|
],
|
||||||
|
(speaker, presenter, video, handRaised, alwaysShow) => {
|
||||||
|
if (this.vm.local)
|
||||||
|
return alwaysShow
|
||||||
|
? SortingBin.SelfAlwaysShown
|
||||||
|
: SortingBin.SelfNotAlwaysShown;
|
||||||
|
else if (presenter) return SortingBin.Presenters;
|
||||||
|
else if (speaker) return SortingBin.Speakers;
|
||||||
|
else if (handRaised) return SortingBin.HandRaised;
|
||||||
|
else if (video) return SortingBin.Video;
|
||||||
|
else return SortingBin.NoVideo;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly id: string,
|
public readonly id: string,
|
||||||
member: RoomMember,
|
private readonly member: RoomMember,
|
||||||
participant: LocalParticipant | RemoteParticipant | undefined,
|
private readonly initialParticipant:
|
||||||
encryptionSystem: EncryptionSystem,
|
| LocalParticipant
|
||||||
livekitRoom: LivekitRoom,
|
| RemoteParticipant
|
||||||
focusURL: string,
|
| undefined,
|
||||||
mediaDevices: MediaDevices,
|
private readonly encryptionSystem: EncryptionSystem,
|
||||||
pretendToBeDisconnected$: Behavior<boolean>,
|
private readonly livekitRoom: LivekitRoom,
|
||||||
displayname$: Observable<string>,
|
private readonly focusURL: string,
|
||||||
handRaised$: Observable<Date | null>,
|
private readonly mediaDevices: MediaDevices,
|
||||||
reaction$: Observable<ReactionOption | null>,
|
private readonly pretendToBeDisconnected$: Behavior<boolean>,
|
||||||
) {
|
private readonly displayname$: Observable<string>,
|
||||||
this.participant$ = new BehaviorSubject(participant);
|
private readonly handRaised$: Observable<Date | null>,
|
||||||
|
private readonly reaction$: Observable<ReactionOption | null>,
|
||||||
if (participant?.isLocal) {
|
) {}
|
||||||
this.vm = new LocalUserMediaViewModel(
|
|
||||||
this.id,
|
|
||||||
member,
|
|
||||||
this.participant$ as Behavior<LocalParticipant>,
|
|
||||||
encryptionSystem,
|
|
||||||
livekitRoom,
|
|
||||||
focusURL,
|
|
||||||
mediaDevices,
|
|
||||||
this.scope.behavior(displayname$),
|
|
||||||
this.scope.behavior(handRaised$),
|
|
||||||
this.scope.behavior(reaction$),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.vm = new RemoteUserMediaViewModel(
|
|
||||||
id,
|
|
||||||
member,
|
|
||||||
this.participant$.asObservable() as Observable<
|
|
||||||
RemoteParticipant | undefined
|
|
||||||
>,
|
|
||||||
encryptionSystem,
|
|
||||||
livekitRoom,
|
|
||||||
focusURL,
|
|
||||||
pretendToBeDisconnected$,
|
|
||||||
this.scope.behavior(displayname$),
|
|
||||||
this.scope.behavior(handRaised$),
|
|
||||||
this.scope.behavior(reaction$),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.speaker$ = this.scope.behavior(observeSpeaker$(this.vm.speaking$));
|
|
||||||
|
|
||||||
this.presenter$ = this.scope.behavior(
|
|
||||||
this.participant$.pipe(
|
|
||||||
switchMap((p) => (p === undefined ? of(false) : sharingScreen$(p))),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateParticipant(
|
public updateParticipant(
|
||||||
newParticipant: LocalParticipant | RemoteParticipant | undefined,
|
newParticipant: LocalParticipant | RemoteParticipant | undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user