From 7c0b67bcc91bcea5b2f58c587dacfc7a6416d4b2 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:38:39 +0200 Subject: [PATCH] feat(sounds): broadcast stream_watch from StreamTile click sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire broadcastStreamWatch (LiveKit data-channel ping) to all three explicit user-action sites in StreamTile: "Stop Watching" context-menu item, "Watch Stream" context-menu item, and the handleWatch button callback. Broadcast is intentionally confined to click sites only — not wired in voiceStore actions — to prevent noisy stream_user_left sounds on automatic teardown paths in useLiveKit.ts. --- packages/web/src/components/voice/StreamTile.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/web/src/components/voice/StreamTile.tsx b/packages/web/src/components/voice/StreamTile.tsx index 04776d67..318cd091 100644 --- a/packages/web/src/components/voice/StreamTile.tsx +++ b/packages/web/src/components/voice/StreamTile.tsx @@ -4,6 +4,7 @@ import { useVoiceStore } from '../../stores/voiceStore'; import { useContextMenuStore, type ContextMenuItem } from '../../stores/contextMenuStore'; import { getActiveRoom, setStreamSubscription } from '../../hooks/useLiveKit'; import { stopScreenShare, changeScreenShare } from '../../utils/screenShare'; +import { encodeStreamWatch } from '../../utils/streamWatchProtocol'; import { ScreenShareSettingsPopover } from './ScreenShareSettingsPopover'; import { useVoiceParticipantMeta } from '../../hooks/useVoiceParticipantMeta'; import type { StreamTile as StreamTileType } from '../../hooks/useLiveKit'; @@ -140,6 +141,14 @@ function StreamAttenuationItem() { ); } +function broadcastStreamWatch(streamerUserId: string, watching: boolean): void { + const room = getActiveRoom(); + if (!room) return; + const payload = encodeStreamWatch({ type: 'stream_watch', target: streamerUserId, watching }); + // Reliable delivery; data channels are room-scoped and small (under 1KB). + void room.localParticipant.publishData(payload, { reliable: true }); +} + export function StreamTile({ tile, large }: StreamTileProps) { const videoRef = useRef(null); @@ -266,6 +275,7 @@ export function StreamTile({ tile, large }: StreamTileProps) { onClick: () => { useVoiceStore.getState().unwatchStream(userId); setStreamSubscription(getActiveRoom(), identity, false); + broadcastStreamWatch(userId, false); }, }); } else { @@ -279,6 +289,7 @@ export function StreamTile({ tile, large }: StreamTileProps) { onClick: () => { useVoiceStore.getState().watchStream(userId); setStreamSubscription(getActiveRoom(), identity, true); + broadcastStreamWatch(userId, true); }, }); } @@ -322,6 +333,7 @@ export function StreamTile({ tile, large }: StreamTileProps) { const handleWatch = useCallback(() => { useVoiceStore.getState().watchStream(userId); setStreamSubscription(getActiveRoom(), participant.identity, true); + broadcastStreamWatch(userId, true); }, [userId, participant.identity]); const hasVideo = liveScreenTrack !== null;