rx-player 3.28.0-dev.2022061700 → 3.28.0-dev.2022062700

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 (49) hide show
  1. package/CHANGELOG.md +5 -3
  2. package/VERSION +1 -1
  3. package/appveyor.yml +1 -1
  4. package/dist/_esm5.processed/core/adaptive/adaptive_representation_selector.d.ts +32 -11
  5. package/dist/_esm5.processed/core/adaptive/adaptive_representation_selector.js +19 -26
  6. package/dist/_esm5.processed/core/api/playback_observer.d.ts +47 -35
  7. package/dist/_esm5.processed/core/api/playback_observer.js +120 -117
  8. package/dist/_esm5.processed/core/api/public_api.d.ts +4 -6
  9. package/dist/_esm5.processed/core/api/public_api.js +18 -18
  10. package/dist/_esm5.processed/core/decrypt/content_decryptor.js +60 -31
  11. package/dist/_esm5.processed/core/init/content_time_boundaries_observer.js +1 -1
  12. package/dist/_esm5.processed/core/init/create_stream_playback_observer.d.ts +1 -1
  13. package/dist/_esm5.processed/core/init/create_stream_playback_observer.js +23 -6
  14. package/dist/_esm5.processed/core/init/initial_seek_and_play.js +3 -3
  15. package/dist/_esm5.processed/core/init/initialize_directfile.js +1 -1
  16. package/dist/_esm5.processed/core/init/load_on_media_source.js +1 -1
  17. package/dist/_esm5.processed/core/init/stall_avoider.js +12 -8
  18. package/dist/_esm5.processed/core/stream/adaptation/create_representation_estimator.js +1 -1
  19. package/dist/_esm5.processed/core/stream/orchestrator/stream_orchestrator.js +5 -4
  20. package/dist/_esm5.processed/core/stream/period/create_empty_adaptation_stream.js +1 -1
  21. package/dist/_esm5.processed/core/stream/period/period_stream.js +21 -5
  22. package/dist/_esm5.processed/core/stream/reload_after_switch.js +1 -1
  23. package/dist/_esm5.processed/core/stream/representation/append_segment_to_buffer.js +1 -1
  24. package/dist/_esm5.processed/core/stream/representation/representation_stream.js +1 -1
  25. package/dist/_esm5.processed/utils/reference.d.ts +29 -19
  26. package/dist/_esm5.processed/utils/reference.js +21 -5
  27. package/dist/rx-player.js +366 -257
  28. package/dist/rx-player.min.js +1 -1
  29. package/package.json +2 -2
  30. package/sonar-project.properties +1 -1
  31. package/src/README.md +84 -68
  32. package/src/core/adaptive/adaptive_representation_selector.ts +47 -40
  33. package/src/core/api/playback_observer.ts +185 -173
  34. package/src/core/api/public_api.ts +23 -21
  35. package/src/core/decrypt/content_decryptor.ts +57 -23
  36. package/src/core/init/content_time_boundaries_observer.ts +1 -1
  37. package/src/core/init/create_stream_playback_observer.ts +69 -47
  38. package/src/core/init/initial_seek_and_play.ts +3 -3
  39. package/src/core/init/initialize_directfile.ts +1 -1
  40. package/src/core/init/load_on_media_source.ts +1 -1
  41. package/src/core/init/stall_avoider.ts +12 -9
  42. package/src/core/stream/adaptation/create_representation_estimator.ts +6 -6
  43. package/src/core/stream/orchestrator/stream_orchestrator.ts +5 -4
  44. package/src/core/stream/period/create_empty_adaptation_stream.ts +1 -1
  45. package/src/core/stream/period/period_stream.ts +33 -14
  46. package/src/core/stream/reload_after_switch.ts +1 -1
  47. package/src/core/stream/representation/append_segment_to_buffer.ts +1 -1
  48. package/src/core/stream/representation/representation_stream.ts +1 -1
  49. package/src/utils/reference.ts +40 -28
@@ -491,20 +491,13 @@ export default class ContentDecryptor extends EventEmitter<IContentDecryptorEven
491
491
  return;
492
492
  }
493
493
 
