@dialtribe/react-sdk 0.1.0-alpha.8 → 0.1.4

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.
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import { createContext, useState, useCallback, useEffect, useContext, useRef, Component } from 'react';
2
+ import React2, { createContext, useState, useCallback, useEffect, useContext, useRef, useMemo, Component } from 'react';
3
3
  import ReactPlayer from 'react-player';
4
+ import { createPortal } from 'react-dom';
4
5
 
5
6
  // src/components/HelloWorld.tsx
6
7
  function HelloWorld({ name = "World" }) {
@@ -21,8 +22,8 @@ function HelloWorld({ name = "World" }) {
21
22
  /* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: "14px" }, children: "@dialtribe/react-sdk is working correctly" })
22
23
  ] });
23
24
  }
24
- var DialTribeContext = createContext(null);
25
- function DialTribeProvider({
25
+ var DialtribeContext = createContext(null);
26
+ function DialtribeProvider({
26
27
  sessionToken: initialToken,
27
28
  onTokenRefresh,
28
29
  onTokenExpired,
@@ -58,19 +59,22 @@ function DialTribeProvider({
58
59
  markExpired,
59
60
  apiBaseUrl
60
61
  };
61
- return /* @__PURE__ */ jsx(DialTribeContext.Provider, { value, children });
62
+ return /* @__PURE__ */ jsx(DialtribeContext.Provider, { value, children });
62
63
  }
63
- function useDialTribe() {
64
- const context = useContext(DialTribeContext);
64
+ function useDialtribe() {
65
+ const context = useContext(DialtribeContext);
65
66
  if (!context) {
66
67
  throw new Error(
67
- 'useDialTribe must be used within a DialTribeProvider. Wrap your app with <DialTribeProvider sessionToken="sess_xxx">...</DialTribeProvider>'
68
+ 'useDialtribe must be used within a DialtribeProvider. Wrap your app with <DialtribeProvider sessionToken="sess_xxx">...</DialtribeProvider>'
68
69
  );
69
70
  }
70
71
  return context;
71
72
  }
73
+ function useDialtribeOptional() {
74
+ return useContext(DialtribeContext);
75
+ }
72
76
 
73
- // src/client/DialTribeClient.ts
77
+ // src/client/DialtribeClient.ts
74
78
  function getDefaultApiBaseUrl() {
75
79
  if (typeof process !== "undefined" && process.env?.NEXT_PUBLIC_DIALTRIBE_API_URL) {
76
80
  return process.env.NEXT_PUBLIC_DIALTRIBE_API_URL;
@@ -84,18 +88,19 @@ function getEndpoints(baseUrl = DIALTRIBE_API_BASE) {
84
88
  broadcast: (id) => `${baseUrl}/broadcasts/${id}`,
85
89
  contentPlay: `${baseUrl}/content/play`,
86
90
  presignedUrl: `${baseUrl}/media/presigned-url`,
87
- sessionStart: `${baseUrl}/session/start`,
88
- sessionPing: `${baseUrl}/session/ping`
91
+ audienceStart: `${baseUrl}/audiences/start`,
92
+ audiencePing: `${baseUrl}/audiences/ping`,
93
+ sessionPing: `${baseUrl}/sessions/ping`
89
94
  };
90
95
  }
91
96
  var ENDPOINTS = getEndpoints();
92
- var DialTribeClient = class {
97
+ var DialtribeClient = class {
93
98
  constructor(config) {
94
99
  this.config = config;
95
100
  this.endpoints = config.apiBaseUrl ? getEndpoints(config.apiBaseUrl) : ENDPOINTS;
96
101
  }
97
102
  /**
98
- * Make an authenticated request to DialTribe API
103
+ * Make an authenticated request to Dialtribe API
99
104
  *
100
105
  * Automatically:
101
106
  * - Adds Authorization header with session token
@@ -209,7 +214,7 @@ var DialTribeClient = class {
209
214
  * @returns audienceId and optional resumePosition
210
215
  */
211
216
  async startSession(params) {
212
- const response = await this.fetch(this.endpoints.sessionStart, {
217
+ const response = await this.fetch(this.endpoints.audienceStart, {
213
218
  method: "POST",
214
219
  body: JSON.stringify(params)
215
220
  });
@@ -228,7 +233,7 @@ var DialTribeClient = class {
228
233
  * - 3: UNMOUNT
229
234
  */
230
235
  async sendSessionPing(params) {
231
- const response = await this.fetch(this.endpoints.sessionPing, {
236
+ const response = await this.fetch(this.endpoints.audiencePing, {
232
237
  method: "POST",
233
238
  body: JSON.stringify(params)
234
239
  });
@@ -760,7 +765,7 @@ function getErrorMessage(error) {
760
765
  }
761
766
  return "Unable to play media. Please try refreshing the page or contact support if the problem persists.";
762
767
  }
763
- function BroadcastPlayer({
768
+ function DialtribePlayer({
764
769
  broadcast,
765
770
  appId,
766
771
  contentId,
@@ -771,18 +776,18 @@ function BroadcastPlayer({
771
776
  className = "",
772
777
  enableKeyboardShortcuts = false
773
778
  }) {
774
- const { sessionToken, setSessionToken, markExpired, apiBaseUrl } = useDialTribe();
779
+ const { sessionToken, setSessionToken, markExpired, apiBaseUrl } = useDialtribe();
775
780
  const clientRef = useRef(null);
776
781
  if (!clientRef.current && sessionToken) {
777
- clientRef.current = new DialTribeClient({
782
+ clientRef.current = new DialtribeClient({
778
783
  sessionToken,
779
784
  apiBaseUrl,
780
785
  onTokenRefresh: (newToken, expiresAt) => {
781
- debug.log(`[DialTribeClient] Token refreshed, expires at ${expiresAt}`);
786
+ debug.log(`[DialtribeClient] Token refreshed, expires at ${expiresAt}`);
782
787
  setSessionToken(newToken, expiresAt);
783
788
  },
784
789
  onTokenExpired: () => {
785
- debug.error("[DialTribeClient] Token expired");
790
+ debug.error("[DialtribeClient] Token expired");
786
791
  markExpired();
787
792
  }
788
793
  });
@@ -793,7 +798,9 @@ function BroadcastPlayer({
793
798
  const playerRef = useRef(null);
794
799
  const transcriptContainerRef = useRef(null);
795
800
  const activeWordRef = useRef(null);
796
- const [audioElement, setAudioElement] = useState(null);
801
+ const [audioElement, setAudioElement] = useState(
802
+ null
803
+ );
797
804
  const [playing, setPlaying] = useState(false);
798
805
  const [played, setPlayed] = useState(0);
799
806
  const [duration, setDuration] = useState(0);
@@ -805,7 +812,9 @@ function BroadcastPlayer({
805
812
  const [hasEnded, setHasEnded] = useState(false);
806
813
  const [hasStreamEnded, setHasStreamEnded] = useState(false);
807
814
  const [showTranscript, setShowTranscript] = useState(false);
808
- const [transcriptData, setTranscriptData] = useState(null);
815
+ const [transcriptData, setTranscriptData] = useState(
816
+ null
817
+ );
809
818
  const [currentTime, setCurrentTime] = useState(0);
810
819
  const [isLoadingTranscript, setIsLoadingTranscript] = useState(false);
811
820
  const [isLoadingVideo, setIsLoadingVideo] = useState(true);
@@ -833,7 +842,9 @@ function BroadcastPlayer({
833
842
  const refreshPresignedUrl = useCallback(
834
843
  async (fileType) => {
835
844
  if (!broadcast.hash || isRefreshingUrl.current || !client) {
836
- debug.log("[URL Refresh] Skipping refresh - no hash, already refreshing, or no client");
845
+ debug.log(
846
+ "[URL Refresh] Skipping refresh - no hash, already refreshing, or no client"
847
+ );
837
848
  return false;
838
849
  }
839
850
  if (fileType === "hls") {
@@ -841,14 +852,18 @@ function BroadcastPlayer({
841
852
  return false;
842
853
  }
843
854
  isRefreshingUrl.current = true;
844
- debug.log(`[URL Refresh] Refreshing ${fileType} URL for broadcast ${broadcast.id}`);
855
+ debug.log(
856
+ `[URL Refresh] Refreshing ${fileType} URL for broadcast ${broadcast.id}`
857
+ );
845
858
  try {
846
859
  const data = await client.refreshPresignedUrl({
847
860
  broadcastId: broadcast.id,
848
861
  hash: broadcast.hash,
849
862
  fileType
850
863
  });
851
- debug.log(`[URL Refresh] Successfully refreshed URL, expires at ${data.expiresAt}`);
864
+ debug.log(
865
+ `[URL Refresh] Successfully refreshed URL, expires at ${data.expiresAt}`
866
+ );
852
867
  setCurrentPlaybackInfo({ url: data.url, type: fileType });
853
868
  setUrlExpiresAt(new Date(data.expiresAt));
854
869
  if (errorMessage.includes("URL") || errorMessage.includes("session") || errorMessage.includes("refresh")) {
@@ -863,7 +878,9 @@ function BroadcastPlayer({
863
878
  }
864
879
  debug.error("[URL Refresh] Failed to refresh presigned URL:", error);
865
880
  setHasError(true);
866
- setErrorMessage("Unable to refresh media URL. The session may have expired.");
881
+ setErrorMessage(
882
+ "Unable to refresh media URL. The session may have expired."
883
+ );
867
884
  if (onError && error instanceof Error) {
868
885
  onError(error);
869
886
  }
@@ -883,7 +900,8 @@ function BroadcastPlayer({
883
900
  };
884
901
  const initializeTrackingSession = useCallback(async () => {
885
902
  if (!contentId || !appId || !client) return;
886
- if (currentPlaybackInfo?.type === "hls" && broadcast.broadcastStatus === 1) return;
903
+ if (currentPlaybackInfo?.type === "hls" && broadcast.broadcastStatus === 1)
904
+ return;
887
905
  if (hasInitializedSession.current) return;
888
906
  hasInitializedSession.current = true;
889
907
  try {
@@ -906,7 +924,9 @@ function BroadcastPlayer({
906
924
  setAudienceId(data.audienceId);
907
925
  if (data.resumePosition && data.resumePosition > 0 && audioElement) {
908
926
  audioElement.currentTime = data.resumePosition;
909
- debug.log(`[Audience Tracking] Resumed playback at ${data.resumePosition}s`);
927
+ debug.log(
928
+ `[Audience Tracking] Resumed playback at ${data.resumePosition}s`
929
+ );
910
930
  }
911
931
  debug.log("[Audience Tracking] Session initialized:", data.audienceId);
912
932
  } catch (error) {
@@ -915,7 +935,19 @@ function BroadcastPlayer({
915
935
  onError(error);
916
936
  }
917
937
  }
918
- }, [contentId, appId, broadcast.id, broadcast.broadcastStatus, foreignId, foreignTier, sessionId, currentPlaybackInfo?.type, audioElement, client, onError]);
938
+ }, [
939
+ contentId,
940
+ appId,
941
+ broadcast.id,
942
+ broadcast.broadcastStatus,
943
+ foreignId,
944
+ foreignTier,
945
+ sessionId,
946
+ currentPlaybackInfo?.type,
947
+ audioElement,
948
+ client,
949
+ onError
950
+ ]);
919
951
  const sendTrackingPing = useCallback(
920
952
  async (eventType) => {
921
953
  if (!audienceId || !sessionId || !client) return;
@@ -939,15 +971,25 @@ function BroadcastPlayer({
939
971
  return { url: broadcast.hlsPlaylistUrl, type: "hls" };
940
972
  }
941
973
  if (broadcast.hash && broadcast.app?.s3Hash) {
942
- const hlsUrl = buildBroadcastCdnUrl(broadcast.app.s3Hash, broadcast.hash, "index.m3u8");
974
+ const hlsUrl = buildBroadcastCdnUrl(
975
+ broadcast.app.s3Hash,
976
+ broadcast.hash,
977
+ "index.m3u8"
978
+ );
943
979
  return { url: hlsUrl, type: "hls" };
944
980
  }
945
981
  }
946
982
  if (broadcast.recordingMp4Url && broadcast.isVideo && broadcast.hash) {
947
- return { url: buildPlaybackUrl(broadcast.id, broadcast.hash), type: "mp4" };
983
+ return {
984
+ url: buildPlaybackUrl(broadcast.id, broadcast.hash),
985
+ type: "mp4"
986
+ };
948
987
  }
949
988
  if (broadcast.recordingMp3Url && broadcast.hash) {
950
- return { url: buildPlaybackUrl(broadcast.id, broadcast.hash), type: "mp3" };
989
+ return {
990
+ url: buildPlaybackUrl(broadcast.id, broadcast.hash),
991
+ type: "mp3"
992
+ };
951
993
  }
952
994
  if (broadcast.hlsPlaylistUrl) {
953
995
  return { url: broadcast.hlsPlaylistUrl, type: "hls" };
@@ -962,7 +1004,9 @@ function BroadcastPlayer({
962
1004
  if (info && (info.type === "mp4" || info.type === "mp3")) {
963
1005
  const expiresAt = new Date(Date.now() + URL_EXPIRATION_MS);
964
1006
  setUrlExpiresAt(expiresAt);
965
- debug.log(`[URL Refresh] Initial ${info.type} URL expires at ${expiresAt.toISOString()}`);
1007
+ debug.log(
1008
+ `[URL Refresh] Initial ${info.type} URL expires at ${expiresAt.toISOString()}`
1009
+ );
966
1010
  }
967
1011
  if (info) {
968
1012
  setPlaying(true);
@@ -1001,12 +1045,34 @@ function BroadcastPlayer({
1001
1045
  setAudioElement(null);
1002
1046
  setPlaying(true);
1003
1047
  }
1004
- }, [broadcast.broadcastStatus, broadcast.recordingMp3Url, broadcast.mp3Size, broadcast.hash, broadcast.id, currentPlaybackInfo]);
1048
+ }, [
1049
+ broadcast.broadcastStatus,
1050
+ broadcast.recordingMp3Url,
1051
+ broadcast.mp3Size,
1052
+ broadcast.hash,
1053
+ broadcast.id,
1054
+ currentPlaybackInfo
1055
+ ]);
1005
1056
  const playbackUrl = currentPlaybackInfo?.url || null;
1006
1057
  const playbackType = currentPlaybackInfo?.type || null;
1007
1058
  const isAudioOnly = playbackType === "mp3" || !broadcast.isVideo && playbackType !== "mp4";
1008
1059
  const isLiveStream = broadcast.broadcastStatus === 1 && playbackType === "hls" && !hasStreamEnded;
1009
1060
  const wasLiveStream = initialPlaybackTypeRef.current === "hls";
1061
+ const playerConfig = useMemo(
1062
+ () => ({
1063
+ file: {
1064
+ forceHLS: playbackType === "hls",
1065
+ hlsOptions: isLiveStream ? {
1066
+ maxLoadingDelay: 10,
1067
+ minAutoBitrate: 0,
1068
+ lowLatencyMode: true,
1069
+ enableWorker: true
1070
+ } : {}
1071
+ }
1072
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1073
+ }),
1074
+ [playbackType, isLiveStream]
1075
+ );
1010
1076
  const formatTimestamp = (seconds) => {
1011
1077
  if (!seconds || isNaN(seconds) || !isFinite(seconds)) return "00:00:00";
1012
1078
  const hrs = Math.floor(seconds / 3600);
@@ -1077,7 +1143,7 @@ function BroadcastPlayer({
1077
1143
  duration: Math.floor(duration || 0)
1078
1144
  };
1079
1145
  const headers = {
1080
- "Authorization": `Bearer ${sessionToken}`,
1146
+ Authorization: `Bearer ${sessionToken}`,
1081
1147
  "Content-Type": "application/json"
1082
1148
  };
1083
1149
  fetch(ENDPOINTS.sessionPing, {
@@ -1095,7 +1161,9 @@ function BroadcastPlayer({
1095
1161
  setIsLoadingTranscript(true);
1096
1162
  fetch(broadcast.transcriptUrl).then((res) => {
1097
1163
  if (!res.ok) {
1098
- throw new Error(`Failed to fetch transcript: ${res.status} ${res.statusText}`);
1164
+ throw new Error(
1165
+ `Failed to fetch transcript: ${res.status} ${res.statusText}`
1166
+ );
1099
1167
  }
1100
1168
  return res.json();
1101
1169
  }).then((data) => {
@@ -1227,7 +1295,7 @@ function BroadcastPlayer({
1227
1295
  setAudioElement(internalPlayer);
1228
1296
  }
1229
1297
  } catch (error) {
1230
- debug.error("[BroadcastPlayer] Error getting internal player:", error);
1298
+ debug.error("[DialtribePlayer] Error getting internal player:", error);
1231
1299
  }
1232
1300
  };
1233
1301
  useEffect(() => {
@@ -1241,7 +1309,17 @@ function BroadcastPlayer({
1241
1309
  return false;
1242
1310
  };
1243
1311
  if (!findAudioElement()) {
1244
- const retryIntervals = [100, 300, 500, 1e3, 1500, 2e3, 3e3, 4e3, 5e3];
1312
+ const retryIntervals = [
1313
+ 100,
1314
+ 300,
1315
+ 500,
1316
+ 1e3,
1317
+ 1500,
1318
+ 2e3,
1319
+ 3e3,
1320
+ 4e3,
1321
+ 5e3
1322
+ ];
1245
1323
  const timeouts = retryIntervals.map(
1246
1324
  (delay) => setTimeout(() => {
1247
1325
  findAudioElement();
@@ -1299,16 +1377,23 @@ function BroadcastPlayer({
1299
1377
  debug.error("Media playback error:", error);
1300
1378
  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");
1301
1379
  if (isPotentialExpiration && currentPlaybackInfo?.type && !isRefreshingUrl.current) {
1302
- debug.log("[Player Error] Detected potential URL expiration, attempting refresh...");
1380
+ debug.log(
1381
+ "[Player Error] Detected potential URL expiration, attempting refresh..."
1382
+ );
1303
1383
  const currentPosition = audioElement?.currentTime || 0;
1304
1384
  const wasPlaying = playing;
1305
1385
  const fileType = currentPlaybackInfo.type;
1306
1386
  if (fileType !== "mp4" && fileType !== "mp3" && fileType !== "hls") {
1307
- debug.error("[Player Error] Invalid file type, cannot refresh:", fileType);
1387
+ debug.error(
1388
+ "[Player Error] Invalid file type, cannot refresh:",
1389
+ fileType
1390
+ );
1308
1391
  } else {
1309
1392
  const refreshed = await refreshPresignedUrl(fileType);
1310
1393
  if (refreshed) {
1311
- debug.log("[Player Error] URL refreshed successfully, resuming playback");
1394
+ debug.log(
1395
+ "[Player Error] URL refreshed successfully, resuming playback"
1396
+ );
1312
1397
  setTimeout(() => {
1313
1398
  if (audioElement && currentPosition > 0) {
1314
1399
  audioElement.currentTime = currentPosition;
@@ -1356,7 +1441,10 @@ function BroadcastPlayer({
1356
1441
  if (!enableKeyboardShortcuts) return;
1357
1442
  const seekBy = (seconds) => {
1358
1443
  if (!audioElement || duration <= 0) return;
1359
- const newTime = Math.max(0, Math.min(duration, audioElement.currentTime + seconds));
1444
+ const newTime = Math.max(
1445
+ 0,
1446
+ Math.min(duration, audioElement.currentTime + seconds)
1447
+ );
1360
1448
  audioElement.currentTime = newTime;
1361
1449
  setPlayed(newTime / duration);
1362
1450
  };
@@ -1424,7 +1512,17 @@ function BroadcastPlayer({
1424
1512
  };
1425
1513
  window.addEventListener("keydown", handleKeyDown);
1426
1514
  return () => window.removeEventListener("keydown", handleKeyDown);
1427
- }, [enableKeyboardShortcuts, audioElement, duration, playing, muted, isAudioOnly, handlePlayPause, toggleMute, toggleFullscreen]);
1515
+ }, [
1516
+ enableKeyboardShortcuts,
1517
+ audioElement,
1518
+ duration,
1519
+ playing,
1520
+ muted,
1521
+ isAudioOnly,
1522
+ handlePlayPause,
1523
+ toggleMute,
1524
+ toggleFullscreen
1525
+ ]);
1428
1526
  if (currentPlaybackInfo !== null && !playbackUrl) {
1429
1527
  return /* @__PURE__ */ 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: [
1430
1528
  /* @__PURE__ */ jsx("h3", { className: "text-lg font-bold text-black dark:text-white mb-4", children: "Broadcast Unavailable" }),
@@ -1435,452 +1533,914 @@ function BroadcastPlayer({
1435
1533
  return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center p-8", children: /* @__PURE__ */ jsx(LoadingSpinner, { variant: "white", text: "Loading..." }) });
1436
1534
  }
1437
1535
  const hasTranscript = broadcast.transcriptStatus === 2 && transcriptData && (transcriptData.segments && transcriptData.segments.some((s) => s.words && s.words.length > 0) || transcriptData.words && transcriptData.words.length > 0);
1438
- return /* @__PURE__ */ jsxs("div", { className: `bg-black rounded-lg shadow-2xl w-full max-h-full flex flex-col overflow-hidden ${className}`, children: [
1439
- /* @__PURE__ */ 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: [
1440
- /* @__PURE__ */ jsxs("div", { children: [
1441
- /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-white", children: broadcast.streamKeyRecord?.foreignName || "Broadcast" }),
1442
- /* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-400", children: [
1443
- broadcast.isVideo ? "Video" : "Audio",
1444
- " \u2022",
1445
- " ",
1446
- broadcast.broadcastStatus === 1 ? /* @__PURE__ */ jsx("span", { className: "text-red-500 font-semibold", children: "\u{1F534} LIVE" }) : playbackType === "hls" ? /* @__PURE__ */ jsx("span", { className: "text-gray-500 font-semibold", children: "OFFLINE" }) : formatTime(duration)
1447
- ] })
1448
- ] }),
1449
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-3", children: renderClipCreator && playbackType !== "hls" && appId && contentId && duration > 0 && /* @__PURE__ */ jsxs(
1450
- "button",
1451
- {
1452
- onClick: () => setShowClipCreator(true),
1453
- 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",
1454
- title: "Create a clip from this broadcast",
1455
- "aria-label": "Create clip from broadcast",
1456
- children: [
1457
- /* @__PURE__ */ jsxs("svg", { className: "w-3 h-3 md:w-4 md:h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: [
1458
- /* @__PURE__ */ 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" }),
1459
- /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 12a9 9 0 11-18 0 9 9 0 0118 0z" })
1460
- ] }),
1461
- /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: "Create Clip" }),
1462
- /* @__PURE__ */ jsx("span", { className: "sm:hidden", children: "Clip" })
1463
- ]
1464
- }
1465
- ) })
1466
- ] }),
1467
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col md:flex-row flex-1 min-h-0 overflow-hidden", children: [
1468
- /* @__PURE__ */ jsxs("div", { className: "shrink-0 md:shrink md:flex-1 flex flex-col overflow-hidden", children: [
1469
- /* @__PURE__ */ jsxs("div", { className: `relative ${isAudioOnly ? "bg-linear-to-br from-zinc-900 via-zinc-800 to-zinc-900 flex items-stretch" : "bg-black"}`, children: [
1470
- isAudioOnly ? /* @__PURE__ */ jsx("div", { className: "relative cursor-pointer w-full flex flex-col", onClick: handleVideoClick, children: !hasError ? /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", { className: "w-full h-full relative", children: [
1471
- /* @__PURE__ */ jsx(AudioWaveform, { audioElement, isPlaying: isLiveStream ? true : playing, isLive: isLiveStream }),
1472
- isLoadingVideo && !hasError && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/90 flex items-center justify-center z-20", children: /* @__PURE__ */ jsx(LoadingSpinner, { variant: "white" }) }),
1473
- hasEnded && !wasLiveStream && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/50 flex items-center justify-center z-20 pointer-events-auto", children: /* @__PURE__ */ jsxs(
1474
- "button",
1475
- {
1476
- onClick: (e) => {
1477
- e.stopPropagation();
1478
- handleRestart();
1479
- },
1480
- 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",
1481
- "aria-label": "Restart playback from beginning",
1482
- children: [
1483
- /* @__PURE__ */ jsx("svg", { className: "w-6 h-6", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ 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" }) }),
1484
- "Restart"
1485
- ]
1486
- }
1487
- ) })
1488
- ] }) }) : /* @__PURE__ */ jsx("div", { className: "w-full h-full flex items-center justify-center", children: /* @__PURE__ */ jsxs("div", { className: "text-center max-w-md px-4", children: [
1489
- /* @__PURE__ */ jsx("div", { className: "text-6xl mb-4", children: "\u26A0\uFE0F" }),
1490
- /* @__PURE__ */ jsx("h3", { className: "text-xl font-semibold text-white mb-3", children: "Playback Error" }),
1491
- /* @__PURE__ */ jsx("p", { className: "text-gray-300 text-sm mb-6", children: errorMessage }),
1492
- /* @__PURE__ */ jsxs(
1493
- "button",
1494
- {
1495
- onClick: handleRetry,
1496
- 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",
1497
- "aria-label": "Retry playback",
1498
- children: [
1499
- /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ 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" }) }),
1500
- "Retry"
1501
- ]
1502
- }
1503
- )
1504
- ] }) }) }) : /* @__PURE__ */ jsxs("div", { className: "aspect-video relative", children: [
1505
- /* @__PURE__ */ jsx("div", { onClick: handleVideoClick, className: "cursor-pointer", children: /* @__PURE__ */ jsx(
1506
- ReactPlayer,
1507
- {
1508
- ref: playerRef,
1509
- src: playbackUrl || void 0,
1510
- playing,
1511
- volume,
1512
- muted,
1513
- width: "100%",
1514
- height: "100%",
1515
- crossOrigin: "anonymous",
1516
- onReady: handlePlayerReady,
1517
- onTimeUpdate: handleTimeUpdate,
1518
- onLoadedMetadata: handleLoadedMetadata,
1519
- onPlay: handlePlay,
1520
- onPause: handlePause,
1521
- onEnded: handleEnded,
1522
- onError: handleError,
1523
- style: { backgroundColor: "#000" }
1524
- },
1525
- playbackUrl || "no-url"
1526
- ) }),
1527
- isLoadingVideo && !hasError && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/90 flex items-center justify-center", children: /* @__PURE__ */ jsx(LoadingSpinner, { variant: "white" }) }),
1528
- hasEnded && !hasError && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/50 flex items-center justify-center", children: /* @__PURE__ */ jsxs(
1529
- "button",
1530
- {
1531
- onClick: (e) => {
1532
- e.stopPropagation();
1533
- handleRestart();
1534
- },
1535
- 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",
1536
- children: [
1537
- /* @__PURE__ */ jsx("svg", { className: "w-6 h-6", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ 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" }) }),
1538
- "Restart"
1539
- ]
1540
- }
1541
- ) }),
1542
- hasError && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/90 flex items-center justify-center p-8", children: /* @__PURE__ */ jsxs("div", { className: "text-center max-w-md", children: [
1543
- /* @__PURE__ */ jsx("div", { className: "text-6xl mb-4", children: "\u26A0\uFE0F" }),
1544
- /* @__PURE__ */ jsx("h3", { className: "text-xl font-semibold text-white mb-3", children: "Playback Error" }),
1545
- /* @__PURE__ */ jsx("p", { className: "text-gray-300 text-sm mb-6", children: errorMessage }),
1546
- /* @__PURE__ */ jsxs(
1547
- "button",
1548
- {
1549
- onClick: handleRetry,
1550
- 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",
1551
- "aria-label": "Retry playback",
1552
- children: [
1553
- /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ 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" }) }),
1554
- "Retry"
1555
- ]
1556
- }
1557
- )
1558
- ] }) })
1536
+ const playerContent = /* @__PURE__ */ jsxs(
1537
+ "div",
1538
+ {
1539
+ className: `bg-black rounded-lg shadow-2xl w-full max-h-full flex flex-col overflow-hidden ${className}`,
1540
+ children: [
1541
+ /* @__PURE__ */ 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: [
1542
+ /* @__PURE__ */ jsxs("div", { children: [
1543
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-white", children: broadcast.streamKeyRecord?.foreignName || "Broadcast" }),
1544
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-400", children: [
1545
+ broadcast.isVideo ? "Video" : "Audio",
1546
+ " \u2022",
1547
+ " ",
1548
+ broadcast.broadcastStatus === 1 ? /* @__PURE__ */ jsx("span", { className: "text-red-500 font-semibold", children: "\u{1F534} LIVE" }) : playbackType === "hls" ? /* @__PURE__ */ jsx("span", { className: "text-gray-500 font-semibold", children: "OFFLINE" }) : formatTime(duration)
1549
+ ] })
1559
1550
  ] }),
1560
- isAudioOnly && /* @__PURE__ */ jsx("div", { className: "hidden", children: /* @__PURE__ */ jsx(
1561
- ReactPlayer,
1551
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-3", children: renderClipCreator && playbackType !== "hls" && appId && contentId && duration > 0 && /* @__PURE__ */ jsxs(
1552
+ "button",
1562
1553
  {
1563
- ref: playerRef,
1564
- src: playbackUrl || void 0,
1565
- playing,
1566
- volume,
1567
- muted,
1568
- width: "0",
1569
- height: "0",
1570
- crossOrigin: "anonymous",
1571
- onReady: handlePlayerReady,
1572
- onTimeUpdate: handleTimeUpdate,
1573
- onLoadedMetadata: handleLoadedMetadata,
1574
- onPlay: handlePlay,
1575
- onPause: handlePause,
1576
- onEnded: handleEnded,
1577
- onError: handleError
1578
- },
1579
- playbackUrl || "no-url"
1554
+ onClick: () => setShowClipCreator(true),
1555
+ 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",
1556
+ title: "Create a clip from this broadcast",
1557
+ "aria-label": "Create clip from broadcast",
1558
+ children: [
1559
+ /* @__PURE__ */ jsxs(
1560
+ "svg",
1561
+ {
1562
+ className: "w-3 h-3 md:w-4 md:h-4",
1563
+ fill: "none",
1564
+ stroke: "currentColor",
1565
+ viewBox: "0 0 24 24",
1566
+ children: [
1567
+ /* @__PURE__ */ jsx(
1568
+ "path",
1569
+ {
1570
+ strokeLinecap: "round",
1571
+ strokeLinejoin: "round",
1572
+ strokeWidth: 2,
1573
+ 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"
1574
+ }
1575
+ ),
1576
+ /* @__PURE__ */ jsx(
1577
+ "path",
1578
+ {
1579
+ strokeLinecap: "round",
1580
+ strokeLinejoin: "round",
1581
+ strokeWidth: 2,
1582
+ d: "M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
1583
+ }
1584
+ )
1585
+ ]
1586
+ }
1587
+ ),
1588
+ /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: "Create Clip" }),
1589
+ /* @__PURE__ */ jsx("span", { className: "sm:hidden", children: "Clip" })
1590
+ ]
1591
+ }
1580
1592
  ) })
1581
1593
  ] }),
1582
- !hasError && !isLiveStream && (wasLiveStream ? parseInt(broadcast.mp3Size || "0") > 0 || parseInt(broadcast.mp4Size || "0") > 0 ? /* @__PURE__ */ jsxs("div", { className: "bg-zinc-900/50 backdrop-blur-sm px-4 md:px-6 py-3 md:py-4 rounded-b-lg", children: [
1583
- /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
1584
- /* @__PURE__ */ jsx(
1585
- "input",
1594
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col md:flex-row flex-1 min-h-0 overflow-hidden", children: [
1595
+ /* @__PURE__ */ jsxs("div", { className: "shrink-0 md:shrink md:flex-1 flex flex-col overflow-hidden", children: [
1596
+ /* @__PURE__ */ jsxs(
1597
+ "div",
1586
1598
  {
1587
- type: "range",
1588
- min: 0,
1589
- max: 0.999999,
1590
- step: "any",
1591
- value: played || 0,
1592
- onMouseDown: handleSeekMouseDown,
1593
- onMouseUp: handleSeekMouseUp,
1594
- onTouchStart: handleSeekTouchStart,
1595
- onTouchEnd: handleSeekTouchEnd,
1596
- onChange: handleSeekChange,
1597
- className: "w-full h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer slider",
1598
- "aria-label": "Seek position",
1599
- "aria-valuemin": 0,
1600
- "aria-valuemax": duration,
1601
- "aria-valuenow": played * duration,
1602
- "aria-valuetext": `${formatTime(played * duration)} of ${formatTime(duration)}`,
1603
- role: "slider"
1599
+ className: `relative ${isAudioOnly ? "bg-linear-to-br from-zinc-900 via-zinc-800 to-zinc-900 flex items-stretch" : "bg-black"}`,
1600
+ children: [
1601
+ isAudioOnly ? /* @__PURE__ */ jsx(
1602
+ "div",
1603
+ {
1604
+ className: "relative cursor-pointer w-full flex flex-col",
1605
+ onClick: handleVideoClick,
1606
+ children: !hasError ? /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", { className: "w-full h-full relative", children: [
1607
+ /* @__PURE__ */ jsx(
1608
+ AudioWaveform,
1609
+ {
1610
+ audioElement,
1611
+ isPlaying: isLiveStream ? true : playing,
1612
+ isLive: isLiveStream
1613
+ }
1614
+ ),
1615
+ isLoadingVideo && !hasError && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/90 flex items-center justify-center z-20", children: /* @__PURE__ */ jsx(LoadingSpinner, { variant: "white" }) }),
1616
+ hasEnded && !wasLiveStream && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/50 flex items-center justify-center z-20 pointer-events-auto", children: /* @__PURE__ */ jsxs(
1617
+ "button",
1618
+ {
1619
+ onClick: (e) => {
1620
+ e.stopPropagation();
1621
+ handleRestart();
1622
+ },
1623
+ 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",
1624
+ "aria-label": "Restart playback from beginning",
1625
+ children: [
1626
+ /* @__PURE__ */ jsx(
1627
+ "svg",
1628
+ {
1629
+ className: "w-6 h-6",
1630
+ fill: "currentColor",
1631
+ viewBox: "0 0 20 20",
1632
+ children: /* @__PURE__ */ jsx(
1633
+ "path",
1634
+ {
1635
+ fillRule: "evenodd",
1636
+ 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",
1637
+ clipRule: "evenodd"
1638
+ }
1639
+ )
1640
+ }
1641
+ ),
1642
+ "Restart"
1643
+ ]
1644
+ }
1645
+ ) })
1646
+ ] }) }) : /* @__PURE__ */ jsx("div", { className: "w-full h-full flex items-center justify-center", children: /* @__PURE__ */ jsxs("div", { className: "text-center max-w-md px-4", children: [
1647
+ /* @__PURE__ */ jsx("div", { className: "text-6xl mb-4", children: "\u26A0\uFE0F" }),
1648
+ /* @__PURE__ */ jsx("h3", { className: "text-xl font-semibold text-white mb-3", children: "Playback Error" }),
1649
+ /* @__PURE__ */ jsx("p", { className: "text-gray-300 text-sm mb-6", children: errorMessage }),
1650
+ /* @__PURE__ */ jsxs(
1651
+ "button",
1652
+ {
1653
+ onClick: handleRetry,
1654
+ 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",
1655
+ "aria-label": "Retry playback",
1656
+ children: [
1657
+ /* @__PURE__ */ jsx(
1658
+ "svg",
1659
+ {
1660
+ className: "w-5 h-5",
1661
+ fill: "none",
1662
+ stroke: "currentColor",
1663
+ viewBox: "0 0 24 24",
1664
+ children: /* @__PURE__ */ jsx(
1665
+ "path",
1666
+ {
1667
+ strokeLinecap: "round",
1668
+ strokeLinejoin: "round",
1669
+ strokeWidth: 2,
1670
+ 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"
1671
+ }
1672
+ )
1673
+ }
1674
+ ),
1675
+ "Retry"
1676
+ ]
1677
+ }
1678
+ )
1679
+ ] }) })
1680
+ }
1681
+ ) : /* @__PURE__ */ jsxs("div", { className: "aspect-video relative", children: [
1682
+ /* @__PURE__ */ jsx("div", { onClick: handleVideoClick, className: "cursor-pointer", children: /* @__PURE__ */ jsx(
1683
+ ReactPlayer,
1684
+ {
1685
+ ref: playerRef,
1686
+ src: playbackUrl || void 0,
1687
+ playing,
1688
+ volume,
1689
+ muted,
1690
+ width: "100%",
1691
+ height: "100%",
1692
+ crossOrigin: "anonymous",
1693
+ config: playerConfig,
1694
+ onReady: handlePlayerReady,
1695
+ onTimeUpdate: handleTimeUpdate,
1696
+ onLoadedMetadata: handleLoadedMetadata,
1697
+ onPlay: handlePlay,
1698
+ onPause: handlePause,
1699
+ onEnded: handleEnded,
1700
+ onError: handleError,
1701
+ style: { backgroundColor: "#000" }
1702
+ },
1703
+ playbackUrl || "no-url"
1704
+ ) }),
1705
+ isLoadingVideo && !hasError && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/90 flex items-center justify-center", children: /* @__PURE__ */ jsx(LoadingSpinner, { variant: "white" }) }),
1706
+ hasEnded && !hasError && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/50 flex items-center justify-center", children: /* @__PURE__ */ jsxs(
1707
+ "button",
1708
+ {
1709
+ onClick: (e) => {
1710
+ e.stopPropagation();
1711
+ handleRestart();
1712
+ },
1713
+ 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",
1714
+ "aria-label": "Restart playback from beginning",
1715
+ children: [
1716
+ /* @__PURE__ */ jsx(
1717
+ "svg",
1718
+ {
1719
+ className: "w-6 h-6",
1720
+ fill: "currentColor",
1721
+ viewBox: "0 0 20 20",
1722
+ "aria-hidden": "true",
1723
+ children: /* @__PURE__ */ jsx(
1724
+ "path",
1725
+ {
1726
+ fillRule: "evenodd",
1727
+ 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",
1728
+ clipRule: "evenodd"
1729
+ }
1730
+ )
1731
+ }
1732
+ ),
1733
+ "Restart"
1734
+ ]
1735
+ }
1736
+ ) }),
1737
+ hasError && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/90 flex items-center justify-center p-8", children: /* @__PURE__ */ jsxs("div", { className: "text-center max-w-md", children: [
1738
+ /* @__PURE__ */ jsx("div", { className: "text-6xl mb-4", children: "\u26A0\uFE0F" }),
1739
+ /* @__PURE__ */ jsx("h3", { className: "text-xl font-semibold text-white mb-3", children: "Playback Error" }),
1740
+ /* @__PURE__ */ jsx("p", { className: "text-gray-300 text-sm mb-6", children: errorMessage }),
1741
+ /* @__PURE__ */ jsxs(
1742
+ "button",
1743
+ {
1744
+ onClick: handleRetry,
1745
+ 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",
1746
+ "aria-label": "Retry playback",
1747
+ children: [
1748
+ /* @__PURE__ */ jsx(
1749
+ "svg",
1750
+ {
1751
+ className: "w-5 h-5",
1752
+ fill: "none",
1753
+ stroke: "currentColor",
1754
+ viewBox: "0 0 24 24",
1755
+ children: /* @__PURE__ */ jsx(
1756
+ "path",
1757
+ {
1758
+ strokeLinecap: "round",
1759
+ strokeLinejoin: "round",
1760
+ strokeWidth: 2,
1761
+ 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"
1762
+ }
1763
+ )
1764
+ }
1765
+ ),
1766
+ "Retry"
1767
+ ]
1768
+ }
1769
+ )
1770
+ ] }) })
1771
+ ] }),
1772
+ isAudioOnly && /* @__PURE__ */ jsx("div", { className: "hidden", children: /* @__PURE__ */ jsx(
1773
+ ReactPlayer,
1774
+ {
1775
+ ref: playerRef,
1776
+ src: playbackUrl || void 0,
1777
+ playing,
1778
+ volume,
1779
+ muted,
1780
+ width: "0",
1781
+ height: "0",
1782
+ crossOrigin: "anonymous",
1783
+ config: playerConfig,
1784
+ onReady: handlePlayerReady,
1785
+ onTimeUpdate: handleTimeUpdate,
1786
+ onLoadedMetadata: handleLoadedMetadata,
1787
+ onPlay: handlePlay,
1788
+ onPause: handlePause,
1789
+ onEnded: handleEnded,
1790
+ onError: handleError
1791
+ },
1792
+ playbackUrl || "no-url"
1793
+ ) })
1794
+ ]
1604
1795
  }
1605
1796
  ),
1606
- /* @__PURE__ */ jsxs("div", { className: "flex justify-between text-xs text-gray-400 mt-1", children: [
1607
- /* @__PURE__ */ jsx("span", { children: formatTime((played || 0) * duration) }),
1608
- /* @__PURE__ */ jsx("span", { children: formatTime(duration) })
1609
- ] })
1610
- ] }),
1611
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
1612
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
1613
- /* @__PURE__ */ jsx(
1614
- "button",
1615
- {
1616
- onClick: handlePlayPause,
1617
- className: "text-white hover:text-blue-400 transition-colors",
1618
- title: playing ? "Pause" : "Play",
1619
- "aria-label": playing ? "Pause" : "Play",
1620
- "aria-pressed": playing,
1621
- children: playing ? /* @__PURE__ */ jsx("svg", { className: "w-8 h-8", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ 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__ */ jsx("svg", { className: "w-8 h-8", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ 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" }) })
1622
- }
1623
- ),
1624
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1797
+ !hasError && !isLiveStream && (wasLiveStream ? parseInt(broadcast.mp3Size || "0") > 0 || parseInt(broadcast.mp4Size || "0") > 0 ? /* @__PURE__ */ jsxs("div", { className: "bg-zinc-900/50 backdrop-blur-sm px-4 md:px-6 py-3 md:py-4 rounded-b-lg", children: [
1798
+ /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
1625
1799
  /* @__PURE__ */ jsx(
1626
- "button",
1800
+ "input",
1627
1801
  {
1628
- onClick: toggleMute,
1629
- className: "text-white hover:text-blue-400 transition-colors",
1630
- title: muted ? "Unmute" : "Mute",
1631
- "aria-label": muted ? "Unmute" : "Mute",
1632
- "aria-pressed": muted,
1633
- children: muted || volume === 0 ? /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ 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__ */ jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ 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" }) })
1802
+ type: "range",
1803
+ min: 0,
1804
+ max: 0.999999,
1805
+ step: "any",
1806
+ value: played || 0,
1807
+ onMouseDown: handleSeekMouseDown,
1808
+ onMouseUp: handleSeekMouseUp,
1809
+ onTouchStart: handleSeekTouchStart,
1810
+ onTouchEnd: handleSeekTouchEnd,
1811
+ onChange: handleSeekChange,
1812
+ className: "w-full h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer slider",
1813
+ "aria-label": "Seek position",
1814
+ "aria-valuemin": 0,
1815
+ "aria-valuemax": duration,
1816
+ "aria-valuenow": played * duration,
1817
+ "aria-valuetext": `${formatTime(
1818
+ played * duration
1819
+ )} of ${formatTime(duration)}`,
1820
+ role: "slider"
1634
1821
  }
1635
1822
  ),
1823
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-between text-xs text-gray-400 mt-1", children: [
1824
+ /* @__PURE__ */ jsx("span", { children: formatTime((played || 0) * duration) }),
1825
+ /* @__PURE__ */ jsx("span", { children: formatTime(duration) })
1826
+ ] })
1827
+ ] }),
1828
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
1829
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
1830
+ /* @__PURE__ */ jsx(
1831
+ "button",
1832
+ {
1833
+ onClick: handlePlayPause,
1834
+ className: "text-white hover:text-blue-400 transition-colors",
1835
+ title: playing ? "Pause" : "Play",
1836
+ "aria-label": playing ? "Pause" : "Play",
1837
+ "aria-pressed": playing,
1838
+ children: playing ? /* @__PURE__ */ jsx(
1839
+ "svg",
1840
+ {
1841
+ className: "w-8 h-8",
1842
+ fill: "currentColor",
1843
+ viewBox: "0 0 20 20",
1844
+ "aria-hidden": "true",
1845
+ children: /* @__PURE__ */ jsx(
1846
+ "path",
1847
+ {
1848
+ fillRule: "evenodd",
1849
+ 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",
1850
+ clipRule: "evenodd"
1851
+ }
1852
+ )
1853
+ }
1854
+ ) : /* @__PURE__ */ jsx(
1855
+ "svg",
1856
+ {
1857
+ className: "w-8 h-8",
1858
+ fill: "currentColor",
1859
+ viewBox: "0 0 20 20",
1860
+ "aria-hidden": "true",
1861
+ children: /* @__PURE__ */ jsx(
1862
+ "path",
1863
+ {
1864
+ fillRule: "evenodd",
1865
+ 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",
1866
+ clipRule: "evenodd"
1867
+ }
1868
+ )
1869
+ }
1870
+ )
1871
+ }
1872
+ ),
1873
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1874
+ /* @__PURE__ */ jsx(
1875
+ "button",
1876
+ {
1877
+ onClick: toggleMute,
1878
+ className: "text-white hover:text-blue-400 transition-colors",
1879
+ title: muted ? "Unmute" : "Mute",
1880
+ "aria-label": muted ? "Unmute" : "Mute",
1881
+ "aria-pressed": muted,
1882
+ children: muted || volume === 0 ? /* @__PURE__ */ jsx(
1883
+ "svg",
1884
+ {
1885
+ className: "w-5 h-5",
1886
+ fill: "currentColor",
1887
+ viewBox: "0 0 20 20",
1888
+ children: /* @__PURE__ */ jsx(
1889
+ "path",
1890
+ {
1891
+ fillRule: "evenodd",
1892
+ 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",
1893
+ clipRule: "evenodd"
1894
+ }
1895
+ )
1896
+ }
1897
+ ) : /* @__PURE__ */ jsx(
1898
+ "svg",
1899
+ {
1900
+ className: "w-5 h-5",
1901
+ fill: "currentColor",
1902
+ viewBox: "0 0 20 20",
1903
+ children: /* @__PURE__ */ jsx(
1904
+ "path",
1905
+ {
1906
+ fillRule: "evenodd",
1907
+ 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",
1908
+ clipRule: "evenodd"
1909
+ }
1910
+ )
1911
+ }
1912
+ )
1913
+ }
1914
+ ),
1915
+ /* @__PURE__ */ jsx(
1916
+ "input",
1917
+ {
1918
+ type: "range",
1919
+ min: 0,
1920
+ max: 1,
1921
+ step: 0.01,
1922
+ value: muted ? 0 : volume || 1,
1923
+ onChange: handleVolumeChange,
1924
+ className: "w-20 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer slider",
1925
+ "aria-label": "Volume",
1926
+ "aria-valuemin": 0,
1927
+ "aria-valuemax": 100,
1928
+ "aria-valuenow": muted ? 0 : Math.round(volume * 100),
1929
+ "aria-valuetext": muted ? "Muted" : `${Math.round(volume * 100)}%`,
1930
+ role: "slider"
1931
+ }
1932
+ )
1933
+ ] })
1934
+ ] }),
1935
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1936
+ !isLiveStream && broadcast.hash && (broadcast.recordingMp4Url || broadcast.recordingMp3Url) && /* @__PURE__ */ jsx(
1937
+ "button",
1938
+ {
1939
+ onClick: () => {
1940
+ const downloadUrl = buildPlaybackUrl(
1941
+ broadcast.id,
1942
+ broadcast.hash,
1943
+ "download"
1944
+ );
1945
+ window.open(downloadUrl, "_blank");
1946
+ },
1947
+ className: "text-white hover:text-blue-400 transition-colors",
1948
+ title: "Download Recording",
1949
+ "aria-label": "Download recording",
1950
+ children: /* @__PURE__ */ jsx(
1951
+ "svg",
1952
+ {
1953
+ className: "w-5 h-5",
1954
+ fill: "none",
1955
+ stroke: "currentColor",
1956
+ viewBox: "0 0 24 24",
1957
+ children: /* @__PURE__ */ jsx(
1958
+ "path",
1959
+ {
1960
+ strokeLinecap: "round",
1961
+ strokeLinejoin: "round",
1962
+ strokeWidth: 2,
1963
+ d: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
1964
+ }
1965
+ )
1966
+ }
1967
+ )
1968
+ }
1969
+ ),
1970
+ !isAudioOnly && /* @__PURE__ */ jsx(
1971
+ "button",
1972
+ {
1973
+ onClick: toggleFullscreen,
1974
+ className: "text-white hover:text-blue-400 transition-colors",
1975
+ title: "Fullscreen",
1976
+ "aria-label": "Toggle fullscreen",
1977
+ children: /* @__PURE__ */ jsx(
1978
+ "svg",
1979
+ {
1980
+ className: "w-5 h-5",
1981
+ fill: "currentColor",
1982
+ viewBox: "0 0 20 20",
1983
+ children: /* @__PURE__ */ jsx(
1984
+ "path",
1985
+ {
1986
+ fillRule: "evenodd",
1987
+ 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",
1988
+ clipRule: "evenodd"
1989
+ }
1990
+ )
1991
+ }
1992
+ )
1993
+ }
1994
+ ),
1995
+ hasTranscript && /* @__PURE__ */ jsx(
1996
+ "button",
1997
+ {
1998
+ onClick: () => setShowTranscript(!showTranscript),
1999
+ className: `transition-colors ${showTranscript ? "text-blue-400" : "text-white hover:text-blue-400"}`,
2000
+ title: showTranscript ? "Hide Transcript" : "Show Transcript",
2001
+ "aria-label": showTranscript ? "Hide transcript" : "Show transcript",
2002
+ "aria-pressed": showTranscript,
2003
+ children: /* @__PURE__ */ jsx(
2004
+ "svg",
2005
+ {
2006
+ className: "w-5 h-5",
2007
+ fill: "none",
2008
+ stroke: "currentColor",
2009
+ viewBox: "0 0 24 24",
2010
+ children: /* @__PURE__ */ jsx(
2011
+ "path",
2012
+ {
2013
+ strokeLinecap: "round",
2014
+ strokeLinejoin: "round",
2015
+ strokeWidth: 2,
2016
+ 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"
2017
+ }
2018
+ )
2019
+ }
2020
+ )
2021
+ }
2022
+ )
2023
+ ] })
2024
+ ] })
2025
+ ] }) : null : /* @__PURE__ */ jsxs("div", { className: "bg-zinc-900/50 backdrop-blur-sm px-4 md:px-6 py-3 md:py-4 rounded-b-lg", children: [
2026
+ /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
1636
2027
  /* @__PURE__ */ jsx(
1637
2028
  "input",
1638
2029
  {
1639
2030
  type: "range",
1640
2031
  min: 0,
1641
- max: 1,
1642
- step: 0.01,
1643
- value: muted ? 0 : volume || 1,
1644
- onChange: handleVolumeChange,
1645
- className: "w-20 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer slider",
1646
- "aria-label": "Volume",
2032
+ max: 0.999999,
2033
+ step: "any",
2034
+ value: played || 0,
2035
+ onMouseDown: handleSeekMouseDown,
2036
+ onMouseUp: handleSeekMouseUp,
2037
+ onTouchStart: handleSeekTouchStart,
2038
+ onTouchEnd: handleSeekTouchEnd,
2039
+ onChange: handleSeekChange,
2040
+ className: "w-full h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer slider",
2041
+ "aria-label": "Seek position",
1647
2042
  "aria-valuemin": 0,
1648
- "aria-valuemax": 100,
1649
- "aria-valuenow": muted ? 0 : Math.round(volume * 100),
1650
- "aria-valuetext": muted ? "Muted" : `${Math.round(volume * 100)}%`,
2043
+ "aria-valuemax": duration,
2044
+ "aria-valuenow": played * duration,
2045
+ "aria-valuetext": `${formatTime(
2046
+ played * duration
2047
+ )} of ${formatTime(duration)}`,
1651
2048
  role: "slider"
1652
2049
  }
1653
- )
2050
+ ),
2051
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-between text-xs text-gray-400 mt-1", children: [
2052
+ /* @__PURE__ */ jsx("span", { children: formatTime((played || 0) * duration) }),
2053
+ /* @__PURE__ */ jsx("span", { children: formatTime(duration) })
2054
+ ] })
2055
+ ] }),
2056
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
2057
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
2058
+ /* @__PURE__ */ jsx(
2059
+ "button",
2060
+ {
2061
+ onClick: handlePlayPause,
2062
+ className: "text-white hover:text-blue-400 transition-colors",
2063
+ title: playing ? "Pause" : "Play",
2064
+ "aria-label": playing ? "Pause" : "Play",
2065
+ "aria-pressed": playing,
2066
+ children: playing ? /* @__PURE__ */ jsx(
2067
+ "svg",
2068
+ {
2069
+ className: "w-8 h-8",
2070
+ fill: "currentColor",
2071
+ viewBox: "0 0 20 20",
2072
+ "aria-hidden": "true",
2073
+ children: /* @__PURE__ */ jsx(
2074
+ "path",
2075
+ {
2076
+ fillRule: "evenodd",
2077
+ 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",
2078
+ clipRule: "evenodd"
2079
+ }
2080
+ )
2081
+ }
2082
+ ) : /* @__PURE__ */ jsx(
2083
+ "svg",
2084
+ {
2085
+ className: "w-8 h-8",
2086
+ fill: "currentColor",
2087
+ viewBox: "0 0 20 20",
2088
+ "aria-hidden": "true",
2089
+ children: /* @__PURE__ */ jsx(
2090
+ "path",
2091
+ {
2092
+ fillRule: "evenodd",
2093
+ 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",
2094
+ clipRule: "evenodd"
2095
+ }
2096
+ )
2097
+ }
2098
+ )
2099
+ }
2100
+ ),
2101
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2102
+ /* @__PURE__ */ jsx(
2103
+ "button",
2104
+ {
2105
+ onClick: toggleMute,
2106
+ className: "text-white hover:text-blue-400 transition-colors",
2107
+ title: muted ? "Unmute" : "Mute",
2108
+ "aria-label": muted ? "Unmute" : "Mute",
2109
+ "aria-pressed": muted,
2110
+ children: muted || volume === 0 ? /* @__PURE__ */ jsx(
2111
+ "svg",
2112
+ {
2113
+ className: "w-5 h-5",
2114
+ fill: "currentColor",
2115
+ viewBox: "0 0 20 20",
2116
+ "aria-hidden": "true",
2117
+ children: /* @__PURE__ */ jsx(
2118
+ "path",
2119
+ {
2120
+ fillRule: "evenodd",
2121
+ 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",
2122
+ clipRule: "evenodd"
2123
+ }
2124
+ )
2125
+ }
2126
+ ) : /* @__PURE__ */ jsx(
2127
+ "svg",
2128
+ {
2129
+ className: "w-5 h-5",
2130
+ fill: "currentColor",
2131
+ viewBox: "0 0 20 20",
2132
+ "aria-hidden": "true",
2133
+ children: /* @__PURE__ */ jsx(
2134
+ "path",
2135
+ {
2136
+ fillRule: "evenodd",
2137
+ 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",
2138
+ clipRule: "evenodd"
2139
+ }
2140
+ )
2141
+ }
2142
+ )
2143
+ }
2144
+ ),
2145
+ /* @__PURE__ */ jsx(
2146
+ "input",
2147
+ {
2148
+ type: "range",
2149
+ min: 0,
2150
+ max: 1,
2151
+ step: 0.01,
2152
+ value: muted ? 0 : volume || 1,
2153
+ onChange: handleVolumeChange,
2154
+ className: "w-20 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer slider",
2155
+ "aria-label": "Volume",
2156
+ "aria-valuemin": 0,
2157
+ "aria-valuemax": 100,
2158
+ "aria-valuenow": muted ? 0 : Math.round(volume * 100),
2159
+ "aria-valuetext": muted ? "Muted" : `${Math.round(volume * 100)}%`,
2160
+ role: "slider"
2161
+ }
2162
+ )
2163
+ ] })
2164
+ ] }),
2165
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2166
+ !isAudioOnly && /* @__PURE__ */ jsx(
2167
+ "button",
2168
+ {
2169
+ onClick: toggleFullscreen,
2170
+ className: "text-white hover:text-blue-400 transition-colors",
2171
+ title: "Toggle fullscreen",
2172
+ "aria-label": "Toggle fullscreen",
2173
+ children: /* @__PURE__ */ jsx(
2174
+ "svg",
2175
+ {
2176
+ className: "w-5 h-5",
2177
+ fill: "currentColor",
2178
+ viewBox: "0 0 20 20",
2179
+ "aria-hidden": "true",
2180
+ children: /* @__PURE__ */ jsx(
2181
+ "path",
2182
+ {
2183
+ fillRule: "evenodd",
2184
+ 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",
2185
+ clipRule: "evenodd"
2186
+ }
2187
+ )
2188
+ }
2189
+ )
2190
+ }
2191
+ ),
2192
+ hasTranscript && /* @__PURE__ */ jsx(
2193
+ "button",
2194
+ {
2195
+ onClick: () => setShowTranscript(!showTranscript),
2196
+ className: `transition-colors ${showTranscript ? "text-blue-400" : "text-white hover:text-blue-400"}`,
2197
+ title: showTranscript ? "Hide transcript" : "Show transcript",
2198
+ "aria-label": showTranscript ? "Hide transcript" : "Show transcript",
2199
+ "aria-pressed": showTranscript,
2200
+ children: /* @__PURE__ */ jsx(
2201
+ "svg",
2202
+ {
2203
+ className: "w-5 h-5",
2204
+ fill: "none",
2205
+ stroke: "currentColor",
2206
+ viewBox: "0 0 24 24",
2207
+ "aria-hidden": "true",
2208
+ children: /* @__PURE__ */ jsx(
2209
+ "path",
2210
+ {
2211
+ strokeLinecap: "round",
2212
+ strokeLinejoin: "round",
2213
+ strokeWidth: 2,
2214
+ 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"
2215
+ }
2216
+ )
2217
+ }
2218
+ )
2219
+ }
2220
+ )
2221
+ ] })
1654
2222
  ] })
1655
- ] }),
1656
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1657
- !isLiveStream && broadcast.hash && (broadcast.recordingMp4Url || broadcast.recordingMp3Url) && /* @__PURE__ */ jsx(
1658
- "button",
1659
- {
1660
- onClick: () => {
1661
- const downloadUrl = buildPlaybackUrl(broadcast.id, broadcast.hash, "download");
1662
- window.open(downloadUrl, "_blank");
1663
- },
1664
- className: "text-white hover:text-blue-400 transition-colors",
1665
- title: "Download Recording",
1666
- "aria-label": "Download recording",
1667
- children: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ 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" }) })
1668
- }
1669
- ),
1670
- !isAudioOnly && /* @__PURE__ */ jsx(
1671
- "button",
1672
- {
1673
- onClick: toggleFullscreen,
1674
- className: "text-white hover:text-blue-400 transition-colors",
1675
- title: "Fullscreen",
1676
- "aria-label": "Toggle fullscreen",
1677
- children: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ 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" }) })
1678
- }
1679
- ),
1680
- hasTranscript && /* @__PURE__ */ jsx(
1681
- "button",
1682
- {
1683
- onClick: () => setShowTranscript(!showTranscript),
1684
- className: `transition-colors ${showTranscript ? "text-blue-400" : "text-white hover:text-blue-400"}`,
1685
- title: showTranscript ? "Hide Transcript" : "Show Transcript",
1686
- "aria-label": showTranscript ? "Hide transcript" : "Show transcript",
1687
- "aria-pressed": showTranscript,
1688
- children: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ 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" }) })
1689
- }
1690
- )
1691
- ] })
1692
- ] })
1693
- ] }) : null : /* @__PURE__ */ jsxs("div", { className: "bg-zinc-900/50 backdrop-blur-sm px-4 md:px-6 py-3 md:py-4 rounded-b-lg", children: [
1694
- /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
1695
- /* @__PURE__ */ jsx(
1696
- "input",
1697
- {
1698
- type: "range",
1699
- min: 0,
1700
- max: 0.999999,
1701
- step: "any",
1702
- value: played || 0,
1703
- onMouseDown: handleSeekMouseDown,
1704
- onMouseUp: handleSeekMouseUp,
1705
- onTouchStart: handleSeekTouchStart,
1706
- onTouchEnd: handleSeekTouchEnd,
1707
- onChange: handleSeekChange,
1708
- className: "w-full h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer slider"
1709
- }
1710
- ),
1711
- /* @__PURE__ */ jsxs("div", { className: "flex justify-between text-xs text-gray-400 mt-1", children: [
1712
- /* @__PURE__ */ jsx("span", { children: formatTime((played || 0) * duration) }),
1713
- /* @__PURE__ */ jsx("span", { children: formatTime(duration) })
1714
- ] })
2223
+ ] }))
1715
2224
  ] }),
