mirror of
https://github.com/vector-im/element-call.git
synced 2026-02-05 04:15:58 +00:00
Use Behaviors even more consistently
This commit is contained in:
@@ -157,10 +157,13 @@ export function useLivekit(
|
||||
useObservableEagerState(
|
||||
useObservable(
|
||||
(room$) =>
|
||||
observeTrackReference$(
|
||||
room$.pipe(map(([room]) => room.localParticipant)),
|
||||
Track.Source.Camera,
|
||||
).pipe(
|
||||
room$.pipe(
|
||||
switchMap(([room]) =>
|
||||
observeTrackReference$(
|
||||
room.localParticipant,
|
||||
Track.Source.Camera,
|
||||
),
|
||||
),
|
||||
map((trackRef) => {
|
||||
const track = trackRef?.publication?.track;
|
||||
return track instanceof LocalVideoTrack ? track : null;
|
||||
|
||||
@@ -251,8 +251,8 @@ class UserMedia {
|
||||
LocalParticipant | RemoteParticipant | undefined
|
||||
>;
|
||||
|
||||
public readonly speaker$: Observable<boolean>;
|
||||
public readonly presenter$: Observable<boolean>;
|
||||
public readonly speaker$: Behavior<boolean>;
|
||||
public readonly presenter$: Behavior<boolean>;
|
||||
public constructor(
|
||||
public readonly id: string,
|
||||
member: RoomMember | undefined,
|
||||
@@ -269,7 +269,7 @@ class UserMedia {
|
||||
this.vm = new LocalUserMediaViewModel(
|
||||
this.id,
|
||||
member,
|
||||
this.participant$.asObservable() as Observable<LocalParticipant>,
|
||||
this.participant$ as Behavior<LocalParticipant>,
|
||||
encryptionSystem,
|
||||
livekitRoom,
|
||||
displayname$.behavior(this.scope),
|
||||
@@ -386,21 +386,23 @@ function getRoomMemberFromRtcMember(
|
||||
|
||||
// TODO: Move wayyyy more business logic from the call and lobby views into here
|
||||
export class CallViewModel extends ViewModel {
|
||||
public readonly localVideo$: Observable<LocalVideoTrack | null> =
|
||||
public readonly localVideo$: Behavior<LocalVideoTrack | null> =
|
||||
observeTrackReference$(
|
||||
of(this.livekitRoom.localParticipant),
|
||||
this.livekitRoom.localParticipant,
|
||||
Track.Source.Camera,
|
||||
).pipe(
|
||||
map((trackRef) => {
|
||||
const track = trackRef?.publication?.track;
|
||||
return track instanceof LocalVideoTrack ? track : null;
|
||||
}),
|
||||
);
|
||||
)
|
||||
.pipe(
|
||||
map((trackRef) => {
|
||||
const track = trackRef?.publication?.track;
|
||||
return track instanceof LocalVideoTrack ? track : null;
|
||||
}),
|
||||
)
|
||||
.behavior(this.scope);
|
||||
|
||||
/**
|
||||
* The raw list of RemoteParticipants as reported by LiveKit
|
||||
*/
|
||||
private readonly rawRemoteParticipants$: Observable<RemoteParticipant[]> =
|
||||
private readonly rawRemoteParticipants$: Behavior<RemoteParticipant[]> =
|
||||
connectedParticipantsObserver(this.livekitRoom)
|
||||
.pipe(startWith([]))
|
||||
.behavior(this.scope);
|
||||
@@ -409,44 +411,46 @@ export class CallViewModel extends ViewModel {
|
||||
* Lists of RemoteParticipants to "hold" on display, even if LiveKit claims that
|
||||
* they've left
|
||||
*/
|
||||
private readonly remoteParticipantHolds$: Observable<RemoteParticipant[][]> =
|
||||
this.connectionState$.pipe(
|
||||
withLatestFrom(this.rawRemoteParticipants$),
|
||||
mergeMap(([s, ps]) => {
|
||||
// Whenever we switch focuses, we should retain all the previous
|
||||
// participants for at least POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS ms to
|
||||
// give their clients time to switch over and avoid jarring layout shifts
|
||||
if (s === ECAddonConnectionState.ECSwitchingFocus) {
|
||||
return concat(
|
||||
// Hold these participants
|
||||
of({ hold: ps }),
|
||||
// Wait for time to pass and the connection state to have changed
|
||||
forkJoin([
|
||||
timer(POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS),
|
||||
this.connectionState$.pipe(
|
||||
filter((s) => s !== ECAddonConnectionState.ECSwitchingFocus),
|
||||
take(1),
|
||||
),
|
||||
// Then unhold them
|
||||
]).pipe(map(() => ({ unhold: ps }))),
|
||||
);
|
||||
} else {
|
||||
return EMPTY;
|
||||
}
|
||||
}),
|
||||
// Accumulate the hold instructions into a single list showing which
|
||||
// participants are being held
|
||||
accumulate([] as RemoteParticipant[][], (holds, instruction) =>
|
||||
"hold" in instruction
|
||||
? [instruction.hold, ...holds]
|
||||
: holds.filter((h) => h !== instruction.unhold),
|
||||
),
|
||||
);
|
||||
private readonly remoteParticipantHolds$: Behavior<RemoteParticipant[][]> =
|
||||
this.connectionState$
|
||||
.pipe(
|
||||
withLatestFrom(this.rawRemoteParticipants$),
|
||||
mergeMap(([s, ps]) => {
|
||||
// Whenever we switch focuses, we should retain all the previous
|
||||
// participants for at least POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS ms to
|
||||
// give their clients time to switch over and avoid jarring layout shifts
|
||||
if (s === ECAddonConnectionState.ECSwitchingFocus) {
|
||||
return concat(
|
||||
// Hold these participants
|
||||
of({ hold: ps }),
|
||||
// Wait for time to pass and the connection state to have changed
|
||||
forkJoin([
|
||||
timer(POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS),
|
||||
this.connectionState$.pipe(
|
||||
filter((s) => s !== ECAddonConnectionState.ECSwitchingFocus),
|
||||
take(1),
|
||||
),
|
||||
// Then unhold them
|
||||
]).pipe(map(() => ({ unhold: ps }))),
|
||||
);
|
||||
} else {
|
||||
return EMPTY;
|
||||
}
|
||||
}),
|
||||
// Accumulate the hold instructions into a single list showing which
|
||||
// participants are being held
|
||||
accumulate([] as RemoteParticipant[][], (holds, instruction) =>
|
||||
"hold" in instruction
|
||||
? [instruction.hold, ...holds]
|
||||
: holds.filter((h) => h !== instruction.unhold),
|
||||
),
|
||||
)
|
||||
.behavior(this.scope);
|
||||
|
||||
/**
|
||||
* The RemoteParticipants including those that are being "held" on the screen
|
||||
*/
|
||||
private readonly remoteParticipants$: Observable<RemoteParticipant[]> =
|
||||
private readonly remoteParticipants$: Behavior<RemoteParticipant[]> =
|
||||
combineLatest(
|
||||
[this.rawRemoteParticipants$, this.remoteParticipantHolds$],
|
||||
(raw, holds) => {
|
||||
@@ -465,7 +469,7 @@ export class CallViewModel extends ViewModel {
|
||||
|
||||
return result;
|
||||
},
|
||||
);
|
||||
).behavior(this.scope);
|
||||
|
||||
/**
|
||||
* Displaynames for each member of the call. This will disambiguate
|
||||
@@ -709,11 +713,13 @@ export class CallViewModel extends ViewModel {
|
||||
/**
|
||||
* List of MediaItems that we want to display, that are of type UserMedia
|
||||
*/
|
||||
private readonly userMedia$: Observable<UserMedia[]> = this.mediaItems$.pipe(
|
||||
map((mediaItems) =>
|
||||
mediaItems.filter((m): m is UserMedia => m instanceof UserMedia),
|
||||
),
|
||||
);
|
||||
private readonly userMedia$: Behavior<UserMedia[]> = this.mediaItems$
|
||||
.pipe(
|
||||
map((mediaItems) =>
|
||||
mediaItems.filter((m): m is UserMedia => m instanceof UserMedia),
|
||||
),
|
||||
)
|
||||
.behavior(this.scope);
|
||||
|
||||
public readonly memberChanges$ = this.userMedia$
|
||||
.pipe(map((mediaItems) => mediaItems.map((m) => m.id)))
|
||||
|
||||
@@ -54,24 +54,16 @@ import { type ReactionOption } from "../reactions";
|
||||
import { type Behavior } from "./Behavior";
|
||||
|
||||
export function observeTrackReference$(
|
||||
participant$: Observable<Participant | undefined>,
|
||||
participant: Participant,
|
||||
source: Track.Source,
|
||||
): Observable<TrackReferenceOrPlaceholder | undefined> {
|
||||
return participant$.pipe(
|
||||
switchMap((p) => {
|
||||
if (p) {
|
||||
return observeParticipantMedia(p).pipe(
|
||||
map(() => ({
|
||||
participant: p,
|
||||
publication: p.getTrackPublication(source),
|
||||
source,
|
||||
})),
|
||||
distinctUntilKeyChanged("publication"),
|
||||
);
|
||||
} else {
|
||||
return of(undefined);
|
||||
}
|
||||
}),
|
||||
): Observable<TrackReferenceOrPlaceholder> {
|
||||
return observeParticipantMedia(participant).pipe(
|
||||
map(() => ({
|
||||
participant: participant,
|
||||
publication: participant.getTrackPublication(source),
|
||||
source,
|
||||
})),
|
||||
distinctUntilKeyChanged("publication"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,7 +75,7 @@ export function observeRtpStreamStats$(
|
||||
RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats | undefined
|
||||
> {
|
||||
return combineLatest([
|
||||
observeTrackReference$(of(participant), source),
|
||||
observeTrackReference$(participant, source),
|
||||
interval(1000).pipe(startWith(0)),
|
||||
]).pipe(
|
||||
switchMap(async ([trackReference]) => {
|
||||
@@ -237,6 +229,18 @@ abstract class BaseMediaViewModel extends ViewModel {
|
||||
*/
|
||||
public abstract readonly local: boolean;
|
||||
|
||||
private observeTrackReference$(
|
||||
source: Track.Source,
|
||||
): Behavior<TrackReferenceOrPlaceholder | undefined> {
|
||||
return this.participant$
|
||||
.pipe(
|
||||
switchMap((p) =>
|
||||
p === undefined ? of(undefined) : observeTrackReference$(p, source),
|
||||
),
|
||||
)
|
||||
.behavior(this.scope);
|
||||
}
|
||||
|
||||
public constructor(
|
||||
/**
|
||||
* An opaque identifier for this media.
|
||||
@@ -261,12 +265,10 @@ abstract class BaseMediaViewModel extends ViewModel {
|
||||
public readonly displayName$: Behavior<string>,
|
||||
) {
|
||||
super();
|
||||
const audio$ = observeTrackReference$(participant$, audioSource).behavior(
|
||||
this.scope,
|
||||
);
|
||||
this.video$ = observeTrackReference$(participant$, videoSource).behavior(
|
||||
this.scope,
|
||||
);
|
||||
|
||||
const audio$ = this.observeTrackReference$(audioSource);
|
||||
this.video$ = this.observeTrackReference$(videoSource);
|
||||
|
||||
this.unencryptedWarning$ = combineLatest(
|
||||
[audio$, this.video$],
|
||||
(a, v) =>
|
||||
@@ -466,7 +468,7 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel {
|
||||
public constructor(
|
||||
id: string,
|
||||
member: RoomMember | undefined,
|
||||
participant$: Observable<LocalParticipant | undefined>,
|
||||
participant$: Behavior<LocalParticipant | undefined>,
|
||||
encryptionSystem: EncryptionSystem,
|
||||
livekitRoom: LivekitRoom,
|
||||
displayName$: Behavior<string>,
|
||||
|
||||
@@ -243,7 +243,7 @@ export async function withLocalMedia(
|
||||
const vm = new LocalUserMediaViewModel(
|
||||
"local",
|
||||
mockMatrixRoomMember(localRtcMember, roomMember),
|
||||
of(localParticipant),
|
||||
constant(localParticipant),
|
||||
{
|
||||
kind: E2eeType.PER_PARTICIPANT,
|
||||
},
|
||||
@@ -331,7 +331,7 @@ export class MockRTCSession extends TypedEventEmitter<
|
||||
}
|
||||
|
||||
public withMemberships(
|
||||
rtcMembers$: Observable<Partial<CallMembership>[]>,
|
||||
rtcMembers$: Behavior<Partial<CallMembership>[]>,
|
||||
): MockRTCSession {
|
||||
rtcMembers$.subscribe((m) => {
|
||||
const old = this.memberships;
|
||||
|
||||
Reference in New Issue
Block a user