@optifye/dashboard-core 6.10.21 → 6.10.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ var dateFns = require('date-fns');
8
8
  var mixpanel = require('mixpanel-browser');
9
9
  var events = require('events');
10
10
  var supabaseJs = require('@supabase/supabase-js');
11
- var Hls3 = require('hls.js');
11
+ var Hls = require('hls.js');
12
12
  var useSWR = require('swr');
13
13
  var motionUtils = require('motion-utils');
14
14
  var motionDom = require('motion-dom');
@@ -49,7 +49,7 @@ function _interopNamespace(e) {
49
49
 
50
50
  var React25__namespace = /*#__PURE__*/_interopNamespace(React25);
51
51
  var mixpanel__default = /*#__PURE__*/_interopDefault(mixpanel);
52
- var Hls3__default = /*#__PURE__*/_interopDefault(Hls3);
52
+ var Hls__default = /*#__PURE__*/_interopDefault(Hls);
53
53
  var useSWR__default = /*#__PURE__*/_interopDefault(useSWR);
54
54
  var SelectPrimitive__namespace = /*#__PURE__*/_interopNamespace(SelectPrimitive);
55
55
  var html2canvas__default = /*#__PURE__*/_interopDefault(html2canvas);
@@ -458,6 +458,9 @@ var _getSupabaseInstance = () => {
458
458
  }
459
459
  return supabaseInstance;
460
460
  };
461
+ var _getSupabaseInstanceOptional = () => {
462
+ return supabaseInstance;
463
+ };
461
464
 
462
465
  // src/lib/services/actionService.ts
