rx-player 3.28.0-dev.2022063000 → 3.28.1-dev.2022083000

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 (108) hide show
  1. package/.github/workflows/checks.yml +20 -18
  2. package/CHANGELOG.md +14 -2
  3. package/VERSION +1 -1
  4. package/dist/_esm5.processed/compat/eme/load_session.d.ts +5 -6
  5. package/dist/_esm5.processed/compat/eme/load_session.js +5 -6
  6. package/dist/_esm5.processed/compat/event_listeners.js +5 -0
  7. package/dist/_esm5.processed/core/api/media_element_track_choice_manager.js +1 -1
  8. package/dist/_esm5.processed/core/api/public_api.js +2 -2
  9. package/dist/_esm5.processed/core/decrypt/create_session.js +33 -4
  10. package/dist/_esm5.processed/core/decrypt/utils/loaded_sessions_store.d.ts +14 -0
  11. package/dist/_esm5.processed/core/decrypt/utils/loaded_sessions_store.js +25 -0
  12. package/dist/_esm5.processed/core/fetchers/segment/segment_fetcher.d.ts +1 -1
  13. package/dist/_esm5.processed/core/fetchers/segment/segment_fetcher.js +1 -1
  14. package/dist/_esm5.processed/core/init/content_time_boundaries_observer.js +110 -38
  15. package/dist/_esm5.processed/core/stream/representation/check_for_discontinuity.js +21 -9
  16. package/dist/_esm5.processed/core/stream/representation/get_buffer_status.js +21 -29
  17. package/dist/_esm5.processed/errors/encrypted_media_error.d.ts +1 -2
  18. package/dist/_esm5.processed/errors/encrypted_media_error.js +0 -1
  19. package/dist/_esm5.processed/errors/error_codes.d.ts +6 -1
  20. package/dist/_esm5.processed/errors/media_error.d.ts +1 -2
  21. package/dist/_esm5.processed/errors/media_error.js +0 -1
  22. package/dist/_esm5.processed/errors/network_error.d.ts +1 -2
  23. package/dist/_esm5.processed/errors/network_error.js +0 -1
  24. package/dist/_esm5.processed/errors/other_error.d.ts +1 -2
  25. package/dist/_esm5.processed/errors/other_error.js +0 -1
  26. package/dist/_esm5.processed/manifest/manifest.d.ts +1 -1
  27. package/dist/_esm5.processed/manifest/manifest.js +1 -1
  28. package/dist/_esm5.processed/manifest/representation_index/static.d.ts +21 -6
  29. package/dist/_esm5.processed/manifest/representation_index/static.js +26 -8
  30. package/dist/_esm5.processed/manifest/representation_index/types.d.ts +55 -44
  31. package/dist/_esm5.processed/parsers/manifest/dash/common/indexes/base.d.ts +21 -8
  32. package/dist/_esm5.processed/parsers/manifest/dash/common/indexes/base.js +25 -10
  33. package/dist/_esm5.processed/parsers/manifest/dash/common/indexes/list.d.ts +26 -12
  34. package/dist/_esm5.processed/parsers/manifest/dash/common/indexes/list.js +26 -13
  35. package/dist/_esm5.processed/parsers/manifest/dash/common/indexes/template.d.ts +23 -7
  36. package/dist/_esm5.processed/parsers/manifest/dash/common/indexes/template.js +65 -22
  37. package/dist/_esm5.processed/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.d.ts +20 -3
  38. package/dist/_esm5.processed/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.js +57 -7
  39. package/dist/_esm5.processed/parsers/manifest/dash/common/indexes/{is_period_fulfilled.d.ts → utils.d.ts} +3 -6
  40. package/dist/_esm5.processed/parsers/manifest/dash/common/indexes/{is_period_fulfilled.js → utils.js} +4 -8
  41. package/dist/_esm5.processed/parsers/manifest/dash/common/parse_periods.js +1 -1
  42. package/dist/_esm5.processed/parsers/manifest/local/parse_local_manifest.js +5 -8
  43. package/dist/_esm5.processed/parsers/manifest/local/representation_index.d.ts +21 -8
  44. package/dist/_esm5.processed/parsers/manifest/local/representation_index.js +49 -14
  45. package/dist/_esm5.processed/parsers/manifest/local/types.d.ts +16 -0
  46. package/dist/_esm5.processed/parsers/manifest/metaplaylist/representation_index.d.ts +20 -6
  47. package/dist/_esm5.processed/parsers/manifest/metaplaylist/representation_index.js +28 -10
  48. package/dist/_esm5.processed/parsers/manifest/smooth/create_parser.js +4 -4
  49. package/dist/_esm5.processed/parsers/manifest/smooth/representation_index.d.ts +21 -12
  50. package/dist/_esm5.processed/parsers/manifest/smooth/representation_index.js +39 -14
  51. package/dist/_esm5.processed/parsers/manifest/utils/get_first_time_from_adaptation.js +1 -1
  52. package/dist/_esm5.processed/parsers/manifest/utils/get_last_time_from_adaptation.js +1 -1
  53. package/dist/_esm5.processed/transports/metaplaylist/pipelines.js +0 -2
  54. package/dist/_esm5.processed/transports/smooth/segment_loader.js +1 -1
  55. package/dist/_esm5.processed/transports/types.d.ts +1 -1
  56. package/dist/_esm5.processed/utils/deep_merge.d.ts +1 -1
  57. package/dist/_esm5.processed/utils/deep_merge.js +6 -5
  58. package/dist/_esm5.processed/utils/task_canceller.d.ts +0 -3
  59. package/dist/_esm5.processed/utils/task_canceller.js +0 -3
  60. package/dist/mpd-parser.wasm +0 -0
  61. package/dist/rx-player.js +1390 -1059
  62. package/dist/rx-player.min.js +1 -1
  63. package/jest.config.js +5 -0
  64. package/package.json +31 -30
  65. package/sonar-project.properties +1 -1
  66. package/src/compat/eme/load_session.ts +5 -6
  67. package/src/compat/event_listeners.ts +5 -0
  68. package/src/core/api/media_element_track_choice_manager.ts +1 -1
  69. package/src/core/api/public_api.ts +2 -2
  70. package/src/core/decrypt/create_session.ts +28 -2
  71. package/src/core/decrypt/utils/loaded_sessions_store.ts +29 -0
  72. package/src/core/fetchers/segment/segment_fetcher.ts +1 -1
  73. package/src/core/init/content_time_boundaries_observer.ts +116 -42
  74. package/src/core/stream/representation/check_for_discontinuity.ts +28 -10
  75. package/src/core/stream/representation/get_buffer_status.ts +27 -34
  76. package/src/errors/encrypted_media_error.ts +1 -2
  77. package/src/errors/error_codes.ts +2 -2
  78. package/src/errors/media_error.ts +1 -2
  79. package/src/errors/network_error.ts +1 -2
  80. package/src/errors/other_error.ts +1 -2
  81. package/src/manifest/__tests__/adaptation.test.ts +4 -3
  82. package/src/manifest/__tests__/representation.test.ts +4 -3
  83. package/src/manifest/manifest.ts +1 -1
  84. package/src/manifest/representation_index/__tests__/static.test.ts +5 -4
  85. package/src/manifest/representation_index/static.ts +28 -9
  86. package/src/manifest/representation_index/types.ts +62 -46
  87. package/src/parsers/manifest/dash/common/indexes/base.ts +27 -11
  88. package/src/parsers/manifest/dash/common/indexes/list.ts +32 -15
  89. package/src/parsers/manifest/dash/common/indexes/template.ts +73 -27
  90. package/src/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.ts +60 -8
  91. package/src/parsers/manifest/dash/common/indexes/{is_period_fulfilled.ts → utils.ts} +4 -13
  92. package/src/parsers/manifest/dash/common/parse_periods.ts +1 -1
  93. package/src/parsers/manifest/local/parse_local_manifest.ts +8 -20
  94. package/src/parsers/manifest/local/representation_index.ts +51 -16
  95. package/src/parsers/manifest/local/types.ts +13 -0
  96. package/src/parsers/manifest/metaplaylist/representation_index.ts +31 -11
  97. package/src/parsers/manifest/smooth/create_parser.ts +4 -4
  98. package/src/parsers/manifest/smooth/representation_index.ts +40 -15
  99. package/src/parsers/manifest/utils/__tests__/get_first_time_from_adaptations.test.ts +4 -3
  100. package/src/parsers/manifest/utils/__tests__/get_last_time_from_adaptation.test.ts +4 -3
  101. package/src/parsers/manifest/utils/get_first_time_from_adaptation.ts +1 -1
  102. package/src/parsers/manifest/utils/get_last_time_from_adaptation.ts +1 -1
  103. package/src/transports/metaplaylist/pipelines.ts +0 -2
  104. package/src/transports/smooth/segment_loader.ts +1 -1
  105. package/src/transports/types.ts +1 -1
  106. package/src/utils/__tests__/initialization_segment_cache.test.ts +7 -0
  107. package/src/utils/deep_merge.ts +7 -4
  108. package/src/utils/task_canceller.ts +0 -3
