avbridge 2.9.0 → 2.11.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 (50) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/{chunk-EY6DZEDT.cjs → chunk-37UOSAVI.cjs} +55 -10
  3. package/dist/chunk-37UOSAVI.cjs.map +1 -0
  4. package/dist/{chunk-5KVLE6YI.js → chunk-EDDWAN2L.js} +3 -2
  5. package/dist/chunk-EDDWAN2L.js.map +1 -0
  6. package/dist/{chunk-SN4WZE24.js → chunk-IHNHHEA2.js} +51 -6
  7. package/dist/chunk-IHNHHEA2.js.map +1 -0
  8. package/dist/{chunk-S4WAZC2T.cjs → chunk-WRKO6Q42.cjs} +3 -2
  9. package/dist/chunk-WRKO6Q42.cjs.map +1 -0
  10. package/dist/element-browser.js +63 -4
  11. package/dist/element-browser.js.map +1 -1
  12. package/dist/element.cjs +18 -5
  13. package/dist/element.cjs.map +1 -1
  14. package/dist/element.d.cts +1 -1
  15. package/dist/element.d.ts +1 -1
  16. package/dist/element.js +17 -4
  17. package/dist/element.js.map +1 -1
  18. package/dist/index.cjs +10 -10
  19. package/dist/index.d.cts +2 -2
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +2 -2
  22. package/dist/{player-DEcidWk6.d.cts → player-DDdNVFDv.d.cts} +23 -1
  23. package/dist/{player-DEcidWk6.d.ts → player-DDdNVFDv.d.ts} +23 -1
  24. package/dist/player.cjs +329 -109
  25. package/dist/player.cjs.map +1 -1
  26. package/dist/player.d.cts +42 -0
  27. package/dist/player.d.ts +42 -0
  28. package/dist/player.js +325 -105
  29. package/dist/player.js.map +1 -1
  30. package/dist/subtitles-5H24MEBJ.js +4 -0
  31. package/dist/{subtitles-4T74JRGT.js.map → subtitles-5H24MEBJ.js.map} +1 -1
  32. package/dist/subtitles-HMVGWTU2.cjs +29 -0
  33. package/dist/{subtitles-QUH4LPI4.cjs.map → subtitles-HMVGWTU2.cjs.map} +1 -1
  34. package/package.json +1 -1
  35. package/src/element/avbridge-player.ts +235 -78
  36. package/src/element/avbridge-subtitles.ts +273 -0
  37. package/src/element/avbridge-video.ts +21 -1
  38. package/src/element/player-styles.ts +85 -35
  39. package/src/index.ts +1 -0
  40. package/src/strategies/fallback/audio-output.ts +39 -4
  41. package/src/strategies/fallback/index.ts +12 -0
  42. package/src/strategies/hybrid/index.ts +9 -0
  43. package/src/subtitles/index.ts +2 -0
  44. package/src/types.ts +25 -0
  45. package/dist/chunk-5KVLE6YI.js.map +0 -1
  46. package/dist/chunk-EY6DZEDT.cjs.map +0 -1
  47. package/dist/chunk-S4WAZC2T.cjs.map +0 -1
  48. package/dist/chunk-SN4WZE24.js.map +0 -1
  49. package/dist/subtitles-4T74JRGT.js +0 -4
  50. package/dist/subtitles-QUH4LPI4.cjs +0 -29
@@ -135,6 +135,7 @@ async function attachSubtitleTracks(video, tracks, bag, onError, transport) {
135
135
  }
