@amazon/vinyl 3.1.0 → 3.2.1

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 (69) hide show
  1. package/dist/index.cjs +236 -187
  2. package/dist/index.cjs.map +4 -4
  3. package/dist/index.js +224 -174
  4. package/dist/index.js.map +4 -4
  5. package/dist/test/integ.es5.js +245 -231
  6. package/dist/test/integ.js +676 -271
  7. package/dist/test/integ.js.map +4 -4
  8. package/dist/types/current/ad/AdControllerImpl.d.ts +18 -2
  9. package/dist/types/current/ad/AdControllerImpl.d.ts.map +1 -1
  10. package/dist/types/current/ad/AdControllerImpl.js +42 -20
  11. package/dist/types/current/drm/DrmControllerImpl.d.ts +17 -1
  12. package/dist/types/current/drm/DrmControllerImpl.d.ts.map +1 -1
  13. package/dist/types/current/drm/DrmControllerImpl.js +45 -10
  14. package/dist/types/current/playback/createStallMonitor.d.ts.map +1 -1
  15. package/dist/types/current/playback/createStallMonitor.js +19 -14
  16. package/dist/types/current/streaming/LoadMetric.d.ts +15 -1
  17. package/dist/types/current/streaming/LoadMetric.d.ts.map +1 -1
  18. package/dist/types/current/streaming/mediaTimelineFilter.d.ts +4 -2
  19. package/dist/types/current/streaming/mediaTimelineFilter.d.ts.map +1 -1
  20. package/dist/types/current/streaming/mediaTimelineFilter.js +23 -8
  21. package/dist/types/current/track/filters/sampleRateFilter.d.ts +8 -4
  22. package/dist/types/current/track/filters/sampleRateFilter.d.ts.map +1 -1
  23. package/dist/types/current/track/filters/sampleRateFilter.js +31 -21
  24. package/dist/types/current/track/mse/MseTrack.d.ts +0 -9
  25. package/dist/types/current/track/mse/MseTrack.d.ts.map +1 -1
  26. package/dist/types/current/track/mse/MseTrack.js +2 -29
  27. package/dist/types/current/tsconfig.tsbuildinfo +1 -1
  28. package/dist/types/current/vinyl/VinylPlayer.d.ts +5 -0
  29. package/dist/types/current/vinyl/VinylPlayer.d.ts.map +1 -1
  30. package/dist/types/current/vinyl/VinylPlayer.js +23 -1
  31. package/dist/types/ts3.7/ad/AdControllerImpl.d.ts +18 -2
  32. package/dist/types/ts3.7/drm/DrmControllerImpl.d.ts +17 -1
  33. package/dist/types/ts3.7/streaming/LoadMetric.d.ts +15 -1
  34. package/dist/types/ts3.7/streaming/mediaTimelineFilter.d.ts +4 -2
  35. package/dist/types/ts3.7/track/filters/sampleRateFilter.d.ts +8 -4
  36. package/dist/types/ts3.7/track/mse/MseTrack.d.ts +0 -9
  37. package/dist/types/ts3.7/vinyl/VinylPlayer.d.ts +5 -0
  38. package/dist/types/ts4.1/ad/AdControllerImpl.d.ts +18 -2
  39. package/dist/types/ts4.1/drm/DrmControllerImpl.d.ts +17 -1
  40. package/dist/types/ts4.1/streaming/LoadMetric.d.ts +15 -1
  41. package/dist/types/ts4.1/streaming/mediaTimelineFilter.d.ts +4 -2
  42. package/dist/types/ts4.1/track/filters/sampleRateFilter.d.ts +8 -4
  43. package/dist/types/ts4.1/track/mse/MseTrack.d.ts +0 -9
  44. package/dist/types/ts4.1/vinyl/VinylPlayer.d.ts +5 -0
  45. package/dist/types/ts4.7/ad/AdControllerImpl.d.ts +18 -2
  46. package/dist/types/ts4.7/drm/DrmControllerImpl.d.ts +17 -1
  47. package/dist/types/ts4.7/streaming/LoadMetric.d.ts +15 -1
  48. package/dist/types/ts4.7/streaming/mediaTimelineFilter.d.ts +4 -2
  49. package/dist/types/ts4.7/track/filters/sampleRateFilter.d.ts +8 -4
  50. package/dist/types/ts4.7/track/mse/MseTrack.d.ts +0 -9
  51. package/dist/types/ts4.7/vinyl/VinylPlayer.d.ts +5 -0
  52. package/dist/unit/types/current/ad/AdControllerImpl.test.js +110 -4
  53. package/dist/unit/types/current/drm/DrmControllerImpl.test.js +53 -9
  54. package/dist/unit/types/current/playback/createStallMonitor.test.js +38 -0
  55. package/dist/unit/types/current/streaming/MseTrack.test.js +11 -8
  56. package/dist/unit/types/current/streaming/mediaTimelineFilter.test.js +60 -54
  57. package/dist/unit/types/current/track/filters/sampleRateFilter.test.js +75 -0
  58. package/dist/unit/types/current/tsconfig.tsbuildinfo +1 -1
  59. package/dist/unit/types/current/vinyl/VinylPlayer.test.js +33 -2
  60. package/dist/vinylTestUtil/index.js +10 -1
  61. package/dist/vinylTestUtil/index.js.map +2 -2
  62. package/dist/vinylTestUtil/types/current/assets/vinylTestAssets.d.ts +5 -0
  63. package/dist/vinylTestUtil/types/current/assets/vinylTestAssets.d.ts.map +1 -1
  64. package/dist/vinylTestUtil/types/current/assets/vinylTestAssets.js +9 -0
  65. package/dist/vinylTestUtil/types/current/tsconfig.tsbuildinfo +1 -1
  66. package/dist/vinylTestUtil/types/ts3.7/assets/vinylTestAssets.d.ts +5 -0
  67. package/dist/vinylTestUtil/types/ts4.1/assets/vinylTestAssets.d.ts +5 -0
  68. package/dist/vinylTestUtil/types/ts4.7/assets/vinylTestAssets.d.ts +5 -0
  69. package/package.json +10 -10
