rx-player 3.27.0-dev.2022032100 → 3.27.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +5 -2
  2. package/VERSION +1 -1
  3. package/dist/_esm5.processed/compat/eme/custom_media_keys/old_webkit_media_keys.js +15 -11
  4. package/dist/_esm5.processed/compat/eme/custom_media_keys/webkit_media_keys.js +22 -6
  5. package/dist/_esm5.processed/compat/eme/generate_key_request.d.ts +4 -6
  6. package/dist/_esm5.processed/compat/eme/generate_key_request.js +4 -6
  7. package/dist/_esm5.processed/compat/get_start_date.d.ts +30 -0
  8. package/dist/_esm5.processed/compat/get_start_date.js +44 -0
  9. package/dist/_esm5.processed/compat/index.d.ts +2 -1
  10. package/dist/_esm5.processed/compat/index.js +2 -1
  11. package/dist/_esm5.processed/config.d.ts +1 -5
  12. package/dist/_esm5.processed/core/api/public_api.js +25 -25
  13. package/dist/_esm5.processed/core/decrypt/content_decryptor.js +11 -3
  14. package/dist/_esm5.processed/core/decrypt/create_or_load_session.js +1 -1
  15. package/dist/_esm5.processed/core/decrypt/create_session.d.ts +3 -1
  16. package/dist/_esm5.processed/core/decrypt/create_session.js +15 -5
  17. package/dist/_esm5.processed/core/decrypt/utils/loaded_sessions_store.d.ts +94 -1
  18. package/dist/_esm5.processed/core/decrypt/utils/loaded_sessions_store.js +237 -96
  19. package/dist/_esm5.processed/core/segment_buffers/garbage_collector.js +4 -1
  20. package/dist/_esm5.processed/core/stream/orchestrator/stream_orchestrator.js +2 -1
  21. package/dist/_esm5.processed/core/stream/period/period_stream.js +9 -3
  22. package/dist/_esm5.processed/core/stream/representation/append_segment_to_buffer.js +4 -3
  23. package/dist/_esm5.processed/core/stream/representation/force_garbage_collection.js +3 -2
  24. package/dist/_esm5.processed/core/stream/representation/get_buffer_status.d.ts +2 -2
  25. package/dist/_esm5.processed/core/stream/representation/get_buffer_status.js +9 -3
  26. package/dist/_esm5.processed/core/stream/representation/get_needed_segments.d.ts +11 -1
  27. package/dist/_esm5.processed/core/stream/representation/get_needed_segments.js +27 -45
  28. package/dist/_esm5.processed/core/stream/representation/representation_stream.js +6 -4
  29. package/dist/_esm5.processed/default_config.d.ts +2 -35
  30. package/dist/_esm5.processed/default_config.js +2 -35
  31. package/dist/_esm5.processed/transports/dash/add_segment_integrity_checks_to_loader.js +39 -38
  32. package/dist/_esm5.processed/utils/reference.js +0 -2
  33. package/dist/_esm5.processed/utils/task_canceller.d.ts +8 -1
  34. package/dist/_esm5.processed/utils/task_canceller.js +9 -1
  35. package/dist/rx-player.js +927 -587
  36. package/dist/rx-player.min.js +1 -1
  37. package/package.json +1 -1
  38. package/sonar-project.properties +1 -1
  39. package/src/compat/eme/custom_media_keys/old_webkit_media_keys.ts +16 -12
  40. package/src/compat/eme/custom_media_keys/webkit_media_keys.ts +21 -8
  41. package/src/compat/eme/generate_key_request.ts +4 -6
  42. package/src/compat/get_start_date.ts +48 -0
  43. package/src/compat/index.ts +2 -0
  44. package/src/core/api/public_api.ts +23 -27
  45. package/src/core/decrypt/content_decryptor.ts +15 -4
  46. package/src/core/decrypt/create_or_load_session.ts +4 -1
  47. package/src/core/decrypt/create_session.ts +23 -9
  48. package/src/core/decrypt/utils/loaded_sessions_store.ts +254 -102
  49. package/src/core/segment_buffers/garbage_collector.ts +4 -0
  50. package/src/core/stream/orchestrator/stream_orchestrator.ts +2 -1
  51. package/src/core/stream/period/period_stream.ts +9 -4
  52. package/src/core/stream/representation/append_segment_to_buffer.ts +17 -13
  53. package/src/core/stream/representation/force_garbage_collection.ts +4 -1
  54. package/src/core/stream/representation/get_buffer_status.ts +21 -13
  55. package/src/core/stream/representation/get_needed_segments.ts +40 -55
  56. package/src/core/stream/representation/representation_stream.ts +6 -4
  57. package/src/default_config.ts +20 -57
  58. package/src/transports/dash/add_segment_integrity_checks_to_loader.ts +41 -44
  59. package/src/utils/reference.ts +0 -2
  60. package/src/utils/task_canceller.ts +16 -1
