@amazon/vinyl 3.1.0 → 3.2.0

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 (51) hide show
  1. package/dist/index.cjs +207 -164
  2. package/dist/index.cjs.map +4 -4
  3. package/dist/index.js +195 -151
  4. package/dist/index.js.map +4 -4
  5. package/dist/test/integ.es5.js +201 -199
  6. package/dist/test/integ.js +417 -222
  7. package/dist/test/integ.js.map +4 -4
  8. package/dist/types/current/drm/DrmControllerImpl.d.ts +17 -1
  9. package/dist/types/current/drm/DrmControllerImpl.d.ts.map +1 -1
  10. package/dist/types/current/drm/DrmControllerImpl.js +45 -10
  11. package/dist/types/current/streaming/LoadMetric.d.ts +15 -1
  12. package/dist/types/current/streaming/LoadMetric.d.ts.map +1 -1
  13. package/dist/types/current/streaming/mediaTimelineFilter.d.ts +4 -2
  14. package/dist/types/current/streaming/mediaTimelineFilter.d.ts.map +1 -1
  15. package/dist/types/current/streaming/mediaTimelineFilter.js +23 -8
  16. package/dist/types/current/track/filters/sampleRateFilter.d.ts +8 -4
  17. package/dist/types/current/track/filters/sampleRateFilter.d.ts.map +1 -1
  18. package/dist/types/current/track/filters/sampleRateFilter.js +31 -21
  19. package/dist/types/current/track/mse/MseTrack.d.ts +0 -9
  20. package/dist/types/current/track/mse/MseTrack.d.ts.map +1 -1
  21. package/dist/types/current/track/mse/MseTrack.js +2 -29
  22. package/dist/types/current/tsconfig.tsbuildinfo +1 -1
  23. package/dist/types/current/vinyl/VinylPlayer.d.ts +5 -0
  24. package/dist/types/current/vinyl/VinylPlayer.d.ts.map +1 -1
  25. package/dist/types/current/vinyl/VinylPlayer.js +23 -1
  26. package/dist/types/ts3.7/drm/DrmControllerImpl.d.ts +17 -1
  27. package/dist/types/ts3.7/streaming/LoadMetric.d.ts +15 -1
  28. package/dist/types/ts3.7/streaming/mediaTimelineFilter.d.ts +4 -2
  29. package/dist/types/ts3.7/track/filters/sampleRateFilter.d.ts +8 -4
  30. package/dist/types/ts3.7/track/mse/MseTrack.d.ts +0 -9
  31. package/dist/types/ts3.7/vinyl/VinylPlayer.d.ts +5 -0
  32. package/dist/types/ts4.1/drm/DrmControllerImpl.d.ts +17 -1
  33. package/dist/types/ts4.1/streaming/LoadMetric.d.ts +15 -1
  34. package/dist/types/ts4.1/streaming/mediaTimelineFilter.d.ts +4 -2
  35. package/dist/types/ts4.1/track/filters/sampleRateFilter.d.ts +8 -4
  36. package/dist/types/ts4.1/track/mse/MseTrack.d.ts +0 -9
  37. package/dist/types/ts4.1/vinyl/VinylPlayer.d.ts +5 -0
  38. package/dist/types/ts4.7/drm/DrmControllerImpl.d.ts +17 -1
  39. package/dist/types/ts4.7/streaming/LoadMetric.d.ts +15 -1
  40. package/dist/types/ts4.7/streaming/mediaTimelineFilter.d.ts +4 -2
  41. package/dist/types/ts4.7/track/filters/sampleRateFilter.d.ts +8 -4
  42. package/dist/types/ts4.7/track/mse/MseTrack.d.ts +0 -9
  43. package/dist/types/ts4.7/vinyl/VinylPlayer.d.ts +5 -0
  44. package/dist/unit/types/current/drm/DrmControllerImpl.test.js +53 -9
  45. package/dist/unit/types/current/streaming/MseTrack.test.js +11 -8
  46. package/dist/unit/types/current/streaming/mediaTimelineFilter.test.js +60 -54
  47. package/dist/unit/types/current/track/filters/sampleRateFilter.test.js +75 -0
  48. package/dist/unit/types/current/tsconfig.tsbuildinfo +1 -1
  49. package/dist/unit/types/current/vinyl/VinylPlayer.test.js +33 -2
  50. package/dist/vinylTestUtil/types/current/tsconfig.tsbuildinfo +1 -1
  51. package/package.json +10 -10
@@ -7,7 +7,7 @@ import { createMockDashDependencies, } from '@amazon/vinyl/vinylTestUtil';
7
7
  import { createEventSpy, useMockLogger } from '@amazon/vinyl-util/testUtil';
8
8
  import { flushPromises } from '@amazon/vinyl-util/browserTestUtil';
