@conference-kit/react 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/components/AudioPlayer.d.ts +5 -0
  2. package/dist/components/AudioPlayer.d.ts.map +1 -0
  3. package/dist/components/AudioPlayer.js +16 -0
  4. package/dist/components/AudioPlayer.js.map +1 -0
  5. package/dist/components/ErrorBanner.d.ts +6 -0
  6. package/dist/components/ErrorBanner.d.ts.map +1 -0
  7. package/dist/components/ErrorBanner.js +12 -0
  8. package/dist/components/ErrorBanner.js.map +1 -0
  9. package/dist/components/StatusBadge.d.ts +7 -0
  10. package/dist/components/StatusBadge.d.ts.map +1 -0
  11. package/dist/components/StatusBadge.js +30 -0
  12. package/dist/components/StatusBadge.js.map +1 -0
  13. package/dist/components/VideoPlayer.d.ts +5 -0
  14. package/dist/components/VideoPlayer.d.ts.map +1 -0
  15. package/dist/components/VideoPlayer.js +35 -0
  16. package/dist/components/VideoPlayer.js.map +1 -0
  17. package/dist/config/features.d.ts +10 -0
  18. package/dist/config/features.d.ts.map +1 -0
  19. package/dist/config/features.js +11 -0
  20. package/dist/config/features.js.map +1 -0
  21. package/dist/context/WebRTCProvider.d.ts +12 -0
  22. package/dist/context/WebRTCProvider.d.ts.map +1 -0
  23. package/dist/context/WebRTCProvider.js +13 -0
  24. package/dist/context/WebRTCProvider.js.map +1 -0
  25. package/dist/hooks/useCall.d.ts +31 -0
  26. package/dist/hooks/useCall.d.ts.map +1 -0
  27. package/dist/hooks/useCall.js +168 -0
  28. package/dist/hooks/useCall.js.map +1 -0
  29. package/dist/hooks/useCallState.d.ts +18 -0
  30. package/dist/hooks/useCallState.d.ts.map +1 -0
  31. package/dist/hooks/useCallState.js +33 -0
  32. package/dist/hooks/useCallState.js.map +1 -0
  33. package/dist/hooks/useDataChannel.d.ts +13 -0
  34. package/dist/hooks/useDataChannel.d.ts.map +1 -0
  35. package/dist/hooks/useDataChannel.js +33 -0
  36. package/dist/hooks/useDataChannel.js.map +1 -0
  37. package/dist/hooks/useDataChannelMessages.d.ts +17 -0
  38. package/dist/hooks/useDataChannelMessages.d.ts.map +1 -0
  39. package/dist/hooks/useDataChannelMessages.js +49 -0
  40. package/dist/hooks/useDataChannelMessages.js.map +1 -0
  41. package/dist/hooks/useMediaStream.d.ts +13 -0
  42. package/dist/hooks/useMediaStream.d.ts.map +1 -0
  43. package/dist/hooks/useMediaStream.js +81 -0
  44. package/dist/hooks/useMediaStream.js.map +1 -0
  45. package/dist/hooks/useMeshRoom.d.ts +42 -0
  46. package/dist/hooks/useMeshRoom.d.ts.map +1 -0
  47. package/dist/hooks/useMeshRoom.js +387 -0
  48. package/dist/hooks/useMeshRoom.js.map +1 -0
  49. package/dist/hooks/useScreenShare.d.ts +8 -0
  50. package/dist/hooks/useScreenShare.d.ts.map +1 -0
  51. package/dist/hooks/useScreenShare.js +41 -0
  52. package/dist/hooks/useScreenShare.js.map +1 -0
  53. package/dist/hooks/useWebRTC.d.ts +21 -0
  54. package/dist/hooks/useWebRTC.d.ts.map +1 -0
  55. package/dist/hooks/useWebRTC.js +96 -0
  56. package/dist/hooks/useWebRTC.js.map +1 -0
  57. package/dist/index.d.ts +16 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +16 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/signaling/SignalingClient.d.ts +58 -0
  62. package/dist/signaling/SignalingClient.d.ts.map +1 -0
  63. package/dist/signaling/SignalingClient.js +147 -0
  64. package/dist/signaling/SignalingClient.js.map +1 -0
  65. package/package.json +1 -1
  66. package/src/components/VideoPlayer.tsx +21 -16
  67. package/src/hooks/useMeshRoom.ts +65 -6
