diff --git a/Client/tauri-client/src/components/ChannelSidebar.ts b/Client/tauri-client/src/components/ChannelSidebar.ts index 690c4cea..c83457ea 100644 --- a/Client/tauri-client/src/components/ChannelSidebar.ts +++ b/Client/tauri-client/src/components/ChannelSidebar.ts @@ -28,6 +28,8 @@ import { } from "@stores/ui.store"; import { voiceStore, getChannelVoiceUsers } from "@stores/voice.store"; import { setUserVolume, getUserVolume } from "@lib/livekitSession"; +import { SCREENSHARE_TILE_ID_OFFSET } from "@lib/constants"; +import { attachStreamPreview, attachScrollCollapse } from "@lib/streamPreview"; // --------------------------------------------------------------------------- // Per-user volume context menu (right-click on voice user row) @@ -310,13 +312,39 @@ function renderVoiceChannelItem( // Don't trigger if the right-click menu is open if (e.button !== 0) return; e.stopPropagation(); - onWatchStream(user.userId); + const tileId = user.screenshare + ? user.userId + SCREENSHARE_TILE_ID_OFFSET + : user.userId; + onWatchStream(tileId); }, { signal }); row.style.cursor = "pointer"; } + // Hover/focus preview for remote users with video + if ((currentUser === null || currentUser.id !== user.userId) + && (user.camera || user.screenshare)) { + const tileId = user.screenshare + ? user.userId + SCREENSHARE_TILE_ID_OFFSET + : user.userId; + attachStreamPreview( + row, + user.userId, + user.username || "Unknown", + user.screenshare, + user.camera, + signal, + () => { + // Placeholder click: join voice channel and watch stream + onVoiceJoin(channel.id); + if (onWatchStream !== undefined) onWatchStream(tileId); + }, + onWatchStream !== undefined ? () => onWatchStream(tileId) : undefined, + ); + } + usersContainer.appendChild(row); } + attachScrollCollapse(usersContainer, signal); wrapper.appendChild(usersContainer); } diff --git a/Client/tauri-client/src/lib/constants.ts b/Client/tauri-client/src/lib/constants.ts new file mode 100644 index 00000000..20c10dc8 --- /dev/null +++ b/Client/tauri-client/src/lib/constants.ts @@ -0,0 +1,2 @@ +/** Offset added to userId to produce a unique tile ID for screenshare tiles in the video grid. */ +export const SCREENSHARE_TILE_ID_OFFSET = 1_000_000; diff --git a/Client/tauri-client/src/lib/livekitSession.ts b/Client/tauri-client/src/lib/livekitSession.ts index 472d6113..34dede22 100644 --- a/Client/tauri-client/src/lib/livekitSession.ts +++ b/Client/tauri-client/src/lib/livekitSession.ts @@ -996,6 +996,19 @@ export class LiveKitSession { return null; } + /** Get a remote participant's video MediaStream by userId and track type. Returns null if not available. */ + getRemoteVideoStream(userId: number, type: "camera" | "screenshare"): MediaStream | null { + if (this.room === null) return null; + const participant = this.room.getParticipantByIdentity(`user-${userId}`); + if (participant === undefined) return null; + // Self-guard: don't return local participant's stream via this method + if (participant === this.room.localParticipant) return null; + const source = type === "screenshare" ? Track.Source.ScreenShare : Track.Source.Camera; + const pub = participant.getTrackPublication(source); + if (pub?.track?.mediaStreamTrack) return new MediaStream([pub.track.mediaStreamTrack]); + return null; + } + getRoom(): Room | null { return this.room; } @@ -1161,6 +1174,7 @@ export const setVoiceSensitivity = session.setVoiceSensitivity.bind(session); export const reapplyAudioProcessing = session.reapplyAudioProcessing.bind(session); export const getLocalCameraStream = session.getLocalCameraStream.bind(session); export const getLocalScreenshareStream = session.getLocalScreenshareStream.bind(session); +export const getRemoteVideoStream = session.getRemoteVideoStream.bind(session); export const getSessionDebugInfo = session.getSessionDebugInfo.bind(session); export const setScreenshareAudioVolume = session.setScreenshareAudioVolume.bind(session); export const muteScreenshareAudio = session.muteScreenshareAudio.bind(session); diff --git a/Client/tauri-client/src/lib/streamPreview.ts b/Client/tauri-client/src/lib/streamPreview.ts new file mode 100644 index 00000000..1e04e26a --- /dev/null +++ b/Client/tauri-client/src/lib/streamPreview.ts @@ -0,0 +1,283 @@ +/** + * Stream preview — hover/focus to see a live video preview of a remote + * participant's camera or screenshare in the voice channel sidebar. + * + * Lifecycle: + * mouseenter/focusin → 300ms debounce → create