@dialtribe/react-sdk 0.1.0-alpha.10

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.
@@ -0,0 +1,3758 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var ReactPlayer = require('react-player');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var ReactPlayer__default = /*#__PURE__*/_interopDefault(ReactPlayer);
10
+
11
+ // src/context/DialTribeProvider.tsx
12
+ var DialTribeContext = react.createContext(null);
13
+ function DialTribeProvider({
14
+ sessionToken: initialToken,
15
+ onTokenRefresh,
16
+ onTokenExpired,
17
+ apiBaseUrl,
18
+ children
19
+ }) {
20
+ const [sessionToken, setSessionTokenState] = react.useState(initialToken);
21
+ const [isExpired, setIsExpired] = react.useState(false);
22
+ const setSessionToken = react.useCallback(
23
+ (newToken, expiresAt) => {
24
+ setSessionTokenState(newToken);
25
+ setIsExpired(false);
26
+ if (expiresAt) {
27
+ onTokenRefresh?.(newToken, expiresAt);
28
+ }
29
+ },
30
+ [onTokenRefresh]
31
+ );
32
+ const markExpired = react.useCallback(() => {
33
+ setIsExpired(true);
34
+ onTokenExpired?.();
35
+ }, [onTokenExpired]);
36
+ react.useEffect(() => {
37
+ if (initialToken !== sessionToken) {
38
+ setSessionTokenState(initialToken);
39
+ setIsExpired(false);
40
+ }
41
+ }, [initialToken, sessionToken]);
42
+ const value = {
43
+ sessionToken,
44
+ setSessionToken,
45
+ isExpired,
46
+ markExpired,
47
+ apiBaseUrl
48
+ };
49
+ return /* @__PURE__ */ jsxRuntime.jsx(DialTribeContext.Provider, { value, children });
50
+ }
51
+ function useDialTribe() {
52
+ const context = react.useContext(DialTribeContext);
53
+ if (!context) {
54
+ throw new Error(
55
+ 'useDialTribe must be used within a DialTribeProvider. Wrap your app with <DialTribeProvider sessionToken="sess_xxx">...</DialTribeProvider>'
56
+ );
57
+ }
58
+ return context;
59
+ }
60
+ function useDialTribeOptional() {
61
+ return react.useContext(DialTribeContext);
62
+ }
63
+
64
+ // src/client/DialTribeClient.ts
65
+ function getDefaultApiBaseUrl() {
66
+ if (typeof process !== "undefined" && process.env?.NEXT_PUBLIC_DIALTRIBE_API_URL) {
67
+ return process.env.NEXT_PUBLIC_DIALTRIBE_API_URL;
68
+ }
69
+ return "https://dialtribe.com/api/public/v1";
70
+ }
71
+ var DIALTRIBE_API_BASE = getDefaultApiBaseUrl();
72
+ function getEndpoints(baseUrl = DIALTRIBE_API_BASE) {
73
+ return {
74
+ broadcasts: `${baseUrl}/broadcasts`,
75
+ broadcast: (id) => `${baseUrl}/broadcasts/${id}`,
76
+ contentPlay: `${baseUrl}/content/play`,
77
+ presignedUrl: `${baseUrl}/media/presigned-url`,
78
+ sessionStart: `${baseUrl}/session/start`,
79
+ sessionPing: `${baseUrl}/session/ping`
80
+ };
81
+ }
82
+ var ENDPOINTS = getEndpoints();
83
+ var DialTribeClient = class {
84
+ constructor(config) {
85
+ this.config = config;
86
+ this.endpoints = config.apiBaseUrl ? getEndpoints(config.apiBaseUrl) : ENDPOINTS;
87
+ }
88
+ /**
89
+ * Make an authenticated request to DialTribe API
90
+ *
91
+ * Automatically:
92
+ * - Adds Authorization header with session token
93
+ * - Checks for X-Session-Token header in response (token refresh)
94
+ * - Calls onTokenRefresh if new token is provided
95
+ * - Calls onTokenExpired on 401 errors
96
+ */
97
+ async fetch(url, options = {}) {
98
+ const headers = new Headers(options.headers);
99
+ headers.set("Authorization", `Bearer ${this.config.sessionToken}`);
100
+ headers.set("Content-Type", "application/json");
101
+ const response = await fetch(url, {
102
+ ...options,
103
+ headers
104
+ });
105
+ const newToken = response.headers.get("X-Session-Token");
106
+ const expiresAt = response.headers.get("X-Session-Expires");
107
+ if (newToken && expiresAt) {
108
+ this.config.onTokenRefresh?.(newToken, expiresAt);
109
+ }
110
+ if (response.status === 401) {
111
+ this.config.onTokenExpired?.();
112
+ throw new Error("Session token expired or invalid");
113
+ }
114
+ return response;
115
+ }
116
+ /**
117
+ * Update the session token
118
+ * Called automatically when token is refreshed, or manually by user
119
+ */
120
+ setSessionToken(token) {
121
+ this.config.sessionToken = token;
122
+ }
123
+ /**
124
+ * Get list of broadcasts for the authenticated app
125
+ */
126
+ async getBroadcasts(params) {
127
+ const searchParams = new URLSearchParams();
128
+ if (params?.page) searchParams.set("page", params.page.toString());
129
+ if (params?.limit) searchParams.set("limit", params.limit.toString());
130
+ if (params?.broadcastStatus) searchParams.set("broadcastStatus", params.broadcastStatus.toString());
131
+ if (params?.search) searchParams.set("search", params.search);
132
+ if (params?.includeDeleted) searchParams.set("includeDeleted", "true");
133
+ const url = `${this.endpoints.broadcasts}${searchParams.toString() ? `?${searchParams}` : ""}`;
134
+ const response = await this.fetch(url);
135
+ if (!response.ok) {
136
+ throw new Error(`Failed to fetch broadcasts: ${response.status} ${response.statusText}`);
137
+ }
138
+ return response.json();
139
+ }
140
+ /**
141
+ * Get a single broadcast by ID
142
+ */
143
+ async getBroadcast(id) {
144
+ const response = await this.fetch(this.endpoints.broadcast(id));
145
+ if (!response.ok) {
146
+ if (response.status === 404) {
147
+ throw new Error("Broadcast not found");
148
+ }
149
+ throw new Error(`Failed to fetch broadcast: ${response.status} ${response.statusText}`);
150
+ }
151
+ return response.json();
152
+ }
153
+ /**
154
+ * Get presigned URL for media playback
155
+ *
156
+ * @param broadcastId - Broadcast ID
157
+ * @param hash - Broadcast hash (optional if using session token)
158
+ * @param action - 'download' to force download, otherwise streams
159
+ */
160
+ async getPlaybackUrl(params) {
161
+ const searchParams = new URLSearchParams({
162
+ broadcastId: params.broadcastId.toString()
163
+ });
164
+ if (params.hash) searchParams.set("hash", params.hash);
165
+ if (params.action) searchParams.set("action", params.action);
166
+ const url = `${this.endpoints.contentPlay}?${searchParams}`;
167
+ const response = await this.fetch(url, {
168
+ redirect: "manual"
169
+ // Don't follow redirect, we want the URL
170
+ });
171
+ const location = response.headers.get("Location");
172
+ if (!location) {
173
+ throw new Error("No playback URL returned from API");
174
+ }
175
+ return location;
176
+ }
177
+ /**
178
+ * Refresh a presigned URL before it expires
179
+ *
180
+ * @param broadcastId - Broadcast ID
181
+ * @param hash - Broadcast hash
182
+ * @param fileType - Type of media file
183
+ */
184
+ async refreshPresignedUrl(params) {
185
+ const searchParams = new URLSearchParams({
186
+ broadcastId: params.broadcastId.toString(),
187
+ hash: params.hash,
188
+ fileType: params.fileType
189
+ });
190
+ const url = `${this.endpoints.presignedUrl}?${searchParams}`;
191
+ const response = await this.fetch(url);
192
+ if (!response.ok) {
193
+ throw new Error(`Failed to refresh URL: ${response.status} ${response.statusText}`);
194
+ }
195
+ return response.json();
196
+ }
197
+ /**
198
+ * Start a new audience tracking session
199
+ *
200
+ * @returns audienceId and optional resumePosition
201
+ */
202
+ async startSession(params) {
203
+ const response = await this.fetch(this.endpoints.sessionStart, {
204
+ method: "POST",
205
+ body: JSON.stringify(params)
206
+ });
207
+ if (!response.ok) {
208
+ throw new Error(`Failed to start session: ${response.status} ${response.statusText}`);
209
+ }
210
+ return response.json();
211
+ }
212
+ /**
213
+ * Send a session ping event
214
+ *
215
+ * Event types:
216
+ * - 0: PAUSE/STOP
217
+ * - 1: PLAY/START
218
+ * - 2: HEARTBEAT
219
+ * - 3: UNMOUNT
220
+ */
221
+ async sendSessionPing(params) {
222
+ const response = await this.fetch(this.endpoints.sessionPing, {
223
+ method: "POST",
224
+ body: JSON.stringify(params)
225
+ });
226
+ if (!response.ok) {
227
+ throw new Error(`Failed to send session ping: ${response.status} ${response.statusText}`);
228
+ }
229
+ if (response.status === 204) {
230
+ return;
231
+ }
232
+ return response.json();
233
+ }
234
+ };
235
+
236
+ // src/utils/media-constraints.ts
237
+ function getMediaConstraints(options) {
238
+ const { isVideo, facingMode = "user" } = options;
239
+ const audioConstraints = {
240
+ autoGainControl: true,
241
+ channelCount: 2,
242
+ // Stereo
243
+ echoCancellation: false,
244
+ noiseSuppression: false,
245
+ sampleRate: 48e3
246
+ // 48kHz
247
+ };
248
+ const videoConstraints = isVideo ? {
249
+ aspectRatio: 16 / 9,
250
+ width: { ideal: 1280 },
251
+ height: { ideal: 720 },
252
+ frameRate: { ideal: 30 },
253
+ facingMode
254
+ // "user" (front) or "environment" (back)
255
+ } : false;
256
+ return {
257
+ audio: audioConstraints,
258
+ video: videoConstraints
259
+ };
260
+ }
261
+ function getMediaRecorderOptions(isVideo) {
262
+ let mimeType;
263
+ if (isVideo) {
264
+ if (MediaRecorder.isTypeSupported("video/mp4;codecs=avc1,mp4a")) {
265
+ mimeType = "video/mp4;codecs=avc1,mp4a";
266
+ } else if (MediaRecorder.isTypeSupported("video/webm;codecs=h264,opus")) {
267
+ mimeType = "video/webm;codecs=h264,opus";
268
+ } else if (MediaRecorder.isTypeSupported("video/webm;codecs=vp8,opus")) {
269
+ mimeType = "video/webm;codecs=vp8,opus";
270
+ console.warn("\u26A0\uFE0F Browser only supports VP8/Opus - recordings will be disabled");
271
+ }
272
+ } else {
273
+ if (MediaRecorder.isTypeSupported("audio/mp4;codecs=mp4a")) {
274
+ mimeType = "audio/mp4;codecs=mp4a";
275
+ } else if (MediaRecorder.isTypeSupported("audio/webm;codecs=opus")) {
276
+ mimeType = "audio/webm;codecs=opus";
277
+ console.warn("\u26A0\uFE0F Browser only supports Opus - recordings may be disabled");
278
+ }
279
+ }
280
+ return {
281
+ mimeType,
282
+ audioBitsPerSecond: 128e3,
283
+ // 128 kbps audio
284
+ videoBitsPerSecond: isVideo ? 25e5 : void 0
285
+ // 2.5 Mbps for 720p video
286
+ };
287
+ }
288
+ function checkBrowserCompatibility() {
289
+ if (typeof window === "undefined") {
290
+ return {
291
+ compatible: false,
292
+ error: "This component requires a browser environment"
293
+ };
294
+ }
295
+ if (!window.MediaRecorder) {
296
+ return {
297
+ compatible: false,
298
+ error: "MediaRecorder API not supported in this browser"
299
+ };
300
+ }
301
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
302
+ return {
303
+ compatible: false,
304
+ error: "getUserMedia API not supported in this browser"
305
+ };
306
+ }
307
+ return { compatible: true };
308
+ }
309
+
310
+ // src/utils/websocket-streamer.ts
311
+ var DEFAULT_ENCODER_SERVER_URL = "https://broadcastapi.dialtribe.com";
312
+ var WebSocketStreamer = class {
313
+ constructor(options) {
314
+ this.websocket = null;
315
+ this.mediaRecorder = null;
316
+ this.bytesSent = 0;
317
+ this.userStopped = false;
318
+ this.streamKey = options.streamKey;
319
+ this.mediaStream = options.mediaStream;
320
+ this.isVideo = options.isVideo;
321
+ this.encoderServerUrl = options.encoderServerUrl || DEFAULT_ENCODER_SERVER_URL;
322
+ this.onBytesUpdate = options.onBytesUpdate;
323
+ this.onStateChange = options.onStateChange;
324
+ this.onError = options.onError;
325
+ }
326
+ /**
327
+ * Validate stream key format
328
+ * Stream keys must follow format: {tierCode}{foreignId}_{randomKey}
329
+ * Tier codes: a (audio shared), b (audio VIP), v (video shared), w (video VIP)
330
+ */
331
+ validateStreamKeyFormat() {
332
+ if (!this.streamKey || this.streamKey.length < 10) {
333
+ throw new Error("Invalid stream key: too short");
334
+ }
335
+ const tierCode = this.streamKey[0];
336
+ if (!["a", "b", "v", "w"].includes(tierCode)) {
337
+ throw new Error(
338
+ `Invalid stream key format: must start with 'a', 'b', 'v', or 'w' (got '${tierCode}')`
339
+ );
340
+ }
341
+ if (!this.streamKey.includes("_")) {
342
+ throw new Error(
343
+ "Invalid stream key format: must contain underscore separator (format: {tier}{id}_{key})"
344
+ );
345
+ }
346
+ console.log("\u2705 Stream key format validated:", {
347
+ tierCode,
348
+ isVideo: tierCode === "v" || tierCode === "w",
349
+ isVIP: tierCode === "b" || tierCode === "w"
350
+ });
351
+ }
352
+ /**
353
+ * Build WebSocket URL from stream key
354
+ */
355
+ buildWebSocketUrl() {
356
+ const url = new URL(this.encoderServerUrl);
357
+ if (url.protocol === "http:") {
358
+ url.protocol = "ws:";
359
+ } else if (url.protocol === "https:") {
360
+ url.protocol = "wss:";
361
+ }
362
+ url.pathname = "/targets/dialtribe";
363
+ url.searchParams.set("key", this.streamKey);
364
+ return url.toString();
365
+ }
366
+ /**
367
+ * Start streaming
368
+ */
369
+ async start() {
370
+ try {
371
+ this.validateStreamKeyFormat();
372
+ this.onStateChange?.("connecting");
373
+ const wsUrl = this.buildWebSocketUrl();
374
+ console.log("\u{1F4E1} Connecting to WebSocket:", wsUrl.replace(this.streamKey, "***"));
375
+ this.websocket = new WebSocket(wsUrl);
376
+ await new Promise((resolve, reject) => {
377
+ if (!this.websocket) {
378
+ reject(new Error("WebSocket not initialized"));
379
+ return;
380
+ }
381
+ this.websocket.addEventListener("open", () => resolve(), { once: true });
382
+ this.websocket.addEventListener("error", (event) => {
383
+ console.error("\u274C WebSocket error event:", event);
384
+ console.error("\u{1F50D} Connection diagnostics:", {
385
+ url: wsUrl.replace(this.streamKey, "***"),
386
+ streamKeyFormat: this.streamKey.substring(0, 5) + "***",
387
+ readyState: this.websocket?.readyState,
388
+ readyStateText: ["CONNECTING", "OPEN", "CLOSING", "CLOSED"][this.websocket?.readyState || 0]
389
+ });
390
+ reject(new Error(
391
+ `WebSocket connection failed (likely 403 Forbidden).
392
+
393
+ Common causes:
394
+ 1. Encoder server cannot connect to the database
395
+ 2. Encoder server is using a different database
396
+ 3. Stream key validation failed on encoder server
397
+
398
+ Please check encoder server logs and DATABASE_URL configuration.`
399
+ ));
400
+ }, { once: true });
401
+ setTimeout(() => {
402
+ reject(new Error(`WebSocket connection timeout. URL: ${wsUrl}`));
403
+ }, 1e4);
404
+ });
405
+ console.log("\u2705 WebSocket connected");
406
+ this.setupWebSocketHandlers();
407
+ const recorderOptions = getMediaRecorderOptions(this.isVideo);
408
+ this.mediaRecorder = new MediaRecorder(this.mediaStream, recorderOptions);
409
+ console.log("\u{1F399}\uFE0F MediaRecorder created with options:", recorderOptions);
410
+ this.setupMediaRecorderHandlers();
411
+ this.mediaRecorder.start(300);
412
+ console.log("\u{1F534} Recording started");
413
+ this.onStateChange?.("live");
414
+ } catch (error) {
415
+ console.error("\u274C Error starting stream:", error);
416
+ this.onError?.(error instanceof Error ? error.message : "Failed to start stream");
417
+ this.onStateChange?.("error");
418
+ this.stop();
419
+ throw error;
420
+ }
421
+ }
422
+ /**
423
+ * Stop streaming
424
+ */
425
+ stop() {
426
+ console.log("\u23F9\uFE0F Stopping stream");
427
+ this.userStopped = true;
428
+ if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
429
+ this.mediaRecorder.stop();
430
+ console.log("\u23F9\uFE0F MediaRecorder stopped");
431
+ }
432
+ if (this.websocket) {
433
+ const readyStateNames = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
434
+ const stateName = readyStateNames[this.websocket.readyState] || "UNKNOWN";
435
+ console.log(`\u{1F50C} WebSocket state: ${stateName} (${this.websocket.readyState})`);
436
+ if (this.websocket.readyState !== WebSocket.CLOSED) {
437
+ this.websocket.close();
438
+ console.log("\u{1F50C} WebSocket close() called");
439
+ } else {
440
+ console.log("\u{1F50C} WebSocket already closed");
441
+ }
442
+ } else {
443
+ console.log("\u26A0\uFE0F No WebSocket to close");
444
+ }
445
+ this.mediaRecorder = null;
446
+ this.websocket = null;
447
+ this.bytesSent = 0;
448
+ this.onStateChange?.("stopped");
449
+ }
450
+ /**
451
+ * Get total bytes sent
452
+ */
453
+ getBytesSent() {
454
+ return this.bytesSent;
455
+ }
456
+ /**
457
+ * Update the media stream (e.g., when flipping camera)
458
+ * This keeps the WebSocket connection alive while swapping the media source
459
+ *
460
+ * Note: Errors are thrown to the caller, not sent to onError callback
461
+ * This allows the caller to handle camera flip failures gracefully
462
+ */
463
+ async updateMediaStream(newMediaStream) {
464
+ console.log("\u{1F504} Updating media stream (hot-swap)");
465
+ if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
466
+ this.mediaRecorder.stop();
467
+ console.log("\u23F9\uFE0F Old MediaRecorder stopped");
468
+ }
469
+ this.mediaStream = newMediaStream;
470
+ const recorderOptions = getMediaRecorderOptions(this.isVideo);
471
+ this.mediaRecorder = new MediaRecorder(this.mediaStream, recorderOptions);
472
+ console.log("\u{1F399}\uFE0F New MediaRecorder created");
473
+ this.setupMediaRecorderHandlers();
474
+ this.mediaRecorder.start(300);
475
+ console.log("\u2705 Media stream updated - streaming continues");
476
+ }
477
+ /**
478
+ * Set up WebSocket event handlers
479
+ */
480
+ setupWebSocketHandlers() {
481
+ if (!this.websocket) return;
482
+ this.websocket.addEventListener("close", (event) => {
483
+ console.log("\u{1F50C} WebSocket closed", { code: event.code, reason: event.reason });
484
+ if (!this.userStopped) {
485
+ console.warn("\u26A0\uFE0F Stream was terminated remotely (not user-initiated)");
486
+ this.onStateChange?.("terminated");
487
+ this.onError?.("Your stream was terminated by an administrator");
488
+ }
489
+ if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
490
+ this.mediaRecorder.stop();
491
+ }
492
+ });
493
+ this.websocket.addEventListener("error", (event) => {
494
+ console.error("\u274C WebSocket error:", event);
495
+ this.onError?.("WebSocket connection error");
496
+ this.onStateChange?.("error");
497
+ });
498
+ }
499
+ /**
500
+ * Set up MediaRecorder event handlers
501
+ */
502
+ setupMediaRecorderHandlers() {
503
+ if (!this.mediaRecorder) return;
504
+ this.mediaRecorder.addEventListener("dataavailable", (event) => {
505
+ if (event.data.size > 0 && this.websocket?.readyState === WebSocket.OPEN) {
506
+ this.websocket.send(event.data);
507
+ this.bytesSent += event.data.size;
508
+ this.onBytesUpdate?.(this.bytesSent);
509
+ console.log(`\u{1F4E4} Sent ${(event.data.size / 1024).toFixed(2)} KB (Total: ${(this.bytesSent / 1024 / 1024).toFixed(2)} MB)`);
510
+ }
511
+ });
512
+ this.mediaRecorder.addEventListener("error", (event) => {
513
+ const errorEvent = event;
514
+ console.error("\u274C MediaRecorder error:", errorEvent.error);
515
+ this.onError?.(`Encoding error: ${errorEvent.error?.toString() || "Unknown error"}`);
516
+ this.onStateChange?.("error");
517
+ this.stop();
518
+ });
519
+ this.mediaRecorder.addEventListener("stop", () => {
520
+ console.log("\u23F9\uFE0F MediaRecorder stopped");
521
+ if (this.websocket?.readyState === WebSocket.OPEN) {
522
+ this.websocket.close();
523
+ }
524
+ });
525
+ }
526
+ };
527
+ function AudioWaveform({
528
+ audioElement,
529
+ mediaStream,
530
+ isPlaying = false,
531
+ isLive = false
532
+ }) {
533
+ const canvasRef = react.useRef(null);
534
+ const animationFrameRef = react.useRef(void 0);
535
+ const [setupError, setSetupError] = react.useState(false);
536
+ const isPlayingRef = react.useRef(isPlaying);
537
+ const isLiveRef = react.useRef(isLive);
538
+ react.useEffect(() => {
539
+ isPlayingRef.current = isPlaying;
540
+ }, [isPlaying]);
541
+ react.useEffect(() => {
542
+ isLiveRef.current = isLive;
543
+ }, [isLive]);
544
+ react.useEffect(() => {
545
+ const canvas = canvasRef.current;
546
+ if (!canvas) return;
547
+ const ctx = canvas.getContext("2d");
548
+ if (!ctx) return;
549
+ if (audioElement) {
550
+ const hasMediaAPI = "play" in audioElement && "pause" in audioElement && "currentTime" in audioElement;
551
+ const isMediaElement = audioElement instanceof HTMLMediaElement;
552
+ if (!hasMediaAPI && !isMediaElement) {
553
+ console.warn(
554
+ "[AudioWaveform] Invalid audio element - missing media API"
555
+ );
556
+ return;
557
+ }
558
+ console.log("[AudioWaveform] Audio element validation:", {
559
+ tagName: audioElement.tagName,
560
+ isHTMLMediaElement: isMediaElement,
561
+ hasMediaAPI,
562
+ willAttemptVisualization: true
563
+ });
564
+ }
565
+ const canUseAudioElement = audioElement && audioElement instanceof HTMLMediaElement;
566
+ const isHLSLiveStream = audioElement && !canUseAudioElement;
567
+ if (!audioElement && !mediaStream || audioElement && !canUseAudioElement) {
568
+ if (isHLSLiveStream) {
569
+ let time = 0;
570
+ let frozenTime = 0;
571
+ let wasFrozen = false;
572
+ const barPhases = Array.from(
573
+ { length: 128 },
574
+ () => Math.random() * Math.PI * 2
575
+ );
576
+ const barSpeeds = Array.from(
577
+ { length: 128 },
578
+ () => 0.8 + Math.random() * 0.4
579
+ );
580
+ const glowPhases = Array.from(
581
+ { length: 128 },
582
+ () => Math.random() * Math.PI * 2
583
+ );
584
+ const glowSpeeds = Array.from(
585
+ { length: 128 },
586
+ () => 0.7 + Math.random() * 0.6
587
+ );
588
+ const drawEnhancedWaveform = () => {
589
+ ctx.fillStyle = "#000";
590
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
591
+ const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
592
+ gradient.addColorStop(0, "#06b6d4");
593
+ gradient.addColorStop(0.3, "#3b82f6");
594
+ gradient.addColorStop(0.6, "#8b5cf6");
595
+ gradient.addColorStop(1, "#ec4899");
596
+ const currentlyLive = isLiveRef.current;
597
+ if (currentlyLive) {
598
+ time += 0.02;
599
+ wasFrozen = false;
600
+ } else if (!wasFrozen) {
601
+ frozenTime = time;
602
+ wasFrozen = true;
603
+ }
604
+ const currentTime = wasFrozen ? frozenTime : time;
605
+ const barCount = 128;
606
+ const barWidth = canvas.width / barCount;
607
+ const gap = 2;
608
+ const maxHeight = canvas.height * 0.9;
609
+ for (let i = 0; i < barCount; i++) {
610
+ const primaryWave = Math.sin(
611
+ i / barCount * Math.PI * 2 * 2.5 - currentTime * barSpeeds[i] + barPhases[i]
612
+ ) * (maxHeight * 0.35);
613
+ const secondaryWave = Math.sin(
614
+ i / barCount * Math.PI * 2 * 4 - currentTime * barSpeeds[i] * 1.3 + barPhases[i] * 0.7
615
+ ) * (maxHeight * 0.15);
616
+ const tertiaryWave = Math.sin(
617
+ i / barCount * Math.PI * 2 * 7 - currentTime * barSpeeds[i] * 0.8 + barPhases[i] * 1.5
618
+ ) * (maxHeight * 0.1);
619
+ const baseHeight = maxHeight * 0.15;
620
+ const combinedWave = primaryWave + secondaryWave + tertiaryWave;
621
+ const barHeight = Math.max(
622
+ 10,
623
+ Math.min(maxHeight, baseHeight + combinedWave)
624
+ );
625
+ const opacityWave1 = Math.sin(
626
+ i / barCount * Math.PI * 2 * 1.5 - currentTime * 1.2
627
+ );
628
+ const opacityWave2 = Math.sin(
629
+ i / barCount * Math.PI * 2 * 3.5 - currentTime * 0.7
630
+ );
631
+ const opacity = 0.3 + opacityWave1 * 0.25 + opacityWave2 * 0.15;
632
+ const glowWave = Math.sin(
633
+ currentTime * glowSpeeds[i] + glowPhases[i]
634
+ );
635
+ const glowIntensity = 8 + glowWave * 12;
636
+ const x = i * barWidth;
637
+ const y = canvas.height / 2 - barHeight / 2;
638
+ ctx.shadowBlur = glowIntensity;
639
+ ctx.shadowColor = "#3b82f6";
640
+ ctx.fillStyle = gradient;
641
+ ctx.globalAlpha = Math.max(0.15, Math.min(0.9, opacity));
642
+ ctx.fillRect(x + gap / 2, y, barWidth - gap, barHeight);
643
+ }
644
+ ctx.globalAlpha = 1;
645
+ ctx.shadowBlur = 0;
646
+ };
647
+ const animationId2 = setInterval(drawEnhancedWaveform, 1e3 / 60);
648
+ return () => clearInterval(animationId2);
649
+ }
650
+ let waveOffset = 0;
651
+ const drawPlaceholder = () => {
652
+ ctx.fillStyle = "#000";
653
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
654
+ const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
655
+ gradient.addColorStop(0, "#06b6d4");
656
+ gradient.addColorStop(0.3, "#3b82f6");
657
+ gradient.addColorStop(0.6, "#8b5cf6");
658
+ gradient.addColorStop(1, "#ec4899");
659
+ waveOffset += 83e-4;
660
+ const barCount = 128;
661
+ const barWidth = canvas.width / barCount;
662
+ const gap = 2;
663
+ const baseHeight = 15;
664
+ const waveAmplitude = 10;
665
+ for (let i = 0; i < barCount; i++) {
666
+ const wave = Math.sin(i / barCount * Math.PI * 2 * 3 - waveOffset) * waveAmplitude;
667
+ const barHeight = baseHeight + wave;
668
+ const opacityWave = Math.sin(
669
+ i / barCount * Math.PI * 2 * 2 - waveOffset * 1.5
670
+ );
671
+ const opacity = 0.5 + opacityWave * 0.3;
672
+ const x = i * barWidth;
673
+ const y = canvas.height / 2 - barHeight / 2;
674
+ ctx.shadowBlur = 15;
675
+ ctx.shadowColor = "#3b82f6";
676
+ ctx.fillStyle = gradient;
677
+ ctx.globalAlpha = opacity;
678
+ ctx.fillRect(x + gap / 2, y, barWidth - gap, barHeight);
679
+ }
680
+ ctx.globalAlpha = 1;
681
+ ctx.shadowBlur = 0;
682
+ };
683
+ const animationId = setInterval(drawPlaceholder, 1e3 / 60);
684
+ return () => clearInterval(animationId);
685
+ }
686
+ let audioContext = null;
687
+ let analyser = null;
688
+ let source = null;
689
+ try {
690
+ audioContext = new AudioContext();
691
+ analyser = audioContext.createAnalyser();
692
+ analyser.fftSize = 2048;
693
+ if (audioElement) {
694
+ console.log("[AudioWaveform] Creating audio source from element:", {
695
+ tagName: audioElement.tagName,
696
+ src: audioElement.src?.substring(0, 80),
697
+ readyState: audioElement.readyState,
698
+ paused: audioElement.paused,
699
+ currentTime: audioElement.currentTime,
700
+ hasSourceNode: !!audioElement.audioSourceNode,
701
+ isNativeElement: audioElement instanceof HTMLMediaElement
702
+ });
703
+ if (!(audioElement instanceof HTMLMediaElement)) {
704
+ console.warn(
705
+ "[AudioWaveform] Cannot visualize custom element (HLS-VIDEO), falling back to static waveform"
706
+ );
707
+ setSetupError(true);
708
+ return;
709
+ }
710
+ if (audioElement.audioSourceNode) {
711
+ console.log(
712
+ "[AudioWaveform] Audio source already exists, reusing it"
713
+ );
714
+ source = audioElement.audioSourceNode;
715
+ source?.connect(analyser);
716
+ analyser.connect(audioContext.destination);
717
+ } else {
718
+ try {
719
+ source = audioContext.createMediaElementSource(audioElement);
720
+ source.connect(analyser);
721
+ analyser.connect(audioContext.destination);
722
+ audioElement.audioSourceNode = source;
723
+ console.log(
724
+ "[AudioWaveform] Audio source created and connected successfully"
725
+ );
726
+ } catch (error) {
727
+ console.error(
728
+ "[AudioWaveform] Failed to create media element source:",
729
+ error
730
+ );
731
+ setSetupError(true);
732
+ return;
733
+ }
734
+ }
735
+ audioElement.addEventListener("play", () => {
736
+ console.log("[AudioWaveform] Play event - setting isPlaying to true");
737
+ isPlayingRef.current = true;
738
+ });
739
+ audioElement.addEventListener("pause", () => {
740
+ console.log(
741
+ "[AudioWaveform] Pause event - setting isPlaying to false"
742
+ );
743
+ isPlayingRef.current = false;
744
+ });
745
+ audioElement.addEventListener("ended", () => {
746
+ console.log(
747
+ "[AudioWaveform] Ended event - setting isPlaying to false"
748
+ );
749
+ isPlayingRef.current = false;
750
+ });
751
+ console.log("[AudioWaveform] Initial audio state:", {
752
+ paused: audioElement.paused,
753
+ currentTime: audioElement.currentTime,
754
+ readyState: audioElement.readyState
755
+ });
756
+ if (!audioElement.paused) {
757
+ isPlayingRef.current = true;
758
+ }
759
+ } else if (mediaStream) {
760
+ source = audioContext.createMediaStreamSource(mediaStream);
761
+ source.connect(analyser);
762
+ isPlayingRef.current = true;
763
+ } else {
764
+ return;
765
+ }
766
+ const bufferLength = analyser.frequencyBinCount;
767
+ const dataArray = new Uint8Array(bufferLength);
768
+ let waveOffset = 0;
769
+ const drawStaticWaveform = () => {
770
+ ctx.fillStyle = "#000";
771
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
772
+ const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
773
+ gradient.addColorStop(0, "#06b6d4");
774
+ gradient.addColorStop(0.3, "#3b82f6");
775
+ gradient.addColorStop(0.6, "#8b5cf6");
776
+ gradient.addColorStop(1, "#ec4899");
777
+ waveOffset += 83e-4;
778
+ const barCount = 128;
779
+ const barWidth = canvas.width / barCount;
780
+ const gap = 2;
781
+ const baseHeight = 15;
782
+ const waveAmplitude = 10;
783
+ for (let i = 0; i < barCount; i++) {
784
+ const wave = Math.sin(i / barCount * Math.PI * 2 * 3 - waveOffset) * waveAmplitude;
785
+ const barHeight = baseHeight + wave;
786
+ const opacityWave = Math.sin(
787
+ i / barCount * Math.PI * 2 * 2 - waveOffset * 1.5
788
+ );
789
+ const opacity = 0.5 + opacityWave * 0.3;
790
+ const x = i * barWidth;
791
+ const y = canvas.height / 2 - barHeight / 2;
792
+ ctx.shadowBlur = 15;
793
+ ctx.shadowColor = "#3b82f6";
794
+ ctx.fillStyle = gradient;
795
+ ctx.globalAlpha = opacity;
796
+ ctx.fillRect(x + gap / 2, y, barWidth - gap, barHeight);
797
+ }
798
+ ctx.globalAlpha = 1;
799
+ ctx.shadowBlur = 0;
800
+ };
801
+ let frameCount = 0;
802
+ const draw = () => {
803
+ if (!analyser) return;
804
+ animationFrameRef.current = requestAnimationFrame(draw);
805
+ analyser.getByteFrequencyData(dataArray);
806
+ const hasActivity = dataArray.some((value) => value > 0);
807
+ const maxValue = Math.max(...dataArray);
808
+ frameCount++;
809
+ if (frameCount < 5 || frameCount % 60 === 0) {
810
+ console.log("[AudioWaveform] Frame", frameCount, "Audio activity:", {
811
+ hasActivity,
812
+ maxValue,
813
+ isPlaying: isPlayingRef.current,
814
+ sampleValues: [
815
+ dataArray[0],
816
+ dataArray[10],
817
+ dataArray[50],
818
+ dataArray[100]
819
+ ],
820
+ avgValue: dataArray.reduce((a, b) => a + b, 0) / dataArray.length
821
+ });
822
+ }
823
+ if (!hasActivity || !isPlayingRef.current) {
824
+ if (frameCount < 5) {
825
+ console.log(
826
+ "[AudioWaveform] No activity or not playing, showing static waveform"
827
+ );
828
+ }
829
+ drawStaticWaveform();
830
+ return;
831
+ }
832
+ ctx.fillStyle = "rgba(0, 0, 0, 0.1)";
833
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
834
+ const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
835
+ gradient.addColorStop(0, "#06b6d4");
836
+ gradient.addColorStop(0.3, "#3b82f6");
837
+ gradient.addColorStop(0.6, "#8b5cf6");
838
+ gradient.addColorStop(1, "#ec4899");
839
+ const barCount = 128;
840
+ const barWidth = canvas.width / barCount;
841
+ const gap = 2;
842
+ for (let i = 0; i < barCount; i++) {
843
+ const barHeight = dataArray[i] / 255 * canvas.height * 0.8;
844
+ const x = i * barWidth;
845
+ const y = canvas.height / 2 - barHeight / 2;
846
+ ctx.shadowBlur = 20;
847
+ ctx.shadowColor = "#3b82f6";
848
+ ctx.fillStyle = gradient;
849
+ ctx.fillRect(x + gap / 2, y, barWidth - gap, barHeight);
850
+ }
851
+ ctx.shadowBlur = 0;
852
+ };
853
+ draw();
854
+ } catch (error) {
855
+ console.error("Error setting up audio visualization:", error);
856
+ setSetupError(true);
857
+ if (ctx) {
858
+ ctx.fillStyle = "#000";
859
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
860
+ ctx.strokeStyle = "#3b82f6";
861
+ ctx.lineWidth = 2;
862
+ ctx.beginPath();
863
+ ctx.moveTo(0, canvas.height / 2);
864
+ ctx.lineTo(canvas.width, canvas.height / 2);
865
+ ctx.stroke();
866
+ }
867
+ }
868
+ return () => {
869
+ if (animationFrameRef.current) {
870
+ cancelAnimationFrame(animationFrameRef.current);
871
+ }
872
+ if (audioContext) {
873
+ audioContext.close();
874
+ }
875
+ };
876
+ }, [audioElement, mediaStream, isLive]);
877
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full h-full", children: [
878
+ /* @__PURE__ */ jsxRuntime.jsx(
879
+ "canvas",
880
+ {
881
+ ref: canvasRef,
882
+ width: 1600,
883
+ height: 400,
884
+ className: "w-full h-full",
885
+ style: { display: "block" }
886
+ }
887
+ ),
888
+ setupError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-400 text-xs text-center mt-2 absolute bottom-4", children: "Audio visualization unavailable" })
889
+ ] });
890
+ }
891
+ function StreamingPreview({
892
+ videoRef,
893
+ isVideoKey,
894
+ isVideoEnabled,
895
+ mediaStream,
896
+ facingMode
897
+ }) {
898
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "dialtribe-streaming-preview flex-1 relative bg-black overflow-hidden", children: [
899
+ isVideoKey && isVideoEnabled && /* @__PURE__ */ jsxRuntime.jsx(
900
+ "video",
901
+ {
902
+ ref: videoRef,
903
+ autoPlay: true,
904
+ muted: true,
905
+ playsInline: true,
906
+ className: `w-full h-full object-cover ${facingMode === "user" ? "scale-x-[-1]" : ""}`,
907
+ style: { maxHeight: "100vh" }
908
+ }
909
+ ),
910
+ !isVideoKey && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full h-full flex items-center justify-center p-8", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full max-w-4xl", children: [
911
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full h-auto border border-gray-800 rounded-lg overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(AudioWaveform, { mediaStream, isPlaying: true }) }),
912
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center mt-4", children: [
913
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-16 h-16 bg-blue-600 rounded-full flex items-center justify-center mx-auto mb-3", children: /* @__PURE__ */ jsxRuntime.jsx(
914
+ "svg",
915
+ {
916
+ className: "w-8 h-8 text-white",
917
+ fill: "none",
918
+ stroke: "currentColor",
919
+ viewBox: "0 0 24 24",
920
+ children: /* @__PURE__ */ jsxRuntime.jsx(
921
+ "path",
922
+ {
923
+ strokeLinecap: "round",
924
+ strokeLinejoin: "round",
925
+ strokeWidth: 2,
926
+ d: "M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"
927
+ }
928
+ )
929
+ }
930
+ ) }),
931
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-white text-xl font-medium", children: "Audio-Only Stream" }),
932
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-400 text-sm mt-1", children: "Your audio is being captured" })
933
+ ] })
934
+ ] }) }),
935
+ isVideoKey && !isVideoEnabled && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full h-full flex items-center justify-center p-8 bg-gray-900", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full max-w-4xl", children: [
936
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full h-auto border border-gray-800 rounded-lg overflow-hidden mb-6", children: /* @__PURE__ */ jsxRuntime.jsx(AudioWaveform, { mediaStream, isPlaying: true }) }),
937
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
938
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-16 h-16 bg-gray-800 rounded-full flex items-center justify-center mx-auto mb-3", children: /* @__PURE__ */ jsxRuntime.jsx(
939
+ "svg",
940
+ {
941
+ className: "w-8 h-8 text-gray-400",
942
+ fill: "none",
943
+ stroke: "currentColor",
944
+ viewBox: "0 0 24 24",
945
+ "aria-hidden": "true",
946
+ children: /* @__PURE__ */ jsxRuntime.jsx(
947
+ "path",
948
+ {
949
+ strokeLinecap: "round",
950
+ strokeLinejoin: "round",
951
+ strokeWidth: 2,
952
+ d: "M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z M3 3l18 18"
953
+ }
954
+ )
955
+ }
956
+ ) }),
957
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-white text-xl font-medium", children: "Camera Off" }),
958
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-400 text-sm mt-1", children: "Your audio is still being broadcast" })
959
+ ] })
960
+ ] }) })
961
+ ] });
962
+ }
963
+ function StreamKeyDisplay({
964
+ streamKey,
965
+ className = "",
966
+ showLabel = true,
967
+ showCopy = true,
968
+ editable = false,
969
+ onChange,
970
+ size = "md",
971
+ layout = "vertical",
972
+ darkMode = false
973
+ }) {
974
+ const [isRevealed, setIsRevealed] = react.useState(false);
975
+ const [copySuccess, setCopySuccess] = react.useState(false);
976
+ const obscureStreamKey = (key) => {
977
+ if (key.length <= 12) {
978
+ return "\u2022".repeat(key.length);
979
+ }
980
+ return `${key.substring(0, 6)}${"\u2022".repeat(key.length - 12)}${key.substring(key.length - 6)}`;
981
+ };
982
+ const handleCopy = async () => {
983
+ try {
984
+ await navigator.clipboard.writeText(streamKey);
985
+ setCopySuccess(true);
986
+ setTimeout(() => setCopySuccess(false), 2e3);
987
+ } catch (err) {
988
+ console.error("Failed to copy stream key:", err);
989
+ }
990
+ };
991
+ const handleReveal = () => {
992
+ setIsRevealed(!isRevealed);
993
+ };
994
+ const sizeClasses2 = {
995
+ sm: {
996
+ label: "text-xs",
997
+ code: "text-xs px-2 py-1",
998
+ button: "text-xs px-2 py-1"
999
+ },
1000
+ md: {
1001
+ label: "text-sm",
1002
+ code: "text-sm px-2 py-1",
1003
+ button: "text-sm px-3 py-1.5"
1004
+ },
1005
+ lg: {
1006
+ label: "text-base",
1007
+ code: "text-base px-3 py-2",
1008
+ button: "text-base px-4 py-2"
1009
+ }
1010
+ };
1011
+ const styles = sizeClasses2[size];
1012
+ const isHorizontal = layout === "horizontal";
1013
+ const containerClass = isHorizontal ? "flex items-center gap-3" : "flex flex-col gap-2";
1014
+ const codeClass = darkMode && !isRevealed ? `${styles.code} font-mono text-white bg-transparent rounded truncate` : `${styles.code} font-mono text-black dark:text-white bg-gray-100 dark:bg-zinc-800 rounded truncate flex-1 min-w-0`;
1015
+ const labelClass = darkMode ? `${styles.label} text-white/80 font-medium whitespace-nowrap` : `${styles.label} text-gray-600 dark:text-gray-400 font-medium`;
1016
+ const revealButtonClass = darkMode ? `${styles.button} text-blue-400 hover:text-blue-300 hover:bg-white/10 rounded transition-colors whitespace-nowrap` : `${styles.button} text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded transition-colors whitespace-nowrap`;
1017
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `dialtribe-stream-key-display ${containerClass} ${className}`, children: [
1018
+ showLabel && /* @__PURE__ */ jsxRuntime.jsx("span", { className: labelClass, children: "Stream Key:" }),
1019
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 min-w-0 overflow-hidden", children: [
1020
+ isRevealed && editable ? /* @__PURE__ */ jsxRuntime.jsx(
1021
+ "input",
1022
+ {
1023
+ type: "text",
1024
+ value: streamKey,
1025
+ onChange: (e) => onChange?.(e.target.value),
1026
+ className: `${styles.code} font-mono text-black dark:text-white bg-white dark:bg-zinc-900 border border-gray-300 dark:border-zinc-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1 min-w-0`,
1027
+ placeholder: "Enter stream key"
1028
+ }
1029
+ ) : /* @__PURE__ */ jsxRuntime.jsx("code", { className: codeClass, children: isRevealed ? streamKey : obscureStreamKey(streamKey) }),
1030
+ /* @__PURE__ */ jsxRuntime.jsx(
1031
+ "button",
1032
+ {
1033
+ onClick: handleReveal,
1034
+ className: `${revealButtonClass} shrink-0`,
1035
+ children: isRevealed ? "Hide" : "Reveal"
1036
+ }
1037
+ ),
1038
+ showCopy && /* @__PURE__ */ jsxRuntime.jsx(
1039
+ "button",
1040
+ {
1041
+ onClick: handleCopy,
1042
+ className: `${styles.button} text-gray-600 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded transition-colors whitespace-nowrap shrink-0`,
1043
+ title: "Copy to clipboard",
1044
+ children: copySuccess ? "Copied!" : "Copy"
1045
+ }
1046
+ )
1047
+ ] })
1048
+ ] });
1049
+ }
1050
+ function StreamingControls({
1051
+ state,
1052
+ isVideoKey,
1053
+ isMuted,
1054
+ isVideoEnabled,
1055
+ facingMode: _facingMode,
1056
+ // Reserved for future use (e.g., showing camera direction)
1057
+ hasMultipleCameras,
1058
+ startTime,
1059
+ bytesSent,
1060
+ showStopConfirm,
1061
+ streamKey,
1062
+ onStreamKeyChange,
1063
+ onStart,
1064
+ onStop,
1065
+ onConfirmStop,
1066
+ onCancelStop,
1067
+ onToggleMute,
1068
+ onToggleVideo,
1069
+ onFlipCamera,
1070
+ onClose,
1071
+ showCloseConfirm,
1072
+ onConfirmClose,
1073
+ onCancelClose,
1074
+ // Device selection props
1075
+ videoDevices = [],
1076
+ audioDevices = [],
1077
+ selectedVideoDeviceId,
1078
+ selectedAudioDeviceId,
1079
+ onVideoDeviceChange,
1080
+ onAudioDeviceChange,
1081
+ mediaStream
1082
+ }) {
1083
+ const [duration, setDuration] = react.useState(0);
1084
+ const [showSettings, setShowSettings] = react.useState(false);
1085
+ react.useEffect(() => {
1086
+ if (state !== "live" || !startTime) return;
1087
+ const interval = setInterval(() => {
1088
+ const elapsed = Math.floor((Date.now() - startTime.getTime()) / 1e3);
1089
+ setDuration(elapsed);
1090
+ }, 1e3);
1091
+ return () => clearInterval(interval);
1092
+ }, [state, startTime]);
1093
+ const formatDuration = (seconds) => {
1094
+ const hours = Math.floor(seconds / 3600);
1095
+ const minutes = Math.floor(seconds % 3600 / 60);
1096
+ const secs = seconds % 60;
1097
+ if (hours > 0) {
1098
+ return `${hours}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
1099
+ }
1100
+ return `${minutes}:${secs.toString().padStart(2, "0")}`;
1101
+ };
1102
+ const formatBytes = (bytes) => {
1103
+ if (bytes === 0) return "0 B";
1104
+ const k = 1024;
1105
+ const sizes = ["B", "KB", "MB", "GB"];
1106
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1107
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
1108
+ };
1109
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1110
+ state === "previewing" && onClose && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute top-0 left-0 right-0 p-4 bg-gradient-to-b from-black/60 to-transparent z-10 flex items-start justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(
1111
+ "button",
1112
+ {
1113
+ onClick: onClose,
1114
+ className: "w-10 h-10 shrink-0 bg-black/50 hover:bg-black/70 backdrop-blur rounded-full flex items-center justify-center transition-colors",
1115
+ title: "Close broadcast preview",
1116
+ "aria-label": "Close broadcast preview",
1117
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
1118
+ }
1119
+ ) }),
1120
+ state === "live" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute top-0 left-0 right-0 p-4 bg-gradient-to-b from-black/60 to-transparent z-10", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
1121
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1122
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 bg-red-600 rounded-full animate-pulse" }),
1123
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-white font-semibold text-lg", children: "LIVE" })
1124
+ ] }),
1125
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4", children: [
1126
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-right", children: [
1127
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-white font-mono text-lg font-semibold", children: formatDuration(duration) }),
1128
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-white/80 text-sm", children: [
1129
+ formatBytes(bytesSent),
1130
+ " sent"
1131
+ ] })
1132
+ ] }),
1133
+ onClose && /* @__PURE__ */ jsxRuntime.jsx(
1134
+ "button",
1135
+ {
1136
+ onClick: onClose,
1137
+ className: "w-10 h-10 bg-black/50 hover:bg-black/70 backdrop-blur rounded-full flex items-center justify-center transition-colors",
1138
+ title: "Close and end broadcast",
1139
+ "aria-label": "Close and end broadcast",
1140
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
1141
+ }
1142
+ )
1143
+ ] })
1144
+ ] }) }),
1145
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black/80 to-transparent z-10", children: [
1146
+ state === "previewing" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex justify-center items-center", children: [
1147
+ /* @__PURE__ */ jsxRuntime.jsx(
1148
+ "button",
1149
+ {
1150
+ onClick: onStart,
1151
+ className: "px-12 py-4 bg-blue-600 hover:bg-blue-700 text-white text-xl font-bold rounded-full transition-all transform hover:scale-105 active:scale-95 shadow-lg",
1152
+ "aria-label": "Start live streaming",
1153
+ children: "Start Streaming"
1154
+ }
1155
+ ),
1156
+ /* @__PURE__ */ jsxRuntime.jsx(
1157
+ "button",
1158
+ {
1159
+ onClick: () => setShowSettings(true),
1160
+ className: "absolute right-0 w-10 h-10 bg-black/50 hover:bg-black/70 backdrop-blur rounded-full flex items-center justify-center transition-colors",
1161
+ title: "Stream settings",
1162
+ "aria-label": "Open stream settings",
1163
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5 text-white", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z", clipRule: "evenodd" }) })
1164
+ }
1165
+ )
1166
+ ] }),
1167
+ state === "connecting" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-12 py-4 bg-blue-600 text-white text-xl font-bold rounded-full flex items-center gap-3", children: [
1168
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" }),
1169
+ "Connecting..."
1170
+ ] }) }),
1171
+ state === "live" && !showStopConfirm && !showCloseConfirm && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-4", children: [
1172
+ /* @__PURE__ */ jsxRuntime.jsx(
1173
+ "button",
1174
+ {
1175
+ onClick: onToggleMute,
1176
+ className: `w-14 h-14 rounded-full flex items-center justify-center transition-all shadow-lg ${isMuted ? "bg-red-600 hover:bg-red-700" : "bg-white/20 hover:bg-white/30 backdrop-blur"}`,
1177
+ title: isMuted ? "Unmute microphone" : "Mute microphone",
1178
+ "aria-label": isMuted ? "Unmute microphone" : "Mute microphone",
1179
+ "aria-pressed": isMuted,
1180
+ children: isMuted ? /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-6 h-6 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(
1181
+ "path",
1182
+ {
1183
+ strokeLinecap: "round",
1184
+ strokeLinejoin: "round",
1185
+ strokeWidth: 2,
1186
+ d: "M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z M17 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2"
1187
+ }
1188
+ ) }) : /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-6 h-6 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(
1189
+ "path",
1190
+ {
1191
+ strokeLinecap: "round",
1192
+ strokeLinejoin: "round",
1193
+ strokeWidth: 2,
1194
+ d: "M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z"
1195
+ }
1196
+ ) })
1197
+ }
1198
+ ),
1199
+ /* @__PURE__ */ jsxRuntime.jsx(
1200
+ "button",
1201
+ {
1202
+ onClick: onStop,
1203
+ className: "w-16 h-16 bg-red-600 hover:bg-red-700 rounded-full flex items-center justify-center transition-all shadow-lg",
1204
+ title: "Stop streaming",
1205
+ "aria-label": "Stop streaming",
1206
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-6 h-6 bg-white rounded-sm", "aria-hidden": "true" })
1207
+ }
1208
+ ),
1209
+ isVideoKey && /* @__PURE__ */ jsxRuntime.jsx(
1210
+ "button",
1211
+ {
1212
+ onClick: onToggleVideo,
1213
+ className: `w-14 h-14 rounded-full flex items-center justify-center transition-all shadow-lg ${!isVideoEnabled ? "bg-red-600 hover:bg-red-700" : "bg-white/20 hover:bg-white/30 backdrop-blur"}`,
1214
+ title: isVideoEnabled ? "Turn camera off" : "Turn camera on",
1215
+ "aria-label": isVideoEnabled ? "Turn camera off" : "Turn camera on",
1216
+ "aria-pressed": !isVideoEnabled,
1217
+ children: isVideoEnabled ? /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-6 h-6 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(
1218
+ "path",
1219
+ {
1220
+ strokeLinecap: "round",
1221
+ strokeLinejoin: "round",
1222
+ strokeWidth: 2,
1223
+ d: "M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
1224
+ }
1225
+ ) }) : /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-6 h-6 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(
1226
+ "path",
1227
+ {
1228
+ strokeLinecap: "round",
1229
+ strokeLinejoin: "round",
1230
+ strokeWidth: 2,
1231
+ d: "M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z M3 3l18 18"
1232
+ }
1233
+ ) })
1234
+ }
1235
+ ),
1236
+ isVideoKey && hasMultipleCameras && /* @__PURE__ */ jsxRuntime.jsx(
1237
+ "button",
1238
+ {
1239
+ onClick: onFlipCamera,
1240
+ className: "w-14 h-14 bg-white/20 hover:bg-white/30 backdrop-blur rounded-full flex items-center justify-center transition-all shadow-lg",
1241
+ title: "Switch camera",
1242
+ "aria-label": "Switch between front and back camera",
1243
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-6 h-6 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(
1244
+ "path",
1245
+ {
1246
+ strokeLinecap: "round",
1247
+ strokeLinejoin: "round",
1248
+ strokeWidth: 2,
1249
+ d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
1250
+ }
1251
+ ) })
1252
+ }
1253
+ ),
1254
+ /* @__PURE__ */ jsxRuntime.jsx(
1255
+ "button",
1256
+ {
1257
+ disabled: true,
1258
+ className: "w-14 h-14 bg-white/10 rounded-full flex items-center justify-center opacity-50 cursor-not-allowed shadow-lg",
1259
+ title: "Create clip (Coming soon)",
1260
+ "aria-label": "Create clip (Coming soon)",
1261
+ "aria-disabled": "true",
1262
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-6 h-6 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(
1263
+ "path",
1264
+ {
1265
+ strokeLinecap: "round",
1266
+ strokeLinejoin: "round",
1267
+ strokeWidth: 2,
1268
+ d: "M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
1269
+ }
1270
+ ) })
1271
+ }
1272
+ )
1273
+ ] }) })
1274
+ ] }),
1275
+ showStopConfirm && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 z-20 flex items-center justify-center p-4 bg-black/70", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-black/95 rounded-2xl p-6 backdrop-blur border border-white/20 max-w-sm w-full", role: "dialog", "aria-labelledby": "stop-dialog-title", children: [
1276
+ /* @__PURE__ */ jsxRuntime.jsx("p", { id: "stop-dialog-title", className: "text-white text-center text-lg font-medium mb-4", children: "Stop streaming?" }),
1277
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-3", children: [
1278
+ /* @__PURE__ */ jsxRuntime.jsx(
1279
+ "button",
1280
+ {
1281
+ onClick: onCancelStop,
1282
+ className: "flex-1 px-6 py-3 bg-white/20 hover:bg-white/30 text-white font-medium rounded-lg transition-colors",
1283
+ "aria-label": "Cancel and continue streaming",
1284
+ children: "Cancel"
1285
+ }
1286
+ ),
1287
+ /* @__PURE__ */ jsxRuntime.jsx(
1288
+ "button",
1289
+ {
1290
+ onClick: onConfirmStop,
1291
+ className: "flex-1 px-6 py-3 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors",
1292
+ "aria-label": "Confirm stop streaming",
1293
+ children: "Stop"
1294
+ }
1295
+ )
1296
+ ] })
1297
+ ] }) }),
1298
+ showCloseConfirm && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 z-20 flex items-center justify-center p-4 bg-black/70", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-black/95 rounded-2xl p-6 backdrop-blur border border-white/20 max-w-sm w-full", role: "dialog", "aria-labelledby": "close-dialog-title", "aria-describedby": "close-dialog-description", children: [
1299
+ /* @__PURE__ */ jsxRuntime.jsx("p", { id: "close-dialog-title", className: "text-white text-center text-lg font-medium mb-2", children: "Close and end stream?" }),
1300
+ /* @__PURE__ */ jsxRuntime.jsx("p", { id: "close-dialog-description", className: "text-white/70 text-center text-sm mb-4", children: "Closing will stop your live broadcast." }),
1301
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-3", children: [
1302
+ /* @__PURE__ */ jsxRuntime.jsx(
1303
+ "button",
1304
+ {
1305
+ onClick: onCancelClose,
1306
+ className: "flex-1 px-6 py-3 bg-white/20 hover:bg-white/30 text-white font-medium rounded-lg transition-colors",
1307
+ "aria-label": "Cancel and continue streaming",
1308
+ children: "Cancel"
1309
+ }
1310
+ ),
1311
+ /* @__PURE__ */ jsxRuntime.jsx(
1312
+ "button",
1313
+ {
1314
+ onClick: onConfirmClose,
1315
+ className: "flex-1 px-6 py-3 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors",
1316
+ "aria-label": "Confirm end broadcast and close",
1317
+ children: "End & Close"
1318
+ }
1319
+ )
1320
+ ] })
1321
+ ] }) }),
1322
+ showSettings && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 z-20 flex items-center justify-center p-4 bg-black/80", children: /* @__PURE__ */ jsxRuntime.jsxs(
1323
+ "div",
1324
+ {
1325
+ className: "bg-black/95 rounded-2xl backdrop-blur border border-white/20 max-w-md w-full max-h-[90%] overflow-hidden flex flex-col",
1326
+ role: "dialog",
1327
+ "aria-labelledby": "settings-dialog-title",
1328
+ children: [
1329
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between p-4 border-b border-white/10", children: [
1330
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { id: "settings-dialog-title", className: "text-white text-lg font-semibold", children: "Stream Settings" }),
1331
+ /* @__PURE__ */ jsxRuntime.jsx(
1332
+ "button",
1333
+ {
1334
+ onClick: () => setShowSettings(false),
1335
+ className: "w-8 h-8 bg-white/10 hover:bg-white/20 rounded-full flex items-center justify-center transition-colors",
1336
+ title: "Close settings",
1337
+ "aria-label": "Close settings",
1338
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-4 h-4 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
1339
+ }
1340
+ )
1341
+ ] }),
1342
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-6", children: [
1343
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1344
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "block text-sm font-medium text-white/80 mb-2", children: "Stream Key" }),
1345
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-white/5 border border-white/10 rounded-lg p-3", children: /* @__PURE__ */ jsxRuntime.jsx(
1346
+ StreamKeyDisplay,
1347
+ {
1348
+ streamKey,
1349
+ editable: !!onStreamKeyChange,
1350
+ onChange: onStreamKeyChange,
1351
+ showCopy: true,
1352
+ size: "sm",
1353
+ layout: "vertical",
1354
+ darkMode: true
1355
+ }
1356
+ ) })
1357
+ ] }),
1358
+ isVideoKey && videoDevices.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1359
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "block text-sm font-medium text-white/80 mb-2", children: "Camera" }),
1360
+ /* @__PURE__ */ jsxRuntime.jsx(
1361
+ "select",
1362
+ {
1363
+ value: selectedVideoDeviceId || "",
1364
+ onChange: (e) => onVideoDeviceChange?.(e.target.value),
1365
+ className: "w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500",
1366
+ "aria-label": "Select camera",
1367
+ children: videoDevices.map((device) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: device.deviceId, className: "bg-zinc-900", children: device.label || `Camera ${device.deviceId.slice(0, 8)}` }, device.deviceId))
1368
+ }
1369
+ ),
1370
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3 aspect-video bg-black rounded-lg overflow-hidden border border-white/10", children: mediaStream && isVideoEnabled ? /* @__PURE__ */ jsxRuntime.jsx(
1371
+ "video",
1372
+ {
1373
+ autoPlay: true,
1374
+ muted: true,
1375
+ playsInline: true,
1376
+ className: "w-full h-full object-cover scale-x-[-1]",
1377
+ ref: (el) => {
1378
+ if (el && mediaStream) {
1379
+ el.srcObject = mediaStream;
1380
+ }
1381
+ }
1382
+ }
1383
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full h-full flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
1384
+ /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-8 h-8 text-gray-500 mx-auto mb-2", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z M3 3l18 18" }) }),
1385
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-xs", children: "Camera off" })
1386
+ ] }) }) })
1387
+ ] }),
1388
+ audioDevices.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1389
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "block text-sm font-medium text-white/80 mb-2", children: "Microphone" }),
1390
+ /* @__PURE__ */ jsxRuntime.jsx(
1391
+ "select",
1392
+ {
1393
+ value: selectedAudioDeviceId || "",
1394
+ onChange: (e) => onAudioDeviceChange?.(e.target.value),
1395
+ className: "w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500",
1396
+ "aria-label": "Select microphone",
1397
+ children: audioDevices.map((device) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: device.deviceId, className: "bg-zinc-900", children: device.label || `Microphone ${device.deviceId.slice(0, 8)}` }, device.deviceId))
1398
+ }
1399
+ ),
1400
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3 h-16 bg-black rounded-lg overflow-hidden border border-white/10", children: mediaStream && !isMuted ? /* @__PURE__ */ jsxRuntime.jsx(AudioWaveform, { mediaStream, isPlaying: true }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full h-full flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-500 text-xs", children: isMuted ? "Microphone muted" : "No audio" }) }) })
1401
+ ] })
1402
+ ] }),
1403
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 border-t border-white/10", children: /* @__PURE__ */ jsxRuntime.jsx(
1404
+ "button",
1405
+ {
1406
+ onClick: () => setShowSettings(false),
1407
+ className: "w-full px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors",
1408
+ "aria-label": "Save and close settings",
1409
+ children: "Done"
1410
+ }
1411
+ ) })
1412
+ ]
1413
+ }
1414
+ ) })
1415
+ ] });
1416
+ }
1417
+ function StreamKeyInput({ onSubmit, inline = false }) {
1418
+ const [streamKey, setStreamKey] = react.useState("");
1419
+ const [error, setError] = react.useState("");
1420
+ const containerClass = inline ? "dialtribe-stream-key-input flex items-center justify-center h-full w-full p-4 overflow-auto" : "dialtribe-stream-key-input flex items-center justify-center min-h-screen p-4";
1421
+ const validateStreamKey = (key) => {
1422
+ const pattern = /^[abvw][a-zA-Z0-9]+_.+$/;
1423
+ return pattern.test(key);
1424
+ };
1425
+ const handleSubmit = (e) => {
1426
+ e.preventDefault();
1427
+ setError("");
1428
+ const trimmedKey = streamKey.trim();
1429
+ if (!trimmedKey) {
1430
+ setError("Please enter a stream key");
1431
+ return;
1432
+ }
1433
+ if (!validateStreamKey(trimmedKey)) {
1434
+ setError(
1435
+ "Invalid stream key format. Expected format: {mediaType}{foreignId}_{key} (e.g., w1_abc123...)"
1436
+ );
1437
+ return;
1438
+ }
1439
+ onSubmit(trimmedKey);
1440
+ };
1441
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: containerClass, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-lg border border-gray-200 dark:border-zinc-800 p-8 max-w-md w-full", children: [
1442
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center mb-8", children: [
1443
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-3xl font-bold text-black dark:text-white mb-2", children: "Start Broadcasting" }),
1444
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600 dark:text-gray-400", children: "Enter your stream key to get started" })
1445
+ ] }),
1446
+ /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, className: "space-y-6", children: [
1447
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1448
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2", children: "Stream Key" }),
1449
+ /* @__PURE__ */ jsxRuntime.jsx(
1450
+ "input",
1451
+ {
1452
+ type: "text",
1453
+ value: streamKey,
1454
+ onChange: (e) => {
1455
+ setStreamKey(e.target.value);
1456
+ setError("");
1457
+ },
1458
+ className: "w-full px-4 py-3 bg-white dark:bg-zinc-800 border border-gray-300 dark:border-zinc-700 rounded-lg text-black dark:text-white font-mono text-sm placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:ring-2 focus:ring-blue-500 focus:border-transparent",
1459
+ placeholder: "w1_abc123...",
1460
+ autoFocus: true
1461
+ }
1462
+ ),
1463
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-sm text-red-600 dark:text-red-400", children: error }),
1464
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-xs text-gray-500 dark:text-gray-400", children: "Paste the stream key provided by your administrator" })
1465
+ ] }),
1466
+ /* @__PURE__ */ jsxRuntime.jsx(
1467
+ "button",
1468
+ {
1469
+ type: "submit",
1470
+ className: "w-full px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors",
1471
+ children: "Continue"
1472
+ }
1473
+ )
1474
+ ] }),
1475
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-8 pt-6 border-t border-gray-200 dark:border-zinc-800", children: [
1476
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400 mb-2", children: "Where do I find my stream key?" }),
1477
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: [
1478
+ "If you have access to the DialTribe dashboard, navigate to your app's ",
1479
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Stream Keys" }),
1480
+ ' page and click the "Broadcast" button next to any active stream key.'
1481
+ ] })
1482
+ ] })
1483
+ ] }) });
1484
+ }
1485
+ function BroadcastStreamer({
1486
+ sessionToken: propSessionToken,
1487
+ streamKey: initialStreamKey,
1488
+ onDone,
1489
+ onStreamKeyChange,
1490
+ encoderServerUrl = DEFAULT_ENCODER_SERVER_URL,
1491
+ apiBaseUrl = DIALTRIBE_API_BASE,
1492
+ inline = false
1493
+ }) {
1494
+ const containerClass = inline ? "dialtribe-broadcast-streamer h-full w-full bg-black relative" : "dialtribe-broadcast-streamer min-h-screen bg-black";
1495
+ const centeredContainerClass = inline ? "dialtribe-broadcast-streamer flex items-center justify-center h-full w-full p-4 bg-black relative" : "dialtribe-broadcast-streamer flex items-center justify-center min-h-screen p-4 bg-black";
1496
+ const dialTribeContext = useDialTribeOptional();
1497
+ const sessionToken = propSessionToken ?? dialTribeContext?.sessionToken ?? null;
1498
+ const [streamKey, setStreamKey] = react.useState(initialStreamKey || null);
1499
+ const [state, setState] = react.useState("idle");
1500
+ react.useEffect(() => {
1501
+ if (initialStreamKey && initialStreamKey !== streamKey) {
1502
+ setStreamKey(initialStreamKey);
1503
+ }
1504
+ }, [initialStreamKey]);
1505
+ const [error, setError] = react.useState(null);
1506
+ const [mediaStream, setMediaStream] = react.useState(null);
1507
+ const [streamer, setStreamer] = react.useState(null);
1508
+ const [bytesSent, setBytesSent] = react.useState(0);
1509
+ const [startTime, setStartTime] = react.useState(null);
1510
+ const [isMuted, setIsMuted] = react.useState(false);
1511
+ const [isVideoEnabled, setIsVideoEnabled] = react.useState(true);
1512
+ const [facingMode, setFacingMode] = react.useState("user");
1513
+ const [showStopConfirm, setShowStopConfirm] = react.useState(false);
1514
+ const [showCloseConfirm, setShowCloseConfirm] = react.useState(false);
1515
+ const [hasMultipleCameras, setHasMultipleCameras] = react.useState(false);
1516
+ const [videoDevices, setVideoDevices] = react.useState([]);
1517
+ const [audioDevices, setAudioDevices] = react.useState([]);
1518
+ const [selectedVideoDeviceId, setSelectedVideoDeviceId] = react.useState();
1519
+ const [selectedAudioDeviceId, setSelectedAudioDeviceId] = react.useState();
1520
+ const videoRef = react.useRef(null);
1521
+ const streamerRef = react.useRef(null);
1522
+ const mediaStreamRef = react.useRef(null);
1523
+ const isVideoKey = streamKey ? streamKey.startsWith("v") || streamKey.startsWith("w") : false;
1524
+ const handleStreamKeySubmit = (key) => {
1525
+ setStreamKey(key);
1526
+ onStreamKeyChange?.(key);
1527
+ };
1528
+ const handleStreamKeyChange = (key) => {
1529
+ setStreamKey(key);
1530
+ onStreamKeyChange?.(key);
1531
+ };
1532
+ react.useEffect(() => {
1533
+ if (!streamKey) return;
1534
+ const compat = checkBrowserCompatibility();
1535
+ if (!compat.compatible) {
1536
+ setError(compat.error || "Browser not compatible");
1537
+ setState("error");
1538
+ return;
1539
+ }
1540
+ detectCameras();
1541
+ requestMediaPermissions();
1542
+ }, [streamKey]);
1543
+ const detectCameras = async () => {
1544
+ try {
1545
+ const devices = await navigator.mediaDevices.enumerateDevices();
1546
+ const videoInputs = devices.filter((device) => device.kind === "videoinput");
1547
+ const audioInputs = devices.filter((device) => device.kind === "audioinput");
1548
+ console.log(`\u{1F4F7} Found ${videoInputs.length} video input device(s)`);
1549
+ console.log(`\u{1F3A4} Found ${audioInputs.length} audio input device(s)`);
1550
+ setHasMultipleCameras(videoInputs.length > 1);
1551
+ setVideoDevices(videoInputs.map((d) => ({
1552
+ deviceId: d.deviceId,
1553
+ label: d.label || `Camera ${d.deviceId.slice(0, 8)}`
1554
+ })));
1555
+ setAudioDevices(audioInputs.map((d) => ({
1556
+ deviceId: d.deviceId,
1557
+ label: d.label || `Microphone ${d.deviceId.slice(0, 8)}`
1558
+ })));
1559
+ if (videoInputs.length > 0 && !selectedVideoDeviceId) {
1560
+ setSelectedVideoDeviceId(videoInputs[0].deviceId);
1561
+ }
1562
+ if (audioInputs.length > 0 && !selectedAudioDeviceId) {
1563
+ setSelectedAudioDeviceId(audioInputs[0].deviceId);
1564
+ }
1565
+ } catch (err) {
1566
+ console.error("\u274C Failed to enumerate devices:", err);
1567
+ setHasMultipleCameras(false);
1568
+ }
1569
+ };
1570
+ react.useEffect(() => {
1571
+ streamerRef.current = streamer;
1572
+ }, [streamer]);
1573
+ react.useEffect(() => {
1574
+ mediaStreamRef.current = mediaStream;
1575
+ }, [mediaStream]);
1576
+ react.useEffect(() => {
1577
+ return () => {
1578
+ if (streamerRef.current) {
1579
+ streamerRef.current.stop();
1580
+ }
1581
+ if (mediaStreamRef.current) {
1582
+ mediaStreamRef.current.getTracks().forEach((track) => track.stop());
1583
+ }
1584
+ };
1585
+ }, []);
1586
+ react.useEffect(() => {
1587
+ if (state === "live") {
1588
+ const handleBeforeUnload = (e) => {
1589
+ e.preventDefault();
1590
+ e.returnValue = "You are currently streaming. Are you sure you want to leave?";
1591
+ return e.returnValue;
1592
+ };
1593
+ window.addEventListener("beforeunload", handleBeforeUnload);
1594
+ return () => window.removeEventListener("beforeunload", handleBeforeUnload);
1595
+ }
1596
+ }, [state]);
1597
+ react.useEffect(() => {
1598
+ if (videoRef.current && mediaStream) {
1599
+ videoRef.current.srcObject = mediaStream;
1600
+ }
1601
+ }, [mediaStream]);
1602
+ const requestMediaPermissions = async () => {
1603
+ if (!streamKey) return;
1604
+ try {
1605
+ setState("requesting");
1606
+ setError(null);
1607
+ const constraints = getMediaConstraints({
1608
+ isVideo: isVideoKey && isVideoEnabled,
1609
+ facingMode
1610
+ });
1611
+ console.log("\u{1F4F8} Requesting media permissions:", constraints);
1612
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
1613
+ console.log("\u2705 Media permissions granted");
1614
+ setMediaStream(stream);
1615
+ setState("previewing");
1616
+ } catch (err) {
1617
+ console.error("\u274C Media permission error:", err);
1618
+ if (err instanceof Error) {
1619
+ if (err.name === "NotAllowedError" || err.name === "PermissionDeniedError") {
1620
+ setError("Camera/microphone access denied. Please enable permissions in your browser settings.");
1621
+ } else if (err.name === "NotFoundError") {
1622
+ setError(isVideoKey ? "No camera or microphone found." : "No microphone found.");
1623
+ } else {
1624
+ setError(err.message || "Failed to access media devices");
1625
+ }
1626
+ } else {
1627
+ setError("Failed to access media devices");
1628
+ }
1629
+ setState("error");
1630
+ }
1631
+ };
1632
+ const handleStartStreaming = async () => {
1633
+ if (!mediaStream || !streamKey) {
1634
+ setError("No media stream available");
1635
+ return;
1636
+ }
1637
+ try {
1638
+ setState("connecting");
1639
+ setError(null);
1640
+ console.log("\u{1F50D} Checking broadcast availability...");
1641
+ const checkResponse = await fetch(`${apiBaseUrl}/broadcasts/check`, {
1642
+ method: "POST",
1643
+ headers: {
1644
+ "Content-Type": "application/json",
1645
+ "Authorization": `Bearer ${sessionToken}`
1646
+ },
1647
+ body: JSON.stringify({
1648
+ streamKey
1649
+ })
1650
+ });
1651
+ if (!checkResponse.ok) {
1652
+ if (checkResponse.status === 401) {
1653
+ console.error("\u274C Session token invalid or expired");
1654
+ setError("Session expired. Please refresh the page and try again.");
1655
+ setState("error");
1656
+ dialTribeContext?.markExpired();
1657
+ return;
1658
+ }
1659
+ if (checkResponse.status === 403) {
1660
+ const data = await checkResponse.json().catch(() => ({}));
1661
+ console.error("\u274C Permission denied:", data);
1662
+ setError(
1663
+ data.error || "Streaming is not enabled for this app. Please upgrade your plan or contact support."
1664
+ );
1665
+ setState("error");
1666
+ return;
1667
+ }
1668
+ if (checkResponse.status === 409) {
1669
+ const data = await checkResponse.json();
1670
+ console.log("\u274C Broadcast conflict:", data);
1671
+ setError(
1672
+ data.error || "A broadcast is already active for this stream key. Please terminate it before starting a new one."
1673
+ );
1674
+ setState("error");
1675
+ return;
1676
+ }
1677
+ const errorData = await checkResponse.json().catch(() => ({ error: checkResponse.statusText }));
1678
+ console.error("\u274C Check failed:", checkResponse.status, errorData);
1679
+ setError(errorData.error || `Failed to check broadcast availability: ${checkResponse.statusText}`);
1680
+ setState("error");
1681
+ return;
1682
+ }
1683
+ const checkData = await checkResponse.json();
1684
+ console.log("\u2705 Broadcast check passed:", checkData);
1685
+ const newStreamer = new WebSocketStreamer({
1686
+ streamKey,
1687
+ mediaStream,
1688
+ isVideo: isVideoKey && isVideoEnabled,
1689
+ encoderServerUrl,
1690
+ onBytesUpdate: setBytesSent,
1691
+ onStateChange: (streamerState) => {
1692
+ if (streamerState === "live") {
1693
+ setState("live");
1694
+ setStartTime(/* @__PURE__ */ new Date());
1695
+ } else if (streamerState === "terminated") {
1696
+ setState("terminated");
1697
+ setStartTime(null);
1698
+ } else if (streamerState === "error") {
1699
+ setState("error");
1700
+ }
1701
+ },
1702
+ onError: (errorMsg) => {
1703
+ setError(errorMsg);
1704
+ setState("error");
1705
+ }
1706
+ });
1707
+ await newStreamer.start();
1708
+ setStreamer(newStreamer);
1709
+ } catch (err) {
1710
+ console.error("\u274C Failed to start streaming:", err);
1711
+ setError(err instanceof Error ? err.message : "Failed to start streaming");
1712
+ setState("previewing");
1713
+ }
1714
+ };
1715
+ const handleStopStreaming = () => {
1716
+ setShowStopConfirm(true);
1717
+ };
1718
+ const confirmStopStreaming = () => {
1719
+ setState("stopping");
1720
+ setShowStopConfirm(false);
1721
+ if (streamer) {
1722
+ streamer.stop();
1723
+ }
1724
+ if (mediaStream) {
1725
+ mediaStream.getTracks().forEach((track) => track.stop());
1726
+ }
1727
+ setState("stopped");
1728
+ setStartTime(null);
1729
+ };
1730
+ const handleToggleMute = () => {
1731
+ if (mediaStream) {
1732
+ const audioTracks = mediaStream.getAudioTracks();
1733
+ audioTracks.forEach((track) => {
1734
+ track.enabled = !track.enabled;
1735
+ });
1736
+ setIsMuted(!isMuted);
1737
+ }
1738
+ };
1739
+ const handleToggleVideo = () => {
1740
+ if (!isVideoKey) return;
1741
+ if (mediaStream) {
1742
+ const videoTracks = mediaStream.getVideoTracks();
1743
+ videoTracks.forEach((track) => {
1744
+ track.enabled = !track.enabled;
1745
+ });
1746
+ setIsVideoEnabled(!isVideoEnabled);
1747
+ }
1748
+ };
1749
+ const handleFlipCamera = async () => {
1750
+ if (!isVideoKey || !hasMultipleCameras) return;
1751
+ const newFacingMode = facingMode === "user" ? "environment" : "user";
1752
+ setFacingMode(newFacingMode);
1753
+ try {
1754
+ const constraints = getMediaConstraints({
1755
+ isVideo: true,
1756
+ facingMode: newFacingMode
1757
+ });
1758
+ const newStream = await navigator.mediaDevices.getUserMedia(constraints);
1759
+ console.log("\u{1F4F7} Camera flipped to:", newFacingMode);
1760
+ if (state === "live" && streamer) {
1761
+ console.log("\u{1F504} Hot-swapping media stream during live broadcast");
1762
+ try {
1763
+ await streamer.updateMediaStream(newStream);
1764
+ if (mediaStream) {
1765
+ mediaStream.getTracks().forEach((track) => track.stop());
1766
+ }
1767
+ setMediaStream(newStream);
1768
+ console.log("\u2705 Camera flipped successfully - broadcast continues");
1769
+ } catch (swapErr) {
1770
+ console.error("\u274C Failed to hot-swap stream:", swapErr);
1771
+ setFacingMode(facingMode);
1772
+ newStream.getTracks().forEach((track) => track.stop());
1773
+ console.warn("\u26A0\uFE0F Keeping original camera - hot-swap failed but broadcast continues");
1774
+ }
1775
+ } else {
1776
+ if (mediaStream) {
1777
+ mediaStream.getTracks().forEach((track) => track.stop());
1778
+ }
1779
+ setMediaStream(newStream);
1780
+ }
1781
+ } catch (err) {
1782
+ console.error("\u274C Failed to get new camera stream:", err);
1783
+ setFacingMode(facingMode);
1784
+ console.warn("\u26A0\uFE0F Camera flip not available - this device may only have one camera");
1785
+ }
1786
+ };
1787
+ const handleRetry = () => {
1788
+ setError(null);
1789
+ setState("idle");
1790
+ requestMediaPermissions();
1791
+ };
1792
+ const handleDone = () => {
1793
+ onDone?.();
1794
+ };
1795
+ const handleClose = () => {
1796
+ if (state === "live") {
1797
+ setShowCloseConfirm(true);
1798
+ } else {
1799
+ if (mediaStream) {
1800
+ mediaStream.getTracks().forEach((track) => track.stop());
1801
+ }
1802
+ onDone?.();
1803
+ }
1804
+ };
1805
+ const confirmClose = () => {
1806
+ setShowCloseConfirm(false);
1807
+ if (streamer) {
1808
+ streamer.stop();
1809
+ }
1810
+ if (mediaStream) {
1811
+ mediaStream.getTracks().forEach((track) => track.stop());
1812
+ }
1813
+ onDone?.();
1814
+ };
1815
+ const handleVideoDeviceChange = async (deviceId) => {
1816
+ if (deviceId === selectedVideoDeviceId) return;
1817
+ setSelectedVideoDeviceId(deviceId);
1818
+ if (state !== "previewing" && state !== "live") return;
1819
+ try {
1820
+ const constraints = {
1821
+ video: { deviceId: { exact: deviceId } },
1822
+ audio: selectedAudioDeviceId ? { deviceId: { exact: selectedAudioDeviceId } } : true
1823
+ };
1824
+ const newStream = await navigator.mediaDevices.getUserMedia(constraints);
1825
+ console.log(`\u{1F4F7} Switched to camera: ${deviceId}`);
1826
+ if (state === "live" && streamer) {
1827
+ try {
1828
+ await streamer.updateMediaStream(newStream);
1829
+ if (mediaStream) {
1830
+ mediaStream.getTracks().forEach((track) => track.stop());
1831
+ }
1832
+ setMediaStream(newStream);
1833
+ } catch (swapErr) {
1834
+ console.error("\u274C Failed to hot-swap video device:", swapErr);
1835
+ newStream.getTracks().forEach((track) => track.stop());
1836
+ }
1837
+ } else {
1838
+ if (mediaStream) {
1839
+ mediaStream.getTracks().forEach((track) => track.stop());
1840
+ }
1841
+ setMediaStream(newStream);
1842
+ }
1843
+ } catch (err) {
1844
+ console.error("\u274C Failed to switch video device:", err);
1845
+ }
1846
+ };
1847
+ const handleAudioDeviceChange = async (deviceId) => {
1848
+ if (deviceId === selectedAudioDeviceId) return;
1849
+ setSelectedAudioDeviceId(deviceId);
1850
+ if (state !== "previewing" && state !== "live") return;
1851
+ try {
1852
+ const constraints = {
1853
+ video: isVideoKey && isVideoEnabled && selectedVideoDeviceId ? { deviceId: { exact: selectedVideoDeviceId } } : isVideoKey && isVideoEnabled,
1854
+ audio: { deviceId: { exact: deviceId } }
1855
+ };
1856
+ const newStream = await navigator.mediaDevices.getUserMedia(constraints);
1857
+ console.log(`\u{1F3A4} Switched to microphone: ${deviceId}`);
1858
+ if (state === "live" && streamer) {
1859
+ try {
1860
+ await streamer.updateMediaStream(newStream);
1861
+ if (mediaStream) {
1862
+ mediaStream.getTracks().forEach((track) => track.stop());
1863
+ }
1864
+ setMediaStream(newStream);
1865
+ } catch (swapErr) {
1866
+ console.error("\u274C Failed to hot-swap audio device:", swapErr);
1867
+ newStream.getTracks().forEach((track) => track.stop());
1868
+ }
1869
+ } else {
1870
+ if (mediaStream) {
1871
+ mediaStream.getTracks().forEach((track) => track.stop());
1872
+ }
1873
+ setMediaStream(newStream);
1874
+ }
1875
+ } catch (err) {
1876
+ console.error("\u274C Failed to switch audio device:", err);
1877
+ }
1878
+ };
1879
+ if (!sessionToken) {
1880
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: centeredContainerClass, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-lg border border-gray-200 dark:border-zinc-800 p-8 max-w-md w-full text-center", children: [
1881
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-16 h-16 bg-red-100 dark:bg-red-900/20 rounded-full flex items-center justify-center mx-auto mb-4", children: /* @__PURE__ */ jsxRuntime.jsx(
1882
+ "svg",
1883
+ {
1884
+ className: "w-8 h-8 text-red-600 dark:text-red-400",
1885
+ fill: "none",
1886
+ stroke: "currentColor",
1887
+ viewBox: "0 0 24 24",
1888
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1889
+ "path",
1890
+ {
1891
+ strokeLinecap: "round",
1892
+ strokeLinejoin: "round",
1893
+ strokeWidth: 2,
1894
+ d: "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
1895
+ }
1896
+ )
1897
+ }
1898
+ ) }),
1899
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-xl font-bold text-black dark:text-white mb-2", children: "Authentication Required" }),
1900
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600 dark:text-gray-400 mb-4", children: "A session token is required to use the broadcast streamer." }),
1901
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-500 mb-6", children: "Wrap your app with DialTribeProvider or pass a sessionToken prop." }),
1902
+ /* @__PURE__ */ jsxRuntime.jsx(
1903
+ "button",
1904
+ {
1905
+ onClick: handleDone,
1906
+ className: "w-full px-6 py-2 bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-black dark:text-white font-medium rounded-lg transition-colors",
1907
+ children: "Close"
1908
+ }
1909
+ )
1910
+ ] }) });
1911
+ }
1912
+ if (!streamKey) {
1913
+ return /* @__PURE__ */ jsxRuntime.jsx(StreamKeyInput, { onSubmit: handleStreamKeySubmit, inline });
1914
+ }
1915
+ if (state === "error") {
1916
+ const isBroadcastConflict = error?.includes("already active") || error?.includes("terminate it before");
1917
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: centeredContainerClass, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-lg border border-gray-200 dark:border-zinc-800 p-8 max-w-md w-full text-center", children: [
1918
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-16 h-16 bg-red-100 dark:bg-red-900/20 rounded-full flex items-center justify-center mx-auto mb-4", children: /* @__PURE__ */ jsxRuntime.jsx(
1919
+ "svg",
1920
+ {
1921
+ className: "w-8 h-8 text-red-600 dark:text-red-400",
1922
+ fill: "none",
1923
+ stroke: "currentColor",
1924
+ viewBox: "0 0 24 24",
1925
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1926
+ "path",
1927
+ {
1928
+ strokeLinecap: "round",
1929
+ strokeLinejoin: "round",
1930
+ strokeWidth: 2,
1931
+ d: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
1932
+ }
1933
+ )
1934
+ }
1935
+ ) }),
1936
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-xl font-bold text-black dark:text-white mb-2", children: isBroadcastConflict ? "Broadcast Already Active" : "Error" }),
1937
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600 dark:text-gray-400 mb-6", children: error }),
1938
+ isBroadcastConflict && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 mb-6", children: "You need to terminate the existing broadcast before starting a new one." }),
1939
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2", children: [
1940
+ !isBroadcastConflict && /* @__PURE__ */ jsxRuntime.jsx(
1941
+ "button",
1942
+ {
1943
+ onClick: handleRetry,
1944
+ className: "w-full px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors",
1945
+ children: "Retry"
1946
+ }
1947
+ ),
1948
+ /* @__PURE__ */ jsxRuntime.jsx(
1949
+ "button",
1950
+ {
1951
+ onClick: handleDone,
1952
+ className: "w-full px-6 py-2 bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-black dark:text-white font-medium rounded-lg transition-colors",
1953
+ children: "Close"
1954
+ }
1955
+ )
1956
+ ] })
1957
+ ] }) });
1958
+ }
1959
+ if (state === "stopped") {
1960
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: centeredContainerClass, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-lg border border-gray-200 dark:border-zinc-800 p-8 max-w-md w-full text-center", children: [
1961
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-16 h-16 bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center mx-auto mb-4", children: /* @__PURE__ */ jsxRuntime.jsx(
1962
+ "svg",
1963
+ {
1964
+ className: "w-8 h-8 text-green-600 dark:text-green-400",
1965
+ fill: "none",
1966
+ stroke: "currentColor",
1967
+ viewBox: "0 0 24 24",
1968
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1969
+ "path",
1970
+ {
1971
+ strokeLinecap: "round",
1972
+ strokeLinejoin: "round",
1973
+ strokeWidth: 2,
1974
+ d: "M5 13l4 4L19 7"
1975
+ }
1976
+ )
1977
+ }
1978
+ ) }),
1979
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-xl font-bold text-black dark:text-white mb-2", children: "Stream Ended" }),
1980
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600 dark:text-gray-400 mb-2", children: "Your broadcast has ended successfully." }),
1981
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-gray-500 dark:text-gray-400 mb-6", children: [
1982
+ "Total sent: ",
1983
+ (bytesSent / 1024 / 1024).toFixed(2),
1984
+ " MB"
1985
+ ] }),
1986
+ /* @__PURE__ */ jsxRuntime.jsx(
1987
+ "button",
1988
+ {
1989
+ onClick: handleDone,
1990
+ className: "w-full px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors",
1991
+ children: "Done"
1992
+ }
1993
+ )
1994
+ ] }) });
1995
+ }
1996
+ if (state === "terminated") {
1997
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: centeredContainerClass, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-lg border border-gray-200 dark:border-zinc-800 p-8 max-w-md w-full text-center", children: [
1998
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-16 h-16 bg-orange-100 dark:bg-orange-900/20 rounded-full flex items-center justify-center mx-auto mb-4", children: /* @__PURE__ */ jsxRuntime.jsx(
1999
+ "svg",
2000
+ {
2001
+ className: "w-8 h-8 text-orange-600 dark:text-orange-400",
2002
+ fill: "none",
2003
+ stroke: "currentColor",
2004
+ viewBox: "0 0 24 24",
2005
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2006
+ "path",
2007
+ {
2008
+ strokeLinecap: "round",
2009
+ strokeLinejoin: "round",
2010
+ strokeWidth: 2,
2011
+ d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
2012
+ }
2013
+ )
2014
+ }
2015
+ ) }),
2016
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-xl font-bold text-black dark:text-white mb-2", children: "Stream Terminated" }),
2017
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600 dark:text-gray-400 mb-2", children: error || "Your broadcast was terminated by an administrator." }),
2018
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-gray-500 dark:text-gray-400 mb-6", children: [
2019
+ "Total sent: ",
2020
+ (bytesSent / 1024 / 1024).toFixed(2),
2021
+ " MB"
2022
+ ] }),
2023
+ /* @__PURE__ */ jsxRuntime.jsx(
2024
+ "button",
2025
+ {
2026
+ onClick: handleDone,
2027
+ className: "w-full px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors",
2028
+ children: "Done"
2029
+ }
2030
+ )
2031
+ ] }) });
2032
+ }
2033
+ if (state === "idle" || state === "requesting") {
2034
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: centeredContainerClass, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
2035
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-16 h-16 border-4 border-gray-700 border-t-blue-600 rounded-full animate-spin mx-auto mb-4" }),
2036
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-white text-lg", children: state === "idle" ? "Initializing..." : "Requesting permissions..." })
2037
+ ] }) });
2038
+ }
2039
+ const controlState = state;
2040
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${containerClass} flex flex-col`, children: [
2041
+ /* @__PURE__ */ jsxRuntime.jsx(
2042
+ StreamingPreview,
2043
+ {
2044
+ videoRef,
2045
+ isVideoKey,
2046
+ isVideoEnabled,
2047
+ mediaStream,
2048
+ facingMode
2049
+ }
2050
+ ),
2051
+ /* @__PURE__ */ jsxRuntime.jsx(
2052
+ StreamingControls,
2053
+ {
2054
+ state: controlState,
2055
+ isVideoKey,
2056
+ isMuted,
2057
+ isVideoEnabled,
2058
+ facingMode,
2059
+ hasMultipleCameras,
2060
+ startTime,
2061
+ bytesSent,
2062
+ showStopConfirm,
2063
+ streamKey,
2064
+ onStreamKeyChange: handleStreamKeyChange,
2065
+ onStart: handleStartStreaming,
2066
+ onStop: handleStopStreaming,
2067
+ onConfirmStop: confirmStopStreaming,
2068
+ onCancelStop: () => setShowStopConfirm(false),
2069
+ onToggleMute: handleToggleMute,
2070
+ onToggleVideo: handleToggleVideo,
2071
+ onFlipCamera: handleFlipCamera,
2072
+ onClose: inline ? void 0 : handleClose,
2073
+ showCloseConfirm,
2074
+ onConfirmClose: confirmClose,
2075
+ onCancelClose: () => setShowCloseConfirm(false),
2076
+ videoDevices,
2077
+ audioDevices,
2078
+ selectedVideoDeviceId,
2079
+ selectedAudioDeviceId,
2080
+ onVideoDeviceChange: handleVideoDeviceChange,
2081
+ onAudioDeviceChange: handleAudioDeviceChange,
2082
+ mediaStream
2083
+ }
2084
+ )
2085
+ ] });
2086
+ }
2087
+ var sizeClasses = {
2088
+ sm: "h-4 w-4 border-2",
2089
+ md: "h-8 w-8 border-4",
2090
+ lg: "h-12 w-12 border-4"
2091
+ };
2092
+ var variantClasses = {
2093
+ default: "border-gray-200 dark:border-gray-800 border-t-black dark:border-t-white",
2094
+ primary: "border-gray-300 dark:border-gray-700 border-t-blue-600 dark:border-t-blue-400",
2095
+ white: "border-gray-600 border-t-white"
2096
+ };
2097
+ function LoadingSpinner({
2098
+ text,
2099
+ size = "md",
2100
+ variant = "default"
2101
+ }) {
2102
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center gap-3", children: [
2103
+ /* @__PURE__ */ jsxRuntime.jsx(
2104
+ "div",
2105
+ {
2106
+ className: `${sizeClasses[size]} ${variantClasses[variant]} rounded-full animate-spin`,
2107
+ role: "status",
2108
+ "aria-label": "Loading"
2109
+ }
2110
+ ),
2111
+ text && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: text })
2112
+ ] });
2113
+ }
2114
+
2115
+ // src/utils/cdn.ts
2116
+ var CDN_DOMAIN = typeof process !== "undefined" && process.env?.CONTENT_CDN_DOMAIN ? process.env.CONTENT_CDN_DOMAIN : "cdn.dialtribe.com";
2117
+ function shardHash(hash) {
2118
+ return hash.toLowerCase().split("").join("/");
2119
+ }
2120
+ function buildBroadcastS3KeyPrefix(appHash, broadcastHash) {
2121
+ return `a/${shardHash(appHash)}/b/${shardHash(broadcastHash)}/`;
2122
+ }
2123
+ function buildBroadcastCdnUrl(appHash, broadcastHash, filename) {
2124
+ const keyPrefix = buildBroadcastS3KeyPrefix(appHash, broadcastHash);
2125
+ return `https://${CDN_DOMAIN}/${keyPrefix}${filename}`;
2126
+ }
2127
+
2128
+ // src/utils/format.ts
2129
+ function formatTime(seconds, includeHours) {
2130
+ if (isNaN(seconds) || !isFinite(seconds) || seconds < 0) {
2131
+ return "0:00";
2132
+ }
2133
+ const hrs = Math.floor(seconds / 3600);
2134
+ const mins = Math.floor(seconds % 3600 / 60);
2135
+ const secs = Math.floor(seconds % 60);
2136
+ const secStr = secs.toString().padStart(2, "0");
2137
+ const minStr = mins.toString().padStart(2, "0");
2138
+ if (hrs > 0 || includeHours) {
2139
+ return `${hrs}:${minStr}:${secStr}`;
2140
+ }
2141
+ return `${mins}:${secStr}`;
2142
+ }
2143
+
2144
+ // src/utils/http-status.ts
2145
+ var HTTP_STATUS = {
2146
+ // 2xx Success
2147
+ OK: 200,
2148
+ CREATED: 201,
2149
+ ACCEPTED: 202,
2150
+ NO_CONTENT: 204,
2151
+ // 3xx Redirection
2152
+ MOVED_PERMANENTLY: 301,
2153
+ FOUND: 302,
2154
+ SEE_OTHER: 303,
2155
+ NOT_MODIFIED: 304,
2156
+ TEMPORARY_REDIRECT: 307,
2157
+ PERMANENT_REDIRECT: 308,
2158
+ // 4xx Client Errors
2159
+ BAD_REQUEST: 400,
2160
+ UNAUTHORIZED: 401,
2161
+ PAYMENT_REQUIRED: 402,
2162
+ FORBIDDEN: 403,
2163
+ NOT_FOUND: 404,
2164
+ METHOD_NOT_ALLOWED: 405,
2165
+ NOT_ACCEPTABLE: 406,
2166
+ CONFLICT: 409,
2167
+ GONE: 410,
2168
+ PAYLOAD_TOO_LARGE: 413,
2169
+ UNSUPPORTED_MEDIA_TYPE: 415,
2170
+ UNPROCESSABLE_ENTITY: 422,
2171
+ TOO_MANY_REQUESTS: 429,
2172
+ // 5xx Server Errors
2173
+ INTERNAL_SERVER_ERROR: 500,
2174
+ NOT_IMPLEMENTED: 501,
2175
+ BAD_GATEWAY: 502,
2176
+ SERVICE_UNAVAILABLE: 503,
2177
+ GATEWAY_TIMEOUT: 504
2178
+ };
2179
+
2180
+ // src/utils/debug.ts
2181
+ var DEBUG = process.env.NODE_ENV === "development";
2182
+ var debug = {
2183
+ log: (...args) => {
2184
+ if (DEBUG) console.log(...args);
2185
+ },
2186
+ warn: (...args) => {
2187
+ if (DEBUG) console.warn(...args);
2188
+ },
2189
+ error: (...args) => {
2190
+ if (DEBUG) console.error(...args);
2191
+ }
2192
+ };
2193
+ var URL_EXPIRATION_MS = 6 * 60 * 60 * 1e3;
2194
+ var REFRESH_THRESHOLD_MS = 5 * 60 * 1e3;
2195
+ var REFRESH_CHECK_INTERVAL_MS = 6e4;
2196
+ var PLAYBACK_RESUME_DELAY_MS = 500;
2197
+ var TRAILING_WORDS = 3;
2198
+ function buildPlaybackUrl(broadcastId, hash, action) {
2199
+ const searchParams = new URLSearchParams({
2200
+ broadcastId: broadcastId.toString(),
2201
+ hash
2202
+ });
2203
+ if (action) {
2204
+ searchParams.set("action", action);
2205
+ }
2206
+ return `${ENDPOINTS.contentPlay}?${searchParams}`;
2207
+ }
2208
+ function getErrorMessage(error) {
2209
+ if (!error) return "Unable to play media. Please try again.";
2210
+ const errorMsg = error instanceof Error ? error.message : String(error);
2211
+ const errorCode = error?.code;
2212
+ const errorStatus = error?.status || error?.statusCode;
2213
+ if (errorMsg.toLowerCase().includes("network") || errorMsg.includes("NetworkError")) {
2214
+ return "No internet connection detected. Please check your network and try again.";
2215
+ }
2216
+ if (errorStatus === 401 || errorMsg.includes("401") || errorMsg.includes("Unauthorized")) {
2217
+ return "Session expired. Please refresh the page and log in again.";
2218
+ }
2219
+ if (errorStatus === 403 || errorMsg.includes("403") || errorMsg.includes("Forbidden")) {
2220
+ return "Access denied. You may not have permission to view this content.";
2221
+ }
2222
+ if (errorStatus === 404 || errorMsg.includes("404") || errorMsg.includes("not found")) {
2223
+ return "Media file not found. It may have been deleted or is still processing.";
2224
+ }
2225
+ if (errorMsg.includes("no supported sources") || errorMsg.includes("NotSupportedError")) {
2226
+ return "This media format is not supported by your browser. Try using Chrome, Firefox, or Safari.";
2227
+ }
2228
+ if (errorMsg.includes("MEDIA_ERR_SRC_NOT_SUPPORTED") || errorCode === 4) {
2229
+ return "Media file is not available or the format is unsupported.";
2230
+ }
2231
+ if (errorMsg.includes("MEDIA_ERR_NETWORK") || errorCode === 2) {
2232
+ return "Network error while loading media. Please check your connection.";
2233
+ }
2234
+ if (errorMsg.includes("MEDIA_ERR_DECODE") || errorCode === 3) {
2235
+ return "Media file is corrupted or cannot be decoded. Please contact support.";
2236
+ }
2237
+ if (errorMsg.includes("AbortError")) {
2238
+ return "Media loading was interrupted. Please try again.";
2239
+ }
2240
+ return "Unable to play media. Please try refreshing the page or contact support if the problem persists.";
2241
+ }
2242
+ function BroadcastPlayer({
2243
+ broadcast,
2244
+ appId,
2245
+ contentId,
2246
+ foreignId,
2247
+ foreignTier = "guest",
2248
+ renderClipCreator,
2249
+ onError,
2250
+ className = "",
2251
+ enableKeyboardShortcuts = false
2252
+ }) {
2253
+ const { sessionToken, setSessionToken, markExpired, apiBaseUrl } = useDialTribe();
2254
+ const clientRef = react.useRef(null);
2255
+ if (!clientRef.current && sessionToken) {
2256
+ clientRef.current = new DialTribeClient({
2257
+ sessionToken,
2258
+ apiBaseUrl,
2259
+ onTokenRefresh: (newToken, expiresAt) => {
2260
+ debug.log(`[DialTribeClient] Token refreshed, expires at ${expiresAt}`);
2261
+ setSessionToken(newToken, expiresAt);
2262
+ },
2263
+ onTokenExpired: () => {
2264
+ debug.error("[DialTribeClient] Token expired");
2265
+ markExpired();
2266
+ }
2267
+ });
2268
+ } else if (clientRef.current && sessionToken) {
2269
+ clientRef.current.setSessionToken(sessionToken);
2270
+ }
2271
+ const client = clientRef.current;
2272
+ const playerRef = react.useRef(null);
2273
+ const transcriptContainerRef = react.useRef(null);
2274
+ const activeWordRef = react.useRef(null);
2275
+ const [audioElement, setAudioElement] = react.useState(null);
2276
+ const [playing, setPlaying] = react.useState(false);
2277
+ const [played, setPlayed] = react.useState(0);
2278
+ const [duration, setDuration] = react.useState(0);
2279
+ const [volume, setVolume] = react.useState(1);
2280
+ const [muted, setMuted] = react.useState(false);
2281
+ const [seeking, setSeeking] = react.useState(false);
2282
+ const [hasError, setHasError] = react.useState(false);
2283
+ const [errorMessage, setErrorMessage] = react.useState("");
2284
+ const [hasEnded, setHasEnded] = react.useState(false);
2285
+ const [hasStreamEnded, setHasStreamEnded] = react.useState(false);
2286
+ const [showTranscript, setShowTranscript] = react.useState(false);
2287
+ const [transcriptData, setTranscriptData] = react.useState(null);
2288
+ const [currentTime, setCurrentTime] = react.useState(0);
2289
+ const [isLoadingTranscript, setIsLoadingTranscript] = react.useState(false);
2290
+ const [isLoadingVideo, setIsLoadingVideo] = react.useState(true);
2291
+ const [autoScrollEnabled, setAutoScrollEnabled] = react.useState(true);
2292
+ const isScrollingProgrammatically = react.useRef(false);
2293
+ const lastActiveWordIndex = react.useRef(-1);
2294
+ const [showClipCreator, setShowClipCreator] = react.useState(false);
2295
+ const initialPlaybackTypeRef = react.useRef(null);
2296
+ const [currentPlaybackInfo, setCurrentPlaybackInfo] = react.useState(null);
2297
+ const [urlExpiresAt, setUrlExpiresAt] = react.useState(null);
2298
+ const isRefreshingUrl = react.useRef(false);
2299
+ const [audienceId, setAudienceId] = react.useState(null);
2300
+ const [sessionId] = react.useState(() => {
2301
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
2302
+ return crypto.randomUUID();
2303
+ }
2304
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
2305
+ const r = Math.random() * 16 | 0;
2306
+ const v = c === "x" ? r : r & 3 | 8;
2307
+ return v.toString(16);
2308
+ });
2309
+ });
2310
+ const heartbeatIntervalRef = react.useRef(null);
2311
+ const hasInitializedSession = react.useRef(false);
2312
+ const refreshPresignedUrl = react.useCallback(
2313
+ async (fileType) => {
2314
+ if (!broadcast.hash || isRefreshingUrl.current || !client) {
2315
+ debug.log("[URL Refresh] Skipping refresh - no hash, already refreshing, or no client");
2316
+ return false;
2317
+ }
2318
+ if (fileType === "hls") {
2319
+ debug.log("[URL Refresh] HLS does not need URL refresh");
2320
+ return false;
2321
+ }
2322
+ isRefreshingUrl.current = true;
2323
+ debug.log(`[URL Refresh] Refreshing ${fileType} URL for broadcast ${broadcast.id}`);
2324
+ try {
2325
+ const data = await client.refreshPresignedUrl({
2326
+ broadcastId: broadcast.id,
2327
+ hash: broadcast.hash,
2328
+ fileType
2329
+ });
2330
+ debug.log(`[URL Refresh] Successfully refreshed URL, expires at ${data.expiresAt}`);
2331
+ setCurrentPlaybackInfo({ url: data.url, type: fileType });
2332
+ setUrlExpiresAt(new Date(data.expiresAt));
2333
+ if (errorMessage.includes("URL") || errorMessage.includes("session") || errorMessage.includes("refresh")) {
2334
+ setHasError(false);
2335
+ setErrorMessage("");
2336
+ }
2337
+ return true;
2338
+ } catch (error) {
2339
+ if (error instanceof Error && error.name === "AbortError") {
2340
+ debug.log("[URL Refresh] Request aborted");
2341
+ return false;
2342
+ }
2343
+ debug.error("[URL Refresh] Failed to refresh presigned URL:", error);
2344
+ setHasError(true);
2345
+ setErrorMessage("Unable to refresh media URL. The session may have expired.");
2346
+ if (onError && error instanceof Error) {
2347
+ onError(error);
2348
+ }
2349
+ return false;
2350
+ } finally {
2351
+ isRefreshingUrl.current = false;
2352
+ }
2353
+ },
2354
+ [broadcast.hash, broadcast.id, errorMessage, client, onError]
2355
+ );
2356
+ const getScreenSize = () => {
2357
+ if (typeof window === "undefined") return "unknown";
2358
+ const width = window.innerWidth;
2359
+ if (width < 768) return "mobile";
2360
+ if (width < 1024) return "tablet";
2361
+ return "desktop";
2362
+ };
2363
+ const initializeTrackingSession = react.useCallback(async () => {
2364
+ if (!contentId || !appId || !client) return;
2365
+ if (currentPlaybackInfo?.type === "hls" && broadcast.broadcastStatus === 1) return;
2366
+ if (hasInitializedSession.current) return;
2367
+ hasInitializedSession.current = true;
2368
+ try {
2369
+ const screenSize = getScreenSize();
2370
+ const platformInfo = `${navigator.platform || "Unknown"} (${screenSize})`;
2371
+ const data = await client.startSession({
2372
+ contentId,
2373
+ broadcastId: broadcast.id,
2374
+ appId,
2375
+ foreignId: foreignId || null,
2376
+ foreignTier: foreignTier || "guest",
2377
+ sessionId,
2378
+ fileType: currentPlaybackInfo?.type || "mp3",
2379
+ platform: platformInfo,
2380
+ userAgent: navigator.userAgent || null,
2381
+ origin: window.location.origin || null,
2382
+ country: null,
2383
+ region: null
2384
+ });
2385
+ setAudienceId(data.audienceId);
2386
+ if (data.resumePosition && data.resumePosition > 0 && audioElement) {
2387
+ audioElement.currentTime = data.resumePosition;
2388
+ debug.log(`[Audience Tracking] Resumed playback at ${data.resumePosition}s`);
2389
+ }
2390
+ debug.log("[Audience Tracking] Session initialized:", data.audienceId);
2391
+ } catch (error) {
2392
+ debug.error("[Audience Tracking] Error initializing session:", error);
2393
+ if (onError && error instanceof Error) {
2394
+ onError(error);
2395
+ }
2396
+ }
2397
+ }, [contentId, appId, broadcast.id, broadcast.broadcastStatus, foreignId, foreignTier, sessionId, currentPlaybackInfo?.type, audioElement, client, onError]);
2398
+ const sendTrackingPing = react.useCallback(
2399
+ async (eventType) => {
2400
+ if (!audienceId || !sessionId || !client) return;
2401
+ try {
2402
+ await client.sendSessionPing({
2403
+ audienceId,
2404
+ sessionId,
2405
+ eventType,
2406
+ currentTime: Math.floor(audioElement?.currentTime || 0),
2407
+ duration: Math.floor(duration || 0)
2408
+ });
2409
+ } catch (error) {
2410
+ debug.error("[Audience Tracking] Error sending ping:", error);
2411
+ }
2412
+ },
2413
+ [audienceId, sessionId, audioElement, duration, client]
2414
+ );
2415
+ const getPlaybackInfo = () => {
2416
+ if (broadcast.broadcastStatus === 1) {
2417
+ if (broadcast.hlsPlaylistUrl) {
2418
+ return { url: broadcast.hlsPlaylistUrl, type: "hls" };
2419
+ }
2420
+ if (broadcast.hash && broadcast.app?.s3Hash) {
2421
+ const hlsUrl = buildBroadcastCdnUrl(broadcast.app.s3Hash, broadcast.hash, "index.m3u8");
2422
+ return { url: hlsUrl, type: "hls" };
2423
+ }
2424
+ }
2425
+ if (broadcast.recordingMp4Url && broadcast.isVideo && broadcast.hash) {
2426
+ return { url: buildPlaybackUrl(broadcast.id, broadcast.hash), type: "mp4" };
2427
+ }
2428
+ if (broadcast.recordingMp3Url && broadcast.hash) {
2429
+ return { url: buildPlaybackUrl(broadcast.id, broadcast.hash), type: "mp3" };
2430
+ }
2431
+ if (broadcast.hlsPlaylistUrl) {
2432
+ return { url: broadcast.hlsPlaylistUrl, type: "hls" };
2433
+ }
2434
+ return null;
2435
+ };
2436
+ react.useEffect(() => {
2437
+ if (!currentPlaybackInfo) {
2438
+ const info = getPlaybackInfo();
2439
+ setCurrentPlaybackInfo(info);
2440
+ initialPlaybackTypeRef.current = info?.type || null;
2441
+ if (info && (info.type === "mp4" || info.type === "mp3")) {
2442
+ const expiresAt = new Date(Date.now() + URL_EXPIRATION_MS);
2443
+ setUrlExpiresAt(expiresAt);
2444
+ debug.log(`[URL Refresh] Initial ${info.type} URL expires at ${expiresAt.toISOString()}`);
2445
+ }
2446
+ if (info) {
2447
+ setPlaying(true);
2448
+ setIsLoadingVideo(true);
2449
+ }
2450
+ }
2451
+ }, [currentPlaybackInfo]);
2452
+ react.useEffect(() => {
2453
+ if (currentPlaybackInfo?.url) {
2454
+ setIsLoadingVideo(true);
2455
+ }
2456
+ }, [currentPlaybackInfo?.url]);
2457
+ react.useEffect(() => {
2458
+ if (!urlExpiresAt || !currentPlaybackInfo?.type) return;
2459
+ const checkExpiration = () => {
2460
+ const now = /* @__PURE__ */ new Date();
2461
+ const timeUntilExpiration = urlExpiresAt.getTime() - now.getTime();
2462
+ if (timeUntilExpiration <= REFRESH_THRESHOLD_MS && timeUntilExpiration > 0) {
2463
+ debug.log("[URL Refresh] Proactively refreshing URL before expiration");
2464
+ const fileType = currentPlaybackInfo.type;
2465
+ if (fileType === "mp4" || fileType === "mp3" || fileType === "hls") {
2466
+ refreshPresignedUrl(fileType);
2467
+ }
2468
+ }
2469
+ };
2470
+ const interval = setInterval(checkExpiration, REFRESH_CHECK_INTERVAL_MS);
2471
+ checkExpiration();
2472
+ return () => {
2473
+ clearInterval(interval);
2474
+ };
2475
+ }, [urlExpiresAt, currentPlaybackInfo?.type, refreshPresignedUrl]);
2476
+ react.useEffect(() => {
2477
+ if (initialPlaybackTypeRef.current === "hls" && currentPlaybackInfo?.type === "hls" && broadcast.broadcastStatus !== 1 && broadcast.recordingMp3Url && broadcast.hash && parseInt(broadcast.mp3Size || "0") > 0) {
2478
+ const secureUrl = buildPlaybackUrl(broadcast.id, broadcast.hash);
2479
+ setCurrentPlaybackInfo({ url: secureUrl, type: "mp3" });
2480
+ setAudioElement(null);
2481
+ setPlaying(true);
2482
+ }
2483
+ }, [broadcast.broadcastStatus, broadcast.recordingMp3Url, broadcast.mp3Size, broadcast.hash, broadcast.id, currentPlaybackInfo]);
2484
+ const playbackUrl = currentPlaybackInfo?.url || null;
2485
+ const playbackType = currentPlaybackInfo?.type || null;
2486
+ const isAudioOnly = playbackType === "mp3" || !broadcast.isVideo && playbackType !== "mp4";
2487
+ const isLiveStream = broadcast.broadcastStatus === 1 && playbackType === "hls" && !hasStreamEnded;
2488
+ const wasLiveStream = initialPlaybackTypeRef.current === "hls";
2489
+ const formatTimestamp = (seconds) => {
2490
+ if (!seconds || isNaN(seconds) || !isFinite(seconds)) return "00:00:00";
2491
+ const hrs = Math.floor(seconds / 3600);
2492
+ const mins = Math.floor(seconds % 3600 / 60);
2493
+ const secs = Math.floor(seconds % 60);
2494
+ return `${hrs.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
2495
+ };
2496
+ const handlePlay = () => {
2497
+ setPlaying(true);
2498
+ setIsLoadingVideo(false);
2499
+ };
2500
+ const handlePause = () => {
2501
+ setPlaying(false);
2502
+ };
2503
+ const handleEnded = () => {
2504
+ setPlaying(false);
2505
+ if (playbackType === "hls") {
2506
+ setHasStreamEnded(true);
2507
+ }
2508
+ if (!wasLiveStream) {
2509
+ setHasEnded(true);
2510
+ }
2511
+ };
2512
+ react.useEffect(() => {
2513
+ if (broadcast.durationSeconds && broadcast.durationSeconds > 0) {
2514
+ setDuration(broadcast.durationSeconds);
2515
+ }
2516
+ }, [broadcast.durationSeconds]);
2517
+ react.useEffect(() => {
2518
+ if (isLiveStream && !playing) {
2519
+ setPlaying(true);
2520
+ }
2521
+ }, [isLiveStream, playing]);
2522
+ react.useEffect(() => {
2523
+ if (currentPlaybackInfo && audioElement && !hasInitializedSession.current) {
2524
+ initializeTrackingSession();
2525
+ }
2526
+ }, [currentPlaybackInfo, audioElement, initializeTrackingSession]);
2527
+ react.useEffect(() => {
2528
+ if (playing && audienceId) {
2529
+ sendTrackingPing(1);
2530
+ heartbeatIntervalRef.current = setInterval(() => {
2531
+ sendTrackingPing(2);
2532
+ }, 15e3);
2533
+ return () => {
2534
+ if (heartbeatIntervalRef.current) {
2535
+ clearInterval(heartbeatIntervalRef.current);
2536
+ heartbeatIntervalRef.current = null;
2537
+ }
2538
+ };
2539
+ } else if (!playing && audienceId) {
2540
+ sendTrackingPing(0);
2541
+ if (heartbeatIntervalRef.current) {
2542
+ clearInterval(heartbeatIntervalRef.current);
2543
+ heartbeatIntervalRef.current = null;
2544
+ }
2545
+ }
2546
+ }, [playing, audienceId, sendTrackingPing]);
2547
+ react.useEffect(() => {
2548
+ return () => {
2549
+ if (audienceId && sessionId && sessionToken) {
2550
+ const payload = {
2551
+ audienceId,
2552
+ sessionId,
2553
+ eventType: 3,
2554
+ // UNMOUNT
2555
+ currentTime: Math.floor(audioElement?.currentTime || 0),
2556
+ duration: Math.floor(duration || 0)
2557
+ };
2558
+ const headers = {
2559
+ "Authorization": `Bearer ${sessionToken}`,
2560
+ "Content-Type": "application/json"
2561
+ };
2562
+ fetch(ENDPOINTS.sessionPing, {
2563
+ method: "POST",
2564
+ headers,
2565
+ body: JSON.stringify(payload),
2566
+ keepalive: true
2567
+ }).catch(() => {
2568
+ });
2569
+ }
2570
+ };
2571
+ }, [audienceId, sessionId, sessionToken, audioElement, duration]);
2572
+ react.useEffect(() => {
2573
+ if (broadcast.transcriptUrl && broadcast.transcriptStatus === 2 && !transcriptData) {
2574
+ setIsLoadingTranscript(true);
2575
+ fetch(broadcast.transcriptUrl).then((res) => {
2576
+ if (!res.ok) {
2577
+ throw new Error(`Failed to fetch transcript: ${res.status} ${res.statusText}`);
2578
+ }
2579
+ return res.json();
2580
+ }).then((data) => {
2581
+ if (data.segments && data.words && !data.segments[0]?.words) {
2582
+ data.segments = data.segments.map((segment, index) => {
2583
+ const segmentWords = data.words.filter((word) => {
2584
+ if (index === data.segments.length - 1) {
2585
+ return word.start >= segment.start;
2586
+ }
2587
+ return word.start >= segment.start && word.start < segment.end;
2588
+ });
2589
+ return {
2590
+ ...segment,
2591
+ words: segmentWords
2592
+ };
2593
+ });
2594
+ }
2595
+ setTranscriptData(data);
2596
+ setIsLoadingTranscript(false);
2597
+ }).catch((error) => {
2598
+ debug.error("[Transcript] Failed to load transcript:", error);
2599
+ setIsLoadingTranscript(false);
2600
+ });
2601
+ }
2602
+ }, [broadcast.transcriptUrl, broadcast.transcriptStatus, transcriptData]);
2603
+ react.useEffect(() => {
2604
+ if (!audioElement) return;
2605
+ const handleTimeUpdate2 = () => {
2606
+ setCurrentTime(audioElement.currentTime);
2607
+ };
2608
+ audioElement.addEventListener("timeupdate", handleTimeUpdate2);
2609
+ return () => audioElement.removeEventListener("timeupdate", handleTimeUpdate2);
2610
+ }, [audioElement]);
2611
+ react.useEffect(() => {
2612
+ if (showTranscript && autoScrollEnabled && activeWordRef.current && transcriptContainerRef.current) {
2613
+ const container = transcriptContainerRef.current;
2614
+ const activeWord = activeWordRef.current;
2615
+ const containerRect = container.getBoundingClientRect();
2616
+ const wordRect = activeWord.getBoundingClientRect();
2617
+ if (wordRect.top < containerRect.top || wordRect.bottom > containerRect.bottom) {
2618
+ isScrollingProgrammatically.current = true;
2619
+ activeWord.scrollIntoView({ behavior: "smooth", block: "center" });
2620
+ setTimeout(() => {
2621
+ isScrollingProgrammatically.current = false;
2622
+ }, 500);
2623
+ }
2624
+ }
2625
+ }, [currentTime, showTranscript, autoScrollEnabled]);
2626
+ react.useEffect(() => {
2627
+ if (!showTranscript || !transcriptContainerRef.current) return;
2628
+ const container = transcriptContainerRef.current;
2629
+ const handleScroll = () => {
2630
+ if (!isScrollingProgrammatically.current && autoScrollEnabled) {
2631
+ setAutoScrollEnabled(false);
2632
+ }
2633
+ };
2634
+ container.addEventListener("scroll", handleScroll, { passive: true });
2635
+ return () => container.removeEventListener("scroll", handleScroll);
2636
+ }, [showTranscript, autoScrollEnabled]);
2637
+ const handlePlayPause = () => {
2638
+ if (hasEnded) {
2639
+ if (audioElement) {
2640
+ audioElement.currentTime = 0;
2641
+ }
2642
+ setHasEnded(false);
2643
+ }
2644
+ setPlaying(!playing);
2645
+ };
2646
+ const handleRestart = () => {
2647
+ if (audioElement) {
2648
+ audioElement.currentTime = 0;
2649
+ }
2650
+ setHasEnded(false);
2651
+ setPlaying(true);
2652
+ };
2653
+ const handleVideoClick = () => {
2654
+ if (!isLiveStream) {
2655
+ handlePlayPause();
2656
+ }
2657
+ };
2658
+ const handleSeekChange = (e) => {
2659
+ const newValue = parseFloat(e.target.value);
2660
+ setPlayed(newValue);
2661
+ };
2662
+ const handleSeekMouseDown = () => {
2663
+ setSeeking(true);
2664
+ };
2665
+ const handleSeekMouseUp = (e) => {
2666
+ const seekValue = parseFloat(e.target.value);
2667
+ setSeeking(false);
2668
+ if (audioElement && duration > 0) {
2669
+ const seekTime = seekValue * duration;
2670
+ audioElement.currentTime = seekTime;
2671
+ setHasEnded(false);
2672
+ }
2673
+ };
2674
+ const handleSeekTouchStart = () => {
2675
+ setSeeking(true);
2676
+ };
2677
+ const handleSeekTouchEnd = (e) => {
2678
+ const seekValue = parseFloat(e.target.value);
2679
+ setSeeking(false);
2680
+ if (audioElement && duration > 0) {
2681
+ const seekTime = seekValue * duration;
2682
+ audioElement.currentTime = seekTime;
2683
+ setHasEnded(false);
2684
+ }
2685
+ };
2686
+ const handleTimeUpdate = (e) => {
2687
+ if (!seeking) {
2688
+ const video = e.currentTarget;
2689
+ const playedFraction = video.duration > 0 ? video.currentTime / video.duration : 0;
2690
+ setPlayed(playedFraction);
2691
+ }
2692
+ };
2693
+ const handleLoadedMetadata = (e) => {
2694
+ const video = e.currentTarget;
2695
+ setAudioElement(video);
2696
+ if (video.duration && !isNaN(video.duration) && video.duration > 0) {
2697
+ setDuration(video.duration);
2698
+ } else if (broadcast.durationSeconds && broadcast.durationSeconds > 0) {
2699
+ setDuration(broadcast.durationSeconds);
2700
+ }
2701
+ };
2702
+ const handlePlayerReady = (player) => {
2703
+ try {
2704
+ const internalPlayer = player?.getInternalPlayer?.();
2705
+ if (internalPlayer && internalPlayer instanceof HTMLMediaElement) {
2706
+ setAudioElement(internalPlayer);
2707
+ }
2708
+ } catch (error) {
2709
+ debug.error("[BroadcastPlayer] Error getting internal player:", error);
2710
+ }
2711
+ };
2712
+ react.useEffect(() => {
2713
+ const findAudioElement = () => {
2714
+ const videoElements = document.querySelectorAll("video, audio");
2715
+ if (videoElements.length > 0) {
2716
+ const element = videoElements[0];
2717
+ setAudioElement(element);
2718
+ return true;
2719
+ }
2720
+ return false;
2721
+ };
2722
+ if (!findAudioElement()) {
2723
+ const retryIntervals = [100, 300, 500, 1e3, 1500, 2e3, 3e3, 4e3, 5e3];
2724
+ const timeouts = retryIntervals.map(
2725
+ (delay) => setTimeout(() => {
2726
+ findAudioElement();
2727
+ }, delay)
2728
+ );
2729
+ return () => timeouts.forEach(clearTimeout);
2730
+ }
2731
+ }, [playbackUrl]);
2732
+ react.useEffect(() => {
2733
+ if (playing && !audioElement) {
2734
+ const videoElements = document.querySelectorAll("video, audio");
2735
+ if (videoElements.length > 0) {
2736
+ const element = videoElements[0];
2737
+ setAudioElement(element);
2738
+ }
2739
+ }
2740
+ }, [playing, audioElement]);
2741
+ const handleVolumeChange = (e) => {
2742
+ setVolume(parseFloat(e.target.value));
2743
+ };
2744
+ const toggleMute = () => {
2745
+ setMuted(!muted);
2746
+ };
2747
+ const toggleFullscreen = async () => {
2748
+ try {
2749
+ if (document.fullscreenElement) {
2750
+ await document.exitFullscreen();
2751
+ return;
2752
+ }
2753
+ let videoElement = null;
2754
+ if (playerRef.current && typeof playerRef.current.getInternalPlayer === "function") {
2755
+ videoElement = playerRef.current.getInternalPlayer();
2756
+ }
2757
+ if (!videoElement || typeof videoElement.requestFullscreen !== "function") {
2758
+ const videoElements = document.querySelectorAll("video");
2759
+ if (videoElements.length > 0) {
2760
+ videoElement = videoElements[0];
2761
+ }
2762
+ }
2763
+ if (!videoElement || typeof videoElement.requestFullscreen !== "function") {
2764
+ const modalElement = document.querySelector(".aspect-video");
2765
+ if (modalElement && typeof modalElement.requestFullscreen === "function") {
2766
+ await modalElement.requestFullscreen();
2767
+ return;
2768
+ }
2769
+ }
2770
+ if (videoElement && typeof videoElement.requestFullscreen === "function") {
2771
+ await videoElement.requestFullscreen();
2772
+ }
2773
+ } catch (error) {
2774
+ debug.error("Error toggling fullscreen:", error);
2775
+ }
2776
+ };
2777
+ const handleError = async (error) => {
2778
+ debug.error("Media playback error:", error);
2779
+ const isPotentialExpiration = error?.code === HTTP_STATUS.FORBIDDEN || error?.status === HTTP_STATUS.FORBIDDEN || error?.statusCode === HTTP_STATUS.FORBIDDEN || error?.code === HTTP_STATUS.NOT_FOUND || error?.status === HTTP_STATUS.NOT_FOUND || error?.statusCode === HTTP_STATUS.NOT_FOUND || error?.message?.includes("403") || error?.message?.includes("404") || error?.message?.includes("Forbidden") || error?.message?.toLowerCase().includes("network") || error?.type === "network" || error?.message?.includes("MEDIA_ERR_SRC_NOT_SUPPORTED");
2780
+ if (isPotentialExpiration && currentPlaybackInfo?.type && !isRefreshingUrl.current) {
2781
+ debug.log("[Player Error] Detected potential URL expiration, attempting refresh...");
2782
+ const currentPosition = audioElement?.currentTime || 0;
2783
+ const wasPlaying = playing;
2784
+ const fileType = currentPlaybackInfo.type;
2785
+ if (fileType !== "mp4" && fileType !== "mp3" && fileType !== "hls") {
2786
+ debug.error("[Player Error] Invalid file type, cannot refresh:", fileType);
2787
+ } else {
2788
+ const refreshed = await refreshPresignedUrl(fileType);
2789
+ if (refreshed) {
2790
+ debug.log("[Player Error] URL refreshed successfully, resuming playback");
2791
+ setTimeout(() => {
2792
+ if (audioElement && currentPosition > 0) {
2793
+ audioElement.currentTime = currentPosition;
2794
+ }
2795
+ if (wasPlaying) {
2796
+ setPlaying(true);
2797
+ }
2798
+ }, PLAYBACK_RESUME_DELAY_MS);
2799
+ return;
2800
+ }
2801
+ }
2802
+ }
2803
+ setHasError(true);
2804
+ setPlaying(false);
2805
+ setIsLoadingVideo(false);
2806
+ setErrorMessage(getErrorMessage(error));
2807
+ if (onError && error instanceof Error) {
2808
+ onError(error);
2809
+ }
2810
+ };
2811
+ const handleRetry = react.useCallback(() => {
2812
+ setHasError(false);
2813
+ setErrorMessage("");
2814
+ setIsLoadingVideo(true);
2815
+ const info = getPlaybackInfo();
2816
+ if (info) {
2817
+ setCurrentPlaybackInfo(null);
2818
+ setTimeout(() => {
2819
+ setCurrentPlaybackInfo(info);
2820
+ setPlaying(true);
2821
+ }, 100);
2822
+ }
2823
+ }, [broadcast]);
2824
+ const handleWordClick = (startTime) => {
2825
+ if (audioElement) {
2826
+ audioElement.currentTime = startTime;
2827
+ setPlayed(startTime / duration);
2828
+ setHasEnded(false);
2829
+ if (!playing) {
2830
+ setPlaying(true);
2831
+ }
2832
+ }
2833
+ };
2834
+ react.useEffect(() => {
2835
+ if (!enableKeyboardShortcuts) return;
2836
+ const seekBy = (seconds) => {
2837
+ if (!audioElement || duration <= 0) return;
2838
+ const newTime = Math.max(0, Math.min(duration, audioElement.currentTime + seconds));
2839
+ audioElement.currentTime = newTime;
2840
+ setPlayed(newTime / duration);
2841
+ };
2842
+ const handleKeyDown = (e) => {
2843
+ const target = e.target;
2844
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target.contentEditable === "true" || target.getAttribute("role") === "textbox") {
2845
+ return;
2846
+ }
2847
+ if (e.ctrlKey || e.metaKey || e.altKey) {
2848
+ return;
2849
+ }
2850
+ switch (e.key) {
2851
+ case " ":
2852
+ case "k":
2853
+ case "K":
2854
+ e.preventDefault();
2855
+ handlePlayPause();
2856
+ break;
2857
+ case "ArrowLeft":
2858
+ e.preventDefault();
2859
+ seekBy(-5);
2860
+ break;
2861
+ case "ArrowRight":
2862
+ e.preventDefault();
2863
+ seekBy(5);
2864
+ break;
2865
+ case "ArrowUp":
2866
+ e.preventDefault();
2867
+ setVolume((prev) => Math.min(1, prev + 0.1));
2868
+ if (muted) setMuted(false);
2869
+ break;
2870
+ case "ArrowDown":
2871
+ e.preventDefault();
2872
+ setVolume((prev) => Math.max(0, prev - 0.1));
2873
+ break;
2874
+ case "m":
2875
+ case "M":
2876
+ e.preventDefault();
2877
+ toggleMute();
2878
+ break;
2879
+ case "f":
2880
+ case "F":
2881
+ if (!isAudioOnly) {
2882
+ e.preventDefault();
2883
+ toggleFullscreen();
2884
+ }
2885
+ break;
2886
+ case "0":
2887
+ case "Home":
2888
+ e.preventDefault();
2889
+ if (audioElement && duration > 0) {
2890
+ audioElement.currentTime = 0;
2891
+ setPlayed(0);
2892
+ setHasEnded(false);
2893
+ }
2894
+ break;
2895
+ case "End":
2896
+ e.preventDefault();
2897
+ if (audioElement && duration > 0) {
2898
+ audioElement.currentTime = duration - 1;
2899
+ setPlayed((duration - 1) / duration);
2900
+ }
2901
+ break;
2902
+ }
2903
+ };
2904
+ window.addEventListener("keydown", handleKeyDown);
2905
+ return () => window.removeEventListener("keydown", handleKeyDown);
2906
+ }, [enableKeyboardShortcuts, audioElement, duration, playing, muted, isAudioOnly, handlePlayPause, toggleMute, toggleFullscreen]);
2907
+ if (currentPlaybackInfo !== null && !playbackUrl) {
2908
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-lg p-6 max-w-md w-full mx-4 border border-gray-200 dark:border-zinc-800", children: [
2909
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-bold text-black dark:text-white mb-4", children: "Broadcast Unavailable" }),
2910
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-600 dark:text-gray-400", children: "No playback URL available for this broadcast. The recording may still be processing." })
2911
+ ] });
2912
+ }
2913
+ if (!playbackUrl) {
2914
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center p-8", children: /* @__PURE__ */ jsxRuntime.jsx(LoadingSpinner, { variant: "white", text: "Loading..." }) });
2915
+ }
2916
+ const hasTranscript = broadcast.transcriptStatus === 2 && transcriptData && (transcriptData.segments && transcriptData.segments.some((s) => s.words && s.words.length > 0) || transcriptData.words && transcriptData.words.length > 0);
2917
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `bg-black rounded-lg shadow-2xl w-full max-h-full flex flex-col overflow-hidden ${className}`, children: [
2918
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-zinc-900/50 backdrop-blur-sm border-b border-zinc-800 px-3 sm:px-4 md:px-6 py-2 sm:py-3 md:py-4 flex justify-between items-center rounded-t-lg shrink-0", children: [
2919
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2920
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-white", children: broadcast.streamKeyRecord?.foreignName || "Broadcast" }),
2921
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-gray-400", children: [
2922
+ broadcast.isVideo ? "Video" : "Audio",
2923
+ " \u2022",
2924
+ " ",
2925
+ broadcast.broadcastStatus === 1 ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500 font-semibold", children: "\u{1F534} LIVE" }) : playbackType === "hls" ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-500 font-semibold", children: "OFFLINE" }) : formatTime(duration)
2926
+ ] })
2927
+ ] }),
2928
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-3", children: renderClipCreator && playbackType !== "hls" && appId && contentId && duration > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
2929
+ "button",
2930
+ {
2931
+ onClick: () => setShowClipCreator(true),
2932
+ className: "px-3 md:px-4 py-1.5 md:py-2 bg-blue-600 hover:bg-blue-700 text-white text-xs md:text-sm font-medium rounded-lg transition-colors flex items-center gap-1 md:gap-2",
2933
+ title: "Create a clip from this broadcast",
2934
+ "aria-label": "Create clip from broadcast",
2935
+ children: [
2936
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { className: "w-3 h-3 md:w-4 md:h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: [
2937
+ /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" }),
2938
+ /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 12a9 9 0 11-18 0 9 9 0 0118 0z" })
2939
+ ] }),
2940
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Create Clip" }),
2941
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sm:hidden", children: "Clip" })
2942
+ ]
2943
+ }
2944
+ ) })
2945
+ ] }),
2946
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col md:flex-row flex-1 min-h-0 overflow-hidden", children: [
2947
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 md:shrink md:flex-1 flex flex-col overflow-hidden", children: [
2948
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative ${isAudioOnly ? "bg-linear-to-br from-zinc-900 via-zinc-800 to-zinc-900 flex items-stretch" : "bg-black"}`, children: [
2949
+ isAudioOnly ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative cursor-pointer w-full flex flex-col", onClick: handleVideoClick, children: !hasError ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full h-full relative", children: [
2950
+ /* @__PURE__ */ jsxRuntime.jsx(AudioWaveform, { audioElement, isPlaying: isLiveStream ? true : playing, isLive: isLiveStream }),
2951
+ isLoadingVideo && !hasError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-black/90 flex items-center justify-center z-20", children: /* @__PURE__ */ jsxRuntime.jsx(LoadingSpinner, { variant: "white" }) }),
2952
+ hasEnded && !wasLiveStream && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-black/50 flex items-center justify-center z-20 pointer-events-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(
2953
+ "button",
2954
+ {
2955
+ onClick: (e) => {
2956
+ e.stopPropagation();
2957
+ handleRestart();
2958
+ },
2959
+ className: "bg-white hover:bg-blue-500 text-black hover:text-white font-semibold py-4 px-8 rounded-full transition-all transform hover:scale-105 flex items-center gap-3",
2960
+ "aria-label": "Restart playback from beginning",
2961
+ children: [
2962
+ /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-6 h-6", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z", clipRule: "evenodd" }) }),
2963
+ "Restart"
2964
+ ]
2965
+ }
2966
+ ) })
2967
+ ] }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full h-full flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center max-w-md px-4", children: [
2968
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-6xl mb-4", children: "\u26A0\uFE0F" }),
2969
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xl font-semibold text-white mb-3", children: "Playback Error" }),
2970
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-300 text-sm mb-6", children: errorMessage }),
2971
+ /* @__PURE__ */ jsxRuntime.jsxs(
2972
+ "button",
2973
+ {
2974
+ onClick: handleRetry,
2975
+ className: "px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors inline-flex items-center gap-2",
2976
+ "aria-label": "Retry playback",
2977
+ children: [
2978
+ /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" }) }),
2979
+ "Retry"
2980
+ ]
2981
+ }
2982
+ )
2983
+ ] }) }) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "aspect-video relative", children: [
2984
+ /* @__PURE__ */ jsxRuntime.jsx("div", { onClick: handleVideoClick, className: "cursor-pointer", children: /* @__PURE__ */ jsxRuntime.jsx(
2985
+ ReactPlayer__default.default,
2986
+ {
2987
+ ref: playerRef,
2988
+ src: playbackUrl || void 0,
2989
+ playing,
2990
+ volume,
2991
+ muted,
2992
+ width: "100%",
2993
+ height: "100%",
2994
+ crossOrigin: "anonymous",
2995
+ onReady: handlePlayerReady,
2996
+ onTimeUpdate: handleTimeUpdate,
2997
+ onLoadedMetadata: handleLoadedMetadata,
2998
+ onPlay: handlePlay,
2999
+ onPause: handlePause,
3000
+ onEnded: handleEnded,
3001
+ onError: handleError,
3002
+ style: { backgroundColor: "#000" }
3003
+ },
3004
+ playbackUrl || "no-url"
3005
+ ) }),
3006
+ isLoadingVideo && !hasError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-black/90 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(LoadingSpinner, { variant: "white" }) }),
3007
+ hasEnded && !hasError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-black/50 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs(
3008
+ "button",
3009
+ {
3010
+ onClick: (e) => {
3011
+ e.stopPropagation();
3012
+ handleRestart();
3013
+ },
3014
+ className: "bg-white hover:bg-blue-500 text-black hover:text-white font-semibold py-4 px-8 rounded-full transition-all transform hover:scale-105 flex items-center gap-3",
3015
+ "aria-label": "Restart playback from beginning",
3016
+ children: [
3017
+ /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-6 h-6", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z", clipRule: "evenodd" }) }),
3018
+ "Restart"
3019
+ ]
3020
+ }
3021
+ ) }),
3022
+ hasError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-black/90 flex items-center justify-center p-8", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center max-w-md", children: [
3023
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-6xl mb-4", children: "\u26A0\uFE0F" }),
3024
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xl font-semibold text-white mb-3", children: "Playback Error" }),
3025
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-300 text-sm mb-6", children: errorMessage }),
3026
+ /* @__PURE__ */ jsxRuntime.jsxs(
3027
+ "button",
3028
+ {
3029
+ onClick: handleRetry,
3030
+ className: "px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors inline-flex items-center gap-2",
3031
+ "aria-label": "Retry playback",
3032
+ children: [
3033
+ /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" }) }),
3034
+ "Retry"
3035
+ ]
3036
+ }
3037
+ )
3038
+ ] }) })
3039
+ ] }),
3040
+ isAudioOnly && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
3041
+ ReactPlayer__default.default,
3042
+ {
3043
+ ref: playerRef,
3044
+ src: playbackUrl || void 0,
3045
+ playing,
3046
+ volume,
3047
+ muted,
3048
+ width: "0",
3049
+ height: "0",
3050
+ crossOrigin: "anonymous",
3051
+ onReady: handlePlayerReady,
3052
+ onTimeUpdate: handleTimeUpdate,
3053
+ onLoadedMetadata: handleLoadedMetadata,
3054
+ onPlay: handlePlay,
3055
+ onPause: handlePause,
3056
+ onEnded: handleEnded,
3057
+ onError: handleError
3058
+ },
3059
+ playbackUrl || "no-url"
3060
+ ) })
3061
+ ] }),
3062
+ !hasError && !isLiveStream && (wasLiveStream ? parseInt(broadcast.mp3Size || "0") > 0 || parseInt(broadcast.mp4Size || "0") > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-zinc-900/50 backdrop-blur-sm px-4 md:px-6 py-3 md:py-4 rounded-b-lg", children: [
3063
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-4", children: [
3064
+ /* @__PURE__ */ jsxRuntime.jsx(
3065
+ "input",
3066
+ {
3067
+ type: "range",
3068
+ min: 0,
3069
+ max: 0.999999,
3070
+ step: "any",
3071
+ value: played || 0,
3072
+ onMouseDown: handleSeekMouseDown,
3073
+ onMouseUp: handleSeekMouseUp,
3074
+ onTouchStart: handleSeekTouchStart,
3075
+ onTouchEnd: handleSeekTouchEnd,
3076
+ onChange: handleSeekChange,
3077
+ className: "w-full h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer slider",
3078
+ "aria-label": "Seek position",
3079
+ "aria-valuemin": 0,
3080
+ "aria-valuemax": duration,
3081
+ "aria-valuenow": played * duration,
3082
+ "aria-valuetext": `${formatTime(played * duration)} of ${formatTime(duration)}`,
3083
+ role: "slider"
3084
+ }
3085
+ ),
3086
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between text-xs text-gray-400 mt-1", children: [
3087
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatTime((played || 0) * duration) }),
3088
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatTime(duration) })
3089
+ ] })
3090
+ ] }),
3091
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
3092
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4", children: [
3093
+ /* @__PURE__ */ jsxRuntime.jsx(
3094
+ "button",
3095
+ {
3096
+ onClick: handlePlayPause,
3097
+ className: "text-white hover:text-blue-400 transition-colors",
3098
+ title: playing ? "Pause" : "Play",
3099
+ "aria-label": playing ? "Pause" : "Play",
3100
+ "aria-pressed": playing,
3101
+ children: playing ? /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-8 h-8", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z", clipRule: "evenodd" }) }) : /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-8 h-8", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z", clipRule: "evenodd" }) })
3102
+ }
3103
+ ),
3104
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
3105
+ /* @__PURE__ */ jsxRuntime.jsx(
3106
+ "button",
3107
+ {
3108
+ onClick: toggleMute,
3109
+ className: "text-white hover:text-blue-400 transition-colors",
3110
+ title: muted ? "Unmute" : "Mute",
3111
+ "aria-label": muted ? "Unmute" : "Mute",
3112
+ "aria-pressed": muted,
3113
+ children: muted || volume === 0 ? /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM12.293 7.293a1 1 0 011.414 0L15 8.586l1.293-1.293a1 1 0 111.414 1.414L16.414 10l1.293 1.293a1 1 0 01-1.414 1.414L15 11.414l-1.293 1.293a1 1 0 01-1.414-1.414L13.586 10l-1.293-1.293a1 1 0 010-1.414z", clipRule: "evenodd" }) }) : /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z", clipRule: "evenodd" }) })
3114
+ }
3115
+ ),
3116
+ /* @__PURE__ */ jsxRuntime.jsx(
3117
+ "input",
3118
+ {
3119
+ type: "range",
3120
+ min: 0,
3121
+ max: 1,
3122
+ step: 0.01,
3123
+ value: muted ? 0 : volume || 1,
3124
+ onChange: handleVolumeChange,
3125
+ className: "w-20 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer slider",
3126
+ "aria-label": "Volume",
3127
+ "aria-valuemin": 0,
3128
+ "aria-valuemax": 100,
3129
+ "aria-valuenow": muted ? 0 : Math.round(volume * 100),
3130
+ "aria-valuetext": muted ? "Muted" : `${Math.round(volume * 100)}%`,
3131
+ role: "slider"
3132
+ }
3133
+ )
3134
+ ] })
3135
+ ] }),
3136
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
3137
+ !isLiveStream && broadcast.hash && (broadcast.recordingMp4Url || broadcast.recordingMp3Url) && /* @__PURE__ */ jsxRuntime.jsx(
3138
+ "button",
3139
+ {
3140
+ onClick: () => {
3141
+ const downloadUrl = buildPlaybackUrl(broadcast.id, broadcast.hash, "download");
3142
+ window.open(downloadUrl, "_blank");
3143
+ },
3144
+ className: "text-white hover:text-blue-400 transition-colors",
3145
+ title: "Download Recording",
3146
+ "aria-label": "Download recording",
3147
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" }) })
3148
+ }
3149
+ ),
3150
+ !isAudioOnly && /* @__PURE__ */ jsxRuntime.jsx(
3151
+ "button",
3152
+ {
3153
+ onClick: toggleFullscreen,
3154
+ className: "text-white hover:text-blue-400 transition-colors",
3155
+ title: "Fullscreen",
3156
+ "aria-label": "Toggle fullscreen",
3157
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 11-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 111.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 111.414-1.414L15 13.586V12a1 1 0 011-1z", clipRule: "evenodd" }) })
3158
+ }
3159
+ ),
3160
+ hasTranscript && /* @__PURE__ */ jsxRuntime.jsx(
3161
+ "button",
3162
+ {
3163
+ onClick: () => setShowTranscript(!showTranscript),
3164
+ className: `transition-colors ${showTranscript ? "text-blue-400" : "text-white hover:text-blue-400"}`,
3165
+ title: showTranscript ? "Hide Transcript" : "Show Transcript",
3166
+ "aria-label": showTranscript ? "Hide transcript" : "Show transcript",
3167
+ "aria-pressed": showTranscript,
3168
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" }) })
3169
+ }
3170
+ )
3171
+ ] })
3172
+ ] })
3173
+ ] }) : null : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-zinc-900/50 backdrop-blur-sm px-4 md:px-6 py-3 md:py-4 rounded-b-lg", children: [
3174
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-4", children: [
3175
+ /* @__PURE__ */ jsxRuntime.jsx(
3176
+ "input",
3177
+ {
3178
+ type: "range",
3179
+ min: 0,
3180
+ max: 0.999999,
3181
+ step: "any",
3182
+ value: played || 0,
3183
+ onMouseDown: handleSeekMouseDown,
3184
+ onMouseUp: handleSeekMouseUp,
3185
+ onTouchStart: handleSeekTouchStart,
3186
+ onTouchEnd: handleSeekTouchEnd,
3187
+ onChange: handleSeekChange,
3188
+ className: "w-full h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer slider",
3189
+ "aria-label": "Seek position",
3190
+ "aria-valuemin": 0,
3191
+ "aria-valuemax": duration,
3192
+ "aria-valuenow": played * duration,
3193
+ "aria-valuetext": `${formatTime(played * duration)} of ${formatTime(duration)}`,
3194
+ role: "slider"
3195
+ }
3196
+ ),
3197
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between text-xs text-gray-400 mt-1", children: [
3198
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatTime((played || 0) * duration) }),
3199
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatTime(duration) })
3200
+ ] })
3201
+ ] }),
3202
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
3203
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4", children: [
3204
+ /* @__PURE__ */ jsxRuntime.jsx(
3205
+ "button",
3206
+ {
3207
+ onClick: handlePlayPause,
3208
+ className: "text-white hover:text-blue-400 transition-colors",
3209
+ title: playing ? "Pause" : "Play",
3210
+ "aria-label": playing ? "Pause" : "Play",
3211
+ "aria-pressed": playing,
3212
+ children: playing ? /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-8 h-8", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z", clipRule: "evenodd" }) }) : /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-8 h-8", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z", clipRule: "evenodd" }) })
3213
+ }
3214
+ ),
3215
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
3216
+ /* @__PURE__ */ jsxRuntime.jsx(
3217
+ "button",
3218
+ {
3219
+ onClick: toggleMute,
3220
+ className: "text-white hover:text-blue-400 transition-colors",
3221
+ title: muted ? "Unmute" : "Mute",
3222
+ "aria-label": muted ? "Unmute" : "Mute",
3223
+ "aria-pressed": muted,
3224
+ children: muted || volume === 0 ? /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM12.293 7.293a1 1 0 011.414 0L15 8.586l1.293-1.293a1 1 0 111.414 1.414L16.414 10l1.293 1.293a1 1 0 01-1.414 1.414L15 11.414l-1.293 1.293a1 1 0 01-1.414-1.414L13.586 10l-1.293-1.293a1 1 0 010-1.414z", clipRule: "evenodd" }) }) : /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z", clipRule: "evenodd" }) })
3225
+ }
3226
+ ),
3227
+ /* @__PURE__ */ jsxRuntime.jsx(
3228
+ "input",
3229
+ {
3230
+ type: "range",
3231
+ min: 0,
3232
+ max: 1,
3233
+ step: 0.01,
3234
+ value: muted ? 0 : volume || 1,
3235
+ onChange: handleVolumeChange,
3236
+ className: "w-20 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer slider",
3237
+ "aria-label": "Volume",
3238
+ "aria-valuemin": 0,
3239
+ "aria-valuemax": 100,
3240
+ "aria-valuenow": muted ? 0 : Math.round(volume * 100),
3241
+ "aria-valuetext": muted ? "Muted" : `${Math.round(volume * 100)}%`,
3242
+ role: "slider"
3243
+ }
3244
+ )
3245
+ ] })
3246
+ ] }),
3247
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
3248
+ !isAudioOnly && /* @__PURE__ */ jsxRuntime.jsx(
3249
+ "button",
3250
+ {
3251
+ onClick: toggleFullscreen,
3252
+ className: "text-white hover:text-blue-400 transition-colors",
3253
+ title: "Toggle fullscreen",
3254
+ "aria-label": "Toggle fullscreen",
3255
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { fillRule: "evenodd", d: "M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 11-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 111.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 111.414-1.414L15 13.586V12a1 1 0 011-1z", clipRule: "evenodd" }) })
3256
+ }
3257
+ ),
3258
+ hasTranscript && /* @__PURE__ */ jsxRuntime.jsx(
3259
+ "button",
3260
+ {
3261
+ onClick: () => setShowTranscript(!showTranscript),
3262
+ className: `transition-colors ${showTranscript ? "text-blue-400" : "text-white hover:text-blue-400"}`,
3263
+ title: showTranscript ? "Hide transcript" : "Show transcript",
3264
+ "aria-label": showTranscript ? "Hide transcript" : "Show transcript",
3265
+ "aria-pressed": showTranscript,
3266
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" }) })
3267
+ }
3268
+ )
3269
+ ] })
3270
+ ] })
3271
+ ] }))
3272
+ ] }),
3273
+ showTranscript && hasTranscript && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 md:flex-none min-h-0 w-full md:w-96 bg-zinc-900 border-t md:border-t-0 border-l border-zinc-800 flex flex-col overflow-hidden", children: [
3274
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 py-3 border-b border-zinc-800 bg-zinc-900/50 shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
3275
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
3276
+ /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5 text-green-400", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" }) }),
3277
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-white", children: "Transcript" }),
3278
+ broadcast.transcriptLanguage && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 px-2 py-0.5 bg-zinc-800 rounded", children: broadcast.transcriptLanguage.toUpperCase() })
3279
+ ] }),
3280
+ broadcast.transcriptUrl && /* @__PURE__ */ jsxRuntime.jsx(
3281
+ "a",
3282
+ {
3283
+ href: broadcast.transcriptUrl,
3284
+ download: `${broadcast.hash || broadcast.id}-transcript.json`,
3285
+ className: "text-gray-400 hover:text-white transition-colors",
3286
+ title: "Download transcript",
3287
+ "aria-label": "Download transcript as JSON file",
3288
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" }) })
3289
+ }
3290
+ )
3291
+ ] }) }),
3292
+ !autoScrollEnabled && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 py-2 bg-zinc-800/50 border-b border-zinc-700 shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
3293
+ "button",
3294
+ {
3295
+ onClick: () => setAutoScrollEnabled(true),
3296
+ className: "w-full px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors flex items-center justify-center gap-2 text-sm font-medium",
3297
+ "aria-label": "Resume automatic scrolling of transcript",
3298
+ children: [
3299
+ /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 14l-7 7m0 0l-7-7m7 7V3" }) }),
3300
+ "Resume Auto-Scroll"
3301
+ ]
3302
+ }
3303
+ ) }),
3304
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: transcriptContainerRef, className: "flex-1 min-h-0 overflow-y-auto px-4 py-4 text-gray-300 leading-relaxed", children: isLoadingTranscript ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center py-8", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-6 w-6 border-2 border-gray-600 border-t-blue-500 rounded-full animate-spin" }) }) : transcriptData?.segments && transcriptData.segments.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-4", children: (() => {
3305
+ const filteredSegments = transcriptData.segments.filter((s) => s.words && s.words.length > 0);
3306
+ let globalWordIndex = 0;
3307
+ const wordMap = /* @__PURE__ */ new Map();
3308
+ filteredSegments.forEach((segment) => {
3309
+ segment.words.forEach((_word, wordIndex) => {
3310
+ wordMap.set(`${segment.id}-${wordIndex}`, globalWordIndex++);
3311
+ });
3312
+ });
3313
+ let currentWordIndex = -1;
3314
+ filteredSegments.forEach((segment) => {
3315
+ segment.words.forEach((word, wordIndex) => {
3316
+ const globalIdx = wordMap.get(`${segment.id}-${wordIndex}`) || -1;
3317
+ if (currentTime >= word.start && globalIdx > currentWordIndex) {
3318
+ currentWordIndex = globalIdx;
3319
+ }
3320
+ });
3321
+ });
3322
+ const previousWordIndex = lastActiveWordIndex.current;
3323
+ let minHighlightIndex = -1;
3324
+ let maxHighlightIndex = -1;
3325
+ if (currentWordIndex >= 0) {
3326
+ minHighlightIndex = Math.max(0, currentWordIndex - TRAILING_WORDS);
3327
+ maxHighlightIndex = currentWordIndex;
3328
+ if (currentWordIndex <= TRAILING_WORDS) {
3329
+ minHighlightIndex = 0;
3330
+ }
3331
+ lastActiveWordIndex.current = currentWordIndex;
3332
+ } else if (currentWordIndex === -1) {
3333
+ minHighlightIndex = 0;
3334
+ maxHighlightIndex = 0;
3335
+ } else if (previousWordIndex >= 0) {
3336
+ minHighlightIndex = Math.max(0, previousWordIndex - TRAILING_WORDS);
3337
+ maxHighlightIndex = previousWordIndex;
3338
+ }
3339
+ return filteredSegments.map((segment, _segmentIndex) => {
3340
+ const isSegmentActive = currentTime >= segment.start && currentTime < segment.end;
3341
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: isSegmentActive ? transcriptContainerRef : null, className: "flex gap-3 items-start leading-relaxed", children: [
3342
+ /* @__PURE__ */ jsxRuntime.jsx(
3343
+ "button",
3344
+ {
3345
+ onClick: () => handleWordClick(segment.start),
3346
+ className: "text-xs text-gray-500 hover:text-gray-300 transition-colors shrink-0 pt-0.5 font-mono",
3347
+ title: `Jump to ${formatTimestamp(segment.start)}`,
3348
+ children: formatTimestamp(segment.start)
3349
+ }
3350
+ ),
3351
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: segment.words.map((word, wordIndex) => {
3352
+ const thisGlobalIndex = wordMap.get(`${segment.id}-${wordIndex}`) ?? -1;
3353
+ const isTimestampActive = currentTime >= word.start && currentTime < word.end;
3354
+ const isInGapFill = minHighlightIndex >= 0 && thisGlobalIndex >= minHighlightIndex && thisGlobalIndex <= maxHighlightIndex;
3355
+ const isWordActive = isInGapFill;
3356
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3357
+ "span",
3358
+ {
3359
+ ref: isTimestampActive ? activeWordRef : null,
3360
+ onClick: () => handleWordClick(word.start),
3361
+ className: `cursor-pointer ${isWordActive ? "text-blue-400 font-medium active-word" : isSegmentActive ? "text-gray-200 segment-word" : "text-gray-400 hover:text-gray-200 inactive-word"}`,
3362
+ title: `${formatTime(word.start)} - ${formatTime(word.end)}`,
3363
+ children: [
3364
+ word.word,
3365
+ " "
3366
+ ]
3367
+ },
3368
+ `${segment.id}-${wordIndex}`
3369
+ );
3370
+ }) })
3371
+ ] }, segment.id);
3372
+ });
3373
+ })() }) : transcriptData?.words && transcriptData.words.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1", children: transcriptData.words.map((word, index) => {
3374
+ const isActive = currentTime >= word.start && currentTime < word.end;
3375
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3376
+ "span",
3377
+ {
3378
+ ref: isActive ? activeWordRef : null,
3379
+ onClick: () => handleWordClick(word.start),
3380
+ className: `inline-block cursor-pointer transition-all ${isActive ? "text-blue-400 underline decoration-blue-400 decoration-2 font-medium" : "text-gray-400 hover:text-gray-200"}`,
3381
+ title: `${formatTime(word.start)} - ${formatTime(word.end)}`,
3382
+ children: [
3383
+ word.word,
3384
+ " "
3385
+ ]
3386
+ },
3387
+ index
3388
+ );
3389
+ }) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center text-gray-500 py-8", children: [
3390
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2", children: "Transcript data not available" }),
3391
+ transcriptData && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-600", children: [
3392
+ "Debug: segments=",
3393
+ transcriptData.segments ? transcriptData.segments.length : 0,
3394
+ ", words=",
3395
+ transcriptData.words ? transcriptData.words.length : 0
3396
+ ] })
3397
+ ] }) })
3398
+ ] })
3399
+ ] }),
3400
+ renderClipCreator && renderClipCreator({
3401
+ isOpen: showClipCreator,
3402
+ onClose: () => setShowClipCreator(false),
3403
+ sourceVideoUrl: playbackType === "mp4" ? playbackUrl || void 0 : void 0,
3404
+ sourceAudioUrl: playbackType === "mp3" ? playbackUrl || void 0 : void 0,
3405
+ sourceDuration: duration,
3406
+ onPauseParent: () => setPlaying(false)
3407
+ }),
3408
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
3409
+ .slider::-webkit-slider-thumb {
3410
+ appearance: none;
3411
+ width: 14px;
3412
+ height: 14px;
3413
+ border-radius: 50%;
3414
+ background: white;
3415
+ cursor: pointer;
3416
+ box-shadow: 0 2px 4px rgba(0,0,0,0.3);
3417
+ }
3418
+ .slider::-webkit-slider-thumb:hover {
3419
+ background: #60a5fa;
3420
+ }
3421
+ .slider::-moz-range-thumb {
3422
+ width: 14px;
3423
+ height: 14px;
3424
+ border-radius: 50%;
3425
+ background: white;
3426
+ cursor: pointer;
3427
+ border: none;
3428
+ box-shadow: 0 2px 4px rgba(0,0,0,0.3);
3429
+ }
3430
+ .slider::-moz-range-thumb:hover {
3431
+ background: #60a5fa;
3432
+ }
3433
+
3434
+ .active-word {
3435
+ transition: color 0s, text-shadow 0s;
3436
+ text-shadow: 0 0 8px rgba(96, 165, 250, 0.6), 0 0 12px rgba(96, 165, 250, 0.4);
3437
+ }
3438
+ .segment-word,
3439
+ .inactive-word {
3440
+ transition: color 0.4s ease-out, text-shadow 0.4s ease-out;
3441
+ text-shadow: none;
3442
+ }
3443
+ .segment-word:hover,
3444
+ .inactive-word:hover {
3445
+ transition: color 0.15s ease-in;
3446
+ }
3447
+ ` })
3448
+ ] });
3449
+ }
3450
+ function BroadcastPlayerModal({
3451
+ broadcast,
3452
+ isOpen,
3453
+ onClose,
3454
+ appId,
3455
+ contentId,
3456
+ foreignId,
3457
+ foreignTier,
3458
+ renderClipCreator,
3459
+ className,
3460
+ enableKeyboardShortcuts = false
3461
+ }) {
3462
+ const closeButtonRef = react.useRef(null);
3463
+ const previousActiveElement = react.useRef(null);
3464
+ react.useEffect(() => {
3465
+ if (!isOpen) return;
3466
+ previousActiveElement.current = document.activeElement;
3467
+ setTimeout(() => {
3468
+ closeButtonRef.current?.focus();
3469
+ }, 100);
3470
+ return () => {
3471
+ if (previousActiveElement.current) {
3472
+ previousActiveElement.current.focus();
3473
+ }
3474
+ };
3475
+ }, [isOpen]);
3476
+ react.useEffect(() => {
3477
+ if (!isOpen) return;
3478
+ const handleKeyDown = (e) => {
3479
+ if (e.key === "Escape") {
3480
+ onClose();
3481
+ }
3482
+ };
3483
+ document.addEventListener("keydown", handleKeyDown);
3484
+ return () => document.removeEventListener("keydown", handleKeyDown);
3485
+ }, [isOpen, onClose]);
3486
+ if (!isOpen) return null;
3487
+ const handleBackdropClick = (e) => {
3488
+ if (e.target === e.currentTarget) {
3489
+ onClose();
3490
+ }
3491
+ };
3492
+ return /* @__PURE__ */ jsxRuntime.jsx(
3493
+ "div",
3494
+ {
3495
+ className: "fixed inset-0 bg-black/70 backdrop-blur-xl flex items-center justify-center z-50 p-2 sm:p-4",
3496
+ onClick: handleBackdropClick,
3497
+ role: "dialog",
3498
+ "aria-modal": "true",
3499
+ "aria-label": "Broadcast player",
3500
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full max-w-7xl max-h-[95vh] sm:max-h-[90vh] overflow-hidden", children: [
3501
+ /* @__PURE__ */ jsxRuntime.jsx(
3502
+ "button",
3503
+ {
3504
+ ref: closeButtonRef,
3505
+ onClick: onClose,
3506
+ className: "absolute top-2 right-2 sm:top-4 sm:right-4 z-10 text-gray-400 hover:text-white text-2xl leading-none transition-colors w-8 h-8 flex items-center justify-center bg-black/50 rounded-full",
3507
+ title: "Close (ESC)",
3508
+ "aria-label": "Close player",
3509
+ children: "\xD7"
3510
+ }
3511
+ ),
3512
+ /* @__PURE__ */ jsxRuntime.jsx(
3513
+ BroadcastPlayer,
3514
+ {
3515
+ broadcast,
3516
+ appId,
3517
+ contentId,
3518
+ foreignId,
3519
+ foreignTier,
3520
+ renderClipCreator,
3521
+ className,
3522
+ enableKeyboardShortcuts,
3523
+ onError: (error) => {
3524
+ debug.error("[BroadcastPlayerModal] Player error:", error);
3525
+ }
3526
+ }
3527
+ )
3528
+ ] })
3529
+ }
3530
+ );
3531
+ }
3532
+ var BroadcastPlayerErrorBoundary = class extends react.Component {
3533
+ constructor(props) {
3534
+ super(props);
3535
+ this.handleReset = () => {
3536
+ this.setState({
3537
+ hasError: false,
3538
+ error: null,
3539
+ errorInfo: null
3540
+ });
3541
+ };
3542
+ this.handleClose = () => {
3543
+ this.setState({
3544
+ hasError: false,
3545
+ error: null,
3546
+ errorInfo: null
3547
+ });
3548
+ this.props.onClose?.();
3549
+ };
3550
+ this.state = {
3551
+ hasError: false,
3552
+ error: null,
3553
+ errorInfo: null
3554
+ };
3555
+ }
3556
+ static getDerivedStateFromError(error) {
3557
+ return { hasError: true, error };
3558
+ }
3559
+ componentDidCatch(error, errorInfo) {
3560
+ console.error("[Player Error Boundary] Caught error:", {
3561
+ error: error.message,
3562
+ stack: error.stack,
3563
+ componentStack: errorInfo.componentStack,
3564
+ broadcastId: this.props.broadcastId,
3565
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3566
+ });
3567
+ this.setState({
3568
+ error,
3569
+ errorInfo
3570
+ });
3571
+ }
3572
+ render() {
3573
+ if (this.state.hasError) {
3574
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/80", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full mx-4 p-6", children: [
3575
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-4", children: [
3576
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-2xl font-bold text-gray-900 dark:text-white", children: "Player Error" }),
3577
+ /* @__PURE__ */ jsxRuntime.jsx(
3578
+ "button",
3579
+ {
3580
+ onClick: this.handleClose,
3581
+ className: "text-gray-400 hover:text-gray-600 dark:hover:text-gray-300",
3582
+ "aria-label": "Close",
3583
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3584
+ "svg",
3585
+ {
3586
+ className: "w-6 h-6",
3587
+ fill: "none",
3588
+ stroke: "currentColor",
3589
+ viewBox: "0 0 24 24",
3590
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3591
+ "path",
3592
+ {
3593
+ strokeLinecap: "round",
3594
+ strokeLinejoin: "round",
3595
+ strokeWidth: 2,
3596
+ d: "M6 18L18 6M6 6l12 12"
3597
+ }
3598
+ )
3599
+ }
3600
+ )
3601
+ }
3602
+ )
3603
+ ] }),
3604
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center mb-4", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-full bg-red-100 dark:bg-red-900/30 p-3", children: /* @__PURE__ */ jsxRuntime.jsx(
3605
+ "svg",
3606
+ {
3607
+ className: "w-12 h-12 text-red-600 dark:text-red-400",
3608
+ fill: "none",
3609
+ stroke: "currentColor",
3610
+ viewBox: "0 0 24 24",
3611
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3612
+ "path",
3613
+ {
3614
+ strokeLinecap: "round",
3615
+ strokeLinejoin: "round",
3616
+ strokeWidth: 2,
3617
+ d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
3618
+ }
3619
+ )
3620
+ }
3621
+ ) }) }),
3622
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center mb-6", children: [
3623
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-lg text-gray-900 dark:text-white mb-2", children: "Something went wrong with the media player" }),
3624
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400", children: this.state.error?.message || "An unexpected error occurred" })
3625
+ ] }),
3626
+ process.env.NODE_ENV === "development" && this.state.errorInfo && /* @__PURE__ */ jsxRuntime.jsxs("details", { className: "mb-6 p-4 bg-gray-100 dark:bg-gray-900 rounded text-xs", children: [
3627
+ /* @__PURE__ */ jsxRuntime.jsx("summary", { className: "cursor-pointer font-semibold text-gray-700 dark:text-gray-300 mb-2", children: "Error Details (Development Only)" }),
3628
+ /* @__PURE__ */ jsxRuntime.jsxs("pre", { className: "overflow-auto text-gray-600 dark:text-gray-400 whitespace-pre-wrap", children: [
3629
+ this.state.error?.stack,
3630
+ "\n\nComponent Stack:",
3631
+ this.state.errorInfo.componentStack
3632
+ ] })
3633
+ ] }),
3634
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-3 justify-center", children: [
3635
+ /* @__PURE__ */ jsxRuntime.jsx(
3636
+ "button",
3637
+ {
3638
+ onClick: this.handleReset,
3639
+ className: "px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors",
3640
+ children: "Try Again"
3641
+ }
3642
+ ),
3643
+ /* @__PURE__ */ jsxRuntime.jsx(
3644
+ "button",
3645
+ {
3646
+ onClick: this.handleClose,
3647
+ className: "px-6 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white rounded-lg font-medium transition-colors",
3648
+ children: "Close Player"
3649
+ }
3650
+ )
3651
+ ] }),
3652
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-center text-sm text-gray-500 dark:text-gray-400 mt-6", children: "If this problem persists, try refreshing the page or contact support." })
3653
+ ] }) });
3654
+ }
3655
+ return this.props.children;
3656
+ }
3657
+ };
3658
+
3659
+ // src/utils/broadcast-popup.ts
3660
+ function calculatePopupDimensions() {
3661
+ const screenWidth = window.screen.width;
3662
+ const screenHeight = window.screen.height;
3663
+ const screenAspectRatio = screenWidth / screenHeight;
3664
+ let width;
3665
+ let height;
3666
+ if (screenAspectRatio > 1.2) {
3667
+ height = Math.min(720, Math.floor(screenHeight * 0.85));
3668
+ width = Math.floor(height * (16 / 9));
3669
+ } else {
3670
+ width = Math.min(720, Math.floor(screenWidth * 0.9));
3671
+ height = Math.floor(width * (16 / 9));
3672
+ }
3673
+ const left = Math.floor((screenWidth - width) / 2);
3674
+ const top = Math.floor((screenHeight - height) / 2);
3675
+ return { width, height, left, top };
3676
+ }
3677
+ function openBroadcastStreamerPopup(options) {
3678
+ const {
3679
+ sessionToken,
3680
+ streamKey,
3681
+ appId,
3682
+ mode,
3683
+ additionalParams,
3684
+ baseUrl = "/broadcasts/new"
3685
+ } = options;
3686
+ const { width, height, left, top } = calculatePopupDimensions();
3687
+ const params = new URLSearchParams();
3688
+ if (mode) {
3689
+ params.append("mode", mode);
3690
+ }
3691
+ if (additionalParams) {
3692
+ Object.entries(additionalParams).forEach(([key, value]) => {
3693
+ params.append(key, value);
3694
+ });
3695
+ }
3696
+ const url = `${baseUrl}${params.toString() ? `?${params.toString()}` : ""}`;
3697
+ const popup = window.open(
3698
+ url,
3699
+ "_blank",
3700
+ `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`
3701
+ );
3702
+ if (!popup) {
3703
+ console.error("Failed to open popup window - popup may be blocked");
3704
+ return null;
3705
+ }
3706
+ const sendMessage = () => {
3707
+ try {
3708
+ popup.postMessage(
3709
+ {
3710
+ type: "STREAM_KEY",
3711
+ sessionToken,
3712
+ streamKey,
3713
+ appId
3714
+ },
3715
+ window.location.origin
3716
+ );
3717
+ } catch (error) {
3718
+ console.error("Failed to send credentials to popup:", error);
3719
+ }
3720
+ };
3721
+ sendMessage();
3722
+ setTimeout(sendMessage, 100);
3723
+ setTimeout(sendMessage, 500);
3724
+ return popup;
3725
+ }
3726
+ var openBroadcastPopup = openBroadcastStreamerPopup;
3727
+
3728
+ exports.AudioWaveform = AudioWaveform;
3729
+ exports.BroadcastPlayer = BroadcastPlayer;
3730
+ exports.BroadcastPlayerErrorBoundary = BroadcastPlayerErrorBoundary;
3731
+ exports.BroadcastPlayerModal = BroadcastPlayerModal;
3732
+ exports.BroadcastStreamer = BroadcastStreamer;
3733
+ exports.CDN_DOMAIN = CDN_DOMAIN;
3734
+ exports.DEFAULT_ENCODER_SERVER_URL = DEFAULT_ENCODER_SERVER_URL;
3735
+ exports.DIALTRIBE_API_BASE = DIALTRIBE_API_BASE;
3736
+ exports.DialTribeClient = DialTribeClient;
3737
+ exports.DialTribeProvider = DialTribeProvider;
3738
+ exports.ENDPOINTS = ENDPOINTS;
3739
+ exports.HTTP_STATUS = HTTP_STATUS;
3740
+ exports.LoadingSpinner = LoadingSpinner;
3741
+ exports.StreamKeyDisplay = StreamKeyDisplay;
3742
+ exports.StreamKeyInput = StreamKeyInput;
3743
+ exports.StreamingControls = StreamingControls;
3744
+ exports.StreamingPreview = StreamingPreview;
3745
+ exports.WebSocketStreamer = WebSocketStreamer;
3746
+ exports.buildBroadcastCdnUrl = buildBroadcastCdnUrl;
3747
+ exports.buildBroadcastS3KeyPrefix = buildBroadcastS3KeyPrefix;
3748
+ exports.calculatePopupDimensions = calculatePopupDimensions;
3749
+ exports.checkBrowserCompatibility = checkBrowserCompatibility;
3750
+ exports.formatTime = formatTime;
3751
+ exports.getMediaConstraints = getMediaConstraints;
3752
+ exports.getMediaRecorderOptions = getMediaRecorderOptions;
3753
+ exports.openBroadcastPopup = openBroadcastPopup;
3754
+ exports.openBroadcastStreamerPopup = openBroadcastStreamerPopup;
3755
+ exports.useDialTribe = useDialTribe;
3756
+ exports.useDialTribeOptional = useDialTribeOptional;
3757
+ //# sourceMappingURL=broadcast-streamer.js.map
3758
+ //# sourceMappingURL=broadcast-streamer.js.map