@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selection-rules.js","names":[],"sources":["../../../../src/playback/primitives/selection-rules.ts"],"sourcesContent":["/**\n * The selection-rule substrate: the shape of a rule, and the two composers that\n * turn a list of them into a pick.\n *\n * Lives here rather than beside `switchVideoTrack` so both track-selection\n * behaviors can share rules. The simple `selectVideoTrack` variant exists\n * specifically to tree-shake the ABR path out, so importing a composer from\n * `behaviors/track-switching.ts` would drag the bandwidth estimator and quality\n * selection back in with it. These four have no dependencies at all — pure\n * generics over a candidate list — so either side can reach them freely.\n *\n * See `internal/design/spf/track-switching-model.md` for the model these\n * implement: a hard constraints pre-pass, then an ordered chain of soft\n * narrowing rules and rankers, with the pick as the first survivor.\n */\n\nimport type { CanPlayTrack } from '../../media/types';\n\n/**\n * Deps handed to each rule and to `applyRules`, mirroring a behavior's setup\n * deps so a rule reads from the same surfaces a behavior does. `context` is\n * optional — it's threaded through but absent on direct setup calls (and\n * unread by today's rules), so the whole deps object can pass straight through.\n */\nexport interface SelectionRuleDeps<State = unknown, Context = unknown, Config = unknown> {\n state: State;\n context?: Context;\n config: Config;\n}\n\n/**\n * A selection rule narrows or reorders the candidate list. It reads the state,\n * context, and config it needs at apply time (tightly-coupled reads), so a\n * rule's `.get()`s subscribe the running effect to exactly what it consulted.\n * Returning an empty list means \"no match\" — the composer skips it, so a soft\n * filter never narrows the set to nothing. A ranker returns the list with its\n * pick at the head.\n */\nexport type SelectionRule<T, State = unknown, Context = unknown, Config = unknown> = (\n tracks: readonly T[],\n deps: SelectionRuleDeps<State, Context, Config>\n) => readonly T[];\n\n/**\n * Apply rules to a candidate list in order; the pick is the first survivor.\n * Two responsibilities the rules don't carry: a rule that returns nothing is\n * skipped (fall-through — a preference never empties the set), and once one\n * survivor remains the chain stops (early-bail — later rules, including the\n * bandwidth ranker, never run, so the effect doesn't subscribe to their\n * signals while the choice is fixed).\n *\n * @param rules - Rules to apply, most authoritative first\n * @param tracks - Candidate tracks\n * @param deps - The behavior's `{ state, context, config }`, passed through to each rule\n * @returns The surviving candidates, pick first\n */\nexport function applyRules<T, State, Context, Config>(\n rules: readonly SelectionRule<T, State, Context, Config>[],\n tracks: readonly T[],\n deps: SelectionRuleDeps<State, Context, Config>\n): readonly T[] {\n let current = tracks;\n for (const rule of rules) {\n const remaining = rule(current, deps);\n if (remaining.length === 0) continue;\n current = remaining;\n if (current.length === 1) break;\n }\n return current;\n}\n\n/**\n * Apply hard constraints to a candidate list — the pre-pass that runs before the\n * rule chain. A constraint shares a rule's signature but its exclusion is\n * *hard*: it removes the unplayable (a codec the environment can't decode, a CDN\n * in failover cooldown) and a removed track is never attempted. Unlike\n * `applyRules`, this never skips an empty result and never early-bails — every\n * constraint always applies, and an empty survivor set is a real outcome\n * (\"nothing playable here\"), not a fall-through. Because each constraint only\n * removes, the order they run in can't change the result.\n *\n * @param constraints - Constraints to apply (pooled, order-independent)\n * @param tracks - Candidate tracks\n * @param deps - The behavior's `{ state, context, config }`, passed to each constraint\n * @returns The playable survivors (possibly empty)\n */\nexport function applyConstraints<T, State, Context, Config>(\n constraints: readonly SelectionRule<T, State, Context, Config>[],\n tracks: readonly T[],\n deps: SelectionRuleDeps<State, Context, Config>\n): readonly T[] {\n let current = tracks;\n for (const constraint of constraints) current = constraint(current, deps);\n return current;\n}\n\n/**\n * Whether two candidate sets hold the same tracks, by id.\n *\n * The `equals` both selection behaviors give their candidate-set `computed`. A\n * live playlist refresh swaps in a new presentation object carrying the same\n * variants, and a constraint's own inputs can churn without changing which\n * tracks survive; in both cases the set is unchanged and the reaction must not\n * re-fire. Compares by id rather than array identity for exactly that.\n */\nexport function sameCandidateSet<T extends { id: string }>(a: readonly T[], b: readonly T[]): boolean {\n return a.length === b.length && a.every((track) => b.some((other) => other.id === track.id));\n}\n\n/**\n * What {@link excludeUnplayableTracks} reads off the config it is handed.\n *\n * Read through a cast rather than constraining the rule's `Config` generic, the\n * same way `screenResolutionCap` reads `screenResolution` off its state: a rule\n * composes into chains whose config types have nothing else in common, and\n * constraining the generic would make every one of those a weak-type mismatch.\n * Each engine defaults `canPlayTrack` to the DOM-bound probe; unwired means \"no\n * capability filtering\" and the constraint passes everything through.\n */\nexport interface CapabilityConstraintConfig {\n canPlayTrack?: CanPlayTrack;\n}\n\n/**\n * Capability constraint — a *hard* filter for the {@link applyConstraints}\n * pre-pass. Removes renditions this environment can't decode, probed via the\n * injected `canPlayTrack` (codec → `MediaSource.isTypeSupported`, plus the\n * container and encryption assertions that probe can't make). Constraining here\n * — before selection — means an unplayable variant is pruned upstream and never\n * picked, instead of surviving into the pipeline to fail late at\n * `createSourceBuffer`. That late throw stays as a defensive structural\n * guarantee; with this constraint it should rarely fire.\n *\n * Lives here rather than beside `switchVideoTrack` for the reason this module\n * exists: both the re-evaluating variant and the pinned `selectVideoTrack` apply\n * it, and reaching it through `behaviors/track-switching.ts` would drag the ABR\n * path into a composition that deliberately omits it.\n *\n * Passes everything through when there's no probe (a composition that didn't\n * wire one, or DOM-free tests). When it prunes *every* track, the empty result is\n * preserved (per `applyConstraints`) — \"nothing playable\" — which each consuming\n * behavior answers by clearing its selection and reporting the type's verdict.\n */\nexport function excludeUnplayableTracks<T, State, Context, Config>(\n tracks: readonly T[],\n { config }: SelectionRuleDeps<State, Context, Config>\n): readonly T[] {\n const canPlay = (config as CapabilityConstraintConfig | undefined)?.canPlayTrack;\n if (!canPlay) return tracks;\n return tracks.filter((track) => canPlay(track as Parameters<CanPlayTrack>[0]));\n}\n"],"mappings":";;;;;;;;;;;;;;AAwDA,SAAgB,WACd,OACA,QACA,MACc;CACd,IAAI,UAAU;CACd,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,YAAY,KAAK,SAAS,IAAI;EACpC,IAAI,UAAU,WAAW,GAAG;EAC5B,UAAU;EACV,IAAI,QAAQ,WAAW,GAAG;CAC5B;CACA,OAAO;AACT;;;;;;;;;;;;;;;;AAiBA,SAAgB,iBACd,aACA,QACA,MACc;CACd,IAAI,UAAU;CACd,KAAK,MAAM,cAAc,aAAa,UAAU,WAAW,SAAS,IAAI;CACxE,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,iBAA2C,GAAiB,GAA0B;CACpG,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,UAAU,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM,EAAE,CAAC;AAC7F;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,wBACd,QACA,EAAE,UACY;CACd,MAAM,UAAW,QAAmD;CACpE,IAAI,CAAC,SAAS,OAAO;CACrB,OAAO,OAAO,QAAQ,UAAU,QAAQ,KAAoC,CAAC;AAC/E"}
@@ -1,3 +1,4 @@
1
+ import { HlsVideoMediaError } from "./playback/adapters/hls-video/error-surface.js";
1
2
  import { HlsBackgroundVideoMediaAPI, HlsBackgroundVideoMediaElement, HlsBackgroundVideoMediaMixin, HlsBackgroundVideoMediaProps, hlsBackgroundVideoMediaDefaultProps } from "./playback/adapters/hls-background-video/adapter.js";
2
3
  import { HlsBackgroundVideoMedia } from "./playback/adapters/hls-background-video/media.js";
3
- export { HlsBackgroundVideoMedia, type HlsBackgroundVideoMediaAPI, HlsBackgroundVideoMediaElement, HlsBackgroundVideoMediaMixin, type HlsBackgroundVideoMediaProps, hlsBackgroundVideoMediaDefaultProps };
4
+ export { HlsBackgroundVideoMedia, type HlsBackgroundVideoMediaAPI, HlsBackgroundVideoMediaElement, HlsBackgroundVideoMediaMixin, type HlsBackgroundVideoMediaProps, type HlsVideoMediaError, hlsBackgroundVideoMediaDefaultProps };
package/dist/dev/hls.d.ts CHANGED
@@ -3,6 +3,7 @@ import { SVTA_UNSUPPORTED_PLAYBACK_FEATURE, SvtaError, svtaCategory, svtaIndex }
3
3
  import { DeriveStartMediaTime } from "./playback/primitives/derive-start-media-time.js";
4
4
  import { derivePerTypeStartMediaTime, deriveSharedMinStartMediaTime } from "./playback/behaviors/establish-start-media-time.js";
5
5
  import { HlsAudioEngineConfig, HlsAudioEngineContext, HlsAudioEngineSignals, HlsAudioEngineState, createHlsAudioEngine } from "./playback/engines/hls/engine-audio-only.js";
6
+ import { SelectTrackRule, preferHighestResolution, screenResolutionCap } from "./playback/behaviors/select-tracks.js";
6
7
  import { BackgroundVideoEngineConfig, BackgroundVideoEngineContext, BackgroundVideoEngineSignals, BackgroundVideoEngineState, createBackgroundVideoEngine } from "./playback/engines/hls/engine-background-video.js";
7
8
  import { HlsVideoEngineConfig, HlsVideoEngineContext, HlsVideoEngineSignals, HlsVideoEngineState, createHlsVideoEngine } from "./playback/engines/hls/engine.js";
