From a6fc710d8ce170861a615ac717d1ac17f14363b0 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 1 Jun 2026 16:12:57 +0200 Subject: [PATCH] Add onPipMediaOrientationUpdate to controls api. --- src/controls.ts | 1 + src/state/CallViewModel/CallViewModel.ts | 25 ++++++++++++++++++++++++ src/state/media/UserMediaViewModel.ts | 13 ++++++++---- src/state/media/observeRtpStreamStats.ts | 4 +++- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/controls.ts b/src/controls.ts index 1ddb17049..1978946dc 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -12,6 +12,7 @@ export interface Controls { canEnterPip(): boolean; enablePip(): void; disablePip(): void; + onPipMediaOrientationUpdate?: (orientation: "landscape" | "portrait") => void; setAvailableAudioDevices(devices: OutputDevice[]): void; setAudioDevice(id: string): void; diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 2bbf6f4ec..bdeca679a 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -1191,6 +1191,31 @@ export function createCallViewModel$( })), ); + spotlight$ + .pipe( + switchMap((media) => { + let layout; + switch (media[0].type) { + case "user": + layout = media[0].videoOrientation$; + break; + case "ringing": + layout = of("landscape" as const); + break; + case "screen share": + layout = of("landscape" as const); + break; + } + return layout; + }), + tap((orientation) => { + logger.info("controls api pip orientation updated:", orientation); + window.controls.onPipMediaOrientationUpdate?.(orientation); + }), + scope.bind(), + ) + .subscribe(); + /** * The media to be used to produce a layout. */ diff --git a/src/state/media/UserMediaViewModel.ts b/src/state/media/UserMediaViewModel.ts index a20c489e6..ea0331030 100644 --- a/src/state/media/UserMediaViewModel.ts +++ b/src/state/media/UserMediaViewModel.ts @@ -48,6 +48,7 @@ export interface BaseUserMediaViewModel extends BaseMemberMediaViewModel { audioEnabled$: Behavior; videoEnabled$: Behavior; videoFit$: Behavior<"cover" | "contain">; + videoOrientation$: Behavior<"landscape" | "portrait">; toggleCropVideo: () => void; /** * The expected identity of the LiveKit participant. Exposed for debugging. @@ -104,6 +105,7 @@ export function createBaseUserMedia( { width: number; height: number } | undefined >(undefined); + const videoSize$ = videoSizeFromParticipant$(participant$); return { ...createMemberMedia(scope, { ...inputs, @@ -129,11 +131,14 @@ export function createBaseUserMedia( videoEnabled$: scope.behavior( media$.pipe(map((m) => m?.cameraTrack?.isMuted === false)), ), - videoFit$: videoFit$( - scope, - videoSizeFromParticipant$(participant$), - targetSize$, + videoOrientation$: scope.behavior( + videoSize$.pipe( + map((s) => (s ? s.width / s.height : 1)), + map((aspect) => (aspect > 1 ? "landscape" : "portrait")), + ), + "portrait", ), + videoFit$: videoFit$(scope, videoSize$, targetSize$), toggleCropVideo: () => toggleCropVideo$.next(), rtcBackendIdentity, handRaised$, diff --git a/src/state/media/observeRtpStreamStats.ts b/src/state/media/observeRtpStreamStats.ts index 63fb1a1b0..457b8f727 100644 --- a/src/state/media/observeRtpStreamStats.ts +++ b/src/state/media/observeRtpStreamStats.ts @@ -32,7 +32,9 @@ export function observeRtpStreamStats$( > { return combineLatest([ observeTrackReference$(participant, source), - interval(1000).pipe(startWith(0)), + // The update frequency is high because we use this value to update the PiP oreintation and the fit/fill video tile props based on that + // We want it to be responsive. For just the debug tools 1s would be sufficient. + interval(350).pipe(startWith(0)), ]).pipe( switchMap(async ([trackReference]) => { const track = trackReference?.publication?.track;