@amazon/vinyl 4.0.0 → 4.0.1-next.20261016184530

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 (76) hide show
  1. package/dist/index.cjs +40 -3
  2. package/dist/index.cjs.map +2 -2
  3. package/dist/index.js +41 -3
  4. package/dist/index.js.map +2 -2
  5. package/dist/test/benchmark.es5.js +5 -4
  6. package/dist/test/benchmark.js +11 -2
  7. package/dist/test/benchmark.js.map +3 -3
  8. package/dist/test/errata/mse-changetype-sequence.html +65 -0
  9. package/dist/test/errata/mse-changetype-sequence.js +179 -0
  10. package/dist/test/index.html +2 -2
  11. package/dist/test/integ.es5.js +287 -276
  12. package/dist/test/integ.js +629 -245
  13. package/dist/test/integ.js.map +4 -4
  14. package/dist/test/styles.css +23 -13
  15. package/dist/types/current/playback/PlaybackControllerImpl.d.ts +12 -0
  16. package/dist/types/current/playback/PlaybackControllerImpl.d.ts.map +1 -1
  17. package/dist/types/current/playback/PlaybackControllerImpl.js +42 -1
  18. package/dist/types/current/streaming/buffering/BufferingController.d.ts.map +1 -1
  19. package/dist/types/current/streaming/buffering/BufferingController.js +9 -2
  20. package/dist/types/current/streaming/buffering/MediaSourceController.d.ts.map +1 -1
  21. package/dist/types/current/streaming/buffering/MediaSourceController.js +4 -2
  22. package/dist/types/current/tsconfig.tsbuildinfo +1 -1
  23. package/dist/types/ts3.7/playback/PlaybackControllerImpl.d.ts +12 -0
  24. package/dist/types/ts4.1/playback/PlaybackControllerImpl.d.ts +12 -0
  25. package/dist/types/ts4.7/playback/PlaybackControllerImpl.d.ts +12 -0
  26. package/dist/unit/types/current/playback/PlaybackControllerImpl.test.js +86 -0
  27. package/dist/unit/types/current/setup.js +6 -5
  28. package/dist/unit/types/current/streaming/SegmentControllerImpl.test.js +44 -1
  29. package/dist/unit/types/current/streaming/buffering/BufferingController.test.js +14 -0
  30. package/dist/unit/types/current/streaming/buffering/MediaSourceController.test.js +13 -0
  31. package/dist/unit/types/current/track/dash/createDashFactories.test.js +15 -5
  32. package/dist/unit/types/current/track/hls/createHlsFactories.test.js +36 -12
  33. package/dist/unit/types/current/tsconfig.tsbuildinfo +1 -1
  34. package/dist/unit/types/current/vinyl/VinylPlayerAdBreaks.test.js +8 -5
  35. package/dist/vinylTestUtil/index.js +92 -60
  36. package/dist/vinylTestUtil/index.js.map +4 -4
  37. package/dist/vinylTestUtil/types/current/media/FrequencyAnalyzer.d.ts +8 -5
  38. package/dist/vinylTestUtil/types/current/media/FrequencyAnalyzer.d.ts.map +1 -1
  39. package/dist/vinylTestUtil/types/current/media/FrequencyAnalyzer.js +23 -29
  40. package/dist/vinylTestUtil/types/current/media/unlockMediaElement.d.ts +10 -1
  41. package/dist/vinylTestUtil/types/current/media/unlockMediaElement.d.ts.map +1 -1
  42. package/dist/vinylTestUtil/types/current/media/unlockMediaElement.js +34 -6
  43. package/dist/vinylTestUtil/types/current/player/createVinylSuite.d.ts.map +1 -1
  44. package/dist/vinylTestUtil/types/current/player/createVinylSuite.js +6 -1
  45. package/dist/vinylTestUtil/types/current/player/mediaRef.d.ts +7 -0
  46. package/dist/vinylTestUtil/types/current/player/mediaRef.d.ts.map +1 -1
  47. package/dist/vinylTestUtil/types/current/player/mediaRef.js +8 -1
  48. package/dist/vinylTestUtil/types/current/tsconfig.tsbuildinfo +1 -1
  49. package/dist/vinylTestUtil/types/current/ui/requireUserInteraction.d.ts +6 -2
  50. package/dist/vinylTestUtil/types/current/ui/requireUserInteraction.d.ts.map +1 -1
  51. package/dist/vinylTestUtil/types/current/ui/requireUserInteraction.js +31 -15
  52. package/dist/vinylTestUtil/types/current/util/playback/expectTrackPlays.d.ts +9 -2
  53. package/dist/vinylTestUtil/types/current/util/playback/expectTrackPlays.d.ts.map +1 -1
  54. package/dist/vinylTestUtil/types/current/util/playback/expectTrackPlays.js +10 -7
  55. package/dist/vinylTestUtil/types/current/util/playback/expectTrackPlaysUntil.d.ts +4 -1
  56. package/dist/vinylTestUtil/types/current/util/playback/expectTrackPlaysUntil.d.ts.map +1 -1
  57. package/dist/vinylTestUtil/types/current/util/playback/expectTrackPlaysUntil.js +3 -2
  58. package/dist/vinylTestUtil/types/ts3.7/media/FrequencyAnalyzer.d.ts +8 -5
  59. package/dist/vinylTestUtil/types/ts3.7/media/unlockMediaElement.d.ts +10 -1
  60. package/dist/vinylTestUtil/types/ts3.7/player/mediaRef.d.ts +7 -0
  61. package/dist/vinylTestUtil/types/ts3.7/ui/requireUserInteraction.d.ts +6 -2
  62. package/dist/vinylTestUtil/types/ts3.7/util/playback/expectTrackPlays.d.ts +9 -2
  63. package/dist/vinylTestUtil/types/ts3.7/util/playback/expectTrackPlaysUntil.d.ts +4 -1
  64. package/dist/vinylTestUtil/types/ts4.1/media/FrequencyAnalyzer.d.ts +8 -5
  65. package/dist/vinylTestUtil/types/ts4.1/media/unlockMediaElement.d.ts +10 -1
  66. package/dist/vinylTestUtil/types/ts4.1/player/mediaRef.d.ts +7 -0
  67. package/dist/vinylTestUtil/types/ts4.1/ui/requireUserInteraction.d.ts +6 -2
  68. package/dist/vinylTestUtil/types/ts4.1/util/playback/expectTrackPlays.d.ts +9 -2
  69. package/dist/vinylTestUtil/types/ts4.1/util/playback/expectTrackPlaysUntil.d.ts +4 -1
  70. package/dist/vinylTestUtil/types/ts4.7/media/FrequencyAnalyzer.d.ts +8 -5
  71. package/dist/vinylTestUtil/types/ts4.7/media/unlockMediaElement.d.ts +10 -1
  72. package/dist/vinylTestUtil/types/ts4.7/player/mediaRef.d.ts +7 -0
  73. package/dist/vinylTestUtil/types/ts4.7/ui/requireUserInteraction.d.ts +6 -2
  74. package/dist/vinylTestUtil/types/ts4.7/util/playback/expectTrackPlays.d.ts +9 -2
  75. package/dist/vinylTestUtil/types/ts4.7/util/playback/expectTrackPlaysUntil.d.ts +4 -1
  76. package/package.json +11 -10