8
- export { type BackgroundVideoEngineConfig, type BackgroundVideoEngineContext, type BackgroundVideoEngineSignals, type BackgroundVideoEngineState, type DeriveStartMediaTime, type HlsAudioEngineConfig, type HlsAudioEngineContext, type HlsAudioEngineSignals, type HlsAudioEngineState, type HlsVideoEngineConfig, type HlsVideoEngineContext, type HlsVideoEngineSignals, type HlsVideoEngineState, type MediaPlaylistMetadata, SVTA_UNSUPPORTED_PLAYBACK_FEATURE, type SvtaError, createBackgroundVideoEngine, createHlsAudioEngine, createHlsVideoEngine, derivePerTypeStartMediaTime, deriveSharedMinStartMediaTime, getMediaPlaylistMetadata, svtaCategory, svtaIndex };
9
+ export { type BackgroundVideoEngineConfig, type BackgroundVideoEngineContext, type BackgroundVideoEngineSignals, type BackgroundVideoEngineState, type DeriveStartMediaTime, type HlsAudioEngineConfig, type HlsAudioEngineContext, type HlsAudioEngineSignals, type HlsAudioEngineState, type HlsVideoEngineConfig, type HlsVideoEngineContext, type HlsVideoEngineSignals, type HlsVideoEngineState, type MediaPlaylistMetadata, SVTA_UNSUPPORTED_PLAYBACK_FEATURE, type SelectTrackRule, type SvtaError, createBackgroundVideoEngine, createHlsAudioEngine, createHlsVideoEngine, derivePerTypeStartMediaTime, deriveSharedMinStartMediaTime, getMediaPlaylistMetadata, preferHighestResolution, screenResolutionCap, svtaCategory, svtaIndex };
package/dist/dev/hls.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { getMediaPlaylistMetadata } from "./media/types/index.js";
2
2
  import { SVTA_UNSUPPORTED_PLAYBACK_FEATURE, svtaCategory, svtaIndex } from "./media/errors.js";
3
3
  import { derivePerTypeStartMediaTime, deriveSharedMinStartMediaTime } from "./playback/behaviors/establish-start-media-time.js";
4
+ import { preferHighestResolution, screenResolutionCap } from "./playback/behaviors/select-tracks.js";
4
5
  import { createHlsVideoEngine } from "./playback/engines/hls/engine.js";
5
6
  import { createHlsAudioEngine } from "./playback/engines/hls/engine-audio-only.js";
6
7
  import { createBackgroundVideoEngine } from "./playback/engines/hls/engine-background-video.js";