@@ -0,0 +1,96 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { Peer } from "@conference-kit/core";
3
+ export function useWebRTC(options) {
4
+ const { side, stream, config, channelLabel, trickle = true, onSignal, enabled = true, } = options;
5
+ const [remoteStream, setRemoteStream] = useState(null);
6
+ const [connectionState, setConnectionState] = useState("new");
7
+ const [iceState, setIceState] = useState("new");
8
+ const [error, setError] = useState(null);
9
+ const [peerInstance, setPeerInstance] = useState(null);
10
+ const peerRef = useRef(null);
11
+ const creationFailed = useRef(false);
12
+ const onSignalRef = useRef();
13
+ useEffect(() => {
14
+ onSignalRef.current = onSignal;
15
+ }, [onSignal]);
16
+ const isClient = useMemo(() => typeof window !== "undefined", []);
17
+ useEffect(() => {
18
+ if (!isClient || !enabled) {
19
+ peerRef.current?.destroy();
20
+ peerRef.current = null;
21
+ setPeerInstance(null);
22
+ creationFailed.current = false;
23
+ return undefined;
24
+ }
25
+ if (peerRef.current || creationFailed.current) {
26
+ return () => undefined;
27
+ }
28
+ try {
29
+ const peer = new Peer({
30
+ side,
31
+ stream: stream ?? undefined,
32
+ config,
33
+ channelLabel,
34
+ trickle,
35
+ });
36
+ peerRef.current = peer;
37
+ setPeerInstance(peer);
38
+ setError(null);
39
+ const handlers = {
40
+ signal: (data) => onSignalRef.current?.(data),
41
+ stream: (remote) => setRemoteStream(remote),
42
+ error: (err) => setError(err),
43
+ connectionStateChange: (state) => setConnectionState(state),
44
+ iceStateChange: (state) => setIceState(state),
45
+ };
46
+ peer.on("signal", handlers.signal);
47
+ peer.on("stream", handlers.stream);
48
+ peer.on("error", handlers.error);
49
+ peer.on("connectionStateChange", handlers.connectionStateChange);
50
+ peer.on("iceStateChange", handlers.iceStateChange);
51
+ return () => {
52
+ peer.off("signal", handlers.signal);
53
+ peer.off("stream", handlers.stream);
54
+ peer.off("error", handlers.error);
55
+ peer.off("connectionStateChange", handlers.connectionStateChange);
56
+ peer.off("iceStateChange", handlers.iceStateChange);
57
+ peer.destroy();
58
+ peerRef.current = null;
59
+ setPeerInstance(null);
60
+ creationFailed.current = false;
61
+ };
62
+ }
63
+ catch (err) {
64
+ creationFailed.current = true;
65
+ setError(err);
66
+ return () => undefined;
67
+ }
68
+ }, [channelLabel, config, enabled, isClient, side, stream, trickle]);
69
+ const signal = useCallback(async (data) => {
70
+ if (!peerRef.current)
71
+ return;
72
+ await peerRef.current.signal(data);
73
+ }, []);
74
+ const sendData = useCallback((payload) => {
75
+ if (!peerRef.current)
76
+ throw new Error("Peer not ready");
77
+ peerRef.current.send(payload);
78
+ }, []);
79
+ const destroy = useCallback(() => {
80
+ peerRef.current?.destroy();
81
+ peerRef.current = null;
82
+ creationFailed.current = false;
83
+ setPeerInstance(null);
84
+ }, []);
85
+ return {
86
+ peer: peerInstance,
87
+ remoteStream,
88
+ connectionState,
89
+ iceState,
90
+ error,
91
+ signal,
92
+ sendData,
93
+ destroy,
94
+ };
95
+ }
96
+ //# sourceMappingURL=useWebRTC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useWebRTC.js","sourceRoot":"","sources":["../../src/hooks/useWebRTC.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ export * from "./hooks/useMediaStream";
2
+ export * from "./hooks/useWebRTC";
3
+ export * from "./hooks/useDataChannel";
4
+ export * from "./hooks/useDataChannelMessages";
5
+ export * from "./hooks/useCall";
6
+ export * from "./hooks/useCallState";
7
+ export * from "./hooks/useMeshRoom";
8
+ export * from "./hooks/useScreenShare";
9
+ export * from "./context/WebRTCProvider";
10
+ export * from "./components/VideoPlayer";
11
+ export * from "./components/AudioPlayer";
12
+ export * from "./components/StatusBadge";
13
+ export * from "./components/ErrorBanner";
14
+ export * from "./signaling/SignalingClient";
15
+ export * from "./config/features";
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,wBAAwB,CAAC;AACvC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ export * from "./hooks/useMediaStream";
2
+ export * from "./hooks/useWebRTC";
3
+ export * from "./hooks/useDataChannel";
4
+ export * from "./hooks/useDataChannelMessages";
5
+ export * from "./hooks/useCall";
6
+ export * from "./hooks/useCallState";
7
+ export * from "./hooks/useMeshRoom";
8
+ export * from "./hooks/useScreenShare";
9
+ export * from "./context/WebRTCProvider";
10
+ export * from "./components/VideoPlayer";
11
+ export * from "./components/AudioPlayer";
12
+ export * from "./components/StatusBadge";
13
+ export * from "./components/ErrorBanner";
14
+ export * from "./signaling/SignalingClient";
15
+ export * from "./config/features";
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,wBAAwB,CAAC;AACvC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,mBAAmB,CAAC"}
@@ -0,0 +1,58 @@
1
+ export type EventMap = {
2
+ open: void;
3
+ close: CloseEvent | undefined;
4
+ error: Error;
5
+ signal: {
6
+ from: string;
7
+ data: unknown;
8
+ };
9
+ broadcast: {
10
+ from: string;
11
+ room?: string | null;
12
+ data: unknown;
13
+ };
14
+ control: {
15
+ action: string;
16
+ from: string;
17
+ room?: string | null;
18
+ data?: unknown;
19
+ };
20
+ presence: {
21
+ room?: string | null;
22
+ peerId: string;
23
+ peers: string[];
24
+ action: "join" | "leave";
25
+ };
26
+ };
27
+ type EventKey = keyof EventMap;
28
+ type Handler<K extends EventKey> = (payload: EventMap[K]) => void;
29
+ export type SignalingClientOptions = {
30
+ url: string;
31
+ peerId: string;
32
+ room?: string | null;
33
+ isHost?: boolean;
34
+ enableWaitingRoom?: boolean;
35
+ autoReconnect?: boolean;
36
+ reconnectDelayMs?: number;
37
+ };
38
+ export declare class SignalingClient {
39
+ private ws;
40
+ private options;
41
+ private emitter;
42
+ private reconnectTimeout;
43
+ private shouldReconnect;
44
+ private pendingQueue;
45
+ constructor(options: SignalingClientOptions);
46
+ connect(): void;
47
+ close(): void;
48
+ sendSignal(to: string, data: unknown): void;
49
+ broadcast(data: unknown): void;
50
+ sendControl(action: string, data?: unknown): void;
51
+ on<K extends EventKey>(event: K, handler: Handler<K>): void;
52
+ off<K extends EventKey>(event: K, handler: Handler<K>): void;
53
+ private enqueue;
54
+ private flushQueue;
55
+ private scheduleReconnect;
56
+ }
57
+ export {};
58
+ //# sourceMappingURL=SignalingClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SignalingClient.d.ts","sourceRoot":"","sources":["../../src/signaling/SignalingClient.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,IAAI,CAAC;IACX,KAAK,EAAE,UAAU,GAAG,SAAS,CAAC;IAC9B,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC;IACxC,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC;IACjE,OAAO,EAAE;QACP,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,CAAC;IACF,QAAQ,EAAE;QACR,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;KAC1B,CAAC;CACH,CAAC;AAEF,KAAK,QAAQ,GAAG,MAAM,QAAQ,CAAC;AAE/B,KAAK,OAAO,CAAC,CAAC,SAAS,QAAQ,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;AAwBlE,MAAM,MAAM,sBAAsB,GAAG;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAyBF,qBAAa,eAAe;IAC1B,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,gBAAgB,CAA8C;IACtE,OAAO,CAAC,eAAe,CAAU;IACjC,OAAO,CAAC,YAAY,CAAyB;gBAEjC,OAAO,EAAE,sBAAsB;IAK3C,OAAO;IAiEP,KAAK;IAOL,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO;IAIpC,SAAS,CAAC,IAAI,EAAE,OAAO;IAIvB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO;IAI1C,EAAE,CAAC,CAAC,SAAS,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IAIpD,GAAG,CAAC,CAAC,SAAS,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IAIrD,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,iBAAiB;CAM1B"}
@@ -0,0 +1,147 @@
1
+ function createEmitter() {
2
+ const listeners = new Map();
3
+ return {
4
+ on(event, handler) {
5
+ const set = listeners.get(event) ?? new Set();
6
+ set.add(handler);
7
+ listeners.set(event, set);
8
+ },
9
+ off(event, handler) {
10
+ const set = listeners.get(event);
11
+ if (!set)
12
+ return;
13
+ set.delete(handler);
14
+ if (set.size === 0)
15
+ listeners.delete(event);
16
+ },
17
+ emit(event, payload) {
18
+ const set = listeners.get(event);
19
+ if (!set)
20
+ return;
21
+ for (const handler of Array.from(set))
22
+ handler(payload);
23
+ },
24
+ };
25
+ }
26
+ export class SignalingClient {
27
+ constructor(options) {
28
+ this.ws = null;
29
+ this.emitter = createEmitter();
30
+ this.reconnectTimeout = null;
31
+ this.pendingQueue = [];
32
+ this.options = options;
33
+ this.shouldReconnect = options.autoReconnect ?? true;
34
+ }
35
+ connect() {
36
+ if (typeof window === "undefined")
37
+ return;
38
+ if (this.ws &&
39
+ (this.ws.readyState === WebSocket.OPEN ||
40
+ this.ws.readyState === WebSocket.CONNECTING)) {
41
+ return;
42
+ }
43
+ const { url, peerId, room, isHost, enableWaitingRoom } = this.options;
44
+ const params = new URLSearchParams({ peerId });
45
+ if (room)
46
+ params.set("room", room);
47
+ if (isHost)
48
+ params.set("host", "1");
49
+ if (enableWaitingRoom)
50
+ params.set("waitingRoom", "1");
51
+ const wsUrl = `${url}?${params.toString()}`;
52
+ this.ws = new WebSocket(wsUrl);
53
+ this.ws.addEventListener("open", () => {
54
+ this.emitter.emit("open", undefined);
55
+ this.flushQueue();
56
+ });
57
+ this.ws.addEventListener("message", (event) => {
58
+ const payload = typeof event.data === "string" ? event.data : "";
59
+ try {
60
+ const parsed = JSON.parse(payload);
61
+ if (parsed.type === "signal") {
62
+ this.emitter.emit("signal", { from: parsed.from, data: parsed.data });
63
+ }
64
+ else if (parsed.type === "broadcast") {
65
+ this.emitter.emit("broadcast", {
66
+ from: parsed.from,
67
+ room: parsed.room,
68
+ data: parsed.data,
69
+ });
70
+ }
71
+ else if (parsed.type === "presence") {
72
+ this.emitter.emit("presence", {
73
+ room: parsed.room,
74
+ peerId: parsed.peerId,
75
+ peers: parsed.peers,
76
+ action: parsed.action,
77
+ });
78
+ }
79
+ else if (parsed.type === "control") {
80
+ this.emitter.emit("control", {
81
+ from: parsed.from,
82
+ room: parsed.room,
83
+ action: parsed.action,
84
+ data: parsed.data,
85
+ });
86
+ }
87
+ }
88
+ catch (error) {
89
+ this.emitter.emit("error", error);
90
+ }
91
+ });
92
+ this.ws.addEventListener("close", (event) => {
93
+ this.emitter.emit("close", event);
94
+ this.scheduleReconnect();
95
+ });
96
+ this.ws.addEventListener("error", () => {
97
+ this.emitter.emit("error", new Error("WebSocket error"));
98
+ });
99
+ }
100
+ close() {
101
+ this.shouldReconnect = false;
102
+ if (this.reconnectTimeout)
103
+ clearTimeout(this.reconnectTimeout);
104
+ this.ws?.close();
105
+ this.ws = null;
106
+ }
107
+ sendSignal(to, data) {
108
+ this.enqueue({ type: "signal", to, data });
109
+ }
110
+ broadcast(data) {
111
+ this.enqueue({ type: "broadcast", data });
112
+ }
113
+ sendControl(action, data) {
114
+ this.enqueue({ type: "control", action, data });
115
+ }
116
+ on(event, handler) {
117
+ this.emitter.on(event, handler);
118
+ }
119
+ off(event, handler) {
120
+ this.emitter.off(event, handler);
121
+ }
122
+ enqueue(message) {
123
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
124
+ this.ws.send(JSON.stringify(message));
125
+ return;
126
+ }
127
+ this.pendingQueue.push(message);
128
+ }
129
+ flushQueue() {
130
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
131
+ return;
132
+ while (this.pendingQueue.length) {
133
+ const msg = this.pendingQueue.shift();
134
+ if (msg)
135
+ this.ws.send(JSON.stringify(msg));
136
+ }
137
+ }
138
+ scheduleReconnect() {
139
+ if (!this.shouldReconnect)
140
+ return;
141
+ const delay = this.options.reconnectDelayMs ?? 1000;
142
+ if (this.reconnectTimeout)
143
+ clearTimeout(this.reconnectTimeout);
144
+ this.reconnectTimeout = setTimeout(() => this.connect(), delay);
145
+ }
146
+ }
147
+ //# sourceMappingURL=SignalingClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SignalingClient.js","sourceRoot":"","sources":["../../src/signaling/SignalingClient.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conference-kit/react",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -1,10 +1,10 @@
1
- import { useEffect, useRef } from "react";
1
+ import { useEffect, useRef, memo } from "react";
2
2
 
