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 Behavior } from "./Behavior";
import { type ObservableScope } from "./ObservableScope";
import { RemoteUserSetting } from "./RemoteUserSettings";
export function observeTrackReference$(
participant: Participant,
@@ -398,7 +399,6 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
return this._videoEnabled$;
}
private readonly _cropVideo$ = new BehaviorSubject(true);
/**
* 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>,
public readonly handRaised$: Behavior<Date | null>,
public readonly reaction$: Behavior<ReactionOption | null>,
public readonly _cropVideo$ = new BehaviorSubject(true),
) {
super(
scope,
@@ -610,36 +611,7 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
* The volume to which this participant's audio is set, as a scalar
* multiplier.
*/
public readonly localVolume$ = this.scope.behavior<number>(
merge(
this.locallyMutedToggle$.pipe(map(() => "toggle mute" as const)),
this.localVolumeAdjustment$,
this.localVolumeCommit$.pipe(map(() => "commit" as const)),
).pipe(
accumulate({ volume: 1, committedVolume: 1 }, (state, event) => {
switch (event) {
case "toggle mute":
return {
...state,
volume: state.volume === 0 ? state.committedVolume : 0,
};
case "commit":
// Dragging the slider to zero should have the same effect as
// muting: keep the original committed volume, as if it were never
// dragged
return {
...state,
committedVolume:
state.volume === 0 ? state.committedVolume : state.volume,
};
default:
// Volume adjustment
return { ...state, volume: event };
}
}),
map(({ volume }) => volume),
),
);
public readonly localVolume$: Behavior<number>;
// This private field is used to override the value from the superclass
private __videoEnabled$: Behavior<boolean>;
@@ -650,9 +622,9 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
/**
* Whether this participant's audio is disabled.
*/
public readonly locallyMuted$ = this.scope.behavior<boolean>(
this.localVolume$.pipe(map((volume) => volume === 0)),
);
public readonly locallyMuted$: Behavior<boolean>;
private readonly remoteUserSetting: RemoteUserSetting;
public constructor(
scope: ObservableScope,
@@ -668,6 +640,7 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
handRaised$: Behavior<Date | null>,
reaction$: Behavior<ReactionOption | null>,
) {
const remoteUserSetting = new RemoteUserSetting(userId);
super(
scope,
id,
@@ -680,6 +653,7 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
mxcAvatarUrl$,
handRaised$,
reaction$,
new BehaviorSubject(remoteUserSetting.cropVideo),
);
this.__speaking$ = this.scope.behavior(
@@ -690,6 +664,47 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
),
);
this.remoteUserSetting = remoteUserSetting;
const storedVolume = this.remoteUserSetting.getValue().volume;
this.localVolume$ = this.scope.behavior<number>(
merge(
this.locallyMutedToggle$.pipe(map(() => "toggle mute" as const)),
this.localVolumeAdjustment$,
this.localVolumeCommit$.pipe(map(() => "commit" as const)),
).pipe(
accumulate(
{ volume: storedVolume, committedVolume: storedVolume },
(state, event) => {
switch (event) {
case "toggle mute":
return {
...state,
volume: state.volume === 0 ? state.committedVolume : 0,
};
case "commit":
// Dragging the slider to zero should have the same effect as
// muting: keep the original committed volume, as if it were never
// dragged
return {
...state,
committedVolume:
state.volume === 0 ? state.committedVolume : state.volume,
};
default:
// Volume adjustment
return { ...state, volume: event };
}
},
),
map(({ volume }) => volume),
),
);
this.locallyMuted$ = this.scope.behavior<boolean>(
this.localVolume$.pipe(map((volume) => volume === 0)),
);
this.__videoEnabled$ = this.scope.behavior(
pretendToBeDisconnected$.pipe(
switchMap((disconnected) =>
@@ -708,7 +723,10 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
switchMap((disconnected) => (disconnected ? of(0) : this.localVolume$)),
this.scope.bind(),
),
]).subscribe(([p, volume]) => p?.setVolume(volume));
]).subscribe(([p, volume]) => {
p?.setVolume(volume);
this.remoteUserSetting.volume = volume;
});
}
public toggleLocallyMuted(): void {
@@ -723,6 +741,11 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
this.localVolumeCommit$.next();
}
public toggleFitContain(): void {
super.toggleFitContain();
this.remoteUserSetting.cropVideo = this._cropVideo$.value;
}
public audioStreamStats$ = combineLatest([
this.participant$,
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,
});
}
}