1716
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
1717
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
1718
- /* @__PURE__ */ jsx("button", { onClick: handlePlayPause, className: "text-white hover:text-blue-400 transition-colors", title: playing ? "Pause" : "Play", children: playing ? /* @__PURE__ */ jsx("svg", { className: "w-8 h-8", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ 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__ */ jsx("svg", { className: "w-8 h-8", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ 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" }) }) }),
2225
+ showTranscript && hasTranscript && /* @__PURE__ */ 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: [
2226
+ /* @__PURE__ */ jsx("div", { className: "px-4 py-3 border-b border-zinc-800 bg-zinc-900/50 shrink-0", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
1719
2227
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1720
- /* @__PURE__ */ jsx("button", { onClick: toggleMute, className: "text-white hover:text-blue-400 transition-colors", title: muted ? "Unmute" : "Mute", children: muted || volume === 0 ? /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ 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__ */ jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ 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" }) }) }),
1721
2228
  /* @__PURE__ */ jsx(
1722
- "input",
2229
+ "svg",
1723
2230
  {
1724
- type: "range",
1725
- min: 0,
1726
- max: 1,
1727
- step: 0.01,
1728
- value: muted ? 0 : volume || 1,
1729
- onChange: handleVolumeChange,
1730
- className: "w-20 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer slider"
2231
+ className: "w-5 h-5 text-green-400",
2232
+ fill: "none",
2233
+ stroke: "currentColor",
2234
+ viewBox: "0 0 24 24",
2235
+ children: /* @__PURE__ */ jsx(
2236
+ "path",
2237
+ {
2238
+ strokeLinecap: "round",
2239
+ strokeLinejoin: "round",
2240
+ strokeWidth: 2,
2241
+ 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"
2242
+ }
2243
+ )
1731
2244
  }
1732
- )
1733
- ] })
1734
- ] }),
1735
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1736
- !isAudioOnly && /* @__PURE__ */ jsx("button", { onClick: toggleFullscreen, className: "text-white hover:text-blue-400 transition-colors", title: "Fullscreen", children: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ 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" }) }) }),
1737
- hasTranscript && /* @__PURE__ */ jsx(
1738
- "button",
2245
+ ),
2246
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-white", children: "Transcript" }),
2247
+ broadcast.transcriptLanguage && /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 px-2 py-0.5 bg-zinc-800 rounded", children: broadcast.transcriptLanguage.toUpperCase() })
2248
+ ] }),
2249
+ broadcast.transcriptUrl && /* @__PURE__ */ jsx(
2250
+ "a",
1739
2251
  {
1740
- onClick: () => setShowTranscript(!showTranscript),
1741
- className: `transition-colors ${showTranscript ? "text-blue-400" : "text-white hover:text-blue-400"}`,
1742
- title: showTranscript ? "Hide Transcript" : "Show Transcript",
1743
- children: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ 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" }) })
2252
+ href: broadcast.transcriptUrl,
2253
+ download: `${broadcast.hash || broadcast.id}-transcript.json`,
2254
+ className: "text-gray-400 hover:text-white transition-colors",
2255
+ title: "Download transcript",
2256
+ "aria-label": "Download transcript as JSON file",
2257
+ children: /* @__PURE__ */ jsx(
2258
+ "svg",
2259
+ {
2260
+ className: "w-5 h-5",
2261
+ fill: "none",
2262
+ stroke: "currentColor",
2263
+ viewBox: "0 0 24 24",
2264
+ "aria-hidden": "true",
2265
+ children: /* @__PURE__ */ jsx(
2266
+ "path",
2267
+ {
2268
+ strokeLinecap: "round",
2269
+ strokeLinejoin: "round",
2270
+ strokeWidth: 2,
2271
+ d: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
2272
+ }
2273
+ )
2274
+ }
2275
+ )
1744
2276
  }
1745
2277
  )
1746
- ] })
1747
- ] })
1748
- ] }))
1749
- ] }),
1750
- showTranscript && hasTranscript && /* @__PURE__ */ 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: [
1751
- /* @__PURE__ */ jsx("div", { className: "px-4 py-3 border-b border-zinc-800 bg-zinc-900/50 shrink-0", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
1752
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1753
- /* @__PURE__ */ jsx("svg", { className: "w-5 h-5 text-green-400", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ 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" }) }),
1754
- /* @__PURE__ */ jsx("span", { className: "font-medium text-white", children: "Transcript" }),
1755
- broadcast.transcriptLanguage && /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 px-2 py-0.5 bg-zinc-800 rounded", children: broadcast.transcriptLanguage.toUpperCase() })
1756
- ] }),
1757
- broadcast.transcriptUrl && /* @__PURE__ */ jsx(
1758
- "a",
1759
- {
1760
- href: broadcast.transcriptUrl,
1761
- download: `${broadcast.hash || broadcast.id}-transcript.json`,
1762
- className: "text-gray-400 hover:text-white transition-colors",
1763
- title: "Download Transcript",
1764
- children: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ 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" }) })
1765
- }
1766
- )
1767
- ] }) }),
1768
- !autoScrollEnabled && /* @__PURE__ */ jsx("div", { className: "px-4 py-2 bg-zinc-800/50 border-b border-zinc-700 shrink-0", children: /* @__PURE__ */ jsxs(
1769
- "button",
1770
- {
1771
- onClick: () => setAutoScrollEnabled(true),
1772
- 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",
1773
- children: [
1774
- /* @__PURE__ */ jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 14l-7 7m0 0l-7-7m7 7V3" }) }),
1775
- "Resume Auto-Scroll"
1776
- ]
1777
- }
1778
- ) }),
1779
- /* @__PURE__ */ 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__ */ jsx("div", { className: "flex items-center justify-center py-8", children: /* @__PURE__ */ 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__ */ jsx("div", { className: "space-y-4", children: (() => {
1780
- const filteredSegments = transcriptData.segments.filter((s) => s.words && s.words.length > 0);
1781
- let globalWordIndex = 0;
1782
- const wordMap = /* @__PURE__ */ new Map();
1783
- filteredSegments.forEach((segment) => {
1784
- segment.words.forEach((_word, wordIndex) => {
1785
- wordMap.set(`${segment.id}-${wordIndex}`, globalWordIndex++);
1786
- });
1787
- });
1788
- let currentWordIndex = -1;
1789
- filteredSegments.forEach((segment) => {
1790
- segment.words.forEach((word, wordIndex) => {
1791
- const globalIdx = wordMap.get(`${segment.id}-${wordIndex}`) || -1;
1792
- if (currentTime >= word.start && globalIdx > currentWordIndex) {
1793
- currentWordIndex = globalIdx;
2278
+ ] }) }),
2279
+ !autoScrollEnabled && /* @__PURE__ */ jsx("div", { className: "px-4 py-2 bg-zinc-800/50 border-b border-zinc-700 shrink-0", children: /* @__PURE__ */ jsxs(
2280
+ "button",
2281
+ {
2282
+ onClick: () => setAutoScrollEnabled(true),
2283
+ 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",
2284
+ "aria-label": "Resume automatic scrolling of transcript",
2285
+ children: [
2286
+ /* @__PURE__ */ jsx(
2287
+ "svg",
2288
+ {
2289
+ className: "w-4 h-4",
2290
+ fill: "none",
2291
+ stroke: "currentColor",
2292
+ viewBox: "0 0 24 24",
2293
+ "aria-hidden": "true",
2294
+ children: /* @__PURE__ */ jsx(
2295
+ "path",
2296
+ {
2297
+ strokeLinecap: "round",
2298
+ strokeLinejoin: "round",
2299
+ strokeWidth: 2,
2300
+ d: "M19 14l-7 7m0 0l-7-7m7 7V3"
2301
+ }
2302
+ )
2303
+ }
2304
+ ),
2305
+ "Resume Auto-Scroll"
2306
+ ]
1794
2307
  }
1795
- });
1796
- });
1797
- const previousWordIndex = lastActiveWordIndex.current;
1798
- let minHighlightIndex = -1;
1799
- let maxHighlightIndex = -1;
1800
- if (currentWordIndex >= 0) {
1801
- minHighlightIndex = Math.max(0, currentWordIndex - TRAILING_WORDS);
1802
- maxHighlightIndex = currentWordIndex;
1803
- if (currentWordIndex <= TRAILING_WORDS) {
1804
- minHighlightIndex = 0;
1805
- }
1806
- lastActiveWordIndex.current = currentWordIndex;
1807
- } else if (currentWordIndex === -1) {
1808
- minHighlightIndex = 0;
1809
- maxHighlightIndex = 0;
1810
- } else if (previousWordIndex >= 0) {
1811
- minHighlightIndex = Math.max(0, previousWordIndex - TRAILING_WORDS);
1812
- maxHighlightIndex = previousWordIndex;
1813
- }
1814
- return filteredSegments.map((segment, _segmentIndex) => {
1815
- const isSegmentActive = currentTime >= segment.start && currentTime < segment.end;
1816
- return /* @__PURE__ */ jsxs("div", { ref: isSegmentActive ? transcriptContainerRef : null, className: "flex gap-3 items-start leading-relaxed", children: [
1817
- /* @__PURE__ */ jsx(
1818
- "button",
1819
- {
1820
- onClick: () => handleWordClick(segment.start),
1821
- className: "text-xs text-gray-500 hover:text-gray-300 transition-colors shrink-0 pt-0.5 font-mono",
1822
- title: `Jump to ${formatTimestamp(segment.start)}`,
1823
- children: formatTimestamp(segment.start)
1824
- }
1825
- ),
1826
- /* @__PURE__ */ jsx("div", { className: "flex-1", children: segment.words.map((word, wordIndex) => {
1827
- const thisGlobalIndex = wordMap.get(`${segment.id}-${wordIndex}`) ?? -1;
1828
- const isTimestampActive = currentTime >= word.start && currentTime < word.end;
1829
- const isInGapFill = minHighlightIndex >= 0 && thisGlobalIndex >= minHighlightIndex && thisGlobalIndex <= maxHighlightIndex;
1830
- const isWordActive = isInGapFill;
1831
- return /* @__PURE__ */ jsxs(
1832
- "span",
1833
- {
1834
- ref: isTimestampActive ? activeWordRef : null,
1835
- onClick: () => handleWordClick(word.start),
1836
- 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"}`,
1837
- title: `${formatTime(word.start)} - ${formatTime(word.end)}`,
1838
- children: [
1839
- word.word,
1840
- " "
1841
- ]
1842
- },
1843
- `${segment.id}-${wordIndex}`
1844
- );
1845
- }) })
1846
- ] }, segment.id);
1847
- });
1848
- })() }) : transcriptData?.words && transcriptData.words.length > 0 ? /* @__PURE__ */ jsx("div", { className: "space-y-1", children: transcriptData.words.map((word, index) => {
1849
- const isActive = currentTime >= word.start && currentTime < word.end;
1850
- return /* @__PURE__ */ jsxs(
1851
- "span",
1852
- {
1853
- ref: isActive ? activeWordRef : null,
1854
- onClick: () => handleWordClick(word.start),
1855
- 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"}`,
1856
- title: `${formatTime(word.start)} - ${formatTime(word.end)}`,
1857
- children: [
1858
- word.word,
1859
- " "
1860
- ]
1861
- },
1862
- index
1863
- );
1864
- }) }) : /* @__PURE__ */ jsxs("div", { className: "text-center text-gray-500 py-8", children: [
1865
- /* @__PURE__ */ jsx("p", { className: "mb-2", children: "Transcript data not available" }),
1866
- transcriptData && /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-600", children: [
1867
- "Debug: segments=",
1868
- transcriptData.segments ? transcriptData.segments.length : 0,
1869
- ", words=",
1870
- transcriptData.words ? transcriptData.words.length : 0
2308
+ ) }),
2309
+ /* @__PURE__ */ jsx(
2310
+ "div",
2311
+ {
2312
+ ref: transcriptContainerRef,
2313
+ className: "flex-1 min-h-0 overflow-y-auto px-4 py-4 text-gray-300 leading-relaxed",
2314
+ children: isLoadingTranscript ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-8", children: /* @__PURE__ */ 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__ */ jsx("div", { className: "space-y-4", children: (() => {
2315
+ const filteredSegments = transcriptData.segments.filter(
2316
+ (s) => s.words && s.words.length > 0
2317
+ );
2318
+ let globalWordIndex = 0;
2319
+ const wordMap = /* @__PURE__ */ new Map();
2320
+ filteredSegments.forEach((segment) => {
2321
+ segment.words.forEach((_word, wordIndex) => {
2322
+ wordMap.set(
2323
+ `${segment.id}-${wordIndex}`,
2324
+ globalWordIndex++
2325
+ );
2326
+ });
2327
+ });
2328
+ let currentWordIndex = -1;
2329
+ filteredSegments.forEach((segment) => {
2330
+ segment.words.forEach((word, wordIndex) => {
2331
+ const globalIdx = wordMap.get(`${segment.id}-${wordIndex}`) || -1;
2332
+ if (currentTime >= word.start && globalIdx > currentWordIndex) {
2333
+ currentWordIndex = globalIdx;
2334
+ }
2335
+ });
2336
+ });
2337
+ const previousWordIndex = lastActiveWordIndex.current;
2338
+ let minHighlightIndex = -1;
2339
+ let maxHighlightIndex = -1;
2340
+ if (currentWordIndex >= 0) {
2341
+ minHighlightIndex = Math.max(
2342
+ 0,
2343
+ currentWordIndex - TRAILING_WORDS
2344
+ );
2345
+ maxHighlightIndex = currentWordIndex;
2346
+ if (currentWordIndex <= TRAILING_WORDS) {
2347
+ minHighlightIndex = 0;
2348
+ }
2349
+ lastActiveWordIndex.current = currentWordIndex;
2350
+ } else if (currentWordIndex === -1) {
2351
+ minHighlightIndex = 0;
2352
+ maxHighlightIndex = 0;
2353
+ } else if (previousWordIndex >= 0) {
2354
+ minHighlightIndex = Math.max(
2355
+ 0,
2356
+ previousWordIndex - TRAILING_WORDS
2357
+ );
2358
+ maxHighlightIndex = previousWordIndex;
2359
+ }
2360
+ return filteredSegments.map((segment, _segmentIndex) => {
2361
+ const isSegmentActive = currentTime >= segment.start && currentTime < segment.end;
2362
+ return /* @__PURE__ */ jsxs(
2363
+ "div",
2364
+ {
2365
+ ref: isSegmentActive ? transcriptContainerRef : null,
2366
+ className: "flex gap-3 items-start leading-relaxed",
2367
+ children: [
2368
+ /* @__PURE__ */ jsx(
2369
+ "button",
2370
+ {
2371
+ onClick: () => handleWordClick(segment.start),
2372
+ className: "text-xs text-gray-500 hover:text-gray-300 transition-colors shrink-0 pt-0.5 font-mono",
2373
+ title: `Jump to ${formatTimestamp(segment.start)}`,
2374
+ children: formatTimestamp(segment.start)
2375
+ }
2376
+ ),
2377
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: segment.words.map((word, wordIndex) => {
2378
+ const thisGlobalIndex = wordMap.get(`${segment.id}-${wordIndex}`) ?? -1;
2379
+ const isTimestampActive = currentTime >= word.start && currentTime < word.end;
2380
+ const isInGapFill = minHighlightIndex >= 0 && thisGlobalIndex >= minHighlightIndex && thisGlobalIndex <= maxHighlightIndex;
2381
+ const isWordActive = isInGapFill;
2382
+ return /* @__PURE__ */ jsxs(
2383
+ "span",
2384
+ {
2385
+ ref: isTimestampActive ? activeWordRef : null,
2386
+ onClick: () => handleWordClick(word.start),
2387
+ 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"}`,
2388
+ title: `${formatTime(
2389
+ word.start
2390
+ )} - ${formatTime(word.end)}`,
2391
+ children: [
2392
+ word.word,
2393
+ " "
2394
+ ]
2395
+ },
2396
+ `${segment.id}-${wordIndex}`
2397
+ );
2398
+ }) })
2399
+ ]
2400
+ },
2401
+ segment.id
2402
+ );
2403
+ });
2404
+ })() }) : transcriptData?.words && transcriptData.words.length > 0 ? /* @__PURE__ */ jsx("div", { className: "space-y-1", children: transcriptData.words.map((word, index) => {
2405
+ const isActive = currentTime >= word.start && currentTime < word.end;
2406
+ return /* @__PURE__ */ jsxs(
2407
+ "span",
2408
+ {
2409
+ ref: isActive ? activeWordRef : null,
2410
+ onClick: () => handleWordClick(word.start),
2411
+ 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"}`,
2412
+ title: `${formatTime(word.start)} - ${formatTime(
2413
+ word.end
2414
+ )}`,
2415
+ children: [
2416
+ word.word,
2417
+ " "
2418
+ ]
2419
+ },
2420
+ index
2421
+ );
2422
+ }) }) : /* @__PURE__ */ jsxs("div", { className: "text-center text-gray-500 py-8", children: [
2423
+ /* @__PURE__ */ jsx("p", { className: "mb-2", children: "Transcript data not available" }),
2424
+ transcriptData && /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-600", children: [
2425
+ "Debug: segments=",
2426
+ transcriptData.segments ? transcriptData.segments.length : 0,
2427
+ ", words=",
2428
+ transcriptData.words ? transcriptData.words.length : 0
2429
+ ] })
2430
+ ] })
2431
+ }
2432
+ )
1871
2433
  ] })
