@amazon/vinyl 2.0.4 → 3.0.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 (167) hide show
  1. package/dist/index.cjs +603 -317
  2. package/dist/index.cjs.map +4 -4
  3. package/dist/index.js +622 -329
  4. package/dist/index.js.map +4 -4
  5. package/dist/test/benchmark.es5.js +8 -5
  6. package/dist/test/benchmark.js +228 -0
  7. package/dist/test/benchmark.js.map +4 -4
  8. package/dist/test/integ.es5.js +182 -156
  9. package/dist/test/integ.js +1853 -796
  10. package/dist/test/integ.js.map +4 -4
  11. package/dist/types/current/index.d.ts +4 -0
  12. package/dist/types/current/index.d.ts.map +1 -1
  13. package/dist/types/current/index.js +4 -0
  14. package/dist/types/current/streaming/AudioOptions.d.ts +39 -0
  15. package/dist/types/current/streaming/AudioOptions.d.ts.map +1 -0
  16. package/dist/types/current/streaming/AudioOptions.js +15 -0
  17. package/dist/types/current/streaming/createDefaultMediaTimelineTransformer.d.ts +2 -2
  18. package/dist/types/current/streaming/createDefaultMediaTimelineTransformer.d.ts.map +1 -1
  19. package/dist/types/current/streaming/createDefaultMediaTimelineTransformer.js +5 -5
  20. package/dist/types/current/text/HtmlTextTrackRenderer.d.ts +60 -0
  21. package/dist/types/current/text/HtmlTextTrackRenderer.d.ts.map +1 -0
  22. package/dist/types/current/text/HtmlTextTrackRenderer.js +231 -0
  23. package/dist/types/current/text/SidecarTextTrackController.d.ts +65 -53
  24. package/dist/types/current/text/SidecarTextTrackController.d.ts.map +1 -1
  25. package/dist/types/current/text/SidecarTextTrackController.js +167 -154
  26. package/dist/types/current/text/SidecarTextTrackLoader.d.ts +5 -0
  27. package/dist/types/current/text/SidecarTextTrackLoader.d.ts.map +1 -1
  28. package/dist/types/current/text/SidecarTextTrackLoader.js +13 -7
  29. package/dist/types/current/text/TextTrack.d.ts +74 -20
  30. package/dist/types/current/text/TextTrack.d.ts.map +1 -1
  31. package/dist/types/current/text/TextTrack.js +20 -0
  32. package/dist/types/current/text/TextTrackRenderer.d.ts +24 -0
  33. package/dist/types/current/text/TextTrackRenderer.d.ts.map +1 -0
  34. package/dist/types/current/text/TextTrackRenderer.js +5 -0
  35. package/dist/types/current/text/mediaTextTrackProvider.d.ts +34 -0
  36. package/dist/types/current/text/mediaTextTrackProvider.d.ts.map +1 -0
  37. package/dist/types/current/text/mediaTextTrackProvider.js +42 -0
  38. package/dist/types/current/text/parseWebVtt.d.ts +16 -2
  39. package/dist/types/current/text/parseWebVtt.d.ts.map +1 -1
  40. package/dist/types/current/text/parseWebVtt.js +176 -48
  41. package/dist/types/current/text/pickPreferredTextTrack.d.ts +2 -1
  42. package/dist/types/current/text/pickPreferredTextTrack.d.ts.map +1 -1
  43. package/dist/types/current/text/pickPreferredTextTrack.js +6 -3
  44. package/dist/types/current/track/Track.d.ts +1 -1
  45. package/dist/types/current/track/dash/createDashFactories.d.ts +21 -10
  46. package/dist/types/current/track/dash/createDashFactories.d.ts.map +1 -1
  47. package/dist/types/current/track/dash/createDashFactories.js +9 -21
  48. package/dist/types/current/track/hls/createHlsFactories.d.ts +21 -10
  49. package/dist/types/current/track/hls/createHlsFactories.d.ts.map +1 -1
  50. package/dist/types/current/track/hls/createHlsFactories.js +9 -21
  51. package/dist/types/current/track/mse/MseTrack.d.ts +1 -2
  52. package/dist/types/current/track/mse/MseTrack.d.ts.map +1 -1
  53. package/dist/types/current/track/mse/MseTrack.js +3 -7
  54. package/dist/types/current/tsconfig.tsbuildinfo +1 -1
  55. package/dist/types/current/vinyl/VinylOptions.d.ts +18 -40
  56. package/dist/types/current/vinyl/VinylOptions.d.ts.map +1 -1
  57. package/dist/types/current/vinyl/VinylOptions.js +12 -11
  58. package/dist/types/current/vinyl/VinylPlayer.d.ts +0 -6
  59. package/dist/types/current/vinyl/VinylPlayer.d.ts.map +1 -1
  60. package/dist/types/current/vinyl/VinylPlayer.js +2 -23
  61. package/dist/types/current/vinyl/createVinylFactories.d.ts +32 -18
  62. package/dist/types/current/vinyl/createVinylFactories.d.ts.map +1 -1
  63. package/dist/types/current/vinyl/createVinylFactories.js +6 -0
  64. package/dist/types/ts3.7/index.d.ts +4 -0
  65. package/dist/types/ts3.7/streaming/AudioOptions.d.ts +39 -0
  66. package/dist/types/ts3.7/streaming/createDefaultMediaTimelineTransformer.d.ts +2 -2
  67. package/dist/types/ts3.7/text/HtmlTextTrackRenderer.d.ts +60 -0
  68. package/dist/types/ts3.7/text/SidecarTextTrackController.d.ts +65 -53
  69. package/dist/types/ts3.7/text/SidecarTextTrackLoader.d.ts +5 -0
  70. package/dist/types/ts3.7/text/TextTrack.d.ts +74 -20
  71. package/dist/types/ts3.7/text/TextTrackRenderer.d.ts +24 -0
  72. package/dist/types/ts3.7/text/mediaTextTrackProvider.d.ts +34 -0
  73. package/dist/types/ts3.7/text/parseWebVtt.d.ts +16 -2
  74. package/dist/types/ts3.7/text/pickPreferredTextTrack.d.ts +2 -1
  75. package/dist/types/ts3.7/track/Track.d.ts +1 -1
  76. package/dist/types/ts3.7/track/dash/createDashFactories.d.ts +21 -10
  77. package/dist/types/ts3.7/track/hls/createHlsFactories.d.ts +21 -10
  78. package/dist/types/ts3.7/track/mse/MseTrack.d.ts +1 -2
  79. package/dist/types/ts3.7/vinyl/VinylOptions.d.ts +18 -40
  80. package/dist/types/ts3.7/vinyl/VinylPlayer.d.ts +0 -6
  81. package/dist/types/ts3.7/vinyl/createVinylFactories.d.ts +32 -18
  82. package/dist/types/ts4.1/index.d.ts +4 -0
  83. package/dist/types/ts4.1/streaming/AudioOptions.d.ts +39 -0
  84. package/dist/types/ts4.1/streaming/createDefaultMediaTimelineTransformer.d.ts +2 -2
  85. package/dist/types/ts4.1/text/HtmlTextTrackRenderer.d.ts +60 -0
  86. package/dist/types/ts4.1/text/SidecarTextTrackController.d.ts +65 -53
  87. package/dist/types/ts4.1/text/SidecarTextTrackLoader.d.ts +5 -0
  88. package/dist/types/ts4.1/text/TextTrack.d.ts +74 -20
  89. package/dist/types/ts4.1/text/TextTrackRenderer.d.ts +24 -0
  90. package/dist/types/ts4.1/text/mediaTextTrackProvider.d.ts +34 -0
  91. package/dist/types/ts4.1/text/parseWebVtt.d.ts +16 -2
  92. package/dist/types/ts4.1/text/pickPreferredTextTrack.d.ts +2 -1
  93. package/dist/types/ts4.1/track/Track.d.ts +1 -1
  94. package/dist/types/ts4.1/track/dash/createDashFactories.d.ts +21 -10
  95. package/dist/types/ts4.1/track/hls/createHlsFactories.d.ts +21 -10
  96. package/dist/types/ts4.1/track/mse/MseTrack.d.ts +1 -2
  97. package/dist/types/ts4.1/vinyl/VinylOptions.d.ts +18 -40
  98. package/dist/types/ts4.1/vinyl/VinylPlayer.d.ts +0 -6
  99. package/dist/types/ts4.1/vinyl/createVinylFactories.d.ts +32 -18
  100. package/dist/types/ts4.7/index.d.ts +4 -0
  101. package/dist/types/ts4.7/streaming/AudioOptions.d.ts +39 -0
  102. package/dist/types/ts4.7/streaming/createDefaultMediaTimelineTransformer.d.ts +2 -2
  103. package/dist/types/ts4.7/text/HtmlTextTrackRenderer.d.ts +60 -0
  104. package/dist/types/ts4.7/text/SidecarTextTrackController.d.ts +65 -53
  105. package/dist/types/ts4.7/text/SidecarTextTrackLoader.d.ts +5 -0
  106. package/dist/types/ts4.7/text/TextTrack.d.ts +74 -20
  107. package/dist/types/ts4.7/text/TextTrackRenderer.d.ts +24 -0
  108. package/dist/types/ts4.7/text/mediaTextTrackProvider.d.ts +34 -0
  109. package/dist/types/ts4.7/text/parseWebVtt.d.ts +16 -2
  110. package/dist/types/ts4.7/text/pickPreferredTextTrack.d.ts +2 -1
  111. package/dist/types/ts4.7/track/Track.d.ts +1 -1
  112. package/dist/types/ts4.7/track/dash/createDashFactories.d.ts +21 -10
  113. package/dist/types/ts4.7/track/hls/createHlsFactories.d.ts +21 -10
  114. package/dist/types/ts4.7/track/mse/MseTrack.d.ts +1 -2
  115. package/dist/types/ts4.7/vinyl/VinylOptions.d.ts +18 -40
  116. package/dist/types/ts4.7/vinyl/VinylPlayer.d.ts +0 -6
  117. package/dist/types/ts4.7/vinyl/createVinylFactories.d.ts +32 -18
  118. package/dist/unit/types/current/index.d.ts +2 -0
  119. package/dist/unit/types/current/index.d.ts.map +1 -1
  120. package/dist/unit/types/current/index.js +2 -0
  121. package/dist/unit/types/current/streaming/MseTrack.test.js +13 -7
  122. package/dist/unit/types/current/streaming/createDefaultMediaTimelineTransformer.test.js +8 -4
  123. package/dist/unit/types/current/text/HtmlTextTrackRenderer.test.d.ts +2 -0
  124. package/dist/unit/types/current/text/HtmlTextTrackRenderer.test.d.ts.map +1 -0
  125. package/dist/unit/types/current/text/HtmlTextTrackRenderer.test.js +231 -0
  126. package/dist/unit/types/current/text/SidecarTextTrackController.test.js +432 -333
  127. package/dist/unit/types/current/text/SidecarTextTrackLoader.test.js +29 -0
  128. package/dist/unit/types/current/text/mediaTextTrackProvider.test.d.ts +2 -0
  129. package/dist/unit/types/current/text/mediaTextTrackProvider.test.d.ts.map +1 -0
  130. package/dist/unit/types/current/text/mediaTextTrackProvider.test.js +86 -0
  131. package/dist/unit/types/current/text/parseWebVtt.test.js +187 -6
  132. package/dist/unit/types/current/text/pickPreferredTextTrack.test.js +12 -1
  133. package/dist/unit/types/current/track/dash/createDashFactories.test.js +1 -2
  134. package/dist/unit/types/current/track/hls/buildHlsMediaTimeline.test.js +1 -2
  135. package/dist/unit/types/current/track/hls/createHlsFactories.test.js +1 -2
  136. package/dist/unit/types/current/tsconfig.tsbuildinfo +1 -1
  137. package/dist/unit/types/current/vinyl/VinylPlayerTextTracks.test.js +102 -177
  138. package/dist/unit/types/current/vinyl/createVinylFactories.test.js +2 -0
  139. package/dist/vinylTestUtil/index.js +14 -8
  140. package/dist/vinylTestUtil/index.js.map +2 -2
  141. package/dist/vinylTestUtil/types/current/mock/player/MockVinylPlayerDependencies.d.ts +3 -2
  142. package/dist/vinylTestUtil/types/current/mock/player/MockVinylPlayerDependencies.d.ts.map +1 -1
  143. package/dist/vinylTestUtil/types/current/mock/player/MockVinylPlayerDependencies.js +5 -2
  144. package/dist/vinylTestUtil/types/current/mock/text/MockTextTrackController.d.ts +3 -3
  145. package/dist/vinylTestUtil/types/current/mock/text/MockTextTrackController.d.ts.map +1 -1
  146. package/dist/vinylTestUtil/types/current/mock/text/MockTextTrackController.js +3 -3
  147. package/dist/vinylTestUtil/types/current/mock/track/dash/createMockDashDependencies.d.ts +1 -2
  148. package/dist/vinylTestUtil/types/current/mock/track/dash/createMockDashDependencies.d.ts.map +1 -1
  149. package/dist/vinylTestUtil/types/current/mock/track/dash/createMockDashDependencies.js +3 -2
  150. package/dist/vinylTestUtil/types/current/mock/track/hls/createMockHlsDependencies.d.ts +1 -2
  151. package/dist/vinylTestUtil/types/current/mock/track/hls/createMockHlsDependencies.d.ts.map +1 -1
  152. package/dist/vinylTestUtil/types/current/mock/track/hls/createMockHlsDependencies.js +3 -2
  153. package/dist/vinylTestUtil/types/current/tsconfig.tsbuildinfo +1 -1
  154. package/dist/vinylTestUtil/types/ts3.7/mock/player/MockVinylPlayerDependencies.d.ts +3 -2
  155. package/dist/vinylTestUtil/types/ts3.7/mock/text/MockTextTrackController.d.ts +3 -3
  156. package/dist/vinylTestUtil/types/ts3.7/mock/track/dash/createMockDashDependencies.d.ts +1 -2
  157. package/dist/vinylTestUtil/types/ts3.7/mock/track/hls/createMockHlsDependencies.d.ts +1 -2
  158. package/dist/vinylTestUtil/types/ts4.1/mock/player/MockVinylPlayerDependencies.d.ts +3 -2
  159. package/dist/vinylTestUtil/types/ts4.1/mock/text/MockTextTrackController.d.ts +3 -3
  160. package/dist/vinylTestUtil/types/ts4.1/mock/track/dash/createMockDashDependencies.d.ts +1 -2
  161. package/dist/vinylTestUtil/types/ts4.1/mock/track/hls/createMockHlsDependencies.d.ts +1 -2
  162. package/dist/vinylTestUtil/types/ts4.7/mock/player/MockVinylPlayerDependencies.d.ts +3 -2
  163. package/dist/vinylTestUtil/types/ts4.7/mock/text/MockTextTrackController.d.ts +3 -3
  164. package/dist/vinylTestUtil/types/ts4.7/mock/track/dash/createMockDashDependencies.d.ts +1 -2
  165. package/dist/vinylTestUtil/types/ts4.7/mock/track/hls/createMockHlsDependencies.d.ts +1 -2
  166. package/docs/USAGE.md +82 -31
  167. package/package.json +9 -9
@@ -15182,7 +15182,7 @@ ${this.lastInitStack}` : "";
15182
15182
  // src/global/environment.ts
15183
15183
  var vinylVersion = parseVersion(
15184
15184
  /* istanbul ignore next */
15185
- "2.0.4"
15185
+ "3.0.0"
15186
15186
  );
15187
15187
 
15188
15188
  // src/global/globalRef.ts
@@ -16425,6 +16425,18 @@ ${this.lastInitStack}` : "";
16425
16425
  }
16426
16426
  };
16427
16427
 
16428
+ // src/streaming/AudioOptions.ts
16429
+ var defaultAudioOptions = {
16430
+ selection: { language: null, descriptive: false }
16431
+ };
16432
+ var audioSelectionValidator = object({
16433
+ language: string().or(array(string()).readonly()).orNull().optional(),
16434
+ descriptive: boolean().optional()
16435
+ });
16436
+ var audioOptionsValidator = object({
16437
+ selection: audioSelectionValidator.optional()
16438
+ });
16439
+
16428
16440
  // src/streaming/SegmentController.ts
16429
16441
  var SEGMENT_START_AFFORDANCE = 0.2;
16430
16442
 
@@ -18219,7 +18231,7 @@ ${this.lastInitStack}` : "";
18219
18231
  return canPlayMimeType(deps, quality);
18220
18232
  }
18221
18233
  async function transformTimeline(timeline) {
18222
- var _a3;
18234
+ var _a3, _b;
18223
18235
  const codecOverrides = deps.options.value.codecOverrides;
18224
18236
  let t = filterTimelineQualities(
18225
18237
  (quality) => canPlayWithOverrides(quality, codecOverrides),
@@ -18236,18 +18248,14 @@ ${this.lastInitStack}` : "";
18236
18248
  throwSamplingRatesUnsupported,
18237
18249
  t
18238
18250
  );
18251
+ const selection = deps.options.value.audio.selection;
18239
18252
  t = filterTimelineQualities(
18240
- createAudioDescriptionFilter(
18241
- (_a3 = deps.options.value.preferDescriptiveAudio) != null ? _a3 : false
18242
- ),
18253
+ createAudioDescriptionFilter((_a3 = selection == null ? void 0 : selection.descriptive) != null ? _a3 : false),
18243
18254
  throwNoPlayableAudio,
18244
18255
  t
18245
18256
  );
18246
18257
  t = filterTimelineQualities(
18247
- createLanguageFilter(
18248
- deps.options.value.preferredAudioLanguage,
18249
- "audio"
18250
- ),
18258
+ createLanguageFilter((_b = selection == null ? void 0 : selection.language) != null ? _b : null, "audio"),
18251
18259
  throwLanguagesUnsupported,
18252
18260
  t
18253
18261
  );
@@ -18255,8 +18263,7 @@ ${this.lastInitStack}` : "";
18255
18263
  }
18256
18264
  return combineData({
18257
18265
  timeline: deps.mediaTimeline,
18258
- preferredAudioLanguage: deps.options.pick("preferredAudioLanguage"),
18259
- preferDescriptiveAudio: deps.options.pick("preferDescriptiveAudio"),
18266
+ audio: deps.options.pick("audio"),
18260
18267
  codecOverrides: deps.options.pick("codecOverrides")
18261
18268
  }).map(async ({ timeline }) => {
18262
18269
  return transformTimeline(await timeline);
@@ -19203,9 +19210,201 @@ ${this.lastInitStack}` : "";
19203
19210
  return out;
19204
19211
  }
19205
19212
 
