@intellectif/lk-react 15.0.0 → 16.1.0

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 (129) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +3 -3
  3. package/dist/{WrittenResponse-BPbhERgk.d.cts → WrittenResponse-6tSj6U3G.d.cts} +1 -1
  4. package/dist/{WrittenResponse-ZAxLR_AU.d.ts → WrittenResponse-xJ7I_5yq.d.ts} +1 -1
  5. package/dist/{chunk-53YOZOGN.cjs → chunk-2S3MK4VX.cjs} +5 -5
  6. package/dist/{chunk-53YOZOGN.cjs.map → chunk-2S3MK4VX.cjs.map} +1 -1
  7. package/dist/{chunk-4H3JTX74.cjs → chunk-2TFNDHJ5.cjs} +13 -13
  8. package/dist/{chunk-4H3JTX74.cjs.map → chunk-2TFNDHJ5.cjs.map} +1 -1
  9. package/dist/{chunk-AF72QUBI.js → chunk-2XNCPLHU.js} +5 -5
  10. package/dist/{chunk-AF72QUBI.js.map → chunk-2XNCPLHU.js.map} +1 -1
  11. package/dist/{chunk-RT5FTHUU.js → chunk-3UXRX4KU.js} +5 -5
  12. package/dist/{chunk-RT5FTHUU.js.map → chunk-3UXRX4KU.js.map} +1 -1
  13. package/dist/{chunk-CE6OGJE6.js → chunk-5QHVYOME.js} +5 -5
  14. package/dist/{chunk-CE6OGJE6.js.map → chunk-5QHVYOME.js.map} +1 -1
  15. package/dist/{chunk-EKBEKDWO.cjs → chunk-B6S5ADHR.cjs} +13 -13
  16. package/dist/{chunk-EKBEKDWO.cjs.map → chunk-B6S5ADHR.cjs.map} +1 -1
  17. package/dist/{chunk-QM4AV6EG.js → chunk-BI547RIA.js} +4 -4
  18. package/dist/{chunk-QM4AV6EG.js.map → chunk-BI547RIA.js.map} +1 -1
  19. package/dist/{chunk-WKOV25NP.cjs → chunk-DFMBVA7D.cjs} +8 -8
  20. package/dist/{chunk-WKOV25NP.cjs.map → chunk-DFMBVA7D.cjs.map} +1 -1
  21. package/dist/{chunk-ZJMTHPHY.js → chunk-DKZWUIQD.js} +3 -3
  22. package/dist/{chunk-ZJMTHPHY.js.map → chunk-DKZWUIQD.js.map} +1 -1
  23. package/dist/{chunk-2HAEQDHQ.js → chunk-DVOPHC2Q.js} +755 -201
  24. package/dist/chunk-DVOPHC2Q.js.map +1 -0
  25. package/dist/{chunk-DQ3VVU5T.cjs → chunk-FZWWVJE4.cjs} +24 -24
  26. package/dist/{chunk-DQ3VVU5T.cjs.map → chunk-FZWWVJE4.cjs.map} +1 -1
  27. package/dist/{chunk-I2QGW4GD.js → chunk-GDMM2WEG.js} +11 -11
  28. package/dist/{chunk-I2QGW4GD.js.map → chunk-GDMM2WEG.js.map} +1 -1
  29. package/dist/{chunk-QEB6O6GU.js → chunk-GUPBIEUA.js} +6 -6
  30. package/dist/{chunk-QEB6O6GU.js.map → chunk-GUPBIEUA.js.map} +1 -1
  31. package/dist/{chunk-33BQNWAQ.js → chunk-IQI5I2X5.js} +5 -5
  32. package/dist/{chunk-33BQNWAQ.js.map → chunk-IQI5I2X5.js.map} +1 -1
  33. package/dist/{chunk-YWQH35VO.js → chunk-JPDR6TTA.js} +4 -4
  34. package/dist/{chunk-YWQH35VO.js.map → chunk-JPDR6TTA.js.map} +1 -1
  35. package/dist/{chunk-UCKA22ZJ.cjs → chunk-K63VPFOV.cjs} +9 -2
  36. package/dist/chunk-K63VPFOV.cjs.map +1 -0
  37. package/dist/{chunk-52GXJNQ3.js → chunk-KSAINXRG.js} +9 -2
  38. package/dist/chunk-KSAINXRG.js.map +1 -0
  39. package/dist/{chunk-ZH5WT2IE.cjs → chunk-ML6L4LJH.cjs} +25 -25
  40. package/dist/{chunk-ZH5WT2IE.cjs.map → chunk-ML6L4LJH.cjs.map} +1 -1
  41. package/dist/{chunk-Z6TVOAO5.cjs → chunk-MQAZJQRE.cjs} +9 -2
  42. package/dist/chunk-MQAZJQRE.cjs.map +1 -0
  43. package/dist/{chunk-UAYWLYFW.cjs → chunk-OO24JGYH.cjs} +18 -18
  44. package/dist/{chunk-UAYWLYFW.cjs.map → chunk-OO24JGYH.cjs.map} +1 -1
  45. package/dist/{chunk-26GQO5I2.cjs → chunk-PTJTC6A3.cjs} +12 -12
  46. package/dist/{chunk-26GQO5I2.cjs.map → chunk-PTJTC6A3.cjs.map} +1 -1
  47. package/dist/{chunk-DP3DM7FR.cjs → chunk-PXGJHZKJ.cjs} +6 -6
  48. package/dist/{chunk-DP3DM7FR.cjs.map → chunk-PXGJHZKJ.cjs.map} +1 -1
  49. package/dist/{chunk-XMJE34FY.cjs → chunk-RSQFGF5W.cjs} +13 -13
  50. package/dist/{chunk-XMJE34FY.cjs.map → chunk-RSQFGF5W.cjs.map} +1 -1
  51. package/dist/{chunk-FKGUKFC7.cjs → chunk-TRP5Z3XW.cjs} +5 -5
  52. package/dist/{chunk-FKGUKFC7.cjs.map → chunk-TRP5Z3XW.cjs.map} +1 -1
  53. package/dist/{chunk-EVBYEWHO.js → chunk-UHKZXOXD.js} +9 -2
  54. package/dist/chunk-UHKZXOXD.js.map +1 -0
  55. package/dist/{chunk-IE5ZNCWN.js → chunk-VC2MPB2Y.js} +5 -5
  56. package/dist/{chunk-IE5ZNCWN.js.map → chunk-VC2MPB2Y.js.map} +1 -1
  57. package/dist/{chunk-SRD4KVAP.cjs → chunk-WOYCT5M6.cjs} +764 -209
  58. package/dist/chunk-WOYCT5M6.cjs.map +1 -0
  59. package/dist/{chunk-H6I2JDNW.js → chunk-ZEDMFTKD.js} +10 -10
  60. package/dist/{chunk-H6I2JDNW.js.map → chunk-ZEDMFTKD.js.map} +1 -1
  61. package/dist/{chunk-HA3VRF6E.js → chunk-ZPCO3CEB.js} +3 -3
  62. package/dist/{chunk-HA3VRF6E.js.map → chunk-ZPCO3CEB.js.map} +1 -1
  63. package/dist/{chunk-L7NE7PR4.cjs → chunk-ZSY4VANQ.cjs} +13 -13
  64. package/dist/{chunk-L7NE7PR4.cjs.map → chunk-ZSY4VANQ.cjs.map} +1 -1
  65. package/dist/components/ActivityPreview.cjs +12 -12
  66. package/dist/components/ActivityPreview.d.cts +2 -2
  67. package/dist/components/ActivityPreview.d.ts +2 -2
  68. package/dist/components/ActivityPreview.js +11 -11
  69. package/dist/components/ActivitySequence.cjs +13 -13
  70. package/dist/components/ActivitySequence.d.cts +2 -2
  71. package/dist/components/ActivitySequence.d.ts +2 -2
  72. package/dist/components/ActivitySequence.js +12 -12
  73. package/dist/components/Dictation.cjs +5 -5
  74. package/dist/components/Dictation.d.cts +1 -1
  75. package/dist/components/Dictation.d.ts +1 -1
  76. package/dist/components/Dictation.js +4 -4
  77. package/dist/components/FillInTheBlanks.cjs +5 -5
  78. package/dist/components/FillInTheBlanks.d.cts +1 -1
  79. package/dist/components/FillInTheBlanks.d.ts +1 -1
  80. package/dist/components/FillInTheBlanks.js +4 -4
  81. package/dist/components/GapSelect.cjs +5 -5
  82. package/dist/components/GapSelect.d.cts +1 -1
  83. package/dist/components/GapSelect.d.ts +1 -1
  84. package/dist/components/GapSelect.js +4 -4
  85. package/dist/components/InteractiveVideo.cjs +15 -11
  86. package/dist/components/InteractiveVideo.d.cts +164 -7
  87. package/dist/components/InteractiveVideo.d.ts +164 -7
  88. package/dist/components/InteractiveVideo.js +10 -10
  89. package/dist/components/MultipleChoice.cjs +5 -5
  90. package/dist/components/MultipleChoice.d.cts +1 -1
  91. package/dist/components/MultipleChoice.d.ts +1 -1
  92. package/dist/components/MultipleChoice.js +4 -4
  93. package/dist/components/PronunciationFeedback.cjs +4 -4
  94. package/dist/components/PronunciationFeedback.d.cts +1 -1
  95. package/dist/components/PronunciationFeedback.d.ts +1 -1
  96. package/dist/components/PronunciationFeedback.js +3 -3
  97. package/dist/components/ReadAloud.cjs +6 -6
  98. package/dist/components/ReadAloud.d.cts +2 -2
  99. package/dist/components/ReadAloud.d.ts +2 -2
  100. package/dist/components/ReadAloud.js +5 -5
  101. package/dist/components/StimulusPanel.cjs +4 -4
  102. package/dist/components/StimulusPanel.d.cts +1 -1
  103. package/dist/components/StimulusPanel.d.ts +1 -1
  104. package/dist/components/StimulusPanel.js +3 -3
  105. package/dist/components/WrittenResponse.cjs +5 -5
  106. package/dist/components/WrittenResponse.d.cts +3 -3
  107. package/dist/components/WrittenResponse.d.ts +3 -3
  108. package/dist/components/WrittenResponse.js +4 -4
  109. package/dist/i18n/LkIntlProvider.cjs +7 -7
  110. package/dist/i18n/LkIntlProvider.d.cts +2 -2
  111. package/dist/i18n/LkIntlProvider.d.ts +2 -2
  112. package/dist/i18n/LkIntlProvider.js +1 -1
  113. package/dist/index.cjs +41 -37
  114. package/dist/index.d.cts +3 -3
  115. package/dist/index.d.ts +3 -3
  116. package/dist/index.js +15 -15
  117. package/dist/{strings-5588Q4Te.d.cts → strings-C6wM5aUQ.d.cts} +23 -2
  118. package/dist/{strings-D0IWcsUm.d.ts → strings-Cd_2F-Bw.d.ts} +23 -2
  119. package/dist/theme/ThemeProvider.cjs +6 -6
  120. package/dist/theme/ThemeProvider.js +1 -1
  121. package/dist/theme/defaults.css +7 -0
  122. package/dist/theme/skin.css +60 -6
  123. package/package.json +3 -3
  124. package/dist/chunk-2HAEQDHQ.js.map +0 -1
  125. package/dist/chunk-52GXJNQ3.js.map +0 -1
  126. package/dist/chunk-EVBYEWHO.js.map +0 -1
  127. package/dist/chunk-SRD4KVAP.cjs.map +0 -1
  128. package/dist/chunk-UCKA22ZJ.cjs.map +0 -1
  129. package/dist/chunk-Z6TVOAO5.cjs.map +0 -1