1872
- ] }) })
1873
- ] })
1874
- ] }),
1875
- renderClipCreator && renderClipCreator({
1876
- isOpen: showClipCreator,
1877
- onClose: () => setShowClipCreator(false),
1878
- sourceVideoUrl: playbackType === "mp4" ? playbackUrl || void 0 : void 0,
1879
- sourceAudioUrl: playbackType === "mp3" ? playbackUrl || void 0 : void 0,
1880
- sourceDuration: duration,
1881
- onPauseParent: () => setPlaying(false)
1882
- }),
1883
- /* @__PURE__ */ jsx("style", { children: `
2434
+ ] }),
2435
+ renderClipCreator && renderClipCreator({
2436
+ isOpen: showClipCreator,
2437
+ onClose: () => setShowClipCreator(false),
2438
+ sourceVideoUrl: playbackType === "mp4" ? playbackUrl || void 0 : void 0,
2439
+ sourceAudioUrl: playbackType === "mp3" ? playbackUrl || void 0 : void 0,
2440
+ sourceDuration: duration,
2441
+ onPauseParent: () => setPlaying(false)
2442
+ }),
2443
+ /* @__PURE__ */ jsx("style", { children: `
1884
2444
  .slider::-webkit-slider-thumb {
1885
2445
  appearance: none;
1886
2446
  width: 14px;
@@ -1920,91 +2480,12 @@ function BroadcastPlayer({
1920
2480
  transition: color 0.15s ease-in;
1921
2481
  }
1922
2482
  ` })
1923
- ] });
1924
- }
1925
- function BroadcastPlayerModal({
1926
- broadcast,
1927
- isOpen,
1928
- onClose,
1929
- appId,
1930
- contentId,
1931
- foreignId,
1932
- foreignTier,
1933
- renderClipCreator,
1934
- className,
1935
- enableKeyboardShortcuts = false
1936
- }) {
1937
- const closeButtonRef = useRef(null);
1938
- const previousActiveElement = useRef(null);
1939
- useEffect(() => {
1940
- if (!isOpen) return;
1941
- previousActiveElement.current = document.activeElement;
1942
- setTimeout(() => {
1943
- closeButtonRef.current?.focus();
1944
- }, 100);
1945
- return () => {
1946
- if (previousActiveElement.current) {
1947
- previousActiveElement.current.focus();
1948
- }
1949
- };
1950
- }, [isOpen]);
1951
- useEffect(() => {
1952
- if (!isOpen) return;
1953
- const handleKeyDown = (e) => {
1954
- if (e.key === "Escape") {
1955
- onClose();
1956
- }
1957
- };
1958
- document.addEventListener("keydown", handleKeyDown);
1959
- return () => document.removeEventListener("keydown", handleKeyDown);
1960
- }, [isOpen, onClose]);
1961
- if (!isOpen) return null;
1962
- const handleBackdropClick = (e) => {
1963
- if (e.target === e.currentTarget) {
1964
- onClose();
1965
- }
1966
- };
1967
- return /* @__PURE__ */ jsx(
1968
- "div",
1969
- {
1970
- className: "fixed inset-0 bg-black/70 backdrop-blur-xl flex items-center justify-center z-50 p-2 sm:p-4",
1971
- onClick: handleBackdropClick,
1972
- role: "dialog",
1973
- "aria-modal": "true",
1974
- "aria-label": "Broadcast player",
1975
- children: /* @__PURE__ */ jsxs("div", { className: "relative w-full max-w-7xl max-h-[95vh] sm:max-h-[90vh] overflow-hidden", children: [
1976
- /* @__PURE__ */ jsx(
1977
- "button",
1978
- {
1979
- ref: closeButtonRef,
1980
- onClick: onClose,
1981
- 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",
1982
- title: "Close (ESC)",
1983
- "aria-label": "Close player",
1984
- children: "\xD7"
1985
- }
1986
- ),
1987
- /* @__PURE__ */ jsx(
1988
- BroadcastPlayer,
1989
- {
1990
- broadcast,
1991
- appId,
1992
- contentId,
1993
- foreignId,
1994
- foreignTier,
1995
- renderClipCreator,
1996
- className,
1997
- enableKeyboardShortcuts,
1998
- onError: (error) => {
1999
- debug.error("[BroadcastPlayerModal] Player error:", error);
2000
- }
2001
- }
2002
- )
2003
- ] })
2483
+ ]
2004
2484
  }
2005
2485
  );
2486
+ return playerContent;
2006
2487
  }
2007
- var BroadcastPlayerErrorBoundary = class extends Component {
2488
+ var DialtribePlayerErrorBoundary = class extends Component {
2008
2489
  constructor(props) {
2009
2490
  super(props);
2010
2491
  this.handleReset = () => {
@@ -2130,7 +2611,2207 @@ var BroadcastPlayerErrorBoundary = class extends Component {
2130
2611
  return this.props.children;
2131
2612
  }
2132
2613
  };
2614
+ var overlayStyles = {
2615
+ modal: {
2616
+ backdrop: "bg-black/70 backdrop-blur-xl p-2 sm:p-4",
2617
+ container: "max-w-7xl max-h-[95vh] sm:max-h-[90vh]"
2618
+ },
2619
+ fullscreen: {
2620
+ backdrop: "bg-black",
2621
+ container: "h-full"
2622
+ }
2623
+ };
2624
+ function DialtribeOverlay({
2625
+ isOpen,
2626
+ onClose,
2627
+ mode = "modal",
2628
+ children,
2629
+ ariaLabel = "Dialog",
2630
+ showCloseButton = true,
2631
+ closeOnBackdropClick = true,
2632
+ closeOnEsc = true
2633
+ }) {
2634
+ const closeButtonRef = useRef(null);
2635
+ const previousActiveElement = useRef(null);
2636
+ useEffect(() => {
2637
+ if (!isOpen) return;
2638
+ previousActiveElement.current = document.activeElement;
2639
+ setTimeout(() => {
2640
+ closeButtonRef.current?.focus();
2641
+ }, 100);
2642
+ return () => {
2643
+ if (previousActiveElement.current) {
2644
+ previousActiveElement.current.focus();
2645
+ }
2646
+ };
2647
+ }, [isOpen]);
2648
+ useEffect(() => {
2649
+ if (!isOpen || !closeOnEsc) return;
2650
+ const handleKeyDown = (e) => {
2651
+ if (e.key === "Escape") {
2652
+ onClose();
2653
+ }
2654
+ };
2655
+ document.addEventListener("keydown", handleKeyDown);
2656
+ return () => document.removeEventListener("keydown", handleKeyDown);
2657
+ }, [isOpen, onClose, closeOnEsc]);
2658
+ if (!isOpen) {
2659
+ return null;
2660
+ }
2661
+ const handleBackdropClick = (e) => {
2662
+ if (closeOnBackdropClick && e.target === e.currentTarget) {
2663
+ onClose();
2664
+ }
2665
+ };
2666
+ const styles = overlayStyles[mode];
2667
+ return /* @__PURE__ */ jsx(
2668
+ "div",
2669
+ {
2670
+ className: `fixed inset-0 flex items-center justify-center z-50 ${styles.backdrop}`,
2671
+ onClick: handleBackdropClick,
2672
+ role: "dialog",
2673
+ "aria-modal": "true",
2674
+ "aria-label": ariaLabel,
2675
+ children: /* @__PURE__ */ jsxs("div", { className: `relative w-full overflow-hidden ${styles.container}`, children: [
2676
+ showCloseButton && /* @__PURE__ */ jsx(
2677
+ "button",
2678
+ {
2679
+ ref: closeButtonRef,
2680
+ onClick: onClose,
2681
+ 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",
2682
+ title: "Close (ESC)",
2683
+ "aria-label": "Close",
2684
+ children: "\xD7"
2685
+ }
2686
+ ),
2687
+ children
2688
+ ] })
2689
+ }
2690
+ );
2691
+ }
2692
+
2693
+ // src/utils/media-constraints.ts
2694
+ function getMediaConstraints(options) {
2695
+ const { isVideo, facingMode = "user" } = options;
2696
+ const audioConstraints = {
2697
+ autoGainControl: true,
2698
+ channelCount: 2,
2699
+ // Stereo
2700
+ echoCancellation: false,
2701
+ noiseSuppression: false,
2702
+ sampleRate: 48e3
2703
+ // 48kHz
2704
+ };
2705
+ const videoConstraints = isVideo ? {
2706
+ aspectRatio: 16 / 9,
2707
+ width: { ideal: 1280 },
2708
+ height: { ideal: 720 },
2709
+ frameRate: { ideal: 30 },
2710
+ facingMode
2711
+ // "user" (front) or "environment" (back)
2712
+ } : false;
2713
+ return {
2714
+ audio: audioConstraints,
2715
+ video: videoConstraints
2716
+ };
2717
+ }
2718
+ function getMediaRecorderOptions(isVideo) {
2719
+ let mimeType;
2720
+ if (isVideo) {
2721
+ if (MediaRecorder.isTypeSupported("video/mp4;codecs=avc1,mp4a")) {
2722
+ mimeType = "video/mp4;codecs=avc1,mp4a";
2723
+ } else if (MediaRecorder.isTypeSupported("video/webm;codecs=h264,opus")) {
2724
+ mimeType = "video/webm;codecs=h264,opus";
2725
+ } else if (MediaRecorder.isTypeSupported("video/webm;codecs=vp8,opus")) {
2726
+ mimeType = "video/webm;codecs=vp8,opus";
2727
+ console.warn("\u26A0\uFE0F Browser only supports VP8/Opus - recordings will be disabled");
2728
+ }
2729
+ } else {
2730
+ if (MediaRecorder.isTypeSupported("audio/mp4;codecs=mp4a")) {
2731
+ mimeType = "audio/mp4;codecs=mp4a";
2732
+ } else if (MediaRecorder.isTypeSupported("audio/webm;codecs=opus")) {
2733
+ mimeType = "audio/webm;codecs=opus";
2734
+ console.warn("\u26A0\uFE0F Browser only supports Opus - recordings may be disabled");
2735
+ }
2736
+ }
2737
+ return {
2738
+ mimeType,
2739
+ audioBitsPerSecond: 128e3,
2740
+ // 128 kbps audio
2741
+ videoBitsPerSecond: isVideo ? 25e5 : void 0
2742
+ // 2.5 Mbps for 720p video
2743
+ };
2744
+ }
2745
+ function checkBrowserCompatibility() {
2746
+ if (typeof window === "undefined") {
2747
+ return {
2748
+ compatible: false,
2749
+ error: "This component requires a browser environment"
2750
+ };
2751
+ }
2752
+ if (!window.MediaRecorder) {
2753
+ return {
2754
+ compatible: false,
2755
+ error: "MediaRecorder API not supported in this browser"
2756
+ };
2757
+ }
2758
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
2759
+ return {
2760
+ compatible: false,
2761
+ error: "getUserMedia API not supported in this browser"
2762
+ };
2763
+ }
2764
+ return { compatible: true };
2765
+ }
2766
+
2767
+ // src/utils/websocket-streamer.ts
2768
+ var DEFAULT_ENCODER_SERVER_URL = "https://broadcastapi.dialtribe.com";
2769
+ var WebSocketStreamer = class {
2770
+ constructor(options) {
2771
+ this.websocket = null;
2772
+ this.mediaRecorder = null;
2773
+ this.bytesSent = 0;
2774
+ this.chunksSent = 0;
2775
+ this.userStopped = false;
2776
+ // Track if user initiated the stop
2777
+ this.isHotSwapping = false;
2778
+ // Track if we're swapping media streams
2779
+ this.startTime = 0;
2780
+ // Canvas-based rendering for seamless camera flips
2781
+ // MediaRecorder records from canvas stream, so track changes don't affect it
2782
+ this.canvasState = null;
2783
+ this.streamKey = options.streamKey;
2784
+ this.mediaStream = options.mediaStream;
2785
+ this.isVideo = options.isVideo;
2786
+ this.encoderServerUrl = options.encoderServerUrl || DEFAULT_ENCODER_SERVER_URL;
2787
+ this.disableCanvasRendering = options.disableCanvasRendering || false;
2788
+ this.onBytesUpdate = options.onBytesUpdate;
2789
+ this.onStateChange = options.onStateChange;
2790
+ this.onError = options.onError;
2791
+ }
2792
+ /**
2793
+ * Calculate scaled dimensions for fitting video into canvas.
2794
+ * @param mode - "contain" fits video inside canvas, "cover" fills canvas (cropping)
2795
+ */
2796
+ calculateScaledDimensions(videoWidth, videoHeight, canvasWidth, canvasHeight, mode) {
2797
+ const videoAspect = videoWidth / videoHeight;
2798
+ const canvasAspect = canvasWidth / canvasHeight;
2799
+ const useWidthBased = mode === "contain" ? videoAspect > canvasAspect : videoAspect <= canvasAspect;
2800
+ if (useWidthBased) {
2801
+ const width = canvasWidth;
2802
+ const height = canvasWidth / videoAspect;
2803
+ return { x: 0, y: (canvasHeight - height) / 2, width, height };
2804
+ } else {
2805
+ const height = canvasHeight;
2806
+ const width = canvasHeight * videoAspect;
2807
+ return { x: (canvasWidth - width) / 2, y: 0, width, height };
2808
+ }
2809
+ }
2810
+ /**
2811
+ * Invalidate cached scaling dimensions (call when video source changes)
2812
+ */
2813
+ invalidateScalingCache() {
2814
+ if (this.canvasState) {
2815
+ this.canvasState.cachedContain = null;
2816
+ this.canvasState.cachedCover = null;
2817
+ this.canvasState.cachedNeedsBackground = false;
2818
+ this.canvasState.lastVideoWidth = 0;
2819
+ this.canvasState.lastVideoHeight = 0;
2820
+ }
2821
+ }
2822
+ /**
2823
+ * Validate stream key format
2824
+ * Stream keys must follow format: {tierCode}{foreignId}_{randomKey}
2825
+ * Tier codes: a (audio shared), b (audio VIP), v (video shared), w (video VIP)
2826
+ */
2827
+ validateStreamKeyFormat() {
2828
+ if (!this.streamKey || this.streamKey.length < 10) {
2829
+ throw new Error("Invalid stream key: too short");
2830
+ }
2831
+ const tierCode = this.streamKey[0];
2832
+ if (!["a", "b", "v", "w"].includes(tierCode)) {
2833
+ throw new Error(
2834
+ `Invalid stream key format: must start with 'a', 'b', 'v', or 'w' (got '${tierCode}')`
2835
+ );
2836
+ }
2837
+ if (!this.streamKey.includes("_")) {
2838
+ throw new Error(
2839
+ "Invalid stream key format: must contain underscore separator (format: {tier}{id}_{key})"
2840
+ );
2841
+ }
2842
+ console.log("\u2705 Stream key format validated:", {
2843
+ tierCode,
2844
+ isVideo: tierCode === "v" || tierCode === "w",
2845
+ isVIP: tierCode === "b" || tierCode === "w"
2846
+ });
2847
+ }
2848
+ /**
2849
+ * Set up canvas-based rendering pipeline for video streams.
2850
+ * This allows seamless camera flips by changing the video source
2851
+ * without affecting MediaRecorder (which records from the canvas).
2852
+ *
2853
+ * This is async to ensure the video is producing frames before returning,
2854
+ * which prevents black initial thumbnails.
2855
+ */
2856
+ async setupCanvasRendering() {
2857
+ console.log("\u{1F3A8} Setting up canvas-based rendering for seamless camera flips");
2858
+ const videoTrack = this.mediaStream.getVideoTracks()[0];
2859
+ const settings = videoTrack?.getSettings() || {};
2860
+ const width = settings.width || 1280;
2861
+ const height = settings.height || 720;
2862
+ console.log(`\u{1F4D0} Video dimensions: ${width}x${height}`);
2863
+ const canvas = document.createElement("canvas");
2864
+ canvas.width = width;
2865
+ canvas.height = height;
2866
+ const ctx = canvas.getContext("2d");
2867
+ if (!ctx) {
2868
+ throw new Error("Failed to get 2D canvas context - canvas rendering unavailable");
2869
+ }
2870
+ const videoElement = document.createElement("video");
2871
+ videoElement.srcObject = this.mediaStream;
2872
+ videoElement.muted = true;
2873
+ videoElement.playsInline = true;
2874
+ await new Promise((resolve) => {
2875
+ const checkReady = () => {
2876
+ if (videoElement.videoWidth > 0 && videoElement.videoHeight > 0) {
2877
+ console.log(`\u{1F4F9} Video ready: ${videoElement.videoWidth}x${videoElement.videoHeight}`);
2878
+ resolve();
2879
+ } else {
2880
+ requestAnimationFrame(checkReady);
2881
+ }
2882
+ };
2883
+ videoElement.addEventListener("loadeddata", () => {
2884
+ checkReady();
2885
+ }, { once: true });
2886
+ videoElement.play().catch((e) => {
2887
+ console.warn("Video autoplay warning:", e);
2888
+ resolve();
2889
+ });
2890
+ setTimeout(() => {
2891
+ console.warn("\u26A0\uFE0F Video ready timeout - continuing anyway");
2892
+ resolve();
2893
+ }, 2e3);
2894
+ });
2895
+ const frameRate = settings.frameRate || 30;
2896
+ const stream = canvas.captureStream(frameRate);
2897
+ const audioTracks = this.mediaStream.getAudioTracks();
2898
+ audioTracks.forEach((track) => {
2899
+ stream.addTrack(track);
2900
+ });
2901
+ console.log(`\u{1F3AC} Canvas stream created with ${frameRate}fps video + ${audioTracks.length} audio track(s)`);
2902
+ this.canvasState = {
2903
+ canvas,
2904
+ ctx,
2905
+ videoElement,
2906
+ stream,
2907
+ renderLoopId: 0,
2908
+ // Will be set below
2909
+ useBlurBackground: true,
2910
+ slowFrameCount: 0,
2911
+ cachedContain: null,
2912
+ cachedCover: null,
2913
+ cachedNeedsBackground: false,
2914
+ lastVideoWidth: 0,
2915
+ lastVideoHeight: 0
2916
+ };
2917
+ if (videoElement.videoWidth > 0 && videoElement.videoHeight > 0) {
2918
+ const vw = videoElement.videoWidth;
2919
+ const vh = videoElement.videoHeight;
2920
+ const cw = canvas.width;
2921
+ const ch = canvas.height;
2922
+ const scale = Math.min(cw / vw, ch / vh);
2923
+ const sw = vw * scale;
2924
+ const sh = vh * scale;
2925
+ const sx = (cw - sw) / 2;
2926
+ const sy = (ch - sh) / 2;
2927
+ ctx.fillStyle = "#000";
2928
+ ctx.fillRect(0, 0, cw, ch);
2929
+ ctx.drawImage(videoElement, sx, sy, sw, sh);
2930
+ console.log("\u{1F5BC}\uFE0F Drew first frame synchronously to prevent black thumbnail");
2931
+ }
2932
+ const state = this.canvasState;
2933
+ const renderFrame = () => {
2934
+ if (!this.canvasState || state !== this.canvasState) return;
2935
+ const { ctx: ctx2, canvas: canvas2, videoElement: videoElement2 } = state;
2936
+ if (videoElement2.paused) {
2937
+ videoElement2.play().catch(() => {
2938
+ });
2939
+ }
2940
+ const canvasWidth = canvas2.width;
2941
+ const canvasHeight = canvas2.height;
2942
+ const videoWidth = videoElement2.videoWidth;
2943
+ const videoHeight = videoElement2.videoHeight;
2944
+ if (videoWidth === 0 || videoHeight === 0) {
2945
+ state.renderLoopId = requestAnimationFrame(renderFrame);
2946
+ return;
2947
+ }
2948
+ if (videoWidth !== state.lastVideoWidth || videoHeight !== state.lastVideoHeight) {
2949
+ state.lastVideoWidth = videoWidth;
2950
+ state.lastVideoHeight = videoHeight;
2951
+ state.cachedContain = this.calculateScaledDimensions(
2952
+ videoWidth,
2953
+ videoHeight,
2954
+ canvasWidth,
2955
+ canvasHeight,
2956
+ "contain"
2957
+ );
2958
+ state.cachedCover = this.calculateScaledDimensions(
2959
+ videoWidth,
2960
+ videoHeight,
2961
+ canvasWidth,
2962
+ canvasHeight,
2963
+ "cover"
2964
+ );
2965
+ state.cachedNeedsBackground = Math.abs(state.cachedContain.width - canvasWidth) > 1 || Math.abs(state.cachedContain.height - canvasHeight) > 1;
2966
+ console.log(`\u{1F4D0} Video dimensions changed: ${videoWidth}x${videoHeight}, needsBackground: ${state.cachedNeedsBackground}`);
2967
+ }
2968
+ const contain = state.cachedContain;
2969
+ const cover = state.cachedCover;
2970
+ const frameStart = performance.now();
2971
+ if (state.cachedNeedsBackground && state.useBlurBackground) {
2972
+ ctx2.save();
2973
+ ctx2.filter = "blur(20px)";
2974
+ ctx2.drawImage(videoElement2, cover.x, cover.y, cover.width, cover.height);
2975
+ ctx2.restore();
2976
+ ctx2.fillStyle = "rgba(0, 0, 0, 0.5)";
2977
+ ctx2.fillRect(0, 0, canvasWidth, canvasHeight);
2978
+ } else if (state.cachedNeedsBackground) {
2979
+ ctx2.fillStyle = "#000";
2980
+ ctx2.fillRect(0, 0, canvasWidth, canvasHeight);
2981
+ }
2982
+ ctx2.drawImage(videoElement2, contain.x, contain.y, contain.width, contain.height);
2983
+ const frameDuration = performance.now() - frameStart;
2984
+ if (frameDuration > 16 && state.useBlurBackground) {
2985
+ state.slowFrameCount++;
2986
+ if (state.slowFrameCount > 5) {
2987
+ console.log("\u26A1 Disabling blur background for performance");
2988
+ state.useBlurBackground = false;
2989
+ }
2990
+ } else if (frameDuration <= 16) {
2991
+ state.slowFrameCount = 0;
2992
+ }
2993
+ state.renderLoopId = requestAnimationFrame(renderFrame);
2994
+ };
2995
+ state.renderLoopId = requestAnimationFrame(renderFrame);
2996
+ console.log("\u2705 Canvas rendering pipeline ready (with adaptive blur background)");
2997
+ return stream;
2998
+ }
2999
+ /**
3000
+ * Clean up canvas rendering resources
3001
+ */
3002
+ cleanupCanvasRendering() {
3003
+ if (!this.canvasState) return;
3004
+ cancelAnimationFrame(this.canvasState.renderLoopId);
3005
+ this.canvasState.videoElement.pause();
3006
+ this.canvasState.videoElement.srcObject = null;
3007
+ this.canvasState.stream.getTracks().forEach((track) => track.stop());
3008
+ this.canvasState = null;
3009
+ }
3010
+ /**
3011
+ * Build WebSocket URL from stream key
3012
+ */
3013
+ buildWebSocketUrl() {
3014
+ const url = new URL(this.encoderServerUrl);
3015
+ if (url.protocol === "http:") {
3016
+ url.protocol = "ws:";
3017
+ } else if (url.protocol === "https:") {
3018
+ url.protocol = "wss:";
3019
+ }
3020
+ url.pathname = "/targets/dialtribe";
3021
+ url.searchParams.set("key", this.streamKey);
3022
+ return url.toString();
3023
+ }
3024
+ /**
3025
+ * Start streaming
3026
+ */
3027
+ async start() {
3028
+ try {
3029
+ this.userStopped = false;
3030
+ this.chunksSent = 0;
3031
+ this.bytesSent = 0;
3032
+ this.startTime = 0;
3033
+ this.validateStreamKeyFormat();
3034
+ this.onStateChange?.("connecting");
3035
+ const wsUrl = this.buildWebSocketUrl();
3036
+ console.log("\u{1F4E1} Connecting to WebSocket:", wsUrl.replace(this.streamKey, "***"));
3037
+ this.websocket = new WebSocket(wsUrl);
3038
+ await new Promise((resolve, reject) => {
3039
+ if (!this.websocket) {
3040
+ reject(new Error("WebSocket not initialized"));
3041
+ return;
3042
+ }
3043
+ const timeoutId = setTimeout(() => {
3044
+ reject(new Error(`WebSocket connection timeout. URL: ${wsUrl}`));
3045
+ }, 1e4);
3046
+ this.websocket.addEventListener("open", () => {
3047
+ clearTimeout(timeoutId);
3048
+ resolve();
3049
+ }, { once: true });
3050
+ this.websocket.addEventListener("error", (event) => {
3051
+ clearTimeout(timeoutId);
3052
+ console.error("\u274C WebSocket error event:", event);
3053
+ console.error("\u{1F50D} Connection diagnostics:", {
3054
+ url: wsUrl.replace(this.streamKey, "***"),
3055
+ streamKeyFormat: this.streamKey.substring(0, 5) + "***",
3056
+ readyState: this.websocket?.readyState,
3057
+ readyStateText: ["CONNECTING", "OPEN", "CLOSING", "CLOSED"][this.websocket?.readyState || 0]
3058
+ });
3059
+ reject(new Error(
3060
+ `WebSocket connection failed (likely 403 Forbidden).
3061
+
3062
+ Common causes:
3063
+ 1. Encoder server cannot connect to the database
3064
+ 2. Encoder server is using a different database
3065
+ 3. Stream key validation failed on encoder server
3066
+
3067
+ Please check encoder server logs and DATABASE_URL configuration.`
3068
+ ));
3069
+ }, { once: true });
3070
+ });
3071
+ console.log("\u2705 WebSocket connected");
3072
+ this.setupWebSocketHandlers();
3073
+ const useCanvas = this.isVideo && !this.disableCanvasRendering;
3074
+ const streamToRecord = useCanvas ? await this.setupCanvasRendering() : this.mediaStream;
3075
+ const recorderOptions = getMediaRecorderOptions(this.isVideo);
3076
+ this.mimeType = recorderOptions.mimeType;
3077
+ this.mediaRecorder = new MediaRecorder(streamToRecord, recorderOptions);
3078
+ console.log("\u{1F399}\uFE0F MediaRecorder created with options:", recorderOptions);
3079
+ if (useCanvas) {
3080
+ console.log("\u{1F3A8} Recording from canvas stream (enables seamless camera flips)");
3081
+ } else if (this.isVideo) {
3082
+ console.log("\u{1F4F9} Recording directly from camera (canvas disabled)");
3083
+ }
3084
+ this.setupMediaRecorderHandlers();
3085
+ this.mediaRecorder.start(300);
3086
+ this.startTime = Date.now();
3087
+ console.log("\u{1F534} Recording started");
3088
+ this.onStateChange?.("live");
3089
+ } catch (error) {
3090
+ console.error("\u274C Error starting stream:", error);
3091
+ this.onError?.(error instanceof Error ? error.message : "Failed to start stream");
3092
+ this.onStateChange?.("error");
3093
+ this.stop();
3094
+ throw error;
3095
+ }
3096
+ }
3097
+ /**
3098
+ * Stop streaming
3099
+ */
3100
+ stop() {
3101
+ console.log("\u23F9\uFE0F Stopping stream");
3102
+ this.userStopped = true;
3103
+ if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
3104
+ this.mediaRecorder.stop();
3105
+ console.log("\u23F9\uFE0F MediaRecorder stopped");
3106
+ }
3107
+ if (this.websocket) {
3108
+ const readyStateNames = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
3109
+ const stateName = readyStateNames[this.websocket.readyState] || "UNKNOWN";
3110
+ console.log(`\u{1F50C} WebSocket state: ${stateName} (${this.websocket.readyState})`);
3111
+ if (this.websocket.readyState !== WebSocket.CLOSED) {
3112
+ this.websocket.close();
3113
+ console.log("\u{1F50C} WebSocket close() called");
3114
+ } else {
3115
+ console.log("\u{1F50C} WebSocket already closed");
3116
+ }
3117
+ } else {
3118
+ console.log("\u26A0\uFE0F No WebSocket to close");
3119
+ }
3120
+ this.cleanupCanvasRendering();
3121
+ this.mediaRecorder = null;
3122
+ this.websocket = null;
3123
+ this.onStateChange?.("stopped");
3124
+ }
3125
+ /**
3126
+ * Get total bytes sent
3127
+ */
3128
+ getBytesSent() {
3129
+ return this.bytesSent;
3130
+ }
3131
+ /**
3132
+ * Get the current source media stream.
3133
+ * This may change after replaceVideoTrack() is called.
3134
+ */
3135
+ getMediaStream() {
3136
+ return this.mediaStream;
3137
+ }
3138
+ /**
3139
+ * Get current diagnostics
3140
+ */
3141
+ getDiagnostics(closeCode, closeReason) {
3142
+ return {
3143
+ mimeType: this.mimeType,
3144
+ chunksSent: this.chunksSent,
3145
+ bytesSent: this.bytesSent,
3146
+ elapsedMs: this.startTime ? Date.now() - this.startTime : 0,
3147
+ closeCode,
3148
+ closeReason
3149
+ };
3150
+ }
3151
+ /**
3152
+ * Replace the video track for camera flips.
3153
+ *
3154
+ * When using canvas-based rendering (video streams), this preloads the new
3155
+ * camera in a temporary video element, waits for it to be ready, then swaps
3156
+ * it in. This ensures continuous frame output with no gaps.
3157
+ *
3158
+ * @param newVideoTrack - The new video track from the flipped camera
3159
+ * @returns Promise that resolves when the swap is complete
3160
+ */
3161
+ async replaceVideoTrack(newVideoTrack) {
3162
+ console.log("\u{1F504} Replacing video track");
3163
+ if (this.canvasState) {
3164
+ console.log("\u{1F3A8} Using canvas-based swap with preloading (no frame gaps)");
3165
+ const audioTracks = this.mediaStream.getAudioTracks();
3166
+ const newStream = new MediaStream([newVideoTrack, ...audioTracks]);
3167
+ const preloadVideo = document.createElement("video");
3168
+ preloadVideo.srcObject = newStream;
3169
+ preloadVideo.muted = true;
3170
+ preloadVideo.playsInline = true;
3171
+ await new Promise((resolve, reject) => {
3172
+ const timeout = setTimeout(() => {
3173
+ console.warn("\u26A0\uFE0F Video preload timeout - switching anyway");
3174
+ if (preloadVideo.paused) {
3175
+ preloadVideo.play().catch(() => {
3176
+ });
3177
+ }
3178
+ resolve();
3179
+ }, 3e3);
3180
+ const checkFullyReady = () => {
3181
+ if (preloadVideo.videoWidth > 0 && preloadVideo.videoHeight > 0 && !preloadVideo.paused) {
3182
+ clearTimeout(timeout);
3183
+ console.log(`\u{1F4F9} New camera ready and playing: ${preloadVideo.videoWidth}x${preloadVideo.videoHeight}`);
3184
+ resolve();
3185
+ return true;
3186
+ }
3187
+ return false;
3188
+ };
3189
+ preloadVideo.addEventListener("loadeddata", () => {
3190
+ preloadVideo.play().then(() => {
3191
+ requestAnimationFrame(() => {
3192
+ if (!checkFullyReady()) {
3193
+ const pollPlaying = setInterval(() => {
3194
+ if (checkFullyReady()) {
3195
+ clearInterval(pollPlaying);
3196
+ }
3197
+ }, 50);
3198
+ setTimeout(() => clearInterval(pollPlaying), 2e3);
3199
+ }
3200
+ });
3201
+ }).catch((e) => {
3202
+ console.warn("Video preload play warning:", e);
3203
+ checkFullyReady();
3204
+ });
3205
+ }, { once: true });
3206
+ preloadVideo.addEventListener("error", (e) => {
3207
+ clearTimeout(timeout);
3208
+ reject(new Error(`Video preload failed: ${e}`));
3209
+ }, { once: true });
3210
+ preloadVideo.play().catch(() => {
3211
+ });
3212
+ });
3213
+ const oldVideoElement = this.canvasState.videoElement;
3214
+ this.canvasState.videoElement = preloadVideo;
3215
+ this.mediaStream.getVideoTracks().forEach((track) => track.stop());
3216
+ oldVideoElement.pause();
3217
+ oldVideoElement.srcObject = null;
3218
+ this.mediaStream = newStream;
3219
+ this.invalidateScalingCache();
3220
+ const settings = newVideoTrack.getSettings();
3221
+ if (settings.width && settings.height) {
3222
+ console.log(`\u{1F4D0} New camera resolution: ${settings.width}x${settings.height}`);
3223
+ }
3224
+ console.log("\u2705 Video source swapped - canvas continues seamlessly");
3225
+ } else {
3226
+ console.warn("\u26A0\uFE0F Canvas not available - attempting direct track replacement");
3227
+ const oldVideoTracks = this.mediaStream.getVideoTracks();
3228
+ this.mediaStream.addTrack(newVideoTrack);
3229
+ console.log("\u2795 New video track added");
3230
+ oldVideoTracks.forEach((track) => {
3231
+ this.mediaStream.removeTrack(track);
3232
+ track.stop();
3233
+ });
3234
+ console.log("\u2796 Old video track(s) removed");
3235
+ console.log("\u2705 Video track replaced");
3236
+ }
3237
+ }
3238
+ /**
3239
+ * Replace the audio track in the current MediaStream without stopping MediaRecorder.
3240
+ *
3241
+ * @param newAudioTrack - The new audio track
3242
+ */
3243
+ replaceAudioTrack(newAudioTrack) {
3244
+ console.log("\u{1F504} Replacing audio track (no MediaRecorder restart)");
3245
+ const oldAudioTracks = this.mediaStream.getAudioTracks();
3246
+ this.mediaStream.addTrack(newAudioTrack);
3247
+ console.log("\u2795 New audio track added to source stream");
3248
+ oldAudioTracks.forEach((track) => {
3249
+ this.mediaStream.removeTrack(track);
3250
+ track.stop();
3251
+ });
3252
+ console.log("\u2796 Old audio track(s) removed from source stream");
3253
+ if (this.canvasState) {
3254
+ this.canvasState.stream.getAudioTracks().forEach((track) => {
3255
+ this.canvasState.stream.removeTrack(track);
3256
+ });
3257
+ this.canvasState.stream.addTrack(newAudioTrack);
3258
+ console.log("\u{1F3A8} Audio track synced to canvas stream");
3259
+ }
3260
+ console.log("\u2705 Audio track replaced - streaming continues seamlessly");
3261
+ }
3262
+ /**
3263
+ * Update the media stream (e.g., when switching devices from settings)
3264
+ * This keeps the WebSocket connection alive while swapping the media source.
3265
+ * Restarts the MediaRecorder with the new stream.
3266
+ *
3267
+ * Note: For camera flips, prefer replaceVideoTrack() which doesn't restart MediaRecorder.
3268
+ * Note: Errors are thrown to the caller, not sent to onError callback.
3269
+ */
3270
+ async updateMediaStream(newMediaStream) {
3271
+ console.log("\u{1F504} Updating media stream (hot-swap)");
3272
+ this.isHotSwapping = true;
3273
+ if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
3274
+ this.mediaRecorder.stop();
3275
+ console.log("\u23F9\uFE0F Old MediaRecorder stopped");
3276
+ }
3277
+ this.mediaStream = newMediaStream;
3278
+ const useCanvas = this.isVideo && !this.disableCanvasRendering;
3279
+ let streamToRecord = this.mediaStream;
3280
+ if (useCanvas) {
3281
+ this.cleanupCanvasRendering();
3282
+ streamToRecord = await this.setupCanvasRendering();
3283
+ console.log("\u{1F3A8} Canvas rendering recreated for new stream");
3284
+ }
3285
+ const recorderOptions = getMediaRecorderOptions(this.isVideo);
3286
+ this.mediaRecorder = new MediaRecorder(streamToRecord, recorderOptions);
3287
+ console.log("\u{1F399}\uFE0F New MediaRecorder created");
3288
+ this.setupMediaRecorderHandlers();
3289
+ this.mediaRecorder.start(300);
3290
+ this.isHotSwapping = false;
3291
+ console.log("\u2705 Media stream updated - streaming continues");
3292
+ }
3293
+ /**
3294
+ * Set up WebSocket event handlers
3295
+ */
3296
+ setupWebSocketHandlers() {
3297
+ if (!this.websocket) return;
3298
+ this.websocket.addEventListener("close", (event) => {
3299
+ console.log("\u{1F50C} WebSocket closed", { code: event.code, reason: event.reason });
3300
+ if (!this.userStopped) {
3301
+ const diagnostics = this.getDiagnostics(event.code, event.reason);
3302
+ console.warn("\u26A0\uFE0F Stream ended unexpectedly", diagnostics);
3303
+ let errorMessage;
3304
+ if (event.code === 1e3) {
3305
+ errorMessage = event.reason || "Stream ended by server";
3306
+ } else if (event.code === 1001) {
3307
+ errorMessage = "Connection closed - server going away";
3308
+ } else if (event.code === 1006) {
3309
+ errorMessage = "Connection lost unexpectedly";
3310
+ } else if (event.code >= 4e3 && event.code < 5e3) {
3311
+ errorMessage = event.reason || "Stream terminated by server";
3312
+ } else {
3313
+ errorMessage = `Connection closed (code: ${event.code})`;
3314
+ }
3315
+ this.onStateChange?.("terminated");
3316
+ this.onError?.(errorMessage, diagnostics);
3317
+ }
3318
+ if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
3319
+ this.mediaRecorder.stop();
3320
+ }
3321
+ });
3322
+ this.websocket.addEventListener("error", (event) => {
3323
+ console.error("\u274C WebSocket error:", event);
3324
+ this.onError?.("WebSocket connection error");
3325
+ this.onStateChange?.("error");
3326
+ });
3327
+ }
3328
+ /**
3329
+ * Set up MediaRecorder event handlers
3330
+ */
3331
+ setupMediaRecorderHandlers() {
3332
+ if (!this.mediaRecorder) return;
3333
+ this.mediaRecorder.addEventListener("dataavailable", (event) => {
3334
+ if (event.data.size > 0 && this.websocket?.readyState === WebSocket.OPEN) {
3335
+ this.websocket.send(event.data);
3336
+ this.bytesSent += event.data.size;
3337
+ this.chunksSent += 1;
3338
+ this.onBytesUpdate?.(this.bytesSent);
3339
+ if (this.chunksSent % 10 === 0) {
3340
+ console.log(`\u{1F4E4} Sent ${this.chunksSent} chunks (${(this.bytesSent / 1024 / 1024).toFixed(2)} MB total)`);
3341
+ }
3342
+ }
3343
+ });
3344
+ this.mediaRecorder.addEventListener("error", (event) => {
3345
+ const errorEvent = event;
3346
+ console.error("\u274C MediaRecorder error:", errorEvent.error);
3347
+ this.onError?.(`Encoding error: ${errorEvent.error?.toString() || "Unknown error"}`);
3348
+ this.onStateChange?.("error");
3349
+ this.stop();
3350
+ });
3351
+ this.mediaRecorder.addEventListener("stop", () => {
3352
+ console.log("\u23F9\uFE0F MediaRecorder stopped");
3353
+ if (!this.isHotSwapping && this.websocket?.readyState === WebSocket.OPEN) {
3354
+ this.websocket.close();
3355
+ }
3356
+ });
3357
+ }
3358
+ };
3359
+ function StreamingPreview({
3360
+ videoRef,
3361
+ isVideoKey,
3362
+ isVideoEnabled,
3363
+ mediaStream,
3364
+ facingMode
3365
+ }) {
3366
+ return /* @__PURE__ */ jsxs("div", { className: "dialtribe-streaming-preview flex-1 relative bg-black overflow-hidden", children: [
3367
+ isVideoKey && isVideoEnabled && /* @__PURE__ */ jsx(
3368
+ "video",
3369
+ {
3370
+ ref: videoRef,
3371
+ autoPlay: true,
3372
+ muted: true,
3373
+ playsInline: true,
3374
+ className: `w-full h-full object-cover ${facingMode === "user" ? "scale-x-[-1]" : ""}`,
3375
+ style: { maxHeight: "100vh" }
3376
+ }
3377
+ ),
3378
+ !isVideoKey && /* @__PURE__ */ jsx("div", { className: "w-full h-full flex items-center justify-center p-8", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-4xl", children: [
3379
+ /* @__PURE__ */ jsx("div", { className: "w-full h-auto border border-gray-800 rounded-lg overflow-hidden", children: /* @__PURE__ */ jsx(AudioWaveform, { mediaStream, isPlaying: true }) }),
3380
+ /* @__PURE__ */ jsxs("div", { className: "text-center mt-4", children: [
3381
+ /* @__PURE__ */ jsx("div", { className: "w-16 h-16 bg-blue-600 rounded-full flex items-center justify-center mx-auto mb-3", children: /* @__PURE__ */ jsx(
3382
+ "svg",
3383
+ {
3384
+ className: "w-8 h-8 text-white",
3385
+ fill: "none",
3386
+ stroke: "currentColor",
3387
+ viewBox: "0 0 24 24",
3388
+ children: /* @__PURE__ */ jsx(
3389
+ "path",
3390
+ {
3391
+ strokeLinecap: "round",
3392
+ strokeLinejoin: "round",
3393
+ strokeWidth: 2,
3394
+ 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"
3395
+ }
3396
+ )
3397
+ }
3398
+ ) }),
3399
+ /* @__PURE__ */ jsx("p", { className: "text-white text-xl font-medium", children: "Audio-Only Stream" }),
3400
+ /* @__PURE__ */ jsx("p", { className: "text-gray-400 text-sm mt-1", children: "Your audio is being captured" })
3401
+ ] })
3402
+ ] }) }),
3403
+ isVideoKey && !isVideoEnabled && /* @__PURE__ */ jsx("div", { className: "w-full h-full flex items-center justify-center p-8 bg-gray-900", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-4xl", children: [
3404
+ /* @__PURE__ */ jsx("div", { className: "w-full h-auto border border-gray-800 rounded-lg overflow-hidden mb-6", children: /* @__PURE__ */ jsx(AudioWaveform, { mediaStream, isPlaying: true }) }),
3405
+ /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
3406
+ /* @__PURE__ */ jsx("div", { className: "w-16 h-16 bg-gray-800 rounded-full flex items-center justify-center mx-auto mb-3", children: /* @__PURE__ */ jsx(
3407
+ "svg",
3408
+ {
3409
+ className: "w-8 h-8 text-gray-400",
3410
+ fill: "none",
3411
+ stroke: "currentColor",
3412
+ viewBox: "0 0 24 24",
3413
+ "aria-hidden": "true",
3414
+ children: /* @__PURE__ */ jsx(
3415
+ "path",
3416
+ {
3417
+ strokeLinecap: "round",
3418
+ strokeLinejoin: "round",
3419
+ strokeWidth: 2,
3420
+ 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"
3421
+ }
3422
+ )
3423
+ }
3424
+ ) }),
3425
+ /* @__PURE__ */ jsx("p", { className: "text-white text-xl font-medium", children: "Camera Off" }),
3426
+ /* @__PURE__ */ jsx("p", { className: "text-gray-400 text-sm mt-1", children: "Your audio is still being broadcast" })
3427
+ ] })
3428
+ ] }) })
3429
+ ] });
3430
+ }
3431
+ function StreamKeyDisplay({
3432
+ streamKey,
3433
+ className = "",
3434
+ showLabel = true,
3435
+ showCopy = true,
3436
+ editable = false,
3437
+ onChange,
3438
+ size = "md",
3439
+ layout = "vertical",
3440
+ darkMode = false
3441
+ }) {
3442
+ const [isRevealed, setIsRevealed] = useState(false);
3443
+ const [copySuccess, setCopySuccess] = useState(false);
3444
+ const obscureStreamKey = (key) => {
3445
+ if (key.length <= 12) {
3446
+ return "\u2022".repeat(key.length);
3447
+ }
3448
+ return `${key.substring(0, 6)}${"\u2022".repeat(key.length - 12)}${key.substring(key.length - 6)}`;
3449
+ };
3450
+ const handleCopy = async () => {
3451
+ try {
3452
+ await navigator.clipboard.writeText(streamKey);
3453
+ setCopySuccess(true);
3454
+ setTimeout(() => setCopySuccess(false), 2e3);
3455
+ } catch (err) {
3456
+ console.error("Failed to copy stream key:", err);
3457
+ }
3458
+ };
3459
+ const handleReveal = () => {
3460
+ setIsRevealed(!isRevealed);
3461
+ };
3462
+ const sizeClasses2 = {
3463
+ sm: {
3464
+ label: "text-xs",
3465
+ code: "text-xs px-2 py-1",
3466
+ button: "text-xs px-2 py-1"
3467
+ },
3468
+ md: {
3469
+ label: "text-sm",
3470
+ code: "text-sm px-2 py-1",
3471
+ button: "text-sm px-3 py-1.5"
3472
+ },
3473
+ lg: {
3474
+ label: "text-base",
3475
+ code: "text-base px-3 py-2",
3476
+ button: "text-base px-4 py-2"
3477
+ }
3478
+ };
3479
+ const styles = sizeClasses2[size];
3480
+ const isHorizontal = layout === "horizontal";
3481
+ const containerClass = isHorizontal ? "flex items-center gap-3" : "flex flex-col gap-2";
3482
+ 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`;
3483
+ const labelClass = darkMode ? `${styles.label} text-white/80 font-medium whitespace-nowrap` : `${styles.label} text-gray-600 dark:text-gray-400 font-medium`;
3484
+ 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`;
3485
+ return /* @__PURE__ */ jsxs("div", { className: `dialtribe-stream-key-display ${containerClass} ${className}`, children: [
3486
+ showLabel && /* @__PURE__ */ jsx("span", { className: labelClass, children: "Stream Key:" }),
3487
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 min-w-0 overflow-hidden", children: [
3488
+ isRevealed && editable ? /* @__PURE__ */ jsx(
3489
+ "input",
3490
+ {
3491
+ type: "text",
3492
+ value: streamKey,
3493
+ onChange: (e) => onChange?.(e.target.value),
3494
+ 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`,
3495
+ placeholder: "Enter stream key"
3496
+ }
3497
+ ) : /* @__PURE__ */ jsx("code", { className: codeClass, children: isRevealed ? streamKey : obscureStreamKey(streamKey) }),
3498
+ /* @__PURE__ */ jsx(
3499
+ "button",
3500
+ {
3501
+ onClick: handleReveal,
3502
+ className: `${revealButtonClass} shrink-0`,
3503
+ children: isRevealed ? "Hide" : "Reveal"
3504
+ }
3505
+ ),
3506
+ showCopy && /* @__PURE__ */ jsx(
3507
+ "button",
3508
+ {
3509
+ onClick: handleCopy,
3510
+ 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`,
3511
+ title: "Copy to clipboard",
3512
+ children: copySuccess ? "Copied!" : "Copy"
3513
+ }
3514
+ )
3515
+ ] })
3516
+ ] });
3517
+ }
3518
+ function StreamingControls({
3519
+ state,
3520
+ isVideoKey,
3521
+ isMuted,
3522
+ isVideoEnabled,
3523
+ facingMode: _facingMode,
3524
+ // Reserved for future use (e.g., showing camera direction)
3525
+ hasMultipleCameras,
3526
+ startTime,
3527
+ bytesSent,
3528
+ showStopConfirm,
3529
+ streamKey,
3530
+ onStreamKeyChange,
3531
+ onStart,
3532
+ onStop,
3533
+ onConfirmStop,
3534
+ onCancelStop,
3535
+ onToggleMute,
3536
+ onToggleVideo,
3537
+ onFlipCamera,
3538
+ onClose,
3539
+ showCloseConfirm,
3540
+ onConfirmClose,
3541
+ onCancelClose,
3542
+ // Device selection props
3543
+ videoDevices = [],
3544
+ audioDevices = [],
3545
+ selectedVideoDeviceId,
3546
+ selectedAudioDeviceId,
3547
+ onVideoDeviceChange,
3548
+ onAudioDeviceChange,
3549
+ mediaStream
3550
+ }) {
3551
+ const [duration, setDuration] = useState(0);
3552
+ const [showSettings, setShowSettings] = useState(false);
3553
+ useEffect(() => {
3554
+ if (state !== "live" || !startTime) return;
3555
+ const interval = setInterval(() => {
3556
+ const elapsed = Math.floor((Date.now() - startTime.getTime()) / 1e3);
3557
+ setDuration(elapsed);
3558
+ }, 1e3);
3559
+ return () => clearInterval(interval);
3560
+ }, [state, startTime]);
3561
+ const formatDuration = (seconds) => {
3562
+ const hours = Math.floor(seconds / 3600);
3563
+ const minutes = Math.floor(seconds % 3600 / 60);
3564
+ const secs = seconds % 60;
3565
+ if (hours > 0) {
3566
+ return `${hours}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
3567
+ }
3568
+ return `${minutes}:${secs.toString().padStart(2, "0")}`;
3569
+ };
3570
+ const formatBytes = (bytes) => {
3571
+ if (bytes === 0) return "0 B";
3572
+ const k = 1024;
3573
+ const sizes = ["B", "KB", "MB", "GB"];
3574
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
3575
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
3576
+ };
3577
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3578
+ state === "previewing" && onClose && /* @__PURE__ */ 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__ */ jsx(
3579
+ "button",
3580
+ {
3581
+ onClick: onClose,
3582
+ 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",
3583
+ title: "Close broadcast preview",
3584
+ "aria-label": "Close broadcast preview",
3585
+ children: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
3586
+ }
3587
+ ) }),
3588
+ state === "live" && /* @__PURE__ */ 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__ */ jsxs("div", { className: "flex items-center justify-between", children: [
3589
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3590
+ /* @__PURE__ */ jsx("div", { className: "w-3 h-3 bg-red-600 rounded-full animate-pulse" }),
3591
+ /* @__PURE__ */ jsx("span", { className: "text-white font-semibold text-lg", children: "LIVE" })
3592
+ ] }),
3593
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
3594
+ /* @__PURE__ */ jsxs("div", { className: "text-right", children: [
3595
+ /* @__PURE__ */ jsx("div", { className: "text-white font-mono text-lg font-semibold", children: formatDuration(duration) }),
3596
+ /* @__PURE__ */ jsxs("div", { className: "text-white/80 text-sm", children: [
3597
+ formatBytes(bytesSent),
3598
+ " sent"
3599
+ ] })
3600
+ ] }),
3601
+ onClose && /* @__PURE__ */ jsx(
3602
+ "button",
3603
+ {
3604
+ onClick: onClose,
3605
+ className: "w-10 h-10 bg-black/50 hover:bg-black/70 backdrop-blur rounded-full flex items-center justify-center transition-colors",
3606
+ title: "Close and end broadcast",
3607
+ "aria-label": "Close and end broadcast",
3608
+ children: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
3609
+ }
3610
+ )
3611
+ ] })
3612
+ ] }) }),
3613
+ /* @__PURE__ */ jsxs("div", { className: "absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black/80 to-transparent z-10", children: [
3614
+ state === "previewing" && /* @__PURE__ */ jsxs("div", { className: "relative flex justify-center items-center", children: [
3615
+ isVideoKey && hasMultipleCameras && /* @__PURE__ */ jsx(
3616
+ "button",
3617
+ {
3618
+ onClick: onFlipCamera,
3619
+ className: "absolute left-0 w-10 h-10 bg-black/50 hover:bg-black/70 backdrop-blur rounded-full flex items-center justify-center transition-colors",
3620
+ title: "Switch camera",
3621
+ "aria-label": "Switch between front and back camera",
3622
+ children: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
3623
+ "path",
3624
+ {
3625
+ strokeLinecap: "round",
3626
+ strokeLinejoin: "round",
3627
+ strokeWidth: 2,
3628
+ 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"
3629
+ }
3630
+ ) })
3631
+ }
3632
+ ),
3633
+ /* @__PURE__ */ jsx(
3634
+ "button",
3635
+ {
3636
+ onClick: onStart,
3637
+ 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",
3638
+ "aria-label": "Start live streaming",
3639
+ children: "Start Streaming"
3640
+ }
3641
+ ),
3642
+ /* @__PURE__ */ jsx(
3643
+ "button",
3644
+ {
3645
+ onClick: () => setShowSettings(true),
3646
+ 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",
3647
+ title: "Stream settings",
3648
+ "aria-label": "Open stream settings",
3649
+ children: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5 text-white", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ 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" }) })
3650
+ }
3651
+ )
3652
+ ] }),
3653
+ state === "connecting" && /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsxs("div", { className: "px-12 py-4 bg-blue-600 text-white text-xl font-bold rounded-full flex items-center gap-3", children: [
3654
+ /* @__PURE__ */ jsx("div", { className: "w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" }),
3655
+ "Connecting..."
3656
+ ] }) }),
3657
+ state === "live" && !showStopConfirm && !showCloseConfirm && /* @__PURE__ */ jsx("div", { className: "space-y-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-4", children: [
3658
+ /* @__PURE__ */ jsx(
3659
+ "button",
3660
+ {
3661
+ onClick: onToggleMute,
3662
+ 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"}`,
3663
+ title: isMuted ? "Unmute microphone" : "Mute microphone",
3664
+ "aria-label": isMuted ? "Unmute microphone" : "Mute microphone",
3665
+ "aria-pressed": isMuted,
3666
+ children: isMuted ? /* @__PURE__ */ jsx("svg", { className: "w-6 h-6 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
3667
+ "path",
3668
+ {
3669
+ strokeLinecap: "round",
3670
+ strokeLinejoin: "round",
3671
+ strokeWidth: 2,
3672
+ 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"
3673
+ }
3674
+ ) }) : /* @__PURE__ */ jsx("svg", { className: "w-6 h-6 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
3675
+ "path",
3676
+ {
3677
+ strokeLinecap: "round",
3678
+ strokeLinejoin: "round",
3679
+ strokeWidth: 2,
3680
+ 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"
3681
+ }
3682
+ ) })
3683
+ }
3684
+ ),
3685
+ /* @__PURE__ */ jsx(
3686
+ "button",
3687
+ {
3688
+ onClick: onStop,
3689
+ className: "w-16 h-16 bg-red-600 hover:bg-red-700 rounded-full flex items-center justify-center transition-all shadow-lg",
3690
+ title: "Stop streaming",
3691
+ "aria-label": "Stop streaming",
3692
+ children: /* @__PURE__ */ jsx("div", { className: "w-6 h-6 bg-white rounded-sm", "aria-hidden": "true" })
3693
+ }
3694
+ ),
3695
+ isVideoKey && /* @__PURE__ */ jsx(
3696
+ "button",
3697
+ {
3698
+ onClick: onToggleVideo,
3699
+ 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"}`,
3700
+ title: isVideoEnabled ? "Turn camera off" : "Turn camera on",
3701
+ "aria-label": isVideoEnabled ? "Turn camera off" : "Turn camera on",
3702
+ "aria-pressed": !isVideoEnabled,
3703
+ children: isVideoEnabled ? /* @__PURE__ */ jsx("svg", { className: "w-6 h-6 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
3704
+ "path",
3705
+ {
3706
+ strokeLinecap: "round",
3707
+ strokeLinejoin: "round",
3708
+ strokeWidth: 2,
3709
+ 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"
3710
+ }
3711
+ ) }) : /* @__PURE__ */ jsx("svg", { className: "w-6 h-6 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
3712
+ "path",
3713
+ {
3714
+ strokeLinecap: "round",
3715
+ strokeLinejoin: "round",
3716
+ strokeWidth: 2,
3717
+ 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"
3718
+ }
3719
+ ) })
3720
+ }
3721
+ ),
3722
+ isVideoKey && hasMultipleCameras && /* @__PURE__ */ jsx(
3723
+ "button",
3724
+ {
3725
+ onClick: onFlipCamera,
3726
+ 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",
3727
+ title: "Switch camera",
3728
+ "aria-label": "Switch between front and back camera",
3729
+ children: /* @__PURE__ */ jsx("svg", { className: "w-6 h-6 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
3730
+ "path",
3731
+ {
3732
+ strokeLinecap: "round",
3733
+ strokeLinejoin: "round",
3734
+ strokeWidth: 2,
3735
+ 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"
3736
+ }
3737
+ ) })
3738
+ }
3739
+ ),
3740
+ /* @__PURE__ */ jsx(
3741
+ "button",
3742
+ {
3743
+ disabled: true,
3744
+ className: "w-14 h-14 bg-white/10 rounded-full flex items-center justify-center opacity-50 cursor-not-allowed shadow-lg",
3745
+ title: "Create clip (Coming soon)",
3746
+ "aria-label": "Create clip (Coming soon)",
3747
+ "aria-disabled": "true",
3748
+ children: /* @__PURE__ */ jsx("svg", { className: "w-6 h-6 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
3749
+ "path",
3750
+ {
3751
+ strokeLinecap: "round",
3752
+ strokeLinejoin: "round",
3753
+ strokeWidth: 2,
3754
+ 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"
3755
+ }
3756
+ ) })
3757
+ }
3758
+ )
3759
+ ] }) })
3760
+ ] }),
3761
+ showStopConfirm && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 z-20 flex items-center justify-center p-4 bg-black/70", children: /* @__PURE__ */ 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: [
3762
+ /* @__PURE__ */ jsx("p", { id: "stop-dialog-title", className: "text-white text-center text-lg font-medium mb-4", children: "Stop streaming?" }),
3763
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-3", children: [
3764
+ /* @__PURE__ */ jsx(
3765
+ "button",
3766
+ {
3767
+ onClick: onCancelStop,
3768
+ className: "flex-1 px-6 py-3 bg-white/20 hover:bg-white/30 text-white font-medium rounded-lg transition-colors",
3769
+ "aria-label": "Cancel and continue streaming",
3770
+ children: "Cancel"
3771
+ }
3772
+ ),
3773
+ /* @__PURE__ */ jsx(
3774
+ "button",
3775
+ {
3776
+ onClick: onConfirmStop,
3777
+ className: "flex-1 px-6 py-3 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors",
3778
+ "aria-label": "Confirm stop streaming",
3779
+ children: "Stop"
3780
+ }
3781
+ )
3782
+ ] })
3783
+ ] }) }),
3784
+ showCloseConfirm && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 z-20 flex items-center justify-center p-4 bg-black/70", children: /* @__PURE__ */ 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: [
3785
+ /* @__PURE__ */ jsx("p", { id: "close-dialog-title", className: "text-white text-center text-lg font-medium mb-2", children: "Close and end stream?" }),
3786
+ /* @__PURE__ */ jsx("p", { id: "close-dialog-description", className: "text-white/70 text-center text-sm mb-4", children: "Closing will stop your live broadcast." }),
3787
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-3", children: [
3788
+ /* @__PURE__ */ jsx(
3789
+ "button",
3790
+ {
3791
+ onClick: onCancelClose,
3792
+ className: "flex-1 px-6 py-3 bg-white/20 hover:bg-white/30 text-white font-medium rounded-lg transition-colors",
3793
+ "aria-label": "Cancel and continue streaming",
3794
+ children: "Cancel"
3795
+ }
3796
+ ),
3797
+ /* @__PURE__ */ jsx(
3798
+ "button",
3799
+ {
3800
+ onClick: onConfirmClose,
3801
+ className: "flex-1 px-6 py-3 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors",
3802
+ "aria-label": "Confirm end broadcast and close",
3803
+ children: "End & Close"
3804
+ }
3805
+ )
3806
+ ] })
3807
+ ] }) }),
3808
+ showSettings && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 z-20 flex items-center justify-center p-4 bg-black/80", children: /* @__PURE__ */ jsxs(
3809
+ "div",
3810
+ {
3811
+ 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",
3812
+ role: "dialog",
3813
+ "aria-labelledby": "settings-dialog-title",
3814
+ children: [
3815
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between p-4 border-b border-white/10", children: [
3816
+ /* @__PURE__ */ jsx("h2", { id: "settings-dialog-title", className: "text-white text-lg font-semibold", children: "Stream Settings" }),
3817
+ /* @__PURE__ */ jsx(
3818
+ "button",
3819
+ {
3820
+ onClick: () => setShowSettings(false),
3821
+ className: "w-8 h-8 bg-white/10 hover:bg-white/20 rounded-full flex items-center justify-center transition-colors",
3822
+ title: "Close settings",
3823
+ "aria-label": "Close settings",
3824
+ children: /* @__PURE__ */ jsx("svg", { className: "w-4 h-4 text-white", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
3825
+ }
3826
+ )
3827
+ ] }),
3828
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-6", children: [
3829
+ /* @__PURE__ */ jsxs("div", { children: [
3830
+ /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-white/80 mb-2", children: "Stream Key" }),
3831
+ /* @__PURE__ */ jsx("div", { className: "bg-white/5 border border-white/10 rounded-lg p-3", children: /* @__PURE__ */ jsx(
3832
+ StreamKeyDisplay,
3833
+ {
3834
+ streamKey,
3835
+ editable: !!onStreamKeyChange,
3836
+ onChange: onStreamKeyChange,
3837
+ showCopy: true,
3838
+ size: "sm",
3839
+ layout: "vertical",
3840
+ darkMode: true
3841
+ }
3842
+ ) })
3843
+ ] }),
3844
+ isVideoKey && videoDevices.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
3845
+ /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-white/80 mb-2", children: "Camera" }),
3846
+ /* @__PURE__ */ jsx(
3847
+ "select",
3848
+ {
3849
+ value: selectedVideoDeviceId || "",
3850
+ onChange: (e) => onVideoDeviceChange?.(e.target.value),
3851
+ 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",
3852
+ "aria-label": "Select camera",
3853
+ children: videoDevices.map((device) => /* @__PURE__ */ jsx("option", { value: device.deviceId, className: "bg-zinc-900", children: device.label || `Camera ${device.deviceId.slice(0, 8)}` }, device.deviceId))
3854
+ }
3855
+ ),
3856
+ /* @__PURE__ */ jsx("div", { className: "mt-3 aspect-video bg-black rounded-lg overflow-hidden border border-white/10", children: mediaStream && isVideoEnabled ? /* @__PURE__ */ jsx(
3857
+ "video",
3858
+ {
3859
+ autoPlay: true,
3860
+ muted: true,
3861
+ playsInline: true,
3862
+ className: "w-full h-full object-cover scale-x-[-1]",
3863
+ ref: (el) => {
3864
+ if (el && mediaStream) {
3865
+ el.srcObject = mediaStream;
3866
+ }
3867
+ }
3868
+ }
3869
+ ) : /* @__PURE__ */ jsx("div", { className: "w-full h-full flex items-center justify-center", children: /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
3870
+ /* @__PURE__ */ 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__ */ 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" }) }),
3871
+ /* @__PURE__ */ jsx("p", { className: "text-gray-500 text-xs", children: "Camera off" })
3872
+ ] }) }) })
3873
+ ] }),
3874
+ audioDevices.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
3875
+ /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-white/80 mb-2", children: "Microphone" }),
3876
+ /* @__PURE__ */ jsx(
3877
+ "select",
3878
+ {
3879
+ value: selectedAudioDeviceId || "",
3880
+ onChange: (e) => onAudioDeviceChange?.(e.target.value),
3881
+ 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",
3882
+ "aria-label": "Select microphone",
3883
+ children: audioDevices.map((device) => /* @__PURE__ */ jsx("option", { value: device.deviceId, className: "bg-zinc-900", children: device.label || `Microphone ${device.deviceId.slice(0, 8)}` }, device.deviceId))
3884
+ }
3885
+ ),
3886
+ /* @__PURE__ */ jsx("div", { className: "mt-3 h-16 bg-black rounded-lg overflow-hidden border border-white/10", children: mediaStream && !isMuted ? /* @__PURE__ */ jsx(AudioWaveform, { mediaStream, isPlaying: true }) : /* @__PURE__ */ jsx("div", { className: "w-full h-full flex items-center justify-center", children: /* @__PURE__ */ jsx("p", { className: "text-gray-500 text-xs", children: isMuted ? "Microphone muted" : "No audio" }) }) })
3887
+ ] })
3888
+ ] }),
3889
+ /* @__PURE__ */ jsx("div", { className: "p-4 border-t border-white/10", children: /* @__PURE__ */ jsx(
3890
+ "button",
3891
+ {
3892
+ onClick: () => setShowSettings(false),
3893
+ className: "w-full px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors",
3894
+ "aria-label": "Save and close settings",
3895
+ children: "Done"
3896
+ }
3897
+ ) })
3898
+ ]
3899
+ }
3900
+ ) })
3901
+ ] });
3902
+ }
3903
+ function StreamKeyInput({ onSubmit, inline = false }) {
3904
+ const [streamKey, setStreamKey] = useState("");
3905
+ const [error, setError] = useState("");
3906
+ 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";
3907
+ const validateStreamKey = (key) => {
3908
+ const pattern = /^[abvw][a-zA-Z0-9]+_.+$/;
3909
+ return pattern.test(key);
3910
+ };
3911
+ const handleSubmit = (e) => {
3912
+ e.preventDefault();
3913
+ setError("");
3914
+ const trimmedKey = streamKey.trim();
3915
+ if (!trimmedKey) {
3916
+ setError("Please enter a stream key");
3917
+ return;
3918
+ }
3919
+ if (!validateStreamKey(trimmedKey)) {
3920
+ setError(
3921
+ "Invalid stream key format. Expected format: {mediaType}{foreignId}_{key} (e.g., w1_abc123...)"
3922
+ );
3923
+ return;
3924
+ }
3925
+ onSubmit(trimmedKey);
3926
+ };
3927
+ return /* @__PURE__ */ jsx("div", { className: containerClass, children: /* @__PURE__ */ 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: [
3928
+ /* @__PURE__ */ jsxs("div", { className: "text-center mb-8", children: [
3929
+ /* @__PURE__ */ jsx("h1", { className: "text-3xl font-bold text-black dark:text-white mb-2", children: "Start Broadcasting" }),
3930
+ /* @__PURE__ */ jsx("p", { className: "text-gray-600 dark:text-gray-400", children: "Enter your stream key to get started" })
3931
+ ] }),
3932
+ /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "space-y-6", children: [
3933
+ /* @__PURE__ */ jsxs("div", { children: [
3934
+ /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2", children: "Stream Key" }),
3935
+ /* @__PURE__ */ jsx(
3936
+ "input",
3937
+ {
3938
+ type: "text",
3939
+ value: streamKey,
3940
+ onChange: (e) => {
3941
+ setStreamKey(e.target.value);
3942
+ setError("");
3943
+ },
3944
+ 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",
3945
+ placeholder: "w1_abc123...",
3946
+ autoFocus: true
3947
+ }
3948
+ ),
3949
+ error && /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm text-red-600 dark:text-red-400", children: error }),
3950
+ /* @__PURE__ */ jsx("p", { className: "mt-2 text-xs text-gray-500 dark:text-gray-400", children: "Paste the stream key provided by your administrator" })
3951
+ ] }),
3952
+ /* @__PURE__ */ jsx(
3953
+ "button",
3954
+ {
3955
+ type: "submit",
3956
+ className: "w-full px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors",
3957
+ children: "Continue"
3958
+ }
3959
+ )
3960
+ ] }),
3961
+ /* @__PURE__ */ jsxs("div", { className: "mt-8 pt-6 border-t border-gray-200 dark:border-zinc-800", children: [
3962
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400 mb-2", children: "Where do I find my stream key?" }),
3963
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: [
3964
+ "If you have access to the DialTribe dashboard, navigate to your app's ",
3965
+ /* @__PURE__ */ jsx("strong", { children: "Stream Keys" }),
3966
+ ' page and click the "Broadcast" button next to any active stream key.'
3967
+ ] })
3968
+ ] })
3969
+ ] }) });
3970
+ }
3971
+ function DialtribeStreamer({
3972
+ sessionToken: propSessionToken,
3973
+ streamKey: initialStreamKey,
3974
+ onDone,
3975
+ onStreamKeyChange,
3976
+ encoderServerUrl = DEFAULT_ENCODER_SERVER_URL,
3977
+ apiBaseUrl = DIALTRIBE_API_BASE,
3978
+ inline = false
3979
+ }) {
3980
+ const containerClass = inline ? "dialtribe-dialtribe-streamer h-full w-full bg-black relative" : "dialtribe-dialtribe-streamer min-h-screen bg-black";
3981
+ const centeredContainerClass = inline ? "dialtribe-dialtribe-streamer flex items-center justify-center h-full w-full p-4 bg-black relative" : "dialtribe-dialtribe-streamer flex items-center justify-center min-h-screen p-4 bg-black";
3982
+ const dialTribeContext = useDialtribeOptional();
3983
+ const sessionToken = propSessionToken ?? dialTribeContext?.sessionToken ?? null;
3984
+ const [streamKey, setStreamKey] = useState(initialStreamKey || null);
3985
+ const [state, setState] = useState("idle");
3986
+ useEffect(() => {
3987
+ if (initialStreamKey && initialStreamKey !== streamKey) {
3988
+ setStreamKey(initialStreamKey);
3989
+ }
3990
+ }, [initialStreamKey]);
3991
+ const [error, setError] = useState(null);
3992
+ const [diagnostics, setDiagnostics] = useState(null);
3993
+ const [mediaStream, setMediaStream] = useState(null);
3994
+ const [streamer, setStreamer] = useState(null);
3995
+ const [bytesSent, setBytesSent] = useState(0);
3996
+ const [startTime, setStartTime] = useState(null);
3997
+ const [isMuted, setIsMuted] = useState(false);
3998
+ const [isVideoEnabled, setIsVideoEnabled] = useState(true);
3999
+ const [facingMode, setFacingMode] = useState("user");
4000
+ const [showStopConfirm, setShowStopConfirm] = useState(false);
4001
+ const [showCloseConfirm, setShowCloseConfirm] = useState(false);
4002
+ const [hasMultipleCameras, setHasMultipleCameras] = useState(false);
4003
+ const [videoDevices, setVideoDevices] = useState([]);
4004
+ const [audioDevices, setAudioDevices] = useState([]);
4005
+ const [selectedVideoDeviceId, setSelectedVideoDeviceId] = useState();
4006
+ const [selectedAudioDeviceId, setSelectedAudioDeviceId] = useState();
4007
+ const videoRef = useRef(null);
4008
+ const streamerRef = useRef(null);
4009
+ const mediaStreamRef = useRef(null);
4010
+ const isVideoKey = streamKey ? streamKey.startsWith("v") || streamKey.startsWith("w") : false;
4011
+ const handleStreamKeySubmit = (key) => {
4012
+ setStreamKey(key);
4013
+ onStreamKeyChange?.(key);
4014
+ };
4015
+ const handleStreamKeyChange = (key) => {
4016
+ setStreamKey(key);
4017
+ onStreamKeyChange?.(key);
4018
+ };
4019
+ useEffect(() => {
4020
+ if (!streamKey) return;
4021
+ const compat = checkBrowserCompatibility();
4022
+ if (!compat.compatible) {
4023
+ setError(compat.error || "Browser not compatible");
4024
+ setState("error");
4025
+ return;
4026
+ }
4027
+ detectCameras();
4028
+ requestMediaPermissions();
4029
+ }, [streamKey]);
4030
+ const detectCameras = async () => {
4031
+ try {
4032
+ const devices = await navigator.mediaDevices.enumerateDevices();
4033
+ const videoInputs = devices.filter((device) => device.kind === "videoinput");
4034
+ const audioInputs = devices.filter((device) => device.kind === "audioinput");
4035
+ console.log(`\u{1F4F7} Found ${videoInputs.length} video input device(s)`);
4036
+ console.log(`\u{1F3A4} Found ${audioInputs.length} audio input device(s)`);
4037
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
4038
+ const hasMultiple = videoInputs.length > 1 || isIOS && isVideoKey;
4039
+ setHasMultipleCameras(hasMultiple);
4040
+ if (isIOS && videoInputs.length <= 1) {
4041
+ console.log("\u{1F4F1} iOS device detected - assuming multiple cameras available");
4042
+ }
4043
+ setVideoDevices(videoInputs.map((d) => ({
4044
+ deviceId: d.deviceId,
4045
+ label: d.label || `Camera ${d.deviceId.slice(0, 8)}`
4046
+ })));
4047
+ setAudioDevices(audioInputs.map((d) => ({
4048
+ deviceId: d.deviceId,
4049
+ label: d.label || `Microphone ${d.deviceId.slice(0, 8)}`
4050
+ })));
4051
+ if (videoInputs.length > 0 && !selectedVideoDeviceId) {
4052
+ setSelectedVideoDeviceId(videoInputs[0].deviceId);
4053
+ }
4054
+ if (audioInputs.length > 0 && !selectedAudioDeviceId) {
4055
+ setSelectedAudioDeviceId(audioInputs[0].deviceId);
4056
+ }
4057
+ } catch (err) {
4058
+ console.error("\u274C Failed to enumerate devices:", err);
4059
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
4060
+ setHasMultipleCameras(isIOS && isVideoKey);
4061
+ }
4062
+ };
4063
+ useEffect(() => {
4064
+ streamerRef.current = streamer;
4065
+ }, [streamer]);
4066
+ useEffect(() => {
4067
+ mediaStreamRef.current = mediaStream;
4068
+ }, [mediaStream]);
4069
+ useEffect(() => {
4070
+ return () => {
4071
+ if (streamerRef.current) {
4072
+ streamerRef.current.stop();
4073
+ }
4074
+ if (mediaStreamRef.current) {
4075
+ mediaStreamRef.current.getTracks().forEach((track) => track.stop());
4076
+ }
4077
+ };
4078
+ }, []);
4079
+ useEffect(() => {
4080
+ if (state === "live") {
4081
+ const handleBeforeUnload = (e) => {
4082
+ e.preventDefault();
4083
+ e.returnValue = "You are currently streaming. Are you sure you want to leave?";
4084
+ return e.returnValue;
4085
+ };
4086
+ window.addEventListener("beforeunload", handleBeforeUnload);
4087
+ return () => window.removeEventListener("beforeunload", handleBeforeUnload);
4088
+ }
4089
+ }, [state]);
4090
+ useEffect(() => {
4091
+ if (videoRef.current && mediaStream) {
4092
+ videoRef.current.srcObject = mediaStream;
4093
+ }
4094
+ }, [mediaStream, isVideoEnabled]);
4095
+ const requestMediaPermissions = async () => {
4096
+ if (!streamKey) return;
4097
+ try {
4098
+ setState("requesting");
4099
+ setError(null);
4100
+ const constraints = getMediaConstraints({
4101
+ isVideo: isVideoKey && isVideoEnabled,
4102
+ facingMode
4103
+ });
4104
+ console.log("\u{1F4F8} Requesting media permissions:", constraints);
4105
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
4106
+ console.log("\u2705 Media permissions granted");
4107
+ setMediaStream(stream);
4108
+ setState("previewing");
4109
+ } catch (err) {
4110
+ console.error("\u274C Media permission error:", err);
4111
+ if (err instanceof Error) {
4112
+ if (err.name === "NotAllowedError" || err.name === "PermissionDeniedError") {
4113
+ setError("Camera/microphone access denied. Please enable permissions in your browser settings.");
4114
+ } else if (err.name === "NotFoundError") {
4115
+ setError(isVideoKey ? "No camera or microphone found." : "No microphone found.");
4116
+ } else {
4117
+ setError(err.message || "Failed to access media devices");
4118
+ }
4119
+ } else {
4120
+ setError("Failed to access media devices");
4121
+ }
4122
+ setState("error");
4123
+ }
4124
+ };
4125
+ const handleStartStreaming = async () => {
4126
+ if (!mediaStream || !streamKey) {
4127
+ setError("No media stream available");
4128
+ return;
4129
+ }
4130
+ try {
4131
+ setState("connecting");
4132
+ setError(null);
4133
+ console.log("\u{1F50D} Checking broadcast availability...");
4134
+ const checkResponse = await fetch(`${apiBaseUrl}/broadcasts/check`, {
4135
+ method: "POST",
4136
+ headers: {
4137
+ "Content-Type": "application/json",
4138
+ "Authorization": `Bearer ${sessionToken}`
4139
+ },
4140
+ body: JSON.stringify({
4141
+ streamKey
4142
+ })
4143
+ });
4144
+ if (!checkResponse.ok) {
4145
+ if (checkResponse.status === 401) {
4146
+ console.error("\u274C Session token invalid or expired");
4147
+ setError("Session expired. Please refresh the page and try again.");
4148
+ setState("error");
4149
+ dialTribeContext?.markExpired();
4150
+ return;
4151
+ }
4152
+ if (checkResponse.status === 403) {
4153
+ const data = await checkResponse.json().catch(() => ({}));
4154
+ console.error("\u274C Permission denied:", data);
4155
+ setError(
4156
+ data.error || "Streaming is not enabled for this app. Please upgrade your plan or contact support."
4157
+ );
4158
+ setState("error");
4159
+ return;
4160
+ }
4161
+ if (checkResponse.status === 409) {
4162
+ const data = await checkResponse.json();
4163
+ console.log("\u274C Broadcast conflict:", data);
4164
+ setError(
4165
+ data.error || "A broadcast is already active for this stream key. Please terminate it before starting a new one."
4166
+ );
4167
+ setState("error");
4168
+ return;
4169
+ }
4170
+ const errorData = await checkResponse.json().catch(() => ({ error: checkResponse.statusText }));
4171
+ console.error("\u274C Check failed:", checkResponse.status, errorData);
4172
+ setError(errorData.error || `Failed to check broadcast availability: ${checkResponse.statusText}`);
4173
+ setState("error");
4174
+ return;
4175
+ }
4176
+ const checkData = await checkResponse.json();
4177
+ console.log("\u2705 Broadcast check passed:", checkData);
4178
+ const newStreamer = new WebSocketStreamer({
4179
+ streamKey,
4180
+ mediaStream,
4181
+ isVideo: isVideoKey && isVideoEnabled,
4182
+ encoderServerUrl,
4183
+ disableCanvasRendering: true,
4184
+ // <-- ADD THIS LINE
4185
+ onBytesUpdate: setBytesSent,
4186
+ onStateChange: (streamerState) => {
4187
+ if (streamerState === "live") {
4188
+ setState("live");
4189
+ setStartTime(/* @__PURE__ */ new Date());
4190
+ } else if (streamerState === "terminated") {
4191
+ setState("terminated");
4192
+ setStartTime(null);
4193
+ } else if (streamerState === "error") {
4194
+ setState("error");
4195
+ }
4196
+ },
4197
+ onError: (errorMsg, diag) => {
4198
+ setError(errorMsg);
4199
+ if (diag) {
4200
+ setDiagnostics(diag);
4201
+ }
4202
+ }
4203
+ });
4204
+ await newStreamer.start();
4205
+ setStreamer(newStreamer);
4206
+ } catch (err) {
4207
+ console.error("\u274C Failed to start streaming:", err);
4208
+ setError(err instanceof Error ? err.message : "Failed to start streaming");
4209
+ setState("previewing");
4210
+ }
4211
+ };
4212
+ const handleStopStreaming = () => {
4213
+ setShowStopConfirm(true);
4214
+ };
4215
+ const confirmStopStreaming = () => {
4216
+ setState("stopping");
4217
+ setShowStopConfirm(false);
4218
+ if (streamer) {
4219
+ streamer.stop();
4220
+ }
4221
+ if (mediaStream) {
4222
+ mediaStream.getTracks().forEach((track) => track.stop());
4223
+ }
4224
+ setState("stopped");
4225
+ setStartTime(null);
4226
+ };
4227
+ const handleToggleMute = () => {
4228
+ if (mediaStream) {
4229
+ const audioTracks = mediaStream.getAudioTracks();
4230
+ audioTracks.forEach((track) => {
4231
+ track.enabled = !track.enabled;
4232
+ });
4233
+ setIsMuted(!isMuted);
4234
+ }
4235
+ };
4236
+ const handleToggleVideo = () => {
4237
+ if (!isVideoKey) return;
4238
+ if (mediaStream) {
4239
+ const videoTracks = mediaStream.getVideoTracks();
4240
+ videoTracks.forEach((track) => {
4241
+ track.enabled = !track.enabled;
4242
+ });
4243
+ setIsVideoEnabled(!isVideoEnabled);
4244
+ }
4245
+ };
4246
+ const handleFlipCamera = async () => {
4247
+ if (!isVideoKey || !hasMultipleCameras) return;
4248
+ const newFacingMode = facingMode === "user" ? "environment" : "user";
4249
+ if (state === "live" && streamer) {
4250
+ console.log("\u{1F504} Flipping camera during live broadcast (canvas-based swap)");
4251
+ try {
4252
+ const constraints = getMediaConstraints({
4253
+ isVideo: true,
4254
+ facingMode: newFacingMode
4255
+ });
4256
+ const newStream = await navigator.mediaDevices.getUserMedia(constraints);
4257
+ console.log("\u{1F4F7} Got new camera stream:", newFacingMode);
4258
+ const newVideoTrack = newStream.getVideoTracks()[0];
4259
+ if (newVideoTrack) {
4260
+ await streamer.replaceVideoTrack(newVideoTrack);
4261
+ }
4262
+ const updatedStream = streamer.getMediaStream();
4263
+ setMediaStream(updatedStream);
4264
+ setFacingMode(newFacingMode);
4265
+ if (videoRef.current) {
4266
+ videoRef.current.srcObject = updatedStream;
4267
+ }
4268
+ console.log("\u2705 Camera flipped successfully - broadcast continues seamlessly");
4269
+ } catch (err) {
4270
+ console.error("\u274C Failed to flip camera:", err);
4271
+ console.warn("\u26A0\uFE0F Camera flip failed - continuing with current camera");
4272
+ }
4273
+ } else {
4274
+ setFacingMode(newFacingMode);
4275
+ try {
4276
+ const constraints = getMediaConstraints({
4277
+ isVideo: true,
4278
+ facingMode: newFacingMode
4279
+ });
4280
+ const newStream = await navigator.mediaDevices.getUserMedia(constraints);
4281
+ console.log("\u{1F4F7} Camera flipped to:", newFacingMode);
4282
+ if (mediaStream) {
4283
+ mediaStream.getTracks().forEach((track) => track.stop());
4284
+ }
4285
+ setMediaStream(newStream);
4286
+ } catch (err) {
4287
+ console.error("\u274C Failed to get new camera stream:", err);
4288
+ setFacingMode(facingMode);
4289
+ console.warn("\u26A0\uFE0F Camera flip not available - this device may only have one camera");
4290
+ }
4291
+ }
4292
+ };
4293
+ const handleRetry = () => {
4294
+ setError(null);
4295
+ setState("idle");
4296
+ requestMediaPermissions();
4297
+ };
4298
+ const handleDone = () => {
4299
+ onDone?.();
4300
+ };
4301
+ const handleClose = () => {
4302
+ if (state === "live") {
4303
+ setShowCloseConfirm(true);
4304
+ } else {
4305
+ if (mediaStream) {
4306
+ mediaStream.getTracks().forEach((track) => track.stop());
4307
+ }
4308
+ onDone?.();
4309
+ }
4310
+ };
4311
+ const confirmClose = () => {
4312
+ setShowCloseConfirm(false);
4313
+ if (streamer) {
4314
+ streamer.stop();
4315
+ }
4316
+ if (mediaStream) {
4317
+ mediaStream.getTracks().forEach((track) => track.stop());
4318
+ }
4319
+ onDone?.();
4320
+ };
4321
+ const handleVideoDeviceChange = async (deviceId) => {
4322
+ if (deviceId === selectedVideoDeviceId) return;
4323
+ setSelectedVideoDeviceId(deviceId);
4324
+ if (state !== "previewing" && state !== "live") return;
4325
+ try {
4326
+ const constraints = {
4327
+ video: { deviceId: { exact: deviceId } },
4328
+ audio: selectedAudioDeviceId ? { deviceId: { exact: selectedAudioDeviceId } } : true
4329
+ };
4330
+ const newStream = await navigator.mediaDevices.getUserMedia(constraints);
4331
+ console.log(`\u{1F4F7} Switched to camera: ${deviceId}`);
4332
+ if (state === "live" && streamer) {
4333
+ try {
4334
+ await streamer.updateMediaStream(newStream);
4335
+ if (mediaStream) {
4336
+ mediaStream.getTracks().forEach((track) => track.stop());
4337
+ }
4338
+ setMediaStream(newStream);
4339
+ } catch (swapErr) {
4340
+ console.error("\u274C Failed to hot-swap video device:", swapErr);
4341
+ newStream.getTracks().forEach((track) => track.stop());
4342
+ }
4343
+ } else {
4344
+ if (mediaStream) {
4345
+ mediaStream.getTracks().forEach((track) => track.stop());
4346
+ }
4347
+ setMediaStream(newStream);
4348
+ }
4349
+ } catch (err) {
4350
+ console.error("\u274C Failed to switch video device:", err);
4351
+ }
4352
+ };
4353
+ const handleAudioDeviceChange = async (deviceId) => {
4354
+ if (deviceId === selectedAudioDeviceId) return;
4355
+ setSelectedAudioDeviceId(deviceId);
4356
+ if (state !== "previewing" && state !== "live") return;
4357
+ try {
4358
+ const constraints = {
4359
+ video: isVideoKey && isVideoEnabled && selectedVideoDeviceId ? { deviceId: { exact: selectedVideoDeviceId } } : isVideoKey && isVideoEnabled,
4360
+ audio: { deviceId: { exact: deviceId } }
4361
+ };
4362
+ const newStream = await navigator.mediaDevices.getUserMedia(constraints);
4363
+ console.log(`\u{1F3A4} Switched to microphone: ${deviceId}`);
4364
+ if (state === "live" && streamer) {
4365
+ try {
4366
+ await streamer.updateMediaStream(newStream);
4367
+ if (mediaStream) {
4368
+ mediaStream.getTracks().forEach((track) => track.stop());
4369
+ }
4370
+ setMediaStream(newStream);
4371
+ } catch (swapErr) {
4372
+ console.error("\u274C Failed to hot-swap audio device:", swapErr);
4373
+ newStream.getTracks().forEach((track) => track.stop());
4374
+ }
4375
+ } else {
4376
+ if (mediaStream) {
4377
+ mediaStream.getTracks().forEach((track) => track.stop());
4378
+ }
4379
+ setMediaStream(newStream);
4380
+ }
4381
+ } catch (err) {
4382
+ console.error("\u274C Failed to switch audio device:", err);
4383
+ }
4384
+ };
4385
+ if (!sessionToken) {
4386
+ return /* @__PURE__ */ jsx("div", { className: centeredContainerClass, children: /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
4387
+ /* @__PURE__ */ jsx("div", { className: "w-16 h-16 border-4 border-gray-700 border-t-blue-600 rounded-full animate-spin mx-auto mb-4" }),
4388
+ /* @__PURE__ */ jsx("p", { className: "text-white text-lg", children: "Connecting..." })
4389
+ ] }) });
4390
+ }
4391
+ if (!streamKey) {
4392
+ return /* @__PURE__ */ jsx(StreamKeyInput, { onSubmit: handleStreamKeySubmit, inline });
4393
+ }
4394
+ if (state === "error") {
4395
+ const isBroadcastConflict = error?.includes("already active") || error?.includes("terminate it before");
4396
+ return /* @__PURE__ */ jsx("div", { className: centeredContainerClass, children: /* @__PURE__ */ 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: [
4397
+ /* @__PURE__ */ 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__ */ jsx(
4398
+ "svg",
4399
+ {
4400
+ className: "w-8 h-8 text-red-600 dark:text-red-400",
4401
+ fill: "none",
4402
+ stroke: "currentColor",
4403
+ viewBox: "0 0 24 24",
4404
+ children: /* @__PURE__ */ jsx(
4405
+ "path",
4406
+ {
4407
+ strokeLinecap: "round",
4408
+ strokeLinejoin: "round",
4409
+ strokeWidth: 2,
4410
+ d: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
4411
+ }
4412
+ )
4413
+ }
4414
+ ) }),
4415
+ /* @__PURE__ */ jsx("h2", { className: "text-xl font-bold text-black dark:text-white mb-2", children: isBroadcastConflict ? "Broadcast Already Active" : "Error" }),
4416
+ /* @__PURE__ */ jsx("p", { className: "text-gray-600 dark:text-gray-400 mb-6", children: error }),
4417
+ isBroadcastConflict && /* @__PURE__ */ 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." }),
4418
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
4419
+ !isBroadcastConflict && /* @__PURE__ */ jsx(
4420
+ "button",
4421
+ {
4422
+ onClick: handleRetry,
4423
+ className: "w-full px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors",
4424
+ children: "Retry"
4425
+ }
4426
+ ),
4427
+ /* @__PURE__ */ jsx(
4428
+ "button",
4429
+ {
4430
+ onClick: handleDone,
4431
+ 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",
4432
+ children: "Close"
4433
+ }
4434
+ )
4435
+ ] })
4436
+ ] }) });
4437
+ }
4438
+ if (state === "stopped") {
4439
+ return /* @__PURE__ */ jsx("div", { className: centeredContainerClass, children: /* @__PURE__ */ 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: [
4440
+ /* @__PURE__ */ 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__ */ jsx(
4441
+ "svg",
4442
+ {
4443
+ className: "w-8 h-8 text-green-600 dark:text-green-400",
4444
+ fill: "none",
4445
+ stroke: "currentColor",
4446
+ viewBox: "0 0 24 24",
4447
+ children: /* @__PURE__ */ jsx(
4448
+ "path",
4449
+ {
4450
+ strokeLinecap: "round",
4451
+ strokeLinejoin: "round",
4452
+ strokeWidth: 2,
4453
+ d: "M5 13l4 4L19 7"
4454
+ }
4455
+ )
4456
+ }
4457
+ ) }),
4458
+ /* @__PURE__ */ jsx("h2", { className: "text-xl font-bold text-black dark:text-white mb-2", children: "Stream Ended" }),
4459
+ /* @__PURE__ */ jsx("p", { className: "text-gray-600 dark:text-gray-400 mb-2", children: "Your broadcast has ended successfully." }),
4460
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-500 dark:text-gray-400 mb-6", children: [
4461
+ "Total sent: ",
4462
+ (bytesSent / 1024 / 1024).toFixed(2),
4463
+ " MB"
4464
+ ] }),
4465
+ /* @__PURE__ */ jsx(
4466
+ "button",
4467
+ {
4468
+ onClick: handleDone,
4469
+ className: "w-full px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors",
4470
+ children: "Done"
4471
+ }
4472
+ )
4473
+ ] }) });
4474
+ }
4475
+ if (state === "terminated") {
4476
+ return /* @__PURE__ */ jsx("div", { className: centeredContainerClass, children: /* @__PURE__ */ 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: [
4477
+ /* @__PURE__ */ 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__ */ jsx(
4478
+ "svg",
4479
+ {
4480
+ className: "w-8 h-8 text-orange-600 dark:text-orange-400",
4481
+ fill: "none",
4482
+ stroke: "currentColor",
4483
+ viewBox: "0 0 24 24",
4484
+ children: /* @__PURE__ */ jsx(
4485
+ "path",
4486
+ {
4487
+ strokeLinecap: "round",
4488
+ strokeLinejoin: "round",
4489
+ strokeWidth: 2,
4490
+ 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"
4491
+ }
4492
+ )
4493
+ }
4494
+ ) }),
4495
+ /* @__PURE__ */ jsx("h2", { className: "text-xl font-bold text-black dark:text-white mb-2", children: "Stream Ended" }),
4496
+ /* @__PURE__ */ jsx("p", { className: "text-gray-600 dark:text-gray-400 mb-2", children: error || "Connection closed unexpectedly" }),
4497
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-500 dark:text-gray-400 mb-2", children: [
4498
+ "Total sent: ",
4499
+ (bytesSent / 1024 / 1024).toFixed(2),
4500
+ " MB"
4501
+ ] }),
4502
+ diagnostics && /* @__PURE__ */ jsxs("div", { className: "text-xs text-gray-400 dark:text-gray-500 mb-6 font-mono bg-gray-100 dark:bg-zinc-800 rounded p-2 text-left", children: [
4503
+ /* @__PURE__ */ jsxs("div", { children: [
4504
+ "Codec: ",
4505
+ diagnostics.mimeType || "unknown"
4506
+ ] }),
4507
+ /* @__PURE__ */ jsxs("div", { children: [
4508
+ "Chunks: ",
4509
+ diagnostics.chunksSent,
4510
+ " (",
4511
+ (diagnostics.bytesSent / 1024).toFixed(1),
4512
+ " KB)"
4513
+ ] }),
4514
+ /* @__PURE__ */ jsxs("div", { children: [
4515
+ "Duration: ",
4516
+ (diagnostics.elapsedMs / 1e3).toFixed(1),
4517
+ "s"
4518
+ ] }),
4519
+ /* @__PURE__ */ jsxs("div", { children: [
4520
+ "Close code: ",
4521
+ diagnostics.closeCode ?? "N/A",
4522
+ diagnostics.closeReason ? ` (${diagnostics.closeReason})` : ""
4523
+ ] })
4524
+ ] }),
4525
+ !diagnostics && /* @__PURE__ */ jsx("div", { className: "mb-6" }),
4526
+ /* @__PURE__ */ jsx(
4527
+ "button",
4528
+ {
4529
+ onClick: handleDone,
4530
+ className: "w-full px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors",
4531
+ children: "Done"
4532
+ }
4533
+ )
4534
+ ] }) });
4535
+ }
4536
+ if (state === "idle" || state === "requesting") {
4537
+ return /* @__PURE__ */ jsx("div", { className: centeredContainerClass, children: /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
4538
+ /* @__PURE__ */ jsx("div", { className: "w-16 h-16 border-4 border-gray-700 border-t-blue-600 rounded-full animate-spin mx-auto mb-4" }),
4539
+ /* @__PURE__ */ jsx("p", { className: "text-white text-lg", children: state === "idle" ? "Initializing..." : "Requesting permissions..." })
4540
+ ] }) });
4541
+ }
4542
+ const controlState = state;
4543
+ return /* @__PURE__ */ jsxs("div", { className: `${containerClass} flex flex-col`, children: [
4544
+ /* @__PURE__ */ jsx(
4545
+ StreamingPreview,
4546
+ {
4547
+ videoRef,
4548
+ isVideoKey,
4549
+ isVideoEnabled,
4550
+ mediaStream,
4551
+ facingMode
4552
+ }
4553
+ ),
4554
+ /* @__PURE__ */ jsx(
4555
+ StreamingControls,
4556
+ {
4557
+ state: controlState,
4558
+ isVideoKey,
4559
+ isMuted,
4560
+ isVideoEnabled,
4561
+ facingMode,
4562
+ hasMultipleCameras,
4563
+ startTime,
4564
+ bytesSent,
4565
+ showStopConfirm,
4566
+ streamKey,
4567
+ onStreamKeyChange: handleStreamKeyChange,
4568
+ onStart: handleStartStreaming,
4569
+ onStop: handleStopStreaming,
4570
+ onConfirmStop: confirmStopStreaming,
4571
+ onCancelStop: () => setShowStopConfirm(false),
4572
+ onToggleMute: handleToggleMute,
4573
+ onToggleVideo: handleToggleVideo,
4574
+ onFlipCamera: handleFlipCamera,
4575
+ onClose: inline ? void 0 : handleClose,
4576
+ showCloseConfirm,
4577
+ onConfirmClose: confirmClose,
4578
+ onCancelClose: () => setShowCloseConfirm(false),
4579
+ videoDevices,
4580
+ audioDevices,
4581
+ selectedVideoDeviceId,
4582
+ selectedAudioDeviceId,
4583
+ onVideoDeviceChange: handleVideoDeviceChange,
4584
+ onAudioDeviceChange: handleAudioDeviceChange,
4585
+ mediaStream
4586
+ }
4587
+ )
4588
+ ] });
4589
+ }
4590
+
4591
+ // src/utils/dialtribe-popup.ts
4592
+ function calculatePopupDimensions() {
4593
+ const screenWidth = window.screen.width;
4594
+ const screenHeight = window.screen.height;
4595
+ const screenAspectRatio = screenWidth / screenHeight;
4596
+ let width;
4597
+ let height;
4598
+ if (screenAspectRatio > 1.2) {
4599
+ height = Math.min(720, Math.floor(screenHeight * 0.85));
4600
+ width = Math.floor(height * (16 / 9));
4601
+ } else {
4602
+ width = Math.min(720, Math.floor(screenWidth * 0.9));
4603
+ height = Math.floor(width * (16 / 9));
4604
+ }
4605
+ const left = Math.floor((screenWidth - width) / 2);
4606
+ const top = Math.floor((screenHeight - height) / 2);
4607
+ return { width, height, left, top };
4608
+ }
4609
+ function openDialtribeStreamerPopup(options) {
4610
+ const {
4611
+ sessionToken,
4612
+ streamKey,
4613
+ streamerUrl,
4614
+ appId,
4615
+ additionalParams
4616
+ } = options;
4617
+ const { width, height, left, top } = calculatePopupDimensions();
4618
+ const params = new URLSearchParams();
4619
+ if (additionalParams) {
4620
+ Object.entries(additionalParams).forEach(([key, value]) => {
4621
+ params.append(key, value);
4622
+ });
4623
+ }
4624
+ const url = `${streamerUrl}${params.toString() ? `?${params.toString()}` : ""}`;
4625
+ const popup = window.open(
4626
+ url,
4627
+ "_blank",
4628
+ `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`
4629
+ );
4630
+ if (!popup) {
4631
+ console.error("Failed to open popup window - popup may be blocked");
4632
+ return null;
4633
+ }
4634
+ const sendMessage = () => {
4635
+ try {
4636
+ popup.postMessage(
4637
+ {
4638
+ type: "STREAM_KEY",
4639
+ sessionToken,
4640
+ streamKey,
4641
+ appId
4642
+ },
4643
+ window.location.origin
4644
+ );
4645
+ } catch (error) {
4646
+ console.error("Failed to send credentials to popup:", error);
4647
+ }
4648
+ };
4649
+ sendMessage();
4650
+ setTimeout(sendMessage, 100);
4651
+ setTimeout(sendMessage, 500);
4652
+ return popup;
4653
+ }
4654
+ var openBroadcastPopup = openDialtribeStreamerPopup;
4655
+ function useDialtribeStreamerPopup() {
4656
+ const [sessionToken, setSessionToken] = useState(null);
4657
+ const [streamKey, setStreamKey] = useState(null);
4658
+ const [apiBaseUrl, setApiBaseUrl] = useState("");
4659
+ const receivedDataRef = useRef(false);
4660
+ useEffect(() => {
4661
+ const handleMessage = (event) => {
4662
+ if (event.data?.type !== "STREAM_KEY") return;
4663
+ const { sessionToken: token, streamKey: key, apiBaseUrl: url } = event.data;
4664
+ if (token && key) {
4665
+ receivedDataRef.current = true;
4666
+ setSessionToken(token);
4667
+ setStreamKey(key);
4668
+ if (url) {
4669
+ setApiBaseUrl(url);
4670
+ }
4671
+ } else if (key) {
4672
+ receivedDataRef.current = true;
4673
+ setStreamKey(key);
4674
+ }
4675
+ };
4676
+ window.addEventListener("message", handleMessage);
4677
+ const requestCredentials = () => {
4678
+ if (window.opener && !receivedDataRef.current) {
4679
+ window.opener.postMessage({ type: "POPUP_READY" }, "*");
4680
+ }
4681
+ };
4682
+ requestCredentials();
4683
+ const pollInterval = setInterval(() => {
4684
+ if (!receivedDataRef.current) {
4685
+ requestCredentials();
4686
+ } else {
4687
+ clearInterval(pollInterval);
4688
+ }
4689
+ }, 200);
4690
+ const timeout = setTimeout(() => {
4691
+ clearInterval(pollInterval);
4692
+ }, 1e4);
4693
+ return () => {
4694
+ window.removeEventListener("message", handleMessage);
4695
+ clearInterval(pollInterval);
4696
+ clearTimeout(timeout);
4697
+ };
4698
+ }, []);
4699
+ return {
4700
+ sessionToken,
4701
+ streamKey,
4702
+ apiBaseUrl,
4703
+ setStreamKey,
4704
+ isReady: receivedDataRef.current
4705
+ };
4706
+ }
4707
+ function useDialtribeStreamerLauncher(options) {
4708
+ const {
4709
+ sessionToken,
4710
+ streamKey,
4711
+ streamerUrl,
4712
+ apiBaseUrl,
4713
+ fallback = "fullscreen",
4714
+ onPopupBlocked,
4715
+ onDone,
4716
+ onStreamKeyChange
4717
+ } = options;
4718
+ const [showFallback, setShowFallback] = useState(false);
4719
+ const [wasBlocked, setWasBlocked] = useState(false);
4720
+ const popupRef = useRef(null);
4721
+ const sessionTokenRef = useRef(sessionToken);
4722
+ const streamKeyRef = useRef(streamKey);
4723
+ const apiBaseUrlRef = useRef(apiBaseUrl);
4724
+ useEffect(() => {
4725
+ sessionTokenRef.current = sessionToken;
4726
+ }, [sessionToken]);
4727
+ useEffect(() => {
4728
+ streamKeyRef.current = streamKey;
4729
+ }, [streamKey]);
4730
+ useEffect(() => {
4731
+ apiBaseUrlRef.current = apiBaseUrl;
4732
+ }, [apiBaseUrl]);
4733
+ useEffect(() => {
4734
+ const handleMessage = (event) => {
4735
+ if (event.data?.type === "POPUP_READY" && popupRef.current) {
4736
+ popupRef.current.postMessage(
4737
+ {
4738
+ type: "STREAM_KEY",
4739
+ sessionToken: sessionTokenRef.current,
4740
+ streamKey: streamKeyRef.current,
4741
+ apiBaseUrl: apiBaseUrlRef.current
4742
+ },
4743
+ "*"
4744
+ );
4745
+ }
4746
+ };
4747
+ window.addEventListener("message", handleMessage);
4748
+ return () => window.removeEventListener("message", handleMessage);
4749
+ }, []);
4750
+ const launch = useCallback(() => {
4751
+ if (!sessionToken) {
4752
+ console.warn("Cannot launch streamer: no session token");
4753
+ return;
4754
+ }
4755
+ setWasBlocked(false);
4756
+ const popup = openDialtribeStreamerPopup({
4757
+ sessionToken,
4758
+ streamKey,
4759
+ streamerUrl
4760
+ });
4761
+ if (popup) {
4762
+ popupRef.current = popup;
4763
+ return;
4764
+ }
4765
+ setWasBlocked(true);
4766
+ onPopupBlocked?.();
4767
+ switch (fallback) {
4768
+ case "fullscreen":
4769
+ setShowFallback(true);
4770
+ break;
4771
+ case "newTab":
4772
+ window.open(streamerUrl, "_blank");
4773
+ break;
4774
+ }
4775
+ }, [sessionToken, streamKey, streamerUrl, fallback, onPopupBlocked]);
4776
+ const closeFallback = useCallback(() => {
4777
+ setShowFallback(false);
4778
+ onDone?.();
4779
+ }, [onDone]);
4780
+ const Fallback = useCallback(() => {
4781
+ if (fallback !== "fullscreen" || !showFallback) {
4782
+ return null;
4783
+ }
4784
+ if (typeof document === "undefined") {
4785
+ return null;
4786
+ }
4787
+ const streamerElement = React2.createElement(DialtribeStreamer, {
4788
+ sessionToken: sessionToken || void 0,
4789
+ streamKey: streamKey || void 0,
4790
+ apiBaseUrl,
4791
+ onDone: closeFallback,
4792
+ onStreamKeyChange
4793
+ });
4794
+ const overlayElement = React2.createElement(
4795
+ DialtribeOverlay,
4796
+ {
4797
+ mode: "fullscreen",
4798
+ isOpen: true,
4799
+ onClose: closeFallback,
4800
+ children: streamerElement
4801
+ }
4802
+ );
4803
+ return createPortal(overlayElement, document.body);
4804
+ }, [fallback, showFallback, closeFallback, sessionToken, streamKey, apiBaseUrl, onStreamKeyChange]);
4805
+ return {
4806
+ launch,
4807
+ Fallback,
4808
+ showFallback,
4809
+ closeFallback,
4810
+ popupRef: popupRef.current,
4811
+ wasBlocked
4812
+ };
4813
+ }
2133
4814
 