7
- export { SVTA_UNSUPPORTED_PLAYBACK_FEATURE, createBackgroundVideoEngine, createHlsAudioEngine, createHlsVideoEngine, derivePerTypeStartMediaTime, deriveSharedMinStartMediaTime, getMediaPlaylistMetadata, svtaCategory, svtaIndex };
8
+ export { SVTA_UNSUPPORTED_PLAYBACK_FEATURE, createBackgroundVideoEngine, createHlsAudioEngine, createHlsVideoEngine, derivePerTypeStartMediaTime, deriveSharedMinStartMediaTime, getMediaPlaylistMetadata, preferHighestResolution, screenResolutionCap, svtaCategory, svtaIndex };
@@ -0,0 +1,26 @@
1
+ //#region src/media/dom/screen.d.ts
2
+ /**
3
+ * Screen resolution, as the signal source for a screen-size rendition cap.
4
+ *
5
+ * Reported as a width and a height rather than a `"720p"`-style tier, because
6
+ * the cap that consumes it compares against real track dimensions. A tier only
7
+ * describes a track once you assume its aspect ratio, and that assumption
8
+ * mis-measures an anamorphic or otherwise non-16:9 rendition.
9
+ *
10
+ * The signal source for the screen-size cap in
11
+ * `internal/design/spf/features/rendition-selection-caps.md`.
12
+ *
13
+ * The screen underneath a window is not stable: rotating a device swaps the axes,
14
+ * and unplugging a monitor or dragging the window to another display changes the
15
+ * numbers *and* which physical screen they describe. So `getScreenResolution`
16
+ * reads at call time and caches nothing, and `watchScreenResolution` layers the
17
+ * reacting on top rather than the reader holding state of its own.
18
+ */
19
+ /** A screen's pixel dimensions. */
20
+ interface ScreenResolution {
21
+ readonly width: number;
22
+ readonly height: number;
23
+ }
24
+ //#endregion
25
+ export { ScreenResolution };
26
+ //# sourceMappingURL=screen.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screen.d.ts","names":[],"sources":["../../../../src/media/dom/screen.ts"],"mappings":";;;;;;;;;;;;;;;;;;;UAuBiB;WACN;WACA"}
@@ -0,0 +1,129 @@
1
+ import { listen } from "@videojs/utils/dom";
2
+ import { isFunction } from "@videojs/utils/predicate";
3
+ import { shallowEqual } from "@videojs/utils/object";
4
+ //#region src/media/dom/screen.ts
5
+ /**
6
+ * Screen resolution, as the signal source for a screen-size rendition cap.
7
+ *
8
+ * Reported as a width and a height rather than a `"720p"`-style tier, because
9
+ * the cap that consumes it compares against real track dimensions. A tier only
10
+ * describes a track once you assume its aspect ratio, and that assumption
11
+ * mis-measures an anamorphic or otherwise non-16:9 rendition.
12
+ *
13
+ * The signal source for the screen-size cap in
14
+ * `internal/design/spf/features/rendition-selection-caps.md`.
15
+ *
16
+ * The screen underneath a window is not stable: rotating a device swaps the axes,
17
+ * and unplugging a monitor or dragging the window to another display changes the
18
+ * numbers *and* which physical screen they describe. So `getScreenResolution`
19
+ * reads at call time and caches nothing, and `watchScreenResolution` layers the
20
+ * reacting on top rather than the reader holding state of its own.
21
+ */
22
+ /**
23
+ * Read the screen's resolution, or `undefined` where there isn't one to read.
24
+ *
25
+ * `undefined` means "unknown", which is the answer a cap needs in order to not
26
+ * cap. `screenResolutionCap` reads it that way and declines to narrow, so an
27
+ * unknown screen is "no cap" rather than a cap of zero — the reading a naive
28
+ * `?? 0` would produce, which would pin every source to its smallest rendition on
29
+ * exactly the environments we know least about.
30
+ *
31
+ * Dimensions are reported as-is, including the axis swap a rotated device
32
+ * applies to them. Normalizing orientation away is a policy question — whether a
33
+ * cap should flap on rotation, or hold the larger budget across both — and
34
+ * belongs to the cap rather than to the reading.
35
+ */
36
+ function getScreenResolution({ useDevicePixelRatio } = { useDevicePixelRatio: true }) {
37
+ const screen = globalThis.screen;
38
+ if (!screen) return void 0;
39
+ const ratio = useDevicePixelRatio ? globalThis.devicePixelRatio || 1 : 1;
40
+ const width = Math.round(screen.width * ratio);
41
+ const height = Math.round(screen.height * ratio);
42
+ return width > 0 && height > 0 ? {
43
+ width,
44
+ height
45
+ } : void 0;
46
+ }
47
+ /**
48
+ * Call `onChange` whenever {@link getScreenResolution} would start answering
49
+ * differently. Returns a function that stops watching.
50
+ *
51
+ * There is no single event for "the screen changed", so this subscribes to every
52
+ * signal that implies one and compares readings to decide whether anything
53
+ * actually moved. Comparing is what makes that safe: the signals overlap and
54
+ * `resize` in particular is noisy, so over-subscribing costs a discarded read
55
+ * rather than a spurious call.
56
+ *
57
+ * `onChange` is called once on subscribe with the starting value — including
58
+ * `undefined` where there is no screen — and after that only on a genuine change.
59
+ * So a consumer gets its initial state from the watcher and never has to pair it
60
+ * with a separate {@link getScreenResolution} call.
61
+ *
62
+ * The signals, and what each one is here for:
63
+ *
64
+ * - **`screen`'s own `change`** — the screen itself being reconfigured, or the
65
+ * window landing on a different one. The direct signal, and the only one that
66
+ * catches a window moving between two same-size, same-ratio displays. From the
67
+ * Window Management API, but on the base `Screen` rather than behind
68
+ * `getScreenDetails()`, so it needs no permission — only a secure context.
69
+ * Measured present in Chromium and absent in WebKit and Firefox, hence the
70
+ * three below rather than this alone.
71
+ * - **`resize`** — the window changing size, which is also what the OS does to it
72
+ * when the display it was on goes away.
73
+ * - **`screen.orientation` change** — rotation, which swaps the axes without
74
+ * necessarily resizing the window.
75
+ * - **a `(resolution: <ratio>dppx)` media query** — the device pixel ratio
76
+ * changing under a window that kept its size, which is the cross-display drag
77
+ * between displays of different density. Each query only answers about the ratio
78
+ * it was built for, so it reports one change and its handler arms the next.
79
+ *
80
+ * Worth keeping despite looking redundant, because it is the only coverage that
81
+ * case has in WebKit and Firefox: neither implements `screen`'s change event,
82
+ * and the drag doesn't resize the window. It is also a cleaner signal in Safari
83
+ * than elsewhere — WebKit holds `devicePixelRatio` independent of page zoom, so
84
+ * there it moves only on a real density change, where Chromium and Gecko fold
85
+ * zoom into it as well.
86
+ *
87
+ * ⚠️ Known gap, on engines without `screen`'s change event: dragging a window
88
+ * between two different-size displays that share a ratio, without the window
89
+ * resizing, changes the reading with nothing firing. Closing it there would mean
90
+ * polling, whose interval and battery cost are a policy decision this function
91
+ * shouldn't be making.
92
+ */
93
+ function watchScreenResolution(onChange, options = { useDevicePixelRatio: true }) {
94
+ const disconnect = new AbortController();
95
+ const { signal } = disconnect;
96
+ let current = getScreenResolution(options);
97
+ const check = () => {
98
+ const next = getScreenResolution(options);
99
+ if (shallowEqual(current, next)) return;
100
+ current = next;
101
+ onChange(next);
102
+ };
103
+ onChange(current);
104
+ const watchRatio = () => {
105
+ const query = globalThis.matchMedia?.(`(resolution: ${globalThis.devicePixelRatio}dppx)`);
106
+ if (!query) return;
107
+ listen(query, "change", () => {
108
+ watchRatio();
109
+ check();
110
+ }, {
111
+ once: true,
112
+ signal
113
+ });
114
+ };
115
+ watchRatio();
116
+ const screen = globalThis.screen;
117
+ const orientation = screen?.orientation;
118
+ if (globalThis.window) listen(globalThis.window, "resize", check, { signal });
119
+ if (isEventTarget(screen)) listen(screen, "change", check, { signal });
120
+ if (orientation) listen(orientation, "change", check, { signal });
121
+ return () => disconnect.abort();
122
+ }
123
+ function isEventTarget(value) {
124
+ return isFunction(value?.addEventListener);
125
+ }
126
+ //#endregion
127
+ export { getScreenResolution, watchScreenResolution };
128
+
129
+ //# sourceMappingURL=screen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screen.js","names":[],"sources":["../../../../src/media/dom/screen.ts"],"sourcesContent":["/**\n * Screen resolution, as the signal source for a screen-size rendition cap.\n *\n * Reported as a width and a height rather than a `\"720p\"`-style tier, because\n * the cap that consumes it compares against real track dimensions. A tier only\n * describes a track once you assume its aspect ratio, and that assumption\n * mis-measures an anamorphic or otherwise non-16:9 rendition.\n *\n * The signal source for the screen-size cap in\n * `internal/design/spf/features/rendition-selection-caps.md`.\n *\n * The screen underneath a window is not stable: rotating a device swaps the axes,\n * and unplugging a monitor or dragging the window to another display changes the\n * numbers *and* which physical screen they describe. So `getScreenResolution`\n * reads at call time and caches nothing, and `watchScreenResolution` layers the\n * reacting on top rather than the reader holding state of its own.\n */\n\nimport { listen } from '@videojs/utils/dom';\nimport { shallowEqual } from '@videojs/utils/object';\nimport { isFunction } from '@videojs/utils/predicate';\n\n/** A screen's pixel dimensions. */\nexport interface ScreenResolution {\n readonly width: number;\n readonly height: number;\n}\n\nexport interface ScreenResolutionOptions {\n /**\n * Scale the reading from CSS pixels into device pixels. On by default, since\n * device pixels are what the screen actually has, and a rendition's dimensions\n * are in the same units.\n *\n * Opt out for CSS pixels. Note that derating — a 3x phone rarely wanting 3x the\n * pixels of its layout — is a scale applied over this reading rather than a\n * reason to turn it off.\n *\n * ⚠️ Chromium and Gecko fold page zoom into `devicePixelRatio`, so with this on,\n * zooming moves the reading even though the screen didn't change. WebKit holds\n * the ratio independent of zoom and is unaffected. Whether a cap should track\n * zoom is the cap's call; this flag is only what puts zoom in scope.\n */\n useDevicePixelRatio: boolean;\n}\n\n/**\n * Read the screen's resolution, or `undefined` where there isn't one to read.\n *\n * `undefined` means \"unknown\", which is the answer a cap needs in order to not\n * cap. `screenResolutionCap` reads it that way and declines to narrow, so an\n * unknown screen is \"no cap\" rather than a cap of zero — the reading a naive\n * `?? 0` would produce, which would pin every source to its smallest rendition on\n * exactly the environments we know least about.\n *\n * Dimensions are reported as-is, including the axis swap a rotated device\n * applies to them. Normalizing orientation away is a policy question — whether a\n * cap should flap on rotation, or hold the larger budget across both — and\n * belongs to the cap rather than to the reading.\n */\nexport function getScreenResolution(\n { useDevicePixelRatio }: ScreenResolutionOptions = { useDevicePixelRatio: true }\n): ScreenResolution | undefined {\n const screen = globalThis.screen;\n if (!screen) return undefined;\n\n // `|| 1` covers a missing or nonsense ratio: a CSS-pixel reading is still true\n // and still cappable, so it isn't worth failing the whole answer over.\n const ratio = useDevicePixelRatio ? globalThis.devicePixelRatio || 1 : 1;\n\n // Rounded because device pixels are whole and a fractional ratio doesn't divide\n // a screen evenly. `NaN` from a nonsense dimension fails the check below, since\n // no comparison against it holds.\n const width = Math.round(screen.width * ratio);\n const height = Math.round(screen.height * ratio);\n\n return width > 0 && height > 0 ? { width, height } : undefined;\n}\n\n/**\n * Call `onChange` whenever {@link getScreenResolution} would start answering\n * differently. Returns a function that stops watching.\n *\n * There is no single event for \"the screen changed\", so this subscribes to every\n * signal that implies one and compares readings to decide whether anything\n * actually moved. Comparing is what makes that safe: the signals overlap and\n * `resize` in particular is noisy, so over-subscribing costs a discarded read\n * rather than a spurious call.\n *\n * `onChange` is called once on subscribe with the starting value — including\n * `undefined` where there is no screen — and after that only on a genuine change.\n * So a consumer gets its initial state from the watcher and never has to pair it\n * with a separate {@link getScreenResolution} call.\n *\n * The signals, and what each one is here for:\n *\n * - **`screen`'s own `change`** — the screen itself being reconfigured, or the\n * window landing on a different one. The direct signal, and the only one that\n * catches a window moving between two same-size, same-ratio displays. From the\n * Window Management API, but on the base `Screen` rather than behind\n * `getScreenDetails()`, so it needs no permission — only a secure context.\n * Measured present in Chromium and absent in WebKit and Firefox, hence the\n * three below rather than this alone.\n * - **`resize`** — the window changing size, which is also what the OS does to it\n * when the display it was on goes away.\n * - **`screen.orientation` change** — rotation, which swaps the axes without\n * necessarily resizing the window.\n * - **a `(resolution: <ratio>dppx)` media query** — the device pixel ratio\n * changing under a window that kept its size, which is the cross-display drag\n * between displays of different density. Each query only answers about the ratio\n * it was built for, so it reports one change and its handler arms the next.\n *\n * Worth keeping despite looking redundant, because it is the only coverage that\n * case has in WebKit and Firefox: neither implements `screen`'s change event,\n * and the drag doesn't resize the window. It is also a cleaner signal in Safari\n * than elsewhere — WebKit holds `devicePixelRatio` independent of page zoom, so\n * there it moves only on a real density change, where Chromium and Gecko fold\n * zoom into it as well.\n *\n * ⚠️ Known gap, on engines without `screen`'s change event: dragging a window\n * between two different-size displays that share a ratio, without the window\n * resizing, changes the reading with nothing firing. Closing it there would mean\n * polling, whose interval and battery cost are a policy decision this function\n * shouldn't be making.\n */\nexport function watchScreenResolution(\n onChange: (resolution: ScreenResolution | undefined) => void,\n options: ScreenResolutionOptions = { useDevicePixelRatio: true }\n): () => void {\n // One signal for every listener, so stopping is one call rather than a handle\n // per subscription. Also makes a late `watchRatio` inert: `addEventListener`\n // drops a listener whose signal has already aborted.\n const disconnect = new AbortController();\n const { signal } = disconnect;\n let current = getScreenResolution(options);\n\n const check = () => {\n const next = getScreenResolution(options);\n if (shallowEqual(current, next)) return;\n\n current = next;\n onChange(next);\n };\n\n // Deliver the starting value up front, so a consumer gets its initial state from\n // the watcher rather than having to pair it with a separate read. Unconditional,\n // rather than falling out of comparing against an empty `current`: an unknown\n // reading is a value too, and a consumer that only ever heard from us about a\n // *known* screen couldn't tell \"there is no screen\" from \"not called yet\".\n //\n // Before the listeners rather than after, so a callback that throws takes\n // nothing with it — there is no subscription yet to strand.\n onChange(current);\n\n // A `dppx` query only answers about the ratio it was built for, so each one\n // reports a single change and the handler builds the next. `once: true` is what\n // keeps that from accumulating listeners: the fired one is gone before the\n // replacement is armed, with no handle to track. Same shape as MDN's snippet\n // for this, whose earlier non-re-arming version fired exactly once and stopped.\n const watchRatio = () => {\n const query = globalThis.matchMedia?.(`(resolution: ${globalThis.devicePixelRatio}dppx)`);\n if (!query) return;\n\n listen(\n query,\n 'change',\n () => {\n watchRatio();\n check();\n },\n { once: true, signal }\n );\n };\n\n watchRatio();\n\n // Each signal is optional for the same reason the reading is: an environment\n // missing one has nothing to report from it, which is not a reason to fail.\n // `screen`'s own change event is subscribed without feature-detecting — where\n // it isn't implemented it simply never fires, and a signal that never fires\n // costs nothing under comparison.\n const screen = globalThis.screen;\n const orientation = screen?.orientation;\n\n if (globalThis.window) listen(globalThis.window, 'resize', check, { signal });\n // NOTE: Chromium browsers support screen change event.\n // See: https://developer.mozilla.org/en-US/docs/Web/API/Screen/change_event\n if (isEventTarget(screen)) listen(screen, 'change', check, { signal });\n if (orientation) listen(orientation, 'change', check, { signal });\n\n return () => disconnect.abort();\n}\n\nfunction isEventTarget(value: any): value is EventTarget {\n return isFunction(value?.addEventListener);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,SAAgB,oBACd,EAAE,wBAAiD,EAAE,qBAAqB,KAAK,GACjD;CAC9B,MAAM,SAAS,WAAW;CAC1B,IAAI,CAAC,QAAQ,OAAO,KAAA;CAIpB,MAAM,QAAQ,sBAAsB,WAAW,oBAAoB,IAAI;CAKvE,MAAM,QAAQ,KAAK,MAAM,OAAO,QAAQ,KAAK;CAC7C,MAAM,SAAS,KAAK,MAAM,OAAO,SAAS,KAAK;CAE/C,OAAO,QAAQ,KAAK,SAAS,IAAI;EAAE;EAAO;CAAO,IAAI,KAAA;AACvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,SAAgB,sBACd,UACA,UAAmC,EAAE,qBAAqB,KAAK,GACnD;CAIZ,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,EAAE,WAAW;CACnB,IAAI,UAAU,oBAAoB,OAAO;CAEzC,MAAM,cAAc;EAClB,MAAM,OAAO,oBAAoB,OAAO;EACxC,IAAI,aAAa,SAAS,IAAI,GAAG;EAEjC,UAAU;EACV,SAAS,IAAI;CACf;CAUA,SAAS,OAAO;CAOhB,MAAM,mBAAmB;EACvB,MAAM,QAAQ,WAAW,aAAa,gBAAgB,WAAW,iBAAiB,MAAM;EACxF,IAAI,CAAC,OAAO;EAEZ,OACE,OACA,gBACM;GACJ,WAAW;GACX,MAAM;EACR,GACA;GAAE,MAAM;GAAM;EAAO,CACvB;CACF;CAEA,WAAW;CAOX,MAAM,SAAS,WAAW;CAC1B,MAAM,cAAc,QAAQ;CAE5B,IAAI,WAAW,QAAQ,OAAO,WAAW,QAAQ,UAAU,OAAO,EAAE,OAAO,CAAC;CAG5E,IAAI,cAAc,MAAM,GAAG,OAAO,QAAQ,UAAU,OAAO,EAAE,OAAO,CAAC;CACrE,IAAI,aAAa,OAAO,aAAa,UAAU,OAAO,EAAE,OAAO,CAAC;CAEhE,aAAa,WAAW,MAAM;AAChC;AAEA,SAAS,cAAc,OAAkC;CACvD,OAAO,WAAW,OAAO,gBAAgB;AAC3C"}
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","names":[],"sources":["../../../src/media/errors.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAmCiB;;EAEf;;EAEA;;EAEA;;;;;;;;;;;;;;;;;;;;;;cA6CW;;;;;;;;;;iBAWG,aAAa;;;;;iBAQb,UAAU"}
1
+ {"version":3,"file":"errors.d.ts","names":[],"sources":["../../../src/media/errors.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAmCiB;;EAEf;;EAEA;;EAEA;;;;;;;;;;;;;;;;;;;;;;cAoDW;;;;;;;;;;iBAWG,aAAa;;;;;iBAQb,UAAU"}
@@ -10,8 +10,15 @@ const SVTA_UNSUPPORTED_AUDIO_FORMAT = 1005;
10
10
  */
11
11
  const SVTA_UNSUPPORTED_DRM_SYSTEM = 4008;
12
12
  /**
13
- * SVTA 2 [Playback] 011 — no video track the environment can play. For a source
14
- * that *has* video renditions where every one was excluded as unplayable.
13
+ * SVTA 2 [Playback] 011 — no video track the environment can play.
14
+ *
15
+ * Covers both ways a composition can end up with nothing to select: renditions
16
+ * that existed and were all excluded as unplayable, and — where the composition
17
+ * composes `reportAbsentTrackType` to say it needs the type — a source carrying
18
+ * none to begin with. Deliberately one code for both, because they are the same
19
+ * answer to a viewer, and because the alternative is a code the SVTA spec doesn't
20
+ * define. Whether an absent type is a failure is the composition's to state,
21
+ * which is why it opts in rather than being read off the source.
15
22
  */
16
23
  const SVTA_NO_SUPPORTED_VIDEO_TRACK = 2011;
17
24
  /** SVTA 2 [Playback] 012 — the audio counterpart of {@link SVTA_NO_SUPPORTED_VIDEO_TRACK}. */
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","names":[],"sources":["../../../src/media/errors.ts"],"sourcesContent":["/**\n * SVTA 2070 (Standardized Error Codes) — the vocabulary for identifying a\n * playback failure or notice, independent of how it's transported or who\n * decides what to do about it.\n *\n * A code is a single integer: the leading digit(s) are the **category** (the\n * error's domain) and the trailing three are the **index** (the specific error\n * within it). Four digits for natively-defined errors, five when an external\n * standard is embedded — `\"03404\"` is an HTTP 404 under the network category.\n * Categories are `0` unknown, `1` media content, `2` playback, `3` network,\n * `4` content protection, `5` accessibility, `6` remote play, `7` advertising,\n * `99` custom.\n *\n * Two properties of the spec shape the types here:\n *\n * - **Severity is deliberately not part of a code.** Per §Approach, \"impact\n * varies with player implementation, breaking the consistency of a specific\n * error mapping to single code.\" So an error carries no fatal flag — whether a\n * condition is fatal depends on the composition observing it, and is decided\n * downstream.\n * - **Reporting is partial and stacked** (Principles 5–6). Errors are reported\n * as encountered, most of them non-fatal, and the *sequence* carries causation\n * a single value can't.\n *\n * See `internal/design/spf/features/errors.md`.\n */\n\n/**\n * A reported condition, identified by its SVTA code.\n *\n * Named for the spec rather than the engine because the vocabulary is\n * format- and player-neutral. Not `MediaError` — that name belongs to the\n * `@videojs/media` DOM-facing class this eventually maps *onto*, and the mapping\n * is the point at which severity and user-facing text get decided.\n */\nexport interface SvtaError {\n /** The SVTA code — see {@link svtaCategory} / {@link svtaIndex}. */\n code: number;\n /** Engineer-facing detail. Optional; the code is the identity. */\n message?: string;\n /** Reporter-specific context (track type, url, the constraint that fired). */\n data?: unknown;\n}\n\n/** SVTA 1 [Media Content] 004 — the video is in a format we can't play (e.g. an MPEG-TS container). */\nexport const SVTA_UNSUPPORTED_VIDEO_FORMAT = 1004;\n\n/** SVTA 1 [Media Content] 005 — the audio counterpart of {@link SVTA_UNSUPPORTED_VIDEO_FORMAT}. */\nexport const SVTA_UNSUPPORTED_AUDIO_FORMAT = 1005;\n\n/**\n * SVTA 4 [Content Protection] 008 — unsupported or unavailable DRM system. Used\n * for \"this source is encrypted and we have no decryption pipeline,\" which is\n * detection, not a license failure.\n */\nexport const SVTA_UNSUPPORTED_DRM_SYSTEM = 4008;\n\n/**\n * SVTA 2 [Playback] 011 — no video track the environment can play. For a source\n * that *has* video renditions where every one was excluded as unplayable.\n */\nexport const SVTA_NO_SUPPORTED_VIDEO_TRACK = 2011;\n\n/** SVTA 2 [Playback] 012 — the audio counterpart of {@link SVTA_NO_SUPPORTED_VIDEO_TRACK}. */\nexport const SVTA_NO_SUPPORTED_AUDIO_TRACK = 2012;\n\n/**\n * SVTA 99 [Custom] 001 — this engine has no pipeline for something the source\n * requires, so the source is unplayable *here* rather than broken.\n *\n * Custom rather than standard because the standard codes available describe\n * either narrower or wider things. The causes (1004/1005 unsupported format,\n * 4008 unsupported DRM) say what one rendition hit; the verdicts (2011/2012 no\n * supported track) say a type emptied without saying why it's unfixable. And\n * 2039 \"Manifest feature unsupported\" covers features that are unsupported but\n * still *playable* — LL-HLS degrading to standard live is a 2039 — so\n * overloading it for a fatal condition would make it useless for the notices it\n * belongs on.\n *\n * Index `001`: the spec defines only `99000` (Unknown) for the custom category\n * and leaves the rest to the publisher, so this is the first code we define.\n *\n * Five digits, and deliberately not special-cased anywhere: {@link svtaCategory}\n * and {@link svtaIndex} decompose it correctly by arithmetic alone, because\n * every standard category is below `8000` and custom starts at `99000`.\n */\nexport const SVTA_UNSUPPORTED_PLAYBACK_FEATURE = 99001;\n\n/**\n * The error's domain — `code / 1000`, per the spec's \"divide by one thousand to\n * obtain the error category\". Works uniformly across the four-digit native form\n * and the five-digit form embedding an external standard: `\"03404\"` is\n * numerically 3404, which decomposes identically. That also makes a numeric code\n * immune to the spec's inconsistent zero-padding (§Approach writes a\n * category-unknown network error as `\"0300\"` where the error index implies\n * category 3 / index 000).\n */\nexport function svtaCategory(code: number): number {\n return Math.floor(code / 1000);\n}\n\n/**\n * The specific error within its category — `code % 1000`. For a five-digit code\n * this is the embedded external value (an HTTP status, a VAST code).\n */\nexport function svtaIndex(code: number): number {\n return code % 1000;\n}\n"],"mappings":";;AA6CA,MAAa,gCAAgC;;AAG7C,MAAa,gCAAgC;;;;;;AAO7C,MAAa,8BAA8B;;;;;AAM3C,MAAa,gCAAgC;;AAG7C,MAAa,gCAAgC;;;;;;;;;;;;;;;;;;;;;AAsB7C,MAAa,oCAAoC;;;;;;;;;;AAWjD,SAAgB,aAAa,MAAsB;CACjD,OAAO,KAAK,MAAM,OAAO,GAAI;AAC/B;;;;;AAMA,SAAgB,UAAU,MAAsB;CAC9C,OAAO,OAAO;AAChB"}
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../../../src/media/errors.ts"],"sourcesContent":["/**\n * SVTA 2070 (Standardized Error Codes) — the vocabulary for identifying a\n * playback failure or notice, independent of how it's transported or who\n * decides what to do about it.\n *\n * A code is a single integer: the leading digit(s) are the **category** (the\n * error's domain) and the trailing three are the **index** (the specific error\n * within it). Four digits for natively-defined errors, five when an external\n * standard is embedded — `\"03404\"` is an HTTP 404 under the network category.\n * Categories are `0` unknown, `1` media content, `2` playback, `3` network,\n * `4` content protection, `5` accessibility, `6` remote play, `7` advertising,\n * `99` custom.\n *\n * Two properties of the spec shape the types here:\n *\n * - **Severity is deliberately not part of a code.** Per §Approach, \"impact\n * varies with player implementation, breaking the consistency of a specific\n * error mapping to single code.\" So an error carries no fatal flag — whether a\n * condition is fatal depends on the composition observing it, and is decided\n * downstream.\n * - **Reporting is partial and stacked** (Principles 5–6). Errors are reported\n * as encountered, most of them non-fatal, and the *sequence* carries causation\n * a single value can't.\n *\n * See `internal/design/spf/features/errors.md`.\n */\n\n/**\n * A reported condition, identified by its SVTA code.\n *\n * Named for the spec rather than the engine because the vocabulary is\n * format- and player-neutral. Not `MediaError` — that name belongs to the\n * `@videojs/media` DOM-facing class this eventually maps *onto*, and the mapping\n * is the point at which severity and user-facing text get decided.\n */\nexport interface SvtaError {\n /** The SVTA code — see {@link svtaCategory} / {@link svtaIndex}. */\n code: number;\n /** Engineer-facing detail. Optional; the code is the identity. */\n message?: string;\n /** Reporter-specific context (track type, url, the constraint that fired). */\n data?: unknown;\n}\n\n/** SVTA 1 [Media Content] 004 — the video is in a format we can't play (e.g. an MPEG-TS container). */\nexport const SVTA_UNSUPPORTED_VIDEO_FORMAT = 1004;\n\n/** SVTA 1 [Media Content] 005 — the audio counterpart of {@link SVTA_UNSUPPORTED_VIDEO_FORMAT}. */\nexport const SVTA_UNSUPPORTED_AUDIO_FORMAT = 1005;\n\n/**\n * SVTA 4 [Content Protection] 008 — unsupported or unavailable DRM system. Used\n * for \"this source is encrypted and we have no decryption pipeline,\" which is\n * detection, not a license failure.\n */\nexport const SVTA_UNSUPPORTED_DRM_SYSTEM = 4008;\n\n/**\n * SVTA 2 [Playback] 011 — no video track the environment can play.\n *\n * Covers both ways a composition can end up with nothing to select: renditions\n * that existed and were all excluded as unplayable, and — where the composition\n * composes `reportAbsentTrackType` to say it needs the type — a source carrying\n * none to begin with. Deliberately one code for both, because they are the same\n * answer to a viewer, and because the alternative is a code the SVTA spec doesn't\n * define. Whether an absent type is a failure is the composition's to state,\n * which is why it opts in rather than being read off the source.\n */\nexport const SVTA_NO_SUPPORTED_VIDEO_TRACK = 2011;\n\n/** SVTA 2 [Playback] 012 — the audio counterpart of {@link SVTA_NO_SUPPORTED_VIDEO_TRACK}. */\nexport const SVTA_NO_SUPPORTED_AUDIO_TRACK = 2012;\n\n/**\n * SVTA 99 [Custom] 001 — this engine has no pipeline for something the source\n * requires, so the source is unplayable *here* rather than broken.\n *\n * Custom rather than standard because the standard codes available describe\n * either narrower or wider things. The causes (1004/1005 unsupported format,\n * 4008 unsupported DRM) say what one rendition hit; the verdicts (2011/2012 no\n * supported track) say a type emptied without saying why it's unfixable. And\n * 2039 \"Manifest feature unsupported\" covers features that are unsupported but\n * still *playable* — LL-HLS degrading to standard live is a 2039 — so\n * overloading it for a fatal condition would make it useless for the notices it\n * belongs on.\n *\n * Index `001`: the spec defines only `99000` (Unknown) for the custom category\n * and leaves the rest to the publisher, so this is the first code we define.\n *\n * Five digits, and deliberately not special-cased anywhere: {@link svtaCategory}\n * and {@link svtaIndex} decompose it correctly by arithmetic alone, because\n * every standard category is below `8000` and custom starts at `99000`.\n */\nexport const SVTA_UNSUPPORTED_PLAYBACK_FEATURE = 99001;\n\n/**\n * The error's domain — `code / 1000`, per the spec's \"divide by one thousand to\n * obtain the error category\". Works uniformly across the four-digit native form\n * and the five-digit form embedding an external standard: `\"03404\"` is\n * numerically 3404, which decomposes identically. That also makes a numeric code\n * immune to the spec's inconsistent zero-padding (§Approach writes a\n * category-unknown network error as `\"0300\"` where the error index implies\n * category 3 / index 000).\n */\nexport function svtaCategory(code: number): number {\n return Math.floor(code / 1000);\n}\n\n/**\n * The specific error within its category — `code % 1000`. For a five-digit code\n * this is the embedded external value (an HTTP status, a VAST code).\n */\nexport function svtaIndex(code: number): number {\n return code % 1000;\n}\n"],"mappings":";;AA6CA,MAAa,gCAAgC;;AAG7C,MAAa,gCAAgC;;;;;;AAO7C,MAAa,8BAA8B;;;;;;;;;;;;AAa3C,MAAa,gCAAgC;;AAG7C,MAAa,gCAAgC;;;;;;;;;;;;;;;;;;;;;AAsB7C,MAAa,oCAAoC;;;;;;;;;;AAWjD,SAAgB,aAAa,MAAsB;CACjD,OAAO,KAAK,MAAM,OAAO,GAAI;AAC/B;;;;;AAMA,SAAgB,UAAU,MAAsB;CAC9C,OAAO,OAAO;AAChB"}
@@ -16,58 +16,44 @@ function matchesPartialTrack(track, filter) {
16
16
  }
17
17
  return true;
18
18
  }
