kodenique-game-sdk 1.0.0

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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/dist/GameContext.d.ts +4 -0
  4. package/dist/GameContext.js +327 -0
  5. package/dist/GameDebug.d.ts +5 -0
  6. package/dist/GameDebug.js +172 -0
  7. package/dist/GamePlayer.d.ts +26 -0
  8. package/dist/GamePlayer.js +54 -0
  9. package/dist/SimplePlayer.d.ts +11 -0
  10. package/dist/SimplePlayer.js +41 -0
  11. package/dist/components/GamePlayerOverlays.d.ts +29 -0
  12. package/dist/components/GamePlayerOverlays.js +467 -0
  13. package/dist/components/GamePlayerVideo.d.ts +7 -0
  14. package/dist/components/GamePlayerVideo.js +86 -0
  15. package/dist/components/index.d.ts +2 -0
  16. package/dist/components/index.js +2 -0
  17. package/dist/config.d.ts +50 -0
  18. package/dist/config.js +46 -0
  19. package/dist/contexts/GameStreamContext.d.ts +24 -0
  20. package/dist/contexts/GameStreamContext.js +170 -0
  21. package/dist/examples/GameStreamExample.d.ts +26 -0
  22. package/dist/examples/GameStreamExample.js +92 -0
  23. package/dist/examples/SimpleAutoSubscribe.d.ts +9 -0
  24. package/dist/examples/SimpleAutoSubscribe.js +29 -0
  25. package/dist/hooks/index.d.ts +2 -0
  26. package/dist/hooks/index.js +1 -0
  27. package/dist/hooks/useGameStream.d.ts +29 -0
  28. package/dist/hooks/useGameStream.js +78 -0
  29. package/dist/hooks/useWebRTC.d.ts +21 -0
  30. package/dist/hooks/useWebRTC.js +555 -0
  31. package/dist/index.d.ts +13 -0
  32. package/dist/index.js +12 -0
  33. package/dist/lib/pusher.d.ts +50 -0
  34. package/dist/lib/pusher.js +137 -0
  35. package/dist/types.d.ts +87 -0
  36. package/dist/types.js +1 -0
  37. package/dist/useGames.d.ts +2 -0
  38. package/dist/useGames.js +73 -0
  39. package/package.json +66 -0