2134
- export { AudioWaveform, BroadcastPlayer, BroadcastPlayerErrorBoundary, BroadcastPlayerModal, CDN_DOMAIN, DIALTRIBE_API_BASE, DialTribeClient, DialTribeProvider, ENDPOINTS, HTTP_STATUS, HelloWorld, LoadingSpinner, buildBroadcastCdnUrl, buildBroadcastS3KeyPrefix, formatTime, useDialTribe };
4815
+ export { AudioWaveform, CDN_DOMAIN, DEFAULT_ENCODER_SERVER_URL, DIALTRIBE_API_BASE, DialtribeClient, DialtribeOverlay, DialtribePlayer, DialtribePlayerErrorBoundary, DialtribeProvider, DialtribeStreamer, ENDPOINTS, HTTP_STATUS, HelloWorld, LoadingSpinner, StreamKeyDisplay, StreamKeyInput, StreamingControls, StreamingPreview, WebSocketStreamer, buildBroadcastCdnUrl, buildBroadcastS3KeyPrefix, calculatePopupDimensions, checkBrowserCompatibility, formatTime, getMediaConstraints, getMediaRecorderOptions, openBroadcastPopup, openDialtribeStreamerPopup, useDialtribe, useDialtribeStreamerLauncher, useDialtribeStreamerPopup };
2135
4816
  //# sourceMappingURL=index.mjs.map
2136
4817
  //# sourceMappingURL=index.mjs.map