@@ -7,6 +7,15 @@ import type { AdsProvider } from './AdsProvider';
7
7
  * The % playhead start tolerance for playback progress when emitting ad progress events.
8
8
  */
9
9
  export declare const AD_START_TOLERANCE = 0.05;
10
+ /**
11
+ * When a break's X-PLAYOUT-LIMIT budget is reached, an ad whose real media end is
12
+ * within this many seconds is allowed to finish on its own `ended` rather than
13
+ * being cut. The limit is a coarse, often whole-second budget while the media's
14
+ * true length is fractional, so without this grace an ad effectively as long as
15
+ * its budget would be clipped a fraction short. The tolerance is measured against
16
+ * the playback-reported (media) duration, not the ad's declared duration.
17
+ */
18
+ export declare const PLAYOUT_LIMIT_TOLERANCE = 1;
10
19
  export interface AdControllerImplDeps {
11
20
  readonly playbackController: ReadonlyPlaybackController;
12
21
  }
@@ -129,8 +138,15 @@ export declare class AdControllerImpl extends EventHostImpl<AdEventMap> implemen
129
138
  private emitAdProgressEvents;
130
139
  private emitTimeUpdate;
131
140
  /**
132
- * Ads breaks can have a playout limit that may cut off ad playback.
133
- * End the ad if that limit is reached.
141
+ * Enforces the break-level playout limit (X-PLAYOUT-LIMIT): a budget shared
142
+ * across all of the break's ads. When cumulative playout reaches it, the
143
+ * whole break ends, dropping any ads that have not started.
144
+ *
145
+ * This is deliberately NOT a per-ad cap. Each ad asset is its own source and
146
+ * ends on its media `ended` (see the 'ended' handler), which is what advances
147
+ * a multi-ad break. Ending an ad at its declared per-ad DURATION instead would
148
+ * truncate the real asset whenever a packager under-declares it — e.g. rounds
149
+ * a 6.732s asset down to a whole-second 6 — cutting off the fractional tail.
134
150
  */
135
151
  private enforcePlayoutLimits;
136
152
  private setPendingBreaks;
@@ -1 +1 @@
1
- {"version":3,"file":"AdControllerImpl.d.ts","sourceRoot":"","sources":["../../../../src/ad/AdControllerImpl.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AdControllerImpl.d.ts","sourceRoot":"","sources":["../../../../src/ad/AdControllerImpl.ts"],"names":[],"mappings":"AAKA,OAAO,EAGH,aAAa,EAehB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACH,KAAK,WAAW,EAEhB,KAAK,MAAM,EACX,KAAK,QAAQ,EAChB,MAAM,eAAe,CAAA;AACtB,OAAO,KAAK,EAER,YAAY,EACZ,UAAU,EAEb,MAAM,gBAAgB,CAAA;AACvB,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,wCAAwC,CAAA;AAExF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAEhD;;GAEG;AACH,eAAO,MAAM,kBAAkB,OAAO,CAAA;AAEtC;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC,MAAM,WAAW,oBAAoB;IACjC,QAAQ,CAAC,kBAAkB,EAAE,0BAA0B,CAAA;CAC1D;AAED,MAAM,WAAW,uBAAuB;IACpC;;;;OAIG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAE9B;;;;;;OAMG;IACH,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;CACpC;AAED,eAAO,MAAM,8BAA8B,EAAE,uBAG5C,CAAA;AA2CD;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,gBACT,SAAQ,aAAa,CAAC,UAAU,CAChC,YAAW,YAAY;IAkCnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAhCzB,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,MAAM,CAEjC;IAED,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,SAAS,CAAwB;IACzC,OAAO,CAAC,cAAc,CAA2B;IACjD,OAAO,CAAC,WAAW,CAA2B;IAE9C,OAAO,CAAC,UAAU,CAAkB;IAGpC,OAAO,CAAC,iBAAiB,CAAkB;IAI3C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAwB;IAE1D,OAAO,CAAC,gBAAgB,CAAI;IAC5B,OAAO,CAAC,eAAe,CAAoB;IAG3C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAwB;IAI3D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAwB;IACtD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmB;IAC5C,OAAO,CAAC,YAAY,CAA4B;IAChD,OAAO,CAAC,WAAW,CAAQ;gBAGN,IAAI,EAAE,oBAAoB,EAC3C,OAAO,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC;IAuD9C,iBAAiB,IAAI,IAAI;IAMzB,OAAO,CAAC,iBAAiB;IAKzB,8EAA8E;IAC9E,OAAO,CAAC,SAAS;IAUjB;;;;;OAKG;YACW,gBAAgB;IAwB9B,IAAI,eAAe,IAAI,QAAQ,GAAG,IAAI,CAErC;IAED,OAAO,CAAC,WAAW;IAqBnB;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAK3B,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI;IAkC5C,YAAY,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAI3C,aAAa,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;YAIpC,SAAS;IAgCvB,OAAO,KAAK,WAAW,GAEtB;IAED,WAAW,IAAI,IAAI;IAWnB;;OAEG;IACH,OAAO,CAAC,eAAe;IA4CvB,IAAI,cAAc,IAAI,WAAW,GAAG,IAAI,CAEvC;IAED;;OAEG;IACH,OAAO,CAAC,UAAU;IAqBlB;;;OAGG;IACH,OAAO,CAAC,aAAa;IAsErB,IAAI,SAAS,IAAI,MAAM,GAAG,IAAI,CAE7B;IAED,OAAO,KAAK,OAAO,GAElB;IAED,OAAO,CAAC,OAAO;IAwBf;;OAEG;IACH,OAAO,CAAC,YAAY,CAkBnB;IAED;;;;;;OAMG;IACH,OAAO,CAAC,cAAc;IAmBtB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAgC3B;;;;OAIG;YACW,oBAAoB;YAyCpB,cAAc;IA+C5B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,oBAAoB;IA4B5B,OAAO,CAAC,gBAAgB;IAYxB,MAAM,IAAI,IAAI;IAKd,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAgB1B,OAAO,CAAC,KAAK;IAMb,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED;;OAEG;IACH,OAAO,IAAI,IAAI;CAWlB"}
@@ -8,6 +8,15 @@ import { AdError } from './AdError';
8
8
  * The % playhead start tolerance for playback progress when emitting ad progress events.
9
9
  */
10
10
  export const AD_START_TOLERANCE = 0.05;
11
+ /**
12
+ * When a break's X-PLAYOUT-LIMIT budget is reached, an ad whose real media end is
13
+ * within this many seconds is allowed to finish on its own `ended` rather than
14
+ * being cut. The limit is a coarse, often whole-second budget while the media's
15
+ * true length is fractional, so without this grace an ad effectively as long as
16
+ * its budget would be clipped a fraction short. The tolerance is measured against
17
+ * the playback-reported (media) duration, not the ad's declared duration.
18
+ */
19
+ export const PLAYOUT_LIMIT_TOLERANCE = 1;
11
20
  export const defaultAdControllerImplOptions = {
12
21
  adLoadTimeout: 15,
13
22
  preloadAheadTime: 10,
@@ -560,30 +569,43 @@ export class AdControllerImpl extends EventHostImpl {
560
569
  });
561
570
  }