@@ -14,17 +14,13 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import { Subscription } from "rxjs";
18
17
  import {
19
18
  closeSession,
19
+ generateKeyRequest,
20
20
  ICustomMediaKeys,
21
21
  ICustomMediaKeySession,
22
+ loadSession,
22
23
  } from "../../../compat";
23
- import {
24
- onKeyMessage$,
25
- onKeyStatusesChange$,
26
- } from "../../../compat/event_listeners";
27
- import config from "../../../config";
28
24
  import log from "../../../log";
29
25
  import isNullOrUndefined from "../../../utils/is_null_or_undefined";
30
26
  import { IProcessedProtectionData } from "../types";
@@ -57,7 +53,7 @@ export default class LoadedSessionsStore {
57
53
 
58
54
  /**
59
55
  * Create a new MediaKeySession and store it in this store.
60
- * @param {Object} initializationData
56
+ * @param {Object} initData
61
57
  * @param {string} sessionType
62
58
  * @returns {Object}
63
59
  */
@@ -67,7 +63,12 @@ export default class LoadedSessionsStore {
67
63
  ) : IStoredSessionEntry {
68
64
  const keySessionRecord = new KeySessionRecord(initData);
69
65
  const mediaKeySession = this._mediaKeys.createSession(sessionType);
70
- const entry = { mediaKeySession, sessionType, keySessionRecord };
66
+ const entry = { mediaKeySession,
67
+ sessionType,
68
+ keySessionRecord,
69
+ isGeneratingRequest: false,
70
+ isLoadingPersistentSession: false,
71
+ closingStatus: { type: "none" as const } };
71
72
  if (!isNullOrUndefined(mediaKeySession.closed)) {
72
73
  mediaKeySession.closed
73
74
  .then(() => {
@@ -85,7 +86,7 @@ export default class LoadedSessionsStore {
85
86
  }
86
87
 
87
88
  log.debug("DRM-LSS: Add MediaKeySession", entry.sessionType);
88
- this._storage.push({ keySessionRecord, mediaKeySession, sessionType });
89
+ this._storage.push({ ...entry });
89
90
  return entry;
90
91
  }
91
92
 
@@ -109,14 +110,145 @@ export default class LoadedSessionsStore {
109
110
  if (stored.keySessionRecord.isCompatibleWith(initializationData)) {
110
111
  this._storage.splice(i, 1);
111
112
  this._storage.push(stored);
112
- return { keySessionRecord: stored.keySessionRecord,
113
- mediaKeySession: stored.mediaKeySession,
114
- sessionType: stored.sessionType };
113
+ return { ...stored };
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+
119
+ /**
120
+ * Get `LoadedSessionsStore`'s entry for a given MediaKeySession.
121
+ * Returns `null` if the given MediaKeySession is not stored in the
122
+ * `LoadedSessionsStore`.
123
+ * @param {MediaKeySession} mediaKeySession
124
+ * @returns {Object|null}
125
+ */
126
+ public getEntryForSession(
127
+ mediaKeySession : MediaKeySession | ICustomMediaKeySession
128
+ ) : IStoredSessionEntry | null {
129
+ for (let i = this._storage.length - 1; i >= 0; i--) {
130
+ const stored = this._storage[i];
131
+ if (stored.mediaKeySession === mediaKeySession) {
132
+ return { ...stored };
115
133
  }
116
134
  }
117
135
  return null;
118
136
  }
119
137
 
138
+ /**
139
+ * Generate a license request on the given MediaKeySession, while indicating
140
+ * to the LoadedSessionsStore that a license-request is pending so
141
+ * session-closing orders are properly scheduled after it is done.
142
+ * @param {Object} mediaKeySession
143
+ * @param {string} initializationDataType - Initialization data type given
144
+ * e.g. by the "encrypted" event for the corresponding request.
145
+ * @param {Uint8Array} initializationData - Initialization data given e.g. by
146
+ * the "encrypted" event for the corresponding request.
147
+ * @returns {Promise}
148
+ */
149
+ public async generateLicenseRequest(
150
+ mediaKeySession : MediaKeySession | ICustomMediaKeySession,
151
+ initializationDataType : string | undefined,
152
+ initializationData : Uint8Array
153
+ ) : Promise<unknown> {
154
+ let entry : IStoredSessionEntry | undefined;
155
+ for (const stored of this._storage) {
156
+ if (stored.mediaKeySession === mediaKeySession) {
157
+ entry = stored;
158
+ break;
159
+ }
160
+ }
161
+ if (entry === undefined) {
162
+ log.error("DRM-LSS: generateRequest error. No MediaKeySession found with " +
163
+ "the given initData and initDataType");
164
+ return generateKeyRequest(mediaKeySession,
165
+ initializationDataType,
166
+ initializationData);
167
+ }
168
+
169
+ entry.isGeneratingRequest = true;
170
+
171
+ // Note the `as string` is needed due to TypeScript not understanding that
172
+ // the `closingStatus` might change in the next checks
173
+ if (entry.closingStatus.type as string !== "none") {
174
+ throw new Error("The `MediaKeySession` is being closed.");
175
+ }
176
+ try {
177
+ await generateKeyRequest(mediaKeySession,
178
+ initializationDataType,
179
+ initializationData);
180
+ } catch (err) {
181
+ if (entry === undefined) {
182
+ throw err;
183
+ }
184
+ entry.isGeneratingRequest = false;
185
+ if (entry.closingStatus.type === "awaiting") {
186
+ entry.closingStatus.start();
187
+ }
188
+ throw err;
189
+
190
+ }
191
+ if (entry === undefined) {
192
+ return undefined;
193
+ }
194
+ entry.isGeneratingRequest = false;
195
+ if (entry.closingStatus.type === "awaiting") {
196
+ entry.closingStatus.start();
197
+ }
198
+ }
199
+
200
+ /**
201
+ * @param {Object} mediaKeySession
202
+ * @param {string} sessionId
203
+ * @returns {Promise}
204
+ */
205
+ public async loadPersistentSession(
206
+ mediaKeySession : MediaKeySession | ICustomMediaKeySession,
207
+ sessionId : string
208
+ ) : Promise<boolean> {
209
+ let entry : IStoredSessionEntry | undefined;
210
+ for (const stored of this._storage) {
211
+ if (stored.mediaKeySession === mediaKeySession) {
212
+ entry = stored;
213
+ break;
214
+ }
215
+ }
216
+ if (entry === undefined) {
217
+ log.error("DRM-LSS: loadPersistentSession error. No MediaKeySession found with " +
218
+ "the given initData and initDataType");
219
+ return loadSession(mediaKeySession, sessionId);
220
+ }
221
+ entry.isLoadingPersistentSession = true;
222
+
223
+ // Note the `as string` is needed due to TypeScript not understanding that
224
+ // the `closingStatus` might change in the next checks
225
+ if (entry.closingStatus.type as string !== "none") {
226
+ throw new Error("The `MediaKeySession` is being closed.");
227
+ }
228
+ let ret : boolean;
229
+ try {
230
+ ret = await loadSession(mediaKeySession, sessionId);
231
+ } catch (err) {
232
+ if (entry === undefined) {
233
+ throw err;
234
+ }
235
+ entry.isLoadingPersistentSession = false;
236
+ if (entry.closingStatus.type === "awaiting") {
237
+ entry.closingStatus.start();
238
+ }
239
+ throw err;
240
+
241
+ }
242
+ if (entry === undefined) {
243
+ return ret;
244
+ }
245
+ entry.isLoadingPersistentSession = false;
246
+ if (entry.closingStatus.type === "awaiting") {
247
+ entry.closingStatus.start();
248
+ }
249
+ return ret;
250
+ }
251
+
120
252
  /**
121
253
  * Close a MediaKeySession and remove its related stored information from the
122
254
  * `LoadedSessionsStore`.
@@ -127,7 +259,7 @@ export default class LoadedSessionsStore {
127
259
  public async closeSession(
128
260
  mediaKeySession : MediaKeySession | ICustomMediaKeySession
129
261
  ) : Promise<boolean> {
130
- let entry;
262
+ let entry : IStoredSessionEntry | undefined;
131
263
  for (const stored of this._storage) {
132
264
  if (stored.mediaKeySession === mediaKeySession) {
133
265
  entry = stored;
@@ -139,8 +271,7 @@ export default class LoadedSessionsStore {
139
271
  "the given initData and initDataType");
140
272
  return Promise.resolve(false);
141
273
  }
142
- await safelyCloseMediaKeySession(entry.mediaKeySession);
143
- return Promise.resolve(true);
274
+ return this._closeEntry(entry);
144
275
  }
145
276
 
146
277
  /**
@@ -175,10 +306,17 @@ export default class LoadedSessionsStore {
175
306
  this._storage = [];
176
307
 
177
308
  const closingProms = allEntries
178
- .map((entry) => safelyCloseMediaKeySession(entry.mediaKeySession));
309
+ .map((entry) => this._closeEntry(entry));
179
310
  await Promise.all(closingProms);
180
311
  }
181
312
 
313
+ /**
314
+ * Get the index of a stored MediaKeySession entry based on its
315
+ * `KeySessionRecord`.
316
+ * Returns -1 if not found.
317
+ * @param {Object} record
318
+ * @returns {number}
319
+ */
182
320
  private getIndex(record : KeySessionRecord) : number {
183
321
  for (let i = 0; i < this._storage.length; i++) {
184
322
  const stored = this._storage[i];
@@ -188,6 +326,50 @@ export default class LoadedSessionsStore {
188
326
  }
189
327
  return -1;
190
328
  }
329
+
330
+ /**
331
+ * Prepare the closure of a `MediaKeySession` stored as an entry of the
332
+ * `LoadedSessionsStore`.
333
+ * Allows to postpone the closure action if another MediaKeySession action
334
+ * is already pending.
335
+ * @param {Object} entry
336
+ * @returns {Promise.<boolean>}
337
+ */
338
+ private async _closeEntry(
339
+ entry : IStoredSessionEntry
340
+ ) : Promise<boolean> {
341
+ const { mediaKeySession } = entry;
342
+ return new Promise((resolve, reject) => {
343
+ if (entry !== undefined &&
344
+ (
345
+ entry.isLoadingPersistentSession || entry.isGeneratingRequest
346
+ ))
347
+ {
348
+ entry.closingStatus = { type: "awaiting",
349
+ start: tryClosingEntryAndResolve };
350
+ } else {
351
+ tryClosingEntryAndResolve();
352
+ }
353
+ function tryClosingEntryAndResolve() {
354
+ if (entry !== undefined) {
355
+ entry.closingStatus = { type: "pending" };
356
+ }
357
+ safelyCloseMediaKeySession(mediaKeySession)
358
+ .then(() => {
359
+ if (entry !== undefined) {
360
+ entry.closingStatus = { type: "done" };
361
+ }
362
+ resolve(true);
363
+ })
364
+ .catch((err) => {
365
+ if (entry !== undefined) {
366
+ entry.closingStatus = { type: "failed" };
367
+ }
368
+ reject(err);
369
+ });
370
+ }
371
+ });
372
+ }
191
373
  }
192
374
 
193
375
  /** Information linked to a `MediaKeySession` created by the `LoadedSessionsStore`. */
@@ -216,102 +398,72 @@ export interface IStoredSessionEntry {
216
398
  * which the MediaKeySession was created.
217
399
  */
218
400
  sessionType : MediaKeySessionType;
401
+
402
+ /**
403
+ * Set to `true` while a `generateRequest` call is pending.
404
+ * This information might be useful as it is one of the operation we have to
405
+ * wait for before closing a MediaKeySession.
406
+ */
407
+ isGeneratingRequest : boolean;
408
+
409
+ /**
410
+ * Set to `true` while a `load` call is pending.
411
+ * This information might be useful as it is one of the operation we have to
412
+ * wait for before closing a MediaKeySession.
413
+ */
414
+ isLoadingPersistentSession : boolean;
415
+
416
+ /**
417
+ * The status of a potential `MediaKeySession`'s close request.
418
+ * Closing a MediaKeySession could be made complex as it normally cannot
419
+ * happen until `generateRequest` or `load` has been called.
420
+ *
421
+ * To avoid problems while still staying compatible to the most devices
422
+ * possible - which may have strange implementation of the specification -
423
+ * we're adding the `closingStatus` property allowing to perform multiple
424
+ * type of interaction while a close operation is either pending or is
425
+ * awaited.
426
+ */
427
+ closingStatus :
428
+ /** Status when the MediaKeySession is currently being closed. */
429
+ { type : "pending" } |
430
+ /** Status when the MediaKeySession has been closed. */
431
+ { type : "done" } |
432
+ /** Status when the MediaKeySession failed to close. */
433
+ { type : "failed" } |
434
+ /**
435
+ * Status when a close order has been received for this MediaKeySession
436
+ * while some sensitive operation (examples are `generateRequest` and `load`
437
+ * calls).
438
+ * The `LoadedSessionsStore` should call `start` once it has finished those
439
+ * operations.
440
+ */
441
+ {
442
+ type : "awaiting";
443
+ start : () => void;
444
+ } |
445
+ /** Status when the MediaKeySession failed to close. */
446
+ { type : "none" };
219
447
  }
220
448
 
221
449
  /**
222
- * Close a MediaKeySession with multiple attempts if needed and do not throw if
223
- * this action throws an error.
450
+ * Close a MediaKeySession and just log an error if it fails (while resolving).
224
451
  * Emits then complete when done.
225
452
  * @param {MediaKeySession} mediaKeySession
226
453
  * @returns {Observable}
227
454
  */
228
- function safelyCloseMediaKeySession(
455
+ async function safelyCloseMediaKeySession(
229
456
  mediaKeySession : MediaKeySession | ICustomMediaKeySession
230
- ) : Promise<unknown> {
231
- return recursivelyTryToCloseMediaKeySession(0);
232
-
233
- /**
234
- * Perform a new attempt at closing the MediaKeySession.
235
- * If this operation fails due to a not-"callable" (an EME term)
236
- * MediaKeySession, retry based on either a timer or on MediaKeySession
237
- * events, whichever comes first.
238
- * Emits then complete when done.
239
- * @param {number} retryNb - The attempt number starting at 0.
240
- * @returns {Observable}
241
- */
242
- async function recursivelyTryToCloseMediaKeySession(
243
- retryNb : number
244
- ) : Promise<unknown> {
245
- log.debug("DRM: Trying to close a MediaKeySession",
246
- mediaKeySession.sessionId,
247
- retryNb);
248
- try {
249
- await closeSession(mediaKeySession);
250
- log.debug("DRM: Succeeded to close MediaKeySession");
251
- return undefined;
252
- } catch (err : unknown) {
253
- // Unitialized MediaKeySession may not close properly until their
254
- // corresponding `generateRequest` or `load` call are handled by the
255
- // browser.
256
- // In that case the EME specification tells us that the browser is
257
- // supposed to reject the `close` call with an InvalidStateError.
258
- if (!(err instanceof Error) || err.name !== "InvalidStateError" ||
259
- mediaKeySession.sessionId !== "")
260
- {
261
- return failToCloseSession(err);
262
- }
263
-
264
- const { EME_SESSION_CLOSING_MAX_RETRY,
265
- EME_SESSION_CLOSING_INITIAL_DELAY,
266
- EME_SESSION_CLOSING_MAX_DELAY } = config.getCurrent();
267
-
268
- // We will retry either:
269
- // - when an event indicates that the MediaKeySession is
270
- // initialized (`callable` is the proper EME term here)
271
- // - after a delay, raising exponentially
272
- const nextRetryNb = retryNb + 1;
273
- if (nextRetryNb > EME_SESSION_CLOSING_MAX_RETRY) {
274
- return failToCloseSession(err);
275
- }
276
- const delay = Math.min(Math.pow(2, retryNb) * EME_SESSION_CLOSING_INITIAL_DELAY,
277
- EME_SESSION_CLOSING_MAX_DELAY);
278
- log.warn("DRM: attempt to close a mediaKeySession failed, " +
279
- "scheduling retry...", delay);
280
-
281
- let ksChangeSub : undefined | Subscription;
282
- const ksChangeProm = new Promise((res) => {
283
- ksChangeSub = onKeyStatusesChange$(mediaKeySession).subscribe(res);
284
- });
285
-
286
- let ksMsgSub : undefined | Subscription;
287
- const ksMsgProm = new Promise((res) => {
288
- ksMsgSub = onKeyMessage$(mediaKeySession).subscribe(res);
289
- });
290
-
291
- let sleepTimer : undefined | number;
292
- const sleepProm = new Promise((res) => {
293
- sleepTimer = window.setTimeout(res, delay);
294
- });
295
-
296
- await Promise.race([ksChangeProm, ksMsgProm, sleepProm]);
297
- ksChangeSub?.unsubscribe();
298
- ksMsgSub?.unsubscribe();
299
- clearTimeout(sleepTimer);
300
-
301
- return recursivelyTryToCloseMediaKeySession(nextRetryNb);
302
- }
303
- }
304
-
305
- /**
306
- * Log error anouncing that we could not close the MediaKeySession and emits
307
- * then complete through Observable.
308
- * TODO Emit warning?
309
- * @returns {Observable}
310
- */
311
- function failToCloseSession(err : unknown) : Promise<null> {
457
+ ) : Promise<void> {
458
+ log.debug("DRM: Trying to close a MediaKeySession", mediaKeySession.sessionId);
459
+ try {
460
+ await closeSession(mediaKeySession);
461
+ log.debug("DRM: Succeeded to close MediaKeySession");
462
+ return ;
463
+ } catch (err : unknown) {
312
464
  log.error("DRM: Could not close MediaKeySession: " +
313
465
  (err instanceof Error ? err.toString() :
314
466
  "Unknown error"));
315
- return Promise.resolve(null);
467
+ return ;
316
468
  }
317
469
  }
@@ -22,6 +22,7 @@ import {
22
22
  ignoreElements,
23
23
  mergeMap,
24
24
  Observable,
25
+ of as observableOf,
25
26
  } from "rxjs";
26
27
  import log from "../../log";
27
28
  import { getInnerAndOuterTimeRanges } from "../../utils/ranges";
@@ -152,6 +153,9 @@ function clearBuffer(
152
153
  const clean$ = observableFrom(
153
154
  cleanedupRanges.map((range) => {
154
155
  log.debug("GC: cleaning range from SegmentBuffer", range);
156
+ if (range.start >= range.end) {
157
+ return observableOf(null);
158
+ }
155
159
  return segmentBuffer.removeBuffer(range.start, range.end);
156
160
  })
157
161
  ).pipe(concatAll(),
@@ -348,7 +348,8 @@ export default function StreamOrchestrator(
348
348
 
349
349
  return observableConcat(
350
350
  ...rangesToClean.map(({ start, end }) =>
351
- segmentBuffer.removeBuffer(start, end).pipe(ignoreElements())),
351
+ start >= end ? EMPTY :
352
+ segmentBuffer.removeBuffer(start, end).pipe(ignoreElements())),
352
353
  playbackObserver.observe(true).pipe(
353
354
  take(1),
354
355
  mergeMap((observation) => {
@@ -170,10 +170,15 @@ export default function PeriodStream({
170
170
  if (SegmentBuffersStore.isNative(bufferType)) {
171
171
  return reloadAfterSwitch(period, bufferType, playbackObserver, 0);
172
172
  }
173
- cleanBuffer$ = segmentBufferStatus.value
174
- .removeBuffer(period.start,
175
- period.end == null ? Infinity :
176
- period.end);
173
+ if (period.end === undefined) {
174
+ cleanBuffer$ = segmentBufferStatus.value.removeBuffer(period.start,
175
+ Infinity);
176
+ } else if (period.end <= period.start) {
177
+ cleanBuffer$ = observableOf(null);
178
+ } else {
179
+ cleanBuffer$ = segmentBufferStatus.value.removeBuffer(period.start,
180
+ period.end);
181
+ }
177
182
  } else {
178
183
  if (segmentBufferStatus.type === "uninitialized") {
179
184
  segmentBuffersStore.disableSegmentBuffer(bufferType);
@@ -24,6 +24,7 @@ import {
24
24
  mergeMap,
25
25
  ignoreElements,
26
26
  Observable,
27
+ take,
27
28
  } from "rxjs";
28
29
  import { MediaError } from "../../../errors";
29
30
  import { IReadOnlyPlaybackObserver } from "../../api";
@@ -60,19 +61,22 @@ export default function appendSegmentToBuffer<T>(
60
61
  throw new MediaError("BUFFER_APPEND_ERROR", reason);
61
62
  }
62
63
 
63
- return playbackObserver.observe(true).pipe(mergeMap((observation) => {
64
- const currentPos = observation.position + observation.wantedTimeOffset;
65
- return observableConcat(
66
- forceGarbageCollection(currentPos, segmentBuffer).pipe(ignoreElements()),
67
- append$
68
- ).pipe(
69
- catchError((forcedGCError : unknown) => {
70
- const reason = forcedGCError instanceof Error ? forcedGCError.toString() :
71
- "Could not clean the buffer";
64
+ return playbackObserver.observe(true).pipe(
65
+ take(1),
66
+ mergeMap((observation) => {
67
+ const currentPos = observation.position + observation.wantedTimeOffset;
68
+ return observableConcat(
69
+ forceGarbageCollection(currentPos, segmentBuffer).pipe(ignoreElements()),
70
+ append$
71
+ ).pipe(
72
+ catchError((forcedGCError : unknown) => {
73
+ const reason = forcedGCError instanceof Error ?
74
+ forcedGCError.toString() :
75
+ "Could not clean the buffer";
72
76
 
73
- throw new MediaError("BUFFER_FULL_ERROR", reason);
74
- })
75
- );
76
- }));
77
+ throw new MediaError("BUFFER_FULL_ERROR", reason);
78
+ })
79
+ );
80
+ }));
77
81
  }));
78
82
  }
@@ -19,6 +19,7 @@ import {
19
19
  defer as observableDefer,
20
20
  from as observableFrom,
21
21
  Observable,
22
+ of as observableOf,
22
23
  } from "rxjs";
23
24
  import config from "../../../config";
24
25
  import log from "../../../log";
@@ -54,7 +55,9 @@ export default function forceGarbageCollection(
54
55
 
55
56
  log.debug("Stream: GC cleaning", cleanedupRanges);
56
57
  return observableFrom(
57
- cleanedupRanges.map(({ start, end }) => bufferingQueue.removeBuffer(start, end))
58
+ cleanedupRanges.map(({ start, end }) =>
59
+ start >= end ? observableOf(null) :
60
+ bufferingQueue.removeBuffer(start, end))
58
61
  ).pipe(concatAll());
59
62
  });
60
63
  }
@@ -62,8 +62,8 @@ export interface IBufferStatus {
62
62
  */
63
63
  shouldRefreshManifest : boolean;
64
64
  /**
65
- * If 'true', the buffer memory is saturated before being able to download
66
- * at least MIN_REQUIRED_BUFFER_AHEAD ( default : 10sec )
65
+ * If 'true', the buffer memory is saturated, thus we may have issues loading
66
+ * new segments.
67
67
  */
68
68
  isBufferFull: boolean;
69
69
  }
@@ -123,16 +123,18 @@ export default function getBufferStatus(
123
123
  const getBufferedHistory = segmentBuffer.getSegmentHistory.bind(segmentBuffer);
124
124
 
125
125
  /** List of segments we will need to download. */
126
- const { neededSegments, isBufferFull } = getNeededSegments({ content,
127
- bufferedSegments,
128
- currentPlaybackTime,
129
- fastSwitchThreshold,
130
- getBufferedHistory,
131
- neededRange,
132
- segmentsBeingPushed,
133
- maxBufferSize });
134
-
135
- const prioritizedNeededSegments: IQueuedSegment[] = neededSegments.map((segment) => (
126
+ const { segmentsToLoad,
127
+ segmentsOnHold,
128
+ isBufferFull } = getNeededSegments({ content,
129
+ bufferedSegments,
130
+ currentPlaybackTime,
131
+ fastSwitchThreshold,
132
+ getBufferedHistory,
133
+ neededRange,
134
+ segmentsBeingPushed,
135
+ maxBufferSize });
136
+
137
+ const prioritizedNeededSegments: IQueuedSegment[] = segmentsToLoad.map((segment) => (
136
138
  {
137
139
  priority: getSegmentPriority(segment.time, wantedStartPosition),
138
140
  segment,
@@ -148,7 +150,8 @@ export default function getBufferStatus(
148
150
  const lastPosition = representation.index.getLastPosition();
149
151
  if (!representation.index.isInitialized() ||
150
152
  period.end === undefined ||
151
- prioritizedNeededSegments.length > 0)
153
+ prioritizedNeededSegments.length > 0 ||
154
+ segmentsOnHold.length > 0)
152
155
  {
153
156
  hasFinishedLoading = false;
154
157
  } else {
@@ -192,6 +195,11 @@ export default function getBufferStatus(
192
195
  if (segmentsBeingPushed.length > 0) {
193
196
  nextSegmentStart = Math.min(...segmentsBeingPushed.map(info => info.segment.time));
194
197
  }
198
+ if (segmentsOnHold.length > 0) {
199
+ nextSegmentStart = nextSegmentStart !== null ?
200
+ Math.min(nextSegmentStart, segmentsOnHold[0].time) :
201
+ segmentsOnHold[0].time;
202
+ }
195
203
  if (prioritizedNeededSegments.length > 0) {
196
204
  nextSegmentStart = nextSegmentStart !== null ?
197
205
  Math.min(nextSegmentStart, prioritizedNeededSegments[0].segment.time) :