@@ -1,11 +1,11 @@
1
1
  'use client';
2
- import { MultipleChoice } from './chunk-RT5FTHUU.js';
3
- import { FillInTheBlanks } from './chunk-IE5ZNCWN.js';
4
- import { GapSelect } from './chunk-CE6OGJE6.js';
5
- import { Dictation } from './chunk-33BQNWAQ.js';
6
- import { stopCaptureGroup, SequenceSlotContext, ReadAloud } from './chunk-QEB6O6GU.js';
7
- import { ActivityErrorBoundary, isDevelopment } from './chunk-ZJMTHPHY.js';
8
- import { useLkStrings } from './chunk-52GXJNQ3.js';
2
+ import { MultipleChoice } from './chunk-3UXRX4KU.js';
3
+ import { FillInTheBlanks } from './chunk-VC2MPB2Y.js';
4
+ import { GapSelect } from './chunk-5QHVYOME.js';
5
+ import { Dictation } from './chunk-IQI5I2X5.js';
6
+ import { stopCaptureGroup, SequenceSlotContext, ReadAloud } from './chunk-GUPBIEUA.js';
7
+ import { ActivityErrorBoundary, isDevelopment } from './chunk-DKZWUIQD.js';
8
+ import { useLkStrings } from './chunk-KSAINXRG.js';
9
9
  import { assertRedactedItemGroup, validateItemGroup, ActivitySchemaError, flattenSequence, readMediaProgress } from '@intellectif/lk-core';
10
10
  import { useMemo, useRef, useState, useEffect, useCallback } from 'react';