3
3
  export type VideoPlayerProps = React.VideoHTMLAttributes<HTMLVideoElement> & {
4
4
  stream?: MediaStream | null;
5
5
  };
6
6
 
7
- export function VideoPlayer({
7
+ export const VideoPlayer = memo(function VideoPlayer({
8
8
  stream,
9
9
  autoPlay = true,
10
10
  playsInline = true,
@@ -12,32 +12,37 @@ export function VideoPlayer({
12
12
  ...props
13
13
  }: VideoPlayerProps) {
14
14
  const ref = useRef<HTMLVideoElement | null>(null);
15
- const prevStream = useRef<MediaStream | null>(null);
15
+ const attachedStream = useRef<MediaStream | null>(null);
16
16
 
17
17
  useEffect(() => {
18
18
  const el = ref.current;
19
19
  if (!el) return;
20
20
 
21
- // Avoid resetting srcObject if the exact same MediaStream instance is already attached.
22
- if (stream && prevStream.current !== stream) {
23
- el.srcObject = stream;
24
- prevStream.current = stream;
25
- }
21
+ // Only update srcObject when stream actually changes (by reference).
22
+ // Avoid clearing on unmount to prevent flicker during parent re-renders.
23
+ if (stream !== attachedStream.current) {
24
+ el.srcObject = stream ?? null;
25
+ attachedStream.current = stream ?? null;
26
26
 
27
- // Kick playback on mobile where autoplay can be flaky even when muted.
28
- if (stream) {
29
- el.play().catch(() => {
30
- /* ignore autoplay rejection; user gesture will resume */
31
- });
27
+ // Kick playback on mobile where autoplay can be flaky even when muted.
28
+ if (stream) {
29
+ el.play().catch(() => {
30
+ /* ignore autoplay rejection; user gesture will resume */
31
+ });
32
+ }
32
33
  }
34
+ }, [stream]);
33
35
 
36
+ // Only clear srcObject when component is truly unmounting, not on re-renders.
37
+ useEffect(() => {
38
+ const el = ref.current;
34
39
  return () => {
35
40
  if (el) {
36
41
  el.srcObject = null;
37
- prevStream.current = null;
42
+ attachedStream.current = null;
38
43
  }
39
44
  };
40
- }, [stream]);
45
+ }, []);
41
46
 
42
47
  return (
43
48
  <video
@@ -48,4 +53,4 @@ export function VideoPlayer({
48
53
  {...props}
49
54
  />
50
55
  );
51
- }
56
+ });
@@ -117,16 +117,26 @@ export function useMeshRoom(options: UseMeshRoomOptions) {
117
117
  },
118
118
  ];
119
119
  }
120
+ // Check if anything actually changed to avoid unnecessary re-renders
121
+ const changed = Object.keys(patch).some(
122
+ (key) =>
123
+ patch[key as keyof MeshParticipant] !==
124
+ existing[key as keyof MeshParticipant]
125
+ );
126
+ if (!changed) return prev;
120
127
  return prev.map((p) => (p.id === id ? { ...p, ...patch } : p));
121
128
  });
