Retain remoteusermedia modifications across sessions

This commit is contained in:
Half-Shot
2025-12-01 16:35:20 +00:00
parent 7fd4424e7b
commit 7b2fde4280
2 changed files with 88 additions and 35 deletions

View File

@@ -56,6 +56,7 @@ import { platform } from "../Platform";
import { type MediaDevices } from "./MediaDevices"; import { type MediaDevices } from "./MediaDevices";
import { type Behavior } from "./Behavior"; import { type Behavior } from "./Behavior";
import { type ObservableScope } from "./ObservableScope"; import { type ObservableScope } from "./ObservableScope";
import { RemoteUserSetting } from "./RemoteUserSettings";
export function observeTrackReference$( export function observeTrackReference$(
participant: Participant, participant: Participant,
@@ -398,7 +399,6 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
return this._videoEnabled$; return this._videoEnabled$;
} }
private readonly _cropVideo$ = new BehaviorSubject(true);
/** /**
* Whether the tile video should be contained inside the tile or be cropped to fit. * Whether the tile video should be contained inside the tile or be cropped to fit.
*/ */
@@ -416,6 +416,7 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
mxcAvatarUrl$: Behavior<string | undefined>, mxcAvatarUrl$: Behavior<string | undefined>,
public readonly handRaised$: Behavior<Date | null>, public readonly handRaised$: Behavior<Date | null>,
public readonly reaction$: Behavior<ReactionOption | null>, public readonly reaction$: Behavior<ReactionOption | null>,
public readonly _cropVideo$ = new BehaviorSubject(true),
) { ) {
super( super(
scope, scope,
@@ -610,13 +611,71 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
* The volume to which this participant's audio is set, as a scalar * The volume to which this participant's audio is set, as a scalar
* multiplier. * multiplier.
*/ */
public readonly localVolume$ = this.scope.behavior<number>( public readonly localVolume$: Behavior<number>;
// This private field is used to override the value from the superclass
private __videoEnabled$: Behavior<boolean>;
public get videoEnabled$(): Behavior<boolean> {
return this.__videoEnabled$;
}
/**
* Whether this participant's audio is disabled.
*/
public readonly locallyMuted$: Behavior<boolean>;
private readonly remoteUserSetting: RemoteUserSetting;
public constructor(
scope: ObservableScope,
id: string,
userId: string,
participant$: Observable<RemoteParticipant | null>,
encryptionSystem: EncryptionSystem,
livekitRoom$: Behavior<LivekitRoom | undefined>,
focusUrl$: Behavior<string | undefined>,
private readonly pretendToBeDisconnected$: Behavior<boolean>,
displayName$: Behavior<string>,
mxcAvatarUrl$: Behavior<string | undefined>,
handRaised$: Behavior<Date | null>,
reaction$: Behavior<ReactionOption | null>,
) {
const remoteUserSetting = new RemoteUserSetting(userId);
super(
scope,
id,
userId,
participant$,
encryptionSystem,
livekitRoom$,
focusUrl$,
displayName$,
mxcAvatarUrl$,
handRaised$,
reaction$,
new BehaviorSubject(remoteUserSetting.cropVideo),
);
this.__speaking$ = this.scope.behavior(
pretendToBeDisconnected$.pipe(
switchMap((disconnected) =>
disconnected ? of(false) : super.speaking$,
),
),
);
this.remoteUserSetting = remoteUserSetting;
const storedVolume = this.remoteUserSetting.getValue().volume;
this.localVolume$ = this.scope.behavior<number>(
merge( merge(
this.locallyMutedToggle$.pipe(map(() => "toggle mute" as const)), this.locallyMutedToggle$.pipe(map(() => "toggle mute" as const)),
this.localVolumeAdjustment$, this.localVolumeAdjustment$,
this.localVolumeCommit$.pipe(map(() => "commit" as const)), this.localVolumeCommit$.pipe(map(() => "commit" as const)),
).pipe( ).pipe(
accumulate({ volume: 1, committedVolume: 1 }, (state, event) => { accumulate(
{ volume: storedVolume, committedVolume: storedVolume },
(state, event) => {
switch (event) { switch (event) {
case "toggle mute": case "toggle mute":
return { return {
@@ -636,60 +695,16 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
// Volume adjustment // Volume adjustment
return { ...state, volume: event }; return { ...state, volume: event };
} }
}), },
),
map(({ volume }) => volume), map(({ volume }) => volume),
), ),
); );
// This private field is used to override the value from the superclass this.locallyMuted$ = this.scope.behavior<boolean>(
private __videoEnabled$: Behavior<boolean>;
public get videoEnabled$(): Behavior<boolean> {
return this.__videoEnabled$;
}
/**
* Whether this participant's audio is disabled.
*/
public readonly locallyMuted$ = this.scope.behavior<boolean>(
this.localVolume$.pipe(map((volume) => volume === 0)), this.localVolume$.pipe(map((volume) => volume === 0)),
); );
public constructor(
scope: ObservableScope,
id: string,
userId: string,
participant$: Observable<RemoteParticipant | null>,
encryptionSystem: EncryptionSystem,
livekitRoom$: Behavior<LivekitRoom | undefined>,
focusUrl$: Behavior<string | undefined>,
private readonly pretendToBeDisconnected$: Behavior<boolean>,
displayName$: Behavior<string>,
mxcAvatarUrl$: Behavior<string | undefined>,
handRaised$: Behavior<Date | null>,
reaction$: Behavior<ReactionOption | null>,
) {
super(
scope,
id,
userId,
participant$,
encryptionSystem,
livekitRoom$,
focusUrl$,
displayName$,
mxcAvatarUrl$,
handRaised$,
reaction$,
);
this.__speaking$ = this.scope.behavior(
pretendToBeDisconnected$.pipe(
switchMap((disconnected) =>
disconnected ? of(false) : super.speaking$,
),
),
);
this.__videoEnabled$ = this.scope.behavior( this.__videoEnabled$ = this.scope.behavior(
pretendToBeDisconnected$.pipe( pretendToBeDisconnected$.pipe(
switchMap((disconnected) => switchMap((disconnected) =>
@@ -708,7 +723,10 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
switchMap((disconnected) => (disconnected ? of(0) : this.localVolume$)), switchMap((disconnected) => (disconnected ? of(0) : this.localVolume$)),
this.scope.bind(), this.scope.bind(),
), ),
]).subscribe(([p, volume]) => p?.setVolume(volume)); ]).subscribe(([p, volume]) => {
p?.setVolume(volume);
this.remoteUserSetting.volume = volume;
});
} }
public toggleLocallyMuted(): void { public toggleLocallyMuted(): void {
@@ -723,6 +741,11 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
this.localVolumeCommit$.next(); this.localVolumeCommit$.next();
} }
public toggleFitContain(): void {
super.toggleFitContain();
this.remoteUserSetting.cropVideo = this._cropVideo$.value;
}
public audioStreamStats$ = combineLatest([ public audioStreamStats$ = combineLatest([
this.participant$, this.participant$,
showConnectionStats.value$, showConnectionStats.value$,

View File

@@ -0,0 +1,30 @@
import { Setting } from "../settings/settings";
export interface RemoteUserSettingData {
volume: number;
cropVideo: boolean;
}
/**
* A set of local modifications for a remote user's media that should persist
* across calls.
*/
export class RemoteUserSetting extends Setting<RemoteUserSettingData> {
constructor(userId: string) {
super(`remoteusersettings-${userId}`, { volume: 1, cropVideo: true });
}
public set volume(volume: number) {
this.setValue({
...this.getValue(),
volume,
});
}
public set cropVideo(cropVideo: boolean) {
this.setValue({
...this.getValue(),
cropVideo,
});
}
}