19213
+ // src/text/HtmlTextTrackRenderer.ts
19214
+ var ROW_EM = 1.5;
19215
+ function computeCueLayout(cue) {
19216
+ const style = {};
19217
+ style.textAlign = cue.align || "center";
19218
+ if (cue.vertical === "rl") style.writingMode = "vertical-rl";
19219
+ else if (cue.vertical === "lr") style.writingMode = "vertical-lr";
19220
+ const size = typeof cue.size === "number" ? cue.size : 100;
19221
+ style.maxWidth = `${size}%`;
19222
+ const line = typeof cue.line === "number" ? cue.line : null;
19223
+ const position = typeof cue.position === "number" ? cue.position : null;
19224
+ if (line == null && position == null) return style;
19225
+ style.position = "absolute";
19226
+ const transforms = [];
19227
+ if (position == null) {
19228
+ style.left = "50%";
19229
+ transforms.push("translateX(-50%)");
19230
+ } else {
19231
+ if (cue.positionAlign === "line-right") {
19232
+ style.right = `${100 - position}%`;
19233
+ } else if (cue.positionAlign === "line-left") {
19234
+ style.left = `${position}%`;
19235
+ } else {
19236
+ style.left = `${position}%`;
19237
+ transforms.push("translateX(-50%)");
19238
+ }
19239
+ }
19240
+ if (line == null) {
19241
+ style.bottom = "0";
19242
+ } else {
19243
+ if (cue.snapToLines === false) {
19244
+ if (cue.lineAlign === "end") {
19245
+ style.bottom = `${100 - line}%`;
19246
+ } else if (cue.lineAlign === "center") {
19247
+ style.top = `${line}%`;
19248
+ transforms.push("translateY(-50%)");
19249
+ } else {
19250
+ style.top = `${line}%`;
19251
+ }
19252
+ } else if (line < 0) {
19253
+ style.bottom = `${(-line - 1) * ROW_EM}em`;
19254
+ } else {
19255
+ style.top = `${line * ROW_EM}em`;
19256
+ }
19257
+ }
19258
+ if (transforms.length > 0) style.transform = transforms.join(" ");
19259
+ return style;
19260
+ }
19261
+ var HTML_ESCAPE = {
19262
+ "&": "&amp;",
19263
+ "<": "&lt;",
19264
+ ">": "&gt;",
19265
+ '"': "&quot;"
19266
+ };
19267
+ function escapeHtml(text) {
19268
+ return text.replace(/["&<>]/g, (ch) => HTML_ESCAPE[ch]);
19269
+ }
19270
+ var PASSTHROUGH_TAGS = /* @__PURE__ */ new Set(["i", "b", "u", "ruby", "rt"]);
19271
+ var SPAN_TAGS = /* @__PURE__ */ new Set(["c", "v", "lang"]);
19272
+ function openTag(inner) {
19273
+ if (/^\d/.test(inner)) return "";
19274
+ const head = inner.split(/\s/, 1)[0];
19275
+ const [name, ...classes] = head.split(".");
19276
+ const annotation = inner.slice(head.length).trim();
19277
+ const classAttr = classes.filter(Boolean).length ? ` class="${escapeHtml(classes.filter(Boolean).join(" "))}"` : "";
19278
+ if (PASSTHROUGH_TAGS.has(name)) return `<${name}${classAttr}>`;
19279
+ if (name === "v") {
19280
+ const title = annotation ? ` title="${escapeHtml(annotation)}"` : "";
19281
+ return `<span${classAttr}${title}>`;
19282
+ }
19283
+ if (name === "lang") {
19284
+ const lang = annotation ? ` lang="${escapeHtml(annotation)}"` : "";
19285
+ return `<span${classAttr}${lang}>`;
19286
+ }
19287
+ if (name === "c") return `<span${classAttr}>`;
19288
+ return "";
19289
+ }
19290
+ function closeTag(name) {
19291
+ if (PASSTHROUGH_TAGS.has(name)) return `</${name}>`;
19292
+ if (SPAN_TAGS.has(name)) return "</span>";
19293
+ return "";
19294
+ }
19295
+ function renderCueText(text) {
19296
+ var _a3;
19297
+ let html = "";
19298
+ for (const token of (_a3 = text.match(/<[^<>]*>|[^<]+|</g)) != null ? _a3 : []) {
19299
+ if (token[0] !== "<" || token === "<") {
19300
+ html += escapeHtml(token).replace(/\n/g, "<br>");
19301
+ } else {
19302
+ const inner = token.slice(1, -1);
19303
+ html += inner[0] === "/" ? closeTag(
19304
+ inner.slice(1).split(/[.\s]/, 1)[0].toLowerCase()
19305
+ ) : openTag(inner);
19306
+ }
19307
+ }
19308
+ return html;
19309
+ }
19310
+ function scopeCueRules(css) {
19311
+ return css.replace(/::cue\(([^)]*)\)/g, ".vinyl-text-cue $1").replace(/::cue\b/g, ".vinyl-text-cue");
19312
+ }
19313
+ var HtmlTextTrackRenderer = class {
19314
+ constructor() {
19315
+ /** The overlay container to mount over the video. */
19316
+ __publicField(this, "element");
19317
+ __publicField(this, "track", null);
19318
+ // Holds the authored STYLE-block CSS (::cue rules rewritten to target our
19319
+ // cue elements). A <style> renders nothing itself, so it lives in-container.
19320
+ __publicField(this, "styleElement");
19321
+ __publicField(this, "onCueChange", () => this.renderActiveCues());
19322
+ this.element = document.createElement("div");
19323
+ this.element.className = "vinyl-text-container";
19324
+ Object.assign(this.element.style, {
19325
+ display: "flex",
19326
+ flexDirection: "column",
19327
+ justifyContent: "flex-end",
19328
+ alignItems: "center",
19329
+ pointerEvents: "none"
19330
+ });
19331
+ this.styleElement = document.createElement("style");
19332
+ this.element.appendChild(this.styleElement);
19333
+ }
19334
+ setTextTrack(track2) {
19335
+ var _a3;
19336
+ if (track2 === this.track) return;
19337
+ (_a3 = this.track) == null ? void 0 : _a3.removeEventListener("cuechange", this.onCueChange);
19338
+ this.track = track2;
19339
+ this.styleElement.textContent = "";
19340
+ if (!track2) {
19341
+ this.element.replaceChildren(this.styleElement);
19342
+ return;
19343
+ }
19344
+ track2.addEventListener("cuechange", this.onCueChange);
19345
+ this.renderActiveCues();
19346
+ }
19347
+ setStyles(styles) {
19348
+ this.styleElement.textContent = styles.map(scopeCueRules).join("\n");
19349
+ }
19350
+ dispose() {
19351
+ this.setTextTrack(null);
19352
+ }
19353
+ renderActiveCues() {
19354
+ var _a3;
19355
+ const active = (_a3 = this.track.activeCues) != null ? _a3 : [];
19356
+ const children = [this.styleElement];
19357
+ for (let i = 0; i < active.length; i++) {
19358
+ children.push(this.createCueBox(active[i]));
19359
+ }
19360
+ this.element.replaceChildren(...children);
19361
+ }
19362
+ createCueBox(cue) {
19363
+ var _a3;
19364
+ const box2 = document.createElement("div");
19365
+ box2.className = "vinyl-text-cue";
19366
+ Object.assign(box2.style, computeCueLayout(cue));
19367
+ box2.innerHTML = renderCueText((_a3 = cue.text) != null ? _a3 : "");
19368
+ return box2;
19369
+ }
19370
+ };
19371
+
19372
+ // src/text/mediaTextTrackProvider.ts
19373
+ function createMediaTextTrackProvider(deps) {
19374
+ const { media } = deps;
19375
+ const cache = /* @__PURE__ */ new Map();
19376
+ const isAttached = (track2) => {
19377
+ for (let i = 0; i < media.textTracks.length; i++) {
19378
+ if (media.textTracks[i] === track2) return true;
19379
+ }
19380
+ return false;
19381
+ };
19382
+ const create = (kind, label, language) => {
19383
+ const track2 = media.addTextTrack(kind, label, language);
19384
+ return {
19385
+ track: track2,
19386
+ clear() {
19387
+ if (!track2.cues) return;
19388
+ const all = Array.from(track2.cues);
19389
+ for (const cue of all) track2.removeCue(cue);
19390
+ }
19391
+ };
19392
+ };
19393
+ return {
19394
+ getOrCreate(kind, label, language) {
19395
+ const key = `${kind}-${label}-${language}`;
19396
+ const cached = cache.get(key);
19397
+ if (cached && isAttached(cached.track)) return cached;
19398
+ const ref = create(kind, label, language);
19399
+ cache.set(key, ref);
19400
+ return ref;
19401
+ }
19402
+ };
19403
+ }
19404
+
19206
19405
  // src/text/parseWebVtt.ts
19207
19406
  var WEBVTT_HEADER = "WEBVTT";
19208
- var TIME_RE = /^\s*(?:(\d+):)?([0-5]?\d):([0-5]?\d)\.(\d{3})\s*-->\s*(?:(\d+):)?([0-5]?\d):([0-5]?\d)\.(\d{3})(?:\s.*)?$/;
19407
+ var TIMESTAMP_RE = /^(?:(\d+):)?([0-5]\d):([0-5]\d)\.(\d{3})/;
19209
19408
  function parseWebVtt(input) {
19210
19409
  if (typeof input !== "string") {
19211
19410
  throw new ValidationError(
@@ -19223,6 +19422,7 @@ ${this.lastInitStack}` : "";
19223
19422
  );
19224
19423
  }
19225
19424
  const cues = [];
19425
+ const styles = [];
19226
19426
  let i = 1;
19227
19427
  while (i < lines.length && lines[i] !== "") i++;
19228
19428
  while (i < lines.length) {
@@ -19230,49 +19430,71 @@ ${this.lastInitStack}` : "";
19230
19430
  i++;
19231
19431
  continue;
19232
19432
  }
19233
- if (lines[i] === "NOTE" || lines[i].startsWith("NOTE ")) {
19433
+ if (lines[i] === "NOTE" || lines[i].startsWith("NOTE ") || lines[i].startsWith("NOTE ") || lines[i] === "REGION") {
19234
19434
  i++;
19235
19435
  while (i < lines.length && lines[i] !== "") i++;
19236
19436
  continue;
19237
19437
  }
19238
- if (lines[i] === "STYLE" || lines[i] === "REGION") {
19438
+ if (lines[i] === "STYLE") {
19239
19439
  i++;
19440
+ const start = i;
19240
19441
  while (i < lines.length && lines[i] !== "") i++;
19442
+ const css = lines.slice(start, i).join("\n").trim();
19443
+ if (css) styles.push(css);
19241
19444
  continue;
19242
19445
  }
19243
19446
  let id = null;
19244
- let timingLine;
19245
- const firstLine = lines[i];
19246
- if (TIME_RE.test(firstLine)) {
19247
- timingLine = firstLine;
19248
- i++;
19249
- } else {
19250
- id = firstLine;
19447
+ if (!lines[i].includes("-->")) {
19448
+ id = lines[i];
19251
19449
  i++;
19252
19450
  if (i >= lines.length) break;
19253
- timingLine = lines[i];
19254
- i++;
19255
19451
  }
19256
- const match = TIME_RE.exec(timingLine);
19257
- if (!match) {
19452
+ const timing = parseTiming(lines[i]);
19453
+ i++;
19454
+ if (!timing) {
19258
19455
  while (i < lines.length && lines[i] !== "") i++;
19259
19456
  continue;
19260
19457
  }
19261
- const startTime = toSeconds(match[1], match[2], match[3], match[4]);
19262
- const endTime = toSeconds(match[5], match[6], match[7], match[8]);
19263
19458
  const payloadLines = [];
19264
19459
  while (i < lines.length && lines[i] !== "") {
19265
19460
  payloadLines.push(lines[i]);
19266
19461
  i++;
19267
19462
  }
19268
- cues.push({
19269
- id,
19270
- startTime,
19271
- endTime,
19272
- text: payloadLines.join("\n")
19273
- });
19274
- }
19275
- return { cues };
19463
+ const text2 = payloadLines.join("\n");
19464
+ cues.push(
19465
+ timing.settings === void 0 ? {
19466
+ id,
19467
+ startTime: timing.startTime,
19468
+ endTime: timing.endTime,
19469
+ text: text2
19470
+ } : {
19471
+ id,
19472
+ startTime: timing.startTime,
19473
+ endTime: timing.endTime,
19474
+ text: text2,
19475
+ settings: timing.settings
19476
+ }
19477
+ );
19478
+ }
19479
+ return { cues, styles };
19480
+ }
19481
+ function parseTiming(line) {
19482
+ const arrow = line.indexOf("-->");
19483
+ if (arrow < 0) return null;
19484
+ const before = line.slice(0, arrow).trim();
19485
+ const start = TIMESTAMP_RE.exec(before);
19486
+ if (!start || start[0].length !== before.length) return null;
19487
+ const rest = line.slice(arrow + 3).trim();
19488
+ const end = TIMESTAMP_RE.exec(rest);
19489
+ if (!end) return null;
19490
+ return {
19491
+ startTime: toSeconds(start[1], start[2], start[3], start[4]),
19492
+ endTime: toSeconds(end[1], end[2], end[3], end[4]),
19493
+ settings: parseCueSettings(rest.slice(end[0].length))
19494
+ };
19495
+ }
19496
+ function toSeconds(hours, minutes, seconds, millis) {
19497
+ return (hours ? Number(hours) * 3600 : 0) + Number(minutes) * 60 + Number(seconds) + Number(millis) / 1e3;
19276
19498
  }
19277
19499
  function isWebVttHeader(line) {
19278
19500
  if (!line.startsWith(WEBVTT_HEADER)) return false;
@@ -19280,18 +19502,80 @@ ${this.lastInitStack}` : "";
19280
19502
  const next = line.charCodeAt(WEBVTT_HEADER.length);
19281
19503
  return next === 32 || next === 9;
19282
19504
  }
19283
- function toSeconds(hours, minutes, seconds, millis) {
19284
- const h = hours ? Number(hours) : 0;
19285
- const m = Number(minutes);
19286
- const s = Number(seconds);
19287
- const ms = Number(millis);
19288
- return h * 3600 + m * 60 + s + ms / 1e3;
19505
+ var ALIGN = /* @__PURE__ */ new Set(["start", "center", "end", "left", "right"]);
19506
+ var LINE_ALIGN = /* @__PURE__ */ new Set(["start", "center", "end"]);
19507
+ var POSITION_ALIGN = /* @__PURE__ */ new Set(["line-left", "center", "line-right", "auto"]);
19508
+ var VERTICAL = /* @__PURE__ */ new Set(["rl", "lr"]);
19509
+ function parseCueSettings(text) {
19510
+ if (!text) return void 0;
19511
+ const settings = {};
19512
+ for (const token of text.trim().split(/\s+/)) {
19513
+ const colon = token.indexOf(":");
19514
+ if (colon <= 0) continue;
19515
+ const key = token.slice(0, colon);
19516
+ const value = token.slice(colon + 1);
19517
+ switch (key) {
19518
+ case "align": {
19519
+ const align = value === "middle" ? "center" : value;
19520
+ if (ALIGN.has(align)) settings.align = align;
19521
+ break;
19522
+ }
19523
+ case "vertical":
19524
+ if (VERTICAL.has(value))
19525
+ settings.vertical = value;
19526
+ break;
19527
+ case "size": {
19528
+ const size = parsePercent(value);
19529
+ if (size != null) settings.size = size;
19530
+ break;
19531
+ }
19532
+ case "line":
19533
+ parseLine(value, settings);
19534
+ break;
19535
+ case "position":
19536
+ parsePosition(value, settings);
19537
+ break;
19538
+ }
19539
+ }
19540
+ return Object.keys(settings).length > 0 ? settings : void 0;
19541
+ }
19542
+ function parseLine(value, settings) {
19543
+ const [pos, align] = value.split(",");
19544
+ if (pos.endsWith("%")) {
19545
+ const percent = parsePercent(pos);
19546
+ if (percent == null) return;
19547
+ settings.line = percent;
19548
+ settings.snapToLines = false;
19549
+ } else {
19550
+ const n = Number(pos);
19551
+ if (!Number.isInteger(n)) return;
19552
+ settings.line = n;
19553
+ settings.snapToLines = true;
19554
+ }
19555
+ const lineAlign = align === "middle" ? "center" : align;
19556
+ if (lineAlign && LINE_ALIGN.has(lineAlign))
19557
+ settings.lineAlign = lineAlign;
19558
+ }
19559
+ function parsePosition(value, settings) {
19560
+ const [pos, align] = value.split(",");
19561
+ const percent = parsePercent(pos);
19562
+ if (percent == null) return;
19563
+ settings.position = percent;
19564
+ const positionAlign = align === "middle" ? "center" : align;
19565
+ if (positionAlign && POSITION_ALIGN.has(positionAlign))
19566
+ settings.positionAlign = positionAlign;
19567
+ }
19568
+ function parsePercent(value) {
19569
+ if (!value.endsWith("%")) return null;
19570
+ const n = Number(value.slice(0, -1));
19571
+ return Number.isFinite(n) ? n : null;
19289
19572
  }
19290
19573
 
19291
19574
  // src/text/pickPreferredTextTrack.ts
19292
19575
  function resolvePreferredTextLanguages(preferred) {
19293
19576
  if (typeof preferred === "string") return [preferred];
19294
19577
  if (preferred) return preferred;
19578
+ if (typeof navigator !== "undefined") return [...navigator.languages];
19295
19579
  return [];
19296
19580
  }
19297
19581
  function pickPreferredTextTrack(tracks, preferred) {
@@ -19329,7 +19613,7 @@ ${this.lastInitStack}` : "";
19329
19613
  var IDLE_POLL_SECONDS = 0.25;
19330
19614
  async function loadWebVttCues(uri, options) {
19331
19615
  var _a3, _b, _c;
19332
- const { abort, requestInit, variables, onCues, getCurrentTime } = options;
19616
+ const { abort, requestInit, variables, onCues, onStyles, getCurrentTime } = options;
19333
19617
  const lookAhead = (_a3 = options.lookAhead) != null ? _a3 : DEFAULT_TEXT_LOOK_AHEAD;
19334
19618
  const lookBehind = (_b = options.lookBehind) != null ? _b : DEFAULT_TEXT_LOOK_BEHIND;
19335
19619
  const init = requestInit != null ? requestInit : void 0;
@@ -19340,6 +19624,7 @@ ${this.lastInitStack}` : "";
19340
19624
  abort,
19341
19625
  variables: variables != null ? variables : void 0,
19342
19626
  onCues,
19627
+ onStyles,
19343
19628
  getCurrentTime,
19344
19629
  lookAhead,
19345
19630
  lookBehind
@@ -19356,13 +19641,15 @@ ${this.lastInitStack}` : "";
19356
19641
  abort,
19357
19642
  variables: variables != null ? variables : void 0,
19358
19643
  onCues,
19644
+ onStyles,
19359
19645
  getCurrentTime,
19360
19646
  lookAhead,
19361
19647
  lookBehind
19362
19648
  });
19363
19649
  }
19364
- const cues = parseWebVtt(body).cues;
19365
- if (cues.length > 0) onCues(cues);
19650
+ const doc = parseWebVtt(body);
19651
+ if (doc.cues.length > 0) onCues(doc.cues);
19652
+ if (onStyles && doc.styles.length > 0) onStyles(doc.styles);
19366
19653
  }
19367
19654
  function looksLikeMediaPlaylist(uri) {
19368
19655
  const path = uri.split(/[?#]/)[0];
@@ -19402,8 +19689,9 @@ ${this.lastInitStack}` : "";
19402
19689
  abort: ctx.abort
19403
19690
  });
19404
19691
  const body = await res.text();
19405
- const cues = parseWebVtt(body).cues;
19406
- if (cues.length > 0) ctx.onCues(cues);
19692
+ const doc = parseWebVtt(body);
19693
+ if (doc.cues.length > 0) ctx.onCues(doc.cues);
19694
+ if (ctx.onStyles && doc.styles.length > 0) ctx.onStyles(doc.styles);
19407
19695
  }
19408
19696
  }
19409
19697
  function pickInWindow(pending2, ranges, ctx) {
@@ -19457,22 +19745,45 @@ ${this.lastInitStack}` : "";
19457
19745
  });
19458
19746
 
19459
19747
  // src/text/SidecarTextTrackController.ts
19748
+ function resolveSelectedTrack(tracks, options) {
19749
+ var _a3, _b, _c, _d;
19750
+ const mode = (_a3 = options.enabled) != null ? _a3 : "forced";
19751
+ if (mode === "off") return null;
19752
+ const selection = (_b = options.selection) != null ? _b : {};
19753
+ if (selection.id != null) {
19754
+ return (_c = tracks.find((t) => t.id === selection.id)) != null ? _c : null;
19755
+ }
19756
+ let pool = tracks;
19757
+ if (selection.kind != null) {
19758
+ pool = pool.filter((t) => t.kind === selection.kind);
19759
+ }
19760
+ const forced = (_d = selection.forced) != null ? _d : mode === "forced";
19761
+ pool = pool.filter((t) => t.forced === forced);
19762
+ return pickPreferredTextTrack(pool, selection.language);
19763
+ }
19460
19764
  var SidecarTextTrackController = class extends EventHostImpl {
19461
19765
  constructor(deps) {
19462
- var _a3, _b;
19463
19766
  super();
19464
19767
  __publicField(this, "deps", deps);
19768
+ // Whether the owning media track is current (rendering allowed). Toggled by
19769
+ // activate()/deactivate() across suspensions such as ad breaks.
19770
+ __publicField(this, "_active", false);
19465
19771
  __publicField(this, "_tracks", []);
19466
- __publicField(this, "_active", null);
19467
- __publicField(this, "_domTrack", null);
19468
- __publicField(this, "loadAbort", null);
19469
- // True once the app has made an explicit selection (including null). Until
19470
- // then, forced tracks auto-select on discovery.
19471
- __publicField(this, "_userSelected", false);
19472
- __publicField(this, "preferredLanguageSub");
19473
- this.preferredLanguageSub = (_b = (_a3 = deps.preferredTextLanguage) == null ? void 0 : _a3.onData((value, previous) => {
19474
- if (previous !== void 0) this.applyPreferredLanguage(value);
19475
- })) != null ? _b : null;
19772
+ __publicField(this, "_activeTextTrack", null);
19773
+ // The DOM text track currently rendering cues, or null when hidden. Owned by
19774
+ // the provider (never created/removed here).
19775
+ __publicField(this, "_ref", null);
19776
+ __publicField(this, "loadAbort", createAbortSlot());
19777
+ __publicField(this, "disposer", createDisposer());
19778
+ const { add } = this.disposer;
19779
+ add(deps.options.onData(() => this.refreshSelection()));
19780
+ add(
19781
+ deps.textTracks.onData((tracksPromise) => {
19782
+ tracksPromise.then((tracks) => this.setTracks(tracks)).catch((error) => {
19783
+ logError(this, "text tracks promise rejected", error);
19784
+ });
19785
+ })
19786
+ );
19476
19787
  }
19477
19788
  get [Symbol.toStringTag]() {
19478
19789
  return "SidecarTextTrackController";
@@ -19481,120 +19792,89 @@ ${this.lastInitStack}` : "";
19481
19792
  return this._tracks;
19482
19793
  }
19483
19794
  get activeTextTrack() {
19795
+ return this._activeTextTrack;
19796
+ }
19797
+ get active() {
19484
19798
  return this._active;
19485
19799
  }
19800
+ get disposed() {
19801
+ return this.disposer.disposed;
19802
+ }
19486
19803
  /**
19487
- * Replaces the discovered text track list. Emits `textTracksChange`. If
19488
- * the previously active track is no longer present, clears the active
19489
- * selection and emits `activeTextTrackChange`.
19804
+ * Replaces the discovered track list, emits `textTracksChange`, then
19805
+ * re-applies the current selection against the new list.
19490
19806
  */
19491
- setTextTracks(tracks) {
19492
- var _a3;
19493
- if (equalDeep(this._tracks, tracks)) return;
19807
+ setTracks(tracks) {
19808
+ if (this.disposed) return;
19494
19809
  const previous = this._tracks;
19810
+ if (equalDeep(previous, tracks)) return;
19495
19811
  this._tracks = tracks;
19496
19812
  this.dispatch("textTracksChange", { previous, current: tracks });
19497
- if (this._active && !tracks.some((t) => t.id === this._active.id)) {
19498
- this.selectById(null);
19499
- }
19500
- const preferred = (_a3 = this.deps.preferredTextLanguage) == null ? void 0 : _a3.value;
19501
- if (preferred != null) {
19502
- this.applyPreferredLanguage(preferred);
19503
- } else if (!this._userSelected && !this._active) {
19504
- const forced = this.pickForcedTrack(tracks);
19505
- if (forced) this.selectById(forced.id);
19506
- }
19507
- }
19508
- setActiveTextTrack(id) {
19509
- this._userSelected = true;
19510
- this.selectById(id);
19511
- }
19512
- /**
19513
- * Selects the track best matching the preferred language(s), or clears the
19514
- * selection when the preference is null or nothing matches.
19515
- */
19516
- applyPreferredLanguage(preferred) {
19517
- var _a3;
19518
- const target = pickPreferredTextTrack(this._tracks, preferred);
19519
- this.setActiveTextTrack((_a3 = target == null ? void 0 : target.id) != null ? _a3 : null);
19813
+ this.refreshSelection();
19520
19814
  }
19521
19815
  /**
19522
- * Applies a selection by id (null clears it) without marking it as an
19523
- * explicit user choice. Shared by setActiveTextTrack and forced auto-select.
19816
+ * Recomputes the selection from the current options and track list,
19817
+ * emitting `activeTextTrackChange` on change and (re)rendering when active.
19524
19818
  */
19525
- selectById(id) {
19526
- var _a3, _b;
19527
- const next = id == null ? null : this._tracks.find((t) => t.id === id);
19528
- if (id != null && !next) return;
19529
- const target = next != null ? next : null;
19530
- if ((target == null ? void 0 : target.id) === ((_a3 = this._active) == null ? void 0 : _a3.id)) return;
19531
- const previous = this._active;
19532
- this.clearDomTrack();
19533
- (_b = this.loadAbort) == null ? void 0 : _b.abort();
19534
- this.loadAbort = null;
19535
- this._active = target;
19536
- this.dispatch("activeTextTrackChange", {
19537
- previous,
19538
- current: target
19539
- });
19540
- if (target) this.startLoad(target);
19541
- }
19542
- /**
19543
- * The forced track to auto-select, or null. Prefers the default track's
19544
- * language, else the first forced track.
19545
- */
19546
- pickForcedTrack(tracks) {
19547
- var _a3;
19548
- const forced = tracks.filter((t) => t.forced);
19549
- if (forced.length === 0) return null;
19550
- const defaultLang = (_a3 = tracks.find((t) => t.default)) == null ? void 0 : _a3.language;
19551
- if (defaultLang != null) {
19552
- const match = forced.find((t) => t.language === defaultLang);
19553
- if (match) return match;
19819
+ refreshSelection() {
19820
+ const previous = this._activeTextTrack;
19821
+ const target = resolveSelectedTrack(
19822
+ this._tracks,
19823
+ this.deps.options.value
19824
+ );
19825
+ const trackChanged = (target == null ? void 0 : target.id) !== (previous == null ? void 0 : previous.id);
19826
+ if (trackChanged) {
19827
+ this._activeTextTrack = target;
19828
+ this.dispatch("activeTextTrackChange", {
19829
+ previous,
19830
+ current: target
19831
+ });
19554
19832
  }
19555
- return forced[0];
19556
- }
19557
- suspend() {
19558
- var _a3;
19559
- (_a3 = this.loadAbort) == null ? void 0 : _a3.abort();
19560
- this.loadAbort = null;
19561
- this.clearDomTrack();
19833
+ if (!this._active) return;
19834
+ if (!target) this.hideCurrent();
19835
+ else if (trackChanged || !this._ref) this.startLoad(target);
19562
19836
  }
19563
- resume() {
19564
- if (!this._active || this._domTrack) return;
19565
- this.startLoad(this._active);
19837
+ deactivate() {
19838
+ this._active = false;
19839
+ this.hideCurrent();
19566
19840
  }
19567
- /**
19568
- * Cancels any in-flight load, clears the DOM TextTrack, and resets state.
19569
- * Called by the host when the underlying media is unloaded.
19570
- */
19571
- dispose() {
19572
- var _a3, _b;
19573
- super.dispose();
19574
- (_a3 = this.preferredLanguageSub) == null ? void 0 : _a3.call(this);
19575
- (_b = this.loadAbort) == null ? void 0 : _b.abort();
19576
- this.loadAbort = null;
19577
- this.clearDomTrack();
19578
- this._tracks = [];
19579
- this._active = null;
19841
+ activate() {
19842
+ this._active = true;
19843
+ if (!this._activeTextTrack || this._ref) return;
19844
+ this.startLoad(this._activeTextTrack);
19580
19845
  }
19581
19846
  startLoad(track2) {
19582
- const media = this.deps.media;
19583
- if (!media) return;
19584
- const abort = new Abort();
19585
- this.loadAbort = abort;
19586
- const dom = this.ensureDomTrack(media, track2);
19847
+ var _a3, _b;
19848
+ this.hideCurrent();
19849
+ const abort = this.loadAbort.value;
19850
+ const ref = this.deps.textTrackProvider.getOrCreate(
19851
+ track2.kind,
19852
+ track2.label,
19853
+ (_a3 = track2.language) != null ? _a3 : void 0
19854
+ );
19855
+ const renderer = this.deps.textTrackRenderer;
19856
+ ref.track.mode = renderer ? "hidden" : "showing";
19857
+ ref.clear();
19858
+ this._ref = ref;
19859
+ renderer == null ? void 0 : renderer.setTextTrack(ref.track);
19587
19860
  const CueCtor = getVttCueConstructor();
19588
19861
  const seen = /* @__PURE__ */ new Set();
19862
+ const styles = /* @__PURE__ */ new Set();
19863
+ const setStyles = (_b = renderer == null ? void 0 : renderer.setStyles) == null ? void 0 : _b.bind(renderer);
19589
19864
  loadWebVttCues(track2.uri, {
19590
19865
  abort,
19591
19866
  requestInit: this.deps.requestInit,
19592
19867
  variables: track2.variables,
19593
- getCurrentTime: () => media.currentTime,
19868
+ getCurrentTime: () => this.deps.playbackController.currentTime,
19594
19869
  onCues: (cues) => {
19595
19870
  if (abort.aborted()) return;
19596
- if (CueCtor) this.appendCues(dom, CueCtor, cues, seen);
19597
- }
19871
+ if (CueCtor) this.appendCues(ref.track, CueCtor, cues, seen);
19872
+ },
19873
+ onStyles: setStyles ? (blocks) => {
19874
+ if (abort.aborted()) return;
19875
+ for (const block of blocks) styles.add(block);
19876
+ setStyles([...styles]);
19877
+ } : void 0
19598
19878
  }).catch((error) => {
19599
19879
  if (abort.aborted()) return;
19600
19880
  this.dispatch("textTrackError", {
@@ -19603,43 +19883,45 @@ ${this.lastInitStack}` : "";
19603
19883
  });
19604
19884
  });
19605
19885
  }
