@videojs/spf 10.0.0-beta.28 → 10.0.0-beta.29

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 (72) hide show
  1. package/dist/default/hls.js +2 -1
  2. package/dist/default/media/dom/screen.js +129 -0
  3. package/dist/default/media/dom/screen.js.map +1 -0
  4. package/dist/default/media/errors.js +9 -2
  5. package/dist/default/media/errors.js.map +1 -1
  6. package/dist/default/media/primitives/select-tracks.js +31 -47
  7. package/dist/default/media/primitives/select-tracks.js.map +1 -1
  8. package/dist/default/playback/adapters/hls-background-video/adapter.js +99 -11
  9. package/dist/default/playback/adapters/hls-background-video/adapter.js.map +1 -1
  10. package/dist/default/playback/adapters/hls-video/error-surface.js.map +1 -1
  11. package/dist/default/playback/adapters/mux-video/adapter.js +16 -3
  12. package/dist/default/playback/adapters/mux-video/adapter.js.map +1 -1
  13. package/dist/default/playback/behaviors/collect-errors.js +36 -2
  14. package/dist/default/playback/behaviors/collect-errors.js.map +1 -1
  15. package/dist/default/playback/behaviors/dom/track-screen-resolution.js +29 -0
  16. package/dist/default/playback/behaviors/dom/track-screen-resolution.js.map +1 -0
  17. package/dist/default/playback/behaviors/select-tracks.js +143 -28
  18. package/dist/default/playback/behaviors/select-tracks.js.map +1 -1
  19. package/dist/default/playback/behaviors/track-switching.js +5 -68
  20. package/dist/default/playback/behaviors/track-switching.js.map +1 -1
  21. package/dist/default/playback/engines/hls/engine-background-video.js +22 -5
  22. package/dist/default/playback/engines/hls/engine-background-video.js.map +1 -1
  23. package/dist/default/playback/primitives/error-messages.js +13 -1
  24. package/dist/default/playback/primitives/error-messages.js.map +1 -1
  25. package/dist/default/playback/primitives/selection-rules.js +85 -0
  26. package/dist/default/playback/primitives/selection-rules.js.map +1 -0
  27. package/dist/dev/hls-background-video.d.ts +2 -1
  28. package/dist/dev/hls.d.ts +2 -1
  29. package/dist/dev/hls.js +2 -1
  30. package/dist/dev/media/dom/screen.d.ts +26 -0
  31. package/dist/dev/media/dom/screen.d.ts.map +1 -0
  32. package/dist/dev/media/dom/screen.js +129 -0
  33. package/dist/dev/media/dom/screen.js.map +1 -0
  34. package/dist/dev/media/errors.d.ts.map +1 -1
  35. package/dist/dev/media/errors.js +9 -2
  36. package/dist/dev/media/errors.js.map +1 -1
  37. package/dist/dev/media/primitives/select-tracks.js +31 -47
  38. package/dist/dev/media/primitives/select-tracks.js.map +1 -1
  39. package/dist/dev/mux-background-video.d.ts +2 -1
  40. package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts +25 -7
  41. package/dist/dev/playback/adapters/hls-background-video/adapter.d.ts.map +1 -1
  42. package/dist/dev/playback/adapters/hls-background-video/adapter.js +99 -11
  43. package/dist/dev/playback/adapters/hls-background-video/adapter.js.map +1 -1
  44. package/dist/dev/playback/adapters/hls-video/error-surface.d.ts.map +1 -1
  45. package/dist/dev/playback/adapters/hls-video/error-surface.js.map +1 -1
  46. package/dist/dev/playback/adapters/mux-video/adapter.d.ts +1 -0
  47. package/dist/dev/playback/adapters/mux-video/adapter.d.ts.map +1 -1
  48. package/dist/dev/playback/adapters/mux-video/adapter.js +16 -3
  49. package/dist/dev/playback/adapters/mux-video/adapter.js.map +1 -1
  50. package/dist/dev/playback/behaviors/collect-errors.js +36 -2
  51. package/dist/dev/playback/behaviors/collect-errors.js.map +1 -1
  52. package/dist/dev/playback/behaviors/dom/track-screen-resolution.js +29 -0
  53. package/dist/dev/playback/behaviors/dom/track-screen-resolution.js.map +1 -0
  54. package/dist/dev/playback/behaviors/select-tracks.d.ts +64 -6
  55. package/dist/dev/playback/behaviors/select-tracks.d.ts.map +1 -1
  56. package/dist/dev/playback/behaviors/select-tracks.js +143 -28
  57. package/dist/dev/playback/behaviors/select-tracks.js.map +1 -1
  58. package/dist/dev/playback/behaviors/track-switching.js +5 -68
  59. package/dist/dev/playback/behaviors/track-switching.js.map +1 -1
  60. package/dist/dev/playback/engines/hls/engine-background-video.d.ts +63 -11
  61. package/dist/dev/playback/engines/hls/engine-background-video.d.ts.map +1 -1
  62. package/dist/dev/playback/engines/hls/engine-background-video.js +22 -5
  63. package/dist/dev/playback/engines/hls/engine-background-video.js.map +1 -1
  64. package/dist/dev/playback/primitives/error-messages.js +13 -1
  65. package/dist/dev/playback/primitives/error-messages.js.map +1 -1
  66. package/dist/dev/playback/primitives/selection-rules.d.ts +38 -0
  67. package/dist/dev/playback/primitives/selection-rules.d.ts.map +1 -0
  68. package/dist/dev/playback/primitives/selection-rules.js +85 -0
  69. package/dist/dev/playback/primitives/selection-rules.js.map +1 -0
  70. package/package.json +3 -3
  71. package/dist/dev/media/primitives/select-tracks.d.ts +0 -35
  72. package/dist/dev/media/primitives/select-tracks.d.ts.map +0 -1
@@ -1,17 +1,62 @@
1
- import { pickTrackUnderPixelArea } from "../../../media/primitives/select-tracks.js";
1
+ import { effect } from "../../../core/signals/effect.js";
2
+ import { SVTA_NO_SUPPORTED_VIDEO_TRACK, SVTA_UNSUPPORTED_DRM_SYSTEM, SVTA_UNSUPPORTED_PLAYBACK_FEATURE, SVTA_UNSUPPORTED_VIDEO_FORMAT } from "../../../media/errors.js";
2
3
  import { createBackgroundVideoEngine } from "../../engines/hls/engine-background-video.js";
4
+ import { UNPLAYABLE_SOURCE_MESSAGE } from "../../primitives/error-messages.js";
5
+ import { firstFatal, hasUnsupportedFeatureCause } from "../hls-video/error-surface.js";
3
6
  //#region src/playback/adapters/hls-background-video/adapter.ts
4
7
  const hlsBackgroundVideoMediaDefaultProps = { src: "" };
5
8
  /**
9
+ * Which reported conditions this composition treats as **fatal** — the ones that
10
+ * reach `error` and fire `'error'`. Severity isn't part of an SVTA code
11
+ * (§Approach: "impact varies with player implementation"), so it's decided at
12
+ * this boundary rather than by the reporter.
13
+ *
14
+ * **Causes are fatal here, unlike on the other two adapters.** There, a cause is
15
+ * context — one unplayable rendition doesn't fail a source whose others still
16
+ * play, and a verdict follows if the type empties. In the pinned variant a cause
17
+ * *is* the verdict: only the pinned rendition's playlist is ever resolved, so a
18
+ * cause can only be about the pick itself, and dropping that pick is final —
19
+ * nothing here re-picks (that is what `switchVideoTrack` exists for, and this
20
+ * engine doesn't compose it). Measured on Chromium: an MPEG-TS source reports
21
+ * 1004 and an encrypted one 4008, each with no verdict behind it, and the element
22
+ * then sits at `readyState 0` with `error` null forever.
23
+ *
24
+ * The verdict is still listed, for the one shape that reports nothing else: a
25
+ * source offering no video renditions at all, which `reportAbsentTrackType`
26
+ * reports from the head of the constraint chain.
27
+ *
28
+ * First-fatal-wins then surfaces the cause rather than the verdict when both are
29
+ * present, which is the more specific of the two.
30
+ */
31
+ const FATAL_SVTA_CODES = /* @__PURE__ */ new Set([
32
+ SVTA_NO_SUPPORTED_VIDEO_TRACK,
33
+ SVTA_UNSUPPORTED_VIDEO_FORMAT,
34
+ SVTA_UNSUPPORTED_DRM_SYSTEM
35
+ ]);
36
+ /**
6
37
  * Mixin that adds the background-video SPF playback engine to any base class,
7
38
  * for an HLS URL.
8
39
  *
9
- * `src` is the whole surface, and the picker always pins the top rendition on
10
- * offer. There is no cap of its own because the manifest is the better place to
11
- * narrow one: a delivery param `?max_resolution=720p` on a Mux stream URL, for
12
- * one keeps the renditions it excludes out of the manifest entirely, rather
13
- * than fetched-then-unpicked. Deriving a cap from the screen instead is on the
14
- * roadmap, and lands here when it does.
40
+ * `src` is the whole input surface, and `error` is the one output: nothing about
41
+ * an unplayable source reaches the media element on its own here an unsupported
42
+ * container, encryption with no EME, and an undecodable codec all leave
43
+ * `HTMLMediaElement.error` null with the element stalled at `readyState 0`
44
+ * (measured on Chromium and WebKit) so a consumer that watched only the
45
+ * `<video>` would see a source that never appears and never says why. The engine
46
+ * reports each condition onto `engine.state.errors` and logs it; this adapter
47
+ * promotes the first fatal one, mapping it the same way the video and audio Medias
48
+ * map theirs. See `internal/design/spf/features/errors.md`.
49
+ *
50
+ * Selection pins the largest rendition that *fits the screen*, and holds it for
51
+ * the session. The manifest is still the better place to narrow further: a
52
+ * delivery param — `?max_resolution=720p` on a Mux stream URL, for one — keeps
53
+ * the renditions it excludes out of the manifest entirely, rather than
54
+ * fetched-then-unpicked.
55
+ *
56
+ * The pin is given up, never moved, if the pick turns out to be unplayable: the
57
+ * container is only known once a media playlist resolves, which is after the pick
58
+ * is made, so the selection clears rather than quietly appending bytes nothing can
59
+ * decode.
15
60
  *
16
61
  * `@videojs/spf/mux-background-video` is this same Media under a Mux-flavored
17
62
  * name — an alias, not a variant. Nothing about the surface changes with the
@@ -29,6 +74,8 @@ const hlsBackgroundVideoMediaDefaultProps = { src: "" };
29
74
  * engine instance and the attached media element are both kept, so neither has to
30
75
  * be rewired.
31
76
  *
77
+ * @fires error - Fired when a fatal condition is reported. Read `error` for it.
78
+ *
32
79
  * @example
33
80
  * class HlsBackgroundVideoMedia extends HlsBackgroundVideoMediaMixin(BackgroundVideoHost) {}
34
81
  *
@@ -42,6 +89,14 @@ function HlsBackgroundVideoMediaMixin(BaseClass) {
42
89
  #engine;
43
90
  #config;
44
91
  #signals;
92
+ #error = null;
93
+ /**
94
+ * The *reported* condition currently surfaced, which is what the re-fire latch
95
+ * keys on. Not `#error.code`: that's the code this adapter chose to surface,
96
+ * and the substitution below can make the two differ.
97
+ */
98
+ #reportedCode = null;
99
+ #stopErrorSync;
45
100
  /** Pending loadstart listener from a deferred play() retry, if any. */
46
101
  #loadstartListener = null;
47
102
  constructor(...args) {
@@ -49,10 +104,46 @@ function HlsBackgroundVideoMediaMixin(BaseClass) {
49
104
  const { config } = args?.[0] ?? {};
50
105
  this.#config = config;
51
106
  this.#engine = this.#createEngine();
107
+ this.#stopErrorSync = effect(() => {
108
+ const errors = this.#signals.state.errors.get();
109
+ this.#setError(firstFatal(errors, FATAL_SVTA_CODES), errors);
110
+ });
52
111
  }
53
112
  get engine() {
54
113
  return this.#engine;
55
114
  }
