@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 +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"}
@@ -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,27 +1,48 @@
1
- import { computed } from "../../core/signals/primitives.js";
1
+ import { computed, peek } 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";
5
+ import { getTracksByType } from "../../media/utils/tracks.js";
5
6
  import { AUDIO_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from "../primitives/track-types.js";
6
- import { pickAudioTrack, pickFirstTrackId } from "../../media/primitives/select-tracks.js";
7
+ import { byDescendingResolution, pickAudioTrackFromTracks, tracksUnderPixelArea } from "../../media/primitives/select-tracks.js";
8
+ import { applyConstraints, applyRules, excludeUnplayableTracks, sameCandidateSet } from "../primitives/selection-rules.js";
7
9
  //#region src/playback/behaviors/select-tracks.ts
8
10
  /**
9
11
  * **Default audio/video track selection on src load / unselect on src unload.**
10
12
  * When a presentation is resolved, sets `selectedVideoTrackId` /
11
- * `selectedAudioTrackId` to a per-type-picker default if no selection already
12
- * exists. When the presentation is unset/reset (transitions back to unresolved),
13
+ * `selectedAudioTrackId` from a per-type default rule chain if no selection
14
+ * already exists. When the presentation is unset/reset (transitions back to unresolved),
13
15
  * clears the selection so a stale id from the previous source doesn't persist.
14
16
  *
15
- * Lifecycle-driven: each transition fires its work once. Does not police the
16
- * selection between transitions; external writes (user picks, ABR, programmatic
17
- * filter-driven re-picks) are left alone.
17
+ * Lifecycle-driven: the pick fires once per transition, and nothing re-picks
18
+ * that is what separates these from the `switch*` variants. External writes (user
19
+ * picks, ABR, programmatic filter-driven re-picks) are left alone, including a
20
+ * write naming a track the manifest never offered.
18
21
  *
19
- * Picker is config-driven: each per-type export wires a sensible default
20
- * (`pickAudioTrack` for audio three-tier language-aware; `pickFirstTrackId`
21
- * for video) and the caller can supply their own via `config.picker` for custom
22
- * selection logic. The behavior's `config` is forwarded to the picker as its
23
- * second argument, so options like `preferredAudioLanguage` reach the picker
24
- * without an intermediate wrapping layer.
22
+ * The one thing policed between transitions is a pick the *constraints* turn
23
+ * against: a rendition's container and encryption are only known once its media
24
+ * playlist resolves, which is after the pick was made, so a selection that becomes
25
+ * unplayable is dropped. Dropped, never moved — re-picking is exactly the behavior
26
+ * `switchVideoTrack` exists to provide. Dropping reports nothing on its own, since
27
+ * whatever made the pick unplayable already reported its own, more specific cause.
28
+ *
29
+ * Selection runs the same rule model `switchVideoTrack` does — a hard
30
+ * `constraints` pre-pass, then an ordered `rules` chain, with the pick as the
31
+ * head (see `internal/design/spf/track-switching-model.md`). What differs is
32
+ * reactivity, not the rules: this evaluates the chain once on resolve and pins
33
+ * the result, where `switchVideoTrack` re-evaluates inside an effect so its rules
34
+ * subscribe to bandwidth and user selection. A rule written for one therefore
35
+ * composes into the other unchanged.
36
+ *
37
+ * Both are config-driven, each per-type export wiring a sensible default: audio's
38
+ * three-tier language policy, and for video the *empty* chain — with nothing
39
+ * narrowing or reordering, the head is the first candidate. The behavior's
40
+ * `config` is forwarded to the rules, so options like `preferredAudioLanguage`
41
+ * reach them without an intermediate layer.
42
+ *
43
+ * Note a rule can only pick among real candidates, where the picker it replaced
44
+ * could return any id at all. An id absent from the manifest was never
45
+ * selectable, so that narrowing is the point rather than a limitation.
25
46
  *
26
47
  * Compose `selectVideoTrack` for the simple "pick a default video track"
27
48
  * behavior, or `switchVideoTrack` (`./track-switching.ts`) for the
@@ -38,25 +59,115 @@ import { pickAudioTrack, pickFirstTrackId } from "../../media/primitives/select-
38
59
  * (`./track-switching.ts`), which resolves standing `userTextTrackSelection`
39
60
  * intent against the constrained, CDN-scoped renditions.
40
61
  */
41
- function setupTrackSelection({ state, config: { selectedKey, picker, pickerConfig } }) {
62
+ function setupTrackSelection({ state, config: { selectedKey, trackType, constraints, rules, ruleConfig } }) {
42
63
  const derivedStateSignal = computed(() => isResolvedPresentation(state.presentation.get()) ? "presentation-resolved" : "presentation-unresolved");
64
+ const deps = {
65
+ state,
66
+ config: ruleConfig
67
+ };
68
+ const candidateSet = computed(() => {
69
+ const presentation = state.presentation.get();
70
+ if (!isResolvedPresentation(presentation)) return [];
71
+ return applyConstraints(constraints, getTracksByType(presentation, trackType), deps);
72
+ }, { equals: sameCandidateSet });
43
73
  return createMachineReactor({
44
74
  initial: "presentation-unresolved",
45
75
  monitor: () => derivedStateSignal.get(),
46
76
  states: {
47
77
  "presentation-unresolved": {},
48
- "presentation-resolved": { entry: () => {
49
- if (!state[selectedKey].get()) {
50
- const id = picker(state.presentation.get(), pickerConfig);
51
- if (id) state[selectedKey].set(id);
52
- }
53
- return () => state[selectedKey].set(void 0);
54
- } }
78
+ "presentation-resolved": {
79
+ entry: () => {
80
+ if (!state[selectedKey].get()) {
81
+ const id = applyRules(rules, peek(candidateSet), deps)[0]?.id;
82
+ if (id) state[selectedKey].set(id);
83
+ }
84
+ return () => state[selectedKey].set(void 0);
85
+ },
86
+ effects: [() => {
87
+ const selectedId = peek(state[selectedKey]);
88
+ if (!selectedId) return;
89
+ if (candidateSet.get().some((track) => track.id === selectedId)) return;
90
+ const presentation = peek(state.presentation);
91
+ if (isResolvedPresentation(presentation) && getTracksByType(presentation, trackType).some((track) => track.id === selectedId)) state[selectedKey].set(void 0);
92
+ }]
93
+ }
55
94
  }
56
95
  });
57
96
  }
58
- /** Default video picker: first track in the video selection set. */
59
- const defaultVideoPicker = (presentation) => pickFirstTrackId(presentation, "video");
97
+ /** Default video chain: none. The first candidate is the pick. */
98
+ const DEFAULT_VIDEO_RULES = [];
99
+ /**
100
+ * Default audio chain: the three-tier policy (`preferredAudioLanguage` →
101
+ * `DEFAULT=YES` → first) as a single narrowing rule. Returning `[]` when nothing
102
+ * is picked lets `applyRules` fall through to the unnarrowed candidates, so the
103
+ * head stays the first track — the same last tier the policy itself ends on.
104
+ */
105
+ const preferAudioPolicy = (tracks, { config }) => {
106
+ const id = pickAudioTrackFromTracks(tracks, config);
107
+ const pick = tracks.find((track) => track.id === id);
108
+ return pick ? [pick] : [];
109
+ };
110
+ const DEFAULT_AUDIO_RULES = [preferAudioPolicy];
111
+ /**
112
+ * Order the candidates by resolution, largest first, with bandwidth breaking ties
113
+ * between renditions of identical dimensions. The background-video default — that
114
+ * variant pins one rendition for the session, and absent a cap the largest is the
115
+ * head.
116
+ *
117
+ * A ranker, so it reorders rather than narrowing: the chain's pick is the head of
118
+ * what it returns, which means ranking never has to collapse to one track. Belongs
119
+ * last in a chain — a sort only reorders what survived the filters ahead of it, and
120
+ * leaving it last is what lets `applyRules` early-bail before it runs.
121
+ *
122
+ * Exported because it is a *rule*, not a variant's private policy: the same one
123
+ * composes into `switchVideoTrack`'s chain when a ranker is wanted there.
124
+ */
125
+ const preferHighestResolution = (tracks) => [...tracks].sort(byDescendingResolution);
126
+ /**
127
+ * Narrow to the renditions that fit the screen, by pixel area — the screen-size
128
+ * cap from `internal/design/spf/features/rendition-selection-caps.md`, as a scope
129
+ * (soft filter) rather than a constraint: an over-cap rendition is wasteful, not
130
+ * unplayable, so nothing here may make a source unplayable.
131
+ *
132
+ * Narrows only — it neither orders the survivors nor resolves the case where none
133
+ * survive, because `applyRules` owns both. So it needs a ranker behind it to pick
134
+ * within the cap: `[screenResolutionCap, preferHighestResolution]` yields the
135
+ * largest rendition that fits. Composed *last*, the pick would instead be whichever
136
+ * fitting rendition the manifest happened to list first.
137
+ *
138
+ * Reading `state.screenResolution` through its signal is what subscribes a
139
+ * re-evaluating chain (`switchVideoTrack`) to screen changes; `selectVideoTrack`
140
+ * pins the first answer instead, by design.
141
+ *
142
+ * Compares areas rather than matching a `"1080p"`-style tier because a tier only
143
+ * describes a rendition once you assume its aspect ratio — the assumption that
144
+ * mis-measures an anamorphic ladder. See `media/dom/screen.ts`.
145
+ *
146
+ * Three ways the cap ends up not applying, all of them fall-through:
147
+ *
148
+ * - **No `screenResolution` signal at all**, because the composition omits
149
+ * `trackScreenResolution`. So composing the cap without its signal source is
150
+ * inert rather than broken.
151
+ * - **A `screenResolution` of `undefined`**, meaning no screen to read. "Unknown"
152
+ * has to mean "don't cap": treating it as an area of zero would pin every source
153
+ * to its smallest rendition on exactly the environments we know least about.
154
+ * - **No rendition fits**, on a screen smaller than the whole ladder. `applyRules`
155
+ * skips the empty result and the chain proceeds unnarrowed, so the ranker behind
156
+ * the cap decides — for `preferHighestResolution`, the largest rendition. A floor
157
+ * is the fix if that ever matters (`rendition-selection-caps.md` carries one), not
158
+ * a special case here.
159
+ */
160
+ const screenResolutionCap = (tracks, { state }) => {
161
+ const screenResolution = state?.screenResolution?.get();
162
+ if (!screenResolution) return [];
163
+ return tracksUnderPixelArea(tracks, screenResolution.width * screenResolution.height);
164
+ };
165
+ /**
166
+ * Default video constraints: the capability pre-pass alone. No
167
+ * `excludeFailedCdns` — this variant's compositions run no failover monitor, so
168
+ * `failedCdns` has no writer and the constraint would always pass through.
169
+ */
170
+ const DEFAULT_VIDEO_CONSTRAINTS = [excludeUnplayableTracks];
60
171
  /**
61
172
  * Select a video track when a presentation loads. Clears the selection on
62
173
  * src unload.
@@ -78,8 +189,10 @@ const selectVideoTrack = defineBehavior({
78
189
  state,
79
190
  config: {
80
191
  selectedKey: VIDEO_TYPE_CONFIG.selectedKey,
81
- picker: config?.picker ?? defaultVideoPicker,
82
- pickerConfig: config
192
+ trackType: "video",
193
+ constraints: config?.constraints ?? DEFAULT_VIDEO_CONSTRAINTS,
194
+ rules: config?.rules ?? DEFAULT_VIDEO_RULES,
195
+ ruleConfig: config
83
196
  }
84
197
  })
85
198
  });
@@ -90,12 +203,14 @@ defineBehavior({
90
203
  state,
91
204
  config: {
92
205
  selectedKey: AUDIO_TYPE_CONFIG.selectedKey,
93
- picker: config?.picker ?? pickAudioTrack,
94
- pickerConfig: config
206
+ trackType: "audio",
207
+ constraints: config?.constraints ?? [],
208
+ rules: config?.rules ?? DEFAULT_AUDIO_RULES,
209
+ ruleConfig: config
95
210
  }
96
211
  })
97
212
  });
98
213
  //#endregion
99
- export { selectVideoTrack };
214
+ export { preferHighestResolution, screenResolutionCap, selectVideoTrack };
100
215
 
101
216
  //# sourceMappingURL=select-tracks.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"select-tracks.js","names":[],"sources":["../../../../src/playback/behaviors/select-tracks.ts"],"sourcesContent":["/**\n * **Default audio/video track selection on src load / unselect on src unload.**\n * When a presentation is resolved, sets `selectedVideoTrackId` /\n * `selectedAudioTrackId` to a per-type-picker default if no selection already\n * exists. When the presentation is unset/reset (transitions back to unresolved),\n * clears the selection so a stale id from the previous source doesn't persist.\n *\n * Lifecycle-driven: each transition fires its work once. Does not police the\n * selection between transitions; external writes (user picks, ABR, programmatic\n * filter-driven re-picks) are left alone.\n *\n * Picker is config-driven: each per-type export wires a sensible default\n * (`pickAudioTrack` for audio — three-tier language-aware; `pickFirstTrackId`\n * for video) and the caller can supply their own via `config.picker` for custom\n * selection logic. The behavior's `config` is forwarded to the picker as its\n * second argument, so options like `preferredAudioLanguage` reach the picker\n * without an intermediate wrapping layer.\n *\n * Compose `selectVideoTrack` for the simple \"pick a default video track\"\n * behavior, or `switchVideoTrack` (`./track-switching.ts`) for the\n * ABR-driven variant. Compose `selectAudioTrack` for the simple default\n * pick, or `switchAudioTrack` (`./track-switching.ts`) for the\n * filter-reactive + mid-stream-flush slot-owner variant — when audio-abr\n * lands, `switchAudioTrack` extends into `switchAudioQuality`. Compose\n * only one per type — they're alternatives, not stackable (each writes\n * the same `selected*TrackId` slot). The simple variants tree-shake out\n * the heavier machinery (bandwidth estimator, quality selection, flush\n * orchestration).\n *\n * Text selection has no simple variant here — it's owned by `switchTextTrack`\n * (`./track-switching.ts`), which resolves standing `userTextTrackSelection`\n * intent against the constrained, CDN-scoped renditions.\n */\n\nimport { defineBehavior } from '../../core/composition/create-composition';\nimport { createMachineReactor } from '../../core/reactors/create-machine-reactor';\nimport { computed, type ReadonlySignal, type Signal } from '../../core/signals/primitives';\nimport {\n type AudioSelectionConfig,\n pickAudioTrack,\n pickFirstTrackId,\n type TrackPicker,\n type TrackSelectionState,\n type VideoSelectionConfig,\n} from '../../media/primitives/select-tracks';\nimport { isResolvedPresentation } from '../../media/types';\nimport { AUDIO_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from '../primitives/track-types';\n\n// ============================================================================\n// Specialization helper\n//\n// `setupTrackSelection` has the same shape as a Behavior `setup` function:\n// `({ state, config }) => Reactor`. Each `selectXTrack` export below calls\n// it from inside its own `defineBehavior` setup, supplying its per-type\n// `selectedKey`, default picker, and forwarded picker config. The lifecycle\n// — pick on entering 'presentation-resolved' if not already selected; clear\n// on entering 'presentation-unresolved' — is shared.\n// ============================================================================\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId';\n\ntype SelectStateMap<K extends SelectedTrackKey> = {\n presentation: ReadonlySignal<TrackSelectionState['presentation']>;\n} & { [P in K]: Signal<TrackSelectionState[P]> };\n\ninterface TrackSelectionSetupConfig<K extends SelectedTrackKey, PickerConfig> {\n selectedKey: K;\n picker: TrackPicker<PickerConfig>;\n pickerConfig?: PickerConfig;\n}\n\nfunction setupTrackSelection<K extends SelectedTrackKey, PickerConfig>({\n state,\n config: { selectedKey, picker, pickerConfig },\n}: {\n state: SelectStateMap<K>;\n config: TrackSelectionSetupConfig<K, PickerConfig>;\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 // Entry: pick a default on entering presentation-resolved if none\n // is set. External writes (user picks, ABR) that already populated\n // the slot are left alone.\n //\n // The returned cleanup runs on state exit — which fires on src\n // unload (presentation-resolved → presentation-unresolved) AND on\n // behavior destroy (presentation-resolved → destroying →\n // destroyed). Putting the clear here rather than as\n // presentation-unresolved.entry is more cohesive (operation +\n // cleanup co-located) and correctly covers destroy (destroy\n // doesn't pass through presentation-unresolved).\n entry: () => {\n if (!state[selectedKey].get()) {\n // `state.presentation.get()` is non-null inside this entry —\n // the reactor's `'presentation-resolved'` gate is exactly\n // `isResolvedPresentation(state.presentation.get())`, which\n // requires a truthy Presentation.\n const id = picker(state.presentation.get()!, pickerConfig);\n if (id) state[selectedKey].set(id);\n }\n return () => state[selectedKey].set(undefined);\n },\n },\n },\n });\n}\n\n// ============================================================================\n// Default pickers\n//\n// Each variant resolves its picker as `config?.picker ?? <default>` and\n// forwards the whole engine config as `pickerConfig`, so a rich picker\n// (`pickAudioTrack`) reads its options directly. Audio uses its primitive\n// picker as-is; video adapts `pickFirstTrackId` (positional `type` arg) into\n// the `TrackPicker` shape.\n// ============================================================================\n\n/** Default video picker: first track in the video selection set. */\nconst defaultVideoPicker: TrackPicker = (presentation) => pickFirstTrackId(presentation, 'video');\n\n// ============================================================================\n// Specialized exports — one per track type\n// ============================================================================\n\n/**\n * Config for `selectVideoTrack`. Pass `picker` to fully override selection\n * logic; otherwise the default `pickFirstTrackId` is used.\n */\nexport interface SelectVideoTrackConfig extends VideoSelectionConfig {\n picker?: TrackPicker<SelectVideoTrackConfig>;\n}\n\n/**\n * Select a video track when a presentation loads. Clears the selection on\n * src unload.\n *\n * This is the simple, non-ABR counterpart to `switchVideoTrack` — compose\n * one or the other, not both (both write `selectedVideoTrackId`). Composing\n * `selectVideoTrack` alone tree-shakes out the ABR code path\n * (bandwidth-estimator, quality-selection); use it for sources without\n * meaningful quality variants, test setups, or players that intentionally\n * pin a quality.\n *\n * @example\n * const reactor = selectVideoTrack.setup({ state });\n */\nexport const selectVideoTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedVideoTrackId'],\n contextKeys: [],\n setup: ({ state, config }: { state: SelectStateMap<'selectedVideoTrackId'>; config?: SelectVideoTrackConfig }) =>\n setupTrackSelection({\n state,\n config: {\n selectedKey: VIDEO_TYPE_CONFIG.selectedKey,\n picker: config?.picker ?? defaultVideoPicker,\n pickerConfig: config,\n },\n }),\n});\n\n/**\n * Config for `selectAudioTrack`. Pass `picker` to fully override selection\n * logic; otherwise the default `pickAudioTrack` is used (three-tier:\n * `preferredAudioLanguage` → `DEFAULT=YES` → first track).\n */\nexport interface SelectAudioTrackConfig extends AudioSelectionConfig {\n picker?: TrackPicker<SelectAudioTrackConfig>;\n}\n\n/**\n * Select an audio track when a presentation loads. Clears the selection\n * on src unload.\n *\n * This is the simple, lifecycle-only counterpart to `switchAudioTrack`\n * (in `./track-switching.ts`) — compose one or the other, not both\n * (both write `selectedAudioTrackId`). `switchAudioTrack` adds\n * filter-reactivity (`userAudioTrackSelection`) and mid-stream-flush\n * orchestration; `selectAudioTrack` covers the default-on-load case\n * without those. Use this variant for test setups, audio-only flows\n * that don't expose language switching, or composition variants that\n * intentionally pin a track.\n *\n * @example\n * const reactor = selectAudioTrack.setup({ state });\n *\n * @example\n * // Custom picker with language preference\n * const reactor = selectAudioTrack.setup({\n * state,\n * config: { preferredAudioLanguage: 'en', picker: myLanguageAwarePicker },\n * });\n */\nexport const selectAudioTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedAudioTrackId'],\n contextKeys: [],\n setup: ({ state, config }: { state: SelectStateMap<'selectedAudioTrackId'>; config?: SelectAudioTrackConfig }) =>\n setupTrackSelection({\n state,\n config: {\n selectedKey: AUDIO_TYPE_CONFIG.selectedKey,\n picker: config?.picker ?? pickAudioTrack,\n pickerConfig: config,\n },\n }),\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuEA,SAAS,oBAA8D,EACrE,OACA,QAAQ,EAAE,aAAa,QAAQ,kBAI9B;CACD,MAAM,qBAAqB,eACzB,uBAAuB,MAAM,aAAa,IAAI,CAAC,IAC1C,0BACA,yBACP;CAEA,OAAO,qBAAqB;EAC1B,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,2BAA2B,CAAC;GAC5B,yBAAyB,EAYvB,aAAa;IACX,IAAI,CAAC,MAAM,YAAY,CAAC,IAAI,GAAG;KAK7B,MAAM,KAAK,OAAO,MAAM,aAAa,IAAI,GAAI,YAAY;KACzD,IAAI,IAAI,MAAM,YAAY,CAAC,IAAI,EAAE;IACnC;IACA,aAAa,MAAM,YAAY,CAAC,IAAI,KAAA,CAAS;GAC/C,EACF;EACF;CACF,CAAC;AACH;;AAaA,MAAM,sBAAmC,iBAAiB,iBAAiB,cAAc,OAAO;;;;;;;;;;;;;;;AA4BhG,MAAa,mBAAmB,eAAe;CAC7C,WAAW,CAAC,gBAAgB,sBAAsB;CAClD,aAAa,CAAC;CACd,QAAQ,EAAE,OAAO,aACf,oBAAoB;EAClB;EACA,QAAQ;GACN,aAAa,kBAAkB;GAC/B,QAAQ,QAAQ,UAAU;GAC1B,cAAc;EAChB;CACF,CAAC;AACL,CAAC;AAkC+B,eAAe;CAC7C,WAAW,CAAC,gBAAgB,sBAAsB;CAClD,aAAa,CAAC;CACd,QAAQ,EAAE,OAAO,aACf,oBAAoB;EAClB;EACA,QAAQ;GACN,aAAa,kBAAkB;GAC/B,QAAQ,QAAQ,UAAU;GAC1B,cAAc;EAChB;CACF,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"select-tracks.js","names":[],"sources":["../../../../src/playback/behaviors/select-tracks.ts"],"sourcesContent":["/**\n * **Default audio/video track selection on src load / unselect on src unload.**\n * When a presentation is resolved, sets `selectedVideoTrackId` /\n * `selectedAudioTrackId` from a per-type default rule chain if no selection\n * already exists. When the presentation is unset/reset (transitions back to unresolved),\n * clears the selection so a stale id from the previous source doesn't persist.\n *\n * Lifecycle-driven: the pick fires once per transition, and nothing re-picks —\n * that is what separates these from the `switch*` variants. External writes (user\n * picks, ABR, programmatic filter-driven re-picks) are left alone, including a\n * write naming a track the manifest never offered.\n *\n * The one thing policed between transitions is a pick the *constraints* turn\n * against: a rendition's container and encryption are only known once its media\n * playlist resolves, which is after the pick was made, so a selection that becomes\n * unplayable is dropped. Dropped, never moved — re-picking is exactly the behavior\n * `switchVideoTrack` exists to provide. Dropping reports nothing on its own, since\n * whatever made the pick unplayable already reported its own, more specific cause.\n *\n * Selection runs the same rule model `switchVideoTrack` does — a hard\n * `constraints` pre-pass, then an ordered `rules` chain, with the pick as the\n * head (see `internal/design/spf/track-switching-model.md`). What differs is\n * reactivity, not the rules: this evaluates the chain once on resolve and pins\n * the result, where `switchVideoTrack` re-evaluates inside an effect so its rules\n * subscribe to bandwidth and user selection. A rule written for one therefore\n * composes into the other unchanged.\n *\n * Both are config-driven, each per-type export wiring a sensible default: audio's\n * three-tier language policy, and for video the *empty* chain — with nothing\n * narrowing or reordering, the head is the first candidate. The behavior's\n * `config` is forwarded to the rules, so options like `preferredAudioLanguage`\n * reach them without an intermediate layer.\n *\n * Note a rule can only pick among real candidates, where the picker it replaced\n * could return any id at all. An id absent from the manifest was never\n * selectable, so that narrowing is the point rather than a limitation.\n *\n * Compose `selectVideoTrack` for the simple \"pick a default video track\"\n * behavior, or `switchVideoTrack` (`./track-switching.ts`) for the\n * ABR-driven variant. Compose `selectAudioTrack` for the simple default\n * pick, or `switchAudioTrack` (`./track-switching.ts`) for the\n * filter-reactive + mid-stream-flush slot-owner variant — when audio-abr\n * lands, `switchAudioTrack` extends into `switchAudioQuality`. Compose\n * only one per type — they're alternatives, not stackable (each writes\n * the same `selected*TrackId` slot). The simple variants tree-shake out\n * the heavier machinery (bandwidth estimator, quality selection, flush\n * orchestration).\n *\n * Text selection has no simple variant here — it's owned by `switchTextTrack`\n * (`./track-switching.ts`), which resolves standing `userTextTrackSelection`\n * intent against the constrained, CDN-scoped renditions.\n */\n\nimport { defineBehavior } from '../../core/composition/create-composition';\nimport { createMachineReactor } from '../../core/reactors/create-machine-reactor';\nimport { computed, peek, type ReadonlySignal, type Signal } from '../../core/signals/primitives';\nimport {\n type AudioSelectionConfig,\n byDescendingResolution,\n pickAudioTrackFromTracks,\n type TrackSelectionState,\n tracksUnderPixelArea,\n} from '../../media/primitives/select-tracks';\nimport { isResolvedPresentation, type TrackType } from '../../media/types';\nimport { getTracksByType } from '../../media/utils/tracks';\nimport {\n applyConstraints,\n applyRules,\n type CapabilityConstraintConfig,\n excludeUnplayableTracks,\n type SelectionRule,\n sameCandidateSet,\n} from '../primitives/selection-rules';\nimport { AUDIO_TYPE_CONFIG, VIDEO_TYPE_CONFIG } from '../primitives/track-types';\n\n// ============================================================================\n// Specialization helper\n//\n// `setupTrackSelection` has the same shape as a Behavior `setup` function:\n// `({ state, config }) => Reactor`. Each `selectXTrack` export below calls\n// it from inside its own `defineBehavior` setup, supplying its per-type\n// `selectedKey`, track type, default rule chain, and forwarded config. The lifecycle\n// — pick on entering 'presentation-resolved' if not already selected; clear\n// on entering 'presentation-unresolved' — is shared.\n// ============================================================================\n\ntype SelectedTrackKey = 'selectedVideoTrackId' | 'selectedAudioTrackId';\n\ntype SelectStateMap<K extends SelectedTrackKey> = {\n presentation: ReadonlySignal<TrackSelectionState['presentation']>;\n} & { [P in K]: Signal<TrackSelectionState[P]> };\n\n/** A selection rule over this behavior's candidate tracks. */\nexport type SelectTrackRule<Config> = SelectionRule<SelectableTrack, unknown, unknown, Config | undefined>;\n\n/** What a rule here needs off a candidate: the id it may become the pick by. */\ntype SelectableTrack = { id: string };\n\ninterface TrackSelectionSetupConfig<K extends SelectedTrackKey, RuleConfig> {\n selectedKey: K;\n trackType: TrackType;\n constraints: readonly SelectTrackRule<RuleConfig>[];\n rules: readonly SelectTrackRule<RuleConfig>[];\n ruleConfig?: RuleConfig;\n}\n\nfunction setupTrackSelection<K extends SelectedTrackKey, RuleConfig>({\n state,\n config: { selectedKey, trackType, constraints, rules, ruleConfig },\n}: {\n state: SelectStateMap<K>;\n config: TrackSelectionSetupConfig<K, RuleConfig>;\n}) {\n const derivedStateSignal = computed(() =>\n isResolvedPresentation(state.presentation.get())\n ? ('presentation-resolved' as const)\n : ('presentation-unresolved' as const)\n );\n\n const deps = { state, config: ruleConfig };\n\n // The playable candidate set — the type's tracks after the hard-constraints\n // pre-pass. A `computed` so it re-evaluates when its inputs change, which is what\n // lets a *pinned* selection still notice it has gone unplayable: `resolve-track`\n // relabels the whole type's container from the first resolved media playlist,\n // long after `entry` made its pick under the fMP4 default.\n //\n // The `equals` gates notification on the set of track ids rather than array\n // identity, matching `setupTrackSwitching`'s. Segment appends and live reloads\n // both swap in a new presentation object carrying the same variants; without\n // this the effect below would re-run on every one of them.\n const candidateSet = computed(\n () => {\n const presentation = state.presentation.get();\n if (!isResolvedPresentation(presentation)) return [];\n return applyConstraints(constraints, getTracksByType(presentation, trackType), deps);\n },\n { equals: sameCandidateSet }\n );\n\n return createMachineReactor({\n initial: 'presentation-unresolved',\n monitor: () => derivedStateSignal.get(),\n states: {\n 'presentation-unresolved': {},\n 'presentation-resolved': {\n // Entry: pick a default on entering presentation-resolved if none\n // is set. External writes (user picks, ABR) that already populated\n // the slot are left alone.\n //\n // The returned cleanup runs on state exit — which fires on src\n // unload (presentation-resolved → presentation-unresolved) AND on\n // behavior destroy (presentation-resolved → destroying →\n // destroyed). Putting the clear here rather than as\n // presentation-unresolved.entry is more cohesive (operation +\n // cleanup co-located) and correctly covers destroy (destroy\n // doesn't pass through presentation-unresolved).\n entry: () => {\n if (!state[selectedKey].get()) {\n // `state.presentation.get()` is non-null inside this entry —\n // the reactor's `'presentation-resolved'` gate is exactly\n // `isResolvedPresentation(state.presentation.get())`, which\n // requires a truthy Presentation.\n //\n // Constraints prune the unplayable, then the chain narrows and ranks;\n // the pick is the head. An empty `rules` chain therefore selects the\n // first candidate, which falls out of the model rather than needing a\n // first-track code path of its own.\n const survivors = applyRules(rules, peek(candidateSet), deps);\n const id = survivors[0]?.id;\n if (id) state[selectedKey].set(id);\n }\n return () => state[selectedKey].set(undefined);\n },\n effects: [\n // Selection is `entry`'s alone — this only ever *de*selects. Keeping the\n // pick out of a reaction is what makes this the pinned variant rather\n // than a worse-spelled `switchVideoTrack`: nothing here re-ranks or moves\n // the pin. It has to be a reaction all the same, because what it watches\n // for is learned late — container and encryption come from a rendition's\n // media playlist, which resolves after the pick was made.\n //\n // Deliberately does *not* report why. Whatever made the pick unplayable\n // reported its own cause as the playlist resolved (1004 container, 4008\n // encryption, via `reportUnsupportedTrackConditions`), which is both more\n // specific than a verdict and already logged. The one condition no cause\n // covers is nothing being playable at all — no rendition resolved, so\n // none reported — which is why that alone emits here. See\n // `internal/design/spf/features/errors.md`.\n () => {\n // Untracked: writing the slot below must not re-enter this reaction.\n const selectedId = peek(state[selectedKey]);\n if (!selectedId) return;\n if (candidateSet.get().some((track) => track.id === selectedId)) return;\n\n // Only a pick the source actually offers is this behavior's to drop. An\n // id absent from the manifest was never selectable, and external writes\n // are left alone — see this module's header.\n const presentation = peek(state.presentation);\n if (\n isResolvedPresentation(presentation) &&\n getTracksByType(presentation, trackType).some((track) => track.id === selectedId)\n ) {\n state[selectedKey].set(undefined);\n }\n },\n ],\n },\n },\n });\n}\n\n// ============================================================================\n// Default rules\n//\n// Each variant resolves its chain as `config?.rules ?? <default>`. The whole\n// behavior config is forwarded as the rules' `config`, so a policy rule reads\n// its own options (`preferredAudioLanguage`) directly off it.\n//\n// Video's default is the *empty* chain: with no rule narrowing or reordering the\n// candidates, the head is the first track — a consequence of the model rather than\n// a first-track code path of its own.\n// ============================================================================\n\n/** Default video chain: none. The first candidate is the pick. */\nconst DEFAULT_VIDEO_RULES: readonly SelectTrackRule<SelectVideoTrackConfig>[] = [];\n\n/**\n * Default audio chain: the three-tier policy (`preferredAudioLanguage` →\n * `DEFAULT=YES` → first) as a single narrowing rule. Returning `[]` when nothing\n * is picked lets `applyRules` fall through to the unnarrowed candidates, so the\n * head stays the first track — the same last tier the policy itself ends on.\n */\nconst preferAudioPolicy: SelectTrackRule<SelectAudioTrackConfig> = (tracks, { config }) => {\n const id = pickAudioTrackFromTracks(tracks as readonly { id: string }[], config);\n const pick = tracks.find((track) => track.id === id);\n return pick ? [pick] : [];\n};\n\nconst DEFAULT_AUDIO_RULES: readonly SelectTrackRule<SelectAudioTrackConfig>[] = [preferAudioPolicy];\n\n/**\n * Order the candidates by resolution, largest first, with bandwidth breaking ties\n * between renditions of identical dimensions. The background-video default — that\n * variant pins one rendition for the session, and absent a cap the largest is the\n * head.\n *\n * A ranker, so it reorders rather than narrowing: the chain's pick is the head of\n * what it returns, which means ranking never has to collapse to one track. Belongs\n * last in a chain — a sort only reorders what survived the filters ahead of it, and\n * leaving it last is what lets `applyRules` early-bail before it runs.\n *\n * Exported because it is a *rule*, not a variant's private policy: the same one\n * composes into `switchVideoTrack`'s chain when a ranker is wanted there.\n */\nexport const preferHighestResolution: SelectTrackRule<unknown> = (tracks) => [...tracks].sort(byDescendingResolution);\n\n/**\n * What {@link screenResolutionCap} reads off the composition state.\n *\n * Structurally compatible with `media/dom/screen`'s `ScreenResolution` rather than\n * importing it: this module sits outside the DOM layer (project references enforce\n * that), and a rule comparing pixel areas needs two numbers, not a screen.\n */\ntype ScreenResolutionRuleState = {\n screenResolution?: ReadonlySignal<{ readonly width: number; readonly height: number } | undefined>;\n};\n\n/**\n * Narrow to the renditions that fit the screen, by pixel area — the screen-size\n * cap from `internal/design/spf/features/rendition-selection-caps.md`, as a scope\n * (soft filter) rather than a constraint: an over-cap rendition is wasteful, not\n * unplayable, so nothing here may make a source unplayable.\n *\n * Narrows only — it neither orders the survivors nor resolves the case where none\n * survive, because `applyRules` owns both. So it needs a ranker behind it to pick\n * within the cap: `[screenResolutionCap, preferHighestResolution]` yields the\n * largest rendition that fits. Composed *last*, the pick would instead be whichever\n * fitting rendition the manifest happened to list first.\n *\n * Reading `state.screenResolution` through its signal is what subscribes a\n * re-evaluating chain (`switchVideoTrack`) to screen changes; `selectVideoTrack`\n * pins the first answer instead, by design.\n *\n * Compares areas rather than matching a `\"1080p\"`-style tier because a tier only\n * describes a rendition once you assume its aspect ratio — the assumption that\n * mis-measures an anamorphic ladder. See `media/dom/screen.ts`.\n *\n * Three ways the cap ends up not applying, all of them fall-through:\n *\n * - **No `screenResolution` signal at all**, because the composition omits\n * `trackScreenResolution`. So composing the cap without its signal source is\n * inert rather than broken.\n * - **A `screenResolution` of `undefined`**, meaning no screen to read. \"Unknown\"\n * has to mean \"don't cap\": treating it as an area of zero would pin every source\n * to its smallest rendition on exactly the environments we know least about.\n * - **No rendition fits**, on a screen smaller than the whole ladder. `applyRules`\n * skips the empty result and the chain proceeds unnarrowed, so the ranker behind\n * the cap decides — for `preferHighestResolution`, the largest rendition. A floor\n * is the fix if that ever matters (`rendition-selection-caps.md` carries one), not\n * a special case here.\n */\nexport const screenResolutionCap: SelectTrackRule<unknown> = (tracks, { state }) => {\n const screenResolution = (state as ScreenResolutionRuleState | undefined)?.screenResolution?.get();\n if (!screenResolution) return [];\n\n return tracksUnderPixelArea(tracks, screenResolution.width * screenResolution.height);\n};\n\n// ============================================================================\n// Specialized exports — one per track type\n// ============================================================================\n\n/**\n * Config for `selectVideoTrack`. Pass `rules` to replace the selection chain, or\n * `constraints` to replace the capability pre-pass; otherwise the chain is empty\n * and the first playable candidate is the pick.\n */\nexport interface SelectVideoTrackConfig extends CapabilityConstraintConfig {\n constraints?: readonly SelectTrackRule<SelectVideoTrackConfig>[];\n rules?: readonly SelectTrackRule<SelectVideoTrackConfig>[];\n}\n\n/**\n * Default video constraints: the capability pre-pass alone. No\n * `excludeFailedCdns` — this variant's compositions run no failover monitor, so\n * `failedCdns` has no writer and the constraint would always pass through.\n */\nconst DEFAULT_VIDEO_CONSTRAINTS: readonly SelectTrackRule<SelectVideoTrackConfig>[] = [excludeUnplayableTracks];\n\n/**\n * Select a video track when a presentation loads. Clears the selection on\n * src unload.\n *\n * This is the simple, non-ABR counterpart to `switchVideoTrack` — compose\n * one or the other, not both (both write `selectedVideoTrackId`). Composing\n * `selectVideoTrack` alone tree-shakes out the ABR code path\n * (bandwidth-estimator, quality-selection); use it for sources without\n * meaningful quality variants, test setups, or players that intentionally\n * pin a quality.\n *\n * @example\n * const reactor = selectVideoTrack.setup({ state });\n */\nexport const selectVideoTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedVideoTrackId'],\n contextKeys: [],\n setup: ({ state, config }: { state: SelectStateMap<'selectedVideoTrackId'>; config?: SelectVideoTrackConfig }) =>\n setupTrackSelection({\n state,\n config: {\n selectedKey: VIDEO_TYPE_CONFIG.selectedKey,\n trackType: 'video',\n constraints: config?.constraints ?? DEFAULT_VIDEO_CONSTRAINTS,\n rules: config?.rules ?? DEFAULT_VIDEO_RULES,\n ruleConfig: config,\n },\n }),\n});\n\n/**\n * Config for `selectAudioTrack`. Pass `rules` to replace the selection chain, or\n * `constraints` to prune candidates before it runs; otherwise the default\n * three-tier policy applies (`preferredAudioLanguage` → `DEFAULT=YES` → first).\n */\nexport interface SelectAudioTrackConfig extends AudioSelectionConfig {\n constraints?: readonly SelectTrackRule<SelectAudioTrackConfig>[];\n rules?: readonly SelectTrackRule<SelectAudioTrackConfig>[];\n}\n\n/**\n * Select an audio track when a presentation loads. Clears the selection\n * on src unload.\n *\n * This is the simple, lifecycle-only counterpart to `switchAudioTrack`\n * (in `./track-switching.ts`) — compose one or the other, not both\n * (both write `selectedAudioTrackId`). `switchAudioTrack` adds\n * filter-reactivity (`userAudioTrackSelection`) and mid-stream-flush\n * orchestration; `selectAudioTrack` covers the default-on-load case\n * without those. Use this variant for test setups, audio-only flows\n * that don't expose language switching, or composition variants that\n * intentionally pin a track.\n *\n * @example\n * const reactor = selectAudioTrack.setup({ state });\n *\n * @example\n * // Language preference, honored by the default audio policy rule\n * const reactor = selectAudioTrack.setup({\n * state,\n * config: { preferredAudioLanguage: 'en' },\n * });\n */\nexport const selectAudioTrack = defineBehavior({\n stateKeys: ['presentation', 'selectedAudioTrackId'],\n contextKeys: [],\n setup: ({ state, config }: { state: SelectStateMap<'selectedAudioTrackId'>; config?: SelectAudioTrackConfig }) =>\n setupTrackSelection({\n state,\n config: {\n selectedKey: AUDIO_TYPE_CONFIG.selectedKey,\n trackType: 'audio',\n constraints: config?.constraints ?? [],\n rules: config?.rules ?? DEFAULT_AUDIO_RULES,\n ruleConfig: config,\n },\n }),\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0GA,SAAS,oBAA4D,EACnE,OACA,QAAQ,EAAE,aAAa,WAAW,aAAa,OAAO,gBAIrD;CACD,MAAM,qBAAqB,eACzB,uBAAuB,MAAM,aAAa,IAAI,CAAC,IAC1C,0BACA,yBACP;CAEA,MAAM,OAAO;EAAE;EAAO,QAAQ;CAAW;CAYzC,MAAM,eAAe,eACb;EACJ,MAAM,eAAe,MAAM,aAAa,IAAI;EAC5C,IAAI,CAAC,uBAAuB,YAAY,GAAG,OAAO,CAAC;EACnD,OAAO,iBAAiB,aAAa,gBAAgB,cAAc,SAAS,GAAG,IAAI;CACrF,GACA,EAAE,QAAQ,iBAAiB,CAC7B;CAEA,OAAO,qBAAqB;EAC1B,SAAS;EACT,eAAe,mBAAmB,IAAI;EACtC,QAAQ;GACN,2BAA2B,CAAC;GAC5B,yBAAyB;IAYvB,aAAa;KACX,IAAI,CAAC,MAAM,YAAY,CAAC,IAAI,GAAG;MAW7B,MAAM,KADY,WAAW,OAAO,KAAK,YAAY,GAAG,IACrC,CAAC,CAAC,EAAE,EAAE;MACzB,IAAI,IAAI,MAAM,YAAY,CAAC,IAAI,EAAE;KACnC;KACA,aAAa,MAAM,YAAY,CAAC,IAAI,KAAA,CAAS;IAC/C;IACA,SAAS,OAeD;KAEJ,MAAM,aAAa,KAAK,MAAM,YAAY;KAC1C,IAAI,CAAC,YAAY;KACjB,IAAI,aAAa,IAAI,CAAC,CAAC,MAAM,UAAU,MAAM,OAAO,UAAU,GAAG;KAKjE,MAAM,eAAe,KAAK,MAAM,YAAY;KAC5C,IACE,uBAAuB,YAAY,KACnC,gBAAgB,cAAc,SAAS,CAAC,CAAC,MAAM,UAAU,MAAM,OAAO,UAAU,GAEhF,MAAM,YAAY,CAAC,IAAI,KAAA,CAAS;IAEpC,CACF;GACF;EACF;CACF,CAAC;AACH;;AAeA,MAAM,sBAA0E,CAAC;;;;;;;AAQjF,MAAM,qBAA8D,QAAQ,EAAE,aAAa;CACzF,MAAM,KAAK,yBAAyB,QAAqC,MAAM;CAC/E,MAAM,OAAO,OAAO,MAAM,UAAU,MAAM,OAAO,EAAE;CACnD,OAAO,OAAO,CAAC,IAAI,IAAI,CAAC;AAC1B;AAEA,MAAM,sBAA0E,CAAC,iBAAiB;;;;;;;;;;;;;;;AAgBlG,MAAa,2BAAqD,WAAW,CAAC,GAAG,MAAM,CAAC,CAAC,KAAK,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CpH,MAAa,uBAAiD,QAAQ,EAAE,YAAY;CAClF,MAAM,mBAAoB,OAAiD,kBAAkB,IAAI;CACjG,IAAI,CAAC,kBAAkB,OAAO,CAAC;CAE/B,OAAO,qBAAqB,QAAQ,iBAAiB,QAAQ,iBAAiB,MAAM;AACtF;;;;;;AAqBA,MAAM,4BAAgF,CAAC,uBAAuB;;;;;;;;;;;;;;;AAgB9G,MAAa,mBAAmB,eAAe;CAC7C,WAAW,CAAC,gBAAgB,sBAAsB;CAClD,aAAa,CAAC;CACd,QAAQ,EAAE,OAAO,aACf,oBAAoB;EAClB;EACA,QAAQ;GACN,aAAa,kBAAkB;GAC/B,WAAW;GACX,aAAa,QAAQ,eAAe;GACpC,OAAO,QAAQ,SAAS;GACxB,YAAY;EACd;CACF,CAAC;AACL,CAAC;AAmC+B,eAAe;CAC7C,WAAW,CAAC,gBAAgB,sBAAsB;CAClD,aAAa,CAAC;CACd,QAAQ,EAAE,OAAO,aACf,oBAAoB;EAClB;EACA,QAAQ;GACN,aAAa,kBAAkB;GAC/B,WAAW;GACX,aAAa,QAAQ,eAAe,CAAC;GACrC,OAAO,QAAQ,SAAS;GACxB,YAAY;EACd;CACF,CAAC;AACL,CAAC"}