19
- /**
20
- * Pick the first track of the given type from a presentation.
21
- *
22
- * Returns the first track in the first switching set of the matching
23
- * selection set, or `undefined` if either is missing. POC-shaped
24
- * default-pick — `pickVideoTrack` / `pickAudioTrack` honor bandwidth +
25
- * language preferences and will replace this once selection callers are
26
- * ready.
27
- */
28
- function pickFirstTrackId(presentation, type) {
29
- return presentation.selectionSets?.find((set) => set.type === type)?.switchingSets[0]?.tracks[0]?.id;
19
+ /** Missing dimensions are treated as area `0`, so a track without them ranks last. */
20
+ function pixelArea(track) {
21
+ return (track.width ?? 0) * (track.height ?? 0);
30
22
  }
31
23
  /**
32
- * Pick the track with the highest pixel area at or below `maxPixelArea`.
33
- * Falls back to the lowest track when nothing satisfies the cap (the
34
- * lowest of the above-cap set is the closest to the cap from above).
35
- * Tiebreak on bandwidth. Missing dimensions are treated as area `0`.
24
+ * Narrow to the tracks at or below `maxPixelArea`, and nothing else: no ordering,
25
+ * no fallback. Survivors keep their incoming order, and an empty result is a real
26
+ * answer "none of these fit".
27
+ *
28
+ * Deliberately only the filter, because as a selection rule this composes under
29
+ * `applyRules`, which already owns both halves a caller might expect here: an empty
30
+ * result is skipped, so a preference can never narrow the candidate set to nothing;
31
+ * and ordering the survivors is a separate rule's job
32
+ * ({@link byDescendingResolution}, bandwidth ABR). Doing either here would duplicate
33
+ * the composer and give one rule two responsibilities.
36
34
  */
37
- function pickTrackUnderPixelArea(tracks, maxPixelArea = Number.POSITIVE_INFINITY) {
38
- if (tracks.length === 0) return void 0;
39
- const sorted = [...tracks].sort((a, b) => (b.width ?? 0) * (b.height ?? 0) - (a.width ?? 0) * (a.height ?? 0) || (b.bandwidth ?? 0) - (a.bandwidth ?? 0));
40
- return sorted.find((t) => (t.width ?? 0) * (t.height ?? 0) <= maxPixelArea) ?? sorted[sorted.length - 1];
35
+ function tracksUnderPixelArea(tracks, maxPixelArea = Number.POSITIVE_INFINITY) {
36
+ return tracks.filter((track) => pixelArea(track) <= maxPixelArea);
41
37
  }
42
38
  /**
43
- * Pick the video track with the highest pixel area.
39
+ * Compare two tracks by resolution, largest first, with bandwidth as the tiebreak
40
+ * for renditions of identical dimensions. Missing dimensions are treated as area
41
+ * `0`, so a track without them sorts last.
44
42
  *
45
- * Pair with `selectVideoTrack`; compose `switchVideoQuality` instead
46
- * for runtime-adapted quality.
43
+ * A comparator rather than a "highest track" function: the selection-rule chain
44
+ * takes the head of the list it produces, so ranking never has to collapse to a
45
+ * single track. `preferHighestResolution` is `sort` over this and nothing more.
47
46
  */
48
- function pickHighestResolutionVideoTrack(presentation) {
49
- const tracks = (presentation.selectionSets?.find((set) => set.type === "video"))?.switchingSets[0]?.tracks;
50
- if (!tracks?.length) return void 0;
51
- return pickTrackUnderPixelArea(tracks)?.id;
47
+ function byDescendingResolution(a, b) {
48
+ return pixelArea(b) - pixelArea(a) || (b.bandwidth ?? 0) - (a.bandwidth ?? 0);
52
49
  }
53
50
  /**
54
- * Pick audio track.
55
- *
56
- * Selection priority:
57
- * 1. First track matching preferred language (if specified)
58
- * 2. First default track
59
- * 3. First audio track
51
+ * Default audio policy over a candidate list: the three-tier pick a selection-rule
52
+ * chain applies once it has narrowed the candidates.
60
53
  *
61
- * @param presentation - Presentation with audio tracks
62
- * @param config - Selection configuration (preferred language)
63
- * @returns Selected audio track ID, or undefined if no audio tracks
54
+ * Priority: `preferredAudioLanguage` match `DEFAULT=YES` first track.
64
55
  */
65
- function pickAudioTrack(presentation, config) {
66
- const audioSet = presentation.selectionSets?.find((set) => set.type === "audio");
67
- if (!audioSet || audioSet.switchingSets.length === 0) return;
68
- const switchingSet = audioSet.switchingSets[0];
69
- if (!switchingSet || switchingSet.tracks.length === 0) return;
70
- const tracks = switchingSet.tracks;
56
+ function pickAudioTrackFromTracks(tracks, config) {
71
57
  if (config?.preferredAudioLanguage) {
72
58
  const languageMatch = tracks.find((track) => track.language === config.preferredAudioLanguage);
73
59
  if (languageMatch) return languageMatch.id;
@@ -77,11 +63,9 @@ function pickAudioTrack(presentation, config) {
77
63
  return tracks[0]?.id;
78
64
  }
79
65
  /**
80
- * Default text-track policy over an explicit candidate list (rather than a whole
81
- * presentation): the opt-in three-tier pick `pickTextTrack` delegates to, factored
82
- * out so a caller that has already narrowed the candidates — a constrained,
83
- * CDN-scoped track-switching chain — applies the same policy without re-deriving
84
- * from the presentation.
66
+ * Default text-track policy over a candidate list: the opt-in three-tier pick
67
+ * `switchTextTrack`'s terminal applies once it has narrowed the renditions to the
68
+ * constrained, CDN-scoped set.
85
69
  *
86
70
  * Priority: `preferredSubtitleLanguage` match → `DEFAULT=YES + AUTOSELECT=YES`
87
71
  * (only when `enableDefaultTrack`) → `undefined` (opt-in). FORCED tracks are
@@ -102,6 +86,6 @@ function pickTextTrackFromTracks(tracks, config) {
102
86
  }
103
87
  }
104
88
  //#endregion
105
- export { matchesPartialTrack, pickAudioTrack, pickFirstTrackId, pickHighestResolutionVideoTrack, pickTextTrackFromTracks, pickTrackUnderPixelArea };
89
+ export { byDescendingResolution, matchesPartialTrack, pickAudioTrackFromTracks, pickTextTrackFromTracks, tracksUnderPixelArea };
106
90
 
107
91
  //# sourceMappingURL=select-tracks.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"select-tracks.js","names":[],"sources":["../../../../src/media/primitives/select-tracks.ts"],"sourcesContent":["import { DEFAULT_QUALITY_CONFIG, selectQuality } from '../abr/quality-selection';\nimport type {\n AudioSelectionSet,\n MaybeResolvedPresentation,\n PartiallyResolvedTextTrack,\n TextTrack,\n TrackType,\n VideoSelectionSet,\n} from '../types';\nimport { SelectedTrackIdKeyByType } from '../utils/track-selection';\n\n/**\n * Default initial bandwidth estimate for cold start (bits per second).\n * Conservative 1 Mbps to avoid over-selecting on slow connections.\n */\nexport const DEFAULT_INITIAL_BANDWIDTH = 1_000_000;\n\n/**\n * State shape for track selection.\n */\nexport interface TrackSelectionState {\n presentation?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\n/**\n * Context shape for track selection.\n * Currently empty - reserved for future use (e.g., bandwidth estimator).\n */\nexport type TrackSelectionContext = Record<string, never>;\n\n/**\n * Action types for track selection.\n * Reserved for future event-driven selection triggers.\n */\nexport type TrackSelectionAction = { type: 'presentation-loaded' };\n\n/**\n * Configuration for video track selection.\n */\nexport interface VideoSelectionConfig {\n /**\n * Initial bandwidth estimate for cold start (bits per second).\n * Used to select video quality before we have real measurements.\n * Default: 1 Mbps (conservative).\n */\n initialBandwidth?: number;\n\n /**\n * Safety margin for quality selection (0-1).\n * Default: 0.85 (15% headroom).\n */\n safetyMargin?: number;\n}\n\n/**\n * Configuration for audio track selection.\n */\nexport interface AudioSelectionConfig {\n /**\n * Preferred audio language (ISO 639 code, e.g., \"en\", \"es\").\n * If not specified, selects first audio track.\n */\n preferredAudioLanguage?: string;\n}\n\n/**\n * Configuration for text track selection.\n */\nexport interface TextSelectionConfig {\n /**\n * Preferred subtitle language (ISO 639 code, e.g., \"en\", \"es\").\n * If specified, selects matching track if available.\n */\n preferredSubtitleLanguage?: string;\n\n /**\n * Include FORCED subtitle tracks in selection.\n * Default: false (follows hls.js/http-streaming pattern)\n *\n * Note: Per Apple's HLS spec, if content has forced and regular subtitles\n * in the same language, the regular track MUST contain both forced and\n * regular content. Therefore, forced-only tracks are redundant and excluded\n * by default.\n */\n includeForcedTracks?: boolean;\n\n /**\n * Auto-select DEFAULT track (requires DEFAULT=YES + AUTOSELECT=YES in HLS).\n * Default: false (user opt-in, matches hls.js/http-streaming)\n *\n * When enabled, tracks marked with both DEFAULT=YES and AUTOSELECT=YES\n * will be automatically selected if no user preference matches.\n */\n enableDefaultTrack?: boolean;\n}\n\n// =============================================================================\n// Helper Functions (Pure Selection Logic)\n// =============================================================================\n\n/**\n * Contract for a track picker — a pure function that consults a\n * presentation (and optional config) and returns the id of the track to\n * select, or `undefined` to leave the slot unset.\n *\n * Behaviors that own a track-selection slot (`selectAudioTrack`,\n * `selectVideoTrack`, `switchVideoTrack`) accept a\n * `TrackPicker` via config. The behavior passes its own config straight\n * through as the picker's second argument — pickers that need richer\n * options (language preferences, default-track filtering, bandwidth-aware\n * selection) read from `config`; pickers that don't (e.g., first-track)\n * ignore it.\n */\nexport type TrackPicker<Config = unknown> = (\n presentation: MaybeResolvedPresentation,\n config?: Config\n) => string | undefined;\n\n/**\n * Test whether a track matches a partial-track description: every present,\n * defined field of `filter` equals the track's. Absent or `undefined` filter\n * fields don't constrain. Used to narrow candidates by a user selection\n * (`{ id }`, `{ language }`, `{ height }`, …).\n *\n * @param track - The track to test\n * @param filter - Partial-track description; only present, defined fields constrain\n * @returns `true` when the track matches every constraining field\n */\nexport function matchesPartialTrack<T>(track: T, filter: Partial<T>): boolean {\n for (const key in filter) {\n const filterValue = filter[key as keyof T];\n if (filterValue !== undefined && track[key as keyof T] !== filterValue) return false;\n }\n return true;\n}\n\n/**\n * Pick the first track of the given type from a presentation.\n *\n * Returns the first track in the first switching set of the matching\n * selection set, or `undefined` if either is missing. POC-shaped\n * default-pick — `pickVideoTrack` / `pickAudioTrack` honor bandwidth +\n * language preferences and will replace this once selection callers are\n * ready.\n */\nexport function pickFirstTrackId(presentation: MaybeResolvedPresentation, type: TrackType): string | undefined {\n return presentation.selectionSets?.find((set) => set.type === type)?.switchingSets[0]?.tracks[0]?.id;\n}\n\n/**\n * Pick video track using quality selection algorithm.\n *\n * Uses bandwidth-based selection with safety margin to pick\n * the highest quality track that fits available bandwidth.\n *\n * @param presentation - Presentation with video tracks\n * @param config - Selection configuration (bandwidth, safety margin)\n * @returns Selected video track ID, or undefined if no video tracks\n */\nexport function pickVideoTrack(\n presentation: MaybeResolvedPresentation,\n config?: VideoSelectionConfig\n): string | undefined {\n const videoSet = presentation.selectionSets?.find((set) => set.type === 'video') as VideoSelectionSet | undefined;\n\n if (!videoSet || videoSet.switchingSets.length === 0) {\n return undefined;\n }\n\n // Get first switching set's tracks (HLS typically has one switching set per type)\n const switchingSet = videoSet.switchingSets[0];\n if (!switchingSet || switchingSet.tracks.length === 0) {\n return undefined;\n }\n\n const initialBandwidth = config?.initialBandwidth ?? DEFAULT_INITIAL_BANDWIDTH;\n const safetyMargin = config?.safetyMargin ?? DEFAULT_QUALITY_CONFIG.safetyMargin;\n\n // selectQuality works with both partially resolved and resolved tracks\n const selected = selectQuality(switchingSet.tracks as any, { bandwidth: initialBandwidth, safetyMargin });\n\n return selected?.id;\n}\n\n/**\n * Translates a \"max resolution\" into a total total pixel area\n * for comparisons with video track resolutions with an assumed\n * 16:9 ratio.\n *\n * Example: \"720p\" translates to a 921600 pixel area.\n *\n * Because 720 * 1280 = 720 * (720 * (16/9) ) = 921_600\n *\n * Accepts:\n * - string with the format '{height}p'. ('720p')\n * - bare number, interpreted as pixel area. (921_600)\n * - anything else will translate to `+Infinity`, meaning no cap specified\n */\nexport function maxResolutionToPixelArea(value: string | number | undefined): number {\n if (value === undefined || value === null) return Number.POSITIVE_INFINITY;\n if (typeof value === 'number') return Number.isFinite(value) && value > 0 ? value : Number.POSITIVE_INFINITY;\n const match = value.trim().match(/^(\\d+)p?$/i);\n if (!match) return Number.POSITIVE_INFINITY;\n const height = Number(match[1]);\n if (!(Number.isFinite(height) && height > 0)) return Number.POSITIVE_INFINITY;\n return (height * height * 16) / 9;\n}\n\ntype RankableTrack = { id: string; width?: number; height?: number; bandwidth?: number };\n\n/**\n * Pick the track with the highest pixel area at or below `maxPixelArea`.\n * Falls back to the lowest track when nothing satisfies the cap (the\n * lowest of the above-cap set is the closest to the cap from above).\n * Tiebreak on bandwidth. Missing dimensions are treated as area `0`.\n */\nexport function pickTrackUnderPixelArea<T extends RankableTrack>(\n tracks: readonly T[],\n maxPixelArea: number = Number.POSITIVE_INFINITY\n): T | undefined {\n if (tracks.length === 0) return undefined;\n\n // Sort descending by pixel area, bandwidth as tiebreaker. List sizes\n // are small (HLS variant counts) — no need to optimize past a sort.\n const sorted = [...tracks].sort(\n (a, b) =>\n (b.width ?? 0) * (b.height ?? 0) - (a.width ?? 0) * (a.height ?? 0) || (b.bandwidth ?? 0) - (a.bandwidth ?? 0)\n );\n\n return sorted.find((t) => (t.width ?? 0) * (t.height ?? 0) <= maxPixelArea) ?? sorted[sorted.length - 1];\n}\n\n/**\n * Pick the video track with the highest pixel area.\n *\n * Pair with `selectVideoTrack`; compose `switchVideoQuality` instead\n * for runtime-adapted quality.\n */\nexport function pickHighestResolutionVideoTrack(presentation: MaybeResolvedPresentation): string | undefined {\n const videoSet = presentation.selectionSets?.find((set) => set.type === 'video') as VideoSelectionSet | undefined;\n const tracks = videoSet?.switchingSets[0]?.tracks;\n if (!tracks?.length) return undefined;\n return pickTrackUnderPixelArea(tracks)?.id;\n}\n\n/**\n * Pick audio track.\n *\n * Selection priority:\n * 1. First track matching preferred language (if specified)\n * 2. First default track\n * 3. First audio track\n *\n * @param presentation - Presentation with audio tracks\n * @param config - Selection configuration (preferred language)\n * @returns Selected audio track ID, or undefined if no audio tracks\n */\nexport function pickAudioTrack(\n presentation: MaybeResolvedPresentation,\n config?: AudioSelectionConfig\n): string | undefined {\n const audioSet = presentation.selectionSets?.find((set) => set.type === 'audio') as AudioSelectionSet | undefined;\n\n if (!audioSet || audioSet.switchingSets.length === 0) {\n return undefined;\n }\n\n // Get first switching set's tracks\n const switchingSet = audioSet.switchingSets[0];\n if (!switchingSet || switchingSet.tracks.length === 0) {\n return undefined;\n }\n\n const tracks = switchingSet.tracks;\n\n // Try preferred language first\n if (config?.preferredAudioLanguage) {\n const languageMatch = tracks.find((track) => track.language === config.preferredAudioLanguage);\n if (languageMatch) {\n return languageMatch.id;\n }\n }\n\n // Try default track\n const defaultTrack = tracks.find((track) => track.default === true);\n if (defaultTrack) {\n return defaultTrack.id;\n }\n\n // Fall back to first track\n return tracks[0]?.id;\n}\n\n/**\n * Pick text track to activate from a presentation. Conforms to the\n * `TrackPicker` contract. The candidate-list core (`pickTextTrackFromTracks`)\n * is the opt-in default policy `switchTextTrack`'s terminal applies once it has\n * narrowed the renditions.\n *\n * Selection priority (if enabled):\n * 1. User preference (preferredSubtitleLanguage)\n * 2. DEFAULT track (if enableDefaultTrack is true and track has DEFAULT=YES + AUTOSELECT=YES)\n * 3. No auto-selection (user opt-in)\n *\n * By default, FORCED tracks are excluded per Apple's HLS spec.\n */\nexport function pickTextTrack(\n presentation: MaybeResolvedPresentation,\n config?: TextSelectionConfig\n): string | undefined {\n const tracks = presentation.selectionSets?.find((set) => set.type === 'text')?.switchingSets?.[0]?.tracks;\n if (!tracks?.length) return undefined;\n return pickTextTrackFromTracks(tracks, config);\n}\n\n/**\n * Default text-track policy over an explicit candidate list (rather than a whole\n * presentation): the opt-in three-tier pick `pickTextTrack` delegates to, factored\n * out so a caller that has already narrowed the candidates — a constrained,\n * CDN-scoped track-switching chain — applies the same policy without re-deriving\n * from the presentation.\n *\n * Priority: `preferredSubtitleLanguage` match → `DEFAULT=YES + AUTOSELECT=YES`\n * (only when `enableDefaultTrack`) → `undefined` (opt-in). FORCED tracks are\n * excluded unless `includeForcedTracks` (Apple-spec: a regular track must carry\n * forced content when both exist, so a forced-only track is redundant).\n */\nexport function pickTextTrackFromTracks(\n tracks: readonly (PartiallyResolvedTextTrack | TextTrack)[],\n config?: TextSelectionConfig\n): string | undefined {\n const availableTracks = config?.includeForcedTracks ? tracks : tracks.filter((track) => !track.forced);\n if (availableTracks.length === 0) return undefined;\n\n const { preferredSubtitleLanguage, enableDefaultTrack = false } = config ?? {};\n\n if (preferredSubtitleLanguage) {\n const languageMatch = availableTracks.find((track) => track.language === preferredSubtitleLanguage);\n if (languageMatch) return languageMatch.id;\n }\n\n if (enableDefaultTrack) {\n const defaultTrack = availableTracks.find((track) => track.default === true);\n if (defaultTrack) return defaultTrack.id;\n }\n\n return undefined;\n}\n\n/**\n * Check if we can select a track of the given type.\n *\n * Returns true when:\n * - Presentation exists\n * - Has tracks of the specified type\n *\n * Generic over track type - works for video, audio, or text.\n */\nexport function canSelectTrack(state: TrackSelectionState, type: TrackType): boolean {\n return !!state?.presentation?.selectionSets?.find((set) => set.type === type)?.switchingSets?.[0]?.tracks.length;\n}\n\n/**\n * Check if we should select a track of the given type.\n *\n * Returns true when:\n * - Track of this type is not already selected\n *\n * Generic over track type - works for video, audio, or text.\n *\n * @TODO figure out reactive model for ABR cases - right now we're only selecting\n * if we have nothing selected (CJP)\n */\nexport function shouldSelectTrack(state: TrackSelectionState, type: TrackType): boolean {\n return !state[SelectedTrackIdKeyByType[type]];\n}\n"],"mappings":";;;;;;;;;;;AAmIA,SAAgB,oBAAuB,OAAU,QAA6B;CAC5E,KAAK,MAAM,OAAO,QAAQ;EACxB,MAAM,cAAc,OAAO;EAC3B,IAAI,gBAAgB,KAAA,KAAa,MAAM,SAAoB,aAAa,OAAO;CACjF;CACA,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,iBAAiB,cAAyC,MAAqC;CAC7G,OAAO,aAAa,eAAe,MAAM,QAAQ,IAAI,SAAS,IAAI,CAAC,EAAE,cAAc,EAAE,EAAE,OAAO,EAAE,EAAE;AACpG;;;;;;;AAqEA,SAAgB,wBACd,QACA,eAAuB,OAAO,mBACf;CACf,IAAI,OAAO,WAAW,GAAG,OAAO,KAAA;CAIhC,MAAM,SAAS,CAAC,GAAG,MAAM,CAAC,CAAC,MACxB,GAAG,OACD,EAAE,SAAS,MAAM,EAAE,UAAU,MAAM,EAAE,SAAS,MAAM,EAAE,UAAU,OAAO,EAAE,aAAa,MAAM,EAAE,aAAa,EAChH;CAEA,OAAO,OAAO,MAAM,OAAO,EAAE,SAAS,MAAM,EAAE,UAAU,MAAM,YAAY,KAAK,OAAO,OAAO,SAAS;AACxG;;;;;;;AAQA,SAAgB,gCAAgC,cAA6D;CAE3G,MAAM,UADW,aAAa,eAAe,MAAM,QAAQ,IAAI,SAAS,OAAO,EAAA,EACtD,cAAc,EAAE,EAAE;CAC3C,IAAI,CAAC,QAAQ,QAAQ,OAAO,KAAA;CAC5B,OAAO,wBAAwB,MAAM,CAAC,EAAE;AAC1C;;;;;;;;;;;;;AAcA,SAAgB,eACd,cACA,QACoB;CACpB,MAAM,WAAW,aAAa,eAAe,MAAM,QAAQ,IAAI,SAAS,OAAO;CAE/E,IAAI,CAAC,YAAY,SAAS,cAAc,WAAW,GACjD;CAIF,MAAM,eAAe,SAAS,cAAc;CAC5C,IAAI,CAAC,gBAAgB,aAAa,OAAO,WAAW,GAClD;CAGF,MAAM,SAAS,aAAa;CAG5B,IAAI,QAAQ,wBAAwB;EAClC,MAAM,gBAAgB,OAAO,MAAM,UAAU,MAAM,aAAa,OAAO,sBAAsB;EAC7F,IAAI,eACF,OAAO,cAAc;CAEzB;CAGA,MAAM,eAAe,OAAO,MAAM,UAAU,MAAM,YAAY,IAAI;CAClE,IAAI,cACF,OAAO,aAAa;CAItB,OAAO,OAAO,EAAE,EAAE;AACpB;;;;;;;;;;;;;AAoCA,SAAgB,wBACd,QACA,QACoB;CACpB,MAAM,kBAAkB,QAAQ,sBAAsB,SAAS,OAAO,QAAQ,UAAU,CAAC,MAAM,MAAM;CACrG,IAAI,gBAAgB,WAAW,GAAG,OAAO,KAAA;CAEzC,MAAM,EAAE,2BAA2B,qBAAqB,UAAU,UAAU,CAAC;CAE7E,IAAI,2BAA2B;EAC7B,MAAM,gBAAgB,gBAAgB,MAAM,UAAU,MAAM,aAAa,yBAAyB;EAClG,IAAI,eAAe,OAAO,cAAc;CAC1C;CAEA,IAAI,oBAAoB;EACtB,MAAM,eAAe,gBAAgB,MAAM,UAAU,MAAM,YAAY,IAAI;EAC3E,IAAI,cAAc,OAAO,aAAa;CACxC;AAGF"}
1
+ {"version":3,"file":"select-tracks.js","names":[],"sources":["../../../../src/media/primitives/select-tracks.ts"],"sourcesContent":["import type { MaybeResolvedPresentation, PartiallyResolvedTextTrack, TextTrack } from '../types';\n\n/**\n * State shape for track selection.\n */\nexport interface TrackSelectionState {\n presentation?: MaybeResolvedPresentation;\n selectedVideoTrackId?: string;\n selectedAudioTrackId?: string;\n selectedTextTrackId?: string;\n}\n\n/**\n * Configuration for audio track selection.\n */\nexport interface AudioSelectionConfig {\n /**\n * Preferred audio language (ISO 639 code, e.g., \"en\", \"es\").\n * If not specified, selects first audio track.\n */\n preferredAudioLanguage?: string;\n}\n\n/**\n * Configuration for text track selection.\n */\nexport interface TextSelectionConfig {\n /**\n * Preferred subtitle language (ISO 639 code, e.g., \"en\", \"es\").\n * If specified, selects matching track if available.\n */\n preferredSubtitleLanguage?: string;\n\n /**\n * Include FORCED subtitle tracks in selection.\n * Default: false (follows hls.js/http-streaming pattern)\n *\n * Note: Per Apple's HLS spec, if content has forced and regular subtitles\n * in the same language, the regular track MUST contain both forced and\n * regular content. Therefore, forced-only tracks are redundant and excluded\n * by default.\n */\n includeForcedTracks?: boolean;\n\n /**\n * Auto-select DEFAULT track (requires DEFAULT=YES + AUTOSELECT=YES in HLS).\n * Default: false (user opt-in, matches hls.js/http-streaming)\n *\n * When enabled, tracks marked with both DEFAULT=YES and AUTOSELECT=YES\n * will be automatically selected if no user preference matches.\n */\n enableDefaultTrack?: boolean;\n}\n\n// =============================================================================\n// Helper Functions (Pure Selection Logic)\n//\n// Candidate-list policies and track geometry, for the selection rules in\n// `playback/behaviors/select-tracks.ts` and `playback/behaviors/track-switching.ts`\n// to compose. Nothing here consults a whole presentation or returns a single id:\n// narrowing a list and ordering a list are the two shapes a rule can take, and the\n// rule chain takes the head of what they leave.\n// =============================================================================\n\n/**\n * Test whether a track matches a partial-track description: every present,\n * defined field of `filter` equals the track's. Absent or `undefined` filter\n * fields don't constrain. Used to narrow candidates by a user selection\n * (`{ id }`, `{ language }`, `{ height }`, …).\n *\n * @param track - The track to test\n * @param filter - Partial-track description; only present, defined fields constrain\n * @returns `true` when the track matches every constraining field\n */\nexport function matchesPartialTrack<T>(track: T, filter: Partial<T>): boolean {\n for (const key in filter) {\n const filterValue = filter[key as keyof T];\n if (filterValue !== undefined && track[key as keyof T] !== filterValue) return false;\n }\n return true;\n}\n\ntype RankableTrack = { id: string; width?: number; height?: number; bandwidth?: number };\n\n/** Missing dimensions are treated as area `0`, so a track without them ranks last. */\nfunction pixelArea(track: RankableTrack): number {\n return (track.width ?? 0) * (track.height ?? 0);\n}\n\n/**\n * Narrow to the tracks at or below `maxPixelArea`, and nothing else: no ordering,\n * no fallback. Survivors keep their incoming order, and an empty result is a real\n * answer — \"none of these fit\".\n *\n * Deliberately only the filter, because as a selection rule this composes under\n * `applyRules`, which already owns both halves a caller might expect here: an empty\n * result is skipped, so a preference can never narrow the candidate set to nothing;\n * and ordering the survivors is a separate rule's job\n * ({@link byDescendingResolution}, bandwidth ABR). Doing either here would duplicate\n * the composer and give one rule two responsibilities.\n */\nexport function tracksUnderPixelArea<T extends RankableTrack>(\n tracks: readonly T[],\n maxPixelArea: number = Number.POSITIVE_INFINITY\n): readonly T[] {\n return tracks.filter((track) => pixelArea(track) <= maxPixelArea);\n}\n\n/**\n * Compare two tracks by resolution, largest first, with bandwidth as the tiebreak\n * for renditions of identical dimensions. Missing dimensions are treated as area\n * `0`, so a track without them sorts last.\n *\n * A comparator rather than a \"highest track\" function: the selection-rule chain\n * takes the head of the list it produces, so ranking never has to collapse to a\n * single track. `preferHighestResolution` is `sort` over this and nothing more.\n */\nexport function byDescendingResolution(a: RankableTrack, b: RankableTrack): number {\n return pixelArea(b) - pixelArea(a) || (b.bandwidth ?? 0) - (a.bandwidth ?? 0);\n}\n\n/**\n * Default audio policy over a candidate list: the three-tier pick a selection-rule\n * chain applies once it has narrowed the candidates.\n *\n * Priority: `preferredAudioLanguage` match → `DEFAULT=YES` → first track.\n */\nexport function pickAudioTrackFromTracks(\n tracks: readonly { id: string; language?: string | undefined; default?: boolean | undefined }[],\n config?: AudioSelectionConfig\n): string | undefined {\n // Try preferred language first\n if (config?.preferredAudioLanguage) {\n const languageMatch = tracks.find((track) => track.language === config.preferredAudioLanguage);\n if (languageMatch) {\n return languageMatch.id;\n }\n }\n\n // Try default track\n const defaultTrack = tracks.find((track) => track.default === true);\n if (defaultTrack) {\n return defaultTrack.id;\n }\n\n // Fall back to first track\n return tracks[0]?.id;\n}\n\n/**\n * Default text-track policy over a candidate list: the opt-in three-tier pick\n * `switchTextTrack`'s terminal applies once it has narrowed the renditions to the\n * constrained, CDN-scoped set.\n *\n * Priority: `preferredSubtitleLanguage` match → `DEFAULT=YES + AUTOSELECT=YES`\n * (only when `enableDefaultTrack`) → `undefined` (opt-in). FORCED tracks are\n * excluded unless `includeForcedTracks` (Apple-spec: a regular track must carry\n * forced content when both exist, so a forced-only track is redundant).\n */\nexport function pickTextTrackFromTracks(\n tracks: readonly (PartiallyResolvedTextTrack | TextTrack)[],\n config?: TextSelectionConfig\n): string | undefined {\n const availableTracks = config?.includeForcedTracks ? tracks : tracks.filter((track) => !track.forced);\n if (availableTracks.length === 0) return undefined;\n\n const { preferredSubtitleLanguage, enableDefaultTrack = false } = config ?? {};\n\n if (preferredSubtitleLanguage) {\n const languageMatch = availableTracks.find((track) => track.language === preferredSubtitleLanguage);\n if (languageMatch) return languageMatch.id;\n }\n\n if (enableDefaultTrack) {\n const defaultTrack = availableTracks.find((track) => track.default === true);\n if (defaultTrack) return defaultTrack.id;\n }\n\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;AA0EA,SAAgB,oBAAuB,OAAU,QAA6B;CAC5E,KAAK,MAAM,OAAO,QAAQ;EACxB,MAAM,cAAc,OAAO;EAC3B,IAAI,gBAAgB,KAAA,KAAa,MAAM,SAAoB,aAAa,OAAO;CACjF;CACA,OAAO;AACT;;AAKA,SAAS,UAAU,OAA8B;CAC/C,QAAQ,MAAM,SAAS,MAAM,MAAM,UAAU;AAC/C;;;;;;;;;;;;;AAcA,SAAgB,qBACd,QACA,eAAuB,OAAO,mBAChB;CACd,OAAO,OAAO,QAAQ,UAAU,UAAU,KAAK,KAAK,YAAY;AAClE;;;;;;;;;;AAWA,SAAgB,uBAAuB,GAAkB,GAA0B;CACjF,OAAO,UAAU,CAAC,IAAI,UAAU,CAAC,MAAM,EAAE,aAAa,MAAM,EAAE,aAAa;AAC7E;;;;;;;AAQA,SAAgB,yBACd,QACA,QACoB;CAEpB,IAAI,QAAQ,wBAAwB;EAClC,MAAM,gBAAgB,OAAO,MAAM,UAAU,MAAM,aAAa,OAAO,sBAAsB;EAC7F,IAAI,eACF,OAAO,cAAc;CAEzB;CAGA,MAAM,eAAe,OAAO,MAAM,UAAU,MAAM,YAAY,IAAI;CAClE,IAAI,cACF,OAAO,aAAa;CAItB,OAAO,OAAO,EAAE,EAAE;AACpB;;;;;;;;;;;AAYA,SAAgB,wBACd,QACA,QACoB;CACpB,MAAM,kBAAkB,QAAQ,sBAAsB,SAAS,OAAO,QAAQ,UAAU,CAAC,MAAM,MAAM;CACrG,IAAI,gBAAgB,WAAW,GAAG,OAAO,KAAA;CAEzC,MAAM,EAAE,2BAA2B,qBAAqB,UAAU,UAAU,CAAC;CAE7E,IAAI,2BAA2B;EAC7B,MAAM,gBAAgB,gBAAgB,MAAM,UAAU,MAAM,aAAa,yBAAyB;EAClG,IAAI,eAAe,OAAO,cAAc;CAC1C;CAEA,IAAI,oBAAoB;EACtB,MAAM,eAAe,gBAAgB,MAAM,UAAU,MAAM,YAAY,IAAI;EAC3E,IAAI,cAAc,OAAO,aAAa;CACxC;AAGF"}
@@ -1,3 +1,4 @@
1
+ import { HlsVideoMediaError } from "./playback/adapters/hls-video/error-surface.js";
1
2
  import { HlsBackgroundVideoMediaAPI, HlsBackgroundVideoMediaElement, HlsBackgroundVideoMediaMixin, HlsBackgroundVideoMediaProps, hlsBackgroundVideoMediaDefaultProps } from "./playback/adapters/hls-background-video/adapter.js";
2
3
  import { HlsBackgroundVideoMedia } from "./playback/adapters/hls-background-video/media.js";
3
- export { HlsBackgroundVideoMedia as MuxBackgroundVideoMedia, type HlsBackgroundVideoMediaAPI as MuxBackgroundVideoMediaAPI, HlsBackgroundVideoMediaElement as MuxBackgroundVideoMediaElement, HlsBackgroundVideoMediaMixin as MuxBackgroundVideoMediaMixin, type HlsBackgroundVideoMediaProps as MuxBackgroundVideoMediaProps, hlsBackgroundVideoMediaDefaultProps as muxBackgroundVideoMediaDefaultProps };
4
+ export { type HlsVideoMediaError, HlsBackgroundVideoMedia as MuxBackgroundVideoMedia, type HlsBackgroundVideoMediaAPI as MuxBackgroundVideoMediaAPI, HlsBackgroundVideoMediaElement as MuxBackgroundVideoMediaElement, HlsBackgroundVideoMediaMixin as MuxBackgroundVideoMediaMixin, type HlsBackgroundVideoMediaProps as MuxBackgroundVideoMediaProps, hlsBackgroundVideoMediaDefaultProps as muxBackgroundVideoMediaDefaultProps };
@@ -1,4 +1,5 @@
1
1
  import { Composition } from "../../../core/composition/create-composition.js";
2
+ import { HlsVideoMediaError } from "../hls-video/error-surface.js";
2
3
  import { BackgroundVideoEngineContext, BackgroundVideoEngineState } from "../../engines/hls/engine-background-video.js";
3
4
  import { Constructor, MixinReturn } from "@videojs/utils/types";
4
5
  //#region src/playback/adapters/hls-background-video/adapter.d.ts
@@ -8,6 +9,7 @@ interface HlsBackgroundVideoMediaProps {
8
9
  declare const hlsBackgroundVideoMediaDefaultProps: HlsBackgroundVideoMediaProps;
9
10
  interface HlsBackgroundVideoMediaAPI extends HlsBackgroundVideoMediaProps {
10
11
  readonly engine: Composition<BackgroundVideoEngineState, BackgroundVideoEngineContext>;
12
+ readonly error: HlsVideoMediaError | null;
11
13
  attach(mediaElement: HTMLMediaElement): void;
12
14
  detach(): void;
13
15
  destroy(): void;
@@ -17,12 +19,26 @@ interface HlsBackgroundVideoMediaAPI extends HlsBackgroundVideoMediaProps {
17
19
  * Mixin that adds the background-video SPF playback engine to any base class,
18
20
  * for an HLS URL.
19
21
  *
20
- * `src` is the whole surface, and the picker always pins the top rendition on
21
- * offer. There is no cap of its own because the manifest is the better place to
22
- * narrow one: a delivery param `?max_resolution=720p` on a Mux stream URL, for
23
- * one keeps the renditions it excludes out of the manifest entirely, rather
24
- * than fetched-then-unpicked. Deriving a cap from the screen instead is on the
25
- * roadmap, and lands here when it does.
22
+ * `src` is the whole input surface, and `error` is the one output: nothing about
23
+ * an unplayable source reaches the media element on its own here an unsupported
24
+ * container, encryption with no EME, and an undecodable codec all leave
25
+ * `HTMLMediaElement.error` null with the element stalled at `readyState 0`
26
+ * (measured on Chromium and WebKit) so a consumer that watched only the
27
+ * `<video>` would see a source that never appears and never says why. The engine
28
+ * reports each condition onto `engine.state.errors` and logs it; this adapter
29
+ * promotes the first fatal one, mapping it the same way the video and audio Medias
30
+ * map theirs. See `internal/design/spf/features/errors.md`.
31
+ *
32
+ * Selection pins the largest rendition that *fits the screen*, and holds it for
33
+ * the session. The manifest is still the better place to narrow further: a
34
+ * delivery param — `?max_resolution=720p` on a Mux stream URL, for one — keeps
35
+ * the renditions it excludes out of the manifest entirely, rather than
36
+ * fetched-then-unpicked.
37
+ *
38
+ * The pin is given up, never moved, if the pick turns out to be unplayable: the
39
+ * container is only known once a media playlist resolves, which is after the pick
40
+ * is made, so the selection clears rather than quietly appending bytes nothing can
41
+ * decode.
26
42
  *
27
43
  * `@videojs/spf/mux-background-video` is this same Media under a Mux-flavored
28
44
  * name — an alias, not a variant. Nothing about the surface changes with the
@@ -40,6 +56,8 @@ interface HlsBackgroundVideoMediaAPI extends HlsBackgroundVideoMediaProps {
40
56
  * engine instance and the attached media element are both kept, so neither has to
41
57
  * be rewired.
42
58
  *
59
+ * @fires error - Fired when a fatal condition is reported. Read `error` for it.
60
+ *
43
61
  * @example
44
62
  * class HlsBackgroundVideoMedia extends HlsBackgroundVideoMediaMixin(BackgroundVideoHost) {}
45
63
  *
@@ -55,5 +73,5 @@ declare const HlsBackgroundVideoMediaElement_base: MixinReturn<{
55
73
  /** Standalone SPF background-video adapter with no base class. */
56
74
  declare class HlsBackgroundVideoMediaElement extends HlsBackgroundVideoMediaElement_base {}
57
75
  //#endregion
58
- export { HlsBackgroundVideoMediaAPI, HlsBackgroundVideoMediaElement, HlsBackgroundVideoMediaMixin, HlsBackgroundVideoMediaProps, hlsBackgroundVideoMediaDefaultProps };
76
+ export { HlsBackgroundVideoMediaAPI, HlsBackgroundVideoMediaElement, HlsBackgroundVideoMediaMixin, HlsBackgroundVideoMediaProps, type HlsVideoMediaError, hlsBackgroundVideoMediaDefaultProps };
59
77
  //# sourceMappingURL=adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","names":[],"sources":["../../../../../src/playback/adapters/hls-background-video/adapter.ts"],"mappings":";;;;UAYiB;EACf;;cAGW,qCAAqC;UAIjC,mCAAmC;WACzC,QAAQ,YAAY,4BAA4B;EACzD,OAAO,cAAc;EACrB;EACA;EACA,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsCM,6BAA6B,aAAa,kBAAkB,WAAW,OAiIpC,YAAY,MAAM;;;;;cAIxD,uCAAuC"}
1
+ {"version":3,"file":"adapter.d.ts","names":[],"sources":["../../../../../src/playback/adapters/hls-background-video/adapter.ts"],"mappings":";;;;;UAyBiB;EACf;;cAGW,qCAAqC;UAIjC,mCAAmC;WACzC,QAAQ,YAAY,4BAA4B;WAChD,OAAO;EAChB,OAAO,cAAc;EACrB;EACA;EACA,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmFM,6BAA6B,aAAa,kBAAkB,WAAW,OAgMpC,YAAY,MAAM;;;;;cAIxD,uCAAuC"}