@@ -180,6 +180,18 @@ export declare class PlaybackControllerImpl extends EventHostImpl<PlaybackContro
180
180
  * @private
181
181
  */
182
182
  private doSeek;
183
+ /**
184
+ * Dispatches `waiting` when a seek while playing targets an unbuffered position and the media element did not.
185
+ * Safari 27 keeps its readyState while seeking, so it never fires `waiting` for the seek.
186
+ * @private
187
+ */
188
+ private initializeSeekWaitingEvent;
189
+ /**
190
+ * Dispatches `playing` when a seek while playing completes and the media element did not. A seek ends `playing`,
191
+ * but WebKit only fires `playing` again if its readyState dropped, which it doesn't for a buffered position.
192
+ * @private
193
+ */
194
+ private initializeSeekPlayingEvent;
183
195
  /**
184
196
  * Dispatches a readyStateChange event when {@link readyState} changes.
185
197
  * @private
@@ -182,6 +182,18 @@ export declare class PlaybackControllerImpl extends EventHostImpl<PlaybackContro
182
182
  * @private
183
183
  */
184
184
  private doSeek;
185
+ /**
186
+ * Dispatches `waiting` when a seek while playing targets an unbuffered position and the media element did not.
187
+ * Safari 27 keeps its readyState while seeking, so it never fires `waiting` for the seek.
188
+ * @private
189
+ */
190
+ private initializeSeekWaitingEvent;
191
+ /**
192
+ * Dispatches `playing` when a seek while playing completes and the media element did not. A seek ends `playing`,
193
+ * but WebKit only fires `playing` again if its readyState dropped, which it doesn't for a buffered position.
194
+ * @private
195
+ */
196
+ private initializeSeekPlayingEvent;
185
197
  /**
186
198
  * Dispatches a readyStateChange event when {@link readyState} changes.
187
199
  * @private
@@ -180,6 +180,18 @@ export declare class PlaybackControllerImpl extends EventHostImpl<PlaybackContro
180
180
  * @private
181
181
  */
