hls.js 1.6.0-beta.1.0.canary.10803 → 1.6.0-beta.1.0.canary.10806

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.
@@ -24,8 +24,8 @@ export default class BasePlaylistController
24
24
  implements NetworkComponentAPI
25
25
  {
26
26
  protected hls: Hls;
27
+ protected canLoad: boolean = false;
27
28
  private timer: number = -1;
28
- private canLoad: boolean = false;
29
29
 
30
30
  constructor(hls: Hls, logPrefix: string) {
31
31
  super(logPrefix, hls.logger);
@@ -170,6 +170,25 @@ export default class BasePlaylistController
170
170
  if (details.live || previousDetails?.live) {
171
171
  const levelOrTrack = 'levelInfo' in data ? data.levelInfo : data.track;
172
172
  details.reloaded(previousDetails);
173
+ // Merge live playlists to adjust fragment starts and fill in delta playlist skipped segments
174
+ if (previousDetails && details.fragments.length > 0) {
175
+ mergeDetails(previousDetails, details);
176
+ }
177
+ if (details.requestScheduled === -1) {
178
+ details.requestScheduled = stats.loading.start;
179
+ }
180
+ const bufferInfo = this.hls.mainForwardBufferInfo;
181
+ const position = bufferInfo ? bufferInfo.end - bufferInfo.len : 0;
182
+ const distanceToLiveEdgeMs = (details.edge - position) * 1000;
183
+ const reloadInterval = computeReloadInterval(
184
+ details,
185
+ distanceToLiveEdgeMs,
186
+ );
187
+ if (details.requestScheduled + reloadInterval < now) {
188
+ details.requestScheduled = now;
189
+ } else {
190
+ details.requestScheduled += reloadInterval;
191
+ }
173
192
  this.log(
174
193
  `live playlist ${index} ${
175
194
  details.advanced
@@ -179,10 +198,6 @@ export default class BasePlaylistController
179
198
  : 'MISSED'
180
199
  }`,
181
200
  );
182
- // Merge live playlists to adjust fragment starts and fill in delta playlist skipped segments
183
- if (previousDetails && details.fragments.length > 0) {
184
- mergeDetails(previousDetails, details);
185
- }
186
201
  if (!this.canLoad || !details.live) {
187
202
  return;
188
203
  }
@@ -196,12 +211,16 @@ export default class BasePlaylistController
196
211
  const endSn = details.endSN;
197
212
  const lastPartIndex = details.lastPartIndex;
198
213
  const hasParts = lastPartIndex !== -1;
199
- const lastPart = lastPartSn === endSn;
200
- // When low latency mode is disabled, we'll skip part requests once the last part index is found
201
- const nextSnStartIndex = lowLatencyMode ? 0 : lastPartIndex;
214
+ const atLastPartOfSegment = lastPartSn === endSn;
202
215
  if (hasParts) {
203
- msn = lastPart ? endSn + 1 : lastPartSn;
204
- part = lastPart ? nextSnStartIndex : lastPartIndex + 1;
216
+ // When low latency mode is disabled, request the last part of the next segment
217
+ if (atLastPartOfSegment) {
218
+ msn = endSn + 1;
219
+ part = lowLatencyMode ? 0 : lastPartIndex;
220
+ } else {
221
+ msn = lastPartSn;
222
+ part = lowLatencyMode ? lastPartIndex + 1 : details.maxPartIndex;
223
+ }
205
224
  } else {
206
225
  msn = endSn + 1;
207
226
  }
@@ -258,7 +277,8 @@ export default class BasePlaylistController
258
277
  msn,
259
278
  part,
260
279
  );
261
- if (lowLatencyMode || !lastPart) {
280
+ if (lowLatencyMode || !atLastPartOfSegment) {
281
+ details.requestScheduled = now;
262
282
  this.loadingPlaylist(levelOrTrack, deliveryDirectives);
263
283
  return;
264
284
  }
@@ -270,25 +290,12 @@ export default class BasePlaylistController
270
290
  part,
271
291
  );
272
292
  }
273
- if (details.requestScheduled === -1) {
274
- details.requestScheduled = stats.loading.start;
275
- }
276
293
  if (deliveryDirectives && msn !== undefined && details.canBlockReload) {
277
- details.requestScheduled -= details.partTarget * 1000 || 1000;
278
- }
279
- const bufferInfo = this.hls.mainForwardBufferInfo;
280
- const position = bufferInfo ? bufferInfo.end - bufferInfo.len : 0;
281
- const distanceToLiveEdgeMs = (details.edge - position) * 1000;
282
- const reloadInterval = computeReloadInterval(
283
- details,
284
- distanceToLiveEdgeMs,
285
- );
286
- if (details.requestScheduled + reloadInterval < now) {
287
- details.requestScheduled = now;
288
- } else {
289
- details.requestScheduled += reloadInterval;
294
+ details.requestScheduled =
295
+ stats.loading.first +
296
+ Math.max(reloadInterval - elapsed * 2, reloadInterval / 2);
290
297
  }
291
- this.scheduleLoading(levelOrTrack, deliveryDirectives);
298
+ this.scheduleLoading(levelOrTrack, deliveryDirectives, details);
292
299
  } else {
293
300
  this.clearTimer();
294
301
  }
