rx-player 3.30.1-dev.2023032800 → 3.31.0-dev.2023052200

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 (96) hide show
  1. package/CHANGELOG.md +14 -2
  2. package/CONTRIBUTING.md +48 -168
  3. package/FILES.md +40 -92
  4. package/VERSION +1 -1
  5. package/dist/_esm5.processed/compat/browser_detection.d.ts +3 -1
  6. package/dist/_esm5.processed/compat/browser_detection.js +7 -2
  7. package/dist/_esm5.processed/compat/has_issues_with_high_media_source_duration.d.ts +21 -0
  8. package/dist/_esm5.processed/compat/has_issues_with_high_media_source_duration.js +26 -0
  9. package/dist/_esm5.processed/core/adaptive/adaptive_representation_selector.js +1 -2
  10. package/dist/_esm5.processed/core/adaptive/buffer_based_chooser.d.ts +18 -1
  11. package/dist/_esm5.processed/core/adaptive/buffer_based_chooser.js +105 -24
  12. package/dist/_esm5.processed/core/adaptive/guess_based_chooser.js +6 -6
  13. package/dist/_esm5.processed/core/adaptive/utils/representation_score_calculator.d.ts +19 -1
  14. package/dist/_esm5.processed/core/adaptive/utils/representation_score_calculator.js +1 -1
  15. package/dist/_esm5.processed/core/api/debug/render.js +1 -1
  16. package/dist/_esm5.processed/core/api/public_api.js +2 -2
  17. package/dist/_esm5.processed/core/api/tracks_management/media_element_track_choice_manager.js +10 -1
  18. package/dist/_esm5.processed/core/api/tracks_management/track_choice_manager.js +20 -117
  19. package/dist/_esm5.processed/core/init/media_source_content_initializer.js +28 -3
  20. package/dist/_esm5.processed/core/init/utils/media_source_duration_updater.d.ts +2 -2
  21. package/dist/_esm5.processed/core/init/utils/media_source_duration_updater.js +29 -18
  22. package/dist/_esm5.processed/core/segment_buffers/index.d.ts +2 -2
  23. package/dist/_esm5.processed/core/stream/adaptation/utils/create_representation_estimator.js +1 -1
  24. package/dist/_esm5.processed/core/stream/period/period_stream.js +1 -1
  25. package/dist/_esm5.processed/core/stream/representation/utils/append_segment_to_buffer.d.ts +4 -2
  26. package/dist/_esm5.processed/core/stream/representation/utils/append_segment_to_buffer.js +2 -2
  27. package/dist/_esm5.processed/core/stream/representation/utils/push_init_segment.js +6 -2
  28. package/dist/_esm5.processed/errors/index.d.ts +2 -2
  29. package/dist/_esm5.processed/errors/media_error.d.ts +23 -1
  30. package/dist/_esm5.processed/errors/media_error.js +18 -5
  31. package/dist/_esm5.processed/manifest/adaptation.d.ts +17 -2
  32. package/dist/_esm5.processed/manifest/adaptation.js +67 -1
  33. package/dist/_esm5.processed/manifest/manifest.d.ts +1 -1
  34. package/dist/_esm5.processed/manifest/manifest.js +3 -3
  35. package/dist/_esm5.processed/manifest/period.js +1 -1
  36. package/dist/_esm5.processed/manifest/representation.d.ts +11 -1
  37. package/dist/_esm5.processed/manifest/representation.js +16 -0
  38. package/dist/_esm5.processed/parsers/manifest/dash/js-parser/parse_from_document.d.ts +1 -1
  39. package/dist/_esm5.processed/parsers/manifest/dash/js-parser/parse_from_document.js +1 -1
  40. package/dist/_esm5.processed/parsers/manifest/dash/wasm-parser/ts/dash-wasm-parser.js +1 -0
  41. package/dist/_esm5.processed/public_types.d.ts +6 -5
  42. package/dist/_esm5.processed/transports/smooth/isobmff/create_boxes.d.ts +4 -6
  43. package/dist/_esm5.processed/transports/smooth/isobmff/create_boxes.js +4 -6
  44. package/dist/rx-player.js +3086 -3024
  45. package/dist/rx-player.min.js +1 -1
  46. package/package.json +25 -26
  47. package/sonar-project.properties +1 -1
  48. package/src/compat/browser_detection.ts +7 -1
  49. package/src/compat/has_issues_with_high_media_source_duration.ts +27 -0
  50. package/src/core/adaptive/__tests__/buffer_based_chooser.test.ts +147 -48
  51. package/src/core/adaptive/adaptive_representation_selector.ts +1 -2
  52. package/src/core/adaptive/buffer_based_chooser.ts +143 -25
  53. package/src/core/adaptive/guess_based_chooser.ts +9 -8
  54. package/src/core/adaptive/utils/representation_score_calculator.ts +22 -2
  55. package/src/core/api/debug/render.ts +1 -1
  56. package/src/core/api/public_api.ts +2 -2
  57. package/src/core/api/tracks_management/media_element_track_choice_manager.ts +17 -8
  58. package/src/core/api/tracks_management/track_choice_manager.ts +20 -132
  59. package/src/core/init/media_source_content_initializer.ts +32 -4
  60. package/src/core/init/utils/media_source_duration_updater.ts +31 -19
  61. package/src/core/segment_buffers/index.ts +2 -0
  62. package/src/core/stream/adaptation/utils/create_representation_estimator.ts +2 -1
  63. package/src/core/stream/period/period_stream.ts +2 -1
  64. package/src/core/stream/representation/utils/append_segment_to_buffer.ts +8 -3
  65. package/src/core/stream/representation/utils/push_init_segment.ts +7 -1
  66. package/src/errors/__tests__/media_error.test.ts +6 -6
  67. package/src/errors/index.ts +4 -1
  68. package/src/errors/media_error.ts +67 -1
  69. package/src/manifest/__tests__/period.test.ts +90 -45
  70. package/src/manifest/adaptation.ts +77 -2
  71. package/src/manifest/manifest.ts +5 -3
  72. package/src/manifest/period.ts +2 -1
  73. package/src/manifest/representation.ts +23 -1
  74. package/src/parsers/manifest/dash/js-parser/parse_from_document.ts +1 -1
  75. package/src/parsers/manifest/dash/wasm-parser/ts/dash-wasm-parser.ts +1 -0
  76. package/src/parsers/texttracks/ttml/parse_ttml.ts +1 -1
  77. package/src/public_types.ts +8 -3
  78. package/src/transports/smooth/isobmff/create_boxes.ts +4 -6
  79. package/src/typings/globals.d.ts +21 -21
  80. package/scripts/doc-generator/construct_table_of_contents.js +0 -76
  81. package/scripts/doc-generator/convert_MD_to_HMTL.js +0 -26
  82. package/scripts/doc-generator/create_documentation.js +0 -331
  83. package/scripts/doc-generator/create_documentation_page.js +0 -209
  84. package/scripts/doc-generator/create_page.js +0 -210
  85. package/scripts/doc-generator/generate_header_html.js +0 -147
  86. package/scripts/doc-generator/generate_page_html.js +0 -115
  87. package/scripts/doc-generator/generate_page_list_html.js +0 -92
  88. package/scripts/doc-generator/generate_sidebar_html.js +0 -85
  89. package/scripts/doc-generator/get_search_data_for_content.js +0 -137
  90. package/scripts/doc-generator/index.js +0 -34
  91. package/scripts/doc-generator/parse_doc_configs.js +0 -327
  92. package/scripts/doc-generator/scripts/lunr.js +0 -10
  93. package/scripts/doc-generator/scripts/script.js +0 -451
  94. package/scripts/doc-generator/styles/code.css +0 -99
  95. package/scripts/doc-generator/styles/style.css +0 -835
  96. package/scripts/doc-generator/utils.js +0 -74