19606
- ensureDomTrack(media, track2) {
19607
- var _a3;
19608
- const dom = media.addTextTrack(
19609
- track2.kind,
19610
- track2.label,
19611
- (_a3 = track2.language) != null ? _a3 : void 0
19612
- );
19613
- dom.mode = "showing";
19614
- this._domTrack = dom;
19615
- return dom;
19616
- }
19617
19886
  appendCues(dom, CueCtor, cues, seen) {
19618
- var _a3;
19619
19887
  for (const cue of cues) {
19620
19888
  const key = `${cue.startTime}|${cue.endTime}|${cue.text}`;
19621
19889
  if (seen.has(key)) continue;
19622
19890
  seen.add(key);
19623
19891
  const domCue = new CueCtor(cue.startTime, cue.endTime, cue.text);
19624
19892
  if (cue.id != null) domCue.id = cue.id;
19625
- const cueStyle = (_a3 = this.deps.cueStyle) == null ? void 0 : _a3.value;
19626
- if (cueStyle && isVttCue(domCue)) {
19627
- applyVttCueStyle(domCue, cueStyle);
19893
+ if (cue.settings && isVttCue(domCue)) {
19894
+ applyVttCueStyle(domCue, cue.settings);
19628
19895
  }
19629
19896
  dom.addCue(domCue);
19630
19897
  }
19631
19898
  }
19632
- clearDomTrack() {
19633
- const dom = this._domTrack;
19634
- if (!dom) return;
19635
- dom.mode = "disabled";
19636
- if (dom.cues) {
19637
- const all = Array.from(
19638
- dom.cues
19639
- );
19640
- for (const cue of all) dom.removeCue(cue);
19641
- }
19642
- this._domTrack = null;
19899
+ /**
19900
+ * Clears cues from the current DOM track and hides it, keeping the track for
19901
+ * later reuse via the provider.
19902
+ */
19903
+ hideCurrent() {
19904
+ var _a3;
19905
+ this.loadAbort.abort();
19906
+ const ref = this._ref;
19907
+ if (!ref) return;
19908
+ (_a3 = this.deps.textTrackRenderer) == null ? void 0 : _a3.setTextTrack(null);
19909
+ ref.clear();
19910
+ ref.track.mode = "disabled";
19911
+ this._ref = null;
19912
+ }
19913
+ /**
19914
+ * Cancels any in-flight load, hides the DOM text track, and resets state.
19915
+ * Called by the host when the underlying media is unloaded.
19916
+ */
19917
+ dispose() {
19918
+ if (this.disposed) return;
19919
+ super.dispose();
19920
+ this.loadAbort.abort();
19921
+ this.hideCurrent();
19922
+ this.disposer.dispose();
19923
+ this._tracks = [];
19924
+ this._activeTextTrack = null;
19643
19925
  }
19644
19926
  };
19645
19927
  function getVttCueConstructor() {
@@ -19649,6 +19931,23 @@ ${this.lastInitStack}` : "";
19649
19931
  }
19650
19932
 
19651
19933
  // src/text/TextTrack.ts
19934
+ var TEXT_TRACK_KINDS = [
19935
+ "subtitles",
19936
+ "captions",
19937
+ "descriptions",
19938
+ "chapters",
19939
+ "metadata"
19940
+ ];
19941
+ var textTrackSelectionValidator = object({
19942
+ id: string().orNull().optional(),
19943
+ kind: isOneOf(...TEXT_TRACK_KINDS).orNull().optional(),
19944
+ language: string().or(array(string()).readonly()).orNull().optional(),
19945
+ forced: boolean().orNull().optional()
19946
+ });
19947
+ var textTrackControllerOptionsValidator = object({
19948
+ enabled: isOneOf("on", "off", "forced").orNull().optional(),
19949
+ selection: textTrackSelectionValidator.orNull().optional()
19950
+ });
19652
19951
  var ALL_TEXT_TRACK_EVENTS = [
19653
19952
  "textTracksChange",
19654
19953
  "activeTextTrackChange",
@@ -20305,7 +20604,7 @@ ${this.lastInitStack}` : "";
20305
20604
  )
20306
20605
  );
