@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
@@ -5592,9 +5592,6 @@
5592
5592
  function map(arrayLike, transform, thisArg) {
5593
5593
  return Array.prototype.map.call(arrayLike, transform, thisArg);
5594
5594
  }
5595
- function every(arrayLike, predicate, thisArg) {
5596
- return Array.prototype.every.call(arrayLike, predicate, thisArg);
5597
- }
5598
5595
  function first(arrayLike) {
5599
5596
  if (!arrayLike) return void 0;
5600
5597
  return arrayLike[0];
@@ -14017,6 +14014,10 @@ ${this.lastInitStack}` : "";
14017
14014
  __publicField(this, "bufferingTrackUri", null);
14018
14015
  __publicField(this, "sessionAbort", null);
14019
14016
  __publicField(this, "pendingMessageProps", null);
14017
+ // Armed per track load; latches on the first session so only the
14018
+ // critical-path session's EME/CDM setup is measured.
14019
+ __publicField(this, "keySetupSpanArmed", true);
14020
+ __publicField(this, "keySetupSpanStart", null);
14020
14021
  __publicField(this, "disposer", createDisposer());
14021
14022
  __publicField(this, "checkIsSupportedCached", memoize(
14022
14023
  async (params) => {
@@ -14182,6 +14183,7 @@ ${this.lastInitStack}` : "";
14182
14183
  if (!hasMimeType(drmInfo)) {
14183
14184
  throw new DrmError("Encrypted content must have a mimeType.");
14184
14185
  }
14186
+ this.markKeySetupStart();
14185
14187
  const mediaKeys = await this.attachMediaKeys(drmInfo);
14186
14188
  this.abortIfDisposed();
14187
14189
  const keySystemOptions = (_a3 = this.options.keySystems[mediaKeys.keySystem]) != null ? _a3 : this.options.keySystems["*"];
@@ -14226,6 +14228,28 @@ ${this.lastInitStack}` : "";
14226
14228
  });
14227
14229
  return newSession;
14228
14230
  }
14231
+ /**
14232
+ * Captures the key-setup span start at the top of the session-creation path
14233
+ * (before media keys are attached), latching so only the first session of
14234
+ * the current track load is measured.
14235
+ */
14236
+ markKeySetupStart() {
14237
+ if (!this.keySetupSpanArmed) return;
14238
+ this.keySetupSpanArmed = false;
14239
+ this.keySetupSpanStart = Date.now();
14240
+ }
14241
+ dispatchLoadSpan(kind, startTime, endTime, messageProps) {
14242
+ this.dispatch("loadSpanMeasured", __spreadValues(__spreadValues({
14243
+ kind,
14244
+ startTime,
14245
+ endTime,
14246
+ mimeType: messageProps.mimeType
14247
+ }, messageProps.trackUri != null && {
14248
+ trackUri: messageProps.trackUri
14249
+ }), messageProps.contentType != null && {
14250
+ contentType: messageProps.contentType
14251
+ }));
14252
+ }
14229
14253
  initializeForPlayback(drmInfo, options) {
14230
14254
  var _a3;
14231
14255
  logDebug(this, "initializeForPlayback", drmInfo);
@@ -14249,6 +14273,8 @@ ${this.lastInitStack}` : "";
14249
14273
  this.drmInfo = drmInfo;
14250
14274
  this.bufferingTrackUri = (_a3 = options == null ? void 0 : options.trackUri) != null ? _a3 : null;
14251
14275
  this.sessionAbort = (_b = options == null ? void 0 : options.abort) != null ? _b : null;
14276
+ this.keySetupSpanArmed = true;
14277
+ this.keySetupSpanStart = null;
14252
14278
  }
14253
14279
  /**
14254
14280
  * If the drm metadata contains initialization data for the current key system, create a new session.
@@ -14260,6 +14286,7 @@ ${this.lastInitStack}` : "";
14260
14286
  this.handleError(new DrmError("DRM not supported."));
14261
14287
  return;
14262
14288
  }
14289
+ this.markKeySetupStart();
14263
14290
  const mediaKeys = await this.attachMediaKeys(drmInfo);
14264
14291
  const drmProtection = (_a3 = drmInfo.contentProtections.find((cP) => {
14265
14292
  return cP.keySystem === mediaKeys.keySystem;
@@ -14398,7 +14425,9 @@ ${this.lastInitStack}` : "";
14398
14425
  event,
14399
14426
  // Captured per session, so a concurrent preloaded track's
14400
14427
  // license exchange keeps its own attribution.
14401
- trackUri
14428
+ trackUri,
14429
+ mimeType: drmInfo.mimeType,
14430
+ contentType: drmInfo.contentType
14402
14431
  }),
14403
14432
  LICENSE_TIMEOUT,
14404
14433
  { message: LICENSE_TIMEOUT_MESSAGE }
@@ -14420,6 +14449,15 @@ ${this.lastInitStack}` : "";
14420
14449
  this.pendingMessageProps = messageProps;
14421
14450
  const keySystem = mediaKeys.keySystem;
14422
14451
  logDebug(this, "message", keySystem, event.message.byteLength);
14452
+ if (this.keySetupSpanStart != null) {
14453
+ this.dispatchLoadSpan(
14454
+ "keySetup",
14455
+ this.keySetupSpanStart,
14456
+ Date.now(),
14457
+ messageProps
14458
+ );
14459
+ this.keySetupSpanStart = null;
14460
+ }
14423
14461
  const keySystemOptions = (_a3 = this.options.keySystems[keySystem]) != null ? _a3 : this.options.keySystems["*"];
14424
14462
  const licenseServerOptionsProvider = keySystemOptions == null ? void 0 : keySystemOptions.licenseServer;
14425
14463
  const licenseServerOptions = (_b = clone(await resolveValueProvider(licenseServerOptionsProvider))) != null ? _b : {};
@@ -14441,13 +14479,12 @@ ${this.lastInitStack}` : "";
14441
14479
  challenge
14442
14480
  );
14443
14481
  if (this.disposed) return;
14444
- this.dispatch("loadSpanMeasured", __spreadValues({
14445
- kind: "license",
14446
- startTime: licenseSpanStart,
14447
- endTime: Date.now()
14448
- }, messageProps.trackUri != null && {
14449
- trackUri: messageProps.trackUri
14450
- }));
14482
+ this.dispatchLoadSpan(
14483
+ "license",
14484
+ licenseSpanStart,
14485
+ Date.now(),
14486
+ messageProps
14487
+ );
14451
14488
  this.pendingMessageProps = null;
14452
14489
  if (session.disposed) return;
14453
14490
  logDebug(this, "update");
@@ -15177,7 +15214,7 @@ ${this.lastInitStack}` : "";
15177
15214
  // src/global/environment.ts
15178
15215
  var vinylVersion = parseVersion(
15179
15216
  /* istanbul ignore next */
