@dialtribe/react-sdk 0.1.0-alpha.5
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/LICENSE +21 -0
- package/README.md +535 -0
- package/dist/broadcast-player.d.mts +517 -0
- package/dist/broadcast-player.d.ts +517 -0
- package/dist/broadcast-player.js +2125 -0
- package/dist/broadcast-player.js.map +1 -0
- package/dist/broadcast-player.mjs +2105 -0
- package/dist/broadcast-player.mjs.map +1 -0
- package/dist/hello-world.d.mts +12 -0
- package/dist/hello-world.d.ts +12 -0
- package/dist/hello-world.js +27 -0
- package/dist/hello-world.js.map +1 -0
- package/dist/hello-world.mjs +25 -0
- package/dist/hello-world.mjs.map +1 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2144 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2123 -0
- package/dist/index.mjs.map +1 -0
- package/dist/styles.css +3 -0
- package/package.json +84 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2123 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { createContext, useState, useCallback, useEffect, useContext, useRef, Component } from 'react';
|
|
3
|
+
import ReactPlayer from 'react-player';
|
|
4
|
+
|
|
5
|
+
// src/components/HelloWorld.tsx
|
|
6
|
+
function HelloWorld({ name = "World" }) {
|
|
7
|
+
return /* @__PURE__ */ jsxs("div", { style: {
|
|
8
|
+
padding: "20px",
|
|
9
|
+
border: "2px solid #4F46E5",
|
|
10
|
+
borderRadius: "8px",
|
|
11
|
+
backgroundColor: "#EEF2FF",
|
|
12
|
+
color: "#4F46E5",
|
|
13
|
+
fontFamily: "system-ui, sans-serif",
|
|
14
|
+
textAlign: "center"
|
|
15
|
+
}, children: [
|
|
16
|
+
/* @__PURE__ */ jsxs("h1", { style: { margin: "0 0 10px 0" }, children: [
|
|
17
|
+
"Hello, ",
|
|
18
|
+
name,
|
|
19
|
+
"!"
|
|
20
|
+
] }),
|
|
21
|
+
/* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: "14px" }, children: "@dialtribe/react-sdk is working correctly" })
|
|
22
|
+
] });
|
|
23
|
+
}
|
|
24
|
+
var DialTribeContext = createContext(null);
|
|
25
|
+
function DialTribeProvider({
|
|
26
|
+
sessionToken: initialToken,
|
|
27
|
+
onTokenRefresh,
|
|
28
|
+
onTokenExpired,
|
|
29
|
+
children
|
|
30
|
+
}) {
|
|
31
|
+
const [sessionToken, setSessionTokenState] = useState(initialToken);
|
|
32
|
+
const [isExpired, setIsExpired] = useState(false);
|
|
33
|
+
const setSessionToken = useCallback(
|
|
34
|
+
(newToken, expiresAt) => {
|
|
35
|
+
setSessionTokenState(newToken);
|
|
36
|
+
setIsExpired(false);
|
|
37
|
+
if (expiresAt) {
|
|
38
|
+
onTokenRefresh?.(newToken, expiresAt);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
[onTokenRefresh]
|
|
42
|
+
);
|
|
43
|
+
const markExpired = useCallback(() => {
|
|
44
|
+
setIsExpired(true);
|
|
45
|
+
onTokenExpired?.();
|
|
46
|
+
}, [onTokenExpired]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (initialToken !== sessionToken) {
|
|
49
|
+
setSessionTokenState(initialToken);
|
|
50
|
+
setIsExpired(false);
|
|
51
|
+
}
|
|
52
|
+
}, [initialToken, sessionToken]);
|
|
53
|
+
const value = {
|
|
54
|
+
sessionToken,
|
|
55
|
+
setSessionToken,
|
|
56
|
+
isExpired,
|
|
57
|
+
markExpired
|
|
58
|
+
};
|
|
59
|
+
return /* @__PURE__ */ jsx(DialTribeContext.Provider, { value, children });
|
|
60
|
+
}
|
|
61
|
+
function useDialTribe() {
|
|
62
|
+
const context = useContext(DialTribeContext);
|
|
63
|
+
if (!context) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
'useDialTribe must be used within a DialTribeProvider. Wrap your app with <DialTribeProvider sessionToken="sess_xxx">...</DialTribeProvider>'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return context;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/client/DialTribeClient.ts
|
|
72
|
+
var DIALTRIBE_API_BASE = "https://dialtribe.com/api/public/v1";
|
|
73
|
+
var ENDPOINTS = {
|
|
74
|
+
broadcasts: `${DIALTRIBE_API_BASE}/broadcasts`,
|
|
75
|
+
broadcast: (id) => `${DIALTRIBE_API_BASE}/broadcasts/${id}`,
|
|
76
|
+
contentPlay: `${DIALTRIBE_API_BASE}/content/play`,
|
|
77
|
+
presignedUrl: `${DIALTRIBE_API_BASE}/media/presigned-url`,
|
|
78
|
+
sessionStart: `${DIALTRIBE_API_BASE}/session/start`,
|
|
79
|
+
sessionPing: `${DIALTRIBE_API_BASE}/session/ping`
|
|
80
|
+
};
|
|
81
|
+
var DialTribeClient = class {
|
|
82
|
+
constructor(config) {
|
|
83
|
+
this.config = config;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Make an authenticated request to DialTribe API
|
|
87
|
+
*
|
|
88
|
+
* Automatically:
|
|
89
|
+
* - Adds Authorization header with session token
|
|
90
|
+
* - Checks for X-Session-Token header in response (token refresh)
|
|
91
|
+
* - Calls onTokenRefresh if new token is provided
|
|
92
|
+
* - Calls onTokenExpired on 401 errors
|
|
93
|
+
*/
|
|
94
|
+
async fetch(url, options = {}) {
|
|
95
|
+
const headers = new Headers(options.headers);
|
|
96
|
+
headers.set("Authorization", `Bearer ${this.config.sessionToken}`);
|
|
97
|
+
headers.set("Content-Type", "application/json");
|
|
98
|
+
const response = await fetch(url, {
|
|
99
|
+
...options,
|
|
100
|
+
headers
|
|
101
|
+
});
|
|
102
|
+
const newToken = response.headers.get("X-Session-Token");
|
|
103
|
+
const expiresAt = response.headers.get("X-Session-Expires");
|
|
104
|
+
if (newToken && expiresAt) {
|
|
105
|
+
this.config.onTokenRefresh?.(newToken, expiresAt);
|
|
106
|
+
}
|
|
107
|
+
if (response.status === 401) {
|
|
108
|
+
this.config.onTokenExpired?.();
|
|
109
|
+
throw new Error("Session token expired or invalid");
|
|
110
|
+
}
|
|
111
|
+
return response;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Update the session token
|
|
115
|
+
* Called automatically when token is refreshed, or manually by user
|
|
116
|
+
*/
|
|
117
|
+
setSessionToken(token) {
|
|
118
|
+
this.config.sessionToken = token;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Get list of broadcasts for the authenticated app
|
|
122
|
+
*/
|
|
123
|
+
async getBroadcasts(params) {
|
|
124
|
+
const searchParams = new URLSearchParams();
|
|
125
|
+
if (params?.page) searchParams.set("page", params.page.toString());
|
|
126
|
+
if (params?.limit) searchParams.set("limit", params.limit.toString());
|
|
127
|
+
if (params?.broadcastStatus) searchParams.set("broadcastStatus", params.broadcastStatus.toString());
|
|
128
|
+
if (params?.search) searchParams.set("search", params.search);
|
|
129
|
+
if (params?.includeDeleted) searchParams.set("includeDeleted", "true");
|
|
130
|
+
const url = `${ENDPOINTS.broadcasts}${searchParams.toString() ? `?${searchParams}` : ""}`;
|
|
131
|
+
const response = await this.fetch(url);
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
throw new Error(`Failed to fetch broadcasts: ${response.status} ${response.statusText}`);
|
|
134
|
+
}
|
|
135
|
+
return response.json();
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get a single broadcast by ID
|
|
139
|
+
*/
|
|
140
|
+
async getBroadcast(id) {
|
|
141
|
+
const response = await this.fetch(ENDPOINTS.broadcast(id));
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
if (response.status === 404) {
|
|
144
|
+
throw new Error("Broadcast not found");
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`Failed to fetch broadcast: ${response.status} ${response.statusText}`);
|
|
147
|
+
}
|
|
148
|
+
return response.json();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get presigned URL for media playback
|
|
152
|
+
*
|
|
153
|
+
* @param broadcastId - Broadcast ID
|
|
154
|
+
* @param hash - Broadcast hash (optional if using session token)
|
|
155
|
+
* @param action - 'download' to force download, otherwise streams
|
|
156
|
+
*/
|
|
157
|
+
async getPlaybackUrl(params) {
|
|
158
|
+
const searchParams = new URLSearchParams({
|
|
159
|
+
broadcastId: params.broadcastId.toString()
|
|
160
|
+
});
|
|
161
|
+
if (params.hash) searchParams.set("hash", params.hash);
|
|
162
|
+
if (params.action) searchParams.set("action", params.action);
|
|
163
|
+
const url = `${ENDPOINTS.contentPlay}?${searchParams}`;
|
|
164
|
+
const response = await this.fetch(url, {
|
|
165
|
+
redirect: "manual"
|
|
166
|
+
// Don't follow redirect, we want the URL
|
|
167
|
+
});
|
|
168
|
+
const location = response.headers.get("Location");
|
|
169
|
+
if (!location) {
|
|
170
|
+
throw new Error("No playback URL returned from API");
|
|
171
|
+
}
|
|
172
|
+
return location;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Refresh a presigned URL before it expires
|
|
176
|
+
*
|
|
177
|
+
* @param broadcastId - Broadcast ID
|
|
178
|
+
* @param hash - Broadcast hash
|
|
179
|
+
* @param fileType - Type of media file
|
|
180
|
+
*/
|
|
181
|
+
async refreshPresignedUrl(params) {
|
|
182
|
+
const searchParams = new URLSearchParams({
|
|
183
|
+
broadcastId: params.broadcastId.toString(),
|
|
184
|
+
hash: params.hash,
|
|
185
|
+
fileType: params.fileType
|
|
186
|
+
});
|
|
187
|
+
const url = `${ENDPOINTS.presignedUrl}?${searchParams}`;
|
|
188
|
+
const response = await this.fetch(url);
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
throw new Error(`Failed to refresh URL: ${response.status} ${response.statusText}`);
|
|
191
|
+
}
|
|
192
|
+
return response.json();
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Start a new audience tracking session
|
|
196
|
+
*
|
|
197
|
+
* @returns audienceId and optional resumePosition
|
|
198
|
+
*/
|
|
199
|
+
async startSession(params) {
|
|
200
|
+
const response = await this.fetch(ENDPOINTS.sessionStart, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
body: JSON.stringify(params)
|
|
203
|
+
});
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
throw new Error(`Failed to start session: ${response.status} ${response.statusText}`);
|
|
206
|
+
}
|
|
207
|
+
return response.json();
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Send a session ping event
|
|
211
|
+
*
|
|
212
|
+
* Event types:
|
|
213
|
+
* - 0: PAUSE/STOP
|
|
214
|
+
* - 1: PLAY/START
|
|
215
|
+
* - 2: HEARTBEAT
|
|
216
|
+
* - 3: UNMOUNT
|
|
217
|
+
*/
|
|
218
|
+
async sendSessionPing(params) {
|
|
219
|
+
const response = await this.fetch(ENDPOINTS.sessionPing, {
|
|
220
|
+
method: "POST",
|
|
221
|
+
body: JSON.stringify(params)
|
|
222
|
+
});
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
throw new Error(`Failed to send session ping: ${response.status} ${response.statusText}`);
|
|
225
|
+
}
|
|
226
|
+
if (response.status === 204) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
return response.json();
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
function AudioWaveform({
|
|
233
|
+
audioElement,
|
|
234
|
+
mediaStream,
|
|
235
|
+
isPlaying = false,
|
|
236
|
+
isLive = false
|
|
237
|
+
}) {
|
|
238
|
+
const canvasRef = useRef(null);
|
|
239
|
+
const animationFrameRef = useRef(void 0);
|
|
240
|
+
const [setupError, setSetupError] = useState(false);
|
|
241
|
+
const isPlayingRef = useRef(isPlaying);
|
|
242
|
+
const isLiveRef = useRef(isLive);
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
isPlayingRef.current = isPlaying;
|
|
245
|
+
}, [isPlaying]);
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
isLiveRef.current = isLive;
|
|
248
|
+
}, [isLive]);
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
const canvas = canvasRef.current;
|
|
251
|
+
if (!canvas) return;
|
|
252
|
+
const ctx = canvas.getContext("2d");
|
|
253
|
+
if (!ctx) return;
|
|
254
|
+
if (audioElement) {
|
|
255
|
+
const hasMediaAPI = "play" in audioElement && "pause" in audioElement && "currentTime" in audioElement;
|
|
256
|
+
const isMediaElement = audioElement instanceof HTMLMediaElement;
|
|
257
|
+
if (!hasMediaAPI && !isMediaElement) {
|
|
258
|
+
console.warn(
|
|
259
|
+
"[AudioWaveform] Invalid audio element - missing media API"
|
|
260
|
+
);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
console.log("[AudioWaveform] Audio element validation:", {
|
|
264
|
+
tagName: audioElement.tagName,
|
|
265
|
+
isHTMLMediaElement: isMediaElement,
|
|
266
|
+
hasMediaAPI,
|
|
267
|
+
willAttemptVisualization: true
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const canUseAudioElement = audioElement && audioElement instanceof HTMLMediaElement;
|
|
271
|
+
const isHLSLiveStream = audioElement && !canUseAudioElement;
|
|
272
|
+
if (!audioElement && !mediaStream || audioElement && !canUseAudioElement) {
|
|
273
|
+
if (isHLSLiveStream) {
|
|
274
|
+
let time = 0;
|
|
275
|
+
let frozenTime = 0;
|
|
276
|
+
let wasFrozen = false;
|
|
277
|
+
const barPhases = Array.from(
|
|
278
|
+
{ length: 128 },
|
|
279
|
+
() => Math.random() * Math.PI * 2
|
|
280
|
+
);
|
|
281
|
+
const barSpeeds = Array.from(
|
|
282
|
+
{ length: 128 },
|
|
283
|
+
() => 0.8 + Math.random() * 0.4
|
|
284
|
+
);
|
|
285
|
+
const glowPhases = Array.from(
|
|
286
|
+
{ length: 128 },
|
|
287
|
+
() => Math.random() * Math.PI * 2
|
|
288
|
+
);
|
|
289
|
+
const glowSpeeds = Array.from(
|
|
290
|
+
{ length: 128 },
|
|
291
|
+
() => 0.7 + Math.random() * 0.6
|
|
292
|
+
);
|
|
293
|
+
const drawEnhancedWaveform = () => {
|
|
294
|
+
ctx.fillStyle = "#000";
|
|
295
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
296
|
+
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
|
|
297
|
+
gradient.addColorStop(0, "#06b6d4");
|
|
298
|
+
gradient.addColorStop(0.3, "#3b82f6");
|
|
299
|
+
gradient.addColorStop(0.6, "#8b5cf6");
|
|
300
|
+
gradient.addColorStop(1, "#ec4899");
|
|
301
|
+
const currentlyLive = isLiveRef.current;
|
|
302
|
+
if (currentlyLive) {
|
|
303
|
+
time += 0.02;
|
|
304
|
+
wasFrozen = false;
|
|
305
|
+
} else if (!wasFrozen) {
|
|
306
|
+
frozenTime = time;
|
|
307
|
+
wasFrozen = true;
|
|
308
|
+
}
|
|
309
|
+
const currentTime = wasFrozen ? frozenTime : time;
|
|
310
|
+
const barCount = 128;
|
|
311
|
+
const barWidth = canvas.width / barCount;
|
|
312
|
+
const gap = 2;
|
|
313
|
+
const maxHeight = canvas.height * 0.9;
|
|
314
|
+
for (let i = 0; i < barCount; i++) {
|
|
315
|
+
const primaryWave = Math.sin(
|
|
316
|
+
i / barCount * Math.PI * 2 * 2.5 - currentTime * barSpeeds[i] + barPhases[i]
|
|
317
|
+
) * (maxHeight * 0.35);
|
|
318
|
+
const secondaryWave = Math.sin(
|
|
319
|
+
i / barCount * Math.PI * 2 * 4 - currentTime * barSpeeds[i] * 1.3 + barPhases[i] * 0.7
|
|
320
|
+
) * (maxHeight * 0.15);
|
|
321
|
+
const tertiaryWave = Math.sin(
|
|
322
|
+
i / barCount * Math.PI * 2 * 7 - currentTime * barSpeeds[i] * 0.8 + barPhases[i] * 1.5
|
|
323
|
+
) * (maxHeight * 0.1);
|
|
324
|
+
const baseHeight = maxHeight * 0.15;
|
|
325
|
+
const combinedWave = primaryWave + secondaryWave + tertiaryWave;
|
|
326
|
+
const barHeight = Math.max(
|
|
327
|
+
10,
|
|
328
|
+
Math.min(maxHeight, baseHeight + combinedWave)
|
|
329
|
+
);
|
|
330
|
+
const opacityWave1 = Math.sin(
|
|
331
|
+
i / barCount * Math.PI * 2 * 1.5 - currentTime * 1.2
|
|
332
|
+
);
|
|
333
|
+
const opacityWave2 = Math.sin(
|
|
334
|
+
i / barCount * Math.PI * 2 * 3.5 - currentTime * 0.7
|
|
335
|
+
);
|
|
336
|
+
const opacity = 0.3 + opacityWave1 * 0.25 + opacityWave2 * 0.15;
|
|
337
|
+
const glowWave = Math.sin(
|
|
338
|
+
currentTime * glowSpeeds[i] + glowPhases[i]
|
|
339
|
+
);
|
|
340
|
+
const glowIntensity = 8 + glowWave * 12;
|
|
341
|
+
const x = i * barWidth;
|
|
342
|
+
const y = canvas.height / 2 - barHeight / 2;
|
|
343
|
+
ctx.shadowBlur = glowIntensity;
|
|
344
|
+
ctx.shadowColor = "#3b82f6";
|
|
345
|
+
ctx.fillStyle = gradient;
|
|
346
|
+
ctx.globalAlpha = Math.max(0.15, Math.min(0.9, opacity));
|
|
347
|
+
ctx.fillRect(x + gap / 2, y, barWidth - gap, barHeight);
|
|
348
|
+
}
|
|
349
|
+
ctx.globalAlpha = 1;
|
|
350
|
+
ctx.shadowBlur = 0;
|
|
351
|
+
};
|
|
352
|
+
const animationId2 = setInterval(drawEnhancedWaveform, 1e3 / 60);
|
|
353
|
+
return () => clearInterval(animationId2);
|
|
354
|
+
}
|
|
355
|
+
let waveOffset = 0;
|
|
356
|
+
const drawPlaceholder = () => {
|
|
357
|
+
ctx.fillStyle = "#000";
|
|
358
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
359
|
+
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
|
|
360
|
+
gradient.addColorStop(0, "#06b6d4");
|
|
361
|
+
gradient.addColorStop(0.3, "#3b82f6");
|
|
362
|
+
gradient.addColorStop(0.6, "#8b5cf6");
|
|
363
|
+
gradient.addColorStop(1, "#ec4899");
|
|
364
|
+
waveOffset += 83e-4;
|
|
365
|
+
const barCount = 128;
|
|
366
|
+
const barWidth = canvas.width / barCount;
|
|
367
|
+
const gap = 2;
|
|
368
|
+
const baseHeight = 15;
|
|
369
|
+
const waveAmplitude = 10;
|
|
370
|
+
for (let i = 0; i < barCount; i++) {
|
|
371
|
+
const wave = Math.sin(i / barCount * Math.PI * 2 * 3 - waveOffset) * waveAmplitude;
|
|
372
|
+
const barHeight = baseHeight + wave;
|
|
373
|
+
const opacityWave = Math.sin(
|
|
374
|
+
i / barCount * Math.PI * 2 * 2 - waveOffset * 1.5
|
|
375
|
+
);
|
|
376
|
+
const opacity = 0.5 + opacityWave * 0.3;
|
|
377
|
+
const x = i * barWidth;
|
|
378
|
+
const y = canvas.height / 2 - barHeight / 2;
|
|
379
|
+
ctx.shadowBlur = 15;
|
|
380
|
+
ctx.shadowColor = "#3b82f6";
|
|
381
|
+
ctx.fillStyle = gradient;
|
|
382
|
+
ctx.globalAlpha = opacity;
|
|
383
|
+
ctx.fillRect(x + gap / 2, y, barWidth - gap, barHeight);
|
|
384
|
+
}
|
|
385
|
+
ctx.globalAlpha = 1;
|
|
386
|
+
ctx.shadowBlur = 0;
|
|
387
|
+
};
|
|
388
|
+
const animationId = setInterval(drawPlaceholder, 1e3 / 60);
|
|
389
|
+
return () => clearInterval(animationId);
|
|
390
|
+
}
|
|
391
|
+
let audioContext = null;
|
|
392
|
+
let analyser = null;
|
|
393
|
+
let source = null;
|
|
394
|
+
try {
|
|
395
|
+
audioContext = new AudioContext();
|
|
396
|
+
analyser = audioContext.createAnalyser();
|
|
397
|
+
analyser.fftSize = 2048;
|
|
398
|
+
if (audioElement) {
|
|
399
|
+
console.log("[AudioWaveform] Creating audio source from element:", {
|
|
400
|
+
tagName: audioElement.tagName,
|
|
401
|
+
src: audioElement.src?.substring(0, 80),
|
|
402
|
+
readyState: audioElement.readyState,
|
|
403
|
+
paused: audioElement.paused,
|
|
404
|
+
currentTime: audioElement.currentTime,
|
|
405
|
+
hasSourceNode: !!audioElement.audioSourceNode,
|
|
406
|
+
isNativeElement: audioElement instanceof HTMLMediaElement
|
|
407
|
+
});
|
|
408
|
+
if (!(audioElement instanceof HTMLMediaElement)) {
|
|
409
|
+
console.warn(
|
|
410
|
+
"[AudioWaveform] Cannot visualize custom element (HLS-VIDEO), falling back to static waveform"
|
|
411
|
+
);
|
|
412
|
+
setSetupError(true);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (audioElement.audioSourceNode) {
|
|
416
|
+
console.log(
|
|
417
|
+
"[AudioWaveform] Audio source already exists, reusing it"
|
|
418
|
+
);
|
|
419
|
+
source = audioElement.audioSourceNode;
|
|
420
|
+
source?.connect(analyser);
|
|
421
|
+
analyser.connect(audioContext.destination);
|
|
422
|
+
} else {
|
|
423
|
+
try {
|
|
424
|
+
source = audioContext.createMediaElementSource(audioElement);
|
|
425
|
+
source.connect(analyser);
|
|
426
|
+
analyser.connect(audioContext.destination);
|
|
427
|
+
audioElement.audioSourceNode = source;
|
|
428
|
+
console.log(
|
|
429
|
+
"[AudioWaveform] Audio source created and connected successfully"
|
|
430
|
+
);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
console.error(
|
|
433
|
+
"[AudioWaveform] Failed to create media element source:",
|
|
434
|
+
error
|
|
435
|
+
);
|
|
436
|
+
setSetupError(true);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
audioElement.addEventListener("play", () => {
|
|
441
|
+
console.log("[AudioWaveform] Play event - setting isPlaying to true");
|
|
442
|
+
isPlayingRef.current = true;
|
|
443
|
+
});
|
|
444
|
+
audioElement.addEventListener("pause", () => {
|
|
445
|
+
console.log(
|
|
446
|
+
"[AudioWaveform] Pause event - setting isPlaying to false"
|
|
447
|
+
);
|
|
448
|
+
isPlayingRef.current = false;
|
|
449
|
+
});
|
|
450
|
+
audioElement.addEventListener("ended", () => {
|
|
451
|
+
console.log(
|
|
452
|
+
"[AudioWaveform] Ended event - setting isPlaying to false"
|
|
453
|
+
);
|
|
454
|
+
isPlayingRef.current = false;
|
|
455
|
+
});
|
|
456
|
+
console.log("[AudioWaveform] Initial audio state:", {
|
|
457
|
+
paused: audioElement.paused,
|
|
458
|
+
currentTime: audioElement.currentTime,
|
|
459
|
+
readyState: audioElement.readyState
|
|
460
|
+
});
|
|
461
|
+
if (!audioElement.paused) {
|
|
462
|
+
isPlayingRef.current = true;
|
|
463
|
+
}
|
|
464
|
+
} else if (mediaStream) {
|
|
465
|
+
source = audioContext.createMediaStreamSource(mediaStream);
|
|
466
|
+
source.connect(analyser);
|
|
467
|
+
isPlayingRef.current = true;
|
|
468
|
+
} else {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const bufferLength = analyser.frequencyBinCount;
|
|
472
|
+
const dataArray = new Uint8Array(bufferLength);
|
|
473
|
+
let waveOffset = 0;
|
|
474
|
+
const drawStaticWaveform = () => {
|
|
475
|
+
ctx.fillStyle = "#000";
|
|
476
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
477
|
+
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
|
|
478
|
+
gradient.addColorStop(0, "#06b6d4");
|
|
479
|
+
gradient.addColorStop(0.3, "#3b82f6");
|
|
480
|
+
gradient.addColorStop(0.6, "#8b5cf6");
|
|
481
|
+
gradient.addColorStop(1, "#ec4899");
|
|
482
|
+
waveOffset += 83e-4;
|
|
483
|
+
const barCount = 128;
|
|
484
|
+
const barWidth = canvas.width / barCount;
|
|
485
|
+
const gap = 2;
|
|
486
|
+
const baseHeight = 15;
|
|
487
|
+
const waveAmplitude = 10;
|
|
488
|
+
for (let i = 0; i < barCount; i++) {
|
|
489
|
+
const wave = Math.sin(i / barCount * Math.PI * 2 * 3 - waveOffset) * waveAmplitude;
|
|
490
|
+
const barHeight = baseHeight + wave;
|
|
491
|
+
const opacityWave = Math.sin(
|
|
492
|
+
i / barCount * Math.PI * 2 * 2 - waveOffset * 1.5
|
|
493
|
+
);
|
|
494
|
+
const opacity = 0.5 + opacityWave * 0.3;
|
|
495
|
+
const x = i * barWidth;
|
|
496
|
+
const y = canvas.height / 2 - barHeight / 2;
|
|
497
|
+
ctx.shadowBlur = 15;
|
|
498
|
+
ctx.shadowColor = "#3b82f6";
|
|
499
|
+
ctx.fillStyle = gradient;
|
|
500
|
+
ctx.globalAlpha = opacity;
|
|
501
|
+
ctx.fillRect(x + gap / 2, y, barWidth - gap, barHeight);
|
|
502
|
+
}
|
|
503
|
+
ctx.globalAlpha = 1;
|
|
504
|
+
ctx.shadowBlur = 0;
|
|
505
|
+
};
|
|
506
|
+
let frameCount = 0;
|
|
507
|
+
const draw = () => {
|
|
508
|
+
if (!analyser) return;
|
|
509
|
+
animationFrameRef.current = requestAnimationFrame(draw);
|
|
510
|
+
analyser.getByteFrequencyData(dataArray);
|
|
511
|
+
const hasActivity = dataArray.some((value) => value > 0);
|
|
512
|
+
const maxValue = Math.max(...dataArray);
|
|
513
|
+
frameCount++;
|
|
514
|
+
if (frameCount < 5 || frameCount % 60 === 0) {
|
|
515
|
+
console.log("[AudioWaveform] Frame", frameCount, "Audio activity:", {
|
|
516
|
+
hasActivity,
|
|
517
|
+
maxValue,
|
|
518
|
+
isPlaying: isPlayingRef.current,
|
|
519
|
+
sampleValues: [
|
|
520
|
+
dataArray[0],
|
|
521
|
+
dataArray[10],
|
|
522
|
+
dataArray[50],
|
|
523
|
+
dataArray[100]
|
|
524
|
+
],
|
|
525
|
+
avgValue: dataArray.reduce((a, b) => a + b, 0) / dataArray.length
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
if (!hasActivity || !isPlayingRef.current) {
|
|
529
|
+
if (frameCount < 5) {
|
|
530
|
+
console.log(
|
|
531
|
+
"[AudioWaveform] No activity or not playing, showing static waveform"
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
drawStaticWaveform();
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.1)";
|
|
538
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
539
|
+
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
|
|
540
|
+
gradient.addColorStop(0, "#06b6d4");
|
|
541
|
+
gradient.addColorStop(0.3, "#3b82f6");
|
|
542
|
+
gradient.addColorStop(0.6, "#8b5cf6");
|
|
543
|
+
gradient.addColorStop(1, "#ec4899");
|
|
544
|
+
const barCount = 128;
|
|
545
|
+
const barWidth = canvas.width / barCount;
|
|
546
|
+
const gap = 2;
|
|
547
|
+
for (let i = 0; i < barCount; i++) {
|
|
548
|
+
const barHeight = dataArray[i] / 255 * canvas.height * 0.8;
|
|
549
|
+
const x = i * barWidth;
|
|
550
|
+
const y = canvas.height / 2 - barHeight / 2;
|
|
551
|
+
ctx.shadowBlur = 20;
|
|
552
|
+
ctx.shadowColor = "#3b82f6";
|
|
553
|
+
ctx.fillStyle = gradient;
|
|
554
|
+
ctx.fillRect(x + gap / 2, y, barWidth - gap, barHeight);
|
|
555
|
+
}
|
|
556
|
+
ctx.shadowBlur = 0;
|
|
557
|
+
};
|
|
558
|
+
draw();
|
|
559
|
+
} catch (error) {
|
|
560
|
+
console.error("Error setting up audio visualization:", error);
|
|
561
|
+
setSetupError(true);
|
|
562
|
+
if (ctx) {
|
|
563
|
+
ctx.fillStyle = "#000";
|
|
564
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
565
|
+
ctx.strokeStyle = "#3b82f6";
|
|
566
|
+
ctx.lineWidth = 2;
|
|
567
|
+
ctx.beginPath();
|
|
568
|
+
ctx.moveTo(0, canvas.height / 2);
|
|
569
|
+
ctx.lineTo(canvas.width, canvas.height / 2);
|
|
570
|
+
ctx.stroke();
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return () => {
|
|
574
|
+
if (animationFrameRef.current) {
|
|
575
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
576
|
+
}
|
|
577
|
+
if (audioContext) {
|
|
578
|
+
audioContext.close();
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
}, [audioElement, mediaStream, isLive]);
|
|
582
|
+
return /* @__PURE__ */ jsxs("div", { className: "w-full h-full", children: [
|
|
583
|
+
/* @__PURE__ */ jsx(
|
|
584
|
+
"canvas",
|
|
585
|
+
{
|
|
586
|
+
ref: canvasRef,
|
|
587
|
+
width: 1600,
|
|
588
|
+
height: 400,
|
|
589
|
+
className: "w-full h-full",
|
|
590
|
+
style: { display: "block" }
|
|
591
|
+
}
|
|
592
|
+
),
|
|
593
|
+
setupError && /* @__PURE__ */ jsx("p", { className: "text-gray-400 text-xs text-center mt-2 absolute bottom-4", children: "Audio visualization unavailable" })
|
|
594
|
+
] });
|
|
595
|
+
}
|
|
596
|
+
var sizeClasses = {
|
|
597
|
+
sm: "h-4 w-4 border-2",
|
|
598
|
+
md: "h-8 w-8 border-4",
|
|
599
|
+
lg: "h-12 w-12 border-4"
|
|
600
|
+
};
|
|
601
|
+
var variantClasses = {
|
|
602
|
+
default: "border-gray-200 dark:border-gray-800 border-t-black dark:border-t-white",
|
|
603
|
+
primary: "border-gray-300 dark:border-gray-700 border-t-blue-600 dark:border-t-blue-400",
|
|
604
|
+
white: "border-gray-600 border-t-white"
|
|
605
|
+
};
|
|
606
|
+
function LoadingSpinner({
|
|
607
|
+
text,
|
|
608
|
+
size = "md",
|
|
609
|
+
variant = "default"
|
|
610
|
+
}) {
|
|
611
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center gap-3", children: [
|
|
612
|
+
/* @__PURE__ */ jsx(
|
|
613
|
+
"div",
|
|
614
|
+
{
|
|
615
|
+
className: `${sizeClasses[size]} ${variantClasses[variant]} rounded-full animate-spin`,
|
|
616
|
+
role: "status",
|
|
617
|
+
"aria-label": "Loading"
|
|
618
|
+
}
|
|
619
|
+
),
|
|
620
|
+
text && /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: text })
|
|
621
|
+
] });
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/utils/cdn.ts
|
|
625
|
+
var CDN_DOMAIN = typeof process !== "undefined" && process.env?.CONTENT_CDN_DOMAIN ? process.env.CONTENT_CDN_DOMAIN : "cdn.dialtribe.com";
|
|
626
|
+
function shardHash(hash) {
|
|
627
|
+
return hash.toLowerCase().split("").join("/");
|
|
628
|
+
}
|
|
629
|
+
function buildBroadcastS3KeyPrefix(appHash, broadcastHash) {
|
|
630
|
+
return `a/${shardHash(appHash)}/b/${shardHash(broadcastHash)}/`;
|
|
631
|
+
}
|
|
632
|
+
function buildBroadcastCdnUrl(appHash, broadcastHash, filename) {
|
|
633
|
+
const keyPrefix = buildBroadcastS3KeyPrefix(appHash, broadcastHash);
|
|
634
|
+
return `https://${CDN_DOMAIN}/${keyPrefix}${filename}`;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/utils/format.ts
|
|
638
|
+
function formatTime(seconds, includeHours) {
|
|
639
|
+
if (isNaN(seconds) || !isFinite(seconds) || seconds < 0) {
|
|
640
|
+
return "0:00";
|
|
641
|
+
}
|
|
642
|
+
const hrs = Math.floor(seconds / 3600);
|
|
643
|
+
const mins = Math.floor(seconds % 3600 / 60);
|
|
644
|
+
const secs = Math.floor(seconds % 60);
|
|
645
|
+
const secStr = secs.toString().padStart(2, "0");
|
|
646
|
+
const minStr = mins.toString().padStart(2, "0");
|
|
647
|
+
if (hrs > 0 || includeHours) {
|
|
648
|
+
return `${hrs}:${minStr}:${secStr}`;
|
|
649
|
+
}
|
|
650
|
+
return `${mins}:${secStr}`;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/utils/http-status.ts
|
|
654
|
+
var HTTP_STATUS = {
|
|
655
|
+
// 2xx Success
|
|
656
|
+
OK: 200,
|
|
657
|
+
CREATED: 201,
|
|
658
|
+
ACCEPTED: 202,
|
|
659
|
+
NO_CONTENT: 204,
|
|
660
|
+
// 3xx Redirection
|
|
661
|
+
MOVED_PERMANENTLY: 301,
|
|
662
|
+
FOUND: 302,
|
|
663
|
+
SEE_OTHER: 303,
|
|
664
|
+
NOT_MODIFIED: 304,
|
|
665
|
+
TEMPORARY_REDIRECT: 307,
|
|
666
|
+
PERMANENT_REDIRECT: 308,
|
|
667
|
+
// 4xx Client Errors
|
|
668
|
+
BAD_REQUEST: 400,
|
|
669
|
+
UNAUTHORIZED: 401,
|
|
670
|
+
PAYMENT_REQUIRED: 402,
|
|
671
|
+
FORBIDDEN: 403,
|
|
672
|
+
NOT_FOUND: 404,
|
|
673
|
+
METHOD_NOT_ALLOWED: 405,
|
|
674
|
+
NOT_ACCEPTABLE: 406,
|
|
675
|
+
CONFLICT: 409,
|
|
676
|
+
GONE: 410,
|
|
677
|
+
PAYLOAD_TOO_LARGE: 413,
|
|
678
|
+
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
679
|
+
UNPROCESSABLE_ENTITY: 422,
|
|
680
|
+
TOO_MANY_REQUESTS: 429,
|
|
681
|
+
// 5xx Server Errors
|
|
682
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
683
|
+
NOT_IMPLEMENTED: 501,
|
|
684
|
+
BAD_GATEWAY: 502,
|
|
685
|
+
SERVICE_UNAVAILABLE: 503,
|
|
686
|
+
GATEWAY_TIMEOUT: 504
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// src/utils/debug.ts
|
|
690
|
+
var DEBUG = process.env.NODE_ENV === "development";
|
|
691
|
+
var debug = {
|
|
692
|
+
log: (...args) => {
|
|
693
|
+
if (DEBUG) console.log(...args);
|
|
694
|
+
},
|
|
695
|
+
warn: (...args) => {
|
|
696
|
+
if (DEBUG) console.warn(...args);
|
|
697
|
+
},
|
|
698
|
+
error: (...args) => {
|
|
699
|
+
if (DEBUG) console.error(...args);
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
var URL_EXPIRATION_MS = 6 * 60 * 60 * 1e3;
|
|
703
|
+
var REFRESH_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
704
|
+
var REFRESH_CHECK_INTERVAL_MS = 6e4;
|
|
705
|
+
var PLAYBACK_RESUME_DELAY_MS = 500;
|
|
706
|
+
var TRAILING_WORDS = 3;
|
|
707
|
+
function buildPlaybackUrl(broadcastId, hash, action) {
|
|
708
|
+
const searchParams = new URLSearchParams({
|
|
709
|
+
broadcastId: broadcastId.toString(),
|
|
710
|
+
hash
|
|
711
|
+
});
|
|
712
|
+
if (action) {
|
|
713
|
+
searchParams.set("action", action);
|
|
714
|
+
}
|
|
715
|
+
return `${ENDPOINTS.contentPlay}?${searchParams}`;
|
|
716
|
+
}
|
|
717
|
+
function getErrorMessage(error) {
|
|
718
|
+
if (!error) return "Unable to play media. Please try again.";
|
|
719
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
720
|
+
const errorCode = error?.code;
|
|
721
|
+
const errorStatus = error?.status || error?.statusCode;
|
|
722
|
+
if (errorMsg.toLowerCase().includes("network") || errorMsg.includes("NetworkError")) {
|
|
723
|
+
return "No internet connection detected. Please check your network and try again.";
|
|
724
|
+
}
|
|
725
|
+
if (errorStatus === 401 || errorMsg.includes("401") || errorMsg.includes("Unauthorized")) {
|
|
726
|
+
return "Session expired. Please refresh the page and log in again.";
|
|
727
|
+
}
|
|
728
|
+
if (errorStatus === 403 || errorMsg.includes("403") || errorMsg.includes("Forbidden")) {
|
|
729
|
+
return "Access denied. You may not have permission to view this content.";
|
|
730
|
+
}
|
|
731
|
+
if (errorStatus === 404 || errorMsg.includes("404") || errorMsg.includes("not found")) {
|
|
732
|
+
return "Media file not found. It may have been deleted or is still processing.";
|
|
733
|
+
}
|
|
734
|
+
if (errorMsg.includes("no supported sources") || errorMsg.includes("NotSupportedError")) {
|
|
735
|
+
return "This media format is not supported by your browser. Try using Chrome, Firefox, or Safari.";
|
|
736
|
+
}
|
|
737
|
+
if (errorMsg.includes("MEDIA_ERR_SRC_NOT_SUPPORTED") || errorCode === 4) {
|
|
738
|
+
return "Media file is not available or the format is unsupported.";
|
|
739
|
+
}
|
|
740
|
+
if (errorMsg.includes("MEDIA_ERR_NETWORK") || errorCode === 2) {
|
|
741
|
+
return "Network error while loading media. Please check your connection.";
|
|
742
|
+
}
|
|
743
|
+
if (errorMsg.includes("MEDIA_ERR_DECODE") || errorCode === 3) {
|
|
744
|
+
return "Media file is corrupted or cannot be decoded. Please contact support.";
|
|
745
|
+
}
|
|
746
|
+
if (errorMsg.includes("AbortError")) {
|
|
747
|
+
return "Media loading was interrupted. Please try again.";
|
|
748
|
+
}
|
|
749
|
+
return "Unable to play media. Please try refreshing the page or contact support if the problem persists.";
|
|
750
|
+
}
|
|
751
|
+
function BroadcastPlayer({
|
|
752
|
+
broadcast,
|
|
753
|
+
appId,
|
|
754
|
+
contentId,
|
|
755
|
+
foreignId,
|
|
756
|
+
foreignTier = "guest",
|
|
757
|
+
renderClipCreator,
|
|
758
|
+
onError,
|
|
759
|
+
className = "",
|
|
760
|
+
enableKeyboardShortcuts = false
|
|
761
|
+
}) {
|
|
762
|
+
const { sessionToken, setSessionToken, markExpired } = useDialTribe();
|
|
763
|
+
const clientRef = useRef(null);
|
|
764
|
+
if (!clientRef.current && sessionToken) {
|
|
765
|
+
clientRef.current = new DialTribeClient({
|
|
766
|
+
sessionToken,
|
|
767
|
+
onTokenRefresh: (newToken, expiresAt) => {
|
|
768
|
+
debug.log(`[DialTribeClient] Token refreshed, expires at ${expiresAt}`);
|
|
769
|
+
setSessionToken(newToken, expiresAt);
|
|
770
|
+
},
|
|
771
|
+
onTokenExpired: () => {
|
|
772
|
+
debug.error("[DialTribeClient] Token expired");
|
|
773
|
+
markExpired();
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
} else if (clientRef.current && sessionToken) {
|
|
777
|
+
clientRef.current.setSessionToken(sessionToken);
|
|
778
|
+
}
|
|
779
|
+
const client = clientRef.current;
|
|
780
|
+
const playerRef = useRef(null);
|
|
781
|
+
const transcriptContainerRef = useRef(null);
|
|
782
|
+
const activeWordRef = useRef(null);
|
|
783
|
+
const [audioElement, setAudioElement] = useState(null);
|
|
784
|
+
const [playing, setPlaying] = useState(false);
|
|
785
|
+
const [played, setPlayed] = useState(0);
|
|
786
|
+
const [duration, setDuration] = useState(0);
|
|
787
|
+
const [volume, setVolume] = useState(1);
|
|
788
|
+
const [muted, setMuted] = useState(false);
|
|
789
|
+
const [seeking, setSeeking] = useState(false);
|
|
790
|
+
const [hasError, setHasError] = useState(false);
|
|
791
|
+
const [errorMessage, setErrorMessage] = useState("");
|
|
792
|
+
const [hasEnded, setHasEnded] = useState(false);
|
|
793
|
+
const [hasStreamEnded, setHasStreamEnded] = useState(false);
|
|
794
|
+
const [showTranscript, setShowTranscript] = useState(false);
|
|
795
|
+
const [transcriptData, setTranscriptData] = useState(null);
|
|
796
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
797
|
+
const [isLoadingTranscript, setIsLoadingTranscript] = useState(false);
|
|
798
|
+
const [isLoadingVideo, setIsLoadingVideo] = useState(true);
|
|
799
|
+
const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
|
|
800
|
+
const isScrollingProgrammatically = useRef(false);
|
|
801
|
+
const lastActiveWordIndex = useRef(-1);
|
|
802
|
+
const [showClipCreator, setShowClipCreator] = useState(false);
|
|
803
|
+
const initialPlaybackTypeRef = useRef(null);
|
|
804
|
+
const [currentPlaybackInfo, setCurrentPlaybackInfo] = useState(null);
|
|
805
|
+
const [urlExpiresAt, setUrlExpiresAt] = useState(null);
|
|
806
|
+
const isRefreshingUrl = useRef(false);
|
|
807
|
+
const [audienceId, setAudienceId] = useState(null);
|
|
808
|
+
const [sessionId] = useState(() => {
|
|
809
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
810
|
+
return crypto.randomUUID();
|
|
811
|
+
}
|
|
812
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
813
|
+
const r = Math.random() * 16 | 0;
|
|
814
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
815
|
+
return v.toString(16);
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
const heartbeatIntervalRef = useRef(null);
|
|
819
|
+
const hasInitializedSession = useRef(false);
|
|
820
|
+
const refreshPresignedUrl = useCallback(
|
|
821
|
+
async (fileType) => {
|
|
822
|
+
if (!broadcast.hash || isRefreshingUrl.current || !client) {
|
|
823
|
+
debug.log("[URL Refresh] Skipping refresh - no hash, already refreshing, or no client");
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
if (fileType === "hls") {
|
|
827
|
+
debug.log("[URL Refresh] HLS does not need URL refresh");
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
isRefreshingUrl.current = true;
|
|
831
|
+
debug.log(`[URL Refresh] Refreshing ${fileType} URL for broadcast ${broadcast.id}`);
|
|
832
|
+
try {
|
|
833
|
+
const data = await client.refreshPresignedUrl({
|
|
834
|
+
broadcastId: broadcast.id,
|
|
835
|
+
hash: broadcast.hash,
|
|
836
|
+
fileType
|
|
837
|
+
});
|
|
838
|
+
debug.log(`[URL Refresh] Successfully refreshed URL, expires at ${data.expiresAt}`);
|
|
839
|
+
setCurrentPlaybackInfo({ url: data.url, type: fileType });
|
|
840
|
+
setUrlExpiresAt(new Date(data.expiresAt));
|
|
841
|
+
if (errorMessage.includes("URL") || errorMessage.includes("session") || errorMessage.includes("refresh")) {
|
|
842
|
+
setHasError(false);
|
|
843
|
+
setErrorMessage("");
|
|
844
|
+
}
|
|
845
|
+
return true;
|
|
846
|
+
} catch (error) {
|
|
847
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
848
|
+
debug.log("[URL Refresh] Request aborted");
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
debug.error("[URL Refresh] Failed to refresh presigned URL:", error);
|
|
852
|
+
setHasError(true);
|
|
853
|
+
setErrorMessage("Unable to refresh media URL. The session may have expired.");
|
|
854
|
+
if (onError && error instanceof Error) {
|
|
855
|
+
onError(error);
|
|
856
|
+
}
|
|
857
|
+
return false;
|
|
858
|
+
} finally {
|
|
859
|
+
isRefreshingUrl.current = false;
|
|
860
|
+
}
|
|
861
|
+
},
|
|
862
|
+
[broadcast.hash, broadcast.id, errorMessage, client, onError]
|
|
863
|
+
);
|
|
864
|
+
const getScreenSize = () => {
|
|
865
|
+
if (typeof window === "undefined") return "unknown";
|
|
866
|
+
const width = window.innerWidth;
|
|
867
|
+
if (width < 768) return "mobile";
|
|
868
|
+
if (width < 1024) return "tablet";
|
|
869
|
+
return "desktop";
|
|
870
|
+
};
|
|
871
|
+
const initializeTrackingSession = useCallback(async () => {
|
|
872
|
+
if (!contentId || !appId || !client) return;
|
|
873
|
+
if (currentPlaybackInfo?.type === "hls" && broadcast.broadcastStatus === 1) return;
|
|
874
|
+
if (hasInitializedSession.current) return;
|
|
875
|
+
hasInitializedSession.current = true;
|
|
876
|
+
try {
|
|
877
|
+
const screenSize = getScreenSize();
|
|
878
|
+
const platformInfo = `${navigator.platform || "Unknown"} (${screenSize})`;
|
|
879
|
+
const data = await client.startSession({
|
|
880
|
+
contentId,
|
|
881
|
+
broadcastId: broadcast.id,
|
|
882
|
+
appId,
|
|
883
|
+
foreignId: foreignId || null,
|
|
884
|
+
foreignTier: foreignTier || "guest",
|
|
885
|
+
sessionId,
|
|
886
|
+
fileType: currentPlaybackInfo?.type || "mp3",
|
|
887
|
+
platform: platformInfo,
|
|
888
|
+
userAgent: navigator.userAgent || null,
|
|
889
|
+
origin: window.location.origin || null,
|
|
890
|
+
country: null,
|
|
891
|
+
region: null
|
|
892
|
+
});
|
|
893
|
+
setAudienceId(data.audienceId);
|
|
894
|
+
if (data.resumePosition && data.resumePosition > 0 && audioElement) {
|
|
895
|
+
audioElement.currentTime = data.resumePosition;
|
|
896
|
+
debug.log(`[Audience Tracking] Resumed playback at ${data.resumePosition}s`);
|
|
897
|
+
}
|
|
898
|
+
debug.log("[Audience Tracking] Session initialized:", data.audienceId);
|
|
899
|
+
} catch (error) {
|
|
900
|
+
debug.error("[Audience Tracking] Error initializing session:", error);
|
|
901
|
+
if (onError && error instanceof Error) {
|
|
902
|
+
onError(error);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}, [contentId, appId, broadcast.id, broadcast.broadcastStatus, foreignId, foreignTier, sessionId, currentPlaybackInfo?.type, audioElement, client, onError]);
|
|
906
|
+
const sendTrackingPing = useCallback(
|
|
907
|
+
async (eventType) => {
|
|
908
|
+
if (!audienceId || !sessionId || !client) return;
|
|
909
|
+
try {
|
|
910
|
+
await client.sendSessionPing({
|
|
911
|
+
audienceId,
|
|
912
|
+
sessionId,
|
|
913
|
+
eventType,
|
|
914
|
+
currentTime: Math.floor(audioElement?.currentTime || 0),
|
|
915
|
+
duration: Math.floor(duration || 0)
|
|
916
|
+
});
|
|
917
|
+
} catch (error) {
|
|
918
|
+
debug.error("[Audience Tracking] Error sending ping:", error);
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
[audienceId, sessionId, audioElement, duration, client]
|
|
922
|
+
);
|
|
923
|
+
const getPlaybackInfo = () => {
|
|
924
|
+
if (broadcast.broadcastStatus === 1) {
|
|
925
|
+
if (broadcast.hlsPlaylistUrl) {
|
|
926
|
+
return { url: broadcast.hlsPlaylistUrl, type: "hls" };
|
|
927
|
+
}
|
|
928
|
+
if (broadcast.hash && broadcast.app?.s3Hash) {
|
|
929
|
+
const hlsUrl = buildBroadcastCdnUrl(broadcast.app.s3Hash, broadcast.hash, "index.m3u8");
|
|
930
|
+
return { url: hlsUrl, type: "hls" };
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (broadcast.recordingMp4Url && broadcast.isVideo && broadcast.hash) {
|
|
934
|
+
return { url: buildPlaybackUrl(broadcast.id, broadcast.hash), type: "mp4" };
|
|
935
|
+
}
|
|
936
|
+
if (broadcast.recordingMp3Url && broadcast.hash) {
|
|
937
|
+
return { url: buildPlaybackUrl(broadcast.id, broadcast.hash), type: "mp3" };
|
|
938
|
+
}
|
|
939
|
+
if (broadcast.hlsPlaylistUrl) {
|
|
940
|
+
return { url: broadcast.hlsPlaylistUrl, type: "hls" };
|
|
941
|
+
}
|
|
942
|
+
return null;
|
|
943
|
+
};
|
|
944
|
+
useEffect(() => {
|
|
945
|
+
if (!currentPlaybackInfo) {
|
|
946
|
+
const info = getPlaybackInfo();
|
|
947
|
+
setCurrentPlaybackInfo(info);
|
|
948
|
+
initialPlaybackTypeRef.current = info?.type || null;
|
|
949
|
+
if (info && (info.type === "mp4" || info.type === "mp3")) {
|
|
950
|
+
const expiresAt = new Date(Date.now() + URL_EXPIRATION_MS);
|
|
951
|
+
setUrlExpiresAt(expiresAt);
|
|
952
|
+
debug.log(`[URL Refresh] Initial ${info.type} URL expires at ${expiresAt.toISOString()}`);
|
|
953
|
+
}
|
|
954
|
+
if (info) {
|
|
955
|
+
setPlaying(true);
|
|
956
|
+
setIsLoadingVideo(true);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}, [currentPlaybackInfo]);
|
|
960
|
+
useEffect(() => {
|
|
961
|
+
if (currentPlaybackInfo?.url) {
|
|
962
|
+
setIsLoadingVideo(true);
|
|
963
|
+
}
|
|
964
|
+
}, [currentPlaybackInfo?.url]);
|
|
965
|
+
useEffect(() => {
|
|
966
|
+
if (!urlExpiresAt || !currentPlaybackInfo?.type) return;
|
|
967
|
+
const checkExpiration = () => {
|
|
968
|
+
const now = /* @__PURE__ */ new Date();
|
|
969
|
+
const timeUntilExpiration = urlExpiresAt.getTime() - now.getTime();
|
|
970
|
+
if (timeUntilExpiration <= REFRESH_THRESHOLD_MS && timeUntilExpiration > 0) {
|
|
971
|
+
debug.log("[URL Refresh] Proactively refreshing URL before expiration");
|
|
972
|
+
const fileType = currentPlaybackInfo.type;
|
|
973
|
+
if (fileType === "mp4" || fileType === "mp3" || fileType === "hls") {
|
|
974
|
+
refreshPresignedUrl(fileType);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
const interval = setInterval(checkExpiration, REFRESH_CHECK_INTERVAL_MS);
|
|
979
|
+
checkExpiration();
|
|
980
|
+
return () => {
|
|
981
|
+
clearInterval(interval);
|
|
982
|
+
};
|
|
983
|
+
}, [urlExpiresAt, currentPlaybackInfo?.type, refreshPresignedUrl]);
|
|
984
|
+
useEffect(() => {
|
|
985
|
+
if (initialPlaybackTypeRef.current === "hls" && currentPlaybackInfo?.type === "hls" && broadcast.broadcastStatus !== 1 && broadcast.recordingMp3Url && broadcast.hash && parseInt(broadcast.mp3Size || "0") > 0) {
|
|
986
|
+
const secureUrl = buildPlaybackUrl(broadcast.id, broadcast.hash);
|
|
987
|
+
setCurrentPlaybackInfo({ url: secureUrl, type: "mp3" });
|
|
988
|
+
setAudioElement(null);
|
|
989
|
+
setPlaying(true);
|
|
990
|
+
}
|
|
991
|
+
}, [broadcast.broadcastStatus, broadcast.recordingMp3Url, broadcast.mp3Size, broadcast.hash, broadcast.id, currentPlaybackInfo]);
|
|
992
|
+
const playbackUrl = currentPlaybackInfo?.url || null;
|
|
993
|
+
const playbackType = currentPlaybackInfo?.type || null;
|
|
994
|
+
const isAudioOnly = playbackType === "mp3" || !broadcast.isVideo && playbackType !== "mp4";
|
|
995
|
+
const isLiveStream = broadcast.broadcastStatus === 1 && playbackType === "hls" && !hasStreamEnded;
|
|
996
|
+
const wasLiveStream = initialPlaybackTypeRef.current === "hls";
|
|
997
|
+
const formatTimestamp = (seconds) => {
|
|
998
|
+
if (!seconds || isNaN(seconds) || !isFinite(seconds)) return "00:00:00";
|
|
999
|
+
const hrs = Math.floor(seconds / 3600);
|
|
1000
|
+
const mins = Math.floor(seconds % 3600 / 60);
|
|
1001
|
+
const secs = Math.floor(seconds % 60);
|
|
1002
|
+
return `${hrs.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
1003
|
+
};
|
|
1004
|
+
const handlePlay = () => {
|
|
1005
|
+
setPlaying(true);
|
|
1006
|
+
setIsLoadingVideo(false);
|
|
1007
|
+
};
|
|
1008
|
+
const handlePause = () => {
|
|
1009
|
+
setPlaying(false);
|
|
1010
|
+
};
|
|
1011
|
+
const handleEnded = () => {
|
|
1012
|
+
setPlaying(false);
|
|
1013
|
+
if (playbackType === "hls") {
|
|
1014
|
+
setHasStreamEnded(true);
|
|
1015
|
+
}
|
|
1016
|
+
if (!wasLiveStream) {
|
|
1017
|
+
setHasEnded(true);
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
useEffect(() => {
|
|
1021
|
+
if (broadcast.durationSeconds && broadcast.durationSeconds > 0) {
|
|
1022
|
+
setDuration(broadcast.durationSeconds);
|
|
1023
|
+
}
|
|
1024
|
+
}, [broadcast.durationSeconds]);
|
|
1025
|
+
useEffect(() => {
|
|
1026
|
+
if (isLiveStream && !playing) {
|
|
1027
|
+
setPlaying(true);
|
|
1028
|
+
}
|
|
1029
|
+
}, [isLiveStream, playing]);
|
|
1030
|
+
useEffect(() => {
|
|
1031
|
+
if (currentPlaybackInfo && audioElement && !hasInitializedSession.current) {
|
|
1032
|
+
initializeTrackingSession();
|
|
1033
|
+
}
|
|
1034
|
+
}, [currentPlaybackInfo, audioElement, initializeTrackingSession]);
|
|
1035
|
+
useEffect(() => {
|
|
1036
|
+
if (playing && audienceId) {
|
|
1037
|
+
sendTrackingPing(1);
|
|
1038
|
+
heartbeatIntervalRef.current = setInterval(() => {
|
|
1039
|
+
sendTrackingPing(2);
|
|
1040
|
+
}, 15e3);
|
|
1041
|
+
return () => {
|
|
1042
|
+
if (heartbeatIntervalRef.current) {
|
|
1043
|
+
clearInterval(heartbeatIntervalRef.current);
|
|
1044
|
+
heartbeatIntervalRef.current = null;
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
} else if (!playing && audienceId) {
|
|
1048
|
+
sendTrackingPing(0);
|
|
1049
|
+
if (heartbeatIntervalRef.current) {
|
|
1050
|
+
clearInterval(heartbeatIntervalRef.current);
|
|
1051
|
+
heartbeatIntervalRef.current = null;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}, [playing, audienceId, sendTrackingPing]);
|
|
1055
|
+
useEffect(() => {
|
|
1056
|
+
return () => {
|
|
1057
|
+
if (audienceId && sessionId && sessionToken) {
|
|
1058
|
+
const payload = {
|
|
1059
|
+
audienceId,
|
|
1060
|
+
sessionId,
|
|
1061
|
+
eventType: 3,
|
|
1062
|
+
// UNMOUNT
|
|
1063
|
+
currentTime: Math.floor(audioElement?.currentTime || 0),
|
|
1064
|
+
duration: Math.floor(duration || 0)
|
|
1065
|
+
};
|
|
1066
|
+
const headers = {
|
|
1067
|
+
"Authorization": `Bearer ${sessionToken}`,
|
|
1068
|
+
"Content-Type": "application/json"
|
|
1069
|
+
};
|
|
1070
|
+
fetch(ENDPOINTS.sessionPing, {
|
|
1071
|
+
method: "POST",
|
|
1072
|
+
headers,
|
|
1073
|
+
body: JSON.stringify(payload),
|
|
1074
|
+
keepalive: true
|
|
1075
|
+
}).catch(() => {
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
}, [audienceId, sessionId, sessionToken, audioElement, duration]);
|
|
1080
|
+
useEffect(() => {
|
|
1081
|
+
if (broadcast.transcriptUrl && broadcast.transcriptStatus === 2 && !transcriptData) {
|
|
1082
|
+
setIsLoadingTranscript(true);
|
|
1083
|
+
fetch(broadcast.transcriptUrl).then((res) => {
|
|
1084
|
+
if (!res.ok) {
|
|
1085
|
+
throw new Error(`Failed to fetch transcript: ${res.status} ${res.statusText}`);
|
|
1086
|
+
}
|
|
1087
|
+
return res.json();
|
|
1088
|
+
}).then((data) => {
|
|
1089
|
+
if (data.segments && data.words && !data.segments[0]?.words) {
|
|
1090
|
+
data.segments = data.segments.map((segment, index) => {
|
|
1091
|
+
const segmentWords = data.words.filter((word) => {
|
|
1092
|
+
if (index === data.segments.length - 1) {
|
|
1093
|
+
return word.start >= segment.start;
|
|
1094
|
+
}
|
|
1095
|
+
return word.start >= segment.start && word.start < segment.end;
|
|
1096
|
+
});
|
|
1097
|
+
return {
|
|
1098
|
+
...segment,
|
|
1099
|
+
words: segmentWords
|
|
1100
|
+
};
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
setTranscriptData(data);
|
|
1104
|
+
setIsLoadingTranscript(false);
|
|
1105
|
+
}).catch((error) => {
|
|
1106
|
+
debug.error("[Transcript] Failed to load transcript:", error);
|
|
1107
|
+
setIsLoadingTranscript(false);
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
}, [broadcast.transcriptUrl, broadcast.transcriptStatus, transcriptData]);
|
|
1111
|
+
useEffect(() => {
|
|
1112
|
+
if (!audioElement) return;
|
|
1113
|
+
const handleTimeUpdate2 = () => {
|
|
1114
|
+
setCurrentTime(audioElement.currentTime);
|
|
1115
|
+
};
|
|
1116
|
+
audioElement.addEventListener("timeupdate", handleTimeUpdate2);
|
|
1117
|
+
return () => audioElement.removeEventListener("timeupdate", handleTimeUpdate2);
|
|
1118
|
+
}, [audioElement]);
|
|
1119
|
+
useEffect(() => {
|
|
1120
|
+
if (showTranscript && autoScrollEnabled && activeWordRef.current && transcriptContainerRef.current) {
|
|
1121
|
+
const container = transcriptContainerRef.current;
|
|
1122
|
+
const activeWord = activeWordRef.current;
|
|
1123
|
+
const containerRect = container.getBoundingClientRect();
|
|
1124
|
+
const wordRect = activeWord.getBoundingClientRect();
|
|
1125
|
+
if (wordRect.top < containerRect.top || wordRect.bottom > containerRect.bottom) {
|
|
1126
|
+
isScrollingProgrammatically.current = true;
|
|
1127
|
+
activeWord.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
1128
|
+
setTimeout(() => {
|
|
1129
|
+
isScrollingProgrammatically.current = false;
|
|
1130
|
+
}, 500);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}, [currentTime, showTranscript, autoScrollEnabled]);
|
|
1134
|
+
useEffect(() => {
|
|
1135
|
+
if (!showTranscript || !transcriptContainerRef.current) return;
|
|
1136
|
+
const container = transcriptContainerRef.current;
|
|
1137
|
+
const handleScroll = () => {
|
|
1138
|
+
if (!isScrollingProgrammatically.current && autoScrollEnabled) {
|
|
1139
|
+
setAutoScrollEnabled(false);
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
container.addEventListener("scroll", handleScroll, { passive: true });
|
|
1143
|
+
return () => container.removeEventListener("scroll", handleScroll);
|
|
1144
|
+
}, [showTranscript, autoScrollEnabled]);
|
|
1145
|
+
const handlePlayPause = () => {
|
|
1146
|
+
if (hasEnded) {
|
|
1147
|
+
if (audioElement) {
|
|
1148
|
+
audioElement.currentTime = 0;
|
|
1149
|
+
}
|
|
1150
|
+
setHasEnded(false);
|
|
1151
|
+
}
|
|
1152
|
+
setPlaying(!playing);
|
|
1153
|
+
};
|
|
1154
|
+
const handleRestart = () => {
|
|
1155
|
+
if (audioElement) {
|
|
1156
|
+
audioElement.currentTime = 0;
|
|
1157
|
+
}
|
|
1158
|
+
setHasEnded(false);
|
|
1159
|
+
setPlaying(true);
|
|
1160
|
+
};
|
|
1161
|
+
const handleVideoClick = () => {
|
|
1162
|
+
if (!isLiveStream) {
|
|
1163
|
+
handlePlayPause();
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
const handleSeekChange = (e) => {
|
|
1167
|
+
const newValue = parseFloat(e.target.value);
|
|
1168
|
+
setPlayed(newValue);
|
|
1169
|
+
};
|
|
1170
|
+
const handleSeekMouseDown = () => {
|
|
1171
|
+
setSeeking(true);
|
|
1172
|
+
};
|
|
1173
|
+
const handleSeekMouseUp = (e) => {
|
|
1174
|
+
const seekValue = parseFloat(e.target.value);
|
|
1175
|
+
setSeeking(false);
|
|
1176
|
+
if (audioElement && duration > 0) {
|
|
1177
|
+
const seekTime = seekValue * duration;
|
|
1178
|
+
audioElement.currentTime = seekTime;
|
|
1179
|
+
setHasEnded(false);
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
const handleSeekTouchStart = () => {
|
|
1183
|
+
setSeeking(true);
|
|
1184
|
+
};
|
|
1185
|
+
const handleSeekTouchEnd = (e) => {
|
|
1186
|
+
const seekValue = parseFloat(e.target.value);
|
|
1187
|
+
setSeeking(false);
|
|
1188
|
+
if (audioElement && duration > 0) {
|
|
1189
|
+
const seekTime = seekValue * duration;
|
|
1190
|
+
audioElement.currentTime = seekTime;
|
|
1191
|
+
setHasEnded(false);
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
const handleTimeUpdate = (e) => {
|
|
1195
|
+
if (!seeking) {
|
|
1196
|
+
const video = e.currentTarget;
|
|
1197
|
+
const playedFraction = video.duration > 0 ? video.currentTime / video.duration : 0;
|
|
1198
|
+
setPlayed(playedFraction);
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
const handleLoadedMetadata = (e) => {
|
|
1202
|
+
const video = e.currentTarget;
|
|
1203
|
+
setAudioElement(video);
|
|
1204
|
+
if (video.duration && !isNaN(video.duration) && video.duration > 0) {
|
|
1205
|
+
setDuration(video.duration);
|
|
1206
|
+
} else if (broadcast.durationSeconds && broadcast.durationSeconds > 0) {
|
|
1207
|
+
setDuration(broadcast.durationSeconds);
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
const handlePlayerReady = (player) => {
|
|
1211
|
+
try {
|
|
1212
|
+
const internalPlayer = player?.getInternalPlayer?.();
|
|
1213
|
+
if (internalPlayer && internalPlayer instanceof HTMLMediaElement) {
|
|
1214
|
+
setAudioElement(internalPlayer);
|
|
1215
|
+
}
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
debug.error("[BroadcastPlayer] Error getting internal player:", error);
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
useEffect(() => {
|
|
1221
|
+
const findAudioElement = () => {
|
|
1222
|
+
const videoElements = document.querySelectorAll("video, audio");
|
|
1223
|
+
if (videoElements.length > 0) {
|
|
1224
|
+
const element = videoElements[0];
|
|
1225
|
+
setAudioElement(element);
|
|
1226
|
+
return true;
|
|
1227
|
+
}
|
|
1228
|
+
return false;
|
|
1229
|
+
};
|
|
1230
|
+
if (!findAudioElement()) {
|
|
1231
|
+
const retryIntervals = [100, 300, 500, 1e3, 1500, 2e3, 3e3, 4e3, 5e3];
|
|
1232
|
+
const timeouts = retryIntervals.map(
|
|
1233
|
+
(delay) => setTimeout(() => {
|
|
1234
|
+
findAudioElement();
|
|
1235
|
+
}, delay)
|
|
1236
|
+
);
|
|
1237
|
+
return () => timeouts.forEach(clearTimeout);
|
|
1238
|
+
}
|
|
1239
|
+
}, [playbackUrl]);
|
|
1240
|
+
useEffect(() => {
|
|
1241
|
+
if (playing && !audioElement) {
|
|
1242
|
+
const videoElements = document.querySelectorAll("video, audio");
|
|
1243
|
+
if (videoElements.length > 0) {
|
|
1244
|
+
const element = videoElements[0];
|
|
1245
|
+
setAudioElement(element);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}, [playing, audioElement]);
|
|
1249
|
+
const handleVolumeChange = (e) => {
|
|
1250
|
+
setVolume(parseFloat(e.target.value));
|
|
1251
|
+
};
|
|
1252
|
+
const toggleMute = () => {
|
|
1253
|
+
setMuted(!muted);
|
|
1254
|
+
};
|
|
1255
|
+
const toggleFullscreen = async () => {
|
|
1256
|
+
try {
|
|
1257
|
+
if (document.fullscreenElement) {
|
|
1258
|
+
await document.exitFullscreen();
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
let videoElement = null;
|
|
1262
|
+
if (playerRef.current && typeof playerRef.current.getInternalPlayer === "function") {
|
|
1263
|
+
videoElement = playerRef.current.getInternalPlayer();
|
|
1264
|
+
}
|
|
1265
|
+
if (!videoElement || typeof videoElement.requestFullscreen !== "function") {
|
|
1266
|
+
const videoElements = document.querySelectorAll("video");
|
|
1267
|
+
if (videoElements.length > 0) {
|
|
1268
|
+
videoElement = videoElements[0];
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (!videoElement || typeof videoElement.requestFullscreen !== "function") {
|
|
1272
|
+
const modalElement = document.querySelector(".aspect-video");
|
|
1273
|
+
if (modalElement && typeof modalElement.requestFullscreen === "function") {
|
|
1274
|
+
await modalElement.requestFullscreen();
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (videoElement && typeof videoElement.requestFullscreen === "function") {
|
|
1279
|
+
await videoElement.requestFullscreen();
|
|
1280
|
+
}
|
|
1281
|
+
} catch (error) {
|
|
1282
|
+
debug.error("Error toggling fullscreen:", error);
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
const handleError = async (error) => {
|
|
1286
|
+
debug.error("Media playback error:", error);
|
|
1287
|
+
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");
|
|
1288
|
+
if (isPotentialExpiration && currentPlaybackInfo?.type && !isRefreshingUrl.current) {
|
|
1289
|
+
debug.log("[Player Error] Detected potential URL expiration, attempting refresh...");
|
|
1290
|
+
const currentPosition = audioElement?.currentTime || 0;
|
|
1291
|
+
const wasPlaying = playing;
|
|
1292
|
+
const fileType = currentPlaybackInfo.type;
|
|
1293
|
+
if (fileType !== "mp4" && fileType !== "mp3" && fileType !== "hls") {
|
|
1294
|
+
debug.error("[Player Error] Invalid file type, cannot refresh:", fileType);
|
|
1295
|
+
} else {
|
|
1296
|
+
const refreshed = await refreshPresignedUrl(fileType);
|
|
1297
|
+
if (refreshed) {
|
|
1298
|
+
debug.log("[Player Error] URL refreshed successfully, resuming playback");
|
|
1299
|
+
setTimeout(() => {
|
|
1300
|
+
if (audioElement && currentPosition > 0) {
|
|
1301
|
+
audioElement.currentTime = currentPosition;
|
|
1302
|
+
}
|
|
1303
|
+
if (wasPlaying) {
|
|
1304
|
+
setPlaying(true);
|
|
1305
|
+
}
|
|
1306
|
+
}, PLAYBACK_RESUME_DELAY_MS);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
setHasError(true);
|
|
1312
|
+
setPlaying(false);
|
|
1313
|
+
setIsLoadingVideo(false);
|
|
1314
|
+
setErrorMessage(getErrorMessage(error));
|
|
1315
|
+
if (onError && error instanceof Error) {
|
|
1316
|
+
onError(error);
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
const handleRetry = useCallback(() => {
|
|
1320
|
+
setHasError(false);
|
|
1321
|
+
setErrorMessage("");
|
|
1322
|
+
setIsLoadingVideo(true);
|
|
1323
|
+
const info = getPlaybackInfo();
|
|
1324
|
+
if (info) {
|
|
1325
|
+
setCurrentPlaybackInfo(null);
|
|
1326
|
+
setTimeout(() => {
|
|
1327
|
+
setCurrentPlaybackInfo(info);
|
|
1328
|
+
setPlaying(true);
|
|
1329
|
+
}, 100);
|
|
1330
|
+
}
|
|
1331
|
+
}, [broadcast]);
|
|
1332
|
+
const handleWordClick = (startTime) => {
|
|
1333
|
+
if (audioElement) {
|
|
1334
|
+
audioElement.currentTime = startTime;
|
|
1335
|
+
setPlayed(startTime / duration);
|
|
1336
|
+
setHasEnded(false);
|
|
1337
|
+
if (!playing) {
|
|
1338
|
+
setPlaying(true);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
useEffect(() => {
|
|
1343
|
+
if (!enableKeyboardShortcuts) return;
|
|
1344
|
+
const seekBy = (seconds) => {
|
|
1345
|
+
if (!audioElement || duration <= 0) return;
|
|
1346
|
+
const newTime = Math.max(0, Math.min(duration, audioElement.currentTime + seconds));
|
|
1347
|
+
audioElement.currentTime = newTime;
|
|
1348
|
+
setPlayed(newTime / duration);
|
|
1349
|
+
};
|
|
1350
|
+
const handleKeyDown = (e) => {
|
|
1351
|
+
const target = e.target;
|
|
1352
|
+
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target.contentEditable === "true" || target.getAttribute("role") === "textbox") {
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
if (e.ctrlKey || e.metaKey || e.altKey) {
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
switch (e.key) {
|
|
1359
|
+
case " ":
|
|
1360
|
+
case "k":
|
|
1361
|
+
case "K":
|
|
1362
|
+
e.preventDefault();
|
|
1363
|
+
handlePlayPause();
|
|
1364
|
+
break;
|
|
1365
|
+
case "ArrowLeft":
|
|
1366
|
+
e.preventDefault();
|
|
1367
|
+
seekBy(-5);
|
|
1368
|
+
break;
|
|
1369
|
+
case "ArrowRight":
|
|
1370
|
+
e.preventDefault();
|
|
1371
|
+
seekBy(5);
|
|
1372
|
+
break;
|
|
1373
|
+
case "ArrowUp":
|
|
1374
|
+
e.preventDefault();
|
|
1375
|
+
setVolume((prev) => Math.min(1, prev + 0.1));
|
|
1376
|
+
if (muted) setMuted(false);
|
|
1377
|
+
break;
|
|
1378
|
+
case "ArrowDown":
|
|
1379
|
+
e.preventDefault();
|
|
1380
|
+
setVolume((prev) => Math.max(0, prev - 0.1));
|
|
1381
|
+
break;
|
|
1382
|
+
case "m":
|
|
1383
|
+
case "M":
|
|
1384
|
+
e.preventDefault();
|
|
1385
|
+
toggleMute();
|
|
1386
|
+
break;
|
|
1387
|
+
case "f":
|
|
1388
|
+
case "F":
|
|
1389
|
+
if (!isAudioOnly) {
|
|
1390
|
+
e.preventDefault();
|
|
1391
|
+
toggleFullscreen();
|
|
1392
|
+
}
|
|
1393
|
+
break;
|
|
1394
|
+
case "0":
|
|
1395
|
+
case "Home":
|
|
1396
|
+
e.preventDefault();
|
|
1397
|
+
if (audioElement && duration > 0) {
|
|
1398
|
+
audioElement.currentTime = 0;
|
|
1399
|
+
setPlayed(0);
|
|
1400
|
+
setHasEnded(false);
|
|
1401
|
+
}
|
|
1402
|
+
break;
|
|
1403
|
+
case "End":
|
|
1404
|
+
e.preventDefault();
|
|
1405
|
+
if (audioElement && duration > 0) {
|
|
1406
|
+
audioElement.currentTime = duration - 1;
|
|
1407
|
+
setPlayed((duration - 1) / duration);
|
|
1408
|
+
}
|
|
1409
|
+
break;
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
1413
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
1414
|
+
}, [enableKeyboardShortcuts, audioElement, duration, playing, muted, isAudioOnly, handlePlayPause, toggleMute, toggleFullscreen]);
|
|
1415
|
+
if (currentPlaybackInfo !== null && !playbackUrl) {
|
|
1416
|
+
return /* @__PURE__ */ jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-lg p-6 max-w-md w-full mx-4 border border-gray-200 dark:border-zinc-800", children: [
|
|
1417
|
+
/* @__PURE__ */ jsx("h3", { className: "text-lg font-bold text-black dark:text-white mb-4", children: "Broadcast Unavailable" }),
|
|
1418
|
+
/* @__PURE__ */ jsx("p", { className: "text-gray-600 dark:text-gray-400", children: "No playback URL available for this broadcast. The recording may still be processing." })
|
|
1419
|
+
] });
|
|
1420
|
+
}
|
|
1421
|
+
if (!playbackUrl) {
|
|
1422
|
+
return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center p-8", children: /* @__PURE__ */ jsx(LoadingSpinner, { variant: "white", text: "Loading..." }) });
|
|
1423
|
+
}
|
|
1424
|
+
const hasTranscript = broadcast.transcriptStatus === 2 && transcriptData && (transcriptData.segments && transcriptData.segments.some((s) => s.words && s.words.length > 0) || transcriptData.words && transcriptData.words.length > 0);
|
|
1425
|
+
return /* @__PURE__ */ jsxs("div", { className: `bg-black rounded-lg shadow-2xl w-full h-full flex flex-col ${className}`, children: [
|
|
1426
|
+
/* @__PURE__ */ jsxs("div", { className: "bg-zinc-900/50 backdrop-blur-sm border-b border-zinc-800 px-4 md:px-6 py-3 md:py-4 flex justify-between items-center rounded-t-lg shrink-0", children: [
|
|
1427
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1428
|
+
/* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-white", children: broadcast.streamKeyRecord?.foreignName || "Broadcast" }),
|
|
1429
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-400", children: [
|
|
1430
|
+
broadcast.isVideo ? "Video" : "Audio",
|
|
1431
|
+
" \u2022",
|
|
1432
|
+
" ",
|
|
1433
|
+
broadcast.broadcastStatus === 1 ? /* @__PURE__ */ jsx("span", { className: "text-red-500 font-semibold", children: "\u{1F534} LIVE" }) : playbackType === "hls" ? /* @__PURE__ */ jsx("span", { className: "text-gray-500 font-semibold", children: "OFFLINE" }) : formatTime(duration)
|
|
1434
|
+
] })
|
|
1435
|
+
] }),
|
|
1436
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-3", children: renderClipCreator && playbackType !== "hls" && appId && contentId && duration > 0 && /* @__PURE__ */ jsxs(
|
|
1437
|
+
"button",
|
|
1438
|
+
{
|
|
1439
|
+
onClick: () => setShowClipCreator(true),
|
|
1440
|
+
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",
|
|
1441
|
+
title: "Create a clip from this broadcast",
|
|
1442
|
+
"aria-label": "Create clip from broadcast",
|
|
1443
|
+
children: [
|
|
1444
|
+
/* @__PURE__ */ jsxs("svg", { className: "w-3 h-3 md:w-4 md:h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: [
|
|
1445
|
+
/* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" }),
|
|
1446
|
+
/* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 12a9 9 0 11-18 0 9 9 0 0118 0z" })
|
|
1447
|
+
] }),
|
|
1448
|
+
/* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: "Create Clip" }),
|
|
1449
|
+
/* @__PURE__ */ jsx("span", { className: "sm:hidden", children: "Clip" })
|
|
1450
|
+
]
|
|
1451
|
+
}
|
|
1452
|
+
) })
|
|
1453
|
+
] }),
|
|
1454
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-1 overflow-hidden", children: [
|
|
1455
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", children: [
|
|
1456
|
+
/* @__PURE__ */ jsxs("div", { className: `relative ${isAudioOnly ? "bg-linear-to-br from-zinc-900 via-zinc-800 to-zinc-900 flex items-stretch" : "bg-black"}`, children: [
|
|
1457
|
+
isAudioOnly ? /* @__PURE__ */ jsx("div", { className: "relative cursor-pointer w-full flex flex-col", onClick: handleVideoClick, children: !hasError ? /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", { className: "w-full h-full relative", children: [
|
|
1458
|
+
/* @__PURE__ */ jsx(AudioWaveform, { audioElement, isPlaying: isLiveStream ? true : playing, isLive: isLiveStream }),
|
|
1459
|
+
isLoadingVideo && !hasError && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/90 flex items-center justify-center z-20", children: /* @__PURE__ */ jsx(LoadingSpinner, { variant: "white" }) }),
|
|
1460
|
+
hasEnded && !wasLiveStream && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/50 flex items-center justify-center z-20 pointer-events-auto", children: /* @__PURE__ */ jsxs(
|
|
1461
|
+
"button",
|
|
1462
|
+
{
|
|
1463
|
+
onClick: (e) => {
|
|
1464
|
+
e.stopPropagation();
|
|
1465
|
+
handleRestart();
|
|
1466
|
+
},
|
|
1467
|
+
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",
|
|
1468
|
+
"aria-label": "Restart playback from beginning",
|
|
1469
|
+
children: [
|
|
1470
|
+
/* @__PURE__ */ jsx("svg", { className: "w-6 h-6", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z", clipRule: "evenodd" }) }),
|
|
1471
|
+
"Restart"
|
|
1472
|
+
]
|
|
1473
|
+
}
|
|
1474
|
+
) })
|
|
1475
|
+
] }) }) : /* @__PURE__ */ jsx("div", { className: "w-full h-full flex items-center justify-center", children: /* @__PURE__ */ jsxs("div", { className: "text-center max-w-md px-4", children: [
|
|
1476
|
+
/* @__PURE__ */ jsx("div", { className: "text-6xl mb-4", children: "\u26A0\uFE0F" }),
|
|
1477
|
+
/* @__PURE__ */ jsx("h3", { className: "text-xl font-semibold text-white mb-3", children: "Playback Error" }),
|
|
1478
|
+
/* @__PURE__ */ jsx("p", { className: "text-gray-300 text-sm mb-6", children: errorMessage }),
|
|
1479
|
+
/* @__PURE__ */ jsxs(
|
|
1480
|
+
"button",
|
|
1481
|
+
{
|
|
1482
|
+
onClick: handleRetry,
|
|
1483
|
+
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",
|
|
1484
|
+
"aria-label": "Retry playback",
|
|
1485
|
+
children: [
|
|
1486
|
+
/* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" }) }),
|
|
1487
|
+
"Retry"
|
|
1488
|
+
]
|
|
1489
|
+
}
|
|
1490
|
+
)
|
|
1491
|
+
] }) }) }) : /* @__PURE__ */ jsxs("div", { className: "aspect-video relative", children: [
|
|
1492
|
+
/* @__PURE__ */ jsx("div", { onClick: handleVideoClick, className: "cursor-pointer", children: /* @__PURE__ */ jsx(
|
|
1493
|
+
ReactPlayer,
|
|
1494
|
+
{
|
|
1495
|
+
ref: playerRef,
|
|
1496
|
+
src: playbackUrl || void 0,
|
|
1497
|
+
playing,
|
|
1498
|
+
volume,
|
|
1499
|
+
muted,
|
|
1500
|
+
width: "100%",
|
|
1501
|
+
height: "100%",
|
|
1502
|
+
crossOrigin: "anonymous",
|
|
1503
|
+
onReady: handlePlayerReady,
|
|
1504
|
+
onTimeUpdate: handleTimeUpdate,
|
|
1505
|
+
onLoadedMetadata: handleLoadedMetadata,
|
|
1506
|
+
onPlay: handlePlay,
|
|
1507
|
+
onPause: handlePause,
|
|
1508
|
+
onEnded: handleEnded,
|
|
1509
|
+
onError: handleError,
|
|
1510
|
+
style: { backgroundColor: "#000" }
|
|
1511
|
+
},
|
|
1512
|
+
playbackUrl || "no-url"
|
|
1513
|
+
) }),
|
|
1514
|
+
isLoadingVideo && !hasError && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/90 flex items-center justify-center", children: /* @__PURE__ */ jsx(LoadingSpinner, { variant: "white" }) }),
|
|
1515
|
+
hasEnded && !hasError && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/50 flex items-center justify-center", children: /* @__PURE__ */ jsxs(
|
|
1516
|
+
"button",
|
|
1517
|
+
{
|
|
1518
|
+
onClick: (e) => {
|
|
1519
|
+
e.stopPropagation();
|
|
1520
|
+
handleRestart();
|
|
1521
|
+
},
|
|
1522
|
+
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",
|
|
1523
|
+
children: [
|
|
1524
|
+
/* @__PURE__ */ jsx("svg", { className: "w-6 h-6", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z", clipRule: "evenodd" }) }),
|
|
1525
|
+
"Restart"
|
|
1526
|
+
]
|
|
1527
|
+
}
|
|
1528
|
+
) }),
|
|
1529
|
+
hasError && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/90 flex items-center justify-center p-8", children: /* @__PURE__ */ jsxs("div", { className: "text-center max-w-md", children: [
|
|
1530
|
+
/* @__PURE__ */ jsx("div", { className: "text-6xl mb-4", children: "\u26A0\uFE0F" }),
|
|
1531
|
+
/* @__PURE__ */ jsx("h3", { className: "text-xl font-semibold text-white mb-3", children: "Playback Error" }),
|
|
1532
|
+
/* @__PURE__ */ jsx("p", { className: "text-gray-300 text-sm mb-6", children: errorMessage }),
|
|
1533
|
+
/* @__PURE__ */ jsxs(
|
|
1534
|
+
"button",
|
|
1535
|
+
{
|
|
1536
|
+
onClick: handleRetry,
|
|
1537
|
+
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",
|
|
1538
|
+
"aria-label": "Retry playback",
|
|
1539
|
+
children: [
|
|
1540
|
+
/* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" }) }),
|
|
1541
|
+
"Retry"
|
|
1542
|
+
]
|
|
1543
|
+
}
|
|
1544
|
+
)
|
|
1545
|
+
] }) })
|
|
1546
|
+
] }),
|
|
1547
|
+
isAudioOnly && /* @__PURE__ */ jsx("div", { className: "hidden", children: /* @__PURE__ */ jsx(
|
|
1548
|
+
ReactPlayer,
|
|
1549
|
+
{
|
|
1550
|
+
ref: playerRef,
|
|
1551
|
+
src: playbackUrl || void 0,
|
|
1552
|
+
playing,
|
|
1553
|
+
volume,
|
|
1554
|
+
muted,
|
|
1555
|
+
width: "0",
|
|
1556
|
+
height: "0",
|
|
1557
|
+
crossOrigin: "anonymous",
|
|
1558
|
+
onReady: handlePlayerReady,
|
|
1559
|
+
onTimeUpdate: handleTimeUpdate,
|
|
1560
|
+
onLoadedMetadata: handleLoadedMetadata,
|
|
1561
|
+
onPlay: handlePlay,
|
|
1562
|
+
onPause: handlePause,
|
|
1563
|
+
onEnded: handleEnded,
|
|
1564
|
+
onError: handleError
|
|
1565
|
+
},
|
|
1566
|
+
playbackUrl || "no-url"
|
|
1567
|
+
) })
|
|
1568
|
+
] }),
|
|
1569
|
+
!hasError && !isLiveStream && (wasLiveStream ? parseInt(broadcast.mp3Size || "0") > 0 || parseInt(broadcast.mp4Size || "0") > 0 ? /* @__PURE__ */ jsxs("div", { className: "bg-zinc-900/50 backdrop-blur-sm px-4 md:px-6 py-3 md:py-4 rounded-b-lg", children: [
|
|
1570
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
|
|
1571
|
+
/* @__PURE__ */ jsx(
|
|
1572
|
+
"input",
|
|
1573
|
+
{
|
|
1574
|
+
type: "range",
|
|
1575
|
+
min: 0,
|
|
1576
|
+
max: 0.999999,
|
|
1577
|
+
step: "any",
|
|
1578
|
+
value: played || 0,
|
|
1579
|
+
onMouseDown: handleSeekMouseDown,
|
|
1580
|
+
onMouseUp: handleSeekMouseUp,
|
|
1581
|
+
onTouchStart: handleSeekTouchStart,
|
|
1582
|
+
onTouchEnd: handleSeekTouchEnd,
|
|
1583
|
+
onChange: handleSeekChange,
|
|
1584
|
+
className: "w-full h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer slider",
|
|
1585
|
+
"aria-label": "Seek position",
|
|
1586
|
+
"aria-valuemin": 0,
|
|
1587
|
+
"aria-valuemax": duration,
|
|
1588
|
+
"aria-valuenow": played * duration,
|
|
1589
|
+
"aria-valuetext": `${formatTime(played * duration)} of ${formatTime(duration)}`,
|
|
1590
|
+
role: "slider"
|
|
1591
|
+
}
|
|
1592
|
+
),
|
|
1593
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-between text-xs text-gray-400 mt-1", children: [
|
|
1594
|
+
/* @__PURE__ */ jsx("span", { children: formatTime((played || 0) * duration) }),
|
|
1595
|
+
/* @__PURE__ */ jsx("span", { children: formatTime(duration) })
|
|
1596
|
+
] })
|
|
1597
|
+
] }),
|
|
1598
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1599
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
|
|
1600
|
+
/* @__PURE__ */ jsx(
|
|
1601
|
+
"button",
|
|
1602
|
+
{
|
|
1603
|
+
onClick: handlePlayPause,
|
|
1604
|
+
className: "text-white hover:text-blue-400 transition-colors",
|
|
1605
|
+
title: playing ? "Pause" : "Play",
|
|
1606
|
+
"aria-label": playing ? "Pause" : "Play",
|
|
1607
|
+
"aria-pressed": playing,
|
|
1608
|
+
children: playing ? /* @__PURE__ */ jsx("svg", { className: "w-8 h-8", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z", clipRule: "evenodd" }) }) : /* @__PURE__ */ jsx("svg", { className: "w-8 h-8", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z", clipRule: "evenodd" }) })
|
|
1609
|
+
}
|
|
1610
|
+
),
|
|
1611
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1612
|
+
/* @__PURE__ */ jsx(
|
|
1613
|
+
"button",
|
|
1614
|
+
{
|
|
1615
|
+
onClick: toggleMute,
|
|
1616
|
+
className: "text-white hover:text-blue-400 transition-colors",
|
|
1617
|
+
title: muted ? "Unmute" : "Mute",
|
|
1618
|
+
"aria-label": muted ? "Unmute" : "Mute",
|
|
1619
|
+
"aria-pressed": muted,
|
|
1620
|
+
children: muted || volume === 0 ? /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM12.293 7.293a1 1 0 011.414 0L15 8.586l1.293-1.293a1 1 0 111.414 1.414L16.414 10l1.293 1.293a1 1 0 01-1.414 1.414L15 11.414l-1.293 1.293a1 1 0 01-1.414-1.414L13.586 10l-1.293-1.293a1 1 0 010-1.414z", clipRule: "evenodd" }) }) : /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z", clipRule: "evenodd" }) })
|
|
1621
|
+
}
|
|
1622
|
+
),
|
|
1623
|
+
/* @__PURE__ */ jsx(
|
|
1624
|
+
"input",
|
|
1625
|
+
{
|
|
1626
|
+
type: "range",
|
|
1627
|
+
min: 0,
|
|
1628
|
+
max: 1,
|
|
1629
|
+
step: 0.01,
|
|
1630
|
+
value: muted ? 0 : volume || 1,
|
|
1631
|
+
onChange: handleVolumeChange,
|
|
1632
|
+
className: "w-20 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer slider",
|
|
1633
|
+
"aria-label": "Volume",
|
|
1634
|
+
"aria-valuemin": 0,
|
|
1635
|
+
"aria-valuemax": 100,
|
|
1636
|
+
"aria-valuenow": muted ? 0 : Math.round(volume * 100),
|
|
1637
|
+
"aria-valuetext": muted ? "Muted" : `${Math.round(volume * 100)}%`,
|
|
1638
|
+
role: "slider"
|
|
1639
|
+
}
|
|
1640
|
+
)
|
|
1641
|
+
] })
|
|
1642
|
+
] }),
|
|
1643
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1644
|
+
!isLiveStream && broadcast.hash && (broadcast.recordingMp4Url || broadcast.recordingMp3Url) && /* @__PURE__ */ jsx(
|
|
1645
|
+
"button",
|
|
1646
|
+
{
|
|
1647
|
+
onClick: () => {
|
|
1648
|
+
const downloadUrl = buildPlaybackUrl(broadcast.id, broadcast.hash, "download");
|
|
1649
|
+
window.open(downloadUrl, "_blank");
|
|
1650
|
+
},
|
|
1651
|
+
className: "text-white hover:text-blue-400 transition-colors",
|
|
1652
|
+
title: "Download Recording",
|
|
1653
|
+
"aria-label": "Download recording",
|
|
1654
|
+
children: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" }) })
|
|
1655
|
+
}
|
|
1656
|
+
),
|
|
1657
|
+
!isAudioOnly && /* @__PURE__ */ jsx(
|
|
1658
|
+
"button",
|
|
1659
|
+
{
|
|
1660
|
+
onClick: toggleFullscreen,
|
|
1661
|
+
className: "text-white hover:text-blue-400 transition-colors",
|
|
1662
|
+
title: "Fullscreen",
|
|
1663
|
+
"aria-label": "Toggle fullscreen",
|
|
1664
|
+
children: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 11-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 111.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 111.414-1.414L15 13.586V12a1 1 0 011-1z", clipRule: "evenodd" }) })
|
|
1665
|
+
}
|
|
1666
|
+
),
|
|
1667
|
+
hasTranscript && /* @__PURE__ */ jsx(
|
|
1668
|
+
"button",
|
|
1669
|
+
{
|
|
1670
|
+
onClick: () => setShowTranscript(!showTranscript),
|
|
1671
|
+
className: `transition-colors ${showTranscript ? "text-blue-400" : "text-white hover:text-blue-400"}`,
|
|
1672
|
+
title: showTranscript ? "Hide Transcript" : "Show Transcript",
|
|
1673
|
+
"aria-label": showTranscript ? "Hide transcript" : "Show transcript",
|
|
1674
|
+
"aria-pressed": showTranscript,
|
|
1675
|
+
children: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" }) })
|
|
1676
|
+
}
|
|
1677
|
+
)
|
|
1678
|
+
] })
|
|
1679
|
+
] })
|
|
1680
|
+
] }) : null : /* @__PURE__ */ jsxs("div", { className: "bg-zinc-900/50 backdrop-blur-sm px-4 md:px-6 py-3 md:py-4 rounded-b-lg", children: [
|
|
1681
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
|
|
1682
|
+
/* @__PURE__ */ jsx(
|
|
1683
|
+
"input",
|
|
1684
|
+
{
|
|
1685
|
+
type: "range",
|
|
1686
|
+
min: 0,
|
|
1687
|
+
max: 0.999999,
|
|
1688
|
+
step: "any",
|
|
1689
|
+
value: played || 0,
|
|
1690
|
+
onMouseDown: handleSeekMouseDown,
|
|
1691
|
+
onMouseUp: handleSeekMouseUp,
|
|
1692
|
+
onTouchStart: handleSeekTouchStart,
|
|
1693
|
+
onTouchEnd: handleSeekTouchEnd,
|
|
1694
|
+
onChange: handleSeekChange,
|
|
1695
|
+
className: "w-full h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer slider"
|
|
1696
|
+
}
|
|
1697
|
+
),
|
|
1698
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-between text-xs text-gray-400 mt-1", children: [
|
|
1699
|
+
/* @__PURE__ */ jsx("span", { children: formatTime((played || 0) * duration) }),
|
|
1700
|
+
/* @__PURE__ */ jsx("span", { children: formatTime(duration) })
|
|
1701
|
+
] })
|
|
1702
|
+
] }),
|
|
1703
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1704
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-4", children: [
|
|
1705
|
+
/* @__PURE__ */ jsx("button", { onClick: handlePlayPause, className: "text-white hover:text-blue-400 transition-colors", title: playing ? "Pause" : "Play", children: playing ? /* @__PURE__ */ jsx("svg", { className: "w-8 h-8", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z", clipRule: "evenodd" }) }) : /* @__PURE__ */ jsx("svg", { className: "w-8 h-8", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z", clipRule: "evenodd" }) }) }),
|
|
1706
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1707
|
+
/* @__PURE__ */ jsx("button", { onClick: toggleMute, className: "text-white hover:text-blue-400 transition-colors", title: muted ? "Unmute" : "Mute", children: muted || volume === 0 ? /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM12.293 7.293a1 1 0 011.414 0L15 8.586l1.293-1.293a1 1 0 111.414 1.414L16.414 10l1.293 1.293a1 1 0 01-1.414 1.414L15 11.414l-1.293 1.293a1 1 0 01-1.414-1.414L13.586 10l-1.293-1.293a1 1 0 010-1.414z", clipRule: "evenodd" }) }) : /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z", clipRule: "evenodd" }) }) }),
|
|
1708
|
+
/* @__PURE__ */ jsx(
|
|
1709
|
+
"input",
|
|
1710
|
+
{
|
|
1711
|
+
type: "range",
|
|
1712
|
+
min: 0,
|
|
1713
|
+
max: 1,
|
|
1714
|
+
step: 0.01,
|
|
1715
|
+
value: muted ? 0 : volume || 1,
|
|
1716
|
+
onChange: handleVolumeChange,
|
|
1717
|
+
className: "w-20 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer slider"
|
|
1718
|
+
}
|
|
1719
|
+
)
|
|
1720
|
+
] })
|
|
1721
|
+
] }),
|
|
1722
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1723
|
+
!isAudioOnly && /* @__PURE__ */ jsx("button", { onClick: toggleFullscreen, className: "text-white hover:text-blue-400 transition-colors", title: "Fullscreen", children: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 11-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 111.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 111.414-1.414L15 13.586V12a1 1 0 011-1z", clipRule: "evenodd" }) }) }),
|
|
1724
|
+
hasTranscript && /* @__PURE__ */ jsx(
|
|
1725
|
+
"button",
|
|
1726
|
+
{
|
|
1727
|
+
onClick: () => setShowTranscript(!showTranscript),
|
|
1728
|
+
className: `transition-colors ${showTranscript ? "text-blue-400" : "text-white hover:text-blue-400"}`,
|
|
1729
|
+
title: showTranscript ? "Hide Transcript" : "Show Transcript",
|
|
1730
|
+
children: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" }) })
|
|
1731
|
+
}
|
|
1732
|
+
)
|
|
1733
|
+
] })
|
|
1734
|
+
] })
|
|
1735
|
+
] }))
|
|
1736
|
+
] }),
|
|
1737
|
+
showTranscript && hasTranscript && /* @__PURE__ */ jsxs("div", { className: "w-full md:w-96 bg-zinc-900 border-l border-zinc-800 flex flex-col overflow-hidden", children: [
|
|
1738
|
+
/* @__PURE__ */ jsx("div", { className: "px-4 py-3 border-b border-zinc-800 bg-zinc-900/50 shrink-0", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1739
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1740
|
+
/* @__PURE__ */ jsx("svg", { className: "w-5 h-5 text-green-400", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" }) }),
|
|
1741
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-white", children: "Transcript" }),
|
|
1742
|
+
broadcast.transcriptLanguage && /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 px-2 py-0.5 bg-zinc-800 rounded", children: broadcast.transcriptLanguage.toUpperCase() })
|
|
1743
|
+
] }),
|
|
1744
|
+
broadcast.transcriptUrl && /* @__PURE__ */ jsx(
|
|
1745
|
+
"a",
|
|
1746
|
+
{
|
|
1747
|
+
href: broadcast.transcriptUrl,
|
|
1748
|
+
download: `${broadcast.hash || broadcast.id}-transcript.json`,
|
|
1749
|
+
className: "text-gray-400 hover:text-white transition-colors",
|
|
1750
|
+
title: "Download Transcript",
|
|
1751
|
+
children: /* @__PURE__ */ jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" }) })
|
|
1752
|
+
}
|
|
1753
|
+
)
|
|
1754
|
+
] }) }),
|
|
1755
|
+
!autoScrollEnabled && /* @__PURE__ */ jsx("div", { className: "px-4 py-2 bg-zinc-800/50 border-b border-zinc-700 shrink-0", children: /* @__PURE__ */ jsxs(
|
|
1756
|
+
"button",
|
|
1757
|
+
{
|
|
1758
|
+
onClick: () => setAutoScrollEnabled(true),
|
|
1759
|
+
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",
|
|
1760
|
+
children: [
|
|
1761
|
+
/* @__PURE__ */ jsx("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 14l-7 7m0 0l-7-7m7 7V3" }) }),
|
|
1762
|
+
"Resume Auto-Scroll"
|
|
1763
|
+
]
|
|
1764
|
+
}
|
|
1765
|
+
) }),
|
|
1766
|
+
/* @__PURE__ */ jsx("div", { ref: transcriptContainerRef, className: "flex-1 overflow-y-auto px-4 py-4 text-gray-300 leading-relaxed", children: isLoadingTranscript ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-8", children: /* @__PURE__ */ jsx("div", { className: "h-6 w-6 border-2 border-gray-600 border-t-blue-500 rounded-full animate-spin" }) }) : transcriptData?.segments && transcriptData.segments.length > 0 ? /* @__PURE__ */ jsx("div", { className: "space-y-4", children: (() => {
|
|
1767
|
+
const filteredSegments = transcriptData.segments.filter((s) => s.words && s.words.length > 0);
|
|
1768
|
+
let globalWordIndex = 0;
|
|
1769
|
+
const wordMap = /* @__PURE__ */ new Map();
|
|
1770
|
+
filteredSegments.forEach((segment) => {
|
|
1771
|
+
segment.words.forEach((_word, wordIndex) => {
|
|
1772
|
+
wordMap.set(`${segment.id}-${wordIndex}`, globalWordIndex++);
|
|
1773
|
+
});
|
|
1774
|
+
});
|
|
1775
|
+
let currentWordIndex = -1;
|
|
1776
|
+
filteredSegments.forEach((segment) => {
|
|
1777
|
+
segment.words.forEach((word, wordIndex) => {
|
|
1778
|
+
const globalIdx = wordMap.get(`${segment.id}-${wordIndex}`) || -1;
|
|
1779
|
+
if (currentTime >= word.start && globalIdx > currentWordIndex) {
|
|
1780
|
+
currentWordIndex = globalIdx;
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
});
|
|
1784
|
+
const previousWordIndex = lastActiveWordIndex.current;
|
|
1785
|
+
let minHighlightIndex = -1;
|
|
1786
|
+
let maxHighlightIndex = -1;
|
|
1787
|
+
if (currentWordIndex >= 0) {
|
|
1788
|
+
minHighlightIndex = Math.max(0, currentWordIndex - TRAILING_WORDS);
|
|
1789
|
+
maxHighlightIndex = currentWordIndex;
|
|
1790
|
+
if (currentWordIndex <= TRAILING_WORDS) {
|
|
1791
|
+
minHighlightIndex = 0;
|
|
1792
|
+
}
|
|
1793
|
+
lastActiveWordIndex.current = currentWordIndex;
|
|
1794
|
+
} else if (currentWordIndex === -1) {
|
|
1795
|
+
minHighlightIndex = 0;
|
|
1796
|
+
maxHighlightIndex = 0;
|
|
1797
|
+
} else if (previousWordIndex >= 0) {
|
|
1798
|
+
minHighlightIndex = Math.max(0, previousWordIndex - TRAILING_WORDS);
|
|
1799
|
+
maxHighlightIndex = previousWordIndex;
|
|
1800
|
+
}
|
|
1801
|
+
return filteredSegments.map((segment, _segmentIndex) => {
|
|
1802
|
+
const isSegmentActive = currentTime >= segment.start && currentTime < segment.end;
|
|
1803
|
+
return /* @__PURE__ */ jsxs("div", { ref: isSegmentActive ? transcriptContainerRef : null, className: "flex gap-3 items-start leading-relaxed", children: [
|
|
1804
|
+
/* @__PURE__ */ jsx(
|
|
1805
|
+
"button",
|
|
1806
|
+
{
|
|
1807
|
+
onClick: () => handleWordClick(segment.start),
|
|
1808
|
+
className: "text-xs text-gray-500 hover:text-gray-300 transition-colors shrink-0 pt-0.5 font-mono",
|
|
1809
|
+
title: `Jump to ${formatTimestamp(segment.start)}`,
|
|
1810
|
+
children: formatTimestamp(segment.start)
|
|
1811
|
+
}
|
|
1812
|
+
),
|
|
1813
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1", children: segment.words.map((word, wordIndex) => {
|
|
1814
|
+
const thisGlobalIndex = wordMap.get(`${segment.id}-${wordIndex}`) ?? -1;
|
|
1815
|
+
const isTimestampActive = currentTime >= word.start && currentTime < word.end;
|
|
1816
|
+
const isInGapFill = minHighlightIndex >= 0 && thisGlobalIndex >= minHighlightIndex && thisGlobalIndex <= maxHighlightIndex;
|
|
1817
|
+
const isWordActive = isInGapFill;
|
|
1818
|
+
return /* @__PURE__ */ jsxs(
|
|
1819
|
+
"span",
|
|
1820
|
+
{
|
|
1821
|
+
ref: isTimestampActive ? activeWordRef : null,
|
|
1822
|
+
onClick: () => handleWordClick(word.start),
|
|
1823
|
+
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"}`,
|
|
1824
|
+
title: `${formatTime(word.start)} - ${formatTime(word.end)}`,
|
|
1825
|
+
children: [
|
|
1826
|
+
word.word,
|
|
1827
|
+
" "
|
|
1828
|
+
]
|
|
1829
|
+
},
|
|
1830
|
+
`${segment.id}-${wordIndex}`
|
|
1831
|
+
);
|
|
1832
|
+
}) })
|
|
1833
|
+
] }, segment.id);
|
|
1834
|
+
});
|
|
1835
|
+
})() }) : transcriptData?.words && transcriptData.words.length > 0 ? /* @__PURE__ */ jsx("div", { className: "space-y-1", children: transcriptData.words.map((word, index) => {
|
|
1836
|
+
const isActive = currentTime >= word.start && currentTime < word.end;
|
|
1837
|
+
return /* @__PURE__ */ jsxs(
|
|
1838
|
+
"span",
|
|
1839
|
+
{
|
|
1840
|
+
ref: isActive ? activeWordRef : null,
|
|
1841
|
+
onClick: () => handleWordClick(word.start),
|
|
1842
|
+
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"}`,
|
|
1843
|
+
title: `${formatTime(word.start)} - ${formatTime(word.end)}`,
|
|
1844
|
+
children: [
|
|
1845
|
+
word.word,
|
|
1846
|
+
" "
|
|
1847
|
+
]
|
|
1848
|
+
},
|
|
1849
|
+
index
|
|
1850
|
+
);
|
|
1851
|
+
}) }) : /* @__PURE__ */ jsxs("div", { className: "text-center text-gray-500 py-8", children: [
|
|
1852
|
+
/* @__PURE__ */ jsx("p", { className: "mb-2", children: "Transcript data not available" }),
|
|
1853
|
+
transcriptData && /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-600", children: [
|
|
1854
|
+
"Debug: segments=",
|
|
1855
|
+
transcriptData.segments ? transcriptData.segments.length : 0,
|
|
1856
|
+
", words=",
|
|
1857
|
+
transcriptData.words ? transcriptData.words.length : 0
|
|
1858
|
+
] })
|
|
1859
|
+
] }) })
|
|
1860
|
+
] })
|
|
1861
|
+
] }),
|
|
1862
|
+
renderClipCreator && renderClipCreator({
|
|
1863
|
+
isOpen: showClipCreator,
|
|
1864
|
+
onClose: () => setShowClipCreator(false),
|
|
1865
|
+
sourceVideoUrl: playbackType === "mp4" ? playbackUrl || void 0 : void 0,
|
|
1866
|
+
sourceAudioUrl: playbackType === "mp3" ? playbackUrl || void 0 : void 0,
|
|
1867
|
+
sourceDuration: duration,
|
|
1868
|
+
onPauseParent: () => setPlaying(false)
|
|
1869
|
+
}),
|
|
1870
|
+
/* @__PURE__ */ jsx("style", { children: `
|
|
1871
|
+
.slider::-webkit-slider-thumb {
|
|
1872
|
+
appearance: none;
|
|
1873
|
+
width: 14px;
|
|
1874
|
+
height: 14px;
|
|
1875
|
+
border-radius: 50%;
|
|
1876
|
+
background: white;
|
|
1877
|
+
cursor: pointer;
|
|
1878
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
|
1879
|
+
}
|
|
1880
|
+
.slider::-webkit-slider-thumb:hover {
|
|
1881
|
+
background: #60a5fa;
|
|
1882
|
+
}
|
|
1883
|
+
.slider::-moz-range-thumb {
|
|
1884
|
+
width: 14px;
|
|
1885
|
+
height: 14px;
|
|
1886
|
+
border-radius: 50%;
|
|
1887
|
+
background: white;
|
|
1888
|
+
cursor: pointer;
|
|
1889
|
+
border: none;
|
|
1890
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
|
1891
|
+
}
|
|
1892
|
+
.slider::-moz-range-thumb:hover {
|
|
1893
|
+
background: #60a5fa;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
.active-word {
|
|
1897
|
+
transition: color 0s, text-shadow 0s;
|
|
1898
|
+
text-shadow: 0 0 8px rgba(96, 165, 250, 0.6), 0 0 12px rgba(96, 165, 250, 0.4);
|
|
1899
|
+
}
|
|
1900
|
+
.segment-word,
|
|
1901
|
+
.inactive-word {
|
|
1902
|
+
transition: color 0.4s ease-out, text-shadow 0.4s ease-out;
|
|
1903
|
+
text-shadow: none;
|
|
1904
|
+
}
|
|
1905
|
+
.segment-word:hover,
|
|
1906
|
+
.inactive-word:hover {
|
|
1907
|
+
transition: color 0.15s ease-in;
|
|
1908
|
+
}
|
|
1909
|
+
` })
|
|
1910
|
+
] });
|
|
1911
|
+
}
|
|
1912
|
+
function BroadcastPlayerModal({
|
|
1913
|
+
broadcast,
|
|
1914
|
+
isOpen,
|
|
1915
|
+
onClose,
|
|
1916
|
+
appId,
|
|
1917
|
+
contentId,
|
|
1918
|
+
foreignId,
|
|
1919
|
+
foreignTier,
|
|
1920
|
+
renderClipCreator,
|
|
1921
|
+
className,
|
|
1922
|
+
enableKeyboardShortcuts = false
|
|
1923
|
+
}) {
|
|
1924
|
+
const closeButtonRef = useRef(null);
|
|
1925
|
+
const previousActiveElement = useRef(null);
|
|
1926
|
+
if (!isOpen) return null;
|
|
1927
|
+
useEffect(() => {
|
|
1928
|
+
if (isOpen) {
|
|
1929
|
+
previousActiveElement.current = document.activeElement;
|
|
1930
|
+
setTimeout(() => {
|
|
1931
|
+
closeButtonRef.current?.focus();
|
|
1932
|
+
}, 100);
|
|
1933
|
+
}
|
|
1934
|
+
return () => {
|
|
1935
|
+
if (previousActiveElement.current) {
|
|
1936
|
+
previousActiveElement.current.focus();
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
}, [isOpen]);
|
|
1940
|
+
useEffect(() => {
|
|
1941
|
+
const handleKeyDown = (e) => {
|
|
1942
|
+
if (e.key === "Escape") {
|
|
1943
|
+
onClose();
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
1947
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
1948
|
+
}, [onClose]);
|
|
1949
|
+
const handleBackdropClick = (e) => {
|
|
1950
|
+
if (e.target === e.currentTarget) {
|
|
1951
|
+
onClose();
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
return /* @__PURE__ */ jsx(
|
|
1955
|
+
"div",
|
|
1956
|
+
{
|
|
1957
|
+
className: "fixed inset-0 bg-black/70 backdrop-blur-xl flex items-center justify-center z-50 p-4",
|
|
1958
|
+
onClick: handleBackdropClick,
|
|
1959
|
+
role: "dialog",
|
|
1960
|
+
"aria-modal": "true",
|
|
1961
|
+
"aria-label": "Broadcast player",
|
|
1962
|
+
children: /* @__PURE__ */ jsxs("div", { className: "relative w-full h-full max-w-7xl max-h-[90vh]", children: [
|
|
1963
|
+
/* @__PURE__ */ jsx(
|
|
1964
|
+
"button",
|
|
1965
|
+
{
|
|
1966
|
+
ref: closeButtonRef,
|
|
1967
|
+
onClick: onClose,
|
|
1968
|
+
className: "absolute top-4 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",
|
|
1969
|
+
title: "Close (ESC)",
|
|
1970
|
+
"aria-label": "Close player",
|
|
1971
|
+
children: "\xD7"
|
|
1972
|
+
}
|
|
1973
|
+
),
|
|
1974
|
+
/* @__PURE__ */ jsx(
|
|
1975
|
+
BroadcastPlayer,
|
|
1976
|
+
{
|
|
1977
|
+
broadcast,
|
|
1978
|
+
appId,
|
|
1979
|
+
contentId,
|
|
1980
|
+
foreignId,
|
|
1981
|
+
foreignTier,
|
|
1982
|
+
renderClipCreator,
|
|
1983
|
+
className,
|
|
1984
|
+
enableKeyboardShortcuts,
|
|
1985
|
+
onError: (error) => {
|
|
1986
|
+
debug.error("[BroadcastPlayerModal] Player error:", error);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
)
|
|
1990
|
+
] })
|
|
1991
|
+
}
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
var BroadcastPlayerErrorBoundary = class extends Component {
|
|
1995
|
+
constructor(props) {
|
|
1996
|
+
super(props);
|
|
1997
|
+
this.handleReset = () => {
|
|
1998
|
+
this.setState({
|
|
1999
|
+
hasError: false,
|
|
2000
|
+
error: null,
|
|
2001
|
+
errorInfo: null
|
|
2002
|
+
});
|
|
2003
|
+
};
|
|
2004
|
+
this.handleClose = () => {
|
|
2005
|
+
this.setState({
|
|
2006
|
+
hasError: false,
|
|
2007
|
+
error: null,
|
|
2008
|
+
errorInfo: null
|
|
2009
|
+
});
|
|
2010
|
+
this.props.onClose?.();
|
|
2011
|
+
};
|
|
2012
|
+
this.state = {
|
|
2013
|
+
hasError: false,
|
|
2014
|
+
error: null,
|
|
2015
|
+
errorInfo: null
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
static getDerivedStateFromError(error) {
|
|
2019
|
+
return { hasError: true, error };
|
|
2020
|
+
}
|
|
2021
|
+
componentDidCatch(error, errorInfo) {
|
|
2022
|
+
console.error("[Player Error Boundary] Caught error:", {
|
|
2023
|
+
error: error.message,
|
|
2024
|
+
stack: error.stack,
|
|
2025
|
+
componentStack: errorInfo.componentStack,
|
|
2026
|
+
broadcastId: this.props.broadcastId,
|
|
2027
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2028
|
+
});
|
|
2029
|
+
this.setState({
|
|
2030
|
+
error,
|
|
2031
|
+
errorInfo
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
render() {
|
|
2035
|
+
if (this.state.hasError) {
|
|
2036
|
+
return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/80", children: /* @__PURE__ */ jsxs("div", { className: "bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full mx-4 p-6", children: [
|
|
2037
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-4", children: [
|
|
2038
|
+
/* @__PURE__ */ jsx("h2", { className: "text-2xl font-bold text-gray-900 dark:text-white", children: "Player Error" }),
|
|
2039
|
+
/* @__PURE__ */ jsx(
|
|
2040
|
+
"button",
|
|
2041
|
+
{
|
|
2042
|
+
onClick: this.handleClose,
|
|
2043
|
+
className: "text-gray-400 hover:text-gray-600 dark:hover:text-gray-300",
|
|
2044
|
+
"aria-label": "Close",
|
|
2045
|
+
children: /* @__PURE__ */ jsx(
|
|
2046
|
+
"svg",
|
|
2047
|
+
{
|
|
2048
|
+
className: "w-6 h-6",
|
|
2049
|
+
fill: "none",
|
|
2050
|
+
stroke: "currentColor",
|
|
2051
|
+
viewBox: "0 0 24 24",
|
|
2052
|
+
children: /* @__PURE__ */ jsx(
|
|
2053
|
+
"path",
|
|
2054
|
+
{
|
|
2055
|
+
strokeLinecap: "round",
|
|
2056
|
+
strokeLinejoin: "round",
|
|
2057
|
+
strokeWidth: 2,
|
|
2058
|
+
d: "M6 18L18 6M6 6l12 12"
|
|
2059
|
+
}
|
|
2060
|
+
)
|
|
2061
|
+
}
|
|
2062
|
+
)
|
|
2063
|
+
}
|
|
2064
|
+
)
|
|
2065
|
+
] }),
|
|
2066
|
+
/* @__PURE__ */ jsx("div", { className: "flex justify-center mb-4", children: /* @__PURE__ */ jsx("div", { className: "rounded-full bg-red-100 dark:bg-red-900/30 p-3", children: /* @__PURE__ */ jsx(
|
|
2067
|
+
"svg",
|
|
2068
|
+
{
|
|
2069
|
+
className: "w-12 h-12 text-red-600 dark:text-red-400",
|
|
2070
|
+
fill: "none",
|
|
2071
|
+
stroke: "currentColor",
|
|
2072
|
+
viewBox: "0 0 24 24",
|
|
2073
|
+
children: /* @__PURE__ */ jsx(
|
|
2074
|
+
"path",
|
|
2075
|
+
{
|
|
2076
|
+
strokeLinecap: "round",
|
|
2077
|
+
strokeLinejoin: "round",
|
|
2078
|
+
strokeWidth: 2,
|
|
2079
|
+
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"
|
|
2080
|
+
}
|
|
2081
|
+
)
|
|
2082
|
+
}
|
|
2083
|
+
) }) }),
|
|
2084
|
+
/* @__PURE__ */ jsxs("div", { className: "text-center mb-6", children: [
|
|
2085
|
+
/* @__PURE__ */ jsx("p", { className: "text-lg text-gray-900 dark:text-white mb-2", children: "Something went wrong with the media player" }),
|
|
2086
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400", children: this.state.error?.message || "An unexpected error occurred" })
|
|
2087
|
+
] }),
|
|
2088
|
+
process.env.NODE_ENV === "development" && this.state.errorInfo && /* @__PURE__ */ jsxs("details", { className: "mb-6 p-4 bg-gray-100 dark:bg-gray-900 rounded text-xs", children: [
|
|
2089
|
+
/* @__PURE__ */ jsx("summary", { className: "cursor-pointer font-semibold text-gray-700 dark:text-gray-300 mb-2", children: "Error Details (Development Only)" }),
|
|
2090
|
+
/* @__PURE__ */ jsxs("pre", { className: "overflow-auto text-gray-600 dark:text-gray-400 whitespace-pre-wrap", children: [
|
|
2091
|
+
this.state.error?.stack,
|
|
2092
|
+
"\n\nComponent Stack:",
|
|
2093
|
+
this.state.errorInfo.componentStack
|
|
2094
|
+
] })
|
|
2095
|
+
] }),
|
|
2096
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-3 justify-center", children: [
|
|
2097
|
+
/* @__PURE__ */ jsx(
|
|
2098
|
+
"button",
|
|
2099
|
+
{
|
|
2100
|
+
onClick: this.handleReset,
|
|
2101
|
+
className: "px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors",
|
|
2102
|
+
children: "Try Again"
|
|
2103
|
+
}
|
|
2104
|
+
),
|
|
2105
|
+
/* @__PURE__ */ jsx(
|
|
2106
|
+
"button",
|
|
2107
|
+
{
|
|
2108
|
+
onClick: this.handleClose,
|
|
2109
|
+
className: "px-6 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white rounded-lg font-medium transition-colors",
|
|
2110
|
+
children: "Close Player"
|
|
2111
|
+
}
|
|
2112
|
+
)
|
|
2113
|
+
] }),
|
|
2114
|
+
/* @__PURE__ */ jsx("p", { className: "text-center text-sm text-gray-500 dark:text-gray-400 mt-6", children: "If this problem persists, try refreshing the page or contact support." })
|
|
2115
|
+
] }) });
|
|
2116
|
+
}
|
|
2117
|
+
return this.props.children;
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
|
|
2121
|
+
export { AudioWaveform, BroadcastPlayer, BroadcastPlayerErrorBoundary, BroadcastPlayerModal, CDN_DOMAIN, DIALTRIBE_API_BASE, DialTribeClient, DialTribeProvider, ENDPOINTS, HTTP_STATUS, HelloWorld, LoadingSpinner, buildBroadcastCdnUrl, buildBroadcastS3KeyPrefix, formatTime, useDialTribe };
|
|
2122
|
+
//# sourceMappingURL=index.mjs.map
|
|
2123
|
+
//# sourceMappingURL=index.mjs.map
|