122
129
  },
123
130
  []
124
131
  );
125
132
 
133
+ const inWaitingRoomRef = useRef(inWaitingRoom);
134
+ inWaitingRoomRef.current = inWaitingRoom;
135
+
126
136
  const ensurePeer = useCallback(
127
137
  (id: string, side?: PeerSide) => {
128
138
  if (id === peerId) return null;
129
- if (features.enableWaitingRoom && inWaitingRoom) return null;
139
+ if (features.enableWaitingRoom && inWaitingRoomRef.current) return null;
130
140
  const existing = peers.current.get(id);
131
141
  if (existing) return existing;
132
142
  // enableDataChannel is supported in the runtime PeerConfig but may lag in published typings; cast to satisfy TS.
@@ -169,6 +179,8 @@ export function useMeshRoom(options: UseMeshRoomOptions) {
169
179
  },
170
180
  [
171
181
  destroyPeer,
182
+ features.enableDataChannel,
183
+ features.enableWaitingRoom,
172
184
  localStream,
173
185
  peerId,
174
186
  rtcConfig,
@@ -209,10 +221,22 @@ export function useMeshRoom(options: UseMeshRoomOptions) {
209
221
  action: "join" | "leave";
210
222
  }) => {
211
223
  const ids = payload.peers;
212
- setRoster(ids);
224
+ // Only update roster if changed
225
+ setRoster((prev) => {
226
+ if (
227
+ prev.length === ids.length &&
228
+ prev.every((id, i) => id === ids[i])
229
+ ) {
230
+ return prev;
231
+ }
232
+ return ids;
233
+ });
213
234
  ids.filter((id) => id !== peerId).forEach((id) => ensurePeer(id));
214
235
  // Remove peers no longer present
215
- setParticipants((prev) => prev.filter((p) => ids.includes(p.id)));
236
+ setParticipants((prev) => {
237
+ const filtered = prev.filter((p) => ids.includes(p.id));
238
+ return filtered.length === prev.length ? prev : filtered;
239
+ });
216
240
  Array.from(peers.current.keys()).forEach((id) => {
217
241
  if (!ids.includes(id)) destroyPeer(id);
218
242
  });
@@ -225,7 +249,15 @@ export function useMeshRoom(options: UseMeshRoomOptions) {
225
249
  const onControl = ({ action, data }: { action: string; data?: any }) => {
226
250
  if (action === "waiting-list") {
227
251
  const waiting = (data?.waiting as string[]) ?? [];
228
- setWaitingList(waiting);
252
+ setWaitingList((prev) => {
253
+ if (
254
+ prev.length === waiting.length &&
255
+ prev.every((id, i) => id === waiting[i])
256
+ ) {
257
+ return prev;
258
+ }
259
+ return waiting;
260
+ });
229
261
  return;
230
262
  }
231
263
  if (action === "waiting") {
@@ -245,6 +277,7 @@ export function useMeshRoom(options: UseMeshRoomOptions) {
245
277
  const peer = (data?.peerId as string) ?? null;
246
278
  if (!peer) return;
247
279
  setRaisedHands((prev) => {
280
+ if (prev.has(peer)) return prev;
248
281
  const next = new Set(prev);
249
282
  next.add(peer);
250
283
  return next;
@@ -255,6 +288,7 @@ export function useMeshRoom(options: UseMeshRoomOptions) {
255
288
  const peer = (data?.peerId as string) ?? null;
256
289
  if (!peer) return;
257
290
  setRaisedHands((prev) => {
291
+ if (!prev.has(peer)) return prev;
258
292
  const next = new Set(prev);
259
293
  next.delete(peer);
260
294
  return next;
@@ -335,9 +369,22 @@ export function useMeshRoom(options: UseMeshRoomOptions) {
335
369
  >(new Map());
336
370
  const activeCandidate = useRef<string | null>(null);
337
371
  const activeSince = useRef<number>(0);
372
+ const silenceSince = useRef<number>(0);
338
373
 
339
374
  useEffect(() => {
340
- if (!features.enableActiveSpeaker) return;
375
+ if (!features.enableActiveSpeaker || signalingStatus !== "open") {
376
+ setActiveSpeakerId(null);
377
+ analyzers.current.forEach((entry) => {
378
+ entry.source.disconnect();
379
+ entry.analyser.disconnect();
380
+ entry.ctx.close();
381
+ });
382
+ analyzers.current.clear();
383
+ activeCandidate.current = null;
384
+ activeSince.current = 0;
385
+ silenceSince.current = 0;
386
+ return;
387
+ }
341
388
 
342
389
  const ensureAnalyzer = (id: string, stream: MediaStream) => {
343
390
  if (analyzers.current.has(id)) return;
@@ -370,6 +417,7 @@ export function useMeshRoom(options: UseMeshRoomOptions) {
370
417
 
371
418
  const minHoldMs = 700;
372
419
  const minLevel = 18;
420
+ const silenceHoldMs = 1200;
373
421
  const interval = window.setInterval(() => {
374
422
  let loudestId: string | null = null;
375
423
  let loudest = 0;
@@ -395,6 +443,16 @@ export function useMeshRoom(options: UseMeshRoomOptions) {
395
443
  prev === activeCandidate.current ? prev : activeCandidate.current
396
444
  );
397
445
  }
446
+
447
+ if (!loudestId) {
448
+ if (!silenceSince.current) silenceSince.current = now;
449
+ const silentLongEnough = now - silenceSince.current >= silenceHoldMs;
450
+ if (silentLongEnough) {
451
+ setActiveSpeakerId((prev) => (prev === null ? prev : null));
452
+ }
453
+ } else {
454
+ silenceSince.current = 0;
455
+ }
398
456
  }, 400);
399
457
 
400
458
  return () => {
@@ -407,8 +465,9 @@ export function useMeshRoom(options: UseMeshRoomOptions) {
407
465
  analyzers.current.clear();
408
466
  activeCandidate.current = null;
409
467
  activeSince.current = 0;
468
+ silenceSince.current = 0;
410
469
  };
411
- }, [features.enableActiveSpeaker, participants]);
470
+ }, [features.enableActiveSpeaker, participants, signalingStatus]);
412
471
 
413
472
  return {
414
473
  localStream,