@pie-players/pie-players-shared 0.3.67 → 0.3.69

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 (87) hide show
  1. package/README.md +59 -0
  2. package/dist/formative/index.d.ts +16 -0
  3. package/dist/formative/index.js +5 -0
  4. package/dist/formative/mastery.d.ts +16 -0
  5. package/dist/formative/mastery.js +43 -0
  6. package/dist/formative/outcome.d.ts +19 -0
  7. package/dist/formative/outcome.js +90 -0
  8. package/dist/formative/policy.d.ts +37 -0
  9. package/dist/formative/policy.js +86 -0
  10. package/dist/formative/session.d.ts +19 -0
  11. package/dist/formative/session.js +113 -0
  12. package/dist/formative/state.d.ts +60 -0
  13. package/dist/formative/state.js +140 -0
  14. package/dist/formative/types.d.ts +164 -0
  15. package/dist/formative/types.js +13 -0
  16. package/dist/i18n/catalogs.d.ts +38 -0
  17. package/dist/i18n/catalogs.js +47 -0
  18. package/dist/i18n/index.d.ts +28 -4
  19. package/dist/i18n/index.js +34 -3
  20. package/dist/i18n/language-tags.d.ts +51 -0
  21. package/dist/i18n/language-tags.js +84 -0
  22. package/dist/i18n/messages/en-US.d.ts +601 -0
  23. package/dist/i18n/messages/en-US.js +617 -0
  24. package/dist/i18n/messages/nl-NL.d.ts +516 -0
  25. package/dist/i18n/messages/nl-NL.js +516 -0
  26. package/dist/i18n/provider.d.ts +168 -0
  27. package/dist/i18n/provider.js +472 -0
  28. package/dist/i18n/types.d.ts +177 -28
  29. package/dist/i18n/types.js +6 -2
  30. package/dist/index.d.ts +3 -1
  31. package/dist/index.js +2 -0
  32. package/dist/pie/custom-element-define.d.ts +24 -0
  33. package/dist/pie/custom-element-define.js +36 -6
  34. package/dist/pie/index.d.ts +1 -1
  35. package/dist/pie/index.js +1 -1
  36. package/dist/pie/versioned-tag.js +3 -2
  37. package/dist/security/sanitize-forbidden-lists.d.ts +8 -0
  38. package/dist/security/sanitize-forbidden-lists.js +41 -0
  39. package/dist/security/sanitize-item-markup.js +3 -36
  40. package/dist/security/sanitize-svg-icon.js +3 -25
  41. package/dist/security/wrap-overwide-images.d.ts +5 -10
  42. package/dist/security/wrap-overwide-images.js +13 -62
  43. package/dist/security/wrap-overwide-tables.d.ts +5 -10
  44. package/dist/security/wrap-overwide-tables.js +13 -62
  45. package/dist/security/wrap-overwide.d.ts +51 -0
  46. package/dist/security/wrap-overwide.js +91 -0
  47. package/dist/server/npm-auth-env.d.ts +13 -0
  48. package/dist/server/npm-auth-env.js +64 -0
  49. package/dist/timed-media/index.d.ts +17 -0
  50. package/dist/timed-media/index.js +16 -0
  51. package/dist/timed-media/media-element-source.d.ts +36 -0
  52. package/dist/timed-media/media-element-source.js +121 -0
  53. package/dist/timed-media/policy.d.ts +36 -0
  54. package/dist/timed-media/policy.js +297 -0
  55. package/dist/timed-media/session.d.ts +21 -0
  56. package/dist/timed-media/session.js +85 -0
  57. package/dist/timed-media/state.d.ts +66 -0
  58. package/dist/timed-media/state.js +357 -0
  59. package/dist/timed-media/types.d.ts +343 -0
  60. package/dist/timed-media/types.js +12 -0
  61. package/dist/tools/term-lookup.d.ts +189 -0
  62. package/dist/tools/term-lookup.js +284 -0
  63. package/dist/types/index.d.ts +105 -1
  64. package/dist/ui/attribute-coercion.d.ts +1 -0
  65. package/dist/ui/attribute-coercion.js +19 -0
  66. package/dist/ui/first-focusable.d.ts +22 -0
  67. package/dist/ui/first-focusable.js +47 -0
  68. package/dist/ui/focus-trap.js +7 -2
  69. package/dist/ui/pointer-drag.d.ts +30 -0
  70. package/dist/ui/pointer-drag.js +34 -0
  71. package/package.json +38 -6
  72. package/dist/i18n/loader.d.ts +0 -35
  73. package/dist/i18n/loader.js +0 -132
  74. package/dist/i18n/simple-i18n.d.ts +0 -68
  75. package/dist/i18n/simple-i18n.js +0 -198
  76. package/dist/i18n/translations/ar/common.json +0 -36
  77. package/dist/i18n/translations/ar/toolkit.json +0 -48
  78. package/dist/i18n/translations/ar/tools.json +0 -103
  79. package/dist/i18n/translations/en/common.json +0 -36
  80. package/dist/i18n/translations/en/toolkit.json +0 -48
  81. package/dist/i18n/translations/en/tools.json +0 -103
  82. package/dist/i18n/translations/es/common.json +0 -36
  83. package/dist/i18n/translations/es/toolkit.json +0 -48
  84. package/dist/i18n/translations/es/tools.json +0 -103
  85. package/dist/i18n/translations/zh/common.json +0 -36
  86. package/dist/i18n/translations/zh/toolkit.json +0 -48
  87. package/dist/i18n/translations/zh/tools.json +0 -103
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Validation for authored timed-media section data.
3
+ *
4
+ * Authored, wire-facing and untrusted, so every field is checked here and the
5
+ * caller never hand-parses. Errors are returned rather than thrown: the section
6
+ * reports them as a framework error and delivers as an ordinary section, which is
7
+ * loud without costing the learner the content.
8
+ */
9
+ /**
10
+ * Applied when `playbackPolicy` is absent entirely. The restrictive values are
11
+ * the defaults because a timed-media section is authored to sequence: a section
12
+ * that forgot the policy block reads as one that wanted sequencing, and the
13
+ * permissive reading would silently deliver an unsequenced video.
14
+ */
15
+ export const TIMED_MEDIA_PLAYBACK_POLICY_DEFAULTS = {
16
+ allowSeekAhead: false,
17
+ pauseOnRequiredCue: true,
18
+ requireMediaCompletion: false,
19
+ };
20
+ const ACTIVATIONS = [
21
+ "reveal",
22
+ "gate",
23
+ "metadata",
24
+ ];
25
+ const GATE_CONDITIONS = [
26
+ "responded",
27
+ "correct",
28
+ "partial-or-better",
29
+ ];
30
+ const UNKNOWN_CORRECTNESS = [
31
+ "release",
32
+ "hold",
33
+ ];
34
+ const SCORING_STRATEGIES = [
35
+ "sum-child-outcomes",
36
+ "average-child-outcomes",
37
+ "host-defined",
38
+ ];
39
+ /** A gate on correctness has to say what an unscorable item does. */
40
+ function isCorrectnessCondition(condition) {
41
+ return condition === "correct" || condition === "partial-or-better";
42
+ }
43
+ function trimmed(value) {
44
+ return typeof value === "string" ? value.trim() : "";
45
+ }
46
+ function normalizeRange(value) {
47
+ if (!value || typeof value !== "object")
48
+ return null;
49
+ const raw = value;
50
+ const start = Number(raw.startSeconds);
51
+ if (!Number.isFinite(start) || start < 0)
52
+ return null;
53
+ const end = Number(raw.endSeconds);
54
+ // An end at or before the start is a window that can never contain a
55
+ // position, so it is treated as no end — the same rule
56
+ // `normalizeMediaFragment` applies in the toolkit, kept identical on purpose.
57
+ if (!Number.isFinite(end) || end <= start)
58
+ return { startSeconds: start };
59
+ return { startSeconds: start, endSeconds: end };
60
+ }
61
+ function normalizePlaybackPolicy(value) {
62
+ if (value === undefined || value === null) {
63
+ return { policy: { ...TIMED_MEDIA_PLAYBACK_POLICY_DEFAULTS }, error: null };
64
+ }
65
+ if (typeof value !== "object") {
66
+ return {
67
+ policy: { ...TIMED_MEDIA_PLAYBACK_POLICY_DEFAULTS },
68
+ error: {
69
+ code: "invalid-playback-policy",
70
+ message: "timedMedia.playbackPolicy must be an object.",
71
+ },
72
+ };
73
+ }
74
+ const raw = value;
75
+ return {
76
+ policy: {
77
+ allowSeekAhead: typeof raw.allowSeekAhead === "boolean"
78
+ ? raw.allowSeekAhead
79
+ : TIMED_MEDIA_PLAYBACK_POLICY_DEFAULTS.allowSeekAhead,
80
+ pauseOnRequiredCue: typeof raw.pauseOnRequiredCue === "boolean"
81
+ ? raw.pauseOnRequiredCue
82
+ : TIMED_MEDIA_PLAYBACK_POLICY_DEFAULTS.pauseOnRequiredCue,
83
+ requireMediaCompletion: typeof raw.requireMediaCompletion === "boolean"
84
+ ? raw.requireMediaCompletion
85
+ : TIMED_MEDIA_PLAYBACK_POLICY_DEFAULTS.requireMediaCompletion,
86
+ },
87
+ error: null,
88
+ };
89
+ }
90
+ function normalizeScoringPolicy(value) {
91
+ if (value === undefined || value === null) {
92
+ return { policy: undefined, error: null };
93
+ }
94
+ const strategy = value && typeof value === "object"
95
+ ? value.strategy
96
+ : undefined;
97
+ const known = SCORING_STRATEGIES.find((candidate) => candidate === strategy);
98
+ if (!known) {
99
+ return {
100
+ policy: undefined,
101
+ error: {
102
+ code: "invalid-scoring-policy",
103
+ message: `timedMedia.scoringPolicy.strategy must be one of ${SCORING_STRATEGIES.join(", ")}.`,
104
+ },
105
+ };
106
+ }
107
+ return { policy: { strategy: known }, error: null };
108
+ }
109
+ /**
110
+ * Validate authored `timedMedia` against the section it belongs to.
111
+ *
112
+ * `resolveStimulusRenderableId` is supplied by the caller because the mapping
113
+ * from an authored `stimulusRef` to a rendered renderable id belongs to the
114
+ * content service that assigned that id — deriving it twice is two
115
+ * implementations of one mapping.
116
+ */
117
+ export function normalizeTimedMediaSectionData(args) {
118
+ const errors = [];
119
+ if (!args.timedMedia || typeof args.timedMedia !== "object") {
120
+ return {
121
+ data: null,
122
+ errors: [
123
+ {
124
+ code: "missing-stimulus-ref",
125
+ message: "timedMedia must be an object carrying a stimulusRef.",
126
+ },
127
+ ],
128
+ };
129
+ }
130
+ const raw = args.timedMedia;
131
+ const stimulusRef = trimmed(raw.stimulusRef);
132
+ if (!stimulusRef) {
133
+ errors.push({
134
+ code: "missing-stimulus-ref",
135
+ message: "timedMedia.stimulusRef is required.",
136
+ });
137
+ }
138
+ const stimulusRenderableId = stimulusRef
139
+ ? args.resolveStimulusRenderableId(stimulusRef)
140
+ : undefined;
141
+ if (stimulusRef && !stimulusRenderableId) {
142
+ errors.push({
143
+ code: "unresolved-stimulus-ref",
144
+ message: `timedMedia.stimulusRef "${stimulusRef}" does not name a renderable in this section.`,
145
+ });
146
+ }
147
+ const { policy: playbackPolicy, error: playbackError } = normalizePlaybackPolicy(raw.playbackPolicy);
148
+ if (playbackError)
149
+ errors.push(playbackError);
150
+ const { policy: scoringPolicy, error: scoringError } = normalizeScoringPolicy(raw.scoringPolicy);
151
+ if (scoringError)
152
+ errors.push(scoringError);
153
+ const allowedItemIds = new Set(args.itemIdentifiers);
154
+ const rawCues = Array.isArray(raw.cues) ? raw.cues : [];
155
+ if (rawCues.length === 0) {
156
+ errors.push({
157
+ code: "no-cues",
158
+ message: "timedMedia.cues must hold at least one cue.",
159
+ });
160
+ }
161
+ const seenIdentifiers = new Set();
162
+ const cues = [];
163
+ for (const [index, entry] of rawCues.entries()) {
164
+ const cue = (entry ?? {});
165
+ const identifier = trimmed(cue.identifier);
166
+ if (!identifier) {
167
+ errors.push({
168
+ code: "invalid-cue-identifier",
169
+ message: `timedMedia.cues[${index}] has no identifier.`,
170
+ });
171
+ continue;
172
+ }
173
+ if (seenIdentifiers.has(identifier)) {
174
+ errors.push({
175
+ code: "duplicate-cue-identifier",
176
+ message: `Cue identifier "${identifier}" appears more than once.`,
177
+ cueIdentifier: identifier,
178
+ });
179
+ continue;
180
+ }
181
+ seenIdentifiers.add(identifier);
182
+ const range = normalizeRange(cue.range);
183
+ if (!range) {
184
+ errors.push({
185
+ code: "invalid-cue-range",
186
+ message: `Cue "${identifier}" needs a range with a finite, non-negative startSeconds.`,
187
+ cueIdentifier: identifier,
188
+ });
189
+ continue;
190
+ }
191
+ const cuePolicy = (cue.policy ?? {});
192
+ const activation = ACTIVATIONS.find((candidate) => candidate === cuePolicy.activation);
193
+ if (!activation) {
194
+ errors.push({
195
+ code: "invalid-cue-activation",
196
+ message: `Cue "${identifier}" needs policy.activation of ${ACTIVATIONS.join(", ")}.`,
197
+ cueIdentifier: identifier,
198
+ });
199
+ continue;
200
+ }
201
+ const rawItemRefs = Array.isArray(cue.itemRefs) ? cue.itemRefs : [];
202
+ const itemRefs = [];
203
+ for (const rawItemRef of rawItemRefs) {
204
+ const itemRef = trimmed(rawItemRef);
205
+ if (!itemRef)
206
+ continue;
207
+ if (!allowedItemIds.has(itemRef)) {
208
+ // Reported rather than dropped quietly: a cue naming an item the
209
+ // section does not hold is the exact authoring slip that produces a
210
+ // checkpoint the learner never sees.
211
+ errors.push({
212
+ code: "unknown-item-ref",
213
+ message: `Cue "${identifier}" names item ref "${itemRef}", which is not in this section.`,
214
+ cueIdentifier: identifier,
215
+ });
216
+ continue;
217
+ }
218
+ if (!itemRefs.includes(itemRef))
219
+ itemRefs.push(itemRef);
220
+ }
221
+ if (itemRefs.length === 0 && activation !== "metadata") {
222
+ errors.push({
223
+ code: "missing-item-refs",
224
+ message: `Cue "${identifier}" activates no item; only a metadata cue may name none.`,
225
+ cueIdentifier: identifier,
226
+ });
227
+ continue;
228
+ }
229
+ let releaseOn;
230
+ let onUnknownCorrectness;
231
+ if (activation === "gate") {
232
+ releaseOn = GATE_CONDITIONS.find((candidate) => candidate === cuePolicy.releaseOn);
233
+ if (!releaseOn) {
234
+ errors.push({
235
+ code: "missing-release-condition",
236
+ message: `Gate cue "${identifier}" needs policy.releaseOn of ${GATE_CONDITIONS.join(", ")}.`,
237
+ cueIdentifier: identifier,
238
+ });
239
+ continue;
240
+ }
241
+ if (isCorrectnessCondition(releaseOn)) {
242
+ onUnknownCorrectness = UNKNOWN_CORRECTNESS.find((candidate) => candidate === cuePolicy.onUnknownCorrectness);
243
+ if (!onUnknownCorrectness) {
244
+ errors.push({
245
+ code: "missing-unknown-correctness",
246
+ message: `Gate cue "${identifier}" releases on correctness, so policy.onUnknownCorrectness must state "release" or "hold" for an item no controller can score.`,
247
+ cueIdentifier: identifier,
248
+ });
249
+ continue;
250
+ }
251
+ // A correctness gate is only passable while the learner still has a Try
252
+ // to spend. Over an item with a finite budget the gate becomes
253
+ // unpassable the moment the budget runs out, and over an item that does
254
+ // not deliver formatively there is never a Try at all — in both cases
255
+ // nothing the learner can do releases playback, and no host action
256
+ // releases it either, because a forced reveal is not a correct answer.
257
+ // Refused here rather than released at runtime: releasing would deliver
258
+ // the opposite of what the author wrote, which is the silent degradation
259
+ // this contract exists to avoid.
260
+ const unpassable = itemRefs.filter((itemRef) => args.resolveItemTryBudget(itemRef) !== "unlimited");
261
+ if (unpassable.length > 0) {
262
+ errors.push({
263
+ code: "gate-requires-unlimited-tries",
264
+ message: `Gate cue "${identifier}" releases on correctness, so every item it names needs formative delivery with maxTries: "unlimited" — otherwise a learner who runs out of Tries can never release playback. Offending item refs: ${unpassable.join(", ")}.`,
265
+ cueIdentifier: identifier,
266
+ });
267
+ continue;
268
+ }
269
+ }
270
+ }
271
+ cues.push({
272
+ identifier,
273
+ range,
274
+ itemRefs,
275
+ activation,
276
+ releaseOn,
277
+ onUnknownCorrectness,
278
+ // A policy that switched pausing off wholesale turns every gate into a
279
+ // reveal, which is the author's own choice and needs no warning.
280
+ holdsPlayback: activation === "gate" && playbackPolicy.pauseOnRequiredCue,
281
+ });
282
+ }
283
+ if (errors.length > 0)
284
+ return { data: null, errors };
285
+ return {
286
+ data: {
287
+ stimulusRef,
288
+ stimulusRenderableId: stimulusRenderableId ?? "",
289
+ // Cue order is authored order, not time order: two cues may share a start
290
+ // and the author's sequence decides which gate holds first.
291
+ cues,
292
+ playbackPolicy,
293
+ scoringPolicy,
294
+ },
295
+ errors: [],
296
+ };
297
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Persistence for the timed-media slice.
3
+ *
4
+ * Same posture as the formative slice: an unreadable slice is rejected whole and
5
+ * delivery starts from clean cue state, while item sessions in the same snapshot
6
+ * are normalized separately and survive. A version bump here must never cost a
7
+ * learner their responses.
8
+ */
9
+ import type { ResolvedTimedMediaSectionData, TimedMediaSectionSessionSlice } from "./types.js";
10
+ export declare function toTimedMediaSectionSlice(state: TimedMediaSectionSessionSlice): TimedMediaSectionSessionSlice;
11
+ /**
12
+ * Validate a persisted slice against the section it is restored into.
13
+ *
14
+ * Returns `null` for an absent slice or one this build cannot read — the caller
15
+ * treats both as "start clean", and the two are indistinguishable, which is what
16
+ * keeps pre-timed-media snapshots valid.
17
+ */
18
+ export declare function normalizeTimedMediaSectionSlice(args: {
19
+ slice: unknown;
20
+ data: ResolvedTimedMediaSectionData | null;
21
+ }): TimedMediaSectionSessionSlice | null;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Persistence for the timed-media slice.
3
+ *
4
+ * Same posture as the formative slice: an unreadable slice is rejected whole and
5
+ * delivery starts from clean cue state, while item sessions in the same snapshot
6
+ * are normalized separately and survive. A version bump here must never cost a
7
+ * learner their responses.
8
+ */
9
+ import { TIMED_MEDIA_SLICE_VERSION, createTimedMediaState } from "./state.js";
10
+ function readNonNegativeNumber(value) {
11
+ const numeric = typeof value === "number" ? value : Number.NaN;
12
+ if (!Number.isFinite(numeric) || numeric < 0)
13
+ return undefined;
14
+ return numeric;
15
+ }
16
+ function readIdentifierList(value, allowed) {
17
+ if (!Array.isArray(value))
18
+ return [];
19
+ const out = [];
20
+ for (const entry of value) {
21
+ if (typeof entry !== "string")
22
+ continue;
23
+ const identifier = entry.trim();
24
+ // Filtered against the cues this section actually holds, matching how
25
+ // `normalizeApplySession` filters item sessions: a snapshot taken against
26
+ // an earlier revision of the section must not resurrect a cue that is gone.
27
+ if (!identifier || !allowed.has(identifier))
28
+ continue;
29
+ if (out.includes(identifier))
30
+ continue;
31
+ out.push(identifier);
32
+ }
33
+ return out;
34
+ }
35
+ export function toTimedMediaSectionSlice(state) {
36
+ return {
37
+ ...state,
38
+ version: TIMED_MEDIA_SLICE_VERSION,
39
+ visitedCueIdentifiers: [...state.visitedCueIdentifiers],
40
+ completedCueIdentifiers: [...state.completedCueIdentifiers],
41
+ };
42
+ }
43
+ /**
44
+ * Validate a persisted slice against the section it is restored into.
45
+ *
46
+ * Returns `null` for an absent slice or one this build cannot read — the caller
47
+ * treats both as "start clean", and the two are indistinguishable, which is what
48
+ * keeps pre-timed-media snapshots valid.
49
+ */
50
+ export function normalizeTimedMediaSectionSlice(args) {
51
+ const { slice, data } = args;
52
+ if (!data)
53
+ return null;
54
+ if (!slice || typeof slice !== "object")
55
+ return null;
56
+ const raw = slice;
57
+ if (raw.version !== TIMED_MEDIA_SLICE_VERSION)
58
+ return null;
59
+ const allowedCueIds = new Set(data.cues.map((cue) => cue.identifier));
60
+ const base = createTimedMediaState();
61
+ const mediaCurrentTime = readNonNegativeNumber(raw.mediaCurrentTime) ?? base.mediaCurrentTime;
62
+ const visitedCueIdentifiers = readIdentifierList(raw.visitedCueIdentifiers, allowedCueIds);
63
+ const completedCueIdentifiers = readIdentifierList(raw.completedCueIdentifiers, allowedCueIds);
64
+ const activeCueIdentifier = typeof raw.activeCueIdentifier === "string" &&
65
+ allowedCueIds.has(raw.activeCueIdentifier)
66
+ ? raw.activeCueIdentifier
67
+ : undefined;
68
+ return {
69
+ version: TIMED_MEDIA_SLICE_VERSION,
70
+ mediaCurrentTime,
71
+ // Never below the restored position: a snapshot written before
72
+ // `maxPositionSeconds` existed, or one carrying a lower value than the
73
+ // position it also records, would hand back timeline the learner already
74
+ // spent under `allowSeekAhead: false`.
75
+ maxPositionSeconds: Math.max(readNonNegativeNumber(raw.maxPositionSeconds) ?? 0, mediaCurrentTime),
76
+ mediaCompleted: raw.mediaCompleted === true,
77
+ visitedCueIdentifiers,
78
+ // A cue cannot be complete without having been visited; a snapshot that says
79
+ // otherwise is inconsistent, and the visited set is the one that decides what
80
+ // is on screen.
81
+ completedCueIdentifiers: completedCueIdentifiers.filter((identifier) => visitedCueIdentifiers.includes(identifier)),
82
+ activeCueIdentifier,
83
+ aggregateComplete: raw.aggregateComplete === true,
84
+ };
85
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The cue reduction: media position plus delivery state in, cue state and the
3
+ * effects the port must be driven with out.
4
+ *
5
+ * Pure and Node-safe, which is what makes cue policy testable without a browser
6
+ * and what keeps it out of every layout that hosts timed media. The caller owns
7
+ * the port and applies the effects; nothing here touches media.
8
+ */
9
+ import type { MediaTimeSourceCapabilities, ResolvedTimedMediaSectionData, TimedMediaDegradation, TimedMediaDeliveryState, TimedMediaInput, TimedMediaReduction, TimedMediaSectionProjection, TimedMediaSectionSessionSlice } from "./types.js";
10
+ export declare const TIMED_MEDIA_SLICE_VERSION: 1;
11
+ export declare function createTimedMediaState(): TimedMediaSectionSessionSlice;
12
+ /**
13
+ * Advance cue state.
14
+ *
15
+ * Two things are monotonic by design. A cue stays visited and its items stay
16
+ * revealed once reached, because un-revealing an item a learner has already
17
+ * answered would take a response off the screen. And a released gate stays
18
+ * complete, so a retry that lowers correctness cannot re-trap a learner
19
+ * mid-playback — the same reasoning that keeps `firstCorrectTry` from being
20
+ * overwritten by a later incorrect Try.
21
+ */
22
+ export declare function reduceTimedMediaState(args: {
23
+ state: TimedMediaSectionSessionSlice;
24
+ data: ResolvedTimedMediaSectionData;
25
+ delivery: TimedMediaDeliveryState;
26
+ input: TimedMediaInput;
27
+ }): TimedMediaReduction;
28
+ /**
29
+ * Which policies the attached port can actually carry out.
30
+ *
31
+ * Absent a port there is nothing to degrade *from* yet, so an unattached section
32
+ * reports its authored intent rather than a capability gap it has not measured.
33
+ */
34
+ export declare function resolveTimedMediaEnforcement(args: {
35
+ playbackPolicy: ResolvedTimedMediaSectionData["playbackPolicy"];
36
+ capabilities: MediaTimeSourceCapabilities | null;
37
+ hasGate: boolean;
38
+ }): {
39
+ pause: "enforced" | "advisory";
40
+ seek: "enforced" | "advisory";
41
+ degradations: TimedMediaDegradation[];
42
+ };
43
+ /**
44
+ * Derive the view layouts read. Recomputed from state on every republish rather
45
+ * than stored, so there is one source of truth and no mirror to go stale.
46
+ */
47
+ export declare function resolveTimedMediaProjection(args: {
48
+ data: ResolvedTimedMediaSectionData;
49
+ state: TimedMediaSectionSessionSlice;
50
+ delivery: TimedMediaDeliveryState;
51
+ capabilities: MediaTimeSourceCapabilities | null;
52
+ }): TimedMediaSectionProjection;
53
+ /**
54
+ * The projection facts that change what a layout renders, as one string.
55
+ *
56
+ * One implementation for the two callers that have to agree: `SectionController`
57
+ * decides whether to emit on it, and the toolkit folds it into the composition
58
+ * revision key. Two encodings drift silently in either direction — the controller
59
+ * emits a change the toolkit coalesces away, or the toolkit republishes for a
60
+ * change the controller never announced.
61
+ *
62
+ * `mediaCurrentTime` is absent because it moves about four times a second and
63
+ * nothing renders it. `maxPositionSeconds` enters only as a coarse bucket; see
64
+ * `POSITION_PERSIST_BUCKET_SECONDS`.
65
+ */
66
+ export declare function timedMediaProjectionSignature(projection: TimedMediaSectionProjection | null | undefined): string;