From 6a4eac13a32ba0ce03241bef64b816537a6b82df Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:19:41 +0200 Subject: [PATCH] feat(sounds): add stream_watch data-channel protocol helpers --- .../web/src/utils/streamWatchProtocol.test.ts | 36 +++++++++++++++++++ packages/web/src/utils/streamWatchProtocol.ts | 35 ++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 packages/web/src/utils/streamWatchProtocol.test.ts create mode 100644 packages/web/src/utils/streamWatchProtocol.ts diff --git a/packages/web/src/utils/streamWatchProtocol.test.ts b/packages/web/src/utils/streamWatchProtocol.test.ts new file mode 100644 index 00000000..7f3a25fe --- /dev/null +++ b/packages/web/src/utils/streamWatchProtocol.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from 'vitest'; +import { + encodeStreamWatch, + parseStreamWatch, + isStreamWatchPayload, +} from './streamWatchProtocol'; + +describe('streamWatchProtocol', () => { + it('round-trips an encoded payload', () => { + const payload = { type: 'stream_watch' as const, target: 'user-1', watching: true }; + const encoded = encodeStreamWatch(payload); + expect(encoded).toBeDefined(); + expect(encoded.length).toBeGreaterThan(0); + const parsed = parseStreamWatch(encoded); + expect(parsed).toEqual(payload); + }); + + it('parseStreamWatch returns null on invalid JSON', () => { + const bad = new TextEncoder().encode('not json'); + expect(parseStreamWatch(bad)).toBeNull(); + }); + + it('parseStreamWatch returns null on wrong message type', () => { + const other = new TextEncoder().encode(JSON.stringify({ type: 'deafen', deafened: true })); + expect(parseStreamWatch(other)).toBeNull(); + }); + + it('isStreamWatchPayload validates shape', () => { + expect(isStreamWatchPayload({ type: 'stream_watch', target: 'u', watching: true })).toBe(true); + expect(isStreamWatchPayload({ type: 'stream_watch', target: 'u', watching: 'yes' })).toBe(false); + expect(isStreamWatchPayload({ type: 'stream_watch', watching: true })).toBe(false); + expect(isStreamWatchPayload({ type: 'other', target: 'u', watching: true })).toBe(false); + expect(isStreamWatchPayload(null)).toBe(false); + expect(isStreamWatchPayload('string')).toBe(false); + }); +}); diff --git a/packages/web/src/utils/streamWatchProtocol.ts b/packages/web/src/utils/streamWatchProtocol.ts new file mode 100644 index 00000000..5073fbdf --- /dev/null +++ b/packages/web/src/utils/streamWatchProtocol.ts @@ -0,0 +1,35 @@ +/** + * Wire format for the LiveKit data-channel ping that announces a viewer + * has begun (or stopped) watching a screen share. Sent only from explicit + * user-action sites (StreamTile click handlers); receiver maintains the + * streamer-side watcher set. + */ +export interface StreamWatchPayload { + type: 'stream_watch'; + target: string; + watching: boolean; +} + +export function encodeStreamWatch(payload: StreamWatchPayload): Uint8Array { + return new TextEncoder().encode(JSON.stringify(payload)); +} + +export function isStreamWatchPayload(value: unknown): value is StreamWatchPayload { + if (typeof value !== 'object' || value === null) return false; + const v = value as Record; + return ( + v.type === 'stream_watch' && + typeof v.target === 'string' && + typeof v.watching === 'boolean' + ); +} + +export function parseStreamWatch(payload: Uint8Array): StreamWatchPayload | null { + try { + const text = new TextDecoder().decode(payload); + const parsed: unknown = JSON.parse(text); + return isStreamWatchPayload(parsed) ? parsed : null; + } catch { + return null; + } +}