@optifye/dashboard-core 6.12.15 → 6.12.16
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/automation.d.ts +1 -0
- package/automation.js +1 -0
- package/dist/automation-Hl2PFMb3.d.mts +444 -0
- package/dist/automation-Hl2PFMb3.d.ts +444 -0
- package/dist/automation.d.mts +2 -0
- package/dist/automation.d.ts +2 -0
- package/dist/automation.js +2312 -0
- package/dist/automation.mjs +2305 -0
- package/dist/index.d.mts +3 -422
- package/dist/index.d.ts +3 -422
- package/dist/index.js +251 -0
- package/dist/index.mjs +251 -1
- package/package.json +11 -2
|
@@ -0,0 +1,2312 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
var lucideReact = require('lucide-react');
|
|
5
|
+
var Hls = require('hls.js');
|
|
6
|
+
require('mixpanel-browser');
|
|
7
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
8
|
+
|
|
9
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
var React__default = /*#__PURE__*/_interopDefault(React);
|
|
12
|
+
var Hls__default = /*#__PURE__*/_interopDefault(Hls);
|
|
13
|
+
|
|
14
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
15
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
16
|
+
}) : x)(function(x) {
|
|
17
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
18
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// src/lib/types/efficiencyLegend.ts
|
|
22
|
+
var DEFAULT_EFFICIENCY_LEGEND = {
|
|
23
|
+
green_min: 80,
|
|
24
|
+
green_max: 100,
|
|
25
|
+
yellow_min: 70,
|
|
26
|
+
yellow_max: 79,
|
|
27
|
+
red_min: 0,
|
|
28
|
+
red_max: 69,
|
|
29
|
+
critical_threshold: 50
|
|
30
|
+
};
|
|
31
|
+
function getEfficiencyColor(efficiency, legend = DEFAULT_EFFICIENCY_LEGEND) {
|
|
32
|
+
if (efficiency >= legend.green_min) {
|
|
33
|
+
return "green";
|
|
34
|
+
} else if (efficiency >= legend.yellow_min) {
|
|
35
|
+
return "yellow";
|
|
36
|
+
} else {
|
|
37
|
+
return "red";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
var _getSupabaseInstance = () => {
|
|
41
|
+
{
|
|
42
|
+
throw new Error(
|
|
43
|
+
"Supabase client has not been initialized in @optifye/dashboard-core. Ensure your application is wrapped in the <SupabaseProvider> component from @optifye/dashboard-core and that it is rendered before any hook or service using Supabase is called."
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/lib/services/hlsAuthService.ts
|
|
49
|
+
async function getAuthTokenForHls(supabase) {
|
|
50
|
+
try {
|
|
51
|
+
const { data: { session } } = await supabase.auth.getSession();
|
|
52
|
+
if (!session?.access_token) {
|
|
53
|
+
console.warn("[HLS Auth] No active session, R2 streaming may fail");
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
console.log("[HLS Auth] Retrieved token for HLS.js requests");
|
|
57
|
+
return session.access_token;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error("[HLS Auth] Error getting auth token:", error);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/lib/utils/r2Detection.ts
|
|
65
|
+
function isR2WorkerUrl(url, r2WorkerDomain) {
|
|
66
|
+
if (!url || !r2WorkerDomain) return false;
|
|
67
|
+
try {
|
|
68
|
+
const workerDomain = new URL(r2WorkerDomain).hostname;
|
|
69
|
+
return url.includes(workerDomain);
|
|
70
|
+
} catch {
|
|
71
|
+
return url.includes(r2WorkerDomain.replace(/https?:\/\//, ""));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/lib/utils/browser.ts
|
|
76
|
+
function isSafari() {
|
|
77
|
+
if (typeof window === "undefined") return false;
|
|
78
|
+
const ua = window.navigator.userAgent;
|
|
79
|
+
const isSafariUA = /^((?!chrome|android).)*safari/i.test(ua);
|
|
80
|
+
const isIOS = /iPad|iPhone|iPod/.test(ua) && !window.MSStream;
|
|
81
|
+
return isSafariUA || isIOS;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/lib/hooks/useHlsStream.ts
|
|
85
|
+
var HLS_CONFIG = {
|
|
86
|
+
// Buffer configuration for 60s segments
|
|
87
|
+
maxBufferLength: 180,
|
|
88
|
+
// Allow ~3 segments buffered for stability
|
|
89
|
+
maxMaxBufferLength: 600,
|
|
90
|
+
// 10 minutes max buffer
|
|
91
|
+
maxBufferSize: 120 * 1e3 * 1e3,
|
|
92
|
+
// 120MB buffer size
|
|
93
|
+
maxBufferHole: 0.5,
|
|
94
|
+
// Tolerate minor timestamp gaps
|
|
95
|
+
// Low latency optimizations
|
|
96
|
+
lowLatencyMode: false,
|
|
97
|
+
// We prioritize stability over latency
|
|
98
|
+
enableWorker: true,
|
|
99
|
+
// Offload processing to web worker
|
|
100
|
+
// Aggressive preloading
|
|
101
|
+
startLevel: -1,
|
|
102
|
+
// Auto-select quality
|
|
103
|
+
autoStartLoad: true,
|
|
104
|
+
startPosition: -1,
|
|
105
|
+
// Start at live edge when available
|
|
106
|
+
// Network optimization
|
|
107
|
+
manifestLoadingMaxRetry: 6,
|
|
108
|
+
levelLoadingMaxRetry: 6,
|
|
109
|
+
fragLoadingMaxRetry: 6,
|
|
110
|
+
manifestLoadingRetryDelay: 250,
|
|
111
|
+
// Faster retries
|
|
112
|
+
levelLoadingRetryDelay: 250,
|
|
113
|
+
fragLoadingRetryDelay: 250,
|
|
114
|
+
manifestLoadingTimeOut: 9e4,
|
|
115
|
+
levelLoadingTimeOut: 9e4,
|
|
116
|
+
fragLoadingTimeOut: 9e4,
|
|
117
|
+
maxConcurrentLoading: 2,
|
|
118
|
+
// ABR (Adaptive Bitrate) settings
|
|
119
|
+
abrEwmaFastLive: 3,
|
|
120
|
+
abrEwmaSlowLive: 9,
|
|
121
|
+
abrEwmaFastVoD: 3,
|
|
122
|
+
abrEwmaSlowVoD: 9,
|
|
123
|
+
abrEwmaDefaultEstimate: 1e6,
|
|
124
|
+
// 1 Mbps default
|
|
125
|
+
abrBandWidthFactor: 0.95,
|
|
126
|
+
abrBandWidthUpFactor: 0.7,
|
|
127
|
+
abrMaxWithRealBitrate: true,
|
|
128
|
+
// Fragment loading optimization
|
|
129
|
+
maxFragLookUpTolerance: 1,
|
|
130
|
+
// Enable all optimizations
|
|
131
|
+
enableCEA708Captions: false,
|
|
132
|
+
// Disable if not needed
|
|
133
|
+
enableWebVTT: false,
|
|
134
|
+
// Disable if not needed
|
|
135
|
+
enableIMSC1: false,
|
|
136
|
+
// Disable if not needed
|
|
137
|
+
// Progressive loading
|
|
138
|
+
progressive: true,
|
|
139
|
+
liveSyncDurationCount: 2,
|
|
140
|
+
// Stay ~2 segments behind live for a full segment buffer
|
|
141
|
+
liveMaxLatencyDurationCount: 3,
|
|
142
|
+
// Cap drift to ~3 segments
|
|
143
|
+
maxLiveSyncPlaybackRate: 1
|
|
144
|
+
// Do not chase live edge; prioritize continuity
|
|
145
|
+
};
|
|
146
|
+
var failedUrls = /* @__PURE__ */ new Map();
|
|
147
|
+
var MAX_FAILURES_PER_URL = 3;
|
|
148
|
+
var FAILURE_EXPIRY_MS = 5 * 60 * 1e3;
|
|
149
|
+
var LIVE_RELOAD_MIN_INTERVAL_MS = 15 * 1e3;
|
|
150
|
+
var DEFAULT_LIVE_OFFSET_SECONDS = 120;
|
|
151
|
+
var DEFAULT_MAX_MANIFEST_AGE_MS = 10 * 60 * 1e3;
|
|
152
|
+
var SEGMENT_MAX_AGE_MS = 10 * 60 * 1e3;
|
|
153
|
+
var SEGMENT_TIMESTAMP_REGEX = /(\d{8}T\d{6}Z)(?=\.ts(?:$|[?#]))/;
|
|
154
|
+
var STALE_MANIFEST_POLL_INITIAL_DELAY_MS = 15 * 1e3;
|
|
155
|
+
var STALE_MANIFEST_POLL_MAX_DELAY_MS = 60 * 1e3;
|
|
156
|
+
function useHlsStream(videoRef, { src, shouldPlay, onFatalError, hlsConfig }) {
|
|
157
|
+
const latestSrcRef = React.useRef(src);
|
|
158
|
+
latestSrcRef.current = src;
|
|
159
|
+
const shouldPlayRef = React.useRef(shouldPlay);
|
|
160
|
+
shouldPlayRef.current = shouldPlay;
|
|
161
|
+
const onFatalErrorRef = React.useRef(onFatalError);
|
|
162
|
+
onFatalErrorRef.current = onFatalError;
|
|
163
|
+
const urlFailure = failedUrls.get(src);
|
|
164
|
+
const isPermanentlyFailed = urlFailure?.permanentlyFailed || false;
|
|
165
|
+
const cleanupFailedUrls = () => {
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
const expiredUrls = [];
|
|
168
|
+
failedUrls.forEach((failure, url) => {
|
|
169
|
+
if (now - failure.timestamp > FAILURE_EXPIRY_MS) {
|
|
170
|
+
expiredUrls.push(url);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
expiredUrls.forEach((url) => {
|
|
174
|
+
console.log(`[HLS] Removing expired failure entry for: ${url}`);
|
|
175
|
+
failedUrls.delete(url);
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
const [restartKey, setRestartKey] = React.useState(0);
|
|
179
|
+
const [isStale, setIsStale] = React.useState(false);
|
|
180
|
+
const [staleReason, setStaleReason] = React.useState(null);
|
|
181
|
+
const hlsRef = React.useRef(null);
|
|
182
|
+
const stallCheckIntervalRef = React.useRef(null);
|
|
183
|
+
const noProgressTimerRef = React.useRef(null);
|
|
184
|
+
const lastTimeUpdateRef = React.useRef(0);
|
|
185
|
+
const softRestartCountRef = React.useRef(0);
|
|
186
|
+
const isNativeHlsRef = React.useRef(false);
|
|
187
|
+
const playbackRateIntervalRef = React.useRef(null);
|
|
188
|
+
const waitingTimerRef = React.useRef(null);
|
|
189
|
+
const playRetryTimerRef = React.useRef(null);
|
|
190
|
+
const playRetryCountRef = React.useRef(0);
|
|
191
|
+
const manifestWatchdogRef = React.useRef(null);
|
|
192
|
+
const nativeFreshnessIntervalRef = React.useRef(null);
|
|
193
|
+
const lastHiddenAtRef = React.useRef(null);
|
|
194
|
+
const manifestRetryTimerRef = React.useRef(null);
|
|
195
|
+
const manifestRetryDelayRef = React.useRef(5e3);
|
|
196
|
+
const lastLiveReloadRef = React.useRef(0);
|
|
197
|
+
const isR2StreamRef = React.useRef(false);
|
|
198
|
+
const nativeStreamUrlRef = React.useRef(null);
|
|
199
|
+
const forcedLiveStartRef = React.useRef(false);
|
|
200
|
+
const activeStreamUrlRef = React.useRef(null);
|
|
201
|
+
const targetDurationRef = React.useRef(null);
|
|
202
|
+
const lastManifestLoadRef = React.useRef(0);
|
|
203
|
+
const lastFragLoadRef = React.useRef(0);
|
|
204
|
+
const lastFragSnRef = React.useRef(null);
|
|
205
|
+
const lastWindowStartSnRef = React.useRef(null);
|
|
206
|
+
const lastWindowEndSnRef = React.useRef(null);
|
|
207
|
+
const lastFragTimeoutSnRef = React.useRef(null);
|
|
208
|
+
const lastFragTimeoutAtRef = React.useRef(null);
|
|
209
|
+
const fragTimeoutCountRef = React.useRef(0);
|
|
210
|
+
const lastManifestEndSnRef = React.useRef(null);
|
|
211
|
+
const lastManifestEndSnUpdatedAtRef = React.useRef(null);
|
|
212
|
+
const staleManifestTriggeredRef = React.useRef(false);
|
|
213
|
+
const staleManifestUrlRef = React.useRef(null);
|
|
214
|
+
const staleManifestEndSnRef = React.useRef(null);
|
|
215
|
+
const staleManifestPollTimerRef = React.useRef(null);
|
|
216
|
+
const staleManifestPollDelayRef = React.useRef(STALE_MANIFEST_POLL_INITIAL_DELAY_MS);
|
|
217
|
+
const lastSegmentTimestampMsRef = React.useRef(null);
|
|
218
|
+
const authTokenRef = React.useRef(null);
|
|
219
|
+
const proxyEnabled = process.env.NEXT_PUBLIC_HLS_PROXY_ENABLED === "true";
|
|
220
|
+
const proxyBaseUrl = (process.env.NEXT_PUBLIC_HLS_PROXY_URL || "/api/stream").replace(/\/$/, "");
|
|
221
|
+
const debugEnabled = process.env.NEXT_PUBLIC_HLS_DEBUG === "true";
|
|
222
|
+
const maxManifestAgeMs = (() => {
|
|
223
|
+
const raw = process.env.NEXT_PUBLIC_HLS_MAX_MANIFEST_AGE_MS;
|
|
224
|
+
const parsed = raw ? Number(raw) : NaN;
|
|
225
|
+
return Number.isFinite(parsed) ? parsed : DEFAULT_MAX_MANIFEST_AGE_MS;
|
|
226
|
+
})();
|
|
227
|
+
const manifestStaleThresholdMs = maxManifestAgeMs > 0 ? Math.min(maxManifestAgeMs, SEGMENT_MAX_AGE_MS) : SEGMENT_MAX_AGE_MS;
|
|
228
|
+
const debugLog = (...args) => {
|
|
229
|
+
if (debugEnabled) {
|
|
230
|
+
console.log(...args);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const getProgramDateTimeMs = (value) => {
|
|
234
|
+
if (value instanceof Date) return value.getTime();
|
|
235
|
+
if (typeof value === "number") return value;
|
|
236
|
+
if (typeof value === "string") {
|
|
237
|
+
const parsed = Date.parse(value);
|
|
238
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
};
|
|
242
|
+
const parseSegmentTimestampMs = (value) => {
|
|
243
|
+
const match = value.match(SEGMENT_TIMESTAMP_REGEX);
|
|
244
|
+
if (!match) return null;
|
|
245
|
+
const stamp = match[1];
|
|
246
|
+
const year = Number(stamp.slice(0, 4));
|
|
247
|
+
const month = Number(stamp.slice(4, 6));
|
|
248
|
+
const day = Number(stamp.slice(6, 8));
|
|
249
|
+
const hour = Number(stamp.slice(9, 11));
|
|
250
|
+
const minute = Number(stamp.slice(11, 13));
|
|
251
|
+
const second = Number(stamp.slice(13, 15));
|
|
252
|
+
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day) || !Number.isFinite(hour) || !Number.isFinite(minute) || !Number.isFinite(second) || month < 1 || month > 12 || day < 1 || day > 31 || hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const timestampMs = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
256
|
+
return Number.isNaN(timestampMs) ? null : timestampMs;
|
|
257
|
+
};
|
|
258
|
+
const getFragmentTimestampMs = (fragment, enforceSegmentAge = false) => {
|
|
259
|
+
if (fragment?.relurl) {
|
|
260
|
+
const parsed = parseSegmentTimestampMs(fragment.relurl);
|
|
261
|
+
if (parsed !== null) return parsed;
|
|
262
|
+
}
|
|
263
|
+
if (fragment?.url) {
|
|
264
|
+
const parsed = parseSegmentTimestampMs(fragment.url);
|
|
265
|
+
if (parsed !== null) return parsed;
|
|
266
|
+
}
|
|
267
|
+
if (enforceSegmentAge) return null;
|
|
268
|
+
return getProgramDateTimeMs(fragment?.programDateTime);
|
|
269
|
+
};
|
|
270
|
+
const getLatestFragmentTimestampMs = (fragments, enforceSegmentAge = false) => {
|
|
271
|
+
if (!Array.isArray(fragments) || fragments.length === 0) return null;
|
|
272
|
+
for (let i = fragments.length - 1; i >= 0; i -= 1) {
|
|
273
|
+
const timestampMs = getFragmentTimestampMs(fragments[i], enforceSegmentAge);
|
|
274
|
+
if (timestampMs !== null) {
|
|
275
|
+
return timestampMs;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
};
|
|
280
|
+
const parseManifestStatus = (manifestText) => {
|
|
281
|
+
let mediaSequence = null;
|
|
282
|
+
let segmentCount = 0;
|
|
283
|
+
let lastProgramDateTimeMs = null;
|
|
284
|
+
let lastSegmentTimestampMs = null;
|
|
285
|
+
const lines = manifestText.split(/\r?\n/);
|
|
286
|
+
for (const rawLine of lines) {
|
|
287
|
+
const line = rawLine.trim();
|
|
288
|
+
if (!line) continue;
|
|
289
|
+
if (line.startsWith("#EXT-X-MEDIA-SEQUENCE:")) {
|
|
290
|
+
const value = Number.parseInt(line.replace("#EXT-X-MEDIA-SEQUENCE:", ""), 10);
|
|
291
|
+
if (Number.isFinite(value)) {
|
|
292
|
+
mediaSequence = value;
|
|
293
|
+
}
|
|
294
|
+
} else if (line.startsWith("#EXT-X-PROGRAM-DATE-TIME:")) {
|
|
295
|
+
const timestamp = line.replace("#EXT-X-PROGRAM-DATE-TIME:", "").trim();
|
|
296
|
+
const parsed = Date.parse(timestamp);
|
|
297
|
+
if (!Number.isNaN(parsed)) {
|
|
298
|
+
lastProgramDateTimeMs = parsed;
|
|
299
|
+
}
|
|
300
|
+
} else if (line.startsWith("#EXTINF:")) {
|
|
301
|
+
segmentCount += 1;
|
|
302
|
+
} else if (!line.startsWith("#")) {
|
|
303
|
+
const segmentTimestampMs = parseSegmentTimestampMs(line);
|
|
304
|
+
if (segmentTimestampMs !== null) {
|
|
305
|
+
lastSegmentTimestampMs = segmentTimestampMs;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
let lastSequenceNumber = null;
|
|
310
|
+
if (mediaSequence !== null && segmentCount > 0) {
|
|
311
|
+
lastSequenceNumber = mediaSequence + segmentCount - 1;
|
|
312
|
+
}
|
|
313
|
+
return { lastProgramDateTimeMs, lastSequenceNumber, lastSegmentTimestampMs };
|
|
314
|
+
};
|
|
315
|
+
const evaluateSegmentFreshness = ({
|
|
316
|
+
segmentTimestampMs,
|
|
317
|
+
fallbackTimestampMs,
|
|
318
|
+
enforceSegmentAge
|
|
319
|
+
}) => {
|
|
320
|
+
if (segmentTimestampMs !== null) {
|
|
321
|
+
const ageMs = Date.now() - segmentTimestampMs;
|
|
322
|
+
return {
|
|
323
|
+
isFresh: ageMs <= SEGMENT_MAX_AGE_MS,
|
|
324
|
+
ageMs,
|
|
325
|
+
reason: `segment age ${Math.round(ageMs / 1e3)}s`
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if (enforceSegmentAge) {
|
|
329
|
+
return { isFresh: false, ageMs: null, reason: "segment timestamp missing" };
|
|
330
|
+
}
|
|
331
|
+
if (fallbackTimestampMs !== null) {
|
|
332
|
+
const ageMs = Date.now() - fallbackTimestampMs;
|
|
333
|
+
return {
|
|
334
|
+
isFresh: ageMs <= SEGMENT_MAX_AGE_MS,
|
|
335
|
+
ageMs,
|
|
336
|
+
reason: `program date time age ${Math.round(ageMs / 1e3)}s`
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
return { isFresh: true, ageMs: null, reason: null };
|
|
340
|
+
};
|
|
341
|
+
const fetchManifestStatus = async (manifestUrl) => {
|
|
342
|
+
const headers = {};
|
|
343
|
+
if (authTokenRef.current) {
|
|
344
|
+
headers.Authorization = `Bearer ${authTokenRef.current}`;
|
|
345
|
+
}
|
|
346
|
+
const response = await fetch(buildCacheBustedUrl(manifestUrl), {
|
|
347
|
+
method: "GET",
|
|
348
|
+
cache: "no-store",
|
|
349
|
+
headers
|
|
350
|
+
});
|
|
351
|
+
if (!response.ok) {
|
|
352
|
+
throw new Error(`Manifest fetch failed: ${response.status}`);
|
|
353
|
+
}
|
|
354
|
+
const manifestText = await response.text();
|
|
355
|
+
return parseManifestStatus(manifestText);
|
|
356
|
+
};
|
|
357
|
+
const stopStaleManifestPolling = () => {
|
|
358
|
+
if (staleManifestPollTimerRef.current) {
|
|
359
|
+
clearTimeout(staleManifestPollTimerRef.current);
|
|
360
|
+
staleManifestPollTimerRef.current = null;
|
|
361
|
+
}
|
|
362
|
+
staleManifestTriggeredRef.current = false;
|
|
363
|
+
staleManifestUrlRef.current = null;
|
|
364
|
+
staleManifestEndSnRef.current = null;
|
|
365
|
+
staleManifestPollDelayRef.current = STALE_MANIFEST_POLL_INITIAL_DELAY_MS;
|
|
366
|
+
setIsStale(false);
|
|
367
|
+
setStaleReason(null);
|
|
368
|
+
};
|
|
369
|
+
const pollStaleManifestOnce = async () => {
|
|
370
|
+
if (!staleManifestTriggeredRef.current) return;
|
|
371
|
+
if (!shouldPlayRef.current) {
|
|
372
|
+
stopStaleManifestPolling();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const manifestUrl = staleManifestUrlRef.current;
|
|
376
|
+
if (!manifestUrl) {
|
|
377
|
+
stopStaleManifestPolling();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
const {
|
|
382
|
+
lastProgramDateTimeMs,
|
|
383
|
+
lastSequenceNumber,
|
|
384
|
+
lastSegmentTimestampMs
|
|
385
|
+
} = await fetchManifestStatus(manifestUrl);
|
|
386
|
+
if (lastSegmentTimestampMs !== null) {
|
|
387
|
+
lastSegmentTimestampMsRef.current = lastSegmentTimestampMs;
|
|
388
|
+
}
|
|
389
|
+
const enforceSegmentAge = isR2StreamRef.current;
|
|
390
|
+
const hasAnyTimestamp = lastSegmentTimestampMs !== null || lastProgramDateTimeMs !== null;
|
|
391
|
+
const freshness = evaluateSegmentFreshness({
|
|
392
|
+
segmentTimestampMs: lastSegmentTimestampMs,
|
|
393
|
+
fallbackTimestampMs: lastProgramDateTimeMs,
|
|
394
|
+
enforceSegmentAge
|
|
395
|
+
});
|
|
396
|
+
const priorEndSn = staleManifestEndSnRef.current;
|
|
397
|
+
const isSequenceAdvanced = typeof lastSequenceNumber === "number" && (priorEndSn === null || lastSequenceNumber > priorEndSn);
|
|
398
|
+
if (freshness.isFresh || !enforceSegmentAge && !hasAnyTimestamp && isSequenceAdvanced) {
|
|
399
|
+
stopStaleManifestPolling();
|
|
400
|
+
if (hlsRef.current) {
|
|
401
|
+
hlsRef.current.startLoad(-1);
|
|
402
|
+
seekToLiveEdge();
|
|
403
|
+
attemptPlay("stale manifest recovered");
|
|
404
|
+
} else {
|
|
405
|
+
setRestartKey((k) => k + 1);
|
|
406
|
+
}
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
} catch (error) {
|
|
410
|
+
debugLog("[HLS] Stale manifest poll failed", error);
|
|
411
|
+
}
|
|
412
|
+
staleManifestPollDelayRef.current = Math.min(
|
|
413
|
+
staleManifestPollDelayRef.current + 5e3,
|
|
414
|
+
STALE_MANIFEST_POLL_MAX_DELAY_MS
|
|
415
|
+
);
|
|
416
|
+
scheduleStaleManifestPoll();
|
|
417
|
+
};
|
|
418
|
+
const scheduleStaleManifestPoll = () => {
|
|
419
|
+
if (!staleManifestTriggeredRef.current) return;
|
|
420
|
+
if (staleManifestPollTimerRef.current) return;
|
|
421
|
+
const delay = staleManifestPollDelayRef.current;
|
|
422
|
+
staleManifestPollTimerRef.current = setTimeout(() => {
|
|
423
|
+
staleManifestPollTimerRef.current = null;
|
|
424
|
+
void pollStaleManifestOnce();
|
|
425
|
+
}, delay);
|
|
426
|
+
};
|
|
427
|
+
const startStaleManifestPolling = (reason) => {
|
|
428
|
+
if (staleManifestTriggeredRef.current) return;
|
|
429
|
+
staleManifestTriggeredRef.current = true;
|
|
430
|
+
staleManifestUrlRef.current = activeStreamUrlRef.current || latestSrcRef.current;
|
|
431
|
+
staleManifestEndSnRef.current = lastManifestEndSnRef.current;
|
|
432
|
+
staleManifestPollDelayRef.current = STALE_MANIFEST_POLL_INITIAL_DELAY_MS;
|
|
433
|
+
setIsStale(true);
|
|
434
|
+
setStaleReason(reason);
|
|
435
|
+
const hls = hlsRef.current;
|
|
436
|
+
if (hls) {
|
|
437
|
+
hls.stopLoad();
|
|
438
|
+
}
|
|
439
|
+
const video = videoRef.current;
|
|
440
|
+
if (video) {
|
|
441
|
+
video.pause();
|
|
442
|
+
}
|
|
443
|
+
console.warn(`[HLS] Manifest stale, pausing playback (${reason})`);
|
|
444
|
+
scheduleStaleManifestPoll();
|
|
445
|
+
};
|
|
446
|
+
const isProxyUrl = (url) => {
|
|
447
|
+
if (!proxyEnabled || !proxyBaseUrl) return false;
|
|
448
|
+
try {
|
|
449
|
+
const base = proxyBaseUrl.startsWith("http") ? proxyBaseUrl : new URL(proxyBaseUrl, window.location.origin).toString();
|
|
450
|
+
return url.startsWith(base);
|
|
451
|
+
} catch {
|
|
452
|
+
return url.includes(proxyBaseUrl);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
const isSnapshotStreamUrl = (url) => url.includes("/api/automation/snapshot/stream/");
|
|
456
|
+
const getR2CameraUuid = (url) => {
|
|
457
|
+
try {
|
|
458
|
+
const parsed = new URL(url);
|
|
459
|
+
const match2 = parsed.pathname.match(/\/segments\/([^\/]+)\/index\.m3u8$/);
|
|
460
|
+
if (match2) {
|
|
461
|
+
return match2[1];
|
|
462
|
+
}
|
|
463
|
+
} catch {
|
|
464
|
+
}
|
|
465
|
+
const match = url.match(/\/segments\/([^\/]+)\/index\.m3u8/);
|
|
466
|
+
return match ? match[1] : null;
|
|
467
|
+
};
|
|
468
|
+
const cleanup = () => {
|
|
469
|
+
if (stallCheckIntervalRef.current) {
|
|
470
|
+
clearInterval(stallCheckIntervalRef.current);
|
|
471
|
+
stallCheckIntervalRef.current = null;
|
|
472
|
+
}
|
|
473
|
+
if (noProgressTimerRef.current) {
|
|
474
|
+
clearTimeout(noProgressTimerRef.current);
|
|
475
|
+
noProgressTimerRef.current = null;
|
|
476
|
+
}
|
|
477
|
+
if (playbackRateIntervalRef.current) {
|
|
478
|
+
clearInterval(playbackRateIntervalRef.current);
|
|
479
|
+
playbackRateIntervalRef.current = null;
|
|
480
|
+
}
|
|
481
|
+
if (waitingTimerRef.current) {
|
|
482
|
+
clearTimeout(waitingTimerRef.current);
|
|
483
|
+
waitingTimerRef.current = null;
|
|
484
|
+
}
|
|
485
|
+
if (playRetryTimerRef.current) {
|
|
486
|
+
clearTimeout(playRetryTimerRef.current);
|
|
487
|
+
playRetryTimerRef.current = null;
|
|
488
|
+
}
|
|
489
|
+
if (manifestWatchdogRef.current) {
|
|
490
|
+
clearInterval(manifestWatchdogRef.current);
|
|
491
|
+
manifestWatchdogRef.current = null;
|
|
492
|
+
}
|
|
493
|
+
if (nativeFreshnessIntervalRef.current) {
|
|
494
|
+
clearInterval(nativeFreshnessIntervalRef.current);
|
|
495
|
+
nativeFreshnessIntervalRef.current = null;
|
|
496
|
+
}
|
|
497
|
+
if (manifestRetryTimerRef.current) {
|
|
498
|
+
clearTimeout(manifestRetryTimerRef.current);
|
|
499
|
+
manifestRetryTimerRef.current = null;
|
|
500
|
+
}
|
|
501
|
+
stopStaleManifestPolling();
|
|
502
|
+
lastHiddenAtRef.current = null;
|
|
503
|
+
if (nativeStreamUrlRef.current) {
|
|
504
|
+
nativeStreamUrlRef.current = null;
|
|
505
|
+
}
|
|
506
|
+
activeStreamUrlRef.current = null;
|
|
507
|
+
forcedLiveStartRef.current = false;
|
|
508
|
+
lastLiveReloadRef.current = 0;
|
|
509
|
+
targetDurationRef.current = null;
|
|
510
|
+
lastManifestLoadRef.current = 0;
|
|
511
|
+
lastFragLoadRef.current = 0;
|
|
512
|
+
lastFragSnRef.current = null;
|
|
513
|
+
lastWindowStartSnRef.current = null;
|
|
514
|
+
lastWindowEndSnRef.current = null;
|
|
515
|
+
lastFragTimeoutSnRef.current = null;
|
|
516
|
+
lastFragTimeoutAtRef.current = null;
|
|
517
|
+
fragTimeoutCountRef.current = 0;
|
|
518
|
+
lastManifestEndSnRef.current = null;
|
|
519
|
+
lastManifestEndSnUpdatedAtRef.current = null;
|
|
520
|
+
staleManifestTriggeredRef.current = false;
|
|
521
|
+
lastSegmentTimestampMsRef.current = null;
|
|
522
|
+
manifestRetryDelayRef.current = 5e3;
|
|
523
|
+
playRetryCountRef.current = 0;
|
|
524
|
+
if (hlsRef.current) {
|
|
525
|
+
hlsRef.current.destroy();
|
|
526
|
+
hlsRef.current = null;
|
|
527
|
+
}
|
|
528
|
+
const video = videoRef.current;
|
|
529
|
+
if (video) {
|
|
530
|
+
video.pause();
|
|
531
|
+
video.removeAttribute("src");
|
|
532
|
+
video.load();
|
|
533
|
+
video.removeEventListener("waiting", handleWaiting);
|
|
534
|
+
video.removeEventListener("timeupdate", handleTimeUpdate);
|
|
535
|
+
video.removeEventListener("loadedmetadata", handleLoadedMetadata);
|
|
536
|
+
video.removeEventListener("canplay", handleCanPlay);
|
|
537
|
+
video.removeEventListener("ended", handleEnded);
|
|
538
|
+
video.removeEventListener("error", handleNativeError);
|
|
539
|
+
if (typeof document !== "undefined") {
|
|
540
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
lastTimeUpdateRef.current = 0;
|
|
544
|
+
softRestartCountRef.current = 0;
|
|
545
|
+
};
|
|
546
|
+
const getLiveOffsetSeconds = () => {
|
|
547
|
+
const targetDuration = targetDurationRef.current;
|
|
548
|
+
if (targetDuration && Number.isFinite(targetDuration)) {
|
|
549
|
+
return Math.max(targetDuration * 2, targetDuration);
|
|
550
|
+
}
|
|
551
|
+
return DEFAULT_LIVE_OFFSET_SECONDS;
|
|
552
|
+
};
|
|
553
|
+
const seekToLiveEdge = () => {
|
|
554
|
+
const hls = hlsRef.current;
|
|
555
|
+
const video = videoRef.current;
|
|
556
|
+
if (!hls || !video) return;
|
|
557
|
+
if (hls.liveSyncPosition !== null && hls.liveSyncPosition !== void 0) {
|
|
558
|
+
video.currentTime = hls.liveSyncPosition;
|
|
559
|
+
} else if (hls.levels?.[hls.currentLevel]?.details) {
|
|
560
|
+
const levelDetails = hls.levels[hls.currentLevel].details;
|
|
561
|
+
const edge = levelDetails?.edge;
|
|
562
|
+
if (edge !== void 0 && edge !== null) {
|
|
563
|
+
const offset = getLiveOffsetSeconds();
|
|
564
|
+
video.currentTime = Math.max(0, edge - offset);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
const getLiveEdgeTime = () => {
|
|
569
|
+
const video = videoRef.current;
|
|
570
|
+
if (!video || video.seekable.length === 0) return null;
|
|
571
|
+
const end = video.seekable.end(video.seekable.length - 1);
|
|
572
|
+
if (!Number.isFinite(end)) return null;
|
|
573
|
+
const offset = getLiveOffsetSeconds();
|
|
574
|
+
return Math.max(0, end - offset);
|
|
575
|
+
};
|
|
576
|
+
const getWaitingTimeoutMs = () => {
|
|
577
|
+
const targetDuration = targetDurationRef.current;
|
|
578
|
+
if (targetDuration && Number.isFinite(targetDuration)) {
|
|
579
|
+
return Math.max(2e4, targetDuration * 1e3 * 0.5);
|
|
580
|
+
}
|
|
581
|
+
return 3e4;
|
|
582
|
+
};
|
|
583
|
+
const getManifestStaleTimeoutMs = () => {
|
|
584
|
+
const targetDuration = targetDurationRef.current;
|
|
585
|
+
if (targetDuration && Number.isFinite(targetDuration)) {
|
|
586
|
+
return Math.max(targetDuration * 2e3, 12e4);
|
|
587
|
+
}
|
|
588
|
+
return 12e4;
|
|
589
|
+
};
|
|
590
|
+
const isManifestUrl = (url) => url.includes(".m3u8");
|
|
591
|
+
const getBufferGap = (video) => {
|
|
592
|
+
if (!video.buffered.length) return null;
|
|
593
|
+
const end = video.buffered.end(video.buffered.length - 1);
|
|
594
|
+
if (!Number.isFinite(end)) return null;
|
|
595
|
+
return Math.max(0, end - video.currentTime);
|
|
596
|
+
};
|
|
597
|
+
const getBufferThresholds = () => {
|
|
598
|
+
const targetDuration = targetDurationRef.current ?? 60;
|
|
599
|
+
return {
|
|
600
|
+
low: Math.max(5, targetDuration * 0.1),
|
|
601
|
+
mid: Math.max(10, targetDuration * 0.2),
|
|
602
|
+
high: Math.max(18, targetDuration * 0.35)
|
|
603
|
+
};
|
|
604
|
+
};
|
|
605
|
+
const shouldAttemptRecovery = () => {
|
|
606
|
+
if (!isR2StreamRef.current) return true;
|
|
607
|
+
if (!lastManifestLoadRef.current) return false;
|
|
608
|
+
const now = Date.now();
|
|
609
|
+
const timeoutMs = getManifestStaleTimeoutMs();
|
|
610
|
+
const manifestAge = now - lastManifestLoadRef.current;
|
|
611
|
+
return manifestAge > timeoutMs;
|
|
612
|
+
};
|
|
613
|
+
const getDesiredLivePosition = () => {
|
|
614
|
+
const hls = hlsRef.current;
|
|
615
|
+
if (hls?.liveSyncPosition !== null && hls?.liveSyncPosition !== void 0) {
|
|
616
|
+
if (Number.isFinite(hls.liveSyncPosition)) {
|
|
617
|
+
return hls.liveSyncPosition;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (hls?.levels?.[hls.currentLevel]?.details) {
|
|
621
|
+
const levelDetails = hls.levels[hls.currentLevel].details;
|
|
622
|
+
const edge = levelDetails?.edge;
|
|
623
|
+
if (edge !== void 0 && edge !== null) {
|
|
624
|
+
return Math.max(0, edge - getLiveOffsetSeconds());
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return getLiveEdgeTime();
|
|
628
|
+
};
|
|
629
|
+
const schedulePlayRetry = (reason) => {
|
|
630
|
+
if (playRetryTimerRef.current) return;
|
|
631
|
+
if (playRetryCountRef.current >= 3) return;
|
|
632
|
+
playRetryCountRef.current += 1;
|
|
633
|
+
playRetryTimerRef.current = setTimeout(() => {
|
|
634
|
+
playRetryTimerRef.current = null;
|
|
635
|
+
attemptPlay();
|
|
636
|
+
}, 1e3);
|
|
637
|
+
};
|
|
638
|
+
const attemptPlay = (reason) => {
|
|
639
|
+
const video = videoRef.current;
|
|
640
|
+
if (!video || !shouldPlayRef.current) return;
|
|
641
|
+
if (staleManifestTriggeredRef.current) return;
|
|
642
|
+
if (!video.paused || video.seeking) return;
|
|
643
|
+
if (video.readyState < 2) return;
|
|
644
|
+
video.play().then(() => {
|
|
645
|
+
playRetryCountRef.current = 0;
|
|
646
|
+
}).catch((err) => {
|
|
647
|
+
if (err?.name === "AbortError") {
|
|
648
|
+
schedulePlayRetry();
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
console.error("[HLS] Play failed:", err);
|
|
652
|
+
});
|
|
653
|
+
};
|
|
654
|
+
const handleCanPlay = () => {
|
|
655
|
+
attemptPlay();
|
|
656
|
+
};
|
|
657
|
+
const handleVisibilityChange = () => {
|
|
658
|
+
if (typeof document === "undefined") return;
|
|
659
|
+
if (document.hidden) {
|
|
660
|
+
lastHiddenAtRef.current = Date.now();
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const lastHiddenAt = lastHiddenAtRef.current;
|
|
664
|
+
lastHiddenAtRef.current = null;
|
|
665
|
+
if (!lastHiddenAt) return;
|
|
666
|
+
if (Date.now() - lastHiddenAt < 3e4) return;
|
|
667
|
+
void refreshLiveStream("tab visible after idle");
|
|
668
|
+
attemptPlay();
|
|
669
|
+
};
|
|
670
|
+
const startManifestWatchdog = () => {
|
|
671
|
+
if (manifestWatchdogRef.current) return;
|
|
672
|
+
if (!isR2StreamRef.current) return;
|
|
673
|
+
manifestWatchdogRef.current = setInterval(() => {
|
|
674
|
+
if (staleManifestTriggeredRef.current) return;
|
|
675
|
+
if (!lastManifestLoadRef.current) return;
|
|
676
|
+
const now = Date.now();
|
|
677
|
+
const staleTimeoutMs = getManifestStaleTimeoutMs();
|
|
678
|
+
if (now - lastManifestLoadRef.current < staleTimeoutMs) return;
|
|
679
|
+
if (now - lastLiveReloadRef.current < LIVE_RELOAD_MIN_INTERVAL_MS) return;
|
|
680
|
+
lastLiveReloadRef.current = now;
|
|
681
|
+
softRestart("manifest stale watchdog");
|
|
682
|
+
}, 15e3);
|
|
683
|
+
};
|
|
684
|
+
const scheduleManifestRetry = (reason) => {
|
|
685
|
+
if (manifestRetryTimerRef.current) return;
|
|
686
|
+
const delay = manifestRetryDelayRef.current;
|
|
687
|
+
manifestRetryTimerRef.current = setTimeout(() => {
|
|
688
|
+
manifestRetryTimerRef.current = null;
|
|
689
|
+
softRestart(reason);
|
|
690
|
+
manifestRetryDelayRef.current = Math.min(manifestRetryDelayRef.current + 5e3, 3e4);
|
|
691
|
+
}, delay);
|
|
692
|
+
};
|
|
693
|
+
const resetManifestRetry = () => {
|
|
694
|
+
if (manifestRetryTimerRef.current) {
|
|
695
|
+
clearTimeout(manifestRetryTimerRef.current);
|
|
696
|
+
manifestRetryTimerRef.current = null;
|
|
697
|
+
}
|
|
698
|
+
manifestRetryDelayRef.current = 5e3;
|
|
699
|
+
};
|
|
700
|
+
const markStaleStream = (reason) => {
|
|
701
|
+
startStaleManifestPolling(reason);
|
|
702
|
+
};
|
|
703
|
+
const setPlaybackRate = (rate) => {
|
|
704
|
+
const video = videoRef.current;
|
|
705
|
+
if (!video) return;
|
|
706
|
+
if (!Number.isFinite(rate)) return;
|
|
707
|
+
if (Math.abs(video.playbackRate - rate) < 0.01) return;
|
|
708
|
+
video.playbackRate = rate;
|
|
709
|
+
};
|
|
710
|
+
const startPlaybackGovernor = () => {
|
|
711
|
+
if (playbackRateIntervalRef.current) return;
|
|
712
|
+
playbackRateIntervalRef.current = setInterval(() => {
|
|
713
|
+
const video = videoRef.current;
|
|
714
|
+
if (!video || video.paused || video.seeking) return;
|
|
715
|
+
if (!isR2StreamRef.current) return;
|
|
716
|
+
const bufferGap = getBufferGap(video);
|
|
717
|
+
if (bufferGap === null) return;
|
|
718
|
+
const { low, mid, high } = getBufferThresholds();
|
|
719
|
+
let desiredRate = 1;
|
|
720
|
+
if (bufferGap < low) {
|
|
721
|
+
desiredRate = 0.8;
|
|
722
|
+
} else if (bufferGap < mid) {
|
|
723
|
+
desiredRate = 0.9;
|
|
724
|
+
} else if (bufferGap < high) {
|
|
725
|
+
desiredRate = 0.95;
|
|
726
|
+
}
|
|
727
|
+
setPlaybackRate(desiredRate);
|
|
728
|
+
}, 2e3);
|
|
729
|
+
};
|
|
730
|
+
const buildCacheBustedUrl = (url) => {
|
|
731
|
+
if (typeof window === "undefined") {
|
|
732
|
+
return url;
|
|
733
|
+
}
|
|
734
|
+
try {
|
|
735
|
+
const parsed = new URL(url, window.location.origin);
|
|
736
|
+
parsed.searchParams.set("ts", Date.now().toString());
|
|
737
|
+
return parsed.toString();
|
|
738
|
+
} catch {
|
|
739
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
740
|
+
return `${url}${separator}ts=${Date.now()}`;
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
const ensureManifestFreshness = async (manifestUrl, enforceSegmentAge, reason) => {
|
|
744
|
+
try {
|
|
745
|
+
const status = await fetchManifestStatus(manifestUrl);
|
|
746
|
+
if (status.lastSegmentTimestampMs !== null) {
|
|
747
|
+
lastSegmentTimestampMsRef.current = status.lastSegmentTimestampMs;
|
|
748
|
+
}
|
|
749
|
+
const freshness = evaluateSegmentFreshness({
|
|
750
|
+
segmentTimestampMs: status.lastSegmentTimestampMs,
|
|
751
|
+
fallbackTimestampMs: status.lastProgramDateTimeMs,
|
|
752
|
+
enforceSegmentAge
|
|
753
|
+
});
|
|
754
|
+
if (!freshness.isFresh) {
|
|
755
|
+
markStaleStream(freshness.reason || reason);
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
return true;
|
|
759
|
+
} catch (error) {
|
|
760
|
+
debugLog("[HLS] Manifest freshness check failed", error);
|
|
761
|
+
{
|
|
762
|
+
markStaleStream("manifest freshness check failed");
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
const startNativeFreshnessMonitor = (manifestUrl) => {
|
|
768
|
+
if (nativeFreshnessIntervalRef.current) return;
|
|
769
|
+
nativeFreshnessIntervalRef.current = setInterval(() => {
|
|
770
|
+
if (staleManifestTriggeredRef.current) return;
|
|
771
|
+
void ensureManifestFreshness(manifestUrl, true, "native freshness check");
|
|
772
|
+
}, 3e4);
|
|
773
|
+
};
|
|
774
|
+
const refreshLiveStream = async (reason) => {
|
|
775
|
+
if (!isR2StreamRef.current) return;
|
|
776
|
+
const now = Date.now();
|
|
777
|
+
if (now - lastLiveReloadRef.current < LIVE_RELOAD_MIN_INTERVAL_MS) {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
lastLiveReloadRef.current = now;
|
|
781
|
+
const video = videoRef.current;
|
|
782
|
+
const hls = hlsRef.current;
|
|
783
|
+
if (hls) {
|
|
784
|
+
console.log(`[HLS] Live reload (${reason})`);
|
|
785
|
+
softRestart(reason);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (video && nativeStreamUrlRef.current) {
|
|
789
|
+
console.log(`[HLS] Native live reload (${reason})`);
|
|
790
|
+
const isFresh = await ensureManifestFreshness(nativeStreamUrlRef.current, true, "native live reload");
|
|
791
|
+
if (!isFresh) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const refreshedUrl = buildCacheBustedUrl(nativeStreamUrlRef.current);
|
|
795
|
+
video.src = refreshedUrl;
|
|
796
|
+
video.load();
|
|
797
|
+
const edgeTime = getLiveEdgeTime();
|
|
798
|
+
if (edgeTime !== null) {
|
|
799
|
+
video.currentTime = edgeTime;
|
|
800
|
+
}
|
|
801
|
+
attemptPlay();
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
const softRestart = (reason) => {
|
|
805
|
+
if (staleManifestTriggeredRef.current) {
|
|
806
|
+
debugLog("[HLS] Skip soft restart while manifest is stale", reason);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
console.warn(`[HLS] Soft restart: ${reason}`);
|
|
810
|
+
const hls = hlsRef.current;
|
|
811
|
+
if (!hls) return;
|
|
812
|
+
try {
|
|
813
|
+
const video = videoRef.current;
|
|
814
|
+
const isR2Stream = isR2StreamRef.current;
|
|
815
|
+
const desiredPosition = isR2Stream ? getDesiredLivePosition() : null;
|
|
816
|
+
hls.stopLoad();
|
|
817
|
+
if (desiredPosition !== null && Number.isFinite(desiredPosition)) {
|
|
818
|
+
hls.startLoad(desiredPosition);
|
|
819
|
+
if (video) {
|
|
820
|
+
video.currentTime = desiredPosition;
|
|
821
|
+
}
|
|
822
|
+
} else {
|
|
823
|
+
hls.startLoad(-1);
|
|
824
|
+
if (!isR2Stream) {
|
|
825
|
+
seekToLiveEdge();
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
softRestartCountRef.current++;
|
|
829
|
+
if (softRestartCountRef.current >= 5) {
|
|
830
|
+
hardRestart(`${reason} (escalated after ${softRestartCountRef.current} soft restarts)`);
|
|
831
|
+
}
|
|
832
|
+
} catch (error) {
|
|
833
|
+
console.error("[HLS] Soft restart failed:", error);
|
|
834
|
+
hardRestart(`${reason} (soft restart error)`);
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
const hardRestart = (reason) => {
|
|
838
|
+
if (staleManifestTriggeredRef.current) {
|
|
839
|
+
debugLog("[HLS] Skip hard restart while manifest is stale", reason);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
console.warn(`[HLS] Hard restart: ${reason}`);
|
|
843
|
+
cleanupFailedUrls();
|
|
844
|
+
const failure = failedUrls.get(src);
|
|
845
|
+
const failureCount = failure ? failure.count : 0;
|
|
846
|
+
const isR2Stream = isR2StreamRef.current;
|
|
847
|
+
if (!isR2Stream && failureCount >= MAX_FAILURES_PER_URL) {
|
|
848
|
+
console.error(`[HLS] URL has failed ${failureCount} times. Marking as permanently failed: ${src}`);
|
|
849
|
+
failedUrls.set(src, {
|
|
850
|
+
count: failureCount,
|
|
851
|
+
timestamp: Date.now(),
|
|
852
|
+
permanentlyFailed: true
|
|
853
|
+
});
|
|
854
|
+
cleanup();
|
|
855
|
+
onFatalErrorRef.current?.();
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (!isR2Stream) {
|
|
859
|
+
failedUrls.set(src, {
|
|
860
|
+
count: failureCount + 1,
|
|
861
|
+
timestamp: Date.now(),
|
|
862
|
+
permanentlyFailed: false
|
|
863
|
+
});
|
|
864
|
+
} else if (failedUrls.has(src)) {
|
|
865
|
+
failedUrls.delete(src);
|
|
866
|
+
}
|
|
867
|
+
cleanup();
|
|
868
|
+
setRestartKey((k) => k + 1);
|
|
869
|
+
softRestartCountRef.current = 0;
|
|
870
|
+
if (reason.includes("404 hard restart")) {
|
|
871
|
+
onFatalErrorRef.current?.();
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
const handleLoadedMetadata = () => {
|
|
875
|
+
if (!isR2StreamRef.current) return;
|
|
876
|
+
const video = videoRef.current;
|
|
877
|
+
if (!video) return;
|
|
878
|
+
const edgeTime = getLiveEdgeTime();
|
|
879
|
+
if (edgeTime !== null) {
|
|
880
|
+
video.currentTime = edgeTime;
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
const handleEnded = () => {
|
|
884
|
+
if (isNativeHlsRef.current) {
|
|
885
|
+
void refreshLiveStream("ended");
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (isR2StreamRef.current) {
|
|
889
|
+
softRestart("ended");
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
const handleWaiting = () => {
|
|
893
|
+
if (waitingTimerRef.current) {
|
|
894
|
+
clearTimeout(waitingTimerRef.current);
|
|
895
|
+
}
|
|
896
|
+
if (isNativeHlsRef.current) {
|
|
897
|
+
if (!isR2StreamRef.current) return;
|
|
898
|
+
waitingTimerRef.current = setTimeout(() => {
|
|
899
|
+
void refreshLiveStream("native waiting timeout");
|
|
900
|
+
}, getWaitingTimeoutMs());
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
console.log("[HLS] Video waiting (buffer underrun)");
|
|
904
|
+
if (isR2StreamRef.current) {
|
|
905
|
+
waitingTimerRef.current = setTimeout(() => {
|
|
906
|
+
const video = videoRef.current;
|
|
907
|
+
if (video && video.readyState < 3) {
|
|
908
|
+
if (shouldAttemptRecovery()) {
|
|
909
|
+
softRestart("waiting timeout");
|
|
910
|
+
} else {
|
|
911
|
+
handleWaiting();
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}, getWaitingTimeoutMs());
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
waitingTimerRef.current = setTimeout(() => {
|
|
918
|
+
const video = videoRef.current;
|
|
919
|
+
if (video && video.readyState < 3) {
|
|
920
|
+
softRestart("waiting timeout");
|
|
921
|
+
}
|
|
922
|
+
}, 1e4);
|
|
923
|
+
};
|
|
924
|
+
const handleTimeUpdate = () => {
|
|
925
|
+
const video = videoRef.current;
|
|
926
|
+
if (!video) return;
|
|
927
|
+
lastTimeUpdateRef.current = video.currentTime;
|
|
928
|
+
playRetryCountRef.current = 0;
|
|
929
|
+
if (waitingTimerRef.current) {
|
|
930
|
+
clearTimeout(waitingTimerRef.current);
|
|
931
|
+
waitingTimerRef.current = null;
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
const handleNativeError = () => {
|
|
935
|
+
console.error("[HLS] Native video error");
|
|
936
|
+
hardRestart("native video error");
|
|
937
|
+
};
|
|
938
|
+
const startStallDetection = () => {
|
|
939
|
+
if (isNativeHlsRef.current || isR2StreamRef.current) return;
|
|
940
|
+
stallCheckIntervalRef.current = setInterval(() => {
|
|
941
|
+
const video = videoRef.current;
|
|
942
|
+
if (!video || video.paused || video.ended) return;
|
|
943
|
+
if (video.readyState < 3 || video.seeking) return;
|
|
944
|
+
const currentTime = video.currentTime;
|
|
945
|
+
const lastTime = lastTimeUpdateRef.current;
|
|
946
|
+
const bufferGap = getBufferGap(video);
|
|
947
|
+
const nearBufferEnd = bufferGap !== null && bufferGap < 0.5;
|
|
948
|
+
if (Math.abs(currentTime - lastTime) < 0.1 && video.readyState >= 2) {
|
|
949
|
+
if (nearBufferEnd && !shouldAttemptRecovery()) {
|
|
950
|
+
if (noProgressTimerRef.current) {
|
|
951
|
+
clearTimeout(noProgressTimerRef.current);
|
|
952
|
+
noProgressTimerRef.current = null;
|
|
953
|
+
}
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
console.warn("[HLS] Playback stall detected");
|
|
957
|
+
if (!noProgressTimerRef.current) {
|
|
958
|
+
noProgressTimerRef.current = setTimeout(() => {
|
|
959
|
+
if (nearBufferEnd && !shouldAttemptRecovery()) {
|
|
960
|
+
noProgressTimerRef.current = null;
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
softRestart("playback stall");
|
|
964
|
+
noProgressTimerRef.current = null;
|
|
965
|
+
}, nearBufferEnd ? getWaitingTimeoutMs() : 12e3);
|
|
966
|
+
}
|
|
967
|
+
} else {
|
|
968
|
+
if (noProgressTimerRef.current) {
|
|
969
|
+
clearTimeout(noProgressTimerRef.current);
|
|
970
|
+
noProgressTimerRef.current = null;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}, 7e3);
|
|
974
|
+
};
|
|
975
|
+
React.useEffect(() => {
|
|
976
|
+
if (!src || !shouldPlay) {
|
|
977
|
+
cleanup();
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
let isCancelled = false;
|
|
981
|
+
const r2WorkerDomain = process.env.NEXT_PUBLIC_R2_WORKER_DOMAIN || "https://r2-stream-proxy.optifye-r2.workers.dev";
|
|
982
|
+
const isR2Stream = isR2WorkerUrl(src, r2WorkerDomain);
|
|
983
|
+
isR2StreamRef.current = isR2Stream;
|
|
984
|
+
if (isPermanentlyFailed && !isR2Stream) {
|
|
985
|
+
console.warn(`[HLS] URL is permanently failed, not attempting to load: ${src}`);
|
|
986
|
+
onFatalErrorRef.current?.();
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (isPermanentlyFailed && isR2Stream && failedUrls.has(src)) {
|
|
990
|
+
failedUrls.delete(src);
|
|
991
|
+
}
|
|
992
|
+
const video = videoRef.current;
|
|
993
|
+
if (!video) return;
|
|
994
|
+
if (typeof document !== "undefined") {
|
|
995
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
996
|
+
}
|
|
997
|
+
const initialize = async () => {
|
|
998
|
+
const canUseNative = video.canPlayType("application/vnd.apple.mpegurl") === "probably";
|
|
999
|
+
isNativeHlsRef.current = canUseNative && !isR2Stream;
|
|
1000
|
+
let authToken = null;
|
|
1001
|
+
if (isR2Stream) {
|
|
1002
|
+
try {
|
|
1003
|
+
const supabase = _getSupabaseInstance();
|
|
1004
|
+
authToken = await getAuthTokenForHls(supabase);
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
console.warn("[HLS] Unable to retrieve auth token for R2 streaming:", error);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
authTokenRef.current = authToken;
|
|
1010
|
+
if (isCancelled) return;
|
|
1011
|
+
const cameraUuid = isR2Stream ? getR2CameraUuid(src) : null;
|
|
1012
|
+
const proxyPlaylistUrl = proxyEnabled && isR2Stream && proxyBaseUrl && cameraUuid ? `${proxyBaseUrl}/segments/${cameraUuid}/index.m3u8` : null;
|
|
1013
|
+
const resolvedSrc = proxyPlaylistUrl || src;
|
|
1014
|
+
const resolvedHlsSrc = src;
|
|
1015
|
+
const shouldUseProxy = proxyEnabled && isR2Stream && canUseNative && !Hls__default.default.isSupported() && isSafari();
|
|
1016
|
+
if (shouldUseProxy) {
|
|
1017
|
+
if (cameraUuid) {
|
|
1018
|
+
isNativeHlsRef.current = true;
|
|
1019
|
+
nativeStreamUrlRef.current = resolvedSrc;
|
|
1020
|
+
activeStreamUrlRef.current = resolvedSrc;
|
|
1021
|
+
console.log("[HLS] Using proxy playlist for Safari R2 stream");
|
|
1022
|
+
const isFresh = await ensureManifestFreshness(resolvedSrc, true, "native proxy start");
|
|
1023
|
+
if (!isFresh) {
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
video.src = resolvedSrc;
|
|
1027
|
+
video.addEventListener("waiting", handleWaiting);
|
|
1028
|
+
video.addEventListener("loadedmetadata", handleLoadedMetadata);
|
|
1029
|
+
video.addEventListener("canplay", handleCanPlay);
|
|
1030
|
+
video.addEventListener("ended", handleEnded);
|
|
1031
|
+
video.addEventListener("error", handleNativeError);
|
|
1032
|
+
startNativeFreshnessMonitor(resolvedSrc);
|
|
1033
|
+
attemptPlay();
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
console.warn("[HLS] Safari R2 proxy unavailable, falling back to direct URL");
|
|
1037
|
+
}
|
|
1038
|
+
if (!proxyEnabled && isR2Stream && canUseNative && !Hls__default.default.isSupported() && isSafari()) {
|
|
1039
|
+
console.warn("[HLS] Safari native R2 streaming requires proxy. Falling back.");
|
|
1040
|
+
onFatalErrorRef.current?.();
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (Hls__default.default.isSupported() && !isNativeHlsRef.current) {
|
|
1044
|
+
const usesSnapshotStream = isSnapshotStreamUrl(resolvedHlsSrc);
|
|
1045
|
+
const mergedConfig = {
|
|
1046
|
+
...HLS_CONFIG,
|
|
1047
|
+
...hlsConfig,
|
|
1048
|
+
enableWorker: usesSnapshotStream ? false : hlsConfig?.enableWorker ?? HLS_CONFIG.enableWorker,
|
|
1049
|
+
xhrSetup: (xhr, url) => {
|
|
1050
|
+
const usesProxy = isProxyUrl(url);
|
|
1051
|
+
if (isSnapshotStreamUrl(url)) {
|
|
1052
|
+
xhr.withCredentials = true;
|
|
1053
|
+
}
|
|
1054
|
+
if (isR2WorkerUrl(url, r2WorkerDomain) || usesProxy) {
|
|
1055
|
+
if (isManifestUrl(url)) {
|
|
1056
|
+
xhr.open("GET", buildCacheBustedUrl(url), true);
|
|
1057
|
+
}
|
|
1058
|
+
if (authToken) {
|
|
1059
|
+
xhr.setRequestHeader("Authorization", `Bearer ${authToken}`);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
},
|
|
1063
|
+
fetchSetup: (context, initParams) => {
|
|
1064
|
+
const isR2Url = isR2WorkerUrl(context.url, r2WorkerDomain);
|
|
1065
|
+
const isManifestRequest = isManifestUrl(context.url);
|
|
1066
|
+
const usesProxy = isProxyUrl(context.url);
|
|
1067
|
+
const isSnapshotStream = isSnapshotStreamUrl(context.url);
|
|
1068
|
+
let requestUrl = context.url;
|
|
1069
|
+
if (isSnapshotStream) {
|
|
1070
|
+
initParams.credentials = "include";
|
|
1071
|
+
}
|
|
1072
|
+
if (isR2Url || usesProxy) {
|
|
1073
|
+
if (authToken) {
|
|
1074
|
+
initParams.headers = {
|
|
1075
|
+
...initParams.headers,
|
|
1076
|
+
"Authorization": `Bearer ${authToken}`
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
if (isManifestRequest) {
|
|
1080
|
+
requestUrl = buildCacheBustedUrl(context.url);
|
|
1081
|
+
initParams.cache = "no-store";
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return new Request(requestUrl, initParams);
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
const hls = new Hls__default.default(mergedConfig);
|
|
1088
|
+
hlsRef.current = hls;
|
|
1089
|
+
hls.attachMedia(video);
|
|
1090
|
+
hls.loadSource(resolvedHlsSrc);
|
|
1091
|
+
activeStreamUrlRef.current = resolvedHlsSrc;
|
|
1092
|
+
hls.on(Hls__default.default.Events.ERROR, (_, data) => {
|
|
1093
|
+
debugLog("[HLS] Error", {
|
|
1094
|
+
type: data.type,
|
|
1095
|
+
details: data.details,
|
|
1096
|
+
fatal: data.fatal,
|
|
1097
|
+
response: data.response?.code,
|
|
1098
|
+
frag: data.frag?.sn
|
|
1099
|
+
});
|
|
1100
|
+
if (data.type === Hls__default.default.ErrorTypes.MEDIA_ERROR && data.details === Hls__default.default.ErrorDetails.BUFFER_STALLED_ERROR) {
|
|
1101
|
+
debugLog("[HLS] Buffer stalled, waiting for next segment");
|
|
1102
|
+
attemptPlay();
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (data.type === Hls__default.default.ErrorTypes.NETWORK_ERROR && (data.details === Hls__default.default.ErrorDetails.FRAG_LOAD_TIMEOUT || data.details === Hls__default.default.ErrorDetails.FRAG_LOAD_ERROR)) {
|
|
1106
|
+
const fragSn = data.frag?.sn;
|
|
1107
|
+
const windowStart = lastWindowStartSnRef.current;
|
|
1108
|
+
const windowEnd = lastWindowEndSnRef.current;
|
|
1109
|
+
if (typeof fragSn === "number" && typeof windowStart === "number" && fragSn < windowStart) {
|
|
1110
|
+
softRestart("frag fell out of window");
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (typeof fragSn === "number" && typeof windowEnd === "number" && fragSn > windowEnd) {
|
|
1114
|
+
softRestart("frag ahead of window");
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const now = Date.now();
|
|
1118
|
+
if (lastFragTimeoutSnRef.current === fragSn && lastFragTimeoutAtRef.current) {
|
|
1119
|
+
if (now - lastFragTimeoutAtRef.current < 12e4) {
|
|
1120
|
+
fragTimeoutCountRef.current += 1;
|
|
1121
|
+
} else {
|
|
1122
|
+
fragTimeoutCountRef.current = 1;
|
|
1123
|
+
}
|
|
1124
|
+
} else {
|
|
1125
|
+
fragTimeoutCountRef.current = 1;
|
|
1126
|
+
lastFragTimeoutSnRef.current = fragSn ?? null;
|
|
1127
|
+
}
|
|
1128
|
+
lastFragTimeoutAtRef.current = now;
|
|
1129
|
+
if (fragTimeoutCountRef.current >= 2) {
|
|
1130
|
+
softRestart("frag load timeout");
|
|
1131
|
+
}
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (data.type === Hls__default.default.ErrorTypes.NETWORK_ERROR && (data.details === Hls__default.default.ErrorDetails.MANIFEST_LOAD_TIMEOUT || data.details === Hls__default.default.ErrorDetails.MANIFEST_LOAD_ERROR)) {
|
|
1135
|
+
if (data.response?.code === 404 && isR2StreamRef.current) {
|
|
1136
|
+
markStaleStream("manifest 404");
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
scheduleManifestRetry("manifest load timeout");
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (!data.fatal) return;
|
|
1143
|
+
console.error("[HLS] Fatal error:", data.type, data.details);
|
|
1144
|
+
if (data.response?.code === 404) {
|
|
1145
|
+
if (data.details === Hls__default.default.ErrorDetails.MANIFEST_LOAD_ERROR || data.details === Hls__default.default.ErrorDetails.LEVEL_LOAD_ERROR) {
|
|
1146
|
+
if (isR2StreamRef.current) {
|
|
1147
|
+
markStaleStream("manifest 404");
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
hardRestart("404 manifest hard restart");
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
softRestart("frag 404");
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
switch (data.type) {
|
|
1157
|
+
case Hls__default.default.ErrorTypes.NETWORK_ERROR:
|
|
1158
|
+
if (isR2StreamRef.current && data.details === Hls__default.default.ErrorDetails.MANIFEST_LOAD_TIMEOUT) {
|
|
1159
|
+
softRestart("manifest load timeout");
|
|
1160
|
+
break;
|
|
1161
|
+
}
|
|
1162
|
+
softRestart(`${data.type}: ${data.details}`);
|
|
1163
|
+
break;
|
|
1164
|
+
case Hls__default.default.ErrorTypes.MEDIA_ERROR:
|
|
1165
|
+
softRestart(`${data.type}: ${data.details}`);
|
|
1166
|
+
break;
|
|
1167
|
+
default:
|
|
1168
|
+
hardRestart(`Fatal ${data.type}: ${data.details}`);
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
hls.on(Hls__default.default.Events.MANIFEST_PARSED, () => {
|
|
1173
|
+
if (failedUrls.has(src)) {
|
|
1174
|
+
console.log(`[HLS] Stream loaded successfully, resetting failure count for: ${src}`);
|
|
1175
|
+
failedUrls.delete(src);
|
|
1176
|
+
}
|
|
1177
|
+
if (isR2Stream) {
|
|
1178
|
+
seekToLiveEdge();
|
|
1179
|
+
}
|
|
1180
|
+
attemptPlay();
|
|
1181
|
+
});
|
|
1182
|
+
hls.on(Hls__default.default.Events.LEVEL_LOADED, (_event, data) => {
|
|
1183
|
+
if (!data?.details) return;
|
|
1184
|
+
const details = data.details;
|
|
1185
|
+
if (isR2Stream) {
|
|
1186
|
+
lastManifestLoadRef.current = Date.now();
|
|
1187
|
+
resetManifestRetry();
|
|
1188
|
+
if (details.endList) {
|
|
1189
|
+
details.endList = false;
|
|
1190
|
+
}
|
|
1191
|
+
details.live = true;
|
|
1192
|
+
details.type = "LIVE";
|
|
1193
|
+
if (details.targetduration && Number.isFinite(details.targetduration)) {
|
|
1194
|
+
targetDurationRef.current = details.targetduration;
|
|
1195
|
+
}
|
|
1196
|
+
if (typeof details.startSN === "number") {
|
|
1197
|
+
lastWindowStartSnRef.current = details.startSN;
|
|
1198
|
+
}
|
|
1199
|
+
if (typeof details.endSN === "number") {
|
|
1200
|
+
lastWindowEndSnRef.current = Math.max(details.endSN - 1, details.startSN ?? details.endSN);
|
|
1201
|
+
}
|
|
1202
|
+
if (Array.isArray(details.fragments) && details.fragments.length > 0) {
|
|
1203
|
+
const firstSn = details.fragments[0]?.sn;
|
|
1204
|
+
const lastSn = details.fragments[details.fragments.length - 1]?.sn;
|
|
1205
|
+
if (typeof firstSn === "number") {
|
|
1206
|
+
lastWindowStartSnRef.current = firstSn;
|
|
1207
|
+
}
|
|
1208
|
+
if (typeof lastSn === "number") {
|
|
1209
|
+
lastWindowEndSnRef.current = lastSn;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
if (!details.endList) {
|
|
1214
|
+
const now = Date.now();
|
|
1215
|
+
const fragments = Array.isArray(details.fragments) ? details.fragments : [];
|
|
1216
|
+
const lastFragment = fragments.length ? fragments[fragments.length - 1] : void 0;
|
|
1217
|
+
const segmentTimestampMs = getLatestFragmentTimestampMs(fragments, true);
|
|
1218
|
+
const enforceSegmentAge = isR2StreamRef.current;
|
|
1219
|
+
if (segmentTimestampMs !== null) {
|
|
1220
|
+
lastSegmentTimestampMsRef.current = segmentTimestampMs;
|
|
1221
|
+
}
|
|
1222
|
+
const freshness = evaluateSegmentFreshness({
|
|
1223
|
+
segmentTimestampMs,
|
|
1224
|
+
fallbackTimestampMs: getProgramDateTimeMs(lastFragment?.programDateTime),
|
|
1225
|
+
enforceSegmentAge
|
|
1226
|
+
});
|
|
1227
|
+
if (!freshness.isFresh) {
|
|
1228
|
+
markStaleStream(freshness.reason || "segment stale");
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
const endSn = typeof details.endSN === "number" ? details.endSN : lastFragment?.sn;
|
|
1232
|
+
if (typeof endSn === "number") {
|
|
1233
|
+
if (lastManifestEndSnRef.current === endSn) {
|
|
1234
|
+
const lastUpdatedAt = lastManifestEndSnUpdatedAtRef.current;
|
|
1235
|
+
if (lastUpdatedAt && now - lastUpdatedAt > manifestStaleThresholdMs) {
|
|
1236
|
+
markStaleStream(`sequence stalled at ${endSn}`);
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
} else {
|
|
1240
|
+
lastManifestEndSnRef.current = endSn;
|
|
1241
|
+
lastManifestEndSnUpdatedAtRef.current = now;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
debugLog("[HLS] Level loaded", {
|
|
1246
|
+
targetduration: details.targetduration,
|
|
1247
|
+
edge: details.edge,
|
|
1248
|
+
fragments: data.details?.fragments?.length
|
|
1249
|
+
});
|
|
1250
|
+
if (isR2Stream && !forcedLiveStartRef.current) {
|
|
1251
|
+
forcedLiveStartRef.current = true;
|
|
1252
|
+
seekToLiveEdge();
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
1255
|
+
hls.on(Hls__default.default.Events.MANIFEST_LOADED, () => {
|
|
1256
|
+
if (!isR2Stream) return;
|
|
1257
|
+
lastManifestLoadRef.current = Date.now();
|
|
1258
|
+
resetManifestRetry();
|
|
1259
|
+
});
|
|
1260
|
+
hls.on(Hls__default.default.Events.FRAG_LOADING, (_event, data) => {
|
|
1261
|
+
if (staleManifestTriggeredRef.current) return;
|
|
1262
|
+
const frag = data?.frag;
|
|
1263
|
+
if (!frag || frag.sn === "initSegment") return;
|
|
1264
|
+
const enforceSegmentAge = isR2StreamRef.current;
|
|
1265
|
+
const segmentTimestampMs = getFragmentTimestampMs(frag, true);
|
|
1266
|
+
if (segmentTimestampMs !== null) {
|
|
1267
|
+
lastSegmentTimestampMsRef.current = segmentTimestampMs;
|
|
1268
|
+
}
|
|
1269
|
+
const freshness = evaluateSegmentFreshness({
|
|
1270
|
+
segmentTimestampMs,
|
|
1271
|
+
fallbackTimestampMs: getProgramDateTimeMs(frag.programDateTime),
|
|
1272
|
+
enforceSegmentAge
|
|
1273
|
+
});
|
|
1274
|
+
if (!freshness.isFresh) {
|
|
1275
|
+
if (frag.loader?.abort) {
|
|
1276
|
+
frag.loader.abort();
|
|
1277
|
+
}
|
|
1278
|
+
markStaleStream(freshness.reason || "segment stale");
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
hls.on(Hls__default.default.Events.FRAG_LOADED, (_event, data) => {
|
|
1282
|
+
if (!isR2Stream) return;
|
|
1283
|
+
lastFragLoadRef.current = Date.now();
|
|
1284
|
+
if (typeof data?.frag?.sn === "number") {
|
|
1285
|
+
lastFragSnRef.current = data.frag.sn;
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
hls.on(Hls__default.default.Events.FRAG_BUFFERED, () => {
|
|
1289
|
+
attemptPlay();
|
|
1290
|
+
});
|
|
1291
|
+
video.addEventListener("waiting", handleWaiting);
|
|
1292
|
+
video.addEventListener("timeupdate", handleTimeUpdate);
|
|
1293
|
+
video.addEventListener("loadedmetadata", handleLoadedMetadata);
|
|
1294
|
+
video.addEventListener("canplay", handleCanPlay);
|
|
1295
|
+
video.addEventListener("ended", handleEnded);
|
|
1296
|
+
startStallDetection();
|
|
1297
|
+
startPlaybackGovernor();
|
|
1298
|
+
startManifestWatchdog();
|
|
1299
|
+
startManifestWatchdog();
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
if (canUseNative) {
|
|
1303
|
+
isNativeHlsRef.current = true;
|
|
1304
|
+
console.log("[HLS] Using native HLS");
|
|
1305
|
+
nativeStreamUrlRef.current = resolvedSrc;
|
|
1306
|
+
activeStreamUrlRef.current = resolvedSrc;
|
|
1307
|
+
if (isR2Stream) {
|
|
1308
|
+
const isFresh = await ensureManifestFreshness(resolvedSrc, true, "native start");
|
|
1309
|
+
if (!isFresh) {
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
video.src = resolvedSrc;
|
|
1314
|
+
video.addEventListener("waiting", handleWaiting);
|
|
1315
|
+
video.addEventListener("loadedmetadata", handleLoadedMetadata);
|
|
1316
|
+
video.addEventListener("canplay", handleCanPlay);
|
|
1317
|
+
video.addEventListener("ended", handleEnded);
|
|
1318
|
+
video.addEventListener("error", handleNativeError);
|
|
1319
|
+
startPlaybackGovernor();
|
|
1320
|
+
startManifestWatchdog();
|
|
1321
|
+
if (isR2Stream) {
|
|
1322
|
+
startNativeFreshnessMonitor(resolvedSrc);
|
|
1323
|
+
}
|
|
1324
|
+
attemptPlay();
|
|
1325
|
+
} else {
|
|
1326
|
+
console.error("[HLS] HLS not supported");
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
initialize();
|
|
1330
|
+
return () => {
|
|
1331
|
+
isCancelled = true;
|
|
1332
|
+
cleanup();
|
|
1333
|
+
};
|
|
1334
|
+
}, [src, shouldPlay, restartKey, isPermanentlyFailed]);
|
|
1335
|
+
return {
|
|
1336
|
+
restartKey,
|
|
1337
|
+
isNativeHls: isNativeHlsRef.current,
|
|
1338
|
+
isStale,
|
|
1339
|
+
staleReason,
|
|
1340
|
+
lastSegmentTimestampMs: lastSegmentTimestampMsRef.current
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// src/lib/hooks/useHlsStreamWithCropping.ts
|
|
1345
|
+
function useHlsStreamWithCropping(videoRef, canvasRef, options) {
|
|
1346
|
+
const { src, shouldPlay, cropping, canvasFps = 30, useRAF = true, onFatalError, hlsConfig } = options;
|
|
1347
|
+
const animationFrameRef = React.useRef(null);
|
|
1348
|
+
const intervalRef = React.useRef(null);
|
|
1349
|
+
const isDrawingRef = React.useRef(false);
|
|
1350
|
+
const hlsState = useHlsStream(videoRef, { src, shouldPlay, onFatalError, hlsConfig });
|
|
1351
|
+
const calculateCropRect = React.useCallback((video, cropping2) => {
|
|
1352
|
+
const videoWidth = video.videoWidth;
|
|
1353
|
+
const videoHeight = video.videoHeight;
|
|
1354
|
+
const sx = cropping2.x / 100 * videoWidth;
|
|
1355
|
+
const sy = cropping2.y / 100 * videoHeight;
|
|
1356
|
+
const sw = cropping2.width / 100 * videoWidth;
|
|
1357
|
+
const sh = cropping2.height / 100 * videoHeight;
|
|
1358
|
+
return { sx, sy, sw, sh };
|
|
1359
|
+
}, []);
|
|
1360
|
+
const drawFrame = React.useCallback(() => {
|
|
1361
|
+
const video = videoRef.current;
|
|
1362
|
+
const canvas = canvasRef.current;
|
|
1363
|
+
if (!video || !canvas || !cropping) return;
|
|
1364
|
+
const ctx = canvas.getContext("2d");
|
|
1365
|
+
if (!ctx) return;
|
|
1366
|
+
if (video.readyState < 2) return;
|
|
1367
|
+
try {
|
|
1368
|
+
const videoWidth = video.videoWidth;
|
|
1369
|
+
const videoHeight = video.videoHeight;
|
|
1370
|
+
if (!videoWidth || !videoHeight) return;
|
|
1371
|
+
const { sx, sy, sw, sh } = calculateCropRect(video, cropping);
|
|
1372
|
+
const canvasContainer = canvas.parentElement;
|
|
1373
|
+
if (canvasContainer) {
|
|
1374
|
+
const containerWidth = canvasContainer.clientWidth;
|
|
1375
|
+
const containerHeight = canvasContainer.clientHeight;
|
|
1376
|
+
if (canvas.width !== containerWidth || canvas.height !== containerHeight) {
|
|
1377
|
+
canvas.width = containerWidth;
|
|
1378
|
+
canvas.height = containerHeight;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
1382
|
+
ctx.drawImage(
|
|
1383
|
+
video,
|
|
1384
|
+
sx,
|
|
1385
|
+
sy,
|
|
1386
|
+
sw,
|
|
1387
|
+
sh,
|
|
1388
|
+
// Source rectangle (cropped portion)
|
|
1389
|
+
0,
|
|
1390
|
+
0,
|
|
1391
|
+
canvas.width,
|
|
1392
|
+
canvas.height
|
|
1393
|
+
// Destination rectangle (full canvas)
|
|
1394
|
+
);
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
console.warn("Canvas drawing error:", err);
|
|
1397
|
+
}
|
|
1398
|
+
}, [videoRef, canvasRef, cropping, calculateCropRect]);
|
|
1399
|
+
const startCanvasRendering = React.useCallback(() => {
|
|
1400
|
+
if (isDrawingRef.current) return;
|
|
1401
|
+
isDrawingRef.current = true;
|
|
1402
|
+
if (useRAF) {
|
|
1403
|
+
const animate = () => {
|
|
1404
|
+
drawFrame();
|
|
1405
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
1406
|
+
};
|
|
1407
|
+
animate();
|
|
1408
|
+
} else {
|
|
1409
|
+
const frameInterval = 1e3 / canvasFps;
|
|
1410
|
+
intervalRef.current = setInterval(drawFrame, frameInterval);
|
|
1411
|
+
}
|
|
1412
|
+
}, [drawFrame, canvasFps, useRAF]);
|
|
1413
|
+
const stopCanvasRendering = React.useCallback(() => {
|
|
1414
|
+
isDrawingRef.current = false;
|
|
1415
|
+
if (animationFrameRef.current) {
|
|
1416
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
1417
|
+
animationFrameRef.current = null;
|
|
1418
|
+
}
|
|
1419
|
+
if (intervalRef.current) {
|
|
1420
|
+
clearInterval(intervalRef.current);
|
|
1421
|
+
intervalRef.current = null;
|
|
1422
|
+
}
|
|
1423
|
+
}, []);
|
|
1424
|
+
React.useEffect(() => {
|
|
1425
|
+
const video = videoRef.current;
|
|
1426
|
+
if (!video || !cropping || !shouldPlay) {
|
|
1427
|
+
stopCanvasRendering();
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
const handlePlay = () => {
|
|
1431
|
+
if (cropping) {
|
|
1432
|
+
startCanvasRendering();
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
const handlePause = () => {
|
|
1436
|
+
stopCanvasRendering();
|
|
1437
|
+
};
|
|
1438
|
+
const handleEnded = () => {
|
|
1439
|
+
stopCanvasRendering();
|
|
1440
|
+
};
|
|
1441
|
+
video.addEventListener("play", handlePlay);
|
|
1442
|
+
video.addEventListener("pause", handlePause);
|
|
1443
|
+
video.addEventListener("ended", handleEnded);
|
|
1444
|
+
if (!video.paused && video.readyState >= 2) {
|
|
1445
|
+
startCanvasRendering();
|
|
1446
|
+
}
|
|
1447
|
+
return () => {
|
|
1448
|
+
stopCanvasRendering();
|
|
1449
|
+
video.removeEventListener("play", handlePlay);
|
|
1450
|
+
video.removeEventListener("pause", handlePause);
|
|
1451
|
+
video.removeEventListener("ended", handleEnded);
|
|
1452
|
+
};
|
|
1453
|
+
}, [videoRef, cropping, shouldPlay, startCanvasRendering, stopCanvasRendering]);
|
|
1454
|
+
return {
|
|
1455
|
+
...hlsState,
|
|
1456
|
+
isCanvasRendering: isDrawingRef.current
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// src/lib/utils/dashboardReload.ts
|
|
1461
|
+
var reloadAttempts = [];
|
|
1462
|
+
var createThrottledReload = (interval = 5e3, maxReloads = 3) => {
|
|
1463
|
+
const circuitBreakerWindow = 1e4;
|
|
1464
|
+
const circuitBreakerThreshold = 5;
|
|
1465
|
+
let last = 0;
|
|
1466
|
+
let queued = false;
|
|
1467
|
+
let reloadCount = 0;
|
|
1468
|
+
let firstReloadTime = 0;
|
|
1469
|
+
const resetWindow = 6e4;
|
|
1470
|
+
const doReload = () => {
|
|
1471
|
+
if (typeof window !== "undefined") {
|
|
1472
|
+
const now = Date.now();
|
|
1473
|
+
reloadAttempts.push(now);
|
|
1474
|
+
const cutoff = now - circuitBreakerWindow;
|
|
1475
|
+
const recentAttempts = reloadAttempts.filter((t) => t > cutoff);
|
|
1476
|
+
reloadAttempts.length = 0;
|
|
1477
|
+
reloadAttempts.push(...recentAttempts);
|
|
1478
|
+
if (recentAttempts.length >= circuitBreakerThreshold) {
|
|
1479
|
+
console.error(`[Dashboard Reload] Circuit breaker triggered! ${recentAttempts.length} reload attempts in ${circuitBreakerWindow}ms`);
|
|
1480
|
+
const errorDiv = document.createElement("div");
|
|
1481
|
+
errorDiv.id = "reload-circuit-breaker";
|
|
1482
|
+
errorDiv.style.cssText = `
|
|
1483
|
+
position: fixed;
|
|
1484
|
+
top: 20px;
|
|
1485
|
+
left: 50%;
|
|
1486
|
+
transform: translateX(-50%);
|
|
1487
|
+
background: #dc2626;
|
|
1488
|
+
color: white;
|
|
1489
|
+
padding: 20px 32px;
|
|
1490
|
+
border-radius: 12px;
|
|
1491
|
+
z-index: 99999;
|
|
1492
|
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
|
|
1493
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
1494
|
+
`;
|
|
1495
|
+
errorDiv.innerHTML = `
|
|
1496
|
+
<div style="display: flex; align-items: center; gap: 16px;">
|
|
1497
|
+
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1498
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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"></path>
|
|
1499
|
+
</svg>
|
|
1500
|
+
<div>
|
|
1501
|
+
<div style="font-weight: 600; font-size: 16px; margin-bottom: 4px;">Too many reload attempts detected</div>
|
|
1502
|
+
<div style="font-size: 14px; opacity: 0.9;">Please check your network connection and refresh manually.</div>
|
|
1503
|
+
</div>
|
|
1504
|
+
</div>
|
|
1505
|
+
`;
|
|
1506
|
+
const existing = document.getElementById("reload-circuit-breaker");
|
|
1507
|
+
if (existing) existing.remove();
|
|
1508
|
+
document.body.appendChild(errorDiv);
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
if (now - firstReloadTime > resetWindow) {
|
|
1512
|
+
reloadCount = 0;
|
|
1513
|
+
firstReloadTime = now;
|
|
1514
|
+
}
|
|
1515
|
+
if (reloadCount === 0) {
|
|
1516
|
+
firstReloadTime = now;
|
|
1517
|
+
}
|
|
1518
|
+
reloadCount++;
|
|
1519
|
+
if (reloadCount > maxReloads) {
|
|
1520
|
+
console.error(`[Dashboard Reload] Maximum reload attempts (${maxReloads}) reached. Stopping to prevent infinite loop.`);
|
|
1521
|
+
const errorDiv = document.createElement("div");
|
|
1522
|
+
errorDiv.style.cssText = `
|
|
1523
|
+
position: fixed;
|
|
1524
|
+
top: 20px;
|
|
1525
|
+
left: 50%;
|
|
1526
|
+
transform: translateX(-50%);
|
|
1527
|
+
background: #ef4444;
|
|
1528
|
+
color: white;
|
|
1529
|
+
padding: 16px 24px;
|
|
1530
|
+
border-radius: 8px;
|
|
1531
|
+
z-index: 9999;
|
|
1532
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
1533
|
+
`;
|
|
1534
|
+
errorDiv.innerHTML = `
|
|
1535
|
+
<div style="display: flex; align-items: center; gap: 12px;">
|
|
1536
|
+
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1537
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
1538
|
+
</svg>
|
|
1539
|
+
<span>Stream connection failed. Please refresh the page manually.</span>
|
|
1540
|
+
</div>
|
|
1541
|
+
`;
|
|
1542
|
+
document.body.appendChild(errorDiv);
|
|
1543
|
+
setTimeout(() => {
|
|
1544
|
+
document.body.removeChild(errorDiv);
|
|
1545
|
+
}, 1e4);
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
console.warn(`[Dashboard Reload] Reloading dashboard (attempt ${reloadCount}/${maxReloads})`);
|
|
1549
|
+
window.location.reload();
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
return () => {
|
|
1553
|
+
const now = Date.now();
|
|
1554
|
+
if (now - last >= interval) {
|
|
1555
|
+
last = now;
|
|
1556
|
+
doReload();
|
|
1557
|
+
} else if (!queued) {
|
|
1558
|
+
queued = true;
|
|
1559
|
+
setTimeout(() => {
|
|
1560
|
+
queued = false;
|
|
1561
|
+
last = Date.now();
|
|
1562
|
+
doReload();
|
|
1563
|
+
}, interval - (now - last));
|
|
1564
|
+
}
|
|
1565
|
+
};
|
|
1566
|
+
};
|
|
1567
|
+
var throttledReloadDashboard = createThrottledReload(5e3, 3);
|
|
1568
|
+
|
|
1569
|
+
// src/lib/utils/rateLimit.ts
|
|
1570
|
+
var rateLimitMap = /* @__PURE__ */ new Map();
|
|
1571
|
+
function checkRateLimit(identifier, options = {}) {
|
|
1572
|
+
const windowMs = options.windowMs || 60 * 1e3;
|
|
1573
|
+
const maxRequests = options.maxRequests || 3;
|
|
1574
|
+
const now = Date.now();
|
|
1575
|
+
const userLimit = rateLimitMap.get(identifier);
|
|
1576
|
+
if (userLimit && userLimit.resetTime < now) {
|
|
1577
|
+
rateLimitMap.delete(identifier);
|
|
1578
|
+
}
|
|
1579
|
+
if (!userLimit || userLimit.resetTime < now) {
|
|
1580
|
+
rateLimitMap.set(identifier, {
|
|
1581
|
+
count: 1,
|
|
1582
|
+
resetTime: now + windowMs
|
|
1583
|
+
});
|
|
1584
|
+
return { allowed: true };
|
|
1585
|
+
}
|
|
1586
|
+
if (userLimit.count >= maxRequests) {
|
|
1587
|
+
const retryAfter = Math.ceil((userLimit.resetTime - now) / 1e3);
|
|
1588
|
+
return { allowed: false, retryAfter };
|
|
1589
|
+
}
|
|
1590
|
+
userLimit.count++;
|
|
1591
|
+
return { allowed: true };
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// src/lib/utils/sentryContext.ts
|
|
1595
|
+
function getSentry() {
|
|
1596
|
+
try {
|
|
1597
|
+
return __require("@sentry/nextjs");
|
|
1598
|
+
} catch {
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
var UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
|
|
1603
|
+
var LONG_HEX_PATTERN = /\b[0-9a-f]{24,}\b/gi;
|
|
1604
|
+
var EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
|
|
1605
|
+
var BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;
|
|
1606
|
+
var URI_PATTERN = /\b[a-z][a-z0-9+.-]{1,31}:\/\/[^\s)]+/gi;
|
|
1607
|
+
var MEDIA_PATH_PATTERN = /\b[^\s)]+\.m3u8(?:\?[^\s)]*)?/gi;
|
|
1608
|
+
var SENSITIVE_ASSIGNMENT_PATTERN = /\b([A-Za-z0-9_.-]*(?:authorization|cookie|password|secret|token|session|email|api[_-]?key)[A-Za-z0-9_.-]*)\b\s*[:=]\s*["']?[^"'\s,;)}\]]+/gi;
|
|
1609
|
+
var SENSITIVE_KEY_PATTERN = /(authorization|cookie|password|secret|token|session|email|api[_-]?key|url|stream|playlist|media)/i;
|
|
1610
|
+
var sanitizeString = (value) => {
|
|
1611
|
+
return value.replace(BEARER_PATTERN, "Bearer [redacted]").replace(SENSITIVE_ASSIGNMENT_PATTERN, (_match, key) => `${key}=[redacted]`).replace(URI_PATTERN, "[url]").replace(MEDIA_PATH_PATTERN, "[media]").replace(EMAIL_PATTERN, "[email]").replace(UUID_PATTERN, ":uuid").replace(LONG_HEX_PATTERN, ":id").replace(/\?.*$/, "").replace(/\s+/g, " ").trim();
|
|
1612
|
+
};
|
|
1613
|
+
var sanitizeRoute = (value) => {
|
|
1614
|
+
if (typeof value !== "string" || value.trim().length === 0) return void 0;
|
|
1615
|
+
try {
|
|
1616
|
+
const parsed = new URL(value, "https://optifye.local");
|
|
1617
|
+
return sanitizeString(parsed.pathname);
|
|
1618
|
+
} catch {
|
|
1619
|
+
return sanitizeString(value.split("?")[0] || value);
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
var sanitizeValue = (value, depth = 0) => {
|
|
1623
|
+
if (depth > 3) return "[truncated]";
|
|
1624
|
+
if (typeof value === "string") return sanitizeString(value);
|
|
1625
|
+
if (typeof value === "number" || typeof value === "boolean" || value === null || value === void 0) return value;
|
|
1626
|
+
if (Array.isArray(value)) {
|
|
1627
|
+
return value.slice(0, 20).map((item) => sanitizeValue(item, depth + 1));
|
|
1628
|
+
}
|
|
1629
|
+
if (typeof value === "object") {
|
|
1630
|
+
const sanitized = {};
|
|
1631
|
+
Object.entries(value).forEach(([key, item]) => {
|
|
1632
|
+
sanitized[key] = SENSITIVE_KEY_PATTERN.test(key) ? "[redacted]" : sanitizeValue(item, depth + 1);
|
|
1633
|
+
});
|
|
1634
|
+
return sanitized;
|
|
1635
|
+
}
|
|
1636
|
+
return String(value);
|
|
1637
|
+
};
|
|
1638
|
+
var sanitizeExtras = (extras) => {
|
|
1639
|
+
if (!extras) return void 0;
|
|
1640
|
+
return sanitizeValue(extras);
|
|
1641
|
+
};
|
|
1642
|
+
function addSentryBreadcrumb(message, options = {}) {
|
|
1643
|
+
const sentry = getSentry();
|
|
1644
|
+
if (!sentry?.addBreadcrumb) return;
|
|
1645
|
+
const route = sanitizeRoute(options.route || options.extras?.route || options.extras?.endpoint || options.extras?.url);
|
|
1646
|
+
const data = sanitizeExtras({
|
|
1647
|
+
surface: options.surface,
|
|
1648
|
+
route,
|
|
1649
|
+
status: options.status,
|
|
1650
|
+
...options.extras
|
|
1651
|
+
});
|
|
1652
|
+
sentry.addBreadcrumb({
|
|
1653
|
+
category: options.category || options.surface || "frontend",
|
|
1654
|
+
message: sanitizeString(message),
|
|
1655
|
+
level: options.severity || "info",
|
|
1656
|
+
data
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// src/lib/services/mixpanelService.ts
|
|
1661
|
+
var ROOT_DASHBOARD_EVENT_NAMES = {
|
|
1662
|
+
operations_overview: "Operations Overview Page Clicked",
|
|
1663
|
+
monitor: "Live Monitor Clicked"
|
|
1664
|
+
};
|
|
1665
|
+
var MIXPANEL_EVENT_NAME_ALIASES = {
|
|
1666
|
+
"Operations Overview clicked": ROOT_DASHBOARD_EVENT_NAMES.operations_overview,
|
|
1667
|
+
"monitor page clicked": ROOT_DASHBOARD_EVENT_NAMES.monitor
|
|
1668
|
+
};
|
|
1669
|
+
var MAX_QUEUED_MIXPANEL_ACTIONS = 200;
|
|
1670
|
+
var isMixpanelInitialized = false;
|
|
1671
|
+
var queuedMixpanelActions = [];
|
|
1672
|
+
var MIXPANEL_WARNING_RATE_LIMIT = {
|
|
1673
|
+
windowMs: 10 * 60 * 1e3,
|
|
1674
|
+
// 10 minutes
|
|
1675
|
+
maxRequests: 2
|
|
1676
|
+
};
|
|
1677
|
+
var baseMixpanelExtras = () => ({
|
|
1678
|
+
isInitialized: isMixpanelInitialized,
|
|
1679
|
+
environment: process.env.NODE_ENV
|
|
1680
|
+
});
|
|
1681
|
+
var shouldReportMixpanel = (key, isError) => {
|
|
1682
|
+
const options = MIXPANEL_WARNING_RATE_LIMIT;
|
|
1683
|
+
return checkRateLimit(`mixpanel:${key}`, options).allowed;
|
|
1684
|
+
};
|
|
1685
|
+
var reportMixpanelWarning = (key, message, extras) => {
|
|
1686
|
+
if (!shouldReportMixpanel(key)) return;
|
|
1687
|
+
addSentryBreadcrumb(message, {
|
|
1688
|
+
surface: "mixpanel",
|
|
1689
|
+
severity: "warning",
|
|
1690
|
+
extras: {
|
|
1691
|
+
key,
|
|
1692
|
+
...baseMixpanelExtras(),
|
|
1693
|
+
...extras
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1696
|
+
};
|
|
1697
|
+
var normalizeCoreEventName = (eventName) => {
|
|
1698
|
+
const canonicalName = MIXPANEL_EVENT_NAME_ALIASES[eventName] || eventName;
|
|
1699
|
+
if (canonicalName !== eventName) {
|
|
1700
|
+
reportMixpanelWarning(`event_alias:${eventName}`, "Mixpanel event alias rewritten to canonical name", {
|
|
1701
|
+
operation: "track_event",
|
|
1702
|
+
originalEventName: eventName,
|
|
1703
|
+
canonicalEventName: canonicalName
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
return canonicalName;
|
|
1707
|
+
};
|
|
1708
|
+
var queueMixpanelAction = (action) => {
|
|
1709
|
+
if (queuedMixpanelActions.length >= MAX_QUEUED_MIXPANEL_ACTIONS) {
|
|
1710
|
+
queuedMixpanelActions.shift();
|
|
1711
|
+
reportMixpanelWarning("queue_overflow", "Mixpanel queue overflow, dropping oldest queued action", {
|
|
1712
|
+
operation: "queue",
|
|
1713
|
+
maxQueuedActions: MAX_QUEUED_MIXPANEL_ACTIONS
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
queuedMixpanelActions.push(action);
|
|
1717
|
+
};
|
|
1718
|
+
var trackCoreEvent = (eventName, properties) => {
|
|
1719
|
+
const normalizedEventName = normalizeCoreEventName(eventName);
|
|
1720
|
+
{
|
|
1721
|
+
const mergedProps2 = {
|
|
1722
|
+
...{},
|
|
1723
|
+
...properties || {}
|
|
1724
|
+
};
|
|
1725
|
+
queueMixpanelAction({
|
|
1726
|
+
type: "track",
|
|
1727
|
+
eventName: normalizedEventName,
|
|
1728
|
+
properties: mergedProps2
|
|
1729
|
+
});
|
|
1730
|
+
reportMixpanelWarning("track_event_queued", "Mixpanel not initialized yet: event queued", {
|
|
1731
|
+
operation: "track_event",
|
|
1732
|
+
eventName: normalizedEventName
|
|
1733
|
+
});
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
|
|
1738
|
+
// src/lib/constants/videoGridMetricMode.ts
|
|
1739
|
+
var VALID_VIDEO_GRID_METRIC_MODES = /* @__PURE__ */ new Set([
|
|
1740
|
+
"efficiency",
|
|
1741
|
+
"recent_flow",
|
|
1742
|
+
"recent_flow_wip_gated"
|
|
1743
|
+
]);
|
|
1744
|
+
var normalizeVideoGridMetricMode = (value, assemblyEnabled = false) => {
|
|
1745
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
1746
|
+
if (VALID_VIDEO_GRID_METRIC_MODES.has(normalized)) {
|
|
1747
|
+
return normalized;
|
|
1748
|
+
}
|
|
1749
|
+
return assemblyEnabled ? "recent_flow_wip_gated" : "efficiency";
|
|
1750
|
+
};
|
|
1751
|
+
var isRecentFlowVideoGridMetricMode = (value, assemblyEnabled = false) => {
|
|
1752
|
+
const normalized = normalizeVideoGridMetricMode(value, assemblyEnabled);
|
|
1753
|
+
return normalized === "recent_flow" || normalized === "recent_flow_wip_gated";
|
|
1754
|
+
};
|
|
1755
|
+
var isWipGatedVideoGridMetricMode = (value, assemblyEnabled = false) => normalizeVideoGridMetricMode(value, assemblyEnabled) === "recent_flow_wip_gated";
|
|
1756
|
+
|
|
1757
|
+
// src/components/dashboard/grid/videoGridMetricUtils.ts
|
|
1758
|
+
var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
1759
|
+
var isVideoGridRecentFlowEnabled = (workspace) => isRecentFlowVideoGridMetricMode(
|
|
1760
|
+
workspace.video_grid_metric_mode,
|
|
1761
|
+
workspace.assembly_enabled === true
|
|
1762
|
+
);
|
|
1763
|
+
var isVideoGridWipGated = (workspace) => isWipGatedVideoGridMetricMode(
|
|
1764
|
+
workspace.video_grid_metric_mode,
|
|
1765
|
+
workspace.assembly_enabled === true
|
|
1766
|
+
);
|
|
1767
|
+
var hasVideoGridRecentFlow = (workspace) => isVideoGridRecentFlowEnabled(workspace) && isFiniteNumber(workspace.recent_flow_percent);
|
|
1768
|
+
var isVideoGridRecentFlowUnavailable = (workspace) => isVideoGridRecentFlowEnabled(workspace) && !hasVideoGridRecentFlow(workspace);
|
|
1769
|
+
var getRawVideoGridMetricValue = (workspace) => {
|
|
1770
|
+
const recentFlowPercent = workspace.recent_flow_percent;
|
|
1771
|
+
if (hasVideoGridRecentFlow(workspace) && isFiniteNumber(recentFlowPercent)) {
|
|
1772
|
+
return recentFlowPercent;
|
|
1773
|
+
}
|
|
1774
|
+
if (isVideoGridRecentFlowUnavailable(workspace)) {
|
|
1775
|
+
return null;
|
|
1776
|
+
}
|
|
1777
|
+
return workspace.efficiency;
|
|
1778
|
+
};
|
|
1779
|
+
var hasIncomingWipMapping = (workspace) => Boolean(workspace.incoming_wip_buffer_name);
|
|
1780
|
+
var getVideoGridBaseColorState = (workspace, legend = DEFAULT_EFFICIENCY_LEGEND) => {
|
|
1781
|
+
const metricValue = getRawVideoGridMetricValue(workspace);
|
|
1782
|
+
if (!isFiniteNumber(metricValue)) {
|
|
1783
|
+
return "neutral";
|
|
1784
|
+
}
|
|
1785
|
+
return getEfficiencyColor(metricValue, legend);
|
|
1786
|
+
};
|
|
1787
|
+
var isLowWipGreenOverride = (workspace, legend = DEFAULT_EFFICIENCY_LEGEND) => {
|
|
1788
|
+
if (workspace.scheduled_break_active === true) {
|
|
1789
|
+
return false;
|
|
1790
|
+
}
|
|
1791
|
+
if (workspace.recent_flow_forced_zero_after_shift === true) {
|
|
1792
|
+
return false;
|
|
1793
|
+
}
|
|
1794
|
+
if (!hasVideoGridRecentFlow(workspace) || !isVideoGridWipGated(workspace)) {
|
|
1795
|
+
return false;
|
|
1796
|
+
}
|
|
1797
|
+
if (getVideoGridBaseColorState(workspace, legend) !== "red") {
|
|
1798
|
+
return false;
|
|
1799
|
+
}
|
|
1800
|
+
if (!hasIncomingWipMapping(workspace)) {
|
|
1801
|
+
return false;
|
|
1802
|
+
}
|
|
1803
|
+
return isFiniteNumber(workspace.incoming_wip_current) && workspace.incoming_wip_current <= 1;
|
|
1804
|
+
};
|
|
1805
|
+
var isHighEfficiencyRedFlowOverride = (workspace, legend = DEFAULT_EFFICIENCY_LEGEND) => {
|
|
1806
|
+
if (workspace.scheduled_break_active === true) {
|
|
1807
|
+
return false;
|
|
1808
|
+
}
|
|
1809
|
+
if (workspace.recent_flow_forced_zero_after_shift === true) {
|
|
1810
|
+
return false;
|
|
1811
|
+
}
|
|
1812
|
+
if (!hasVideoGridRecentFlow(workspace) || !isVideoGridWipGated(workspace)) {
|
|
1813
|
+
return false;
|
|
1814
|
+
}
|
|
1815
|
+
if (isLowWipGreenOverride(workspace, legend)) {
|
|
1816
|
+
return false;
|
|
1817
|
+
}
|
|
1818
|
+
if (getVideoGridBaseColorState(workspace, legend) !== "red") {
|
|
1819
|
+
return false;
|
|
1820
|
+
}
|
|
1821
|
+
return isFiniteNumber(workspace.efficiency) && workspace.efficiency > 100;
|
|
1822
|
+
};
|
|
1823
|
+
var getVideoGridMetricValue = (workspace, legend = DEFAULT_EFFICIENCY_LEGEND) => isHighEfficiencyRedFlowOverride(workspace, legend) ? workspace.efficiency : getRawVideoGridMetricValue(workspace);
|
|
1824
|
+
var toMinuteBucket = (minuteBucket) => Number.isFinite(minuteBucket) ? Math.floor(minuteBucket) : Math.floor(Date.now() / 6e4);
|
|
1825
|
+
var getEffectiveFlowMinuteBucket = (workspace) => {
|
|
1826
|
+
const effectiveAt = workspace.recent_flow_effective_end_at;
|
|
1827
|
+
if (!effectiveAt) {
|
|
1828
|
+
return void 0;
|
|
1829
|
+
}
|
|
1830
|
+
const timestamp = Date.parse(effectiveAt);
|
|
1831
|
+
if (!Number.isFinite(timestamp)) {
|
|
1832
|
+
return void 0;
|
|
1833
|
+
}
|
|
1834
|
+
return Math.floor(timestamp / 6e4);
|
|
1835
|
+
};
|
|
1836
|
+
var hashWorkspaceKey = (workspace) => {
|
|
1837
|
+
const workspaceKey = workspace.workspace_uuid || workspace.workspace_name || "unknown";
|
|
1838
|
+
let hash = 0;
|
|
1839
|
+
for (let index = 0; index < workspaceKey.length; index += 1) {
|
|
1840
|
+
hash = (hash * 31 + workspaceKey.charCodeAt(index)) % 2147483647;
|
|
1841
|
+
}
|
|
1842
|
+
return hash;
|
|
1843
|
+
};
|
|
1844
|
+
var getSyntheticLowWipDisplayValue = (workspace, minuteBucket) => {
|
|
1845
|
+
const bucket = getEffectiveFlowMinuteBucket(workspace) ?? toMinuteBucket(minuteBucket);
|
|
1846
|
+
const offset = (hashWorkspaceKey(workspace) % 11 + bucket % 11 + 11) % 11;
|
|
1847
|
+
return 100 + offset;
|
|
1848
|
+
};
|
|
1849
|
+
var getVideoGridDisplayValue = (workspace, legend = DEFAULT_EFFICIENCY_LEGEND, minuteBucket) => isLowWipGreenOverride(workspace, legend) ? getSyntheticLowWipDisplayValue(workspace, minuteBucket) : getVideoGridMetricValue(workspace, legend);
|
|
1850
|
+
var getVideoGridColorState = (workspace, legend = DEFAULT_EFFICIENCY_LEGEND) => {
|
|
1851
|
+
const baseColor = getVideoGridBaseColorState(workspace, legend);
|
|
1852
|
+
if (!hasVideoGridRecentFlow(workspace)) {
|
|
1853
|
+
return baseColor;
|
|
1854
|
+
}
|
|
1855
|
+
if (!isVideoGridWipGated(workspace)) {
|
|
1856
|
+
return baseColor;
|
|
1857
|
+
}
|
|
1858
|
+
if (baseColor !== "red") {
|
|
1859
|
+
return baseColor;
|
|
1860
|
+
}
|
|
1861
|
+
if (isLowWipGreenOverride(workspace, legend)) {
|
|
1862
|
+
return "green";
|
|
1863
|
+
}
|
|
1864
|
+
if (isHighEfficiencyRedFlowOverride(workspace, legend)) {
|
|
1865
|
+
return getEfficiencyColor(workspace.efficiency, legend);
|
|
1866
|
+
}
|
|
1867
|
+
if (!hasIncomingWipMapping(workspace)) {
|
|
1868
|
+
return baseColor;
|
|
1869
|
+
}
|
|
1870
|
+
if (!isFiniteNumber(workspace.incoming_wip_current)) {
|
|
1871
|
+
return "neutral";
|
|
1872
|
+
}
|
|
1873
|
+
return baseColor;
|
|
1874
|
+
};
|
|
1875
|
+
function getTrendArrowAndColor(trend) {
|
|
1876
|
+
if (trend > 0) {
|
|
1877
|
+
return { arrow: "\u2191", color: "text-green-400" };
|
|
1878
|
+
} else if (trend < 0) {
|
|
1879
|
+
return { arrow: "\u2193", color: "text-red-400" };
|
|
1880
|
+
} else {
|
|
1881
|
+
return { arrow: "\u2192", color: "text-gray-400" };
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
var VideoCard = React__default.default.memo(({
|
|
1885
|
+
workspace,
|
|
1886
|
+
hlsUrl,
|
|
1887
|
+
shouldPlay,
|
|
1888
|
+
onClick,
|
|
1889
|
+
onFatalError,
|
|
1890
|
+
legend,
|
|
1891
|
+
cropping,
|
|
1892
|
+
canvasFps = 30,
|
|
1893
|
+
useRAF = true,
|
|
1894
|
+
className = "",
|
|
1895
|
+
compact = false,
|
|
1896
|
+
displayMinuteBucket,
|
|
1897
|
+
displayName,
|
|
1898
|
+
lastSeenLabel,
|
|
1899
|
+
onMouseEnter,
|
|
1900
|
+
onMouseLeave
|
|
1901
|
+
}) => {
|
|
1902
|
+
const videoRef = React.useRef(null);
|
|
1903
|
+
const canvasRef = React.useRef(null);
|
|
1904
|
+
const effectiveLegend = legend || DEFAULT_EFFICIENCY_LEGEND;
|
|
1905
|
+
const { isStale: isStreamStale } = useHlsStreamWithCropping(videoRef, canvasRef, {
|
|
1906
|
+
src: hlsUrl,
|
|
1907
|
+
shouldPlay,
|
|
1908
|
+
cropping,
|
|
1909
|
+
canvasFps,
|
|
1910
|
+
useRAF,
|
|
1911
|
+
onFatalError: onFatalError ?? (() => throttledReloadDashboard())
|
|
1912
|
+
});
|
|
1913
|
+
const showOffline = Boolean(isStreamStale);
|
|
1914
|
+
const lastSeenText = lastSeenLabel || "Unknown";
|
|
1915
|
+
const workspaceDisplayName = displayName || workspace.displayName || workspace.workspace_name;
|
|
1916
|
+
const videoGridMetricValue = getVideoGridMetricValue(workspace, effectiveLegend);
|
|
1917
|
+
const videoGridDisplayValue = getVideoGridDisplayValue(workspace, effectiveLegend, displayMinuteBucket);
|
|
1918
|
+
const videoGridColorState = getVideoGridColorState(workspace, effectiveLegend);
|
|
1919
|
+
const isRecentFlowCard = isVideoGridRecentFlowEnabled(workspace);
|
|
1920
|
+
const isHighEfficiencyOverride = isHighEfficiencyRedFlowOverride(workspace, effectiveLegend);
|
|
1921
|
+
const hasDisplayMetric = typeof videoGridDisplayValue === "number" && Number.isFinite(videoGridDisplayValue);
|
|
1922
|
+
const hasBarMetric = typeof videoGridMetricValue === "number" && Number.isFinite(videoGridMetricValue);
|
|
1923
|
+
const shouldRenderMetricBadge = hasDisplayMetric;
|
|
1924
|
+
const badgeTitle = isHighEfficiencyOverride ? `Efficiency ${Math.round(videoGridDisplayValue ?? 0)}%` : hasVideoGridRecentFlow(workspace) ? `Flow ${Math.round(videoGridDisplayValue ?? 0)}%` : isRecentFlowCard ? "Flow unavailable" : `Efficiency ${Math.round(videoGridDisplayValue ?? 0)}%`;
|
|
1925
|
+
const badgeLabel = `${Math.round(videoGridDisplayValue ?? 0)}%`;
|
|
1926
|
+
const efficiencyOverlayClass = videoGridColorState === "green" ? "bg-[#00D654]/25" : videoGridColorState === "yellow" ? "bg-[#FFD700]/30" : videoGridColorState === "red" ? "bg-[#FF2D0A]/30" : "bg-transparent";
|
|
1927
|
+
const efficiencyBarClass = videoGridColorState === "green" ? "bg-[#00AB45]" : videoGridColorState === "yellow" ? "bg-[#FFB020]" : videoGridColorState === "red" ? "bg-[#E34329]" : "bg-gray-500/70";
|
|
1928
|
+
const efficiencyStatus = videoGridColorState === "green" ? "High" : videoGridColorState === "yellow" ? "Medium" : videoGridColorState === "red" ? "Low" : "Neutral";
|
|
1929
|
+
const trendInfo = workspace.trend !== void 0 ? getTrendArrowAndColor(workspace.trend) : null;
|
|
1930
|
+
const handleClick = React.useCallback(() => {
|
|
1931
|
+
trackCoreEvent("Workspace Card Clicked", {
|
|
1932
|
+
workspace_id: workspace.workspace_uuid,
|
|
1933
|
+
workspace_name: workspace.workspace_name,
|
|
1934
|
+
efficiency: workspace.efficiency,
|
|
1935
|
+
status: efficiencyStatus,
|
|
1936
|
+
trend: workspace.trend
|
|
1937
|
+
});
|
|
1938
|
+
if (onClick) {
|
|
1939
|
+
onClick();
|
|
1940
|
+
}
|
|
1941
|
+
}, [onClick, workspace, efficiencyStatus]);
|
|
1942
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1943
|
+
"div",
|
|
1944
|
+
{
|
|
1945
|
+
className: `workspace-card relative bg-gray-950 rounded-md overflow-hidden cursor-pointer transform hover:scale-[1.005] active:scale-[0.995] transition-transform duration-200 shadow-sm touch-manipulation ${className}`,
|
|
1946
|
+
style: { width: "100%", height: "100%" },
|
|
1947
|
+
onClick: handleClick,
|
|
1948
|
+
onMouseEnter,
|
|
1949
|
+
onMouseLeave,
|
|
1950
|
+
onTouchStart: (e) => e.currentTarget.classList.add("touch-active"),
|
|
1951
|
+
onTouchEnd: (e) => e.currentTarget.classList.remove("touch-active"),
|
|
1952
|
+
title: displayName,
|
|
1953
|
+
tabIndex: 0,
|
|
1954
|
+
"aria-label": `Open workspace ${displayName}`,
|
|
1955
|
+
onKeyDown: (e) => {
|
|
1956
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1957
|
+
e.preventDefault();
|
|
1958
|
+
handleClick();
|
|
1959
|
+
}
|
|
1960
|
+
},
|
|
1961
|
+
children: [
|
|
1962
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full h-full overflow-hidden bg-black", children: [
|
|
1963
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-black z-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "animate-pulse flex flex-col items-center", children: [
|
|
1964
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Camera, { className: `w-5 h-5 sm:${compact ? "w-4 h-4" : "w-6 h-6"} text-gray-500` }),
|
|
1965
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-[11px] sm:${compact ? "text-[10px]" : "text-xs"} text-gray-500 mt-1`, children: "Loading..." })
|
|
1966
|
+
] }) }),
|
|
1967
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute inset-0 z-10", children: [
|
|
1968
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1969
|
+
"video",
|
|
1970
|
+
{
|
|
1971
|
+
ref: videoRef,
|
|
1972
|
+
className: `h-full w-full object-cover ${cropping ? "hidden" : ""}`,
|
|
1973
|
+
playsInline: true,
|
|
1974
|
+
muted: true,
|
|
1975
|
+
disablePictureInPicture: true,
|
|
1976
|
+
controlsList: "nodownload noplaybackrate"
|
|
1977
|
+
}
|
|
1978
|
+
),
|
|
1979
|
+
cropping && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1980
|
+
"canvas",
|
|
1981
|
+
{
|
|
1982
|
+
ref: canvasRef,
|
|
1983
|
+
className: "h-full w-full object-cover"
|
|
1984
|
+
}
|
|
1985
|
+
)
|
|
1986
|
+
] }),
|
|
1987
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `absolute inset-0 z-20 pointer-events-none ${efficiencyOverlayClass}` }),
|
|
1988
|
+
showOffline && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 z-40 flex items-center justify-center bg-black/70 px-2 text-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1", children: [
|
|
1989
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { className: `${compact ? "w-4 h-4" : "w-5 h-5"} text-amber-300` }),
|
|
1990
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: `font-semibold text-white ${compact ? "text-[11px]" : "text-xs"}`, children: "Not streaming" }),
|
|
1991
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: `text-gray-200 ${compact ? "text-[10px]" : "text-[11px]"}`, children: [
|
|
1992
|
+
"Last seen: ",
|
|
1993
|
+
lastSeenText
|
|
1994
|
+
] })
|
|
1995
|
+
] }) }),
|
|
1996
|
+
shouldRenderMetricBadge && /* @__PURE__ */ jsxRuntime.jsx("div", { className: `absolute ${compact ? "top-1 right-1" : "top-2 right-2"} z-30`, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1997
|
+
"div",
|
|
1998
|
+
{
|
|
1999
|
+
"data-testid": "video-card-metric-badge",
|
|
2000
|
+
className: `bg-black/70 backdrop-blur-sm rounded ${compact ? "px-1.5 py-1" : "px-2 py-1"} text-white border border-white/10`,
|
|
2001
|
+
title: badgeTitle,
|
|
2002
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${compact ? "text-[10px]" : "text-xs"} font-semibold`, children: badgeLabel })
|
|
2003
|
+
}
|
|
2004
|
+
) }),
|
|
2005
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `absolute bottom-0 left-0 right-0 ${compact ? "h-0.5" : "h-1"} bg-black/50 z-30`, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2006
|
+
"div",
|
|
2007
|
+
{
|
|
2008
|
+
"data-testid": "video-card-metric-bar",
|
|
2009
|
+
className: `h-full ${efficiencyBarClass} transition-all duration-500`,
|
|
2010
|
+
style: { width: `${hasBarMetric ? Math.min(100, Math.max(0, videoGridMetricValue)) : videoGridColorState === "neutral" ? 100 : 0}%` }
|
|
2011
|
+
}
|
|
2012
|
+
) })
|
|
2013
|
+
] }),
|
|
2014
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: `absolute bottom-0 left-0 right-0 bg-black bg-opacity-60 p-1.5 sm:${compact ? "p-1" : "p-1.5"} flex min-w-0 justify-between items-center gap-1 z-10`, children: [
|
|
2015
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 items-center gap-1.5", children: [
|
|
2016
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Camera, { size: compact ? 10 : 12, className: "shrink-0 text-white" }),
|
|
2017
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: `min-w-0 truncate text-white text-[11px] sm:${compact ? "text-[10px]" : "text-xs"} font-medium tracking-wide`, children: workspaceDisplayName })
|
|
2018
|
+
] }),
|
|
2019
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex shrink-0 items-center ${compact ? "gap-1" : "gap-1.5"}`, children: [
|
|
2020
|
+
trendInfo && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2021
|
+
"div",
|
|
2022
|
+
{
|
|
2023
|
+
className: `${compact ? "text-sm" : "text-lg"} ${trendInfo.color}`,
|
|
2024
|
+
style: { lineHeight: 1, display: "flex", alignItems: "center" },
|
|
2025
|
+
children: trendInfo.arrow
|
|
2026
|
+
}
|
|
2027
|
+
),
|
|
2028
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2029
|
+
"div",
|
|
2030
|
+
{
|
|
2031
|
+
className: `${compact ? "w-1 h-1" : "w-1.5 h-1.5"} rounded-full ${showOffline ? "bg-red-500" : "bg-green-500"}`
|
|
2032
|
+
}
|
|
2033
|
+
),
|
|
2034
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-white text-[11px] sm:${compact ? "text-[10px]" : "text-xs"}`, children: showOffline ? "Offline" : "Live" })
|
|
2035
|
+
] })
|
|
2036
|
+
] })
|
|
2037
|
+
]
|
|
2038
|
+
}
|
|
2039
|
+
);
|
|
2040
|
+
}, (prevProps, nextProps) => {
|
|
2041
|
+
if (prevProps.workspace.efficiency !== nextProps.workspace.efficiency || prevProps.workspace.assembly_enabled !== nextProps.workspace.assembly_enabled || prevProps.workspace.video_grid_metric_mode !== nextProps.workspace.video_grid_metric_mode || prevProps.workspace.recent_flow_percent !== nextProps.workspace.recent_flow_percent || prevProps.workspace.recent_flow_effective_end_at !== nextProps.workspace.recent_flow_effective_end_at || prevProps.workspace.recent_flow_forced_zero_after_shift !== nextProps.workspace.recent_flow_forced_zero_after_shift || prevProps.workspace.scheduled_break_active !== nextProps.workspace.scheduled_break_active || prevProps.workspace.incoming_wip_current !== nextProps.workspace.incoming_wip_current || prevProps.workspace.incoming_wip_buffer_name !== nextProps.workspace.incoming_wip_buffer_name || prevProps.workspace.trend !== nextProps.workspace.trend || prevProps.workspace.performance_score !== nextProps.workspace.performance_score || prevProps.workspace.pph !== nextProps.workspace.pph) {
|
|
2042
|
+
return false;
|
|
2043
|
+
}
|
|
2044
|
+
if (prevProps.workspace.workspace_uuid !== nextProps.workspace.workspace_uuid || prevProps.workspace.workspace_name !== nextProps.workspace.workspace_name || prevProps.workspace.line_id !== nextProps.workspace.line_id) {
|
|
2045
|
+
return false;
|
|
2046
|
+
}
|
|
2047
|
+
if (prevProps.displayName !== nextProps.displayName) {
|
|
2048
|
+
return false;
|
|
2049
|
+
}
|
|
2050
|
+
if (prevProps.lastSeenLabel !== nextProps.lastSeenLabel) {
|
|
2051
|
+
return false;
|
|
2052
|
+
}
|
|
2053
|
+
if (prevProps.legend !== nextProps.legend) {
|
|
2054
|
+
return false;
|
|
2055
|
+
}
|
|
2056
|
+
if (prevProps.compact !== nextProps.compact) {
|
|
2057
|
+
return false;
|
|
2058
|
+
}
|
|
2059
|
+
if (prevProps.displayMinuteBucket !== nextProps.displayMinuteBucket) {
|
|
2060
|
+
return false;
|
|
2061
|
+
}
|
|
2062
|
+
if (prevProps.hlsUrl !== nextProps.hlsUrl || prevProps.shouldPlay !== nextProps.shouldPlay) {
|
|
2063
|
+
return false;
|
|
2064
|
+
}
|
|
2065
|
+
if (prevProps.cropping?.x !== nextProps.cropping?.x || prevProps.cropping?.y !== nextProps.cropping?.y || prevProps.cropping?.width !== nextProps.cropping?.width || prevProps.cropping?.height !== nextProps.cropping?.height) {
|
|
2066
|
+
return false;
|
|
2067
|
+
}
|
|
2068
|
+
return true;
|
|
2069
|
+
});
|
|
2070
|
+
VideoCard.displayName = "VideoCard";
|
|
2071
|
+
var MOBILE_SCROLL_THRESHOLD = 15;
|
|
2072
|
+
var MOBILE_BREAKPOINT_PX = 640;
|
|
2073
|
+
var sortWorkspaces = (left, right) => {
|
|
2074
|
+
if (left.line_id !== right.line_id) {
|
|
2075
|
+
return left.line_id.localeCompare(right.line_id);
|
|
2076
|
+
}
|
|
2077
|
+
const leftMatch = left.workspace_name.match(/WS(\d+)/);
|
|
2078
|
+
const rightMatch = right.workspace_name.match(/WS(\d+)/);
|
|
2079
|
+
if (leftMatch && rightMatch) {
|
|
2080
|
+
return parseInt(leftMatch[1], 10) - parseInt(rightMatch[1], 10);
|
|
2081
|
+
}
|
|
2082
|
+
return left.workspace_name.localeCompare(right.workspace_name, void 0, { numeric: true });
|
|
2083
|
+
};
|
|
2084
|
+
var RecentFlowSnapshotGrid = ({
|
|
2085
|
+
workspaces,
|
|
2086
|
+
videoStreamsByWorkspaceId,
|
|
2087
|
+
legend,
|
|
2088
|
+
className = ""
|
|
2089
|
+
}) => {
|
|
2090
|
+
const containerRef = React.useRef(null);
|
|
2091
|
+
const readinessRef = React.useRef(null);
|
|
2092
|
+
const [gridCols, setGridCols] = React.useState(4);
|
|
2093
|
+
const [gridRows, setGridRows] = React.useState(1);
|
|
2094
|
+
const [isMobileScrollableGrid, setIsMobileScrollableGrid] = React.useState(false);
|
|
2095
|
+
const sortedWorkspaces = React.useMemo(
|
|
2096
|
+
() => [...workspaces || []].sort(sortWorkspaces),
|
|
2097
|
+
[workspaces]
|
|
2098
|
+
);
|
|
2099
|
+
const effectiveLegend = legend || DEFAULT_EFFICIENCY_LEGEND;
|
|
2100
|
+
const displayMinuteBucket = Math.floor(Date.now() / 6e4);
|
|
2101
|
+
const expectedVideoCount = React.useMemo(
|
|
2102
|
+
() => sortedWorkspaces.filter((workspace) => {
|
|
2103
|
+
const workspaceId = workspace.workspace_uuid || workspace.workspace_name;
|
|
2104
|
+
return Boolean(workspaceId && videoStreamsByWorkspaceId[workspaceId]?.hls_url);
|
|
2105
|
+
}).length,
|
|
2106
|
+
[sortedWorkspaces, videoStreamsByWorkspaceId]
|
|
2107
|
+
);
|
|
2108
|
+
const publishSnapshotReadiness = React.useCallback((readyVideoCount) => {
|
|
2109
|
+
const status = {
|
|
2110
|
+
expectedVideoCount,
|
|
2111
|
+
readyVideoCount,
|
|
2112
|
+
status: expectedVideoCount === 0 ? "no_videos" : readyVideoCount >= expectedVideoCount ? "ready" : "loading",
|
|
2113
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2114
|
+
};
|
|
2115
|
+
const isReady = expectedVideoCount > 0 && status.status === "ready";
|
|
2116
|
+
if (typeof window !== "undefined") {
|
|
2117
|
+
window.__OPTIFYE_SNAPSHOT_READY__ = isReady;
|
|
2118
|
+
window.__OPTIFYE_SNAPSHOT_VIDEO_STATUS__ = status;
|
|
2119
|
+
}
|
|
2120
|
+
const marker = readinessRef.current;
|
|
2121
|
+
if (marker) {
|
|
2122
|
+
marker.dataset.ready = isReady ? "true" : "false";
|
|
2123
|
+
marker.dataset.status = status.status;
|
|
2124
|
+
marker.dataset.expectedVideoCount = String(status.expectedVideoCount);
|
|
2125
|
+
marker.dataset.readyVideoCount = String(status.readyVideoCount);
|
|
2126
|
+
marker.dataset.updatedAt = status.updatedAt;
|
|
2127
|
+
}
|
|
2128
|
+
}, [expectedVideoCount]);
|
|
2129
|
+
const calculateOptimalGrid = React.useCallback(() => {
|
|
2130
|
+
if (!containerRef.current) return;
|
|
2131
|
+
const containerPadding = 16;
|
|
2132
|
+
const rawContainerWidth = containerRef.current.clientWidth;
|
|
2133
|
+
const containerWidth = rawContainerWidth - containerPadding;
|
|
2134
|
+
const containerHeight = containerRef.current.clientHeight - containerPadding;
|
|
2135
|
+
const count = sortedWorkspaces.length;
|
|
2136
|
+
if (count === 0) {
|
|
2137
|
+
setGridCols(1);
|
|
2138
|
+
setGridRows(1);
|
|
2139
|
+
setIsMobileScrollableGrid(false);
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
const shouldUseMobileScroll = rawContainerWidth < MOBILE_BREAKPOINT_PX && count >= MOBILE_SCROLL_THRESHOLD;
|
|
2143
|
+
const optimalLayouts = {
|
|
2144
|
+
1: 1,
|
|
2145
|
+
2: 2,
|
|
2146
|
+
3: 3,
|
|
2147
|
+
4: 2,
|
|
2148
|
+
5: 3,
|
|
2149
|
+
6: 3,
|
|
2150
|
+
7: 4,
|
|
2151
|
+
8: 4,
|
|
2152
|
+
9: 3,
|
|
2153
|
+
10: 5,
|
|
2154
|
+
11: 4,
|
|
2155
|
+
12: 4,
|
|
2156
|
+
13: 5,
|
|
2157
|
+
14: 5,
|
|
2158
|
+
15: 5,
|
|
2159
|
+
16: 4,
|
|
2160
|
+
17: 6,
|
|
2161
|
+
18: 6,
|
|
2162
|
+
19: 5,
|
|
2163
|
+
20: 5,
|
|
2164
|
+
21: 7,
|
|
2165
|
+
22: 6,
|
|
2166
|
+
23: 6,
|
|
2167
|
+
24: 6
|
|
2168
|
+
};
|
|
2169
|
+
let bestCols = optimalLayouts[count] || Math.ceil(Math.sqrt(count));
|
|
2170
|
+
const containerAspectRatio = containerWidth / containerHeight;
|
|
2171
|
+
const targetAspectRatio = 16 / 9;
|
|
2172
|
+
const gap = 8;
|
|
2173
|
+
if (containerAspectRatio > targetAspectRatio * 1.5 && count > 6) {
|
|
2174
|
+
bestCols = Math.min(bestCols + 1, Math.ceil(count / 2));
|
|
2175
|
+
}
|
|
2176
|
+
const minCellWidth = 100;
|
|
2177
|
+
const availableWidth = containerWidth - gap * (bestCols - 1);
|
|
2178
|
+
const cellWidth = availableWidth / bestCols;
|
|
2179
|
+
if (cellWidth < minCellWidth && bestCols > 1) {
|
|
2180
|
+
bestCols = Math.max(1, Math.floor((containerWidth + gap) / (minCellWidth + gap)));
|
|
2181
|
+
}
|
|
2182
|
+
setGridCols(bestCols);
|
|
2183
|
+
setGridRows(Math.ceil(count / bestCols));
|
|
2184
|
+
setIsMobileScrollableGrid(shouldUseMobileScroll);
|
|
2185
|
+
}, [sortedWorkspaces.length]);
|
|
2186
|
+
React.useEffect(() => {
|
|
2187
|
+
calculateOptimalGrid();
|
|
2188
|
+
window.addEventListener("resize", calculateOptimalGrid);
|
|
2189
|
+
return () => window.removeEventListener("resize", calculateOptimalGrid);
|
|
2190
|
+
}, [calculateOptimalGrid]);
|
|
2191
|
+
React.useEffect(() => {
|
|
2192
|
+
const attachedVideos = /* @__PURE__ */ new Set();
|
|
2193
|
+
const videoEvents = ["loadeddata", "canplay", "playing", "timeupdate", "error", "stalled"];
|
|
2194
|
+
const hasDecodedFrame = (video) => video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && video.videoWidth > 0 && video.videoHeight > 0;
|
|
2195
|
+
const updateReadiness = () => {
|
|
2196
|
+
const videos = Array.from(containerRef.current?.querySelectorAll("video") ?? []);
|
|
2197
|
+
const readyVideoCount = videos.filter(hasDecodedFrame).length;
|
|
2198
|
+
publishSnapshotReadiness(Math.min(readyVideoCount, expectedVideoCount));
|
|
2199
|
+
for (const video of videos) {
|
|
2200
|
+
if (attachedVideos.has(video)) continue;
|
|
2201
|
+
attachedVideos.add(video);
|
|
2202
|
+
for (const eventName of videoEvents) {
|
|
2203
|
+
video.addEventListener(eventName, updateReadiness);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
publishSnapshotReadiness(0);
|
|
2208
|
+
updateReadiness();
|
|
2209
|
+
const intervalId = window.setInterval(updateReadiness, 250);
|
|
2210
|
+
return () => {
|
|
2211
|
+
window.clearInterval(intervalId);
|
|
2212
|
+
for (const video of attachedVideos) {
|
|
2213
|
+
for (const eventName of videoEvents) {
|
|
2214
|
+
video.removeEventListener(eventName, updateReadiness);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
if (typeof window !== "undefined") {
|
|
2218
|
+
window.__OPTIFYE_SNAPSHOT_READY__ = false;
|
|
2219
|
+
window.__OPTIFYE_SNAPSHOT_VIDEO_STATUS__ = void 0;
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
}, [expectedVideoCount, publishSnapshotReadiness]);
|
|
2223
|
+
if (!sortedWorkspaces.length) {
|
|
2224
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2225
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2226
|
+
"div",
|
|
2227
|
+
{
|
|
2228
|
+
ref: readinessRef,
|
|
2229
|
+
"data-testid": "snapshot-video-readiness",
|
|
2230
|
+
"data-ready": "false",
|
|
2231
|
+
"data-status": "no_videos",
|
|
2232
|
+
"data-expected-video-count": "0",
|
|
2233
|
+
"data-ready-video-count": "0",
|
|
2234
|
+
hidden: true
|
|
2235
|
+
}
|
|
2236
|
+
),
|
|
2237
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-h-[320px] items-center justify-center rounded-md border border-dashed border-slate-300 bg-slate-50 text-sm font-medium text-slate-500", children: "No workstation snapshot available" })
|
|
2238
|
+
] });
|
|
2239
|
+
}
|
|
2240
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2241
|
+
"div",
|
|
2242
|
+
{
|
|
2243
|
+
"aria-label": "Recent-flow workstation snapshot",
|
|
2244
|
+
className: `relative h-full min-h-0 w-full overflow-hidden bg-slate-50/30 ${className}`,
|
|
2245
|
+
children: [
|
|
2246
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2247
|
+
"div",
|
|
2248
|
+
{
|
|
2249
|
+
ref: readinessRef,
|
|
2250
|
+
"data-testid": "snapshot-video-readiness",
|
|
2251
|
+
"data-ready": "false",
|
|
2252
|
+
"data-status": expectedVideoCount === 0 ? "no_videos" : "loading",
|
|
2253
|
+
"data-expected-video-count": expectedVideoCount,
|
|
2254
|
+
"data-ready-video-count": "0",
|
|
2255
|
+
hidden: true
|
|
2256
|
+
}
|
|
2257
|
+
),
|
|
2258
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2259
|
+
"div",
|
|
2260
|
+
{
|
|
2261
|
+
ref: containerRef,
|
|
2262
|
+
"data-testid": "video-grid-scroll-container",
|
|
2263
|
+
"data-mobile-scrollable": isMobileScrollableGrid ? "true" : "false",
|
|
2264
|
+
className: `absolute inset-0 w-full overflow-x-hidden px-1 py-1 sm:px-2 sm:py-2 ${isMobileScrollableGrid ? "overflow-y-auto" : "overflow-hidden"}`,
|
|
2265
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2266
|
+
"div",
|
|
2267
|
+
{
|
|
2268
|
+
"data-testid": "video-grid-layout",
|
|
2269
|
+
className: `grid min-w-0 w-full gap-1.5 sm:gap-2 ${isMobileScrollableGrid ? "content-start" : "h-full"}`,
|
|
2270
|
+
style: {
|
|
2271
|
+
gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,
|
|
2272
|
+
gridTemplateRows: isMobileScrollableGrid ? void 0 : `repeat(${gridRows}, 1fr)`,
|
|
2273
|
+
gridAutoFlow: "row"
|
|
2274
|
+
},
|
|
2275
|
+
children: sortedWorkspaces.map((workspace) => {
|
|
2276
|
+
const workspaceId = workspace.workspace_uuid || workspace.workspace_name;
|
|
2277
|
+
const stream = workspaceId ? videoStreamsByWorkspaceId[workspaceId] : null;
|
|
2278
|
+
const hlsUrl = stream?.hls_url || "";
|
|
2279
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2280
|
+
"div",
|
|
2281
|
+
{
|
|
2282
|
+
"data-workspace-id": workspaceId,
|
|
2283
|
+
className: isMobileScrollableGrid ? "workspace-card relative min-w-0 w-full aspect-video min-h-[92px]" : "workspace-card relative min-w-0 w-full h-full",
|
|
2284
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2285
|
+
VideoCard,
|
|
2286
|
+
{
|
|
2287
|
+
workspace,
|
|
2288
|
+
hlsUrl,
|
|
2289
|
+
shouldPlay: Boolean(hlsUrl),
|
|
2290
|
+
legend: effectiveLegend,
|
|
2291
|
+
cropping: stream?.crop || void 0,
|
|
2292
|
+
canvasFps: 10,
|
|
2293
|
+
useRAF: false,
|
|
2294
|
+
displayMinuteBucket,
|
|
2295
|
+
displayName: workspace.displayName || workspace.workspace_name,
|
|
2296
|
+
compact: true
|
|
2297
|
+
}
|
|
2298
|
+
) })
|
|
2299
|
+
},
|
|
2300
|
+
`${workspace.line_id}-${workspaceId}`
|
|
2301
|
+
);
|
|
2302
|
+
})
|
|
2303
|
+
}
|
|
2304
|
+
)
|
|
2305
|
+
}
|
|
2306
|
+
)
|
|
2307
|
+
]
|
|
2308
|
+
}
|
|
2309
|
+
);
|
|
2310
|
+
};
|
|
2311
|
+
|
|
2312
|
+
exports.RecentFlowSnapshotGrid = RecentFlowSnapshotGrid;
|