@scarlett-player/hls 1.8.1 → 1.9.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-EP22IZ63.js} +226 -46
- package/dist/index.cjs +226 -46
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/light.cjs +226 -46
- package/dist/light.d.cts +2 -2
- package/dist/light.d.ts +2 -2
- package/dist/light.js +1 -1
- package/dist/{sanitize-url-DcZXD_K-.d.cts → sanitize-url-BvhRz6uj.d.cts} +2 -0
- package/dist/{sanitize-url-DcZXD_K-.d.ts → sanitize-url-BvhRz6uj.d.ts} +2 -0
- package/package.json +2 -2
|
@@ -107,6 +107,24 @@ function parseHlsError(data) {
|
|
|
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?.();
|
|
@@ -214,6 +249,8 @@ function setupHlsEventHandlers(hls, api, callbacks) {
|
|
|
214
249
|
hls.off(event, handler);
|
|
215
250
|
}
|
|
216
251
|
handlers.length = 0;
|
|
252
|
+
api.setState("audioTracks", []);
|
|
253
|
+
api.setState("currentAudioTrack", null);
|
|
217
254
|
};
|
|
218
255
|
}
|
|
219
256
|
function setupVideoEventHandlers(video, api) {
|
|
@@ -253,19 +290,27 @@ function setupVideoEventHandlers(video, api) {
|
|
|
253
290
|
addHandler("timeupdate", () => {
|
|
254
291
|
api.setState("currentTime", video.currentTime);
|
|
255
292
|
api.emit("playback:timeupdate", { currentTime: video.currentTime });
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
293
|
+
if (video.seekable && video.seekable.length > 0) {
|
|
294
|
+
if (api.getState("live") || !Number.isFinite(video.duration)) {
|
|
295
|
+
const start = video.seekable.start(0);
|
|
296
|
+
const end = video.seekable.end(video.seekable.length - 1);
|
|
297
|
+
api.setState("seekableRange", { start, end });
|
|
298
|
+
const latency = Math.max(0, end - video.currentTime);
|
|
299
|
+
api.setState("liveEdge", latency < 10);
|
|
300
|
+
api.setState("liveLatency", latency);
|
|
301
|
+
}
|
|
264
302
|
}
|
|
265
303
|
});
|
|
266
304
|
addHandler("durationchange", () => {
|
|
267
|
-
|
|
268
|
-
|
|
305
|
+
const rawDuration = video.duration;
|
|
306
|
+
const isLive = !Number.isFinite(rawDuration) || rawDuration === Infinity;
|
|
307
|
+
if (isLive) {
|
|
308
|
+
api.setState("live", true);
|
|
309
|
+
api.setState("duration", 0);
|
|
310
|
+
} else {
|
|
311
|
+
api.setState("duration", rawDuration || 0);
|
|
312
|
+
}
|
|
313
|
+
api.emit("media:loadedmetadata", { duration: api.getState("duration") });
|
|
269
314
|
});
|
|
270
315
|
addHandler("waiting", () => {
|
|
271
316
|
api.setState("waiting", true);
|
|
@@ -401,7 +446,7 @@ function createValidatingPlaylistLoader(Hls) {
|
|
|
401
446
|
}
|
|
402
447
|
|
|
403
448
|
// src/version.ts
|
|
404
|
-
var PKG_VERSION = true ? "1.
|
|
449
|
+
var PKG_VERSION = true ? "1.9.0" : "0.0.0-dev";
|
|
405
450
|
|
|
406
451
|
// src/create-hls-plugin.ts
|
|
407
452
|
var DEFAULT_CONFIG = {
|
|
@@ -461,6 +506,11 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
461
506
|
let onlineListener = null;
|
|
462
507
|
let reconnectTriggerError = null;
|
|
463
508
|
let reconnectExhausted = false;
|
|
509
|
+
let isReconnecting = false;
|
|
510
|
+
let isLiveClassified = false;
|
|
511
|
+
let stallWatchdogTimer = null;
|
|
512
|
+
let lastStallCheckTime = 0;
|
|
513
|
+
let lastStallCheckPosition = 0;
|
|
464
514
|
const applyPoster = () => {
|
|
465
515
|
if (!video) return;
|
|
466
516
|
video.poster = api?.getState("poster") || "";
|
|
@@ -492,6 +542,10 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
492
542
|
clearTimeout(retryTimeout);
|
|
493
543
|
retryTimeout = null;
|
|
494
544
|
}
|
|
545
|
+
if (stallWatchdogTimer) {
|
|
546
|
+
clearTimeout(stallWatchdogTimer);
|
|
547
|
+
stallWatchdogTimer = null;
|
|
548
|
+
}
|
|
495
549
|
if (hls) {
|
|
496
550
|
hls.destroy();
|
|
497
551
|
hls = null;
|
|
@@ -603,20 +657,20 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
603
657
|
const handleHlsError = (error) => {
|
|
604
658
|
const Hls = loader.getHlsConstructor();
|
|
605
659
|
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
660
|
if (error.fatal) {
|
|
661
|
+
const now = Date.now();
|
|
662
|
+
if (now - errorWindowStart > ERROR_WINDOW_MS) {
|
|
663
|
+
errorCount = 1;
|
|
664
|
+
errorWindowStart = now;
|
|
665
|
+
} else {
|
|
666
|
+
errorCount++;
|
|
667
|
+
}
|
|
668
|
+
if (errorCount >= MAX_ERRORS_IN_WINDOW) {
|
|
669
|
+
api?.logger.error(`Too many fatal errors (${errorCount} in ${ERROR_WINDOW_MS}ms), giving up`);
|
|
670
|
+
emitFatalError(error, true);
|
|
671
|
+
teardownPipeline(new Error(error.details));
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
620
674
|
api?.logger.error("Fatal HLS error", { type: error.type, details: error.details });
|
|
621
675
|
switch (error.type) {
|
|
622
676
|
case "network": {
|
|
@@ -895,9 +949,15 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
895
949
|
if (resolved || session !== loadSession) return;
|
|
896
950
|
resolved = true;
|
|
897
951
|
releaseAbort();
|
|
898
|
-
api?.logger.error(`HLS load timed out after ${timeout_ms}ms`, { src });
|
|
952
|
+
api?.logger.error(`HLS load timed out after ${timeout_ms}ms`, { src: sanitizeUrl(src) });
|
|
953
|
+
const timeoutError = {
|
|
954
|
+
type: "network",
|
|
955
|
+
details: "Video took too long to load (network timeout)",
|
|
956
|
+
fatal: true
|
|
957
|
+
};
|
|
958
|
+
emitFatalError(timeoutError, true);
|
|
899
959
|
teardownPipeline();
|
|
900
|
-
reject(new Error(
|
|
960
|
+
reject(new Error(timeoutError.details));
|
|
901
961
|
}, timeout_ms);
|
|
902
962
|
}
|
|
903
963
|
hls.attachMedia(videoEl);
|
|
@@ -914,6 +974,51 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
914
974
|
reconnectResumePosition = 0;
|
|
915
975
|
reconnectTriggerError = null;
|
|
916
976
|
reconnectExhausted = false;
|
|
977
|
+
isReconnecting = false;
|
|
978
|
+
};
|
|
979
|
+
const startStallWatchdog = () => {
|
|
980
|
+
if (!video || stallWatchdogTimer) return;
|
|
981
|
+
lastStallCheckTime = Date.now();
|
|
982
|
+
lastStallCheckPosition = video.currentTime;
|
|
983
|
+
const levelTargetDuration = hls?.targetDuration ?? hls?.levels?.[hls?.currentLevel]?.details?.targetduration ?? 0;
|
|
984
|
+
const targetDuration = Math.max(15, 4 * (levelTargetDuration || 0) || 15);
|
|
985
|
+
const checkInterval = Math.min(targetDuration, 3e4);
|
|
986
|
+
const check = () => {
|
|
987
|
+
if (!video || !api) return;
|
|
988
|
+
const isPlaying = api.getState("playing");
|
|
989
|
+
const isSeeking = api.getState("seeking");
|
|
990
|
+
if (!isPlaying || isSeeking) {
|
|
991
|
+
stallWatchdogTimer = setTimeout(check, checkInterval);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const elapsed = Date.now() - lastStallCheckTime;
|
|
995
|
+
const positionDelta = video.currentTime - lastStallCheckPosition;
|
|
996
|
+
if (positionDelta > 0.5) {
|
|
997
|
+
lastStallCheckTime = Date.now();
|
|
998
|
+
lastStallCheckPosition = video.currentTime;
|
|
999
|
+
} else if (elapsed > targetDuration * 1e3) {
|
|
1000
|
+
api.logger.warn("Playback stall detected \u2014 no timeupdate progress", {
|
|
1001
|
+
elapsed: Math.round(elapsed),
|
|
1002
|
+
currentTime: video.currentTime,
|
|
1003
|
+
targetDuration: Math.round(targetDuration)
|
|
1004
|
+
});
|
|
1005
|
+
const stallError = {
|
|
1006
|
+
type: "network",
|
|
1007
|
+
details: "Playback stalled \u2014 no data received",
|
|
1008
|
+
fatal: true
|
|
1009
|
+
};
|
|
1010
|
+
maybeScheduleReconnect(stallError);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
stallWatchdogTimer = setTimeout(check, checkInterval);
|
|
1014
|
+
};
|
|
1015
|
+
stallWatchdogTimer = setTimeout(check, checkInterval);
|
|
1016
|
+
};
|
|
1017
|
+
const stopStallWatchdog = () => {
|
|
1018
|
+
if (stallWatchdogTimer) {
|
|
1019
|
+
clearTimeout(stallWatchdogTimer);
|
|
1020
|
+
stallWatchdogTimer = null;
|
|
1021
|
+
}
|
|
917
1022
|
};
|
|
918
1023
|
const emitReconnectExhausted = (elapsedMs, windowMs) => {
|
|
919
1024
|
if (reconnectExhausted) return;
|
|
@@ -939,17 +1044,30 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
939
1044
|
const scheduleReconnectAttempt = () => {
|
|
940
1045
|
if (reconnectExhausted) return;
|
|
941
1046
|
if (reconnectTimer) return;
|
|
942
|
-
const
|
|
1047
|
+
const isLive = (api?.getState("live") ?? false) && isLiveClassified;
|
|
1048
|
+
const configWindowMs = mergedConfig.reconnectWindowMs ?? 3e5;
|
|
1049
|
+
const window_ms = isLive ? Infinity : configWindowMs;
|
|
943
1050
|
const elapsed_ms = Date.now() - reconnectWindowStart;
|
|
944
1051
|
if (elapsed_ms > window_ms) {
|
|
945
1052
|
api?.logger.warn(`Auto-reconnect window exhausted after ${reconnectAttempts} attempts`);
|
|
946
1053
|
emitReconnectExhausted(elapsed_ms, window_ms);
|
|
947
1054
|
return;
|
|
948
1055
|
}
|
|
1056
|
+
const LONG_OUTAGE_MS = 6e5;
|
|
1057
|
+
if (isLive && elapsed_ms > LONG_OUTAGE_MS) {
|
|
1058
|
+
api?.emit("error:reconnecting", {
|
|
1059
|
+
attempt: reconnectAttempts + 1,
|
|
1060
|
+
delayMs: 0,
|
|
1061
|
+
elapsedMs: elapsed_ms,
|
|
1062
|
+
windowMs: window_ms,
|
|
1063
|
+
longOutage: true
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
949
1066
|
const base_delay = mergedConfig.reconnectBaseDelayMs ?? 2e3;
|
|
950
1067
|
const max_delay = mergedConfig.reconnectMaxDelayMs ?? 3e4;
|
|
951
1068
|
const backoff = Math.min(base_delay * Math.pow(2, reconnectAttempts), max_delay);
|
|
952
|
-
const
|
|
1069
|
+
const isAtCap = backoff >= max_delay;
|
|
1070
|
+
const delay = isLive && isAtCap ? 3e4 : Math.round(backoff * (0.7 + Math.random() * 0.3));
|
|
953
1071
|
api?.logger.info(`Scheduling auto-reconnect attempt ${reconnectAttempts + 1} in ${delay}ms`);
|
|
954
1072
|
api?.emit("error:reconnecting", {
|
|
955
1073
|
attempt: reconnectAttempts + 1,
|
|
@@ -964,7 +1082,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
964
1082
|
};
|
|
965
1083
|
const maybeScheduleReconnect = (error) => {
|
|
966
1084
|
if (mergedConfig.autoReconnect === false) return;
|
|
967
|
-
if (!
|
|
1085
|
+
if (!currentSrc) return;
|
|
1086
|
+
const isLive = (api?.getState("live") ?? false) && isLiveClassified;
|
|
1087
|
+
if (!isLive && !hasPlayedContent) return;
|
|
968
1088
|
if (error.type !== "network" && error.type !== "media") return;
|
|
969
1089
|
if (reconnectWindowStart === 0) {
|
|
970
1090
|
reconnectWindowStart = Date.now();
|
|
@@ -975,13 +1095,14 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
975
1095
|
};
|
|
976
1096
|
const attemptReconnect = async () => {
|
|
977
1097
|
if (!api || !currentSrc) return;
|
|
1098
|
+
isReconnecting = true;
|
|
978
1099
|
const session = ++loadSession;
|
|
979
1100
|
reconnectAttempts++;
|
|
980
1101
|
const saved_src = currentSrc;
|
|
981
1102
|
const was_live = api.getState("live");
|
|
982
1103
|
const was_native = isNative;
|
|
983
1104
|
const resume_position = reconnectResumePosition;
|
|
984
|
-
api.logger.info(`Auto-reconnect attempt ${reconnectAttempts}`, { src: saved_src });
|
|
1105
|
+
api.logger.info(`Auto-reconnect attempt ${reconnectAttempts}`, { src: sanitizeUrl(saved_src) });
|
|
985
1106
|
try {
|
|
986
1107
|
teardownPipeline(new Error("HLS load cancelled: reconnecting"));
|
|
987
1108
|
networkRetryCount = 0;
|
|
@@ -1007,14 +1128,21 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1007
1128
|
});
|
|
1008
1129
|
api.logger.info("Auto-reconnect succeeded");
|
|
1009
1130
|
cancelReconnect();
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1131
|
+
const wasPaused = api.getState("paused");
|
|
1132
|
+
if (!wasPaused) {
|
|
1133
|
+
try {
|
|
1134
|
+
await video?.play();
|
|
1135
|
+
} catch {
|
|
1136
|
+
}
|
|
1137
|
+
} else {
|
|
1138
|
+
api.logger.info("Stream reconnected while paused; maintaining pause at live edge");
|
|
1013
1139
|
}
|
|
1014
1140
|
} catch {
|
|
1015
1141
|
if (session !== loadSession) return;
|
|
1016
1142
|
api?.logger.warn(`Auto-reconnect attempt ${reconnectAttempts} failed`);
|
|
1017
1143
|
scheduleReconnectAttempt();
|
|
1144
|
+
} finally {
|
|
1145
|
+
isReconnecting = false;
|
|
1018
1146
|
}
|
|
1019
1147
|
};
|
|
1020
1148
|
const plugin = {
|
|
@@ -1048,6 +1176,7 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1048
1176
|
});
|
|
1049
1177
|
const unsubSeek = api.on("playback:seeking", ({ time }) => {
|
|
1050
1178
|
if (!video) return;
|
|
1179
|
+
if (!Number.isFinite(time)) return;
|
|
1051
1180
|
const clampedTime = Math.max(0, Math.min(time, video.duration || 0));
|
|
1052
1181
|
video.currentTime = clampedTime;
|
|
1053
1182
|
});
|
|
@@ -1100,20 +1229,43 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1100
1229
|
}
|
|
1101
1230
|
}
|
|
1102
1231
|
});
|
|
1232
|
+
const unsubAudioTrack = api.on("track:audio", ({ trackId }) => {
|
|
1233
|
+
if (!hls || isNative) {
|
|
1234
|
+
api?.logger.warn("Audio track selection not available");
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const index = audioTrackIndex(trackId);
|
|
1238
|
+
const tracks = hls.audioTracks ?? [];
|
|
1239
|
+
if (index < 0 || index >= tracks.length) {
|
|
1240
|
+
api?.logger.warn("Ignoring unknown audio track selection", { trackId });
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
hls.audioTrack = index;
|
|
1244
|
+
api?.logger.debug(`Audio: queued switch to track ${index}`);
|
|
1245
|
+
});
|
|
1103
1246
|
if (typeof window !== "undefined") {
|
|
1104
1247
|
onlineListener = () => {
|
|
1248
|
+
const hasActiveReconnect = reconnectTimer !== null || reconnectWindowStart > 0 || isReconnecting || reconnectAttempts > 0 && !reconnectExhausted;
|
|
1249
|
+
if (!hasActiveReconnect) {
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1105
1252
|
if (reconnectTimer) {
|
|
1106
1253
|
api?.logger.info("Browser back online, reconnecting immediately");
|
|
1107
1254
|
clearTimeout(reconnectTimer);
|
|
1108
1255
|
reconnectTimer = null;
|
|
1109
|
-
void attemptReconnect();
|
|
1110
1256
|
}
|
|
1257
|
+
void attemptReconnect();
|
|
1111
1258
|
};
|
|
1112
1259
|
window.addEventListener("online", onlineListener);
|
|
1113
1260
|
}
|
|
1114
1261
|
const unsubPoster = api.subscribeToState((event) => {
|
|
1115
1262
|
if (event.key === "poster") applyPoster();
|
|
1116
1263
|
});
|
|
1264
|
+
const unsubLive = api.subscribeToState((event) => {
|
|
1265
|
+
if (event.key === "live") {
|
|
1266
|
+
isLiveClassified = true;
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1117
1269
|
api.onDestroy(() => {
|
|
1118
1270
|
unsubPlay();
|
|
1119
1271
|
unsubPause();
|
|
@@ -1122,8 +1274,20 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1122
1274
|
unsubMute();
|
|
1123
1275
|
unsubRate();
|
|
1124
1276
|
unsubQuality();
|
|
1277
|
+
unsubAudioTrack();
|
|
1125
1278
|
unsubPoster();
|
|
1279
|
+
unsubLive();
|
|
1126
1280
|
});
|
|
1281
|
+
const unsubPlayState = api.subscribeToState((event) => {
|
|
1282
|
+
if (event.key === "playing") {
|
|
1283
|
+
if (event.value) {
|
|
1284
|
+
startStallWatchdog();
|
|
1285
|
+
} else {
|
|
1286
|
+
stopStallWatchdog();
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
api.onDestroy(unsubPlayState);
|
|
1127
1291
|
},
|
|
1128
1292
|
async destroy() {
|
|
1129
1293
|
api?.logger.info(`HLS plugin${variant.logSuffix} destroying`);
|
|
@@ -1142,10 +1306,12 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1142
1306
|
},
|
|
1143
1307
|
async loadSource(src) {
|
|
1144
1308
|
if (!api) throw new Error("Plugin not initialized");
|
|
1145
|
-
api.logger.info(`Loading HLS source${variant.logSuffix}`, { src });
|
|
1309
|
+
api.logger.info(`Loading HLS source${variant.logSuffix}`, { src: sanitizeUrl(src) });
|
|
1146
1310
|
const session = ++loadSession;
|
|
1147
1311
|
cancelReconnect();
|
|
1148
1312
|
hasPlayedContent = false;
|
|
1313
|
+
isLiveClassified = false;
|
|
1314
|
+
api.setState("live", false);
|
|
1149
1315
|
cleanup(new Error("HLS load cancelled: superseded by a new load"));
|
|
1150
1316
|
currentSrc = src;
|
|
1151
1317
|
applyPoster();
|
|
@@ -1195,14 +1361,24 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1195
1361
|
return isNative;
|
|
1196
1362
|
},
|
|
1197
1363
|
getLiveInfo() {
|
|
1198
|
-
if (isNative || !hls) return null;
|
|
1199
1364
|
const live = api?.getState("live") || false;
|
|
1200
1365
|
if (!live) return null;
|
|
1366
|
+
if (isNative) {
|
|
1367
|
+
return {
|
|
1368
|
+
isLive: true,
|
|
1369
|
+
latency: 0,
|
|
1370
|
+
targetLatency: 3,
|
|
1371
|
+
drift: 0,
|
|
1372
|
+
liveSyncPosition: video?.seekable?.length ? Math.max(0, video.seekable.end(video.seekable.length - 1) - 3) : void 0
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
if (!hls) return null;
|
|
1201
1376
|
return {
|
|
1202
1377
|
isLive: true,
|
|
1203
1378
|
latency: hls.latency || 0,
|
|
1204
1379
|
targetLatency: hls.targetLatency || 3,
|
|
1205
|
-
drift: hls.drift || 0
|
|
1380
|
+
drift: hls.drift || 0,
|
|
1381
|
+
liveSyncPosition: hls.liveSyncPosition ?? (video?.seekable?.length ? Math.max(0, video.seekable.end(video.seekable.length - 1) - 3) : void 0)
|
|
1206
1382
|
};
|
|
1207
1383
|
},
|
|
1208
1384
|
/**
|
|
@@ -1228,7 +1404,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1228
1404
|
const currentTime = video?.currentTime || 0;
|
|
1229
1405
|
const savedSrc = currentSrc;
|
|
1230
1406
|
const session = ++loadSession;
|
|
1407
|
+
cancelReconnect();
|
|
1231
1408
|
cleanup(new Error("HLS load cancelled: switching to native HLS"));
|
|
1409
|
+
currentSrc = savedSrc;
|
|
1232
1410
|
await loadNative(savedSrc);
|
|
1233
1411
|
if (session !== loadSession) return;
|
|
1234
1412
|
if (video && currentTime > 0) {
|
|
@@ -1265,7 +1443,9 @@ function createHLSPluginWith(loader, variant, config) {
|
|
|
1265
1443
|
const currentTime = video?.currentTime || 0;
|
|
1266
1444
|
const savedSrc = currentSrc;
|
|
1267
1445
|
const session = ++loadSession;
|
|
1446
|
+
cancelReconnect();
|
|
1268
1447
|
cleanup(new Error("HLS load cancelled: switching to hls.js"));
|
|
1448
|
+
currentSrc = savedSrc;
|
|
1269
1449
|
await loadWithHlsJs(savedSrc);
|
|
1270
1450
|
if (session !== loadSession) return;
|
|
1271
1451
|
if (video && currentTime > 0) {
|