package/jest.config.js CHANGED
@@ -6,6 +6,11 @@ module.exports = {
6
6
  roots: ["<rootDir>/src"],
7
7
  preset: "ts-jest",
8
8
  testEnvironment: "jsdom",
9
+ // Without this, Jest just fails when importing rxjs, for some arcane
10
+ // ESM-vs-CommonJS reasons linked to how it works internally.
11
+ moduleNameMapper: {
12
+ '^rxjs$': require.resolve('rxjs'),
13
+ },
9
14
  testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"],
10
15
  collectCoverageFrom: [
11
16
  "src/**/*.ts",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rx-player",
3
3
  "author": "Canal+",
4
- "version": "3.28.0-dev.2022063000",
4
+ "version": "3.28.1-dev.2022083000",
5
5
  "description": "Canal+ HTML5 Video Player",
6
6
  "main": "./dist/rx-player.js",
7
7
  "keywords": [
@@ -48,7 +48,7 @@
48
48
  "doc": "rm -rf doc/generated; node ./scripts/doc-generator/index.js doc/ doc/generated \"$(cat VERSION)\"",
49
49
  "lint": "eslint src -c .eslintrc.js",
50
50
  "lint:demo": "eslint demo/full/scripts",
51
- "lint:tests": "eslint tests/**/*.js",
51
+ "lint:tests": "eslint tests/**/*.js --ignore-pattern '/tests/performance/bundle*'",
52
52
  "list": "node scripts/list-npm-scripts.js",
53
53
  "prepublishOnly": "npm run build:modular",
54
54
  "standalone": "node ./scripts/run_standalone_demo.js",
@@ -81,34 +81,35 @@
81
81
  "rxjs": "7.4.0"
82
82
  },
83
83
  "devDependencies": {
84
- "@babel/core": "7.18.5",
85
- "@babel/plugin-transform-runtime": "7.18.5",
86
- "@babel/preset-env": "7.18.2",
87
- "@babel/preset-react": "7.17.12",
88
- "@types/chai": "4.3.1",
89
- "@types/jest": "27.5.1",
84
+ "@babel/core": "7.18.13",
85
+ "@babel/plugin-transform-runtime": "7.18.10",
86
+ "@babel/preset-env": "7.18.10",
87
+ "@babel/preset-react": "7.18.6",
88
+ "@types/chai": "4.3.3",
89
+ "@types/jest": "28.1.8",
90
90
  "@types/mocha": "9.1.1",
91
- "@types/node": "17.0.42",
92
- "@types/sinon": "10.0.11",
93
- "@typescript-eslint/eslint-plugin": "5.27.1",
94
- "@typescript-eslint/eslint-plugin-tslint": "5.27.1",
95
- "@typescript-eslint/parser": "5.27.1",
91
+ "@types/node": "18.7.13",
92
+ "@types/sinon": "10.0.13",
93
+ "@typescript-eslint/eslint-plugin": "5.35.1",
94
+ "@typescript-eslint/eslint-plugin-tslint": "5.35.1",
95
+ "@typescript-eslint/parser": "5.35.1",
96
96
  "arraybuffer-loader": "1.0.8",
97
97
  "babel-loader": "8.2.5",
98
98
  "chai": "4.3.6",
99
- "cheerio": "1.0.0-rc.11",
100
- "core-js": "3.22.8",
101
- "esbuild": "0.14.43",
102
- "eslint": "8.17.0",
99
+ "cheerio": "1.0.0-rc.12",
100
+ "core-js": "3.25.0",
101
+ "esbuild": "0.15.5",
102
+ "eslint": "8.23.0",
103
103
  "eslint-plugin-import": "2.26.0",
104
- "eslint-plugin-jsdoc": "39.3.2",
105
- "eslint-plugin-react": "7.30.0",
104
+ "eslint-plugin-jsdoc": "39.3.6",
105
+ "eslint-plugin-react": "7.31.1",
106
106
  "esm": "3.2.25",
107
107
  "express": "4.18.1",
108
- "highlight.js": "11.5.1",
108
+ "highlight.js": "11.6.0",
109
109
  "html-entities": "2.3.3",
110
- "jest": "27.5.1",
111
- "karma": "6.3.20",
110
+ "jest": "28.1.3",
111
+ "jest-environment-jsdom": "28.1.3",
112
+ "karma": "6.4.0",
112
113
  "karma-chrome-launcher": "3.1.1",
113
114
  "karma-firefox-launcher": "2.1.2",
114
115
  "karma-mocha": "2.0.1",
@@ -117,19 +118,19 @@
117
118
  "mocha": "10.0.0",
118
119
  "mocha-loader": "5.1.5",
119
120
  "raw-loader": "4.0.2",
120
- "react": "18.1.0",
121
- "react-dom": "18.1.0",
121
+ "react": "18.2.0",
122
+ "react-dom": "18.2.0",
122
123
  "regenerator-runtime": "0.13.9",
123
124
  "rimraf": "3.0.2",
124
125
  "semver": "7.3.7",
125
126
  "sinon": "14.0.0",
126
- "terser-webpack-plugin": "5.3.3",
127
- "ts-jest": "27.1.5",
128
- "ts-loader": "9.3.0",
127
+ "terser-webpack-plugin": "5.3.5",
128
+ "ts-jest": "28.0.8",
129
+ "ts-loader": "9.3.1",
129
130
  "tslint": "6.1.3",
130
- "typescript": "4.7.3",
131
- "webpack": "5.73.0",
132
- "webpack-bundle-analyzer": "4.5.0",
131
+ "typescript": "4.8.2",
132
+ "webpack": "5.74.0",
133
+ "webpack-bundle-analyzer": "4.6.1",
133
134
  "webpack-cli": "4.10.0"
134
135
  },
135
136
  "scripts-list": {
@@ -1,7 +1,7 @@
1
1
  sonar.projectKey=rx-player
2
2
  sonar.organization=rx-player
3
3
  sonar.projectName=rx-player
4
- sonar.projectVersion=3.28.0-dev.2022063000
4
+ sonar.projectVersion=3.28.1-dev.2022083000
5
5
  sonar.sources=./src,./demo,./tests
6
6
  sonar.exclusions=demo/full/bundle.js,demo/standalone/lib.js,demo/bundle.js
7
7
  sonar.host.url=https://sonarcloud.io
@@ -23,15 +23,14 @@ const EME_WAITING_DELAY_LOADED_SESSION_EMPTY_KEYSTATUSES = 100;
23
23
  * Load a persistent session, based on its `sessionId`, on the given
24
24
  * MediaKeySession.
25
25
  *
26
- * Returns an Observable which emits:
27
- * - true if the persistent MediaKeySession was found and loaded
28
- * - false if no persistent MediaKeySession was found with that `sessionId`.
29
- * Then completes.
26
+ * Returns a Promise which resolves with:
27
+ * - `true` if the persistent MediaKeySession was found and loaded
28
+ * - `false` if no persistent MediaKeySession was found with that `sessionId`.
30
29
  *
31
- * The Observable throws if anything goes wrong in the process.
30
+ * The Promise rejects if anything goes wrong in the process.
32
31
  * @param {MediaKeySession} session
33
32
  * @param {string} sessionId
34
- * @returns {Observable}
33
+ * @returns {Promise.<boolean>}
35
34
  */
36
35
  export default async function loadSession(
37
36
  session : MediaKeySession | ICustomMediaKeySession,
@@ -205,11 +205,13 @@ function getPageActivityRef(
205
205
  const ref = createSharedReference(true);
206
206
  stopListening.register(() => {
207
207
  clearTimeout(currentTimeout);
208
+ currentTimeout = undefined;
208
209
  ref.finish();
209
210
  });
210
211
 
211
212
  isDocVisibleRef.onUpdate(function onDocVisibilityChange(isVisible : boolean) : void {
212
213
  clearTimeout(currentTimeout); // clear potential previous timeout
214
+ currentTimeout = undefined;
213
215
  if (!isVisible) {
214
216
  const { INACTIVITY_DELAY } = config.getCurrent();
215
217
  currentTimeout = window.setTimeout(() => {
@@ -313,6 +315,7 @@ function getVideoVisibilityRef(
313
315
  const ref = createSharedReference(true);
314
316
  stopListening.register(() => {
315
317
  clearTimeout(currentTimeout);
318
+ currentTimeout = undefined;
316
319
  ref.finish();
317
320
  });
318
321
 
@@ -324,6 +327,8 @@ function getVideoVisibilityRef(
324
327
  return ref;
325
328
 
326
329
  function checkCurrentVisibility() : void {
330
+ clearTimeout(currentTimeout);
331
+ currentTimeout = undefined;
327
332
  if (pipStatus.getValue().isEnabled || isDocVisibleRef.getValue()) {
328
333
  ref.setValueIfChanged(true);
329
334
  } else {
@@ -102,7 +102,7 @@ function createAudioTracks(
102
102
  const track = { language: audioTrack.language,
103
103
  id,
104
104
  normalized: normalizeLanguage(audioTrack.language),
105
- audioDescription: false,
105
+ audioDescription: audioTrack.kind === "descriptions",
106
106
  representations: [] as Representation[] };
107
107
  newAudioTracks.push({ track,
108
108
  nativeTrack: audioTrack });
@@ -432,7 +432,7 @@ class Player extends EventEmitter<IPublicAPIEvent> {
432
432
  // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1194624
433
433
  videoElement.preload = "auto";
434
434
 
435
- this.version = /* PLAYER_VERSION */"3.28.0-dev.2022063000";
435
+ this.version = /* PLAYER_VERSION */"3.28.1-dev.2022083000";
436
436
  this.log = log;
437
437
  this.state = "STOPPED";
438
438
  this.videoElement = videoElement;
@@ -2943,7 +2943,7 @@ class Player extends EventEmitter<IPublicAPIEvent> {
2943
2943
  return activeRepresentations[currentPeriod.id];
2944
2944
  }
2945
2945
  }
2946
- Player.version = /* PLAYER_VERSION */"3.28.0-dev.2022063000";
2946
+ Player.version = /* PLAYER_VERSION */"3.28.1-dev.2022083000";
2947
2947
 
2948
2948
  /** Every events sent by the RxPlayer's public API. */
2949
2949
  interface IPublicAPIEvent {
@@ -117,8 +117,18 @@ async function createAndTryToRetrievePersistentSession(
117
117
  if (!hasLoadedSession) {
118
118
  log.warn("DRM: No data stored for the loaded session");
119
119
  persistentSessionsStore.delete(storedEntry.sessionId);
120
+
121
+ // The EME specification is kind of implicit about it but it seems from my
122
+ // understanding (Paul B.) that a MediaKeySession on wich a `load` attempt
123
+ // did not succeed due to the loaded session not being found by the
124
+ // browser/CDM, should neither be used anymore nor closed.
125
+ // Thus, we're creating another `"persistent-license"` `MediaKeySession`
126
+ // in that specific case.
127
+ loadedSessionsStore.removeSessionWithoutClosingIt(entry.mediaKeySession);
128
+ const newEntry = loadedSessionsStore.createSession(initData,
129
+ "persistent-license");
120
130
  return { type: MediaKeySessionLoadingType.Created,
121
- value: entry };
131
+ value: newEntry };
122
132
  }
123
133
 
124
134
  if (hasLoadedSession && isSessionUsable(entry.mediaKeySession)) {
@@ -153,7 +163,23 @@ async function createAndTryToRetrievePersistentSession(
153
163
  persistentSessionsStore.delete(persistentEntry.sessionId);
154
164
  }
155
165
 
156
- await loadedSessionsStore.closeSession(entry.mediaKeySession);
166
+ try {
167
+ await loadedSessionsStore.closeSession(entry.mediaKeySession);
168
+ } catch (err) {
169
+ // From reading the EME specification in details, it seems that a
170
+ // `MediaKeySession`'s ability to be closed is tightly linked to its
171
+ // possession of a "sanitized session ID" set as `sessionId`.
172
+ // This is never clearly stated however and I'm (Paul B.) always afraid of
173
+ // breaking compatibility when it comes to EME code.
174
+ // So we still try to close the `MediaKeySession` in any case, only, if it
175
+ // fails and it didn't had any `sessionId` set, we just ignore the error.
176
+ // Note that trying to close the `MediaKeySession` might incur some delays
177
+ // in those rare cases.
178
+ if (entry.mediaKeySession.sessionId !== "") {
179
+ throw err;
180
+ }
181
+ loadedSessionsStore.removeSessionWithoutClosingIt(entry.mediaKeySession);
182
+ }
157
183
  if (cancelSignal.cancellationError !== null) {
158
184
  throw cancelSignal.cancellationError;
159
185
  }
@@ -22,6 +22,7 @@ import {
22
22
  loadSession,
23
23
  } from "../../../compat";
24
24
  import log from "../../../log";
25
+ import assert from "../../../utils/assert";
25
26
  import isNullOrUndefined from "../../../utils/is_null_or_undefined";
26
27
  import { IProcessedProtectionData } from "../types";
27
28
  import KeySessionRecord from "./key_session_record";
@@ -310,6 +311,34 @@ export default class LoadedSessionsStore {
310
311
  await Promise.all(closingProms);
311
312
  }
312
313
 
314
+ /**
315
+ * Find the given `MediaKeySession` in the `LoadedSessionsStore` and removes
316
+ * any reference to it without actually closing it.
317
+ *
318
+ * Returns `true` if the given `mediaKeySession` has been found and removed,
319
+ * `false` otherwise.
320
+ *
321
+ * Note that this may create a `MediaKeySession` leakage in the wrong
322
+ * conditions, cases where this method should be called should be very
323
+ * carefully evaluated.
324
+ * @param {MediaKeySession} mediaKeySession
325
+ * @returns {boolean}
326
+ */
327
+ public removeSessionWithoutClosingIt(
328
+ mediaKeySession : MediaKeySession | ICustomMediaKeySession
329
+ ) : boolean {
330
+ assert(mediaKeySession.sessionId === "",
331
+ "Initialized `MediaKeySession`s should always be properly closed");
332
+ for (let i = this._storage.length - 1; i >= 0; i--) {
333
+ const stored = this._storage[i];
334
+ if (stored.mediaKeySession === mediaKeySession) {
335
+ this._storage.splice(i, 1);
336
+ return true;
337
+ }
338
+ }
339
+ return false;
340
+ }
341
+
313
342
  /**
314
343
  * Get the index of a stored MediaKeySession entry based on its
315
344
  * `KeySessionRecord`.
@@ -61,7 +61,7 @@ const generateRequestID = idGenerator();
61
61
  * `options` argument, which may retry a segment request when it fails.
62
62
  *
63
63
  * @param {string} bufferType
64
- * @param {Object} transport
64
+ * @param {Object} pipeline
65
65
  * @param {Object} callbacks
66
66
  * @param {Object} options
67
67
  * @returns {Function}
@@ -31,6 +31,7 @@ import Manifest, {
31
31
  } from "../../manifest";
32
32
  import { fromEvent } from "../../utils/event_emitter";
33
33
  import filterMap from "../../utils/filter_map";
34
+ import isNullOrUndefined from "../../utils/is_null_or_undefined";
34
35
  import createSharedReference from "../../utils/reference";
35
36
  import { IReadOnlyPlaybackObserver } from "../api";
36
37
  import {
@@ -86,7 +87,7 @@ export default function ContentTimeBoundariesObserver(
86
87
  "earliest time announced in the Manifest.");
87
88
  return EVENTS.warning(warning);
88
89
  } else if (
89
- wantedPosition > maximumPositionCalculator.getCurrentMaximumPosition()
90
+ wantedPosition > maximumPositionCalculator.getMaximumAvailablePosition()
90
91
  ) {
91
92
  const warning = new MediaError("MEDIA_TIME_AFTER_MANIFEST",
92
93
  "The current position is after the latest " +
@@ -106,13 +107,10 @@ export default function ContentTimeBoundariesObserver(
106
107
  const updateDurationOnManifestUpdate$ = fromEvent(manifest, "manifestUpdate").pipe(
107
108
  startWith(null),
108
109
  tap(() => {
109
- if (!manifest.isDynamic) {
110
- const maxPos = maximumPositionCalculator.getCurrentMaximumPosition();
111
- contentDuration.setValue(maxPos);
112
- } else {
113
- // TODO handle finished dynamic contents?
114
- contentDuration.setValue(undefined);
115
- }
110
+ const duration = manifest.isDynamic ?
111
+ maximumPositionCalculator.getEndingPosition() :
112
+ maximumPositionCalculator.getMaximumAvailablePosition();
113
+ contentDuration.setValue(duration);
116
114
  }),
117
115
  ignoreElements()
118
116
  );
@@ -120,24 +118,23 @@ export default function ContentTimeBoundariesObserver(
120
118
  const updateDurationAndTimeBoundsOnTrackChange$ = streams.pipe(
121
119
  tap((message) => { // Update Manifest's bounds and duration if necessary
122
120
  if (message.type === "adaptationChange") {
121
+ if (!manifest.isLastPeriodKnown) {
122
+ return;
123
+ }
123
124
  const lastPeriod = manifest.periods[manifest.periods.length - 1];
124
125
  if (message.value.period.id === lastPeriod?.id) {
125
- if (message.value.type === "audio") {
126
- maximumPositionCalculator
127
- .updateLastAudioAdaptation(message.value.adaptation);
128
- if (!manifest.isDynamic) {
129
- contentDuration.setValue(
130
- maximumPositionCalculator.getCurrentMaximumPosition()
131
- );
132
- }
133
- } else if (message.value.type === "video") {
134
- maximumPositionCalculator
135
- .updateLastVideoAdaptation(message.value.adaptation);
136
- if (!manifest.isDynamic) {
137
- contentDuration.setValue(
138
- maximumPositionCalculator.getCurrentMaximumPosition()
139
- );
126
+ if (message.value.type === "audio" || message.value.type === "video") {
127
+ if (message.value.type === "audio") {
128
+ maximumPositionCalculator
129
+ .updateLastAudioAdaptation(message.value.adaptation);
130
+ } else {
131
+ maximumPositionCalculator
132
+ .updateLastVideoAdaptation(message.value.adaptation);
140
133
  }
134
+ const newDuration = manifest.isDynamic ?
135
+ maximumPositionCalculator.getMaximumAvailablePosition() :
136
+ maximumPositionCalculator.getEndingPosition();
137
+ contentDuration.setValue(newDuration);
141
138
  }
142
139
  }
143
140
  }
@@ -182,7 +179,7 @@ class MaximumPositionCalculator {
182
179
  * If no Adaptation has been set, it should be set to `null`.
183
180
  *
184
181
  * Allows to calculate the maximum position more precizely in
185
- * `getCurrentMaximumPosition`.
182
+ * `getMaximumAvailablePosition` and `getEndingPosition`.
186
183
  * @param {Object|null} adaptation
187
184
  */
188
185
  public updateLastAudioAdaptation(adaptation : Adaptation | null) : void {
@@ -194,7 +191,7 @@ class MaximumPositionCalculator {
194
191
  * If no Adaptation has been set, it should be set to `null`.
195
192
  *
196
193
  * Allows to calculate the maximum position more precizely in
197
- * `getCurrentMaximumPosition`.
194
+ * `getMaximumAvailablePosition` and `getEndingPosition`.
198
195
  * @param {Object|null} adaptation
199
196
  */
200
197
  public updateLastVideoAdaptation(adaptation : Adaptation | null) : void {
@@ -202,11 +199,11 @@ class MaximumPositionCalculator {
202
199
  }
203
200
 
204
201
  /**
205
- * Returns an estimate of the maximum position reachable under the current
206
- * circumstances.
202
+ * Returns an estimate of the maximum position currently reachable (i.e.
203
+ * segments are available) under the current circumstances.
207
204
  * @returns {number}
208
205
  */
209
- public getCurrentMaximumPosition() : number {
206
+ public getMaximumAvailablePosition() : number {
210
207
  if (this._manifest.isDynamic) {
211
208
  return this._manifest.getLivePosition() ??
212
209
  this._manifest.getMaximumSafePosition();
@@ -220,7 +217,7 @@ class MaximumPositionCalculator {
220
217
  return this._manifest.getMaximumSafePosition();
221
218
  } else {
222
219
  const lastVideoPosition =
223
- getLastPositionFromAdaptation(this._lastVideoAdaptation);
220
+ getLastAvailablePositionFromAdaptation(this._lastVideoAdaptation);
224
221
  if (typeof lastVideoPosition !== "number") {
225
222
  return this._manifest.getMaximumSafePosition();
226
223
  }
@@ -228,16 +225,16 @@ class MaximumPositionCalculator {
228
225
  }
229
226
  } else if (this._lastVideoAdaptation === null) {
230
227
  const lastAudioPosition =
231
- getLastPositionFromAdaptation(this._lastAudioAdaptation);
228
+ getLastAvailablePositionFromAdaptation(this._lastAudioAdaptation);
232
229
  if (typeof lastAudioPosition !== "number") {
233
230
  return this._manifest.getMaximumSafePosition();
234
231
  }
235
232
  return lastAudioPosition;
236
233
  } else {
237
- const lastAudioPosition = getLastPositionFromAdaptation(
234
+ const lastAudioPosition = getLastAvailablePositionFromAdaptation(
238
235
  this._lastAudioAdaptation
239
236
  );
240
- const lastVideoPosition = getLastPositionFromAdaptation(
237
+ const lastVideoPosition = getLastAvailablePositionFromAdaptation(
241
238
  this._lastVideoAdaptation
242
239
  );
243
240
  if (typeof lastAudioPosition !== "number" ||
@@ -249,20 +246,59 @@ class MaximumPositionCalculator {
249
246
  }
250
247
  }
251
248
  }
249
+
250
+ /**
251
+ * Returns an estimate of the actual ending position once
252
+ * the full content is available.
253
+ * Returns `undefined` if that could not be determined, for various reasons.
254
+ * @returns {number|undefined}
255
+ */
256
+ public getEndingPosition() : number | undefined {
257
+ if (!this._manifest.isDynamic) {
258
+ return this.getMaximumAvailablePosition();
259
+ }
260
+ if (this._lastVideoAdaptation === undefined ||
261
+ this._lastAudioAdaptation === undefined)
262
+ {
263
+ return undefined;
264
+ } else if (this._lastAudioAdaptation === null) {
265
+ if (this._lastVideoAdaptation === null) {
266
+ return undefined;
267
+ } else {
268
+ return getEndingPositionFromAdaptation(this._lastVideoAdaptation) ??
269
+ undefined;
270
+ }
271
+ } else if (this._lastVideoAdaptation === null) {
272
+ return getEndingPositionFromAdaptation(this._lastAudioAdaptation) ??
273
+ undefined;
274
+ } else {
275
+ const lastAudioPosition =
276
+ getEndingPositionFromAdaptation(this._lastAudioAdaptation);
277
+ const lastVideoPosition =
278
+ getEndingPositionFromAdaptation(this._lastVideoAdaptation);
279
+ if (typeof lastAudioPosition !== "number" ||
280
+ typeof lastVideoPosition !== "number")
281
+ {
282
+ return undefined;
283
+ } else {
284
+ return Math.min(lastAudioPosition, lastVideoPosition);
285
+ }
286
+ }
287
+ }
252
288
  }
253
289
 
254
290
  /**
255
- * Returns "last time of reference" from the adaptation given.
291
+ * Returns last currently available position from the Adaptation given.
256
292
  * `undefined` if a time could not be found.
257
- * Null if the Adaptation has no segments (it could be that it didn't started or
293
+ * `null` if the Adaptation has no segments (it could be that it didn't started or
258
294
  * that it already finished for example).
259
295
  *
260
- * We consider the earliest last time from every representations in the given
261
- * adaptation.
296
+ * We consider the earliest last available position from every Representation
297
+ * in the given Adaptation.
262
298
  * @param {Object} adaptation
263
299
  * @returns {Number|undefined|null}
264
300
  */
265
- function getLastPositionFromAdaptation(
301
+ function getLastAvailablePositionFromAdaptation(
266
302
  adaptation: Adaptation
267
303
  ) : number | undefined | null {
268
304
  const { representations } = adaptation;
@@ -278,18 +314,56 @@ function getLastPositionFromAdaptation(
278
314
  for (let i = 0; i < representations.length; i++) {
279
315
  if (representations[i].index !== lastIndex) {
280
316
  lastIndex = representations[i].index;
281
- const lastPosition = representations[i].index.getLastPosition();
317
+ const lastPosition = representations[i].index.getLastAvailablePosition();
282
318
  if (lastPosition === undefined) { // we cannot tell
283
319
  return undefined;
284
320
  }
285
321
  if (lastPosition !== null) {
286
- min = min == null ? lastPosition :
287
- Math.min(min, lastPosition);
322
+ min = isNullOrUndefined(min) ? lastPosition :
323
+ Math.min(min, lastPosition);
288
324
  }
289
325
  }
290
326
  }
291
- if (min === null) { // It means that all positions were null === no segments (yet?)
292
- return null;
327
+ return min;
328
+ }
329
+
330
+ /**
331
+ * Returns ending time from the Adaptation given, once all its segments are
332
+ * available.
333
+ * `undefined` if a time could not be found.
334
+ * `null` if the Adaptation has no segments (it could be that it already
335
+ * finished for example).
336
+ *
337
+ * We consider the earliest ending time from every Representation in the given
338
+ * Adaptation.
339
+ * @param {Object} adaptation
340
+ * @returns {Number|undefined|null}
341
+ */
342
+ function getEndingPositionFromAdaptation(
343
+ adaptation: Adaptation
344
+ ) : number | undefined | null {
345
+ const { representations } = adaptation;
346
+ let min : null | number = null;
347
+
348
+ /**
349
+ * Some Manifest parsers use the exact same `IRepresentationIndex` reference
350
+ * for each Representation of a given Adaptation, because in the actual source
351
+ * Manifest file, indexing data is often defined at Adaptation-level.
352
+ * This variable allows to optimize the logic here when this is the case.
353
+ */
354
+ let lastIndex : IRepresentationIndex | undefined;
355
+ for (let i = 0; i < representations.length; i++) {
356
+ if (representations[i].index !== lastIndex) {
357
+ lastIndex = representations[i].index;
358
+ const lastPosition = representations[i].index.getEnd();
359
+ if (lastPosition === undefined) { // we cannot tell
360
+ return undefined;
361
+ }
362
+ if (lastPosition !== null) {
363
+ min = isNullOrUndefined(min) ? lastPosition :
364
+ Math.min(min, lastPosition);
365
+ }
366
+ }
293
367
  }
294
368
  return min;
295
369
  }
@@ -102,10 +102,17 @@ export default function checkForDiscontinuity(
102
102
  (nextSegmentStart === null ||
103
103
  nextBufferedSegment.infos.segment.end <= nextSegmentStart)
104
104
  ) {
105
+ const discontinuityEnd = nextBufferedSegment.bufferedStart;
106
+ if (!hasFinishedLoading &&
107
+ representation.index.awaitSegmentBetween(checkedRange.start,
108
+ discontinuityEnd) !== false)
109
+ {
110
+ return null;
111
+ }
105
112
  log.debug("RS: current discontinuity encountered",
106
113
  adaptation.type, nextBufferedSegment.bufferedStart);
107
114
  return { start: undefined,
108
- end: nextBufferedSegment.bufferedStart };
115
+ end: discontinuityEnd };
109
116
  }
110
117
 
111
118
  // Check if there's a discontinuity BETWEEN segments of the current range
@@ -116,16 +123,27 @@ export default function checkForDiscontinuity(
116
123
 
117
124
  // If there was a hole between two consecutives segments, and if this hole
118
125
  // comes before the next segment to load, there is a discontinuity (that hole!)
119
- if (nextHoleIdx !== null &&
120
- (nextSegmentStart === null ||
121
- bufferedSegments[nextHoleIdx].infos.segment.end <= nextSegmentStart))
122
- {
123
- const start = bufferedSegments[nextHoleIdx - 1].bufferedEnd as number;
124
- const end = bufferedSegments[nextHoleIdx].bufferedStart as number;
125
- log.debug("RS: future discontinuity encountered", adaptation.type, start, end);
126
- return { start, end };
126
+ if (nextHoleIdx !== null) {
127
+ const segmentInfoBeforeHole = bufferedSegments[nextHoleIdx - 1];
128
+ const segmentInfoAfterHole = bufferedSegments[nextHoleIdx];
129
+
130
+ if (nextSegmentStart === null ||
131
+ segmentInfoAfterHole.infos.segment.end <= nextSegmentStart)
132
+ {
133
+ if (!hasFinishedLoading && representation.index
134
+ .awaitSegmentBetween(segmentInfoBeforeHole.infos.segment.end,
135
+ segmentInfoAfterHole.infos.segment.time) !== false)
136
+ {
137
+ return null;
138
+ }
139
+ const start = segmentInfoBeforeHole.bufferedEnd as number;
140
+ const end = segmentInfoAfterHole.bufferedStart as number;
141
+ log.debug("RS: future discontinuity encountered", adaptation.type, start, end);
142
+ return { start, end };
143
+ }
144
+ }
127
145
 
128
- } else if (nextSegmentStart === null) {
146
+ if (nextSegmentStart === null) {
129
147
  // If no hole between segments and no segment to load, check for a
130
148
  // discontinuity at the end of the Period
131
149