20307
20606
  add(
20308
- deps.preferredAudioLanguage.onData((_value, previous) => {
20607
+ deps.audio.onData((_value, previous) => {
20309
20608
  if (previous !== void 0) this.reloadStreams();
20310
20609
  })
20311
20610
  );
@@ -20314,11 +20613,6 @@ ${this.lastInitStack}` : "";
20314
20613
  if (previous !== void 0) this.reloadStreams();
20315
20614
  })
20316
20615
  );
20317
- add(
20318
- deps.preferDescriptiveAudio.onData((_value, previous) => {
20319
- if (previous !== void 0) this.reloadStreams();
20320
- })
20321
- );
20322
20616
  add(
20323
20617
  deps.abr.onData((value, previous) => {
20324
20618
  if (previous === void 0) return;
@@ -20525,7 +20819,7 @@ ${this.lastInitStack}` : "";
20525
20819
  onActivated(loadOptions) {
20526
20820
  var _a3;
20527
20821
  if (this.disposed) return;
20528
- (_a3 = this.deps.textTrackController) == null ? void 0 : _a3.resume();
20822
+ (_a3 = this.deps.textTrackController) == null ? void 0 : _a3.activate();
20529
20823
  this.deps.playbackSource.disableRemotePlayback = true;
20530
20824
  this.streams.forEach((stream) => {
20531
20825
  this.deps.drmController.initializeForPlayback(
@@ -20558,7 +20852,7 @@ ${this.lastInitStack}` : "";
20558
20852
  trackUri: this.uri,
20559
20853
  abort: this.disposeAbort
20560
20854
  });
20561
- (_b = this.deps.textTrackController) == null ? void 0 : _b.suspend();
20855
+ (_b = this.deps.textTrackController) == null ? void 0 : _b.deactivate();
20562
20856
  this.deps.playbackSource.src = null;
20563
20857
  this.deps.playbackSource.load();
20564
20858
  this.deps.mediaSourceController.deactivate();
@@ -21611,9 +21905,8 @@ ${this.lastInitStack}` : "";
21611
21905
  qualitySelector: (deps2) => new QualitySelectorImpl({
21612
21906
  options: deps2.options.pick("abr")
21613
21907
  }),
21614
- preferredAudioLanguage: (deps2) => deps2.options.pick("preferredAudioLanguage"),
21908
+ audio: (deps2) => deps2.options.pick("audio"),
21615
21909
  allowedContentTypes: (deps2) => deps2.options.pick("allowedContentTypes"),
21616
- preferDescriptiveAudio: (deps2) => deps2.options.pick("preferDescriptiveAudio"),
21617
21910
  abr: (deps2) => deps2.options.pick("abr"),
21618
21911
  manifestTransformed: createDefaultDashManifestTransformer,
21619
21912
  mediaTimeline: (deps2) => deps2.manifestTransformed.map(async (manifestPromise) => {
@@ -21623,26 +21916,21 @@ ${this.lastInitStack}` : "";
21623
21916
  mediaTimelineTransformed: createDefaultMediaTimelineTransformer,
21624
21917
  textTrackController: (deps2) => {
21625
21918
  var _a3;
21626
- const controller = new SidecarTextTrackController({
21627
- media: deps2.media,
21919
+ return new SidecarTextTrackController({
21920
+ textTrackProvider: deps2.textTrackProvider,
21921
+ textTrackRenderer: deps2.textTrackRenderer,
21922
+ playbackController: deps2.playbackController,
21628
21923
  requestInit: (_a3 = loadOptions.requestInit) != null ? _a3 : void 0,
21629
- preferredTextLanguage: deps2.options.pick(
21630
- "preferredTextLanguage"
21631
- ),
21632
- cueStyle: deps2.options.pick("textCueStyle")
21633
- });
21634
- deps2.manifestTransformed.onData((manifestPromise) => {
21635
- manifestPromise.then((data2) => {
21636
- controller.setTextTracks(
21637
- discoverDashTextTracks(
21924
+ options: deps2.options.pick("text"),
21925
+ textTracks: deps2.manifestTransformed.map(
21926
+ (manifestPromise) => manifestPromise.then(
21927
+ (data2) => discoverDashTextTracks(
21638
21928
  data2.manifest,
21639
21929
  data2.baseUrl
21640
21930
  )
21641
- );
21642
- }).catch(() => {
21643
- });
21931
+ )
21932
+ )
21644
21933
  });
21645
- return controller;
21646
21934
  }
21647
21935
  }));
21648
21936
  };
@@ -23082,9 +23370,8 @@ ${this.lastInitStack}` : "";
23082
23370
  qualitySelector: (deps2) => new QualitySelectorImpl({
23083
23371
  options: deps2.options.pick("abr")
23084
23372
  }),
23085
- preferredAudioLanguage: (deps2) => deps2.options.pick("preferredAudioLanguage"),
23373
+ audio: (deps2) => deps2.options.pick("audio"),
23086
23374
  allowedContentTypes: (deps2) => deps2.options.pick("allowedContentTypes"),
23087
- preferDescriptiveAudio: (deps2) => deps2.options.pick("preferDescriptiveAudio"),
23088
23375
  abr: (deps2) => deps2.options.pick("abr"),
23089
23376
  mediaTimeline: (deps2) => deps2.manifestTransformed.map(async (manifestPromise) => {
23090
23377
  const data2 = await manifestPromise;
@@ -23093,26 +23380,21 @@ ${this.lastInitStack}` : "";
23093
23380
  mediaTimelineTransformed: createDefaultMediaTimelineTransformer,
23094
23381
  textTrackController: (deps2) => {
23095
23382
  var _a3;
23096
- const controller = new SidecarTextTrackController({
23097
- media: deps2.media,
23383
+ return new SidecarTextTrackController({
23384
+ textTrackProvider: deps2.textTrackProvider,
23385
+ textTrackRenderer: deps2.textTrackRenderer,
23386
+ playbackController: deps2.playbackController,
23098
23387
  requestInit: (_a3 = loadOptions.requestInit) != null ? _a3 : void 0,
23099
- preferredTextLanguage: deps2.options.pick(
23100
- "preferredTextLanguage"
23101
- ),
23102
- cueStyle: deps2.options.pick("textCueStyle")
23103
- });
23104
- deps2.manifestTransformed.onData((manifestPromise) => {
23105
- manifestPromise.then((data2) => {
23106
- controller.setTextTracks(
23107
- discoverHlsTextTracks(
23388
+ options: deps2.options.pick("text"),
23389
+ textTracks: deps2.manifestTransformed.map(
23390
+ (manifestPromise) => manifestPromise.then(
23391
+ (data2) => discoverHlsTextTracks(
23108
23392
  data2.mainPlaylist,
23109
23393
  data2.baseUrl
23110
23394
  )
23111
- );
23112
- }).catch(() => {
23113
- });
23395
+ )
23396
+ )
23114
23397
  });
23115
- return controller;
23116
23398
  }
23117
23399
  }));
23118
23400
  };
@@ -23682,24 +23964,24 @@ ${this.lastInitStack}` : "";
23682
23964
  var defaultVinylOptions = {
23683
23965
  abr: defaultQualitySelectorImplOptions,
23684
23966
  loudnessNormalization: defaultLoudnessNormalizationControllerImplOptions,
23685
- preferredAudioLanguage: null,
23686
- preferredTextLanguage: null,
23687
- preferDescriptiveAudio: false,
23967
+ audio: defaultAudioOptions,
23688
23968
  codecOverrides: {},
23689
23969
  allowedContentTypes: null,
23690
- textCueStyle: { size: 90 }
23970
+ text: {
23971
+ // Forced-only captions in the platform's preferred languages (no
23972
+ // explicit language → navigator.languages).
23973
+ enabled: "forced"
23974
+ }
23691
23975
  };
23692
23976
  var vinylOptionsValidator = object({
23693
23977
  abr: qualitySelectorImplOptionsValidator,
23694
23978
  loudnessNormalization: loudnessNormalizationControllerImplOptionsValidator,
23695
- preferredAudioLanguage: string().or(array(string()).readonly()).orNull(),
23696
- preferredTextLanguage: string().or(array(string()).readonly()).orNull(),
23697
- preferDescriptiveAudio: boolean(),
23979
+ audio: audioOptionsValidator,
23698
23980
  codecOverrides: record(string(), isOneOf("allow", "deny")),
23699
- allowedContentTypes: array(isOneOf(...RESTRICTABLE_CONTENT_TYPES)).cast().describe(
23981
+ allowedContentTypes: array(isOneOf(...RESTRICTABLE_CONTENT_TYPES)).readonly().describe(
23700
23982
  "Allow list of media content types (audio, video) to stream; other media streams are ignored. Text tracks are unaffected."
23701
23983
  ).orNull(),
23702
- textCueStyle: vttCueStyleValidator.orNull()
23984
+ text: textTrackControllerOptionsValidator
23703
23985
  });
23704
23986
 
23705
23987
  // src/vinyl/playerConfigFactory.ts
@@ -23731,6 +24013,14 @@ ${this.lastInitStack}` : "";
23731
24013
  return patchMediaElement(options.media, patches.media);
23732
24014
  },
23733
24015
  media: (deps) => deps.patchedMedia.patched,
24016
+ // One provider per media element (the player container creates each
24017
+ // dependency once), so text tracks are reused rather than accumulated.
24018
+ textTrackProvider: (deps) => createMediaTextTrackProvider({ media: deps.media }),
24019
+ // App-provided cue renderer (or null for native rendering).
24020
+ textTrackRenderer: () => {
24021
+ var _a4;
24022
+ return (_a4 = options.textTrackRenderer) != null ? _a4 : null;
24023
+ },
23734
24024
  playbackController: (deps) => new PlaybackControllerImpl(deps, options.playbackController),
23735
24025
  playbackSource: (deps) => new PlaybackSourceImpl(deps),
23736
24026
  loudnessNormalizationController: (deps) => new LoudnessNormalizationControllerImpl({
@@ -23853,10 +24143,6 @@ ${this.lastInitStack}` : "";
23853
24143
  const previous = activeTrack;
23854
24144
  if (previous === current) return;
23855
24145
  activeTrack = current;
23856
- const prevTextController = previous == null ? void 0 : previous.textTrackController;
23857
- if (!this.deps.adController.currentAdBreak) {
23858
- prevTextController == null ? void 0 : prevTextController.setActiveTextTrack(null);
23859
- }
23860
24146
  sub == null ? void 0 : sub();
23861
24147
  sub = null;
23862
24148
  textSub == null ? void 0 : textSub();
@@ -24273,16 +24559,6 @@ ${this.lastInitStack}` : "";
24273
24559
  var _a3, _b, _c;
24274
24560
  return (_c = (_b = (_a3 = this.activeTrack) == null ? void 0 : _a3.textTrackController) == null ? void 0 : _b.activeTextTrack) != null ? _c : null;
24275
24561
  }
24276
- /**
24277
- * Selects the text track with the given id, or clears the active text
24278
- * track when called with null. No-op when the current track does not
24279
- * surface text tracks or the id is unknown.
24280
- */
24281
- setActiveTextTrack(id) {
24282
- var _a3;
24283
- const controller = (_a3 = this.deps.trackController.activeTrack) == null ? void 0 : _a3.textTrackController;
24284
- controller == null ? void 0 : controller.setActiveTextTrack(id);
24285
- }
24286
24562
  emitTextTrackChangeEventsFor(previous, current) {
24287
24563
  var _a3, _b, _c, _d, _e, _f;
24288
24564
  const prevList = (_a3 = previous == null ? void 0 : previous.textTrackController) == null ? void 0 : _a3.textTracks;
@@ -25910,9 +26186,9 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
25910
26186
  super(...arguments);
25911
26187
  __publicField(this, "textTracks", []);
25912
26188
  __publicField(this, "activeTextTrack", null);
25913
- __publicField(this, "setActiveTextTrack", spyFactory25("setActiveTextTrack"));
25914
- __publicField(this, "suspend", spyFactory25("suspend"));
25915
- __publicField(this, "resume", spyFactory25("resume"));
26189
+ __publicField(this, "active", false);
26190
+ __publicField(this, "deactivate", spyFactory25("deactivate"));
26191
+ __publicField(this, "activate", spyFactory25("activate"));
25916
26192
  }
25917
26193
  };
25918
26194
 
@@ -25950,9 +26226,10 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
25950
26226
  mediaQualityMetadataResolver: createSpy5().and.callFake(
25951
26227
  () => createEmptyMediaQualityMetadata()
25952
26228
  ),
25953
- preferredAudioLanguage: data(null),
26229
+ audio: data({
26230
+ selection: { language: null, descriptive: false }
26231
+ }),
25954
26232
  allowedContentTypes: data(null),
25955
- preferDescriptiveAudio: data(false),
25956
26233
  abr: data(defaultQualitySelectorImplOptions)
25957
26234
  };
25958
26235
  }
@@ -25972,6 +26249,7 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
25972
26249
  var createSpy6 = jasmine.createSpy;
25973
26250
  function createMockVinylDependencies() {
25974
26251
  const playbackController = new MockPlaybackController();
26252
+ const media = createMockMedia();
25975
26253
  return {
25976
26254
  options: data(defaultVinylOptions),
25977
26255
  trackController: new MockTrackController(),
@@ -25979,7 +26257,9 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
25979
26257
  playbackController,
25980
26258
  capabilities: new MockCapabilities(),
25981
26259
  drmController: new MockDrmController(),
25982
- media: createMockMedia(),
26260
+ media,
26261
+ textTrackProvider: createMediaTextTrackProvider({ media }),
26262
+ textTrackRenderer: null,
25983
26263
  createDashFactories: (_loadOptions) => externalDependencies(createMockDashDependencies()),
25984
26264
  createHlsFactories: (_loadOptions) => externalDependencies(createMockDashDependencies()),
25985
26265
  autoResetController: new MockAutoResetController(),
@@ -26098,9 +26378,10 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
26098
26378
  mediaQualityMetadataResolver: createSpy7().and.callFake(
26099
26379
  () => createEmptyMediaQualityMetadata()
26100
26380
  ),
26101
- preferredAudioLanguage: data(null),
26381
+ audio: data({
26382
+ selection: { language: null, descriptive: false }
26383
+ }),
26102
26384
  allowedContentTypes: data(null),
26103
- preferDescriptiveAudio: data(false),
26104
26385
  abr: data(defaultQualitySelectorImplOptions)
26105
26386
  };
26106
26387
  }
@@ -37145,9 +37426,13 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
37145
37426
  drmController,
37146
37427
  mediaTimeline: data(Promise.resolve(timeline)),
37147
37428
  options: data({
37148
- preferredAudioLanguage,
37149
- codecOverrides,
37150
- preferDescriptiveAudio
37429
+ audio: {
37430
+ selection: {
37431
+ language: preferredAudioLanguage,
37432
+ descriptive: preferDescriptiveAudio
37433
+ }
37434
+ },
37435
+ codecOverrides
37151
37436
  })
37152
37437
  };
37153
37438
  }
@@ -37446,7 +37731,7 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
37446
37731
  getDuration: () => Promise.resolve(Infinity)
37447
37732
  };
37448
37733
  const options = data({
37449
- preferredAudioLanguage: "en"
37734
+ audio: { selection: { language: "en" } }
37450
37735
  });
37451
37736
  const deps = {
37452
37737
  capabilities,
@@ -37459,7 +37744,7 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
37459
37744
  });
37460
37745
  let result = await transformed.value;
37461
37746
  expect(result.periods[0].qualities[0].metadata.qualityId).toBe("en");
37462
- options.value = { preferredAudioLanguage: "ja" };
37747
+ options.value = { audio: { selection: { language: "ja" } } };
37463
37748
  result = await transformed.value;
37464
37749
  expect(result.periods[0].qualities[0].metadata.qualityId).toBe("ja");
37465
37750
  unsub();
@@ -39209,9 +39494,9 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
39209
39494
  it("suspends the controller on deactivate and resumes it on activate", () => {
39210
39495
  track2 = createTrack();
39211
39496
  track2.activate({});
39212
- expect(deps.textTrackController.resume).toHaveBeenCalled();
39497
+ expect(deps.textTrackController.activate).toHaveBeenCalled();
39213
39498
  track2.deactivate();
39214
- expect(deps.textTrackController.suspend).toHaveBeenCalled();
39499
+ expect(deps.textTrackController.deactivate).toHaveBeenCalled();
39215
39500
  });
39216
39501
  it("returns null (not throw) when read after disposal", () => {
39217
39502
  const t = track2 = createTrack();
@@ -39414,18 +39699,24 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
39414
39699
  expect(getAudioStream().reset).not.toHaveBeenCalled();
39415
39700
  });
39416
39701
  const allowed = ["audio"];
39702
+ const jaAudio = {
39703
+ selection: { language: "ja" }
39704
+ };
39705
+ const descriptiveAudio = {
39706
+ selection: { descriptive: true }
39707
+ };
39417
39708
  for (const scenario of [
39418
39709
  {
39419
- name: "preferredAudioLanguage",
39420
- apply: () => deps.preferredAudioLanguage.value = "ja"
39710
+ name: "audio language",
39711
+ apply: () => deps.audio.value = jaAudio
39421
39712
  },
39422
39713
  {
39423
39714
  name: "allowedContentTypes",
39424
39715
  apply: () => deps.allowedContentTypes.value = allowed
39425
39716
  },
39426
39717
  {
39427
- name: "preferDescriptiveAudio",
39428
- apply: () => deps.preferDescriptiveAudio.value = true
39718
+ name: "audio descriptive",
39719
+ apply: () => deps.audio.value = descriptiveAudio
39429
39720
  }
39430
39721
  ]) {
39431
39722
  describe(`when ${scenario.name} changes`, () => {
@@ -40968,6 +41259,342 @@ ${(_a3 = this.failedResult.failedExpectations[0]) == null ? void 0 : _a3.message
40968
41259
  });
40969
41260
  });
40970
41261
 
41262
+ // test/unit/text/HtmlTextTrackRenderer.test.ts
41263
+ describe("computeCueLayout", () => {
41264
+ it("flows a cue with no line/position (defaults: center, full width)", () => {
41265
+ expect(computeCueLayout({})).toEqual({
41266
+ textAlign: "center",
41267
+ maxWidth: "100%"
41268
+ });
41269
+ });
41270
+ it("maps align and size", () => {
41271
+ const layout = computeCueLayout({ align: "start", size: 80 });
41272
+ expect(layout.textAlign).toBe("start");
41273
+ expect(layout.maxWidth).toBe("80%");
41274
+ });
41275
+ it("maps vertical writing modes", () => {
41276
+ expect(computeCueLayout({ vertical: "rl" }).writingMode).toBe(
41277
+ "vertical-rl"
41278
+ );
41279
+ expect(computeCueLayout({ vertical: "lr" }).writingMode).toBe(
41280
+ "vertical-lr"
41281
+ );
41282
+ });
41283
+ it("centers horizontally and bottom-anchors when only a line is set", () => {
41284
+ const layout = computeCueLayout({ line: 40, snapToLines: false });
41285
+ expect(layout.position).toBe("absolute");
41286
+ expect(layout.left).toBe("50%");
41287
+ expect(layout.top).toBe("40%");
41288
+ expect(layout.transform).toBe("translateX(-50%)");
41289
+ });
41290
+ it("percentage line honors lineAlign end and center", () => {
41291
+ expect(
41292
+ computeCueLayout({ line: 20, snapToLines: false, lineAlign: "end" }).bottom
41293
+ ).toBe("80%");
41294
+ const center = computeCueLayout({
41295
+ line: 30,
41296
+ snapToLines: false,
41297
+ lineAlign: "center"
41298
+ });
41299
+ expect(center.top).toBe("30%");
41300
+ expect(center.transform).toContain("translateY(-50%)");
41301
+ });
41302
+ it("snap-to-lines integer line: negative counts up from the bottom", () => {
41303
+ expect(computeCueLayout({ line: -3, snapToLines: true }).bottom).toBe(
41304
+ "3em"
41305
+ );
41306
+ expect(computeCueLayout({ line: 2, snapToLines: true }).top).toBe("3em");
41307
+ });
41308
+ it("positions horizontally by positionAlign", () => {
41309
+ expect(
41310
+ computeCueLayout({ position: 20, positionAlign: "line-left" }).left
41311
+ ).toBe("20%");
41312
+ expect(
41313
+ computeCueLayout({ position: 20, positionAlign: "line-right" }).right
41314
+ ).toBe("80%");
41315
+ const center = computeCueLayout({
41316
+ position: 30,
41317
+ positionAlign: "center"
41318
+ });
41319
+ expect(center.left).toBe("30%");
41320
+ expect(center.transform).toBe("translateX(-50%)");
41321
+ });
41322
+ it("a position without a line bottom-anchors", () => {
41323
+ expect(computeCueLayout({ position: 10 }).bottom).toBe("0");
41324
+ });
41325
+ });
41326
+ describe("renderCueText", () => {
41327
+ it("escapes plain text and turns newlines into <br>", () => {
41328
+ expect(renderCueText("a & b\nc")).toBe("a &amp; b<br>c");
41329
+ });
41330
+ it("escapes a stray < that is not a tag", () => {
41331
+ expect(renderCueText("a < b")).toBe("a &lt; b");
41332
+ });
41333
+ it("converts <c.class> to a styled span", () => {
41334
+ expect(renderCueText("<c.styledotitalic>hi</c>")).toBe(
41335
+ '<span class="styledotitalic">hi</span>'
41336
+ );
41337
+ });
41338
+ it("joins multiple cue classes", () => {
41339
+ expect(renderCueText("<c.a.b>x</c>")).toBe('<span class="a b">x</span>');
41340
+ });
41341
+ it("passes through i/b/u tags", () => {
41342
+ expect(renderCueText("<i>x</i><b>y</b><u>z</u>")).toBe(
41343
+ "<i>x</i><b>y</b><u>z</u>"
41344
+ );
41345
+ });
41346
+ it("maps voice and lang tags to spans with attributes", () => {
41347
+ expect(renderCueText("<v Bob>hi</v>")).toBe(
41348
+ '<span title="Bob">hi</span>'
41349
+ );
41350
+ expect(renderCueText("<lang en>x</lang>")).toBe(
41351
+ '<span lang="en">x</span>'
41352
+ );
41353
+ });
41354
+ it("renders voice/lang without an annotation as a bare span", () => {
41355
+ expect(renderCueText("<v>hi</v>")).toBe("<span>hi</span>");
41356
+ expect(renderCueText("<lang>x</lang>")).toBe("<span>x</span>");
41357
+ });
41358
+ it("escapes annotation values", () => {
41359
+ expect(renderCueText('<v a"b>x</v>')).toBe(
41360
+ '<span title="a&quot;b">x</span>'
41361
+ );
41362
+ });
41363
+ it("drops timestamp tags and unknown tags", () => {
41364
+ expect(renderCueText("<00:00:01.000>a")).toBe("a");
41365
+ expect(renderCueText("<xyz>a</xyz>")).toBe("a");
41366
+ });
41367
+ });
41368
+ describe("HtmlTextTrackRenderer", () => {
41369
+ function fakeElement() {
41370
+ return {
41371
+ style: {},
41372
+ className: "",
41373
+ innerHTML: "",
41374
+ textContent: "",
41375
+ children: [],
41376
+ appendChild(node) {
41377
+ this.children.push(node);
41378
+ },
41379
+ replaceChildren(...nodes) {
41380
+ this.children = nodes;
41381
+ }
41382
+ };
41383
+ }
41384
+ function domChildren() {
41385
+ return Array.from(
41386
+ container.children
41387
+ );
41388
+ }
41389
+ function cueBoxes() {
41390
+ return domChildren().filter((c) => c.className === "vinyl-text-cue");
41391
+ }
41392
+ function styleElement() {
41393
+ return domChildren()[0];
41394
+ }
41395
+ class FakeTextTrack {
41396
+ constructor() {
41397
+ __publicField(this, "activeCues", []);
41398
+ __publicField(this, "listeners", /* @__PURE__ */ new Map());
41399
+ __publicField(this, "addEventListener", jasmine.createSpy("addEventListener").and.callFake(
41400
+ (type, fn) => this.listeners.set(type, fn)
41401
+ ));
41402
+ __publicField(this, "removeEventListener", jasmine.createSpy("removeEventListener").and.callFake((type) => this.listeners.delete(type)));
41403
+ }
41404
+ fireCueChange() {
41405
+ var _a3;
41406
+ (_a3 = this.listeners.get("cuechange")) == null ? void 0 : _a3();
41407
+ }
41408
+ }
41409
+ function cue(text, geometry = {}) {
41410
+ return __spreadValues({ text }, geometry);
41411
+ }
41412
+ let view;
41413
+ let container;
41414
+ let stubbedDocument = false;
41415
+ beforeEach(() => {
41416
+ if (typeof document === "undefined") {
41417
+ ;
41418
+ globalThis.document = {
41419
+ createElement: () => fakeElement()
41420
+ };
41421
+ stubbedDocument = true;
41422
+ }
41423
+ view = new HtmlTextTrackRenderer();
41424
+ container = view.element;
41425
+ });
41426
+ afterEach(() => {
41427
+ view.dispose();
41428
+ if (stubbedDocument) {
41429
+ delete globalThis.document;
41430
+ stubbedDocument = false;
41431
+ }
41432
+ });
41433
+ it("creates a positioned overlay container", () => {
41434
+ expect(container.className).toBe("vinyl-text-container");
41435
+ expect(container.style.justifyContent).toBe("flex-end");
41436
+ expect(container.style.pointerEvents).toBe("none");
41437
+ });
41438
+ it("renders the active cues, escaping text and applying layout", () => {
41439
+ const track2 = new FakeTextTrack();
41440
+ track2.activeCues = [cue("hi\nthere", { align: "start" })];
41441
+ view.setTextTrack(track2);
41442
+ expect(track2.addEventListener).toHaveBeenCalledWith(
41443
+ "cuechange",
41444
+ jasmine.any(Function)
41445
+ );
41446
+ const boxes = cueBoxes();
41447
+ expect(boxes.length).toBe(1);
41448
+ expect(boxes[0].innerHTML).toBe("hi<br>there");
41449
+ expect(boxes[0].style.textAlign).toBe("start");
41450
+ });
41451
+ it("re-renders on cuechange", () => {
41452
+ const track2 = new FakeTextTrack();
41453
+ track2.activeCues = [];
41454
+ view.setTextTrack(track2);
41455
+ expect(cueBoxes().length).toBe(0);
41456
+ track2.activeCues = [cue("now showing")];
41457
+ track2.fireCueChange();
41458
+ expect(cueBoxes().length).toBe(1);
41459
+ });
41460
+ it("renders an empty box for a cue with no text", () => {
41461
+ const track2 = new FakeTextTrack();
41462
+ track2.activeCues = [{ align: "center" }];
41463
+ view.setTextTrack(track2);
41464
+ expect(cueBoxes()[0].innerHTML).toBe("");
41465
+ });
41466
+ it("handles a track with null activeCues", () => {
41467
+ const track2 = new FakeTextTrack();
41468
+ track2.activeCues = null;
41469
+ view.setTextTrack(track2);
41470
+ expect(cueBoxes().length).toBe(0);
41471
+ });
41472
+ it("is a no-op when set to the same track", () => {
41473
+ const track2 = new FakeTextTrack();
41474
+ view.setTextTrack(track2);
41475
+ view.setTextTrack(track2);
41476
+ expect(track2.addEventListener).toHaveBeenCalledTimes(1);
41477
+ });
41478
+ it("detaches and clears when set to null", () => {
41479
+ const track2 = new FakeTextTrack();
41480
+ track2.activeCues = [cue("bye")];
41481
+ view.setTextTrack(track2);
41482
+ view.setTextTrack(null);
41483
+ expect(track2.removeEventListener).toHaveBeenCalledWith(
41484
+ "cuechange",
41485
+ jasmine.any(Function)
41486
+ );
41487
+ expect(cueBoxes().length).toBe(0);
41488
+ });
41489
+ it("injects authored STYLE rules with ::cue selectors rewritten", () => {
41490
+ view.setStyles([
41491
+ "::cue(.loud) { font-weight: bold }\n::cue { color: red }"
41492
+ ]);
41493
+ const style = styleElement();
41494
+ expect(style.textContent).toBe(
41495
+ ".vinyl-text-cue .loud { font-weight: bold }\n.vinyl-text-cue { color: red }"
41496
+ );
41497
+ });
41498
+ it("clears authored styles when the track changes", () => {
41499
+ view.setStyles(["::cue { color: red }"]);
41500
+ const style = styleElement();
41501
+ expect(style.textContent).not.toBe("");
41502
+ view.setTextTrack(new FakeTextTrack());
41503
+ expect(style.textContent).toBe("");
41504
+ });
41505
+ it("dispose detaches the current track", () => {
41506
+ const track2 = new FakeTextTrack();
41507
+ view.setTextTrack(track2);
41508
+ view.dispose();
41509
+ expect(track2.removeEventListener).toHaveBeenCalled();
41510
+ });
41511
+ });
41512
+
41513
+ // test/unit/text/mediaTextTrackProvider.test.ts
41514
+ var FakeMediaElement = class {
41515
+ constructor() {
41516
+ __publicField(this, "tracks", []);
41517
+ __publicField(this, "addTextTrack", jasmine.createSpy("addTextTrack").and.callFake((kind, label, language) => {
41518
+ const track2 = {
41519
+ kind,
41520
+ label,
41521
+ language,
41522
+ mode: "disabled",
41523
+ cues: [],
41524
+ addCue(cue) {
41525
+ this.cues.push(cue);
41526
+ },
41527
+ removeCue(cue) {
41528
+ const i = this.cues.indexOf(cue);
41529
+ if (i >= 0) this.cues.splice(i, 1);
41530
+ }
41531
+ };
41532
+ this.tracks.push(track2);
41533
+ return track2;
41534
+ }));
41535
+ }
41536
+ // ArrayLike view over the live tracks (what `isAttached` scans).
41537
+ get textTracks() {
41538
+ return this.tracks;
41539
+ }
41540
+ asMedia() {
41541
+ return this;
41542
+ }
41543
+ cue() {
41544
+ return { id: "" };
41545
+ }
41546
+ };
41547
+ describe("createMediaTextTrackProvider", () => {
41548
+ let media;
41549
+ let provider;
41550
+ beforeEach(() => {
41551
+ media = new FakeMediaElement();
41552
+ provider = createMediaTextTrackProvider({ media: media.asMedia() });
41553
+ });
41554
+ it("creates a track from the given kind/label/language", () => {
41555
+ const ref = provider.getOrCreate("subtitles", "English", "en");
41556
+ expect(media.addTextTrack).toHaveBeenCalledOnceWith(
41557
+ "subtitles",
41558
+ "English",
41559
+ "en"
41560
+ );
41561
+ expect(media.tracks).toContain(ref.track);
41562
+ });
41563
+ it("memoizes by (kind,label,language) \u2014 same key returns the same track", () => {
41564
+ const a = provider.getOrCreate("subtitles", "English", "en");
41565
+ const b = provider.getOrCreate("subtitles", "English", "en");
41566
+ expect(b).toBe(a);
41567
+ expect(media.addTextTrack).toHaveBeenCalledTimes(1);
41568
+ });
41569
+ it("creates a distinct track per key", () => {
41570
+ provider.getOrCreate("subtitles", "English", "en");
41571
+ provider.getOrCreate("subtitles", "Espanol", "es");
41572
+ expect(media.addTextTrack).toHaveBeenCalledTimes(2);
41573
+ });
41574
+ it("recreates a track that was detached from the media element", () => {
41575
+ const first2 = provider.getOrCreate("subtitles", "English", "en");
41576
+ media.tracks.length = 0;
41577
+ const second = provider.getOrCreate("subtitles", "English", "en");
41578
+ expect(second).not.toBe(first2);
41579
+ expect(media.addTextTrack).toHaveBeenCalledTimes(2);
41580
+ expect(media.tracks).toContain(second.track);
41581
+ });
41582
+ it("clear() removes all cues from the track", () => {
41583
+ var _a3, _b;
41584
+ const ref = provider.getOrCreate("subtitles", "English", "en");
41585
+ ref.track.addCue(media.cue());
41586
+ ref.track.addCue(media.cue());
41587
+ expect((_a3 = ref.track.cues) == null ? void 0 : _a3.length).toBe(2);
41588
+ ref.clear();
41589
+ expect((_b = ref.track.cues) == null ? void 0 : _b.length).toBe(0);
41590
+ });
41591
+ it("clear() is a no-op when the track has no cues (cues null)", () => {
41592
+ const ref = provider.getOrCreate("subtitles", "English", "en");
41593
+ ref.track.cues = null;
41594
+ expect(() => ref.clear()).not.toThrow();
41595
+ });
41596
+ });
41597
+
40971
41598
  // test/unit/text/parseWebVtt.test.ts
40972
41599
  describe("parseWebVtt", () => {
40973
41600
  it("parses a minimal cue", () => {
@@ -41076,15 +41703,30 @@ ok`;
41076
41703
  const doc = parseWebVtt(text);
41077
41704
  expect(doc.cues.length).toBe(1);
41078
41705
  });
41079
- it("skips STYLE blocks", () => {
41706
+ it("captures STYLE block CSS while still parsing cues", () => {
41080
41707
  const text = `WEBVTT
41081
41708
 
41082
41709
  STYLE
41083
- ::cue { color: red; }
41710
+ ::cue(.loud) { font-weight: bold }
41711
+
41712
+ STYLE
41713
+ ::cue { color: red }
41084
41714
 
41085
41715
  00:00:01.000 --> 00:00:02.000
41086
41716
  styled`;
41087
- expect(parseWebVtt(text).cues.length).toBe(1);
41717
+ const doc = parseWebVtt(text);
41718
+ expect(doc.cues.length).toBe(1);
41719
+ expect(doc.styles).toEqual([
41720
+ "::cue(.loud) { font-weight: bold }",
41721
+ "::cue { color: red }"
41722
+ ]);
41723
+ });
41724
+ it("reports no styles when there are no STYLE blocks", () => {
41725
+ const text = `WEBVTT
41726
+
41727
+ 00:00:01.000 --> 00:00:02.000
41728
+ x`;
41729
+ expect(parseWebVtt(text).styles).toEqual([]);
41088
41730
  });
41089
41731
  it("skips REGION blocks", () => {
41090
41732
  const text = `WEBVTT
@@ -41095,14 +41737,96 @@ width:40%
41095
41737
 
41096
41738
  00:00:01.000 --> 00:00:02.000
41097
41739
  regional`;
41098
- expect(parseWebVtt(text).cues.length).toBe(1);
41740
+ const doc = parseWebVtt(text);
41741
+ expect(doc.cues.length).toBe(1);
41742
+ expect(doc.styles).toEqual([]);
41099
41743
  });
41100
- it("tolerates cue settings on the timing line", () => {
41744
+ it("parses cue settings on the timing line", () => {
41101
41745
  const text = `WEBVTT
41102
41746
 
41103
41747
  00:00:01.000 --> 00:00:02.000 line:50% align:start
41104
41748
  positioned`;
41105
- expect(parseWebVtt(text).cues[0].text).toBe("positioned");
41749
+ const cue = parseWebVtt(text).cues[0];
41750
+ expect(cue.text).toBe("positioned");
41751
+ expect(cue.settings).toEqual({
41752
+ line: 50,
41753
+ snapToLines: false,
41754
+ align: "start"
41755
+ });
41756
+ });
41757
+ it("parses integer/negative lines as snap-to-lines, with lineAlign", () => {
41758
+ const text = `WEBVTT
41759
+
41760
+ 00:00:01.000 --> 00:00:02.000 line:-3,end
41761
+ a
41762
+
41763
+ 00:00:03.000 --> 00:00:04.000 line:5
41764
+ b`;
41765
+ const [a, b] = parseWebVtt(text).cues;
41766
+ expect(a.settings).toEqual({
41767
+ line: -3,
41768
+ snapToLines: true,
41769
+ lineAlign: "end"
41770
+ });
41771
+ expect(b.settings).toEqual({ line: 5, snapToLines: true });
41772
+ });
41773
+ it("maps the legacy 'middle' keyword to 'center'", () => {
41774
+ const text = `WEBVTT
41775
+
41776
+ 00:00:01.000 --> 00:00:02.000 align:middle line:85%,middle position:50%,middle
41777
+ a`;
41778
+ expect(parseWebVtt(text).cues[0].settings).toEqual({
41779
+ align: "center",
41780
+ line: 85,
41781
+ snapToLines: false,
41782
+ lineAlign: "center",
41783
+ position: 50,
41784
+ positionAlign: "center"
41785
+ });
41786
+ });
41787
+ it("parses position (with align), size and vertical", () => {
41788
+ const text = `WEBVTT
41789
+
41790
+ 00:00:01.000 --> 00:00:02.000 position:20%,line-left size:80% vertical:rl
41791
+ a`;
41792
+ expect(parseWebVtt(text).cues[0].settings).toEqual({
41793
+ position: 20,
41794
+ positionAlign: "line-left",
41795
+ size: 80,
41796
+ vertical: "rl"
41797
+ });
41798
+ });
41799
+ it("omits settings and skips malformed/unknown tokens", () => {
41800
+ const text = `WEBVTT
41801
+
41802
+ 00:00:01.000 --> 00:00:02.000
41803
+ plain
41804
+
41805
+ 00:00:03.000 --> 00:00:04.000 align:bogus size:oops region:foo bareword
41806
+ b`;
41807
+ const [plain, b] = parseWebVtt(text).cues;
41808
+ expect(plain.settings).toBeUndefined();
41809
+ expect(b.settings).toBeUndefined();
41810
+ });
41811
+ it("skips malformed line/position/percent values and invalid aligns", () => {
41812
+ const text = `WEBVTT
41813
+
41814
+ 00:00:01.000 --> 00:00:02.000 line:x% size:y%
41815
+ a
41816
+
41817
+ 00:00:03.000 --> 00:00:04.000 line:1.5 position:bad
41818
+ b
41819
+
41820
+ 00:00:05.000 --> 00:00:06.000 line:5,bogus position:20%,nope
41821
+ c`;
41822
+ const [a, b, c] = parseWebVtt(text).cues;
41823
+ expect(a.settings).toBeUndefined();
41824
+ expect(b.settings).toBeUndefined();
41825
+ expect(c.settings).toEqual({
41826
+ line: 5,
41827
+ snapToLines: true,
41828
+ position: 20
41829
+ });
41106
41830
  });
41107
41831
  it("skips a malformed cue and continues", () => {
41108
41832
  const text = `WEBVTT
@@ -41117,6 +41841,114 @@ good`;
41117
41841
  expect(cues.length).toBe(1);
41118
41842
  expect(cues[0].text).toBe("good");
41119
41843
  });
41844
+ it("skips a line with --> but an invalid start timestamp", () => {
41845
+ const text = `WEBVTT
41846
+
41847
+ BForced --> 00:00:02.000
41848
+ skipped
41849
+
41850
+ 00:00:02.000 --> 00:00:03.000
41851
+ good`;
41852
+ const cues = parseWebVtt(text).cues;
41853
+ expect(cues.length).toBe(1);
41854
+ expect(cues[0].text).toBe("good");
41855
+ });
41856
+ it("ignores unparseable text glued after the end timestamp", () => {
41857
+ const cue = parseWebVtt(
41858
+ "WEBVTT\n\n00:00:01.000 --> 00:00:02.000x\ntext"
41859
+ ).cues[0];
41860
+ expect(cue.endTime).toBe(2);
41861
+ expect(cue.settings).toBeUndefined();
41862
+ expect(cue.text).toBe("text");
41863
+ });
41864
+ it("parses multi-digit hours (spec allows two or more)", () => {
41865
+ const cues = parseWebVtt(
41866
+ "WEBVTT\n\n100:00:01.000 --> 100:00:02.500\nhi"
41867
+ ).cues;
41868
+ expect(cues.length).toBe(1);
41869
+ expect(cues[0].startTime).toBe(360001);
41870
+ expect(cues[0].endTime).toBe(360002.5);
41871
+ });
41872
+ it("treats consecutive blank lines between cues as separators", () => {
41873
+ const text = "WEBVTT\n\n00:00:01.000 --> 00:00:02.000\none\n\n\n00:00:03.000 --> 00:00:04.000\ntwo";
41874
+ expect(parseWebVtt(text).cues.map((c) => c.text)).toEqual([
41875
+ "one",
41876
+ "two"
41877
+ ]);
41878
+ });
41879
+ for (const timing of [
41880
+ "12 --> 00:00:02.000",
41881
+ // two digits then no ':'
41882
+ "7 --> 00:00:02.000",
41883
+ // one digit then no second digit
41884
+ "00:00:01.000 00:00:02.000",
41885
+ // no --> ⇒ treated as an identifier
41886
+ "5:30.000 --> 00:00:02.000",
41887
+ // MM:SS minutes not two digits
41888
+ "75:30.000 --> 00:00:02.000",
41889
+ // MM:SS minutes > 59
41890
+ "12:34 --> 00:00:02.000",
41891
+ // seconds not terminated by '.'
41892
+ "00:99:00.000 --> 00:00:02.000",
41893
+ // HH:MM:SS minutes > 59
41894
+ "00:00:99.000 --> 00:00:02.000",
41895
+ // HH:MM:SS seconds > 59
41896
+ "00:00:0.400 --> 00:00:02.000",
41897
+ // HH:MM:SS seconds not two digits
41898
+ "12:3:45.000 --> 00:00:02.000",
41899
+ // minutes not two digits before ':'
41900
+ "00:00:00 --> 00:00:02.000",
41901
+ // HH:MM:SS missing .mmm
41902
+ "00:00:00.00 --> 00:00:02.000",
41903
+ // milliseconds only two digits
41904
+ "00:00.0 --> 00:00:02.000",
41905
+ // milliseconds only one digit
41906
+ "00:00. --> 00:00:02.000",
41907
+ // milliseconds missing
41908
+ "12:ab --> 00:00:02.000",
41909
+ // non-digit minute/second
41910
+ "00:00:01.0"
41911
+ // truncated timestamp at end of input
41912
+ ]) {
41913
+ it(`skips a line with an invalid timestamp: "${timing}"`, () => {
41914
+ const text = `WEBVTT
41915
+
41916
+ ${timing}
41917
+ filler
41918
+
41919
+ 00:00:05.000 --> 00:00:06.000
41920
+ ok`;
41921
+ expect(parseWebVtt(text).cues.map((c) => c.text)).toEqual(["ok"]);
41922
+ });
41923
+ }
41924
+ for (const identifier of ["NOTES", "REGIONAL", "STYLES"]) {
41925
+ it(`treats keyword prefix "${identifier}" as an identifier`, () => {
41926
+ const cues = parseWebVtt(
41927
+ `WEBVTT
41928
+
41929
+ ${identifier}
41930
+ 00:00:01.000 --> 00:00:02.000
41931
+ hi`
41932
+ ).cues;
41933
+ expect(cues.length).toBe(1);
41934
+ expect(cues[0].id).toBe(identifier);
41935
+ });
41936
+ }
41937
+ it("normalizes CRLF line endings and splits CRLF-separated cues", () => {
41938
+ const text = "WEBVTT\r\n\r\n00:00:01.000 --> 00:00:02.000\r\nline one\r\nline two\r\n\r\n00:00:03.000 --> 00:00:04.000\r\nsecond";
41939
+ expect(parseWebVtt(text).cues.map((c) => c.text)).toEqual([
41940
+ "line one\nline two",
41941
+ "second"
41942
+ ]);
41943
+ });
41944
+ it("accepts a NOTE block at end of input", () => {
41945
+ expect(parseWebVtt("WEBVTT\n\nNOTE").cues).toEqual([]);
41946
+ });
41947
+ it("handles input that ends mid-timestamp", () => {
41948
+ expect(parseWebVtt("WEBVTT\n\n00:00:00.000 --> 00:00:0").cues).toEqual(
41949
+ []
41950
+ );
41951
+ });
41120
41952
  it("returns empty cues on header-only input", () => {
41121
41953
  expect(parseWebVtt("WEBVTT\n").cues).toEqual([]);
41122
41954
  });
@@ -41193,7 +42025,21 @@ trailing-id`;
41193
42025
  }, overrides);
41194
42026
  }
41195
42027
  describe("pickPreferredTextTrack", () => {
41196
- it("returns null when the preference is null (captions off)", () => {
42028
+ it("falls back to navigator.languages when the preference is null", () => {
42029
+ setMockNavigator().languages = ["en"];
42030
+ const en = track({ id: "en", language: "en" });
42031
+ expect(pickPreferredTextTrack([en], null)).toBe(en);
42032
+ });
42033
+ it("returns null when no track relates to navigator.languages", () => {
42034
+ setMockNavigator().languages = ["en"];
42035
+ expect(
42036
+ pickPreferredTextTrack([track({ language: "ja" })], null)
42037
+ ).toBeNull();
42038
+ });
42039
+ it("returns null for a null preference when navigator is unavailable", () => {
42040
+ spyOnPropertySafe(globalThis, "navigator").and.returnValue(
42041
+ void 0
42042
+ );
41197
42043
  expect(pickPreferredTextTrack([track()], null)).toBeNull();
41198
42044
  });
41199
42045
  it("returns null when there are no tracks", () => {
@@ -41246,28 +42092,60 @@ trailing-id`;
41246
42092
  async function flushUntil(predicate) {
41247
42093
  for (let i = 0; i < 100 && !predicate(); i++) await flushPromises();
41248
42094
  }
41249
- var FakeMediaElement = class {
42095
+ var FakeTextTrackProvider = class {
41250
42096
  constructor() {
41251
- __publicField(this, "addTextTrack", jasmine.createSpy("addTextTrack").and.callFake((kind, label, language) => {
42097
+ // Every distinct track created (one per rendition key).
42098
+ __publicField(this, "created", []);
42099
+ // The track handed out by the most recent getOrCreate (the active one).
42100
+ __publicField(this, "lastTrack", null);
42101
+ __publicField(this, "cache", /* @__PURE__ */ new Map());
42102
+ __publicField(this, "getOrCreate", jasmine.createSpy("getOrCreate").and.callFake((kind, label, language) => {
42103
+ const key = `${kind}-${label}-${language}`;
42104
+ const existing = this.cache.get(key);
42105
+ if (existing) {
42106
+ this.lastTrack = existing.track;
42107
+ return existing;
42108
+ }
42109
+ const storedCues = [];
41252
42110
  const track2 = {
41253
42111
  kind,
41254
42112
  label: label != null ? label : "",
41255
42113
  language: language != null ? language : "",
41256
42114
  mode: "disabled",
41257
- cues: [],
42115
+ storedCues,
42116
+ // Real TextTrack.cues returns null when mode is 'disabled'.
42117
+ get cues() {
42118
+ return this.mode === "disabled" ? null : storedCues;
42119
+ },
41258
42120
  addCue(cue) {
41259
- this.cues.push(cue);
42121
+ storedCues.push(cue);
41260
42122
  },
41261
42123
  removeCue(cue) {
41262
- const i = this.cues.indexOf(cue);
41263
- if (i >= 0) this.cues.splice(i, 1);
42124
+ const i = storedCues.indexOf(cue);
42125
+ if (i >= 0) storedCues.splice(i, 1);
42126
+ }
42127
+ };
42128
+ const ref = {
42129
+ track: track2,
42130
+ clear() {
42131
+ if (track2.mode === "disabled") return;
42132
+ storedCues.length = 0;
41264
42133
  }
41265
42134
  };
42135
+ this.created.push(track2);
42136
+ this.cache.set(key, ref);
41266
42137
  this.lastTrack = track2;
41267
- return track2;
42138
+ return ref;
41268
42139
  }));
41269
- __publicField(this, "lastTrack", null);
41270
- __publicField(this, "currentTime", 0);
42140
+ }
42141
+ /** Tracks currently rendering captions (showing with at least one cue). */
42142
+ showingWithCues() {
42143
+ return this.created.filter(
42144
+ (t) => {
42145
+ var _a3, _b;
42146
+ return t.mode === "showing" && ((_b = (_a3 = t.cues) == null ? void 0 : _a3.length) != null ? _b : 0) > 0;
42147
+ }
42148
+ );
41271
42149
  }
41272
42150
  };
41273
42151
  function makeTrack(overrides = {}) {
@@ -41284,18 +42162,47 @@ trailing-id`;
41284
42162
  }, overrides);
41285
42163
  }
41286
42164
  describe("SidecarTextTrackController", () => {
41287
- let media;
41288
- let controller;
41289
42165
  let requester;
41290
42166
  let originalCue;
42167
+ const disposers = [];
42168
+ function build(opts) {
42169
+ var _a3;
42170
+ const provider = new FakeTextTrackProvider();
42171
+ const textTracks$ = data(
42172
+ Promise.resolve([])
42173
+ );
42174
+ const options$ = data(
42175
+ (_a3 = opts == null ? void 0 : opts.options) != null ? _a3 : { selection: {} }
42176
+ );
42177
+ const playback = { currentTime: 0 };
42178
+ const controller = new SidecarTextTrackController({
42179
+ textTrackProvider: provider,
42180
+ textTracks: textTracks$,
42181
+ options: options$,
42182
+ playbackController: playback,
42183
+ requestInit: opts == null ? void 0 : opts.requestInit,
42184
+ textTrackRenderer: opts == null ? void 0 : opts.textTrackRenderer
42185
+ });
42186
+ if ((opts == null ? void 0 : opts.activate) !== false) controller.activate();
42187
+ disposers.push(() => controller.dispose());
42188
+ return {
42189
+ controller,
42190
+ provider,
42191
+ options$,
42192
+ textTracks$,
42193
+ playback,
42194
+ async setTracks(tracks) {
42195
+ textTracks$.value = Promise.resolve(tracks);
42196
+ await flushPromises();
42197
+ },
42198
+ select(selection) {
42199
+ options$.value = __spreadProps(__spreadValues({}, options$.value), { selection });
42200
+ }
42201
+ };
42202
+ }
41291
42203
  beforeEach(() => {
41292
42204
  requester = new MockRequester();
41293
42205
  requesterWithRetryRef.set(() => requester);
41294
- media = new FakeMediaElement();
41295
- controller = new SidecarTextTrackController({
41296
- media,
41297
- cueStyle: data({ size: 90 })
41298
- });
41299
42206
  originalCue = globalThis.VTTCue;
41300
42207
  globalThis.VTTCue = function VTTCue2(startTime, endTime, text) {
41301
42208
  this.startTime = startTime;
@@ -41306,137 +42213,182 @@ trailing-id`;
41306
42213
  };
41307
42214
  });
41308
42215
  afterEach(() => {
41309
- controller.dispose();
42216
+ for (const dispose of disposers.splice(0)) dispose();
41310
42217
  globalThis.VTTCue = originalCue;
41311
42218
  });
41312
42219
  function respondVtt(body) {
41313
42220
  requester.request.and.resolveTo(new Response(body));
41314
42221
  }
41315
42222
  it("starts with no tracks and no active selection", () => {
42223
+ const { controller } = build();
41316
42224
  expect(controller.textTracks).toEqual([]);
41317
42225
  expect(controller.activeTextTrack).toBeNull();
41318
42226
  });
41319
- describe("forced auto-selection", () => {
41320
- it("auto-selects a forced track on discovery", () => {
42227
+ describe("resolveSelectedTrack", () => {
42228
+ const full = makeTrack({ id: "full", language: "en", forced: false });
42229
+ const forced = makeTrack({ id: "forced", language: "en", forced: true });
42230
+ const captions = makeTrack({
42231
+ id: "cc",
42232
+ language: "en",
42233
+ forced: false,
42234
+ kind: "captions"
42235
+ });
42236
+ it("renders nothing when enabled is 'off' (even with an id)", () => {
42237
+ expect(
42238
+ resolveSelectedTrack([full, forced], {
42239
+ enabled: "off",
42240
+ selection: { id: "full" }
42241
+ })
42242
+ ).toBeNull();
42243
+ });
42244
+ it("enabled 'on' selects the full track", () => {
41321
42245
  var _a3;
41322
- controller.setTextTracks([
41323
- makeTrack({ id: "full", label: "English" }),
41324
- makeTrack({ id: "forced", label: "Forced", forced: true })
41325
- ]);
41326
- expect((_a3 = controller.activeTextTrack) == null ? void 0 : _a3.id).toBe("forced");
42246
+ expect(
42247
+ (_a3 = resolveSelectedTrack([full, forced], {
42248
+ enabled: "on",
42249
+ selection: { language: "en" }
42250
+ })) == null ? void 0 : _a3.id
42251
+ ).toBe("full");
41327
42252
  });
41328
- it("prefers the forced track matching the default language", () => {
42253
+ it("enabled 'forced' selects the forced track", () => {
41329
42254
  var _a3;
41330
- controller.setTextTracks([
41331
- makeTrack({ id: "en-f", language: "en", forced: true }),
41332
- makeTrack({
41333
- id: "es-f",
41334
- language: "es",
41335
- forced: true,
41336
- default: true
41337
- })
41338
- ]);
41339
- expect((_a3 = controller.activeTextTrack) == null ? void 0 : _a3.id).toBe("es-f");
42255
+ expect(
42256
+ (_a3 = resolveSelectedTrack([full, forced], {
42257
+ enabled: "forced",
42258
+ selection: { language: "en" }
42259
+ })) == null ? void 0 : _a3.id
42260
+ ).toBe("forced");
41340
42261
  });
41341
- it("does not auto-select when there is no forced track", () => {
41342
- controller.setTextTracks([makeTrack({ id: "full" })]);
41343
- expect(controller.activeTextTrack).toBeNull();
42262
+ it("selection.forced overrides the enabled mode", () => {
42263
+ var _a3;
42264
+ expect(
42265
+ (_a3 = resolveSelectedTrack([full, forced], {
42266
+ enabled: "on",
42267
+ selection: { language: "en", forced: true }
42268
+ })) == null ? void 0 : _a3.id
42269
+ ).toBe("forced");
41344
42270
  });
41345
- it("does not auto-select after the app made an explicit choice", () => {
41346
- controller.setActiveTextTrack(null);
41347
- controller.setTextTracks([
41348
- makeTrack({ id: "forced", forced: true })
41349
- ]);
41350
- expect(controller.activeTextTrack).toBeNull();
42271
+ it("an explicit id wins over other criteria", () => {
42272
+ var _a3;
42273
+ expect(
42274
+ (_a3 = resolveSelectedTrack([full, forced], {
42275
+ selection: { id: "forced", language: "en", forced: false }
42276
+ })) == null ? void 0 : _a3.id
42277
+ ).toBe("forced");
41351
42278
  });
41352
- it("does not override an explicit selection with a forced track", () => {
42279
+ it("unknown id selects nothing", () => {
42280
+ expect(
42281
+ resolveSelectedTrack([full], { selection: { id: "nope" } })
42282
+ ).toBeNull();
42283
+ });
42284
+ it("forced defaults to true (only forced tracks)", () => {
41353
42285
  var _a3;
41354
- controller.setTextTracks([makeTrack({ id: "full" })]);
41355
- controller.setActiveTextTrack("full");
41356
- controller.setTextTracks([
41357
- makeTrack({ id: "full" }),
41358
- makeTrack({ id: "forced", forced: true })
41359
- ]);
41360
- expect((_a3 = controller.activeTextTrack) == null ? void 0 : _a3.id).toBe("full");
42286
+ expect(
42287
+ (_a3 = resolveSelectedTrack([full, forced], {
42288
+ selection: { language: "en" }
42289
+ })) == null ? void 0 : _a3.id
42290
+ ).toBe("forced");
42291
+ });
42292
+ it("forced:false selects the full track", () => {
42293
+ var _a3;
42294
+ expect(
42295
+ (_a3 = resolveSelectedTrack([full, forced], {
42296
+ selection: { language: "en", forced: false }
42297
+ })) == null ? void 0 : _a3.id
42298
+ ).toBe("full");
42299
+ });
42300
+ it("kind restricts the candidate pool", () => {
42301
+ var _a3;
42302
+ expect(
42303
+ (_a3 = resolveSelectedTrack([full, captions], {
42304
+ selection: {
42305
+ language: "en",
42306
+ forced: false,
42307
+ kind: "captions"
42308
+ }
42309
+ })) == null ? void 0 : _a3.id
42310
+ ).toBe("cc");
42311
+ });
42312
+ it("falls back to navigator.languages when no language is given", () => {
42313
+ var _a3;
42314
+ expect(
42315
+ (_a3 = resolveSelectedTrack([full], { selection: { forced: false } })) == null ? void 0 : _a3.id
42316
+ ).toBe("full");
42317
+ });
42318
+ it("an ordered language list prefers earlier entries", () => {
42319
+ var _a3;
42320
+ const es = makeTrack({ id: "es", language: "es", forced: false });
42321
+ expect(
42322
+ (_a3 = resolveSelectedTrack([full, es], {
42323
+ selection: { language: ["es", "en"], forced: false }
42324
+ })) == null ? void 0 : _a3.id
42325
+ ).toBe("es");
41361
42326
  });
41362
- });
41363
- it("emits textTracksChange when discovered list changes", () => {
41364
- const handler = jasmine.createSpy("textTracksChange");
41365
- controller.on("textTracksChange", handler);
41366
- const t = makeTrack();
41367
- controller.setTextTracks([t]);
41368
- expect(handler).toHaveBeenCalledTimes(1);
41369
- controller.setTextTracks([t]);
41370
- expect(handler).toHaveBeenCalledTimes(1);
41371
- });
41372
- it("detects changed track properties", () => {
41373
- const handler = jasmine.createSpy();
41374
- controller.on("textTracksChange", handler);
41375
- controller.setTextTracks([makeTrack({ label: "A" })]);
41376
- controller.setTextTracks([makeTrack({ label: "B" })]);
41377
- expect(handler).toHaveBeenCalledTimes(2);
41378
42327
  });
41379
42328
  it("selecting a known id loads cues and creates a TextTrack", async () => {
41380
- var _a3, _b, _c;
42329
+ var _a3, _b, _c, _d;
41381
42330
  respondVtt(`WEBVTT
41382
42331
 
41383
42332
  00:00:01.000 --> 00:00:02.000
41384
42333
  first`);
41385
- const t = makeTrack();
41386
- controller.setTextTracks([t]);
42334
+ const h = build({ options: { selection: { id: "a" } } });
41387
42335
  const activeChange = jasmine.createSpy();
41388
- controller.on("activeTextTrackChange", activeChange);
41389
- controller.setActiveTextTrack(t.id);
41390
- expect(controller.activeTextTrack).toBe(t);
42336
+ h.controller.on("activeTextTrackChange", activeChange);
42337
+ await h.setTracks([makeTrack()]);
42338
+ expect((_a3 = h.controller.activeTextTrack) == null ? void 0 : _a3.id).toBe("a");
41391
42339
  expect(activeChange).toHaveBeenCalledTimes(1);
41392
42340
  await flushUntil(() => {
41393
- var _a4, _b2;
41394
- return ((_b2 = (_a4 = media.lastTrack) == null ? void 0 : _a4.cues.length) != null ? _b2 : 0) > 0;
42341
+ var _a4, _b2, _c2;
42342
+ return ((_c2 = (_b2 = (_a4 = h.provider.lastTrack) == null ? void 0 : _a4.cues) == null ? void 0 : _b2.length) != null ? _c2 : 0) > 0;
41395
42343
  });
41396
- expect(media.addTextTrack).toHaveBeenCalledWith(
42344
+ expect(h.provider.getOrCreate).toHaveBeenCalledWith(
41397
42345
  "subtitles",
41398
42346
  "English",
41399
42347
  "en"
41400
42348
  );
41401
- expect((_a3 = media.lastTrack) == null ? void 0 : _a3.mode).toBe("showing");
41402
- expect((_b = media.lastTrack) == null ? void 0 : _b.cues.length).toBe(1);
41403
- const cue = (_c = media.lastTrack) == null ? void 0 : _c.cues[0];
41404
- expect(cue.size).toBe(90);
42349
+ expect((_b = h.provider.lastTrack) == null ? void 0 : _b.mode).toBe("showing");
42350
+ expect((_d = (_c = h.provider.lastTrack) == null ? void 0 : _c.cues) == null ? void 0 : _d.length).toBe(1);
41405
42351
  });
41406
- describe("cueStyle", () => {
41407
- async function loadFirstCue(ctrl) {
41408
- var _a3;
42352
+ describe("authored cue settings", () => {
42353
+ it("applies the cue's own parsed settings to the VTTCue", async () => {
42354
+ var _a3, _b;
42355
+ respondVtt(
42356
+ `WEBVTT
42357
+
42358
+ 00:00:01.000 --> 00:00:02.000 align:left size:80%
42359
+ x`
42360
+ );
42361
+ const h = build({ options: { selection: { id: "a" } } });
42362
+ await h.setTracks([makeTrack()]);
42363
+ await flushUntil(
42364
+ () => {
42365
+ var _a4, _b2, _c;
42366
+ return ((_c = (_b2 = (_a4 = h.provider.lastTrack) == null ? void 0 : _a4.cues) == null ? void 0 : _b2.length) != null ? _c : 0) > 0;
42367
+ }
42368
+ );
42369
+ const cue = (_b = (_a3 = h.provider.lastTrack) == null ? void 0 : _a3.cues) == null ? void 0 : _b[0];
42370
+ expect(cue.align).toBe("left");
42371
+ expect(cue.size).toBe(80);
42372
+ });
42373
+ it("leaves the cue at defaults when it declares no settings", async () => {
42374
+ var _a3, _b;
41409
42375
  respondVtt(`WEBVTT
41410
42376
 
41411
42377
  00:00:01.000 --> 00:00:02.000
41412
- first`);
41413
- const t = makeTrack();
41414
- ctrl.setTextTracks([t]);
41415
- ctrl.setActiveTextTrack(t.id);
41416
- await flushUntil(() => {
41417
- var _a4, _b;
41418
- return ((_b = (_a4 = media.lastTrack) == null ? void 0 : _a4.cues.length) != null ? _b : 0) > 0;
41419
- });
41420
- return (_a3 = media.lastTrack) == null ? void 0 : _a3.cues[0];
41421
- }
41422
- it("does not style cues when cueStyle is null", async () => {
41423
- const ctrl = new SidecarTextTrackController({
41424
- media,
41425
- cueStyle: data(null)
41426
- });
41427
- const cue = await loadFirstCue(ctrl);
41428
- expect(cue.size).toBe(100);
41429
- ctrl.dispose();
41430
- });
41431
- it("does not style cues when no cueStyle dep is provided", async () => {
41432
- const ctrl = new SidecarTextTrackController({
41433
- media
41434
- });
41435
- const cue = await loadFirstCue(ctrl);
42378
+ x`);
42379
+ const h = build({ options: { selection: { id: "a" } } });
42380
+ await h.setTracks([makeTrack()]);
42381
+ await flushUntil(
42382
+ () => {
42383
+ var _a4, _b2, _c;
42384
+ return ((_c = (_b2 = (_a4 = h.provider.lastTrack) == null ? void 0 : _a4.cues) == null ? void 0 : _b2.length) != null ? _c : 0) > 0;
42385
+ }
42386
+ );
42387
+ const cue = (_b = (_a3 = h.provider.lastTrack) == null ? void 0 : _a3.cues) == null ? void 0 : _b[0];
41436
42388
  expect(cue.size).toBe(100);
41437
- ctrl.dispose();
41438
42389
  });
41439
- it("does not style non-VTTCue cues", async () => {
42390
+ it("does not apply settings to non-VTTCue cues", async () => {
42391
+ var _a3, _b;
41440
42392
  ;
41441
42393
  globalThis.VTTCue = void 0;
41442
42394
  globalThis.TextTrackCue = function TextTrackCue(start, end, text) {
@@ -41446,31 +42398,190 @@ first`);
41446
42398
  this.id = "";
41447
42399
  this.size = 100;
41448
42400
  };
41449
- const ctrl = new SidecarTextTrackController({
41450
- media,
41451
- cueStyle: data({ size: 80 })
41452
- });
41453
- const cue = await loadFirstCue(ctrl);
42401
+ respondVtt(
42402
+ `WEBVTT
42403
+
42404
+ 00:00:01.000 --> 00:00:02.000 size:80%
42405
+ ignored`
42406
+ );
42407
+ const h = build({ options: { selection: { id: "a" } } });
42408
+ await h.setTracks([makeTrack()]);
42409
+ await flushUntil(
42410
+ () => {
42411
+ var _a4, _b2, _c;
42412
+ return ((_c = (_b2 = (_a4 = h.provider.lastTrack) == null ? void 0 : _a4.cues) == null ? void 0 : _b2.length) != null ? _c : 0) > 0;
42413
+ }
42414
+ );
42415
+ const cue = (_b = (_a3 = h.provider.lastTrack) == null ? void 0 : _a3.cues) == null ? void 0 : _b[0];
41454
42416
  expect(cue.size).toBe(100);
41455
- ctrl.dispose();
41456
42417
  globalThis.TextTrackCue = void 0;
41457
42418
  });
41458
42419
  });
42420
+ describe("text track renderer", () => {
42421
+ function fakeRenderer() {
42422
+ return {
42423
+ setTextTrack: jasmine.createSpy(
42424
+ "setTextTrack"
42425
+ ),
42426
+ setStyles: jasmine.createSpy(
42427
+ "setStyles"
42428
+ )
42429
+ };
42430
+ }
42431
+ it("keeps the DOM track 'hidden' and hands it to the renderer", async () => {
42432
+ var _a3;
42433
+ respondVtt(`WEBVTT
42434
+
42435
+ 00:00:01.000 --> 00:00:02.000
42436
+ x`);
42437
+ const renderer = fakeRenderer();
42438
+ const h = build({
42439
+ options: { selection: { id: "a" } },
42440
+ textTrackRenderer: renderer
42441
+ });
42442
+ await h.setTracks([makeTrack()]);
42443
+ await flushUntil(
42444
+ () => {
42445
+ var _a4, _b, _c;
42446
+ return ((_c = (_b = (_a4 = h.provider.lastTrack) == null ? void 0 : _a4.cues) == null ? void 0 : _b.length) != null ? _c : 0) > 0;
42447
+ }
42448
+ );
42449
+ expect((_a3 = h.provider.lastTrack) == null ? void 0 : _a3.mode).toBe("hidden");
42450
+ expect(renderer.setTextTrack).toHaveBeenCalledWith(
42451
+ h.provider.lastTrack
42452
+ );
42453
+ });
42454
+ it("detaches the renderer when captions are turned off", async () => {
42455
+ respondVtt(`WEBVTT
42456
+
42457
+ 00:00:01.000 --> 00:00:02.000
42458
+ x`);
42459
+ const renderer = fakeRenderer();
42460
+ const h = build({
42461
+ options: { selection: { id: "a" } },
42462
+ textTrackRenderer: renderer
42463
+ });
42464
+ await h.setTracks([makeTrack()]);
42465
+ await flushUntil(
42466
+ () => {
42467
+ var _a3, _b, _c;
42468
+ return ((_c = (_b = (_a3 = h.provider.lastTrack) == null ? void 0 : _a3.cues) == null ? void 0 : _b.length) != null ? _c : 0) > 0;
42469
+ }
42470
+ );
42471
+ renderer.setTextTrack.calls.reset();
42472
+ h.options$.value = { enabled: "off" };
42473
+ expect(renderer.setTextTrack).toHaveBeenCalledWith(null);
42474
+ });
42475
+ it("forwards authored STYLE blocks to the renderer", async () => {
42476
+ respondVtt(
42477
+ `WEBVTT
42478
+
42479
+ STYLE
42480
+ ::cue { color: red }
42481
+
42482
+ 00:00:01.000 --> 00:00:02.000
42483
+ x`
42484
+ );
42485
+ const renderer = fakeRenderer();
42486
+ const h = build({
42487
+ options: { selection: { id: "a" } },
42488
+ textTrackRenderer: renderer
42489
+ });
42490
+ await h.setTracks([makeTrack()]);
42491
+ await flushUntil(() => renderer.setStyles.calls.count() > 0);
42492
+ expect(renderer.setStyles).toHaveBeenCalledWith([
42493
+ "::cue { color: red }"
42494
+ ]);
42495
+ });
42496
+ it("ignores styles that arrive after the load is cancelled", async () => {
42497
+ let resolveFetch = () => {
42498
+ };
42499
+ requester.request.and.returnValue(
42500
+ new Promise((res) => {
42501
+ resolveFetch = res;
42502
+ })
42503
+ );
42504
+ const renderer = fakeRenderer();
42505
+ const h = build({
42506
+ options: { selection: { id: "a" } },
42507
+ textTrackRenderer: renderer
42508
+ });
42509
+ await h.setTracks([makeTrack()]);
42510
+ h.options$.value = { enabled: "off" };
42511
+ renderer.setStyles.calls.reset();
42512
+ resolveFetch(
42513
+ new Response(
42514
+ `WEBVTT
42515
+
42516
+ STYLE
42517
+ ::cue { color: red }
42518
+
42519
+ 00:00:01.000 --> 00:00:02.000
42520
+ x`
42521
+ )
42522
+ );
42523
+ await flushPromises();
42524
+ expect(renderer.setStyles).not.toHaveBeenCalled();
42525
+ });
42526
+ it("works with a renderer that cannot consume styles", async () => {
42527
+ respondVtt(
42528
+ `WEBVTT
42529
+
42530
+ STYLE
42531
+ ::cue { color: red }
42532
+
42533
+ 00:00:01.000 --> 00:00:02.000
42534
+ x`
42535
+ );
42536
+ const renderer = {
42537
+ setTextTrack: jasmine.createSpy(
42538
+ "setTextTrack"
42539
+ )
42540
+ };
42541
+ const h = build({
42542
+ options: { selection: { id: "a" } },
42543
+ textTrackRenderer: renderer
42544
+ });
42545
+ await h.setTracks([makeTrack()]);
42546
+ await flushUntil(
42547
+ () => {
42548
+ var _a3, _b, _c;
42549
+ return ((_c = (_b = (_a3 = h.provider.lastTrack) == null ? void 0 : _a3.cues) == null ? void 0 : _b.length) != null ? _c : 0) > 0;
42550
+ }
42551
+ );
42552
+ expect(renderer.setTextTrack).toHaveBeenCalled();
42553
+ });
42554
+ it("renders natively ('showing') when no renderer is injected", async () => {
42555
+ var _a3;
42556
+ respondVtt(`WEBVTT
42557
+
42558
+ 00:00:01.000 --> 00:00:02.000
42559
+ x`);
42560
+ const h = build({ options: { selection: { id: "a" } } });
42561
+ await h.setTracks([makeTrack()]);
42562
+ await flushUntil(
42563
+ () => {
42564
+ var _a4, _b, _c;
42565
+ return ((_c = (_b = (_a4 = h.provider.lastTrack) == null ? void 0 : _a4.cues) == null ? void 0 : _b.length) != null ? _c : 0) > 0;
42566
+ }
42567
+ );
42568
+ expect((_a3 = h.provider.lastTrack) == null ? void 0 : _a3.mode).toBe("showing");
42569
+ });
42570
+ });
41459
42571
  it("preserves cue id when present", async () => {
41460
- var _a3;
42572
+ var _a3, _b;
41461
42573
  respondVtt(`WEBVTT
41462
42574
 
41463
42575
  idA
41464
42576
  00:00:01.000 --> 00:00:02.000
41465
42577
  first`);
41466
- const t = makeTrack();
41467
- controller.setTextTracks([t]);
41468
- controller.setActiveTextTrack(t.id);
42578
+ const h = build({ options: { selection: { id: "a" } } });
42579
+ await h.setTracks([makeTrack()]);
41469
42580
  await flushUntil(() => {
41470
- var _a4, _b;
41471
- return ((_b = (_a4 = media.lastTrack) == null ? void 0 : _a4.cues.length) != null ? _b : 0) > 0;
42581
+ var _a4, _b2, _c;
42582
+ return ((_c = (_b2 = (_a4 = h.provider.lastTrack) == null ? void 0 : _a4.cues) == null ? void 0 : _b2.length) != null ? _c : 0) > 0;
41472
42583
  });
41473
- const cue = (_a3 = media.lastTrack) == null ? void 0 : _a3.cues[0];
42584
+ const cue = (_b = (_a3 = h.provider.lastTrack) == null ? void 0 : _a3.cues) == null ? void 0 : _b[0];
41474
42585
  expect(cue.id).toBe("idA");
41475
42586
  });
41476
42587
  it("dedupes cues that appear in more than one segment", async () => {
@@ -41504,63 +42615,67 @@ only-b`;
41504
42615
  if (call === 2) return Promise.resolve(new Response(segA));
41505
42616
  return Promise.resolve(new Response(segB));
41506
42617
  });
41507
- const t = makeTrack({ uri: "https://x.test/sub.m3u8" });
41508
- controller.setTextTracks([t]);
41509
- controller.setActiveTextTrack(t.id);
42618
+ const h = build({ options: { selection: { id: "a" } } });
42619
+ await h.setTracks([makeTrack({ uri: "https://x.test/sub.m3u8" })]);
41510
42620
  await flushUntil(() => {
41511
- var _a4, _b2;
41512
- return ((_b2 = (_a4 = media.lastTrack) == null ? void 0 : _a4.cues.length) != null ? _b2 : 0) >= 3;
42621
+ var _a4, _b2, _c;
42622
+ return ((_c = (_b2 = (_a4 = h.provider.lastTrack) == null ? void 0 : _a4.cues) == null ? void 0 : _b2.length) != null ? _c : 0) >= 3;
41513
42623
  });
41514
- const texts = ((_b = (_a3 = media.lastTrack) == null ? void 0 : _a3.cues) != null ? _b : []).map(
42624
+ const texts = ((_b = (_a3 = h.provider.lastTrack) == null ? void 0 : _a3.cues) != null ? _b : []).map(
41515
42625
  (c) => c.text
41516
42626
  );
41517
42627
  expect(texts).toEqual(["only-a", "shared", "only-b"]);
41518
42628
  });
41519
- it("selecting an unknown id is a no-op", () => {
42629
+ it("selecting an unknown id renders nothing and stays inactive", async () => {
41520
42630
  const handler = jasmine.createSpy();
41521
- controller.on("activeTextTrackChange", handler);
41522
- controller.setTextTracks([makeTrack()]);
41523
- controller.setActiveTextTrack("unknown");
42631
+ const h = build({ options: { selection: { id: "unknown" } } });
42632
+ h.controller.on("activeTextTrackChange", handler);
42633
+ await h.setTracks([makeTrack()]);
41524
42634
  expect(handler).not.toHaveBeenCalled();
41525
- expect(controller.activeTextTrack).toBeNull();
42635
+ expect(h.controller.activeTextTrack).toBeNull();
42636
+ expect(h.provider.getOrCreate).not.toHaveBeenCalled();
41526
42637
  });
41527
- it("selecting null clears the current active track", () => {
42638
+ it("disabling clears the current active track", async () => {
42639
+ var _a3;
41528
42640
  respondVtt(`WEBVTT
41529
42641
  `);
41530
- const t = makeTrack();
41531
- controller.setTextTracks([t]);
41532
- controller.setActiveTextTrack(t.id);
41533
- controller.setActiveTextTrack(null);
41534
- expect(controller.activeTextTrack).toBeNull();
42642
+ const h = build({ options: { selection: { id: "a" } } });
42643
+ await h.setTracks([makeTrack()]);
42644
+ expect((_a3 = h.controller.activeTextTrack) == null ? void 0 : _a3.id).toBe("a");
42645
+ h.options$.value = { enabled: "off" };
42646
+ expect(h.controller.activeTextTrack).toBeNull();
41535
42647
  });
41536
- it("clears active selection when its track is removed", () => {
42648
+ it("clears active selection when its track is removed", async () => {
42649
+ var _a3;
41537
42650
  respondVtt(`WEBVTT
41538
42651
  `);
42652
+ const h = build({ options: { selection: { id: "a" } } });
41539
42653
  const t1 = makeTrack({ id: "a" });
41540
42654
  const t2 = makeTrack({ id: "b", uri: "https://x.test/b.vtt" });
41541
- controller.setTextTracks([t1, t2]);
41542
- controller.setActiveTextTrack("a");
41543
- controller.setTextTracks([t2]);
41544
- expect(controller.activeTextTrack).toBeNull();
42655
+ await h.setTracks([t1, t2]);
42656
+ expect((_a3 = h.controller.activeTextTrack) == null ? void 0 : _a3.id).toBe("a");
42657
+ await h.setTracks([t2]);
42658
+ expect(h.controller.activeTextTrack).toBeNull();
41545
42659
  });
41546
- it("keeps active selection when track list updates with same active", () => {
41547
- var _a3;
42660
+ it("keeps the active selection when the list updates with it still present", async () => {
42661
+ var _a3, _b;
41548
42662
  respondVtt(`WEBVTT
41549
42663
  `);
42664
+ const h = build({ options: { selection: { id: "a" } } });
41550
42665
  const t1 = makeTrack({ id: "a" });
41551
42666
  const t2 = makeTrack({ id: "b" });
41552
- controller.setTextTracks([t1]);
41553
- controller.setActiveTextTrack("a");
41554
- controller.setTextTracks([t1, t2]);
41555
- expect((_a3 = controller.activeTextTrack) == null ? void 0 : _a3.id).toBe("a");
42667
+ await h.setTracks([t1]);
42668
+ expect((_a3 = h.controller.activeTextTrack) == null ? void 0 : _a3.id).toBe("a");
42669
+ await h.setTracks([t1, t2]);
42670
+ expect((_b = h.controller.activeTextTrack) == null ? void 0 : _b.id).toBe("a");
41556
42671
  });
41557
42672
  it("emits textTrackError on fetch failure", async () => {
41558
42673
  requester.request.and.rejectWith(new Error("boom"));
42674
+ const h = build({ options: { selection: { id: "a" } } });
41559
42675
  const onError = jasmine.createSpy("textTrackError");
41560
- controller.on("textTrackError", onError);
42676
+ h.controller.on("textTrackError", onError);
41561
42677
  const t = makeTrack();
41562
- controller.setTextTracks([t]);
41563
- controller.setActiveTextTrack(t.id);
42678
+ await h.setTracks([t]);
41564
42679
  await flushUntil(() => onError.calls.count() > 0);
41565
42680
  expect(onError).toHaveBeenCalledTimes(1);
41566
42681
  const arg = onError.calls.mostRecent().args[0];
@@ -41569,17 +42684,15 @@ only-b`;
41569
42684
  });
41570
42685
  it("wraps non-Error rejections", async () => {
41571
42686
  requester.request.and.rejectWith("plain");
42687
+ const h = build({ options: { selection: { id: "a" } } });
41572
42688
  const onError = jasmine.createSpy();
41573
- controller.on("textTrackError", onError);
41574
- const t = makeTrack();
41575
- controller.setTextTracks([t]);
41576
- controller.setActiveTextTrack(t.id);
42689
+ h.controller.on("textTrackError", onError);
42690
+ await h.setTracks([makeTrack()]);
41577
42691
  await flushUntil(() => onError.calls.count() > 0);
41578
- const arg = onError.calls.mostRecent().args[0];
41579
- expect(arg.error.message).toBe("plain");
42692
+ expect(onError.calls.mostRecent().args[0].error.message).toBe("plain");
41580
42693
  });
41581
42694
  it("drops cues that arrive after the load has been cancelled", async () => {
41582
- var _a3, _b;
42695
+ var _a3;
41583
42696
  let resolveFetch = () => {
41584
42697
  };
41585
42698
  requester.request.and.returnValue(
@@ -41587,10 +42700,10 @@ only-b`;
41587
42700
  resolveFetch = res;
41588
42701
  })
41589
42702
  );
41590
- const t = makeTrack();
41591
- controller.setTextTracks([t]);
41592
- controller.setActiveTextTrack(t.id);
41593
- controller.setActiveTextTrack(null);
42703
+ const h = build({ options: { selection: { id: "a" } } });
42704
+ await h.setTracks([makeTrack()]);
42705
+ const track2 = h.provider.lastTrack;
42706
+ h.options$.value = { enabled: "off" };
41594
42707
  resolveFetch(
41595
42708
  new Response(`WEBVTT
41596
42709
 
@@ -41598,9 +42711,9 @@ only-b`;
41598
42711
  post`)
41599
42712
  );
41600
42713
  await flushPromises();
41601
- expect((_b = (_a3 = media.lastTrack) == null ? void 0 : _a3.cues.length) != null ? _b : 0).toBe(0);
42714
+ expect((_a3 = track2 == null ? void 0 : track2.storedCues.length) != null ? _a3 : 0).toBe(0);
41602
42715
  });
41603
- it("does not emit textTrackError when load is cancelled by switching tracks", async () => {
42716
+ it("does not emit textTrackError when a load is cancelled by switching", async () => {
41604
42717
  let rejectFirst = () => {
41605
42718
  };
41606
42719
  let callCount = 0;
@@ -41611,17 +42724,16 @@ post`)
41611
42724
  else rej(new Error("unused"));
41612
42725
  })
41613
42726
  );
42727
+ const h = build({ options: { selection: { id: "a" } } });
41614
42728
  const onError = jasmine.createSpy("textTrackError");
41615
- controller.on("textTrackError", onError);
41616
- const t = makeTrack();
41617
- controller.setTextTracks([t]);
41618
- controller.setActiveTextTrack(t.id);
41619
- controller.setActiveTextTrack(null);
42729
+ h.controller.on("textTrackError", onError);
42730
+ await h.setTracks([makeTrack()]);
42731
+ h.options$.value = { enabled: "off" };
41620
42732
  rejectFirst(new Error("would-be error"));
41621
42733
  await flushPromises();
41622
42734
  expect(onError).not.toHaveBeenCalled();
41623
42735
  });
41624
- it("switching active track clears the previous DOM cues", async () => {
42736
+ it("switching language reuses the per-rendition track and swaps its cues", async () => {
41625
42737
  var _a3;
41626
42738
  let call = 0;
41627
42739
  requester.request.and.callFake(() => {
@@ -41638,33 +42750,65 @@ two`
41638
42750
  )
41639
42751
  );
41640
42752
  });
41641
- const a = makeTrack({ id: "a" });
41642
- const b = makeTrack({
41643
- id: "b",
41644
- uri: "https://x.test/b.vtt",
41645
- label: "Two"
41646
- });
41647
- controller.setTextTracks([a, b]);
41648
- controller.setActiveTextTrack("a");
42753
+ const en = makeTrack({ id: "en", language: "en", label: "English" });
42754
+ const es = makeTrack({
42755
+ id: "es",
42756
+ language: "es",
42757
+ label: "Espanol",
42758
+ uri: "https://x.test/es.vtt"
42759
+ });
42760
+ const h = build({ options: { selection: { id: "en" } } });
42761
+ await h.setTracks([en, es]);
41649
42762
  await flushUntil(() => {
41650
- var _a4, _b;
41651
- return ((_b = (_a4 = media.lastTrack) == null ? void 0 : _a4.cues.length) != null ? _b : 0) > 0;
42763
+ var _a4, _b, _c;
42764
+ return ((_c = (_b = (_a4 = h.provider.lastTrack) == null ? void 0 : _a4.cues) == null ? void 0 : _b.length) != null ? _c : 0) > 0;
41652
42765
  });
41653
- const firstTrack = media.lastTrack;
41654
- expect(firstTrack == null ? void 0 : firstTrack.cues.length).toBe(1);
41655
- controller.setActiveTextTrack("b");
41656
- expect(firstTrack == null ? void 0 : firstTrack.cues.length).toBe(0);
41657
- expect(firstTrack == null ? void 0 : firstTrack.mode).toBe("disabled");
42766
+ const enTrack = h.provider.lastTrack;
42767
+ h.select({ id: "es" });
42768
+ expect(enTrack == null ? void 0 : enTrack.storedCues.length).toBe(0);
42769
+ expect(enTrack == null ? void 0 : enTrack.mode).toBe("disabled");
41658
42770
  await flushUntil(
41659
42771
  () => {
41660
- var _a4, _b;
41661
- return media.lastTrack !== firstTrack && ((_b = (_a4 = media.lastTrack) == null ? void 0 : _a4.cues.length) != null ? _b : 0) > 0;
42772
+ var _a4, _b, _c;
42773
+ return h.provider.lastTrack !== enTrack && ((_c = (_b = (_a4 = h.provider.lastTrack) == null ? void 0 : _a4.cues) == null ? void 0 : _b.length) != null ? _c : 0) > 0;
41662
42774
  }
41663
42775
  );
41664
- expect((_a3 = media.lastTrack) == null ? void 0 : _a3.cues.length).toBe(1);
42776
+ expect(h.provider.lastTrack).not.toBe(enTrack);
42777
+ expect((_a3 = h.provider.lastTrack) == null ? void 0 : _a3.language).toBe("es");
42778
+ expect(h.provider.showingWithCues().length).toBe(1);
41665
42779
  });
41666
- it("suspend clears the DOM track and resume rebuilds it, keeping the selection", async () => {
41667
- var _a3, _b, _c;
42780
+ it("cycling languages never accumulates or double-shows cues", async () => {
42781
+ requester.request.and.callFake(
42782
+ () => Promise.resolve(
42783
+ new Response(`WEBVTT
42784
+
42785
+ 00:00:01.000 --> 00:00:02.000
42786
+ cue`)
42787
+ )
42788
+ );
42789
+ const langs = ["en", "es", "ja"].map(
42790
+ (language) => makeTrack({
42791
+ id: language,
42792
+ language,
42793
+ uri: `https://x.test/${language}.vtt`
42794
+ })
42795
+ );
42796
+ const h = build({ options: { selection: { id: "en" } } });
42797
+ await h.setTracks(langs);
42798
+ for (const id of ["en", "es", "ja", "en"]) {
42799
+ h.select({ id });
42800
+ await flushUntil(
42801
+ () => {
42802
+ var _a3, _b, _c;
42803
+ return ((_c = (_b = (_a3 = h.provider.lastTrack) == null ? void 0 : _a3.cues) == null ? void 0 : _b.length) != null ? _c : 0) > 0;
42804
+ }
42805
+ );
42806
+ expect(h.provider.showingWithCues().length).withContext(`after selecting ${id}`).toBe(1);
42807
+ }
42808
+ expect(h.provider.created.length).toBe(3);
42809
+ });
42810
+ it("deactivate hides the DOM track and activate rebuilds it, keeping the selection", async () => {
42811
+ var _a3, _b, _c, _d;
41668
42812
  requester.request.and.callFake(
41669
42813
  () => Promise.resolve(
41670
42814
  new Response(`WEBVTT
@@ -41673,96 +42817,94 @@ two`
41673
42817
  one`)
41674
42818
  )
41675
42819
  );
41676
- const t = makeTrack();
41677
- controller.setTextTracks([t]);
41678
- controller.setActiveTextTrack(t.id);
42820
+ const h = build({ options: { selection: { id: "a" } } });
42821
+ await h.setTracks([makeTrack()]);
41679
42822
  await flushUntil(() => {
41680
- var _a4, _b2;
41681
- return ((_b2 = (_a4 = media.lastTrack) == null ? void 0 : _a4.cues.length) != null ? _b2 : 0) > 0;
41682
- });
41683
- const first2 = media.lastTrack;
41684
- expect(first2 == null ? void 0 : first2.mode).toBe("showing");
41685
- controller.suspend();
41686
- expect(first2 == null ? void 0 : first2.cues.length).toBe(0);
41687
- expect(first2 == null ? void 0 : first2.mode).toBe("disabled");
41688
- expect((_a3 = controller.activeTextTrack) == null ? void 0 : _a3.id).toBe(t.id);
41689
- controller.resume();
41690
- await flushUntil(
41691
- () => {
41692
- var _a4, _b2;
41693
- return media.lastTrack !== first2 && ((_b2 = (_a4 = media.lastTrack) == null ? void 0 : _a4.cues.length) != null ? _b2 : 0) > 0;
41694
- }
41695
- );
41696
- expect((_b = media.lastTrack) == null ? void 0 : _b.mode).toBe("showing");
41697
- expect((_c = media.lastTrack) == null ? void 0 : _c.cues.length).toBe(1);
42823
+ var _a4, _b2, _c2;
42824
+ return ((_c2 = (_b2 = (_a4 = h.provider.lastTrack) == null ? void 0 : _a4.cues) == null ? void 0 : _b2.length) != null ? _c2 : 0) > 0;
42825
+ });
42826
+ const track2 = h.provider.lastTrack;
42827
+ expect(track2 == null ? void 0 : track2.mode).toBe("showing");
42828
+ expect(h.controller.active).toBeTrue();
42829
+ h.controller.deactivate();
42830
+ expect(h.controller.active).toBeFalse();
42831
+ expect(track2 == null ? void 0 : track2.storedCues.length).toBe(0);
42832
+ expect(track2 == null ? void 0 : track2.mode).toBe("disabled");
42833
+ expect((_a3 = h.controller.activeTextTrack) == null ? void 0 : _a3.id).toBe("a");
42834
+ h.controller.activate();
42835
+ expect(h.controller.active).toBeTrue();
42836
+ await flushUntil(() => {
42837
+ var _a4, _b2, _c2;
42838
+ return ((_c2 = (_b2 = (_a4 = h.provider.lastTrack) == null ? void 0 : _a4.cues) == null ? void 0 : _b2.length) != null ? _c2 : 0) > 0;
42839
+ });
42840
+ expect((_b = h.provider.lastTrack) == null ? void 0 : _b.mode).toBe("showing");
42841
+ expect((_d = (_c = h.provider.lastTrack) == null ? void 0 : _c.cues) == null ? void 0 : _d.length).toBe(1);
41698
42842
  });
41699
- it("resume is a no-op when nothing is active", () => {
41700
- controller.resume();
41701
- expect(media.addTextTrack).not.toHaveBeenCalled();
42843
+ it("activate is a no-op when nothing is selected", () => {
42844
+ const h = build();
42845
+ h.controller.activate();
42846
+ expect(h.provider.getOrCreate).not.toHaveBeenCalled();
41702
42847
  });
41703
- it("resume is a no-op when already rendering", async () => {
42848
+ it("activate is a no-op when already rendering", async () => {
41704
42849
  respondVtt(`WEBVTT
41705
42850
 
41706
42851
  00:00:01.000 --> 00:00:02.000
41707
42852
  one`);
41708
- const t = makeTrack();
41709
- controller.setTextTracks([t]);
41710
- controller.setActiveTextTrack(t.id);
42853
+ const h = build({ options: { selection: { id: "a" } } });
42854
+ await h.setTracks([makeTrack()]);
41711
42855
  await flushUntil(() => {
41712
- var _a3, _b;
41713
- return ((_b = (_a3 = media.lastTrack) == null ? void 0 : _a3.cues.length) != null ? _b : 0) > 0;
42856
+ var _a3, _b, _c;
42857
+ return ((_c = (_b = (_a3 = h.provider.lastTrack) == null ? void 0 : _a3.cues) == null ? void 0 : _b.length) != null ? _c : 0) > 0;
41714
42858
  });
41715
- media.addTextTrack.calls.reset();
41716
- controller.resume();
41717
- expect(media.addTextTrack).not.toHaveBeenCalled();
42859
+ h.provider.getOrCreate.calls.reset();
42860
+ h.controller.activate();
42861
+ expect(h.provider.getOrCreate).not.toHaveBeenCalled();
41718
42862
  });
41719
- it("skips DOM cue creation when no VTTCue ctor is available", async () => {
42863
+ it("does not render while deactivated even as the selection resolves", async () => {
41720
42864
  var _a3;
41721
- ;
41722
- globalThis.VTTCue = void 0;
41723
42865
  respondVtt(`WEBVTT
41724
42866
 
41725
42867
  00:00:01.000 --> 00:00:02.000
41726
- ignored`);
41727
- const t = makeTrack();
41728
- controller.setTextTracks([t]);
41729
- controller.setActiveTextTrack(t.id);
41730
- await flushPromises();
41731
- expect((_a3 = media.lastTrack) == null ? void 0 : _a3.cues.length).toBe(0);
42868
+ one`);
42869
+ const h = build({
42870
+ options: { selection: { id: "a" } },
42871
+ activate: false
42872
+ });
42873
+ h.controller.deactivate();
42874
+ await h.setTracks([makeTrack()]);
42875
+ expect((_a3 = h.controller.activeTextTrack) == null ? void 0 : _a3.id).toBe("a");
42876
+ expect(h.provider.getOrCreate).not.toHaveBeenCalled();
41732
42877
  });
41733
- it("can run without a DOM media element", async () => {
41734
- const headless = new SidecarTextTrackController({ media: null });
41735
- const t = makeTrack();
42878
+ it("skips DOM cue creation when no VTTCue ctor is available", async () => {
42879
+ var _a3, _b, _c;
42880
+ ;
42881
+ globalThis.VTTCue = void 0;
41736
42882
  respondVtt(`WEBVTT
41737
42883
 
41738
42884
  00:00:01.000 --> 00:00:02.000
41739
- x`);
41740
- headless.setTextTracks([t]);
41741
- headless.setActiveTextTrack(t.id);
42885
+ ignored`);
42886
+ const h = build({ options: { selection: { id: "a" } } });
42887
+ await h.setTracks([makeTrack()]);
41742
42888
  await flushPromises();
41743
- expect(headless.activeTextTrack).toBe(t);
41744
- headless.dispose();
42889
+ expect((_c = (_b = (_a3 = h.provider.lastTrack) == null ? void 0 : _a3.cues) == null ? void 0 : _b.length) != null ? _c : 0).toBe(0);
41745
42890
  });
41746
42891
  it("forwards requestInit when configured", async () => {
41747
- const customController = new SidecarTextTrackController({
41748
- media,
41749
- requestInit: { credentials: "include" }
41750
- });
41751
42892
  respondVtt(`WEBVTT
41752
42893
  `);
42894
+ const h = build({
42895
+ options: { selection: { id: "a" } },
42896
+ requestInit: { credentials: "include" }
42897
+ });
41753
42898
  const t = makeTrack();
41754
- customController.setTextTracks([t]);
41755
- customController.setActiveTextTrack(t.id);
42899
+ await h.setTracks([t]);
41756
42900
  await flushPromises();
41757
42901
  expect(requester.request).toHaveBeenCalledWith(
41758
42902
  t.uri,
41759
42903
  jasmine.objectContaining({ credentials: "include" }),
41760
42904
  jasmine.any(Object)
41761
42905
  );
41762
- customController.dispose();
41763
42906
  });
41764
42907
  it("drives HLS caption segment order from the media playhead", async () => {
41765
- media.currentTime = 12;
41766
42908
  const playlistBody = `#EXTM3U
41767
42909
  #EXT-X-TARGETDURATION:5
41768
42910
  #EXTINF:5.0,
@@ -41789,14 +42931,13 @@ d.vtt
41789
42931
  ${path}`)
41790
42932
  );
41791
42933
  });
41792
- const t = makeTrack({ uri: "https://x.test/sub.m3u8" });
41793
- controller.setTextTracks([t]);
41794
- controller.setActiveTextTrack(t.id);
42934
+ const h = build({ options: { selection: { id: "a" } } });
42935
+ h.playback.currentTime = 12;
42936
+ await h.setTracks([makeTrack({ uri: "https://x.test/sub.m3u8" })]);
41795
42937
  await flushUntil(() => fetched.length > 0);
41796
42938
  expect(fetched[0]).toBe("c.vtt");
41797
42939
  });
41798
42940
  it("dispose aborts in-flight loads and clears state", async () => {
41799
- var _a3, _b;
41800
42941
  let resolveResponse = () => {
41801
42942
  };
41802
42943
  requester.request.and.callFake(
@@ -41804,11 +42945,11 @@ ${path}`)
41804
42945
  resolveResponse = res;
41805
42946
  })
41806
42947
  );
41807
- const t = makeTrack();
41808
- controller.setTextTracks([t]);
41809
- controller.setActiveTextTrack(t.id);
41810
- expect(media.lastTrack).not.toBeNull();
41811
- controller.dispose();
42948
+ const h = build({ options: { selection: { id: "a" } } });
42949
+ await h.setTracks([makeTrack()]);
42950
+ const track2 = h.provider.lastTrack;
42951
+ expect(track2).not.toBeNull();
42952
+ h.controller.dispose();
41812
42953
  resolveResponse(
41813
42954
  new Response(`WEBVTT
41814
42955
 
@@ -41816,103 +42957,42 @@ ${path}`)
41816
42957
  post`)
41817
42958
  );
41818
42959
  await flushPromises();
41819
- expect((_a3 = media.lastTrack) == null ? void 0 : _a3.mode).toBe("disabled");
41820
- expect((_b = media.lastTrack) == null ? void 0 : _b.cues.length).toBe(0);
41821
- expect(controller.activeTextTrack).toBeNull();
41822
- expect(controller.textTracks).toEqual([]);
42960
+ expect(track2 == null ? void 0 : track2.mode).toBe("disabled");
42961
+ expect(track2 == null ? void 0 : track2.storedCues.length).toBe(0);
42962
+ expect(h.controller.activeTextTrack).toBeNull();
42963
+ expect(h.controller.textTracks).toEqual([]);
41823
42964
  });
41824
- it("handles tracks with null language", async () => {
42965
+ it("creates the DOM track with an undefined language for null-language tracks", async () => {
41825
42966
  respondVtt(`WEBVTT
41826
42967
  `);
41827
- const t = makeTrack({ language: null });
41828
- controller.setTextTracks([t]);
41829
- controller.setActiveTextTrack(t.id);
42968
+ const h = build({ options: { selection: { id: "a" } } });
42969
+ await h.setTracks([makeTrack({ language: null })]);
41830
42970
  await flushPromises();
41831
- expect(media.addTextTrack).toHaveBeenCalledWith(
42971
+ expect(h.provider.getOrCreate).toHaveBeenCalledWith(
41832
42972
  "subtitles",
41833
42973
  "English",
41834
42974
  void 0
41835
42975
  );
41836
42976
  });
41837
- it("setTextTracks with same array reference is a no-op", () => {
42977
+ it("setting the same resolved track list twice emits textTracksChange once", async () => {
41838
42978
  const handler = jasmine.createSpy();
41839
- controller.on("textTracksChange", handler);
42979
+ const h = build();
42980
+ h.controller.on("textTracksChange", handler);
41840
42981
  const list = [makeTrack()];
41841
- controller.setTextTracks(list);
41842
- controller.setTextTracks(list);
42982
+ await h.setTracks(list);
42983
+ await h.setTracks([makeTrack()]);
41843
42984
  expect(handler).toHaveBeenCalledTimes(1);
41844
42985
  });
41845
- it("clearing active when none is active is a no-op", () => {
41846
- const handler = jasmine.createSpy();
41847
- controller.on("activeTextTrackChange", handler);
41848
- controller.setActiveTextTrack(null);
41849
- expect(handler).not.toHaveBeenCalled();
41850
- });
41851
- it("selecting same id is a no-op", () => {
42986
+ it("re-selecting the same id is a no-op", async () => {
41852
42987
  respondVtt(`WEBVTT
41853
42988
  `);
41854
- const t = makeTrack();
42989
+ const h = build({ options: { selection: { id: "a" } } });
42990
+ await h.setTracks([makeTrack()]);
41855
42991
  const handler = jasmine.createSpy();
41856
- controller.setTextTracks([t]);
41857
- controller.setActiveTextTrack(t.id);
41858
- controller.on("activeTextTrackChange", handler);
41859
- controller.setActiveTextTrack(t.id);
42992
+ h.controller.on("activeTextTrackChange", handler);
42993
+ h.select({ id: "a" });
41860
42994
  expect(handler).not.toHaveBeenCalled();
41861
42995
  });
41862
- describe("preferredTextLanguage", () => {
41863
- const en = makeTrack({ id: "en", language: "en", label: "English" });
41864
- const es = makeTrack({ id: "es", language: "es", label: "Espanol" });
41865
- let preferredTextLanguage;
41866
- let prefController;
41867
- beforeEach(() => {
41868
- preferredTextLanguage = data(
41869
- null
41870
- );
41871
- prefController = new SidecarTextTrackController({
41872
- media: null,
41873
- preferredTextLanguage
41874
- });
41875
- });
41876
- afterEach(() => prefController.dispose());
41877
- it("selects the matching track when the preference changes", () => {
41878
- var _a3;
41879
- prefController.setTextTracks([en, es]);
41880
- expect(prefController.activeTextTrack).toBeNull();
41881
- preferredTextLanguage.value = "es";
41882
- expect((_a3 = prefController.activeTextTrack) == null ? void 0 : _a3.id).toBe("es");
41883
- });
41884
- it("clears the selection when the preference is set back to null", () => {
41885
- var _a3;
41886
- prefController.setTextTracks([en, es]);
41887
- preferredTextLanguage.value = "es";
41888
- expect((_a3 = prefController.activeTextTrack) == null ? void 0 : _a3.id).toBe("es");
41889
- preferredTextLanguage.value = null;
41890
- expect(prefController.activeTextTrack).toBeNull();
41891
- });
41892
- it("re-applies the preference when the track list changes", () => {
41893
- var _a3;
41894
- preferredTextLanguage.value = "es";
41895
- expect(prefController.activeTextTrack).toBeNull();
41896
- prefController.setTextTracks([en, es]);
41897
- expect((_a3 = prefController.activeTextTrack) == null ? void 0 : _a3.id).toBe("es");
41898
- });
41899
- it("a preferred language wins over forced auto-selection", () => {
41900
- var _a3;
41901
- preferredTextLanguage.value = "es";
41902
- prefController.setTextTracks([
41903
- makeTrack({ id: "en-forced", language: "en", forced: true }),
41904
- es
41905
- ]);
41906
- expect((_a3 = prefController.activeTextTrack) == null ? void 0 : _a3.id).toBe("es");
41907
- });
41908
- it("leaves forced auto-selection intact while no preference is set", () => {
41909
- var _a3;
41910
- prefController.setTextTracks([
41911
- makeTrack({ id: "forced", forced: true })
41912
- ]);
41913
- expect((_a3 = prefController.activeTextTrack) == null ? void 0 : _a3.id).toBe("forced");
41914
- });
41915
- });
41916
42996
  });
41917
42997
 
41918
42998
  // test/unit/text/SidecarTextTrackLoader.test.ts
@@ -41996,6 +43076,39 @@ from-segment`;
41996
43076
  await loadWebVttCues("https://x.test/sub.txt", options);
41997
43077
  expect(batches.flat().map((c) => c.text)).toEqual(["from-segment"]);
41998
43078
  });
43079
+ it("forwards STYLE blocks parsed from a segment via onStyles", async () => {
43080
+ const playlistText = `#EXTM3U
43081
+ #EXT-X-TARGETDURATION:10
43082
+ #EXTINF:10.0,
43083
+ seg.vtt
43084
+ #EXT-X-ENDLIST
43085
+ `;
43086
+ const segmentText = `WEBVTT
43087
+
43088
+ STYLE
43089
+ ::cue { color: red }
43090
+
43091
+ 00:00:01.000 --> 00:00:02.000
43092
+ from-segment`;
43093
+ let call = 0;
43094
+ requester.request.and.callFake(() => {
43095
+ call++;
43096
+ if (call === 1) {
43097
+ return respondText(
43098
+ playlistText,
43099
+ "application/vnd.apple.mpegurl",
43100
+ "https://x.test/sub.m3u8"
43101
+ );
43102
+ }
43103
+ return respondText(segmentText, "text/vtt");
43104
+ });
43105
+ const styleBatches = [];
43106
+ const { options } = streamingOpts({
43107
+ onStyles: (styles) => styleBatches.push([...styles])
43108
+ });
43109
+ await loadWebVttCues("https://x.test/sub.m3u8", options);
43110
+ expect(styleBatches).toEqual([["::cue { color: red }"]]);
43111
+ });
41999
43112
  it("falls back to the request URI when response.url is empty (body sniffed)", async () => {
42000
43113
  const playlistText = `#EXTM3U
42001
43114
  #EXT-X-VERSION:3
@@ -43370,13 +44483,8 @@ x`);
43370
44483
  });
43371
44484
  const deps = createContainer(factories).dependencies;
43372
44485
  const options = dashFactoryDeps.options.value;
43373
- expect(deps.preferredAudioLanguage.value).toBe(
43374
- options.preferredAudioLanguage
43375
- );
44486
+ expect(deps.audio.value).toBe(options.audio);
43376
44487
  expect(deps.allowedContentTypes.value).toBe(options.allowedContentTypes);
43377
- expect(deps.preferDescriptiveAudio.value).toBe(
43378
- options.preferDescriptiveAudio
43379
- );
43380
44488
  expect(deps.abr.value).toBe(options.abr);
43381
44489
  });
43382
44490
  it("provides streaming options to createContentStreamFactories", () => {
@@ -46840,9 +47948,8 @@ x`);
46840
47948
  drmController,
46841
47949
  mediaTimeline: data(Promise.resolve(timeline)),
46842
47950
  options: data({
46843
- preferredAudioLanguage: null,
46844
- codecOverrides: {},
46845
- preferDescriptiveAudio: false
47951
+ audio: { selection: { language: null, descriptive: false } },
47952
+ codecOverrides: {}
46846
47953
  })
46847
47954
  }).value;
46848
47955
  const audio = result.periods[0].qualities.filter(
@@ -47336,13 +48443,8 @@ x`);
47336
48443
  });
47337
48444
  const deps = createContainer(factories).dependencies;
47338
48445
  const options = hlsFactoryDeps.options.value;
47339
- expect(deps.preferredAudioLanguage.value).toBe(
47340
- options.preferredAudioLanguage
47341
- );
48446
+ expect(deps.audio.value).toBe(options.audio);
47342
48447
  expect(deps.allowedContentTypes.value).toBe(options.allowedContentTypes);
47343
- expect(deps.preferDescriptiveAudio.value).toBe(
47344
- options.preferDescriptiveAudio
47345
- );
47346
48448
  expect(deps.abr.value).toBe(options.abr);
47347
48449
  });
47348
48450
  it("exposes the shared adController without owning it", () => {
@@ -51686,6 +52788,8 @@ x`);
51686
52788
  objectContaining25({
51687
52789
  patchedMedia: any30(Object),
51688
52790
  media: any30(Object),
52791
+ textTrackProvider: any30(Object),
52792
+ textTrackRenderer: null,
51689
52793
  playbackController: any30(PlaybackControllerImpl),
51690
52794
  playbackSource: any30(PlaybackSourceImpl),
51691
52795
  loudnessNormalizationController: any30(
@@ -52839,228 +53943,145 @@ x`);
52839
53943
  afterEach(() => {
52840
53944
  player.dispose();
52841
53945
  });
53946
+ function info(overrides = {}) {
53947
+ return __spreadValues({
53948
+ id: "t1",
53949
+ kind: "subtitles",
53950
+ language: "en",
53951
+ label: "A",
53952
+ default: false,
53953
+ forced: false,
53954
+ characteristics: [],
53955
+ uri: "a.vtt",
53956
+ mimeType: "text/vtt"
53957
+ }, overrides);
53958
+ }
52842
53959
  function makeTrackWithController() {
52843
53960
  const track2 = new MockTrack();
52844
- const controller = new SidecarTextTrackController({ media: null });
53961
+ const controller = new MockTextTrackController();
52845
53962
  track2.textTrackController = controller;
52846
53963
  return { track: track2, controller };
52847
53964
  }
53965
+ function setTracks(controller, tracks) {
53966
+ const previous = controller.textTracks;
53967
+ controller.textTracks = tracks;
53968
+ controller.dispatch("textTracksChange", { previous, current: tracks });
53969
+ }
53970
+ function setActive(controller, current) {
53971
+ const previous = controller.activeTextTrack;
53972
+ controller.activeTextTrack = current;
53973
+ controller.dispatch("activeTextTrackChange", { previous, current });
53974
+ }
53975
+ function activate(track2) {
53976
+ deps.trackController.activeTrack = track2;
53977
+ deps.trackController.dispatch("trackActivated", { track: track2 });
53978
+ }
52848
53979
  it("returns empty defaults when no current track", () => {
52849
53980
  expect(player.textTracks).toEqual([]);
52850
53981
  expect(player.activeTextTrack).toBeNull();
52851
53982
  });
52852
53983
  it("returns text tracks from the current track", () => {
52853
53984
  const { track: track2, controller } = makeTrackWithController();
52854
- controller.setTextTracks([
52855
- {
52856
- id: "t1",
52857
- kind: "subtitles",
52858
- language: "en",
52859
- label: "English",
52860
- default: false,
52861
- forced: false,
52862
- characteristics: [],
52863
- uri: "https://x/sub.vtt",
52864
- mimeType: "text/vtt"
52865
- }
52866
- ]);
52867
- deps.trackController.activeTrack = track2;
52868
- deps.trackController.dispatch("trackActivated", { track: track2 });
53985
+ controller.textTracks = [info()];
53986
+ activate(track2);
52869
53987
  expect(player.textTracks.length).toBe(1);
52870
53988
  });
52871
- it("selects an active text track via setActiveTextTrack", () => {
53989
+ it("reflects the current track controller active selection", () => {
52872
53990
  var _a3;
52873
53991
  const { track: track2, controller } = makeTrackWithController();
52874
- controller.setTextTracks([
52875
- {
52876
- id: "t1",
52877
- kind: "subtitles",
52878
- language: "en",
52879
- label: "English",
52880
- default: false,
52881
- forced: false,
52882
- characteristics: [],
52883
- uri: "https://x/sub.vtt",
52884
- mimeType: "text/vtt"
52885
- }
52886
- ]);
52887
- deps.trackController.activeTrack = track2;
52888
- deps.trackController.dispatch("trackActivated", { track: track2 });
52889
- player.setActiveTextTrack("t1");
53992
+ controller.textTracks = [info()];
53993
+ activate(track2);
53994
+ setActive(controller, info());
52890
53995
  expect((_a3 = player.activeTextTrack) == null ? void 0 : _a3.id).toBe("t1");
52891
- player.setActiveTextTrack(null);
53996
+ setActive(controller, null);
52892
53997
  expect(player.activeTextTrack).toBeNull();
52893
53998
  });
52894
- it("ignores setActiveTextTrack when current track has no controller", () => {
53999
+ it("reports null active when the current track has no controller", () => {
52895
54000
  const track2 = new MockTrack();
52896
- deps.trackController.activeTrack = track2;
52897
- deps.trackController.dispatch("trackActivated", { track: track2 });
52898
- player.setActiveTextTrack("anything");
54001
+ activate(track2);
52899
54002
  expect(player.activeTextTrack).toBeNull();
52900
54003
  });
52901
54004
  it("redispatches textTracksChange events from the active controller", () => {
52902
54005
  const { track: track2, controller } = makeTrackWithController();
52903
- deps.trackController.activeTrack = track2;
52904
- deps.trackController.dispatch("trackActivated", { track: track2 });
54006
+ activate(track2);
52905
54007
  const spy = createEventSpy(player, "textTracksChange");
52906
- controller.setTextTracks([
52907
- {
52908
- id: "t1",
52909
- kind: "subtitles",
52910
- language: null,
52911
- label: "A",
52912
- default: false,
52913
- forced: false,
52914
- characteristics: [],
52915
- uri: "a.vtt",
52916
- mimeType: "text/vtt"
52917
- }
52918
- ]);
54008
+ setTracks(controller, [info({ language: null })]);
52919
54009
  expect(spy).toHaveBeenCalled();
52920
54010
  });
52921
- it("fires textTracksChange when switching to a track with different list", () => {
54011
+ it("fires textTracksChange when switching to a track with a different list", () => {
52922
54012
  const a = makeTrackWithController();
52923
54013
  const b = makeTrackWithController();
52924
- const trackInfoA = {
52925
- id: "t1",
52926
- kind: "subtitles",
52927
- language: "en",
52928
- label: "A",
52929
- default: false,
52930
- forced: false,
52931
- characteristics: [],
52932
- uri: "a.vtt",
52933
- mimeType: "text/vtt"
52934
- };
52935
- const trackInfoB = {
52936
- id: "t2",
52937
- kind: "subtitles",
52938
- language: "fr",
52939
- label: "B",
52940
- default: false,
52941
- forced: false,
52942
- characteristics: [],
52943
- uri: "b.vtt",
52944
- mimeType: "text/vtt"
52945
- };
52946
- a.controller.setTextTracks([trackInfoA]);
52947
- b.controller.setTextTracks([trackInfoB]);
52948
- deps.trackController.activeTrack = a.track;
52949
- deps.trackController.dispatch("trackActivated", { track: a.track });
54014
+ setTracks(a.controller, [info({ id: "t1", language: "en" })]);
54015
+ setTracks(b.controller, [
54016
+ info({ id: "t2", language: "fr", label: "B" })
54017
+ ]);
54018
+ activate(a.track);
52950
54019
  const spy = createEventSpy(player, "textTracksChange");
52951
- deps.trackController.activeTrack = b.track;
52952
- deps.trackController.dispatch("trackActivated", { track: b.track });
54020
+ activate(b.track);
52953
54021
  expect(spy).toHaveBeenCalled();
52954
54022
  });
52955
- it("deactivates the previous track's captions on trackDeactivated", () => {
52956
- var _a3;
52957
- const a = makeTrackWithController();
52958
- a.controller.setTextTracks([
52959
- {
52960
- id: "t1",
52961
- kind: "subtitles",
52962
- language: "en",
52963
- label: "A",
52964
- default: false,
52965
- forced: false,
52966
- characteristics: [],
52967
- uri: "a.vtt",
52968
- mimeType: "text/vtt"
52969
- }
52970
- ]);
52971
- deps.trackController.activeTrack = a.track;
52972
- deps.trackController.dispatch("trackActivated", { track: a.track });
52973
- a.controller.setActiveTextTrack("t1");
52974
- expect((_a3 = a.controller.activeTextTrack) == null ? void 0 : _a3.id).toBe("t1");
52975
- deps.trackController.activeTrack = null;
52976
- deps.trackController.dispatch("trackDeactivated", { track: a.track });
52977
- expect(a.controller.activeTextTrack).toBeNull();
52978
- });
52979
54023
  it("fires activeTextTrackChange when switching tracks", () => {
52980
54024
  const a = makeTrackWithController();
52981
54025
  const b = makeTrackWithController();
52982
- a.controller.setTextTracks([
52983
- {
52984
- id: "t1",
52985
- kind: "subtitles",
52986
- language: "en",
52987
- label: "A",
52988
- default: false,
52989
- forced: false,
52990
- characteristics: [],
52991
- uri: "a.vtt",
52992
- mimeType: "text/vtt"
52993
- }
52994
- ]);
52995
- a.controller.setActiveTextTrack("t1");
52996
- deps.trackController.activeTrack = a.track;
52997
- deps.trackController.dispatch("trackActivated", { track: a.track });
54026
+ setTracks(a.controller, [info({ id: "t1" })]);
54027
+ setActive(a.controller, info({ id: "t1" }));
54028
+ activate(a.track);
52998
54029
  const spy = createEventSpy(player, "activeTextTrackChange");
52999
- deps.trackController.activeTrack = b.track;
53000
- deps.trackController.dispatch("trackActivated", { track: b.track });
54030
+ activate(b.track);
53001
54031
  expect(spy).toHaveBeenCalled();
53002
54032
  });
53003
54033
  it("re-announces the content caption after an ad break so it does not read as off", () => {
53004
- var _a3, _b, _c;
54034
+ var _a3, _b;
53005
54035
  const content = makeTrackWithController();
53006
- content.controller.setTextTracks([
53007
- {
53008
- id: "c-forced",
53009
- kind: "subtitles",
53010
- language: "en",
53011
- label: "English (Forced)",
53012
- default: true,
53013
- forced: true,
53014
- characteristics: [],
53015
- uri: "c.vtt",
53016
- mimeType: "text/vtt"
53017
- }
53018
- ]);
53019
- expect((_a3 = content.controller.activeTextTrack) == null ? void 0 : _a3.id).toBe("c-forced");
53020
- const ad = makeTrackWithController();
53021
- ad.controller.setTextTracks([
53022
- {
53023
- id: "ad-cc",
53024
- kind: "subtitles",
53025
- language: "en",
53026
- label: "English",
53027
- default: false,
53028
- forced: false,
53029
- characteristics: [],
53030
- uri: "ad.vtt",
53031
- mimeType: "text/vtt"
53032
- }
53033
- ]);
53034
- deps.trackController.activeTrack = content.track;
53035
- deps.trackController.dispatch("trackActivated", {
53036
- track: content.track
54036
+ const contentForced = info({
54037
+ id: "c-forced",
54038
+ label: "English (Forced)",
54039
+ default: true,
54040
+ forced: true,
54041
+ uri: "c.vtt"
53037
54042
  });
54043
+ setTracks(content.controller, [contentForced]);
54044
+ setActive(content.controller, contentForced);
54045
+ const ad = makeTrackWithController();
54046
+ const adCc = info({ id: "ad-cc", label: "English", uri: "ad.vtt" });
54047
+ setTracks(ad.controller, [adCc]);
54048
+ activate(content.track);
53038
54049
  deps.adController.currentAdBreak = { placement: "midroll" };
53039
- content.controller.suspend();
53040
- deps.trackController.activeTrack = ad.track;
53041
- deps.trackController.dispatch("trackActivated", { track: ad.track });
53042
- player.setActiveTextTrack("ad-cc");
53043
- expect((_b = player.activeTextTrack) == null ? void 0 : _b.id).toBe("ad-cc");
54050
+ content.controller.deactivate();
54051
+ activate(ad.track);
54052
+ setActive(ad.controller, adCc);
54053
+ expect((_a3 = player.activeTextTrack) == null ? void 0 : _a3.id).toBe("ad-cc");
53044
54054
  deps.adController.currentAdBreak = null;
53045
54055
  const spy = createEventSpy(player, "activeTextTrackChange");
53046
- deps.trackController.activeTrack = content.track;
53047
- content.controller.resume();
53048
- deps.trackController.dispatch("trackActivated", {
53049
- track: content.track
53050
- });
53051
- expect((_c = player.activeTextTrack) == null ? void 0 : _c.id).toBe("c-forced");
54056
+ content.controller.activate();
54057
+ activate(content.track);
54058
+ expect((_b = player.activeTextTrack) == null ? void 0 : _b.id).toBe("c-forced");
53052
54059
  expect(spy).toHaveBeenCalled();
53053
54060
  const lastCurrent = spy.calls.mostRecent().args[0].current;
53054
54061
  expect(lastCurrent == null ? void 0 : lastCurrent.id).toBe("c-forced");
53055
54062
  });
54063
+ it("announces empty tracks and null active when the track is torn down", () => {
54064
+ const a = makeTrackWithController();
54065
+ const t = info({ id: "t1" });
54066
+ setTracks(a.controller, [t]);
54067
+ setActive(a.controller, t);
54068
+ activate(a.track);
54069
+ const tracksSpy = createEventSpy(player, "textTracksChange");
54070
+ const activeSpy = createEventSpy(player, "activeTextTrackChange");
54071
+ deps.trackController.activeTrack = null;
54072
+ deps.trackController.dispatch("trackDeactivated", { track: a.track });
54073
+ expect(tracksSpy).toHaveBeenCalled();
54074
+ expect(tracksSpy.calls.mostRecent().args[0].current).toEqual([]);
54075
+ expect(activeSpy).toHaveBeenCalled();
54076
+ expect(activeSpy.calls.mostRecent().args[0].current).toBeNull();
54077
+ expect(player.activeTextTrack).toBeNull();
54078
+ });
53056
54079
  it("does not emit activeTextTrackChange when neither track has an active selection", () => {
53057
54080
  const a = makeTrackWithController();
53058
54081
  const b = makeTrackWithController();
53059
- deps.trackController.activeTrack = a.track;
53060
- deps.trackController.dispatch("trackActivated", { track: a.track });
54082
+ activate(a.track);
53061
54083
  const spy = createEventSpy(player, "activeTextTrackChange");
53062
- deps.trackController.activeTrack = b.track;
53063
- deps.trackController.dispatch("trackActivated", { track: b.track });
54084
+ activate(b.track);
53064
54085
  expect(spy).not.toHaveBeenCalled();
53065
54086
  });
53066
54087
  });
@@ -54893,7 +55914,9 @@ x`);
54893
55914
  await loadAndAwaitAdBreaks();
54894
55915
  await poll(() => player.textTracks.length > 0, { timeout: 15 });
54895
55916
  const track2 = player.textTracks[0];
54896
- player.setActiveTextTrack(track2.id);
55917
+ player.configure({
55918
+ text: { enabled: "on", selection: { id: track2.id } }
55919
+ });
54897
55920
  const showingWithCues = () => {
54898
55921
  const dom = mediaRef.value.textTracks;
54899
55922
  return Array.from({ length: dom.length }, (_, i) => dom[i]).filter(
@@ -55033,7 +56056,9 @@ x`);
55033
56056
  }
55034
56057
  async function loadAndPlay(preferredAudioLanguage, lang) {
55035
56058
  const player = suite.player;
55036
- player.configure({ preferredAudioLanguage });
56059
+ player.configure({
56060
+ audio: { selection: { language: preferredAudioLanguage } }
56061
+ });
55037
56062
  player.load({ type: "hls", uri: ASSET });
55038
56063
  await player.play().catch(() => void 0);
55039
56064
  expect(
@@ -55066,7 +56091,7 @@ x`);
55066
56091
  await loadAndPlay("ja", "ja");
55067
56092
  expect(await poll(() => player.currentTime > 1, { timeout: 30 })).withContext("content advanced").toBeTrue();
55068
56093
  const resumeBefore = player.currentTime;
55069
- player.configure({ preferredAudioLanguage: "de" });
56094
+ player.configure({ audio: { selection: { language: "de" } } });
55070
56095
  expect(await poll(() => audioIsOnly(player, "de"), { timeout: 30 })).withContext(`content audio switched to 'de'`).toBeTrue();
55071
56096
  expect(
55072
56097
  await poll(
@@ -55205,6 +56230,9 @@ x`);
55205
56230
  await new Promise((r) => setTimeout(r, 50));
55206
56231
  }
55207
56232
  }
56233
+ function selectTextById(id) {
56234
+ suite.player.configure({ text: { enabled: "on", selection: { id } } });
56235
+ }
55208
56236
  it("DASH discovers sidecar text tracks", async () => {
55209
56237
  await loadAndAwaitTextTracks(dashPlaylist);
55210
56238
  const tracks = suite.player.textTracks;
@@ -55218,7 +56246,7 @@ x`);
55218
56246
  const tracks = suite.player.textTracks;
55219
56247
  if (tracks.length === 0) pending("no text tracks discovered");
55220
56248
  const target = (_a3 = tracks.find((t) => t.language === "en")) != null ? _a3 : tracks[0];
55221
- suite.player.setActiveTextTrack(target.id);
56249
+ selectTextById(target.id);
55222
56250
  const deadline = Date.now() + 1e4;
55223
56251
  let found = false;
55224
56252
  while (!found && Date.now() < deadline) {
@@ -55243,11 +56271,40 @@ x`);
55243
56271
  await loadAndAwaitTextTracks(dashPlaylist);
55244
56272
  const tracks = suite.player.textTracks;
55245
56273
  if (tracks.length === 0) pending("no text tracks discovered");
55246
- suite.player.setActiveTextTrack(tracks[0].id);
56274
+ selectTextById(tracks[0].id);
55247
56275
  expect(suite.player.activeTextTrack).not.toBeNull();
55248
- suite.player.setActiveTextTrack(null);
56276
+ suite.player.configure({ text: { enabled: "off" } });
55249
56277
  expect(suite.player.activeTextTrack).toBeNull();
55250
56278
  });
56279
+ async function awaitCuesShowing() {
56280
+ const deadline = Date.now() + 1e4;
56281
+ while (Date.now() < deadline) {
56282
+ if (showingWithCues() > 0) return true;
56283
+ await new Promise((r) => setTimeout(r, 50));
56284
+ }
56285
+ return false;
56286
+ }
56287
+ function showingWithCues() {
56288
+ var _a3, _b;
56289
+ const dom = mediaRef.value.textTracks;
56290
+ let n = 0;
56291
+ for (let i = 0; i < dom.length; i++) {
56292
+ if (dom[i].mode === "showing" && ((_b = (_a3 = dom[i].cues) == null ? void 0 : _a3.length) != null ? _b : 0) > 0) n++;
56293
+ }
56294
+ return n;
56295
+ }
56296
+ it("cycling languages never leaves more than one caption track showing", async () => {
56297
+ await loadAndAwaitTextTracks(dashPlaylist);
56298
+ const tracks = suite.player.textTracks;
56299
+ if (tracks.length < 2) pending("need at least two text tracks");
56300
+ for (const track2 of [...tracks, ...tracks]) {
56301
+ selectTextById(track2.id);
56302
+ await awaitCuesShowing();
56303
+ expect(showingWithCues()).withContext(
56304
+ `more than one caption track rendering after selecting ${track2.language}`
56305
+ ).toBeLessThanOrEqual(1);
56306
+ }
56307
+ });
55251
56308
  });
55252
56309
 
55253
56310
  // test/integ/track/TrackController.test.ts