@@ -297,8 +304,9 @@ export default class BasePlaylistController
297
304
  protected scheduleLoading(
298
305
  levelOrTrack: Level | MediaPlaylist,
299
306
  deliveryDirectives?: HlsUrlParameters,
307
+ updatedDetails?: LevelDetails,
300
308
  ) {
301
- const details = levelOrTrack.details;
309
+ const details = updatedDetails || levelOrTrack.details;
302
310
  if (!details) {
303
311
  this.loadingPlaylist(levelOrTrack, deliveryDirectives);
304
312
  return;
@@ -316,22 +324,8 @@ export default class BasePlaylistController
316
324
  estimatedTimeUntilUpdate,
317
325
  )} ms`,
318
326
  );
319
- // this.log(
320
- // `live reload ${details.updated ? 'REFRESHED' : 'MISSED'}
321
- // reload in ${estimatedTimeUntilUpdate / 1000}
322
- // round trip ${(stats.loading.end - stats.loading.start) / 1000}
323
- // diff ${
324
- // (reloadInterval -
325
- // (estimatedTimeUntilUpdate +
326
- // stats.loading.end -
327
- // stats.loading.start)) /
328
- // 1000
329
- // }
330
- // reload interval ${reloadInterval / 1000}
331
- // target duration ${details.targetduration}
332
- // distance to edge ${distanceToLiveEdgeMs / 1000}`
333
- // );
334
327
 
328
+ this.clearTimer();
335
329
  this.timer = self.setTimeout(
336
330
  () => this.loadingPlaylist(levelOrTrack, deliveryDirectives),
337
331
  estimatedTimeUntilUpdate,
@@ -379,6 +373,7 @@ export default class BasePlaylistController
379
373
  } else {
380
374
  const delay = getRetryDelay(retryConfig, retryCount);
381
375
  // Schedule level/track reload
376
+ this.clearTimer();
382
377
  this.timer = self.setTimeout(() => this.loadPlaylist(), delay);
383
378
  this.warn(
384
379
  `Retrying playlist loading ${retryCount + 1}/${
@@ -1871,7 +1871,7 @@ export default class BaseStreamController
1871
1871
  }
1872
1872
  }
1873
1873
 
1874
- protected resetWhenMissingContext(chunkMeta: ChunkMetadata) {
1874
+ protected resetWhenMissingContext(chunkMeta: ChunkMetadata | Fragment) {
1875
1875
  this.warn(
1876
1876
  `The loading context changed while buffering fragment ${chunkMeta.sn} of ${this.playlistLabel()} ${chunkMeta.level}. This chunk will not be buffered.`,
1877
1877
  );
@@ -1114,7 +1114,7 @@ MediaSource ${JSON.stringify(attachMediaSourceData)} from ${logFromSource}`,
1114
1114
  player.media?.play();
1115
1115
  }
1116
1116
  } else if (scheduledItem !== null) {
1117
- this.resumePrimary(scheduledItem, index);
1117
+ this.resumePrimary(scheduledItem, index, currentItem);
1118
1118
  if (this.shouldPlay) {
1119
1119
  this.hls.media?.play();
1120
1120
  }
@@ -1144,12 +1144,16 @@ MediaSource ${JSON.stringify(attachMediaSourceData)} from ${logFromSource}`,
1144
1144
  private resumePrimary(
1145
1145
  scheduledItem: InterstitialSchedulePrimaryItem,
1146
1146
  index: number,
1147
+ fromItem: InterstitialScheduleItem | null,
1147
1148
  ) {
1148
1149
  this.playingItem = scheduledItem;
1149
1150
  this.playingAsset = null;
1150
1151
  this.waitingItem = null;
1151
1152
 
1152
1153
  this.bufferedToItem(scheduledItem);
1154
+ if (!fromItem) {
1155
+ return;
1156
+ }
1153
1157
 
1154
1158
  this.log(`resuming ${segmentToString(scheduledItem)}`);
1155
1159
 
@@ -1231,13 +1235,22 @@ MediaSource ${JSON.stringify(attachMediaSourceData)} from ${logFromSource}`,
1231
1235
  } else {
1232
1236
  this.transferMediaTo(hls, media);
1233
1237
  if (skipSeekToStartPosition) {
1234
- hls.startLoad(timelinePos, skipSeekToStartPosition);
1238
+ this.startLoadingPrimaryAt(timelinePos, skipSeekToStartPosition);
1235
1239
  }
1236
1240
  }