463
466
  var actionService = {
@@ -2089,6 +2092,11 @@ var workspaceService = {
2089
2092
  _workspacesInFlight: /* @__PURE__ */ new Map(),
2090
2093
  _workspacesCacheExpiryMs: 10 * 60 * 1e3,
2091
2094
  // 10 minutes cache
2095
+ // Cache for workspace video streams (R2 mapping + crops)
2096
+ _workspaceVideoStreamsCache: /* @__PURE__ */ new Map(),
2097
+ _workspaceVideoStreamsInFlight: /* @__PURE__ */ new Map(),
2098
+ _workspaceVideoStreamsCacheExpiryMs: 5 * 60 * 1e3,
2099
+ // 5 minutes cache
2092
2100
  async getWorkspaces(lineId, options) {
2093
2101
  const enabledOnly = options?.enabledOnly ?? false;
2094
2102
  const force = options?.force ?? false;
@@ -2135,6 +2143,61 @@ var workspaceService = {
2135
2143
  async getEnabledWorkspaces(lineId, options) {
2136
2144
  return this.getWorkspaces(lineId, { enabledOnly: true, force: options?.force });
2137
2145
  },
2146
+ async getWorkspaceVideoStreams(params) {
2147
+ const workspaceIds = (params.workspaceIds || []).filter(Boolean);
2148
+ const lineIds = (params.lineIds || []).filter(Boolean);
2149
+ const force = params.force ?? false;
2150
+ if (!workspaceIds.length && !lineIds.length) {
2151
+ return {};
2152
+ }
2153
+ const workspaceKey = workspaceIds.slice().sort().join(",");
2154
+ const lineKey = lineIds.slice().sort().join(",");
2155
+ const cacheKey = workspaceKey ? `workspaces:${workspaceKey}` : `lines:${lineKey}`;
2156
+ const now2 = Date.now();
2157
+ const cached = this._workspaceVideoStreamsCache.get(cacheKey);
2158
+ if (!force && cached && now2 - cached.timestamp < this._workspaceVideoStreamsCacheExpiryMs) {
2159
+ return cached.streams;
2160
+ }
2161
+ const inFlight = this._workspaceVideoStreamsInFlight.get(cacheKey);
2162
+ if (!force && inFlight) {
2163
+ return inFlight;
2164
+ }
2165
+ const fetchPromise = (async () => {
2166
+ try {
2167
+ const token = await getAuthToken2();
2168
+ const apiUrl = getBackendUrl2();
2169
+ const response = await fetch(`${apiUrl}/api/workspaces/video-streams`, {
2170
+ method: "POST",
2171
+ headers: {
2172
+ "Authorization": `Bearer ${token}`,
2173
+ "Content-Type": "application/json"
2174
+ },
2175
+ body: JSON.stringify({
2176
+ workspace_ids: workspaceIds.length ? workspaceIds : void 0,
2177
+ line_ids: lineIds.length ? lineIds : void 0
2178
+ })
2179
+ });
2180
+ if (!response.ok) {
2181
+ const errorText = await response.text();
2182
+ throw new Error(`Backend API error (${response.status}): ${errorText}`);
2183
+ }
2184
+ const data = await response.json();
2185
+ const streams = data.streams || {};
2186
+ this._workspaceVideoStreamsCache.set(cacheKey, {
2187
+ streams,
2188
+ timestamp: Date.now()
2189
+ });
2190
+ return streams;
2191
+ } catch (error) {
2192
+ console.error("Error fetching workspace video streams:", error);
2193
+ throw error;
2194
+ } finally {
2195
+ this._workspaceVideoStreamsInFlight.delete(cacheKey);
2196
+ }
2197
+ })();
2198
+ this._workspaceVideoStreamsInFlight.set(cacheKey, fetchPromise);
2199
+ return fetchPromise;
2200
+ },
2138
2201
  /**
2139
2202
  * Fetches workspace display names from the database
2140
2203
  * Returns a map of workspace_id -> display_name
@@ -4944,13 +5007,98 @@ function isValidShiftId(shiftId) {
4944
5007
  const id3 = typeof shiftId === "string" ? parseInt(shiftId, 10) : shiftId;
4945
5008
  return Number.isFinite(id3) && id3 >= 0;
4946
5009
  }
5010
+ var createSupabaseClient = (url, key) => supabaseJs.createClient(url, key, {
5011
+ auth: {
5012
+ autoRefreshToken: true,
5013
+ persistSession: true,
5014
+ detectSessionInUrl: true,
5015
+ flowType: "pkce",
5016
+ // Enable debug logging in development
5017
+ debug: process.env.NODE_ENV === "development"
5018
+ },
5019
+ db: {
5020
+ schema: "public"
5021
+ // Default schema, we'll use .schema('ai') in queries
5022
+ },
5023
+ global: {
5024
+ headers: {
5025
+ "x-application-name": "optifye-dashboard"
5026
+ },
5027
+ // Add global fetch timeout (5 minutes)
5028
+ fetch: async (url2, options = {}) => {
5029
+ const controller = new AbortController();
5030
+ const timeoutId = setTimeout(() => controller.abort(), 3e5);
5031
+ try {
5032
+ const response = await fetch(url2, {
5033
+ ...options,
5034
+ signal: controller.signal
5035
+ });
5036
+ clearTimeout(timeoutId);
5037
+ return response;
5038
+ } catch (error) {
5039
+ clearTimeout(timeoutId);
5040
+ throw error;
5041
+ }
5042
+ }
5043
+ }
5044
+ });
5045
+ var getAnonClient = () => {
5046
+ const url = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
5047
+ const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
5048
+ if (!url || !key) {
5049
+ console.warn("[dashboard-core] Missing supabase env variables");
5050
+ }
5051
+ return supabaseJs.createClient(url, key, {
5052
+ auth: {
5053
+ autoRefreshToken: true,
5054
+ persistSession: true,
5055
+ detectSessionInUrl: true,
5056
+ flowType: "pkce",
5057
+ debug: process.env.NODE_ENV === "development"
5058
+ },
5059
+ global: {
5060
+ headers: {
5061
+ "x-application-name": "optifye-dashboard"
5062
+ },
5063
+ // Add global fetch timeout (5 minutes)
5064
+ fetch: async (url2, options = {}) => {
5065
+ const controller = new AbortController();
5066
+ const timeoutId = setTimeout(() => controller.abort(), 3e5);
5067
+ try {
5068
+ const response = await fetch(url2, {
5069
+ ...options,
5070
+ signal: controller.signal
5071
+ });
5072
+ clearTimeout(timeoutId);
5073
+ return response;
5074
+ } catch (error) {
5075
+ clearTimeout(timeoutId);
5076
+ throw error;
5077
+ }
5078
+ }
5079
+ }
5080
+ });
5081
+ };
5082
+
5083
+ // src/lib/api/s3-clips-supabase.ts
5084
+ var cachedClient = null;
5085
+ var cachedConfig = null;
4947
5086
  var getSupabaseClient = () => {
5087
+ const existing = _getSupabaseInstanceOptional();
5088
+ if (existing) {
5089
+ return existing;
5090
+ }
4948
5091
  const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
4949
5092
  const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
4950
5093
  if (!url || !key) {
4951
5094
  throw new Error("Supabase configuration missing");
4952
5095
  }
4953
- return supabaseJs.createClient(url, key);
5096
+ if (!cachedClient || cachedConfig?.url !== url || cachedConfig?.key !== key) {
5097
+ cachedClient = createSupabaseClient(url, key);
5098
+ cachedConfig = { url, key };
5099
+ _setSupabaseInstance(cachedClient);
5100
+ }
5101
+ return cachedClient;
4954
5102
  };
4955
5103
  var getAuthToken3 = async () => {
4956
5104
  try {
@@ -6012,9 +6160,13 @@ var createSupervisorService = (supabase) => {
6012
6160
  var simulateApiDelay = (ms = 1e3) => {
6013
6161
  return new Promise((resolve) => setTimeout(resolve, ms));
6014
6162
  };
6163
+
6164
+ // src/lib/services/timezone.ts
6015
6165
  var TimezoneService = class _TimezoneService {
6016
6166
  constructor() {
6017
6167
  this.timezoneCache = /* @__PURE__ */ new Map();
6168
+ this.fallbackClient = null;
6169
+ this.fallbackConfig = null;
6018
6170
  this.supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
6019
6171
  this.supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
6020
6172
  }
@@ -6024,6 +6176,21 @@ var TimezoneService = class _TimezoneService {
6024
6176
  }
6025
6177
  return _TimezoneService.instance;
6026
6178
  }
6179
+ getClient() {
6180
+ const existing = _getSupabaseInstanceOptional();
6181
+ if (existing) {
6182
+ return existing;
6183
+ }
6184
+ if (!this.supabaseUrl || !this.supabaseAnonKey) {
6185
+ throw new Error("Supabase configuration missing");
6186
+ }
6187
+ if (!this.fallbackClient || this.fallbackConfig?.url !== this.supabaseUrl || this.fallbackConfig?.key !== this.supabaseAnonKey) {
6188
+ this.fallbackClient = createSupabaseClient(this.supabaseUrl, this.supabaseAnonKey);
6189
+ this.fallbackConfig = { url: this.supabaseUrl, key: this.supabaseAnonKey };
6190
+ _setSupabaseInstance(this.fallbackClient);
6191
+ }
6192
+ return this.fallbackClient;
6193
+ }
6027
6194
  /**
6028
6195
  * Fetch timezone for a specific line from Supabase
6029
6196
  */
@@ -6032,7 +6199,7 @@ var TimezoneService = class _TimezoneService {
6032
6199
  return this.timezoneCache.get(lineId);
6033
6200
  }
6034
6201
  try {
6035
- const supabase = supabaseJs.createClient(this.supabaseUrl, this.supabaseAnonKey);
6202
+ const supabase = this.getClient();
6036
6203
  const { data, error } = await supabase.from("line_operating_hours").select("timezone").eq("line_id", lineId).order("created_at", { ascending: false }).limit(1).single();
6037
6204
  if (error) {
6038
6205
  console.warn(`Failed to fetch timezone for line ${lineId}:`, error);
@@ -6057,7 +6224,7 @@ var TimezoneService = class _TimezoneService {
6057
6224
  return this.timezoneCache.get(cacheKey);
6058
6225
  }
6059
6226
  try {
6060
- const supabase = supabaseJs.createClient(this.supabaseUrl, this.supabaseAnonKey);
6227
+ const supabase = this.getClient();
6061
6228
  const { data, error } = await supabase.from("line_operating_hours").select(`
6062
6229
  timezone,
6063
6230
  line:lines!inner(
@@ -6087,7 +6254,7 @@ var TimezoneService = class _TimezoneService {
6087
6254
  return this.timezoneCache.get(cacheKey);
6088
6255
  }
6089
6256
  try {
6090
- const supabase = supabaseJs.createClient(this.supabaseUrl, this.supabaseAnonKey);
6257
+ const supabase = this.getClient();
6091
6258
  const { data: workspaceData, error: workspaceError } = await supabase.from("workspaces").select("line_id").eq("id", workspaceId).single();
6092
6259
  if (workspaceError) {
6093
6260
  console.warn(`Failed to fetch workspace ${workspaceId}:`, workspaceError);
@@ -6119,7 +6286,7 @@ var TimezoneService = class _TimezoneService {
6119
6286
  return result;
6120
6287
  }
6121
6288
  try {
6122
- const supabase = supabaseJs.createClient(this.supabaseUrl, this.supabaseAnonKey);
6289
+ const supabase = this.getClient();
6123
6290
  const { data, error } = await supabase.from("line_operating_hours").select("line_id, timezone").in("line_id", uncachedLineIds).order("created_at", { ascending: false });
6124
6291
  if (error) {
6125
6292
  console.warn("Failed to fetch timezones for lines:", error);
@@ -7137,12 +7304,16 @@ var SupabaseProvider = ({ client, children }) => {
7137
7304
  const contextValue = React25.useMemo(() => ({ supabase: client }), [client]);
7138
7305
  return /* @__PURE__ */ jsxRuntime.jsx(SupabaseContext.Provider, { value: contextValue, children });
7139
7306
  };
7140
- var useSupabase = () => {
7307
+ var useOptionalSupabase = () => {
7141
7308
  const context = React25.useContext(SupabaseContext);
7142
- if (context === void 0) {
7309
+ return context?.supabase ?? null;
7310
+ };
7311
+ var useSupabase = () => {
7312
+ const client = useOptionalSupabase();
7313
+ if (!client) {
7143
7314
  throw new Error("useSupabase must be used within a SupabaseProvider.");
7144
7315
  }
7145
- return context.supabase;
7316
+ return client;
7146
7317
  };
7147
7318
 
7148
7319
  // src/lib/hooks/useSessionKeepAlive.ts
@@ -10899,16 +11070,63 @@ var useWorkspaceOperators = (workspaceId, options) => {
10899
11070
  refetch: fetchData
10900
11071
  };
10901
11072
  };
11073
+
11074
+ // src/lib/services/hlsAuthService.ts
11075
+ async function getAuthTokenForHls(supabase) {
11076
+ try {
11077
+ const { data: { session } } = await supabase.auth.getSession();
11078
+ if (!session?.access_token) {
11079
+ console.warn("[HLS Auth] No active session, R2 streaming may fail");
11080
+ return null;
11081
+ }
11082
+ console.log("[HLS Auth] Retrieved token for HLS.js requests");
11083
+ return session.access_token;
11084
+ } catch (error) {
11085
+ console.error("[HLS Auth] Error getting auth token:", error);
11086
+ return null;
11087
+ }
11088
+ }
11089
+
11090
+ // src/lib/utils/r2Detection.ts
11091
+ function isR2WorkerUrl(url, r2WorkerDomain) {
11092
+ if (!url || !r2WorkerDomain) return false;
11093
+ try {
11094
+ const workerDomain = new URL(r2WorkerDomain).hostname;
11095
+ return url.includes(workerDomain);
11096
+ } catch {
11097
+ return url.includes(r2WorkerDomain.replace(/https?:\/\//, ""));
11098
+ }
11099
+ }
11100
+
11101
+ // src/lib/utils/browser.ts
11102
+ function isSafari() {
11103
+ if (typeof window === "undefined") return false;
11104
+ const ua = window.navigator.userAgent;
11105
+ const isSafariUA = /^((?!chrome|android).)*safari/i.test(ua);
11106
+ const isIOS = /iPad|iPhone|iPod/.test(ua) && !window.MSStream;
11107
+ return isSafariUA || isIOS;
11108
+ }
11109
+ function getBrowserName() {
11110
+ if (typeof window === "undefined") return "server";
11111
+ const ua = window.navigator.userAgent;
11112
+ if (/chrome/i.test(ua) && !/edge/i.test(ua)) return "chrome";
11113
+ if (/safari/i.test(ua) && !/chrome/i.test(ua)) return "safari";
11114
+ if (/firefox/i.test(ua)) return "firefox";
11115
+ if (/edge/i.test(ua)) return "edge";
11116
+ return "unknown";
11117
+ }
11118
+
11119
+ // src/lib/hooks/useHlsStream.ts
10902
11120
  var HLS_CONFIG = {
10903
- // Buffer configuration for instant playback
10904
- maxBufferLength: 30,
10905
- // Increased for smoother playback
11121
+ // Buffer configuration for 60s segments
11122
+ maxBufferLength: 180,
11123
+ // Allow ~3 segments buffered for stability
10906
11124
  maxMaxBufferLength: 600,
10907
11125
  // 10 minutes max buffer
10908
11126
  maxBufferSize: 120 * 1e3 * 1e3,
10909
11127
  // 120MB buffer size
10910
- maxBufferHole: 0.1,
10911
- // Minimal tolerance for buffer holes
11128
+ maxBufferHole: 0.5,
11129
+ // Tolerate minor timestamp gaps
10912
11130
  // Low latency optimizations
10913
11131
  lowLatencyMode: false,
10914
11132
  // We prioritize stability over latency
@@ -10919,7 +11137,7 @@ var HLS_CONFIG = {
10919
11137
  // Auto-select quality
10920
11138
  autoStartLoad: true,
10921
11139
  startPosition: -1,
10922
- // Start from beginning
11140
+ // Start at live edge when available
10923
11141
  // Network optimization
10924
11142
  manifestLoadingMaxRetry: 6,
10925
11143
  levelLoadingMaxRetry: 6,
@@ -10928,9 +11146,10 @@ var HLS_CONFIG = {
10928
11146
  // Faster retries
10929
11147
  levelLoadingRetryDelay: 250,
10930
11148
  fragLoadingRetryDelay: 250,
10931
- manifestLoadingTimeOut: 2e4,
10932
- levelLoadingTimeOut: 2e4,
10933
- fragLoadingTimeOut: 2e4,
11149
+ manifestLoadingTimeOut: 9e4,
11150
+ levelLoadingTimeOut: 9e4,
11151
+ fragLoadingTimeOut: 9e4,
11152
+ maxConcurrentLoading: 2,
10934
11153
  // ABR (Adaptive Bitrate) settings
10935
11154
  abrEwmaFastLive: 3,
10936
11155
  abrEwmaSlowLive: 9,
@@ -10942,7 +11161,7 @@ var HLS_CONFIG = {
10942
11161
  abrBandWidthUpFactor: 0.7,
10943
11162
  abrMaxWithRealBitrate: true,
10944
11163
  // Fragment loading optimization
10945
- maxFragLookUpTolerance: 0.25,
11164
+ maxFragLookUpTolerance: 1,
10946
11165
  // Enable all optimizations
10947
11166
  enableCEA708Captions: false,
10948
11167
  // Disable if not needed
@@ -10952,12 +11171,21 @@ var HLS_CONFIG = {
10952
11171
  // Disable if not needed
10953
11172
  // Progressive loading
10954
11173
  progressive: true,
10955
- liveSyncDurationCount: 2
10956
- // Follow live edge aggressively
11174
+ liveSyncDurationCount: 2,
11175
+ // Stay ~2 segments behind live for a full segment buffer
11176
+ liveMaxLatencyDurationCount: 3,
11177
+ // Cap drift to ~3 segments
11178
+ maxLiveSyncPlaybackRate: 1
11179
+ // Do not chase live edge; prioritize continuity
10957
11180
  };
10958
11181
  var failedUrls = /* @__PURE__ */ new Map();
10959
11182
  var MAX_FAILURES_PER_URL = 3;
10960
11183
  var FAILURE_EXPIRY_MS = 5 * 60 * 1e3;
11184
+ var LIVE_RELOAD_MIN_INTERVAL_MS = 15 * 1e3;
11185
+ var DEFAULT_LIVE_OFFSET_SECONDS = 120;
11186
+ var DEFAULT_MAX_MANIFEST_AGE_MS = 10 * 60 * 1e3;
11187
+ var STALE_MANIFEST_POLL_INITIAL_DELAY_MS = 15 * 1e3;
11188
+ var STALE_MANIFEST_POLL_MAX_DELAY_MS = 60 * 1e3;
10961
11189
  function resetFailedUrl(url) {
10962
11190
  if (failedUrls.has(url)) {
10963
11191
  console.log(`[HLS] Manually resetting failed URL: ${url}`);
@@ -10968,7 +11196,11 @@ function isUrlPermanentlyFailed(url) {
10968
11196
  const failure = failedUrls.get(url);
10969
11197
  return failure?.permanentlyFailed || false;
10970
11198
  }
10971
- function useHlsStream(videoRef, { src, shouldPlay, onFatalError }) {
11199
+ function useHlsStream(videoRef, { src, shouldPlay, onFatalError, hlsConfig }) {
11200
+ const latestSrcRef = React25.useRef(src);
11201
+ latestSrcRef.current = src;
11202
+ const shouldPlayRef = React25.useRef(shouldPlay);
11203
+ shouldPlayRef.current = shouldPlay;
10972
11204
  const urlFailure = failedUrls.get(src);
10973
11205
  const isPermanentlyFailed = urlFailure?.permanentlyFailed || false;
10974
11206
  const cleanupFailedUrls = () => {
@@ -10991,7 +11223,194 @@ function useHlsStream(videoRef, { src, shouldPlay, onFatalError }) {
10991
11223
  const lastTimeUpdateRef = React25.useRef(0);
10992
11224
  const softRestartCountRef = React25.useRef(0);
10993
11225
  const isNativeHlsRef = React25.useRef(false);
11226
+ const playbackRateIntervalRef = React25.useRef(null);
10994
11227
  const waitingTimerRef = React25.useRef(null);
11228
+ const playRetryTimerRef = React25.useRef(null);
11229
+ const playRetryCountRef = React25.useRef(0);
11230
+ const manifestWatchdogRef = React25.useRef(null);
11231
+ const lastHiddenAtRef = React25.useRef(null);
11232
+ const manifestRetryTimerRef = React25.useRef(null);
11233
+ const manifestRetryDelayRef = React25.useRef(5e3);
11234
+ const lastLiveReloadRef = React25.useRef(0);
11235
+ const isR2StreamRef = React25.useRef(false);
11236
+ const nativeStreamUrlRef = React25.useRef(null);
11237
+ const forcedLiveStartRef = React25.useRef(false);
11238
+ const activeStreamUrlRef = React25.useRef(null);
11239
+ const targetDurationRef = React25.useRef(null);
11240
+ const lastManifestLoadRef = React25.useRef(0);
11241
+ const lastFragLoadRef = React25.useRef(0);
11242
+ const lastFragSnRef = React25.useRef(null);
11243
+ const lastWindowStartSnRef = React25.useRef(null);
11244
+ const lastWindowEndSnRef = React25.useRef(null);
11245
+ const lastFragTimeoutSnRef = React25.useRef(null);
11246
+ const lastFragTimeoutAtRef = React25.useRef(null);
11247
+ const fragTimeoutCountRef = React25.useRef(0);
11248
+ const lastManifestEndSnRef = React25.useRef(null);
11249
+ const lastManifestEndSnUpdatedAtRef = React25.useRef(null);
11250
+ const staleManifestTriggeredRef = React25.useRef(false);
11251
+ const staleManifestUrlRef = React25.useRef(null);
11252
+ const staleManifestEndSnRef = React25.useRef(null);
11253
+ const staleManifestPollTimerRef = React25.useRef(null);
11254
+ const staleManifestPollDelayRef = React25.useRef(STALE_MANIFEST_POLL_INITIAL_DELAY_MS);
11255
+ const authTokenRef = React25.useRef(null);
11256
+ const proxyEnabled = process.env.NEXT_PUBLIC_HLS_PROXY_ENABLED === "true";
11257
+ const proxyBaseUrl = (process.env.NEXT_PUBLIC_HLS_PROXY_URL || "/api/stream").replace(/\/$/, "");
11258
+ const debugEnabled = process.env.NEXT_PUBLIC_HLS_DEBUG === "true";
11259
+ const maxManifestAgeMs = (() => {
11260
+ const raw = process.env.NEXT_PUBLIC_HLS_MAX_MANIFEST_AGE_MS;
11261
+ const parsed = raw ? Number(raw) : NaN;
11262
+ return Number.isFinite(parsed) ? parsed : DEFAULT_MAX_MANIFEST_AGE_MS;
11263
+ })();
11264
+ const debugLog = (...args) => {
11265
+ if (debugEnabled) {
11266
+ console.log(...args);
11267
+ }
11268
+ };
11269
+ const getProgramDateTimeMs = (value) => {
11270
+ if (value instanceof Date) return value.getTime();
11271
+ if (typeof value === "number") return value;
11272
+ if (typeof value === "string") {
11273
+ const parsed = Date.parse(value);
11274
+ return Number.isNaN(parsed) ? null : parsed;
11275
+ }
11276
+ return null;
11277
+ };
11278
+ const parseManifestStatus = (manifestText) => {
11279
+ let mediaSequence = null;
11280
+ let segmentCount = 0;
11281
+ let lastProgramDateTimeMs = null;
11282
+ const lines = manifestText.split(/\r?\n/);
11283
+ for (const rawLine of lines) {
11284
+ const line = rawLine.trim();
11285
+ if (!line) continue;
11286
+ if (line.startsWith("#EXT-X-MEDIA-SEQUENCE:")) {
11287
+ const value = Number.parseInt(line.replace("#EXT-X-MEDIA-SEQUENCE:", ""), 10);
11288
+ if (Number.isFinite(value)) {
11289
+ mediaSequence = value;
11290
+ }
11291
+ } else if (line.startsWith("#EXT-X-PROGRAM-DATE-TIME:")) {
11292
+ const timestamp = line.replace("#EXT-X-PROGRAM-DATE-TIME:", "").trim();
11293
+ const parsed = Date.parse(timestamp);
11294
+ if (!Number.isNaN(parsed)) {
11295
+ lastProgramDateTimeMs = parsed;
11296
+ }
11297
+ } else if (line.startsWith("#EXTINF:")) {
11298
+ segmentCount += 1;
11299
+ }
11300
+ }
11301
+ let lastSequenceNumber = null;
11302
+ if (mediaSequence !== null && segmentCount > 0) {
11303
+ lastSequenceNumber = mediaSequence + segmentCount - 1;
11304
+ }
11305
+ return { lastProgramDateTimeMs, lastSequenceNumber };
11306
+ };
11307
+ const stopStaleManifestPolling = () => {
11308
+ if (staleManifestPollTimerRef.current) {
11309
+ clearTimeout(staleManifestPollTimerRef.current);
11310
+ staleManifestPollTimerRef.current = null;
11311
+ }
11312
+ staleManifestTriggeredRef.current = false;
11313
+ staleManifestUrlRef.current = null;
11314
+ staleManifestEndSnRef.current = null;
11315
+ staleManifestPollDelayRef.current = STALE_MANIFEST_POLL_INITIAL_DELAY_MS;
11316
+ };
11317
+ const pollStaleManifestOnce = async () => {
11318
+ if (!staleManifestTriggeredRef.current) return;
11319
+ if (!shouldPlayRef.current) {
11320
+ stopStaleManifestPolling();
11321
+ return;
11322
+ }
11323
+ const manifestUrl = staleManifestUrlRef.current;
11324
+ if (!manifestUrl) {
11325
+ stopStaleManifestPolling();
11326
+ return;
11327
+ }
11328
+ try {
11329
+ const headers = {};
11330
+ if (authTokenRef.current) {
11331
+ headers.Authorization = `Bearer ${authTokenRef.current}`;
11332
+ }
11333
+ const response = await fetch(buildCacheBustedUrl(manifestUrl), {
11334
+ method: "GET",
11335
+ cache: "no-store",
11336
+ headers
11337
+ });
11338
+ if (!response.ok) {
11339
+ throw new Error(`Manifest poll failed: ${response.status}`);
11340
+ }
11341
+ const manifestText = await response.text();
11342
+ const { lastProgramDateTimeMs, lastSequenceNumber } = parseManifestStatus(manifestText);
11343
+ const now2 = Date.now();
11344
+ const isFreshByProgramDateTime = lastProgramDateTimeMs !== null && now2 - lastProgramDateTimeMs <= maxManifestAgeMs;
11345
+ const priorEndSn = staleManifestEndSnRef.current;
11346
+ const isSequenceAdvanced = typeof lastSequenceNumber === "number" && (priorEndSn === null || lastSequenceNumber > priorEndSn);
11347
+ if (isFreshByProgramDateTime || isSequenceAdvanced) {
11348
+ stopStaleManifestPolling();
11349
+ if (hlsRef.current) {
11350
+ hlsRef.current.startLoad(-1);
11351
+ seekToLiveEdge();
11352
+ attemptPlay("stale manifest recovered");
11353
+ } else {
11354
+ setRestartKey((k) => k + 1);
11355
+ }
11356
+ return;
11357
+ }
11358
+ } catch (error) {
11359
+ debugLog("[HLS] Stale manifest poll failed", error);
11360
+ }
11361
+ staleManifestPollDelayRef.current = Math.min(
11362
+ staleManifestPollDelayRef.current + 5e3,
11363
+ STALE_MANIFEST_POLL_MAX_DELAY_MS
11364
+ );
11365
+ scheduleStaleManifestPoll();
11366
+ };
11367
+ const scheduleStaleManifestPoll = () => {
11368
+ if (!staleManifestTriggeredRef.current) return;
11369
+ if (staleManifestPollTimerRef.current) return;
11370
+ const delay2 = staleManifestPollDelayRef.current;
11371
+ staleManifestPollTimerRef.current = setTimeout(() => {
11372
+ staleManifestPollTimerRef.current = null;
11373
+ void pollStaleManifestOnce();
11374
+ }, delay2);
11375
+ };
11376
+ const startStaleManifestPolling = (reason) => {
11377
+ if (staleManifestTriggeredRef.current) return;
11378
+ staleManifestTriggeredRef.current = true;
11379
+ staleManifestUrlRef.current = activeStreamUrlRef.current || latestSrcRef.current;
11380
+ staleManifestEndSnRef.current = lastManifestEndSnRef.current;
11381
+ staleManifestPollDelayRef.current = STALE_MANIFEST_POLL_INITIAL_DELAY_MS;
11382
+ const hls = hlsRef.current;
11383
+ if (hls) {
11384
+ hls.stopLoad();
11385
+ }
11386
+ const video = videoRef.current;
11387
+ if (video) {
11388
+ video.pause();
11389
+ }
11390
+ console.warn(`[HLS] Manifest stale, pausing playback (${reason})`);
11391
+ scheduleStaleManifestPoll();
11392
+ };
11393
+ const isProxyUrl = (url) => {
11394
+ if (!proxyEnabled || !proxyBaseUrl) return false;
11395
+ try {
11396
+ const base = proxyBaseUrl.startsWith("http") ? proxyBaseUrl : new URL(proxyBaseUrl, window.location.origin).toString();
11397
+ return url.startsWith(base);
11398
+ } catch {
11399
+ return url.includes(proxyBaseUrl);
11400
+ }
11401
+ };
11402
+ const getR2CameraUuid = (url) => {
11403
+ try {
11404
+ const parsed = new URL(url);
11405
+ const match2 = parsed.pathname.match(/\/segments\/([^\/]+)\/index\.m3u8$/);
11406
+ if (match2) {
11407
+ return match2[1];
11408
+ }
11409
+ } catch {
11410
+ }
11411
+ const match = url.match(/\/segments\/([^\/]+)\/index\.m3u8/);
11412
+ return match ? match[1] : null;
11413
+ };
10995
11414
  const cleanup = () => {
10996
11415
  if (stallCheckIntervalRef.current) {
10997
11416
  clearInterval(stallCheckIntervalRef.current);
@@ -11001,10 +11420,48 @@ function useHlsStream(videoRef, { src, shouldPlay, onFatalError }) {
11001
11420
  clearTimeout(noProgressTimerRef.current);
11002
11421
  noProgressTimerRef.current = null;
11003
11422
  }
11423
+ if (playbackRateIntervalRef.current) {
11424
+ clearInterval(playbackRateIntervalRef.current);
11425
+ playbackRateIntervalRef.current = null;
11426
+ }
11004
11427
  if (waitingTimerRef.current) {
11005
11428
  clearTimeout(waitingTimerRef.current);
11006
11429
  waitingTimerRef.current = null;
11007
11430
  }
11431
+ if (playRetryTimerRef.current) {
11432
+ clearTimeout(playRetryTimerRef.current);
11433
+ playRetryTimerRef.current = null;
11434
+ }
11435
+ if (manifestWatchdogRef.current) {
11436
+ clearInterval(manifestWatchdogRef.current);
11437
+ manifestWatchdogRef.current = null;
11438
+ }
11439
+ if (manifestRetryTimerRef.current) {
11440
+ clearTimeout(manifestRetryTimerRef.current);
11441
+ manifestRetryTimerRef.current = null;
11442
+ }
11443
+ stopStaleManifestPolling();
11444
+ lastHiddenAtRef.current = null;
11445
+ if (nativeStreamUrlRef.current) {
11446
+ nativeStreamUrlRef.current = null;
11447
+ }
11448
+ activeStreamUrlRef.current = null;
11449
+ forcedLiveStartRef.current = false;
11450
+ lastLiveReloadRef.current = 0;
11451
+ targetDurationRef.current = null;
11452
+ lastManifestLoadRef.current = 0;
11453
+ lastFragLoadRef.current = 0;
11454
+ lastFragSnRef.current = null;
11455
+ lastWindowStartSnRef.current = null;
11456
+ lastWindowEndSnRef.current = null;
11457
+ lastFragTimeoutSnRef.current = null;
11458
+ lastFragTimeoutAtRef.current = null;
11459
+ fragTimeoutCountRef.current = 0;
11460
+ lastManifestEndSnRef.current = null;
11461
+ lastManifestEndSnUpdatedAtRef.current = null;
11462
+ staleManifestTriggeredRef.current = false;
11463
+ manifestRetryDelayRef.current = 5e3;
11464
+ playRetryCountRef.current = 0;
11008
11465
  if (hlsRef.current) {
11009
11466
  hlsRef.current.destroy();
11010
11467
  hlsRef.current = null;
@@ -11016,11 +11473,24 @@ function useHlsStream(videoRef, { src, shouldPlay, onFatalError }) {
11016
11473
  video.load();
11017
11474
  video.removeEventListener("waiting", handleWaiting);
11018
11475
  video.removeEventListener("timeupdate", handleTimeUpdate);
11476
+ video.removeEventListener("loadedmetadata", handleLoadedMetadata);
11477
+ video.removeEventListener("canplay", handleCanPlay);
11478
+ video.removeEventListener("ended", handleEnded);
11019
11479
  video.removeEventListener("error", handleNativeError);
11480
+ if (typeof document !== "undefined") {
11481
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
11482
+ }
11020
11483
  }
11021
11484
  lastTimeUpdateRef.current = 0;
11022
11485
  softRestartCountRef.current = 0;
11023
11486
  };
11487
+ const getLiveOffsetSeconds = () => {
11488
+ const targetDuration = targetDurationRef.current;
11489
+ if (targetDuration && Number.isFinite(targetDuration)) {
11490
+ return Math.max(targetDuration * 2, targetDuration);
11491
+ }
11492
+ return DEFAULT_LIVE_OFFSET_SECONDS;
11493
+ };
11024
11494
  const seekToLiveEdge = () => {
11025
11495
  const hls = hlsRef.current;
11026
11496
  const video = videoRef.current;
@@ -11029,19 +11499,237 @@ function useHlsStream(videoRef, { src, shouldPlay, onFatalError }) {
11029
11499
  video.currentTime = hls.liveSyncPosition;
11030
11500
  } else if (hls.levels?.[hls.currentLevel]?.details) {
11031
11501
  const levelDetails = hls.levels[hls.currentLevel].details;
11032
- if (levelDetails && levelDetails.edge !== void 0) {
11033
- video.currentTime = Math.max(0, levelDetails.edge - 5);
11502
+ const edge = levelDetails?.edge;
11503
+ if (edge !== void 0 && edge !== null) {
11504
+ const offset = getLiveOffsetSeconds();
11505
+ video.currentTime = Math.max(0, edge - offset);
11506
+ }
11507
+ }
11508
+ };
11509
+ const getLiveEdgeTime = () => {
11510
+ const video = videoRef.current;
11511
+ if (!video || video.seekable.length === 0) return null;
11512
+ const end = video.seekable.end(video.seekable.length - 1);
11513
+ if (!Number.isFinite(end)) return null;
11514
+ const offset = getLiveOffsetSeconds();
11515
+ return Math.max(0, end - offset);
11516
+ };
11517
+ const getWaitingTimeoutMs = () => {
11518
+ const targetDuration = targetDurationRef.current;
11519
+ if (targetDuration && Number.isFinite(targetDuration)) {
11520
+ return Math.max(2e4, targetDuration * 1e3 * 0.5);
11521
+ }
11522
+ return 3e4;
11523
+ };
11524
+ const getManifestStaleTimeoutMs = () => {
11525
+ const targetDuration = targetDurationRef.current;
11526
+ if (targetDuration && Number.isFinite(targetDuration)) {
11527
+ return Math.max(targetDuration * 2e3, 12e4);
11528
+ }
11529
+ return 12e4;
11530
+ };
11531
+ const isManifestUrl = (url) => url.includes(".m3u8");
11532
+ const getBufferGap = (video) => {
11533
+ if (!video.buffered.length) return null;
11534
+ const end = video.buffered.end(video.buffered.length - 1);
11535
+ if (!Number.isFinite(end)) return null;
11536
+ return Math.max(0, end - video.currentTime);
11537
+ };
11538
+ const getBufferThresholds = () => {
11539
+ const targetDuration = targetDurationRef.current ?? 60;
11540
+ return {
11541
+ low: Math.max(5, targetDuration * 0.1),
11542
+ mid: Math.max(10, targetDuration * 0.2),
11543
+ high: Math.max(18, targetDuration * 0.35)
11544
+ };
11545
+ };
11546
+ const shouldAttemptRecovery = () => {
11547
+ if (!isR2StreamRef.current) return true;
11548
+ if (!lastManifestLoadRef.current) return false;
11549
+ const now2 = Date.now();
11550
+ const timeoutMs = getManifestStaleTimeoutMs();
11551
+ const manifestAge = now2 - lastManifestLoadRef.current;
11552
+ return manifestAge > timeoutMs;
11553
+ };
11554
+ const getDesiredLivePosition = () => {
11555
+ const hls = hlsRef.current;
11556
+ if (hls?.liveSyncPosition !== null && hls?.liveSyncPosition !== void 0) {
11557
+ if (Number.isFinite(hls.liveSyncPosition)) {
11558
+ return hls.liveSyncPosition;
11559
+ }
11560
+ }
11561
+ if (hls?.levels?.[hls.currentLevel]?.details) {
11562
+ const levelDetails = hls.levels[hls.currentLevel].details;
11563
+ const edge = levelDetails?.edge;
11564
+ if (edge !== void 0 && edge !== null) {
11565
+ return Math.max(0, edge - getLiveOffsetSeconds());
11566
+ }
11567
+ }
11568
+ return getLiveEdgeTime();
11569
+ };
11570
+ const schedulePlayRetry = (reason) => {
11571
+ if (playRetryTimerRef.current) return;
11572
+ if (playRetryCountRef.current >= 3) return;
11573
+ playRetryCountRef.current += 1;
11574
+ playRetryTimerRef.current = setTimeout(() => {
11575
+ playRetryTimerRef.current = null;
11576
+ attemptPlay();
11577
+ }, 1e3);
11578
+ };
11579
+ const attemptPlay = (reason) => {
11580
+ const video = videoRef.current;
11581
+ if (!video || !shouldPlayRef.current) return;
11582
+ if (!video.paused || video.seeking) return;
11583
+ if (video.readyState < 2) return;
11584
+ video.play().then(() => {
11585
+ playRetryCountRef.current = 0;
11586
+ }).catch((err) => {
11587
+ if (err?.name === "AbortError") {
11588
+ schedulePlayRetry();
11589
+ return;
11034
11590
  }
11591
+ console.error("[HLS] Play failed:", err);
11592
+ });
11593
+ };
11594
+ const handleCanPlay = () => {
11595
+ attemptPlay();
11596
+ };
11597
+ const handleVisibilityChange = () => {
11598
+ if (typeof document === "undefined") return;
11599
+ if (document.hidden) {
11600
+ lastHiddenAtRef.current = Date.now();
11601
+ return;
11602
+ }
11603
+ const lastHiddenAt = lastHiddenAtRef.current;
11604
+ lastHiddenAtRef.current = null;
11605
+ if (!lastHiddenAt) return;
11606
+ if (Date.now() - lastHiddenAt < 3e4) return;
11607
+ refreshLiveStream("tab visible after idle");
11608
+ attemptPlay();
11609
+ };
11610
+ const startManifestWatchdog = () => {
11611
+ if (manifestWatchdogRef.current) return;
11612
+ if (!isR2StreamRef.current) return;
11613
+ manifestWatchdogRef.current = setInterval(() => {
11614
+ if (staleManifestTriggeredRef.current) return;
11615
+ if (!lastManifestLoadRef.current) return;
11616
+ const now2 = Date.now();
11617
+ const staleTimeoutMs = getManifestStaleTimeoutMs();
11618
+ if (now2 - lastManifestLoadRef.current < staleTimeoutMs) return;
11619
+ if (now2 - lastLiveReloadRef.current < LIVE_RELOAD_MIN_INTERVAL_MS) return;
11620
+ lastLiveReloadRef.current = now2;
11621
+ softRestart("manifest stale watchdog");
11622
+ }, 15e3);
11623
+ };
11624
+ const scheduleManifestRetry = (reason) => {
11625
+ if (manifestRetryTimerRef.current) return;
11626
+ const delay2 = manifestRetryDelayRef.current;
11627
+ manifestRetryTimerRef.current = setTimeout(() => {
11628
+ manifestRetryTimerRef.current = null;
11629
+ softRestart(reason);
11630
+ manifestRetryDelayRef.current = Math.min(manifestRetryDelayRef.current + 5e3, 3e4);
11631
+ }, delay2);
11632
+ };
11633
+ const resetManifestRetry = () => {
11634
+ if (manifestRetryTimerRef.current) {
11635
+ clearTimeout(manifestRetryTimerRef.current);
11636
+ manifestRetryTimerRef.current = null;
11637
+ }
11638
+ manifestRetryDelayRef.current = 5e3;
11639
+ };
11640
+ const markStaleStream = (reason) => {
11641
+ startStaleManifestPolling(reason);
11642
+ };
11643
+ const setPlaybackRate = (rate) => {
11644
+ const video = videoRef.current;
11645
+ if (!video) return;
11646
+ if (!Number.isFinite(rate)) return;
11647
+ if (Math.abs(video.playbackRate - rate) < 0.01) return;
11648
+ video.playbackRate = rate;
11649
+ };
11650
+ const startPlaybackGovernor = () => {
11651
+ if (playbackRateIntervalRef.current) return;
11652
+ playbackRateIntervalRef.current = setInterval(() => {
11653
+ const video = videoRef.current;
11654
+ if (!video || video.paused || video.seeking) return;
11655
+ if (!isR2StreamRef.current) return;
11656
+ const bufferGap = getBufferGap(video);
11657
+ if (bufferGap === null) return;
11658
+ const { low, mid, high } = getBufferThresholds();
11659
+ let desiredRate = 1;
11660
+ if (bufferGap < low) {
11661
+ desiredRate = 0.8;
11662
+ } else if (bufferGap < mid) {
11663
+ desiredRate = 0.9;
11664
+ } else if (bufferGap < high) {
11665
+ desiredRate = 0.95;
11666
+ }
11667
+ setPlaybackRate(desiredRate);
11668
+ }, 2e3);
11669
+ };
11670
+ const buildCacheBustedUrl = (url) => {
11671
+ if (typeof window === "undefined") {
11672
+ return url;
11673
+ }
11674
+ try {
11675
+ const parsed = new URL(url, window.location.origin);
11676
+ parsed.searchParams.set("ts", Date.now().toString());
11677
+ return parsed.toString();
11678
+ } catch {
11679
+ const separator = url.includes("?") ? "&" : "?";
11680
+ return `${url}${separator}ts=${Date.now()}`;
11681
+ }
11682
+ };
11683
+ const refreshLiveStream = (reason) => {
11684
+ if (!isR2StreamRef.current) return;
11685
+ const now2 = Date.now();
11686
+ if (now2 - lastLiveReloadRef.current < LIVE_RELOAD_MIN_INTERVAL_MS) {
11687
+ return;
11688
+ }
11689
+ lastLiveReloadRef.current = now2;
11690
+ const video = videoRef.current;
11691
+ const hls = hlsRef.current;
11692
+ if (hls) {
11693
+ console.log(`[HLS] Live reload (${reason})`);
11694
+ softRestart(reason);
11695
+ return;
11696
+ }
11697
+ if (video && nativeStreamUrlRef.current) {
11698
+ console.log(`[HLS] Native live reload (${reason})`);
11699
+ const refreshedUrl = buildCacheBustedUrl(nativeStreamUrlRef.current);
11700
+ video.src = refreshedUrl;
11701
+ video.load();
11702
+ const edgeTime = getLiveEdgeTime();
11703
+ if (edgeTime !== null) {
11704
+ video.currentTime = edgeTime;
11705
+ }
11706
+ attemptPlay();
11035
11707
  }
11036
11708
  };
11037
11709
  const softRestart = (reason) => {
11710
+ if (staleManifestTriggeredRef.current) {
11711
+ debugLog("[HLS] Skip soft restart while manifest is stale", reason);
11712
+ return;
11713
+ }
11038
11714
  console.warn(`[HLS] Soft restart: ${reason}`);
11039
11715
  const hls = hlsRef.current;
11040
11716
  if (!hls) return;
11041
11717
  try {
11718
+ const video = videoRef.current;
11719
+ const isR2Stream = isR2StreamRef.current;
11720
+ const desiredPosition = isR2Stream ? getDesiredLivePosition() : null;
11042
11721
  hls.stopLoad();
11043
- hls.startLoad(-1);
11044
- seekToLiveEdge();
11722
+ if (desiredPosition !== null && Number.isFinite(desiredPosition)) {
11723
+ hls.startLoad(desiredPosition);
11724
+ if (video) {
11725
+ video.currentTime = desiredPosition;
11726
+ }
11727
+ } else {
11728
+ hls.startLoad(-1);
11729
+ if (!isR2Stream) {
11730
+ seekToLiveEdge();
11731
+ }
11732
+ }
11045
11733
  softRestartCountRef.current++;
11046
11734
  if (softRestartCountRef.current >= 5) {
11047
11735
  hardRestart(`${reason} (escalated after ${softRestartCountRef.current} soft restarts)`);
@@ -11052,11 +11740,16 @@ function useHlsStream(videoRef, { src, shouldPlay, onFatalError }) {
11052
11740
  }
11053
11741
  };
11054
11742
  const hardRestart = (reason) => {
11743
+ if (staleManifestTriggeredRef.current) {
11744
+ debugLog("[HLS] Skip hard restart while manifest is stale", reason);
11745
+ return;
11746
+ }
11055
11747
  console.warn(`[HLS] Hard restart: ${reason}`);
11056
11748
  cleanupFailedUrls();
11057
11749
  const failure = failedUrls.get(src);
11058
11750
  const failureCount = failure ? failure.count : 0;
11059
- if (failureCount >= MAX_FAILURES_PER_URL) {
11751
+ const isR2Stream = isR2StreamRef.current;
11752
+ if (!isR2Stream && failureCount >= MAX_FAILURES_PER_URL) {
11060
11753
  console.error(`[HLS] URL has failed ${failureCount} times. Marking as permanently failed: ${src}`);
11061
11754
  failedUrls.set(src, {
11062
11755
  count: failureCount,
@@ -11069,11 +11762,15 @@ function useHlsStream(videoRef, { src, shouldPlay, onFatalError }) {
11069
11762
  }
11070
11763
  return;
11071
11764
  }
11072
- failedUrls.set(src, {
11073
- count: failureCount + 1,
11074
- timestamp: Date.now(),
11075
- permanentlyFailed: false
11076
- });
11765
+ if (!isR2Stream) {
11766
+ failedUrls.set(src, {
11767
+ count: failureCount + 1,
11768
+ timestamp: Date.now(),
11769
+ permanentlyFailed: false
11770
+ });
11771
+ } else if (failedUrls.has(src)) {
11772
+ failedUrls.delete(src);
11773
+ }
11077
11774
  cleanup();
11078
11775
  setRestartKey((k) => k + 1);
11079
11776
  softRestartCountRef.current = 0;
@@ -11083,12 +11780,49 @@ function useHlsStream(videoRef, { src, shouldPlay, onFatalError }) {
11083
11780
  }
11084
11781
  }
11085
11782
  };
11783
+ const handleLoadedMetadata = () => {
11784
+ if (!isR2StreamRef.current) return;
11785
+ const video = videoRef.current;
11786
+ if (!video) return;
11787
+ const edgeTime = getLiveEdgeTime();
11788
+ if (edgeTime !== null) {
11789
+ video.currentTime = edgeTime;
11790
+ }
11791
+ };
11792
+ const handleEnded = () => {
11793
+ if (isNativeHlsRef.current) {
11794
+ refreshLiveStream("ended");
11795
+ return;
11796
+ }
11797
+ if (isR2StreamRef.current) {
11798
+ softRestart("ended");
11799
+ }
11800
+ };
11086
11801
  const handleWaiting = () => {
11087
- if (isNativeHlsRef.current) return;
11088
- console.log("[HLS] Video waiting (buffer underrun)");
11089
11802
  if (waitingTimerRef.current) {
11090
11803
  clearTimeout(waitingTimerRef.current);
11091
11804
  }
11805
+ if (isNativeHlsRef.current) {
11806
+ if (!isR2StreamRef.current) return;
11807
+ waitingTimerRef.current = setTimeout(() => {
11808
+ refreshLiveStream("native waiting timeout");
11809
+ }, getWaitingTimeoutMs());
11810
+ return;
11811
+ }
11812
+ console.log("[HLS] Video waiting (buffer underrun)");
11813
+ if (isR2StreamRef.current) {
11814
+ waitingTimerRef.current = setTimeout(() => {
11815
+ const video = videoRef.current;
11816
+ if (video && video.readyState < 3) {
11817
+ if (shouldAttemptRecovery()) {
11818
+ softRestart("waiting timeout");
11819
+ } else {
11820
+ handleWaiting();
11821
+ }
11822
+ }
11823
+ }, getWaitingTimeoutMs());
11824
+ return;
11825
+ }
11092
11826
  waitingTimerRef.current = setTimeout(() => {
11093
11827
  const video = videoRef.current;
11094
11828
  if (video && video.readyState < 3) {
@@ -11100,6 +11834,7 @@ function useHlsStream(videoRef, { src, shouldPlay, onFatalError }) {
11100
11834
  const video = videoRef.current;
11101
11835
  if (!video) return;
11102
11836
  lastTimeUpdateRef.current = video.currentTime;
11837
+ playRetryCountRef.current = 0;
11103
11838
  if (waitingTimerRef.current) {
11104
11839
  clearTimeout(waitingTimerRef.current);
11105
11840
  waitingTimerRef.current = null;
@@ -11110,19 +11845,33 @@ function useHlsStream(videoRef, { src, shouldPlay, onFatalError }) {
11110
11845
  hardRestart("native video error");
11111
11846
  };
11112
11847
  const startStallDetection = () => {
11113
- if (isNativeHlsRef.current) return;
11848
+ if (isNativeHlsRef.current || isR2StreamRef.current) return;
11114
11849
  stallCheckIntervalRef.current = setInterval(() => {
11115
11850
  const video = videoRef.current;
11116
11851
  if (!video || video.paused || video.ended) return;
11852
+ if (video.readyState < 3 || video.seeking) return;
11117
11853
  const currentTime = video.currentTime;
11118
11854
  const lastTime = lastTimeUpdateRef.current;
11855
+ const bufferGap = getBufferGap(video);
11856
+ const nearBufferEnd = bufferGap !== null && bufferGap < 0.5;
11119
11857
  if (Math.abs(currentTime - lastTime) < 0.1 && video.readyState >= 2) {
11858
+ if (nearBufferEnd && !shouldAttemptRecovery()) {
11859
+ if (noProgressTimerRef.current) {
11860
+ clearTimeout(noProgressTimerRef.current);
11861
+ noProgressTimerRef.current = null;
11862
+ }
11863
+ return;
11864
+ }
11120
11865
  console.warn("[HLS] Playback stall detected");
11121
11866
  if (!noProgressTimerRef.current) {
11122
11867
  noProgressTimerRef.current = setTimeout(() => {
11868
+ if (nearBufferEnd && !shouldAttemptRecovery()) {
11869
+ noProgressTimerRef.current = null;
11870
+ return;
11871
+ }
11123
11872
  softRestart("playback stall");
11124
11873
  noProgressTimerRef.current = null;
11125
- }, 8e3);
11874
+ }, nearBufferEnd ? getWaitingTimeoutMs() : 12e3);
11126
11875
  }
11127
11876
  } else {
11128
11877
  if (noProgressTimerRef.current) {
@@ -11133,65 +11882,311 @@ function useHlsStream(videoRef, { src, shouldPlay, onFatalError }) {
11133
11882
  }, 7e3);
11134
11883
  };
11135
11884
  React25.useEffect(() => {
11136
- if (isPermanentlyFailed) {
11885
+ if (!src || !shouldPlay) {
11886
+ cleanup();
11887
+ return;
11888
+ }
11889
+ let isCancelled = false;
11890
+ const r2WorkerDomain = process.env.NEXT_PUBLIC_R2_WORKER_DOMAIN || "https://r2-stream-proxy.optifye-r2.workers.dev";
11891
+ const isR2Stream = isR2WorkerUrl(src, r2WorkerDomain);
11892
+ isR2StreamRef.current = isR2Stream;
11893
+ if (isPermanentlyFailed && !isR2Stream) {
11137
11894
  console.warn(`[HLS] URL is permanently failed, not attempting to load: ${src}`);
11138
11895
  if (onFatalError) {
11139
11896
  onFatalError();
11140
11897
  }
11141
11898
  return;
11142
11899
  }
11143
- if (!src || !shouldPlay) {
11144
- cleanup();
11145
- return;
11900
+ if (isPermanentlyFailed && isR2Stream && failedUrls.has(src)) {
11901
+ failedUrls.delete(src);
11146
11902
  }
11147
11903
  const video = videoRef.current;
11148
11904
  if (!video) return;
11149
- isNativeHlsRef.current = video.canPlayType("application/vnd.apple.mpegurl") === "probably";
11150
- if (Hls3__default.default.isSupported() && !isNativeHlsRef.current) {
11151
- const hls = new Hls3__default.default(HLS_CONFIG);
11152
- hlsRef.current = hls;
11153
- hls.attachMedia(video);
11154
- hls.loadSource(src);
11155
- hls.on(Hls3__default.default.Events.ERROR, (_, data) => {
11156
- if (!data.fatal) return;
11157
- console.error("[HLS] Fatal error:", data.type, data.details);
11158
- if (data.response?.code === 404) {
11159
- hardRestart("404 hard restart");
11905
+ if (typeof document !== "undefined") {
11906
+ document.addEventListener("visibilitychange", handleVisibilityChange);
11907
+ }
11908
+ const initialize = async () => {
11909
+ const canUseNative = video.canPlayType("application/vnd.apple.mpegurl") === "probably";
11910
+ isNativeHlsRef.current = canUseNative && !isR2Stream;
11911
+ let authToken = null;
11912
+ if (isR2Stream) {
11913
+ try {
11914
+ const supabase = _getSupabaseInstance();
11915
+ authToken = await getAuthTokenForHls(supabase);
11916
+ } catch (error) {
11917
+ console.warn("[HLS] Unable to retrieve auth token for R2 streaming:", error);
11918
+ }
11919
+ }
11920
+ authTokenRef.current = authToken;
11921
+ if (isCancelled) return;
11922
+ const cameraUuid = isR2Stream ? getR2CameraUuid(src) : null;
11923
+ const proxyPlaylistUrl = proxyEnabled && isR2Stream && proxyBaseUrl && cameraUuid ? `${proxyBaseUrl}/segments/${cameraUuid}/index.m3u8` : null;
11924
+ const resolvedSrc = proxyPlaylistUrl || src;
11925
+ const resolvedHlsSrc = src;
11926
+ const shouldUseProxy = proxyEnabled && isR2Stream && canUseNative && !Hls__default.default.isSupported() && isSafari();
11927
+ if (shouldUseProxy) {
11928
+ if (cameraUuid) {
11929
+ isNativeHlsRef.current = true;
11930
+ nativeStreamUrlRef.current = resolvedSrc;
11931
+ activeStreamUrlRef.current = resolvedSrc;
11932
+ console.log("[HLS] Using proxy playlist for Safari R2 stream");
11933
+ video.src = resolvedSrc;
11934
+ video.addEventListener("waiting", handleWaiting);
11935
+ video.addEventListener("loadedmetadata", handleLoadedMetadata);
11936
+ video.addEventListener("canplay", handleCanPlay);
11937
+ video.addEventListener("ended", handleEnded);
11938
+ video.addEventListener("error", handleNativeError);
11939
+ attemptPlay();
11160
11940
  return;
11161
11941
  }
11162
- switch (data.type) {
11163
- case Hls3__default.default.ErrorTypes.NETWORK_ERROR:
11164
- case Hls3__default.default.ErrorTypes.MEDIA_ERROR:
11165
- softRestart(`${data.type}: ${data.details}`);
11166
- break;
11167
- default:
11168
- hardRestart(`Fatal ${data.type}: ${data.details}`);
11169
- break;
11170
- }
11171
- });
11172
- hls.on(Hls3__default.default.Events.MANIFEST_PARSED, () => {
11173
- if (failedUrls.has(src)) {
11174
- console.log(`[HLS] Stream loaded successfully, resetting failure count for: ${src}`);
11175
- failedUrls.delete(src);
11942
+ console.warn("[HLS] Safari R2 proxy unavailable, falling back to direct URL");
11943
+ }
11944
+ if (!proxyEnabled && isR2Stream && canUseNative && !Hls__default.default.isSupported() && isSafari()) {
11945
+ console.warn("[HLS] Safari native R2 streaming requires proxy. Falling back.");
11946
+ if (onFatalError) {
11947
+ onFatalError();
11176
11948
  }
11177
- video.play().catch((err) => {
11178
- console.error("[HLS] Play failed:", err);
11949
+ return;
11950
+ }
11951
+ if (Hls__default.default.isSupported() && !isNativeHlsRef.current) {
11952
+ const mergedConfig = {
11953
+ ...HLS_CONFIG,
11954
+ ...hlsConfig,
11955
+ xhrSetup: (xhr, url) => {
11956
+ const usesProxy = isProxyUrl(url);
11957
+ if (isR2WorkerUrl(url, r2WorkerDomain) || usesProxy) {
11958
+ if (isManifestUrl(url)) {
11959
+ xhr.open("GET", buildCacheBustedUrl(url), true);
11960
+ }
11961
+ if (authToken) {
11962
+ xhr.setRequestHeader("Authorization", `Bearer ${authToken}`);
11963
+ }
11964
+ }
11965
+ },
11966
+ fetchSetup: (context, initParams) => {
11967
+ const isR2Url = isR2WorkerUrl(context.url, r2WorkerDomain);
11968
+ const isManifestRequest = isManifestUrl(context.url);
11969
+ const usesProxy = isProxyUrl(context.url);
11970
+ let requestUrl = context.url;
11971
+ if (isR2Url || usesProxy) {
11972
+ if (authToken) {
11973
+ initParams.headers = {
11974
+ ...initParams.headers,
11975
+ "Authorization": `Bearer ${authToken}`
11976
+ };
11977
+ }
11978
+ if (isManifestRequest) {
11979
+ requestUrl = buildCacheBustedUrl(context.url);
11980
+ initParams.cache = "no-store";
11981
+ }
11982
+ }
11983
+ return new Request(requestUrl, initParams);
11984
+ }
11985
+ };
11986
+ const hls = new Hls__default.default(mergedConfig);
11987
+ hlsRef.current = hls;
11988
+ hls.attachMedia(video);
11989
+ hls.loadSource(resolvedHlsSrc);
11990
+ activeStreamUrlRef.current = resolvedHlsSrc;
11991
+ hls.on(Hls__default.default.Events.ERROR, (_, data) => {
11992
+ debugLog("[HLS] Error", {
11993
+ type: data.type,
11994
+ details: data.details,
11995
+ fatal: data.fatal,
11996
+ response: data.response?.code,
11997
+ frag: data.frag?.sn
11998
+ });
11999
+ if (data.type === Hls__default.default.ErrorTypes.MEDIA_ERROR && data.details === Hls__default.default.ErrorDetails.BUFFER_STALLED_ERROR) {
12000
+ debugLog("[HLS] Buffer stalled, waiting for next segment");
12001
+ attemptPlay();
12002
+ return;
12003
+ }
12004
+ 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)) {
12005
+ const fragSn = data.frag?.sn;
12006
+ const windowStart = lastWindowStartSnRef.current;
12007
+ const windowEnd = lastWindowEndSnRef.current;
12008
+ if (typeof fragSn === "number" && typeof windowStart === "number" && fragSn < windowStart) {
12009
+ softRestart("frag fell out of window");
12010
+ return;
12011
+ }
12012
+ if (typeof fragSn === "number" && typeof windowEnd === "number" && fragSn > windowEnd) {
12013
+ softRestart("frag ahead of window");
12014
+ return;
12015
+ }
12016
+ const now2 = Date.now();
12017
+ if (lastFragTimeoutSnRef.current === fragSn && lastFragTimeoutAtRef.current) {
12018
+ if (now2 - lastFragTimeoutAtRef.current < 12e4) {
12019
+ fragTimeoutCountRef.current += 1;
12020
+ } else {
12021
+ fragTimeoutCountRef.current = 1;
12022
+ }
12023
+ } else {
12024
+ fragTimeoutCountRef.current = 1;
12025
+ lastFragTimeoutSnRef.current = fragSn ?? null;
12026
+ }
12027
+ lastFragTimeoutAtRef.current = now2;
12028
+ if (fragTimeoutCountRef.current >= 2) {
12029
+ softRestart("frag load timeout");
12030
+ }
12031
+ return;
12032
+ }
12033
+ 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)) {
12034
+ scheduleManifestRetry("manifest load timeout");
12035
+ return;
12036
+ }
12037
+ if (!data.fatal) return;
12038
+ console.error("[HLS] Fatal error:", data.type, data.details);
12039
+ if (data.response?.code === 404) {
12040
+ if (data.details === Hls__default.default.ErrorDetails.MANIFEST_LOAD_ERROR || data.details === Hls__default.default.ErrorDetails.LEVEL_LOAD_ERROR) {
12041
+ if (isR2StreamRef.current) {
12042
+ scheduleManifestRetry("manifest 404");
12043
+ return;
12044
+ }
12045
+ hardRestart("404 manifest hard restart");
12046
+ return;
12047
+ }
12048
+ softRestart("frag 404");
12049
+ return;
12050
+ }
12051
+ switch (data.type) {
12052
+ case Hls__default.default.ErrorTypes.NETWORK_ERROR:
12053
+ if (isR2StreamRef.current && data.details === Hls__default.default.ErrorDetails.MANIFEST_LOAD_TIMEOUT) {
12054
+ softRestart("manifest load timeout");
12055
+ break;
12056
+ }
12057
+ softRestart(`${data.type}: ${data.details}`);
12058
+ break;
12059
+ case Hls__default.default.ErrorTypes.MEDIA_ERROR:
12060
+ softRestart(`${data.type}: ${data.details}`);
12061
+ break;
12062
+ default:
12063
+ hardRestart(`Fatal ${data.type}: ${data.details}`);
12064
+ break;
12065
+ }
11179
12066
  });
11180
- });
11181
- video.addEventListener("waiting", handleWaiting);
11182
- video.addEventListener("timeupdate", handleTimeUpdate);
11183
- startStallDetection();
11184
- } else if (isNativeHlsRef.current) {
11185
- console.log("[HLS] Using native HLS");
11186
- video.src = src;
11187
- video.addEventListener("error", handleNativeError);
11188
- video.play().catch((err) => {
11189
- console.error("[HLS] Native play failed:", err);
11190
- });
11191
- } else {
11192
- console.error("[HLS] HLS not supported");
11193
- }
11194
- return cleanup;
12067
+ hls.on(Hls__default.default.Events.MANIFEST_PARSED, () => {
12068
+ if (failedUrls.has(src)) {
12069
+ console.log(`[HLS] Stream loaded successfully, resetting failure count for: ${src}`);
12070
+ failedUrls.delete(src);
12071
+ }
12072
+ if (isR2Stream) {
12073
+ seekToLiveEdge();
12074
+ }
12075
+ attemptPlay();
12076
+ });
12077
+ hls.on(Hls__default.default.Events.LEVEL_LOADED, (_event, data) => {
12078
+ if (!data?.details) return;
12079
+ const details = data.details;
12080
+ if (isR2Stream) {
12081
+ lastManifestLoadRef.current = Date.now();
12082
+ resetManifestRetry();
12083
+ if (details.endList) {
12084
+ details.endList = false;
12085
+ }
12086
+ details.live = true;
12087
+ details.type = "LIVE";
12088
+ if (details.targetduration && Number.isFinite(details.targetduration)) {
12089
+ targetDurationRef.current = details.targetduration;
12090
+ }
12091
+ if (typeof details.startSN === "number") {
12092
+ lastWindowStartSnRef.current = details.startSN;
12093
+ }
12094
+ if (typeof details.endSN === "number") {
12095
+ lastWindowEndSnRef.current = Math.max(details.endSN - 1, details.startSN ?? details.endSN);
12096
+ }
12097
+ if (Array.isArray(details.fragments) && details.fragments.length > 0) {
12098
+ const firstSn = details.fragments[0]?.sn;
12099
+ const lastSn = details.fragments[details.fragments.length - 1]?.sn;
12100
+ if (typeof firstSn === "number") {
12101
+ lastWindowStartSnRef.current = firstSn;
12102
+ }
12103
+ if (typeof lastSn === "number") {
12104
+ lastWindowEndSnRef.current = lastSn;
12105
+ }
12106
+ }
12107
+ }
12108
+ if (maxManifestAgeMs > 0 && !details.endList) {
12109
+ const now2 = Date.now();
12110
+ const fragments = Array.isArray(details.fragments) ? details.fragments : [];
12111
+ const lastFragment = fragments.length ? fragments[fragments.length - 1] : void 0;
12112
+ const programDateTimeMs = getProgramDateTimeMs(lastFragment?.programDateTime);
12113
+ if (programDateTimeMs && now2 - programDateTimeMs > maxManifestAgeMs) {
12114
+ markStaleStream(`segment age ${Math.round((now2 - programDateTimeMs) / 1e3)}s`);
12115
+ return;
12116
+ }
12117
+ const endSn = typeof details.endSN === "number" ? details.endSN : lastFragment?.sn;
12118
+ if (typeof endSn === "number") {
12119
+ if (lastManifestEndSnRef.current === endSn) {
12120
+ const lastUpdatedAt = lastManifestEndSnUpdatedAtRef.current;
12121
+ if (lastUpdatedAt && now2 - lastUpdatedAt > maxManifestAgeMs) {
12122
+ markStaleStream(`sequence stalled at ${endSn}`);
12123
+ return;
12124
+ }
12125
+ } else {
12126
+ lastManifestEndSnRef.current = endSn;
12127
+ lastManifestEndSnUpdatedAtRef.current = now2;
12128
+ }
12129
+ }
12130
+ }
12131
+ debugLog("[HLS] Level loaded", {
12132
+ targetduration: details.targetduration,
12133
+ edge: details.edge,
12134
+ fragments: data.details?.fragments?.length
12135
+ });
12136
+ if (isR2Stream && !forcedLiveStartRef.current) {
12137
+ forcedLiveStartRef.current = true;
12138
+ seekToLiveEdge();
12139
+ }
12140
+ });
12141
+ hls.on(Hls__default.default.Events.MANIFEST_LOADED, () => {
12142
+ if (!isR2Stream) return;
12143
+ lastManifestLoadRef.current = Date.now();
12144
+ resetManifestRetry();
12145
+ });
12146
+ hls.on(Hls__default.default.Events.FRAG_LOADED, (_event, data) => {
12147
+ if (!isR2Stream) return;
12148
+ lastFragLoadRef.current = Date.now();
12149
+ if (typeof data?.frag?.sn === "number") {
12150
+ lastFragSnRef.current = data.frag.sn;
12151
+ }
12152
+ });
12153
+ hls.on(Hls__default.default.Events.FRAG_BUFFERED, () => {
12154
+ attemptPlay();
12155
+ });
12156
+ video.addEventListener("waiting", handleWaiting);
12157
+ video.addEventListener("timeupdate", handleTimeUpdate);
12158
+ video.addEventListener("loadedmetadata", handleLoadedMetadata);
12159
+ video.addEventListener("canplay", handleCanPlay);
12160
+ video.addEventListener("ended", handleEnded);
12161
+ startStallDetection();
12162
+ startPlaybackGovernor();
12163
+ startManifestWatchdog();
12164
+ startManifestWatchdog();
12165
+ return;
12166
+ }
12167
+ if (canUseNative) {
12168
+ isNativeHlsRef.current = true;
12169
+ console.log("[HLS] Using native HLS");
12170
+ video.src = resolvedSrc;
12171
+ nativeStreamUrlRef.current = resolvedSrc;
12172
+ activeStreamUrlRef.current = resolvedSrc;
12173
+ video.addEventListener("waiting", handleWaiting);
12174
+ video.addEventListener("loadedmetadata", handleLoadedMetadata);
12175
+ video.addEventListener("canplay", handleCanPlay);
12176
+ video.addEventListener("ended", handleEnded);
12177
+ video.addEventListener("error", handleNativeError);
12178
+ startPlaybackGovernor();
12179
+ startManifestWatchdog();
12180
+ attemptPlay();
12181
+ } else {
12182
+ console.error("[HLS] HLS not supported");
12183
+ }
12184
+ };
12185
+ initialize();
12186
+ return () => {
12187
+ isCancelled = true;
12188
+ cleanup();
12189
+ };
11195
12190
  }, [src, shouldPlay, restartKey, onFatalError, isPermanentlyFailed]);
11196
12191
  return {
11197
12192
  restartKey,
@@ -11199,11 +12194,11 @@ function useHlsStream(videoRef, { src, shouldPlay, onFatalError }) {
11199
12194
  };
11200
12195
  }
11201
12196
  function useHlsStreamWithCropping(videoRef, canvasRef, options) {
11202
- const { src, shouldPlay, cropping, canvasFps = 30, useRAF = true, onFatalError } = options;
12197
+ const { src, shouldPlay, cropping, canvasFps = 30, useRAF = true, onFatalError, hlsConfig } = options;
11203
12198
  const animationFrameRef = React25.useRef(null);
11204
12199
  const intervalRef = React25.useRef(null);
11205
12200
  const isDrawingRef = React25.useRef(false);
11206
- const hlsState = useHlsStream(videoRef, { src, shouldPlay, onFatalError });
12201
+ const hlsState = useHlsStream(videoRef, { src, shouldPlay, onFatalError, hlsConfig });
11207
12202
  const calculateCropRect = React25.useCallback((video, cropping2) => {
11208
12203
  const videoWidth = video.videoWidth;
11209
12204
  const videoHeight = video.videoHeight;
@@ -11937,6 +12932,95 @@ var useWorkspaceDisplayNamesMap = (workspaceIds, lineId, companyId) => {
11937
12932
  refetch: fetchDisplayNames
11938
12933
  };
11939
12934
  };
12935
+ var useWorkspaceVideoStreams = (workspaces) => {
12936
+ const workspaceIdsKey = React25.useMemo(() => {
12937
+ const ids = /* @__PURE__ */ new Set();
12938
+ for (const workspace of workspaces) {
12939
+ if (workspace.workspace_uuid) {
12940
+ ids.add(workspace.workspace_uuid);
12941
+ }
12942
+ }
12943
+ return Array.from(ids).sort().join(",");
12944
+ }, [workspaces]);
12945
+ const lineIdsKey = React25.useMemo(() => {
12946
+ const ids = /* @__PURE__ */ new Set();
12947
+ for (const workspace of workspaces) {
12948
+ if (workspace.line_id) {
12949
+ ids.add(workspace.line_id);
12950
+ }
12951
+ }
12952
+ return Array.from(ids).sort().join(",");
12953
+ }, [workspaces]);
12954
+ const requestKey = React25.useMemo(() => {
12955
+ if (workspaceIdsKey) {
12956
+ return `workspaces:${workspaceIdsKey}`;
12957
+ }
12958
+ if (lineIdsKey) {
12959
+ return `lines:${lineIdsKey}`;
12960
+ }
12961
+ return "";
12962
+ }, [workspaceIdsKey, lineIdsKey]);
12963
+ const workspaceIds = React25.useMemo(
12964
+ () => workspaceIdsKey ? workspaceIdsKey.split(",") : [],
12965
+ [workspaceIdsKey]
12966
+ );
12967
+ const lineIds = React25.useMemo(
12968
+ () => lineIdsKey ? lineIdsKey.split(",") : [],
12969
+ [lineIdsKey]
12970
+ );
12971
+ const [state, setState] = React25.useState(() => ({
12972
+ streamsByWorkspaceId: {},
12973
+ isLoading: Boolean(requestKey),
12974
+ error: null
12975
+ }));
12976
+ const lastKeyRef = React25.useRef(requestKey);
12977
+ React25.useEffect(() => {
12978
+ lastKeyRef.current = requestKey;
12979
+ if (!workspaceIds.length && !lineIds.length) {
12980
+ setState({
12981
+ streamsByWorkspaceId: {},
12982
+ isLoading: false,
12983
+ error: null
12984
+ });
12985
+ return;
12986
+ }
12987
+ let isCancelled = false;
12988
+ setState((prev) => ({ ...prev, isLoading: true, error: null }));
12989
+ const fetchStreams = async () => {
12990
+ try {
12991
+ const streams = await workspaceService.getWorkspaceVideoStreams({
12992
+ workspaceIds,
12993
+ lineIds
12994
+ });
12995
+ if (!isCancelled) {
12996
+ setState({
12997
+ streamsByWorkspaceId: streams,
12998
+ isLoading: false,
12999
+ error: null
13000
+ });
13001
+ }
13002
+ } catch (error) {
13003
+ if (!isCancelled) {
13004
+ setState((prev) => ({
13005
+ ...prev,
13006
+ isLoading: false,
13007
+ error: error instanceof Error ? error.message : "Failed to load video streams"
13008
+ }));
13009
+ }
13010
+ }
13011
+ };
13012
+ fetchStreams();
13013
+ return () => {
13014
+ isCancelled = true;
13015
+ };
13016
+ }, [workspaceIdsKey, lineIdsKey]);
13017
+ const isLoading = state.isLoading || lastKeyRef.current !== requestKey;
13018
+ return {
13019
+ streamsByWorkspaceId: state.streamsByWorkspaceId,
13020
+ isLoading,
13021
+ error: state.error
13022
+ };
13023
+ };
11940
13024
  var useActiveBreaks = (lineIds) => {
11941
13025
  const [activeBreaks, setActiveBreaks] = React25.useState([]);
11942
13026
  const [isLoading, setIsLoading] = React25.useState(true);
@@ -17469,24 +18553,6 @@ var createThrottledReload = (interval = 5e3, maxReloads = 3) => {
17469
18553
  };
17470
18554
  var throttledReloadDashboard = createThrottledReload(5e3, 3);
17471
18555
 
17472
- // src/lib/utils/browser.ts
17473
- function isSafari() {
17474
- if (typeof window === "undefined") return false;
17475
- const ua = window.navigator.userAgent;
17476
- const isSafariUA = /^((?!chrome|android).)*safari/i.test(ua);
17477
- const isIOS = /iPad|iPhone|iPod/.test(ua) && !window.MSStream;
17478
- return isSafariUA || isIOS;
17479
- }
17480
- function getBrowserName() {
17481
- if (typeof window === "undefined") return "server";
17482
- const ua = window.navigator.userAgent;
17483
- if (/chrome/i.test(ua) && !/edge/i.test(ua)) return "chrome";
17484
- if (/safari/i.test(ua) && !/chrome/i.test(ua)) return "safari";
17485
- if (/firefox/i.test(ua)) return "firefox";
17486
- if (/edge/i.test(ua)) return "edge";
17487
- return "unknown";
17488
- }
17489
-
17490
18556
  // src/lib/utils/index.ts
17491
18557
  var formatIdleTime = (idleTimeInSeconds) => {
17492
18558
  if (!idleTimeInSeconds || idleTimeInSeconds <= 0) {
@@ -17505,83 +18571,29 @@ var formatIdleTime = (idleTimeInSeconds) => {
17505
18571
  parts.push(`${seconds}s`);
17506
18572
  return parts.join(" ");
17507
18573
  };
17508
- var createSupabaseClient = (url, key) => supabaseJs.createClient(url, key, {
17509
- auth: {
17510
- autoRefreshToken: true,
17511
- persistSession: true,
17512
- detectSessionInUrl: true,
17513
- flowType: "pkce",
17514
- // Enable debug logging in development
17515
- debug: process.env.NODE_ENV === "development"
17516
- },
17517
- db: {
17518
- schema: "public"
17519
- // Default schema, we'll use .schema('ai') in queries
17520
- },
17521
- global: {
17522
- headers: {
17523
- "x-application-name": "optifye-dashboard"
17524
- },
17525
- // Add global fetch timeout (5 minutes)
17526
- fetch: async (url2, options = {}) => {
17527
- const controller = new AbortController();
17528
- const timeoutId = setTimeout(() => controller.abort(), 3e5);
17529
- try {
17530
- const response = await fetch(url2, {
17531
- ...options,
17532
- signal: controller.signal
17533
- });
17534
- clearTimeout(timeoutId);
17535
- return response;
17536
- } catch (error) {
17537
- clearTimeout(timeoutId);
17538
- throw error;
17539
- }
17540
- }
18574
+ var cachedClient2 = null;
18575
+ var cachedConfig2 = null;
18576
+ var getCachedClient = (url, key) => {
18577
+ if (cachedClient2 && cachedConfig2?.url === url && cachedConfig2?.key === key) {
18578
+ return cachedClient2;
17541
18579
  }
17542
- });
17543
- var getAnonClient = () => {
17544
- const url = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
17545
- const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
17546
- if (!url || !key) {
17547
- console.warn("[dashboard-core] Missing supabase env variables");
17548
- }
17549
- return supabaseJs.createClient(url, key, {
17550
- auth: {
17551
- autoRefreshToken: true,
17552
- persistSession: true,
17553
- detectSessionInUrl: true,
17554
- flowType: "pkce",
17555
- debug: process.env.NODE_ENV === "development"
17556
- },
17557
- global: {
17558
- headers: {
17559
- "x-application-name": "optifye-dashboard"
17560
- },
17561
- // Add global fetch timeout (5 minutes)
17562
- fetch: async (url2, options = {}) => {
17563
- const controller = new AbortController();
17564
- const timeoutId = setTimeout(() => controller.abort(), 3e5);
17565
- try {
17566
- const response = await fetch(url2, {
17567
- ...options,
17568
- signal: controller.signal
17569
- });
17570
- clearTimeout(timeoutId);
17571
- return response;
17572
- } catch (error) {
17573
- clearTimeout(timeoutId);
17574
- throw error;
17575
- }
17576
- }
17577
- }
17578
- });
18580
+ cachedClient2 = createSupabaseClient(url, key);
18581
+ cachedConfig2 = { url, key };
18582
+ _setSupabaseInstance(cachedClient2);
18583
+ return cachedClient2;
17579
18584
  };
17580
-
17581
- // src/lib/supabaseClient.ts
17582
18585
  function useSupabaseClient() {
17583
18586
  const { supabaseUrl, supabaseKey } = useDashboardConfig();
17584
- const supabase = React25.useMemo(() => createSupabaseClient(supabaseUrl, supabaseKey), [supabaseUrl, supabaseKey]);
18587
+ const providerClient = useOptionalSupabase();
18588
+ const supabase = React25.useMemo(() => {
18589
+ if (providerClient) {
18590
+ return providerClient;
18591
+ }
18592
+ if (!supabaseUrl || !supabaseKey) {
18593
+ throw new Error("Supabase configuration missing in DashboardConfig");
18594
+ }
18595
+ return getCachedClient(supabaseUrl, supabaseKey);
18596
+ }, [providerClient, supabaseUrl, supabaseKey]);
17585
18597
  return supabase;
17586
18598
  }
17587
18599
  var LayoutGroupContext = React25.createContext({});
@@ -27662,22 +28674,14 @@ var VideoCard = React25__namespace.default.memo(({
27662
28674
  const videoRef = React25.useRef(null);
27663
28675
  const canvasRef = React25.useRef(null);
27664
28676
  const effectiveLegend = legend || DEFAULT_EFFICIENCY_LEGEND;
27665
- if (cropping) {
27666
- useHlsStreamWithCropping(videoRef, canvasRef, {
27667
- src: hlsUrl,
27668
- shouldPlay,
27669
- cropping,
27670
- canvasFps,
27671
- useRAF,
27672
- onFatalError: onFatalError ?? (() => throttledReloadDashboard())
27673
- });
27674
- } else {
27675
- useHlsStream(videoRef, {
27676
- src: hlsUrl,
27677
- shouldPlay,
27678
- onFatalError: onFatalError ?? (() => throttledReloadDashboard())
27679
- });
27680
- }
28677
+ useHlsStreamWithCropping(videoRef, canvasRef, {
28678
+ src: hlsUrl,
28679
+ shouldPlay,
28680
+ cropping,
28681
+ canvasFps,
28682
+ useRAF,
28683
+ onFatalError: onFatalError ?? (() => throttledReloadDashboard())
28684
+ });
27681
28685
  const workspaceDisplayName = displayName || getWorkspaceDisplayName(workspace.workspace_name, workspace.line_id);
27682
28686
  const efficiencyColor = getEfficiencyColor(workspace.efficiency, effectiveLegend);
27683
28687
  const efficiencyOverlayClass = efficiencyColor === "green" ? "bg-[#00D654]/25" : efficiencyColor === "yellow" ? "bg-[#FFD700]/30" : "bg-[#FF2D0A]/30";
@@ -27812,6 +28816,8 @@ var VideoGridView = React25__namespace.default.memo(({
27812
28816
  className = "",
27813
28817
  legend,
27814
28818
  videoSources = {},
28819
+ videoStreamsByWorkspaceId,
28820
+ videoStreamsLoading = false,
27815
28821
  displayNames = {},
27816
28822
  onWorkspaceHover,
27817
28823
  onWorkspaceHoverEnd
@@ -27823,6 +28829,15 @@ var VideoGridView = React25__namespace.default.memo(({
27823
28829
  const [gridRows, setGridRows] = React25.useState(1);
27824
28830
  const [visibleWorkspaces, setVisibleWorkspaces] = React25.useState(/* @__PURE__ */ new Set());
27825
28831
  const [failedStreams, setFailedStreams] = React25.useState(/* @__PURE__ */ new Set());
28832
+ const [r2FallbackWorkspaces, setR2FallbackWorkspaces] = React25.useState(/* @__PURE__ */ new Set());
28833
+ const workspaceIdsKey = React25.useMemo(() => {
28834
+ const ids = workspaces.map((ws) => ws.workspace_uuid || ws.workspace_name);
28835
+ return ids.sort().join(",");
28836
+ }, [workspaces]);
28837
+ React25.useEffect(() => {
28838
+ setFailedStreams(/* @__PURE__ */ new Set());
28839
+ setR2FallbackWorkspaces(/* @__PURE__ */ new Set());
28840
+ }, [workspaceIdsKey]);
27826
28841
  const videoConfig = useVideoConfig();
27827
28842
  const dashboardConfig = useDashboardConfig();
27828
28843
  const { cropping, canvasConfig, hlsUrls } = videoConfig;
@@ -27842,13 +28857,16 @@ var VideoGridView = React25__namespace.default.memo(({
27842
28857
  }
27843
28858
  return mergedVideoSources.workspaceHlsUrls[wsName] || mergedVideoSources.defaultHlsUrl;
27844
28859
  }, [mergedVideoSources]);
27845
- const getWorkspaceCropping = React25.useCallback((workspaceName) => {
28860
+ const getWorkspaceCropping = React25.useCallback((workspaceId, workspaceName) => {
28861
+ if (videoStreamsByWorkspaceId) {
28862
+ return videoStreamsByWorkspaceId[workspaceId]?.crop ?? void 0;
28863
+ }
27846
28864
  if (!cropping) return void 0;
27847
28865
  if (cropping.workspaceOverrides?.[workspaceName]) {
27848
28866
  return cropping.workspaceOverrides[workspaceName];
27849
28867
  }
27850
28868
  return cropping.default;
27851
- }, [cropping]);
28869
+ }, [cropping, videoStreamsByWorkspaceId]);
27852
28870
  const filteredWorkspaces = React25.useMemo(() => {
27853
28871
  return selectedLine === 1 ? workspaces.filter((w) => {
27854
28872
  if (w.workspace_name === "WS5-5") return true;
@@ -27870,6 +28888,7 @@ var VideoGridView = React25__namespace.default.memo(({
27870
28888
  }
27871
28889
  }) : workspaces;
27872
28890
  }, [workspaces, selectedLine]);
28891
+ const streamsReady = !videoStreamsLoading;
27873
28892
  const calculateOptimalGrid = React25.useCallback(() => {
27874
28893
  if (!containerRef.current) return;
27875
28894
  const containerPadding = 16;
@@ -27973,12 +28992,31 @@ var VideoGridView = React25__namespace.default.memo(({
27973
28992
  const navParams = getWorkspaceNavigationParams(workspaceId, displayName, workspace.line_id);
27974
28993
  router$1.push(`/workspace/${workspaceId}${navParams}`);
27975
28994
  }, [router$1, dashboardConfig]);
27976
- const handleStreamError = React25.useCallback((workspaceId) => {
28995
+ const handleStreamError = React25.useCallback((workspaceId, options) => {
28996
+ const isR2Stream = options?.isR2Stream ?? false;
28997
+ const hasFallback = Boolean(options?.fallbackUrl);
28998
+ if (isR2Stream && hasFallback) {
28999
+ console.warn(`[VideoGridView] R2 stream failed for workspace: ${workspaceId}. Falling back to media config.`);
29000
+ setR2FallbackWorkspaces((prev) => new Set(prev).add(workspaceId));
29001
+ setFailedStreams((prev) => {
29002
+ const next = new Set(prev);
29003
+ next.delete(workspaceId);
29004
+ return next;
29005
+ });
29006
+ trackCoreEvent("Video Stream Error", {
29007
+ workspace_id: workspaceId,
29008
+ view_type: "video_grid",
29009
+ stream_source: "r2",
29010
+ fallback: "media_config"
29011
+ });
29012
+ return;
29013
+ }
27977
29014
  console.error(`[VideoGridView] Stream failed for workspace: ${workspaceId}`);
27978
29015
  setFailedStreams((prev) => new Set(prev).add(workspaceId));
27979
29016
  trackCoreEvent("Video Stream Error", {
27980
29017
  workspace_id: workspaceId,
27981
- view_type: "video_grid"
29018
+ view_type: "video_grid",
29019
+ stream_source: isR2Stream ? "r2" : "media_config"
27982
29020
  });
27983
29021
  }, []);
27984
29022
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `relative overflow-hidden h-full w-full ${className}`, children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: "h-full w-full p-3 sm:p-2", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -27990,57 +29028,92 @@ var VideoGridView = React25__namespace.default.memo(({
27990
29028
  gridTemplateRows: `repeat(${gridRows}, 1fr)`,
27991
29029
  gridAutoFlow: "row"
27992
29030
  },
27993
- children: filteredWorkspaces.sort((a, b) => {
27994
- if (a.line_id !== b.line_id) {
27995
- return (a.line_id || "").localeCompare(b.line_id || "");
27996
- }
27997
- const aMatch = a.workspace_name.match(/WS(\d+)/);
27998
- const bMatch = b.workspace_name.match(/WS(\d+)/);
27999
- if (aMatch && bMatch) {
28000
- const aNum = parseInt(aMatch[1]);
28001
- const bNum = parseInt(bMatch[1]);
28002
- return aNum - bNum;
28003
- }
28004
- return a.workspace_name.localeCompare(b.workspace_name);
28005
- }).map((workspace) => {
28006
- const workspaceId = workspace.workspace_uuid || workspace.workspace_name;
28007
- const workspaceKey = `${workspace.line_id || "unknown"}-${workspaceId}`;
28008
- const isVisible = visibleWorkspaces.has(workspaceId);
28009
- const isVeryLowEfficiency = workspace.show_exclamation ?? (workspace.efficiency < 50 && workspace.efficiency >= 10);
28010
- const workspaceCropping = getWorkspaceCropping(workspace.workspace_name);
28011
- return /* @__PURE__ */ jsxRuntime.jsx(
29031
+ children: (() => {
29032
+ const sortedWorkspaces = [...filteredWorkspaces].sort((a, b) => {
29033
+ if (a.line_id !== b.line_id) {
29034
+ return (a.line_id || "").localeCompare(b.line_id || "");
29035
+ }
29036
+ const aMatch = a.workspace_name.match(/WS(\d+)/);
29037
+ const bMatch = b.workspace_name.match(/WS(\d+)/);
29038
+ if (aMatch && bMatch) {
29039
+ const aNum = parseInt(aMatch[1]);
29040
+ const bNum = parseInt(bMatch[1]);
29041
+ return aNum - bNum;
29042
+ }
29043
+ return a.workspace_name.localeCompare(b.workspace_name);
29044
+ });
29045
+ const workspaceCards = sortedWorkspaces.map((workspace) => {
29046
+ const workspaceId = workspace.workspace_uuid || workspace.workspace_name;
29047
+ `${workspace.line_id || "unknown"}-${workspaceId}`;
29048
+ const isVisible = visibleWorkspaces.has(workspaceId);
29049
+ const isVeryLowEfficiency = workspace.show_exclamation ?? (workspace.efficiency < 50 && workspace.efficiency >= 10);
29050
+ const workspaceCropping = getWorkspaceCropping(workspaceId, workspace.workspace_name);
29051
+ const workspaceStream = videoStreamsByWorkspaceId?.[workspaceId];
29052
+ const r2Url = workspaceStream?.hls_url;
29053
+ const fallbackUrl = getWorkspaceHlsUrl(workspace.workspace_name, workspace.line_id);
29054
+ const hasR2Stream = Boolean(r2Url);
29055
+ const useFallback = r2FallbackWorkspaces.has(workspaceId) || streamsReady && !hasR2Stream;
29056
+ const hlsUrl = useFallback ? fallbackUrl : r2Url ?? "";
29057
+ const isR2Stream = !useFallback && hasR2Stream;
29058
+ const canAttemptR2 = hasR2Stream && !r2FallbackWorkspaces.has(workspaceId);
29059
+ const shouldPlay = isVisible && Boolean(hlsUrl) && (!failedStreams.has(workspaceId) || canAttemptR2);
29060
+ return {
29061
+ workspace,
29062
+ workspaceId,
29063
+ isVisible,
29064
+ isVeryLowEfficiency,
29065
+ workspaceCropping,
29066
+ fallbackUrl,
29067
+ hlsUrl,
29068
+ isR2Stream,
29069
+ shouldPlay
29070
+ };
29071
+ });
29072
+ const croppedActiveCount = workspaceCards.reduce((count, card) => {
29073
+ if (card.shouldPlay && card.workspaceCropping) {
29074
+ return count + 1;
29075
+ }
29076
+ return count;
29077
+ }, 0);
29078
+ const throttleCropping = croppedActiveCount > 10;
29079
+ const effectiveCanvasFps = throttleCropping ? 10 : canvasConfig?.fps;
29080
+ const effectiveUseRAF = throttleCropping ? false : canvasConfig?.useRAF;
29081
+ return workspaceCards.map((card) => /* @__PURE__ */ jsxRuntime.jsx(
28012
29082
  "div",
28013
29083
  {
28014
- "data-workspace-id": workspaceId,
29084
+ "data-workspace-id": card.workspaceId,
28015
29085
  className: "workspace-card relative w-full h-full",
28016
29086
  children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0", children: /* @__PURE__ */ jsxRuntime.jsx(
28017
29087
  VideoCard,
28018
29088
  {
28019
- workspace,
28020
- hlsUrl: getWorkspaceHlsUrl(workspace.workspace_name, workspace.line_id),
28021
- shouldPlay: isVisible && !failedStreams.has(workspaceId),
28022
- onClick: () => handleWorkspaceClick(workspace),
28023
- onFatalError: () => handleStreamError(workspaceId),
28024
- isVeryLowEfficiency,
29089
+ workspace: card.workspace,
29090
+ hlsUrl: card.hlsUrl,
29091
+ shouldPlay: card.shouldPlay,
29092
+ onClick: () => handleWorkspaceClick(card.workspace),
29093
+ onFatalError: () => handleStreamError(card.workspaceId, {
29094
+ isR2Stream: card.isR2Stream,
29095
+ fallbackUrl: card.fallbackUrl
29096
+ }),
29097
+ isVeryLowEfficiency: card.isVeryLowEfficiency,
28025
29098
  legend: effectiveLegend,
28026
- cropping: workspaceCropping,
28027
- canvasFps: canvasConfig?.fps,
29099
+ cropping: card.workspaceCropping,
29100
+ canvasFps: effectiveCanvasFps,
28028
29101
  displayName: (
28029
29102
  // Create line-aware lookup key: lineId_workspaceName
28030
29103
  // This ensures correct mapping when multiple lines have same workspace names
28031
- displayNames[`${workspace.line_id}_${workspace.workspace_name}`] || // Always pass line_id to fallback to ensure correct mapping per line
28032
- getWorkspaceDisplayName(workspace.workspace_name, workspace.line_id)
29104
+ displayNames[`${card.workspace.line_id}_${card.workspace.workspace_name}`] || // Always pass line_id to fallback to ensure correct mapping per line
29105
+ getWorkspaceDisplayName(card.workspace.workspace_name, card.workspace.line_id)
28033
29106
  ),
28034
- useRAF: canvasConfig?.useRAF,
29107
+ useRAF: effectiveUseRAF,
28035
29108
  compact: !selectedLine,
28036
- onMouseEnter: onWorkspaceHover ? () => onWorkspaceHover(workspaceId) : void 0,
28037
- onMouseLeave: onWorkspaceHoverEnd ? () => onWorkspaceHoverEnd(workspaceId) : void 0
29109
+ onMouseEnter: onWorkspaceHover ? () => onWorkspaceHover(card.workspaceId) : void 0,
29110
+ onMouseLeave: onWorkspaceHoverEnd ? () => onWorkspaceHoverEnd(card.workspaceId) : void 0
28038
29111
  }
28039
29112
  ) })
28040
29113
  },
28041
- workspaceKey
28042
- );
28043
- })
29114
+ card.workspaceId
29115
+ ));
29116
+ })()
28044
29117
  }
28045
29118
  ) }) });
28046
29119
  });
@@ -29676,33 +30749,6 @@ var VideoControls = ({
29676
30749
  }
29677
30750
  );
29678
30751
  };
29679
-
29680
- // src/lib/utils/r2Detection.ts
29681
- function isR2WorkerUrl(url, r2WorkerDomain) {
29682
- if (!url || !r2WorkerDomain) return false;
29683
- try {
29684
- const workerDomain = new URL(r2WorkerDomain).hostname;
29685
- return url.includes(workerDomain);
29686
- } catch {
29687
- return url.includes(r2WorkerDomain.replace(/https?:\/\//, ""));
29688
- }
29689
- }
29690
-
29691
- // src/lib/services/hlsAuthService.ts
29692
- async function getAuthTokenForHls(supabase) {
29693
- try {
29694
- const { data: { session } } = await supabase.auth.getSession();
29695
- if (!session?.access_token) {
29696
- console.warn("[HLS Auth] No active session, R2 streaming may fail");
29697
- return null;
29698
- }
29699
- console.log("[HLS Auth] Retrieved token for HLS.js requests");
29700
- return session.access_token;
29701
- } catch (error) {
29702
- console.error("[HLS Auth] Error getting auth token:", error);
29703
- return null;
29704
- }
29705
- }
29706
30752
  var ERROR_MAPPING = {
29707
30753
  "networkError": {
29708
30754
  code: 2,
@@ -30087,24 +31133,24 @@ var HlsVideoPlayer = React25.forwardRef(({
30087
31133
  console.log(`[HlsVideoPlayer] Remuxing enabled - using remux playlist URL: ${remuxUrl}`);
30088
31134
  if (safariMode) {
30089
31135
  video.src = remuxUrl;
30090
- } else if (Hls3__default.default.isSupported()) {
30091
- const hls = new Hls3__default.default(mergedHlsConfig);
31136
+ } else if (Hls__default.default.isSupported()) {
31137
+ const hls = new Hls__default.default(mergedHlsConfig);
30092
31138
  hlsRef.current = hls;
30093
- hls.on(Hls3.Events.MANIFEST_PARSED, () => {
31139
+ hls.on(Hls.Events.MANIFEST_PARSED, () => {
30094
31140
  console.log("[HlsVideoPlayer] Remuxed manifest parsed, ready to play");
30095
31141
  setIsReady(true);
30096
31142
  eventCallbacksRef.current.onReady?.(player);
30097
31143
  });
30098
- hls.on(Hls3.Events.ERROR, (_event, data) => {
31144
+ hls.on(Hls.Events.ERROR, (_event, data) => {
30099
31145
  console.error("[HlsVideoPlayer] HLS.js error (remuxing):", data);
30100
31146
  if (data.fatal) {
30101
31147
  let errorInfo;
30102
31148
  switch (data.type) {
30103
- case Hls3.ErrorTypes.NETWORK_ERROR:
31149
+ case Hls.ErrorTypes.NETWORK_ERROR:
30104
31150
  errorInfo = ERROR_MAPPING.networkError;
30105
31151
  hls.startLoad();
30106
31152
  break;
30107
- case Hls3.ErrorTypes.MEDIA_ERROR:
31153
+ case Hls.ErrorTypes.MEDIA_ERROR:
30108
31154
  errorInfo = ERROR_MAPPING.mediaError;
30109
31155
  hls.recoverMediaError();
30110
31156
  break;
@@ -30116,7 +31162,7 @@ var HlsVideoPlayer = React25.forwardRef(({
30116
31162
  eventCallbacksRef.current.onError?.(player, errorInfo);
30117
31163
  }
30118
31164
  });
30119
- hls.on(Hls3.Events.FRAG_LOADED, () => {
31165
+ hls.on(Hls.Events.FRAG_LOADED, () => {
30120
31166
  setIsLoading(false);
30121
31167
  eventCallbacksRef.current.onLoadingChange?.(false);
30122
31168
  });
@@ -30138,19 +31184,19 @@ var HlsVideoPlayer = React25.forwardRef(({
30138
31184
  blobUrlRef.current = blobUrl;
30139
31185
  video.src = blobUrl;
30140
31186
  }
30141
- } else if (Hls3__default.default.isSupported()) {
31187
+ } else if (Hls__default.default.isSupported()) {
30142
31188
  const blob = new Blob([effectiveSrc], { type: "application/vnd.apple.mpegurl" });
30143
31189
  const blobUrl = URL.createObjectURL(blob);
30144
31190
  blobUrlRef.current = blobUrl;
30145
31191
  console.log(`[HlsVideoPlayer] Non-Safari browser (${browserName}) - using HLS.js with blob URL`);
30146
- const hls = new Hls3__default.default(mergedHlsConfig);
31192
+ const hls = new Hls__default.default(mergedHlsConfig);
30147
31193
  hlsRef.current = hls;
30148
- hls.on(Hls3.Events.MANIFEST_PARSED, () => {
31194
+ hls.on(Hls.Events.MANIFEST_PARSED, () => {
30149
31195
  console.log("[HlsVideoPlayer] Manifest parsed, ready to play");
30150
31196
  setIsReady(true);
30151
31197
  eventCallbacksRef.current.onReady?.(player);
30152
31198
  });
30153
- hls.on(Hls3.Events.ERROR, (event, data) => {
31199
+ hls.on(Hls.Events.ERROR, (event, data) => {
30154
31200
  console.error("[HlsVideoPlayer] HLS.js error:", data);
30155
31201
  if (maybeHandleR2Fallback(data)) {
30156
31202
  return;
@@ -30158,17 +31204,17 @@ var HlsVideoPlayer = React25.forwardRef(({
30158
31204
  if (data.fatal) {
30159
31205
  let errorInfo;
30160
31206
  switch (data.type) {
30161
- case Hls3.ErrorTypes.NETWORK_ERROR:
31207
+ case Hls.ErrorTypes.NETWORK_ERROR:
30162
31208
  errorInfo = ERROR_MAPPING.networkError;
30163
31209
  console.log("[HlsVideoPlayer] Attempting to recover from network error");
30164
31210
  hls.startLoad();
30165
31211
  break;
30166
- case Hls3.ErrorTypes.MEDIA_ERROR:
31212
+ case Hls.ErrorTypes.MEDIA_ERROR:
30167
31213
  errorInfo = ERROR_MAPPING.mediaError;
30168
31214
  console.log("[HlsVideoPlayer] Attempting to recover from media error");
30169
31215
  hls.recoverMediaError();
30170
31216
  break;
30171
- case Hls3.ErrorTypes.MUX_ERROR:
31217
+ case Hls.ErrorTypes.MUX_ERROR:
30172
31218
  errorInfo = ERROR_MAPPING.muxError;
30173
31219
  break;
30174
31220
  default:
@@ -30179,9 +31225,9 @@ var HlsVideoPlayer = React25.forwardRef(({
30179
31225
  eventCallbacksRef.current.onError?.(player, errorInfo);
30180
31226
  }
30181
31227
  });
30182
- hls.on(Hls3.Events.FRAG_LOADING, () => {
31228
+ hls.on(Hls.Events.FRAG_LOADING, () => {
30183
31229
  });
30184
- hls.on(Hls3.Events.FRAG_LOADED, () => {
31230
+ hls.on(Hls.Events.FRAG_LOADED, () => {
30185
31231
  setIsLoading(false);
30186
31232
  eventCallbacksRef.current.onLoadingChange?.(false);
30187
31233
  });
@@ -30193,14 +31239,14 @@ var HlsVideoPlayer = React25.forwardRef(({
30193
31239
  onError?.(player, errorInfo);
30194
31240
  }
30195
31241
  } else {
30196
- if (Hls3__default.default.isSupported() && !isSafari()) {
30197
- const hls = new Hls3__default.default(mergedHlsConfig);
31242
+ if (Hls__default.default.isSupported() && !isSafari()) {
31243
+ const hls = new Hls__default.default(mergedHlsConfig);
30198
31244
  hlsRef.current = hls;
30199
- hls.on(Hls3.Events.MANIFEST_PARSED, () => {
31245
+ hls.on(Hls.Events.MANIFEST_PARSED, () => {
30200
31246
  setIsReady(true);
30201
31247
  eventCallbacksRef.current.onReady?.(player);
30202
31248
  });
30203
- hls.on(Hls3.Events.ERROR, (event, data) => {
31249
+ hls.on(Hls.Events.ERROR, (event, data) => {
30204
31250
  console.error("[HlsVideoPlayer] HLS.js error:", data);
30205
31251
  if (maybeHandleR2Fallback(data)) {
30206
31252
  return;
@@ -30208,11 +31254,11 @@ var HlsVideoPlayer = React25.forwardRef(({
30208
31254
  if (data.fatal) {
30209
31255
  let errorInfo;
30210
31256
  switch (data.type) {
30211
- case Hls3.ErrorTypes.NETWORK_ERROR:
31257
+ case Hls.ErrorTypes.NETWORK_ERROR:
30212
31258
  errorInfo = ERROR_MAPPING.networkError;
30213
31259
  hls.startLoad();
30214
31260
  break;
30215
- case Hls3.ErrorTypes.MEDIA_ERROR:
31261
+ case Hls.ErrorTypes.MEDIA_ERROR:
30216
31262
  errorInfo = ERROR_MAPPING.mediaError;
30217
31263
  hls.recoverMediaError();
30218
31264
  break;
@@ -31046,25 +32092,8 @@ var CroppedHlsVideoPlayer = React25.forwardRef(({
31046
32092
  });
31047
32093
  CroppedHlsVideoPlayer.displayName = "CroppedHlsVideoPlayer";
31048
32094
  var CroppedVideoPlayer = CroppedHlsVideoPlayer;
31049
- var getSupabaseClient2 = () => {
31050
- const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
31051
- const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
31052
- if (!url || !key) {
31053
- throw new Error("Supabase configuration missing");
31054
- }
31055
- return supabaseJs.createClient(url, key);
31056
- };
31057
- var getAuthToken4 = async () => {
31058
- try {
31059
- const supabase = getSupabaseClient2();
31060
- const { data: { session } } = await supabase.auth.getSession();
31061
- return session?.access_token || null;
31062
- } catch (error) {
31063
- console.error("[useWorkspaceCrop] Error getting auth token:", error);
31064
- return null;
31065
- }
31066
- };
31067
32095
  function useWorkspaceCrop(workspaceId) {
32096
+ const supabase = useSupabase();
31068
32097
  const [crop, setCrop] = React25.useState(null);
31069
32098
  const [isLoading, setIsLoading] = React25.useState(true);
31070
32099
  const [error, setError] = React25.useState(null);
@@ -31077,7 +32106,8 @@ function useWorkspaceCrop(workspaceId) {
31077
32106
  setIsLoading(true);
31078
32107
  setError(null);
31079
32108
  try {
31080
- const token = await getAuthToken4();
32109
+ const { data: { session } } = await supabase.auth.getSession();
32110
+ const token = session?.access_token || null;
31081
32111
  if (!token) {
31082
32112
  throw new Error("Authentication required");
31083
32113
  }
@@ -31107,7 +32137,7 @@ function useWorkspaceCrop(workspaceId) {
31107
32137
  }
31108
32138
  };
31109
32139
  fetchCrop();
31110
- }, [workspaceId]);
32140
+ }, [workspaceId, supabase]);
31111
32141
  return { crop, isLoading, error };
31112
32142
  }
31113
32143
  function Skeleton({ className, ...props }) {
@@ -32096,6 +33126,7 @@ var FileManagerFilters = ({
32096
33126
  const [idleLabelFilter, setIdleLabelFilter] = React25.useState(null);
32097
33127
  const [showIdleLabelFilterModal, setShowIdleLabelFilterModal] = React25.useState(false);
32098
33128
  const timezone = useAppTimezone();
33129
+ const supabase = useSupabase();
32099
33130
  const [clipMetadata, setClipMetadata] = React25.useState({});
32100
33131
  const [loadingCategories, setLoadingCategories] = React25.useState(/* @__PURE__ */ new Set());
32101
33132
  const [categoryPages, setCategoryPages] = React25.useState({});
@@ -32166,7 +33197,7 @@ var FileManagerFilters = ({
32166
33197
  method: "POST",
32167
33198
  headers: {
32168
33199
  "Content-Type": "application/json",
32169
- "Authorization": `Bearer ${await getAuthToken5()}`
33200
+ "Authorization": `Bearer ${await getAuthToken4()}`
32170
33201
  },
32171
33202
  body: JSON.stringify({
32172
33203
  action: "clip-metadata",
@@ -32199,13 +33230,8 @@ var FileManagerFilters = ({
32199
33230
  });
32200
33231
  }
32201
33232
  }, [workspaceId, date, shift]);
32202
- const getAuthToken5 = async () => {
33233
+ const getAuthToken4 = async () => {
32203
33234
  try {
32204
- const { createClient: createClient5 } = await import('@supabase/supabase-js');
32205
- const supabase = createClient5(
32206
- process.env.NEXT_PUBLIC_SUPABASE_URL || "",
32207
- process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || ""
32208
- );
32209
33235
  const { data: { session } } = await supabase.auth.getSession();
32210
33236
  return session?.access_token || null;
32211
33237
  } catch (error) {
@@ -32225,7 +33251,7 @@ var FileManagerFilters = ({
32225
33251
  method: "POST",
32226
33252
  headers: {
32227
33253
  "Content-Type": "application/json",
32228
- "Authorization": `Bearer ${await getAuthToken5()}`
33254
+ "Authorization": `Bearer ${await getAuthToken4()}`
32229
33255
  },
32230
33256
  body: JSON.stringify({
32231
33257
  action: "percentile-clips",
@@ -33432,6 +34458,7 @@ var BottlenecksContent = ({
33432
34458
  }) => {
33433
34459
  console.log("\u{1F3AB} [BottlenecksContent] Rendered with ticketId:", ticketId || "NONE", "workspaceId:", workspaceId, "date:", date, "shift:", shift);
33434
34460
  const dashboardConfig = useDashboardConfig();
34461
+ const supabase = useSupabase();
33435
34462
  const { session } = useAuth();
33436
34463
  const timezone = useAppTimezone();
33437
34464
  const { shiftConfig, isLoading: isShiftConfigLoading } = useDynamicShiftConfig(lineId);
@@ -33797,26 +34824,21 @@ var BottlenecksContent = ({
33797
34824
  fetchClipCounts();
33798
34825
  }
33799
34826
  }, [workspaceId, effectiveDateString, effectiveShiftId, s3ClipsService, fetchClipCounts, isEffectiveShiftReady]);
33800
- const getAuthToken5 = React25.useCallback(async () => {
34827
+ const getAuthToken4 = React25.useCallback(async () => {
33801
34828
  try {
33802
- const { createClient: createClient5 } = await import('@supabase/supabase-js');
33803
- const supabase = createClient5(
33804
- process.env.NEXT_PUBLIC_SUPABASE_URL || "",
33805
- process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || ""
33806
- );
33807
34829
  const { data: { session: session2 } } = await supabase.auth.getSession();
33808
34830
  return session2?.access_token || null;
33809
34831
  } catch (error2) {
33810
34832
  console.error("[BottlenecksContent] Error getting auth token:", error2);
33811
34833
  return null;
33812
34834
  }
33813
- }, []);
34835
+ }, [supabase]);
33814
34836
  React25.useEffect(() => {
33815
34837
  if (!triageMode || !workspaceId || !isEffectiveShiftReady) return;
33816
34838
  const fetchTriageClips = async () => {
33817
34839
  setIsLoadingTriageClips(true);
33818
34840
  try {
33819
- const token = await getAuthToken5();
34841
+ const token = await getAuthToken4();
33820
34842
  if (!token) {
33821
34843
  console.error("[BottlenecksContent] No auth token available");
33822
34844
  return;
@@ -33864,7 +34886,7 @@ var BottlenecksContent = ({
33864
34886
  }
33865
34887
  };
33866
34888
  fetchTriageClips();
33867
- }, [triageMode, workspaceId, effectiveDateString, effectiveShiftId, getAuthToken5, isEffectiveShiftReady]);
34889
+ }, [triageMode, workspaceId, effectiveDateString, effectiveShiftId, getAuthToken4, isEffectiveShiftReady]);
33868
34890
  React25.useEffect(() => {
33869
34891
  if (!triageMode || triageClips.length === 0 || !session?.access_token) {
33870
34892
  return;
@@ -34057,13 +35079,7 @@ var BottlenecksContent = ({
34057
35079
  return;
34058
35080
  }
34059
35081
  console.log(`[BottlenecksContent] Loading metadata for category: ${categoryId}${forceRefresh ? " (force refresh)" : ""}`);
34060
- const { createClient: createClient5 } = await import('@supabase/supabase-js');
34061
- const supabase = createClient5(
34062
- process.env.NEXT_PUBLIC_SUPABASE_URL || "",
34063
- process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || ""
34064
- );
34065
- const { data: { session: session2 } } = await supabase.auth.getSession();
34066
- const authToken = session2?.access_token;
35082
+ const authToken = await getAuthToken4();
34067
35083
  if (!authToken) {
34068
35084
  console.error("Authentication required for metadata loading");
34069
35085
  return;
@@ -34133,10 +35149,10 @@ var BottlenecksContent = ({
34133
35149
  setCategoryMetadata(metadataClips);
34134
35150
  categoryMetadataRef.current = metadataClips;
34135
35151
  console.log(`[BottlenecksContent] Loaded metadata for ${categoryId}: ${metadataClips.length} clips`);
34136
- if (categoryId === "idle_time" && metadataClips.length > 0 && session2?.access_token) {
35152
+ if (categoryId === "idle_time" && metadataClips.length > 0 && session?.access_token) {
34137
35153
  const idleClipIds2 = metadataClips.map((c) => c.clipId).filter(Boolean);
34138
35154
  console.log(`[BottlenecksContent] Fetching classifications for ${idleClipIds2.length} metadata clips`);
34139
- fetchClassifications(idleClipIds2, session2.access_token).then((classifications) => {
35155
+ fetchClassifications(idleClipIds2, session.access_token).then((classifications) => {
34140
35156
  console.log("[BottlenecksContent] Received metadata classifications:", Object.keys(classifications).length);
34141
35157
  setClipClassifications((prev) => ({
34142
35158
  ...prev,
@@ -40552,6 +41568,8 @@ var WorkspaceGrid = React25__namespace.default.memo(({
40552
41568
  hasFlowBuffers = false,
40553
41569
  legend = DEFAULT_EFFICIENCY_LEGEND,
40554
41570
  videoSources = {},
41571
+ videoStreamsByWorkspaceId = {},
41572
+ videoStreamsLoading = false,
40555
41573
  displayNames = {},
40556
41574
  onWorkspaceHover,
40557
41575
  onWorkspaceHoverEnd
@@ -40619,6 +41637,8 @@ var WorkspaceGrid = React25__namespace.default.memo(({
40619
41637
  {
40620
41638
  workspaces,
40621
41639
  videoSources,
41640
+ videoStreamsByWorkspaceId,
41641
+ videoStreamsLoading,
40622
41642
  displayNames,
40623
41643
  legend,
40624
41644
  onWorkspaceHover,
@@ -43570,26 +44590,26 @@ var SingleVideoStream = ({
43570
44590
  const hlsStreamUrl = streamUrl || getCameraStreamUrl(workspaceName, baseUrl);
43571
44591
  console.log(`Using camera URL for ${workspaceName}: ${hlsStreamUrl}`);
43572
44592
  const mergedHlsConfig = { ...DEFAULT_HLS_CONFIG, ...hlsConfig };
43573
- if (Hls3__default.default.isSupported()) {
43574
- const hls = new Hls3__default.default(mergedHlsConfig);
44593
+ if (Hls__default.default.isSupported()) {
44594
+ const hls = new Hls__default.default(mergedHlsConfig);
43575
44595
  hlsRef.current = hls;
43576
- hls.on(Hls3__default.default.Events.MEDIA_ATTACHED, () => {
44596
+ hls.on(Hls__default.default.Events.MEDIA_ATTACHED, () => {
43577
44597
  console.log("HLS media attached");
43578
44598
  hls.loadSource(hlsStreamUrl);
43579
44599
  });
43580
- hls.on(Hls3__default.default.Events.MANIFEST_PARSED, () => {
44600
+ hls.on(Hls__default.default.Events.MANIFEST_PARSED, () => {
43581
44601
  console.log("HLS manifest parsed");
43582
44602
  attemptPlay(video);
43583
44603
  });
43584
- hls.on(Hls3__default.default.Events.ERROR, (_, data) => {
44604
+ hls.on(Hls__default.default.Events.ERROR, (_, data) => {
43585
44605
  if (data.fatal) {
43586
44606
  console.error("Fatal HLS error:", data.type, data.details);
43587
44607
  switch (data.type) {
43588
- case Hls3__default.default.ErrorTypes.NETWORK_ERROR:
44608
+ case Hls__default.default.ErrorTypes.NETWORK_ERROR:
43589
44609
  console.error("Fatal network error encountered");
43590
44610
  setError("Network error: Please check your connection");
43591
44611
  break;
43592
- case Hls3__default.default.ErrorTypes.MEDIA_ERROR:
44612
+ case Hls__default.default.ErrorTypes.MEDIA_ERROR:
43593
44613
  console.error("Fatal media error encountered, trying to recover");
43594
44614
  hls.recoverMediaError();
43595
44615
  break;
@@ -48556,6 +49576,10 @@ function HomeView({
48556
49576
  JSON.stringify(workspaceMetrics.map((w) => `${w.workspace_uuid}-${Math.round(w.efficiency)}-${w.trend}`)),
48557
49577
  selectedLineId
48558
49578
  ]);
49579
+ const {
49580
+ streamsByWorkspaceId: videoStreamsByWorkspaceId,
49581
+ isLoading: videoStreamsLoading
49582
+ } = useWorkspaceVideoStreams(workspaceMetrics);
48559
49583
  const memoizedKPIs = React25.useMemo(() => kpis, [
48560
49584
  // Only update reference when values change by at least 1%
48561
49585
  kpis?.efficiency?.value ? Math.round(kpis.efficiency.value) : null,
@@ -48750,6 +49774,8 @@ function HomeView({
48750
49774
  factoryView: factoryViewId,
48751
49775
  legend: efficiencyLegend,
48752
49776
  videoSources,
49777
+ videoStreamsByWorkspaceId,
49778
+ videoStreamsLoading,
48753
49779
  displayNames: workspaceDisplayNames,
48754
49780
  hasFlowBuffers,
48755
49781
  className: "h-full",
@@ -48780,6 +49806,8 @@ function HomeView({
48780
49806
  factoryView: factoryViewId,
48781
49807
  legend: efficiencyLegend,
48782
49808
  videoSources,
49809
+ videoStreamsByWorkspaceId,
49810
+ videoStreamsLoading,
48783
49811
  displayNames: workspaceDisplayNames,
48784
49812
  hasFlowBuffers,
48785
49813
  className: "h-full",
@@ -61594,7 +62622,11 @@ async function getManufacturingInsights(userQuestion, lineId, shiftId, companyId
61594
62622
  );
61595
62623
  }
61596
62624
  function createStreamProxyHandler(config) {
61597
- const cloudFrontDomain = config?.cloudFrontDomain || "https://d1eiob0chi5jw.cloudfront.net";
62625
+ const baseUrl = config?.baseUrl || config?.cloudFrontDomain || "https://d1eiob0chi5jw.cloudfront.net";
62626
+ const forwardAuth = config?.forwardAuth ?? false;
62627
+ const noCacheManifest = config?.noCacheManifest ?? false;
62628
+ const staticAuthToken = config?.staticAuthToken;
62629
+ const debugAuth = config?.debugAuth ?? false;
61598
62630
  return async function handler(req, res) {
61599
62631
  if (req.method !== "GET") {
61600
62632
  return res.status(405).json({ error: "Method not allowed" });
@@ -61604,15 +62636,28 @@ function createStreamProxyHandler(config) {
61604
62636
  return res.status(400).json({ error: "Missing path parameter" });
61605
62637
  }
61606
62638
  const fullPath = path.join("/");
61607
- const targetUrl = `${cloudFrontDomain}/${fullPath}`;
62639
+ const targetUrl = `${baseUrl}/${fullPath}`;
61608
62640
  try {
61609
62641
  console.log(`[HLS Stream Proxy] Streaming: ${targetUrl}`);
62642
+ const resolvedAuthHeader = (() => {
62643
+ if (forwardAuth && req.headers.authorization) {
62644
+ return req.headers.authorization;
62645
+ }
62646
+ if (staticAuthToken) {
62647
+ return staticAuthToken.startsWith("Bearer ") ? staticAuthToken : `Bearer ${staticAuthToken}`;
62648
+ }
62649
+ return void 0;
62650
+ })();
62651
+ if (debugAuth) {
62652
+ console.log("[HLS Stream Proxy] Auth header present:", Boolean(resolvedAuthHeader));
62653
+ }
61610
62654
  const response = await fetch(targetUrl, {
61611
62655
  method: "GET",
61612
62656
  headers: {
61613
62657
  "User-Agent": "NextJS-HLS-Stream-Proxy",
61614
62658
  // Pass through range headers for partial content requests
61615
- ...req.headers.range && { "Range": req.headers.range }
62659
+ ...req.headers.range && { "Range": req.headers.range },
62660
+ ...resolvedAuthHeader ? { "Authorization": resolvedAuthHeader } : {}
61616
62661
  }
61617
62662
  });
61618
62663
  if (!response.ok) {
@@ -61633,6 +62678,10 @@ function createStreamProxyHandler(config) {
61633
62678
  }
61634
62679
  if (fullPath.endsWith(".m3u8")) {
61635
62680
  res.setHeader("Content-Type", "application/vnd.apple.mpegurl");
62681
+ if (noCacheManifest) {
62682
+ res.setHeader("Cache-Control", "no-store, must-revalidate");
62683
+ res.setHeader("Pragma", "no-cache");
62684
+ }
61636
62685
  } else if (fullPath.endsWith(".ts")) {
61637
62686
  res.setHeader("Content-Type", "video/mp2t");
61638
62687
  }
@@ -62104,6 +63153,7 @@ exports.useMetrics = useMetrics;
62104
63153
  exports.useMobileMenu = useMobileMenu;
62105
63154
  exports.useMultiLineShiftConfigs = useMultiLineShiftConfigs;
62106
63155
  exports.useNavigation = useNavigation;
63156
+ exports.useOptionalSupabase = useOptionalSupabase;
62107
63157
  exports.useOverrides = useOverrides;
62108
63158
  exports.usePageOverride = usePageOverride;
62109
63159
  exports.usePrefetchClipCounts = usePrefetchClipCounts;
@@ -62141,6 +63191,7 @@ exports.useWorkspaceMetrics = useWorkspaceMetrics;
62141
63191
  exports.useWorkspaceNavigation = useWorkspaceNavigation;
62142
63192
  exports.useWorkspaceOperators = useWorkspaceOperators;
62143
63193
  exports.useWorkspaceUptimeTimeline = useWorkspaceUptimeTimeline;
63194
+ exports.useWorkspaceVideoStreams = useWorkspaceVideoStreams;
62144
63195
  exports.userService = userService;
62145
63196
  exports.videoPrefetchManager = videoPrefetchManager;
62146
63197
  exports.videoPreloader = videoPreloader;