@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.d.mts +87 -4
- package/dist/index.d.ts +87 -4
- package/dist/index.js +1388 -337
- package/dist/index.mjs +1370 -321
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
7307
|
+
var useOptionalSupabase = () => {
|
|
7141
7308
|
const context = React25.useContext(SupabaseContext);
|
|
7142
|
-
|
|
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
|
|
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
|
|
10904
|
-
maxBufferLength:
|
|
10905
|
-
//
|
|
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.
|
|
10911
|
-
//
|
|
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
|
|
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:
|
|
10932
|
-
levelLoadingTimeOut:
|
|
10933
|
-
fragLoadingTimeOut:
|
|
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:
|
|
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
|
-
//
|
|
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
|
-
|
|
11033
|
-
|
|
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
|
-
|
|
11044
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11073
|
-
|
|
11074
|
-
|
|
11075
|
-
|
|
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
|
-
},
|
|
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 (
|
|
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 (
|
|
11144
|
-
|
|
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
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
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
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11165
|
-
|
|
11166
|
-
|
|
11167
|
-
|
|
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
|
-
|
|
11178
|
-
|
|
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
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
|
|
11188
|
-
|
|
11189
|
-
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
|
|
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
|
|
17509
|
-
|
|
17510
|
-
|
|
17511
|
-
|
|
17512
|
-
|
|
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
|
-
|
|
17544
|
-
|
|
17545
|
-
|
|
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
|
|
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
|
-
|
|
27666
|
-
|
|
27667
|
-
|
|
27668
|
-
|
|
27669
|
-
|
|
27670
|
-
|
|
27671
|
-
|
|
27672
|
-
|
|
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:
|
|
27994
|
-
|
|
27995
|
-
|
|
27996
|
-
|
|
27997
|
-
|
|
27998
|
-
|
|
27999
|
-
|
|
28000
|
-
|
|
28001
|
-
|
|
28002
|
-
|
|
28003
|
-
|
|
28004
|
-
|
|
28005
|
-
|
|
28006
|
-
|
|
28007
|
-
const
|
|
28008
|
-
|
|
28009
|
-
|
|
28010
|
-
|
|
28011
|
-
|
|
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:
|
|
28021
|
-
shouldPlay:
|
|
28022
|
-
onClick: () => handleWorkspaceClick(workspace),
|
|
28023
|
-
onFatalError: () => handleStreamError(workspaceId
|
|
28024
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
30091
|
-
const hls = new
|
|
31136
|
+
} else if (Hls__default.default.isSupported()) {
|
|
31137
|
+
const hls = new Hls__default.default(mergedHlsConfig);
|
|
30092
31138
|
hlsRef.current = hls;
|
|
30093
|
-
hls.on(
|
|
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(
|
|
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
|
|
31149
|
+
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
30104
31150
|
errorInfo = ERROR_MAPPING.networkError;
|
|
30105
31151
|
hls.startLoad();
|
|
30106
31152
|
break;
|
|
30107
|
-
case
|
|
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(
|
|
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 (
|
|
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
|
|
31192
|
+
const hls = new Hls__default.default(mergedHlsConfig);
|
|
30147
31193
|
hlsRef.current = hls;
|
|
30148
|
-
hls.on(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
31228
|
+
hls.on(Hls.Events.FRAG_LOADING, () => {
|
|
30183
31229
|
});
|
|
30184
|
-
hls.on(
|
|
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 (
|
|
30197
|
-
const hls = new
|
|
31242
|
+
if (Hls__default.default.isSupported() && !isSafari()) {
|
|
31243
|
+
const hls = new Hls__default.default(mergedHlsConfig);
|
|
30198
31244
|
hlsRef.current = hls;
|
|
30199
|
-
hls.on(
|
|
31245
|
+
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
30200
31246
|
setIsReady(true);
|
|
30201
31247
|
eventCallbacksRef.current.onReady?.(player);
|
|
30202
31248
|
});
|
|
30203
|
-
hls.on(
|
|
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
|
|
31257
|
+
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
30212
31258
|
errorInfo = ERROR_MAPPING.networkError;
|
|
30213
31259
|
hls.startLoad();
|
|
30214
31260
|
break;
|
|
30215
|
-
case
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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 &&
|
|
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,
|
|
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 (
|
|
43574
|
-
const hls = new
|
|
44593
|
+
if (Hls__default.default.isSupported()) {
|
|
44594
|
+
const hls = new Hls__default.default(mergedHlsConfig);
|
|
43575
44595
|
hlsRef.current = hls;
|
|
43576
|
-
hls.on(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 = `${
|
|
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;
|