182
182
  private doSeek;
183
+ /**
184
+ * Dispatches `waiting` when a seek while playing targets an unbuffered position and the media element did not.
185
+ * Safari 27 keeps its readyState while seeking, so it never fires `waiting` for the seek.
186
+ * @private
187
+ */
188
+ private initializeSeekWaitingEvent;
189
+ /**
190
+ * Dispatches `playing` when a seek while playing completes and the media element did not. A seek ends `playing`,
191
+ * but WebKit only fires `playing` again if its readyState dropped, which it doesn't for a buffered position.
192
+ * @private
193
+ */
194
+ private initializeSeekPlayingEvent;
183
195
  /**
184
196
  * Dispatches a readyStateChange event when {@link readyState} changes.
185
197
  * @private
@@ -1024,6 +1024,92 @@ describe('PlaybackController', () => {
1024
1024
  expect(waitedSpy).not.toHaveBeenCalled();
1025
1025
  });
1026
1026
  });
1027
+ describe('when a seek while playing targets an unbuffered position', () => {
1028
+ let waitingSpy;
1029
+ beforeEach(() => {
1030
+ waitingSpy = createSpy('waiting');
1031
+ controller.on('waiting', waitingSpy);
1032
+ media.buffered = new MockTimeRanges([[0, 20]]);
1033
+ media.currentTime = 22;
1034
+ media.seeking = true;
1035
+ });
1036
+ it('emits waiting if the media element does not', async () => {
1037
+ media.dispatchEvent(mockEvent('seeking'));
1038
+ expect(waitingSpy).not.toHaveBeenCalled();
1039
+ await clock.tick();
1040
+ expect(waitingSpy).toHaveBeenCalledTimes(1);
1041
+ expect(controller.waiting).toBeTrue();
1042
+ media.dispatchEvent(mockEvent('waiting'));
1043
+ expect(waitingSpy).toHaveBeenCalledTimes(1);
1044
+ media.seeking = false;
1045
+ media.dispatchEvent(mockEvent('playing'));
1046
+ expect(controller.waiting).toBeFalse();
1047
+ });
1048
+ it('emits waiting once if the media element does', async () => {
1049
+ media.dispatchEvent(mockEvent('seeking'));
1050
+ media.dispatchEvent(mockEvent('waiting'));
1051
+ await clock.tick();
1052
+ expect(waitingSpy).toHaveBeenCalledTimes(1);
1053
+ });
1054
+ it('does not emit waiting when paused', async () => {
1055
+ media.paused = true;
1056
+ media.dispatchEvent(mockEvent('seeking'));
1057
+ await clock.tick();
1058
+ expect(waitingSpy).not.toHaveBeenCalled();
1059
+ });
1060
+ it('does not emit waiting when the seek completes first', async () => {
1061
+ media.dispatchEvent(mockEvent('seeking'));
1062
+ media.seeking = false;
1063
+ media.dispatchEvent(mockEvent('seeked'));
1064
+ await clock.tick();
1065
+ expect(waitingSpy).not.toHaveBeenCalled();
1066
+ });
1067
+ });
1068
+ describe('when a seek while playing completes', () => {
1069
+ let playingSpy;
1070
+ beforeEach(() => {
1071
+ media.readyState = PlaybackReadyState.HAVE_ENOUGH_DATA;
1072
+ media.dispatchEvent(mockEvent('playing'));
1073
+ playingSpy = createSpy('playing');
1074
+ controller.on('playing', playingSpy);
1075
+ media.dispatchEvent(mockEvent('seeking'));
1076
+ expect(controller.playing).toBeFalse();
1077
+ });
1078
+ it('emits playing if the media element does not', () => {
1079
+ media.dispatchEvent(mockEvent('seeked'));
1080
+ expect(playingSpy).toHaveBeenCalledTimes(1);
1081
+ expect(controller.playing).toBeTrue();
1082
+ media.dispatchEvent(mockEvent('playing'));
1083
+ expect(playingSpy).toHaveBeenCalledTimes(1);
1084
+ });
1085
+ it('emits playing once if the media element does', () => {
1086
+ media.dispatchEvent(mockEvent('playing'));
1087
+ media.dispatchEvent(mockEvent('seeked'));
1088
+ expect(playingSpy).toHaveBeenCalledTimes(1);
1089
+ });
1090
+ it('does not emit playing when paused', () => {
1091
+ media.paused = true;
1092
+ media.dispatchEvent(mockEvent('seeked'));
1093
+ expect(playingSpy).not.toHaveBeenCalled();
1094
+ });
1095
+ it('does not emit playing without future data', () => {
1096
+ media.readyState = PlaybackReadyState.HAVE_CURRENT_DATA;
1097
+ media.dispatchEvent(mockEvent('seeked'));
1098
+ expect(playingSpy).not.toHaveBeenCalled();
1099
+ });
1100
+ });
1101
+ describe('when a seek while playing targets a buffered position', () => {
1102
+ it('does not emit waiting', async () => {
1103
+ const waitingSpy = createSpy('waiting');
1104
+ controller.on('waiting', waitingSpy);
1105
+ media.buffered = new MockTimeRanges([[0, 30]]);
1106
+ media.currentTime = 22;
1107
+ media.seeking = true;
1108
+ media.dispatchEvent(mockEvent('seeking'));
1109
+ await clock.tick();
1110
+ expect(waitingSpy).not.toHaveBeenCalled();
1111
+ });
1112
+ });
1027
1113
  });
1028
1114
  describe('pause', () => {
1029
1115
  it('invokes pause on the element', () => {
@@ -2,24 +2,25 @@
2
2
  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
- import { getGlobalRegistry, setGlobalRefDebug } from '@amazon/vinyl-util';
5
+ import { getGlobalRegistry, getSearchParams, setGlobalRefDebug, } from '@amazon/vinyl-util';
6
6
  import { addCustomMatchers, ConsoleReporter, flushPromises, } from '@amazon/vinyl-util/browserTestUtil';
7
7
  import { RestApiReporter } from '@amazon/vinyl-util/testUtil';
8
- import { initializeFrequencyAnalyzer } from '@amazon/vinyl/vinylTestUtil';
8
+ import { initializeFrequencyAnalyzer, initializeMediaUnlock, } from '@amazon/vinyl/vinylTestUtil';
9
9
  addCustomMatchers();
10
10
  // Diagnostic: GlobalRefImpl records the stack
11
11
  setGlobalRefDebug(true);
12
12
  // Configured here instead of the jasmine.config.json to work with both ts-node and the html
13
- // test runner.
13
+ // test runner. `?stopOnSpecFailure=false` runs every spec, as test:integ does.
14
14
  jasmine.getEnv().configure({
15
- stopOnSpecFailure: true,
15
+ stopOnSpecFailure: getSearchParams().get('stopOnSpecFailure') !== 'false',
16
16
  stopSpecOnExpectationFailure: true,
17
17
  failSpecWithNoExpectations: true,
18
18
  });
19
19
  // Register the reporters
20
20
  jasmine.getEnv().addReporter(new ConsoleReporter());
21
21
  RestApiReporter.installFromSearchParam();
22
- // Prompts for user interaction and unlocks the AudioContext if checkAudio is present in the query params.
22
+ // In a browser, prompts for a click to unlock media playback, then attaches the analyzer if checkAudio is set.
23
+ initializeMediaUnlock();
23
24
  initializeFrequencyAnalyzer();
24
25
  afterEach(async () => {
25
26
  await flushPromises();
@@ -2,7 +2,7 @@
2
2
  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
- import { createEmptyMediaQualityMetadata, defaultSegmentControllerBaseOptions, PREFETCH_POLL_THROTTLE, prefetchPriorityQueuesRef, SegmentControllerImpl, } from '@amazon/vinyl';
5
+ import { clearCodecDenylist, createEmptyMediaQualityMetadata, denylistCodecsFromMimeType, defaultSegmentControllerBaseOptions, PREFETCH_POLL_THROTTLE, prefetchPriorityQueuesRef, SegmentControllerImpl, } from '@amazon/vinyl';
6
6
  import { Abort, AbortError, Deferred, floorToNearest, mergeDeep, networkState, noop, sleep, } from '@amazon/vinyl-util';
7
7
  import { MockPlaybackController } from '@amazon/vinyl/vinylTestUtil';
8
8
  import { data } from '@amazon/vinyl-observable';
@@ -362,6 +362,49 @@ describe('SegmentControllerImpl', () => {
362
362
  // segmentController is for 'audio', but only 'video' qualities exist
363
363
  expect(await segmentController.getSegment(5)).toBeNull();
364
364
  });
365
+ describe('when a codec has been denylisted', () => {
366
+ const OPUS = 'audio/mp4; codecs="opus"';
367
+ const AAC = 'audio/mp4; codecs="mp4a.40.2"';
368
+ let selectQuality;
369
+ beforeEach(() => {
370
+ timelineData.value = Promise.resolve({
371
+ periods: [
372
+ {
373
+ startTime: 0,
374
+ endTime: 100,
375
+ qualities: [OPUS, AAC].map((mimeType, i) => ({
376
+ metadata: {
377
+ ...createEmptyMediaQualityMetadata(),
378
+ contentType: 'audio',
379
+ mimeType,
380
+ decoderId: String(i),
381
+ },
382
+ getSegment: (time) => segmentProvider.getSegment(time),
383
+ })),
384
+ },
385
+ ],
386
+ minBufferTime: 10,
387
+ getAdBreaks: () => Promise.resolve([]),
388
+ getDuration: () => Promise.resolve(100),
389
+ });
390
+ selectQuality = spyOn(mockDeps.qualitySelector, 'selectQuality').and.returnValue(0);
391
+ });
392
+ afterEach(() => clearCodecDenylist());
393
+ function selectedFrom() {
394
+ return selectQuality.calls.mostRecent().args[0].map((metadata) => metadata.mimeType);
395
+ }
396
+ it('selects only from qualities whose codecs are not denylisted', async () => {
397
+ denylistCodecsFromMimeType(OPUS);
398
+ await segmentController.getSegment(5);
399
+ expect(selectedFrom()).toEqual([AAC]);
400
+ });
401
+ it('selects from every quality if all of their codecs are denylisted', async () => {
402
+ denylistCodecsFromMimeType(OPUS);
403
+ denylistCodecsFromMimeType(AAC);
404
+ await segmentController.getSegment(5);
405
+ expect(selectedFrom()).toEqual([OPUS, AAC]);
406
+ });
407
+ });
365
408
  it('returns null when quality getSegment returns null within period', async () => {
366
409
  await fillPrefetch();
367
410
  segmentController.clear();
@@ -447,6 +447,20 @@ describe('BufferingControllerImpl', () => {
447
447
  await nextPollImmediate();
448
448
  expect(sourceBufferController.appendInit).toHaveBeenCalledOnceWith(any(ArrayBuffer), 'opus');
449
449
  });
450
+ it('continues after the buffered segment once the new initialization segment is appended', async () => {
451
+ await open();
452
+ sourceBufferController.setTimestampOffset.calls.reset();
453
+ segmentController.getSegment.and.callFake((time) => {
454
+ const mockSegment = createMockSegment(time);
455
+ if (mockSegment.startTime >= 10) {
456
+ mockSegment.quality.decoderId = 'next period';
457
+ mockSegment.timestampOffset = mockSegment.startTime - 4;
458
+ }
459
+ return Promise.resolve(mockSegment);
460
+ });
461
+ await setTime(9);
462
+ expect(sourceBufferController.setTimestampOffset).toHaveBeenCalledOnceWith(10);
463
+ });
450
464
  });
451
465
  describe('when segment controller returns null', () => {
452
466
  beforeEach(() => {
@@ -256,6 +256,11 @@ describe('MediaSourceControllerImpl', () => {
256
256
  expect(() => controller.createSourceBuffer('audio', 'invalid/type')).toThrowError(MediaSourceError, 'error creating source buffer');
257
257
  });
258
258
  describe('SourceBufferRef disposal', () => {
259
+ beforeEach(() => {
260
+ mockMediaSource.sourceBuffers = [
261
+ mockSourceBuffer,
262
+ ];
263
+ });
259
264
  it('removes source buffer from media source', () => {
260
265
  mockMediaSource.readyState = 'open';
261
266
  const ref = controller.createSourceBuffer('audio', 'audio/mp4');
@@ -269,6 +274,14 @@ describe('MediaSourceControllerImpl', () => {
269
274
  ref.dispose();
270
275
  expect(mockMediaSource.removeSourceBuffer).not.toHaveBeenCalled();
271
276
  });
277
+ it('does not remove a source buffer the media source already removed', () => {
278
+ mockMediaSource.readyState = 'open';
279
+ const ref = controller.createSourceBuffer('audio', 'audio/mp4');
280
+ mockMediaSource.sourceBuffers =
281
+ [];
282
+ ref.dispose();
283
+ expect(mockMediaSource.removeSourceBuffer).not.toHaveBeenCalled();
284
+ });
272
285
  });
273
286
  });
274
287
  describe('endOfStream', () => {
@@ -46,7 +46,8 @@ describe('createDashFactories', () => {
46
46
  type: 'dash',
47
47
  manifestProvider,
48
48
  });
49
- const deps = createContainer(factories).dependencies;
49
+ const container = createContainer(factories);
50
+ const deps = container.dependencies;
50
51
  expect(deps).toEqual(objectContaining({
51
52
  baseUrlSelector: pickFirstBaseUrlSelector,
52
53
  segmentRequestInit: undefined,
@@ -59,6 +60,7 @@ describe('createDashFactories', () => {
59
60
  contentStreamFactory: any(Function),
60
61
  qualitySelector: any(QualitySelectorImpl),
61
62
  }));
63
+ container.dispose();
62
64
  });
63
65
  it('wires per-track option observables from the player options', () => {
64
66
  const factoryCreator = createDashFactories(null);
@@ -67,11 +69,13 @@ describe('createDashFactories', () => {
67
69
  type: 'dash',
68
70
  manifestProvider,
69
71
  });
70
- const deps = createContainer(factories).dependencies;
72
+ const container = createContainer(factories);
73
+ const deps = container.dependencies;
71
74
  const options = dashFactoryDeps.options.value;
72
75
  expect(deps.audio.value).toBe(options.audio);
73
76
  expect(deps.allowedContentTypes.value).toBe(options.allowedContentTypes);
74
77
  expect(deps.abr.value).toBe(options.abr);
78
+ container.dispose();
75
79
  });
76
80
  it('provides streaming options to createContentStreamFactories', () => {
77
81
  const factoryCreator = createDashFactories({
@@ -86,8 +90,10 @@ describe('createDashFactories', () => {
86
90
  type: 'dash',
87
91
  manifestProvider,
88
92
  });
89
- const deps = createContainer(factories).dependencies;
93
+ const container = createContainer(factories);
94
+ const deps = container.dependencies;
90
95
  expect(deps.contentStreamFactory('audio')).toEqual(any(Object));
96
+ container.dispose();
91
97
  });
92
98
  it('creates a sidecar text track controller and populates from manifest', async () => {
93
99
  const factoryCreator = createDashFactories(null);
@@ -114,12 +120,14 @@ describe('createDashFactories', () => {
114
120
  type: 'dash',
115
121
  manifestProvider: provider,
116
122
  });
117
- const deps = createContainer(factories).dependencies;
123
+ const container = createContainer(factories);
124
+ const deps = container.dependencies;
118
125
  const ttc = deps.textTrackController;
119
126
  await deps.manifestTransformed.value;
120
127
  await new Promise((r) => setTimeout(r, 0));
121
128
  expect(ttc).toBeDefined();
122
129
  expect(ttc.textTracks.length).toBeGreaterThan(0);
130
+ container.dispose();
123
131
  });
124
132
  it('text track controller swallows manifest fetch errors', async () => {
125
133
  const factoryCreator = createDashFactories(null);
@@ -129,10 +137,12 @@ describe('createDashFactories', () => {
129
137
  type: 'dash',
130
138
  manifestProvider: provider,
131
139
  });
132
- const deps = createContainer(factories).dependencies;
140
+ const container = createContainer(factories);
141
+ const deps = container.dependencies;
133
142
  const controller = deps.textTrackController;
134
143
  await deps.manifestTransformed.value.catch(() => undefined);
135
144
  await new Promise((r) => setTimeout(r, 0));
136
145
  expect(controller.textTracks).toEqual([]);
146
+ container.dispose();
137
147
  });
138
148
  });
@@ -36,7 +36,8 @@ describe('createHlsFactories', () => {
36
36
  type: 'hls',
37
37
  manifestProvider,
38
38
  });
39
- const deps = createContainer(factories).dependencies;
39
+ const container = createContainer(factories);
40
+ const deps = container.dependencies;
40
41
  expect(deps).toEqual(objectContaining({
41
42
  segmentRequestInit: undefined,
42
43
  manifestController: any(ManifestControllerImpl),
@@ -47,6 +48,7 @@ describe('createHlsFactories', () => {
47
48
  contentStreamFactory: any(Function),
48
49
  qualitySelector: any(QualitySelectorImpl),
49
50
  }));
51
+ container.dispose();
50
52
  });
51
53
  it('wires per-track option observables from the player options', () => {
52
54
  const factoryCreator = createHlsFactories(null);
@@ -55,11 +57,13 @@ describe('createHlsFactories', () => {
55
57
  type: 'hls',
56
58
  manifestProvider,
57
59
  });
58
- const deps = createContainer(factories).dependencies;
60
+ const container = createContainer(factories);
61
+ const deps = container.dependencies;
59
62
  const options = hlsFactoryDeps.options.value;
60
63
  expect(deps.audio.value).toBe(options.audio);
61
64
  expect(deps.allowedContentTypes.value).toBe(options.allowedContentTypes);
62
65
  expect(deps.abr.value).toBe(options.abr);
66
+ container.dispose();
63
67
  });
64
68
  it('exposes the shared adController without owning it', () => {
65
69
  const factoryCreator = createHlsFactories(null);
@@ -93,8 +97,10 @@ describe('createHlsFactories', () => {
93
97
  type: 'hls',
94
98
  manifestProvider,
95
99
  });
96
- const deps = createContainer(factories).dependencies;
100
+ const container = createContainer(factories);
101
+ const deps = container.dependencies;
97
102
  expect(deps.contentStreamFactory('audio')).toEqual(any(Object));
103
+ container.dispose();
98
104
  });
99
105
  it('uses createUrlHlsManifestProvider when no manifestProvider given', () => {
100
106
  const factoryCreator = createHlsFactories(null);
@@ -102,8 +108,10 @@ describe('createHlsFactories', () => {
102
108
  uri: 'https://example.com/main.m3u8',
103
109
  type: 'hls',
104
110
  });
105
- const deps = createContainer(factories).dependencies;
111
+ const container = createContainer(factories);
112
+ const deps = container.dependencies;
106
113
  expect(deps.manifestController).toEqual(any(ManifestControllerImpl));
114
+ container.dispose();
107
115
  });
108
116
  it('creates a sidecar text track controller and populates from manifest', async () => {
109
117
  const factoryCreator = createHlsFactories(null);
@@ -128,7 +136,8 @@ describe('createHlsFactories', () => {
128
136
  type: 'hls',
129
137
  manifestProvider: provider,
130
138
  });
131
- const deps = createContainer(factories).dependencies;
139
+ const container = createContainer(factories);
140
+ const deps = container.dependencies;
132
141
  // Resolve textTrackController first so it subscribes to
133
142
  // manifestTransformed before the manifest promise resolves.
134
143
  const controller = deps.textTrackController;
@@ -137,6 +146,7 @@ describe('createHlsFactories', () => {
137
146
  expect(controller).toBeDefined();
138
147
  expect(controller.textTracks.length).toBe(1);
139
148
  expect(controller.textTracks[0].language).toBe('en');
149
+ container.dispose();
140
150
  });
141
151
  it('text track controller swallows manifest fetch errors', async () => {
142
152
  const factoryCreator = createHlsFactories(null);
@@ -146,13 +156,15 @@ describe('createHlsFactories', () => {
146
156
  type: 'hls',
147
157
  manifestProvider: provider,
148
158
  });
149
- const deps = createContainer(factories).dependencies;
159
+ const container = createContainer(factories);
160
+ const deps = container.dependencies;
150
161
  const controller = deps.textTrackController;
151
162
  // Trigger the transform; the promise will reject but the
152
163
  // textTrackController must not throw.
153
164
  await deps.manifestTransformed.value.catch(() => undefined);
154
165
  await new Promise((r) => setTimeout(r, 0));
155
166
  expect(controller.textTracks).toEqual([]);
167
+ container.dispose();
156
168
  });
157
169
  describe('ad interstitial discovery', () => {
158
170
  function interstitialManifest(overrides = {}) {
@@ -184,11 +196,13 @@ describe('createHlsFactories', () => {
184
196
  type: 'hls',
185
197
  manifestProvider: provider,
186
198
  });
187
- const deps = createContainer(factories).dependencies;
199
+ const container = createContainer(factories);
200
+ const deps = container.dependencies;
188
201
  const timeline = await deps.mediaTimeline.value;
189
202
  const adBreaks = await timeline.getAdBreaks();
190
203
  expect(adBreaks.length).toBe(1);
191
204
  expect(adBreaks[0].id).toBe('ad1');
205
+ container.dispose();
192
206
  });
193
207
  it('merges stitched SCTE-35 breaks with interstitials by start time', async () => {
194
208
  const factoryCreator = createHlsFactories(null);
@@ -230,13 +244,15 @@ describe('createHlsFactories', () => {
230
244
  type: 'hls',
231
245
  manifestProvider: provider,
232
246
  });
233
- const deps = createContainer(factories).dependencies;
247
+ const container = createContainer(factories);
248
+ const deps = container.dependencies;
234
249
  const timeline = await deps.mediaTimeline.value;
235
250
  const adBreaks = await timeline.getAdBreaks();
236
251
  expect(adBreaks.map((b) => [b.id, b.startTime, b.insertion])).toEqual([
237
252
  ['splice-1', 2, 'stitched'],
238
253
  ['ad1', 11, 'interstitial'],
239
254
  ]);
255
+ container.dispose();
240
256
  });
241
257
  it('omits ad breaks when the playlist has none', async () => {
242
258
  const factoryCreator = createHlsFactories(null);
@@ -245,9 +261,11 @@ describe('createHlsFactories', () => {
245
261
  type: 'hls',
246
262
  manifestProvider,
247
263
  });
248
- const deps = createContainer(factories).dependencies;
264
+ const container = createContainer(factories);
265
+ const deps = container.dependencies;
249
266
  const timeline = await deps.mediaTimeline.value;
250
267
  expect(await timeline.getAdBreaks()).toEqual([]);
268
+ container.dispose();
251
269
  });
252
270
  it('discovers ads against a live (non-ended) playlist', async () => {
253
271
  const factoryCreator = createHlsFactories(null);
@@ -257,9 +275,11 @@ describe('createHlsFactories', () => {
257
275
  type: 'hls',
258
276
  manifestProvider: provider,
259
277
  });
260
- const deps = createContainer(factories).dependencies;
278
+ const container = createContainer(factories);
279
+ const deps = container.dependencies;
261
280
  const timeline = await deps.mediaTimeline.value;
262
281
  expect((await timeline.getAdBreaks()).length).toBe(1);
282
+ container.dispose();
263
283
  });
264
284
  it('returns no ad breaks when there are no variants', async () => {
265
285
  const factoryCreator = createHlsFactories(null);
@@ -275,9 +295,11 @@ describe('createHlsFactories', () => {
275
295
  type: 'hls',
276
296
  manifestProvider: provider,
277
297
  });
278
- const deps = createContainer(factories).dependencies;
298
+ const container = createContainer(factories);
299
+ const deps = container.dependencies;
279
300
  const timeline = await deps.mediaTimeline.value;
280
301
  expect(await timeline.getAdBreaks()).toEqual([]);
302
+ container.dispose();
281
303
  });
282
304
  it('surfaces media playlist fetch errors from getAdBreaks', async () => {
283
305
  const factoryCreator = createHlsFactories(null);
@@ -290,12 +312,14 @@ describe('createHlsFactories', () => {
290
312
  type: 'hls',
291
313
  manifestProvider: provider,
292
314
  });
293
- const deps = createContainer(factories).dependencies;
315
+ const container = createContainer(factories);
316
+ const deps = container.dependencies;
294
317
  const timeline = await deps.mediaTimeline.value;
295
318
  // Discovery is now lazy: getAdBreaks() rejects on a failed media
296
319
  // playlist fetch, leaving error handling to the caller (e.g.
297
320
  // MseTrack swallows it via its errorHandler).
298
321
  await expectAsync(timeline.getAdBreaks()).toBeRejectedWithError('media down');
322
+ container.dispose();
299
323
  });
300
324
  });
301
325
  });