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
@@ -14,25 +14,17 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import {
18
- defer as observableDefer,
19
- fromEvent as observableFromEvent,
20
- interval as observableInterval,
21
- map,
22
- merge as observableMerge,
23
- Observable,
24
- share,
25
- shareReplay,
26
- skip,
27
- startWith,
28
- } from "rxjs";
29
17
  import config from "../../config";
30
18
  import log from "../../log";
31
19
  import noop from "../../utils/noop";
32
20
  import objectAssign from "../../utils/object_assign";
33
21
  import { getRange } from "../../utils/ranges";
34
- import { CancellationSignal } from "../../utils/task_canceller";
35
-
22
+ import createSharedReference, {
23
+ IReadOnlySharedReference,
24
+ } from "../../utils/reference";
25
+ import TaskCanceller, {
26
+ CancellationSignal,
27
+ } from "../../utils/task_canceller";
36
28
 
37
29
  /**
38
30
  * HTMLMediaElement Events for which playback observations are calculated and
@@ -93,19 +85,23 @@ export default class PlaybackObserver {
93
85
  private _internalSeekingEventsIncomingCounter : number;
94
86
 
95
87
  /**
96
- * Last playback observation made by the `PlaybackObserver`.
97
- *
98
- * `null` if no observation has been made yet.
88
+ * Stores the last playback observation produced by the `PlaybackObserver`.:
99
89
  */
100
- private _lastObservation : IPlaybackObservation | null;
90
+ private _observationRef : IReadOnlySharedReference<IPlaybackObservation>;
101
91
 
102
92
  /**
103
- * Lazily-created shared Observable that will emit playback observations.
104
- * Set to `null` until the first time it is generated.
93
+ * `TaskCanceller` allowing to free all resources and stop producing playback
94
+ * observations.
105
95
  */
106
- private _observation$ : Observable<IPlaybackObservation> | null;
96
+ private _canceller : TaskCanceller;
107
97
 
108
98
  /**
99
+ * Create a new `PlaybackObserver`, which allows to produce new "playback
100
+ * observations" on various media events and intervals.
101
+ *
102
+ * Note that creating a `PlaybackObserver` lead to the usage of resources,
103
+ * such as event listeners which will only be freed once the `stop` method is
104
+ * called.
109
105
  * @param {HTMLMediaElement} mediaElement
110
106
  * @param {Object} options
111
107
  */
@@ -114,8 +110,22 @@ export default class PlaybackObserver {
114
110
  this._mediaElement = mediaElement;
115
111
  this._withMediaSource = options.withMediaSource;
116
112
  this._lowLatencyMode = options.lowLatencyMode;
117
- this._lastObservation = null;
118
- this._observation$ = null;
113
+ this._canceller = new TaskCanceller();
114
+ this._observationRef = this._createSharedReference();
115
+ }
116
+
117
+ /**
118
+ * Stop the `PlaybackObserver` from emitting playback observations and free all
119
+ * resources reserved to emitting them such as event listeners, intervals and
120
+ * subscribing callbacks.
121
+ *
122
+ * Once `stop` is called, no new playback observation will ever be emitted.
123
+ *
124
+ * Note that it is important to call stop once the `PlaybackObserver` is no
125
+ * more needed to avoid unnecessarily leaking resources.
126
+ */
127
+ public stop() {
128
+ this._canceller.cancel();
119
129
  }
120
130
 
121
131
  /**
@@ -162,29 +172,17 @@ export default class PlaybackObserver {
162
172
  }
163
173
 
164
174
  /**
165
- * Returns an Observable regularly emitting playback observation, optionally
166
- * starting with the last one.
175
+ * Returns an `IReadOnlySharedReference` storing the last playback observation
176
+ * produced by the `PlaybackObserver` and updated each time a new one is
177
+ * produced.
167
178
  *
168
- * Note that this Observable is shared and unique, so that multiple `observe`
169
- * call will return the exact same Observable and multiple concurrent
170
- * `subscribe` will receive the same events at the same time.
171
- * This was done for performance and simplicity reasons.
179
+ * This value can then be for example subscribed to to be notified of future
180
+ * playback observations.
172
181
  *
173
- * @param {boolean} includeLastObservation
174
- * @returns {Observable}
182
+ * @returns {Object}
175
183
  */