15180
- "3.1.0"
15217
+ "3.2.0"
15181
15218
  );
15182
15219
 
15183
15220
  // src/global/globalRef.ts
@@ -17743,6 +17780,86 @@ ${this.lastInitStack}` : "";
17743
17780
  return filtered;
17744
17781
  }
17745
17782
 
17783
+ // src/streaming/DescriptorRecord.ts
17784
+ function getDescriptorValue(record2, uri, id) {
17785
+ var _a3, _b;
17786
+ let arr = record2[uri];
17787
+ if (!arr) return null;
17788
+ if (id !== void 0) arr = arr.filter((descriptor) => descriptor.id === id);
17789
+ return (_b = (_a3 = first(arr)) == null ? void 0 : _a3.value) != null ? _b : null;
17790
+ }
17791
+ var descriptorValidator = object({
17792
+ id: string().maybe().optional(),
17793
+ value: string().maybe().optional()
17794
+ });
17795
+ var descriptorRecordValidator = record(string(), array(descriptorValidator).readonly().orUndefined());
17796
+
17797
+ // src/streaming/MediaQualityMetadata.ts
17798
+ var ALL_CONTENT_TYPES = [
17799
+ "video",
17800
+ "audio",
17801
+ "text"
17802
+ ];
17803
+ var RESTRICTABLE_CONTENT_TYPES = [
17804
+ "video",
17805
+ "audio"
17806
+ ];
17807
+ var drmProtectionValidator = descriptorValidator.extend({
17808
+ keySystem: enumOf(DrmKeySystem),
17809
+ pro: string().maybe().optional(),
17810
+ pssh: string().maybe().optional()
17811
+ });
17812
+ var mediaFormatMetadataValidator = object({
17813
+ contentProtections: array(drmProtectionValidator).readonly(),
17814
+ contentType: isOneOf("audio", "video", "text").orNull(),
17815
+ initDataType: string().orNull(),
17816
+ encryptionScheme: string().orNull(),
17817
+ mimeType: string().orNull()
17818
+ });
17819
+ function createEmptyMediaFormatMetadata() {
17820
+ return {
17821
+ contentType: null,
17822
+ mimeType: null,
17823
+ initDataType: null,
17824
+ encryptionScheme: null,
17825
+ contentProtections: []
17826
+ };
17827
+ }
17828
+ function createEmptyMediaQualityMetadata() {
17829
+ return __spreadProps(__spreadValues({}, createEmptyMediaFormatMetadata()), {
17830
+ decoderId: createShortUid(),
17831
+ qualityId: createShortUid(),
17832
+ codecs: null,
17833
+ bandwidth: null,
17834
+ bandwidthTotal: null,
17835
+ supplementalProperties: {},
17836
+ audioSamplingRate: null,
17837
+ frameRate: null,
17838
+ height: null,
17839
+ width: null,
17840
+ lang: null,
17841
+ characteristics: [],
17842
+ groupId: createShortUid(),
17843
+ switchingGroupIds: null
17844
+ });
17845
+ }
17846
+ var mediaQualityMetadataValidator = mediaFormatMetadataValidator.extend({
17847
+ audioSamplingRate: array(number()).readonly().orNull(),
17848
+ bandwidth: number().orNull(),
17849
+ bandwidthTotal: number().orNull(),
17850
+ codecs: string().orNull(),
17851
+ decoderId: string(),
17852
+ frameRate: tuple(number(), number()).readonly().orNull(),
17853
+ height: number().orNull(),
17854
+ qualityId: string(),
17855
+ supplementalProperties: descriptorRecordValidator,
17856
+ width: number().orNull(),
17857
+ lang: string().orNull(),
17858
+ characteristics: array(string()).readonly(),
17859
+ groupId: string(),
17860
+ switchingGroupIds: array(string()).readonly().orNull()
17861
+ });
17862
+
17746
17863
  // src/track/filters/languageFilter.ts
17747
17864
  function throwLanguagesUnsupported() {
17748
17865
  throw new MediaUnsupportedError(
@@ -18024,6 +18141,13 @@ ${this.lastInitStack}` : "";
18024
18141
  }
18025
18142
 
18026
18143
  // src/streaming/mediaTimelineFilter.ts
