@scarlett-player/hls 1.8.1 → 1.10.0
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/{chunk-VD46AQJB.js → chunk-UB2TS72E.js} +240 -58
- package/dist/index.cjs +252 -78
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -9
- package/dist/light.cjs +252 -78
- package/dist/light.d.cts +3 -3
- package/dist/light.d.ts +3 -3
- package/dist/light.js +1 -9
- package/dist/{sanitize-url-DcZXD_K-.d.cts → types-DnvcTuSn.d.cts} +3 -26
- package/dist/{sanitize-url-DcZXD_K-.d.ts → types-DnvcTuSn.d.ts} +3 -26
- package/package.json +2 -2
|
@@ -5,15 +5,7 @@ var __export = (target, all) => {
|
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
// src/sanitize-url.ts
|
|
8
|
-
|
|
9
|
-
if (!url) return void 0;
|
|
10
|
-
try {
|
|
11
|
-
const parsed = new URL(url);
|
|
12
|
-
return `${parsed.origin}${parsed.pathname}`;
|
|
13
|
-
} catch {
|
|
14
|
-
return void 0;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
8
|
+
import { sanitizeUrl } from "@scarlett-player/core";
|
|
17
9
|
|
|
18
10
|
// src/create-hls-plugin.ts
|
|
19
11
|
import { ErrorCode } from "@scarlett-player/core";
|
|
@@ -98,15 +90,41 @@ function mapErrorType(hlsType) {
|
|
|
98
90
|
}
|
|
99
91
|
}
|
|
100
92
|
function parseHlsError(data) {
|
|
93
|
+
const frag = data.frag;
|
|
94
|
+
const response = data.response;
|
|
95
|
+
const context = data.context;
|
|
101
96
|
return {
|
|
102
97
|
type: mapErrorType(data.type),
|
|
103
98
|
details: data.details || "Unknown error",
|
|
104
99
|
fatal: data.fatal || false,
|
|
105
|
-
|
|
100
|
+
// hls.js puts the request URL in a different place per error type:
|
|
101
|
+
// playlist errors set data.url, while fragment and key load errors set
|
|
102
|
+
// data.url not at all and carry it on data.frag.url and data.response.url.
|
|
103
|
+
// Reading only data.url dropped the failing segment from every diagnostic
|
|
104
|
+
// an origin outage produces - which is the case the diagnostics exist for.
|
|
105
|
+
url: data.url ?? frag?.url ?? response?.url ?? context?.url,
|
|
106
106
|
reason: data.reason,
|
|
107
107
|
response: data.response
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
|
+
function audioTrackId(index) {
|
|
111
|
+
return `audio-${index}`;
|
|
112
|
+
}
|
|
113
|
+
function audioTrackIndex(id) {
|
|
114
|
+
if (!id) return -1;
|
|
115
|
+
const match = /^audio-(0|[1-9]\d*)$/.exec(id);
|
|
116
|
+
return match ? Number.parseInt(match[1], 10) : -1;
|
|
117
|
+
}
|
|
118
|
+
function formatAudioTrack(track, index, active) {
|
|
119
|
+
return {
|
|
120
|
+
id: audioTrackId(index),
|
|
121
|
+
// A manifest may declare neither NAME nor LANGUAGE; a numbered fallback
|
|
122
|
+
// still gives the viewer something selectable rather than a blank row.
|
|
123
|
+
label: track.name || track.lang || `Audio ${index + 1}`,
|
|
124
|
+
language: track.lang,
|
|
125
|
+
active
|
|
126
|
+
};
|
|
127
|
+
}
|
|
110
128
|
function setupHlsEventHandlers(hls, api, callbacks) {
|
|
111
129
|
const handlers = [];
|
|
112
130
|
const addHandler = (event, handler) => {
|
|
@@ -150,6 +168,24 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
150
168
|
});
|
|
151
169
|
callbacks.onLevelSwitched?.(data.level);
|
|
152
170
|
});
|
|
171
|
+
const publishAudioTracks = (tracks, activeIndex) => {
|
|
172
|
+
const audioTracks = tracks.map(
|
|
173
|
+
(track, index) => formatAudioTrack(track, index, index === activeIndex)
|
|
174
|
+
);
|
|
175
|
+
api.setState("audioTracks", audioTracks);
|
|
176
|
+
api.setState("currentAudioTrack", audioTracks[activeIndex] ?? null);
|
|
177
|
+
};
|
|
178
|
+
addHandler("hlsAudioTracksUpdated", (_event, data) => {
|
|
179
|
+
const tracks = data.audioTracks ?? [];
|
|
180
|
+
api.logger.debug("HLS audio tracks updated", { tracks: tracks.length });
|
|
181
|
+
publishAudioTracks(tracks, hls.audioTrack);
|
|
182
|
+
callbacks.onAudioTracksUpdated?.(tracks);
|
|
183
|
+
});
|
|
184
|
+
addHandler("hlsAudioTrackSwitched", (_event, data) => {
|
|
185
|
+
api.logger.debug("HLS audio track switched", { id: data.id });
|
|
186
|
+
publishAudioTracks(hls.audioTracks ?? [], data.id);
|
|
187
|
+
callbacks.onAudioTrackSwitched?.(data.id);
|
|
188
|
+
});
|
|
153
189
|
let lastBandwidthUpdate = 0;
|
|
154
190
|
addHandler("hlsFragLoaded", () => {
|
|
155
191
|
const now = Date.now();
|
|
@@ -170,16 +206,15 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
170
206
|
if (data.details?.live !== void 0) {
|
|
171
207
|
api.setState("live", data.details.live);
|
|
172
208
|
if (data.details.live) {
|
|
209
|
+
const details = data.details;
|
|
210
|
+
const start = details.fragmentStart ?? (details.fragments?.[0]?.start ?? 0);
|
|
211
|
+
const end = details.edge ?? details.totalduration ?? 0;
|
|
212
|
+
api.setState("seekableRange", { start, end });
|
|
173
213
|
const video = hls.media;
|
|
174
|
-
if (video
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
api.setState("
|
|
178
|
-
const threshold = (data.details.targetduration ?? 3) * 3;
|
|
179
|
-
const isAtLiveEdge = end - video.currentTime < threshold;
|
|
180
|
-
api.setState("liveEdge", isAtLiveEdge);
|
|
181
|
-
const latency = end - video.currentTime;
|
|
182
|
-
api.setState("liveLatency", Math.max(0, latency));
|
|
214
|
+
if (video) {
|
|
215
|
+
const latency = Math.max(0, end - video.currentTime);
|
|
216
|
+
api.setState("liveLatency", latency);
|
|
217
|
+
api.setState("liveEdge", latency < (details.targetduration ?? 3) * 3);
|
|
183
218
|
}
|
|
184
219
|
}
|
|
185
220
|
callbacks.onLiveUpdate?.();
|
|
@@ -197,14 +232,16 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
197
232
|
api.logger.error(`HLS fatal error: ${error.details} (type=${error.type})`, {
|
|
198
233
|
type: error.type,
|
|
199
234
|
details: error.details,
|
|
200
|
-
|
|
235
|
+
status: error.response?.code,
|
|
236
|
+
url: sanitizeUrl(error.url)
|
|
201
237
|
});
|
|
202
238
|
} else {
|
|
203
239
|
api.logger.warn(`HLS error: ${error.details} (type=${error.type}, fatal=${error.fatal})`, {
|
|
204
240
|
type: error.type,
|
|
205
241
|
details: error.details,
|
|
206
242
|
fatal: error.fatal,
|
|
207
|
-
|
|
243
|
+
status: error.response?.code,
|
|
244
|
+
url: sanitizeUrl(error.url)
|
|
208
245
|
});
|
|
209
246
|
}
|
|
210
247
|
callbacks.onError?.(error);
|
|
@@ -214,6 +251,8 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
214
251
|
hls.off(event, handler);
|
|
215
252
|
}
|
|
216
253
|
handlers.length = 0;
|
|
254
|
+
api.setState("audioTracks", []);
|
|
255
|
+
api.setState("currentAudioTrack", null);
|
|
217
256
|
};
|
|
218
257
|
}
|
|
219
258
|
function setupVideoEventHandlers(video, api) {
|
|
@@ -253,19 +292,27 @@ function setupVideoEventHandlers(video, api) {
|
|
|
253
292
|
addHandler("timeupdate", () => {
|
|
254
293
|
api.setState("currentTime", video.currentTime);
|
|
255
294
|
api.emit("playback:timeupdate", { currentTime: video.currentTime });
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
295
|
+
if (video.seekable && video.seekable.length > 0) {
|
|
296
|
+
if (api.getState("live") || !Number.isFinite(video.duration)) {
|
|
297
|
+
const start = video.seekable.start(0);
|
|
298
|
+
const end = video.seekable.end(video.seekable.length - 1);
|
|
299
|
+
api.setState("seekableRange", { start, end });
|
|
300
|
+
const latency = Math.max(0, end - video.currentTime);
|
|
301
|
+
api.setState("liveEdge", latency < 10);
|
|
302
|
+
api.setState("liveLatency", latency);
|
|
303
|
+
}
|
|
264
304
|
}
|
|
265
305
|
});
|
|
266
306
|
addHandler("durationchange", () => {
|
|
267
|
-
|
|
268
|
-
|
|
307
|
+
const rawDuration = video.duration;
|
|
308
|
+
const isLive = !Number.isFinite(rawDuration) || rawDuration === Infinity;
|
|
309
|
+
if (isLive) {
|
|
310
|
+
api.setState("live", true);
|
|
311
|
+
api.setState("duration", 0);
|
|
312
|
+
} else {
|
|
313
|
+
api.setState("duration", rawDuration || 0);
|
|
314
|
+
}
|
|
315
|
+
api.emit("media:loadedmetadata", { duration: api.getState("duration") });
|
|
269
316
|
});
|
|
270
317
|
addHandler("waiting", () => {
|
|
271
318
|
api.setState("waiting", true);
|
|
@@ -401,7 +448,7 @@ function createValidatingPlaylistLoader(Hls) {
|
|
|
401
448
|
}
|
|
402
449
|
|
|
403
450
|
// src/version.ts
|
|
404
|
-
var PKG_VERSION = true ? "1.
|
|
451
|
+
var PKG_VERSION = true ? "1.10.0" : "0.0.0-dev";
|
|
405
452
|
|
|
406
453
|
// src/create-hls-plugin.ts
|
|
407
454
|
var DEFAULT_CONFIG = {
|
|
@@ -461,6 +508,11 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
461
508
|
let onlineListener = null;
|
|
462
509
|
let reconnectTriggerError = null;
|
|
463
510
|
let reconnectExhausted = false;
|
|
511
|
+
let isReconnecting = false;
|
|
512
|
+
let isLiveClassified = false;
|
|
513
|
+
let stallWatchdogTimer = null;
|
|
514
|
+
let lastStallCheckTime = 0;
|
|
515
|
+
let lastStallCheckPosition = 0;
|
|
464
516
|
const applyPoster = () => {
|
|
465
517
|
if (!video) return;
|
|
466
518
|
video.poster = api?.getState("poster") || "";
|
|
@@ -492,6 +544,10 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
492
544
|
clearTimeout(retryTimeout);
|
|
493
545
|
retryTimeout = null;
|
|
494
546
|
}
|
|
547
|
+
if (stallWatchdogTimer) {
|
|
548
|
+
clearTimeout(stallWatchdogTimer);
|
|
549
|
+
stallWatchdogTimer = null;
|
|
550
|
+
}
|
|
495
551
|
if (hls) {
|
|
496
552
|
hls.destroy();
|
|
497
553
|
hls = null;
|
|
@@ -603,20 +659,20 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
603
659
|
const handleHlsError = (error) => {
|
|
604
660
|
const Hls = loader.getHlsConstructor();
|
|
605
661
|
if (!Hls || !hls) return false;
|
|
606
|
-
const now = Date.now();
|
|
607
|
-
if (now - errorWindowStart > ERROR_WINDOW_MS) {
|
|
608
|
-
errorCount = 1;
|
|
609
|
-
errorWindowStart = now;
|
|
610
|
-
} else {
|
|
611
|
-
errorCount++;
|
|
612
|
-
}
|
|
613
|
-
if (errorCount >= MAX_ERRORS_IN_WINDOW) {
|
|
614
|
-
api?.logger.error(`Too many errors (${errorCount} in ${ERROR_WINDOW_MS}ms), giving up`);
|
|
615
|
-
emitFatalError(error, true);
|
|
616
|
-
teardownPipeline(new Error(error.details));
|
|
617
|
-
return true;
|
|
618
|
-
}
|
|
619
662
|
if (error.fatal) {
|
|
663
|
+
const now = Date.now();
|
|
664
|
+
if (now - errorWindowStart > ERROR_WINDOW_MS) {
|
|
665
|
+
errorCount = 1;
|
|
666
|
+
errorWindowStart = now;
|
|
667
|
+
} else {
|
|
668
|
+
errorCount++;
|
|
669
|
+
}
|
|
670
|
+
if (errorCount >= MAX_ERRORS_IN_WINDOW) {
|
|
671
|
+
api?.logger.error(`Too many fatal errors (${errorCount} in ${ERROR_WINDOW_MS}ms), giving up`);
|
|
672
|
+
emitFatalError(error, true);
|
|
673
|
+
teardownPipeline(new Error(error.details));
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
620
676
|
api?.logger.error("Fatal HLS error", { type: error.type, details: error.details });
|
|
621
677
|
switch (error.type) {
|
|
622
678
|
case "network": {
|
|
@@ -895,9 +951,15 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
895
951
|
if (resolved || session !== loadSession) return;
|
|
896
952
|
resolved = true;
|
|
897
953
|
releaseAbort();
|
|
898
|
-
api?.logger.error(`HLS load timed out after ${timeout_ms}ms`, { src });
|
|
954
|
+
api?.logger.error(`HLS load timed out after ${timeout_ms}ms`, { src: sanitizeUrl(src) });
|
|
955
|
+
const timeoutError = {
|
|
956
|
+
type: "network",
|
|
957
|
+
details: "Video took too long to load (network timeout)",
|
|
958
|
+
fatal: true
|
|
959
|
+
};
|
|
960
|
+
emitFatalError(timeoutError, true);
|
|
899
961
|
teardownPipeline();
|
|
900
|
-
reject(new Error(
|
|
962
|
+
reject(new Error(timeoutError.details));
|
|
901
963
|
}, timeout_ms);
|
|
902
964
|
}
|
|
903
965
|
hls.attachMedia(videoEl);
|
|
@@ -914,6 +976,51 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
914
976
|
reconnectResumePosition = 0;
|
|
915
977
|
reconnectTriggerError = null;
|
|
916
978
|
reconnectExhausted = false;
|
|
979
|
+
isReconnecting = false;
|
|
980
|
+
};
|
|
981
|
+
const startStallWatchdog = () => {
|
|
982
|
+
if (!video || stallWatchdogTimer) return;
|
|
983
|
+
lastStallCheckTime = Date.now();
|
|
984
|
+
lastStallCheckPosition = video.currentTime;
|
|
985
|
+
const levelTargetDuration = hls?.targetDuration ?? hls?.levels?.[hls?.currentLevel]?.details?.targetduration ?? 0;
|
|
986
|
+
const targetDuration = Math.max(15, 4 * (levelTargetDuration || 0) || 15);
|
|
987
|
+
const checkInterval = Math.min(targetDuration, 3e4);
|
|
988
|
+
const check = () => {
|
|
989
|
+
if (!video || !api) return;
|
|
990
|
+
const isPlaying = api.getState("playing");
|
|
991
|
+
const isSeeking = api.getState("seeking");
|
|
992
|
+
if (!isPlaying || isSeeking) {
|
|
993
|
+
stallWatchdogTimer = setTimeout(check, checkInterval);
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
const elapsed = Date.now() - lastStallCheckTime;
|
|
997
|
+
const positionDelta = video.currentTime - lastStallCheckPosition;
|
|
998
|
+
if (positionDelta > 0.5) {
|
|
999
|
+
lastStallCheckTime = Date.now();
|
|
1000
|
+
lastStallCheckPosition = video.currentTime;
|
|
1001
|
+
} else if (elapsed > targetDuration * 1e3) {
|
|
1002
|
+
api.logger.warn("Playback stall detected \u2014 no timeupdate progress", {
|
|
1003
|
+
elapsed: Math.round(elapsed),
|
|
1004
|
+
currentTime: video.currentTime,
|
|
1005
|
+
targetDuration: Math.round(targetDuration)
|
|
1006
|
+
});
|
|
1007
|
+
const stallError = {
|
|
1008
|
+
type: "network",
|
|
1009
|
+
details: "Playback stalled \u2014 no data received",
|
|
1010
|
+
fatal: true
|
|
1011
|
+
};
|
|
1012
|
+
maybeScheduleReconnect(stallError);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
stallWatchdogTimer = setTimeout(check, checkInterval);
|
|
1016
|
+
};
|
|
1017
|
+
stallWatchdogTimer = setTimeout(check, checkInterval);
|
|
1018
|
+
};
|
|
1019
|
+
const stopStallWatchdog = () => {
|
|
1020
|
+
if (stallWatchdogTimer) {
|
|
1021
|
+
clearTimeout(stallWatchdogTimer);
|
|
1022
|
+
stallWatchdogTimer = null;
|
|
1023
|
+
}
|
|
917
1024
|
};
|
|
918
1025
|
const emitReconnectExhausted = (elapsedMs, windowMs) => {
|
|
919
1026
|
if (reconnectExhausted) return;
|
|
@@ -939,17 +1046,30 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
939
1046
|
const scheduleReconnectAttempt = () => {
|
|
940
1047
|
if (reconnectExhausted) return;
|
|
941
1048
|
if (reconnectTimer) return;
|
|
942
|
-
const
|
|
1049
|
+
const isLive = (api?.getState("live") ?? false) && isLiveClassified;
|
|
1050
|
+
const configWindowMs = mergedConfig.reconnectWindowMs ?? 3e5;
|
|
1051
|
+
const window_ms = isLive ? Infinity : configWindowMs;
|
|
943
1052
|
const elapsed_ms = Date.now() - reconnectWindowStart;
|
|
944
1053
|
if (elapsed_ms > window_ms) {
|
|
945
1054
|
api?.logger.warn(`Auto-reconnect window exhausted after ${reconnectAttempts} attempts`);
|
|
946
1055
|
emitReconnectExhausted(elapsed_ms, window_ms);
|
|
947
1056
|
return;
|
|
948
1057
|
}
|
|
1058
|
+
const LONG_OUTAGE_MS = 6e5;
|
|
1059
|
+
if (isLive && elapsed_ms > LONG_OUTAGE_MS) {
|
|
1060
|
+
api?.emit("error:reconnecting", {
|
|
1061
|
+
attempt: reconnectAttempts + 1,
|
|
1062
|
+
delayMs: 0,
|
|
1063
|
+
elapsedMs: elapsed_ms,
|
|
1064
|
+
windowMs: window_ms,
|
|
1065
|
+
longOutage: true
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
949
1068
|
const base_delay = mergedConfig.reconnectBaseDelayMs ?? 2e3;
|
|
950
1069
|
const max_delay = mergedConfig.reconnectMaxDelayMs ?? 3e4;
|
|
951
1070
|
const backoff = Math.min(base_delay * Math.pow(2, reconnectAttempts), max_delay);
|
|
952
|
-
const
|
|
1071
|
+
const isAtCap = backoff >= max_delay;
|
|
1072
|
+
const delay = isLive && isAtCap ? 3e4 : Math.round(backoff * (0.7 + Math.random() * 0.3));
|
|
953
1073
|
api?.logger.info(`Scheduling auto-reconnect attempt ${reconnectAttempts + 1} in ${delay}ms`);
|
|
954
1074
|
api?.emit("error:reconnecting", {
|
|
955
1075
|
attempt: reconnectAttempts + 1,
|
|
@@ -964,7 +1084,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
964
1084
|
};
|
|
965
1085
|
const maybeScheduleReconnect = (error) => {
|
|
966
1086
|
if (mergedConfig.autoReconnect === false) return;
|
|
967
|
-
if (!
|
|
1087
|
+
if (!currentSrc) return;
|
|
1088
|
+
const isLive = (api?.getState("live") ?? false) && isLiveClassified;
|
|
1089
|
+
if (!isLive && !hasPlayedContent) return;
|
|
968
1090
|
if (error.type !== "network" && error.type !== "media") return;
|
|
969
1091
|
if (reconnectWindowStart === 0) {
|
|
970
1092
|
reconnectWindowStart = Date.now();
|
|
@@ -975,13 +1097,14 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
975
1097
|
};
|
|
976
1098
|
const attemptReconnect = async () => {
|
|
977
1099
|
if (!api || !currentSrc) return;
|
|
1100
|
+
isReconnecting = true;
|
|
978
1101
|
const session = ++loadSession;
|
|
979
1102
|
reconnectAttempts++;
|
|
980
1103
|
const saved_src = currentSrc;
|
|
981
1104
|
const was_live = api.getState("live");
|
|
982
1105
|
const was_native = isNative;
|
|
983
1106
|
const resume_position = reconnectResumePosition;
|
|
984
|
-
api.logger.info(`Auto-reconnect attempt ${reconnectAttempts}`, { src: saved_src });
|
|
1107
|
+
api.logger.info(`Auto-reconnect attempt ${reconnectAttempts}`, { src: sanitizeUrl(saved_src) });
|
|
985
1108
|
try {
|
|
986
1109
|
teardownPipeline(new Error("HLS load cancelled: reconnecting"));
|
|
987
1110
|
networkRetryCount = 0;
|
|
@@ -1007,14 +1130,21 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1007
1130
|
});
|
|
1008
1131
|
api.logger.info("Auto-reconnect succeeded");
|
|
1009
1132
|
cancelReconnect();
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1133
|
+
const wasPaused = api.getState("paused");
|
|
1134
|
+
if (!wasPaused) {
|
|
1135
|
+
try {
|
|
1136
|
+
await video?.play();
|
|
1137
|
+
} catch {
|
|
1138
|
+
}
|
|
1139
|
+
} else {
|
|
1140
|
+
api.logger.info("Stream reconnected while paused; maintaining pause at live edge");
|
|
1013
1141
|
}
|
|
1014
1142
|
} catch {
|
|
1015
1143
|
if (session !== loadSession) return;
|
|
1016
1144
|
api?.logger.warn(`Auto-reconnect attempt ${reconnectAttempts} failed`);
|
|
1017
1145
|
scheduleReconnectAttempt();
|
|
1146
|
+
} finally {
|
|
1147
|
+
isReconnecting = false;
|
|
1018
1148
|
}
|
|
1019
1149
|
};
|
|
1020
1150
|
const plugin = {
|
|
@@ -1048,6 +1178,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1048
1178
|
});
|
|
1049
1179
|
const unsubSeek = api.on("playback:seeking", ({ time }) => {
|
|
1050
1180
|
if (!video) return;
|
|
1181
|
+
if (!Number.isFinite(time)) return;
|
|
1051
1182
|
const clampedTime = Math.max(0, Math.min(time, video.duration || 0));
|
|
1052
1183
|
video.currentTime = clampedTime;
|
|
1053
1184
|
});
|
|
@@ -1100,20 +1231,43 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1100
1231
|
}
|
|
1101
1232
|
}
|
|
1102
1233
|
});
|
|
1234
|
+
const unsubAudioTrack = api.on("track:audio", ({ trackId }) => {
|
|
1235
|
+
if (!hls || isNative) {
|
|
1236
|
+
api?.logger.warn("Audio track selection not available");
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
const index = audioTrackIndex(trackId);
|
|
1240
|
+
const tracks = hls.audioTracks ?? [];
|
|
1241
|
+
if (index < 0 || index >= tracks.length) {
|
|
1242
|
+
api?.logger.warn("Ignoring unknown audio track selection", { trackId });
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
hls.audioTrack = index;
|
|
1246
|
+
api?.logger.debug(`Audio: queued switch to track ${index}`);
|
|
1247
|
+
});
|
|
1103
1248
|
if (typeof window !== "undefined") {
|
|
1104
1249
|
onlineListener = () => {
|
|
1250
|
+
const hasActiveReconnect = reconnectTimer !== null || reconnectWindowStart > 0 || isReconnecting || reconnectAttempts > 0 && !reconnectExhausted;
|
|
1251
|
+
if (!hasActiveReconnect) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1105
1254
|
if (reconnectTimer) {
|
|
1106
1255
|
api?.logger.info("Browser back online, reconnecting immediately");
|
|
1107
1256
|
clearTimeout(reconnectTimer);
|
|
1108
1257
|
reconnectTimer = null;
|
|
1109
|
-
void attemptReconnect();
|
|
1110
1258
|
}
|
|
1259
|
+
void attemptReconnect();
|
|
1111
1260
|
};
|
|
1112
1261
|
window.addEventListener("online", onlineListener);
|
|
1113
1262
|
}
|
|
1114
1263
|
const unsubPoster = api.subscribeToState((event) => {
|
|
1115
1264
|
if (event.key === "poster") applyPoster();
|
|
1116
1265
|
});
|
|
1266
|
+
const unsubLive = api.subscribeToState((event) => {
|
|
1267
|
+
if (event.key === "live") {
|
|
1268
|
+
isLiveClassified = true;
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1117
1271
|
api.onDestroy(() => {
|
|
1118
1272
|
unsubPlay();
|
|
1119
1273
|
unsubPause();
|
|
@@ -1122,8 +1276,20 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1122
1276
|
unsubMute();
|
|
1123
1277
|
unsubRate();
|
|
1124
1278
|
unsubQuality();
|
|
1279
|
+
unsubAudioTrack();
|
|
1125
1280
|
unsubPoster();
|
|
1281
|
+
unsubLive();
|
|
1126
1282
|
});
|
|
1283
|
+
const unsubPlayState = api.subscribeToState((event) => {
|
|
1284
|
+
if (event.key === "playing") {
|
|
1285
|
+
if (event.value) {
|
|
1286
|
+
startStallWatchdog();
|
|
1287
|
+
} else {
|
|
1288
|
+
stopStallWatchdog();
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
api.onDestroy(unsubPlayState);
|
|
1127
1293
|
},
|
|
1128
1294
|
async destroy() {
|
|
1129
1295
|
api?.logger.info(`HLS plugin${variant.logSuffix} destroying`);
|
|
@@ -1142,10 +1308,12 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1142
1308
|
},
|
|
1143
1309
|
async loadSource(src) {
|
|
1144
1310
|
if (!api) throw new Error("Plugin not initialized");
|
|
1145
|
-
api.logger.info(`Loading HLS source${variant.logSuffix}`, { src });
|
|
1311
|
+
api.logger.info(`Loading HLS source${variant.logSuffix}`, { src: sanitizeUrl(src) });
|
|
1146
1312
|
const session = ++loadSession;
|
|
1147
1313
|
cancelReconnect();
|
|
1148
1314
|
hasPlayedContent = false;
|
|
1315
|
+
isLiveClassified = false;
|
|
1316
|
+
api.setState("live", false);
|
|
1149
1317
|
cleanup(new Error("HLS load cancelled: superseded by a new load"));
|
|
1150
1318
|
currentSrc = src;
|
|
1151
1319
|
applyPoster();
|
|
@@ -1195,14 +1363,24 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1195
1363
|
return isNative;
|
|
1196
1364
|
},
|
|
1197
1365
|
getLiveInfo() {
|
|
1198
|
-
if (isNative || !hls) return null;
|
|
1199
1366
|
const live = api?.getState("live") || false;
|
|
1200
1367
|
if (!live) return null;
|
|
1368
|
+
if (isNative) {
|
|
1369
|
+
return {
|
|
1370
|
+
isLive: true,
|
|
1371
|
+
latency: 0,
|
|
1372
|
+
targetLatency: 3,
|
|
1373
|
+
drift: 0,
|
|
1374
|
+
liveSyncPosition: video?.seekable?.length ? Math.max(0, video.seekable.end(video.seekable.length - 1) - 3) : void 0
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
if (!hls) return null;
|
|
1201
1378
|
return {
|
|
1202
1379
|
isLive: true,
|
|
1203
1380
|
latency: hls.latency || 0,
|
|
1204
1381
|
targetLatency: hls.targetLatency || 3,
|
|
1205
|
-
drift: hls.drift || 0
|
|
1382
|
+
drift: hls.drift || 0,
|
|
1383
|
+
liveSyncPosition: hls.liveSyncPosition ?? (video?.seekable?.length ? Math.max(0, video.seekable.end(video.seekable.length - 1) - 3) : void 0)
|
|
1206
1384
|
};
|
|
1207
1385
|
},
|
|
1208
1386
|
/**
|
|
@@ -1228,7 +1406,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1228
1406
|
const currentTime = video?.currentTime || 0;
|
|
1229
1407
|
const savedSrc = currentSrc;
|
|
1230
1408
|
const session = ++loadSession;
|
|
1409
|
+
cancelReconnect();
|
|
1231
1410
|
cleanup(new Error("HLS load cancelled: switching to native HLS"));
|
|
1411
|
+
currentSrc = savedSrc;
|
|
1232
1412
|
await loadNative(savedSrc);
|
|
1233
1413
|
if (session !== loadSession) return;
|
|
1234
1414
|
if (video && currentTime > 0) {
|
|
@@ -1265,7 +1445,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1265
1445
|
const currentTime = video?.currentTime || 0;
|
|
1266
1446
|
const savedSrc = currentSrc;
|
|
1267
1447
|
const session = ++loadSession;
|
|
1448
|
+
cancelReconnect();
|
|
1268
1449
|
cleanup(new Error("HLS load cancelled: switching to hls.js"));
|
|
1450
|
+
currentSrc = savedSrc;
|
|
1269
1451
|
await loadWithHlsJs(savedSrc);
|
|
1270
1452
|
if (session !== loadSession) return;
|
|
1271
1453
|
if (video && currentTime > 0) {
|