562
571
  /**
563
- * Ads breaks can have a playout limit that may cut off ad playback.
564
- * End the ad if that limit is reached.
572
+ * Enforces the break-level playout limit (X-PLAYOUT-LIMIT): a budget shared
573
+ * across all of the break's ads. When cumulative playout reaches it, the
574
+ * whole break ends, dropping any ads that have not started.
575
+ *
576
+ * This is deliberately NOT a per-ad cap. Each ad asset is its own source and
577
+ * ends on its media `ended` (see the 'ended' handler), which is what advances
578
+ * a multi-ad break. Ending an ad at its declared per-ad DURATION instead would
579
+ * truncate the real asset whenever a packager under-declares it — e.g. rounds
580
+ * a 6.732s asset down to a whole-second 6 — cutting off the fractional tail.
565
581
  */
566
582
  enforcePlayoutLimits(adState) {
583
+ if (!adState.started)
584
+ return;
585
+ const { parentBreak } = adState;
586
+ // Enforce independent of a finite content duration so live ads with a
587
+ // declared playout limit are still bounded.
588
+ const limit = parentBreak.adBreak.playoutLimit;
589
+ if (limit == null)
590
+ return;
567
591
  const pC = this.deps.playbackController;
568
- const { ad, parentBreak } = adState;
569
- // Enforce playout limits independent of a finite content duration so
570
- // live ads with a declared playout limit are still bounded.
571
- if (adState.started) {
572
- const elapsed = Math.max(0, pC.currentTime - adState.timeStart);
573
- const adBreak = parentBreak.adBreak;
574
- const limit = adBreak.playoutLimit;
575
- if (limit != null &&
576
- parentBreak.playoutElapsed + elapsed >= limit) {
577
- // The break's total playout limit is reached: end the whole
578
- // break, dropping any ads that have not started.
579
- parentBreak.clear();
580
- this.endAd(adState);
581
- }
582
- else if (ad.duration != null && elapsed >= ad.duration) {
583
- // This ad reached its declared duration: advance to the next.
584
- this.endAd(adState);
585
- }
592
+ const elapsed = Math.max(0, pC.currentTime - adState.timeStart);
593
+ if (parentBreak.playoutElapsed + elapsed < limit)
594
+ return;
595
+ // Budget reached — but if the current ad is essentially at its natural
596
+ // media end (within PLAYOUT_LIMIT_TOLERANCE of the playback-reported
597
+ // duration), let it finish on its own `ended` rather than clipping the
598
+ // tail. A known finite, positive duration is required; an unknown (0) or
599
+ // non-finite duration (e.g. live) enforces the limit strictly.
600
+ if (isFinite(pC.duration) &&
601
+ pC.duration > 0 &&
602
+ pC.duration - pC.currentTime <= PLAYOUT_LIMIT_TOLERANCE) {
603
+ return;
586
604
  }
605
+ // The break's total playout limit is reached: end the whole break,
606
+ // dropping any ads that have not started.
607
+ parentBreak.clear();
608
+ this.endAd(adState);
587
609
  }
588
610
  setPendingBreaks(newPending) {
589
611
  this.pendingAdBreaks = newPending.filter((adBreak) => !this.isBreakSuppressed(adBreak));
@@ -2,7 +2,7 @@ import { EventHostImpl, type Maybe } from '@amazon/vinyl-util';
2
2
  import { type DrmOptions } from './DrmOptions';
3
3
  import type { DrmController, DrmControllerEventMap, DrmKeySystemSupport, DrmPlaybackOptions } from './DrmController';
4
4
  import type { CommonEme, CommonMediaKeyMessageEvent, CommonMediaKeys, CommonMediaKeySession } from './commonEme/CommonEme';
5
- import type { MediaFormatMetadata } from '../streaming/MediaQualityMetadata';
5
+ import type { ContentType, MediaFormatMetadata } from '../streaming/MediaQualityMetadata';
6
6
  import type { TrackUri } from '../track/Track';
7
7
  /**
8
8
  * The number of seconds a license request will be allowed before timing out.
@@ -30,6 +30,13 @@ export interface DrmControllerMessageProps {
30
30
  * to their own track.
31
31
  */
32
32
  readonly trackUri: TrackUri | null;
33
+ /**
34
+ * The MIME and content type of the session's content, captured at creation
35
+ * so the license span can distinguish the certificate exchange from the
36
+ * per-content-type license exchanges.
37
+ */
38
+ readonly mimeType: string;
39
+ readonly contentType: ContentType | null;
33
40
  }
34
41
  export declare class DrmControllerImpl extends EventHostImpl<DrmControllerEventMap> implements DrmController {
35
42
  private readonly deps;
@@ -44,6 +51,8 @@ export declare class DrmControllerImpl extends EventHostImpl<DrmControllerEventM
44
51
  private bufferingTrackUri;
45
52
  private sessionAbort;
46
53
  private pendingMessageProps;
54
+ private keySetupSpanArmed;
55
+ private keySetupSpanStart;
47
56
  private readonly disposer;
48
57
  constructor(deps: DrmControllerImplDeps, options?: Maybe<Partial<DrmOptions>>);
49
58
  configure(options: Maybe<Partial<DrmOptions>>): void;
@@ -64,6 +73,13 @@ export declare class DrmControllerImpl extends EventHostImpl<DrmControllerEventM
64
73
  * The new session will be added to the current sessions list.
65
74
  */
66
75
  private maybeCreateSession;
76
+ /**
77
+ * Captures the key-setup span start at the top of the session-creation path
78
+ * (before media keys are attached), latching so only the first session of
79
+ * the current track load is measured.
80
+ */
81
+ private markKeySetupStart;
82
+ private dispatchLoadSpan;
67
83
  initializeForPlayback(drmInfo: MediaFormatMetadata | null, options?: DrmPlaybackOptions): void;
68
84
  /** True when the manifest carried pssh (sessions come from it, not events). */
69
85
  private manifestProvidesInitData;
@@ -1 +1 @@
1
- {"version":3,"file":"DrmControllerImpl.d.ts","sourceRoot":"","sources":["../../../../src/drm/DrmControllerImpl.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DrmControllerImpl.d.ts","sourceRoot":"","sources":["../../../../src/drm/DrmControllerImpl.ts"],"names":[],"mappings":"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"}
@@ -35,6 +35,10 @@ export class DrmControllerImpl extends EventHostImpl {
35
35
  bufferingTrackUri = null;
36
36
  sessionAbort = null;
37
37
  pendingMessageProps = null;
38
+ // Armed per track load; latches on the first session so only the
39
+ // critical-path session's EME/CDM setup is measured.
40
+ keySetupSpanArmed = true;
41
+ keySetupSpanStart = null;
38
42
  disposer = createDisposer();
39
43
  constructor(deps, options) {
40
44
  super();
@@ -160,6 +164,7 @@ export class DrmControllerImpl extends EventHostImpl {
160
164
  if (!hasMimeType(drmInfo)) {
161
165
  throw new DrmError('Encrypted content must have a mimeType.');
162
166
  }
167
+ this.markKeySetupStart();
163
168
  const mediaKeys = await this.attachMediaKeys(drmInfo);
164
169
  this.abortIfDisposed();
165
170
  // Resolve server certificate from configuration
@@ -204,6 +209,33 @@ export class DrmControllerImpl extends EventHostImpl {
204
209
  });
205
210
  return newSession;
206
211
  }
212
+ /**
213
+ * Captures the key-setup span start at the top of the session-creation path
214
+ * (before media keys are attached), latching so only the first session of
215
+ * the current track load is measured.
216
+ */
217
+ markKeySetupStart() {
218
+ if (!this.keySetupSpanArmed)
219
+ return;
220
+ this.keySetupSpanArmed = false;
221
+ this.keySetupSpanStart = Date.now();
222
+ }
223
+ dispatchLoadSpan(kind, startTime, endTime, messageProps) {
224
+ this.dispatch('loadSpanMeasured', {
225
+ kind,
226
+ startTime,
227
+ endTime,
228
+ mimeType: messageProps.mimeType,
229
+ // Attributed at the source to the session's track; omitted (not set
230
+ // to undefined) when unknown, per exactOptionalPropertyTypes.
231
+ ...(messageProps.trackUri != null && {
232
+ trackUri: messageProps.trackUri,
233
+ }),
234
+ ...(messageProps.contentType != null && {
235
+ contentType: messageProps.contentType,
236
+ }),
237
+ });
238
+ }
207
239
  initializeForPlayback(drmInfo, options) {
208
240
  logDebug(this, 'initializeForPlayback', drmInfo);
209
241
  if (drmInfo?.contentProtections.length) {
@@ -222,6 +254,9 @@ export class DrmControllerImpl extends EventHostImpl {
222
254
  this.drmInfo = drmInfo;
223
255
  this.bufferingTrackUri = options?.trackUri ?? null;
224
256
  this.sessionAbort = options?.abort ?? null;
257
+ // Re-arm so the new track load's first session is measured.
258
+ this.keySetupSpanArmed = true;
259
+ this.keySetupSpanStart = null;
225
260
  }
226
261
  /**
227
262
  * If the drm metadata contains initialization data for the current key system, create a new session.
@@ -232,6 +267,7 @@ export class DrmControllerImpl extends EventHostImpl {
232
267
  this.handleError(new DrmError('DRM not supported.'));
233
268
  return;
234
269
  }
270
+ this.markKeySetupStart();
235
271
  const mediaKeys = await this.attachMediaKeys(drmInfo);
236
272
  const drmProtection = drmInfo.contentProtections.find((cP) => {
237
273
  return cP.keySystem === mediaKeys.keySystem;
@@ -355,6 +391,8 @@ export class DrmControllerImpl extends EventHostImpl {
355
391
  // Captured per session, so a concurrent preloaded track's
356
392
  // license exchange keeps its own attribution.
357
393
  trackUri: trackUri,
394
+ mimeType: drmInfo.mimeType,
395
+ contentType: drmInfo.contentType,
358
396
  }), LICENSE_TIMEOUT, { message: LICENSE_TIMEOUT_MESSAGE }).catch(this.handleError);
359
397
  });
360
398
  session.on('error', (event) => {
@@ -372,6 +410,12 @@ export class DrmControllerImpl extends EventHostImpl {
372
410
  this.pendingMessageProps = messageProps;
373
411
  const keySystem = mediaKeys.keySystem;
374
412
  logDebug(this, 'message', keySystem, event.message.byteLength);
413
+ // The CDM's first message ends the setup span; it is contiguous with the
414
+ // license round-trip measured below.
415
+ if (this.keySetupSpanStart != null) {
416
+ this.dispatchLoadSpan('keySetup', this.keySetupSpanStart, Date.now(), messageProps);
417
+ this.keySetupSpanStart = null;
418
+ }
375
419
  const keySystemOptions = this.options.keySystems[keySystem] ?? this.options.keySystems['*'];
376
420
  const licenseServerOptionsProvider = keySystemOptions?.licenseServer;
377
421
  // Get the license server configuration for the current key system.
@@ -393,16 +437,7 @@ export class DrmControllerImpl extends EventHostImpl {
393
437
  const key = await this.licenseProvider(keySystem, licenseServerOptions, challenge);
394
438
  if (this.disposed)
395
439
  return;
396
- this.dispatch('loadSpanMeasured', {
397
- kind: 'license',
398
- startTime: licenseSpanStart,
399
- endTime: Date.now(),
400
- // Attributed at the source to the session's track; omitted (not set
401
- // to undefined) when unknown, per exactOptionalPropertyTypes.
402
- ...(messageProps.trackUri != null && {
403
- trackUri: messageProps.trackUri,
404
- }),
405
- });
440
+ this.dispatchLoadSpan('license', licenseSpanStart, Date.now(), messageProps);
406
441
  this.pendingMessageProps = null;
407
442
  if (session.disposed)
408
443
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"createStallMonitor.d.ts","sourceRoot":"","sources":["../../../../src/playback/createStallMonitor.ts"],"names":[],"mappings":"AAKA,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAA;AAC9E,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAE/D,MAAM,WAAW,mBAAmB;IAChC;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAEhC;;OAEG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CACjC;AAED,MAAM,WAAW,cAAc;IAC3B,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAA;IAC3B,4BAA4B;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,8CAA8C;IAC9C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,2BAA2B;IAC3B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAA;CACpC;AAED,MAAM,WAAW,qBAAqB;IAClC,6CAA6C;IAC7C,QAAQ,CAAC,cAAc,EAAE,MAAM,IAAI,CAAA;IACnC,yCAAyC;IACzC,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAA;CACxD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAC9B,UAAU,EAAE,0BAA0B,EACtC,SAAS,EAAE,qBAAqB,EAChC,OAAO,GAAE,mBAAwB,GAClC,MAAM,IAAI,CA8EZ"}
1
+ {"version":3,"file":"createStallMonitor.d.ts","sourceRoot":"","sources":["../../../../src/playback/createStallMonitor.ts"],"names":[],"mappings":"AAKA,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAA;AAC9E,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAE/D,MAAM,WAAW,mBAAmB;IAChC;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAEhC;;OAEG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CACjC;AAED,MAAM,WAAW,cAAc;IAC3B,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAA;IAC3B,4BAA4B;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,8CAA8C;IAC9C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,2BAA2B;IAC3B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAA;CACpC;AAED,MAAM,WAAW,qBAAqB;IAClC,6CAA6C;IAC7C,QAAQ,CAAC,cAAc,EAAE,MAAM,IAAI,CAAA;IACnC,yCAAyC;IACzC,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAA;CACxD;AASD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAC9B,UAAU,EAAE,0BAA0B,EACtC,SAAS,EAAE,qBAAqB,EAChC,OAAO,GAAE,mBAAwB,GAClC,MAAM,IAAI,CA+EZ"}
@@ -4,6 +4,12 @@
4
4
  */
5
5
  import { createDisposer } from '@amazon/vinyl-util';
6
6
  import { StallEndedReason } from './ReadonlyPlaybackController';
7
+ /**
8
+ * Minimum forward `currentTime` change (seconds) that counts as real progress. Guards the
9
+ * progress check against floating-point jitter so a negligible advance isn't mistaken for the
10
+ * play head moving.
11
+ */
12
+ const PROGRESS_EPSILON = 1e-6;
7
13
  /**
8
14
  * Watches a {@link ReadonlyPlaybackController} for stalls while playing: the play head freezing
9
15
  * (no `timeUpdate`) for longer than {@link StallMonitorOptions.stallThreshold} seconds.
@@ -26,6 +32,7 @@ export function createStallMonitor(controller, callbacks, options = {}) {
26
32
  let lastProgressAt = Date.now();
27
33
  let inStall = false;
28
34
  let pollId = null;
35
+ const { add, dispose } = createDisposer();
29
36
  const endStall = (reason) => {
30
37
  if (!inStall)
31
38
  return;
@@ -44,6 +51,7 @@ export function createStallMonitor(controller, callbacks, options = {}) {
44
51
  clearInterval(pollId);
45
52
  pollId = null;
46
53
  };
54
+ add(stopPoll);
47
55
  const startPoll = () => {
48
56
  if (pollId != null)
49
57
  return;
@@ -63,18 +71,22 @@ export function createStallMonitor(controller, callbacks, options = {}) {
63
71
  observedPlaying = false;
64
72
  stopPoll();
65
73
  };
66
- const { add, dispose } = createDisposer();
67
74
  add(controller.on('playing', () => {
68
- // Resuming from a stall closes it; otherwise this is a fresh start of playback, so
69
- // begin the freeze clock now (no `timeUpdate` has necessarily arrived yet).
75
+ // Resuming from a stall closes it (using the pre-resume freeze start for its duration).
70
76
  if (inStall)
71
77
  endStall(StallEndedReason.PLAYING);
72
- else
73
- lastProgressAt = Date.now();
78
+ // Whether resuming or freshly starting, the play head is advancing again, so restart the
79
+ // freeze clock. Without this the poll could immediately re-detect a stall from the stale
80
+ // pre-freeze timestamp before the first post-resume `timeUpdate` resets it.
81
+ lastProgressAt = Date.now();
74
82
  observedPlaying = true;
75
83
  startPoll();
76
84
  }));
77
- add(controller.on('timeUpdate', () => {
85
+ add(controller.on('timeUpdate', ({ previous, current }) => {
86
+ // Browsers emit `timeUpdate` while frozen (unchanged/backward `currentTime`); only a
87
+ // forward advance is progress, else a stall is ended and re-detected, doubling events.
88
+ if (previous != null && current - previous <= PROGRESS_EPSILON)
89
+ return;
78
90
  // The play head advanced: close any open stall, then reset the freeze clock.
79
91
  if (inStall)
80
92
  endStall(StallEndedReason.PLAYING);
@@ -83,12 +95,5 @@ export function createStallMonitor(controller, callbacks, options = {}) {
83
95
  add(controller.on('pause', () => suspend(StallEndedReason.PAUSE)));
84
96
  add(controller.on('seeking', () => suspend(StallEndedReason.SEEKING)));
85
97
  add(controller.on('emptied', () => suspend(StallEndedReason.EMPTIED)));
86
- let stopped = false;
87
- return () => {
88
- if (stopped)
89
- return;
90
- stopped = true;
91
- stopPoll();
92
- dispose();
93
- };
98
+ return dispose;
94
99
  }
@@ -1,14 +1,17 @@
1
1
  import type { Timestamp } from '@amazon/vinyl-util';
2
2
  import type { TrackUri } from '../track/Track';
3
+ import type { ContentType } from './MediaQualityMetadata';
3
4
  /**
4
5
  * The stage of the track-load journey a {@link LoadSpanMeasurement} describes.
5
6
  *
6
7
  * - `manifest`: fetching and parsing the DASH/HLS manifest.
8
+ * - `keySetup`: the EME/CDM setup, from session creation to the CDM's first
9
+ * license message. Precedes and is contiguous with `license`.
7
10
  * - `license`: the DRM license request/response exchange.
8
11
  * - `initSegment`: fetching the initialization segment.
9
12
  * - `firstSegment`: fetching the first media segment.
10
13
  */
11
- export type LoadSpanKind = 'manifest' | 'license' | 'initSegment' | 'firstSegment';
14
+ export type LoadSpanKind = 'manifest' | 'keySetup' | 'license' | 'initSegment' | 'firstSegment';
12
15
  /**
13
16
  * A timed span for one stage of a track load.
14
17
  *
@@ -35,6 +38,17 @@ export interface LoadSpanMeasurement {
35
38
  * buffering DRM info is set).
36
39
  */
37
40
  readonly trackUri?: TrackUri;
41
+ /**
42
+ * The MIME type of the content this span was measured for, when known. Set
43
+ * for DRM `license` spans so a certificate exchange and the per-content-type
44
+ * license exchanges are distinguishable.
45
+ */
46
+ readonly mimeType?: string;
47
+ /**
48
+ * The content type this span was measured for, when known. Set alongside
49
+ * {@link mimeType} for DRM `license` spans.
50
+ */
51
+ readonly contentType?: ContentType;
38
52
  }
39
53
  /**
40
54
  * A {@link LoadSpanMeasurement} attributed to the track it was measured for.
@@ -1 +1 @@
1
- {"version":3,"file":"LoadMetric.d.ts","sourceRoot":"","sources":["../../../../src/streaming/LoadMetric.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAE9C;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GACpB,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,cAAc,CAAA;AAE3D;;;;;;;;GAQG;AACH,MAAM,WAAW,mBAAmB;IAChC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;IAE3B;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;IAE7B;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAA;IAE3B;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAA;CAC/B;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAc,SAAQ,mBAAmB;IACtD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAA;CACnC"}
1
+ {"version":3,"file":"LoadMetric.d.ts","sourceRoot":"","sources":["../../../../src/streaming/LoadMetric.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAEzD;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GACpB,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,cAAc,CAAA;AAExE;;;;;;;;GAQG;AACH,MAAM,WAAW,mBAAmB;IAChC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;IAE3B;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;IAE7B;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAA;IAE3B;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAA;IAE5B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAE1B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAA;CACrC;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAc,SAAQ,mBAAmB;IACtD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAA;CACnC"}
@@ -3,12 +3,14 @@ import type { MediaQualityMetadata } from './MediaQualityMetadata';
3
3
  import type { FilterPredicate, FilterPredicateAsync, Maybe } from '@amazon/vinyl-util';
4
4
  /**
5
5
  * Filters qualities in a MediaTimeline using a synchronous predicate.
6
- * If filtering would remove all qualities from any period, throws via `throwError`.
6
+ * If filtering drops an audio or video stream to zero in any period, throws via
7
+ * `throwError`.
7
8
  */
8
9
  export declare function filterTimelineQualities(filter: Maybe<FilterPredicate<MediaQualityMetadata>>, throwError: () => never, timeline: MediaTimeline): MediaTimeline;
9
10
  /**
10
11
  * Filters qualities in a MediaTimeline using an asynchronous predicate.
11
- * If filtering would remove all qualities from any period, throws via `throwError`.
12
+ * If filtering drops an audio or video stream to zero in any period, throws via
13
+ * `throwError`.
12
14
  */
13
15
  export declare function filterTimelineQualitiesAsync(filter: FilterPredicateAsync<MediaQualityMetadata>, throwError: () => never, timeline: MediaTimeline): Promise<MediaTimeline>;
14
16
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"mediaTimelineFilter.d.ts","sourceRoot":"","sources":["../../../../src/streaming/mediaTimelineFilter.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAGR,aAAa,EAChB,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAClE,OAAO,KAAK,EACR,eAAe,EACf,oBAAoB,EACpB,KAAK,EACR,MAAM,oBAAoB,CAAA;AAG3B;;;GAGG;AACH,wBAAgB,uBAAuB,CACnC,MAAM,EAAE,KAAK,CAAC,eAAe,CAAC,oBAAoB,CAAC,CAAC,EACpD,UAAU,EAAE,MAAM,KAAK,EACvB,QAAQ,EAAE,aAAa,GACxB,aAAa,CAqBf;AAED;;;GAGG;AACH,wBAAsB,4BAA4B,CAC9C,MAAM,EAAE,oBAAoB,CAAC,oBAAoB,CAAC,EAClD,UAAU,EAAE,MAAM,KAAK,EACvB,QAAQ,EAAE,aAAa,GACxB,OAAO,CAAC,aAAa,CAAC,CAyBxB;AAED;;GAEG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAA"}
1
+ {"version":3,"file":"mediaTimelineFilter.d.ts","sourceRoot":"","sources":["../../../../src/streaming/mediaTimelineFilter.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAGR,aAAa,EAChB,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAElE,OAAO,KAAK,EACR,eAAe,EACf,oBAAoB,EACpB,KAAK,EACR,MAAM,oBAAoB,CAAA;AA0B3B;;;;GAIG;AACH,wBAAgB,uBAAuB,CACnC,MAAM,EAAE,KAAK,CAAC,eAAe,CAAC,oBAAoB,CAAC,CAAC,EACpD,UAAU,EAAE,MAAM,KAAK,EACvB,QAAQ,EAAE,aAAa,GACxB,aAAa,CAkBf;AAED;;;;GAIG;AACH,wBAAsB,4BAA4B,CAC9C,MAAM,EAAE,oBAAoB,CAAC,oBAAoB,CAAC,EAClD,UAAU,EAAE,MAAM,KAAK,EACvB,QAAQ,EAAE,aAAa,GACxB,OAAO,CAAC,aAAa,CAAC,CAsBxB;AAED;;GAEG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAA"}
@@ -2,10 +2,28 @@
2
2
  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
+ import { RESTRICTABLE_CONTENT_TYPES } from './MediaQualityMetadata';
5
6
  import { map, memoize } from '@amazon/vinyl-util';
7
+ /**
8
+ * Throws via `throwError` when filtering drops every rendition of an audio or
9
+ * video stream the period originally carried. Each surviving media stream needs
10
+ * a source buffer to append to; a stream filtered to zero can never create one,
11
+ * stalling playback on an unfulfilled `readyToAppend`, so a whole-stream drop is
12
+ * treated as unsupported media rather than a silent stall. Text is excluded —
13
+ * captions are an optional sidecar pipeline, not an MSE source buffer.
14
+ */
15
+ function assertMediaStreamsRetained(original, filtered, throwError) {
16
+ for (const contentType of RESTRICTABLE_CONTENT_TYPES) {
17
+ if (original.some((q) => q.metadata.contentType === contentType) &&
18
+ !filtered.some((q) => q.metadata.contentType === contentType)) {
19
+ throwError();
20
+ }
21
+ }
22
+ }
6
23
  /**
7
24
  * Filters qualities in a MediaTimeline using a synchronous predicate.
8
- * If filtering would remove all qualities from any period, throws via `throwError`.
25
+ * If filtering drops an audio or video stream to zero in any period, throws via
26
+ * `throwError`.
9
27
  */
10
28
  export function filterTimelineQualities(filter, throwError, timeline) {
11
29
  if (!filter)
@@ -13,16 +31,15 @@ export function filterTimelineQualities(filter, throwError, timeline) {
13
31
  const mapMetadataArray = memoize((qualities) => map(qualities, (q) => q.metadata), (qualities) => qualities, 1);
14
32
  const periods = timeline.periods.map((period) => {
15
33
  const filtered = period.qualities.filter((q, index) => filter(q.metadata, index, mapMetadataArray(period.qualities)));
34
+ assertMediaStreamsRetained(period.qualities, filtered, throwError);
16
35
  return { ...period, qualities: filtered };
17
36
  });
18
- if (periods.some((p) => p.qualities.length === 0)) {
19
- throwError();
20
- }
21
37
  return { ...timeline, periods };
22
38
  }
23
39
  /**
24
40
  * Filters qualities in a MediaTimeline using an asynchronous predicate.
25
- * If filtering would remove all qualities from any period, throws via `throwError`.
41
+ * If filtering drops an audio or video stream to zero in any period, throws via
42
+ * `throwError`.
26
43
  */
27
44
  export async function filterTimelineQualitiesAsync(filter, throwError, timeline) {
28
45
  const mapMetadataArray = memoize((qualities) => map(qualities, (q) => q.metadata), (qualities) => qualities, 1);
@@ -35,11 +52,9 @@ export async function filterTimelineQualitiesAsync(filter, throwError, timeline)
35
52
  filtered.push(period.qualities[i]);
36
53
  }
37
54
  }
55
+ assertMediaStreamsRetained(period.qualities, filtered, throwError);
38
56
  periods.push({ ...period, qualities: filtered });
39
57
  }
40
- if (periods.some((p) => p.qualities.length === 0)) {
41
- throwError();
42
- }
43
58
  return { ...timeline, periods };
44
59
  }
45
60
  /**
@@ -18,10 +18,14 @@ export interface AudioSampleRateRangeOptions {
18
18
  * The upper bound is `maxSampleRate` when set, otherwise the platform's
19
19
  * `AudioContext`-reported output rate (Firefox is hard-capped at 48kHz, which it
20
20
  * cannot decode beyond via MSE). The optional lower bound is `minSampleRate`.
21
- * Both bounds are soft: if every audio rendition is above the max the lowest is
22
- * kept, and if every audio rendition is below the min the highest is kept, so
23
- * playback is never stranded. Only audio renditions count toward those
24
- * fallbacks — a co-present video quality (no audio rate) never influences them.
21
+ * Both bounds are soft, and soft *together*: when no audio rendition sits in the
22
+ * acceptable `[minSampleRate, maxSampleRate]` band, a single fallback is kept so
23
+ * playback is never stranded — the highest rate the ceiling can decode, or, when
24
+ * every rate is above the ceiling, the lowest. This holds even when renditions
25
+ * straddle an empty band (some below the floor, some above the ceiling). Only
26
+ * audio renditions count toward the fallback — a co-present video quality (no
27
+ * audio rate) never influences it. Firefox is the exception: its 48kHz cap is
28
+ * hard, with no fallback.
25
29
  */
26
30
  export declare function withinAudioSampleRateRange(options: AudioSampleRateRangeOptions, metadata: MediaQualityMetadata, _index: number, array: ArrayLike<MediaQualityMetadata>): boolean;
27
31
  //# sourceMappingURL=sampleRateFilter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sampleRateFilter.d.ts","sourceRoot":"","sources":["../../../../../src/track/filters/sampleRateFilter.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAA;AAWhF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAE7D,wBAAgB,6BAA6B,IAAI,KAAK,CAErD;AAED,MAAM,WAAW,2BAA2B;IACxC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAA;IACnC;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtC,+CAA+C;IAC/C,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACzC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CACtC,OAAO,EAAE,2BAA2B,EACpC,QAAQ,EAAE,oBAAoB,EAC9B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,SAAS,CAAC,oBAAoB,CAAC,GACvC,OAAO,CA+CT"}
1
+ {"version":3,"file":"sampleRateFilter.d.ts","sourceRoot":"","sources":["../../../../../src/track/filters/sampleRateFilter.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAA;AAUhF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAE7D,wBAAgB,6BAA6B,IAAI,KAAK,CAErD;AAED,MAAM,WAAW,2BAA2B;IACxC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAA;IACnC;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtC,+CAA+C;IAC/C,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACzC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,0BAA0B,CACtC,OAAO,EAAE,2BAA2B,EACpC,QAAQ,EAAE,oBAAoB,EAC9B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,SAAS,CAAC,oBAAoB,CAAC,GACvC,OAAO,CAmDT"}
@@ -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 { hasBrowser, Browser, last, MediaUnsupportedError, every, map, max, min, } from '@amazon/vinyl-util';
5
+ import { hasBrowser, Browser, last, MediaUnsupportedError, map, max, min, } from '@amazon/vinyl-util';
6
6
  export function throwSamplingRatesUnsupported() {
7
7
  throw new MediaUnsupportedError('No supported sample rate', 'sampling-rate');
8
8
  }
@@ -13,34 +13,46 @@ export function throwSamplingRatesUnsupported() {
13
13
  * The upper bound is `maxSampleRate` when set, otherwise the platform's
14
14
  * `AudioContext`-reported output rate (Firefox is hard-capped at 48kHz, which it
15
15
  * cannot decode beyond via MSE). The optional lower bound is `minSampleRate`.
16
- * Both bounds are soft: if every audio rendition is above the max the lowest is
17
- * kept, and if every audio rendition is below the min the highest is kept, so
18
- * playback is never stranded. Only audio renditions count toward those
19
- * fallbacks — a co-present video quality (no audio rate) never influences them.
16
+ * Both bounds are soft, and soft *together*: when no audio rendition sits in the
17
+ * acceptable `[minSampleRate, maxSampleRate]` band, a single fallback is kept so
18
+ * playback is never stranded — the highest rate the ceiling can decode, or, when
19
+ * every rate is above the ceiling, the lowest. This holds even when renditions
20
+ * straddle an empty band (some below the floor, some above the ceiling). Only
21
+ * audio renditions count toward the fallback — a co-present video quality (no
22
+ * audio rate) never influences it. Firefox is the exception: its 48kHz cap is
23
+ * hard, with no fallback.
20
24
  */
21
25
  export function withinAudioSampleRateRange(options, metadata, _index, array) {
22
- // Only audio renditions are gated on sampling rate; video and any other
23
- // content type (including muxed video that happens to carry an audio rate)
24
- // pass through untouched.
26
+ // Muxed video carrying an audio rate, and every non-audio type, pass through.
25
27
  if (metadata.contentType !== 'audio')
26
28
  return true;
27
29
  const samplingRate = last(metadata.audioSamplingRate);
28
30
  if (!samplingRate)
29
31
  return true; // sampling rate not set
32
+ const { minSampleRate } = options;
33
+ // An explicit maxSampleRate takes precedence over the AudioContext rate.
34
+ const maxSampleRate = options.maxSampleRate ?? options.capabilities.sampleRate;
30
35
  const audioRates = () => map(array, (item) => item.contentType === 'audio'
31
36
  ? last(item.audioSamplingRate)
32
37
  : undefined).filter((rate) => rate != null);
33
- // Lower bound (soft floor), independent of the platform max: below it, keep
34
- // only the highest when every audio rendition is below the floor.
35
- const { minSampleRate } = options;
36
- if (minSampleRate != null && samplingRate < minSampleRate) {
38
+ // The single rate to keep when this rendition is out of band — or undefined
39
+ // when some rendition IS in band, so out-of-band renditions are dropped.
40
+ // Evaluating both bounds together is what stops a straddle from stranding.
41
+ const outOfBandFallback = () => {
37
42
  const rates = audioRates();
38
- return (every(rates, (rate) => rate < minSampleRate) &&
39
- samplingRate === max(rates));
43
+ const inBand = (rate) => (minSampleRate == null || rate >= minSampleRate) &&
44
+ (!maxSampleRate || rate <= maxSampleRate);
45
+ if (rates.some(inBand))
46
+ return undefined;
47
+ const decodable = maxSampleRate
48
+ ? rates.filter((rate) => rate <= maxSampleRate)
49
+ : rates;
50
+ return decodable.length ? max(decodable) : min(rates);
51
+ };
52
+ // Lower bound (soft floor), independent of the platform max.
53
+ if (minSampleRate != null && samplingRate < minSampleRate) {
54
+ return samplingRate === outOfBandFallback();
40
55
  }
41
- // Upper bound. An explicit maxSampleRate takes precedence over the
42
- // AudioContext-reported output rate.
43
- const maxSampleRate = options.maxSampleRate ?? options.capabilities.sampleRate;
44
56
  // No platform max to gauge support: keep. This also lets >48kHz through on
45
57
  // Firefox (its cap below is skipped) — accepted.
46
58
  if (!maxSampleRate)
@@ -51,8 +63,6 @@ export function withinAudioSampleRateRange(options, metadata, _index, array) {
51
63
  }
52
64
  if (samplingRate <= maxSampleRate)
53
65
  return true;
54
- // Above the max: keep only the lowest when every audio rendition exceeds it.
55
- const rates = audioRates();
56
- return (every(rates, (rate) => rate > maxSampleRate) &&
57
- samplingRate === min(rates));
66
+ // Above the ceiling (soft): keep only the fallback when no rate is in band.
67
+ return samplingRate === outOfBandFallback();
58
68
  }