18144
+ function assertMediaStreamsRetained(original, filtered, throwError) {
18145
+ for (const contentType2 of RESTRICTABLE_CONTENT_TYPES) {
18146
+ if (original.some((q) => q.metadata.contentType === contentType2) && !filtered.some((q) => q.metadata.contentType === contentType2)) {
18147
+ throwError();
18148
+ }
18149
+ }
18150
+ }
18027
18151
  function filterTimelineQualities(filter, throwError, timeline) {
18028
18152
  if (!filter) return timeline;
18029
18153
  const mapMetadataArray = memoize(
@@ -18035,11 +18159,9 @@ ${this.lastInitStack}` : "";
18035
18159
  const filtered = period.qualities.filter(
18036
18160
  (q, index) => filter(q.metadata, index, mapMetadataArray(period.qualities))
18037
18161
  );
18162
+ assertMediaStreamsRetained(period.qualities, filtered, throwError);
18038
18163
  return __spreadProps(__spreadValues({}, period), { qualities: filtered });
18039
18164
  });
18040
- if (periods.some((p) => p.qualities.length === 0)) {
18041
- throwError();
18042
- }
18043
18165
  return __spreadProps(__spreadValues({}, timeline), { periods });
18044
18166
  }
18045
18167
  async function filterTimelineQualitiesAsync(filter, throwError, timeline) {
@@ -18057,11 +18179,9 @@ ${this.lastInitStack}` : "";
18057
18179
  filtered.push(period.qualities[i]);
18058
18180
  }
18059
18181
  }
18182
+ assertMediaStreamsRetained(period.qualities, filtered, throwError);
18060
18183
  periods.push(__spreadProps(__spreadValues({}, period), { qualities: filtered }));
18061
18184
  }
18062
- if (periods.some((p) => p.qualities.length === 0)) {
18063
- throwError();
18064
- }
18065
18185
  return __spreadProps(__spreadValues({}, timeline), { periods });
18066
18186
  }
18067
18187
 
@@ -18135,23 +18255,28 @@ ${this.lastInitStack}` : "";
18135
18255
  if (metadata.contentType !== "audio") return true;
18136
18256
  const samplingRate = last(metadata.audioSamplingRate);
18137
18257
  if (!samplingRate) return true;
18258
+ const { minSampleRate } = options;
18259
+ const maxSampleRate = (_a3 = options.maxSampleRate) != null ? _a3 : options.capabilities.sampleRate;
18138
18260
  const audioRates = () => map(
18139
18261
  array2,
18140
18262
  (item) => item.contentType === "audio" ? last(item.audioSamplingRate) : void 0
18141
18263
  ).filter((rate) => rate != null);
18142
- const { minSampleRate } = options;
18264
+ const outOfBandFallback = () => {
18265
+ const rates = audioRates();
18266
+ const inBand = (rate) => (minSampleRate == null || rate >= minSampleRate) && (!maxSampleRate || rate <= maxSampleRate);
18267
+ if (rates.some(inBand)) return void 0;
18268
+ const decodable = maxSampleRate ? rates.filter((rate) => rate <= maxSampleRate) : rates;
18269
+ return decodable.length ? max(decodable) : min(rates);
18270
+ };
18143
18271
  if (minSampleRate != null && samplingRate < minSampleRate) {
18144
- const rates2 = audioRates();
18145
- return every(rates2, (rate) => rate < minSampleRate) && samplingRate === max(rates2);
18272
+ return samplingRate === outOfBandFallback();
18146
18273
  }
18147
- const maxSampleRate = (_a3 = options.maxSampleRate) != null ? _a3 : options.capabilities.sampleRate;
18148
18274
  if (!maxSampleRate) return true;
18149
18275
  if (hasBrowser("Firefox" /* FIREFOX */)) {
18150
18276
  return samplingRate <= 48e3;
18151
18277
  }
18152
18278
  if (samplingRate <= maxSampleRate) return true;
18153
- const rates = audioRates();
18154
- return every(rates, (rate) => rate > maxSampleRate) && samplingRate === min(rates);
18279
+ return samplingRate === outOfBandFallback();
18155
18280
  }
18156
18281
 
18157
18282
  // src/util/media/codecOverrides.ts
@@ -18238,20 +18363,6 @@ ${this.lastInitStack}` : "";
18238
18363
  });
18239
18364
  }
18240
18365
 
18241
- // src/streaming/DescriptorRecord.ts
18242
- function getDescriptorValue(record2, uri, id) {
18243
- var _a3, _b;
18244
- let arr = record2[uri];
18245
- if (!arr) return null;
18246
- if (id !== void 0) arr = arr.filter((descriptor) => descriptor.id === id);
18247
- return (_b = (_a3 = first(arr)) == null ? void 0 : _a3.value) != null ? _b : null;
18248
- }
18249
- var descriptorValidator = object({
18250
- id: string().maybe().optional(),
18251
- value: string().maybe().optional()
18252
- });
18253
- var descriptorRecordValidator = record(string(), array(descriptorValidator).readonly().orUndefined());
18254
-
18255
18366
  // src/streaming/ManifestControllerImpl.ts
