Drop observable-hooks in favor of useBehavior

With this change, views now read every reactive value through `useBehavior`, as
`architecture.md` already prescribes. Where a view reads a plain stream, the view
model now exposes a `Behavior`.
This commit is contained in:
Johannes Marbach
2026-09-28 17:54:50 +02:00
parent addc1b10a0
commit 3f63a6fc3c
22 changed files with 459 additions and 113 deletions
+41
View File
@@ -25,6 +25,7 @@ import {
mockRemoteScreenShare,
} from "../../utils/test";
import { constant } from "../Behavior";
import { showConnectionStats } from "../../settings/settings";
global.MediaStreamTrack = class {} as unknown as {
new (): MediaStreamTrack;
@@ -42,6 +43,12 @@ vi.mock("../../Platform", () => ({
},
}));
const observeRtpStreamStatsMock = vi.hoisted(() => vi.fn());
vi.mock("./observeRtpStreamStats", async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
observeRtpStreamStats$: observeRtpStreamStatsMock,
}));
const rtcMembership = mockRtcMembership("@alice:example.org", "AAAA");
test("control a participant's volume", () => {
@@ -289,3 +296,37 @@ test("remote media is not in waiting state when user does not intend to publish
);
expect(vm.waitingForMedia$.value).toBe(false);
});
test("user media polls stream stats only while the setting is on", () => {
const participant = mockRemoteParticipant({});
const vm = mockRemoteMedia(rtcMembership, {}, participant);
onTestFinished(() => showConnectionStats.setValue(false));
withTestScheduler(({ cold, expectObservable, schedule }) => {
const stats = { type: "inbound-rtp" } as RTCInboundRtpStreamStats;
observeRtpStreamStatsMock.mockImplementation(() =>
cold("-s", { s: stats }),
);
schedule("-a-b", {
a() {
showConnectionStats.setValue(true);
},
b() {
showConnectionStats.setValue(false);
},
});
expectObservable(vm.audioStreamStats$).toBe("u-su", {
u: undefined,
s: stats,
});
});
expect(observeRtpStreamStatsMock).toHaveBeenCalledWith(
participant,
Track.Source.Microphone,
"inbound-rtp",
);
expect(observeRtpStreamStatsMock).toHaveBeenCalledWith(
participant,
Track.Source.Camera,
"inbound-rtp",
);
});
+19 -22
View File
@@ -10,7 +10,6 @@ import {
BehaviorSubject,
combineLatest,
map,
type Observable,
of,
Subject,
switchMap,
@@ -54,10 +53,10 @@ export interface BaseUserMediaViewModel extends BaseMemberMediaViewModel {
rtcBackendIdentity: string;
handRaised$: Behavior<Date | null>;
reaction$: Behavior<ReactionOption | null>;
audioStreamStats$: Observable<
audioStreamStats$: Behavior<
RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats | undefined
>;
videoStreamStats$: Observable<
videoStreamStats$: Behavior<
RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats | undefined
>;
/**
@@ -94,6 +93,21 @@ export function createBaseUserMedia(
);
const toggleCropVideo$ = new Subject<void>();
const videoAspectRatio$ = new BehaviorSubject(NaN);
const streamStats$ = (
scope: ObservableScope,
source: Track.Source,
): Behavior<
RTCInboundRtpStreamStats | RTCOutboundRtpStreamStats | undefined
> =>
scope.behavior(
combineLatest([participant$, showConnectionStats.value$]).pipe(
switchMap(([p, showConnectionStats]) =>
!p || !showConnectionStats
? of(undefined)
: observeRtpStreamStats$(p, source, statsType),
),
),
);
return {
...createMemberMedia(scope, {
@@ -130,25 +144,8 @@ export function createBaseUserMedia(
rtcBackendIdentity,
handRaised$,
reaction$,
audioStreamStats$: combineLatest([
participant$,
showConnectionStats.value$,
]).pipe(
switchMap(([p, showConnectionStats]) => {
//
if (!p || !showConnectionStats) return of(undefined);
return observeRtpStreamStats$(p, Track.Source.Microphone, statsType);
}),
),
videoStreamStats$: combineLatest([
participant$,
showConnectionStats.value$,
]).pipe(
switchMap(([p, showConnectionStats]) => {
if (!p || !showConnectionStats) return of(undefined);
return observeRtpStreamStats$(p, Track.Source.Camera, statsType);
}),
),
audioStreamStats$: streamStats$(scope, Track.Source.Microphone),
videoStreamStats$: streamStats$(scope, Track.Source.Camera),
setVideoAspectRatio: (ratio) => videoAspectRatio$.next(ratio),
};
}