@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.
@@ -5,15 +5,7 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // src/sanitize-url.ts
8
- function sanitizeUrl(url) {
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
- url: data.url,
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 && video.seekable && video.seekable.length > 0) {
175
- const start = video.seekable.start(0);
176
- const end = video.seekable.end(video.seekable.length - 1);
177
- api.setState("seekableRange", { start, end });
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
- url: error.url
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
- url: error.url
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
- const isLive = api.getState("live");
257
- if (isLive && video.seekable && video.seekable.length > 0) {
258
- const start = video.seekable.start(0);
259
- const end = video.seekable.end(video.seekable.length - 1);
260
- api.setState("seekableRange", { start, end });
261
- const isAtLiveEdge = end - video.currentTime < 10;
262
- api.setState("liveEdge", isAtLiveEdge);
263
- api.setState("liveLatency", Math.max(0, end - video.currentTime));
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
- api.setState("duration", video.duration || 0);
268
- api.emit("media:loadedmetadata", { duration: video.duration || 0 });
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.8.1" : "0.0.0-dev";
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("Video took too long to load (network timeout)"));
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 window_ms = mergedConfig.reconnectWindowMs ?? 3e5;
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 delay = Math.round(backoff * (0.7 + Math.random() * 0.3));
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 (!hasPlayedContent || !currentSrc) return;
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
- try {
1011
- await video?.play();
1012
- } catch {
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) {