494
- let linkedKeys;
495
- if (sessionInfo.source === MediaKeySessionLoadingType.Created) {
496
- // When the license has been fetched, there might be implicit key ids
497
- // linked to the session depending on the `singleLicensePer` option.
498
- linkedKeys = getFetchedLicenseKeysInfo(initializationData,
499
- options.singleLicensePer,
500
- evt.value.whitelistedKeyIds,
501
- evt.value.blacklistedKeyIDs);
502
- } else {
503
- // When the MediaKeySession is just a cached/persisted one, we don't
504
- // have any concept of "implicit key id".
505
- linkedKeys = { whitelisted: evt.value.whitelistedKeyIds,
506
- blacklisted: evt.value.blacklistedKeyIDs };
507
- }
494
+ const linkedKeys = getKeyIdsLinkedToSession(
495
+ initializationData,
496
+ sessionInfo.record,
497
+ options.singleLicensePer,
498
+ sessionInfo.source === MediaKeySessionLoadingType.Created,
499
+ evt.value.whitelistedKeyIds,
500
+ evt.value.blacklistedKeyIDs);
508
501
 
509
502
  sessionInfo.record.associateKeyIds(linkedKeys.whitelisted);
510
503
  sessionInfo.record.associateKeyIds(linkedKeys.blacklisted);
@@ -1053,22 +1046,46 @@ interface IAttachedMediaKeysData {
1053
1046
  }
1054
1047
 
1055
1048
  /**
1056
- * Returns information on all keys - explicit or implicit - that are linked to
1057
- * a loaded license.
1049
+ * Returns set of all usable and unusable keys - explicit or implicit - that are
1050
+ * linked to a `MediaKeySession`.
1058
1051
  *
1059
1052
  * In the RxPlayer, there is a concept of "explicit" key ids, which are key ids
1060
1053
  * found in a license whose status can be known through the `keyStatuses`
1061
1054
  * property from a `MediaKeySession`, and of "implicit" key ids, which are key
1062
1055
  * ids which were expected to be in a fetched license, but apparently weren't.
1063
- * @param {Object} initializationData
1064
- * @param {string|undefined} singleLicensePer
1065
- * @param {Array.<Uint8Array>} usableKeyIds
1066
- * @param {Array.<Uint8Array>} unusableKeyIds
1067
- * @returns {Object}
1056
+ *
1057
+ * @param {Object} initializationData - Initialization data object used to make
1058
+ * the request for the current license.
1059
+ * @param {Object} keySessionRecord - The `KeySessionRecord` associated with the
1060
+ * session that has been loaded. It might give supplementary information on
1061
+ * keys implicitly linked to the license.
1062
+ * @param {string|undefined} singleLicensePer - Setting allowing to indicate the
1063
+ * scope a given license should have.
1064
+ * @param {boolean} isCurrentLicense - If `true` the license has been fetched
1065
+ * especially for the current content.
1066
+ *
1067
+ * Knowing this allows to determine that if decryption keys that should have
1068
+ * been referenced in the fetched license (according to the `singleLicensePer`
1069
+ * setting) are missing, then the keys surely must have been voluntarly
1070
+ * removed from the license.
1071
+ *
1072
+ * If it is however set to `false`, it means that the license is an older
1073
+ * license that might have been linked to another content, thus we cannot make
1074
+ * that assumption.
1075
+ * @param {Array.<Uint8Array>} usableKeyIds - Key ids that are present in the
1076
+ * license and can be used.
1077
+ * @param {Array.<Uint8Array>} unusableKeyIds - Key ids that are present in the
1078
+ * license yet cannot be used.
1079
+ * @returns {Object} - Returns an object with the following properties:
1080
+ * - `whitelisted`: Array of key ids for keys that are known to be usable
1081
+ * - `blacklisted`: Array of key ids for keys that are considered unusable.
1082
+ * The qualities linked to those keys should not be played.
1068
1083
  */