115
+ /**
116
+ * The current fatal condition, or `null`. Only *fatal* ones appear here — the
117
+ * engine reports non-fatal ones too (they stay in `engine.state.errors`), and
118
+ * promoting them would say playback had failed when it hadn't. Which ones are
119
+ * fatal is wider here than on the video and audio Medias; see
120
+ * {@link FATAL_SVTA_CODES}. Resets per source. Fires `'error'` when set.
121
+ *
122
+ * Mapped the same way theirs are: a sequence holding an
123
+ * unimplemented-capability cause surfaces as
124
+ * {@link SVTA_UNSUPPORTED_PLAYBACK_FEATURE} (99001) with the specifics logged,
125
+ * because "this player can't play this source" is what a consumer can act on,
126
+ * where a raw container or DRM code only says what to go and look up.
127
+ */
128
+ get error() {
129
+ return this.#error;
130
+ }
131
+ #setError(reported, errors) {
132
+ if (!reported) {
133
+ this.#error = null;
134
+ this.#reportedCode = null;
135
+ return;
136
+ }
137
+ if (this.#reportedCode === reported.code) return;
138
+ this.#reportedCode = reported.code;
139
+ console.error(UNPLAYABLE_SOURCE_MESSAGE, { conditions: errors });
140
+ this.#error = {
141
+ code: hasUnsupportedFeatureCause(errors) ? SVTA_UNSUPPORTED_PLAYBACK_FEATURE : reported.code,
142
+ message: reported.message ?? "",
143
+ ...reported.data === void 0 ? {} : { data: reported.data }
144
+ };
145
+ this.dispatchEvent?.(new Event("error"));
146
+ }
56
147
  attach(mediaElement) {
57
148
  super.attach?.(mediaElement);
58
149
  mediaElement.loop = true;
@@ -68,6 +159,7 @@ function HlsBackgroundVideoMediaMixin(BaseClass) {
68
159
  }
69
160
  destroy() {
70
161
  this.#cancelPendingPlay();
162
+ this.#stopErrorSync();
71
163
  this.#engine.destroy();
72
164
  }
73
165
  get src() {
@@ -96,11 +188,7 @@ function HlsBackgroundVideoMediaMixin(BaseClass) {
96
188
  }
97
189
  }
98
190
  #createEngine() {
99
- const adapterPicker = (presentation) => {
100
- return pickTrackUnderPixelArea((presentation.selectionSets?.find((s) => s.type === "video"))?.switchingSets[0]?.tracks ?? [])?.id;
101
- };
102
191
  return createBackgroundVideoEngine({
103
- picker: adapterPicker,
104
192
  ...this.#config,
105
193
  onSignalsReady: (signals) => {
106
194
  this.#signals = signals;
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.js","names":["#config","#engine","#createEngine","#signals","#cancelPendingPlay","#loadstartListener"],"sources":["../../../../../src/playback/adapters/hls-background-video/adapter.ts"],"sourcesContent":["import type { Constructor, MixinReturn } from '@videojs/utils/types';\nimport type { Composition } from '../../../core/composition/create-composition';\nimport { pickTrackUnderPixelArea, type TrackPicker } from '../../../media/primitives/select-tracks';\nimport type { VideoSelectionSet } from '../../../media/types';\nimport {\n type BackgroundVideoEngineConfig,\n type BackgroundVideoEngineContext,\n type BackgroundVideoEngineSignals,\n type BackgroundVideoEngineState,\n createBackgroundVideoEngine,\n} from '../../engines/hls/engine-background-video';\n\nexport interface HlsBackgroundVideoMediaProps {\n src: string;\n}\n\nexport const hlsBackgroundVideoMediaDefaultProps: HlsBackgroundVideoMediaProps = {\n src: '',\n};\n\nexport interface HlsBackgroundVideoMediaAPI extends HlsBackgroundVideoMediaProps {\n readonly engine: Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext>;\n attach(mediaElement: HTMLMediaElement): void;\n detach(): void;\n destroy(): void;\n play(): Promise<void>;\n}\n\n/**\n * Mixin that adds the background-video SPF playback engine to any base class,\n * for an HLS URL.\n *\n * `src` is the whole surface, and the picker always pins the top rendition on\n * offer. There is no cap of its own because the manifest is the better place to\n * narrow one: a delivery param — `?max_resolution=720p` on a Mux stream URL, for\n * one — keeps the renditions it excludes out of the manifest entirely, rather\n * than fetched-then-unpicked. Deriving a cap from the screen instead is on the\n * roadmap, and lands here when it does.\n *\n * `@videojs/spf/mux-background-video` is this same Media under a Mux-flavored\n * name — an alias, not a variant. Nothing about the surface changes with the\n * import path.\n *\n * Everything else the use case fixes rather than exposes: video-only, looping,\n * muted, autoplaying, loading as soon as there is a source. `attach` writes that\n * onto the element and nothing here declares `loop` / `muted` / `autoplay` /\n * `preload` of its own — a host-bound Media inherits all four from the host\n * already, and shadowing them with fixed values would only make reads describe\n * an intention rather than what the element is doing.\n *\n * A new src re-resolves the presentation, tearing down the state, SourceBuffers,\n * and in-flight requests the previous one built before the next begins. The\n * engine instance and the attached media element are both kept, so neither has to\n * be rewired.\n *\n * @example\n * class HlsBackgroundVideoMedia extends HlsBackgroundVideoMediaMixin(BackgroundVideoHost) {}\n *\n * const media = new HlsBackgroundVideoMedia();\n * media.attach(document.querySelector('video'));\n * media.src = 'https://stream.mux.com/PLAYBACK_ID.m3u8?max_resolution=720p';\n * media.play();\n */\nexport function HlsBackgroundVideoMediaMixin<Base extends Constructor<any>>(BaseClass: Base) {\n class HlsBackgroundVideoMediaImpl extends BaseClass {\n #engine: Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext>;\n #config: BackgroundVideoEngineConfig;\n #signals!: BackgroundVideoEngineSignals;\n\n /** Pending loadstart listener from a deferred play() retry, if any. */\n #loadstartListener: (() => void) | null = null;\n\n constructor(...args: any[]) {\n super(...args);\n\n const { config } = args?.[0] ?? {};\n this.#config = config;\n this.#engine = this.#createEngine();\n }\n\n get engine(): Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext> {\n return this.#engine;\n }\n\n // -------------------------------------------------------------------------\n // Media element lifecycle\n // -------------------------------------------------------------------------\n\n attach(mediaElement: HTMLMediaElement): void {\n super.attach?.(mediaElement);\n // The one place the fixed behavior is stated. Muted and autoplay are what\n // let it start without a gesture, loop is the defining behavior, and\n // `preload` says out loud what the engine does regardless — it subtracts\n // preload monitoring and loads from the moment it has a source.\n mediaElement.loop = true;\n mediaElement.muted = true;\n mediaElement.autoplay = true;\n mediaElement.preload = 'auto';\n\n this.#signals.context.mediaElement.set(mediaElement);\n }\n\n detach(): void {\n this.#cancelPendingPlay();\n this.#signals.context.mediaElement.set(undefined);\n super.detach?.();\n }\n\n destroy(): void {\n this.#cancelPendingPlay();\n this.#engine.destroy();\n }\n\n // -------------------------------------------------------------------------\n // src — synchronous IDL attribute (WHATWG §4.8.11.2)\n // -------------------------------------------------------------------------\n\n get src(): string {\n return this.#signals.state.presentation.get()?.url ?? '';\n }\n\n set src(value: string) {\n // Same line the HLS Medias draw: the presentation is set from a fresh\n // object every time, so re-resolving a URL already playing would restart\n // it for no reason.\n if (value === this.src) return;\n\n this.#cancelPendingPlay();\n this.#signals.state.presentation.set(value ? { url: value } : undefined);\n }\n\n // -------------------------------------------------------------------------\n // play() — WHATWG §4.8.11.8\n // Delegates to the attached media element's native play().\n // -------------------------------------------------------------------------\n\n async play(): Promise<void> {\n const mediaElement = this.#signals.context.mediaElement.get();\n if (!mediaElement) {\n return Promise.reject(new Error('HlsBackgroundVideoMediaElement: no media element attached'));\n }\n\n try {\n return await mediaElement.play();\n } catch (err) {\n // If we have a pending HLS source, the rejection may be because MSE\n // hasn't attached a blob URL yet. Wait for loadstart (src assigned by\n // MSE setup) and retry once.\n if (this.src) {\n return new Promise<void>((resolve, reject) => {\n const listener = () => {\n this.#loadstartListener = null;\n mediaElement.play().then(resolve, reject);\n };\n this.#loadstartListener = listener;\n mediaElement.addEventListener('loadstart', listener, { once: true });\n });\n }\n throw err;\n }\n }\n\n // -------------------------------------------------------------------------\n // Private\n // -------------------------------------------------------------------------\n\n #createEngine(): Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext> {\n // No cap to apply, so the pick is whichever rendition is largest. Passing\n // no maximum is what makes that the answer, rather than a rule of its own.\n const adapterPicker: TrackPicker = (presentation) => {\n const videoSet = presentation.selectionSets?.find((s) => s.type === 'video') as VideoSelectionSet | undefined;\n const tracks = videoSet?.switchingSets[0]?.tracks ?? [];\n return pickTrackUnderPixelArea(tracks)?.id;\n };\n\n return createBackgroundVideoEngine({\n picker: adapterPicker,\n ...this.#config,\n onSignalsReady: (signals) => {\n this.#signals = signals;\n },\n });\n }\n\n #cancelPendingPlay(): void {\n if (!this.#loadstartListener) return;\n const mediaElement = this.#signals.context.mediaElement.get();\n mediaElement?.removeEventListener('loadstart', this.#loadstartListener);\n this.#loadstartListener = null;\n }\n }\n\n return HlsBackgroundVideoMediaImpl as unknown as MixinReturn<Base, HlsBackgroundVideoMediaAPI>;\n}\n\n/** Standalone SPF background-video adapter with no base class. */\nexport class HlsBackgroundVideoMediaElement extends HlsBackgroundVideoMediaMixin(class {}) {}\n"],"mappings":";;;AAgBA,MAAa,sCAAoE,EAC/E,KAAK,GACP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,SAAgB,6BAA4D,WAAiB;CAC3F,MAAM,oCAAoC,UAAU;EAClD;EACA;EACA;;EAGA,qBAA0C;EAE1C,YAAY,GAAG,MAAa;GAC1B,MAAM,GAAG,IAAI;GAEb,MAAM,EAAE,WAAW,OAAO,MAAM,CAAC;GACjC,KAAKA,UAAU;GACf,KAAKC,UAAU,KAAKC,cAAc;EACpC;EAEA,IAAI,SAAgF;GAClF,OAAO,KAAKD;EACd;EAMA,OAAO,cAAsC;GAC3C,MAAM,SAAS,YAAY;GAK3B,aAAa,OAAO;GACpB,aAAa,QAAQ;GACrB,aAAa,WAAW;GACxB,aAAa,UAAU;GAEvB,KAAKE,SAAS,QAAQ,aAAa,IAAI,YAAY;EACrD;EAEA,SAAe;GACb,KAAKC,mBAAmB;GACxB,KAAKD,SAAS,QAAQ,aAAa,IAAI,KAAA,CAAS;GAChD,MAAM,SAAS;EACjB;EAEA,UAAgB;GACd,KAAKC,mBAAmB;GACxB,KAAKH,QAAQ,QAAQ;EACvB;EAMA,IAAI,MAAc;GAChB,OAAO,KAAKE,SAAS,MAAM,aAAa,IAAI,CAAC,EAAE,OAAO;EACxD;EAEA,IAAI,IAAI,OAAe;GAIrB,IAAI,UAAU,KAAK,KAAK;GAExB,KAAKC,mBAAmB;GACxB,KAAKD,SAAS,MAAM,aAAa,IAAI,QAAQ,EAAE,KAAK,MAAM,IAAI,KAAA,CAAS;EACzE;EAOA,MAAM,OAAsB;GAC1B,MAAM,eAAe,KAAKA,SAAS,QAAQ,aAAa,IAAI;GAC5D,IAAI,CAAC,cACH,OAAO,QAAQ,uBAAO,IAAI,MAAM,2DAA2D,CAAC;GAG9F,IAAI;IACF,OAAO,MAAM,aAAa,KAAK;GACjC,SAAS,KAAK;IAIZ,IAAI,KAAK,KACP,OAAO,IAAI,SAAe,SAAS,WAAW;KAC5C,MAAM,iBAAiB;MACrB,KAAKE,qBAAqB;MAC1B,aAAa,KAAK,CAAC,CAAC,KAAK,SAAS,MAAM;KAC1C;KACA,KAAKA,qBAAqB;KAC1B,aAAa,iBAAiB,aAAa,UAAU,EAAE,MAAM,KAAK,CAAC;IACrE,CAAC;IAEH,MAAM;GACR;EACF;EAMA,gBAAuF;GAGrF,MAAM,iBAA8B,iBAAiB;IAGnD,OAAO,yBAFU,aAAa,eAAe,MAAM,MAAM,EAAE,SAAS,OAAO,EAAA,EAClD,cAAc,EAAE,EAAE,UAAU,CAAC,CACjB,CAAC,EAAE;GAC1C;GAEA,OAAO,4BAA4B;IACjC,QAAQ;IACR,GAAG,KAAKL;IACR,iBAAiB,YAAY;KAC3B,KAAKG,WAAW;IAClB;GACF,CAAC;EACH;EAEA,qBAA2B;GACzB,IAAI,CAAC,KAAKE,oBAAoB;GAE9B,KAD0BF,SAAS,QAAQ,aAAa,IAC7C,CAAC,EAAE,oBAAoB,aAAa,KAAKE,kBAAkB;GACtE,KAAKA,qBAAqB;EAC5B;CACF;CAEA,OAAO;AACT;;AAGA,IAAa,iCAAb,cAAoD,6BAA6B,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"adapter.js","names":["#config","#engine","#createEngine","#stopErrorSync","#signals","#setError","#error","#reportedCode","#cancelPendingPlay","#loadstartListener"],"sources":["../../../../../src/playback/adapters/hls-background-video/adapter.ts"],"sourcesContent":["import type { Constructor, MixinReturn } from '@videojs/utils/types';\nimport type { Composition } from '../../../core/composition/create-composition';\nimport { effect } from '../../../core/signals/effect';\nimport {\n SVTA_NO_SUPPORTED_VIDEO_TRACK,\n SVTA_UNSUPPORTED_DRM_SYSTEM,\n SVTA_UNSUPPORTED_PLAYBACK_FEATURE,\n SVTA_UNSUPPORTED_VIDEO_FORMAT,\n type SvtaError,\n} from '../../../media/errors';\nimport {\n type BackgroundVideoEngineConfig,\n type BackgroundVideoEngineContext,\n type BackgroundVideoEngineSignals,\n type BackgroundVideoEngineState,\n createBackgroundVideoEngine,\n} from '../../engines/hls/engine-background-video';\nimport { UNPLAYABLE_SOURCE_MESSAGE } from '../../primitives/error-messages';\nimport { firstFatal, type HlsVideoMediaError, hasUnsupportedFeatureCause } from '../hls-video/error-surface';\n\n// The same error shape the video and audio Medias expose, under the name they\n// publish it as — one type for all three surfaces rather than a background-flavored\n// copy of it.\nexport type { HlsVideoMediaError } from '../hls-video/error-surface';\n\nexport interface HlsBackgroundVideoMediaProps {\n src: string;\n}\n\nexport const hlsBackgroundVideoMediaDefaultProps: HlsBackgroundVideoMediaProps = {\n src: '',\n};\n\nexport interface HlsBackgroundVideoMediaAPI extends HlsBackgroundVideoMediaProps {\n readonly engine: Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext>;\n readonly error: HlsVideoMediaError | null;\n attach(mediaElement: HTMLMediaElement): void;\n detach(): void;\n destroy(): void;\n play(): Promise<void>;\n}\n\n/**\n * Which reported conditions this composition treats as **fatal** — the ones that\n * reach `error` and fire `'error'`. Severity isn't part of an SVTA code\n * (§Approach: \"impact varies with player implementation\"), so it's decided at\n * this boundary rather than by the reporter.\n *\n * **Causes are fatal here, unlike on the other two adapters.** There, a cause is\n * context — one unplayable rendition doesn't fail a source whose others still\n * play, and a verdict follows if the type empties. In the pinned variant a cause\n * *is* the verdict: only the pinned rendition's playlist is ever resolved, so a\n * cause can only be about the pick itself, and dropping that pick is final —\n * nothing here re-picks (that is what `switchVideoTrack` exists for, and this\n * engine doesn't compose it). Measured on Chromium: an MPEG-TS source reports\n * 1004 and an encrypted one 4008, each with no verdict behind it, and the element\n * then sits at `readyState 0` with `error` null forever.\n *\n * The verdict is still listed, for the one shape that reports nothing else: a\n * source offering no video renditions at all, which `reportAbsentTrackType`\n * reports from the head of the constraint chain.\n *\n * First-fatal-wins then surfaces the cause rather than the verdict when both are\n * present, which is the more specific of the two.\n */\nconst FATAL_SVTA_CODES: ReadonlySet<number> = new Set<number>([\n SVTA_NO_SUPPORTED_VIDEO_TRACK,\n SVTA_UNSUPPORTED_VIDEO_FORMAT,\n SVTA_UNSUPPORTED_DRM_SYSTEM,\n]);\n\n/**\n * Mixin that adds the background-video SPF playback engine to any base class,\n * for an HLS URL.\n *\n * `src` is the whole input surface, and `error` is the one output: nothing about\n * an unplayable source reaches the media element on its own here — an unsupported\n * container, encryption with no EME, and an undecodable codec all leave\n * `HTMLMediaElement.error` null with the element stalled at `readyState 0`\n * (measured on Chromium and WebKit) — so a consumer that watched only the\n * `<video>` would see a source that never appears and never says why. The engine\n * reports each condition onto `engine.state.errors` and logs it; this adapter\n * promotes the first fatal one, mapping it the same way the video and audio Medias\n * map theirs. See `internal/design/spf/features/errors.md`.\n *\n * Selection pins the largest rendition that *fits the screen*, and holds it for\n * the session. The manifest is still the better place to narrow further: a\n * delivery param — `?max_resolution=720p` on a Mux stream URL, for one — keeps\n * the renditions it excludes out of the manifest entirely, rather than\n * fetched-then-unpicked.\n *\n * The pin is given up, never moved, if the pick turns out to be unplayable: the\n * container is only known once a media playlist resolves, which is after the pick\n * is made, so the selection clears rather than quietly appending bytes nothing can\n * decode.\n *\n * `@videojs/spf/mux-background-video` is this same Media under a Mux-flavored\n * name — an alias, not a variant. Nothing about the surface changes with the\n * import path.\n *\n * Everything else the use case fixes rather than exposes: video-only, looping,\n * muted, autoplaying, loading as soon as there is a source. `attach` writes that\n * onto the element and nothing here declares `loop` / `muted` / `autoplay` /\n * `preload` of its own — a host-bound Media inherits all four from the host\n * already, and shadowing them with fixed values would only make reads describe\n * an intention rather than what the element is doing.\n *\n * A new src re-resolves the presentation, tearing down the state, SourceBuffers,\n * and in-flight requests the previous one built before the next begins. The\n * engine instance and the attached media element are both kept, so neither has to\n * be rewired.\n *\n * @fires error - Fired when a fatal condition is reported. Read `error` for it.\n *\n * @example\n * class HlsBackgroundVideoMedia extends HlsBackgroundVideoMediaMixin(BackgroundVideoHost) {}\n *\n * const media = new HlsBackgroundVideoMedia();\n * media.attach(document.querySelector('video'));\n * media.src = 'https://stream.mux.com/PLAYBACK_ID.m3u8?max_resolution=720p';\n * media.play();\n */\nexport function HlsBackgroundVideoMediaMixin<Base extends Constructor<any>>(BaseClass: Base) {\n class HlsBackgroundVideoMediaImpl extends BaseClass {\n #engine: Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext>;\n #config: BackgroundVideoEngineConfig;\n #signals!: BackgroundVideoEngineSignals;\n #error: HlsVideoMediaError | null = null;\n /**\n * The *reported* condition currently surfaced, which is what the re-fire latch\n * keys on. Not `#error.code`: that's the code this adapter chose to surface,\n * and the substitution below can make the two differ.\n */\n #reportedCode: number | null = null;\n #stopErrorSync: () => void;\n\n /** Pending loadstart listener from a deferred play() retry, if any. */\n #loadstartListener: (() => void) | null = null;\n\n constructor(...args: any[]) {\n super(...args);\n\n const { config } = args?.[0] ?? {};\n this.#config = config;\n this.#engine = this.#createEngine();\n\n // Promote the first fatal condition out of the engine's reported sequence\n // onto this surface. Clearing rides the same signal: `collectErrors` resets\n // the slot per source, so a new source starts with no error without this\n // needing its own source-change hook.\n this.#stopErrorSync = effect(() => {\n const errors = this.#signals.state.errors.get();\n this.#setError(firstFatal(errors, FATAL_SVTA_CODES), errors);\n });\n }\n\n get engine(): Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext> {\n return this.#engine;\n }\n\n /**\n * The current fatal condition, or `null`. Only *fatal* ones appear here — the\n * engine reports non-fatal ones too (they stay in `engine.state.errors`), and\n * promoting them would say playback had failed when it hadn't. Which ones are\n * fatal is wider here than on the video and audio Medias; see\n * {@link FATAL_SVTA_CODES}. Resets per source. Fires `'error'` when set.\n *\n * Mapped the same way theirs are: a sequence holding an\n * unimplemented-capability cause surfaces as\n * {@link SVTA_UNSUPPORTED_PLAYBACK_FEATURE} (99001) with the specifics logged,\n * because \"this player can't play this source\" is what a consumer can act on,\n * where a raw container or DRM code only says what to go and look up.\n */\n get error(): HlsVideoMediaError | null {\n return this.#error;\n }\n\n #setError(reported: SvtaError | undefined, errors: readonly SvtaError[] | undefined): void {\n if (!reported) {\n // Cleared (new source). No event: `'error'` announces a failure, and\n // consumers reset their own copy on source change.\n this.#error = null;\n this.#reportedCode = null;\n return;\n }\n // Keyed on the code, not the object: a later append re-runs this effect\n // with an equal-but-new array, and re-firing `'error'` for a condition\n // already surfaced would look like a second failure.\n if (this.#reportedCode === reported.code) return;\n this.#reportedCode = reported.code;\n\n // Logged for every fatal condition, not just the substituted ones: a source\n // with no video renditions is as dead as an unplayable container, and it\n // would otherwise reach a developer as a bare code. One generic sentence\n // rather than one per case — the conditions beside it carry the specifics.\n //\n // Prose stays here rather than on `error.message`, matching the other two:\n // viewer-facing copy is the consumer's to localize, and a background video\n // has no chrome to put it in anyway.\n console.error(UNPLAYABLE_SOURCE_MESSAGE, { conditions: errors });\n\n this.#error = {\n code: hasUnsupportedFeatureCause(errors) ? SVTA_UNSUPPORTED_PLAYBACK_FEATURE : reported.code,\n message: reported.message ?? '',\n ...(reported.data === undefined ? {} : { data: reported.data }),\n };\n // Optional-chained: with an EventTarget-less base (`HlsBackgroundVideoMediaElement`\n // standalone) there's nowhere to dispatch.\n this.dispatchEvent?.(new Event('error'));\n }\n\n // -------------------------------------------------------------------------\n // Media element lifecycle\n // -------------------------------------------------------------------------\n\n attach(mediaElement: HTMLMediaElement): void {\n super.attach?.(mediaElement);\n // The one place the fixed behavior is stated. Muted and autoplay are what\n // let it start without a gesture, loop is the defining behavior, and\n // `preload` says out loud what the engine does regardless — it subtracts\n // preload monitoring and loads from the moment it has a source.\n mediaElement.loop = true;\n mediaElement.muted = true;\n mediaElement.autoplay = true;\n mediaElement.preload = 'auto';\n\n this.#signals.context.mediaElement.set(mediaElement);\n }\n\n detach(): void {\n this.#cancelPendingPlay();\n this.#signals.context.mediaElement.set(undefined);\n super.detach?.();\n }\n\n destroy(): void {\n this.#cancelPendingPlay();\n this.#stopErrorSync();\n this.#engine.destroy();\n }\n\n // -------------------------------------------------------------------------\n // src — synchronous IDL attribute (WHATWG §4.8.11.2)\n // -------------------------------------------------------------------------\n\n get src(): string {\n return this.#signals.state.presentation.get()?.url ?? '';\n }\n\n set src(value: string) {\n // Same line the HLS Medias draw: the presentation is set from a fresh\n // object every time, so re-resolving a URL already playing would restart\n // it for no reason.\n if (value === this.src) return;\n\n this.#cancelPendingPlay();\n this.#signals.state.presentation.set(value ? { url: value } : undefined);\n }\n\n // -------------------------------------------------------------------------\n // play() — WHATWG §4.8.11.8\n // Delegates to the attached media element's native play().\n // -------------------------------------------------------------------------\n\n async play(): Promise<void> {\n const mediaElement = this.#signals.context.mediaElement.get();\n if (!mediaElement) {\n return Promise.reject(new Error('HlsBackgroundVideoMediaElement: no media element attached'));\n }\n\n try {\n return await mediaElement.play();\n } catch (err) {\n // If we have a pending HLS source, the rejection may be because MSE\n // hasn't attached a blob URL yet. Wait for loadstart (src assigned by\n // MSE setup) and retry once.\n if (this.src) {\n return new Promise<void>((resolve, reject) => {\n const listener = () => {\n this.#loadstartListener = null;\n mediaElement.play().then(resolve, reject);\n };\n this.#loadstartListener = listener;\n mediaElement.addEventListener('loadstart', listener, { once: true });\n });\n }\n throw err;\n }\n }\n\n // -------------------------------------------------------------------------\n // Private\n // -------------------------------------------------------------------------\n\n #createEngine(): Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext> {\n // No selection config of its own: the engine's default rule chain already\n // narrows to the largest rendition that fits the screen, which is exactly\n // what this adapter used to hand over as a bespoke picker.\n return createBackgroundVideoEngine({\n ...this.#config,\n onSignalsReady: (signals) => {\n this.#signals = signals;\n },\n });\n }\n\n #cancelPendingPlay(): void {\n if (!this.#loadstartListener) return;\n const mediaElement = this.#signals.context.mediaElement.get();\n mediaElement?.removeEventListener('loadstart', this.#loadstartListener);\n this.#loadstartListener = null;\n }\n }\n\n return HlsBackgroundVideoMediaImpl as unknown as MixinReturn<Base, HlsBackgroundVideoMediaAPI>;\n}\n\n/** Standalone SPF background-video adapter with no base class. */\nexport class HlsBackgroundVideoMediaElement extends HlsBackgroundVideoMediaMixin(class {}) {}\n"],"mappings":";;;;;;AA6BA,MAAa,sCAAoE,EAC/E,KAAK,GACP;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAM,mCAAwC,IAAI,IAAY;CAC5D;CACA;CACA;AACF,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDD,SAAgB,6BAA4D,WAAiB;CAC3F,MAAM,oCAAoC,UAAU;EAClD;EACA;EACA;EACA,SAAoC;;;;;;EAMpC,gBAA+B;EAC/B;;EAGA,qBAA0C;EAE1C,YAAY,GAAG,MAAa;GAC1B,MAAM,GAAG,IAAI;GAEb,MAAM,EAAE,WAAW,OAAO,MAAM,CAAC;GACjC,KAAKA,UAAU;GACf,KAAKC,UAAU,KAAKC,cAAc;GAMlC,KAAKC,iBAAiB,aAAa;IACjC,MAAM,SAAS,KAAKC,SAAS,MAAM,OAAO,IAAI;IAC9C,KAAKC,UAAU,WAAW,QAAQ,gBAAgB,GAAG,MAAM;GAC7D,CAAC;EACH;EAEA,IAAI,SAAgF;GAClF,OAAO,KAAKJ;EACd;;;;;;;;;;;;;;EAeA,IAAI,QAAmC;GACrC,OAAO,KAAKK;EACd;EAEA,UAAU,UAAiC,QAAgD;GACzF,IAAI,CAAC,UAAU;IAGb,KAAKA,SAAS;IACd,KAAKC,gBAAgB;IACrB;GACF;GAIA,IAAI,KAAKA,kBAAkB,SAAS,MAAM;GAC1C,KAAKA,gBAAgB,SAAS;GAU9B,QAAQ,MAAM,2BAA2B,EAAE,YAAY,OAAO,CAAC;GAE/D,KAAKD,SAAS;IACZ,MAAM,2BAA2B,MAAM,IAAI,oCAAoC,SAAS;IACxF,SAAS,SAAS,WAAW;IAC7B,GAAI,SAAS,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM,SAAS,KAAK;GAC/D;GAGA,KAAK,gBAAgB,IAAI,MAAM,OAAO,CAAC;EACzC;EAMA,OAAO,cAAsC;GAC3C,MAAM,SAAS,YAAY;GAK3B,aAAa,OAAO;GACpB,aAAa,QAAQ;GACrB,aAAa,WAAW;GACxB,aAAa,UAAU;GAEvB,KAAKF,SAAS,QAAQ,aAAa,IAAI,YAAY;EACrD;EAEA,SAAe;GACb,KAAKI,mBAAmB;GACxB,KAAKJ,SAAS,QAAQ,aAAa,IAAI,KAAA,CAAS;GAChD,MAAM,SAAS;EACjB;EAEA,UAAgB;GACd,KAAKI,mBAAmB;GACxB,KAAKL,eAAe;GACpB,KAAKF,QAAQ,QAAQ;EACvB;EAMA,IAAI,MAAc;GAChB,OAAO,KAAKG,SAAS,MAAM,aAAa,IAAI,CAAC,EAAE,OAAO;EACxD;EAEA,IAAI,IAAI,OAAe;GAIrB,IAAI,UAAU,KAAK,KAAK;GAExB,KAAKI,mBAAmB;GACxB,KAAKJ,SAAS,MAAM,aAAa,IAAI,QAAQ,EAAE,KAAK,MAAM,IAAI,KAAA,CAAS;EACzE;EAOA,MAAM,OAAsB;GAC1B,MAAM,eAAe,KAAKA,SAAS,QAAQ,aAAa,IAAI;GAC5D,IAAI,CAAC,cACH,OAAO,QAAQ,uBAAO,IAAI,MAAM,2DAA2D,CAAC;GAG9F,IAAI;IACF,OAAO,MAAM,aAAa,KAAK;GACjC,SAAS,KAAK;IAIZ,IAAI,KAAK,KACP,OAAO,IAAI,SAAe,SAAS,WAAW;KAC5C,MAAM,iBAAiB;MACrB,KAAKK,qBAAqB;MAC1B,aAAa,KAAK,CAAC,CAAC,KAAK,SAAS,MAAM;KAC1C;KACA,KAAKA,qBAAqB;KAC1B,aAAa,iBAAiB,aAAa,UAAU,EAAE,MAAM,KAAK,CAAC;IACrE,CAAC;IAEH,MAAM;GACR;EACF;EAMA,gBAAuF;GAIrF,OAAO,4BAA4B;IACjC,GAAG,KAAKT;IACR,iBAAiB,YAAY;KAC3B,KAAKI,WAAW;IAClB;GACF,CAAC;EACH;EAEA,qBAA2B;GACzB,IAAI,CAAC,KAAKK,oBAAoB;GAE9B,KAD0BL,SAAS,QAAQ,aAAa,IAC7C,CAAC,EAAE,oBAAoB,aAAa,KAAKK,kBAAkB;GACtE,KAAKA,qBAAqB;EAC5B;CACF;CAEA,OAAO;AACT;;AAGA,IAAa,iCAAb,cAAoD,6BAA6B,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"error-surface.d.ts","names":[],"sources":["../../../../../src/playback/adapters/hls-video/error-surface.ts"],"mappings":";;;;;;;;;;;UA6BiB,2BAA2B;;WAEjC"}
1
+ {"version":3,"file":"error-surface.d.ts","names":[],"sources":["../../../../../src/playback/adapters/hls-video/error-surface.ts"],"mappings":";;;;;;;;;;;UAkCiB,2BAA2B;;WAEjC"}
@@ -1 +1 @@
1
- {"version":3,"file":"error-surface.js","names":[],"sources":["../../../../../src/playback/adapters/hls-video/error-surface.ts"],"sourcesContent":["/**\n * The shared half of promoting reported conditions onto a media surface.\n *\n * Both HLS adapters do the same things: pick the first condition they treat as\n * fatal, latch it so a later append doesn't re-fire, and name a better-equipped\n * Media when their own class points at one. Only the *policy* differs — which\n * codes are fatal, which the video adapter and the audio-only adapter answer\n * differently — so that stays with each adapter and everything else lives here.\n *\n * See `internal/design/spf/features/errors.md` for the causes-vs-verdicts split\n * this rests on.\n */\nimport type { ErrorLike } from '@videojs/media';\nimport {\n SVTA_UNSUPPORTED_AUDIO_FORMAT,\n SVTA_UNSUPPORTED_DRM_SYSTEM,\n SVTA_UNSUPPORTED_VIDEO_FORMAT,\n type SvtaError,\n} from '../../../media/errors';\n\n/**\n * The error shape a media surface exposes: `@videojs/media`'s {@link ErrorLike}\n * plus the reporter context the engine carries alongside a condition.\n *\n * `code` is the **SVTA code**, not a `MediaError.MEDIA_ERR_*` value. Consumers\n * that map codes to copy currently only know 1–5, so an SVTA code falls through\n * to showing `message`; an extensible code lookup above the engine is the\n * follow-up that fixes it.\n */\nexport interface HlsVideoMediaError extends ErrorLike {\n /** Reporter context (which selection emptied, which track, …). */\n readonly data?: unknown;\n}\n\n/**\n * `message`, plus the alternative-Media sentence when `media`'s class names one.\n *\n * Read off the class rather than passed in so a subclass can point at a\n * better-equipped sibling — the SPF Mux Medias name the hls.js-backed one, which\n * plays the MPEG-TS and DRM sources SPF doesn't — without either adapter knowing\n * that sibling exists.\n */\nexport function withAlternativeMediaSuggestion(message: string, media: object): string {\n const { alternativeMediaSuggestion } = media.constructor as { alternativeMediaSuggestion?: string };\n const suggestion = alternativeMediaSuggestion?.trim();\n\n return suggestion ? `${message} ${suggestion}` : message;\n}\n\n/** The first condition `fatalCodes` covers — the root cause, not its consequences. */\nexport function firstFatal(\n errors: readonly SvtaError[] | undefined,\n fatalCodes: ReadonlySet<number>\n): SvtaError | undefined {\n return errors?.find((error) => fatalCodes.has(error.code));\n}\n\n/**\n * The causes that mean the engine has no pipeline for what the source delivers —\n * a container it can't append, or encryption it can't decrypt.\n *\n * What these have in common is that no retry, no other CDN, and no other\n * rendition of the same source fixes them: the source needs a capability this\n * engine doesn't have. That's the distinction the surfaced code exists to draw,\n * and it's why the set is these three rather than \"every cause\".\n */\nconst UNSUPPORTED_FEATURE_CAUSES: ReadonlySet<number> = new Set<number>([\n SVTA_UNSUPPORTED_VIDEO_FORMAT,\n SVTA_UNSUPPORTED_AUDIO_FORMAT,\n SVTA_UNSUPPORTED_DRM_SYSTEM,\n]);\n\n/**\n * Whether anything in the sequence is a cause of the \"we don't implement this\"\n * kind.\n *\n * Deliberately `some` over the whole sequence rather than a per-type match\n * against the verdict. A verdict means one type's candidates emptied, but the\n * *reason* the source is unplayable can sit on another type — an audio-only\n * source whose sole rendition is encrypted empties the audio candidates, and a\n * video source with encrypted video empties the video ones. Both are the same\n * answer to the viewer, so both get the same code.\n */\nexport function hasUnsupportedFeatureCause(errors: readonly SvtaError[] | undefined): boolean {\n return errors?.some((error) => UNSUPPORTED_FEATURE_CAUSES.has(error.code)) ?? false;\n}\n"],"mappings":";;;;;;;;;;AA0CA,SAAgB,+BAA+B,SAAiB,OAAuB;CACrF,MAAM,EAAE,+BAA+B,MAAM;CAC7C,MAAM,aAAa,4BAA4B,KAAK;CAEpD,OAAO,aAAa,GAAG,QAAQ,GAAG,eAAe;AACnD;;AAGA,SAAgB,WACd,QACA,YACuB;CACvB,OAAO,QAAQ,MAAM,UAAU,WAAW,IAAI,MAAM,IAAI,CAAC;AAC3D;;;;;;;;;;AAWA,MAAM,6CAAkD,IAAI,IAAY;CACtE;CACA;CACA;AACF,CAAC;;;;;;;;;;;;AAaD,SAAgB,2BAA2B,QAAmD;CAC5F,OAAO,QAAQ,MAAM,UAAU,2BAA2B,IAAI,MAAM,IAAI,CAAC,KAAK;AAChF"}
1
+ {"version":3,"file":"error-surface.js","names":[],"sources":["../../../../../src/playback/adapters/hls-video/error-surface.ts"],"sourcesContent":["/**\n * The shared half of promoting reported conditions onto a media surface.\n *\n * Every adapter that has one does the same things: pick the first condition it\n * treats as fatal, latch it so a later append doesn't re-fire, and name a\n * better-equipped Media when its own class points at one. Only the *policy*\n * differs — which codes are fatal, which the video, audio-only, and background\n * adapters each answer differently — so that stays with each adapter and\n * everything else lives here.\n *\n * `firstFatal` is the part all three share. The `ErrorLike` mapping below it is\n * for the two that feed a store and a dialog; the background adapter surfaces the\n * reported condition unmapped, so it takes the picker and nothing else.\n *\n * See `internal/design/spf/features/errors.md` for the causes-vs-verdicts split\n * this rests on.\n */\nimport type { ErrorLike } from '@videojs/media';\nimport {\n SVTA_UNSUPPORTED_AUDIO_FORMAT,\n SVTA_UNSUPPORTED_DRM_SYSTEM,\n SVTA_UNSUPPORTED_VIDEO_FORMAT,\n type SvtaError,\n} from '../../../media/errors';\n\n/**\n * The error shape a media surface exposes: `@videojs/media`'s {@link ErrorLike}\n * plus the reporter context the engine carries alongside a condition.\n *\n * `code` is the **SVTA code**, not a `MediaError.MEDIA_ERR_*` value. Consumers\n * that map codes to copy currently only know 1–5, so an SVTA code falls through\n * to showing `message`; an extensible code lookup above the engine is the\n * follow-up that fixes it.\n */\nexport interface HlsVideoMediaError extends ErrorLike {\n /** Reporter context (which selection emptied, which track, …). */\n readonly data?: unknown;\n}\n\n/**\n * `message`, plus the alternative-Media sentence when `media`'s class names one.\n *\n * Read off the class rather than passed in so a subclass can point at a\n * better-equipped sibling — the SPF Mux Medias name the hls.js-backed one, which\n * plays the MPEG-TS and DRM sources SPF doesn't — without either adapter knowing\n * that sibling exists.\n */\nexport function withAlternativeMediaSuggestion(message: string, media: object): string {\n const { alternativeMediaSuggestion } = media.constructor as { alternativeMediaSuggestion?: string };\n const suggestion = alternativeMediaSuggestion?.trim();\n\n return suggestion ? `${message} ${suggestion}` : message;\n}\n\n/** The first condition `fatalCodes` covers — the root cause, not its consequences. */\nexport function firstFatal(\n errors: readonly SvtaError[] | undefined,\n fatalCodes: ReadonlySet<number>\n): SvtaError | undefined {\n return errors?.find((error) => fatalCodes.has(error.code));\n}\n\n/**\n * The causes that mean the engine has no pipeline for what the source delivers —\n * a container it can't append, or encryption it can't decrypt.\n *\n * What these have in common is that no retry, no other CDN, and no other\n * rendition of the same source fixes them: the source needs a capability this\n * engine doesn't have. That's the distinction the surfaced code exists to draw,\n * and it's why the set is these three rather than \"every cause\".\n */\nconst UNSUPPORTED_FEATURE_CAUSES: ReadonlySet<number> = new Set<number>([\n SVTA_UNSUPPORTED_VIDEO_FORMAT,\n SVTA_UNSUPPORTED_AUDIO_FORMAT,\n SVTA_UNSUPPORTED_DRM_SYSTEM,\n]);\n\n/**\n * Whether anything in the sequence is a cause of the \"we don't implement this\"\n * kind.\n *\n * Deliberately `some` over the whole sequence rather than a per-type match\n * against the verdict. A verdict means one type's candidates emptied, but the\n * *reason* the source is unplayable can sit on another type — an audio-only\n * source whose sole rendition is encrypted empties the audio candidates, and a\n * video source with encrypted video empties the video ones. Both are the same\n * answer to the viewer, so both get the same code.\n */\nexport function hasUnsupportedFeatureCause(errors: readonly SvtaError[] | undefined): boolean {\n return errors?.some((error) => UNSUPPORTED_FEATURE_CAUSES.has(error.code)) ?? false;\n}\n"],"mappings":";;;;;;;;;;AA+CA,SAAgB,+BAA+B,SAAiB,OAAuB;CACrF,MAAM,EAAE,+BAA+B,MAAM;CAC7C,MAAM,aAAa,4BAA4B,KAAK;CAEpD,OAAO,aAAa,GAAG,QAAQ,GAAG,eAAe;AACnD;;AAGA,SAAgB,WACd,QACA,YACuB;CACvB,OAAO,QAAQ,MAAM,UAAU,WAAW,IAAI,MAAM,IAAI,CAAC;AAC3D;;;;;;;;;;AAWA,MAAM,6CAAkD,IAAI,IAAY;CACtE;CACA;CACA;AACF,CAAC;;;;;;;;;;;;AAaD,SAAgB,2BAA2B,QAAmD;CAC5F,OAAO,QAAQ,MAAM,UAAU,2BAA2B,IAAI,MAAM,IAAI,CAAC,KAAK;AAChF"}
@@ -24,6 +24,7 @@ interface MuxMediaAPI extends MuxMediaProps {
24
24
  * source and dispatches `sourcechange` itself.
25
25
  *
26
26
  * @fires sourcechange - Fired when `source` changes, either directly or by parsing a new `src`. Read `source` for the new value.
27
+ * @fires contentdatachange - Fired when the derived `contentData` changes. Read `contentData` for the new value.
27
28
  */
28
29
  declare function MuxMediaMixin<Base extends Constructor<any>>(BaseClass: Base): MixinReturn<Base, MuxMediaAPI> & {
29
30
  readonly alternativeMediaSuggestion: string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","names":[],"sources":["../../../../../src/playback/adapters/mux-video/adapter.ts"],"mappings":";;;UAUiB;EACf;EACA,QAAQ;;cAGG,sBAAsB;UAKlB,oBAAoB;WAC1B,aAAa;;;;;;;;;;;;;;;;;;iBAmBR,cAAc,aAAa,kBAAkB,WAAW,OAkFpC,YAAY,MAAM;WACzC"}
1
+ {"version":3,"file":"adapter.d.ts","names":[],"sources":["../../../../../src/playback/adapters/mux-video/adapter.ts"],"mappings":";;;UAWiB;EACf;EACA,QAAQ;;cAGG,sBAAsB;UAKlB,oBAAoB;WAC1B,aAAa;;;;;;;;;;;;;;;;;;;iBAoBR,cAAc,aAAa,kBAAkB,WAAW,OAsGpC,YAAY,MAAM;WACzC"}
@@ -1,3 +1,4 @@
1
+ import { shallowEqual } from "@videojs/utils/object";
1
2
  import { createMuxPosterURL, createMuxStoryboardURL, createMuxVideoURL, parseMuxVideoURL } from "@videojs/media/dom/mux/source";
2
3
  //#region src/playback/adapters/mux-video/adapter.ts
3
4
  const muxMediaDefaultProps = {
@@ -19,6 +20,7 @@ const muxMediaDefaultProps = {
19
20
  * source and dispatches `sourcechange` itself.
20
21
  *
21
22
  * @fires sourcechange - Fired when `source` changes, either directly or by parsing a new `src`. Read `source` for the new value.
23
+ * @fires contentdatachange - Fired when the derived `contentData` changes. Read `contentData` for the new value.
22
24
  */
23
25
  function MuxMediaMixin(BaseClass) {
24
26
  class MuxMediaImpl extends BaseClass {
@@ -36,6 +38,7 @@ function MuxMediaMixin(BaseClass) {
36
38
  return "Try the hls.js-backed Mux media instead: import the `hls-js` flavor in place of the `spf` one.";
37
39
  }
38
40
  #source = muxMediaDefaultProps.source;
41
+ #contentData = {};
39
42
  /**
40
43
  * Media source URL. Setting a Mux stream URL
41
44
  * (`https://stream.mux.com/<playback-id>.m3u8?...`) extracts the playback ID
@@ -65,23 +68,33 @@ function MuxMediaMixin(BaseClass) {
65
68
  const source = value ?? null;
66
69
  if (source === this.#source) return;
67
70
  this.#source = source;
71
+ const contentDataChanged = this.#refreshContentData();
68
72
  super.src = source && (createMuxVideoURL(source) ?? source.src) || "";
69
73
  this.dispatchEvent?.(new Event("sourcechange"));
74
+ if (contentDataChanged) this.dispatchEvent?.(new Event("contentdatachange"));
70
75
  }
71
76
  /**
72
77
  * Image URLs `source` describes rather than plays: `poster` from its `poster`
73
78
  * params, `storyboard` from its `storyboard` params.
74
79
  *
75
- * Read-only and re-derived on read, so read it again after `sourcechange`.
76
- * Nothing here is applied for you.
80
+ * Derived from `source` and nothing else. The same object is handed back
81
+ * until one of those URLs changes, and `contentdatachange` announces it when
82
+ * it does. Nothing here is applied for you.
77
83
  */
78
84
  get contentData() {
85
+ return this.#contentData;
86
+ }
87
+ /** Rebuild the derived bag, reporting whether anything about it changed. */
88
+ #refreshContentData() {
79
89
  const poster = createMuxPosterURL(this.#source);
80
90
  const storyboard = createMuxStoryboardURL(this.#source);
81
- return {
91
+ const next = {
82
92
  ...poster && { poster },
83
93
  ...storyboard && { storyboard }
84
94
  };
95
+ if (shallowEqual(this.#contentData, next)) return false;
96
+ this.#contentData = next;
97
+ return true;
85
98
  }
86
99
  }
87
100
  return MuxMediaImpl;
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.js","names":["#source"],"sources":["../../../../../src/playback/adapters/mux-video/adapter.ts"],"sourcesContent":["import {\n createMuxPosterURL,\n createMuxStoryboardURL,\n createMuxVideoURL,\n type MuxContentData,\n type MuxSourceBase,\n parseMuxVideoURL,\n} from '@videojs/media/dom/mux/source';\nimport type { Constructor, MixinReturn } from '@videojs/utils/types';\n\nexport interface MuxMediaProps {\n src: string;\n source: MuxSourceBase | null;\n}\n\nexport const muxMediaDefaultProps: MuxMediaProps = {\n src: '',\n source: null,\n};\n\nexport interface MuxMediaAPI extends MuxMediaProps {\n readonly contentData: MuxContentData;\n}\n\n/**\n * Mux identity over any SPF Media: the structured `source`, the `src` derived\n * from it, and the image URLs it describes.\n *\n * Everything here is Mux identity, so it carries no engine and both flavors get\n * it unchanged — the video Media over the full HLS engine, the audio-only Media\n * over the subtractive one. A mixin rather than a shared base class because each\n * flavor extends a different SPF Media, so there is no common class to put this\n * on, only a common `src` accessor to write through.\n *\n * Unlike the hls.js-backed `MuxMedia`, there is no inherited `source` to\n * delegate to — the SPF Medias know only `src` — so this owns the structured\n * source and dispatches `sourcechange` itself.\n *\n * @fires sourcechange - Fired when `source` changes, either directly or by parsing a new `src`. Read `source` for the new value.\n */\nexport function MuxMediaMixin<Base extends Constructor<any>>(BaseClass: Base) {\n class MuxMediaImpl extends BaseClass {\n /**\n * Named on the error copy when this engine can't play a source: the\n * hls.js-backed Mux Media plays the MPEG-TS and DRM-protected sources that\n * SPF does not, and it backs both `<mux-video>` and `<mux-audio>`.\n *\n * Names the flavor rather than an import path, because one Media is reached\n * through three of them — `@videojs/html`, `@videojs/react`, and this package\n * — and each has a different counterpart. The flavor suffix is the one thing\n * common to the layers a consumer imports elements and components from.\n */\n static get alternativeMediaSuggestion(): string | undefined {\n return 'Try the hls.js-backed Mux media instead: import the `hls-js` flavor in place of the `spf` one.';\n }\n\n #source: MuxSourceBase | null = muxMediaDefaultProps.source;\n\n /**\n * Media source URL. Setting a Mux stream URL\n * (`https://stream.mux.com/<playback-id>.m3u8?...`) extracts the playback ID\n * and query params into `source`; other URLs are kept as a plain `source.src`.\n *\n * Only playback options carry over. Mux identity comes from the URL, and the\n * signed `poster`, `storyboard`, and `drm` tokens are scoped to a playback ID,\n * so carrying them onto a different source would build rejected URLs.\n */\n get src(): string {\n return super.src;\n }\n\n set src(value: string) {\n // A URL already describing the current source leaves it alone. The elements\n // reflect the derived `src` back to the host, and re-deriving would drop the\n // params a Mux URL does not carry, such as `poster`.\n if (super.src === value) return;\n\n this.source = parseMuxVideoURL(value) ?? (value ? { src: value } : null);\n }\n\n /**\n * Structured Mux source. Setting it derives `src` from the playback ID, custom\n * domain, and `playback` params (appended as `snake_case` query params). A\n * `playback.token` replaces all other params — signed URLs bake them into the\n * token.\n */\n get source(): MuxSourceBase | null {\n return this.#source;\n }\n\n set source(value: MuxSourceBase | null) {\n const source = value ?? null;\n // Changing anything takes a new object, so handing the same one back costs\n // nothing.\n if (source === this.#source) return;\n\n this.#source = source;\n super.src = (source && (createMuxVideoURL(source) ?? source.src)) || '';\n\n this.dispatchEvent?.(new Event('sourcechange'));\n }\n\n /**\n * Image URLs `source` describes rather than plays: `poster` from its `poster`\n * params, `storyboard` from its `storyboard` params.\n *\n * Read-only and re-derived on read, so read it again after `sourcechange`.\n * Nothing here is applied for you.\n */\n get contentData(): MuxContentData {\n const poster = createMuxPosterURL(this.#source);\n const storyboard = createMuxStoryboardURL(this.#source);\n\n return {\n ...(poster && { poster }),\n ...(storyboard && { storyboard }),\n };\n }\n }\n\n // `MixinReturn` sources statics from `Base`, so this mixin's own needs adding\n // back to the type or callers can't read it.\n return MuxMediaImpl as unknown as MixinReturn<Base, MuxMediaAPI> & {\n readonly alternativeMediaSuggestion: string | undefined;\n };\n}\n"],"mappings":";;AAeA,MAAa,uBAAsC;CACjD,KAAK;CACL,QAAQ;AACV;;;;;;;;;;;;;;;;;AAsBA,SAAgB,cAA6C,WAAiB;CAC5E,MAAM,qBAAqB,UAAU;;;;;;;;;;;EAWnC,WAAW,6BAAiD;GAC1D,OAAO;EACT;EAEA,UAAgC,qBAAqB;;;;;;;;;;EAWrD,IAAI,MAAc;GAChB,OAAO,MAAM;EACf;EAEA,IAAI,IAAI,OAAe;GAIrB,IAAI,MAAM,QAAQ,OAAO;GAEzB,KAAK,SAAS,iBAAiB,KAAK,MAAM,QAAQ,EAAE,KAAK,MAAM,IAAI;EACrE;;;;;;;EAQA,IAAI,SAA+B;GACjC,OAAO,KAAKA;EACd;EAEA,IAAI,OAAO,OAA6B;GACtC,MAAM,SAAS,SAAS;GAGxB,IAAI,WAAW,KAAKA,SAAS;GAE7B,KAAKA,UAAU;GACf,MAAM,MAAO,WAAW,kBAAkB,MAAM,KAAK,OAAO,QAAS;GAErE,KAAK,gBAAgB,IAAI,MAAM,cAAc,CAAC;EAChD;;;;;;;;EASA,IAAI,cAA8B;GAChC,MAAM,SAAS,mBAAmB,KAAKA,OAAO;GAC9C,MAAM,aAAa,uBAAuB,KAAKA,OAAO;GAEtD,OAAO;IACL,GAAI,UAAU,EAAE,OAAO;IACvB,GAAI,cAAc,EAAE,WAAW;GACjC;EACF;CACF;CAIA,OAAO;AAGT"}
1
+ {"version":3,"file":"adapter.js","names":["#source","#refreshContentData","#contentData"],"sources":["../../../../../src/playback/adapters/mux-video/adapter.ts"],"sourcesContent":["import {\n createMuxPosterURL,\n createMuxStoryboardURL,\n createMuxVideoURL,\n type MuxContentData,\n type MuxSourceBase,\n parseMuxVideoURL,\n} from '@videojs/media/dom/mux/source';\nimport { shallowEqual } from '@videojs/utils/object';\nimport type { Constructor, MixinReturn } from '@videojs/utils/types';\n\nexport interface MuxMediaProps {\n src: string;\n source: MuxSourceBase | null;\n}\n\nexport const muxMediaDefaultProps: MuxMediaProps = {\n src: '',\n source: null,\n};\n\nexport interface MuxMediaAPI extends MuxMediaProps {\n readonly contentData: MuxContentData;\n}\n\n/**\n * Mux identity over any SPF Media: the structured `source`, the `src` derived\n * from it, and the image URLs it describes.\n *\n * Everything here is Mux identity, so it carries no engine and both flavors get\n * it unchanged — the video Media over the full HLS engine, the audio-only Media\n * over the subtractive one. A mixin rather than a shared base class because each\n * flavor extends a different SPF Media, so there is no common class to put this\n * on, only a common `src` accessor to write through.\n *\n * Unlike the hls.js-backed `MuxMedia`, there is no inherited `source` to\n * delegate to — the SPF Medias know only `src` — so this owns the structured\n * source and dispatches `sourcechange` itself.\n *\n * @fires sourcechange - Fired when `source` changes, either directly or by parsing a new `src`. Read `source` for the new value.\n * @fires contentdatachange - Fired when the derived `contentData` changes. Read `contentData` for the new value.\n */\nexport function MuxMediaMixin<Base extends Constructor<any>>(BaseClass: Base) {\n class MuxMediaImpl extends BaseClass {\n /**\n * Named on the error copy when this engine can't play a source: the\n * hls.js-backed Mux Media plays the MPEG-TS and DRM-protected sources that\n * SPF does not, and it backs both `<mux-video>` and `<mux-audio>`.\n *\n * Names the flavor rather than an import path, because one Media is reached\n * through three of them — `@videojs/html`, `@videojs/react`, and this package\n * — and each has a different counterpart. The flavor suffix is the one thing\n * common to the layers a consumer imports elements and components from.\n */\n static get alternativeMediaSuggestion(): string | undefined {\n return 'Try the hls.js-backed Mux media instead: import the `hls-js` flavor in place of the `spf` one.';\n }\n\n #source: MuxSourceBase | null = muxMediaDefaultProps.source;\n #contentData: MuxContentData = {};\n\n /**\n * Media source URL. Setting a Mux stream URL\n * (`https://stream.mux.com/<playback-id>.m3u8?...`) extracts the playback ID\n * and query params into `source`; other URLs are kept as a plain `source.src`.\n *\n * Only playback options carry over. Mux identity comes from the URL, and the\n * signed `poster`, `storyboard`, and `drm` tokens are scoped to a playback ID,\n * so carrying them onto a different source would build rejected URLs.\n */\n get src(): string {\n return super.src;\n }\n\n set src(value: string) {\n // A URL already describing the current source leaves it alone. The elements\n // reflect the derived `src` back to the host, and re-deriving would drop the\n // params a Mux URL does not carry, such as `poster`.\n if (super.src === value) return;\n\n this.source = parseMuxVideoURL(value) ?? (value ? { src: value } : null);\n }\n\n /**\n * Structured Mux source. Setting it derives `src` from the playback ID, custom\n * domain, and `playback` params (appended as `snake_case` query params). A\n * `playback.token` replaces all other params — signed URLs bake them into the\n * token.\n */\n get source(): MuxSourceBase | null {\n return this.#source;\n }\n\n set source(value: MuxSourceBase | null) {\n const source = value ?? null;\n // Changing anything takes a new object, so handing the same one back costs\n // nothing.\n if (source === this.#source) return;\n\n this.#source = source;\n\n // Refresh the bag before announcing `sourcechange`, because listeners read\n // `contentData` from that event. Announcing its own change waits until\n // after, so `src` is in step by the time either event fires.\n const contentDataChanged = this.#refreshContentData();\n\n super.src = (source && (createMuxVideoURL(source) ?? source.src)) || '';\n\n this.dispatchEvent?.(new Event('sourcechange'));\n\n if (contentDataChanged) this.dispatchEvent?.(new Event('contentdatachange'));\n }\n\n /**\n * Image URLs `source` describes rather than plays: `poster` from its `poster`\n * params, `storyboard` from its `storyboard` params.\n *\n * Derived from `source` and nothing else. The same object is handed back\n * until one of those URLs changes, and `contentdatachange` announces it when\n * it does. Nothing here is applied for you.\n */\n get contentData(): MuxContentData {\n return this.#contentData;\n }\n\n /** Rebuild the derived bag, reporting whether anything about it changed. */\n #refreshContentData(): boolean {\n const poster = createMuxPosterURL(this.#source);\n const storyboard = createMuxStoryboardURL(this.#source);\n\n const next: MuxContentData = {\n ...(poster && { poster }),\n ...(storyboard && { storyboard }),\n };\n\n if (shallowEqual(this.#contentData, next)) return false;\n\n this.#contentData = next;\n return true;\n }\n }\n\n // `MixinReturn` sources statics from `Base`, so this mixin's own needs adding\n // back to the type or callers can't read it.\n return MuxMediaImpl as unknown as MixinReturn<Base, MuxMediaAPI> & {\n readonly alternativeMediaSuggestion: string | undefined;\n };\n}\n"],"mappings":";;;AAgBA,MAAa,uBAAsC;CACjD,KAAK;CACL,QAAQ;AACV;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,cAA6C,WAAiB;CAC5E,MAAM,qBAAqB,UAAU;;;;;;;;;;;EAWnC,WAAW,6BAAiD;GAC1D,OAAO;EACT;EAEA,UAAgC,qBAAqB;EACrD,eAA+B,CAAC;;;;;;;;;;EAWhC,IAAI,MAAc;GAChB,OAAO,MAAM;EACf;EAEA,IAAI,IAAI,OAAe;GAIrB,IAAI,MAAM,QAAQ,OAAO;GAEzB,KAAK,SAAS,iBAAiB,KAAK,MAAM,QAAQ,EAAE,KAAK,MAAM,IAAI;EACrE;;;;;;;EAQA,IAAI,SAA+B;GACjC,OAAO,KAAKA;EACd;EAEA,IAAI,OAAO,OAA6B;GACtC,MAAM,SAAS,SAAS;GAGxB,IAAI,WAAW,KAAKA,SAAS;GAE7B,KAAKA,UAAU;GAKf,MAAM,qBAAqB,KAAKC,oBAAoB;GAEpD,MAAM,MAAO,WAAW,kBAAkB,MAAM,KAAK,OAAO,QAAS;GAErE,KAAK,gBAAgB,IAAI,MAAM,cAAc,CAAC;GAE9C,IAAI,oBAAoB,KAAK,gBAAgB,IAAI,MAAM,mBAAmB,CAAC;EAC7E;;;;;;;;;EAUA,IAAI,cAA8B;GAChC,OAAO,KAAKC;EACd;;EAGA,sBAA+B;GAC7B,MAAM,SAAS,mBAAmB,KAAKF,OAAO;GAC9C,MAAM,aAAa,uBAAuB,KAAKA,OAAO;GAEtD,MAAM,OAAuB;IAC3B,GAAI,UAAU,EAAE,OAAO;IACvB,GAAI,cAAc,EAAE,WAAW;GACjC;GAEA,IAAI,aAAa,KAAKE,cAAc,IAAI,GAAG,OAAO;GAElD,KAAKA,eAAe;GACpB,OAAO;EACT;CACF;CAIA,OAAO;AAGT"}
@@ -1,4 +1,4 @@
1
- import { computed, update } from "../../core/signals/primitives.js";
1
+ import { computed, peek, update } from "../../core/signals/primitives.js";
2
2
  import { defineBehavior } from "../../core/composition/create-composition.js";
3
3
  import { createMachineReactor } from "../../core/reactors/create-machine-reactor.js";
4
4
  import { isResolvedPresentation } from "../../media/types/index.js";
@@ -51,6 +51,40 @@ function emitError(state, error) {
51
51
  update(state.errors, (errors) => [...errors ?? [], error]);
52
52
  }
53
53
  /**
54
+ * A "constraint" that reports a type the source carries **no** renditions of, for
55
+ * a composition that can't play without it.
56
+ *
57
+ * Strange on purpose, and the strangeness is the point: it never constrains
58
+ * anything, always returning its input untouched. It is shaped as a rule so a
59
+ * composition opts in by adding it to `constraints` — nothing to thread through
60
+ * config, and no cost at all to a composition that leaves it out.
61
+ *
62
+ * **Belongs first in the chain.** A constraint sees the list as it stands at its
63
+ * own position, so only at the head does an empty input mean "the source offers
64
+ * none of this type" rather than "the constraints ahead of me pruned them all."
65
+ *
66
+ * This is the one failure no per-rendition cause can report: causes come from
67
+ * `reportUnsupportedTrackConditions` as each media playlist resolves, and here
68
+ * nothing resolves, because there is nothing to resolve. Everything else already
69
+ * reports something more specific than a verdict.
70
+ *
71
+ * Idempotent because the constraint chain runs inside a `computed` that re-derives
72
+ * on every `presentation` write — segment appends and live reloads included — and
73
+ * the sequence deliberately keeps duplicates. `peek` is what keeps that computed
74
+ * from subscribing to the slot this writes.
75
+ *
76
+ * @example
77
+ * // engine-background-video.ts — video-only, so a source with none can't play
78
+ * constraints: [reportAbsentTrackType(SVTA_NO_SUPPORTED_VIDEO_TRACK), excludeUnplayableTracks]
79
+ */
80
+ function reportAbsentTrackType(code) {
81
+ return (tracks, { state }) => {
82
+ const reported = state.errors && peek(state.errors);
83
+ if (!tracks.length && !reported?.some((error) => error.code === code)) emitError(state, { code });
84
+ return tracks;
85
+ };
86
+ }
87
+ /**
54
88
  * Own `errors` for the resolved source's lifetime.
55
89
  *
56
90
  * @example
@@ -72,6 +106,6 @@ const collectErrors = defineBehavior({
72
106
  }
73
107
  });
74
108
  //#endregion
75
- export { collectErrors, emitError };
109
+ export { collectErrors, emitError, reportAbsentTrackType };
76
110
 
77
111
  //# sourceMappingURL=collect-errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"collect-errors.js","names":[],"sources":["../../../../src/playback/behaviors/collect-errors.ts"],"sourcesContent":["/**\n * **Owns the engine's error sequence.** Reporters append through\n * {@link emitError}; this behavior owns the slot and its per-source lifecycle,\n * clearing it on exit so a new source starts clean and the sequence can't grow\n * unbounded across a session.\n *\n * Same split as `setupFailoverMonitor` and `failedCdns`: writes come from\n * wherever the condition is detected, one behavior owns the slot. Deliberately\n * has no `effects` — it holds no policy and derives nothing. Severity is decided\n * at the adapter, not here (see `internal/design/spf/features/errors.md`), which\n * is why this is a lifecycle owner rather than an error *handler*.\n *\n * Clearing binds to *exit* of `presentation-resolved`, mirroring the sibling\n * mixins' clear-on-teardown (`emptied` / `MEDIA_DETACHED`). A live reload swaps\n * the presentation object without leaving the resolved state, so it doesn't\n * clear — only an actual source change or destroy does. Known gap: a\n * resolved→resolved source swap that never passes through unresolved carries the\n * prior source's errors forward; `resolve-track` guards the same transition with\n * a commit-time id check, and doing likewise here is a follow-up.\n *\n * The vocabulary itself ({@link SvtaError} and the codes) is DOM- and\n * signal-free in `media/errors`; only the write seam lives here, with the slot\n * it writes.\n */\n\nimport { defineBehavior } from '../../core/composition/create-composition';\nimport { createMachineReactor } from '../../core/reactors/create-machine-reactor';\nimport { computed, type ReadonlySignal, type Signal, update } from '../../core/signals/primitives';\nimport type { SvtaError } from '../../media/errors';\nimport { isResolvedPresentation, type MaybeResolvedPresentation } from '../../media/types';\n\nexport interface CollectErrorsState {\n presentation?: MaybeResolvedPresentation;\n errors?: SvtaError[];\n}\n\n/**\n * State an error reporter writes into. The slot is *optional*: a behavior reports\n * through this seam without declaring ownership in its own typed slice, and\n * emission no-ops when `collectErrors` isn't composed. Same contract as\n * `failedCdns` / `failoverFetch`.\n */\nexport interface ErrorEmitterState {\n errors?: Signal<SvtaError[] | undefined>;\n}\n\n/**\n * Append `error` to the engine's error sequence. No-op when no owner is\n * composed. Replaces the array rather than mutating it, so signal consumers\n * notify; duplicates are kept, since a repeated condition is a real observation.\n * Writes go through `update` so concurrent reporters can't lose each other's\n * appends.\n *\n * Every emission is logged, deliberately *before* the owner check. A condition\n * emitted with no `collectErrors` composed is dropped on the floor — that's the\n * case where a log is the only evidence it happened at all, so gating the log on\n * the same check would hide exactly what's worth seeing. Emissions that *are*\n * collected still get logged, because reaching `state.errors` is no guarantee of\n * reaching a person: only *verdicts* are promoted to the media surface, so every\n * cause (and any non-fatal notice) is otherwise invisible outside a debugger.\n *\n * Ungated rather than `__DEV__`-only, matching the other reporting paths in this\n * package (`resolve-presentation`, `track-switching`, the segment actors).\n */\nexport function emitError(state: ErrorEmitterState, error: SvtaError): void {\n console.error('[spf] reported condition', error);\n if (!state.errors) return;\n update(state.errors, (errors) => [...(errors ?? []), error]);\n}\n\n/**\n * Own `errors` for the resolved source's lifetime.\n *\n * @example\n * const reactor = collectErrors.setup({ state });\n */\nexport const collectErrors = defineBehavior({\n stateKeys: ['presentation', 'errors'],\n contextKeys: [],\n setup: ({\n state,\n }: {\n state: {\n presentation: ReadonlySignal<CollectErrorsState['presentation']>;\n errors: Signal<CollectErrorsState['errors']>;\n };\n }) => {\n const derivedStateSignal = computed(() =>\n isResolvedPresentation(state.presentation.get())\n ? ('presentation-resolved' as const)\n : ('presentation-unresolved' as const)\n );\n\n return createMachineReactor({\n initial: 'presentation-unresolved',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'presentation-unresolved': {},\n 'presentation-resolved': {\n // Cleanup-binds-to-setup: reset for the next source on exit (src\n // unload + destroy).\n entry: () => () => state.errors.set(undefined),\n },\n },\n });\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA,SAAgB,UAAU,OAA0B,OAAwB;CAC1E,QAAQ,MAAM,4BAA4B,KAAK;CAC/C,IAAI,CAAC,MAAM,QAAQ;CACnB,OAAO,MAAM,SAAS,WAAW,CAAC,GAAI,UAAU,CAAC,GAAI,KAAK,CAAC;AAC7D;;;;;;;AAQA,MAAa,gBAAgB,eAAe;CAC1C,WAAW,CAAC,gBAAgB,QAAQ;CACpC,aAAa,CAAC;CACd,QAAQ,EACN,YAMI;EACJ,MAAM,qBAAqB,eACzB,uBAAuB,MAAM,aAAa,IAAI,CAAC,IAC1C,0BACA,yBACP;EAEA,OAAO,qBAAqB;GAC1B,SAAS;GACT,eAAe,mBAAmB,IAAI;GACtC,QAAQ;IACN,2BAA2B,CAAC;IAC5B,yBAAyB,EAGvB,mBAAmB,MAAM,OAAO,IAAI,KAAA,CAAS,EAC/C;GACF;EACF,CAAC;CACH;AACF,CAAC"}
1
+ {"version":3,"file":"collect-errors.js","names":[],"sources":["../../../../src/playback/behaviors/collect-errors.ts"],"sourcesContent":["/**\n * **Owns the engine's error sequence.** Reporters append through\n * {@link emitError}; this behavior owns the slot and its per-source lifecycle,\n * clearing it on exit so a new source starts clean and the sequence can't grow\n * unbounded across a session.\n *\n * Same split as `setupFailoverMonitor` and `failedCdns`: writes come from\n * wherever the condition is detected, one behavior owns the slot. Deliberately\n * has no `effects` — it holds no policy and derives nothing. Severity is decided\n * at the adapter, not here (see `internal/design/spf/features/errors.md`), which\n * is why this is a lifecycle owner rather than an error *handler*.\n *\n * Clearing binds to *exit* of `presentation-resolved`, mirroring the sibling\n * mixins' clear-on-teardown (`emptied` / `MEDIA_DETACHED`). A live reload swaps\n * the presentation object without leaving the resolved state, so it doesn't\n * clear — only an actual source change or destroy does. Known gap: a\n * resolved→resolved source swap that never passes through unresolved carries the\n * prior source's errors forward; `resolve-track` guards the same transition with\n * a commit-time id check, and doing likewise here is a follow-up.\n *\n * The vocabulary itself ({@link SvtaError} and the codes) is DOM- and\n * signal-free in `media/errors`; only the write seam lives here, with the slot\n * it writes.\n */\n\nimport { defineBehavior } from '../../core/composition/create-composition';\nimport { createMachineReactor } from '../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal, type Signal, update } from '../../core/signals/primitives';\nimport type { SvtaError } from '../../media/errors';\nimport { isResolvedPresentation, type MaybeResolvedPresentation } from '../../media/types';\nimport type { SelectionRule } from '../primitives/selection-rules';\n\nexport interface CollectErrorsState {\n presentation?: MaybeResolvedPresentation;\n errors?: SvtaError[];\n}\n\n/**\n * State an error reporter writes into. The slot is *optional*: a behavior reports\n * through this seam without declaring ownership in its own typed slice, and\n * emission no-ops when `collectErrors` isn't composed. Same contract as\n * `failedCdns` / `failoverFetch`.\n */\nexport interface ErrorEmitterState {\n errors?: Signal<SvtaError[] | undefined>;\n}\n\n/**\n * Append `error` to the engine's error sequence. No-op when no owner is\n * composed. Replaces the array rather than mutating it, so signal consumers\n * notify; duplicates are kept, since a repeated condition is a real observation.\n * Writes go through `update` so concurrent reporters can't lose each other's\n * appends.\n *\n * Every emission is logged, deliberately *before* the owner check. A condition\n * emitted with no `collectErrors` composed is dropped on the floor — that's the\n * case where a log is the only evidence it happened at all, so gating the log on\n * the same check would hide exactly what's worth seeing. Emissions that *are*\n * collected still get logged, because reaching `state.errors` is no guarantee of\n * reaching a person: only *verdicts* are promoted to the media surface, so every\n * cause (and any non-fatal notice) is otherwise invisible outside a debugger.\n *\n * Ungated rather than `__DEV__`-only, matching the other reporting paths in this\n * package (`resolve-presentation`, `track-switching`, the segment actors).\n */\nexport function emitError(state: ErrorEmitterState, error: SvtaError): void {\n console.error('[spf] reported condition', error);\n if (!state.errors) return;\n update(state.errors, (errors) => [...(errors ?? []), error]);\n}\n\n/**\n * A \"constraint\" that reports a type the source carries **no** renditions of, for\n * a composition that can't play without it.\n *\n * Strange on purpose, and the strangeness is the point: it never constrains\n * anything, always returning its input untouched. It is shaped as a rule so a\n * composition opts in by adding it to `constraints` — nothing to thread through\n * config, and no cost at all to a composition that leaves it out.\n *\n * **Belongs first in the chain.** A constraint sees the list as it stands at its\n * own position, so only at the head does an empty input mean \"the source offers\n * none of this type\" rather than \"the constraints ahead of me pruned them all.\"\n *\n * This is the one failure no per-rendition cause can report: causes come from\n * `reportUnsupportedTrackConditions` as each media playlist resolves, and here\n * nothing resolves, because there is nothing to resolve. Everything else already\n * reports something more specific than a verdict.\n *\n * Idempotent because the constraint chain runs inside a `computed` that re-derives\n * on every `presentation` write — segment appends and live reloads included — and\n * the sequence deliberately keeps duplicates. `peek` is what keeps that computed\n * from subscribing to the slot this writes.\n *\n * @example\n * // engine-background-video.ts — video-only, so a source with none can't play\n * constraints: [reportAbsentTrackType(SVTA_NO_SUPPORTED_VIDEO_TRACK), excludeUnplayableTracks]\n */\nexport function reportAbsentTrackType<T>(code: number): SelectionRule<T, ErrorEmitterState> {\n return (tracks, { state }) => {\n const reported = state.errors && peek(state.errors);\n if (!tracks.length && !reported?.some((error) => error.code === code)) {\n emitError(state, { code });\n }\n return tracks;\n };\n}\n\n/**\n * Own `errors` for the resolved source's lifetime.\n *\n * @example\n * const reactor = collectErrors.setup({ state });\n */\nexport const collectErrors = defineBehavior({\n stateKeys: ['presentation', 'errors'],\n contextKeys: [],\n setup: ({\n state,\n }: {\n state: {\n presentation: ReadonlySignal<CollectErrorsState['presentation']>;\n errors: Signal<CollectErrorsState['errors']>;\n };\n }) => {\n const derivedStateSignal = computed(() =>\n isResolvedPresentation(state.presentation.get())\n ? ('presentation-resolved' as const)\n : ('presentation-unresolved' as const)\n );\n\n return createMachineReactor({\n initial: 'presentation-unresolved',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'presentation-unresolved': {},\n 'presentation-resolved': {\n // Cleanup-binds-to-setup: reset for the next source on exit (src\n // unload + destroy).\n entry: () => () => state.errors.set(undefined),\n },\n },\n });\n },\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiEA,SAAgB,UAAU,OAA0B,OAAwB;CAC1E,QAAQ,MAAM,4BAA4B,KAAK;CAC/C,IAAI,CAAC,MAAM,QAAQ;CACnB,OAAO,MAAM,SAAS,WAAW,CAAC,GAAI,UAAU,CAAC,GAAI,KAAK,CAAC;AAC7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAgB,sBAAyB,MAAmD;CAC1F,QAAQ,QAAQ,EAAE,YAAY;EAC5B,MAAM,WAAW,MAAM,UAAU,KAAK,MAAM,MAAM;EAClD,IAAI,CAAC,OAAO,UAAU,CAAC,UAAU,MAAM,UAAU,MAAM,SAAS,IAAI,GAClE,UAAU,OAAO,EAAE,KAAK,CAAC;EAE3B,OAAO;CACT;AACF;;;;;;;AAQA,MAAa,gBAAgB,eAAe;CAC1C,WAAW,CAAC,gBAAgB,QAAQ;CACpC,aAAa,CAAC;CACd,QAAQ,EACN,YAMI;EACJ,MAAM,qBAAqB,eACzB,uBAAuB,MAAM,aAAa,IAAI,CAAC,IAC1C,0BACA,yBACP;EAEA,OAAO,qBAAqB;GAC1B,SAAS;GACT,eAAe,mBAAmB,IAAI;GACtC,QAAQ;IACN,2BAA2B,CAAC;IAC5B,yBAAyB,EAGvB,mBAAmB,MAAM,OAAO,IAAI,KAAA,CAAS,EAC/C;GACF;EACF,CAAC;CACH;AACF,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { defineBehavior } from "../../../core/composition/create-composition.js";
2
+ import { watchScreenResolution } from "../../../media/dom/screen.js";
3
+ //#region src/playback/behaviors/dom/track-screen-resolution.ts
4
+ /**
5
+ * Mirror the screen's pixel dimensions into reactive state, so a rendition cap
6
+ * can narrow candidates to what the screen can actually show without reading the
7
+ * environment at pick time — which would make the picker impure, and would never
8
+ * re-pick when the screen changed.
9
+ *
10
+ * Populated at setup rather than on the first change: `watchScreenResolution`
11
+ * reports its starting value, so nothing downstream waits on a screen that may
12
+ * never move. `undefined` where there is no screen to read, which is the value a
13
+ * cap reads as "no cap" — see `getScreenResolution` on why that beats a zero.
14
+ *
15
+ * Reads no other slot, and has no source-identity reset: the screen is
16
+ * independent of the presentation, so a new `src` doesn't invalidate the reading.
17
+ */
18
+ function trackScreenResolutionSetup({ state, config }) {
19
+ return watchScreenResolution((resolution) => state.screenResolution.set(resolution), { useDevicePixelRatio: config?.useDevicePixelRatio ?? true });
20
+ }
21
+ const trackScreenResolution = defineBehavior({
22
+ stateKeys: ["screenResolution"],
23
+ contextKeys: [],
24
+ setup: trackScreenResolutionSetup
25
+ });
26
+ //#endregion
27
+ export { trackScreenResolution };
28
+
29
+ //# sourceMappingURL=track-screen-resolution.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"track-screen-resolution.js","names":[],"sources":["../../../../../src/playback/behaviors/dom/track-screen-resolution.ts"],"sourcesContent":["/**\n * Mirror the screen's pixel dimensions into reactive state, so a rendition cap\n * can narrow candidates to what the screen can actually show without reading the\n * environment at pick time — which would make the picker impure, and would never\n * re-pick when the screen changed.\n *\n * Populated at setup rather than on the first change: `watchScreenResolution`\n * reports its starting value, so nothing downstream waits on a screen that may\n * never move. `undefined` where there is no screen to read, which is the value a\n * cap reads as \"no cap\" — see `getScreenResolution` on why that beats a zero.\n *\n * Reads no other slot, and has no source-identity reset: the screen is\n * independent of the presentation, so a new `src` doesn't invalidate the reading.\n */\nimport { defineBehavior } from '../../../core/composition/create-composition';\nimport type { Signal } from '../../../core/signals/primitives';\nimport { type ScreenResolution, watchScreenResolution } from '../../../media/dom/screen';\n\nexport interface ScreenResolutionState {\n screenResolution?: ScreenResolution;\n}\n\nexport interface TrackScreenResolutionConfig {\n /**\n * Whether the reading is scaled into device pixels. Defaults to `true` — see\n * `ScreenResolutionOptions.useDevicePixelRatio`, including its note on page\n * zoom being folded into the ratio outside WebKit.\n */\n useDevicePixelRatio?: boolean;\n}\n\nfunction trackScreenResolutionSetup({\n state,\n config,\n}: {\n state: { screenResolution: Signal<ScreenResolutionState['screenResolution']> };\n config?: TrackScreenResolutionConfig;\n}): () => void {\n const useDevicePixelRatio = config?.useDevicePixelRatio ?? true;\n\n // No `effect` wrapper, unlike its `track*` siblings: they re-subscribe when\n // `context.mediaElement` changes, and this behavior has no reactive dependency\n // to re-run on. The watcher's own teardown is the whole cleanup.\n return watchScreenResolution((resolution) => state.screenResolution.set(resolution), { useDevicePixelRatio });\n}\n\nexport const trackScreenResolution = defineBehavior({\n stateKeys: ['screenResolution'],\n contextKeys: [],\n setup: trackScreenResolutionSetup,\n});\n"],"mappings":";;;;;;;;;;;;;;;;;AA+BA,SAAS,2BAA2B,EAClC,OACA,UAIa;CAMb,OAAO,uBAAuB,eAAe,MAAM,iBAAiB,IAAI,UAAU,GAAG,EAAE,qBAL3D,QAAQ,uBAAuB,KAKgD,CAAC;AAC9G;AAEA,MAAa,wBAAwB,eAAe;CAClD,WAAW,CAAC,kBAAkB;CAC9B,aAAa,CAAC;CACd,OAAO;AACT,CAAC"}
@@ -1,14 +1,72 @@
1
1
  import "../../core/signals/primitives.js";
2
2
  import "../../index.js";
3
- import { TrackPicker, VideoSelectionConfig } from "../../media/primitives/select-tracks.js";
3
+ import { CapabilityConstraintConfig, SelectionRule } from "../primitives/selection-rules.js";
4
4
  //#region src/playback/behaviors/select-tracks.d.ts
5
+ /** A selection rule over this behavior's candidate tracks. */
6
+ type SelectTrackRule<Config> = SelectionRule<SelectableTrack, unknown, unknown, Config | undefined>;
7
+ /** What a rule here needs off a candidate: the id it may become the pick by. */
8
+ type SelectableTrack = {
9
+ id: string;
10
+ };
5
11
  /**
6
- * Config for `selectVideoTrack`. Pass `picker` to fully override selection
7
- * logic; otherwise the default `pickFirstTrackId` is used.
12
+ * Order the candidates by resolution, largest first, with bandwidth breaking ties
13
+ * between renditions of identical dimensions. The background-video default that
14
+ * variant pins one rendition for the session, and absent a cap the largest is the
15
+ * head.
16
+ *
17
+ * A ranker, so it reorders rather than narrowing: the chain's pick is the head of
18
+ * what it returns, which means ranking never has to collapse to one track. Belongs
19
+ * last in a chain — a sort only reorders what survived the filters ahead of it, and
20
+ * leaving it last is what lets `applyRules` early-bail before it runs.
21
+ *
22
+ * Exported because it is a *rule*, not a variant's private policy: the same one
23
+ * composes into `switchVideoTrack`'s chain when a ranker is wanted there.
8
24
  */
9
- interface SelectVideoTrackConfig extends VideoSelectionConfig {
10
- picker?: TrackPicker<SelectVideoTrackConfig>;
25
+ declare const preferHighestResolution: SelectTrackRule<unknown>;
26
+ /**
27
+ * Narrow to the renditions that fit the screen, by pixel area — the screen-size
28
+ * cap from `internal/design/spf/features/rendition-selection-caps.md`, as a scope
29
+ * (soft filter) rather than a constraint: an over-cap rendition is wasteful, not
30
+ * unplayable, so nothing here may make a source unplayable.
31
+ *
32
+ * Narrows only — it neither orders the survivors nor resolves the case where none
33
+ * survive, because `applyRules` owns both. So it needs a ranker behind it to pick
34
+ * within the cap: `[screenResolutionCap, preferHighestResolution]` yields the
35
+ * largest rendition that fits. Composed *last*, the pick would instead be whichever
36
+ * fitting rendition the manifest happened to list first.
37
+ *
38
+ * Reading `state.screenResolution` through its signal is what subscribes a
39
+ * re-evaluating chain (`switchVideoTrack`) to screen changes; `selectVideoTrack`
40
+ * pins the first answer instead, by design.
41
+ *
42
+ * Compares areas rather than matching a `"1080p"`-style tier because a tier only
43
+ * describes a rendition once you assume its aspect ratio — the assumption that
44
+ * mis-measures an anamorphic ladder. See `media/dom/screen.ts`.
45
+ *
46
+ * Three ways the cap ends up not applying, all of them fall-through:
47
+ *
48
+ * - **No `screenResolution` signal at all**, because the composition omits
49
+ * `trackScreenResolution`. So composing the cap without its signal source is
50
+ * inert rather than broken.
51
+ * - **A `screenResolution` of `undefined`**, meaning no screen to read. "Unknown"
52
+ * has to mean "don't cap": treating it as an area of zero would pin every source
53
+ * to its smallest rendition on exactly the environments we know least about.
54
+ * - **No rendition fits**, on a screen smaller than the whole ladder. `applyRules`
55
+ * skips the empty result and the chain proceeds unnarrowed, so the ranker behind
56
+ * the cap decides — for `preferHighestResolution`, the largest rendition. A floor
57
+ * is the fix if that ever matters (`rendition-selection-caps.md` carries one), not
58
+ * a special case here.
59
+ */
60
+ declare const screenResolutionCap: SelectTrackRule<unknown>;
61
+ /**
62
+ * Config for `selectVideoTrack`. Pass `rules` to replace the selection chain, or
63
+ * `constraints` to replace the capability pre-pass; otherwise the chain is empty
64
+ * and the first playable candidate is the pick.
65
+ */
66
+ interface SelectVideoTrackConfig extends CapabilityConstraintConfig {
67
+ constraints?: readonly SelectTrackRule<SelectVideoTrackConfig>[];
68
+ rules?: readonly SelectTrackRule<SelectVideoTrackConfig>[];
11
69
  }
12
70
  //#endregion
13
- export { SelectVideoTrackConfig };
71
+ export { SelectTrackRule, SelectVideoTrackConfig, preferHighestResolution, screenResolutionCap };
14
72
  //# sourceMappingURL=select-tracks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"select-tracks.d.ts","names":[],"sources":["../../../../src/playback/behaviors/select-tracks.ts"],"mappings":";;;;;;;;UA0IiB,+BAA+B;EAC9C,SAAS,YAAY"}
1
+ {"version":3,"file":"select-tracks.d.ts","names":[],"sources":["../../../../src/playback/behaviors/select-tracks.ts"],"mappings":";;;;;KA6FY,gBAAgB,UAAU,cAAc,mCAAmC;;KAGlF;EAAoB;;;;;;;;;;;;;;;;cA+JZ,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA+CzB,qBAAqB;;;;;;UAgBjB,+BAA+B;EAC9C,uBAAuB,gBAAgB;EACvC,iBAAiB,gBAAgB"}