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

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