18256
18367
  var ManifestControllerImpl = class {
18257
18368
  constructor(deps) {
@@ -18351,72 +18462,6 @@ ${this.lastInitStack}` : "";
18351
18462
  }
18352
18463
  };
18353
18464
 
18354
- // src/streaming/MediaQualityMetadata.ts
18355
- var ALL_CONTENT_TYPES = [
18356
- "video",
18357
- "audio",
18358
- "text"
18359
- ];
18360
- var RESTRICTABLE_CONTENT_TYPES = [
18361
- "video",
18362
- "audio"
18363
- ];
18364
- var drmProtectionValidator = descriptorValidator.extend({
18365
- keySystem: enumOf(DrmKeySystem),
18366
- pro: string().maybe().optional(),
18367
- pssh: string().maybe().optional()
18368
- });
18369
- var mediaFormatMetadataValidator = object({
18370
- contentProtections: array(drmProtectionValidator).readonly(),
18371
- contentType: isOneOf("audio", "video", "text").orNull(),
18372
- initDataType: string().orNull(),
18373
- encryptionScheme: string().orNull(),
18374
- mimeType: string().orNull()
18375
- });
18376
- function createEmptyMediaFormatMetadata() {
18377
- return {
18378
- contentType: null,
18379
- mimeType: null,
18380
- initDataType: null,
18381
- encryptionScheme: null,
18382
- contentProtections: []
18383
- };
18384
- }
18385
- function createEmptyMediaQualityMetadata() {
18386
- return __spreadProps(__spreadValues({}, createEmptyMediaFormatMetadata()), {
18387
- decoderId: createShortUid(),
18388
- qualityId: createShortUid(),
18389
- codecs: null,
18390
- bandwidth: null,
18391
- bandwidthTotal: null,
18392
- supplementalProperties: {},
18393
- audioSamplingRate: null,
18394
- frameRate: null,
18395
- height: null,
18396
- width: null,
18397
- lang: null,
18398
- characteristics: [],
18399
- groupId: createShortUid(),
18400
- switchingGroupIds: null
18401
- });
18402
- }
18403
- var mediaQualityMetadataValidator = mediaFormatMetadataValidator.extend({
18404
- audioSamplingRate: array(number()).readonly().orNull(),
18405
- bandwidth: number().orNull(),
18406
- bandwidthTotal: number().orNull(),
18407
- codecs: string().orNull(),
18408
- decoderId: string(),
18409
- frameRate: tuple(number(), number()).readonly().orNull(),
18410
- height: number().orNull(),
18411
- qualityId: string(),
18412
- supplementalProperties: descriptorRecordValidator,
18413
- width: number().orNull(),
18414
- lang: string().orNull(),
18415
- characteristics: array(string()).readonly(),
18416
- groupId: string(),
18417
- switchingGroupIds: array(string()).readonly().orNull()
18418
- });
18419
-
18420
18465
  // src/streaming/MediaTimeline.ts
18421
18466
  function getMediaPeriodAtTime(timeline, time) {
18422
18467
  const index = sortedInsertionIndex(timeline.periods, time, (time2, period2) => {
@@ -20529,10 +20574,7 @@ ${this.lastInitStack}` : "";
20529
20574
  this.deps = deps;
20530
20575
  add(
20531
20576
  deps.contentTypesValue.onData((contentTypesPromise) => {
20532
- contentTypesPromise.then((value) => {
20533
- this.setContentTypes(value);
20534
- this.validateRequiredContentTypesPlayable();
20535
- }).catch(this.errorHandler);
20577
+ contentTypesPromise.then((value) => this.setContentTypes(value)).catch(this.errorHandler);
20536
20578
  })
20537
20579
  );
20538
20580
  this.on("streamingQualityChange", (event) => {
@@ -20633,33 +20675,8 @@ ${this.lastInitStack}` : "";
20633
20675
  });
20634
20676
  }
20635
20677
  }
20636
- this.validateRequiredContentTypesPlayable();
20637
20678
  });
20638
20679
  }
20639
- /**
20640
- * Fails fast when a media content type the manifest declared (audio/video)
20641
- * has no playable qualities left after timeline filtering. Its stream can
20642
- * never create a source buffer, so without this the track waits forever for
20643
- * a source buffer that never arrives, surfacing only as an opaque
20644
- * `readyToAppend` timeout. Runs once both the required content types and the
20645
- * filtered timeline qualities are known (either order).
20646
- */
20647
- validateRequiredContentTypesPlayable() {
20648
- const qualities = this._qualities;
20649
- if (!qualities) return;
20650
- const present = new Set(qualities.map((q) => q.contentType));
20651
- for (const contentType2 of this._contentTypes) {
20652
- if (!present.has(contentType2)) {
20653
- this.errorHandler(
20654
- new MediaUnsupportedError(
20655
- `No playable ${contentType2} rendition`,
20656
- `no-playable-${contentType2}`
20657
- )
20658
- );
20659
- return;
20660
- }
20661
- }
20662
- }
20663
20680
  async getAds() {
20664
20681
  const timeline = await this.deps.mediaTimeline.value;
20665
20682
  return {
@@ -24081,6 +24098,7 @@ ${this.lastInitStack}` : "";
24081
24098
  playerRegistryRef.value.addPlayer(this);
24082
24099
  this.redispatchSubControllerEvents();
24083
24100
  this.redispatchCurrentTrackEvents();
24101
+ this.initializeLoadSpanLogging();
24084
24102
  this.initializeAutoResetHandling();
24085
24103
  }
24086
24104
  get [Symbol.toStringTag]() {
@@ -24098,6 +24116,26 @@ ${this.lastInitStack}` : "";
24098
24116
  get autoResetController() {
24099
24117
  return this.deps.autoResetController;
24100
24118
  }
24119
+ /**
24120
+ * Logs each republished load span once at the verbose level, formatting its
24121
+ * bounds as ISO date strings and its duration in milliseconds.
24122
+ */
24123
+ initializeLoadSpanLogging() {
24124
+ this.disposer.add(
24125
+ this.on("loadSpan", (span) => {
24126
+ if (getLogLevel() > 0 /* VERBOSE */) return;
24127
+ logVerbose(this, "loadSpan", {
24128
+ kind: span.kind,
24129
+ trackUri: span.trackUri,
24130
+ mimeType: span.mimeType,
24131
+ contentType: span.contentType,
24132
+ startTime: new Date(span.startTime).toISOString(),
24133
+ endTime: new Date(span.endTime).toISOString(),
24134
+ durationMs: (span.endTime - span.startTime).toFixed(2)
24135
+ });
24136
+ })
24137
+ );
24138
+ }
24101
24139
  redispatchSubControllerEvents() {
24102
24140
  const add = this.disposer.add;
24103
24141
  add(
@@ -31554,7 +31592,7 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
31554
31592
  drmController.setBufferingDrmInfo(drmInfo);
31555
31593
  await emitEncrypted(new Uint8Array([1, 2, 3]), "cenc");
31556
31594
  await emitMessage(0, new ArrayBuffer(1));
31557
- expect(spy).toHaveBeenCalledOnceWith(
31595
+ expect(spy).toHaveBeenCalledWith(
31558
31596
  objectContaining2({
31559
31597
  kind: "license",
31560
31598
  startTime: any4(Number),
@@ -31562,22 +31600,65 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
31562
31600
  })
31563
31601
  );
31564
31602
  });
31603
+ it("measures the key setup, contiguous with and before the license", async () => {
31604
+ licenseProvider.and.resolveTo(new Uint8Array([1]).buffer);
31605
+ const spy = createEventSpy(drmController, "loadSpanMeasured");
31606
+ drmController.setBufferingDrmInfo(drmInfo, { trackUri: "track-a" });
31607
+ await emitEncrypted(new Uint8Array([1, 2, 3]), "cenc");
31608
+ await emitMessage(0, new ArrayBuffer(1));
31609
+ const spans = spy.calls.all().map((call) => call.args[0]);
31610
+ expect(spans.map((span) => span.kind)).toEqual([
31611
+ "keySetup",
31612
+ "license"
31613
+ ]);
31614
+ const [keySetup, license] = spans;
31615
+ expect(keySetup.trackUri).toEqual("track-a");
31616
+ expect(keySetup.endTime).toEqual(license.startTime);
31617
+ });
31618
+ it("carries the content mime and content type on the license span", async () => {
31619
+ licenseProvider.and.resolveTo(new Uint8Array([1]).buffer);
31620
+ const spy = createEventSpy(drmController, "loadSpanMeasured");
31621
+ drmController.setBufferingDrmInfo(drmInfo);
31622
+ await emitEncrypted(new Uint8Array([1, 2, 3]), "cenc");
31623
+ await emitMessage(0, new ArrayBuffer(1));
31624
+ expect(spy).toHaveBeenCalledWith(
31625
+ objectContaining2({
31626
+ kind: "license",
31627
+ mimeType: "audio/mp4",
31628
+ contentType: "audio"
31629
+ })
31630
+ );
31631
+ });
31565
31632
  it("does not measure a failed license exchange", async () => {
31566
31633
  licenseProvider.and.rejectWith(new Error("license failed"));
31567
31634
  const spy = createEventSpy(drmController, "loadSpanMeasured");
31568
31635
  drmController.setBufferingDrmInfo(drmInfo);
31569
31636
  await emitEncrypted(new Uint8Array([1, 2, 3]), "cenc");
31570
31637
  await emitMessage(0, new ArrayBuffer(1));
31571
- expect(spy).not.toHaveBeenCalled();
31638
+ expect(spy).not.toHaveBeenCalledWith(
31639
+ objectContaining2({ kind: "license" })
31640
+ );
31572
31641
  errorSpy.calls.reset();
31573
31642
  });
31643
+ it("measures key setup only for the first session of a track load", async () => {
31644
+ licenseProvider.and.resolveTo(new Uint8Array([1]).buffer);
31645
+ const spy = createEventSpy(drmController, "loadSpanMeasured");
31646
+ drmController.setBufferingDrmInfo(drmInfo, { trackUri: "track-a" });
31647
+ await emitEncrypted(new Uint8Array([1]), "cenc");
31648
+ await emitEncrypted(new Uint8Array([2]), "cenc");
31649
+ expect(mediaKeys.createSession).toHaveBeenCalledTimes(2);
31650
+ await emitMessage(0, new ArrayBuffer(1));
31651
+ await emitMessage(1, new ArrayBuffer(1));
31652
+ const keySetupSpans = spy.calls.all().filter((call) => call.args[0].kind === "keySetup");
31653
+ expect(keySetupSpans.length).toEqual(1);
31654
+ });
31574
31655
  it("attributes the license span to the buffering track uri", async () => {
31575
31656
  licenseProvider.and.resolveTo(new Uint8Array([1]).buffer);
31576
31657
  const spy = createEventSpy(drmController, "loadSpanMeasured");
31577
31658
  drmController.setBufferingDrmInfo(drmInfo, { trackUri: "track-a" });
31578
31659
  await emitEncrypted(new Uint8Array([1, 2, 3]), "cenc");
31579
31660
  await emitMessage(0, new ArrayBuffer(1));
31580
- expect(spy).toHaveBeenCalledOnceWith(
31661
+ expect(spy).toHaveBeenCalledWith(
31581
31662
  objectContaining2({
31582
31663
  kind: "license",
31583
31664
  trackUri: "track-a"
@@ -31606,16 +31687,10 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
31606
31687
  expect(mediaKeys.createSession).toHaveBeenCalledTimes(2);
31607
31688
  await emitMessage(1, new ArrayBuffer(1));
31608
31689
  await emitMessage(0, new ArrayBuffer(1));
31609
- expect(spy).toHaveBeenCalledTimes(2);
31610
- expect(spy).toHaveBeenCalledWith(
31611
- objectContaining2({
31612
- kind: "license",
31613
- trackUri: "track-preloaded"
31614
- })
31615
- );
31616
- expect(spy).toHaveBeenCalledWith(
31617
- objectContaining2({ kind: "license", trackUri: "track-active" })
31618
- );
31690
+ const licenseTrackUris = spy.calls.all().map((call) => call.args[0]).filter((span) => span.kind === "license").map((span) => span.trackUri);
31691
+ expect(licenseTrackUris.length).toEqual(2);
31692
+ expect(licenseTrackUris).toContain("track-preloaded");
31693
+ expect(licenseTrackUris).toContain("track-active");
31619
31694
  });
31620
31695
  });
31621
31696
  describe("serverCertificate", () => {
@@ -38396,105 +38471,101 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
38396
38471
  getSegment: () => Promise.resolve(null)
38397
38472
  };
38398
38473
  }
38399
- describe("filterTimelineQualities", () => {
38400
- const timeline = {
38401
- periods: [
38402
- {
38403
- startTime: 0,
38404
- endTime: 10,
38405
- qualities: [
38406
- createQuality2("audio", 128e3),
38407
- createQuality2("audio", 256e3),
38408
- createQuality2("video", 5e5)
38409
- ]
38410
- }
38411
- ],
38474
+ function timelineOf2(qualities) {
38475
+ return {
38476
+ periods: [{ startTime: 0, endTime: 10, qualities: [...qualities] }],
38412
38477
  minBufferTime: 2,
38413
38478
  getAdBreaks: () => Promise.resolve([]),
38414
38479
  getDuration: () => Promise.resolve(Infinity)
38415
38480
  };
38481
+ }
38482
+ function contentTypesOf(timeline) {
38483
+ return new Set(
38484
+ timeline.periods[0].qualities.map((q) => q.metadata.contentType)
38485
+ );
38486
+ }
38487
+ function throwEmpty() {
38488
+ throw new Error("no qualities");
38489
+ }
38490
+ describe("filterTimelineQualities", () => {
38491
+ const timeline = timelineOf2([
38492
+ createQuality2("audio", 128e3),
38493
+ createQuality2("audio", 256e3),
38494
+ createQuality2("video", 5e5)
38495
+ ]);
38416
38496
  it("returns the timeline unchanged when the filter is null", () => {
38417
- const result = filterTimelineQualities(
38418
- null,
38419
- () => {
38420
- throw new Error("empty");
38421
- },
38422
- timeline
38423
- );
38497
+ const result = filterTimelineQualities(null, throwEmpty, timeline);
38424
38498
  expect(result).toBe(timeline);
38425
38499
  });
38426
- it("filters qualities by predicate", () => {
38500
+ it("filters qualities by predicate while keeping every present stream", () => {
38427
38501
  const result = filterTimelineQualities(
38428
- (q) => q.contentType === "audio",
38429
- () => {
38430
- throw new Error("empty");
38431
- },
38502
+ (q) => q.bandwidth !== 256e3,
38503
+ throwEmpty,
38432
38504
  timeline
38433
38505
  );
38434
38506
  expect(result.periods[0].qualities.length).toBe(2);
38435
- expect(
38436
- result.periods[0].qualities.every(
38437
- (q) => q.metadata.contentType === "audio"
38438
- )
38439
- ).toBeTrue();
38507
+ expect(contentTypesOf(result)).toEqual(/* @__PURE__ */ new Set(["audio", "video"]));
38440
38508
  });
38441
38509
  it("preserves minBufferTime", () => {
38442
- const result = filterTimelineQualities(
38443
- () => true,
38444
- () => {
38445
- throw new Error("empty");
38446
- },
38447
- timeline
38448
- );
38510
+ const result = filterTimelineQualities(() => true, throwEmpty, timeline);
38449
38511
  expect(result.minBufferTime).toBe(2);
38450
38512
  });
38451
38513
  it("throws when filtering removes all qualities", () => {
38514
+ expect(
38515
+ () => filterTimelineQualities(() => false, throwEmpty, timeline)
38516
+ ).toThrowError("no qualities");
38517
+ });
38518
+ it("throws when filtering drops a present video stream to zero", () => {
38452
38519
  expect(
38453
38520
  () => filterTimelineQualities(
38454
- () => false,
38455
- () => {
38456
- throw new Error("no qualities");
38457
- },
38521
+ (q) => q.contentType === "audio",
38522
+ throwEmpty,
38458
38523
  timeline
38459
38524
  )
38460
38525
  ).toThrowError("no qualities");
38461
38526
  });
38527
+ it("throws when filtering drops a present audio stream to zero", () => {
38528
+ expect(
38529
+ () => filterTimelineQualities(
38530
+ (q) => q.contentType === "video",
38531
+ throwEmpty,
38532
+ timeline
38533
+ )
38534
+ ).toThrowError("no qualities");
38535
+ });
38536
+ it("does not throw when losing only an optional text sidecar stream", () => {
38537
+ const withText = timelineOf2([
38538
+ createQuality2("audio", 128e3),
38539
+ createQuality2("video", 5e5),
38540
+ createQuality2("text", 1e3)
38541
+ ]);
38542
+ const result = filterTimelineQualities(
38543
+ (q) => q.contentType !== "text",
38544
+ throwEmpty,
38545
+ withText
38546
+ );
38547
+ expect(contentTypesOf(result)).toEqual(/* @__PURE__ */ new Set(["audio", "video"]));
38548
+ });
38462
38549
  });
38463
38550
  describe("filterTimelineQualitiesAsync", () => {
38464
- const timeline = {
38465
- periods: [
38466
- {
38467
- startTime: 0,
38468
- endTime: 10,
38469
- qualities: [
38470
- createQuality2("audio", 128e3),
38471
- createQuality2("video", 5e5)
38472
- ]
38473
- }
38474
- ],
38475
- minBufferTime: 2,
38476
- getAdBreaks: () => Promise.resolve([]),
38477
- getDuration: () => Promise.resolve(Infinity)
38478
- };
38479
- it("filters qualities by async predicate", async () => {
38551
+ const timeline = timelineOf2([
38552
+ createQuality2("audio", 128e3),
38553
+ createQuality2("audio", 256e3),
38554
+ createQuality2("video", 5e5)
38555
+ ]);
38556
+ it("filters qualities by async predicate while keeping every present stream", async () => {
38480
38557
  const result = await filterTimelineQualitiesAsync(
38481
- (q) => Promise.resolve(q.contentType === "audio"),
38482
- () => {
38483
- throw new Error("empty");
38484
- },
38558
+ (q) => Promise.resolve(q.bandwidth !== 256e3),
38559
+ throwEmpty,
38485
38560
  timeline
38486
38561
  );
38487
- expect(result.periods[0].qualities.length).toBe(1);
38488
- expect(result.periods[0].qualities[0].metadata.contentType).toBe(
38489
- "audio"
38490
- );
38562
+ expect(result.periods[0].qualities.length).toBe(2);
38563
+ expect(contentTypesOf(result)).toEqual(/* @__PURE__ */ new Set(["audio", "video"]));
38491
38564
  });
38492
38565
  it("preserves minBufferTime", async () => {
38493
38566
  const result = await filterTimelineQualitiesAsync(
38494
38567
  () => Promise.resolve(true),
38495
- () => {
38496
- throw new Error("empty");
38497
- },
38568
+ throwEmpty,
38498
38569
  timeline
38499
38570
  );
38500
38571
  expect(result.minBufferTime).toBe(2);
@@ -38503,13 +38574,33 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
38503
38574
  await expectAsync(
38504
38575
  filterTimelineQualitiesAsync(
38505
38576
  () => Promise.resolve(false),
38506
- () => {
38507
- throw new Error("no qualities");
38508
- },
38577
+ throwEmpty,
38578
+ timeline
38579
+ )
38580
+ ).toBeRejectedWithError("no qualities");
38581
+ });
38582
+ it("throws when filtering drops a present video stream to zero", async () => {
38583
+ await expectAsync(
38584
+ filterTimelineQualitiesAsync(
38585
+ (q) => Promise.resolve(q.contentType === "audio"),
38586
+ throwEmpty,
38509
38587
  timeline
38510
38588
  )
38511
38589
  ).toBeRejectedWithError("no qualities");
38512
38590
  });
38591
+ it("does not throw when losing only an optional text sidecar stream", async () => {
38592
+ const withText = timelineOf2([
38593
+ createQuality2("audio", 128e3),
38594
+ createQuality2("video", 5e5),
38595
+ createQuality2("text", 1e3)
38596
+ ]);
38597
+ const result = await filterTimelineQualitiesAsync(
38598
+ (q) => Promise.resolve(q.contentType !== "text"),
38599
+ throwEmpty,
38600
+ withText
38601
+ );
38602
+ expect(contentTypesOf(result)).toEqual(/* @__PURE__ */ new Set(["audio", "video"]));
38603
+ });
38513
38604
  });
38514
38605
 
38515
38606
  // test/unit/streaming/mediaTimelineLanguageFilter.test.ts
@@ -39341,7 +39432,8 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
39341
39432
  expect(track2.qualities).toEqual([qualityMetadata]);
39342
39433
  expect(track2.qualitiesUnfiltered).toEqual([qualityMetadata]);
39343
39434
  });
39344
- it("fails fast when a required content type is entirely filtered out", async () => {
39435
+ it("plays the surviving content type when another is entirely filtered out", async () => {
39436
+ deps.contentTypesValue.value = Promise.resolve(/* @__PURE__ */ new Set(["video"]));
39345
39437
  const videoQuality2 = __spreadProps(__spreadValues({}, createEmptyMediaQualityMetadata()), {
39346
39438
  contentType: "video",
39347
39439
  qualityId: "v1"
@@ -39365,10 +39457,10 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
39365
39457
  });
39366
39458
  track2 = createTrack();
39367
39459
  await flushPromises();
39368
- expect(track2.error).toEqual(any14(MediaUnsupportedError));
39369
- expect(track2.error.code).toBe(
39370
- "no-playable-audio"
39371
- );
39460
+ expect(track2.error).toBeNull();
39461
+ expect(track2.contentTypes).toEqual(/* @__PURE__ */ new Set(["video"]));
39462
+ expect(getStream("video")).not.toBeNull();
39463
+ expect(getStream("audio")).toBeNull();
39372
39464
  });
39373
39465
  it("reads qualities from the filtered timeline but qualitiesUnfiltered from the raw one", async () => {
39374
39466
  const makeTimeline = (qualities) => ({
@@ -47894,6 +47986,25 @@ x`);
47894
47986
  expect(withinMin(mid, 1, [low, mid])).toBe(true);
47895
47987
  expect(withinMin(low, 0, [low, mid])).toBe(false);
47896
47988
  });
47989
+ it("keeps the highest when every rate (of three) is below the floor", () => {
47990
+ const low = audioQuality2([16e3]);
47991
+ const mid = audioQuality2([24e3]);
47992
+ const high = audioQuality2([32e3]);
47993
+ expect(withinMin(high, 2, [low, mid, high])).toBe(true);
47994
+ expect(withinMin(mid, 1, [low, mid, high])).toBe(false);
47995
+ expect(withinMin(low, 0, [low, mid, high])).toBe(false);
47996
+ });
47997
+ it("keeps the highest below-floor rate when no platform max is reported", () => {
47998
+ capabilities.sampleRate = null;
47999
+ const low = audioQuality2([16e3]);
48000
+ const high = audioQuality2([32e3]);
48001
+ expect(withinMin(high, 1, [low, high])).toBe(true);
48002
+ expect(withinMin(low, 0, [low, high])).toBe(false);
48003
+ });
48004
+ it("keeps a single audio rendition below the floor", () => {
48005
+ const only = audioQuality2([24e3]);
48006
+ expect(withinMin(only, 0, [only])).toBe(true);
48007
+ });
47897
48008
  it("ignores non-audio qualities in the floor fallback", () => {
47898
48009
  const audio = audioQuality2([24e3]);
47899
48010
  const video = createEmptyMediaQualityMetadata();
@@ -47903,6 +48014,55 @@ x`);
47903
48014
  expect(withinMin(video, 1, [audio, video])).toBe(true);
47904
48015
  });
47905
48016
  });
48017
+ describe("withinAudioSampleRateRange straddling an empty band", () => {
48018
+ let capabilities;
48019
+ beforeEach(() => {
48020
+ setUserAgent("Chrome");
48021
+ capabilities = new MockCapabilities();
48022
+ capabilities.sampleRate = 48e3;
48023
+ });
48024
+ function audioQuality2(rate) {
48025
+ const quality = createEmptyMediaQualityMetadata();
48026
+ quality.contentType = "audio";
48027
+ quality.audioSamplingRate = rate;
48028
+ return quality;
48029
+ }
48030
+ function within(metadata, index, array2) {
48031
+ return withinAudioSampleRateRange(
48032
+ { capabilities, minSampleRate: 44100 },
48033
+ metadata,
48034
+ index,
48035
+ array2
48036
+ );
48037
+ }
48038
+ it("keeps the highest decodable rate when one is below the floor and one above the ceiling", () => {
48039
+ const low = audioQuality2([22050]);
48040
+ const high = audioQuality2([96e3]);
48041
+ expect(within(low, 0, [low, high])).toBe(true);
48042
+ expect(within(high, 1, [low, high])).toBe(false);
48043
+ });
48044
+ it("keeps the below-floor rate when the ceiling matches the output device rate", () => {
48045
+ capabilities.sampleRate = 44100;
48046
+ const low = audioQuality2([22050]);
48047
+ const high = audioQuality2([48e3]);
48048
+ expect(within(low, 0, [low, high])).toBe(true);
48049
+ expect(within(high, 1, [low, high])).toBe(false);
48050
+ });
48051
+ it("still prefers an in-band rate over the straddle fallback", () => {
48052
+ const low = audioQuality2([22050]);
48053
+ const mid = audioQuality2([44100]);
48054
+ const high = audioQuality2([96e3]);
48055
+ expect(within(mid, 1, [low, mid, high])).toBe(true);
48056
+ expect(within(low, 0, [low, mid, high])).toBe(false);
48057
+ expect(within(high, 2, [low, mid, high])).toBe(false);
48058
+ });
48059
+ it("keeps the lowest when every rate is above the ceiling", () => {
48060
+ const low = audioQuality2([88200]);
48061
+ const high = audioQuality2([96e3]);
48062
+ expect(within(low, 0, [low, high])).toBe(true);
48063
+ expect(within(high, 1, [low, high])).toBe(false);
48064
+ });
48065
+ });
47906
48066
 
47907
48067
  // test/unit/track/hls/buildHlsMediaTimeline.test.ts
47908
48068
  describe("buildHlsMediaTimeline", () => {
@@ -53347,6 +53507,7 @@ x`);
53347
53507
  let depFactories;
53348
53508
  let player;
53349
53509
  let mockOptions;
53510
+ const loggerRef2 = useMockLogger();
53350
53511
  polyfillCustomEvent();
53351
53512
  beforeEach(() => {
53352
53513
  mockOptions = { media: new MockHTMLAudioElement() };
@@ -53571,6 +53732,40 @@ x`);
53571
53732
  });
53572
53733
  expect(spy).not.toHaveBeenCalled();
53573
53734
  });
53735
+ it("logs each republished span at the verbose level with formatted times", () => {
53736
+ loggerRef2.value.logLevel = 0 /* VERBOSE */;
53737
+ deps.drmController.dispatch("loadSpanMeasured", {
53738
+ kind: "license",
53739
+ startTime: 1e3,
53740
+ endTime: 3500,
53741
+ trackUri: "track-a",
53742
+ mimeType: "audio/mp4",
53743
+ contentType: "audio"
53744
+ });
53745
+ expect(loggerRef2.value.verbose).toHaveBeenCalledOnceWith(
53746
+ player,
53747
+ "loadSpan",
53748
+ {
53749
+ kind: "license",
53750
+ trackUri: "track-a",
53751
+ mimeType: "audio/mp4",
53752
+ contentType: "audio",
53753
+ startTime: (/* @__PURE__ */ new Date(1e3)).toISOString(),
53754
+ endTime: (/* @__PURE__ */ new Date(3500)).toISOString(),
53755
+ durationMs: "2500.00"
53756
+ }
53757
+ );
53758
+ });
53759
+ it("does not format or log a span when the verbose level is off", () => {
53760
+ loggerRef2.value.logLevel = 1 /* DEBUG */;
53761
+ deps.drmController.dispatch("loadSpanMeasured", {
53762
+ kind: "license",
53763
+ startTime: 1e3,
53764
+ endTime: 3500,
53765
+ trackUri: "track-a"
53766
+ });
53767
+ expect(loggerRef2.value.verbose).not.toHaveBeenCalled();
53768
+ });
53574
53769
  });
53575
53770
  it("reloads the current track on a codecUnsupported event", () => {
53576
53771
  const trackController = deps.trackController;