From 5f8809ce031fb19a50d2a73075130b6fc61eb4ff Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 30 Oct 2024 14:29:16 +0100 Subject: [PATCH 1/7] make tiles based on rtc member --- src/room/InCallView.tsx | 4 +- src/state/CallViewModel.ts | 270 ++++++++++++++++++++++-------------- src/state/MediaViewModel.ts | 100 ++++++++----- 3 files changed, 234 insertions(+), 140 deletions(-) diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 9492b2f01..a5847f0ef 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -118,7 +118,7 @@ export const ActiveCall: FC = (props) => { useEffect(() => { if (livekitRoom !== undefined) { const vm = new CallViewModel( - props.rtcSession.room, + props.rtcSession, livekitRoom, props.e2eeSystem, connStateObservable, @@ -127,7 +127,7 @@ export const ActiveCall: FC = (props) => { return (): void => vm.destroy(); } }, [ - props.rtcSession.room, + props.rtcSession, livekitRoom, props.e2eeSystem, connStateObservable, diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index db2833b85..734d24102 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -18,12 +18,9 @@ import { RemoteParticipant, Track, } from "livekit-client"; +import { Room as MatrixRoom, RoomMember } from "matrix-js-sdk/src/matrix"; import { - Room as MatrixRoom, - RoomMember, - RoomStateEvent, -} from "matrix-js-sdk/src/matrix"; -import { + BehaviorSubject, EMPTY, Observable, Subject, @@ -51,6 +48,10 @@ import { withLatestFrom, } from "rxjs"; import { logger } from "matrix-js-sdk/src/logger"; +import { + MatrixRTCSession, + MatrixRTCSessionEvent, +} from "matrix-js-sdk/src/matrixrtc"; import { ViewModel } from "./ViewModel"; import { @@ -164,28 +165,37 @@ enum SortingBin { class UserMedia { private readonly scope = new ObservableScope(); public readonly vm: UserMediaViewModel; + public participant: BehaviorSubject< + LocalParticipant | RemoteParticipant | undefined + >; + public readonly speaker: Observable; public readonly presenter: Observable; - public constructor( public readonly id: string, member: RoomMember | undefined, - participant: LocalParticipant | RemoteParticipant, + participant: LocalParticipant | RemoteParticipant | undefined, encryptionSystem: EncryptionSystem, ) { - this.vm = participant.isLocal - ? new LocalUserMediaViewModel( - id, - member, - participant as LocalParticipant, - encryptionSystem, - ) - : new RemoteUserMediaViewModel( - id, - member, - participant as RemoteParticipant, - encryptionSystem, - ); + this.participant = new BehaviorSubject(participant); + + if (participant && participant.isLocal) { + this.vm = new LocalUserMediaViewModel( + this.id, + member, + this.participant.asObservable() as Observable, + encryptionSystem, + ); + } else { + this.vm = new RemoteUserMediaViewModel( + id, + member, + this.participant.asObservable() as Observable< + RemoteParticipant | undefined + >, + encryptionSystem, + ); + } this.speaker = this.vm.speaking.pipe( // Require 1 s of continuous speaking to become a speaker, and 60 s of @@ -195,7 +205,7 @@ class UserMedia { timer(s ? 1000 : 60000), // If the speaking flag resets to its original value during this time, // end the silencing window to stick with that original value - this.vm.speaking.pipe(filter((s1) => s1 !== s)), + this.vm!.speaking.pipe(filter((s1) => s1 !== s)), ), ), startWith(false), @@ -205,13 +215,21 @@ class UserMedia { this.scope.state(), ); - this.presenter = observeParticipantEvents( - participant, - ParticipantEvent.TrackPublished, - ParticipantEvent.TrackUnpublished, - ParticipantEvent.LocalTrackPublished, - ParticipantEvent.LocalTrackUnpublished, - ).pipe(map((p) => p.isScreenShareEnabled)); + this.presenter = this.participant.pipe( + switchMap( + (p) => + (p && + observeParticipantEvents( + p, + ParticipantEvent.TrackPublished, + ParticipantEvent.TrackUnpublished, + ParticipantEvent.LocalTrackPublished, + ParticipantEvent.LocalTrackUnpublished, + ).pipe(map((p) => p.isScreenShareEnabled))) ?? + of(false), + ), + this.scope.state(), + ); } public destroy(): void { @@ -222,6 +240,7 @@ class UserMedia { class ScreenShare { public readonly vm: ScreenShareViewModel; + private participant: BehaviorSubject; public constructor( id: string, @@ -229,10 +248,12 @@ class ScreenShare { participant: LocalParticipant | RemoteParticipant, encryptionSystem: EncryptionSystem, ) { + this.participant = new BehaviorSubject(participant); + this.vm = new ScreenShareViewModel( id, member, - participant, + this.participant.asObservable(), encryptionSystem, ); } @@ -244,7 +265,7 @@ class ScreenShare { type MediaItem = UserMedia | ScreenShare; -function findMatrixMember( +function findMatrixRoomMember( room: MatrixRoom, id: string, ): RoomMember | undefined { @@ -344,8 +365,15 @@ export class CallViewModel extends ViewModel { this.remoteParticipants, observeParticipantMedia(this.livekitRoom.localParticipant), duplicateTiles.value, - // Also react to changes in the list of members - fromEvent(this.matrixRoom, RoomStateEvent.Update).pipe(startWith(null)), + // Also react to changes in the MatrixRTC session list: + fromEvent( + this.matrixRTCSession, + MatrixRTCSessionEvent.MembershipsChanged, + ).pipe(startWith(null)), + // fromEvent( + // this.matrixRTCSession, + // MatrixRTCSessionEvent.EncryptionKeyChanged, + // ).pipe(startWith(null)), ]).pipe( scan( ( @@ -354,42 +382,64 @@ export class CallViewModel extends ViewModel { ) => { const newItems = new Map( function* (this: CallViewModel): Iterable<[string, MediaItem]> { - for (const p of [localParticipant, ...remoteParticipants]) { - const id = p === localParticipant ? "local" : p.identity; - const member = findMatrixMember(this.matrixRoom, id); - if (member === undefined) - logger.warn( - `Ruh, roh! No matrix member found for SFU participant '${p.identity}': creating g-g-g-ghost!`, + for (const rtcMember of this.matrixRTCSession.memberships) { + const room = this.matrixRTCSession.room; + // WARN! This is not exactly the sender but the user defined in the state key. + // This will be available once we change to the new "member as object" format in the MatrixRTC object. + let mediaId = rtcMember.sender + ":" + rtcMember.deviceId; + let participant = undefined; + if ( + rtcMember.sender === room.client.getUserId()! && + rtcMember.deviceId === room.client.getDeviceId() + ) { + mediaId = "local"; + participant = localParticipant; + } else { + participant = remoteParticipants.find( + (p) => p.identity === mediaId, ); + } + + const member = findMatrixRoomMember(room, mediaId); - // Create as many tiles for this participant as called for by - // the duplicateTiles option for (let i = 0; i < 1 + duplicateTiles; i++) { - const userMediaId = `${id}:${i}`; + const indexedMediaId = `${mediaId}:${i}`; + const prevMedia = prevItems.get(indexedMediaId); + if (prevMedia && prevMedia instanceof UserMedia) { + if ( + prevMedia.participant.value === undefined && + participant !== undefined + ) { + // Update the BahviourSubject in the UserMedia. + prevMedia.participant.next(participant); + } + } yield [ - userMediaId, - prevItems.get(userMediaId) ?? + indexedMediaId, + // We create UserMedia with or without a participant. + // This will be the initial value of a BehaviourSubject. + // Once a participant appears we will update the BehaviourSubject. (see above) + prevMedia ?? new UserMedia( - userMediaId, + mediaId, member, - p, + participant, + this.encryptionSystem, + ), + ]; + } + if (participant && participant.isScreenShareEnabled) { + const screenShareId = `${mediaId}:screen-share`; + yield [ + screenShareId, + prevItems.get(screenShareId) ?? + new ScreenShare( + screenShareId, + member, + participant, this.encryptionSystem, ), ]; - - if (p.isScreenShareEnabled) { - const screenShareId = `${userMediaId}:screen-share`; - yield [ - screenShareId, - prevItems.get(screenShareId) ?? - new ScreenShare( - screenShareId, - member, - p, - this.encryptionSystem, - ), - ]; - } } } }.bind(this)(), @@ -432,42 +482,43 @@ export class CallViewModel extends ViewModel { distinctUntilChanged(), ); - private readonly spotlightSpeaker: Observable = - this.userMedia.pipe( - switchMap((mediaItems) => - mediaItems.length === 0 - ? of([]) - : combineLatest( - mediaItems.map((m) => - m.vm.speaking.pipe(map((s) => [m, s] as const)), - ), + private readonly spotlightSpeaker: Observable< + UserMediaViewModel | undefined + > = this.userMedia.pipe( + switchMap((mediaItems) => + mediaItems.length === 0 + ? of([]) + : combineLatest( + mediaItems.map((m) => + m.vm.speaking.pipe(map((s) => [m, s] as const)), ), - ), - scan<(readonly [UserMedia, boolean])[], UserMedia, null>( - (prev, mediaItems) => { - // Only remote users that are still in the call should be sticky - const [stickyMedia, stickySpeaking] = - (!prev?.vm.local && mediaItems.find(([m]) => m === prev)) || []; - // Decide who to spotlight: - // If the previous speaker is still speaking, stick with them rather - // than switching eagerly to someone else - return stickySpeaking - ? stickyMedia! - : // Otherwise, select any remote user who is speaking - (mediaItems.find(([m, s]) => !m.vm.local && s)?.[0] ?? - // Otherwise, stick with the person who was last speaking - stickyMedia ?? - // Otherwise, spotlight an arbitrary remote user - mediaItems.find(([m]) => !m.vm.local)?.[0] ?? - // Otherwise, spotlight the local user - mediaItems.find(([m]) => m.vm.local)![0]); - }, - null, - ), - map((speaker) => speaker.vm), - this.scope.state(), - throttleTime(1600, undefined, { leading: true, trailing: true }), - ); + ), + ), + scan<(readonly [UserMedia, boolean])[], UserMedia | undefined, null>( + (prev, mediaItems) => { + // Only remote users that are still in the call should be sticky + const [stickyMedia, stickySpeaking] = + (!prev?.vm.local && mediaItems.find(([m]) => m === prev)) || []; + // Decide who to spotlight: + // If the previous speaker is still speaking, stick with them rather + // than switching eagerly to someone else + return stickySpeaking + ? stickyMedia! + : // Otherwise, select any remote user who is speaking + (mediaItems.find(([m, s]) => !m.vm.local && s)?.[0] ?? + // Otherwise, stick with the person who was last speaking + stickyMedia ?? + // Otherwise, spotlight an arbitrary remote user + mediaItems.find(([m]) => !m.vm.local)?.[0] ?? + // Otherwise, spotlight the local user + mediaItems.find(([m]) => m.vm.local)?.[0]); + }, + null, + ), + map((speaker) => speaker?.vm), + this.scope.state(), + throttleTime(1600, undefined, { leading: true, trailing: true }), + ); private readonly grid: Observable = this.userMedia.pipe( switchMap((mediaItems) => { @@ -510,20 +561,29 @@ export class CallViewModel extends ViewModel { > = this.screenShares.pipe( map((screenShares) => screenShares.length > 0 - ? ([of(screenShares.map((m) => m.vm)), this.spotlightSpeaker] as const) + ? ([ + of(screenShares.map((m) => m.vm)), + this.spotlightSpeaker.pipe( + map((speaker) => (speaker && speaker) ?? null), + ), + ] as const) : ([ - this.spotlightSpeaker.pipe(map((speaker) => [speaker!])), + this.spotlightSpeaker.pipe( + map((speaker) => (speaker && [speaker]) ?? []), + ), this.spotlightSpeaker.pipe( switchMap((speaker) => - speaker.local - ? of(null) - : this.localUserMedia.pipe( - switchMap((vm) => - vm.alwaysShow.pipe( - map((alwaysShow) => (alwaysShow ? vm : null)), + speaker + ? speaker.local + ? of(null) + : this.localUserMedia.pipe( + switchMap((vm) => + vm.alwaysShow.pipe( + map((alwaysShow) => (alwaysShow ? vm : null)), + ), ), - ), - ), + ) + : of(null), ), ), ] as const), @@ -843,7 +903,7 @@ export class CallViewModel extends ViewModel { public constructor( // A call is permanently tied to a single Matrix room and LiveKit room - private readonly matrixRoom: MatrixRoom, + private readonly matrixRTCSession: MatrixRTCSession, private readonly livekitRoom: LivekitRoom, private readonly encryptionSystem: EncryptionSystem, private readonly connectionState: Observable, diff --git a/src/state/MediaViewModel.ts b/src/state/MediaViewModel.ts index 51a821af1..32c092572 100644 --- a/src/state/MediaViewModel.ts +++ b/src/state/MediaViewModel.ts @@ -68,33 +68,50 @@ export function useDisplayName(vm: MediaViewModel): string { } export function observeTrackReference( - participant: Participant, + participant: Observable, source: Track.Source, -): Observable { - return observeParticipantMedia(participant).pipe( - map(() => ({ - participant, - publication: participant.getTrackPublication(source), - source, - })), - distinctUntilKeyChanged("publication"), +): Observable { + const obs = participant.pipe( + switchMap((p) => { + if (p) { + return observeParticipantMedia(p).pipe( + map(() => ({ + participant: p, + publication: p.getTrackPublication(source), + source, + })), + distinctUntilKeyChanged("publication"), + ); + } else { + return of(undefined); + } + }), ); + return obs; } abstract class BaseMediaViewModel extends ViewModel { /** * Whether the media belongs to the local user. */ - public readonly local = this.participant.isLocal; + public readonly local = this.participant.pipe( + // We can assume, that the user is not local if the participant is undefined + // We assume the local LK participant will always be available. + map((p) => p?.isLocal ?? false), + ); /** * The LiveKit video track for this media. */ - public readonly video: Observable; + public readonly video: Observable; /** * Whether there should be a warning that this media is unencrypted. */ public readonly unencryptedWarning: Observable; + public readonly isRTCParticipantAvailable = this.participant.pipe( + map((p) => !!p), + ); + public constructor( /** * An opaque identifier for this media. @@ -106,7 +123,12 @@ abstract class BaseMediaViewModel extends ViewModel { // TODO: Fully separate the data layer from the UI layer by keeping the // member object internal public readonly member: RoomMember | undefined, - protected readonly participant: LocalParticipant | RemoteParticipant, + // We dont necassarly have a participant if a user connects via MatrixRTC but not (not yet) through + // livekit. + protected readonly participant: Observable< + LocalParticipant | RemoteParticipant | undefined + >, + encryptionSystem: EncryptionSystem, audioSource: AudioSource, videoSource: VideoSource, @@ -122,8 +144,8 @@ abstract class BaseMediaViewModel extends ViewModel { [audio, this.video], (a, v) => encryptionSystem.kind !== E2eeType.NONE && - (a.publication?.isEncrypted === false || - v.publication?.isEncrypted === false), + (a?.publication?.isEncrypted === false || + v?.publication?.isEncrypted === false), ).pipe(this.scope.state()); } } @@ -143,12 +165,20 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel { /** * Whether the participant is speaking. */ - public readonly speaking = observeParticipantEvents( - this.participant, - ParticipantEvent.IsSpeakingChanged, - ).pipe( - map((p) => p.isSpeaking), - this.scope.state(), + public readonly speaking = this.participant.pipe( + switchMap((p) => { + if (p) { + return observeParticipantEvents( + p, + ParticipantEvent.IsSpeakingChanged, + ).pipe( + map((p) => p.isSpeaking), + this.scope.state(), + ); + } else { + return of(false); + } + }), ); /** @@ -169,7 +199,7 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel { public constructor( id: string, member: RoomMember | undefined, - participant: LocalParticipant | RemoteParticipant, + participant: Observable, encryptionSystem: EncryptionSystem, ) { super( @@ -181,12 +211,17 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel { Track.Source.Camera, ); - const media = observeParticipantMedia(participant).pipe(this.scope.state()); + // const media = observeParticipantMedia(participant).pipe(this.scope.state()); + + const media = participant.pipe( + switchMap((p) => (p && observeParticipantMedia(p)) ?? of(undefined)), + this.scope.state(), + ); this.audioEnabled = media.pipe( - map((m) => m.microphoneTrack?.isMuted === false), + map((m) => m?.microphoneTrack?.isMuted === false), ); this.videoEnabled = media.pipe( - map((m) => m.cameraTrack?.isMuted === false), + map((m) => m?.cameraTrack?.isMuted === false), ); } @@ -204,7 +239,7 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel { */ public readonly mirror = this.video.pipe( switchMap((v) => { - const track = v.publication?.track; + const track = v?.publication?.track; if (!(track instanceof LocalTrack)) return of(false); // Watch for track restarts, because they indicate a camera switch return fromEvent(track, TrackEvent.Restarted).pipe( @@ -226,7 +261,7 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel { public constructor( id: string, member: RoomMember | undefined, - participant: LocalParticipant, + participant: Observable, encryptionSystem: EncryptionSystem, ) { super(id, member, participant, encryptionSystem); @@ -286,17 +321,16 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel { public constructor( id: string, member: RoomMember | undefined, - participant: RemoteParticipant, + participant: Observable, encryptionSystem: EncryptionSystem, ) { super(id, member, participant, encryptionSystem); // Sync the local volume with LiveKit - this.localVolume - .pipe(this.scope.bind()) - .subscribe((volume) => - (this.participant as RemoteParticipant).setVolume(volume), - ); + combineLatest([ + participant, + this.localVolume.pipe(this.scope.bind()), + ]).subscribe(([p, volume]) => p && p.setVolume(volume)); } public toggleLocallyMuted(): void { @@ -319,7 +353,7 @@ export class ScreenShareViewModel extends BaseMediaViewModel { public constructor( id: string, member: RoomMember | undefined, - participant: LocalParticipant | RemoteParticipant, + participant: Observable, encryptionSystem: EncryptionSystem, ) { super( From 3f233a65551106052ea2f10ee64f60383a0f1174 Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 30 Oct 2024 19:40:14 +0100 Subject: [PATCH 2/7] display missing lk participant + fix tile multiplier --- src/state/CallViewModel.ts | 7 ++----- src/tile/GridTile.tsx | 12 +++++++++++- src/tile/MediaView.tsx | 4 ++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index 734d24102..786e51383 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -406,10 +406,7 @@ export class CallViewModel extends ViewModel { const indexedMediaId = `${mediaId}:${i}`; const prevMedia = prevItems.get(indexedMediaId); if (prevMedia && prevMedia instanceof UserMedia) { - if ( - prevMedia.participant.value === undefined && - participant !== undefined - ) { + if (prevMedia.participant.value !== participant) { // Update the BahviourSubject in the UserMedia. prevMedia.participant.next(participant); } @@ -421,7 +418,7 @@ export class CallViewModel extends ViewModel { // Once a participant appears we will update the BehaviourSubject. (see above) prevMedia ?? new UserMedia( - mediaId, + indexedMediaId, member, participant, this.encryptionSystem, diff --git a/src/tile/GridTile.tsx b/src/tile/GridTile.tsx index 3675e9a7c..0cbe1d258 100644 --- a/src/tile/GridTile.tsx +++ b/src/tile/GridTile.tsx @@ -84,6 +84,9 @@ const UserMediaTile = forwardRef( const videoEnabled = useObservableEagerState(vm.videoEnabled); const speaking = useObservableEagerState(vm.speaking); const cropVideo = useObservableEagerState(vm.cropVideo); + const isRTCParticipantAvailable = useObservableEagerState( + vm.isRTCParticipantAvailable, + ); const onSelectFitContain = useCallback( (e: Event) => { e.preventDefault(); @@ -134,7 +137,10 @@ const UserMediaTile = forwardRef( className={styles.muteIcon} /> } - displayName={displayName} + displayName={ + displayName + + (isRTCParticipantAvailable ? "" : " missing Livekit Participant...") + } primaryButton={ { e.preventDefault(); @@ -248,6 +255,9 @@ const RemoteUserMediaTile = forwardRef< mirror={false} menuStart={ <> + {/* {isRTCParticipantAvailable + ? "is available" + : "Loading RTC participant"} */} { style?: ComponentProps["style"]; targetWidth: number; targetHeight: number; - video: TrackReferenceOrPlaceholder; + video: TrackReferenceOrPlaceholder | undefined; videoFit: "cover" | "contain"; mirror: boolean; member: RoomMember | undefined; @@ -83,7 +83,7 @@ export const MediaView = forwardRef( src={member?.getMxcAvatarUrl()} className={styles.avatar} /> - {video.publication !== undefined && ( + {video?.publication !== undefined && ( Date: Wed, 30 Oct 2024 20:10:06 +0100 Subject: [PATCH 3/7] add show_non_member_participants config option --- src/config/ConfigOptions.ts | 2 ++ src/state/CallViewModel.ts | 52 ++++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index 65f04c958..6718fc34f 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -112,6 +112,7 @@ export interface ResolvedConfigOptions extends ConfigOptions { enable_video: boolean; }; app_prompt: boolean; + show_non_member_participants: boolean; } export const DEFAULT_CONFIG: ResolvedConfigOptions = { @@ -127,4 +128,5 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = { enable_video: true, }, app_prompt: true, + show_non_member_participants: false, }; diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index 786e51383..125e473c7 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -72,6 +72,7 @@ import { duplicateTiles } from "../settings/settings"; import { isFirefox } from "../Platform"; import { setPipEnabled } from "../controls"; import { EncryptionSystem } from "../e2ee/sharedKeyManagement"; +import { Config } from "../config/Config"; // How long we wait after a focus switch before showing the real participant // list again @@ -442,7 +443,56 @@ export class CallViewModel extends ViewModel { }.bind(this)(), ); - for (const [id, t] of prevItems) if (!newItems.has(id)) t.destroy(); + // Generate non member items (items without a corresponding MatrixRTC member) + // Those items should not be rendered, they are participants in livekit that do not have a corresponding + // matrix rtc members. This cannot be any good: + // - A malicious user impersonates someone + // - Someone injects abusive content + // - The user cannot have encryption keys so it makes no sense to participate + // We can only trust users that have a matrixRTC member event. + // + // This is still available as a debug option. This can be useful + // - If one wants to test scalability using the livekit cli. + // - If an experimental project does not yet do the matrixRTC bits. + // - If someone wants to debug if the LK connection works but matrixRTC room state failed to arrive. + const debugShowNonMember = Config.get().show_non_member_participants; + const newNonMemberItems = debugShowNonMember + ? new Map( + function* (this: CallViewModel): Iterable<[string, MediaItem]> { + for (let p = 0; p < remoteParticipants.length; p++) { + for (let i = 0; i < 1 + duplicateTiles; i++) { + const participant = remoteParticipants[p]; + const maybeNoMemberParticipantId = + participant.identity + ":" + i; + if (!newItems.has(maybeNoMemberParticipantId)) { + yield [ + maybeNoMemberParticipantId, + // We create UserMedia with or without a participant. + // This will be the initial value of a BehaviourSubject. + // Once a participant appears we will update the BehaviourSubject. (see above) + prevItems.get(maybeNoMemberParticipantId) ?? + new UserMedia( + maybeNoMemberParticipantId, + undefined, + participant, + this.encrypted, + ), + ]; + } + } + } + }.bind(this)(), + ) + : new Map(); + if (newNonMemberItems.size > 0) { + logger.debug("Added NonMember items: ", newNonMemberItems); + } + const combinedNew = new Map([ + ...newNonMemberItems.entries(), + ...newItems.entries(), + ]); + + for (const [id, t] of prevItems) if (!combinedNew.has(id)) t.destroy(); return newItems; }, new Map(), From e1e202d7c8d2b93df5d5a5a7dae1bf625761ef67 Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 4 Nov 2024 12:23:51 +0100 Subject: [PATCH 4/7] per member tiles --- public/locales/en-GB/app.json | 1 + src/App.tsx | 9 +++++++ src/Header.tsx | 5 +++- src/config/ConfigOptions.ts | 4 ++-- src/room/InCallView.tsx | 2 ++ src/settings/SettingsModal.tsx | 17 +++++++++++++ src/settings/settings.ts | 2 ++ src/state/CallViewModel.ts | 44 ++++++++++++++++++++++------------ src/state/MediaViewModel.ts | 29 +++++++++++++++++++--- src/tile/GridTile.tsx | 2 ++ src/tile/MediaView.tsx | 9 +++++++ 11 files changed, 103 insertions(+), 21 deletions(-) diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 02dd77401..62274c216 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -146,6 +146,7 @@ "feedback_tab_thank_you": "Thanks, we received your feedback!", "feedback_tab_title": "Feedback", "more_tab_title": "More", + "non_member_tiles": "Show non member tiles", "opt_in_description": "<0><1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.", "preferences_tab_body": "Here you can configure extra options for an improved experience", "preferences_tab_h4": "Preferences", diff --git a/src/App.tsx b/src/App.tsx index 8d841dba7..1bc23be83 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,6 +28,8 @@ import { Initializer } from "./initializer"; import { MediaDevicesProvider } from "./livekit/MediaDevicesContext"; import { widget } from "./widget"; import { useTheme } from "./useTheme"; +import { nonMemberTiles } from "./settings/settings"; +import { Config } from "./config/Config"; const SentryRoute = Sentry.withSentryRouting(Route); @@ -71,6 +73,13 @@ export const App: FC = ({ history }) => { .catch(logger.error); }); + // Update settings to use the non member tile information from the config if set + useEffect(() => { + if (loaded && Config.get().show_non_member_tiles) { + nonMemberTiles.setValue(true); + } + }); + const errorPage = ; return ( diff --git a/src/Header.tsx b/src/Header.tsx index 69e77935c..c17a0288a 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -117,6 +117,7 @@ interface RoomHeaderInfoProps { avatarUrl: string | null; encrypted: boolean; participantCount: number | null; + nonMemberItemCount: number | null; } export const RoomHeaderInfo: FC = ({ @@ -125,6 +126,7 @@ export const RoomHeaderInfo: FC = ({ avatarUrl, encrypted, participantCount, + nonMemberItemCount, }) => { const { t } = useTranslation(); const size = useMediaQuery("(max-width: 550px)") ? "sm" : "lg"; @@ -157,7 +159,8 @@ export const RoomHeaderInfo: FC = ({ aria-label={t("header_participants_label")} /> - {t("participant_count", { count: participantCount ?? 0 })} + {t("participant_count", { count: participantCount ?? 0 })}{" "} + {(nonMemberItemCount ?? 0) > 0 && <>(+ {nonMemberItemCount})} )} diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index 6718fc34f..c8f6d3e9c 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -112,7 +112,7 @@ export interface ResolvedConfigOptions extends ConfigOptions { enable_video: boolean; }; app_prompt: boolean; - show_non_member_participants: boolean; + show_non_member_tiles: boolean; } export const DEFAULT_CONFIG: ResolvedConfigOptions = { @@ -128,5 +128,5 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = { enable_video: true, }, app_prompt: true, - show_non_member_participants: false, + show_non_member_tiles: false, }; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index a5847f0ef..66ef263d6 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -194,6 +194,7 @@ export const InCallView: FC = ({ } }, [connState, onLeave]); + const nonMemberItemCount = useObservableEagerState(vm.nonMemberItemCount); const containerRef1 = useRef(null); const [containerRef2, bounds] = useMeasure(); const boundsValid = bounds.height > 0; @@ -633,6 +634,7 @@ export const InCallView: FC = ({ avatarUrl={matrixInfo.roomAvatar} encrypted={matrixInfo.e2eeSystem.kind !== E2eeType.NONE} participantCount={participantCount} + nonMemberItemCount={nonMemberItemCount} /> diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index db702ef8f..6ef40c890 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -27,6 +27,7 @@ import { useSetting, developerSettingsTab as developerSettingsTabSetting, duplicateTiles as duplicateTilesSetting, + nonMemberTiles as nonMemberTilesSetting, useOptInAnalytics, } from "./settings"; import { isFirefox } from "../Platform"; @@ -68,6 +69,8 @@ export const SettingsModal: FC = ({ ); const [duplicateTiles, setDuplicateTiles] = useSetting(duplicateTilesSetting); + const [nonMemberTiles, setNonMemberTiles] = useSetting(nonMemberTilesSetting); + // Generate a `SelectInput` with a list of devices for a given device kind. const generateDeviceSelection = ( devices: MediaDevice, @@ -236,6 +239,20 @@ export const SettingsModal: FC = ({ )} /> + + ): void => { + setNonMemberTiles(event.target.checked); + }, + [setNonMemberTiles], + )} + /> + ), }; diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 109a882b2..293d5d590 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -72,6 +72,8 @@ export const developerSettingsTab = new Setting( export const duplicateTiles = new Setting("duplicate-tiles", 0); +export const nonMemberTiles = new Setting("non-member-tiles", true); + export const audioInput = new Setting( "audio-input", undefined, diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index 125e473c7..cc5f05444 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -68,7 +68,7 @@ import { } from "./MediaViewModel"; import { accumulate, finalizeValue } from "../utils/observable"; import { ObservableScope } from "./ObservableScope"; -import { duplicateTiles } from "../settings/settings"; +import { duplicateTiles, nonMemberTiles } from "../settings/settings"; import { isFirefox } from "../Platform"; import { setPipEnabled } from "../controls"; import { EncryptionSystem } from "../e2ee/sharedKeyManagement"; @@ -177,6 +177,7 @@ class UserMedia { member: RoomMember | undefined, participant: LocalParticipant | RemoteParticipant | undefined, encryptionSystem: EncryptionSystem, + rtcSession: MatrixRTCSession, ) { this.participant = new BehaviorSubject(participant); @@ -186,6 +187,7 @@ class UserMedia { member, this.participant.asObservable() as Observable, encryptionSystem, + rtcSession, ); } else { this.vm = new RemoteUserMediaViewModel( @@ -195,6 +197,7 @@ class UserMedia { RemoteParticipant | undefined >, encryptionSystem, + rtcSession, ); } @@ -362,6 +365,7 @@ export class CallViewModel extends ViewModel { }, ); + public readonly nonMemberItemCount = new BehaviorSubject(0); private readonly mediaItems: Observable = combineLatest([ this.remoteParticipants, observeParticipantMedia(this.livekitRoom.localParticipant), @@ -371,15 +375,18 @@ export class CallViewModel extends ViewModel { this.matrixRTCSession, MatrixRTCSessionEvent.MembershipsChanged, ).pipe(startWith(null)), - // fromEvent( - // this.matrixRTCSession, - // MatrixRTCSessionEvent.EncryptionKeyChanged, - // ).pipe(startWith(null)), + nonMemberTiles.value, ]).pipe( scan( ( prevItems, - [remoteParticipants, { participant: localParticipant }, duplicateTiles], + [ + remoteParticipants, + { participant: localParticipant }, + duplicateTiles, + _participantChange, + nonMemberTiles, + ], ) => { const newItems = new Map( function* (this: CallViewModel): Iterable<[string, MediaItem]> { @@ -423,6 +430,7 @@ export class CallViewModel extends ViewModel { member, participant, this.encryptionSystem, + this.matrixRTCSession, ), ]; } @@ -455,27 +463,28 @@ export class CallViewModel extends ViewModel { // - If one wants to test scalability using the livekit cli. // - If an experimental project does not yet do the matrixRTC bits. // - If someone wants to debug if the LK connection works but matrixRTC room state failed to arrive. - const debugShowNonMember = Config.get().show_non_member_participants; + const debugShowNonMember = nonMemberTiles; //Config.get().show_non_member_tiles; const newNonMemberItems = debugShowNonMember ? new Map( function* (this: CallViewModel): Iterable<[string, MediaItem]> { - for (let p = 0; p < remoteParticipants.length; p++) { + for (const participant of remoteParticipants) { for (let i = 0; i < 1 + duplicateTiles; i++) { - const participant = remoteParticipants[p]; - const maybeNoMemberParticipantId = + const maybeNonMemberParticipantId = participant.identity + ":" + i; - if (!newItems.has(maybeNoMemberParticipantId)) { + if (!newItems.has(maybeNonMemberParticipantId)) { + const nonMemberId = maybeNonMemberParticipantId; yield [ - maybeNoMemberParticipantId, + nonMemberId, // We create UserMedia with or without a participant. // This will be the initial value of a BehaviourSubject. // Once a participant appears we will update the BehaviourSubject. (see above) - prevItems.get(maybeNoMemberParticipantId) ?? + prevItems.get(nonMemberId) ?? new UserMedia( - maybeNoMemberParticipantId, + nonMemberId, undefined, participant, this.encrypted, + this.matrixRTCSession, ), ]; } @@ -487,13 +496,18 @@ export class CallViewModel extends ViewModel { if (newNonMemberItems.size > 0) { logger.debug("Added NonMember items: ", newNonMemberItems); } + const newNonMemberItemCount = + newNonMemberItems.size / (1 + duplicateTiles); + if (this.nonMemberItemCount.value !== newNonMemberItemCount) + this.nonMemberItemCount.next(newNonMemberItemCount); + const combinedNew = new Map([ ...newNonMemberItems.entries(), ...newItems.entries(), ]); for (const [id, t] of prevItems) if (!combinedNew.has(id)) t.destroy(); - return newItems; + return combinedNew; }, new Map(), ), diff --git a/src/state/MediaViewModel.ts b/src/state/MediaViewModel.ts index 32c092572..c83d6c08a 100644 --- a/src/state/MediaViewModel.ts +++ b/src/state/MediaViewModel.ts @@ -37,6 +37,11 @@ import { switchMap, } from "rxjs"; import { useEffect } from "react"; +import { + MatrixRTCSession, + MatrixRTCSessionEvent, +} from "matrix-js-sdk/src/matrixrtc"; +import { logger } from "matrix-js-sdk/src/logger"; import { ViewModel } from "./ViewModel"; import { useReactiveState } from "../useReactiveState"; @@ -196,11 +201,16 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel { */ public readonly cropVideo: Observable = this._cropVideo; + public readonly keys = new BehaviorSubject( + [] as { index: number; key: Uint8Array }[], + ); + public constructor( id: string, member: RoomMember | undefined, participant: Observable, encryptionSystem: EncryptionSystem, + rtcSession: MatrixRTCSession, ) { super( id, @@ -211,7 +221,18 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel { Track.Source.Camera, ); - // const media = observeParticipantMedia(participant).pipe(this.scope.state()); + // rtcSession.on( + // MatrixRTCSessionEvent.EncryptionKeyChanged, + // (key, index, participantId) => { + // if (id.startsWith(participantId)) + // logger.info("got new keys: ", participant, { index, key }); + // logger.info("All keys for participant ", participant, " - ", [ + // ...this.keys.value, + // { index, key }, + // ]); + // this.keys.next([...this.keys.value, { index, key }]); + // }, + // ); const media = participant.pipe( switchMap((p) => (p && observeParticipantMedia(p)) ?? of(undefined)), @@ -263,8 +284,9 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel { member: RoomMember | undefined, participant: Observable, encryptionSystem: EncryptionSystem, + rtcSession: MatrixRTCSession, ) { - super(id, member, participant, encryptionSystem); + super(id, member, participant, encryptionSystem, rtcSession); } } @@ -323,8 +345,9 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel { member: RoomMember | undefined, participant: Observable, encryptionSystem: EncryptionSystem, + rtcSession: MatrixRTCSession, ) { - super(id, member, participant, encryptionSystem); + super(id, member, participant, encryptionSystem, rtcSession); // Sync the local volume with LiveKit combineLatest([ diff --git a/src/tile/GridTile.tsx b/src/tile/GridTile.tsx index 0cbe1d258..980cb4f26 100644 --- a/src/tile/GridTile.tsx +++ b/src/tile/GridTile.tsx @@ -84,6 +84,7 @@ const UserMediaTile = forwardRef( const videoEnabled = useObservableEagerState(vm.videoEnabled); const speaking = useObservableEagerState(vm.speaking); const cropVideo = useObservableEagerState(vm.cropVideo); + const keys = useObservableEagerState(vm.keys); const isRTCParticipantAvailable = useObservableEagerState( vm.isRTCParticipantAvailable, ); @@ -121,6 +122,7 @@ const UserMediaTile = forwardRef( ref={ref} video={video} member={vm.member} + keys={keys} unencryptedWarning={unencryptedWarning} videoEnabled={videoEnabled && showVideo} videoFit={cropVideo ? "cover" : "contain"} diff --git a/src/tile/MediaView.tsx b/src/tile/MediaView.tsx index 6cc9086f6..a8ec58c94 100644 --- a/src/tile/MediaView.tsx +++ b/src/tile/MediaView.tsx @@ -29,6 +29,7 @@ interface Props extends ComponentProps { videoFit: "cover" | "contain"; mirror: boolean; member: RoomMember | undefined; + keys: { index: number; key: Uint8Array }[]; videoEnabled: boolean; unencryptedWarning: boolean; nameTagLeadingIcon?: ReactNode; @@ -48,6 +49,7 @@ export const MediaView = forwardRef( videoFit, mirror, member, + keys, videoEnabled, unencryptedWarning, nameTagLeadingIcon, @@ -98,11 +100,18 @@ export const MediaView = forwardRef( minature={avatarSize < 96} showTimer={handRaiseTimerVisible} /> + {/* {keys && + keys.map(({ index, key }) => ( + + index:{index}, key:{key} + + ))} */}
{nameTagLeadingIcon} {displayName} + {unencryptedWarning && ( Date: Mon, 4 Nov 2024 15:38:17 +0100 Subject: [PATCH 5/7] merge fixes --- src/state/CallViewModel.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index cc5f05444..a3455af89 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -72,7 +72,6 @@ import { duplicateTiles, nonMemberTiles } from "../settings/settings"; import { isFirefox } from "../Platform"; import { setPipEnabled } from "../controls"; import { EncryptionSystem } from "../e2ee/sharedKeyManagement"; -import { Config } from "../config/Config"; // How long we wait after a focus switch before showing the real participant // list again @@ -295,11 +294,11 @@ function findMatrixRoomMember( export class CallViewModel extends ViewModel { public readonly localVideo: Observable = observeTrackReference( - this.livekitRoom.localParticipant, + of(this.livekitRoom.localParticipant), Track.Source.Camera, ).pipe( map((trackRef) => { - const track = trackRef.publication?.track; + const track = trackRef?.publication?.track; return track instanceof LocalVideoTrack ? track : null; }), ); @@ -366,6 +365,7 @@ export class CallViewModel extends ViewModel { ); public readonly nonMemberItemCount = new BehaviorSubject(0); + private readonly mediaItems: Observable = combineLatest([ this.remoteParticipants, observeParticipantMedia(this.livekitRoom.localParticipant), @@ -409,15 +409,25 @@ export class CallViewModel extends ViewModel { } const member = findMatrixRoomMember(room, mediaId); - + if (!member) { + logger.error("Could not find member for media id: ", mediaId); + } for (let i = 0; i < 1 + duplicateTiles; i++) { const indexedMediaId = `${mediaId}:${i}`; - const prevMedia = prevItems.get(indexedMediaId); + let prevMedia = prevItems.get(indexedMediaId); if (prevMedia && prevMedia instanceof UserMedia) { if (prevMedia.participant.value !== participant) { // Update the BahviourSubject in the UserMedia. prevMedia.participant.next(participant); } + if (prevMedia.vm.member === undefined) { + // We have a previous media created because of the `debugShowNonMember` flag. + // In this case we actually replace the media item. + // This "hack" never occurs if we do not use the `debugShowNonMember` debugging + // option and if we always find a room member for each rtc member (which also + // only fails if we have a fundamental problem) + prevMedia = undefined; + } } yield [ indexedMediaId, @@ -483,7 +493,7 @@ export class CallViewModel extends ViewModel { nonMemberId, undefined, participant, - this.encrypted, + this.encryptionSystem, this.matrixRTCSession, ), ]; From 91302349b4d03d17867cb4b8ff9aaf0284fd1dc9 Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 4 Nov 2024 15:55:51 +0100 Subject: [PATCH 6/7] add media key debugging option --- public/locales/en-GB/app.json | 1 + src/settings/SettingsModal.tsx | 17 ++++++++++ src/settings/settings.ts | 4 ++- src/state/MediaViewModel.ts | 29 +++++++++-------- src/tile/MediaView.tsx | 58 ++++++++++++++++++++++++++++------ 5 files changed, 86 insertions(+), 23 deletions(-) diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 62274c216..a87aeef39 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -152,6 +152,7 @@ "preferences_tab_h4": "Preferences", "preferences_tab_show_hand_raised_timer_description": "Show a timer when a participant raises their hand", "preferences_tab_show_hand_raised_timer_label": "Show hand raise duration", + "show_media_keys": "Show media encryption keys", "speaker_device_selection_label": "Speaker" }, "star_rating_input_label_one": "{{count}} stars", diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 6ef40c890..2b1b89955 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -28,6 +28,7 @@ import { developerSettingsTab as developerSettingsTabSetting, duplicateTiles as duplicateTilesSetting, nonMemberTiles as nonMemberTilesSetting, + showMediaKeys as showMediaKeysSetting, useOptInAnalytics, } from "./settings"; import { isFirefox } from "../Platform"; @@ -71,6 +72,8 @@ export const SettingsModal: FC = ({ const [nonMemberTiles, setNonMemberTiles] = useSetting(nonMemberTilesSetting); + const [showMediaKeys, setShowMediaKeys] = useSetting(showMediaKeysSetting); + // Generate a `SelectInput` with a list of devices for a given device kind. const generateDeviceSelection = ( devices: MediaDevice, @@ -253,6 +256,20 @@ export const SettingsModal: FC = ({ )} /> + + ): void => { + setShowMediaKeys(event.target.checked); + }, + [setShowMediaKeys], + )} + /> + ), }; diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 293d5d590..f6cf3803f 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -72,7 +72,9 @@ export const developerSettingsTab = new Setting( export const duplicateTiles = new Setting("duplicate-tiles", 0); -export const nonMemberTiles = new Setting("non-member-tiles", true); +export const nonMemberTiles = new Setting("non-member-tiles", false); + +export const showMediaKeys = new Setting("non-member-tiles", false); export const audioInput = new Setting( "audio-input", diff --git a/src/state/MediaViewModel.ts b/src/state/MediaViewModel.ts index c83d6c08a..f3257243f 100644 --- a/src/state/MediaViewModel.ts +++ b/src/state/MediaViewModel.ts @@ -41,7 +41,6 @@ import { MatrixRTCSession, MatrixRTCSessionEvent, } from "matrix-js-sdk/src/matrixrtc"; -import { logger } from "matrix-js-sdk/src/logger"; import { ViewModel } from "./ViewModel"; import { useReactiveState } from "../useReactiveState"; @@ -221,18 +220,22 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel { Track.Source.Camera, ); - // rtcSession.on( - // MatrixRTCSessionEvent.EncryptionKeyChanged, - // (key, index, participantId) => { - // if (id.startsWith(participantId)) - // logger.info("got new keys: ", participant, { index, key }); - // logger.info("All keys for participant ", participant, " - ", [ - // ...this.keys.value, - // { index, key }, - // ]); - // this.keys.next([...this.keys.value, { index, key }]); - // }, - // ); + combineLatest([ + participant, + fromEvent(rtcSession, MatrixRTCSessionEvent.EncryptionKeyChanged).pipe( + startWith(null), + ), + ]).subscribe(([par, ev]) => { + for (const participantKeys of rtcSession.getEncryptionKeys()) { + if (participantKeys[0] === par?.identity) { + this.keys.next( + Array.from(participantKeys[1].entries()).map(([i, k]) => { + return { index: i, key: k }; + }), + ); + } + } + }); const media = participant.pipe( switchMap((p) => (p && observeParticipantMedia(p)) ?? of(undefined)), diff --git a/src/tile/MediaView.tsx b/src/tile/MediaView.tsx index a8ec58c94..642ee9735 100644 --- a/src/tile/MediaView.tsx +++ b/src/tile/MediaView.tsx @@ -7,8 +7,8 @@ Please see LICENSE in the repository root for full details. import { TrackReferenceOrPlaceholder } from "@livekit/components-core"; import { animated } from "@react-spring/web"; -import { RoomMember } from "matrix-js-sdk/src/matrix"; -import { ComponentProps, ReactNode, forwardRef } from "react"; +import { encodeUnpaddedBase64, RoomMember } from "matrix-js-sdk/src/matrix"; +import { ComponentProps, FC, ReactNode, forwardRef } from "react"; import { useTranslation } from "react-i18next"; import classNames from "classnames"; import { VideoTrack } from "@livekit/components-react"; @@ -18,7 +18,11 @@ import { ErrorIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import styles from "./MediaView.module.css"; import { Avatar } from "../Avatar"; import { RaisedHandIndicator } from "../reactions/RaisedHandIndicator"; -import { showHandRaisedTimer, useSetting } from "../settings/settings"; +import { + showHandRaisedTimer, + showMediaKeys as showMediaKeysSettings, + useSetting, +} from "../settings/settings"; interface Props extends ComponentProps { className?: string; @@ -62,6 +66,7 @@ export const MediaView = forwardRef( ) => { const { t } = useTranslation(); const [handRaiseTimerVisible] = useSetting(showHandRaisedTimer); + const [showMediaKeys] = useSetting(showMediaKeysSettings); const avatarSize = Math.round(Math.min(targetWidth, targetHeight) / 2); @@ -100,12 +105,7 @@ export const MediaView = forwardRef( minature={avatarSize < 96} showTimer={handRaiseTimerVisible} /> - {/* {keys && - keys.map(({ index, key }) => ( - - index:{index}, key:{key} - - ))} */} + {keys && showMediaKeys && }
{nameTagLeadingIcon} @@ -132,5 +132,45 @@ export const MediaView = forwardRef( ); }, ); +interface MediaKeyListProps { + keys: { + index: number; + key: Uint8Array; + }[]; +} +export const MediaKeyList: FC = ({ keys }) => { + return ( +
+ {keys.map(({ index, key }) => ( +
+ + index:{index} + + + key:{key ? encodeUnpaddedBase64(key) : "unavailable"} + +
+ ))} +
+ ); +}; MediaView.displayName = "MediaView"; From 2e88ec98ebb14d7d23541de9217da7564f442b5f Mon Sep 17 00:00:00 2001 From: Timo Date: Tue, 5 Nov 2024 12:44:12 +0100 Subject: [PATCH 7/7] prettier --- src/room/InCallView.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 66ef263d6..ab54f856b 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -126,12 +126,7 @@ export const ActiveCall: FC = (props) => { setVm(vm); return (): void => vm.destroy(); } - }, [ - props.rtcSession, - livekitRoom, - props.e2eeSystem, - connStateObservable, - ]); + }, [props.rtcSession, livekitRoom, props.e2eeSystem, connStateObservable]); if (livekitRoom === undefined || vm === null) return null;