@@ -17,6 +17,54 @@
17
17
  import log from "../../log";
18
18
  import arrayFindIndex from "../../utils/array_find_index";
19
19
  import getBufferLevels from "./utils/get_buffer_levels";
20
+ import {
21
+ IRepresentationMaintainabilityScore,
22
+ ScoreConfidenceLevel,
23
+ } from "./utils/representation_score_calculator";
24
+
25
+ /**
26
+ * Minimum amount of time, in milliseconds, during which we are blocked from
27
+ * raising in quality after it had been considered as too high.
28
+ */
29
+ const MINIMUM_BLOCK_RAISE_DELAY = 6000;
30
+
31
+ /**
32
+ * Maximum amount of time, in milliseconds, during which we are blocked from
33
+ * raising in quality after it had been considered as too high.
34
+ */
35
+ const MAXIMUM_BLOCK_RAISE_DELAY = 15000;
36
+
37
+ /**
38
+ * Amount of time, in milliseconds, with which the blocking time in raising
39
+ * the quality will be incremented if the current quality estimate is seen
40
+ * as too unstable.
41
+ */
42
+ const RAISE_BLOCKING_DELAY_INCREMENT = 3000;
43
+
44
+ /**
45
+ * Amount of time, in milliseconds, with which the blocking time in raising
46
+ * the quality will be dcremented if the current quality estimate is seen
47
+ * as relatively stable, until `MINIMUM_BLOCK_RAISE_DELAY` is reached.
48
+ */
49
+ const RAISE_BLOCKING_DELAY_DECREMENT = 1000;
50
+
51
+ /**
52
+ * Amount of time, in milliseconds, after the "raise blocking delay" currently
53
+ * in place (during which it is forbidden to raise up in quality), during which
54
+ * we might want to raise the "raise blocking delay" if the last chosen quality
55
+ * seems unsuitable.
56
+ *
57
+ * For example, let's consider that the current raise blocking delay is at
58
+ * `4000`, or 4 seconds, and that this `STABILITY_CHECK_DELAY` is at `5000`, or
59
+ * 5 seconds.
60
+ * Here it means that if the estimated quality is found to be unsuitable less
61
+ * than 4+5 = 9 seconds after it last was, we will increment the raise blocking
62
+ * delay by `RAISE_BLOCKING_DELAY_INCREMENT` (unless `MAXIMUM_BLOCK_RAISE_DELAY`
63
+ * is reached).
64
+ * Else, if takes more than 9 seconds, the raise blocking delay might be
65
+ * decremented.
66
+ */
67
+ const STABILITY_CHECK_DELAY = 9000;
20
68
 