176
- public observe(includeLastObservation : boolean) : Observable<IPlaybackObservation> {
177
- return observableDefer(() : Observable<IPlaybackObservation> => {
178
- if (this._observation$ === null || this._lastObservation === null) {
179
- this._lastObservation = this._generateInitialObservation();
180
- this._observation$ = this._createInnerObservable().pipe(share());
181
- return this.observe(includeLastObservation);
182
- } else {
183
- return includeLastObservation ?
184
- this._observation$.pipe(startWith(this._lastObservation)) :
185
- this._observation$;
186
- }
187
- });
184
+ public getReference() : IReadOnlySharedReference<IPlaybackObservation> {
185
+ return this._observationRef;
188
186
  }
189
187
 
190
188
  /**
@@ -195,26 +193,19 @@ export default class PlaybackObserver {
195
193
  * be first emitted synchronously.
196
194
  * - `clearSignal`: If set, the callback will be unregistered when this
197
195
  * CancellationSignal emits.
198
- * @returns {Function} - Allows to easily unregister the callback
199
196
  */
200
197
  public listen(
201
198
  cb : (observation : IPlaybackObservation) => void,
202
199
  options? : { includeLastObservation? : boolean | undefined;
203
200
  clearSignal? : CancellationSignal | undefined; }
204
- ) : () => void {
205
- if (options?.clearSignal?.isCancelled === true) {
201
+ ) {
202
+ if (this._canceller.isUsed || options?.clearSignal?.isCancelled === true) {
206
203
  return noop;
207
204
  }
208
- const sub = this.observe(options?.includeLastObservation ?? false)
209
- .subscribe(cb);
210
- const unregister = options?.clearSignal?.register(() => {
211
- sub.unsubscribe();
212
- }) ?? noop;
213
-
214
- return () => {
215
- unregister();
216
- sub.unsubscribe();
217
- };
205
+ this._observationRef.onUpdate(cb, {
206
+ clearSignal: options?.clearSignal,
207
+ emitCurrentValue: options?.includeLastObservation,
208
+ });
218
209
  }
219
210
 
220
211
  /**
@@ -228,99 +219,122 @@ export default class PlaybackObserver {
228
219
  *
229
220
  * As argument, this method takes a function which will allow to produce
230
221
  * the new set of properties to be present on each observation.
231
- * @param {Function} mapObservable
222
+ * @param {Function} transform
232
223
  * @returns {Object}
233
224
  */
234
225
  public deriveReadOnlyObserver<TDest>(
235
- mapObservable : (
236
- observation$ : Observable<IPlaybackObservation>
237
- ) => Observable<TDest>
226
+ transform : (
227
+ observationRef : IReadOnlySharedReference<IPlaybackObservation>,
228
+ cancellationSignal : CancellationSignal
229
+ ) => IReadOnlySharedReference<TDest>
238
230
  ) : IReadOnlyPlaybackObserver<TDest> {
239
- return generateReadOnlyObserver(this, mapObservable);
231
+ return generateReadOnlyObserver(this, transform, this._canceller.signal);
240
232
  }
241
233
 
242
234
  /**
243
- * Creates the observable that will generate playback observations.
235
+ * Creates the `IReadOnlySharedReference` that will generate playback
236
+ * observations.
244
237
  * @returns {Observable}
245
238
  */
246
- private _createInnerObservable() : Observable<IPlaybackObservation> {
247
-
248
-
249
- return observableDefer(() : Observable<IPlaybackObservation> => {
250
- const { SAMPLING_INTERVAL_MEDIASOURCE,
251
- SAMPLING_INTERVAL_LOW_LATENCY,
252
- SAMPLING_INTERVAL_NO_MEDIASOURCE } = config.getCurrent();
253
- const getCurrentObservation = (
254
- event : IPlaybackObserverEventType
255
- ) : IPlaybackObservation => {
256
- let tmpEvt: IPlaybackObserverEventType = event;
257
- if (tmpEvt === "seeking" && this._internalSeekingEventsIncomingCounter > 0) {
258
- tmpEvt = "internal-seeking";
259
- this._internalSeekingEventsIncomingCounter -= 1;
260
- }
261
- const lastObservation = this._lastObservation ??
262
- this._generateInitialObservation();
263
- const mediaTimings = getMediaInfos(this._mediaElement, tmpEvt);
264
- const internalSeeking = mediaTimings.seeking &&
265
- // We've just received the event for internally seeking
266
- (tmpEvt === "internal-seeking" ||
267
- // or We're still waiting on the previous internal-seek
268
- (lastObservation.internalSeeking && tmpEvt !== "seeking"));
269
- const rebufferingStatus = getRebufferingStatus(
270
- lastObservation,
271
- mediaTimings,
272
- { lowLatencyMode: this._lowLatencyMode,
273
- withMediaSource: this._withMediaSource });
274
-
275
- const freezingStatus = getFreezingStatus(lastObservation, mediaTimings);
276
- const timings = objectAssign(
277
- {},
278
- { rebuffering: rebufferingStatus,
279
- freezing: freezingStatus,
280
- internalSeeking },
281
- mediaTimings);
282
- if (log.hasLevel("DEBUG")) {
283
- log.debug("API: current media element state tick",
284
- "event", timings.event,
285
- "position", timings.position,
286
- "seeking", timings.seeking,
287
- "internalSeeking", timings.internalSeeking,
288
- "rebuffering", timings.rebuffering !== null,
289
- "freezing", timings.freezing !== null,
290
- "ended", timings.ended,
291
- "paused", timings.paused,
292
- "playbackRate", timings.playbackRate,
293
- "readyState", timings.readyState);
294
- }
295
- return timings;
239
+ private _createSharedReference() : IReadOnlySharedReference<IPlaybackObservation> {
240
+ if (this._observationRef !== undefined) {
241
+ return this._observationRef;
242
+ }
243
+
244
+ let lastObservation : IPlaybackObservation | null;
245
+ const { SAMPLING_INTERVAL_MEDIASOURCE,
246
+ SAMPLING_INTERVAL_LOW_LATENCY,
247
+ SAMPLING_INTERVAL_NO_MEDIASOURCE } = config.getCurrent();
248
+
249
+ const getCurrentObservation = (
250
+ event : IPlaybackObserverEventType
251
+ ) : IPlaybackObservation => {
252
+ let tmpEvt: IPlaybackObserverEventType = event;
253
+ if (tmpEvt === "seeking" && this._internalSeekingEventsIncomingCounter > 0) {
254
+ tmpEvt = "internal-seeking";
255
+ this._internalSeekingEventsIncomingCounter -= 1;
256
+ }
257
+ const _lastObservation = lastObservation ?? this._generateInitialObservation();
258
+ const mediaTimings = getMediaInfos(this._mediaElement, tmpEvt);
259
+ const internalSeeking = mediaTimings.seeking &&
260
+ // We've just received the event for internally seeking
261
+ (tmpEvt === "internal-seeking" ||
262
+ // or We're still waiting on the previous internal-seek
263
+ (_lastObservation.internalSeeking && tmpEvt !== "seeking"));
264
+ const rebufferingStatus = getRebufferingStatus(
265
+ _lastObservation,
266
+ mediaTimings,
267
+ { lowLatencyMode: this._lowLatencyMode,
268
+ withMediaSource: this._withMediaSource });
269
+
270
+ const freezingStatus = getFreezingStatus(_lastObservation, mediaTimings);
271
+ const timings = objectAssign(
272
+ {},
273
+ { rebuffering: rebufferingStatus,
274
+ freezing: freezingStatus,
275
+ internalSeeking },
276
+ mediaTimings);
277
+ if (log.hasLevel("DEBUG")) {
278
+ log.debug("API: current media element state tick",
279
+ "event", timings.event,
280
+ "position", timings.position,
281
+ "seeking", timings.seeking,
282
+ "internalSeeking", timings.internalSeeking,
283
+ "rebuffering", timings.rebuffering !== null,
284
+ "freezing", timings.freezing !== null,
285
+ "ended", timings.ended,
286
+ "paused", timings.paused,
287
+ "playbackRate", timings.playbackRate,
288
+ "readyState", timings.readyState);
289
+ }
290
+ return timings;
291
+ };
292
+
293
+ const returnedSharedReference = createSharedReference(getCurrentObservation("init"));
294
+
295
+ const generateObservationForEvent = (event : IPlaybackObserverEventType) => {
296
+ const newObservation = getCurrentObservation(event);
297
+ if (log.hasLevel("DEBUG")) {
298
+ log.debug("API: current playback timeline:\n" +
299
+ prettyPrintBuffered(newObservation.buffered,
300
+ newObservation.position),
301
+ `\n${event}`);
302
+ }
303
+ lastObservation = newObservation;
304
+ returnedSharedReference.setValue(newObservation);
305
+ };
306
+
307
+ const interval = this._lowLatencyMode ? SAMPLING_INTERVAL_LOW_LATENCY :
308
+ this._withMediaSource ? SAMPLING_INTERVAL_MEDIASOURCE :
309
+ SAMPLING_INTERVAL_NO_MEDIASOURCE;
310
+ let intervalId = setInterval(onInterval, interval);
311
+ const removeEventListeners = SCANNED_MEDIA_ELEMENTS_EVENTS.map((eventName) => {
312
+ this._mediaElement.addEventListener(eventName, onMediaEvent);
313
+ function onMediaEvent() {
314
+ restartInterval();
315
+ generateObservationForEvent(eventName);
316
+ }
317
+ return () => {
318
+ this._mediaElement.removeEventListener(eventName, onMediaEvent);
296
319
  };
320
+ });
297
321
 
298
- const eventObs : Array< Observable< IPlaybackObserverEventType > > =
299
- SCANNED_MEDIA_ELEMENTS_EVENTS.map((eventName) =>
300
- observableFromEvent(this._mediaElement, eventName)
301
- .pipe(map(() => eventName)));
302
-
303
- const interval = this._lowLatencyMode ? SAMPLING_INTERVAL_LOW_LATENCY :
304
- this._withMediaSource ? SAMPLING_INTERVAL_MEDIASOURCE :
305
- SAMPLING_INTERVAL_NO_MEDIASOURCE;
306
-
307
- const interval$ : Observable<"timeupdate"> =
308
- observableInterval(interval)
309
- .pipe(map(() => "timeupdate"));
310
-
311
- return observableMerge(interval$, ...eventObs).pipe(
312
- map((event : IPlaybackObserverEventType) => {
313
- const newObservation = getCurrentObservation(event);
314
- if (log.hasLevel("DEBUG")) {
315
- log.debug("API: current playback timeline:\n" +
316
- prettyPrintBuffered(newObservation.buffered,
317
- newObservation.position),
318
- `\n${event}`);
319
- }
320
- this._lastObservation = newObservation;
321
- return newObservation;
322
- }));
322
+ this._canceller.signal.register(() => {
323
+ clearInterval(intervalId);
324
+ removeEventListeners.forEach(cb => cb());
325
+ returnedSharedReference.finish();
323
326
  });
327
+
328
+ return returnedSharedReference;
329
+
330
+ function onInterval() {
331
+ generateObservationForEvent("timeupdate");
332
+ }
333
+
334
+ function restartInterval() {
335
+ clearInterval(intervalId);
336
+ intervalId = setInterval(onInterval, interval);
337
+ }
324
338
  }
325
339
 
326
340
  private _generateInitialObservation() : IPlaybackObservation {
@@ -472,18 +486,16 @@ export interface IReadOnlyPlaybackObserver<TObservationType> {
472
486
  */
473
487
  getIsPaused() : boolean;
474
488
  /**
475
- * Returns an Observable regularly emitting playback observation, optionally
476
- * starting with the last one.
489
+ * Returns an `IReadOnlySharedReference` storing the last playback observation
490
+ * produced by the `IReadOnlyPlaybackObserver` and updated each time a new one
491
+ * is produced.
477
492
  *
478
- * Note that this Observable is shared and unique, so that multiple `observe`
479
- * call will return the exact same Observable and multiple concurrent
480
- * `subscribe` will receive the same events at the same time.
481
- * This was done for performance and simplicity reasons.
493
+ * This value can then be for example subscribed to to be notified of future
494
+ * playback observations.
482
495
  *
483
- * @param {boolean} includeLastObservation
484
- * @returns {Observable}
496
+ * @returns {Object}
485
497
  */
486
- observe(includeLastObservation : boolean) : Observable<TObservationType>;
498
+ getReference() : IReadOnlySharedReference<TObservationType>;
487
499
  /**
488
500
  * Register a callback so it regularly receives playback observations.
489
501
  * @param {Function} cb
@@ -498,18 +510,20 @@ export interface IReadOnlyPlaybackObserver<TObservationType> {
498
510
  cb : (observation : TObservationType) => void,
499
511
  options? : { includeLastObservation? : boolean | undefined;
500
512
  clearSignal? : CancellationSignal | undefined; }
501
- ) : () => void;
513
+ ) : void;
502
514
  /**
503
515
  * Generate a new `IReadOnlyPlaybackObserver` from this one.
504
516
  *
505
517
  * As argument, this method takes a function which will allow to produce
506
518
  * the new set of properties to be present on each observation.
507
- * @param {Function} mapObservable
519
+ * @param {Function} transform
508
520
  * @returns {Object}
509
521
  */
510
522
  deriveReadOnlyObserver<TDest>(
511
- mapObservable : (observation$ : Observable<TObservationType>) => Observable<TDest>,
512
- mapObservation : (observation : TObservationType) => TDest
523
+ transform : (
524
+ observationRef : IReadOnlySharedReference<TObservationType>,
525
+ cancellationSignal : CancellationSignal
526
+ ) => IReadOnlySharedReference<TDest>
513
527
  ) : IReadOnlyPlaybackObserver<TDest>;
514
528
  }
515
529
 
@@ -826,16 +840,18 @@ function prettyPrintBuffered(
826
840
  * Create `IReadOnlyPlaybackObserver` from a source `IReadOnlyPlaybackObserver`
827
841
  * and a mapping function.
828
842
  * @param {Object} src
829
- * @param {Function} mapObservable
843
+ * @param {Function} transform
830
844
  * @returns {Object}
831
845
  */
832
846
  function generateReadOnlyObserver<TSource, TDest>(
833
847
  src : IReadOnlyPlaybackObserver<TSource>,
834
- mapObservable : (observation$ : Observable<TSource>) => Observable<TDest>
848
+ transform : (
849
+ observationRef : IReadOnlySharedReference<TSource>,
850
+ cancellationSignal : CancellationSignal
851
+ ) => IReadOnlySharedReference<TDest>,
852
+ cancellationSignal : CancellationSignal
835
853
  ) : IReadOnlyPlaybackObserver<TDest> {
836
- const newObs = observableDefer(() =>
837
- mapObservable(src.observe(true))
838
- ).pipe(shareReplay({ bufferSize: 1, refCount: true }));
854
+ const mappedRef = transform(src.getReference(), cancellationSignal);
839
855
  return {
840
856
  getCurrentTime() {
841
857
  return src.getCurrentTime();
@@ -846,33 +862,29 @@ function generateReadOnlyObserver<TSource, TDest>(
846
862
  getIsPaused() {
847
863
  return src.getIsPaused();
848
864
  },
849
- observe(includeLastObservation : boolean) : Observable<TDest> {
850
- return includeLastObservation ? newObs :
851
- newObs.pipe(skip(1));
865
+ getReference() : IReadOnlySharedReference<TDest> {
866
+ return mappedRef;
852
867
  },
853
868
  listen(
854
869
  cb : (observation : TDest) => void,
855
870
  options? : { includeLastObservation? : boolean | undefined;
856
871
  clearSignal? : CancellationSignal | undefined; }
857
- ) : () => void {
858
- if (options?.clearSignal?.isCancelled === true) {
859
- return noop;
872
+ ) : void {
873
+ if (cancellationSignal.isCancelled || options?.clearSignal?.isCancelled === true) {
874
+ return ;
860
875
  }
861
- const obs = options?.includeLastObservation === true ? newObs :
862
- newObs.pipe(skip(1));
863
- const sub = obs.subscribe(cb);
864
- const unregister = options?.clearSignal?.register(() => {
865
- sub.unsubscribe();
866
- }) ?? noop;
867
- return () => {
868
- unregister();
869
- sub.unsubscribe();
870
- };
876
+ mappedRef.onUpdate(cb, {
877
+ clearSignal: options?.clearSignal,
878
+ emitCurrentValue: options?.includeLastObservation,
879
+ });
871
880
  },
872
881
  deriveReadOnlyObserver<TNext>(
873
- newUdateObserver : (observation$ : Observable<TDest>) => Observable<TNext>
882
+ newTransformFn : (
883
+ observationRef : IReadOnlySharedReference<TDest>,
884
+ signal : CancellationSignal
885
+ ) => IReadOnlySharedReference<TNext>
874
886
  ) : IReadOnlyPlaybackObserver<TNext> {
875
- return generateReadOnlyObserver(this, newUdateObserver);
887
+ return generateReadOnlyObserver(this, newTransformFn, cancellationSignal);
876
888
  },
877
889
  };
878
890
  }
@@ -170,7 +170,10 @@ class Player extends EventEmitter<IPublicAPIEvent> {
170
170
  /** Current version of the RxPlayer. */
171
171
  public readonly version : string;
172
172
 
173
- /** Media element attached to the RxPlayer. */
173
+ /**
174
+ * Media element attached to the RxPlayer.
175
+ * Set to `null` when the RxPlayer is disposed.
176
+ */
174
177
  public videoElement : HTMLMediaElement|null; // null on dispose
175
178
 
176
179
  /** Logger the RxPlayer uses. */
@@ -429,7 +432,7 @@ class Player extends EventEmitter<IPublicAPIEvent> {
429
432
  // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1194624
430
433
  videoElement.preload = "auto";
431
434
 
432
- this.version = /* PLAYER_VERSION */"3.28.0-dev.2022061700";
435
+ this.version = /* PLAYER_VERSION */"3.28.0-dev.2022062700";
433
436
  this.log = log;
434
437
  this.state = "STOPPED";
435
438
  this.videoElement = videoElement;
@@ -720,6 +723,10 @@ class Player extends EventEmitter<IPublicAPIEvent> {
720
723
  lowLatencyMode,
721
724
  });
722
725
 
726
+ currentContentCanceller.signal.register(() => {
727
+ playbackObserver.stop();
728
+ });
729
+
723
730
  /** Emit playback events. */
724
731
  let playback$ : Connectable<IInitEvent>;
725
732
 
@@ -971,7 +978,7 @@ class Player extends EventEmitter<IPublicAPIEvent> {
971
978
  share());
972
979
 
973
980
  /** Emit when the media element emits a "seeking" event. */
974
- const observation$ = playbackObserver.observe(true);
981
+ const observation$ = playbackObserver.getReference().asObservable();
975
982
 
976
983
  const stateChangingEvent$ = observation$.pipe(filter(o => {
977
984
  return o.event === "seeking" || o.event === "ended" ||
@@ -1051,12 +1058,18 @@ class Player extends EventEmitter<IPublicAPIEvent> {
1051
1058
  currentContentCanceller.cancel();
1052
1059
  });
1053
1060
 
1054
-
1055
1061
  // Link playback events to the corresponding callbacks
1056
1062
  playback$.subscribe({
1057
1063
  next: (x) => this._priv_onPlaybackEvent(x),
1058
1064
  error: (err : Error) => this._priv_onPlaybackError(err),
1059
- complete: () => this._priv_onPlaybackFinished(),
1065
+ complete: () => {
1066
+ if (!contentInfos.currentContentCanceller.isUsed) {
1067
+ log.info("API: Previous playback finished. Stopping and cleaning-up...");
1068
+ contentInfos.currentContentCanceller.cancel();
1069
+ this._priv_cleanUpCurrentContentState();
1070
+ this._priv_setPlayerState(PLAYER_STATES.STOPPED);
1071
+ }
1072
+ },
1060
1073
  });
1061
1074
 
1062
1075
  // initialize the content only when the lock is inactive
@@ -2319,8 +2332,10 @@ class Player extends EventEmitter<IPublicAPIEvent> {
2319
2332
 
2320
2333
  // DRM-related clean-up
2321
2334
  const freeUpContentLock = () => {
2322
- log.debug("Unlocking `contentLock`. Next content can begin.");
2323
- this._priv_contentLock.setValue(false);
2335
+ if (this.videoElement !== null) { // If not disposed
2336
+ log.debug("Unlocking `contentLock`. Next content can begin.");
2337
+ this._priv_contentLock.setValue(false);
2338
+ }
2324
2339
  };
2325
2340
 
2326
2341
  if (!isNullOrUndefined(this.videoElement)) {
@@ -2450,19 +2465,6 @@ class Player extends EventEmitter<IPublicAPIEvent> {
2450
2465
  }
2451
2466
  }
2452
2467
 
2453
- /**
2454
- * Triggered when the playback Observable completes.
2455
- * Clean-up ressources and signal that the content has ended.
2456
- */
2457
- private _priv_onPlaybackFinished() : void {
2458
- log.info("API: Previous playback finished. Stopping and cleaning-up...");
2459
- if (this._priv_contentInfos !== null) {
2460
- this._priv_contentInfos.currentContentCanceller.cancel();
2461
- }
2462
- this._priv_cleanUpCurrentContentState();
2463
- this._priv_setPlayerState(PLAYER_STATES.ENDED);
2464
- }
2465
-
2466
2468
  /**
2467
2469
  * Triggered when we received a warning event during playback.
2468
2470
  * Trigger the right API event.
@@ -2941,7 +2943,7 @@ class Player extends EventEmitter<IPublicAPIEvent> {
2941
2943
  return activeRepresentations[currentPeriod.id];
2942
2944
  }
2943
2945
  }
2944
- Player.version = /* PLAYER_VERSION */"3.28.0-dev.2022061700";
2946
+ Player.version = /* PLAYER_VERSION */"3.28.0-dev.2022062700";
2945
2947
 
2946
2948
  /** Every events sent by the RxPlayer's public API. */
2947
2949
  interface IPublicAPIEvent {