136
136
  for (const t of tracks) {
137
137
  if (!t.sidecarUrl) continue;
138
+ console.log(`[avbridge:subs] attaching track id=${t.id} format=${t.format} url=${t.sidecarUrl.slice(0, 60)}`);
138
139
  try {
139
140
  let url = t.sidecarUrl;
140
141
  if (t.format === "srt") {
@@ -169,5 +170,5 @@ exports.SubtitleResourceBag = SubtitleResourceBag;
169
170
  exports.attachSubtitleTracks = attachSubtitleTracks;
170
171
  exports.discoverSidecars = discoverSidecars;
171
172
  exports.srtToVtt = srtToVtt;
172
- //# sourceMappingURL=chunk-S4WAZC2T.cjs.map
173
- //# sourceMappingURL=chunk-S4WAZC2T.cjs.map
173
+ //# sourceMappingURL=chunk-WRKO6Q42.cjs.map
174
+ //# sourceMappingURL=chunk-WRKO6Q42.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/subtitles/srt.ts","../src/subtitles/vtt.ts","../src/subtitles/render.ts","../src/subtitles/index.ts"],"names":["fetchWith"],"mappings":";;;;;AAuBO,SAAS,SAAS,GAAA,EAAqB;AAE5C,EAAA,IAAI,GAAA,CAAI,WAAW,CAAC,CAAA,KAAM,OAAQ,GAAA,GAAM,GAAA,CAAI,MAAM,CAAC,CAAA;AAEnD,EAAA,MAAM,UAAA,GAAa,GAAA,CAAI,OAAA,CAAQ,OAAA,EAAS,IAAI,EAAE,OAAA,CAAQ,KAAA,EAAO,IAAI,CAAA,CAAE,IAAA,EAAK;AAExE,EAAA,MAAM,MAAA,GAAS,UAAA,CAAW,KAAA,CAAM,QAAQ,CAAA;AACxC,EAAA,MAAM,GAAA,GAAgB,CAAC,QAAA,EAAU,EAAE,CAAA;AAEnC,EAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAC1B,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,IAAI,CAAA;AAE9B,IAAA,IAAI,KAAA,CAAM,MAAA,GAAS,CAAA,IAAK,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA,CAAE,IAAA,EAAM,CAAA,EAAG;AACrD,MAAA,KAAA,CAAM,KAAA,EAAM;AAAA,IACd;AACA,IAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AAExB,IAAA,MAAM,MAAA,GAAS,MAAM,KAAA,EAAM;AAC3B,IAAA,MAAM,SAAA,GAAY,cAAc,MAAM,CAAA;AACtC,IAAA,IAAI,CAAC,SAAA,EAAW;AAEhB,IAAA,GAAA,CAAI,KAAK,SAAS,CAAA;AAClB,IAAA,KAAA,MAAW,CAAA,IAAK,KAAA,EAAO,GAAA,CAAI,IAAA,CAAK,CAAC,CAAA;AACjC,IAAA,GAAA,CAAI,KAAK,EAAE,CAAA;AAAA,EACb;AAEA,EAAA,OAAO,GAAA,CAAI,KAAK,IAAI,CAAA;AACtB;AAEA,SAAS,cAAc,IAAA,EAA6B;AAElD,EAAA,MAAM,IAAI,6FAAA,CAA8F,IAAA;AAAA,IACtG,KAAK,IAAA;AAAK,GACZ;AACA,EAAA,IAAI,CAAC,GAAG,OAAO,IAAA;AACf,EAAA,MAAM,GAAA,GAAM,CAAC,CAAA,EAAW,EAAA,EAAY,CAAA,EAAW,OAC7C,CAAA,EAAG,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,IAAI,EAAE,CAAA,CAAA,EAAI,CAAC,CAAA,CAAA,EAAI,EAAA,CAAG,MAAA,CAAO,CAAA,EAAG,GAAG,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,CAAC,CAAC,CAAA,CAAA;AACnE,EAAA,OAAO,CAAA,EAAG,GAAA,CAAI,CAAA,CAAE,CAAC,GAAG,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAC,CAAC,CAAA,KAAA,EAAQ,GAAA,CAAI,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,EAAG,EAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAC,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,KAAK,EAAE,CAAA,CAAA;AACvF;;;AC5DO,SAAS,MAAM,IAAA,EAAuB;AAC3C,EAAA,MAAM,UAAU,IAAA,CAAK,OAAA,CAAQ,SAAA,EAAW,EAAE,EAAE,SAAA,EAAU;AACtD,EAAA,OAAO,OAAA,CAAQ,WAAW,QAAQ,CAAA;AACpC;;;ACUO,IAAM,kBAAN,MAAsB;AAAA,EACnB,EAAA;AAAA,EACA,OAAc,EAAC;AAAA,EAEvB,YAAY,MAAA,EAAqB;AAC/B,IAAA,IAAA,CAAK,EAAA,GAAK,QAAA,CAAS,aAAA,CAAc,KAAK,CAAA;AACtC,IAAA,IAAA,CAAK,EAAA,CAAG,MAAM,OAAA,GACZ,gKAAA;AACF,IAAA,MAAA,CAAO,WAAA,CAAY,KAAK,EAAE,CAAA;AAAA,EAC5B;AAAA,EAEA,QAAQ,IAAA,EAAoB;AAC1B,IAAA,IAAA,CAAK,IAAA,GAAO,SAAS,IAAI,CAAA;AAAA,EAC3B;AAAA,EAEA,OAAO,WAAA,EAA2B;AAChC,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA,KAAM,WAAA,IAAe,CAAA,CAAE,KAAA,IAAS,WAAA,IAAe,CAAA,CAAE,GAAG,CAAA;AACnF,IAAA,IAAA,CAAK,EAAA,CAAG,WAAA,GAAc,MAAA,EAAQ,IAAA,IAAQ,EAAA;AAAA,EACxC;AAAA;AAAA,EAGA,QAAQ,IAAA,EAAoB;AAE1B,IAAA,IAAI,IAAA,CAAK,EAAA,CAAG,WAAA,KAAgB,IAAA,EAAM;AAChC,MAAA,IAAA,CAAK,GAAG,WAAA,GAAc,IAAA;AAAA,IACxB;AAAA,EACF;AAAA,EAEA,OAAA,GAAgB;AACd,IAAA,IAAA,CAAK,GAAG,MAAA,EAAO;AACf,IAAA,IAAA,CAAK,OAAO,EAAC;AAAA,EACf;AACF;AAEA,SAAS,SAAS,IAAA,EAAqB;AACrC,EAAA,MAAM,OAAc,EAAC;AACrB,EAAA,MAAM,SAAS,IAAA,CAAK,OAAA,CAAQ,SAAS,IAAI,CAAA,CAAE,MAAM,QAAQ,CAAA;AACzD,EAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAC1B,IAAA,MAAM,QAAQ,KAAA,CAAM,KAAA,CAAM,IAAI,CAAA,CAAE,OAAO,OAAO,CAAA;AAC9C,IAAA,IAAI,MAAM,MAAA,KAAW,CAAA,IAAK,KAAA,CAAM,CAAC,MAAM,QAAA,EAAU;AACjD,IAAA,MAAM,SAAA,GAAY,MAAM,SAAA,CAAU,CAAC,MAAM,CAAA,CAAE,QAAA,CAAS,KAAK,CAAC,CAAA;AAC1D,IAAA,IAAI,YAAY,CAAA,EAAG;AACnB,IAAA,MAAM,IAAI,2EAAA,CAA4E,IAAA;AAAA,MACpF,MAAM,SAAS;AAAA,KACjB;AACA,IAAA,IAAI,CAAC,CAAA,EAAG;AACR,IAAA,MAAM,IAAI,CAAC,CAAA,EAAW,IAAY,CAAA,EAAW,EAAA,KAC3C,OAAO,CAAC,CAAA,GAAI,OAAO,MAAA,CAAO,EAAE,IAAI,EAAA,GAAK,MAAA,CAAO,CAAC,CAAA,GAAI,MAAA,CAAO,EAAE,CAAA,GAAI,GAAA;AAChE,IAAA,IAAA,CAAK,IAAA,CAAK;AAAA,MACR,KAAA,EAAO,CAAA,CAAE,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAC,CAAA;AAAA,MAC/B,GAAA,EAAK,CAAA,CAAE,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAC,CAAA;AAAA,MAC7B,MAAM,KAAA,CAAM,KAAA,CAAM,YAAY,CAAC,CAAA,CAAE,KAAK,IAAI;AAAA,KAC3C,CAAA;AAAA,EACH;AACA,EAAA,OAAO,IAAA;AACT;;;AC7CA,eAAsB,gBAAA,CACpB,MACA,SAAA,EAC8B;AAC9B,EAAA,MAAM,QAAA,GAAW,IAAA,CAAK,IAAA,CAAK,OAAA,CAAQ,YAAY,EAAE,CAAA;AACjD,EAAA,MAAM,QAA6B,EAAC;AAGpC,EAAA,WAAA,MAAiB,CAAC,IAAA,EAAM,MAAM,CAAA,IAAM,SAAA,EAAoE;AACtG,IAAA,IAAI,MAAA,CAAO,SAAS,MAAA,EAAQ;AAC5B,IAAA,IAAI,CAAC,IAAA,CAAK,UAAA,CAAW,QAAQ,CAAA,EAAG;AAChC,IAAA,MAAM,KAAA,GAAQ,KAAK,WAAA,EAAY;AAC/B,IAAA,IAAI,MAAA,GAA+B,IAAA;AACnC,IAAA,IAAI,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,EAAG,MAAA,GAAS,KAAA;AAAA,SAAA,IAC5B,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,EAAG,MAAA,GAAS,KAAA;AAC1C,IAAA,IAAI,CAAC,MAAA,EAAQ;AAEb,IAAA,MAAM,WAAA,GAAc,MAAO,MAAA,CAAgC,OAAA,EAAQ;AACnE,IAAA,MAAM,GAAA,GAAM,GAAA,CAAI,eAAA,CAAgB,WAAW,CAAA;AAG3C,IAAA,MAAM,YAAY,IAAA,CAAK,KAAA,CAAM,SAAS,MAAM,CAAA,CAAE,MAAM,gCAAgC,CAAA;AACpF,IAAA,KAAA,CAAM,IAAA,CAAK;AAAA,MACT,GAAA;AAAA,MACA,MAAA;AAAA,MACA,QAAA,EAAU,YAAY,CAAC;AAAA,KACxB,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,KAAA;AACT;AAOO,IAAM,sBAAN,MAA0B;AAAA,EACvB,IAAA,uBAAW,GAAA,EAAY;AAAA;AAAA,EAG/B,MAAM,GAAA,EAAmB;AACvB,IAAA,IAAA,CAAK,IAAA,CAAK,IAAI,GAAG,CAAA;AAAA,EACnB;AAAA;AAAA,EAGA,gBAAgB,IAAA,EAAoB;AAClC,IAAA,MAAM,GAAA,GAAM,GAAA,CAAI,eAAA,CAAgB,IAAI,CAAA;AACpC,IAAA,IAAA,CAAK,IAAA,CAAK,IAAI,GAAG,CAAA;AACjB,IAAA,OAAO,GAAA;AAAA,EACT;AAAA;AAAA,EAGA,SAAA,GAAkB;AAChB,IAAA,KAAA,MAAW,CAAA,IAAK,IAAA,CAAK,IAAA,EAAM,GAAA,CAAI,gBAAgB,CAAC,CAAA;AAChD,IAAA,IAAA,CAAK,KAAK,KAAA,EAAM;AAAA,EAClB;AACF;AAeA,eAAsB,oBAAA,CACpB,KAAA,EACA,MAAA,EACA,GAAA,EACA,SACA,SAAA,EACe;AACf,EAAA,MAAM,OAAA,GAAUA,4BAAU,SAAS,CAAA;AAGnC,EAAA,KAAA,MAAW,KAAK,KAAA,CAAM,IAAA,CAAK,MAAM,gBAAA,CAAiB,sBAAsB,CAAC,CAAA,EAAG;AAC1E,IAAA,CAAA,CAAE,MAAA,EAAO;AAAA,EACX;AAEA,EAAA,KAAA,MAAW,KAAK,MAAA,EAAQ;AACtB,IAAA,IAAI,CAAC,EAAE,UAAA,EAAY;AAEnB,IAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,mCAAA,EAAsC,CAAA,CAAE,EAAE,WAAW,CAAA,CAAE,MAAM,CAAA,KAAA,EAAQ,CAAA,CAAE,UAAA,CAAW,KAAA,CAAM,CAAA,EAAG,EAAE,CAAC,CAAA,CAAE,CAAA;AAC5G,IAAA,IAAI;AACF,MAAA,IAAI,MAAM,CAAA,CAAE,UAAA;AACZ,MAAA,IAAI,CAAA,CAAE,WAAW,KAAA,EAAO;AACtB,QAAA,MAAM,MAAM,MAAM,OAAA,CAAQ,CAAA,CAAE,UAAA,EAAY,WAAW,WAAW,CAAA;AAC9D,QAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAC5B,QAAA,MAAM,GAAA,GAAM,SAAS,IAAI,CAAA;AACzB,QAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK,CAAC,GAAG,CAAA,EAAG,EAAE,IAAA,EAAM,UAAA,EAAY,CAAA;AACjD,QAAA,GAAA,GAAM,MAAM,GAAA,CAAI,eAAA,CAAgB,IAAI,CAAA,GAAI,GAAA,CAAI,gBAAgB,IAAI,CAAA;AAAA,MAClE,CAAA,MAAA,IAAW,CAAA,CAAE,MAAA,KAAW,KAAA,EAAO;AAE7B,QAAA,MAAM,MAAM,MAAM,OAAA,CAAQ,CAAA,CAAE,UAAA,EAAY,WAAW,WAAW,CAAA;AAC9D,QAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAC5B,QAAA,IAAI,CAAC,KAAA,CAAM,IAAI,CAAA,EAAG;AAEhB,UAAA,OAAA,CAAQ,IAAA,CAAK,4CAAA,EAA8C,CAAA,CAAE,UAAU,CAAA;AAAA,QACzE;AAAA,MACF;AACA,MAAA,MAAM,OAAA,GAAU,QAAA,CAAS,aAAA,CAAc,OAAO,CAAA;AAC9C,MAAA,OAAA,CAAQ,IAAA,GAAO,WAAA;AACf,MAAA,OAAA,CAAQ,GAAA,GAAM,GAAA;AACd,MAAA,OAAA,CAAQ,OAAA,GAAU,EAAE,QAAA,IAAY,KAAA;AAChC,MAAA,OAAA,CAAQ,KAAA,GAAQ,CAAA,CAAE,QAAA,IAAY,CAAA,SAAA,EAAY,EAAE,EAAE,CAAA,CAAA;AAC9C,MAAA,OAAA,CAAQ,QAAQ,QAAA,GAAW,MAAA;AAC3B,MAAA,KAAA,CAAM,YAAY,OAAO,CAAA;AAAA,IAC3B,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,CAAA,GAAI,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAC5D,MAAA,OAAA,GAAU,GAAG,CAAC,CAAA;AAAA,IAChB;AAAA,EACF;AACF","file":"chunk-WRKO6Q42.cjs","sourcesContent":["/**\n * SRT → WebVTT converter.\n *\n * SRT cues:\n *\n * 1\n * 00:00:20,000 --> 00:00:24,400\n * Subtitle text, possibly multiple lines.\n *\n * WebVTT cues:\n *\n * WEBVTT\n *\n * 00:00:20.000 --> 00:00:24.400\n * Subtitle text, possibly multiple lines.\n *\n * The differences in v1 are:\n * - leading `WEBVTT` magic line\n * - `,` → `.` for milliseconds\n * - cue index lines are stripped (WebVTT allows them but SRT-style ints can\n * confuse some parsers; we drop them)\n * - BOM is stripped\n */\nexport function srtToVtt(srt: string): string {\n // Strip BOM\n if (srt.charCodeAt(0) === 0xfeff) srt = srt.slice(1);\n // Normalize line endings\n const normalized = srt.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\").trim();\n\n const blocks = normalized.split(/\\n{2,}/);\n const out: string[] = [\"WEBVTT\", \"\"];\n\n for (const block of blocks) {\n const lines = block.split(\"\\n\");\n // Drop the leading numeric index, if present.\n if (lines.length > 0 && /^\\d+$/.test(lines[0].trim())) {\n lines.shift();\n }\n if (lines.length === 0) continue;\n\n const timing = lines.shift()!;\n const vttTiming = convertTiming(timing);\n if (!vttTiming) continue; // skip malformed cue\n\n out.push(vttTiming);\n for (const l of lines) out.push(l);\n out.push(\"\");\n }\n\n return out.join(\"\\n\");\n}\n\nfunction convertTiming(line: string): string | null {\n // SRT: HH:MM:SS,mmm --> HH:MM:SS,mmm (optional cue settings after)\n const m = /^(\\d{1,2}):(\\d{2}):(\\d{2})[,.](\\d{1,3})\\s*-->\\s*(\\d{1,2}):(\\d{2}):(\\d{2})[,.](\\d{1,3})(.*)$/.exec(\n line.trim(),\n );\n if (!m) return null;\n const fmt = (h: string, mm: string, s: string, ms: string) =>\n `${h.padStart(2, \"0\")}:${mm}:${s}.${ms.padEnd(3, \"0\").slice(0, 3)}`;\n return `${fmt(m[1], m[2], m[3], m[4])} --> ${fmt(m[5], m[6], m[7], m[8])}${m[9] ?? \"\"}`;\n}\n","/** Light validation for incoming VTT — we do not parse cues, just confirm header. */\nexport function isVtt(text: string): boolean {\n const trimmed = text.replace(/^\\ufeff/, \"\").trimStart();\n return trimmed.startsWith(\"WEBVTT\");\n}\n","/**\n * Custom subtitle overlay for the fallback strategy. We don't have a `<video>`\n * with text tracks here, so we render cues into a positioned div ourselves.\n *\n * v1 only handles plain-text WebVTT cues with `HH:MM:SS.mmm` timing. Cue\n * settings, voice tags, and styling are ignored.\n */\n\ninterface Cue {\n start: number;\n end: number;\n text: string;\n}\n\nexport class SubtitleOverlay {\n private el: HTMLDivElement;\n private cues: Cue[] = [];\n\n constructor(parent: HTMLElement) {\n this.el = document.createElement(\"div\");\n this.el.style.cssText =\n \"position:absolute;left:0;right:0;bottom:8%;text-align:center;color:white;text-shadow:0 0 4px black;font-family:sans-serif;font-size:1.4em;pointer-events:none;\";\n parent.appendChild(this.el);\n }\n\n loadVtt(text: string): void {\n this.cues = parseVtt(text);\n }\n\n update(currentTime: number): void {\n const active = this.cues.find((c) => currentTime >= c.start && currentTime <= c.end);\n this.el.textContent = active?.text ?? \"\";\n }\n\n /** Set the currently-displayed text directly (bypasses loadVtt/update). */\n setText(text: string): void {\n // Only touch the DOM if it actually changed — rAF tick runs 60Hz.\n if (this.el.textContent !== text) {\n this.el.textContent = text;\n }\n }\n\n destroy(): void {\n this.el.remove();\n this.cues = [];\n }\n}\n\nfunction parseVtt(text: string): Cue[] {\n const cues: Cue[] = [];\n const blocks = text.replace(/\\r\\n/g, \"\\n\").split(/\\n{2,}/);\n for (const block of blocks) {\n const lines = block.split(\"\\n\").filter(Boolean);\n if (lines.length === 0 || lines[0] === \"WEBVTT\") continue;\n const timingIdx = lines.findIndex((l) => l.includes(\"-->\"));\n if (timingIdx < 0) continue;\n const m = /(\\d{2}):(\\d{2}):(\\d{2})\\.(\\d{3})\\s*-->\\s*(\\d{2}):(\\d{2}):(\\d{2})\\.(\\d{3})/.exec(\n lines[timingIdx],\n );\n if (!m) continue;\n const t = (h: string, mm: string, s: string, ms: string) =>\n Number(h) * 3600 + Number(mm) * 60 + Number(s) + Number(ms) / 1000;\n cues.push({\n start: t(m[1], m[2], m[3], m[4]),\n end: t(m[5], m[6], m[7], m[8]),\n text: lines.slice(timingIdx + 1).join(\"\\n\"),\n });\n }\n return cues;\n}\n","import type { SubtitleTrackInfo, TransportConfig } from \"../types.js\";\nimport { fetchWith } from \"../util/transport.js\";\nimport { srtToVtt } from \"./srt.js\";\nimport { isVtt } from \"./vtt.js\";\n\nexport { srtToVtt } from \"./srt.js\";\nexport { SubtitleOverlay } from \"./render.js\";\n\n/**\n * Discover sidecar `.srt` / `.vtt` files next to the source. Requires the\n * caller to pass a `FileSystemDirectoryHandle` (e.g. via the File System\n * Access API). Without that handle we can't enumerate sibling files.\n *\n * The returned `url` fields are blob URLs created via `URL.createObjectURL`.\n * They must be revoked by the caller (e.g. via `revokeSubtitleResources()`)\n * when the player tears down or the source changes — otherwise repeated\n * source swaps in a single-page app will leak.\n */\nexport interface DiscoveredSidecar {\n url: string;\n format: \"srt\" | \"vtt\";\n language?: string;\n}\n\nexport async function discoverSidecars(\n file: File,\n directory: FileSystemDirectoryHandle,\n): Promise<DiscoveredSidecar[]> {\n const baseName = file.name.replace(/\\.[^.]+$/, \"\");\n const found: DiscoveredSidecar[] = [];\n\n // Walk the directory and look for `${baseName}*.srt` / `*.vtt`.\n for await (const [name, handle] of (directory as unknown as AsyncIterable<[string, FileSystemHandle]>)) {\n if (handle.kind !== \"file\") continue;\n if (!name.startsWith(baseName)) continue;\n const lower = name.toLowerCase();\n let format: \"srt\" | \"vtt\" | null = null;\n if (lower.endsWith(\".srt\")) format = \"srt\";\n else if (lower.endsWith(\".vtt\")) format = \"vtt\";\n if (!format) continue;\n\n const sidecarFile = await (handle as FileSystemFileHandle).getFile();\n const url = URL.createObjectURL(sidecarFile);\n\n // Try to extract a language tag (eg. movie.en.srt → \"en\").\n const langMatch = name.slice(baseName.length).match(/[._-]([a-z]{2,3})(?:[._-]|\\.)/i);\n found.push({\n url,\n format,\n language: langMatch?.[1],\n });\n }\n\n return found;\n}\n\n/**\n * Owns every blob URL created during sidecar discovery and SRT→VTT\n * conversion for a single player session. Revoking the bag releases all of\n * them in one shot at teardown.\n */\nexport class SubtitleResourceBag {\n private urls = new Set<string>();\n\n /** Track an externally-created blob URL (e.g. from `discoverSidecars`). */\n track(url: string): void {\n this.urls.add(url);\n }\n\n /** Convenience: create a blob URL and track it in one call. */\n createObjectURL(blob: Blob): string {\n const url = URL.createObjectURL(blob);\n this.urls.add(url);\n return url;\n }\n\n /** Revoke every tracked URL. Idempotent — safe to call multiple times. */\n revokeAll(): void {\n for (const u of this.urls) URL.revokeObjectURL(u);\n this.urls.clear();\n }\n}\n\n/**\n * Attach `<track>` elements for each subtitle to the player's `<video>`. SRT\n * sources are converted to VTT first via blob URLs because `<track>` only\n * accepts WebVTT.\n *\n * Pass a {@link SubtitleResourceBag} so the player can revoke the generated\n * blob URLs at teardown. Without one, every SRT subtitle leaks a blob URL\n * per attach.\n *\n * Errors during fetch/parse are caught per-track and reported via the\n * `onError` callback (if provided) so a single bad subtitle doesn't break\n * bootstrap. Subtitles are *not* load-bearing for playback.\n */\nexport async function attachSubtitleTracks(\n video: HTMLVideoElement,\n tracks: SubtitleTrackInfo[],\n bag?: SubtitleResourceBag,\n onError?: (err: Error, track: SubtitleTrackInfo) => void,\n transport?: TransportConfig,\n): Promise<void> {\n const doFetch = fetchWith(transport);\n\n // Clear existing dynamically-attached tracks.\n for (const t of Array.from(video.querySelectorAll(\"track[data-avbridge]\"))) {\n t.remove();\n }\n\n for (const t of tracks) {\n if (!t.sidecarUrl) continue;\n // eslint-disable-next-line no-console\n console.log(`[avbridge:subs] attaching track id=${t.id} format=${t.format} url=${t.sidecarUrl.slice(0, 60)}`);\n try {\n let url = t.sidecarUrl;\n if (t.format === \"srt\") {\n const res = await doFetch(t.sidecarUrl, transport?.requestInit);\n const text = await res.text();\n const vtt = srtToVtt(text);\n const blob = new Blob([vtt], { type: \"text/vtt\" });\n url = bag ? bag.createObjectURL(blob) : URL.createObjectURL(blob);\n } else if (t.format === \"vtt\") {\n // Validate quickly so a malformed file fails loudly here.\n const res = await doFetch(t.sidecarUrl, transport?.requestInit);\n const text = await res.text();\n if (!isVtt(text)) {\n // eslint-disable-next-line no-console\n console.warn(\"[avbridge] subtitle missing WEBVTT header:\", t.sidecarUrl);\n }\n }\n const trackEl = document.createElement(\"track\");\n trackEl.kind = \"subtitles\";\n trackEl.src = url;\n trackEl.srclang = t.language ?? \"und\";\n trackEl.label = t.language ?? `Subtitle ${t.id}`;\n trackEl.dataset.avbridge = \"true\";\n video.appendChild(trackEl);\n } catch (err) {\n const e = err instanceof Error ? err : new Error(String(err));\n onError?.(e, t);\n }\n }\n}\n"]}
@@ -31144,6 +31144,7 @@ async function attachSubtitleTracks(video, tracks, bag, onError, transport) {
31144
31144
  }
31145
31145
  for (const t of tracks) {
31146
31146
  if (!t.sidecarUrl) continue;
31147
+ console.log(`[avbridge:subs] attaching track id=${t.id} format=${t.format} url=${t.sidecarUrl.slice(0, 60)}`);
31147
31148
  try {
31148
31149
  let url2 = t.sidecarUrl;
31149
31150
  if (t.format === "srt") {
@@ -32808,6 +32809,10 @@ var AudioOutput = class {
32808
32809
  _volume = 1;
32809
32810
  /** User-set muted flag. When true, gain is forced to 0. */
32810
32811
  _muted = false;
32812
+ /** Playback rate. Scales the media clock and each AudioBufferSourceNode's
32813
+ * playbackRate so audio pitches up/down accordingly (same as native
32814
+ * <video>.playbackRate). Default 1. */
32815
+ _rate = 1;
32811
32816
  constructor() {
32812
32817
  this.ctx = new AudioContext();
32813
32818
  this.gain = this.ctx.createGain();
@@ -32829,6 +32834,20 @@ var AudioOutput = class {
32829
32834
  getMuted() {
32830
32835
  return this._muted;
32831
32836
  }
32837
+ /** Set playback rate. Scales the media clock and pitches audio output
32838
+ * (same as native <video>.playbackRate — speed without pitch correction).
32839
+ * Rebases the anchor so the clock transition is seamless. */
32840
+ setPlaybackRate(rate) {
32841
+ if (rate === this._rate) return;
32842
+ const t = this.now();
32843
+ this.mediaTimeOfAnchor = t;
32844
+ this.ctxTimeAtAnchor = this.ctx.currentTime;
32845
+ this.wallAnchorMs = performance.now();
32846
+ this._rate = rate;
32847
+ }
32848
+ getPlaybackRate() {
32849
+ return this._rate;
32850
+ }
32832
32851
  applyGain() {
32833
32852
  const target = this._muted ? 0 : this._volume;
32834
32853
  try {
@@ -32849,12 +32868,12 @@ var AudioOutput = class {
32849
32868
  now() {
32850
32869
  if (this.noAudio) {
32851
32870
  if (this.state === "playing") {
32852
- return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3;
32871
+ return this.mediaTimeOfAnchor + (performance.now() - this.wallAnchorMs) / 1e3 * this._rate;
32853
32872
  }
32854
32873
  return this.mediaTimeOfAnchor;
32855
32874
  }
32856
32875
  if (this.state === "playing") {
32857
- return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor);
32876
+ return this.mediaTimeOfAnchor + (this.ctx.currentTime - this.ctxTimeAtAnchor) * this._rate;
32858
32877
  }
32859
32878
  return this.mediaTimeOfAnchor;
32860
32879
  }
@@ -32906,7 +32925,8 @@ var AudioOutput = class {
32906
32925
  const node3 = this.ctx.createBufferSource();
32907
32926
  node3.buffer = buffer;
32908
32927
  node3.connect(this.gain);
32909
- let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor);
32928
+ if (this._rate !== 1) node3.playbackRate.value = this._rate;
32929
+ let ctxStart = this.ctxTimeAtAnchor + (this.mediaTimeOfNext - this.mediaTimeOfAnchor) / this._rate;
32910
32930
  if (ctxStart < this.ctx.currentTime) {
32911
32931
  this.ctxTimeAtAnchor = this.ctx.currentTime;
32912
32932
  this.mediaTimeOfAnchor = this.mediaTimeOfNext;
@@ -32933,6 +32953,10 @@ var AudioOutput = class {
32933
32953
  if (this.ctx.state === "suspended") {
32934
32954
  await this.ctx.resume();
32935
32955
  }
32956
+ try {
32957
+ this.gain.connect(this.ctx.destination);
32958
+ } catch {
32959
+ }
32936
32960
  if (this.state === "paused") {
32937
32961
  this.ctxTimeAtAnchor = this.ctx.currentTime;
32938
32962
  this.state = "playing";
@@ -32959,6 +32983,10 @@ var AudioOutput = class {
32959
32983
  this.mediaTimeOfAnchor = this.now();
32960
32984
  this.state = "paused";
32961
32985
  if (this.noAudio) return;
32986
+ try {
32987
+ this.gain.disconnect();
32988
+ } catch {
32989
+ }
32962
32990
  if (this.ctx.state === "running") {
32963
32991
  await this.ctx.suspend();
32964
32992
  }
@@ -33738,6 +33766,14 @@ async function createHybridSession(ctx, target, transport) {
33738
33766
  get: () => ctx.duration ?? NaN
33739
33767
  });
33740
33768
  }
33769
+ Object.defineProperty(target, "playbackRate", {
33770
+ configurable: true,
33771
+ get: () => audio.getPlaybackRate(),
33772
+ set: (v) => {
33773
+ audio.setPlaybackRate(v);
33774
+ target.dispatchEvent(new Event("ratechange"));
33775
+ }
33776
+ });
33741
33777
  Object.defineProperty(target, "readyState", {
33742
33778
  configurable: true,
33743
33779
  get: () => {
@@ -33848,6 +33884,7 @@ async function createHybridSession(ctx, target, transport) {
33848
33884
  delete target.muted;
33849
33885
  delete target.readyState;
33850
33886
  delete target.seekable;
33887
+ delete target.playbackRate;
33851
33888
  } catch {
33852
33889
  }
33853
33890
  },
@@ -34386,6 +34423,14 @@ async function createFallbackSession(ctx, target, transport) {
34386
34423
  get: () => ctx.duration ?? NaN
34387
34424
  });
34388
34425
  }
34426
+ Object.defineProperty(target, "playbackRate", {
34427
+ configurable: true,
34428
+ get: () => audio.getPlaybackRate(),
34429
+ set: (v) => {
34430
+ audio.setPlaybackRate(v);
34431
+ target.dispatchEvent(new Event("ratechange"));
34432
+ }
34433
+ });
34389
34434
  Object.defineProperty(target, "readyState", {
34390
34435
  configurable: true,
34391
34436
  get: () => {
@@ -34517,6 +34562,7 @@ async function createFallbackSession(ctx, target, transport) {
34517
34562
  delete target.muted;
34518
34563
  delete target.readyState;
34519
34564
  delete target.seekable;
34565
+ delete target.playbackRate;
34520
34566
  } catch {
34521
34567
  }
34522
34568
  },
@@ -35750,14 +35796,27 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
35750
35796
  sidecarUrl: subtitle.url
35751
35797
  };
35752
35798
  this._subtitleTracks.push(track);
35799
+ console.log(`[avbridge:subs] addSubtitle id=${track.id} format=${format} lang=${subtitle.language ?? "?"}`);
35753
35800
  await attachSubtitleTracks2(
35754
35801
  this._videoEl,
35755
35802
  this._subtitleTracks,
35756
35803
  void 0,
35757
35804
  (err, t) => {
35758
- console.warn(`[avbridge] subtitle ${t.id} failed: ${err.message}`);
35805
+ console.warn(`[avbridge:subs] subtitle ${t.id} failed: ${err.message}`);
35759
35806
  }
35760
35807
  );
35808
+ const textTracks = this._videoEl.textTracks;
35809
+ for (let i = 0; i < textTracks.length; i++) {
35810
+ if (textTracks[i].label === (subtitle.language ?? `Subtitle ${track.id}`)) {
35811
+ textTracks[i].mode = "showing";
35812
+ console.log(`[avbridge:subs] enabled textTrack[${i}] mode=showing`);
35813
+ break;
35814
+ }
35815
+ }
35816
+ this._dispatch("trackschange", {
35817
+ audioTracks: this._audioTracks,
35818
+ subtitleTracks: this.subtitleTracks
35819
+ });
35761
35820
  }
35762
35821
  /**
35763
35822
  * Disable the automatic `screen.orientation.lock()` that runs on