21
69
  /**
22
70
  * Choose a bitrate based on the currently available buffer.
@@ -29,18 +77,41 @@ import getBufferLevels from "./utils/get_buffer_levels";
29
77
  * "maintanable" or not.
30
78
  * If so, we may switch to a better quality, or conversely to a worse quality.
31
79
  *
80
+ * It also rely on mechanisms to avoid fluctuating too much between qualities.
81
+ *
32
82
  * @class BufferBasedChooser
33
83
  */
34
84
  export default class BufferBasedChooser {
35
85
  private _levelsMap : number[];
36
86
  private _bitrates : number[];
37
87
 
88
+ /**
89
+ * Laast timestamp, in terms of `performance.now`, at which the current
90
+ * quality was seen as too high by this algorithm.
91
+ * Begins at `undefined`.
92
+ */
93
+ private _lastUnsuitableQualityTimestamp: number | undefined;
94
+
95
+ /**
96
+ * After lowering in quality, we forbid raising during a set amount of time.
97
+ * This amount is adaptive may continue to raise if it seems that quality
98
+ * is switching too much between low and high qualities.
99
+ *
100
+ * `_blockRaiseDelay` represents this time in milliseconds.
101
+ */
102
+ private _blockRaiseDelay: number;
103
+
38
104
  /**
39
105
  * @param {Array.<number>} bitrates
40
106
  */
41
107
  constructor(bitrates : number[]) {
42
- this._levelsMap = getBufferLevels(bitrates);
108
+ this._levelsMap = getBufferLevels(bitrates).map(bl => {
109
+ return bl + 4; // Add some buffer security as it will be used conjointly with
110
+ // other algorithms anyway
111
+ });
43
112
  this._bitrates = bitrates;
113
+ this._lastUnsuitableQualityTimestamp = undefined;
114
+ this._blockRaiseDelay = MINIMUM_BLOCK_RAISE_DELAY;
44
115
  log.debug("ABR: Steps for buffer based chooser.",
45
116
  this._levelsMap.map((l, i) => `bufferLevel: ${l}, bitrate: ${bitrates[i]}`)
46
117
  .join(" ,"));
@@ -59,7 +130,18 @@ export default class BufferBasedChooser {
59
130
  if (currentBitrate == null) {
60
131
  return bitrates[0];
61
132
  }
62
- const currentBitrateIndex = arrayFindIndex(bitrates, b => b === currentBitrate);
133
+
134
+ let currentBitrateIndex = -1;
135
+ for (let i = 0; i < bitrates.length; i++) {
136
+ // There could be bitrate duplicates. Only take the last one to simplify
137
+ const bitrate = bitrates[i];
138
+ if (bitrate === currentBitrate) {
139
+ currentBitrateIndex = i;
140
+ } else if (bitrate > currentBitrate) {
141
+ break;
142
+ }
143
+ }
144
+
63
145
  if (currentBitrateIndex < 0 || bitrates.length !== bufferLevels.length) {
64
146
  log.error("ABR: Current Bitrate not found in the calculated levels");
65
147
  return bitrates[0];
@@ -67,35 +149,71 @@ export default class BufferBasedChooser {
67
149
 
68
150
  let scaledScore : number|undefined;
69
151
  if (currentScore !== undefined) {
70
- scaledScore = speed === 0 ? currentScore : (currentScore / speed);
152
+ scaledScore = speed === 0 ? currentScore.score : (currentScore.score / speed);
71
153
  }
72
154
 
73
- if (scaledScore !== undefined && scaledScore > 1) {
74
- const currentBufferLevel = bufferLevels[currentBitrateIndex];
75
- const nextIndex = (() => {
76
- for (let i = currentBitrateIndex + 1; i < bufferLevels.length; i++) {
77
- if (bufferLevels[i] > currentBufferLevel) {
78
- return i;
79
- }
80
- }
81
- })();
82
- if (nextIndex !== undefined) {
83
- const nextBufferLevel = bufferLevels[nextIndex];
84
- if (bufferGap >= nextBufferLevel) {
85
- return bitrates[nextIndex];
155
+ const actualBufferGap = isFinite(bufferGap) ?
156
+ bufferGap :
157
+ 0;
158
+
159
+ const now = performance.now();
160
+
161
+ if (
162
+ actualBufferGap < bufferLevels[currentBitrateIndex] ||
163
+ (
164
+ scaledScore !== undefined && scaledScore < 1 &&
165
+ currentScore?.confidenceLevel === ScoreConfidenceLevel.HIGH
166
+ )
167
+ ) {
168
+ const timeSincePrev = this._lastUnsuitableQualityTimestamp === undefined ?
169
+ -1 :
170
+ now - this._lastUnsuitableQualityTimestamp;
171
+ if (timeSincePrev < this._blockRaiseDelay + STABILITY_CHECK_DELAY) {
172
+ const newDelay = this._blockRaiseDelay + RAISE_BLOCKING_DELAY_INCREMENT;
173
+ this._blockRaiseDelay = Math.min(newDelay, MAXIMUM_BLOCK_RAISE_DELAY);
174
+ log.debug("ABR: Incrementing blocking raise in BufferBasedChooser due " +
175
+ "to unstable quality",
176
+ this._blockRaiseDelay);
177
+ } else {
178
+ const newDelay = this._blockRaiseDelay - RAISE_BLOCKING_DELAY_DECREMENT;
179
+ this._blockRaiseDelay = Math.max(MINIMUM_BLOCK_RAISE_DELAY, newDelay);
180
+ log.debug("ABR: Lowering quality in BufferBasedChooser", this._blockRaiseDelay);
181
+ }
182
+ this._lastUnsuitableQualityTimestamp = now;
183
+ // Security if multiple bitrates are equal, we now take the first one
184
+ const baseIndex = arrayFindIndex(bitrates, (b) => b === currentBitrate);
185
+ for (let i = baseIndex - 1; i >= 0; i--) {
186
+ if (actualBufferGap >= bufferLevels[i]) {
187
+ return bitrates[i];
86
188
  }
87
189
  }
190
+ return bitrates[0];
191
+ }
192
+
193
+ if (
194
+ (
195
+ this._lastUnsuitableQualityTimestamp !== undefined &&
196
+ now - this._lastUnsuitableQualityTimestamp < this._blockRaiseDelay
197
+ ) ||
198
+ scaledScore === undefined || scaledScore < 1.15 ||
199
+ currentScore?.confidenceLevel !== ScoreConfidenceLevel.HIGH
200
+ ) {
201
+ return currentBitrate;
88
202
  }
89
203
 
90
- if (scaledScore === undefined || scaledScore < 1.15) {
91
- const currentBufferLevel = bufferLevels[currentBitrateIndex];
92
- if (bufferGap < currentBufferLevel) {
93
- for (let i = currentBitrateIndex - 1; i >= 0; i--) {
94
- if (bitrates[i] < currentBitrate) {
95
- return bitrates[i];
96
- }
204
+ const currentBufferLevel = bufferLevels[currentBitrateIndex];
205
+ const nextIndex = (() => {
206
+ for (let i = currentBitrateIndex + 1; i < bufferLevels.length; i++) {
207
+ if (bufferLevels[i] > currentBufferLevel) {
208
+ return i;
97
209
  }
98
- return currentBitrate;
210
+ }
211
+ })();
212
+ if (nextIndex !== undefined) {
213
+ const nextBufferLevel = bufferLevels[nextIndex];
214
+ if (bufferGap >= nextBufferLevel) {
215
+ log.debug("ABR: Raising quality in BufferBasedChooser", bitrates[nextIndex]);
216
+ return bitrates[nextIndex];
99
217
  }
100
218
  }
101
219
  return currentBitrate;
@@ -113,7 +231,7 @@ export interface IBufferBasedChooserPlaybackObservation {
113
231
  /** The bitrate of the currently downloaded segments, in bps. */
114
232
  currentBitrate? : number | undefined;
115
233
  /** The "maintainability score" of the currently downloaded segments. */
116
- currentScore? : number | undefined;
234
+ currentScore? : IRepresentationMaintainabilityScore | undefined;
117
235
  /** Playback rate wanted (e.g. `1` is regular playback, `2` is double speed etc.). */
118
236
  speed : number;
119
237
  }
@@ -23,6 +23,7 @@ import LastEstimateStorage, {
23
23
  } from "./utils/last_estimate_storage";
24
24
  import { IRequestInfo } from "./utils/pending_requests_store";
25
25
  import RepresentationScoreCalculator, {
26
+ IRepresentationMaintainabilityScore,
26
27
  ScoreConfidenceLevel,
27
28
  } from "./utils/representation_score_calculator";
28
29
 
@@ -178,11 +179,11 @@ export default class GuessBasedChooser {
178
179
  private _canGuessHigher(
179
180
  bufferGap : number,
180
181
  speed : number,
181
- [score, scoreConfidenceLevel] : [number, ScoreConfidenceLevel]
182
+ { score, confidenceLevel } : IRepresentationMaintainabilityScore
182
183
  ) : boolean {
183
184
  return isFinite(bufferGap) && bufferGap >= 2.5 &&
184
185
  performance.now() > this._blockGuessesUntil &&
185
- scoreConfidenceLevel === ScoreConfidenceLevel.HIGH &&
186
+ confidenceLevel === ScoreConfidenceLevel.HIGH &&
186
187
  score / speed > 1.01;
187
188
  }
188
189
 
@@ -197,13 +198,13 @@ export default class GuessBasedChooser {
197
198
  */
198
199
  private _shouldStopGuess(
199
200
  lastGuess : Representation,
200
- scoreData : [number, ScoreConfidenceLevel] | undefined,
201
+ scoreData : IRepresentationMaintainabilityScore | undefined,
201
202
  bufferGap : number,
202
203
  requests : IRequestInfo[]
203
204
  ) : boolean {
204
- if (scoreData !== undefined && scoreData[0] < 1.01) {
205
+ if (scoreData !== undefined && scoreData.score < 1.01) {
205
206
  return true;
206
- } else if ((scoreData === undefined || scoreData[0] < 1.2) && bufferGap < 0.6) {
207
+ } else if ((scoreData === undefined || scoreData.score < 1.2) && bufferGap < 0.6) {
207
208
  return true;
208
209
  }
209
210
 
@@ -233,11 +234,11 @@ export default class GuessBasedChooser {
233
234
  private _isLastGuessValidated(
234
235
  lastGuess : Representation,
235
236
  incomingBestBitrate : number,
236
- scoreData : [number, ScoreConfidenceLevel] | undefined
237
+ scoreData : IRepresentationMaintainabilityScore | undefined
237
238
  ) : boolean {
238
239
  if (scoreData !== undefined &&
239
- scoreData[1] === ScoreConfidenceLevel.HIGH &&
240
- scoreData[0] > 1.5)
240
+ scoreData.confidenceLevel === ScoreConfidenceLevel.HIGH &&
241
+ scoreData.score > 1.5)
241
242
  {
242
243
  return true;
243
244
  }
@@ -18,6 +18,26 @@ import log from "../../../log";
18
18
  import { Representation } from "../../../manifest";
19
19
  import EWMA from "./ewma";
20
20
 
21
+ /**
22
+ * Object representing a maintainability score as calculated by the
23
+ * `RepresentationScoreCalculator`.
24
+ */
25
+ export interface IRepresentationMaintainabilityScore {
26
+ /**
27
+ * Weighted mean of dividing the loaded segment's duration by the time to make
28
+ * their request.
29
+ */
30
+ score : number;
31
+
32
+ /**
33
+ * The confidence we have on the calculated `score` in reflecting a useful
34
+ * maintainability hint for the concerned Representation.
35
+ *
36
+ * Basically, the more segments have been loaded, the higher the confidence.
37
+ */
38
+ confidenceLevel: ScoreConfidenceLevel;
39
+ }
40
+
21
41
  /**
22
42
  * Calculate the "maintainability score" of a given Representation:
23
43
  * - A score higher than 1 means that the Representation can theorically
@@ -107,7 +127,7 @@ export default class RepresentationScoreCalculator {
107
127
  */
108
128
  public getEstimate(
109
129
  representation : Representation
110
- ) : [number, ScoreConfidenceLevel] | undefined {
130
+ ) : IRepresentationMaintainabilityScore | undefined {
111
131
  if (this._currentRepresentationData === null ||
112
132
  this._currentRepresentationData.representation.id !== representation.id)
113
133
  {
@@ -119,7 +139,7 @@ export default class RepresentationScoreCalculator {
119
139
  loadedDuration >= 10 ? ScoreConfidenceLevel.HIGH :
120
140
  ScoreConfidenceLevel.LOW;
121
141
 
122
- return [estimate, confidenceLevel];
142
+ return { score: estimate, confidenceLevel };
123
143
  }
124
144
 
125
145
  /**
@@ -29,7 +29,7 @@ export default function renderDebugElement(
29
29
  debugWrapperElt.style.backgroundColor = "#00000099";
30
30
  debugWrapperElt.style.padding = "7px";
31
31
  debugWrapperElt.style.fontSize = "13px";
32
- debugWrapperElt.style.fontFamily = "mono";
32
+ debugWrapperElt.style.fontFamily = "mono, monospace";
33
33
  debugWrapperElt.style.color = "white";
34
34
  debugWrapperElt.style.display = "inline-block";
35
35
  debugWrapperElt.style.bottom = "0px";
@@ -364,7 +364,7 @@ class Player extends EventEmitter<IPublicAPIEvent> {
364
364
  // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1194624
365
365
  videoElement.preload = "auto";
366
366
 
367
- this.version = /* PLAYER_VERSION */"3.30.1-dev.2023032800";
367
+ this.version = /* PLAYER_VERSION */"3.31.0-dev.2023052200";
368
368
  this.log = log;
369
369
  this.state = "STOPPED";
370
370
  this.videoElement = videoElement;
@@ -2986,7 +2986,7 @@ class Player extends EventEmitter<IPublicAPIEvent> {
2986
2986
  return mediaElementTrackChoiceManager;
2987
2987
  }
2988
2988
  }
2989
- Player.version = /* PLAYER_VERSION */"3.30.1-dev.2023032800";
2989
+ Player.version = /* PLAYER_VERSION */"3.31.0-dev.2023052200";
2990
2990
 
2991
2991
  /** Every events sent by the RxPlayer's public API. */
2992
2992
  interface IPublicAPIEvent {
@@ -120,10 +120,7 @@ function createAudioTracks(
120
120
  */
121
121
  function createTextTracks(
122
122
  textTracks: ICompatTextTrackList
123
- ): Array<{ track: { id: string;
124
- normalized: string;
125
- language: string;
126
- closedCaption: boolean; };
123
+ ): Array<{ track: ITextTrack;
127
124
  nativeTrack: TextTrack; }> {
128
125
  const newTextTracks = [];
129
126
  const languagesOccurences: Partial<Record<string, number>> = {};
@@ -137,10 +134,20 @@ function createTextTracks(
137
134
  "_" +
138
135
  occurences.toString();
139
136
  languagesOccurences[language] = occurences + 1;
140
- const track = { language: textTrack.language,
141
- id,
142
- normalized: normalizeLanguage(textTrack.language),
143
- closedCaption: textTrack.kind === "captions" };
137
+
138
+ // Safari seems to be indicating that the subtitles track is a forced
139
+ // subtitles track by setting the `kind` attribute to `"forced"`.
140
+ // As of now (2023-04-04), this is not standard.
141
+ // @see https://github.com/whatwg/html/issues/4472
142
+ const forced = (textTrack.kind as string) === "forced" ?
143
+ true :
144
+ undefined;
145
+ const track = { language: textTrack.language,
146
+ forced,
147
+ label: textTrack.label,
148
+ id,
149
+ normalized: normalizeLanguage(textTrack.language),
150
+ closedCaption: textTrack.kind === "captions" };
144
151
  newTextTracks.push({ track,
145
152
  nativeTrack: textTrack });
146
153
  }
@@ -471,6 +478,8 @@ export default class MediaElementTrackChoiceManager
471
478
  public getAvailableTextTracks(): IAvailableTextTrack[] {
472
479
  return this._textTracks.map(({ track, nativeTrack }) => {
473
480
  return { id: track.id,
481
+ label: track.label,
482
+ forced: track.forced,
474
483
  language: track.language,
475
484
  normalized: track.normalized,
476
485
  closedCaption: track.closedCaption,
@@ -26,7 +26,6 @@ import {
26
26
  Representation,
27
27
  } from "../../../manifest";
28
28
  import {
29
- IAudioRepresentation,
30
29
  IAudioTrack,
31
30
  IAudioTrackPreference,
32
31
  IAvailableAudioTrack,
@@ -34,7 +33,6 @@ import {
34
33
  IAvailableVideoTrack,
35
34
  ITextTrack,
36
35
  ITextTrackPreference,
37
- IVideoRepresentation,
38
36
  IVideoTrack,
39
37
  IVideoTrackPreference,
40
38
  } from "../../../public_types";
@@ -42,6 +40,7 @@ import arrayFind from "../../../utils/array_find";
42
40
  import arrayIncludes from "../../../utils/array_includes";
43
41
  import isNullOrUndefined from "../../../utils/is_null_or_undefined";
44
42
  import normalizeLanguage from "../../../utils/languages";
43
+ import objectAssign from "../../../utils/object_assign";
45
44
  import { ISharedReference } from "../../../utils/reference";
46
45
  import SortedList from "../../../utils/sorted_list";
47
46
  import takeFirstSet from "../../../utils/take_first_set";
@@ -618,19 +617,7 @@ export default class TrackChoiceManager {
618
617
  if (isNullOrUndefined(chosenTrack)) {
619
618
  return null;
620
619
  }
621
-
622
- const audioTrack : IAudioTrack = {
623
- language: takeFirstSet<string>(chosenTrack.language, ""),
624
- normalized: takeFirstSet<string>(chosenTrack.normalizedLanguage, ""),
625
- audioDescription: chosenTrack.isAudioDescription === true,
626
- id: chosenTrack.id,
627
- representations: chosenTrack.representations.map(parseAudioRepresentation),
628
- label: chosenTrack.label,
629
- };
630
- if (chosenTrack.isDub === true) {
631
- audioTrack.dub = true;
632
- }
633
- return audioTrack;
620
+ return chosenTrack.toAudioTrack();
634
621
  }
635
622
 
636
623
  /**
@@ -655,18 +642,7 @@ export default class TrackChoiceManager {
655
642
  if (isNullOrUndefined(chosenTextAdaptation)) {
656
643
  return null;
657
644
  }
658
-
659
- const formatted : ITextTrack = {
660
- language: takeFirstSet<string>(chosenTextAdaptation.language, ""),
661
- normalized: takeFirstSet<string>(chosenTextAdaptation.normalizedLanguage, ""),
662
- closedCaption: chosenTextAdaptation.isClosedCaption === true,
663
- id: chosenTextAdaptation.id,
664
- label: chosenTextAdaptation.label,
665
- };
666
- if (chosenTextAdaptation.isForcedSubtitles !== undefined) {
667
- formatted.forced = chosenTextAdaptation.isForcedSubtitles;
668
- }
669
- return formatted;
645
+ return chosenTextAdaptation.toTextTrack();
670
646
  }
671
647
 
672
648
  /**
@@ -692,36 +668,7 @@ export default class TrackChoiceManager {
692
668
  return null;
693
669
  }
694
670
  const currAdaptation = chosenVideoAdaptation.adaptation;
695
-
696
- const trickModeTracks = currAdaptation.trickModeTracks !== undefined ?
697
- currAdaptation.trickModeTracks.map((trickModeAdaptation) => {
698
- const representations = trickModeAdaptation.representations
699
- .map(parseVideoRepresentation);
700
- const trickMode : IVideoTrack = { id: trickModeAdaptation.id,
701
- representations,
702
- isTrickModeTrack: true };
703
- if (trickModeAdaptation.isSignInterpreted === true) {
704
- trickMode.signInterpreted = true;
705
- }
706
- return trickMode;
707
- }) :
708
- undefined;
709
-
710
- const videoTrack: IVideoTrack = {
711
- id: currAdaptation.id,
712
- representations: currAdaptation.representations.map(parseVideoRepresentation),
713
- label: currAdaptation.label,
714
- };
715
- if (currAdaptation.isSignInterpreted === true) {
716
- videoTrack.signInterpreted = true;
717
- }
718
- if (currAdaptation.isTrickModeTrack === true) {
719
- videoTrack.isTrickModeTrack = true;
720
- }
721
- if (trickModeTracks !== undefined) {
722
- videoTrack.trickModeTracks = trickModeTracks;
723
- }
724
- return videoTrack;
671
+ return currAdaptation.toVideoTrack();
725
672
  }
726
673
 
727
674
  /**
@@ -745,20 +692,9 @@ export default class TrackChoiceManager {
745
692
 
746
693
  return audioInfos.adaptations
747
694
  .map((adaptation) => {
748
- const formatted : IAvailableAudioTrack = {
749
- language: takeFirstSet<string>(adaptation.language, ""),
750
- normalized: takeFirstSet<string>(adaptation.normalizedLanguage, ""),
751
- audioDescription: adaptation.isAudioDescription === true,
752
- id: adaptation.id,
753
- active: currentId === null ? false :
754
- currentId === adaptation.id,
755
- representations: adaptation.representations.map(parseAudioRepresentation),
756
- label: adaptation.label,
757
- };
758
- if (adaptation.isDub === true) {
759
- formatted.dub = true;
760
- }
761
- return formatted;
695
+ const active = currentId === null ? false :
696
+ currentId === adaptation.id;
697
+ return objectAssign(adaptation.toAudioTrack(), { active });
762
698
  });
763
699
  }
764
700
 
@@ -784,19 +720,9 @@ export default class TrackChoiceManager {
784
720
 
785
721
  return textInfos.adaptations
786
722
  .map((adaptation) => {
787
- const formatted : IAvailableTextTrack = {
788
- language: takeFirstSet<string>(adaptation.language, ""),
789
- normalized: takeFirstSet<string>(adaptation.normalizedLanguage, ""),
790
- closedCaption: adaptation.isClosedCaption === true,
791
- id: adaptation.id,
792
- active: currentId === null ? false :
793
- currentId === adaptation.id,
794
- label: adaptation.label,
795
- };
796
- if (adaptation.isForcedSubtitles !== undefined) {
797
- formatted.forced = adaptation.isForcedSubtitles;
798
- }
799
- return formatted;
723
+ const active = currentId === null ? false :
724
+ currentId === adaptation.id;
725
+ return objectAssign(adaptation.toTextTrack(), { active });
800
726
  });
801
727
  }
802
728
 
@@ -821,37 +747,21 @@ export default class TrackChoiceManager {
821
747
 
822
748
  return videoInfos.adaptations
823
749
  .map((adaptation) => {
824
- const trickModeTracks = adaptation.trickModeTracks !== undefined ?
825
- adaptation.trickModeTracks.map((trickModeAdaptation) => {
750
+ const active = currentId === null ? false :
751
+ currentId === adaptation.id;
752
+ const track = adaptation.toVideoTrack();
753
+ const trickModeTracks = track.trickModeTracks !== undefined ?
754
+ track.trickModeTracks.map((trickModeAdaptation) => {
826
755
  const isActive = currentId === null ? false :
827
756
  currentId === trickModeAdaptation.id;
828
- const representations = trickModeAdaptation.representations
829
- .map(parseVideoRepresentation);
830
- const trickMode : IAvailableVideoTrack = { id: trickModeAdaptation.id,
831
- representations,
832
- isTrickModeTrack: true,
833
- active: isActive };
834
- if (trickModeAdaptation.isSignInterpreted === true) {
835
- trickMode.signInterpreted = true;
836
- }
837
- return trickMode;
757
+ return objectAssign(trickModeAdaptation, { active: isActive });
838
758
  }) :
839
- undefined;
840
-
841
- const formatted: IAvailableVideoTrack = {
842
- id: adaptation.id,
843
- active: currentId === null ? false :
844
- currentId === adaptation.id,
845
- representations: adaptation.representations.map(parseVideoRepresentation),
846
- label: adaptation.label,
847
- };
848
- if (adaptation.isSignInterpreted === true) {
849
- formatted.signInterpreted = true;
850
- }
759
+ [];
760
+ const availableTrack = objectAssign(track, { active });
851
761
  if (trickModeTracks !== undefined) {
852
- formatted.trickModeTracks = trickModeTracks;
762
+ availableTrack.trickModeTracks = trickModeTracks;
853
763
  }
854
- return formatted;
764
+ return availableTrack;
855
765
  });
856
766
  }
857
767
 
@@ -1359,28 +1269,6 @@ function getPeriodItem(
1359
1269
  }
1360
1270
  }
1361
1271
 
1362
- /**
1363
- * Parse video Representation into a IVideoRepresentation.
1364
- * @param {Object} representation
1365
- * @returns {Object}
1366
- */
1367
- function parseVideoRepresentation(
1368
- { id, bitrate, frameRate, width, height, codec, hdrInfo } : Representation
1369
- ) : IVideoRepresentation {
1370
- return { id, bitrate, frameRate, width, height, codec, hdrInfo };
1371
- }
1372
-
1373
- /**
1374
- * Parse audio Representation into a ITMAudioRepresentation.
1375
- * @param {Object} representation
1376
- * @returns {Object}
1377
- */
1378
- function parseAudioRepresentation(
1379
- { id, bitrate, codec } : Representation
1380
- ) : IAudioRepresentation {
1381
- return { id, bitrate, codec };
1382
- }
1383
-
1384
1272
  function getRightVideoTrack(
1385
1273
  adaptation : Adaptation,
1386
1274
  isTrickModeEnabled : boolean
@@ -502,8 +502,36 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
502
502
  */
503
503
  function handleStreamOrchestratorCallbacks() : IStreamOrchestratorCallbacks {
504
504
  return {
505
- needsBufferFlush: () =>
506
- playbackObserver.setCurrentTime(mediaElement.currentTime + 0.001),
505
+ needsBufferFlush: () => {
506
+ const seekedTime = mediaElement.currentTime + 0.001;
507
+ playbackObserver.setCurrentTime(seekedTime);
508
+
509
+ // Seek again once data begins to be buffered.
510
+ // This is sadly necessary on some browsers to avoid decoding
511
+ // issues after a flush.
512
+ //
513
+ // NOTE: there's in theory a potential race condition in the following
514
+ // logic as the callback could be called when media data is still
515
+ // being removed by the browser - which is an asynchronous process.
516
+ // The following condition checking for buffered data could thus lead
517
+ // to a false positive where we're actually checking previous data.
518
+ // For now, such scenario is avoided by setting the
519
+ // `includeLastObservation` option to `false` and calling
520
+ // `needsBufferFlush` once MSE media removal operations have been
521
+ // explicitely validated by the browser, but that's a complex and easy
522
+ // to break system.
523
+ playbackObserver.listen((obs, stopListening) => {
524
+ if (
525
+ // Data is buffered around the current position
526
+ obs.currentRange !== null ||
527
+ // Or, for whatever reason, playback is already advancing
528
+ obs.position > seekedTime + 0.1
529
+ ) {
530
+ stopListening();
531
+ playbackObserver.setCurrentTime(obs.position + 0.001);
532
+ }
533
+ }, { includeLastObservation: false, clearSignal: cancelSignal });
534
+ },
507
535
 
508
536
  streamStatusUpdate(value) {
509
537
  // Announce discontinuities if found
@@ -664,7 +692,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
664
692
  this.trigger("activePeriodChanged", { period });
665
693
  });
666
694
  contentTimeBoundariesObserver.addEventListener("durationUpdate", (newDuration) => {
667
- mediaSourceDurationUpdater.updateDuration(newDuration.duration, !newDuration.isEnd);
695
+ mediaSourceDurationUpdater.updateDuration(newDuration.duration, newDuration.isEnd);
668
696
  });
669
697
  contentTimeBoundariesObserver.addEventListener("endOfStream", () => {
670
698
  if (endOfStreamCanceller === null) {
@@ -683,7 +711,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
683
711
  });
684
712
  const currentDuration = contentTimeBoundariesObserver.getCurrentDuration();
685
713
  mediaSourceDurationUpdater.updateDuration(currentDuration.duration,
686
- !currentDuration.isEnd);
714
+ currentDuration.isEnd);
687
715
  return contentTimeBoundariesObserver;
688
716
  }
689
717