1069
- function getFetchedLicenseKeysInfo(
1084
+ function getKeyIdsLinkedToSession(
1070
1085
  initializationData : IProcessedProtectionData,
1086
+ keySessionRecord : KeySessionRecord,
1071
1087
  singleLicensePer : undefined | "init-data" | "content" | "periods",
1088
+ isCurrentLicense : boolean,
1072
1089
  usableKeyIds : Uint8Array[],
1073
1090
  unusableKeyIds : Uint8Array[]
1074
1091
  ) : { whitelisted : Uint8Array[];
@@ -1081,6 +1098,19 @@ function getFetchedLicenseKeysInfo(
1081
1098
  const associatedKeyIds = [...usableKeyIds,
1082
1099
  ...unusableKeyIds];
1083
1100
 
1101
+ // Add all key ids associated to the `KeySessionRecord` yet not in
1102
+ // `usableKeyIds` nor in `unusableKeyIds`
1103
+ const allKnownKeyIds = keySessionRecord.getAssociatedKeyIds();
1104
+ for (const kid of allKnownKeyIds) {
1105
+ if (!associatedKeyIds.some(ak => areKeyIdsEqual(ak, kid))) {
1106
+ if (log.hasLevel("DEBUG")) {
1107
+ log.debug("DRM: KeySessionRecord's key missing in the license, blacklisting it",
1108
+ bytesToHex(kid));
1109
+ }
1110
+ associatedKeyIds.push(kid);
1111
+ }
1112
+ }
1113
+
1084
1114
  if (singleLicensePer !== undefined && singleLicensePer !== "init-data") {
1085
1115
  // We want to add the current key ids in the blacklist if it is
1086
1116
  // not already there.
@@ -1102,11 +1132,15 @@ function getFetchedLicenseKeysInfo(
1102
1132
  return !associatedKeyIds.some(k => areKeyIdsEqual(k, expected));
1103
1133
  });
1104
1134
  if (missingKeyIds.length > 0) {
1135
+ if (log.hasLevel("DEBUG")) {
1136
+ log.debug("DRM: init data keys missing in the license, blacklisting them",
1137
+ missingKeyIds.map(m => bytesToHex(m)).join(", "));
1138
+ }
1105
1139
  associatedKeyIds.push(...missingKeyIds) ;
1106
1140
  }
1107
1141
  }
1108
1142
 
1109
- if (content !== undefined) {
1143
+ if (isCurrentLicense && content !== undefined) {
1110
1144
  if (singleLicensePer === "content") {
1111
1145
  // Put it in a Set to automatically filter out duplicates (by ref)
1112
1146
  const contentKeys = new Set<Uint8Array>();
@@ -73,7 +73,7 @@ export default function ContentTimeBoundariesObserver(
73
73
 
74
74
  // trigger warnings when the wanted time is before or after the manifest's
75
75
  // segments
76
- const outOfManifest$ = playbackObserver.observe(true).pipe(
76
+ const outOfManifest$ = playbackObserver.getReference().asObservable().pipe(
77
77
  filterMap<IContentTimeObserverPlaybackObservation, IWarningEvent, null>((
78
78
  { position }
79
79
  ) => {
@@ -14,13 +14,11 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import {
18
- combineLatest as observableCombineLatest,
19
- map,
20
- Observable,
21
- } from "rxjs";
22
17
  import Manifest from "../../manifest";
23
- import { IReadOnlySharedReference } from "../../utils/reference";
18
+ import createSharedReference, {
19
+ IReadOnlySharedReference,
20
+ } from "../../utils/reference";
21
+ import { CancellationSignal } from "../../utils/task_canceller";
24
22
  import {
25
23
  IPlaybackObservation,
26
24
  IReadOnlyPlaybackObserver,
@@ -47,7 +45,7 @@ export interface IStreamPlaybackObserverArguments {
47
45
  * @param {Object} manifest
48
46
  * @param {Object} playbackObserver
49
47
  * @param {Object} args
50
- * @returns {Observable}
48
+ * @returns {Object}
51
49
  */
52
50
  export default function createStreamPlaybackObserver(
53
51
  manifest : Manifest,
@@ -58,47 +56,71 @@ export default function createStreamPlaybackObserver(
58
56
  speed,
59
57
  startTime } : IStreamPlaybackObserverArguments
60
58
  ) : IReadOnlyPlaybackObserver<IStreamOrchestratorPlaybackObservation> {
61
- return playbackObserver.deriveReadOnlyObserver(function mapObservable(
62
- observation$ : Observable<IPlaybackObservation>
63
- ) : Observable<IStreamOrchestratorPlaybackObservation> {
64
- return observableCombineLatest([observation$, speed.asObservable()]).pipe(
65
- map(([observation, lastSpeed]) => {
66
- let pendingPosition : number | undefined;
67
- if (!initialSeekPerformed.getValue()) {
68
- pendingPosition = startTime;
69
- } else if (!manifest.isDynamic || manifest.isLastPeriodKnown) {
70
- // HACK: When the position is actually further than the maximum
71
- // position for a finished content, we actually want to be loading
72
- // the last segment before ending.
73
- // For now, this behavior is implicitely forced by making as if we
74
- // want to seek one second before the period's end (despite never
75
- // doing it).
76
- const lastPeriod = manifest.periods[manifest.periods.length - 1];
77
- if (lastPeriod !== undefined &&
78
- lastPeriod.end !== undefined &&
79
- observation.position > lastPeriod.end)
80
- {
81
- pendingPosition = lastPeriod.end - 1;
82
- }
59
+ return playbackObserver.deriveReadOnlyObserver(function transform(
60
+ observationRef : IReadOnlySharedReference<IPlaybackObservation>,
61
+ cancellationSignal : CancellationSignal
62
+ ) : IReadOnlySharedReference<IStreamOrchestratorPlaybackObservation> {
63
+ const newRef = createSharedReference(constructStreamPlaybackObservation());
64
+
65
+ speed.onUpdate(emitStreamPlaybackObservation, {
66
+ clearSignal: cancellationSignal,
67
+ emitCurrentValue: false,
68
+ });
69
+
70
+ observationRef.onUpdate(emitStreamPlaybackObservation, {
71
+ clearSignal: cancellationSignal,
72
+ emitCurrentValue: false,
73
+ });
74
+
75
+ cancellationSignal.register(() => {
76
+ newRef.finish();
77
+ });
78
+
79
+ return newRef;
80
+
81
+ function constructStreamPlaybackObservation() {
82
+ const observation = observationRef.getValue();
83
+ const lastSpeed = speed.getValue();
84
+ let pendingPosition : number | undefined;
85
+ if (!initialSeekPerformed.getValue()) {
86
+ pendingPosition = startTime;
87
+ } else if (!manifest.isDynamic || manifest.isLastPeriodKnown) {
88
+ // HACK: When the position is actually further than the maximum
89
+ // position for a finished content, we actually want to be loading
90
+ // the last segment before ending.
91
+ // For now, this behavior is implicitely forced by making as if we
92
+ // want to seek one second before the period's end (despite never
93
+ // doing it).
94
+ const lastPeriod = manifest.periods[manifest.periods.length - 1];
95
+ if (lastPeriod !== undefined &&
96
+ lastPeriod.end !== undefined &&
97
+ observation.position > lastPeriod.end)
98
+ {
99
+ pendingPosition = lastPeriod.end - 1;
83
100
  }
101
+ }
102
+
103
+ return {
104
+ // TODO more exact according to the current Adaptation chosen?
105
+ maximumPosition: manifest.getMaximumSafePosition(),
106
+ position: {
107
+ last: observation.position,
108
+ pending: pendingPosition,
109
+ },
110
+ duration: observation.duration,
111
+ paused: {
112
+ last: observation.paused,
113
+ pending: initialPlayPerformed.getValue() ? undefined :
114
+ !autoPlay === observation.paused ? undefined :
115
+ !autoPlay,
116
+ },
117
+ readyState: observation.readyState,
118
+ speed: lastSpeed,
119
+ };
120
+ }
84
121
 
85
- return {
86
- // TODO more exact according to the current Adaptation chosen?
87
- maximumPosition: manifest.getMaximumSafePosition(),
88
- position: {
89
- last: observation.position,
90
- pending: pendingPosition,
91
- },
92
- duration: observation.duration,
93
- paused: {
94
- last: observation.paused,
95
- pending: initialPlayPerformed.getValue() ? undefined :
96
- !autoPlay === observation.paused ? undefined :
97
- !autoPlay,
98
- },
99
- readyState: observation.readyState,
100
- speed: lastSpeed,
101
- };
102
- }));
122
+ function emitStreamPlaybackObservation() {
123
+ newRef.setValue(constructStreamPlaybackObservation());
124
+ }
103
125
  });
104
126
  }
@@ -83,7 +83,7 @@ export function waitUntilPlayable(
83
83
 
84
84
  /**
85
85
  * Try to play content then handle autoplay errors.
86
- * @param {HTMLMediaElement} - mediaElement
86
+ * @param {HTMLMediaElement} mediaElement
87
87
  * @returns {Observable}
88
88
  */
89
89
  function autoPlay(
@@ -161,12 +161,12 @@ export default function initialSeekAndPlay(
161
161
  const seekAndPlay$ = seek$.pipe(
162
162
  mergeMap(() : Observable<IWarningEvent | undefined> => {
163
163
  if (!shouldValidateMetadata() || mediaElement.duration > 0) {
164
- return waitUntilPlayable(playbackObserver.observe(true));
164
+ return waitUntilPlayable(playbackObserver.getReference().asObservable());
165
165
  } else {
166
166
  const error = new MediaError("MEDIA_ERR_NOT_LOADED_METADATA",
167
167
  "Cannot load automatically: your browser " +
168
168
  "falsely announced having loaded the content.");
169
- return waitUntilPlayable(playbackObserver.observe(true))
169
+ return waitUntilPlayable(playbackObserver.getReference().asObservable())
170
170
  .pipe(startWith(EVENTS.warning(error)));
171
171
  }
172
172
  }),
@@ -158,7 +158,7 @@ export default function initializeDirectfileContent({
158
158
  // through a throwing Observable.
159
159
  const mediaError$ = throwOnMediaError(mediaElement);
160
160
 
161
- const observation$ = playbackObserver.observe(true);
161
+ const observation$ = playbackObserver.getReference().asObservable();
162
162
 
163
163
  // Set the speed set by the user on the media element while pausing a
164
164
  // little longer while the buffer is empty.
@@ -125,7 +125,7 @@ export default function createMediaSourceLoader(
125
125
  startTime: initialTime,
126
126
  mustAutoPlay: autoPlay });
127
127
 
128
- const observation$ = playbackObserver.observe(true);
128
+ const observation$ = playbackObserver.getReference().asObservable();
129
129
  const streamEvents$ = initialPlayPerformed.asObservable().pipe(
130
130
  filter((hasPlayed) => hasPlayed),
131
131
  mergeMap(() => streamEventsEmitter(manifest, mediaElement, observation$)));
@@ -139,7 +139,7 @@ export default function StallAvoider(
139
139
  * order (first ordered by Period's start, then by bufferType in any order.
140
140
  */
141
141
  const discontinuitiesStore$ = discontinuityUpdate$.pipe(
142
- withLatestFrom(playbackObserver.observe(true)),
142
+ withLatestFrom(playbackObserver.getReference().asObservable()),
143
143
  scan(
144
144
  (discontinuitiesStore, [evt, observation]) =>
145
145
  updateDiscontinuitiesStore(discontinuitiesStore, evt, observation),
@@ -179,7 +179,7 @@ export default function StallAvoider(
179
179
  * Period handled by that stream to unlock the situation.
180
180
  */
181
181
  const unlock$ = lockedStream$.pipe(
182
- withLatestFrom(playbackObserver.observe(true)),
182
+ withLatestFrom(playbackObserver.getReference().asObservable()),
183
183
  tap(([lockedStreamEvt, observation]) => {
184
184
  // TODO(PaulB) also skip when the user's wanted speed is set to `0`, as we
185
185
  // might not want to seek in that case?
@@ -211,7 +211,7 @@ export default function StallAvoider(
211
211
  ignoreElements()
212
212
  );
213
213
 
214
- const stall$ = playbackObserver.observe(true).pipe(
214
+ const stall$ = playbackObserver.getReference().asObservable().pipe(
215
215
  withLatestFrom(discontinuitiesStore$),
216
216
  map(([observation, discontinuitiesStore]) => {
217
217
  const { buffered,
@@ -279,12 +279,15 @@ export default function StallAvoider(
279
279
  if (ignoredStallTimeStamp === null) {
280
280
  ignoredStallTimeStamp = now;
281
281
  }
282
- if (isSeekingApproximate &&
283
- observation.position < lastSeekingPosition &&
284
- now - ignoredStallTimeStamp < FORCE_DISCONTINUITY_SEEK_DELAY)
285
- {
286
- return { type: "stalled" as const,
287
- value: stalledReason };
282
+ if (isSeekingApproximate && observation.position < lastSeekingPosition) {
283
+ log.debug("Init: the device appeared to have seeked back by itself.");
284
+ if (now - ignoredStallTimeStamp < FORCE_DISCONTINUITY_SEEK_DELAY) {
285
+ return { type: "stalled" as const,
286
+ value: stalledReason };
287
+ } else {
288
+ log.warn("Init: ignored stall for too long, checking discontinuity",
289
+ now - ignoredStallTimeStamp);
290
+ }
288
291
  }
289
292
  lastSeekingPosition = null;
290
293
  }
@@ -70,12 +70,12 @@ export default function getRepresentationEstimate(
70
70
  updateRepresentationsReference();
71
71
  manifest.addEventListener("decipherabilityUpdate", updateRepresentationsReference);
72
72
  const unregisterCleanUp = cancellationSignal.register(cleanUp);
73
- const [ estimateRef,
74
- abrCallbacks ] = representationEstimator(content,
75
- currentRepresentation,
76
- representations,
77
- playbackObserver,
78
- cancellationSignal);
73
+ const { estimates: estimateRef,
74
+ callbacks: abrCallbacks } = representationEstimator(content,
75
+ currentRepresentation,
76
+ representations,
77
+ playbackObserver,
78
+ cancellationSignal);
79
79
  return { abrCallbacks, estimateRef };
80
80
 
81
81
  function updateRepresentationsReference() : void {
@@ -142,7 +142,7 @@ export default function StreamOrchestrator(
142
142
  Infinity;
143
143
  return BufferGarbageCollector({
144
144
  segmentBuffer,
145
- currentTime$: playbackObserver.observe(true)
145
+ currentTime$: playbackObserver.getReference().asObservable()
146
146
  .pipe(map(o => o.position.pending ?? o.position.last)),
147
147
  maxBufferBehind$: maxBufferBehind.asObservable().pipe(
148
148
  map(val => Math.min(val, defaultMaxBehind))),
@@ -264,7 +264,8 @@ export default function StreamOrchestrator(
264
264
 
265
265
  // Restart the current Stream when the wanted time is in another period
266
266
  // than the ones already considered
267
- const restartStreamsWhenOutOfBounds$ = playbackObserver.observe(true).pipe(
267
+ const observation$ = playbackObserver.getReference().asObservable();
268
+ const restartStreamsWhenOutOfBounds$ = observation$.pipe(
268
269
  filterMap<
269
270
  IStreamOrchestratorPlaybackObservation,
270
271
  Period,
@@ -329,7 +330,7 @@ export default function StreamOrchestrator(
329
330
  // to reduce the risk of race conditions where the next observation
330
331
  // was going to be emitted synchronously.
331
332
  nextTickObs().pipe(ignoreElements()),
332
- playbackObserver.observe(true).pipe(
333
+ playbackObserver.getReference().asObservable().pipe(
333
334
  take(1),
334
335
  mergeMap((observation) => {
335
336
  const shouldAutoPlay = !(observation.paused.pending ??
@@ -399,7 +400,7 @@ export default function StreamOrchestrator(
399
400
  const destroyNextStreams$ = new Subject<void>();
400
401
 
401
402
  // Emits when the current position goes over the end of the current Stream.
402
- const endOfCurrentStream$ = playbackObserver.observe(true)
403
+ const endOfCurrentStream$ = playbackObserver.getReference().asObservable()
403
404
  .pipe(filter(({ position }) =>
404
405
  basePeriod.end != null &&
405
406
  (position.pending ?? position.last) >= basePeriod.end));
@@ -48,7 +48,7 @@ export default function createEmptyAdaptationStream(
48
48
  const { period } = content;
49
49
  let hasFinishedLoading = false;
50
50
  const wantedBufferAhead$ = wantedBufferAhead.asObservable();
51
- const observation$ = playbackObserver.observe(true);
51
+ const observation$ = playbackObserver.getReference().asObservable();
52
52
  return observableCombineLatest([observation$,
53
53
  wantedBufferAhead$]).pipe(
54
54
  mergeMap(([observation, wba]) => {
@@ -42,7 +42,10 @@ import Manifest, {
42
42
  import { IAudioTrackSwitchingMode } from "../../../public_types";
43
43
  import objectAssign from "../../../utils/object_assign";
44
44
  import { getLeftSizeOfRange } from "../../../utils/ranges";
45
- import { IReadOnlySharedReference } from "../../../utils/reference";
45
+ import createSharedReference, {
46
+ IReadOnlySharedReference,
47
+ } from "../../../utils/reference";
48
+ import { CancellationSignal } from "../../../utils/task_canceller";
46
49
  import WeakMapMemory from "../../../utils/weak_map_memory";
47
50
  import { IRepresentationEstimator } from "../../adaptive";
48
51
  import { IReadOnlyPlaybackObserver } from "../../api";
@@ -357,18 +360,34 @@ function createAdaptationStreamPlaybackObserver(
357
360
  initialPlaybackObserver : IReadOnlyPlaybackObserver<IPeriodStreamPlaybackObservation>,
358
361
  segmentBuffer : SegmentBuffer
359
362
  ) : IReadOnlyPlaybackObserver<IAdaptationStreamPlaybackObservation> {
360
- return initialPlaybackObserver.deriveReadOnlyObserver(
361
- (observation$) => observation$.pipe(map(mapObservation)),
362
- mapObservation
363
- );
363
+ return initialPlaybackObserver.deriveReadOnlyObserver(function transform(
364
+ observationRef : IReadOnlySharedReference<IPeriodStreamPlaybackObservation>,
365
+ cancellationSignal : CancellationSignal
366
+ ) : IReadOnlySharedReference<IAdaptationStreamPlaybackObservation> {
367
+ const newRef = createSharedReference(constructAdaptationStreamPlaybackObservation());
368
+
369
+ observationRef.onUpdate(emitAdaptationStreamPlaybackObservation, {
370
+ clearSignal: cancellationSignal,
371
+ emitCurrentValue: false,
372
+ });
373
+
374
+ cancellationSignal.register(() => {
375
+ newRef.finish();
376
+ });
377
+
378
+ return newRef;
379
+
380
+ function constructAdaptationStreamPlaybackObservation(
381
+ ) : IAdaptationStreamPlaybackObservation {
382
+ const baseObservation = observationRef.getValue();
383
+ const buffered = segmentBuffer.getBufferedRanges();
384
+ const bufferGap = getLeftSizeOfRange(buffered, baseObservation.position.last);
385
+ return objectAssign({}, baseObservation, { bufferGap });
386
+ }
387
+
388
+ function emitAdaptationStreamPlaybackObservation() {
389
+ newRef.setValue(constructAdaptationStreamPlaybackObservation());
390
+ }
391
+ });
364
392
 
365
- function mapObservation(
366
- baseObservation : IPeriodStreamPlaybackObservation
367
- ) : IAdaptationStreamPlaybackObservation {
368
- const buffered = segmentBuffer.getBufferedRanges();
369
- return objectAssign({},
370
- baseObservation,
371
- { bufferGap: getLeftSizeOfRange(buffered,
372
- baseObservation.position.last) });
373
- }
374
393
  }
@@ -62,7 +62,7 @@ export default function reloadAfterSwitch(
62
62
  // It can happen when `reloadAfterSwitch` is called as a side-effect of the
63
63
  // same event that triggers the playback observation to be emitted.
64
64
  return nextTickObs().pipe(
65
- mergeMap(() => playbackObserver.observe(true)),
65
+ mergeMap(() => playbackObserver.getReference().asObservable()),
66
66
  map((observation) => {
67
67
  const currentTime = playbackObserver.getCurrentTime();
68
68
  const pos = currentTime + deltaPos;
@@ -61,7 +61,7 @@ export default function appendSegmentToBuffer<T>(
61
61
  throw new MediaError("BUFFER_APPEND_ERROR", reason);
62
62
  }
63
63
 
64
- return playbackObserver.observe(true).pipe(
64
+ return playbackObserver.getReference().asObservable().pipe(
65
65
  take(1),
66
66
  mergeMap((observation) => {
67
67
  const currentPos = observation.position.pending ??
@@ -166,7 +166,7 @@ export default function RepresentationStream<TSegmentDataType>({
166
166
 
167
167
  /** Observable emitting the stream "status" and filling `lastSegmentQueue`. */
168
168
  const status$ = observableCombineLatest([
169
- playbackObserver.observe(true),
169
+ playbackObserver.getReference().asObservable(),
170
170
  bufferGoal$,
171
171
  maxBufferSize$,
172
172
  terminate$.pipe(take(1),
@@ -57,11 +57,26 @@ import { CancellationSignal } from "./task_canceller";
57
57
  * those use cases makes the intent of the corresponding code clearer.
58
58
  */
59
59
  export interface ISharedReference<T> {
60
- /** Get the last set value for this shared reference. */
60
+ /**
61
+ * Get the last set value for this shared reference.
62
+ * @returns {*}
63
+ */
61
64
  getValue() : T;
62
65
 
63
- /** Update the value of this shared reference. */
66
+ /**
67
+ * Update the value of this shared reference.
68
+ * @param {*} newVal
69
+ */
64
70
  setValue(newVal : T) : void;
71
+
72
+ /**
73
+ * Update the value of this shared reference only if the value changed.
74
+ *
75
+ * Note that this function only performs a strict equality reference through
76
+ * the "===" operator. Different objects that are structurally the same will
77
+ * thus be considered different.
78
+ * @param {*} newVal
79
+ */
65
80
  setValueIfChanged(newVal : T) : void;
66
81
 
67
82
  /**
@@ -72,25 +87,6 @@ export interface ISharedReference<T> {
72
87
  * @returns {Observable}
73
88
  */
74
89
  asObservable(skipCurrentValue? : boolean) : Observable<T>;
75
- /**
76
- * Triggers a callback each time this reference's value is updated.
77
- *
78
- * Can be given several options as argument:
79
- * - clearSignal: When the attach `CancellationSignal` emits, the given
80
- * callback will not be called anymore on reference updates.
81
- * - emitCurrentValue: If `true`, the callback will be called directly and
82
- * synchronously on this call with its current value.
83
- * @param {Function} cb
84
- * @param {Object} [options]
85
- */
86
- onUpdate(
87
- cb : (val : T) => void,
88
- options? : {
89
- clearSignal?: CancellationSignal;
90
- emitCurrentValue?: boolean;
91
- },
92
- ) : void;
93
-
94
90
  /**
95
91
  * Allows to register a callback to be called each time the value inside the
96
92
  * reference is updated.
@@ -105,11 +101,10 @@ export interface ISharedReference<T> {
105
101
  onUpdate(
106
102
  cb : (val : T) => void,
107
103
  options? : {
108
- clearSignal?: CancellationSignal;
109
- emitCurrentValue?: boolean;
104
+ clearSignal?: CancellationSignal | undefined;
105
+ emitCurrentValue?: boolean | undefined;
110
106
  },
111
107
  ) : void;
112
-
113
108
  /**
114
109
  * Indicate that no new values will be emitted.
115
110
  * Allows to automatically close all Observables generated from this shared
@@ -217,15 +212,15 @@ export function createSharedReference<T>(initialValue : T) : ISharedReference<T>
217
212
 
218
213
  /**
219
214
  * Update the value of this shared reference.
220
- * @param {*}
215
+ * @param {*} newVal
221
216
  */
222
217
  setValue(newVal : T) : void {
223
218
  if (isFinished) {
224
219
  if (__ENVIRONMENT__.CURRENT_ENV === __ENVIRONMENT__.DEV as number) {
225
- throw new Error("Finished shared references cannot be updated");
226
- } else {
227
- return;
220
+ /* eslint-disable-next-line no-console */
221
+ console.error("Finished shared references cannot be updated");
228
222
  }
223
+ return;
229
224
  }
230
225
  value = newVal;
231
226
 
@@ -244,6 +239,10 @@ export function createSharedReference<T>(initialValue : T) : ISharedReference<T>
244
239
  }
245
240
  },
246
241
 
242
+ /**
243
+ * Update the value of this shared reference only if it changed.
244
+ * @param {*} newVal
245
+ */
247
246
  setValueIfChanged(newVal : T) : void {
248
247
  if (newVal !== value) {
249
248
  this.setValue(newVal);
@@ -345,14 +344,27 @@ export function createSharedReference<T>(initialValue : T) : ISharedReference<T>
345
344
  if (!cbObj.hasBeenCleared) {
346
345
  cbObj.complete();
347
346
  }
347
+ cbObj.hasBeenCleared = true;
348
348
  } catch (_) {
349
349
  /* nothing */
350
350
  }
351
351
  }
352
+ cbs.length = 0;
352
353
  },
353
354
  };
354
355
  }
355
356
 
357
+ /**
358
+ * Create a new `ISharedReference` based on another one by mapping over its
359
+ * referenced value each time it is updated.
360
+ * @param {Object} originalRef - The Original `ISharedReference` you wish to map
361
+ * over.
362
+ * @param {Function} mappingFn - The mapping function which will receives
363
+ * `originalRef`'s value and outputs this new reference's value.
364
+ * @param {Object | undefined} [cancellationSignal] - Optionally, a
365
+ * `CancellationSignal` which will finish that reference when it emits.
366
+ * @returns {Object} - The new, mapped, reference.
367
+ */
356
368
  export function createMappedReference<T, U>(
357
369
  originalRef : IReadOnlySharedReference<T>,
358
370
  mappingFn : (x : T) => U,