11
11
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -107,6 +107,7 @@ var SHORTCUT_KEYS = [
107
107
  { action: "chapter", keys: "[ / ]" },
108
108
  { action: "mute", keys: "M" },
109
109
  { action: "captions", keys: "C" },
110
+ { action: "secondary-captions", keys: "Shift + C" },
110
111
  { action: "transcript", keys: "T" },
111
112
  { action: "picture-in-picture", keys: "P" },
112
113
  { action: "fullscreen", keys: "F" },
@@ -114,17 +115,87 @@ var SHORTCUT_KEYS = [
114
115
  ];
115
116
  var JUMP_SECONDS = 10;
116
117
  var FRAME_SECONDS = 1 / 30;
118
+
119
+ // src/components/InteractiveVideo/tracks.ts
120
+ var primarySubtag = (tag) => (tag.split("-")[0] ?? tag).toLowerCase();
121
+ function preferCaptions(tracks) {
122
+ return tracks.find((track) => track.kind === "captions") ?? tracks[0];
123
+ }
124
+ function matchLanguage(tracks, language) {
125
+ const wanted = language.toLowerCase();
126
+ const exact = tracks.filter((track) => track.srclang.toLowerCase() === wanted);
127
+ if (exact.length > 0) {
128
+ return preferCaptions(exact);
129
+ }
130
+ const family = primarySubtag(wanted);
131
+ return preferCaptions(tracks.filter((track) => primarySubtag(track.srclang) === family));
132
+ }
133
+ function drawable(tracks) {
134
+ return tracks.filter((track) => track.kind === "captions" || track.kind === "subtitles");
135
+ }
136
+ function secondaryCandidates(tracks, primary) {
137
+ const taken = primary?.srclang.toLowerCase();
138
+ return drawable(tracks).filter((track) => track.srclang.toLowerCase() !== taken);
139
+ }
140
+ function resolveCaptionTracks(tracks, preferences) {
141
+ const usable = drawable(tracks);
142
+ if (usable.length === 0) {
143
+ return {};
144
+ }
145
+ const primary = (preferences.captionLanguage === null ? void 0 : matchLanguage(usable, preferences.captionLanguage)) ?? usable.find((track) => track.default === true) ?? usable[0];
146
+ const second = preferences.secondaryCaptionLanguage;
147
+ const secondary = second === null ? void 0 : matchLanguage(secondaryCandidates(usable, primary), second);
148
+ return secondary === void 0 ? { primary } : { primary, secondary };
149
+ }
150
+ var overlap = (a, b) => Math.min(a.end, b.end) - Math.max(a.start, b.start);
151
+ var LOOKBACK = 8;
152
+ function pairSecondaryCues(primary, secondary) {
153
+ const rows = primary.map(() => []);
154
+ for (const cue of secondary) {
155
+ let low = 0;
156
+ let high = primary.length;
157
+ while (low < high) {
158
+ const middle = low + high >> 1;
159
+ if (primary[middle].start < cue.end) {
160
+ low = middle + 1;
161
+ } else {
162
+ high = middle;
163
+ }
164
+ }
165
+ let best = -1;
166
+ let bestShare = 0;
167
+ let misses = 0;
168
+ for (let index = low - 1; index >= 0 && misses <= LOOKBACK; index -= 1) {
169
+ const share = overlap(primary[index], cue);
170
+ if (share > 0) {
171
+ misses = 0;
172
+ if (share >= bestShare) {
173
+ best = index;
174
+ bestShare = share;
175
+ }
176
+ } else {
177
+ misses += 1;
178
+ }
179
+ }
180
+ if (best !== -1) {
181
+ rows[best].push(cue);
182
+ }
183
+ }
184
+ return rows;
185
+ }
117
186
  function Menu({
118
187
  label,
119
188
  onClose,
120
- children
189
+ children,
190
+ returnTo
121
191
  }) {
122
192
  const ref = useRef(null);
123
193
  useEffect(() => {
124
194
  const items = ref.current?.querySelectorAll('[role^="menuitem"]');
195
+ const back = returnTo === void 0 ? null : ref.current?.querySelector(`[data-menu-key="${returnTo}"]`);
125
196
  const checked = ref.current?.querySelector('[aria-checked="true"]');
126
- (checked ?? items?.[0])?.focus();
127
- }, []);
197
+ (back ?? checked ?? items?.[0])?.focus();
198
+ }, [returnTo]);
128
199
  const onKeyDown = (event) => {
129
200
  const items = Array.from(
130
201
  ref.current?.querySelectorAll('[role^="menuitem"]') ?? []
@@ -176,66 +247,136 @@ function SpeedMenu({
176
247
  function SettingsMenu({
177
248
  preferences,
178
249
  tracks,
179
- activeTrack,
250
+ primaryTrack,
251
+ secondaryTrack,
180
252
  captionsFailed,
181
253
  strings,
182
254
  onChange,
255
+ onCaptionTrack,
256
+ onSecondaryTrack,
183
257
  onShowShortcuts,
184
258
  onClose
185
259
  }) {
186
260
  const [page, setPage] = useState("main");
187
- const captionsValue = !preferences.captions || activeTrack === void 0 ? strings.videoOff : activeTrack.label;
261
+ const [returnTo, setReturnTo] = useState(void 0);
262
+ const open = (next) => {
263
+ setReturnTo(void 0);
264
+ setPage(next);
265
+ };
266
+ const leave = () => {
267
+ setReturnTo(page);
268
+ setPage(page === "second" ? "captions" : "main");
269
+ };
270
+ const captionsValue = !preferences.captions || primaryTrack === void 0 ? strings.videoOff : secondaryTrack === void 0 ? primaryTrack.label : strings.videoCaptionPair(primaryTrack.label, secondaryTrack.label);
271
+ const partners = secondaryCandidates(tracks, primaryTrack);
272
+ const failedNotes = /* @__PURE__ */ jsxs(Fragment, { children: [
273
+ captionsFailed.primary && page === "captions" ? /* @__PURE__ */ jsx("p", { className: "lk-iv-menu-note", role: "status", children: strings.videoCaptionsFailed }) : null,
274
+ captionsFailed.secondary ? /* @__PURE__ */ jsx("p", { className: "lk-iv-menu-note", role: "status", children: strings.videoSecondaryCaptionsFailed }) : null
275
+ ] });
188
276
  const back = /* @__PURE__ */ jsxs(
189
277
  "button",
190
278
  {
191
279
  type: "button",
192
280
  role: "menuitem",
193
281
  className: "lk-iv-menu-item lk-iv-menu-back",
194
- onClick: () => setPage("main"),
282
+ onClick: leave,
195
283
  children: [
196
284
  /* @__PURE__ */ jsx(ChevronIcon, { direction: "left" }),
197
- /* @__PURE__ */ jsx("span", { children: page === "captions" ? strings.videoCaptionLanguage : strings.videoCaptionSize })
285
+ /* @__PURE__ */ jsx("span", { children: page === "captions" ? strings.videoCaptionLanguage : page === "second" ? strings.videoSecondCaptionLanguage : strings.videoCaptionSize })
198
286
  ]
199
287
  }
200
288
  );
289
+ const trackRadio = (track, on, choose) => /* @__PURE__ */ jsxs(
290
+ "button",
291
+ {
292
+ type: "button",
293
+ role: "menuitemradio",
294
+ "aria-checked": on,
295
+ className: "lk-iv-menu-item",
296
+ lang: track.srclang,
297
+ onClick: choose,
298
+ children: [
299
+ /* @__PURE__ */ jsx("span", { dir: "auto", children: track.label }),
300
+ on ? /* @__PURE__ */ jsx(CheckIcon, {}) : null
301
+ ]
302
+ },
303
+ `${track.kind}:${track.srclang}`
304
+ );
201
305
  if (page === "captions") {
202
- return /* @__PURE__ */ jsxs(Menu, { label: strings.videoCaptionLanguage, onClose, children: [
306
+ return /* @__PURE__ */ jsxs(
307
+ Menu,
308
+ {
309
+ label: strings.videoCaptionLanguage,
310
+ onClose,
311
+ returnTo,
312
+ children: [
313
+ back,
314
+ /* @__PURE__ */ jsxs(
315
+ "button",
316
+ {
317
+ type: "button",
318
+ role: "menuitemradio",
319
+ "aria-checked": !preferences.captions,
320
+ className: "lk-iv-menu-item",
321
+ onClick: () => onChange({ captions: false }),
322
+ children: [
323
+ /* @__PURE__ */ jsx("span", { children: strings.videoOff }),
324
+ !preferences.captions ? /* @__PURE__ */ jsx(CheckIcon, {}) : null
325
+ ]
326
+ }
327
+ ),
328
+ tracks.map(
329
+ (track) => trackRadio(
330
+ track,
331
+ preferences.captions && primaryTrack === track,
332
+ () => onCaptionTrack(track)
333
+ )
334
+ ),
335
+ partners.length > 0 ? /* @__PURE__ */ jsxs(
336
+ "button",
337
+ {
338
+ type: "button",
339
+ role: "menuitem",
340
+ className: "lk-iv-menu-item",
341
+ "data-menu-key": "second",
342
+ onClick: () => open("second"),
343
+ children: [
344
+ /* @__PURE__ */ jsx("span", { children: strings.videoSecondCaptionLanguage }),
345
+ /* @__PURE__ */ jsxs("span", { className: "lk-iv-menu-value", children: [
346
+ /* @__PURE__ */ jsx("span", { dir: "auto", lang: secondaryTrack?.srclang, children: secondaryTrack === void 0 ? strings.videoOff : secondaryTrack.label }),
347
+ /* @__PURE__ */ jsx(ChevronIcon, { direction: "right" })
348
+ ] })
349
+ ]
350
+ }
351
+ ) : null,
352
+ failedNotes
353
+ ]
354
+ },
355
+ "captions"
356
+ );
357
+ }
358
+ if (page === "second") {
359
+ return /* @__PURE__ */ jsxs(Menu, { label: strings.videoSecondCaptionLanguage, onClose, children: [
203
360
  back,
204
361
  /* @__PURE__ */ jsxs(
205
362
  "button",
206
363
  {
207
364
  type: "button",
208
365
  role: "menuitemradio",
209
- "aria-checked": !preferences.captions,
366
+ "aria-checked": secondaryTrack === void 0,
210
367
  className: "lk-iv-menu-item",
211
- onClick: () => onChange({ captions: false }),
368
+ onClick: () => onSecondaryTrack(null),
212
369
  children: [
213
370
  /* @__PURE__ */ jsx("span", { children: strings.videoOff }),
214
- !preferences.captions ? /* @__PURE__ */ jsx(CheckIcon, {}) : null
371
+ secondaryTrack === void 0 ? /* @__PURE__ */ jsx(CheckIcon, {}) : null
215
372
  ]
216
373
  }
217
374
  ),
218
- tracks.map((track) => {
219
- const on = preferences.captions && activeTrack === track;
220
- return /* @__PURE__ */ jsxs(
221
- "button",
222
- {
223
- type: "button",
224
- role: "menuitemradio",
225
- "aria-checked": on,
226
- className: "lk-iv-menu-item",
227
- lang: track.srclang,
228
- onClick: () => onChange({ captions: true, captionLanguage: track.srclang }),
229
- children: [
230
- /* @__PURE__ */ jsx("span", { children: track.label }),
231
- on ? /* @__PURE__ */ jsx(CheckIcon, {}) : null
232
- ]
233
- },
234
- `${track.kind}:${track.srclang}`
235
- );
236
- }),
237
- captionsFailed ? /* @__PURE__ */ jsx("p", { className: "lk-iv-menu-note", role: "status", children: strings.videoCaptionsFailed }) : null
238
- ] }, "captions");
375
+ partners.map(
376
+ (track) => trackRadio(track, secondaryTrack === track, () => onSecondaryTrack(track))
377
+ ),
378
+ failedNotes
379
+ ] }, "second");
239
380
  }
240
381
  if (page === "size") {
241
382
  return /* @__PURE__ */ jsxs(Menu, { label: strings.videoCaptionSize, onClose, children: [
@@ -257,7 +398,7 @@ function SettingsMenu({
257
398
  ))
258
399
  ] }, "size");
259
400
  }
260
- return /* @__PURE__ */ jsxs(Menu, { label: strings.videoSettings, onClose, children: [
401
+ return /* @__PURE__ */ jsxs(Menu, { label: strings.videoSettings, onClose, returnTo, children: [
261
402
  tracks.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
262
403
  /* @__PURE__ */ jsxs(
263
404
  "button",
@@ -265,11 +406,12 @@ function SettingsMenu({
265
406
  type: "button",
266
407
  role: "menuitem",
267
408
  className: "lk-iv-menu-item",
268
- onClick: () => setPage("captions"),
409
+ "data-menu-key": "captions",
410
+ onClick: () => open("captions"),
269
411
  children: [
270
412
  /* @__PURE__ */ jsx("span", { children: strings.videoCaptionLanguage }),
271
413
  /* @__PURE__ */ jsxs("span", { className: "lk-iv-menu-value", children: [
272
- captionsValue,
414
+ /* @__PURE__ */ jsx("span", { dir: "auto", children: captionsValue }),
273
415
  /* @__PURE__ */ jsx(ChevronIcon, { direction: "right" })
274
416
  ] })
275
417
  ]
@@ -281,7 +423,8 @@ function SettingsMenu({
281
423
  type: "button",
282
424
  role: "menuitem",
283
425
  className: "lk-iv-menu-item",
284
- onClick: () => setPage("size"),
426
+ "data-menu-key": "size",
427
+ onClick: () => open("size"),
285
428
  children: [
286
429
  /* @__PURE__ */ jsx("span", { children: strings.videoCaptionSize }),
287
430
  /* @__PURE__ */ jsxs("span", { className: "lk-iv-menu-value", children: [
@@ -375,6 +518,9 @@ function ContentsPanel({
375
518
  chapters,
376
519
  quizzes,
377
520
  cues,
521
+ primaryLanguage,
522
+ secondaryCues,
523
+ secondaryLanguage,
378
524
  activeCue,
379
525
  limit,
380
526
  strings,
@@ -386,10 +532,18 @@ function ContentsPanel({
386
532
  const followUntil = useRef(0);
387
533
  const hasTranscript = cues.length > 0;
388
534
  const shownTab = hasTranscript ? tab : "contents";
535
+ const paired = useMemo(() => pairSecondaryCues(cues, secondaryCues), [cues, secondaryCues]);
389
536
  const lines = useMemo(() => {
390
537
  const trimmed = query.trim().toLowerCase();
391
- return cues.map((cue, index) => ({ cue, index })).filter(({ cue }) => cue.start <= limit).filter(({ cue }) => trimmed === "" || cue.text.toLowerCase().includes(trimmed));
392
- }, [cues, limit, query]);
538
+ return cues.map((cue, index) => ({
539
+ cue,
540
+ index,
541
+ // The spoiler limit holds for the second language as for the first.
542
+ second: (paired[index] ?? []).filter((line) => line.start <= limit).map((line) => line.text).join(" ")
543
+ })).filter(({ cue }) => cue.start <= limit).filter(
544
+ ({ cue, second }) => trimmed === "" || cue.text.toLowerCase().includes(trimmed) || second.toLowerCase().includes(trimmed)
545
+ );
546
+ }, [cues, paired, limit, query]);
393
547
  useEffect(() => {
394
548
  if (shownTab !== "transcript" || query.trim() !== "" || Date.now() < followUntil.current) {
395
549
  return;
@@ -512,7 +666,7 @@ function ContentsPanel({
512
666
  onTouchMove: () => {
513
667
  followUntil.current = Date.now() + 4e3;
514
668
  },
515
- children: lines.length === 0 ? /* @__PURE__ */ jsx("p", { className: "lk-iv-empty", children: strings.videoTranscriptNoMatch }) : lines.map(({ cue, index }) => /* @__PURE__ */ jsxs(
669
+ children: lines.length === 0 ? /* @__PURE__ */ jsx("p", { className: "lk-iv-empty", children: strings.videoTranscriptNoMatch }) : lines.map(({ cue, index, second }) => /* @__PURE__ */ jsxs(
516
670
  "button",
517
671
  {
518
672
  type: "button",
@@ -521,7 +675,10 @@ function ContentsPanel({
521
675
  onClick: () => onSeek(cue.start),
522
676
  children: [
523
677
  /* @__PURE__ */ jsx("span", { className: "lk-iv-row-time", children: clock(cue.start) }),
524
- /* @__PURE__ */ jsx("span", { children: highlight(cue.text) })
678
+ secondaryCues.length === 0 ? /* @__PURE__ */ jsx("span", { lang: primaryLanguage, dir: "auto", children: highlight(cue.text) }) : /* @__PURE__ */ jsxs("span", { className: "lk-iv-line-text", children: [
679
+ /* @__PURE__ */ jsx("span", { lang: primaryLanguage, dir: "auto", children: highlight(cue.text) }),
680
+ second === "" ? null : /* @__PURE__ */ jsx("span", { className: "lk-iv-line-secondary", lang: secondaryLanguage, dir: "auto", children: highlight(second) })
681
+ ] })
525
682
  ]
526
683
  },
527
684
  `${cue.start}-${index}`
@@ -543,8 +700,10 @@ function EndScreen({
543
700
  strings,
544
701
  onOpenQuiz,
545
702
  onWatchAgain,
703
+ pending,
546
704
  onFinish
547
705
  }) {
706
+ const pendingId = `${idPrefix}-end-pending`;
548
707
  const headingRef = useRef(null);
549
708
  useEffect(() => {
550
709
  headingRef.current?.focus();
@@ -569,12 +728,23 @@ function EndScreen({
569
728
  ]
570
729
  }
571
730
  ) }, quiz.id)) }) : null,
731
+ onFinish !== void 0 && pending ? /* @__PURE__ */ jsx("p", { id: pendingId, className: "lk-iv-end-pending", role: "status", children: strings.videoAnswerPending }) : null,
572
732
  /* @__PURE__ */ jsxs("div", { className: "lk-iv-end-actions", children: [
573
733
  /* @__PURE__ */ jsxs("button", { type: "button", className: "lk-iv-action", onClick: onWatchAgain, children: [
574
734
  /* @__PURE__ */ jsx(ReplayIcon, {}),
575
735
  strings.videoWatchAgain
576
736
  ] }),
577
- onFinish !== void 0 ? /* @__PURE__ */ jsx("button", { type: "button", className: "lk-iv-action lk-iv-action-primary", onClick: onFinish, children: strings.videoFinish }) : null
737
+ onFinish !== void 0 ? /* @__PURE__ */ jsx(
738
+ "button",
739
+ {
740
+ type: "button",
741
+ className: "lk-iv-action lk-iv-action-primary",
742
+ "aria-disabled": pending || void 0,
743
+ "aria-describedby": pending ? pendingId : void 0,
744
+ onClick: onFinish,
745
+ children: strings.videoFinish
746
+ }
747
+ ) : null
578
748
  ] })
579
749
  ] }) });
580
750
  }
@@ -586,45 +756,73 @@ var DEFAULT_PREFERENCES = {
586
756
  muted: false,
587
757
  captions: true,
588
758
  captionLanguage: null,
759
+ secondaryCaptionLanguage: null,
589
760
  captionSize: "medium",
590
761
  captionBackground: true,
591
762
  panel: false,
592
763
  shortcuts: true
593
764
  };
594
765
  var STORAGE_KEY = "lk.video.v1";
595
- function sanitizePreferences(raw) {
596
- if (typeof raw !== "object" || raw === null) {
597
- return DEFAULT_PREFERENCES;
766
+ var LANGUAGE_TAG = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
767
+ var BOOLEAN_FIELDS = ["muted", "captions", "captionBackground", "panel", "shortcuts"];
768
+ var LANGUAGE_FIELDS = ["captionLanguage", "secondaryCaptionLanguage"];
769
+ function sanitizePartialPreferences(raw) {
770
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
771
+ return {};
598
772
  }
599
773
  const value = raw;
600
- const bool = (input, fallback) => typeof input === "boolean" ? input : fallback;
601
- const speed = SPEEDS.includes(value.speed) ? value.speed : DEFAULT_PREFERENCES.speed;
602
- const volume = typeof value.volume === "number" && Number.isFinite(value.volume) ? Math.min(1, Math.max(0, value.volume)) : DEFAULT_PREFERENCES.volume;
603
- const size = value.captionSize === "small" || value.captionSize === "medium" || value.captionSize === "large" ? value.captionSize : DEFAULT_PREFERENCES.captionSize;
604
- const language = typeof value.captionLanguage === "string" && /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/.test(value.captionLanguage) ? value.captionLanguage : null;
605
- return {
606
- speed,
607
- volume,
608
- muted: bool(value.muted, DEFAULT_PREFERENCES.muted),
609
- captions: bool(value.captions, DEFAULT_PREFERENCES.captions),
610
- captionLanguage: language,
611
- captionSize: size,
612
- captionBackground: bool(value.captionBackground, DEFAULT_PREFERENCES.captionBackground),
613
- panel: bool(value.panel, DEFAULT_PREFERENCES.panel),
614
- shortcuts: bool(value.shortcuts, DEFAULT_PREFERENCES.shortcuts)
615
- };
774
+ const has = (key) => Object.hasOwn(value, key);
775
+ const out = {};
776
+ if (has("speed") && SPEEDS.includes(value.speed)) {
777
+ out.speed = value.speed;
778
+ }
779
+ if (has("volume") && typeof value.volume === "number" && Number.isFinite(value.volume)) {
780
+ out.volume = Math.min(1, Math.max(0, value.volume));
781
+ }
782
+ for (const field of BOOLEAN_FIELDS) {
783
+ if (has(field) && typeof value[field] === "boolean") {
784
+ out[field] = value[field];
785
+ }
786
+ }
787
+ for (const field of LANGUAGE_FIELDS) {
788
+ const tag = value[field];
789
+ if (has(field) && (tag === null || typeof tag === "string" && LANGUAGE_TAG.test(tag))) {
790
+ out[field] = tag;
791
+ }
792
+ }
793
+ if (has("captionSize") && (value.captionSize === "small" || value.captionSize === "medium" || value.captionSize === "large")) {
794
+ out.captionSize = value.captionSize;
795
+ }
796
+ return out;
797
+ }
798
+ function normalised(preferences) {
799
+ const { captionLanguage: first, secondaryCaptionLanguage: second } = preferences;
800
+ return first !== null && second !== null && first.toLowerCase() === second.toLowerCase() ? { ...preferences, secondaryCaptionLanguage: null } : preferences;
801
+ }
802
+ function resolvePreferences(layers) {
803
+ return normalised({
804
+ ...DEFAULT_PREFERENCES,
805
+ ...sanitizePartialPreferences(layers.defaults),
806
+ ...sanitizePartialPreferences(layers.stored),
807
+ ...sanitizePartialPreferences(layers.force),
808
+ ...sanitizePartialPreferences(layers.chosen)
809
+ });
616
810
  }
617
- function readPreferences() {
811
+ function applyPreferenceChange(previous, change) {
812
+ return normalised({ ...previous, ...sanitizePartialPreferences(change) });
813
+ }
814
+ function readStoredPreferences() {
618
815
  try {
619
816
  const stored = globalThis.localStorage?.getItem(STORAGE_KEY);
620
- return stored ? sanitizePreferences(JSON.parse(stored)) : DEFAULT_PREFERENCES;
817
+ return stored ? sanitizePartialPreferences(JSON.parse(stored)) : {};
621
818
  } catch {
622
- return DEFAULT_PREFERENCES;
819
+ return {};
623
820
  }
624
821
  }
625
- function writePreferences(preferences) {
822
+ function rememberPreferences(change) {
626
823
  try {
627
- globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(preferences));
824
+ const next = { ...readStoredPreferences(), ...sanitizePartialPreferences(change) };
825
+ globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(next));
628
826
  } catch {
629
827
  }
630
828
  }
@@ -944,6 +1142,55 @@ function onNextFrame(run) {
944
1142
  const timer = setTimeout(run, 33);
945
1143
  return () => clearTimeout(timer);
946
1144
  }
1145
+ function trackKey(track) {
1146
+ return `${track.kind}:${track.srclang}:${track.src}`;
1147
+ }
1148
+ var NO_CUES = [];
1149
+ function useCaptionLine(track, load, peek) {
1150
+ const key = track === void 0 ? void 0 : trackKey(track);
1151
+ const [line, setLine] = useState({
1152
+ cues: NO_CUES,
1153
+ failed: false
1154
+ });
1155
+ const trackRef = useRef(track);
1156
+ trackRef.current = track;
1157
+ useEffect(() => {
1158
+ const wanted = trackRef.current;
1159
+ if (key === void 0 || wanted === void 0) {
1160
+ return;
1161
+ }
1162
+ let live = true;
1163
+ load(wanted).then(
1164
+ (cues) => {
1165
+ if (live) {
1166
+ setLine({ key, cues, failed: false });
1167
+ }
1168
+ },
1169
+ () => {
1170
+ if (live) {
1171
+ setLine({ key, cues: NO_CUES, failed: true });
1172
+ }
1173
+ }
1174
+ );
1175
+ return () => {
1176
+ live = false;
1177
+ };
1178
+ }, [key, load]);
1179
+ if (key === void 0) {
1180
+ return { cues: NO_CUES, failed: false };
1181
+ }
1182
+ if (line.key === key) {
1183
+ return line;
1184
+ }
1185
+ return { cues: peek(key) ?? NO_CUES, failed: false };
1186
+ }
1187
+ function captionsShown(tracks, preferences) {
1188
+ if (!preferences.captions) {
1189
+ return { srclang: null, secondary: null };
1190
+ }
1191
+ const shown = resolveCaptionTracks(tracks, preferences);
1192
+ return { srclang: shown.primary?.srclang ?? null, secondary: shown.secondary?.srclang ?? null };
1193
+ }
947
1194
  var playerCount = 0;
948
1195
  var ITEM_TYPES = /* @__PURE__ */ new Set([
949
1196
  "multiple-choice",
@@ -1021,21 +1268,25 @@ function Player(props) {
1021
1268
  playerCount += 1;
1022
1269
  return `lk-iv-${playerCount}`;
1023
1270
  });
1024
- const [preferences, setPreferences] = useState(() => ({
1025
- ...DEFAULT_PREFERENCES,
1026
- ...props.preferences
1027
- }));
1028
- const preferenceOverride = useRef(props.preferences);
1271
+ const forced = useRef(props.preferences).current;
1272
+ const defaultsKey = JSON.stringify(props.defaultPreferences ?? null);
1273
+ const defaults = useMemo(() => JSON.parse(defaultsKey), [defaultsKey]);
1274
+ const chosen = useRef({});
1275
+ const [preferences, setPreferences] = useState(
1276
+ () => resolvePreferences({ force: forced, defaults: props.defaultPreferences })
1277
+ );
1278
+ const preferencesRef = useRef(preferences);
1279
+ preferencesRef.current = preferences;
1029
1280
  useEffect(() => {
1030
- setPreferences({ ...readPreferences(), ...preferenceOverride.current });
1031
- }, []);
1032
- const changePreferences = useCallback((change) => {
1033
- setPreferences((previous) => {
1034
- const next = { ...previous, ...change };
1035
- writePreferences(next);
1036
- return next;
1037
- });
1038
- }, []);
1281
+ setPreferences(
1282
+ resolvePreferences({
1283
+ force: forced,
1284
+ stored: readStoredPreferences(),
1285
+ defaults,
1286
+ chosen: chosen.current
1287
+ })
1288
+ );
1289
+ }, [forced, defaults]);
1039
1290
  const [submitted] = useState(() => new Set(props.submittedSlotIds ?? []));
1040
1291
  const [answered, setAnswered] = useState(() => {
1041
1292
  const seeded = /* @__PURE__ */ new Set();
@@ -1048,18 +1299,61 @@ function Player(props) {
1048
1299
  });
1049
1300
  const answeredRef = useRef(answered);
1050
1301
  answeredRef.current = answered;
1051
- const scores = useRef(/* @__PURE__ */ new Map());
1052
- const markAnswered = useCallback((slotId) => {
1302
+ const setSlotAnswered = useCallback((slotId, on) => {
1053
1303
  setAnswered((previous) => {
1054
- if (previous.has(slotId)) {
1304
+ if (previous.has(slotId) === on) {
1055
1305
  return previous;
1056
1306
  }
1057
1307
  const next = new Set(previous);
1058
- next.add(slotId);
1308
+ if (on) {
1309
+ next.add(slotId);
1310
+ } else {
1311
+ next.delete(slotId);
1312
+ }
1059
1313
  answeredRef.current = next;
1060
1314
  return next;
1061
1315
  });
1062
1316
  }, []);
1317
+ const [scores, setScores] = useState(() => /* @__PURE__ */ new Map());
1318
+ const setSlotScore = useCallback((slotId, score) => {
1319
+ setScores((previous) => {
1320
+ if (previous.get(slotId) === score) {
1321
+ return previous;
1322
+ }
1323
+ const next = new Map(previous);
1324
+ if (score === void 0) {
1325
+ next.delete(slotId);
1326
+ } else {
1327
+ next.set(slotId, score);
1328
+ }
1329
+ return next;
1330
+ });
1331
+ }, []);
1332
+ const [pending, setPendingState] = useState(() => /* @__PURE__ */ new Set());
1333
+ const pendingRef = useRef(pending);
1334
+ const setPendingKey = useCallback((key, on) => {
1335
+ setPendingState((previous) => {
1336
+ if (previous.has(key) === on) {
1337
+ return previous;
1338
+ }
1339
+ const next = new Set(previous);
1340
+ if (on) {
1341
+ next.add(key);
1342
+ } else {
1343
+ next.delete(key);
1344
+ }
1345
+ pendingRef.current = next;
1346
+ return next;
1347
+ });
1348
+ }, []);
1349
+ const latestTakes = useRef(/* @__PURE__ */ new Map());
1350
+ const alive = useRef(true);
1351
+ useEffect(() => {
1352
+ alive.current = true;
1353
+ return () => {
1354
+ alive.current = false;
1355
+ };
1356
+ }, []);
1063
1357
  const finished = useCallback(
1064
1358
  (cueId) => (slotsByCue.get(cueId) ?? []).every((slot) => answeredRef.current.has(slot.slotId)),
1065
1359
  [slotsByCue]
@@ -1068,6 +1362,7 @@ function Player(props) {
1068
1362
  const videoRef = useRef(null);
1069
1363
  const playButtonRef = useRef(null);
1070
1364
  const quizHeadingRef = useRef(null);
1365
+ const [portalContainer, setPortalContainer] = useState(null);
1071
1366
  const [playing, setPlaying] = useState(false);
1072
1367
  const [waiting, setWaiting] = useState(false);
1073
1368
  const [errorCode, setErrorCode] = useState(null);
@@ -1304,43 +1599,124 @@ function Player(props) {
1304
1599
  }
1305
1600
  ] : [];
1306
1601
  }, [media, locale, strings.videoCaptionLanguage]);
1307
- const activeTrack = tracks.find((track) => track.srclang === preferences.captionLanguage) ?? tracks.find((track) => track.default === true) ?? tracks[0];
1308
- const [captionCues, setCaptionCues] = useState([]);
1309
- const [captionsFailed, setCaptionsFailed] = useState(false);
1602
+ const { primary: primaryTrack, secondary: secondaryTrack } = useMemo(
1603
+ () => resolveCaptionTracks(tracks, {
1604
+ captionLanguage: preferences.captionLanguage,
1605
+ secondaryCaptionLanguage: preferences.secondaryCaptionLanguage
1606
+ }),
1607
+ [tracks, preferences.captionLanguage, preferences.secondaryCaptionLanguage]
1608
+ );
1310
1609
  const loaderRef = useRef(captionsLoader);
1311
1610
  loaderRef.current = captionsLoader;
1312
- useEffect(() => {
1313
- if (activeTrack === void 0) {
1314
- setCaptionCues([]);
1315
- return;
1611
+ const loading = useRef(/* @__PURE__ */ new Map());
1612
+ const loaded = useRef(/* @__PURE__ */ new Map());
1613
+ const loadCues = useCallback((track) => {
1614
+ const key = trackKey(track);
1615
+ const pending2 = loading.current.get(key);
1616
+ if (pending2 !== void 0) {
1617
+ return pending2;
1316
1618
  }
1317
- let live = true;
1318
- setCaptionsFailed(false);
1319
- const load = loaderRef.current ? loaderRef.current(activeTrack) : fetch(activeTrack.src, { credentials: "same-origin" }).then((response) => {
1320
- if (!response.ok) {
1321
- throw new Error(String(response.status));
1619
+ const request = Promise.resolve().then(
1620
+ () => loaderRef.current ? loaderRef.current(track) : fetch(track.src, { credentials: "same-origin" }).then((response) => {
1621
+ if (!response.ok) {
1622
+ throw new Error(String(response.status));
1623
+ }
1624
+ return response.text();
1625
+ })
1626
+ ).then((text) => {
1627
+ const parsed = parseWebVtt(text);
1628
+ if (parsed.length === 0) {
1629
+ throw new Error("The caption file held no cues.");
1322
1630
  }
1323
- return response.text();
1631
+ loaded.current.set(key, parsed);
1632
+ return parsed;
1324
1633
  });
1325
- Promise.resolve(load).then((text) => {
1326
- if (live) {
1327
- const parsed = parseWebVtt(text);
1328
- setCaptionCues(parsed);
1329
- setCaptionsFailed(parsed.length === 0);
1634
+ loading.current.set(key, request);
1635
+ request.catch(() => {
1636
+ loading.current.delete(key);
1637
+ });
1638
+ return request;
1639
+ }, []);
1640
+ const peekCues = useCallback((key) => loaded.current.get(key), []);
1641
+ const primaryLine = useCaptionLine(primaryTrack, loadCues, peekCues);
1642
+ const secondaryLine = useCaptionLine(secondaryTrack, loadCues, peekCues);
1643
+ const primaryCues = primaryLine.cues;
1644
+ const secondaryCues = secondaryLine.cues;
1645
+ const captionsFailed = { primary: primaryLine.failed, secondary: secondaryLine.failed };
1646
+ const hasCaptions = primaryCues.length > 0 || secondaryCues.length > 0;
1647
+ const hasTranscript = primaryCues.length > 0;
1648
+ const activeCueIndex = useMemo(() => cueIndexAt(primaryCues, current), [primaryCues, current]);
1649
+ const secondaryCueIndex = useMemo(
1650
+ () => cueIndexAt(secondaryCues, current),
1651
+ [secondaryCues, current]
1652
+ );
1653
+ const spoilerLimit = timeline.navigation === "no-skip-ahead" && renderMode !== "review" ? Math.max(furthestShown, current) : Number.POSITIVE_INFINITY;
1654
+ const onPreferencesChangeRef = useRef(props.onPreferencesChange);
1655
+ onPreferencesChangeRef.current = props.onPreferencesChange;
1656
+ const tracksRef = useRef(tracks);
1657
+ tracksRef.current = tracks;
1658
+ const lastSecondary = useRef(null);
1659
+ const changePreferences = useCallback(
1660
+ (change) => {
1661
+ const previous = preferencesRef.current;
1662
+ const next = applyPreferenceChange(previous, change);
1663
+ if (previous.secondaryCaptionLanguage !== null) {
1664
+ lastSecondary.current = previous.secondaryCaptionLanguage;
1330
1665
  }
1331
- }).catch(() => {
1332
- if (live) {
1333
- setCaptionCues([]);
1334
- setCaptionsFailed(true);
1666
+ chosen.current = { ...chosen.current, ...change };
1667
+ preferencesRef.current = next;
1668
+ setPreferences(next);
1669
+ rememberPreferences(change);
1670
+ onPreferencesChangeRef.current?.(next, change);
1671
+ const before = captionsShown(tracksRef.current, previous);
1672
+ const after = captionsShown(tracksRef.current, next);
1673
+ if (before.srclang !== after.srclang || before.secondary !== after.secondary) {
1674
+ emit("video-captions-changed", after);
1335
1675
  }
1676
+ },
1677
+ [emit]
1678
+ );
1679
+ const chooseCaptionTrack = (track) => {
1680
+ const swaps = primaryTrack !== void 0 && secondaryTrack !== void 0 && track.srclang.toLowerCase() === secondaryTrack.srclang.toLowerCase();
1681
+ changePreferences({
1682
+ captions: true,
1683
+ captionLanguage: track.srclang,
1684
+ ...swaps ? { secondaryCaptionLanguage: primaryTrack.srclang } : {}
1336
1685
  });
1337
- return () => {
1338
- live = false;
1339
- };
1340
- }, [activeTrack]);
1341
- const hasCaptions = captionCues.length > 0;
1342
- const activeCueIndex = useMemo(() => cueIndexAt(captionCues, current), [captionCues, current]);
1343
- const spoilerLimit = timeline.navigation === "no-skip-ahead" && renderMode !== "review" ? Math.max(furthestShown, current) : Number.POSITIVE_INFINITY;
1686
+ };
1687
+ const chooseSecondaryTrack = (track) => {
1688
+ changePreferences(
1689
+ track === null ? { secondaryCaptionLanguage: null } : { secondaryCaptionLanguage: track.srclang, captions: true }
1690
+ );
1691
+ };
1692
+ const toggleSecondary = () => {
1693
+ if (secondaryTrack !== void 0) {
1694
+ changePreferences(
1695
+ preferences.captions ? { secondaryCaptionLanguage: null } : { captions: true }
1696
+ );
1697
+ return;
1698
+ }
1699
+ const wanted = [
1700
+ lastSecondary.current,
1701
+ preferences.secondaryCaptionLanguage,
1702
+ sanitizePartialPreferences(defaults).secondaryCaptionLanguage ?? null,
1703
+ secondaryCandidates(tracks, primaryTrack)[0]?.srclang ?? null
1704
+ ];
1705
+ for (const language of wanted) {
1706
+ if (language === null) {
1707
+ continue;
1708
+ }
1709
+ const { secondary } = resolveCaptionTracks(tracks, {
1710
+ captionLanguage: preferences.captionLanguage,
1711
+ secondaryCaptionLanguage: language
1712
+ });
1713
+ if (secondary !== void 0) {
1714
+ changePreferences({ secondaryCaptionLanguage: secondary.srclang, captions: true });
1715
+ return;
1716
+ }
1717
+ }
1718
+ announce(strings.videoNoSecondLanguage);
1719
+ };
1344
1720
  useEffect(() => {
1345
1721
  const video = videoRef.current;
1346
1722
  if (video !== null) {
@@ -1507,14 +1883,17 @@ function Player(props) {
1507
1883
  return;
1508
1884
  case "c":
1509
1885
  case "C":
1510
- if (hasCaptions) {
1886
+ if (event.shiftKey) {
1887
+ done();
1888
+ toggleSecondary();
1889
+ } else if (hasCaptions) {
1511
1890
  done();
1512
1891
  changePreferences({ captions: !preferences.captions });
1513
1892
  }
1514
1893
  return;
1515
1894
  case "t":
1516
1895
  case "T":
1517
- if (hasCaptions) {
1896
+ if (hasTranscript) {
1518
1897
  done();
1519
1898
  setPanelTab("transcript");
1520
1899
  changePreferences({ panel: !(preferences.panel && panelTab === "transcript") });
@@ -1564,7 +1943,7 @@ function Player(props) {
1564
1943
  }));
1565
1944
  const totalQuestions = quizzes.reduce((sum, quiz) => sum + quiz.total, 0);
1566
1945
  const answeredCount = quizzes.reduce((sum, quiz) => sum + quiz.answered, 0);
1567
- const graded = [...scores.current.values()];
1946
+ const graded = [...scores.values()];
1568
1947
  const percent = graded.length === 0 ? null : Math.round(graded.reduce((a, b) => a + b, 0) / graded.length * 100);
1569
1948
  const summary = useCallback(() => {
1570
1949
  return {
@@ -1586,11 +1965,12 @@ function Player(props) {
1586
1965
  finishedOnce.current = true;
1587
1966
  onFinished?.(summary());
1588
1967
  }, [onFinished, summary]);
1968
+ const answerPending = pending.size > 0;
1589
1969
  useEffect(() => {
1590
- if (ended && totalQuestions > 0 && answeredCount === totalQuestions && renderMode !== "review") {
1970
+ if (ended && !answerPending && totalQuestions > 0 && answeredCount === totalQuestions && renderMode !== "review") {
1591
1971
  finish();
1592
1972
  }
1593
- }, [ended, totalQuestions, answeredCount, renderMode, finish]);
1973
+ }, [ended, answerPending, totalQuestions, answeredCount, renderMode, finish]);
1594
1974
  const slotBindings = useMemo(() => {
1595
1975
  const bindings = /* @__PURE__ */ new Map();
1596
1976
  if (recordingBinding === void 0) {
@@ -1611,41 +1991,147 @@ function Player(props) {
1611
1991
  }
1612
1992
  return bindings;
1613
1993
  }, [slots, recordingBinding]);
1614
- const renderQuestion = (slot, cueId) => {
1615
- const activity = slot.activity;
1616
- const place = {
1617
- slotId: slot.slotId,
1618
- index: slot.index,
1619
- activityId: activity.id,
1620
- cueId
1994
+ const places = useMemo(() => {
1995
+ const byId = /* @__PURE__ */ new Map();
1996
+ for (const [cueId, cueSlots] of slotsByCue) {
1997
+ for (const slot of cueSlots) {
1998
+ byId.set(slot.slotId, {
1999
+ slotId: slot.slotId,
2000
+ index: slot.index,
2001
+ activityId: slot.activity.id,
2002
+ cueId
2003
+ });
2004
+ }
2005
+ }
2006
+ return byId;
2007
+ }, [slotsByCue]);
2008
+ const reportTo = useRef({ onSubmit, onActivityComplete, onInteraction, renderMode });
2009
+ reportTo.current = { onSubmit, onActivityComplete, onInteraction, renderMode };
2010
+ const calls = useRef(/* @__PURE__ */ new Map());
2011
+ const callsFor = (place) => {
2012
+ const made = calls.current.get(place.slotId);
2013
+ if (made !== void 0) {
2014
+ return made;
2015
+ }
2016
+ const { slotId } = place;
2017
+ const warned = /* @__PURE__ */ new Set();
2018
+ const refused = (call) => {
2019
+ if (reportTo.current.renderMode !== "review") {
2020
+ return false;
2021
+ }
2022
+ if (isDevelopment() && !warned.has(call)) {
2023
+ warned.add(call);
2024
+ console.warn(
2025
+ `InteractiveVideo: \`${call}()\` was called for question "${slotId}" in renderMode "review", where nothing is submittable. It was ignored.`
2026
+ );
2027
+ }
2028
+ return true;
2029
+ };
2030
+ const next = {
2031
+ submit: (response) => {
2032
+ if (!alive.current || refused("submit")) {
2033
+ return;
2034
+ }
2035
+ setSlotAnswered(slotId, true);
2036
+ reportTo.current.onSubmit?.(response, place);
2037
+ },
2038
+ complete: (result) => {
2039
+ if (!alive.current || refused("complete")) {
2040
+ return;
2041
+ }
2042
+ setSlotAnswered(slotId, true);
2043
+ setSlotScore(slotId, result.maxScore > 0 ? result.score / result.maxScore : void 0);
2044
+ reportTo.current.onActivityComplete?.(result, place);
2045
+ },
2046
+ clear: () => {
2047
+ if (!alive.current || refused("clear")) {
2048
+ return;
2049
+ }
2050
+ setSlotAnswered(slotId, false);
2051
+ setSlotScore(slotId, void 0);
2052
+ },
2053
+ setPending: (on) => {
2054
+ if (!alive.current) {
2055
+ return;
2056
+ }
2057
+ setPendingKey(`host:${slotId}`, on === true);
2058
+ },
2059
+ emit: (type, payload) => {
2060
+ if (!alive.current) {
2061
+ return;
2062
+ }
2063
+ reportTo.current.onInteraction?.({
2064
+ type,
2065
+ activityId: place.activityId,
2066
+ timestamp: Date.now(),
2067
+ ...payload !== void 0 ? { payload } : {}
2068
+ });
2069
+ }
1621
2070
  };
1622
- const restored = props.responses !== void 0 && Object.hasOwn(props.responses, slot.slotId) ? props.responses[slot.slotId] : void 0;
1623
- const outcome = props.outcomes !== void 0 && Object.hasOwn(props.outcomes, slot.slotId) ? props.outcomes[slot.slotId] : void 0;
2071
+ calls.current.set(slotId, next);
2072
+ return next;
2073
+ };
2074
+ const reportTake = (slotId, take, state) => {
2075
+ if (!alive.current) {
2076
+ return;
2077
+ }
2078
+ const latest = latestTakes.current.get(slotId);
2079
+ if (latest !== void 0 && take < latest) {
2080
+ return;
2081
+ }
2082
+ latestTakes.current.set(slotId, take);
2083
+ setPendingKey(`take:${slotId}`, state === "in-flight");
2084
+ };
2085
+ const channels = useRef(/* @__PURE__ */ new Map());
2086
+ const channelFor = (slotId) => {
2087
+ let channel = channels.current.get(slotId);
2088
+ if (channel === void 0) {
2089
+ channel = {
2090
+ captureGroup: `${playerId}::${slotId}`,
2091
+ takeState: (take, state) => reportTake(slotId, take, state)
2092
+ };
2093
+ channels.current.set(slotId, channel);
2094
+ }
2095
+ return channel;
2096
+ };
2097
+ const seedsFor = (slotId) => ({
2098
+ restored: props.responses !== void 0 && Object.hasOwn(props.responses, slotId) ? props.responses[slotId] : void 0,
2099
+ outcome: props.outcomes !== void 0 && Object.hasOwn(props.outcomes, slotId) ? props.outcomes[slotId] : void 0
2100
+ });
2101
+ const questionFor = (slot, place, active) => {
2102
+ const { restored, outcome } = seedsFor(slot.slotId);
2103
+ return {
2104
+ activity: slot.activity,
2105
+ slot: place,
2106
+ renderMode,
2107
+ active,
2108
+ ...locale !== void 0 ? { locale } : {},
2109
+ ...restored !== void 0 ? { defaultValue: restored } : {},
2110
+ defaultSubmitted: submitted.has(slot.slotId),
2111
+ ...outcome !== void 0 ? { outcome } : {},
2112
+ portalContainer,
2113
+ ...callsFor(place)
2114
+ };
2115
+ };
2116
+ const sdkQuestion = (slot, place) => {
2117
+ const activity = slot.activity;
2118
+ const { restored, outcome } = seedsFor(slot.slotId);
2119
+ const reported = callsFor(place);
1624
2120
  const common = {
1625
2121
  renderMode,
1626
2122
  ...restored !== void 0 ? { defaultValue: restored } : {},
1627
2123
  ...submitted.has(slot.slotId) ? { defaultSubmitted: true } : {},
1628
2124
  ...outcome !== void 0 ? { outcome } : {},
1629
- onSubmit: (response) => {
1630
- markAnswered(slot.slotId);
1631
- onSubmit?.(response, place);
1632
- },
1633
- onComplete: (result) => {
1634
- markAnswered(slot.slotId);
1635
- if (result.maxScore > 0) {
1636
- scores.current.set(slot.slotId, result.score / result.maxScore);
1637
- }
1638
- onActivityComplete?.(result, place);
1639
- },
2125
+ onSubmit: reported.submit,
2126
+ onComplete: reported.complete,
1640
2127
  ...onInteraction !== void 0 ? { onInteraction } : {},
1641
2128
  ...sanitizeHtml !== void 0 ? { sanitizeHtml } : {},
1642
2129
  ...props.strings !== void 0 ? { strings: props.strings } : {},
1643
2130
  ...locale !== void 0 ? { locale } : {}
1644
2131
  };
1645
- let question;
1646
2132
  switch (activity.type) {
1647
2133
  case "multiple-choice":
1648
- question = /* @__PURE__ */ jsx(
2134
+ return /* @__PURE__ */ jsx(
1649
2135
  MultipleChoice,
1650
2136
  {
1651
2137
  data: activity,
@@ -1653,12 +2139,10 @@ function Player(props) {
1653
2139
  ...shuffleSeed !== void 0 ? { shuffleSeed } : {}
1654
2140
  }
1655
2141
  );
1656
- break;
1657
2142
  case "fill-in-the-blanks":
1658
- question = /* @__PURE__ */ jsx(FillInTheBlanks, { data: activity, ...common });
1659
- break;
2143
+ return /* @__PURE__ */ jsx(FillInTheBlanks, { data: activity, ...common });
1660
2144
  case "gap-select":
1661
- question = /* @__PURE__ */ jsx(
2145
+ return /* @__PURE__ */ jsx(
1662
2146
  GapSelect,
1663
2147
  {
1664
2148
  data: activity,
@@ -1666,14 +2150,12 @@ function Player(props) {
1666
2150
  ...shuffleSeed !== void 0 ? { shuffleSeed } : {}
1667
2151
  }
1668
2152
  );
1669
- break;
1670
2153
  case "dictation":
1671
- question = /* @__PURE__ */ jsx(Dictation, { data: activity, ...common });
1672
- break;
2154
+ return /* @__PURE__ */ jsx(Dictation, { data: activity, ...common });
1673
2155
  case "read-aloud": {
1674
2156
  const binding = slotBindings.get(slot.slotId);
1675
2157
  const assessment = props.assessments !== void 0 && Object.hasOwn(props.assessments, slot.slotId) ? props.assessments[slot.slotId] : void 0;
1676
- question = /* @__PURE__ */ jsx(
2158
+ return /* @__PURE__ */ jsx(
1677
2159
  ReadAloud,
1678
2160
  {
1679
2161
  data: activity,
@@ -1683,21 +2165,10 @@ function Player(props) {
1683
2165
  ...workletUrl !== void 0 ? { workletUrl } : {}
1684
2166
  }
1685
2167
  );
1686
- break;
1687
2168
  }
1688
2169
  default:
1689
- question = null;
1690
- }
1691
- return question;
1692
- };
1693
- const channels = useRef(/* @__PURE__ */ new Map());
1694
- const channelFor = (slotId) => {
1695
- let channel = channels.current.get(slotId);
1696
- if (channel === void 0) {
1697
- channel = { captureGroup: `${playerId}::${slotId}`, takeState: () => void 0 };
1698
- channels.current.set(slotId, channel);
2170
+ return null;
1699
2171
  }
1700
- return channel;
1701
2172
  };
1702
2173
  if (devError !== null) {
1703
2174
  throw devError;
@@ -1718,7 +2189,9 @@ function Player(props) {
1718
2189
  emit("video-quiz-question-shown", { cueId: openCue.id, slotId: slot.slotId });
1719
2190
  }
1720
2191
  };
1721
- const showCaption = preferences.captions && hasCaptions && activeCueIndex !== -1 && openQuiz === null && !ended && errorCode === null;
2192
+ const captionsHidden = !preferences.captions || openQuiz !== null || ended || errorCode !== null;
2193
+ const primaryText = captionsHidden ? void 0 : primaryCues[activeCueIndex]?.text;
2194
+ const secondaryText = captionsHidden ? void 0 : secondaryCues[secondaryCueIndex]?.text;
1722
2195
  const chromeVisible = !playing || chromeShown || menu !== null || scrubbing || focusInChrome || sheet || openQuiz !== null;
1723
2196
  const volumeLevel = preferences.muted || preferences.volume === 0 ? "muted" : preferences.volume < 0.5 ? "low" : "high";
1724
2197
  return (
@@ -1830,15 +2303,43 @@ function Player(props) {
1830
2303
  }
1831
2304
  ),
1832
2305
  label !== void 0 ? /* @__PURE__ */ jsx("span", { className: "lk-iv-label", children: label }) : null,
1833
- showCaption ? /* @__PURE__ */ jsx(
1834
- "div",
1835
- {
1836
- className: "lk-iv-caption",
1837
- "data-size": preferences.captionSize,
1838
- "data-background": preferences.captionBackground || void 0,
1839
- lang: activeTrack?.srclang,
1840
- children: /* @__PURE__ */ jsx("span", { children: captionCues[activeCueIndex]?.text })
1841
- }
2306
+ primaryText !== void 0 || secondaryText !== void 0 ? (
2307
+ // The lines keep `.lk-iv-caption`, `data-size` and `data-background`
2308
+ // as well, so a stylesheet written against 15.x still matches them.
2309
+ /* @__PURE__ */ jsxs(
2310
+ "div",
2311
+ {
2312
+ className: "lk-iv-captions",
2313
+ "data-size": preferences.captionSize,
2314
+ "data-background": preferences.captionBackground || void 0,
2315
+ children: [
2316
+ primaryText !== void 0 ? /* @__PURE__ */ jsx(
2317
+ "div",
2318
+ {
2319
+ className: "lk-iv-caption",
2320
+ "data-role": "primary",
2321
+ "data-size": preferences.captionSize,
2322
+ "data-background": preferences.captionBackground || void 0,
2323
+ lang: primaryTrack?.srclang,
2324
+ dir: "auto",
2325
+ children: /* @__PURE__ */ jsx("span", { children: primaryText })
2326
+ }
2327
+ ) : null,
2328
+ secondaryText !== void 0 ? /* @__PURE__ */ jsx(
2329
+ "div",
2330
+ {
2331
+ className: "lk-iv-caption",
2332
+ "data-role": "secondary",
2333
+ "data-size": preferences.captionSize,
2334
+ "data-background": preferences.captionBackground || void 0,
2335
+ lang: secondaryTrack?.srclang,
2336
+ dir: "auto",
2337
+ children: /* @__PURE__ */ jsx("span", { children: secondaryText })
2338
+ }
2339
+ ) : null
2340
+ ]
2341
+ }
2342
+ )
1842
2343
  ) : null,
1843
2344
  waiting && errorCode === null && openQuiz === null ? /* @__PURE__ */ jsx("span", { className: "lk-iv-spinner", "aria-hidden": "true" }) : null,
1844
2345
  errorCode !== null ? /* @__PURE__ */ jsxs("div", { className: "lk-iv-error", role: "alert", children: [
@@ -1887,7 +2388,7 @@ function Player(props) {
1887
2388
  buffered,
1888
2389
  chapters,
1889
2390
  markers,
1890
- cues: captionCues,
2391
+ cues: primaryCues,
1891
2392
  previewLimit: spoilerLimit,
1892
2393
  strings,
1893
2394
  onSeek: seek,
@@ -1972,7 +2473,7 @@ function Player(props) {
1972
2473
  )
1973
2474
  ] }),
1974
2475
  /* @__PURE__ */ jsxs("div", { className: "lk-iv-bar-group", children: [
1975
- quizzes.length > 0 || chapters.length > 0 || hasCaptions ? /* @__PURE__ */ jsx(
2476
+ quizzes.length > 0 || chapters.length > 0 || hasTranscript ? /* @__PURE__ */ jsx(
1976
2477
  "button",
1977
2478
  {
1978
2479
  type: "button",
@@ -1990,12 +2491,7 @@ function Player(props) {
1990
2491
  className: "lk-iv-button",
1991
2492
  "aria-label": preferences.captions ? strings.videoCaptionsHide : strings.videoCaptionsShow,
1992
2493
  "aria-pressed": preferences.captions,
1993
- onClick: () => {
1994
- changePreferences({ captions: !preferences.captions });
1995
- emit("video-captions-changed", {
1996
- srclang: preferences.captions ? null : activeTrack?.srclang ?? null
1997
- });
1998
- },
2494
+ onClick: () => changePreferences({ captions: !preferences.captions }),
1999
2495
  children: /* @__PURE__ */ jsx(CaptionsIcon, { on: preferences.captions })
2000
2496
  }
2001
2497
  ) : null,
@@ -2043,8 +2539,11 @@ function Player(props) {
2043
2539
  {
2044
2540
  preferences,
2045
2541
  tracks,
2046
- activeTrack,
2542
+ primaryTrack,
2543
+ secondaryTrack,
2047
2544
  captionsFailed,
2545
+ onCaptionTrack: chooseCaptionTrack,
2546
+ onSecondaryTrack: chooseSecondaryTrack,
2048
2547
  strings,
2049
2548
  onChange: changePreferences,
2050
2549
  onShowShortcuts: () => {
@@ -2090,7 +2589,7 @@ function Player(props) {
2090
2589
  "aria-labelledby": `${playerId}-quiz-title`,
2091
2590
  hidden: openQuiz === null,
2092
2591
  onKeyDown: (event) => {
2093
- if (event.key === "Escape" && !required && openQuiz !== null) {
2592
+ if (event.key === "Escape" && !event.defaultPrevented && !required && openQuiz !== null) {
2094
2593
  event.preventDefault();
2095
2594
  closeQuiz("skip");
2096
2595
  }
@@ -2137,15 +2636,29 @@ function Player(props) {
2137
2636
  ] }) : null
2138
2637
  ] }) : null,
2139
2638
  /* @__PURE__ */ jsx("div", { className: "lk-iv-quiz-body", children: cues.filter((cue) => mounted.has(cue.id)).map(
2140
- (cue) => (slotsByCue.get(cue.id) ?? []).map((slot, index) => /* @__PURE__ */ jsx(
2141
- QuestionPane,
2142
- {
2143
- hidden: !(openQuiz?.cueId === cue.id && step === index),
2144
- channel: channelFor(slot.slotId),
2145
- children: renderQuestion(slot, cue.id)
2146
- },
2147
- slot.slotId
2148
- ))
2639
+ (cue) => (slotsByCue.get(cue.id) ?? []).map((slot, index) => {
2640
+ const shown = openQuiz?.cueId === cue.id && step === index;
2641
+ const place = places.get(slot.slotId);
2642
+ return /* @__PURE__ */ jsx(
2643
+ QuestionPane,
2644
+ {
2645
+ hidden: !shown,
2646
+ channel: channelFor(slot.slotId),
2647
+ children: /* @__PURE__ */ jsx(
2648
+ SlotQuestion,
2649
+ {
2650
+ title: slot.activity.title,
2651
+ strings,
2652
+ render: props.renderQuestion,
2653
+ question: props.renderQuestion === void 0 ? void 0 : questionFor(slot, place, shown),
2654
+ sdk: () => sdkQuestion(slot, place),
2655
+ setHostPending: callsFor(place).setPending
2656
+ }
2657
+ )
2658
+ },
2659
+ slot.slotId
2660
+ );
2661
+ })
2149
2662
  ) }),
2150
2663
  openCue !== void 0 ? /* @__PURE__ */ jsxs("div", { className: "lk-iv-quiz-foot", children: [
2151
2664
  /* @__PURE__ */ jsxs("div", { className: "lk-iv-quiz-foot-start", children: [
@@ -2201,7 +2714,14 @@ function Player(props) {
2201
2714
  }
2202
2715
  },
2203
2716
  onWatchAgain: togglePlay,
2204
- onFinish: renderMode === "review" ? void 0 : finish
2717
+ pending: answerPending,
2718
+ onFinish: renderMode === "review" ? void 0 : () => {
2719
+ if (pendingRef.current.size > 0) {
2720
+ announce(strings.videoAnswerPending);
2721
+ return;
2722
+ }
2723
+ finish();
2724
+ }
2205
2725
  }
2206
2726
  ) : null,
2207
2727
  sheet ? /* @__PURE__ */ jsx(
@@ -2225,7 +2745,10 @@ function Player(props) {
2225
2745
  onTab: setPanelTab,
2226
2746
  chapters,
2227
2747
  quizzes,
2228
- cues: captionCues,
2748
+ cues: primaryCues,
2749
+ primaryLanguage: primaryTrack?.srclang,
2750
+ secondaryCues: secondaryTrack === void 0 ? NO_CUES : secondaryCues,
2751
+ secondaryLanguage: secondaryTrack?.srclang,
2229
2752
  activeCue: activeCueIndex,
2230
2753
  limit: spoilerLimit,
2231
2754
  strings,
@@ -2249,12 +2772,43 @@ function Player(props) {
2249
2772
  }
2250
2773
  }
2251
2774
  ) : null,
2252
- /* @__PURE__ */ jsx("div", { className: "lk-iv-live", "aria-live": "polite", "aria-atomic": "true", children: announcement })
2775
+ /* @__PURE__ */ jsx("div", { className: "lk-iv-live", "aria-live": "polite", "aria-atomic": "true", children: announcement }),
2776
+ /* @__PURE__ */ jsx("div", { ref: setPortalContainer, className: "lk-iv-portal" })
2253
2777
  ]
2254
2778
  }
2255
2779
  )
2256
2780
  );
2257
2781
  }
2782
+ function SlotQuestion({
2783
+ title,
2784
+ strings,
2785
+ ...content
2786
+ }) {
2787
+ return /* @__PURE__ */ jsx(
2788
+ ActivityErrorBoundary,
2789
+ {
2790
+ ...title !== void 0 ? { activityTitle: title } : {},
2791
+ strings,
2792
+ children: /* @__PURE__ */ jsx(SlotContent, { ...content })
2793
+ }
2794
+ );
2795
+ }
2796
+ function SlotContent({
2797
+ render,
2798
+ question,
2799
+ sdk,
2800
+ setHostPending
2801
+ }) {
2802
+ const hosted = render !== void 0 && question !== void 0 ? render(question) : void 0;
2803
+ const hosting = hosted !== void 0;
2804
+ useEffect(() => {
2805
+ if (!hosting) {
2806
+ return;
2807
+ }
2808
+ return () => setHostPending(false);
2809
+ }, [hosting, setHostPending]);
2810
+ return hosting ? hosted : sdk();
2811
+ }
2258
2812
  function QuestionPane({
2259
2813
  hidden,
2260
2814
  channel,
@@ -2325,6 +2879,6 @@ function InteractiveVideo2(props) {
2325
2879
  );
2326
2880
  }
2327
2881
 
2328
- export { InteractiveVideo2 as InteractiveVideo };
2329
- //# sourceMappingURL=chunk-2HAEQDHQ.js.map
2330
- //# sourceMappingURL=chunk-2HAEQDHQ.js.map
2882
+ export { InteractiveVideo2 as InteractiveVideo, resolveCaptionTracks };
2883
+ //# sourceMappingURL=chunk-DVOPHC2Q.js.map
2884
+ //# sourceMappingURL=chunk-DVOPHC2Q.js.map