@@ -0,0 +1,555 @@
1
+ import { useRef, useEffect, useCallback, useState } from "react";
2
+ const MAX_RECONNECT_ATTEMPTS = 5;
3
+ const MIN_CONNECTION_INTERVAL = 200;
4
+ const CONNECTION_TIMEOUT = 10000;
5
+ const FETCH_TIMEOUT = 10000;
6
+ export function useWebRTC(options) {
7
+ const { streamUrl, serverUrl, streamName, onLoad, onError, onConnectionStateChange, } = options;
8
+ const videoRef = useRef(null);
9
+ const pcRef = useRef(null);
10
+ const statsIntervalRef = useRef(null);
11
+ const reconnectTimeoutRef = useRef(null);
12
+ const lastBytesRef = useRef(0);
13
+ const lastTimestampRef = useRef(Date.now());
14
+ const isConnectingRef = useRef(false);
15
+ const isConnectedRef = useRef(false);
16
+ const reconnectAttemptsRef = useRef(0);
17
+ const lastConnectionAttemptRef = useRef(0);
18
+ const prevStreamUrlRef = useRef();
19
+ const [isLoading, setIsLoading] = useState(true);
20
+ const [hasError, setHasError] = useState(false);
21
+ const [errorMessage, setErrorMessage] = useState("");
22
+ const [networkQuality, setNetworkQuality] = useState("good");
23
+ const [latency, setLatency] = useState(0);
24
+ const [bitrate, setBitrate] = useState(0);
25
+ const startStatsMonitor = useCallback(() => {
26
+ if (statsIntervalRef.current)
27
+ clearInterval(statsIntervalRef.current);
28
+ statsIntervalRef.current = setInterval(async () => {
29
+ if (!pcRef.current || pcRef.current.connectionState !== "connected")
30
+ return;
31
+ try {
32
+ const stats = await pcRef.current.getStats();
33
+ let totalBytesReceived = 0;
34
+ let jitterBufferDelay = 0;
35
+ let jitterBufferEmittedCount = 0;
36
+ let packetsLost = 0;
37
+ let packetsReceived = 0;
38
+ stats.forEach((report) => {
39
+ if (report.type === "inbound-rtp" && report.kind === "video") {
40
+ totalBytesReceived += report.bytesReceived || 0;
41
+ packetsLost += report.packetsLost || 0;
42
+ packetsReceived += report.packetsReceived || 0;
43
+ if (report.jitterBufferDelay && report.jitterBufferEmittedCount) {
44
+ jitterBufferDelay = report.jitterBufferDelay;
45
+ jitterBufferEmittedCount = report.jitterBufferEmittedCount;
46
+ }
47
+ }
48
+ });
49
+ // Calculate latency
50
+ if (jitterBufferEmittedCount > 0) {
51
+ const calculatedLatency = Math.round((jitterBufferDelay / jitterBufferEmittedCount) * 1000);
52
+ setLatency(calculatedLatency);
53
+ }
54
+ // Calculate bitrate
55
+ const now = Date.now();
56
+ const timeDiff = (now - lastTimestampRef.current) / 1000;
57
+ if (timeDiff > 0 && lastBytesRef.current > 0) {
58
+ const calculatedBitrate = Math.round(((totalBytesReceived - lastBytesRef.current) * 8) / timeDiff / 1000);
59
+ setBitrate(calculatedBitrate);
60
+ // Determine network quality based on bitrate
61
+ if (calculatedBitrate > 1500) {
62
+ setNetworkQuality("good");
63
+ }
64
+ else if (calculatedBitrate > 800) {
65
+ setNetworkQuality("fair");
66
+ }
67
+ else if (calculatedBitrate > 300) {
68
+ setNetworkQuality("poor");
69
+ }
70
+ else {
71
+ setNetworkQuality("bad");
72
+ }
73
+ }
74
+ lastBytesRef.current = totalBytesReceived;
75
+ lastTimestampRef.current = now;
76
+ // Check packet loss
77
+ if (packetsReceived > 0) {
78
+ const lossRate = packetsLost / (packetsLost + packetsReceived);
79
+ if (lossRate > 0.05) {
80
+ setNetworkQuality("poor");
81
+ }
82
+ }
83
+ }
84
+ catch (error) {
85
+ console.error("[useWebRTC] Stats error:", error);
86
+ }
87
+ }, 1000);
88
+ }, []);
89
+ const scheduleReconnect = useCallback((delay = 3000) => {
90
+ if (reconnectTimeoutRef.current) {
91
+ clearTimeout(reconnectTimeoutRef.current);
92
+ reconnectTimeoutRef.current = null;
93
+ }
94
+ if (reconnectAttemptsRef.current >= MAX_RECONNECT_ATTEMPTS) {
95
+ console.error("[useWebRTC] Max reconnection attempts reached");
96
+ setErrorMessage("Unable to connect - please refresh the page");
97
+ return;
98
+ }
99
+ reconnectAttemptsRef.current++;
100
+ console.log(`[useWebRTC] Scheduling reconnect attempt ${reconnectAttemptsRef.current}/${MAX_RECONNECT_ATTEMPTS} in ${delay}ms`);
101
+ reconnectTimeoutRef.current = setTimeout(() => {
102
+ console.log("[useWebRTC] Auto-reconnecting...");
103
+ if (videoRef.current) {
104
+ connectWebRTC();
105
+ }
106
+ }, delay);
107
+ }, []);
108
+ const buildWhepUrl = useCallback((streamUrl, serverUrl, streamName) => {
109
+ if (streamUrl) {
110
+ console.log("[useWebRTC] Original stream URL:", streamUrl);
111
+ const urlMatch = streamUrl.match(/stream=([^&]+)/);
112
+ const streamId = urlMatch ? urlMatch[1] : null;
113
+ console.log("[useWebRTC] Extracted stream ID:", streamId);
114
+ if (streamId) {
115
+ const serverMatch = streamUrl.match(/\/\/([^:\/]+)/);
116
+ const server = serverMatch ? serverMatch[1] : "stg-srs.wspo.club";
117
+ console.log("[useWebRTC] Server:", server);
118
+ const whepUrl = `http://${server}:1985/rtc/v1/whep/?app=live&stream=${streamId}`;
119
+ console.log("[useWebRTC] Rebuilt clean URL:", whepUrl);
120
+ return whepUrl;
121
+ }
122
+ else {
123
+ let fixedUrl = streamUrl;
124
+ if (fixedUrl.includes("https://")) {
125
+ fixedUrl = fixedUrl.replace("https://", "http://");
126
+ }
127
+ if (fixedUrl.includes(":1985/v1/whep/")) {
128
+ fixedUrl = fixedUrl.replace(":1985/v1/whep/", ":1985/rtc/v1/whep/");
129
+ }
130
+ if (fixedUrl.includes("&access_key=")) {
131
+ fixedUrl = fixedUrl.split("&access_key=")[0];
132
+ }
133
+ console.log("[useWebRTC] Fallback fixed URL:", fixedUrl);
134
+ return fixedUrl;
135
+ }
136
+ }
137
+ else if (serverUrl && streamName) {
138
+ const whepUrl = `http://${serverUrl}:1985/rtc/v1/whep/?app=live&stream=${streamName}`;
139
+ console.log("[useWebRTC] Constructed stream URL:", whepUrl);
140
+ return whepUrl;
141
+ }
142
+ else {
143
+ throw new Error("No stream URL or server/stream configuration provided");
144
+ }
145
+ }, []);
146
+ const setupVideoPlayback = useCallback((video, stream) => {
147
+ console.log("[useWebRTC] Setting srcObject on video element");
148
+ video.srcObject = stream;
149
+ video.muted = true;
150
+ video.volume = 0;
151
+ video.defaultMuted = true;
152
+ video.setAttribute("autoplay", "true");
153
+ video.setAttribute("playsinline", "true");
154
+ video.setAttribute("muted", "true");
155
+ console.log("[useWebRTC] Video element state:", {
156
+ readyState: video.readyState,
157
+ networkState: video.networkState,
158
+ paused: video.paused,
159
+ muted: video.muted,
160
+ autoplay: video.autoplay,
161
+ srcObject: !!video.srcObject,
162
+ });
163
+ isConnectedRef.current = true;
164
+ isConnectingRef.current = false;
165
+ reconnectAttemptsRef.current = 0;
166
+ const handlePlaying = () => {
167
+ console.log("[useWebRTC] Video playing event fired!");
168
+ setIsLoading(false);
169
+ setHasError(false);
170
+ onLoad === null || onLoad === void 0 ? void 0 : onLoad();
171
+ };
172
+ video.addEventListener("playing", handlePlaying, { once: true });
173
+ const attemptPlay = (attempt = 1, maxAttempts = 30) => {
174
+ const state = {
175
+ readyState: video.readyState,
176
+ paused: video.paused,
177
+ networkState: video.networkState,
178
+ currentTime: video.currentTime,
179
+ };
180
+ console.log(`[useWebRTC] Play attempt ${attempt}/${maxAttempts}`, state);
181
+ if (video.readyState > 0 || attempt > 5) {
182
+ const playPromise = video.play();
183
+ if (playPromise !== undefined) {
184
+ playPromise
185
+ .then(() => {
186
+ console.log(`[useWebRTC] Play promise resolved on attempt ${attempt}`);
187
+ })
188
+ .catch((err) => {
189
+ console.log(`[useWebRTC] Play promise rejected on attempt ${attempt}:`, err.message);
190
+ });
191
+ }
192
+ }
193
+ else {
194
+ console.log(`[useWebRTC] Waiting for data... readyState is still 0`);
195
+ }
196
+ if (attempt < maxAttempts && video.paused) {
197
+ setTimeout(() => {
198
+ if (video.paused) {
199
+ attemptPlay(attempt + 1, maxAttempts);
200
+ }
201
+ }, 200);
202
+ }
203
+ else if (attempt >= maxAttempts && video.paused) {
204
+ console.log("[useWebRTC] All play attempts exhausted - user may need to click");
205
+ }
206
+ };
207
+ setTimeout(() => attemptPlay(), 100);
208
+ }, [onLoad]);
209
+ const connectWebRTC = useCallback(async () => {
210
+ console.log("[useWebRTC] Starting connectWebRTC...");
211
+ if (isConnectingRef.current) {
212
+ console.log("[useWebRTC] Already connecting, skipping duplicate attempt");
213
+ return;
214
+ }
215
+ const now = Date.now();
216
+ if (now - lastConnectionAttemptRef.current < MIN_CONNECTION_INTERVAL) {
217
+ console.log("[useWebRTC] Too soon since last connection attempt, skipping");
218
+ return;
219
+ }
220
+ try {
221
+ if (pcRef.current &&
222
+ (pcRef.current.connectionState === "connected" ||
223
+ pcRef.current.connectionState === "connecting")) {
224
+ console.log("[useWebRTC] Already connected/connecting, skipping reconnection");
225
+ return;
226
+ }
227
+ isConnectingRef.current = true;
228
+ lastConnectionAttemptRef.current = now;
229
+ if (pcRef.current) {
230
+ console.log("[useWebRTC] Cleaning up existing connection");
231
+ try {
232
+ pcRef.current.close();
233
+ }
234
+ catch (e) {
235
+ console.log("[useWebRTC] Error closing connection:", e);
236
+ }
237
+ pcRef.current = null;
238
+ }
239
+ if (videoRef.current && videoRef.current.srcObject) {
240
+ console.log("[useWebRTC] Clearing existing video stream");
241
+ const stream = videoRef.current.srcObject;
242
+ stream.getTracks().forEach((track) => track.stop());
243
+ videoRef.current.srcObject = null;
244
+ }
245
+ setIsLoading(true);
246
+ setHasError(false);
247
+ setErrorMessage("");
248
+ const whepUrl = buildWhepUrl(streamUrl, serverUrl, streamName);
249
+ const pc = new RTCPeerConnection({
250
+ iceServers: [],
251
+ bundlePolicy: "max-bundle",
252
+ rtcpMuxPolicy: "require",
253
+ });
254
+ pc.addTransceiver("video", { direction: "recvonly" });
255
+ pc.addTransceiver("audio", { direction: "recvonly" });
256
+ let connectionEstablished = false;
257
+ let connectionTimeout = null;
258
+ let streamSet = false;
259
+ pc.ontrack = (event) => {
260
+ console.log("[useWebRTC] Got track:", event.track.kind, "ID:", event.track.id);
261
+ const video = videoRef.current;
262
+ if (video && event.streams[0] && !streamSet && !video.srcObject) {
263
+ streamSet = true;
264
+ setupVideoPlayback(video, event.streams[0]);
265
+ if (connectionTimeout) {
266
+ clearTimeout(connectionTimeout);
267
+ connectionTimeout = null;
268
+ }
269
+ }
270
+ else if (event.track.kind === "video" && video) {
271
+ console.log("[useWebRTC] Video track received, ensuring playback...");
272
+ video.play().catch(() => {
273
+ console.log("[useWebRTC] Autoplay prevented - user interaction required");
274
+ });
275
+ }
276
+ };
277
+ pc.oniceconnectionstatechange = () => {
278
+ var _a, _b;
279
+ const state = pc.iceConnectionState;
280
+ console.log("[useWebRTC] ICE connection state:", state);
281
+ if (state === "connected" || state === "completed") {
282
+ clearTimeout(connectionTimeout);
283
+ connectionEstablished = true;
284
+ reconnectAttemptsRef.current = 0;
285
+ if ((_a = videoRef.current) === null || _a === void 0 ? void 0 : _a.srcObject) {
286
+ console.log("[useWebRTC] ICE connected and video stream present - clearing loading");
287
+ setIsLoading(false);
288
+ setHasError(false);
289
+ }
290
+ if (!statsIntervalRef.current) {
291
+ startStatsMonitor();
292
+ }
293
+ console.log("[useWebRTC] ICE connected to stream");
294
+ }
295
+ else if (state === "failed") {
296
+ isConnectedRef.current = false;
297
+ isConnectingRef.current = false;
298
+ clearTimeout(connectionTimeout);
299
+ console.error("[useWebRTC] ICE connection failed");
300
+ if (reconnectAttemptsRef.current < MAX_RECONNECT_ATTEMPTS) {
301
+ scheduleReconnect(1000);
302
+ }
303
+ else {
304
+ setHasError(true);
305
+ setIsLoading(false);
306
+ setErrorMessage("Connection failed - click to retry");
307
+ }
308
+ }
309
+ else if (state === "disconnected") {
310
+ console.warn("[useWebRTC] ICE disconnected - but we have tracks, ignoring!");
311
+ if ((_b = videoRef.current) === null || _b === void 0 ? void 0 : _b.srcObject) {
312
+ console.log("[useWebRTC] Video stream exists, continuing playback");
313
+ setIsLoading(false);
314
+ setHasError(false);
315
+ if (videoRef.current.paused) {
316
+ videoRef.current.play().catch(() => { });
317
+ }
318
+ }
319
+ else if (connectionEstablished) {
320
+ setTimeout(() => {
321
+ var _a, _b;
322
+ if (((_a = pcRef.current) === null || _a === void 0 ? void 0 : _a.iceConnectionState) === "disconnected" &&
323
+ !((_b = videoRef.current) === null || _b === void 0 ? void 0 : _b.srcObject)) {
324
+ console.warn("[useWebRTC] Still disconnected and no video - reconnecting");
325
+ isConnectedRef.current = false;
326
+ setErrorMessage("Connection lost - reconnecting...");
327
+ scheduleReconnect(1000);
328
+ }
329
+ }, 3000);
330
+ }
331
+ }
332
+ };
333
+ pc.onconnectionstatechange = () => {
334
+ const state = pc.connectionState;
335
+ console.log("[useWebRTC] Connection state:", state);
336
+ onConnectionStateChange === null || onConnectionStateChange === void 0 ? void 0 : onConnectionStateChange(state);
337
+ if (state === "connected") {
338
+ setIsLoading(false);
339
+ setHasError(false);
340
+ }
341
+ };
342
+ const offer = await pc.createOffer();
343
+ await pc.setLocalDescription(offer);
344
+ console.log("[useWebRTC] Sending WHEP request to:", whepUrl);
345
+ connectionTimeout = setTimeout(() => {
346
+ var _a;
347
+ if (pcRef.current === pc && !((_a = videoRef.current) === null || _a === void 0 ? void 0 : _a.srcObject)) {
348
+ console.warn("[useWebRTC] No video after 10s - timing out");
349
+ pc.close();
350
+ pcRef.current = null;
351
+ isConnectingRef.current = false;
352
+ setHasError(true);
353
+ setIsLoading(false);
354
+ setErrorMessage("No stream - click to retry");
355
+ if (reconnectAttemptsRef.current < MAX_RECONNECT_ATTEMPTS) {
356
+ scheduleReconnect(1000);
357
+ }
358
+ }
359
+ }, CONNECTION_TIMEOUT);
360
+ const controller = new AbortController();
361
+ const fetchTimeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
362
+ try {
363
+ console.log("[useWebRTC] Sending POST request to WHEP endpoint...");
364
+ const response = await fetch(whepUrl, {
365
+ method: "POST",
366
+ headers: { "Content-Type": "application/sdp" },
367
+ body: offer.sdp,
368
+ signal: controller.signal,
369
+ });
370
+ clearTimeout(fetchTimeout);
371
+ console.log("[useWebRTC] WHEP Response status:", response.status);
372
+ if (!response.ok) {
373
+ let errorDetails = "";
374
+ try {
375
+ errorDetails = await response.text();
376
+ console.error("[useWebRTC] Server error response:", errorDetails);
377
+ }
378
+ catch (e) {
379
+ // Ignore if can't read response
380
+ }
381
+ if (response.status === 500 || response.status === 404) {
382
+ console.warn(`[useWebRTC] HTTP ${response.status} error - will retry automatically`);
383
+ throw new Error("RETRY_SILENT");
384
+ }
385
+ else {
386
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
387
+ }
388
+ }
389
+ const answerSdp = await response.text();
390
+ console.log("[useWebRTC] Got SDP answer, setting remote description");
391
+ await pc.setRemoteDescription({ type: "answer", sdp: answerSdp });
392
+ pcRef.current = pc;
393
+ console.log("[useWebRTC] WebRTC setup complete, waiting for ICE connection...");
394
+ clearTimeout(connectionTimeout);
395
+ }
396
+ catch (fetchError) {
397
+ clearTimeout(fetchTimeout);
398
+ clearTimeout(connectionTimeout);
399
+ if (fetchError.name === "AbortError") {
400
+ throw new Error("Connection timeout - server not responding");
401
+ }
402
+ throw fetchError;
403
+ }
404
+ }
405
+ catch (error) {
406
+ console.error("[useWebRTC] WebRTC connection error:", error);
407
+ isConnectingRef.current = false;
408
+ let shouldAutoReconnect = true;
409
+ let showError = false;
410
+ if (error instanceof Error) {
411
+ if (error.message.includes("RETRY_SILENT")) {
412
+ shouldAutoReconnect = true;
413
+ showError = false;
414
+ }
415
+ else {
416
+ shouldAutoReconnect =
417
+ reconnectAttemptsRef.current < MAX_RECONNECT_ATTEMPTS;
418
+ showError = reconnectAttemptsRef.current >= MAX_RECONNECT_ATTEMPTS - 1;
419
+ }
420
+ }
421
+ if (showError || !shouldAutoReconnect) {
422
+ setHasError(true);
423
+ setIsLoading(false);
424
+ setErrorMessage("Stream unavailable - click to retry");
425
+ onError === null || onError === void 0 ? void 0 : onError(error instanceof Error ? error : new Error("Connection failed"));
426
+ }
427
+ else {
428
+ setIsLoading(true);
429
+ setHasError(false);
430
+ }
431
+ if (shouldAutoReconnect) {
432
+ const delay = 500 + reconnectAttemptsRef.current * 500;
433
+ scheduleReconnect(Math.min(delay, 2500));
434
+ }
435
+ }
436
+ }, [
437
+ streamUrl,
438
+ serverUrl,
439
+ streamName,
440
+ buildWhepUrl,
441
+ setupVideoPlayback,
442
+ startStatsMonitor,
443
+ scheduleReconnect,
444
+ onConnectionStateChange,
445
+ onError,
446
+ ]);
447
+ const retry = useCallback(() => {
448
+ console.log("[useWebRTC] Manual retry triggered");
449
+ reconnectAttemptsRef.current = 0;
450
+ isConnectedRef.current = false;
451
+ isConnectingRef.current = false;
452
+ lastConnectionAttemptRef.current = 0;
453
+ setHasError(false);
454
+ setIsLoading(true);
455
+ connectWebRTC();
456
+ }, [connectWebRTC]);
457
+ const extractStreamKey = useCallback((url) => {
458
+ if (!url)
459
+ return null;
460
+ const match = url.match(/stream=([^&]+)/);
461
+ return match ? match[1] : url;
462
+ }, []);
463
+ useEffect(() => {
464
+ var _a;
465
+ console.log("[useWebRTC] useEffect triggered", {
466
+ streamUrl,
467
+ serverUrl,
468
+ streamName,
469
+ hasPeerConnection: !!pcRef.current,
470
+ peerConnectionState: (_a = pcRef.current) === null || _a === void 0 ? void 0 : _a.connectionState,
471
+ isConnecting: isConnectingRef.current,
472
+ });
473
+ if (!streamUrl && (!serverUrl || !streamName)) {
474
+ console.error("[useWebRTC] Missing stream configuration");
475
+ setHasError(true);
476
+ setErrorMessage("Missing stream configuration");
477
+ setIsLoading(false);
478
+ return;
479
+ }
480
+ const currentStreamKey = extractStreamKey(streamUrl) || `${serverUrl}:${streamName}`;
481
+ const prevStreamKey = extractStreamKey(prevStreamUrlRef.current);
482
+ console.log("[useWebRTC] Stream comparison", {
483
+ currentStreamKey,
484
+ prevStreamKey,
485
+ same: currentStreamKey === prevStreamKey,
486
+ });
487
+ if (currentStreamKey === prevStreamKey) {
488
+ if (pcRef.current &&
489
+ (pcRef.current.connectionState === "connected" ||
490
+ pcRef.current.connectionState === "connecting")) {
491
+ console.log("[useWebRTC] Same stream, already connected/connecting - ignoring re-render");
492
+ return;
493
+ }
494
+ }
495
+ prevStreamUrlRef.current = streamUrl || `${serverUrl}:${streamName}`;
496
+ if (prevStreamKey && currentStreamKey !== prevStreamKey && pcRef.current) {
497
+ console.log("[useWebRTC] Stream changed, disconnecting old connection");
498
+ pcRef.current.close();
499
+ pcRef.current = null;
500
+ isConnectedRef.current = false;
501
+ isConnectingRef.current = false;
502
+ }
503
+ if ((!pcRef.current || pcRef.current.connectionState === "closed") &&
504
+ !isConnectingRef.current) {
505
+ console.log("[useWebRTC] Initiating new connection...");
506
+ reconnectAttemptsRef.current = 0;
507
+ setTimeout(() => connectWebRTC(), 0);
508
+ }
509
+ else {
510
+ console.log("[useWebRTC] Skipping connection - already exists or connecting");
511
+ }
512
+ }, [streamUrl, serverUrl, streamName, extractStreamKey, connectWebRTC]);
513
+ useEffect(() => {
514
+ return () => {
515
+ console.log("[useWebRTC] Cleanup - unmounting");
516
+ isConnectedRef.current = false;
517
+ isConnectingRef.current = false;
518
+ prevStreamUrlRef.current = undefined;
519
+ if (statsIntervalRef.current) {
520
+ clearInterval(statsIntervalRef.current);
521
+ statsIntervalRef.current = null;
522
+ }
523
+ if (reconnectTimeoutRef.current) {
524
+ clearTimeout(reconnectTimeoutRef.current);
525
+ reconnectTimeoutRef.current = null;
526
+ }
527
+ if (videoRef.current && videoRef.current.srcObject) {
528
+ const stream = videoRef.current.srcObject;
529
+ stream.getTracks().forEach((track) => {
530
+ track.stop();
531
+ });
532
+ videoRef.current.srcObject = null;
533
+ }
534
+ if (pcRef.current) {
535
+ try {
536
+ pcRef.current.close();
537
+ }
538
+ catch (e) {
539
+ console.log("[useWebRTC] Error closing peer connection:", e);
540
+ }
541
+ pcRef.current = null;
542
+ }
543
+ };
544
+ }, []);
545
+ return {
546
+ videoRef,
547
+ isLoading,
548
+ hasError,
549
+ errorMessage,
550
+ networkQuality,
551
+ latency,
552
+ bitrate,
553
+ retry,
554
+ };
555
+ }
@@ -0,0 +1,13 @@
1
+ export { GameProvider } from "./GameContext";
2
+ export { useGames } from "./useGames";
3
+ export { GamePlayer } from "./GamePlayer";
4
+ export { SimplePlayer } from "./SimplePlayer";
5
+ export { API_CONFIG, getConfig, getWebRTCUrl, getWebSocketUrl } from "./config";
6
+ export { useGameStream } from "./hooks/useGameStream";
7
+ export { useWebRTC } from "./hooks/useWebRTC";
8
+ export { NetworkIndicator, ErrorOverlay, LoadingOverlay, ScoreOverlay, } from "./components/GamePlayerOverlays";
9
+ export { GamePlayerVideo } from "./components/GamePlayerVideo";
10
+ export type { Game, Team, Round, Creator, GamesApiResponse, GameProviderProps, GameContextType, UseGamesOptions, UseGamesReturn, } from "./types";
11
+ export type { GamePlayerProps } from "./GamePlayer";
12
+ export type { SimplePlayerProps } from "./SimplePlayer";
13
+ export type { UseWebRTCOptions, UseWebRTCReturn } from "./hooks/useWebRTC";
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ // Main exports
2
+ export { GameProvider } from "./GameContext";
3
+ export { useGames } from "./useGames";
4
+ export { GamePlayer } from "./GamePlayer";
5
+ export { SimplePlayer } from "./SimplePlayer";
6
+ export { API_CONFIG, getConfig, getWebRTCUrl, getWebSocketUrl } from "./config";
7
+ // Subscription system exports
8
+ export { useGameStream } from "./hooks/useGameStream";
9
+ // Component exports
10
+ export { useWebRTC } from "./hooks/useWebRTC";
11
+ export { NetworkIndicator, ErrorOverlay, LoadingOverlay, ScoreOverlay, } from "./components/GamePlayerOverlays";
12
+ export { GamePlayerVideo } from "./components/GamePlayerVideo";
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Pusher integration for real-time game updates
3
+ * Alternative to WebSocket for easier setup
4
+ *
5
+ * Note: Install pusher-js to use this: npm install pusher-js
6
+ */
7
+ export interface PusherConfig {
8
+ key: string;
9
+ cluster: string;
10
+ authEndpoint?: string;
11
+ }
12
+ export declare class GamePusherClient {
13
+ private config;
14
+ private pusher;
15
+ private channels;
16
+ constructor(config: PusherConfig);
17
+ /**
18
+ * Initialize Pusher connection
19
+ */
20
+ connect(): Promise<void>;
21
+ /**
22
+ * Subscribe to a game channel
23
+ */
24
+ subscribeToGame(gameId: string, callbacks: {
25
+ onGameUpdate?: (game: any) => void;
26
+ onScoreUpdate?: (scores: any) => void;
27
+ onStatusUpdate?: (status: string) => void;
28
+ onGameEnded?: () => void;
29
+ }): void;
30
+ /**
31
+ * Unsubscribe from a game channel
32
+ */
33
+ unsubscribeFromGame(gameId: string): void;
34
+ /**
35
+ * Disconnect from Pusher
36
+ */
37
+ disconnect(): void;
38
+ /**
39
+ * Check if connected
40
+ */
41
+ isConnected(): boolean;
42
+ }
43
+ /**
44
+ * Initialize the global Pusher client
45
+ */
46
+ export declare function initializePusher(config: PusherConfig): GamePusherClient;
47
+ /**
48
+ * Get the global Pusher client
49
+ */
50
+ export declare function getPusherClient(): GamePusherClient | null;