9
9
  import { externalDependencies } from '@amazon/vinyl-di';
10
- import { Abort, MediaUnsupportedError } from '@amazon/vinyl-util';
10
+ import { Abort } from '@amazon/vinyl-util';
11
11
  var any = jasmine.any;
12
12
  describe('MseTrack', () => {
13
13
  useMockLogger();
@@ -585,11 +585,12 @@ describe('MseTrack', () => {
585
585
  expect(track.qualities).toEqual([qualityMetadata]);
586
586
  expect(track.qualitiesUnfiltered).toEqual([qualityMetadata]);
587
587
  });
588
- it('fails fast when a required content type is entirely filtered out', async () => {
589
- // contentTypes (from the manifest) is {audio, video}, but the
590
- // transformed timeline has only a video quality — audio was fully
591
- // filtered. The audio stream could never create a source buffer, so
592
- // the track must fail rather than stall on the readyToAppend wait.
588
+ it('plays the surviving content type when another is entirely filtered out', async () => {
589
+ // The content types now come from the FILTERED timeline: with every
590
+ // audio rendition filtered, only 'video' is required. No audio stream
591
+ // is created, so the load is never stranded waiting for an audio
592
+ // source buffer that will never arrive, and the track does not error.
593
+ deps.contentTypesValue.value = Promise.resolve(new Set(['video']));
593
594
  const videoQuality = {
594
595
  ...createEmptyMediaQualityMetadata(),
595
596
  contentType: 'video',
@@ -614,8 +615,10 @@ describe('MseTrack', () => {
614
615
  });
615
616
  track = createTrack();
616
617
  await flushPromises();
617
- expect(track.error).toEqual(any(MediaUnsupportedError));
618
- expect(track.error.code).toBe('no-playable-audio');
618
+ expect(track.error).toBeNull();
619
+ expect(track.contentTypes).toEqual(new Set(['video']));
620
+ expect(getStream('video')).not.toBeNull();
621
+ expect(getStream('audio')).toBeNull();
619
622
  });
620
623
  it('reads qualities from the filtered timeline but qualitiesUnfiltered from the raw one', async () => {
621
624
  // The transformed timeline is filtered (e.g. by preferred audio
@@ -13,81 +13,87 @@ function createQuality(contentType, bandwidth) {
13
13
  getSegment: () => Promise.resolve(null),
14
14
  };
15
15
  }
16
- describe('filterTimelineQualities', () => {
17
- const timeline = {
18
- periods: [
19
- {
20
- startTime: 0,
21
- endTime: 10,
22
- qualities: [
23
- createQuality('audio', 128000),
24
- createQuality('audio', 256000),
25
- createQuality('video', 500000),
26
- ],
27
- },
28
- ],
16
+ function timelineOf(qualities) {
17
+ return {
18
+ periods: [{ startTime: 0, endTime: 10, qualities: [...qualities] }],
29
19
  minBufferTime: 2,
30
20
  getAdBreaks: () => Promise.resolve([]),
31
21
  getDuration: () => Promise.resolve(Infinity),
32
22
  };
23
+ }
24
+ function contentTypesOf(timeline) {
25
+ return new Set(timeline.periods[0].qualities.map((q) => q.metadata.contentType));
26
+ }
27
+ function throwEmpty() {
28
+ throw new Error('no qualities');
29
+ }
30
+ describe('filterTimelineQualities', () => {
31
+ const timeline = timelineOf([
32
+ createQuality('audio', 128000),
33
+ createQuality('audio', 256000),
34
+ createQuality('video', 500000),
35
+ ]);
33
36
  it('returns the timeline unchanged when the filter is null', () => {
34
37
  // A null predicate (e.g. no language preference) is a no-op.
35
- const result = filterTimelineQualities(null, () => {
36
- throw new Error('empty');
37
- }, timeline);
38
+ const result = filterTimelineQualities(null, throwEmpty, timeline);
38
39
  expect(result).toBe(timeline);
39
40
  });
40
- it('filters qualities by predicate', () => {
41
- const result = filterTimelineQualities((q) => q.contentType === 'audio', () => {
42
- throw new Error('empty');
43
- }, timeline);
41
+ it('filters qualities by predicate while keeping every present stream', () => {
42
+ const result = filterTimelineQualities((q) => q.bandwidth !== 256000, throwEmpty, timeline);
44
43
  expect(result.periods[0].qualities.length).toBe(2);
45
- expect(result.periods[0].qualities.every((q) => q.metadata.contentType === 'audio')).toBeTrue();
44
+ expect(contentTypesOf(result)).toEqual(new Set(['audio', 'video']));
46
45
  });
47
46
  it('preserves minBufferTime', () => {
48
- const result = filterTimelineQualities(() => true, () => {
49
- throw new Error('empty');
50
- }, timeline);
47
+ const result = filterTimelineQualities(() => true, throwEmpty, timeline);
51
48
  expect(result.minBufferTime).toBe(2);
52
49
  });
53
50
  it('throws when filtering removes all qualities', () => {
54
- expect(() => filterTimelineQualities(() => false, () => {
55
- throw new Error('no qualities');
56
- }, timeline)).toThrowError('no qualities');
51
+ expect(() => filterTimelineQualities(() => false, throwEmpty, timeline)).toThrowError('no qualities');
52
+ });
53
+ it('throws when filtering drops a present video stream to zero', () => {
54
+ expect(() => filterTimelineQualities((q) => q.contentType === 'audio', throwEmpty, timeline)).toThrowError('no qualities');
55
+ });
56
+ it('throws when filtering drops a present audio stream to zero', () => {
57
+ expect(() => filterTimelineQualities((q) => q.contentType === 'video', throwEmpty, timeline)).toThrowError('no qualities');
58
+ });
59
+ it('does not throw when losing only an optional text sidecar stream', () => {
60
+ const withText = timelineOf([
61
+ createQuality('audio', 128000),
62
+ createQuality('video', 500000),
63
+ createQuality('text', 1000),
64
+ ]);
65
+ const result = filterTimelineQualities((q) => q.contentType !== 'text', throwEmpty, withText);
66
+ expect(contentTypesOf(result)).toEqual(new Set(['audio', 'video']));
57
67
  });
58
68
  });
59
69
  describe('filterTimelineQualitiesAsync', () => {
60
- const timeline = {
61
- periods: [
62
- {
63
- startTime: 0,
64
- endTime: 10,
65
- qualities: [
66
- createQuality('audio', 128000),
67
- createQuality('video', 500000),
68
- ],
69
- },
70
- ],
71
- minBufferTime: 2,
72
- getAdBreaks: () => Promise.resolve([]),
73
- getDuration: () => Promise.resolve(Infinity),
74
- };
75
- it('filters qualities by async predicate', async () => {
76
- const result = await filterTimelineQualitiesAsync((q) => Promise.resolve(q.contentType === 'audio'), () => {
77
- throw new Error('empty');
78
- }, timeline);
79
- expect(result.periods[0].qualities.length).toBe(1);
80
- expect(result.periods[0].qualities[0].metadata.contentType).toBe('audio');
70
+ const timeline = timelineOf([
71
+ createQuality('audio', 128000),
72
+ createQuality('audio', 256000),
73
+ createQuality('video', 500000),
74
+ ]);
75
+ it('filters qualities by async predicate while keeping every present stream', async () => {
76
+ const result = await filterTimelineQualitiesAsync((q) => Promise.resolve(q.bandwidth !== 256000), throwEmpty, timeline);
77
+ expect(result.periods[0].qualities.length).toBe(2);
78
+ expect(contentTypesOf(result)).toEqual(new Set(['audio', 'video']));
81
79
  });
82
80
  it('preserves minBufferTime', async () => {
83
- const result = await filterTimelineQualitiesAsync(() => Promise.resolve(true), () => {
84
- throw new Error('empty');
85
- }, timeline);
81
+ const result = await filterTimelineQualitiesAsync(() => Promise.resolve(true), throwEmpty, timeline);
86
82
  expect(result.minBufferTime).toBe(2);
87
83
  });
88
84
  it('throws when filtering removes all qualities', async () => {
89
- await expectAsync(filterTimelineQualitiesAsync(() => Promise.resolve(false), () => {
90
- throw new Error('no qualities');
91
- }, timeline)).toBeRejectedWithError('no qualities');
85
+ await expectAsync(filterTimelineQualitiesAsync(() => Promise.resolve(false), throwEmpty, timeline)).toBeRejectedWithError('no qualities');
86
+ });
87
+ it('throws when filtering drops a present video stream to zero', async () => {
88
+ await expectAsync(filterTimelineQualitiesAsync((q) => Promise.resolve(q.contentType === 'audio'), throwEmpty, timeline)).toBeRejectedWithError('no qualities');
89
+ });
90
+ it('does not throw when losing only an optional text sidecar stream', async () => {
91
+ const withText = timelineOf([
92
+ createQuality('audio', 128000),
93
+ createQuality('video', 500000),
94
+ createQuality('text', 1000),
95
+ ]);
96
+ const result = await filterTimelineQualitiesAsync((q) => Promise.resolve(q.contentType !== 'text'), throwEmpty, withText);
97
+ expect(contentTypesOf(result)).toEqual(new Set(['audio', 'video']));
92
98
  });
93
99
  });
@@ -192,6 +192,27 @@ describe('withinAudioSampleRateRange minSampleRate floor', () => {
192
192
  expect(withinMin(mid, 1, [low, mid])).toBe(true);
193
193
  expect(withinMin(low, 0, [low, mid])).toBe(false);
194
194
  });
195
+ it('keeps the highest when every rate (of three) is below the floor', () => {
196
+ const low = audioQuality([16_000]);
197
+ const mid = audioQuality([24_000]);
198
+ const high = audioQuality([32_000]);
199
+ expect(withinMin(high, 2, [low, mid, high])).toBe(true);
200
+ expect(withinMin(mid, 1, [low, mid, high])).toBe(false);
201
+ expect(withinMin(low, 0, [low, mid, high])).toBe(false);
202
+ });
203
+ it('keeps the highest below-floor rate when no platform max is reported', () => {
204
+ // No AudioContext rate, so the floor is the only bound; with every rate
205
+ // below it, the highest is kept as the fallback.
206
+ capabilities.sampleRate = null;
207
+ const low = audioQuality([16_000]);
208
+ const high = audioQuality([32_000]);
209
+ expect(withinMin(high, 1, [low, high])).toBe(true);
210
+ expect(withinMin(low, 0, [low, high])).toBe(false);
211
+ });
212
+ it('keeps a single audio rendition below the floor', () => {
213
+ const only = audioQuality([24_000]);
214
+ expect(withinMin(only, 0, [only])).toBe(true);
215
+ });
195
216
  it('ignores non-audio qualities in the floor fallback', () => {
196
217
  const audio = audioQuality([24_000]); // below the floor
197
218
  const video = createEmptyMediaQualityMetadata();
@@ -202,3 +223,57 @@ describe('withinAudioSampleRateRange minSampleRate floor', () => {
202
223
  expect(withinMin(video, 1, [audio, video])).toBe(true);
203
224
  });
204
225
  });
226
+ // The floor and ceiling are soft together: when renditions straddle an empty
227
+ // band (some below the floor, some above the ceiling, none in between) neither
228
+ // bound alone must strand audio.
229
+ describe('withinAudioSampleRateRange straddling an empty band', () => {
230
+ let capabilities;
231
+ beforeEach(() => {
232
+ // The soft ceiling is a non-Firefox behavior; Firefox's 48kHz cap is
233
+ // hard (covered above), so pin a non-Firefox UA to exercise the fallback.
234
+ setUserAgent('Chrome');
235
+ capabilities = new MockCapabilities();
236
+ // Ceiling at 48kHz; floor at 44.1kHz → acceptable band is [44.1k, 48k].
237
+ capabilities.sampleRate = 48_000;
238
+ });
239
+ function audioQuality(rate) {
240
+ const quality = createEmptyMediaQualityMetadata();
241
+ quality.contentType = 'audio';
242
+ quality.audioSamplingRate = rate;
243
+ return quality;
244
+ }
245
+ function within(metadata, index, array) {
246
+ return withinAudioSampleRateRange({ capabilities, minSampleRate: 44_100 }, metadata, index, array);
247
+ }
248
+ it('keeps the highest decodable rate when one is below the floor and one above the ceiling', () => {
249
+ const low = audioQuality([22_050]); // below the 44.1kHz floor
250
+ const high = audioQuality([96_000]); // above the 48kHz ceiling
251
+ // Empty band → the below-floor rate is decodable and kept; the
252
+ // above-ceiling rate is dropped, so audio is never stranded.
253
+ expect(within(low, 0, [low, high])).toBe(true);
254
+ expect(within(high, 1, [low, high])).toBe(false);
255
+ });
256
+ it('keeps the below-floor rate when the ceiling matches the output device rate', () => {
257
+ // Output device runs at 44.1kHz, so a 48kHz rendition is above the
258
+ // ceiling while a 22.05kHz rendition is below the floor.
259
+ capabilities.sampleRate = 44_100;
260
+ const low = audioQuality([22_050]);
261
+ const high = audioQuality([48_000]);
262
+ expect(within(low, 0, [low, high])).toBe(true);
263
+ expect(within(high, 1, [low, high])).toBe(false);
264
+ });
265
+ it('still prefers an in-band rate over the straddle fallback', () => {
266
+ const low = audioQuality([22_050]); // below floor
267
+ const mid = audioQuality([44_100]); // in band
268
+ const high = audioQuality([96_000]); // above ceiling
269
+ expect(within(mid, 1, [low, mid, high])).toBe(true);
270
+ expect(within(low, 0, [low, mid, high])).toBe(false);
271
+ expect(within(high, 2, [low, mid, high])).toBe(false);
272
+ });
273
+ it('keeps the lowest when every rate is above the ceiling', () => {
274
+ const low = audioQuality([88_200]);
275
+ const high = audioQuality([96_000]);
276
+ expect(within(low, 0, [low, high])).toBe(true);
277
+ expect(within(high, 1, [low, high])).toBe(false);
278
+ });
279
+ });