1237
1241
  if (!skipSeekToStartPosition) {
1238
1242
  // Set primary position to resume time
1239
1243
  this.timelinePos = timelinePos;
1240
- hls.startLoad(timelinePos, skipSeekToStartPosition);
1244
+ this.startLoadingPrimaryAt(timelinePos, skipSeekToStartPosition);
1245
+ }
1246
+ }
1247
+
1248
+ private startLoadingPrimaryAt(
1249
+ timelinePos: number,
1250
+ skipSeekToStartPosition?: boolean,
1251
+ ) {
1252
+ if (this.hls.loadingEnabled) {
1253
+ this.hls.startLoad(timelinePos, skipSeekToStartPosition);
1241
1254
  }
1242
1255
  }
1243
1256
 
@@ -1688,7 +1701,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))}`,
1688
1701
  private preloadPrimary(item: InterstitialSchedulePrimaryItem) {
1689
1702
  const index = this.findItemIndex(item);
1690
1703
  const timelinePos = this.getPrimaryResumption(item, index);
1691
- this.hls.startLoad(timelinePos);
1704
+ this.startLoadingPrimaryAt(timelinePos);
1692
1705
  }
1693
1706
 
1694
1707
  private bufferedToEvent(
@@ -1869,7 +1882,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))}`,
1869
1882
  }
1870
1883
  const playerConfig: Partial<HlsConfig> = {
1871
1884
  ...userConfig,
1872
- // autoStartLoad: false,
1885
+ autoStartLoad: true,
1873
1886
  startFragPrefetch: true,
1874
1887
  primarySessionId: primary.sessionId,
1875
1888
  assetPlayerId: assetItem.identifier,
@@ -380,18 +380,6 @@ export default class LevelController extends BasePlaylistController {
380
380
  altAudio: !audioOnly && audioTracks.some((t) => !!t.url),
381
381
  };
382
382
  this.hls.trigger(Events.MANIFEST_PARSED, edata);
383
-
384
- // Initiate loading after all controllers have received MANIFEST_PARSED
385
- const {
386
- config: { autoStartLoad, startPosition },
387
- forceStartLoad,
388
- } = this.hls;
389
- if (autoStartLoad || forceStartLoad) {
390
- this.log(
391
- `${autoStartLoad ? 'auto' : 'force'} startLoad with configured startPosition ${startPosition}`,
392
- );
393
- this.hls.startLoad(startPosition);
394
- }
395
383
  }
396
384
 
397
385
  get levels(): Level[] | null {
@@ -606,8 +594,8 @@ export default class LevelController extends BasePlaylistController {
606
594
  return;
607
595
  }
608
596
 
609
- // only process level loaded events matching with expected level
610
- if (curLevel === this.currentLevel) {
597
+ // only process level loaded events matching with expected level or prior to switch when media playlist is loaded directly
598
+ if (curLevel === this.currentLevel || data.withoutMultiVariant) {
611
599
  // reset level load error counter on successful level loaded only if there is no issues with fragments
612
600
  if (curLevel.fragmentError === 0) {
613
601
  curLevel.loadError = 0;
@@ -676,6 +664,9 @@ export default class LevelController extends BasePlaylistController {
676
664
  }
677
665
 
678
666
  removeLevel(levelIndex: number) {
667
+ if (this._levels.length === 1) {
668
+ return;
669
+ }
679
670
  const levels = this._levels.filter((level, index) => {
680
671
  if (index !== levelIndex) {
681
672
  return true;
@@ -697,6 +688,14 @@ export default class LevelController extends BasePlaylistController {
697
688
  if (this.currentLevelIndex > -1 && this.currentLevel?.details) {
698
689
  this.currentLevelIndex = this.currentLevel.details.fragments[0].level;
699
690
  }
691
+ if (this.manualLevelIndex > -1) {
692
+ this.manualLevelIndex = this.currentLevelIndex;
693
+ }
694
+ const maxLevel = levels.length - 1;
695
+ this._firstLevel = Math.min(this._firstLevel, maxLevel);
696
+ if (this._startLevel) {
697
+ this._startLevel = Math.min(this._startLevel, maxLevel);
698
+ }
700
699
  this.hls.trigger(Events.LEVELS_UPDATED, { levels });
701
700
  }
702
701
 
@@ -1095,6 +1095,9 @@ export default class StreamController
1095
1095
  ) {
1096
1096
  if (this.level > -1 && this.fragCurrent) {
1097
1097
  this.level = this.fragCurrent.level;
1098
+ if (this.level === -1) {
1099
+ this.resetWhenMissingContext(this.fragCurrent);
1100
+ }
1098
1101
  }
1099
1102
  this.levels = data.levels;
1100
1103
  }
package/src/hls.ts CHANGED
@@ -98,8 +98,9 @@ export default class Hls implements HlsEventEmitter {
98
98
  private cmcdController?: CMCDController;
99
99
  private _media: HTMLMediaElement | null = null;
100
100
  private _url: string | null = null;
101
- private triggeringException?: boolean;
102
101
  private _sessionId?: string;
102
+ private triggeringException?: boolean;
103
+ private started: boolean = false;
103
104
 
104
105
  /**
105
106
  * Get the video-dev/hls.js package version.
@@ -527,10 +528,17 @@ export default class Hls implements HlsEventEmitter {
527
528
  (skipSeekToStartPosition ? ', <skip seek to start>' : '')
528
529
  })`,
529
530
  );
531
+ this.started = true;
530
532
  this.resumeBuffering();
531
- this.networkControllers.forEach((controller) => {
532
- controller.startLoad(startPosition, skipSeekToStartPosition);
533
- });
533
+ for (let i = 0; i < this.networkControllers.length; i++) {
534
+ this.networkControllers[i].startLoad(
535
+ startPosition,
536
+ skipSeekToStartPosition,
537
+ );
538
+ if (!this.started || !this.networkControllers) {
539
+ break;
540
+ }
541
+ }
534
542
  }
535
543
 
536
544
  /**
@@ -538,9 +546,20 @@ export default class Hls implements HlsEventEmitter {
538
546
  */
539
547
  stopLoad() {
540
548
  this.logger.log('stopLoad');
541
- this.networkControllers.forEach((controller) => {
542
- controller.stopLoad();
543
- });
549
+ this.started = false;
550
+ for (let i = 0; i < this.networkControllers.length; i++) {
551
+ this.networkControllers[i].stopLoad();
552
+ if (this.started || !this.networkControllers) {
553
+ break;
554
+ }
555
+ }
556
+ }
557
+
558
+ /**
559
+ * Returns whether loading, toggled with `startLoad()` and `stopLoad()`, is active or not`.
560
+ */
561
+ get loadingEnabled(): boolean {
562
+ return this.started;
544
563
  }
545
564
 
546
565
  /**
@@ -155,6 +155,22 @@ export class LevelDetails {
155
155
  return -1;
156
156
  }
157
157
 
158
+ get maxPartIndex(): number {
159
+ const partList = this.partList;
160
+ if (partList) {
161
+ const lastIndex = this.lastPartIndex;
162
+ if (lastIndex !== -1) {
163
+ for (let i = partList.length; i--; ) {
164
+ if (partList[i].index > lastIndex) {
165
+ return partList[i].index;
166
+ }
167
+ }
168
+ return lastIndex;
169
+ }
170
+ }
171
+ return 0;
172
+ }
173
+
158
174
  get lastPartSn(): number {
159
175
  if (this.partList?.length) {
160
176
  return this.partList[this.partList.length - 1].fragment.sn;
@@ -250,7 +250,9 @@ class PlaylistLoader implements NetworkComponentAPI {
250
250
  ) {
251
251
  // same URL can't overlap, or wait for blocking request
252
252
  if (loaderContext.url === context.url) {
253
- logger.log(`[playlist-loader]: playlist request ongoing`);
253
+ logger.log(
254
+ `[playlist-loader]: ignore ${context.url} ongoing request`,
255
+ );
254
256
  } else {
255
257
  logger.log(
256
258
  `[playlist-loader]: ignore ${context.url} in favor of ${loaderContext.url}`,
@@ -349,7 +351,10 @@ class PlaylistLoader implements NetworkComponentAPI {
349
351
  }
350
352
 
351
353
  stats.parsing.start = performance.now();
352
- if (M3U8Parser.isMediaPlaylist(string)) {
354
+ if (
355
+ M3U8Parser.isMediaPlaylist(string) ||
356
+ context.type !== PlaylistContextType.MANIFEST
357
+ ) {
353
358
  this.handleTrackOrLevelPlaylist(
354
359
  response,
355
360
  stats,
@@ -386,6 +391,22 @@ class PlaylistLoader implements NetworkComponentAPI {
386
391
  loader.load(context, loaderConfig, loaderCallbacks);
387
392
  }
388
393
 
394
+ private checkAutostartLoad() {
395
+ if (!this.hls) {
396
+ return;
397
+ }
398
+ const {
399
+ config: { autoStartLoad, startPosition },
400
+ forceStartLoad,
401
+ } = this.hls;
402
+ if (autoStartLoad || forceStartLoad) {
403
+ this.hls.logger.log(
404
+ `${autoStartLoad ? 'auto' : 'force'} startLoad with configured startPosition ${startPosition}`,
405
+ );
406
+ this.hls.startLoad(startPosition);
407
+ }
408
+ }
409
+
389
410
  private handleMasterPlaylist(
390
411
  response: LoaderResponse,
391
412
  stats: LoaderStats,
@@ -474,6 +495,8 @@ class PlaylistLoader implements NetworkComponentAPI {
474
495
  startTimeOffset,
475
496
  variableList,
476
497
  });
498
+
499
+ this.checkAutostartLoad();
477
500
  }
478
501
 
479
502
  private handleTrackOrLevelPlaylist(
@@ -493,7 +516,7 @@ class PlaylistLoader implements NetworkComponentAPI {
493
516
  ? (id as number)
494
517
  : 0;
495
518
  const levelType = mapContextToLevelType(context);
496
- const levelDetails: LevelDetails = M3U8Parser.parseLevelPlaylist(
519
+ const levelDetails = M3U8Parser.parseLevelPlaylist(
497
520
  response.data as string,
498
521
  url,
499
522
  levelId,
@@ -543,6 +566,14 @@ class PlaylistLoader implements NetworkComponentAPI {
543
566
  networkDetails,
544
567
  loader,
545
568
  );
569
+
570
+ if (
571
+ type === PlaylistContextType.MANIFEST &&
572
+ (!levelDetails.playlistParsingError ||
573
+ (!levelDetails.fragments.length && levelDetails.live))
574
+ ) {
575
+ this.checkAutostartLoad();
576
+ }
546
577
  }
547
578
 
548
579
  private handleManifestParsingError(
@@ -662,7 +693,9 @@ class PlaylistLoader implements NetworkComponentAPI {
662
693
  ? (level as number)
663
694
  : undefined;
664
695
  if (!levelDetails.fragments.length) {
665
- const error = new Error('No Segments found in Playlist');
696
+ const error = (levelDetails.playlistParsingError = new Error(
697
+ 'No Segments found in Playlist',
698
+ ));
666
699
  hls.trigger(Events.ERROR, {
667
700
  type: ErrorTypes.NETWORK_ERROR,
668
701
  details: ErrorDetails.LEVEL_EMPTY_ERROR,
@@ -721,6 +754,7 @@ class PlaylistLoader implements NetworkComponentAPI {
721
754
  stats,
722
755
  networkDetails,
723
756
  deliveryDirectives,
757
+ withoutMultiVariant: type === PlaylistContextType.MANIFEST,
724
758
  });
725
759
  break;
726
760
  case PlaylistContextType.AUDIO_TRACK:
@@ -220,6 +220,7 @@ export interface LevelLoadedData {
220
220
  networkDetails: any;
221
221
  stats: LoaderStats;
222
222
  deliveryDirectives: HlsUrlParameters | null;
223
+ withoutMultiVariant?: boolean;
223
224
  }
224
225
 
225
226
  export interface LevelUpdatedData {
@@ -538,10 +538,13 @@ export function findPart(
538
538
 
539
539
  export function reassignFragmentLevelIndexes(levels: Level[]) {
540
540
  levels.forEach((level, index) => {
541
- const { details } = level;
542
- if (details?.fragments) {
543
- details.fragments.forEach((fragment) => {
541
+ const fragments = level.details?.fragments;
542
+ if (fragments) {
543
+ fragments.forEach((fragment) => {
544
544
  fragment.level = index;
545
+ if (fragment.initSegment) {
546
+ fragment.initSegment.level = index;
547
+ }
545
548
  });
546
549
  }
547
550
  });