vidply 1.0.27 → 1.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/dev/{vidply.TranscriptManager-GZKY44ON.js → vidply.TranscriptManager-T677KF4N.js} +5 -6
  2. package/dist/dev/vidply.TranscriptManager-T677KF4N.js.map +7 -0
  3. package/dist/dev/{vidply.chunk-UH5MTGKF.js → vidply.chunk-GS2JX5RQ.js} +149 -100
  4. package/dist/dev/vidply.chunk-GS2JX5RQ.js.map +7 -0
  5. package/dist/dev/{vidply.de-THBIMP4S.js → vidply.de-SNL6AJ4D.js} +10 -2
  6. package/dist/dev/{vidply.de-THBIMP4S.js.map → vidply.de-SNL6AJ4D.js.map} +2 -2
  7. package/dist/dev/{vidply.es-6VWDNNNL.js → vidply.es-2QCQKZ4U.js} +10 -2
  8. package/dist/dev/{vidply.es-6VWDNNNL.js.map → vidply.es-2QCQKZ4U.js.map} +2 -2
  9. package/dist/dev/vidply.esm.js +1681 -317
  10. package/dist/dev/vidply.esm.js.map +4 -4
  11. package/dist/dev/{vidply.fr-WHTWCHWT.js → vidply.fr-FJAZRL4L.js} +10 -2
  12. package/dist/dev/{vidply.fr-WHTWCHWT.js.map → vidply.fr-FJAZRL4L.js.map} +2 -2
  13. package/dist/dev/{vidply.ja-BFQNPOFI.js → vidply.ja-2XQOW53T.js} +10 -2
  14. package/dist/dev/vidply.ja-2XQOW53T.js.map +7 -0
  15. package/dist/legacy/vidply.js +1829 -361
  16. package/dist/legacy/vidply.js.map +4 -4
  17. package/dist/legacy/vidply.min.js +1 -1
  18. package/dist/legacy/vidply.min.meta.json +103 -35
  19. package/dist/prod/vidply.TranscriptManager-WFZSW6NR.min.js +6 -0
  20. package/dist/prod/vidply.chunk-LGTJRPUL.min.js +6 -0
  21. package/dist/prod/vidply.de-FR3XX54P.min.js +6 -0
  22. package/dist/prod/vidply.es-3IJCQLJ7.min.js +6 -0
  23. package/dist/prod/vidply.esm.min.js +8 -8
  24. package/dist/prod/vidply.fr-NC4VEAPH.min.js +6 -0
  25. package/dist/prod/vidply.ja-4ZC6ZQLV.min.js +6 -0
  26. package/dist/vidply.esm.min.meta.json +115 -47
  27. package/package.json +1 -1
  28. package/src/controls/ControlBar.js +3 -7
  29. package/src/controls/TranscriptManager.js +8 -8
  30. package/src/core/AudioDescriptionManager.js +701 -0
  31. package/src/core/Player.js +4776 -4921
  32. package/src/core/SignLanguageManager.js +1134 -0
  33. package/src/features/PlaylistManager.js +12 -12
  34. package/src/i18n/languages/de.js +9 -1
  35. package/src/i18n/languages/en.js +9 -1
  36. package/src/i18n/languages/es.js +9 -1
  37. package/src/i18n/languages/fr.js +9 -1
  38. package/src/i18n/languages/ja.js +9 -1
  39. package/src/utils/DOMUtils.js +153 -114
  40. package/src/utils/MenuFactory.js +374 -0
  41. package/dist/dev/vidply.TranscriptManager-GZKY44ON.js.map +0 -7
  42. package/dist/dev/vidply.chunk-UH5MTGKF.js.map +0 -7
  43. package/dist/dev/vidply.ja-BFQNPOFI.js.map +0 -7
  44. package/dist/prod/vidply.TranscriptManager-UZ6DUFB6.min.js +0 -6
  45. package/dist/prod/vidply.chunk-MBUR3U5L.min.js +0 -6
  46. package/dist/prod/vidply.de-SWFW4HYT.min.js +0 -6
  47. package/dist/prod/vidply.es-7BJ2DJAY.min.js +0 -6
  48. package/dist/prod/vidply.fr-DPVR5DFY.min.js +0 -6
  49. package/dist/prod/vidply.ja-PEBVWKVH.min.js +0 -6
@@ -34,15 +34,21 @@
34
34
  var init_DOMUtils = __esm({
35
35
  "src/utils/DOMUtils.js"() {
36
36
  DOMUtils = {
37
+ /**
38
+ * Create an element with options
39
+ * @param {string} tag - HTML tag name
40
+ * @param {Object} options - Element options
41
+ * @returns {HTMLElement}
42
+ */
37
43
  createElement(tag, options = {}) {
38
44
  const element = document.createElement(tag);
39
45
  if (options.className) {
40
46
  element.className = options.className;
41
47
  }
42
48
  if (options.attributes) {
43
- Object.entries(options.attributes).forEach(([key, value]) => {
49
+ for (const [key, value] of Object.entries(options.attributes)) {
44
50
  element.setAttribute(key, value);
45
- });
51
+ }
46
52
  }
47
53
  if (options.innerHTML) {
48
54
  element.innerHTML = options.innerHTML;
@@ -54,150 +60,190 @@
54
60
  Object.assign(element.style, options.style);
55
61
  }
56
62
  if (options.children) {
57
- options.children.forEach((child) => {
63
+ for (const child of options.children) {
58
64
  if (child) element.appendChild(child);
59
- });
65
+ }
60
66
  }
61
67
  return element;
62
68
  },
63
- addClass(element, className) {
64
- if (element && className) {
65
- element.classList.add(className);
66
- }
67
- },
68
- removeClass(element, className) {
69
- if (element && className) {
70
- element.classList.remove(className);
71
- }
72
- },
73
- toggleClass(element, className) {
74
- if (element && className) {
75
- element.classList.toggle(className);
76
- }
77
- },
78
- hasClass(element, className) {
79
- return element && element.classList.contains(className);
80
- },
69
+ /**
70
+ * Show element (remove display:none)
71
+ * @param {HTMLElement} element
72
+ */
81
73
  show(element) {
82
- if (element) {
83
- element.style.display = "";
84
- }
74
+ (element == null ? void 0 : element.style) && (element.style.display = "");
85
75
  },
76
+ /**
77
+ * Hide element
78
+ * @param {HTMLElement} element
79
+ */
86
80
  hide(element) {
87
- if (element) {
88
- element.style.display = "none";
89
- }
81
+ (element == null ? void 0 : element.style) && (element.style.display = "none");
90
82
  },
91
- fadeIn(element, duration = 300) {
83
+ /**
84
+ * Fade in element using CSS transitions (GPU accelerated)
85
+ * @param {HTMLElement} element
86
+ * @param {number} duration - Duration in ms
87
+ * @param {Function} [onComplete] - Callback when complete
88
+ */
89
+ fadeIn(element, duration = 300, onComplete) {
92
90
  if (!element) return;
93
91
  element.style.opacity = "0";
94
92
  element.style.display = "";
95
- let start = null;
96
- const animate = (timestamp) => {
97
- if (!start) start = timestamp;
98
- const progress = timestamp - start;
99
- const opacity = Math.min(progress / duration, 1);
100
- element.style.opacity = opacity;
101
- if (progress < duration) {
102
- requestAnimationFrame(animate);
103
- }
104
- };
105
- requestAnimationFrame(animate);
93
+ element.style.transition = "opacity ".concat(duration, "ms ease");
94
+ element.offsetHeight;
95
+ element.style.opacity = "1";
96
+ if (onComplete) {
97
+ const cleanup = () => {
98
+ element.removeEventListener("transitionend", cleanup);
99
+ onComplete();
100
+ };
101
+ element.addEventListener("transitionend", cleanup, { once: true });
102
+ setTimeout(cleanup, duration + 50);
103
+ }
106
104
  },
107
- fadeOut(element, duration = 300) {
105
+ /**
106
+ * Fade out element using CSS transitions (GPU accelerated)
107
+ * @param {HTMLElement} element
108
+ * @param {number} duration - Duration in ms
109
+ * @param {Function} [onComplete] - Callback when complete
110
+ */
111
+ fadeOut(element, duration = 300, onComplete) {
108
112
  if (!element) return;
109
- const startOpacity = parseFloat(getComputedStyle(element).opacity) || 1;
110
- let start = null;
111
- const animate = (timestamp) => {
112
- if (!start) start = timestamp;
113
- const progress = timestamp - start;
114
- const opacity = Math.max(startOpacity - progress / duration, 0);
115
- element.style.opacity = opacity;
116
- if (progress < duration) {
117
- requestAnimationFrame(animate);
118
- } else {
119
- element.style.display = "none";
120
- }
113
+ element.style.transition = "opacity ".concat(duration, "ms ease");
114
+ element.style.opacity = "0";
115
+ const cleanup = () => {
116
+ element.removeEventListener("transitionend", cleanup);
117
+ element.style.display = "none";
118
+ if (onComplete) onComplete();
121
119
  };
122
- requestAnimationFrame(animate);
120
+ element.addEventListener("transitionend", cleanup, { once: true });
121
+ setTimeout(cleanup, duration + 50);
123
122
  },
123
+ /**
124
+ * Get element's offset position and dimensions
125
+ * @param {HTMLElement} element
126
+ * @returns {Object} { top, left, width, height }
127
+ */
124
128
  offset(element) {
125
- if (!element) return { top: 0, left: 0 };
129
+ if (!element) return { top: 0, left: 0, width: 0, height: 0 };
126
130
  const rect = element.getBoundingClientRect();
127
131
  return {
128
- top: rect.top + window.pageYOffset,
129
- left: rect.left + window.pageXOffset,
132
+ top: rect.top + window.scrollY,
133
+ left: rect.left + window.scrollX,
130
134
  width: rect.width,
131
135
  height: rect.height
132
136
  };
133
137
  },
138
+ /**
139
+ * Escape HTML special characters
140
+ * @param {string} str - String to escape
141
+ * @returns {string} Escaped string
142
+ */
134
143
  escapeHTML(str) {
135
- const div = document.createElement("div");
136
- div.textContent = str;
137
- return div.innerHTML;
144
+ const escapeMap = {
145
+ "&": "&amp;",
146
+ "<": "&lt;",
147
+ ">": "&gt;",
148
+ '"': "&quot;",
149
+ "'": "&#x27;"
150
+ };
151
+ return str.replace(/[&<>"']/g, (char) => escapeMap[char]);
138
152
  },
153
+ /**
154
+ * Basic HTML sanitization for VTT captions
155
+ * Allows safe formatting tags, removes dangerous content
156
+ * @param {string} html - HTML string to sanitize
157
+ * @returns {string} Sanitized HTML
158
+ */
139
159
  sanitizeHTML(html) {
140
- const temp = document.createElement("div");
141
160
  const safeHtml = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, "").replace(/on\w+\s*=/gi, "").replace(/javascript:/gi, "");
161
+ const temp = document.createElement("div");
142
162
  temp.innerHTML = safeHtml;
143
163
  return temp.innerHTML;
144
164
  },
145
165
  /**
146
- * Create a tooltip element that is aria-hidden (not read by screen readers)
166
+ * Create a tooltip element (aria-hidden)
147
167
  * @param {string} text - Tooltip text
148
- * @param {string} classPrefix - Class prefix for styling
149
- * @returns {HTMLElement} Tooltip element
168
+ * @param {string} classPrefix - Class prefix
169
+ * @returns {HTMLElement}
150
170
  */
151
171
  createTooltip(text, classPrefix = "vidply") {
152
- const tooltip = this.createElement("span", {
172
+ return this.createElement("span", {
153
173
  className: "".concat(classPrefix, "-tooltip"),
154
174
  textContent: text,
155
- attributes: {
156
- "aria-hidden": "true"
157
- }
175
+ attributes: { "aria-hidden": "true" }
158
176
  });
159
- return tooltip;
160
177
  },
161
178
  /**
162
- * Attach a tooltip to an element
163
- * @param {HTMLElement} element - Element to attach tooltip to
179
+ * Attach a tooltip to an element with hover/focus behavior
180
+ * @param {HTMLElement} element - Target element
164
181
  * @param {string} text - Tooltip text
165
- * @param {string} classPrefix - Class prefix for styling
182
+ * @param {string} classPrefix - Class prefix
166
183
  */
167
184
  attachTooltip(element, text, classPrefix = "vidply") {
185
+ var _a;
168
186
  if (!element || !text) return;
169
- const existingTooltip = element.querySelector(".".concat(classPrefix, "-tooltip"));
170
- if (existingTooltip) {
171
- existingTooltip.remove();
172
- }
187
+ (_a = element.querySelector(".".concat(classPrefix, "-tooltip"))) == null ? void 0 : _a.remove();
173
188
  const tooltip = this.createTooltip(text, classPrefix);
174
189
  element.appendChild(tooltip);
175
- const showTooltip = () => {
176
- tooltip.classList.add("".concat(classPrefix, "-tooltip-visible"));
177
- };
178
- const hideTooltip = () => {
179
- tooltip.classList.remove("".concat(classPrefix, "-tooltip-visible"));
180
- };
181
- element.addEventListener("mouseenter", showTooltip);
182
- element.addEventListener("mouseleave", hideTooltip);
183
- element.addEventListener("focus", showTooltip);
184
- element.addEventListener("blur", hideTooltip);
190
+ const visibleClass = "".concat(classPrefix, "-tooltip-visible");
191
+ const show = () => tooltip.classList.add(visibleClass);
192
+ const hide = () => tooltip.classList.remove(visibleClass);
193
+ element.addEventListener("mouseenter", show);
194
+ element.addEventListener("mouseleave", hide);
195
+ element.addEventListener("focus", show);
196
+ element.addEventListener("blur", hide);
185
197
  },
186
198
  /**
187
- * Create visible button text that is hidden by CSS but visible when CSS is disabled
199
+ * Create button text element (visible when CSS disabled)
188
200
  * @param {string} text - Button text
189
- * @param {string} classPrefix - Class prefix for styling
190
- * @returns {HTMLElement} Button text element
201
+ * @param {string} classPrefix - Class prefix
202
+ * @returns {HTMLElement}
191
203
  */
192
204
  createButtonText(text, classPrefix = "vidply") {
193
- const buttonText = this.createElement("span", {
205
+ return this.createElement("span", {
194
206
  className: "".concat(classPrefix, "-button-text"),
195
207
  textContent: text,
196
- attributes: {
197
- "aria-hidden": "true"
198
- }
208
+ attributes: { "aria-hidden": "true" }
199
209
  });
200
- return buttonText;
210
+ },
211
+ /**
212
+ * Add class to element (null-safe)
213
+ * @param {HTMLElement} element
214
+ * @param {string} className
215
+ */
216
+ addClass(element, className) {
217
+ var _a;
218
+ (_a = element == null ? void 0 : element.classList) == null ? void 0 : _a.add(className);
219
+ },
220
+ /**
221
+ * Remove class from element (null-safe)
222
+ * @param {HTMLElement} element
223
+ * @param {string} className
224
+ */
225
+ removeClass(element, className) {
226
+ var _a;
227
+ (_a = element == null ? void 0 : element.classList) == null ? void 0 : _a.remove(className);
228
+ },
229
+ /**
230
+ * Toggle class on element (null-safe)
231
+ * @param {HTMLElement} element
232
+ * @param {string} className
233
+ */
234
+ toggleClass(element, className) {
235
+ var _a;
236
+ (_a = element == null ? void 0 : element.classList) == null ? void 0 : _a.toggle(className);
237
+ },
238
+ /**
239
+ * Check if element has class (null-safe)
240
+ * @param {HTMLElement} element
241
+ * @param {string} className
242
+ * @returns {boolean}
243
+ */
244
+ hasClass(element, className) {
245
+ var _a, _b;
246
+ return (_b = (_a = element == null ? void 0 : element.classList) == null ? void 0 : _a.contains(className)) != null ? _b : false;
201
247
  }
202
248
  };
203
249
  }
@@ -307,6 +353,7 @@
307
353
  },
308
354
  transcript: {
309
355
  title: "Transcript",
356
+ ariaLabel: "Video Transcript",
310
357
  close: "Close transcript",
311
358
  loading: "Loading transcript...",
312
359
  noTranscript: "No transcript available for this video.",
@@ -373,7 +420,14 @@
373
420
  currentlyPlaying: "Currently playing",
374
421
  notPlaying: "Not playing",
375
422
  pressEnterPlay: "Press Enter to play",
376
- pressEnterRestart: "Press Enter to restart"
423
+ pressEnterRestart: "Press Enter to restart",
424
+ keyboardInstructions: "Playlist navigation: Use Up and Down arrow keys to move between tracks. Press Page Up or Page Down to skip 5 tracks. Press Home to go to first track, End to go to last track. Press Enter or Space to play the selected track.",
425
+ endOfPlaylist: "End of playlist. {current} of {total}.",
426
+ beginningOfPlaylist: "Beginning of playlist. 1 of {total}.",
427
+ jumpedToLastTrack: "Jumped to last track. {current} of {total}.",
428
+ jumpedToFirstTrack: "Jumped to first track. 1 of {total}.",
429
+ firstTrack: "First track. 1 of {total}.",
430
+ lastTrack: "Last track. {current} of {total}."
377
431
  }
378
432
  };
379
433
  }
@@ -487,6 +541,7 @@
487
541
  },
488
542
  transcript: {
489
543
  title: "Transkript",
544
+ ariaLabel: "Video-Transkript",
490
545
  close: "Transkript schließen",
491
546
  loading: "Transkript wird geladen...",
492
547
  noTranscript: "Kein Transkript für dieses Video verfügbar.",
@@ -553,7 +608,14 @@
553
608
  currentlyPlaying: "Wird gerade abgespielt",
554
609
  notPlaying: "Nicht aktiv",
555
610
  pressEnterPlay: "Eingabetaste zum Abspielen",
556
- pressEnterRestart: "Eingabetaste zum Neustart"
611
+ pressEnterRestart: "Eingabetaste zum Neustart",
612
+ keyboardInstructions: "Wiedergabelisten-Navigation: Verwenden Sie die Pfeiltasten nach oben und unten, um zwischen Titeln zu wechseln. Drücken Sie Bild auf oder Bild ab, um 5 Titel zu überspringen. Drücken Sie Pos1, um zum ersten Titel zu springen, Ende für den letzten Titel. Drücken Sie die Eingabetaste oder Leertaste, um den ausgewählten Titel abzuspielen.",
613
+ endOfPlaylist: "Ende der Wiedergabeliste. {current} von {total}.",
614
+ beginningOfPlaylist: "Anfang der Wiedergabeliste. 1 von {total}.",
615
+ jumpedToLastTrack: "Zum letzten Titel gesprungen. {current} von {total}.",
616
+ jumpedToFirstTrack: "Zum ersten Titel gesprungen. 1 von {total}.",
617
+ firstTrack: "Erster Titel. 1 von {total}.",
618
+ lastTrack: "Letzter Titel. {current} von {total}."
557
619
  }
558
620
  };
559
621
  }
@@ -667,6 +729,7 @@
667
729
  },
668
730
  transcript: {
669
731
  title: "Transcripción",
732
+ ariaLabel: "Transcripción de video",
670
733
  close: "Cerrar transcripción",
671
734
  loading: "Cargando transcripción...",
672
735
  noTranscript: "No hay transcripción disponible para este video.",
@@ -733,7 +796,14 @@
733
796
  currentlyPlaying: "Reproduciendo actualmente",
734
797
  notPlaying: "Sin reproducir",
735
798
  pressEnterPlay: "Pulsa Enter para reproducir",
736
- pressEnterRestart: "Pulsa Enter para reiniciar"
799
+ pressEnterRestart: "Pulsa Enter para reiniciar",
800
+ keyboardInstructions: "Navegación de lista de reproducción: Use las teclas de flecha arriba y abajo para moverse entre pistas. Pulse Retroceder página o Avanzar página para saltar 5 pistas. Pulse Inicio para ir a la primera pista, Fin para la última pista. Pulse Intro o Espacio para reproducir la pista seleccionada.",
801
+ endOfPlaylist: "Fin de la lista de reproducción. {current} de {total}.",
802
+ beginningOfPlaylist: "Inicio de la lista de reproducción. 1 de {total}.",
803
+ jumpedToLastTrack: "Saltó a la última pista. {current} de {total}.",
804
+ jumpedToFirstTrack: "Saltó a la primera pista. 1 de {total}.",
805
+ firstTrack: "Primera pista. 1 de {total}.",
806
+ lastTrack: "Última pista. {current} de {total}."
737
807
  }
738
808
  };
739
809
  }
@@ -847,6 +917,7 @@
847
917
  },
848
918
  transcript: {
849
919
  title: "Transcription",
920
+ ariaLabel: "Transcription vidéo",
850
921
  close: "Fermer la transcription",
851
922
  loading: "Chargement de la transcription...",
852
923
  noTranscript: "Aucune transcription disponible pour cette vidéo.",
@@ -913,7 +984,14 @@
913
984
  currentlyPlaying: "En cours de lecture",
914
985
  notPlaying: "Non en lecture",
915
986
  pressEnterPlay: "Appuyez sur Entrée pour lire",
916
- pressEnterRestart: "Appuyez sur Entrée pour recommencer"
987
+ pressEnterRestart: "Appuyez sur Entrée pour recommencer",
988
+ keyboardInstructions: "Navigation de la liste de lecture : Utilisez les touches fléchées haut et bas pour naviguer entre les pistes. Appuyez sur Page précédente ou Page suivante pour sauter 5 pistes. Appuyez sur Début pour aller à la première piste, Fin pour la dernière piste. Appuyez sur Entrée ou Espace pour lire la piste sélectionnée.",
989
+ endOfPlaylist: "Fin de la liste de lecture. {current} sur {total}.",
990
+ beginningOfPlaylist: "Début de la liste de lecture. 1 sur {total}.",
991
+ jumpedToLastTrack: "Sauté à la dernière piste. {current} sur {total}.",
992
+ jumpedToFirstTrack: "Sauté à la première piste. 1 sur {total}.",
993
+ firstTrack: "Première piste. 1 sur {total}.",
994
+ lastTrack: "Dernière piste. {current} sur {total}."
917
995
  }
918
996
  };
919
997
  }
@@ -1027,6 +1105,7 @@
1027
1105
  },
1028
1106
  transcript: {
1029
1107
  title: "文字起こし",
1108
+ ariaLabel: "ビデオ文字起こし",
1030
1109
  close: "文字起こしを閉じる",
1031
1110
  loading: "文字起こしを読み込み中...",
1032
1111
  noTranscript: "このビデオの文字起こしはありません。",
@@ -1093,7 +1172,14 @@
1093
1172
  currentlyPlaying: "再生中",
1094
1173
  notPlaying: "停止中",
1095
1174
  pressEnterPlay: "Enterキーで再生",
1096
- pressEnterRestart: "Enterキーで最初から再生"
1175
+ pressEnterRestart: "Enterキーで最初から再生",
1176
+ keyboardInstructions: "プレイリストナビゲーション:上下の矢印キーでトラック間を移動します。Page UpまたはPage Downで5トラックをスキップします。Homeで最初のトラックへ、Endで最後のトラックへ移動します。EnterまたはSpaceで選択したトラックを再生します。",
1177
+ endOfPlaylist: "プレイリストの終わりです。{current}/{total}。",
1178
+ beginningOfPlaylist: "プレイリストの始めです。1/{total}。",
1179
+ jumpedToLastTrack: "最後のトラックにジャンプしました。{current}/{total}。",
1180
+ jumpedToFirstTrack: "最初のトラックにジャンプしました。1/{total}。",
1181
+ firstTrack: "最初のトラックです。1/{total}。",
1182
+ lastTrack: "最後のトラックです。{current}/{total}。"
1097
1183
  }
1098
1184
  };
1099
1185
  }
@@ -2824,7 +2910,7 @@
2824
2910
  className: "".concat(this.player.options.classPrefix, "-transcript-window"),
2825
2911
  attributes: {
2826
2912
  "role": "dialog",
2827
- "aria-label": "Video Transcript",
2913
+ "aria-label": i18n.t("transcript.ariaLabel"),
2828
2914
  "tabindex": "-1"
2829
2915
  }
2830
2916
  });
@@ -3228,8 +3314,7 @@
3228
3314
  descriptionTrack = textTracks.find((track) => track.kind === "descriptions");
3229
3315
  }
3230
3316
  const metadataTrack = textTracks.find((track) => track.kind === "metadata");
3231
- const hasDescriptionTrack = descriptionTrack && this.player.state.audioDescriptionEnabled;
3232
- if (!captionTrack && !hasDescriptionTrack && !metadataTrack) {
3317
+ if (!captionTrack && !descriptionTrack && !metadataTrack) {
3233
3318
  this.showNoTranscriptMessage();
3234
3319
  return;
3235
3320
  }
@@ -3267,7 +3352,7 @@
3267
3352
  allCues.push({ cue, type: "caption" });
3268
3353
  });
3269
3354
  }
3270
- if (descriptionTrack && descriptionTrack.cues && this.player.state.audioDescriptionEnabled) {
3355
+ if (descriptionTrack && descriptionTrack.cues) {
3271
3356
  Array.from(descriptionTrack.cues).forEach((cue) => {
3272
3357
  allCues.push({ cue, type: "description" });
3273
3358
  });
@@ -5461,6 +5546,35 @@
5461
5546
  init_Icons();
5462
5547
  init_i18n();
5463
5548
  init_FocusUtils();
5549
+
5550
+ // src/utils/PerformanceUtils.js
5551
+ function debounce(func, wait = 100) {
5552
+ let timeout;
5553
+ return function executedFunction(...args) {
5554
+ const later = () => {
5555
+ clearTimeout(timeout);
5556
+ func(...args);
5557
+ };
5558
+ clearTimeout(timeout);
5559
+ timeout = setTimeout(later, wait);
5560
+ };
5561
+ }
5562
+ function isMobile(breakpoint = 768) {
5563
+ return window.innerWidth < breakpoint;
5564
+ }
5565
+ function rafWithTimeout(callback, timeout = 100) {
5566
+ let called = false;
5567
+ const execute = () => {
5568
+ if (!called) {
5569
+ called = true;
5570
+ callback();
5571
+ }
5572
+ };
5573
+ requestAnimationFrame(execute);
5574
+ setTimeout(execute, timeout);
5575
+ }
5576
+
5577
+ // src/controls/ControlBar.js
5464
5578
  var ControlBar = class {
5465
5579
  constructor(player) {
5466
5580
  this.player = player;
@@ -5480,17 +5594,13 @@
5480
5594
  this.setupAutoHide();
5481
5595
  this.setupOverflowDetection();
5482
5596
  }
5483
- // Helper method to check if we're on a mobile device
5484
- isMobile() {
5485
- return window.innerWidth < 768;
5486
- }
5487
5597
  // Helper method to detect touch devices
5488
5598
  isTouchDevice() {
5489
5599
  return "ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
5490
5600
  }
5491
5601
  // Smart menu positioning to avoid overflow
5492
5602
  positionMenu(menu, button, immediate = false) {
5493
- const isMobile2 = this.isMobile();
5603
+ const mobile = isMobile();
5494
5604
  const isOverflowMenu = menu.classList.contains("".concat(this.player.options.classPrefix, "-overflow-menu-list"));
5495
5605
  const isFullscreen = this.player.state.fullscreen;
5496
5606
  if (isFullscreen && menu.parentElement === this.player.container) {
@@ -5531,7 +5641,7 @@
5531
5641
  }
5532
5642
  return;
5533
5643
  }
5534
- if (isMobile2) {
5644
+ if (mobile) {
5535
5645
  const isVolumeMenu = menu.classList.contains("".concat(this.player.options.classPrefix, "-volume-menu"));
5536
5646
  const doMobilePositioning = () => {
5537
5647
  const parentContainer = button.parentElement;
@@ -7904,35 +8014,6 @@
7904
8014
  init_DOMUtils();
7905
8015
  init_i18n();
7906
8016
  init_StorageManager();
7907
-
7908
- // src/utils/PerformanceUtils.js
7909
- function debounce(func, wait = 100) {
7910
- let timeout;
7911
- return function executedFunction(...args) {
7912
- const later = () => {
7913
- clearTimeout(timeout);
7914
- func(...args);
7915
- };
7916
- clearTimeout(timeout);
7917
- timeout = setTimeout(later, wait);
7918
- };
7919
- }
7920
- function isMobile(breakpoint = 768) {
7921
- return window.innerWidth < breakpoint;
7922
- }
7923
- function rafWithTimeout(callback, timeout = 100) {
7924
- let called = false;
7925
- const execute = () => {
7926
- if (!called) {
7927
- called = true;
7928
- callback();
7929
- }
7930
- };
7931
- requestAnimationFrame(execute);
7932
- setTimeout(execute, timeout);
7933
- }
7934
-
7935
- // src/controls/CaptionManager.js
7936
8017
  var CaptionManager = class {
7937
8018
  constructor(player) {
7938
8019
  this.player = player;
@@ -8464,37 +8545,1511 @@
8464
8545
  init_DraggableResizable();
8465
8546
  init_MenuUtils();
8466
8547
  init_FormUtils();
8467
- var playerInstanceCounter = 0;
8468
- var Player = class _Player extends EventEmitter {
8469
- constructor(element, options = {}) {
8470
- super();
8471
- this.element = typeof element === "string" ? document.querySelector(element) : element;
8472
- if (!this.element) {
8473
- throw new Error("VidPly: Element not found");
8548
+
8549
+ // src/core/AudioDescriptionManager.js
8550
+ var AudioDescriptionManager = class {
8551
+ constructor(player) {
8552
+ this.player = player;
8553
+ this.enabled = false;
8554
+ this.desiredState = false;
8555
+ this.src = player.options.audioDescriptionSrc;
8556
+ this.sourceElement = null;
8557
+ this.originalSource = null;
8558
+ this.captionTracks = [];
8559
+ }
8560
+ /**
8561
+ * Initialize audio description from source elements
8562
+ * Called during player initialization
8563
+ */
8564
+ initFromSourceElements(sourceElements, trackElements) {
8565
+ for (const sourceEl of sourceElements) {
8566
+ const descSrc = sourceEl.getAttribute("data-desc-src");
8567
+ const origSrc = sourceEl.getAttribute("data-orig-src");
8568
+ if (descSrc || origSrc) {
8569
+ if (!this.sourceElement) {
8570
+ this.sourceElement = sourceEl;
8571
+ }
8572
+ if (origSrc) {
8573
+ if (!this.originalSource) {
8574
+ this.originalSource = origSrc;
8575
+ }
8576
+ if (!this.player.originalSrc) {
8577
+ this.player.originalSrc = origSrc;
8578
+ }
8579
+ } else {
8580
+ const currentSrcAttr = sourceEl.getAttribute("src");
8581
+ if (!this.originalSource && currentSrcAttr) {
8582
+ this.originalSource = currentSrcAttr;
8583
+ }
8584
+ if (!this.player.originalSrc && currentSrcAttr) {
8585
+ this.player.originalSrc = currentSrcAttr;
8586
+ }
8587
+ }
8588
+ if (descSrc && !this.src) {
8589
+ this.src = descSrc;
8590
+ }
8591
+ }
8474
8592
  }
8475
- playerInstanceCounter++;
8476
- this.instanceId = playerInstanceCounter;
8477
- if (this.element.tagName !== "VIDEO" && this.element.tagName !== "AUDIO") {
8478
- const mediaType = options.mediaType || "video";
8479
- const mediaElement = document.createElement(mediaType);
8480
- Array.from(this.element.attributes).forEach((attr) => {
8481
- if (attr.name !== "id" && attr.name !== "class" && !attr.name.startsWith("data-")) {
8482
- mediaElement.setAttribute(attr.name, attr.value);
8593
+ trackElements.forEach((trackEl) => {
8594
+ const trackKind = trackEl.getAttribute("kind");
8595
+ const trackDescSrc = trackEl.getAttribute("data-desc-src");
8596
+ if ((trackKind === "captions" || trackKind === "subtitles" || trackKind === "chapters" || trackKind === "descriptions") && trackDescSrc) {
8597
+ this.captionTracks.push({
8598
+ trackElement: trackEl,
8599
+ originalSrc: trackEl.getAttribute("src"),
8600
+ describedSrc: trackDescSrc,
8601
+ originalTrackSrc: trackEl.getAttribute("data-orig-src") || trackEl.getAttribute("src"),
8602
+ explicit: true
8603
+ });
8604
+ this.player.log("Found explicit described ".concat(trackKind, " track: ").concat(trackEl.getAttribute("src"), " -> ").concat(trackDescSrc));
8605
+ }
8606
+ });
8607
+ }
8608
+ /**
8609
+ * Check if audio description is available
8610
+ */
8611
+ isAvailable() {
8612
+ const hasSourceElementsWithDesc = this.player.sourceElements.some(
8613
+ (el) => el.getAttribute("data-desc-src")
8614
+ );
8615
+ return !!(this.src || hasSourceElementsWithDesc || this.captionTracks.length > 0);
8616
+ }
8617
+ /**
8618
+ * Enable audio description
8619
+ */
8620
+ async enable() {
8621
+ const hasSourceElementsWithDesc = this.player.sourceElements.some(
8622
+ (el) => el.getAttribute("data-desc-src")
8623
+ );
8624
+ const hasTracksWithDesc = this.captionTracks.length > 0;
8625
+ if (!this.src && !hasSourceElementsWithDesc && !hasTracksWithDesc) {
8626
+ console.warn("VidPly: No audio description source, source elements, or tracks provided");
8627
+ return;
8628
+ }
8629
+ this.desiredState = true;
8630
+ const currentTime = this.player.state.currentTime;
8631
+ const wasPlaying = this.player.state.playing;
8632
+ const posterValue = this.player.element.poster || this.player.element.getAttribute("poster") || this.player.options.poster;
8633
+ const shouldKeepPoster = currentTime < 0.1 && !wasPlaying;
8634
+ const currentCaptionText = this._getCurrentCaptionText();
8635
+ if (this.sourceElement) {
8636
+ await this._enableWithSourceElement(currentTime, wasPlaying, posterValue, shouldKeepPoster, currentCaptionText);
8637
+ } else {
8638
+ await this._enableWithDirectSrc(currentTime, wasPlaying, posterValue, shouldKeepPoster);
8639
+ }
8640
+ }
8641
+ /**
8642
+ * Disable audio description
8643
+ */
8644
+ async disable() {
8645
+ if (!this.player.originalSrc) {
8646
+ return;
8647
+ }
8648
+ this.desiredState = false;
8649
+ const currentTime = this.player.state.currentTime;
8650
+ const wasPlaying = this.player.state.playing;
8651
+ const posterValue = this.player.element.poster || this.player.element.getAttribute("poster") || this.player.options.poster;
8652
+ const shouldKeepPoster = currentTime < 0.1 && !wasPlaying;
8653
+ const currentCaptionText = this._getCurrentCaptionText();
8654
+ if (this.sourceElement) {
8655
+ await this._disableWithSourceElement(currentTime, wasPlaying, posterValue, shouldKeepPoster, currentCaptionText);
8656
+ } else {
8657
+ await this._disableWithDirectSrc(currentTime, wasPlaying, posterValue);
8658
+ }
8659
+ }
8660
+ /**
8661
+ * Toggle audio description
8662
+ */
8663
+ async toggle() {
8664
+ const descriptionTrack = this.player.findTextTrack("descriptions");
8665
+ const hasAudioDescriptionSrc = this.isAvailable();
8666
+ if (descriptionTrack && !hasAudioDescriptionSrc) {
8667
+ if (descriptionTrack.mode === "showing") {
8668
+ descriptionTrack.mode = "hidden";
8669
+ this.enabled = false;
8670
+ this.player.emit("audiodescriptiondisabled");
8671
+ } else {
8672
+ descriptionTrack.mode = "showing";
8673
+ this.enabled = true;
8674
+ this.player.emit("audiodescriptionenabled");
8675
+ }
8676
+ } else if (descriptionTrack && hasAudioDescriptionSrc) {
8677
+ if (this.enabled) {
8678
+ this.desiredState = false;
8679
+ await this.disable();
8680
+ } else {
8681
+ descriptionTrack.mode = "showing";
8682
+ this.desiredState = true;
8683
+ await this.enable();
8684
+ }
8685
+ } else if (hasAudioDescriptionSrc) {
8686
+ if (this.enabled) {
8687
+ this.desiredState = false;
8688
+ await this.disable();
8689
+ } else {
8690
+ this.desiredState = true;
8691
+ await this.enable();
8692
+ }
8693
+ }
8694
+ }
8695
+ /**
8696
+ * Get current caption text for synchronization
8697
+ */
8698
+ _getCurrentCaptionText() {
8699
+ if (this.player.captionManager && this.player.captionManager.currentTrack && this.player.captionManager.currentCue) {
8700
+ return this.player.captionManager.currentCue.text;
8701
+ }
8702
+ return null;
8703
+ }
8704
+ /**
8705
+ * Validate that a track URL exists
8706
+ */
8707
+ async _validateTrackExists(url) {
8708
+ try {
8709
+ const response = await fetch(url, { method: "HEAD" });
8710
+ return response.ok;
8711
+ } catch (e) {
8712
+ return false;
8713
+ }
8714
+ }
8715
+ /**
8716
+ * Swap caption tracks to described versions
8717
+ */
8718
+ async _swapCaptionTracks(toDescribed = true) {
8719
+ if (this.captionTracks.length === 0) return [];
8720
+ const swappedTracks = [];
8721
+ const validationPromises = this.captionTracks.map(async (trackInfo) => {
8722
+ if (trackInfo.trackElement && trackInfo.describedSrc) {
8723
+ if (trackInfo.explicit === true) {
8724
+ try {
8725
+ const exists = await this._validateTrackExists(
8726
+ toDescribed ? trackInfo.describedSrc : trackInfo.originalSrc
8727
+ );
8728
+ return { trackInfo, exists };
8729
+ } catch (e) {
8730
+ return { trackInfo, exists: false };
8731
+ }
8732
+ }
8733
+ }
8734
+ return { trackInfo, exists: false };
8735
+ });
8736
+ const validationResults = await Promise.all(validationPromises);
8737
+ const tracksToSwap = validationResults.filter((result) => result.exists);
8738
+ if (tracksToSwap.length > 0) {
8739
+ const trackModes = /* @__PURE__ */ new Map();
8740
+ tracksToSwap.forEach(({ trackInfo }) => {
8741
+ const textTrack = trackInfo.trackElement.track;
8742
+ if (textTrack) {
8743
+ trackModes.set(trackInfo, {
8744
+ wasShowing: textTrack.mode === "showing",
8745
+ wasHidden: textTrack.mode === "hidden"
8746
+ });
8747
+ } else {
8748
+ trackModes.set(trackInfo, { wasShowing: false, wasHidden: false });
8483
8749
  }
8484
8750
  });
8485
- const tracks = this.element.querySelectorAll("track");
8486
- tracks.forEach((track) => {
8487
- mediaElement.appendChild(track.cloneNode(true));
8751
+ const tracksToReadd = tracksToSwap.map(({ trackInfo }) => {
8752
+ const attributes = {};
8753
+ Array.from(trackInfo.trackElement.attributes).forEach((attr) => {
8754
+ attributes[attr.name] = attr.value;
8755
+ });
8756
+ const result = {
8757
+ trackInfo,
8758
+ oldSrc: trackInfo.trackElement.getAttribute("src"),
8759
+ parent: trackInfo.trackElement.parentNode,
8760
+ nextSibling: trackInfo.trackElement.nextSibling,
8761
+ attributes
8762
+ };
8763
+ trackInfo.trackElement.remove();
8764
+ return result;
8488
8765
  });
8489
- this.element.innerHTML = "";
8490
- this.element.appendChild(mediaElement);
8491
- this.element = mediaElement;
8492
- }
8493
- this._originalElement = this.element;
8494
- this.options = __spreadValues({
8495
- // Display
8496
- width: null,
8497
- height: null,
8766
+ this.player.element.load();
8767
+ await new Promise((resolve) => {
8768
+ setTimeout(() => {
8769
+ tracksToReadd.forEach(({ trackInfo, parent, nextSibling, attributes }) => {
8770
+ swappedTracks.push(trackInfo);
8771
+ const newTrackElement = document.createElement("track");
8772
+ const newSrc = toDescribed ? trackInfo.describedSrc : trackInfo.originalSrc;
8773
+ newTrackElement.setAttribute("src", newSrc);
8774
+ Object.keys(attributes).forEach((attrName) => {
8775
+ if (attrName !== "src" && attrName !== "data-desc-src") {
8776
+ newTrackElement.setAttribute(attrName, attributes[attrName]);
8777
+ }
8778
+ });
8779
+ const targetParent = parent || this.player.element;
8780
+ if (nextSibling && nextSibling.parentNode) {
8781
+ targetParent.insertBefore(newTrackElement, nextSibling);
8782
+ } else {
8783
+ targetParent.appendChild(newTrackElement);
8784
+ }
8785
+ trackInfo.trackElement = newTrackElement;
8786
+ });
8787
+ this.player.invalidateTrackCache();
8788
+ const setupNewTracks = () => {
8789
+ this.player.setManagedTimeout(() => {
8790
+ swappedTracks.forEach((trackInfo) => {
8791
+ const newTextTrack = trackInfo.trackElement.track;
8792
+ if (newTextTrack) {
8793
+ const modeInfo = trackModes.get(trackInfo) || { wasShowing: false, wasHidden: false };
8794
+ newTextTrack.mode = "hidden";
8795
+ const restoreMode = () => {
8796
+ if (modeInfo.wasShowing || modeInfo.wasHidden) {
8797
+ newTextTrack.mode = "hidden";
8798
+ } else {
8799
+ newTextTrack.mode = "disabled";
8800
+ }
8801
+ };
8802
+ if (newTextTrack.readyState >= 2) {
8803
+ restoreMode();
8804
+ } else {
8805
+ newTextTrack.addEventListener("load", restoreMode, { once: true });
8806
+ newTextTrack.addEventListener("error", restoreMode, { once: true });
8807
+ }
8808
+ }
8809
+ });
8810
+ }, 300);
8811
+ };
8812
+ if (this.player.element.readyState >= 1) {
8813
+ setTimeout(setupNewTracks, 200);
8814
+ } else {
8815
+ this.player.element.addEventListener("loadedmetadata", setupNewTracks, { once: true });
8816
+ setTimeout(setupNewTracks, 2e3);
8817
+ }
8818
+ resolve();
8819
+ }, 100);
8820
+ });
8821
+ }
8822
+ return swappedTracks;
8823
+ }
8824
+ /**
8825
+ * Update source elements to described versions
8826
+ */
8827
+ _updateSourceElements(toDescribed = true) {
8828
+ const sourceElements = this.player.sourceElements;
8829
+ const sourcesToUpdate = [];
8830
+ sourceElements.forEach((sourceEl) => {
8831
+ const descSrcAttr = sourceEl.getAttribute("data-desc-src");
8832
+ const currentSrc = sourceEl.getAttribute("src");
8833
+ if (descSrcAttr) {
8834
+ const type = sourceEl.getAttribute("type");
8835
+ let origSrc = sourceEl.getAttribute("data-orig-src") || currentSrc;
8836
+ sourcesToUpdate.push({
8837
+ src: toDescribed ? descSrcAttr : origSrc,
8838
+ type,
8839
+ origSrc,
8840
+ descSrc: descSrcAttr
8841
+ });
8842
+ } else {
8843
+ sourcesToUpdate.push({
8844
+ src: sourceEl.getAttribute("src"),
8845
+ type: sourceEl.getAttribute("type"),
8846
+ origSrc: null,
8847
+ descSrc: null
8848
+ });
8849
+ }
8850
+ });
8851
+ if (this.player.element.hasAttribute("src")) {
8852
+ this.player.element.removeAttribute("src");
8853
+ }
8854
+ sourceElements.forEach((sourceEl) => sourceEl.remove());
8855
+ sourcesToUpdate.forEach((sourceInfo) => {
8856
+ const newSource = document.createElement("source");
8857
+ newSource.setAttribute("src", sourceInfo.src);
8858
+ if (sourceInfo.type) {
8859
+ newSource.setAttribute("type", sourceInfo.type);
8860
+ }
8861
+ if (sourceInfo.origSrc) {
8862
+ newSource.setAttribute("data-orig-src", sourceInfo.origSrc);
8863
+ }
8864
+ if (sourceInfo.descSrc) {
8865
+ newSource.setAttribute("data-desc-src", sourceInfo.descSrc);
8866
+ }
8867
+ const firstTrack = this.player.element.querySelector("track");
8868
+ if (firstTrack) {
8869
+ this.player.element.insertBefore(newSource, firstTrack);
8870
+ } else {
8871
+ this.player.element.appendChild(newSource);
8872
+ }
8873
+ });
8874
+ this.player._sourceElementsDirty = true;
8875
+ this.player._sourceElementsCache = null;
8876
+ }
8877
+ /**
8878
+ * Wait for media to be ready
8879
+ */
8880
+ async _waitForMediaReady(needSeek = false) {
8881
+ await new Promise((resolve) => {
8882
+ if (this.player.element.readyState >= 1) {
8883
+ resolve();
8884
+ } else {
8885
+ const onLoad = () => {
8886
+ this.player.element.removeEventListener("loadedmetadata", onLoad);
8887
+ resolve();
8888
+ };
8889
+ this.player.element.addEventListener("loadedmetadata", onLoad);
8890
+ }
8891
+ });
8892
+ await new Promise((resolve) => setTimeout(resolve, 300));
8893
+ if (needSeek) {
8894
+ await new Promise((resolve) => {
8895
+ if (this.player.element.readyState >= 3) {
8896
+ resolve();
8897
+ } else {
8898
+ const onCanPlay = () => {
8899
+ this.player.element.removeEventListener("canplay", onCanPlay);
8900
+ this.player.element.removeEventListener("canplaythrough", onCanPlay);
8901
+ resolve();
8902
+ };
8903
+ this.player.element.addEventListener("canplay", onCanPlay, { once: true });
8904
+ this.player.element.addEventListener("canplaythrough", onCanPlay, { once: true });
8905
+ setTimeout(() => {
8906
+ this.player.element.removeEventListener("canplay", onCanPlay);
8907
+ this.player.element.removeEventListener("canplaythrough", onCanPlay);
8908
+ resolve();
8909
+ }, 3e3);
8910
+ }
8911
+ });
8912
+ }
8913
+ }
8914
+ /**
8915
+ * Restore playback state after source change
8916
+ */
8917
+ async _restorePlaybackState(currentTime, wasPlaying, shouldKeepPoster, currentCaptionText) {
8918
+ let syncTime = currentTime;
8919
+ if (currentCaptionText && this.player.captionManager && this.player.captionManager.tracks.length > 0) {
8920
+ await new Promise((resolve) => setTimeout(resolve, 500));
8921
+ const matchingTime = this.player.findMatchingCaptionTime(
8922
+ currentCaptionText,
8923
+ this.player.captionManager.tracks
8924
+ );
8925
+ if (matchingTime !== null) {
8926
+ syncTime = matchingTime;
8927
+ if (this.player.options.debug) {
8928
+ this.player.log("[VidPly] Syncing via caption: ".concat(currentTime, "s -> ").concat(syncTime, "s"));
8929
+ }
8930
+ }
8931
+ }
8932
+ if (syncTime > 0) {
8933
+ this.player.seek(syncTime);
8934
+ await new Promise((resolve) => setTimeout(resolve, 100));
8935
+ }
8936
+ if (wasPlaying) {
8937
+ await this.player.play();
8938
+ this.player.setManagedTimeout(() => {
8939
+ this.player.hidePosterOverlay();
8940
+ }, 100);
8941
+ } else {
8942
+ this.player.pause();
8943
+ if (!shouldKeepPoster) {
8944
+ this.player.hidePosterOverlay();
8945
+ }
8946
+ }
8947
+ }
8948
+ /**
8949
+ * Enable with source element method
8950
+ */
8951
+ async _enableWithSourceElement(currentTime, wasPlaying, posterValue, shouldKeepPoster, currentCaptionText) {
8952
+ await this._swapCaptionTracks(true);
8953
+ this._updateSourceElements(true);
8954
+ if (posterValue && this.player.element.tagName === "VIDEO") {
8955
+ this.player.element.poster = posterValue;
8956
+ }
8957
+ this.player.element.load();
8958
+ await this._waitForMediaReady(currentTime > 0 || wasPlaying);
8959
+ await this._restorePlaybackState(currentTime, wasPlaying, shouldKeepPoster, currentCaptionText);
8960
+ if (!this.desiredState) return;
8961
+ this.enabled = true;
8962
+ this.player.state.audioDescriptionEnabled = true;
8963
+ this.player.emit("audiodescriptionenabled");
8964
+ this._reloadTranscript();
8965
+ }
8966
+ /**
8967
+ * Enable with direct src method
8968
+ */
8969
+ async _enableWithDirectSrc(currentTime, wasPlaying, posterValue, shouldKeepPoster) {
8970
+ await this._swapCaptionTracks(true);
8971
+ if (posterValue && this.player.element.tagName === "VIDEO") {
8972
+ this.player.element.poster = posterValue;
8973
+ }
8974
+ this.player.element.src = this.src;
8975
+ await this._waitForMediaReady(currentTime > 0 || wasPlaying);
8976
+ if (currentTime > 0) {
8977
+ this.player.seek(currentTime);
8978
+ await new Promise((resolve) => setTimeout(resolve, 100));
8979
+ }
8980
+ if (wasPlaying) {
8981
+ await this.player.play();
8982
+ } else {
8983
+ this.player.pause();
8984
+ if (!shouldKeepPoster) {
8985
+ this.player.hidePosterOverlay();
8986
+ }
8987
+ }
8988
+ if (!this.desiredState) return;
8989
+ this.enabled = true;
8990
+ this.player.state.audioDescriptionEnabled = true;
8991
+ this.player.emit("audiodescriptionenabled");
8992
+ this._reloadTranscript();
8993
+ }
8994
+ /**
8995
+ * Disable with source element method
8996
+ */
8997
+ async _disableWithSourceElement(currentTime, wasPlaying, posterValue, shouldKeepPoster, currentCaptionText) {
8998
+ await this._swapCaptionTracks(false);
8999
+ this._updateSourceElements(false);
9000
+ if (posterValue && this.player.element.tagName === "VIDEO") {
9001
+ this.player.element.poster = posterValue;
9002
+ }
9003
+ this.player.element.load();
9004
+ this.player.invalidateTrackCache();
9005
+ await this._waitForMediaReady(currentTime > 0 || wasPlaying);
9006
+ await this._restorePlaybackState(currentTime, wasPlaying, shouldKeepPoster, currentCaptionText);
9007
+ if (this.player.captionManager) {
9008
+ this.player.captionManager.destroy();
9009
+ this.player.captionManager = new CaptionManager(this.player);
9010
+ }
9011
+ if (this.desiredState) return;
9012
+ this.enabled = false;
9013
+ this.player.state.audioDescriptionEnabled = false;
9014
+ this.player.emit("audiodescriptiondisabled");
9015
+ this._reloadTranscript();
9016
+ }
9017
+ /**
9018
+ * Disable with direct src method
9019
+ */
9020
+ async _disableWithDirectSrc(currentTime, wasPlaying, posterValue) {
9021
+ await this._swapCaptionTracks(false);
9022
+ if (posterValue && this.player.element.tagName === "VIDEO") {
9023
+ this.player.element.poster = posterValue;
9024
+ }
9025
+ const originalSrcToUse = this.originalSource || this.player.originalSrc;
9026
+ this.player.element.src = originalSrcToUse;
9027
+ this.player.element.load();
9028
+ await this._waitForMediaReady(currentTime > 0 || wasPlaying);
9029
+ if (currentTime > 0) {
9030
+ this.player.seek(currentTime);
9031
+ }
9032
+ if (wasPlaying) {
9033
+ await this.player.play();
9034
+ }
9035
+ if (this.desiredState) return;
9036
+ this.enabled = false;
9037
+ this.player.state.audioDescriptionEnabled = false;
9038
+ this.player.emit("audiodescriptiondisabled");
9039
+ this._reloadTranscript();
9040
+ }
9041
+ /**
9042
+ * Reload transcript after audio description state change
9043
+ */
9044
+ _reloadTranscript() {
9045
+ if (this.player.transcriptManager && this.player.transcriptManager.isVisible) {
9046
+ this.player.setManagedTimeout(() => {
9047
+ if (this.player.transcriptManager && this.player.transcriptManager.loadTranscriptData) {
9048
+ this.player.transcriptManager.loadTranscriptData();
9049
+ }
9050
+ }, 800);
9051
+ }
9052
+ }
9053
+ /**
9054
+ * Update sources (called when playlist changes)
9055
+ */
9056
+ updateSources(audioDescriptionSrc) {
9057
+ this.src = audioDescriptionSrc || null;
9058
+ this.enabled = false;
9059
+ this.desiredState = false;
9060
+ this.sourceElement = null;
9061
+ this.originalSource = null;
9062
+ this.captionTracks = [];
9063
+ }
9064
+ /**
9065
+ * Reinitialize from current player elements (called after playlist loads new tracks)
9066
+ */
9067
+ reinitialize() {
9068
+ this.player.invalidateTrackCache();
9069
+ this.initFromSourceElements(this.player.sourceElements, this.player.trackElements);
9070
+ }
9071
+ /**
9072
+ * Cleanup
9073
+ */
9074
+ destroy() {
9075
+ this.enabled = false;
9076
+ this.desiredState = false;
9077
+ this.captionTracks = [];
9078
+ this.sourceElement = null;
9079
+ this.originalSource = null;
9080
+ }
9081
+ };
9082
+
9083
+ // src/core/SignLanguageManager.js
9084
+ init_DOMUtils();
9085
+ init_Icons();
9086
+ init_i18n();
9087
+ init_DraggableResizable();
9088
+ init_MenuUtils();
9089
+ init_FormUtils();
9090
+ var SignLanguageManager = class {
9091
+ constructor(player) {
9092
+ this.player = player;
9093
+ this.src = player.options.signLanguageSrc;
9094
+ this.sources = player.options.signLanguageSources || {};
9095
+ this.currentLanguage = null;
9096
+ this.desiredPosition = player.options.signLanguagePosition || "bottom-right";
9097
+ this.wrapper = null;
9098
+ this.header = null;
9099
+ this.video = null;
9100
+ this.selector = null;
9101
+ this.settingsButton = null;
9102
+ this.settingsMenu = null;
9103
+ this.resizeHandles = [];
9104
+ this.enabled = false;
9105
+ this.settingsMenuVisible = false;
9106
+ this.settingsMenuJustOpened = false;
9107
+ this.documentClickHandlerAdded = false;
9108
+ this.handlers = null;
9109
+ this.settingsHandlers = null;
9110
+ this.interactionHandlers = null;
9111
+ this.draggable = null;
9112
+ this.documentClickHandler = null;
9113
+ this.settingsMenuKeyHandler = null;
9114
+ this.customKeyHandler = null;
9115
+ this.dragOptionButton = null;
9116
+ this.dragOptionText = null;
9117
+ this.resizeOptionButton = null;
9118
+ this.resizeOptionText = null;
9119
+ }
9120
+ /**
9121
+ * Check if sign language is available
9122
+ */
9123
+ isAvailable() {
9124
+ return Object.keys(this.sources).length > 0 || !!this.src;
9125
+ }
9126
+ /**
9127
+ * Enable sign language video
9128
+ */
9129
+ enable() {
9130
+ const hasMultipleSources = Object.keys(this.sources).length > 0;
9131
+ const hasSingleSource = !!this.src;
9132
+ if (!hasMultipleSources && !hasSingleSource) {
9133
+ console.warn("No sign language video source provided");
9134
+ return;
9135
+ }
9136
+ if (this.wrapper) {
9137
+ this.wrapper.style.display = "block";
9138
+ this.enabled = true;
9139
+ this.player.state.signLanguageEnabled = true;
9140
+ this.player.emit("signlanguageenabled");
9141
+ this.player.setManagedTimeout(() => {
9142
+ if (this.settingsButton && document.contains(this.settingsButton)) {
9143
+ this.settingsButton.focus({ preventScroll: true });
9144
+ }
9145
+ }, 150);
9146
+ return;
9147
+ }
9148
+ let initialLang = null;
9149
+ let initialSrc = null;
9150
+ if (hasMultipleSources) {
9151
+ initialLang = this._determineInitialLanguage();
9152
+ initialSrc = this.sources[initialLang];
9153
+ this.currentLanguage = initialLang;
9154
+ } else {
9155
+ initialSrc = this.src;
9156
+ }
9157
+ this._createWrapper();
9158
+ this._createHeader(hasMultipleSources, initialLang);
9159
+ this._createVideo(initialSrc);
9160
+ this._createResizeHandles();
9161
+ this.wrapper.appendChild(this.header);
9162
+ this.wrapper.appendChild(this.video);
9163
+ this.resizeHandles.forEach((handle) => this.wrapper.appendChild(handle));
9164
+ this._applyInitialSize();
9165
+ this.player.container.appendChild(this.wrapper);
9166
+ requestAnimationFrame(() => {
9167
+ this.constrainPosition();
9168
+ });
9169
+ this.video.currentTime = this.player.state.currentTime;
9170
+ if (!this.player.state.paused) {
9171
+ this.video.play();
9172
+ }
9173
+ this._setupInteraction();
9174
+ this._setupEventHandlers(hasMultipleSources);
9175
+ this.enabled = true;
9176
+ this.player.state.signLanguageEnabled = true;
9177
+ this.player.emit("signlanguageenabled");
9178
+ this.player.setManagedTimeout(() => {
9179
+ if (this.settingsButton && document.contains(this.settingsButton)) {
9180
+ this.settingsButton.focus({ preventScroll: true });
9181
+ }
9182
+ }, 150);
9183
+ }
9184
+ /**
9185
+ * Disable sign language video
9186
+ */
9187
+ disable() {
9188
+ if (this.settingsMenuVisible) {
9189
+ this.hideSettingsMenu({ focusButton: false });
9190
+ }
9191
+ if (this.wrapper) {
9192
+ this.wrapper.style.display = "none";
9193
+ }
9194
+ this.enabled = false;
9195
+ this.player.state.signLanguageEnabled = false;
9196
+ this.player.emit("signlanguagedisabled");
9197
+ }
9198
+ /**
9199
+ * Toggle sign language video
9200
+ */
9201
+ toggle() {
9202
+ if (this.enabled) {
9203
+ this.disable();
9204
+ } else {
9205
+ this.enable();
9206
+ }
9207
+ }
9208
+ /**
9209
+ * Switch to a different sign language
9210
+ */
9211
+ switchLanguage(langCode) {
9212
+ if (!this.sources[langCode] || !this.video) {
9213
+ return;
9214
+ }
9215
+ const currentTime = this.video.currentTime;
9216
+ const wasPlaying = !this.video.paused;
9217
+ this.video.src = this.sources[langCode];
9218
+ this.currentLanguage = langCode;
9219
+ this.video.currentTime = currentTime;
9220
+ if (wasPlaying) {
9221
+ this.video.play().catch(() => {
9222
+ });
9223
+ }
9224
+ this.player.emit("signlanguagelanguagechanged", langCode);
9225
+ }
9226
+ /**
9227
+ * Get language label
9228
+ */
9229
+ getLanguageLabel(langCode) {
9230
+ const langNames = {
9231
+ "en": "English",
9232
+ "de": "Deutsch",
9233
+ "es": "Español",
9234
+ "fr": "Français",
9235
+ "it": "Italiano",
9236
+ "ja": "日本語",
9237
+ "pt": "Português",
9238
+ "ar": "العربية",
9239
+ "hi": "हिन्दी"
9240
+ };
9241
+ return langNames[langCode] || langCode.toUpperCase();
9242
+ }
9243
+ /**
9244
+ * Determine initial sign language
9245
+ */
9246
+ _determineInitialLanguage() {
9247
+ var _a;
9248
+ if (this.player.captionManager && this.player.captionManager.currentTrack) {
9249
+ const captionLang = (_a = this.player.captionManager.currentTrack.language) == null ? void 0 : _a.toLowerCase().split("-")[0];
9250
+ if (captionLang && this.sources[captionLang]) {
9251
+ return captionLang;
9252
+ }
9253
+ }
9254
+ if (this.player.options.language) {
9255
+ const playerLang = this.player.options.language.toLowerCase().split("-")[0];
9256
+ if (this.sources[playerLang]) {
9257
+ return playerLang;
9258
+ }
9259
+ }
9260
+ return Object.keys(this.sources)[0];
9261
+ }
9262
+ /**
9263
+ * Create wrapper element
9264
+ */
9265
+ _createWrapper() {
9266
+ this.wrapper = document.createElement("div");
9267
+ this.wrapper.className = "vidply-sign-language-wrapper";
9268
+ this.wrapper.setAttribute("tabindex", "0");
9269
+ this.wrapper.setAttribute("aria-label", i18n.t("player.signLanguageDragResize"));
9270
+ }
9271
+ /**
9272
+ * Create header element
9273
+ */
9274
+ _createHeader(hasMultipleSources, initialLang) {
9275
+ const classPrefix = this.player.options.classPrefix;
9276
+ this.header = DOMUtils.createElement("div", {
9277
+ className: "".concat(classPrefix, "-sign-language-header"),
9278
+ attributes: { "tabindex": "0" }
9279
+ });
9280
+ const headerLeft = DOMUtils.createElement("div", {
9281
+ className: "".concat(classPrefix, "-sign-language-header-left")
9282
+ });
9283
+ const title = DOMUtils.createElement("h3", {
9284
+ textContent: i18n.t("player.signLanguageVideo")
9285
+ });
9286
+ this._createSettingsButton(headerLeft);
9287
+ if (hasMultipleSources) {
9288
+ this._createLanguageSelector(headerLeft, initialLang);
9289
+ }
9290
+ headerLeft.appendChild(title);
9291
+ const closeButton = this._createCloseButton();
9292
+ this.header.appendChild(headerLeft);
9293
+ this.header.appendChild(closeButton);
9294
+ this.settingsMenuVisible = false;
9295
+ this.settingsMenu = null;
9296
+ this.settingsMenuJustOpened = false;
9297
+ }
9298
+ /**
9299
+ * Create settings button
9300
+ */
9301
+ _createSettingsButton(container) {
9302
+ const classPrefix = this.player.options.classPrefix;
9303
+ const ariaLabel = i18n.t("player.signLanguageSettings");
9304
+ this.settingsButton = DOMUtils.createElement("button", {
9305
+ className: "".concat(classPrefix, "-sign-language-settings"),
9306
+ attributes: {
9307
+ "type": "button",
9308
+ "aria-label": ariaLabel,
9309
+ "aria-expanded": "false"
9310
+ }
9311
+ });
9312
+ this.settingsButton.appendChild(createIconElement("settings"));
9313
+ DOMUtils.attachTooltip(this.settingsButton, ariaLabel, classPrefix);
9314
+ this.settingsHandlers = {
9315
+ click: (e) => {
9316
+ e.preventDefault();
9317
+ e.stopPropagation();
9318
+ if (this.documentClickHandler) {
9319
+ this.settingsMenuJustOpened = true;
9320
+ setTimeout(() => {
9321
+ this.settingsMenuJustOpened = false;
9322
+ }, 100);
9323
+ }
9324
+ if (this.settingsMenuVisible) {
9325
+ this.hideSettingsMenu();
9326
+ } else {
9327
+ this.showSettingsMenu();
9328
+ }
9329
+ },
9330
+ keydown: (e) => {
9331
+ if (e.key === "d" || e.key === "D") {
9332
+ e.preventDefault();
9333
+ e.stopPropagation();
9334
+ this.toggleKeyboardDragMode();
9335
+ } else if (e.key === "r" || e.key === "R") {
9336
+ e.preventDefault();
9337
+ e.stopPropagation();
9338
+ this.toggleResizeMode();
9339
+ } else if (e.key === "Escape" && this.settingsMenuVisible) {
9340
+ e.preventDefault();
9341
+ e.stopPropagation();
9342
+ this.hideSettingsMenu();
9343
+ }
9344
+ }
9345
+ };
9346
+ this.settingsButton.addEventListener("click", this.settingsHandlers.click);
9347
+ this.settingsButton.addEventListener("keydown", this.settingsHandlers.keydown);
9348
+ container.appendChild(this.settingsButton);
9349
+ }
9350
+ /**
9351
+ * Create language selector
9352
+ */
9353
+ _createLanguageSelector(container, initialLang) {
9354
+ const classPrefix = this.player.options.classPrefix;
9355
+ const selectId = "".concat(classPrefix, "-sign-language-select-").concat(Date.now());
9356
+ const options = Object.keys(this.sources).map((langCode) => ({
9357
+ value: langCode,
9358
+ text: this.getLanguageLabel(langCode),
9359
+ selected: langCode === initialLang
9360
+ }));
9361
+ const { label, select } = createLabeledSelect({
9362
+ classPrefix,
9363
+ labelClass: "".concat(classPrefix, "-sign-language-label"),
9364
+ selectClass: "".concat(classPrefix, "-sign-language-select"),
9365
+ labelText: "settings.language",
9366
+ selectId,
9367
+ options,
9368
+ onChange: (e) => {
9369
+ e.stopPropagation();
9370
+ this.switchLanguage(e.target.value);
9371
+ }
9372
+ });
9373
+ this.selector = select;
9374
+ const selectorWrapper = DOMUtils.createElement("div", {
9375
+ className: "".concat(classPrefix, "-sign-language-selector-wrapper")
9376
+ });
9377
+ selectorWrapper.appendChild(label);
9378
+ selectorWrapper.appendChild(this.selector);
9379
+ preventDragOnElement(selectorWrapper);
9380
+ container.appendChild(selectorWrapper);
9381
+ }
9382
+ /**
9383
+ * Create close button
9384
+ */
9385
+ _createCloseButton() {
9386
+ const classPrefix = this.player.options.classPrefix;
9387
+ const ariaLabel = i18n.t("player.closeSignLanguage");
9388
+ const closeButton = DOMUtils.createElement("button", {
9389
+ className: "".concat(classPrefix, "-sign-language-close"),
9390
+ attributes: {
9391
+ "type": "button",
9392
+ "aria-label": ariaLabel
9393
+ }
9394
+ });
9395
+ closeButton.appendChild(createIconElement("close"));
9396
+ DOMUtils.attachTooltip(closeButton, ariaLabel, classPrefix);
9397
+ closeButton.addEventListener("click", () => {
9398
+ var _a, _b;
9399
+ this.disable();
9400
+ if ((_b = (_a = this.player.controlBar) == null ? void 0 : _a.controls) == null ? void 0 : _b.signLanguage) {
9401
+ setTimeout(() => {
9402
+ this.player.controlBar.controls.signLanguage.focus({ preventScroll: true });
9403
+ }, 0);
9404
+ }
9405
+ });
9406
+ return closeButton;
9407
+ }
9408
+ /**
9409
+ * Create video element
9410
+ */
9411
+ _createVideo(src) {
9412
+ this.video = document.createElement("video");
9413
+ this.video.className = "vidply-sign-language-video";
9414
+ this.video.src = src;
9415
+ this.video.setAttribute("aria-label", i18n.t("player.signLanguage"));
9416
+ this.video.muted = true;
9417
+ this.video.setAttribute("playsinline", "");
9418
+ }
9419
+ /**
9420
+ * Create resize handles
9421
+ */
9422
+ _createResizeHandles() {
9423
+ const classPrefix = this.player.options.classPrefix;
9424
+ this.resizeHandles = ["n", "s", "e", "w", "ne", "nw", "se", "sw"].map((dir) => {
9425
+ const handle = DOMUtils.createElement("div", {
9426
+ className: "".concat(classPrefix, "-sign-resize-handle ").concat(classPrefix, "-sign-resize-").concat(dir),
9427
+ attributes: {
9428
+ "data-direction": dir,
9429
+ "data-vidply-managed-resize": "true",
9430
+ "aria-hidden": "true"
9431
+ }
9432
+ });
9433
+ handle.style.display = "none";
9434
+ return handle;
9435
+ });
9436
+ }
9437
+ /**
9438
+ * Apply initial size
9439
+ */
9440
+ _applyInitialSize() {
9441
+ var _a;
9442
+ const saved = this.player.storage.getSignLanguagePreferences();
9443
+ if ((_a = saved == null ? void 0 : saved.size) == null ? void 0 : _a.width) {
9444
+ this.wrapper.style.width = saved.size.width;
9445
+ } else {
9446
+ this.wrapper.style.width = "280px";
9447
+ }
9448
+ this.wrapper.style.height = "auto";
9449
+ }
9450
+ /**
9451
+ * Setup interaction (drag and resize)
9452
+ */
9453
+ _setupInteraction() {
9454
+ const isMobile2 = window.innerWidth < 768;
9455
+ const isFullscreen = this.player.state.fullscreen;
9456
+ if (isMobile2 && !isFullscreen) {
9457
+ if (this.draggable) {
9458
+ this.draggable.destroy();
9459
+ this.draggable = null;
9460
+ }
9461
+ return;
9462
+ }
9463
+ if (this.draggable) return;
9464
+ const classPrefix = this.player.options.classPrefix;
9465
+ this.draggable = new DraggableResizable(this.wrapper, {
9466
+ dragHandle: this.header,
9467
+ resizeHandles: this.resizeHandles,
9468
+ constrainToViewport: true,
9469
+ maintainAspectRatio: true,
9470
+ minWidth: 150,
9471
+ minHeight: 100,
9472
+ classPrefix: "".concat(classPrefix, "-sign"),
9473
+ keyboardDragKey: "d",
9474
+ keyboardResizeKey: "r",
9475
+ keyboardStep: 10,
9476
+ keyboardStepLarge: 50,
9477
+ pointerResizeIndicatorText: i18n.t("player.signLanguageResizeActive"),
9478
+ onPointerResizeToggle: (enabled) => {
9479
+ this.resizeHandles.forEach((handle) => {
9480
+ handle.style.display = enabled ? "block" : "none";
9481
+ });
9482
+ },
9483
+ onDragStart: (e) => {
9484
+ if (e.target.closest(".".concat(classPrefix, "-sign-language-close")) || e.target.closest(".".concat(classPrefix, "-sign-language-settings")) || e.target.closest(".".concat(classPrefix, "-sign-language-select")) || e.target.closest(".".concat(classPrefix, "-sign-language-label")) || e.target.closest(".".concat(classPrefix, "-sign-language-settings-menu"))) {
9485
+ return false;
9486
+ }
9487
+ return true;
9488
+ }
9489
+ });
9490
+ this._setupCustomKeyHandler();
9491
+ this.interactionHandlers = {
9492
+ draggable: this.draggable,
9493
+ customKeyHandler: this.customKeyHandler
9494
+ };
9495
+ }
9496
+ /**
9497
+ * Setup custom keyboard handler
9498
+ */
9499
+ _setupCustomKeyHandler() {
9500
+ this.customKeyHandler = (e) => {
9501
+ var _a, _b, _c, _d;
9502
+ const key = e.key.toLowerCase();
9503
+ if (this.settingsMenuVisible) return;
9504
+ if (key === "home") {
9505
+ e.preventDefault();
9506
+ e.stopPropagation();
9507
+ if (this.draggable) {
9508
+ if (this.draggable.pointerResizeMode) {
9509
+ this.draggable.disablePointerResizeMode();
9510
+ }
9511
+ this.draggable.manuallyPositioned = false;
9512
+ this.constrainPosition();
9513
+ }
9514
+ return;
9515
+ }
9516
+ if (key === "r") {
9517
+ e.preventDefault();
9518
+ e.stopPropagation();
9519
+ if (this.toggleResizeMode()) {
9520
+ this.wrapper.focus({ preventScroll: true });
9521
+ }
9522
+ return;
9523
+ }
9524
+ if (key === "escape") {
9525
+ e.preventDefault();
9526
+ e.stopPropagation();
9527
+ if ((_a = this.draggable) == null ? void 0 : _a.pointerResizeMode) {
9528
+ this.draggable.disablePointerResizeMode();
9529
+ return;
9530
+ }
9531
+ if ((_b = this.draggable) == null ? void 0 : _b.keyboardDragMode) {
9532
+ this.draggable.disableKeyboardDragMode();
9533
+ return;
9534
+ }
9535
+ this.disable();
9536
+ if ((_d = (_c = this.player.controlBar) == null ? void 0 : _c.controls) == null ? void 0 : _d.signLanguage) {
9537
+ setTimeout(() => {
9538
+ this.player.controlBar.controls.signLanguage.focus({ preventScroll: true });
9539
+ }, 0);
9540
+ }
9541
+ }
9542
+ };
9543
+ this.wrapper.addEventListener("keydown", this.customKeyHandler);
9544
+ }
9545
+ /**
9546
+ * Setup event handlers
9547
+ */
9548
+ _setupEventHandlers(hasMultipleSources) {
9549
+ this.handlers = {
9550
+ play: () => {
9551
+ if (this.video) this.video.play();
9552
+ },
9553
+ pause: () => {
9554
+ if (this.video) this.video.pause();
9555
+ },
9556
+ timeupdate: () => {
9557
+ if (this.video && Math.abs(this.video.currentTime - this.player.state.currentTime) > 0.5) {
9558
+ this.video.currentTime = this.player.state.currentTime;
9559
+ }
9560
+ },
9561
+ ratechange: () => {
9562
+ if (this.video) this.video.playbackRate = this.player.state.playbackSpeed;
9563
+ }
9564
+ };
9565
+ this.player.on("play", this.handlers.play);
9566
+ this.player.on("pause", this.handlers.pause);
9567
+ this.player.on("timeupdate", this.handlers.timeupdate);
9568
+ this.player.on("ratechange", this.handlers.ratechange);
9569
+ if (hasMultipleSources) {
9570
+ this.handlers.captionChange = () => {
9571
+ var _a, _b;
9572
+ if (((_a = this.player.captionManager) == null ? void 0 : _a.currentTrack) && this.selector) {
9573
+ const captionLang = (_b = this.player.captionManager.currentTrack.language) == null ? void 0 : _b.toLowerCase().split("-")[0];
9574
+ if (captionLang && this.sources[captionLang] && this.currentLanguage !== captionLang) {
9575
+ this.switchLanguage(captionLang);
9576
+ this.selector.value = captionLang;
9577
+ }
9578
+ }
9579
+ };
9580
+ this.player.on("captionsenabled", this.handlers.captionChange);
9581
+ }
9582
+ }
9583
+ /**
9584
+ * Constrain position within video wrapper
9585
+ */
9586
+ constrainPosition() {
9587
+ var _a;
9588
+ if (!this.wrapper || !this.player.videoWrapper) return;
9589
+ if ((_a = this.draggable) == null ? void 0 : _a.manuallyPositioned) return;
9590
+ if (!this.wrapper.style.width) {
9591
+ this.wrapper.style.width = "280px";
9592
+ }
9593
+ const videoWrapperRect = this.player.videoWrapper.getBoundingClientRect();
9594
+ const containerRect = this.player.container.getBoundingClientRect();
9595
+ const wrapperRect = this.wrapper.getBoundingClientRect();
9596
+ const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
9597
+ const videoWrapperTop = videoWrapperRect.top - containerRect.top;
9598
+ const videoWrapperWidth = videoWrapperRect.width;
9599
+ const videoWrapperHeight = videoWrapperRect.height;
9600
+ let wrapperWidth = wrapperRect.width || 280;
9601
+ let wrapperHeight = wrapperRect.height || 280 * 9 / 16;
9602
+ let left, top;
9603
+ const margin = 16;
9604
+ const controlsHeight = 95;
9605
+ const position = this.desiredPosition || "bottom-right";
9606
+ switch (position) {
9607
+ case "bottom-right":
9608
+ left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
9609
+ top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
9610
+ break;
9611
+ case "bottom-left":
9612
+ left = videoWrapperLeft + margin;
9613
+ top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
9614
+ break;
9615
+ case "top-right":
9616
+ left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
9617
+ top = videoWrapperTop + margin;
9618
+ break;
9619
+ case "top-left":
9620
+ left = videoWrapperLeft + margin;
9621
+ top = videoWrapperTop + margin;
9622
+ break;
9623
+ default:
9624
+ left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
9625
+ top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
9626
+ }
9627
+ left = Math.max(videoWrapperLeft, Math.min(left, videoWrapperLeft + videoWrapperWidth - wrapperWidth));
9628
+ top = Math.max(videoWrapperTop, Math.min(top, videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight));
9629
+ this.wrapper.style.left = "".concat(left, "px");
9630
+ this.wrapper.style.top = "".concat(top, "px");
9631
+ this.wrapper.style.right = "auto";
9632
+ this.wrapper.style.bottom = "auto";
9633
+ }
9634
+ /**
9635
+ * Show settings menu
9636
+ */
9637
+ showSettingsMenu() {
9638
+ var _a;
9639
+ this.settingsMenuJustOpened = true;
9640
+ setTimeout(() => {
9641
+ this.settingsMenuJustOpened = false;
9642
+ }, 350);
9643
+ this._addDocumentClickHandler();
9644
+ if (this.settingsMenu) {
9645
+ this.settingsMenu.style.display = "block";
9646
+ this.settingsMenuVisible = true;
9647
+ (_a = this.settingsButton) == null ? void 0 : _a.setAttribute("aria-expanded", "true");
9648
+ this._attachMenuKeyboardNavigation();
9649
+ this._positionSettingsMenu();
9650
+ this._updateDragOptionState();
9651
+ this._updateResizeOptionState();
9652
+ focusFirstMenuItem(this.settingsMenu, ".".concat(this.player.options.classPrefix, "-sign-language-settings-item"));
9653
+ return;
9654
+ }
9655
+ this._createSettingsMenu();
9656
+ }
9657
+ /**
9658
+ * Hide settings menu
9659
+ */
9660
+ hideSettingsMenu({ focusButton = true } = {}) {
9661
+ if (this.settingsMenu) {
9662
+ this.settingsMenu.style.display = "none";
9663
+ this.settingsMenuVisible = false;
9664
+ this.settingsMenuJustOpened = false;
9665
+ if (this.settingsMenuKeyHandler) {
9666
+ this.settingsMenu.removeEventListener("keydown", this.settingsMenuKeyHandler);
9667
+ this.settingsMenuKeyHandler = null;
9668
+ }
9669
+ const classPrefix = this.player.options.classPrefix;
9670
+ const menuItems = Array.from(this.settingsMenu.querySelectorAll(".".concat(classPrefix, "-sign-language-settings-item")));
9671
+ menuItems.forEach((item) => item.setAttribute("tabindex", "-1"));
9672
+ if (this.settingsButton) {
9673
+ this.settingsButton.setAttribute("aria-expanded", "false");
9674
+ if (focusButton) {
9675
+ this.settingsButton.focus({ preventScroll: true });
9676
+ }
9677
+ }
9678
+ }
9679
+ }
9680
+ /**
9681
+ * Add document click handler
9682
+ */
9683
+ _addDocumentClickHandler() {
9684
+ if (this.documentClickHandlerAdded) return;
9685
+ this.documentClickHandler = (e) => {
9686
+ if (this.settingsMenuJustOpened) return;
9687
+ if (this.settingsButton && (this.settingsButton === e.target || this.settingsButton.contains(e.target))) {
9688
+ return;
9689
+ }
9690
+ if (this.settingsMenu && this.settingsMenu.contains(e.target)) {
9691
+ return;
9692
+ }
9693
+ if (this.settingsMenuVisible) {
9694
+ this.hideSettingsMenu();
9695
+ }
9696
+ };
9697
+ setTimeout(() => {
9698
+ document.addEventListener("mousedown", this.documentClickHandler, true);
9699
+ this.documentClickHandlerAdded = true;
9700
+ }, 300);
9701
+ }
9702
+ /**
9703
+ * Create settings menu
9704
+ */
9705
+ _createSettingsMenu() {
9706
+ var _a, _b;
9707
+ const classPrefix = this.player.options.classPrefix;
9708
+ this.settingsMenu = DOMUtils.createElement("div", {
9709
+ className: "".concat(classPrefix, "-sign-language-settings-menu"),
9710
+ attributes: { "role": "menu" }
9711
+ });
9712
+ const dragOption = createMenuItem({
9713
+ classPrefix,
9714
+ itemClass: "".concat(classPrefix, "-sign-language-settings-item"),
9715
+ icon: "move",
9716
+ label: "player.enableSignDragMode",
9717
+ hasTextClass: true,
9718
+ onClick: () => {
9719
+ this.toggleKeyboardDragMode();
9720
+ this.hideSettingsMenu();
9721
+ }
9722
+ });
9723
+ dragOption.setAttribute("role", "switch");
9724
+ dragOption.setAttribute("aria-checked", "false");
9725
+ this._removeTooltipFromMenuItem(dragOption);
9726
+ this.dragOptionButton = dragOption;
9727
+ this.dragOptionText = dragOption.querySelector(".".concat(classPrefix, "-settings-text"));
9728
+ this._updateDragOptionState();
9729
+ const resizeOption = createMenuItem({
9730
+ classPrefix,
9731
+ itemClass: "".concat(classPrefix, "-sign-language-settings-item"),
9732
+ icon: "resize",
9733
+ label: "player.enableSignResizeMode",
9734
+ hasTextClass: true,
9735
+ onClick: (event) => {
9736
+ event.preventDefault();
9737
+ event.stopPropagation();
9738
+ const enabled = this.toggleResizeMode({ focus: false });
9739
+ if (enabled) {
9740
+ this.hideSettingsMenu({ focusButton: false });
9741
+ setTimeout(() => {
9742
+ if (this.wrapper) this.wrapper.focus({ preventScroll: true });
9743
+ }, 20);
9744
+ } else {
9745
+ this.hideSettingsMenu({ focusButton: true });
9746
+ }
9747
+ }
9748
+ });
9749
+ resizeOption.setAttribute("role", "switch");
9750
+ resizeOption.setAttribute("aria-checked", "false");
9751
+ this._removeTooltipFromMenuItem(resizeOption);
9752
+ this.resizeOptionButton = resizeOption;
9753
+ this.resizeOptionText = resizeOption.querySelector(".".concat(classPrefix, "-settings-text"));
9754
+ this._updateResizeOptionState();
9755
+ const closeOption = createMenuItem({
9756
+ classPrefix,
9757
+ itemClass: "".concat(classPrefix, "-sign-language-settings-item"),
9758
+ icon: "close",
9759
+ label: "transcript.closeMenu",
9760
+ onClick: () => this.hideSettingsMenu()
9761
+ });
9762
+ this._removeTooltipFromMenuItem(closeOption);
9763
+ this.settingsMenu.appendChild(dragOption);
9764
+ this.settingsMenu.appendChild(resizeOption);
9765
+ this.settingsMenu.appendChild(closeOption);
9766
+ this.settingsMenu.style.visibility = "hidden";
9767
+ this.settingsMenu.style.display = "block";
9768
+ if ((_a = this.settingsButton) == null ? void 0 : _a.parentNode) {
9769
+ this.settingsButton.insertAdjacentElement("afterend", this.settingsMenu);
9770
+ } else if (this.wrapper) {
9771
+ this.wrapper.appendChild(this.settingsMenu);
9772
+ }
9773
+ this._positionSettingsMenuImmediate();
9774
+ requestAnimationFrame(() => {
9775
+ if (this.settingsMenu) {
9776
+ this.settingsMenu.style.visibility = "visible";
9777
+ }
9778
+ });
9779
+ this._attachMenuKeyboardNavigation();
9780
+ this.settingsMenuVisible = true;
9781
+ (_b = this.settingsButton) == null ? void 0 : _b.setAttribute("aria-expanded", "true");
9782
+ this._updateDragOptionState();
9783
+ this._updateResizeOptionState();
9784
+ focusFirstMenuItem(this.settingsMenu, ".".concat(classPrefix, "-sign-language-settings-item"));
9785
+ }
9786
+ /**
9787
+ * Remove tooltip from menu item
9788
+ */
9789
+ _removeTooltipFromMenuItem(item) {
9790
+ const classPrefix = this.player.options.classPrefix;
9791
+ const tooltip = item.querySelector(".".concat(classPrefix, "-tooltip"));
9792
+ if (tooltip) tooltip.remove();
9793
+ const buttonText = item.querySelector(".".concat(classPrefix, "-button-text"));
9794
+ if (buttonText) buttonText.remove();
9795
+ }
9796
+ /**
9797
+ * Attach menu keyboard navigation
9798
+ */
9799
+ _attachMenuKeyboardNavigation() {
9800
+ if (this.settingsMenuKeyHandler) {
9801
+ this.settingsMenu.removeEventListener("keydown", this.settingsMenuKeyHandler);
9802
+ }
9803
+ this.settingsMenuKeyHandler = attachMenuKeyboardNavigation(
9804
+ this.settingsMenu,
9805
+ this.settingsButton,
9806
+ ".".concat(this.player.options.classPrefix, "-sign-language-settings-item"),
9807
+ () => this.hideSettingsMenu({ focusButton: true })
9808
+ );
9809
+ }
9810
+ /**
9811
+ * Position settings menu immediately
9812
+ */
9813
+ _positionSettingsMenuImmediate() {
9814
+ if (!this.settingsMenu || !this.settingsButton) return;
9815
+ const buttonRect = this.settingsButton.getBoundingClientRect();
9816
+ const menuRect = this.settingsMenu.getBoundingClientRect();
9817
+ const viewportWidth = window.innerWidth;
9818
+ const viewportHeight = window.innerHeight;
9819
+ const parentContainer = this.settingsButton.parentElement;
9820
+ if (!parentContainer) return;
9821
+ const parentRect = parentContainer.getBoundingClientRect();
9822
+ const buttonCenterX = buttonRect.left + buttonRect.width / 2 - parentRect.left;
9823
+ const buttonBottom = buttonRect.bottom - parentRect.top;
9824
+ const buttonTop = buttonRect.top - parentRect.top;
9825
+ const spaceAbove = buttonRect.top;
9826
+ const spaceBelow = viewportHeight - buttonRect.bottom;
9827
+ let menuTop = buttonBottom + 8;
9828
+ let menuBottom = null;
9829
+ if (spaceBelow < menuRect.height + 20 && spaceAbove > spaceBelow) {
9830
+ menuTop = null;
9831
+ const parentHeight = parentRect.bottom - parentRect.top;
9832
+ menuBottom = parentHeight - buttonTop + 8;
9833
+ this.settingsMenu.classList.add("vidply-menu-above");
9834
+ } else {
9835
+ this.settingsMenu.classList.remove("vidply-menu-above");
9836
+ }
9837
+ let menuLeft = buttonCenterX - menuRect.width / 2;
9838
+ let menuRight = "auto";
9839
+ let transformX = "translateX(0)";
9840
+ const menuLeftAbsolute = buttonRect.left + buttonRect.width / 2 - menuRect.width / 2;
9841
+ if (menuLeftAbsolute < 10) {
9842
+ menuLeft = 0;
9843
+ } else if (menuLeftAbsolute + menuRect.width > viewportWidth - 10) {
9844
+ menuLeft = "auto";
9845
+ menuRight = 0;
9846
+ } else {
9847
+ menuLeft = buttonCenterX;
9848
+ transformX = "translateX(-50%)";
9849
+ }
9850
+ if (menuTop !== null) {
9851
+ this.settingsMenu.style.top = "".concat(menuTop, "px");
9852
+ this.settingsMenu.style.bottom = "auto";
9853
+ } else if (menuBottom !== null) {
9854
+ this.settingsMenu.style.top = "auto";
9855
+ this.settingsMenu.style.bottom = "".concat(menuBottom, "px");
9856
+ }
9857
+ if (menuLeft !== "auto") {
9858
+ this.settingsMenu.style.left = "".concat(menuLeft, "px");
9859
+ this.settingsMenu.style.right = "auto";
9860
+ } else {
9861
+ this.settingsMenu.style.left = "auto";
9862
+ this.settingsMenu.style.right = "".concat(menuRight, "px");
9863
+ }
9864
+ this.settingsMenu.style.transform = transformX;
9865
+ }
9866
+ /**
9867
+ * Position settings menu with RAF
9868
+ */
9869
+ _positionSettingsMenu() {
9870
+ requestAnimationFrame(() => {
9871
+ setTimeout(() => {
9872
+ this._positionSettingsMenuImmediate();
9873
+ }, 10);
9874
+ });
9875
+ }
9876
+ /**
9877
+ * Toggle keyboard drag mode
9878
+ */
9879
+ toggleKeyboardDragMode() {
9880
+ if (this.draggable) {
9881
+ const wasEnabled = this.draggable.keyboardDragMode;
9882
+ this.draggable.toggleKeyboardDragMode();
9883
+ const isEnabled = this.draggable.keyboardDragMode;
9884
+ if (!wasEnabled && isEnabled) {
9885
+ this._enableMoveMode();
9886
+ }
9887
+ this._updateDragOptionState();
9888
+ }
9889
+ }
9890
+ /**
9891
+ * Enable move mode visual feedback
9892
+ */
9893
+ _enableMoveMode() {
9894
+ this.wrapper.classList.add("".concat(this.player.options.classPrefix, "-sign-move-mode"));
9895
+ this._updateResizeOptionState();
9896
+ setTimeout(() => {
9897
+ this.wrapper.classList.remove("".concat(this.player.options.classPrefix, "-sign-move-mode"));
9898
+ }, 2e3);
9899
+ }
9900
+ /**
9901
+ * Toggle resize mode
9902
+ */
9903
+ toggleResizeMode({ focus = true } = {}) {
9904
+ if (!this.draggable) return false;
9905
+ if (this.draggable.pointerResizeMode) {
9906
+ this.draggable.disablePointerResizeMode({ focus });
9907
+ this._updateResizeOptionState();
9908
+ return false;
9909
+ }
9910
+ this.draggable.enablePointerResizeMode({ focus });
9911
+ this._updateResizeOptionState();
9912
+ return true;
9913
+ }
9914
+ /**
9915
+ * Update drag option state
9916
+ */
9917
+ _updateDragOptionState() {
9918
+ var _a;
9919
+ if (!this.dragOptionButton) return;
9920
+ const isEnabled = !!((_a = this.draggable) == null ? void 0 : _a.keyboardDragMode);
9921
+ const text = isEnabled ? i18n.t("player.disableSignDragMode") : i18n.t("player.enableSignDragMode");
9922
+ const ariaLabel = isEnabled ? i18n.t("player.disableSignDragModeAria") : i18n.t("player.enableSignDragModeAria");
9923
+ this.dragOptionButton.setAttribute("aria-checked", isEnabled ? "true" : "false");
9924
+ this.dragOptionButton.setAttribute("aria-label", ariaLabel);
9925
+ if (this.dragOptionText) {
9926
+ this.dragOptionText.textContent = text;
9927
+ }
9928
+ }
9929
+ /**
9930
+ * Update resize option state
9931
+ */
9932
+ _updateResizeOptionState() {
9933
+ var _a;
9934
+ if (!this.resizeOptionButton) return;
9935
+ const isEnabled = !!((_a = this.draggable) == null ? void 0 : _a.pointerResizeMode);
9936
+ const text = isEnabled ? i18n.t("player.disableSignResizeMode") : i18n.t("player.enableSignResizeMode");
9937
+ const ariaLabel = isEnabled ? i18n.t("player.disableSignResizeModeAria") : i18n.t("player.enableSignResizeModeAria");
9938
+ this.resizeOptionButton.setAttribute("aria-checked", isEnabled ? "true" : "false");
9939
+ this.resizeOptionButton.setAttribute("aria-label", ariaLabel);
9940
+ if (this.resizeOptionText) {
9941
+ this.resizeOptionText.textContent = text;
9942
+ }
9943
+ }
9944
+ /**
9945
+ * Save preferences
9946
+ */
9947
+ savePreferences() {
9948
+ if (!this.wrapper) return;
9949
+ this.player.storage.saveSignLanguagePreferences({
9950
+ size: { width: this.wrapper.style.width }
9951
+ });
9952
+ }
9953
+ /**
9954
+ * Update sources (called when playlist changes)
9955
+ */
9956
+ updateSources(signLanguageSrc, signLanguageSources) {
9957
+ this.src = signLanguageSrc || null;
9958
+ this.sources = signLanguageSources || {};
9959
+ this.currentLanguage = null;
9960
+ }
9961
+ /**
9962
+ * Cleanup
9963
+ */
9964
+ cleanup() {
9965
+ var _a;
9966
+ if (this.settingsMenuVisible) {
9967
+ this.hideSettingsMenu({ focusButton: false });
9968
+ }
9969
+ if (this.documentClickHandler && this.documentClickHandlerAdded) {
9970
+ document.removeEventListener("mousedown", this.documentClickHandler, true);
9971
+ this.documentClickHandlerAdded = false;
9972
+ this.documentClickHandler = null;
9973
+ }
9974
+ if (this.settingsHandlers && this.settingsButton) {
9975
+ this.settingsButton.removeEventListener("click", this.settingsHandlers.click);
9976
+ this.settingsButton.removeEventListener("keydown", this.settingsHandlers.keydown);
9977
+ }
9978
+ this.settingsHandlers = null;
9979
+ if (this.handlers) {
9980
+ this.player.off("play", this.handlers.play);
9981
+ this.player.off("pause", this.handlers.pause);
9982
+ this.player.off("timeupdate", this.handlers.timeupdate);
9983
+ this.player.off("ratechange", this.handlers.ratechange);
9984
+ if (this.handlers.captionChange) {
9985
+ this.player.off("captionsenabled", this.handlers.captionChange);
9986
+ }
9987
+ this.handlers = null;
9988
+ }
9989
+ if (this.wrapper && this.customKeyHandler) {
9990
+ this.wrapper.removeEventListener("keydown", this.customKeyHandler);
9991
+ }
9992
+ if (this.draggable) {
9993
+ if (this.draggable.pointerResizeMode) {
9994
+ this.draggable.disablePointerResizeMode();
9995
+ }
9996
+ this.draggable.destroy();
9997
+ this.draggable = null;
9998
+ }
9999
+ this.interactionHandlers = null;
10000
+ if ((_a = this.wrapper) == null ? void 0 : _a.parentNode) {
10001
+ if (this.video) {
10002
+ this.video.pause();
10003
+ this.video.src = "";
10004
+ }
10005
+ this.wrapper.parentNode.removeChild(this.wrapper);
10006
+ }
10007
+ this.wrapper = null;
10008
+ this.video = null;
10009
+ this.settingsButton = null;
10010
+ this.settingsMenu = null;
10011
+ }
10012
+ /**
10013
+ * Destroy
10014
+ */
10015
+ destroy() {
10016
+ this.cleanup();
10017
+ this.enabled = false;
10018
+ }
10019
+ };
10020
+
10021
+ // src/core/Player.js
10022
+ var playerInstanceCounter = 0;
10023
+ var Player = class _Player extends EventEmitter {
10024
+ constructor(element, options = {}) {
10025
+ super();
10026
+ this.element = typeof element === "string" ? document.querySelector(element) : element;
10027
+ if (!this.element) {
10028
+ throw new Error("VidPly: Element not found");
10029
+ }
10030
+ playerInstanceCounter++;
10031
+ this.instanceId = playerInstanceCounter;
10032
+ if (this.element.tagName !== "VIDEO" && this.element.tagName !== "AUDIO") {
10033
+ const mediaType = options.mediaType || "video";
10034
+ const mediaElement = document.createElement(mediaType);
10035
+ Array.from(this.element.attributes).forEach((attr) => {
10036
+ if (attr.name !== "id" && attr.name !== "class" && !attr.name.startsWith("data-")) {
10037
+ mediaElement.setAttribute(attr.name, attr.value);
10038
+ }
10039
+ });
10040
+ const tracks = this.element.querySelectorAll("track");
10041
+ tracks.forEach((track) => {
10042
+ mediaElement.appendChild(track.cloneNode(true));
10043
+ });
10044
+ this.element.innerHTML = "";
10045
+ this.element.appendChild(mediaElement);
10046
+ this.element = mediaElement;
10047
+ }
10048
+ this._originalElement = this.element;
10049
+ this.options = __spreadValues({
10050
+ // Display
10051
+ width: null,
10052
+ height: null,
8498
10053
  poster: null,
8499
10054
  responsive: true,
8500
10055
  fillContainer: false,
@@ -8649,6 +10204,58 @@
8649
10204
  this.settingsDialog = null;
8650
10205
  this.metadataCueChangeHandler = null;
8651
10206
  this.metadataAlertHandlers = /* @__PURE__ */ new Map();
10207
+ this.audioDescriptionManager = new AudioDescriptionManager(this);
10208
+ this.signLanguageManager = new SignLanguageManager(this);
10209
+ Object.defineProperties(this, {
10210
+ signLanguageWrapper: {
10211
+ get: () => this.signLanguageManager.wrapper,
10212
+ set: (v) => {
10213
+ this.signLanguageManager.wrapper = v;
10214
+ }
10215
+ },
10216
+ signLanguageVideo: {
10217
+ get: () => this.signLanguageManager.video,
10218
+ set: (v) => {
10219
+ this.signLanguageManager.video = v;
10220
+ }
10221
+ },
10222
+ signLanguageHeader: {
10223
+ get: () => this.signLanguageManager.header,
10224
+ set: (v) => {
10225
+ this.signLanguageManager.header = v;
10226
+ }
10227
+ },
10228
+ signLanguageSettingsButton: {
10229
+ get: () => this.signLanguageManager.settingsButton,
10230
+ set: (v) => {
10231
+ this.signLanguageManager.settingsButton = v;
10232
+ }
10233
+ },
10234
+ signLanguageSettingsMenu: {
10235
+ get: () => this.signLanguageManager.settingsMenu,
10236
+ set: (v) => {
10237
+ this.signLanguageManager.settingsMenu = v;
10238
+ }
10239
+ },
10240
+ signLanguageSettingsMenuVisible: {
10241
+ get: () => this.signLanguageManager.settingsMenuVisible,
10242
+ set: (v) => {
10243
+ this.signLanguageManager.settingsMenuVisible = v;
10244
+ }
10245
+ },
10246
+ signLanguageDraggable: {
10247
+ get: () => this.signLanguageManager.draggable,
10248
+ set: (v) => {
10249
+ this.signLanguageManager.draggable = v;
10250
+ }
10251
+ },
10252
+ currentSignLanguage: {
10253
+ get: () => this.signLanguageManager.currentLanguage,
10254
+ set: (v) => {
10255
+ this.signLanguageManager.currentLanguage = v;
10256
+ }
10257
+ }
10258
+ });
8652
10259
  this.init();
8653
10260
  }
8654
10261
  async init() {
@@ -8911,53 +10518,7 @@
8911
10518
  }
8912
10519
  this.currentSource = src;
8913
10520
  this._pendingSource = null;
8914
- const sourceElements = this.sourceElements;
8915
- for (const sourceEl of sourceElements) {
8916
- const descSrc = sourceEl.getAttribute("data-desc-src");
8917
- const origSrc = sourceEl.getAttribute("data-orig-src");
8918
- if (descSrc || origSrc) {
8919
- if (!this.audioDescriptionSourceElement) {
8920
- this.audioDescriptionSourceElement = sourceEl;
8921
- }
8922
- if (origSrc) {
8923
- if (!this.originalAudioDescriptionSource) {
8924
- this.originalAudioDescriptionSource = origSrc;
8925
- }
8926
- if (!this.originalSrc) {
8927
- this.originalSrc = origSrc;
8928
- }
8929
- } else {
8930
- const currentSrcAttr = sourceEl.getAttribute("src");
8931
- if (!this.originalAudioDescriptionSource && currentSrcAttr) {
8932
- this.originalAudioDescriptionSource = currentSrcAttr;
8933
- }
8934
- if (!this.originalSrc && currentSrcAttr) {
8935
- this.originalSrc = currentSrcAttr;
8936
- }
8937
- }
8938
- if (descSrc && !this.audioDescriptionSrc) {
8939
- this.audioDescriptionSrc = descSrc;
8940
- }
8941
- }
8942
- }
8943
- const trackElements = this.trackElements;
8944
- trackElements.forEach((trackEl) => {
8945
- const trackKind = trackEl.getAttribute("kind");
8946
- const trackDescSrc = trackEl.getAttribute("data-desc-src");
8947
- if (trackKind === "captions" || trackKind === "subtitles" || trackKind === "chapters") {
8948
- if (trackDescSrc) {
8949
- this.audioDescriptionCaptionTracks.push({
8950
- trackElement: trackEl,
8951
- originalSrc: trackEl.getAttribute("src"),
8952
- describedSrc: trackDescSrc,
8953
- originalTrackSrc: trackEl.getAttribute("data-orig-src") || trackEl.getAttribute("src"),
8954
- explicit: true
8955
- // Explicitly defined, so we should validate it
8956
- });
8957
- this.log("Found explicit described ".concat(trackKind, " track: ").concat(trackEl.getAttribute("src"), " -> ").concat(trackDescSrc));
8958
- }
8959
- }
8960
- });
10521
+ this.audioDescriptionManager.initFromSourceElements(this.sourceElements, this.trackElements);
8961
10522
  if (!this.originalSrc) {
8962
10523
  this.originalSrc = src;
8963
10524
  }
@@ -9202,6 +10763,9 @@
9202
10763
  if (trackConfig.default) {
9203
10764
  track.default = true;
9204
10765
  }
10766
+ if (trackConfig.describedSrc) {
10767
+ track.setAttribute("data-desc-src", trackConfig.describedSrc);
10768
+ }
9205
10769
  const firstChild = this.element.firstChild;
9206
10770
  if (firstChild && firstChild.nodeType === Node.ELEMENT_NODE && firstChild.tagName !== "TRACK") {
9207
10771
  this.element.insertBefore(track, firstChild);
@@ -9216,6 +10780,13 @@
9216
10780
  this.audioDescriptionSrc = config.audioDescriptionSrc || null;
9217
10781
  this.signLanguageSrc = config.signLanguageSrc || null;
9218
10782
  this.originalSrc = config.src;
10783
+ if (this.audioDescriptionManager) {
10784
+ this.audioDescriptionManager.updateSources(config.audioDescriptionSrc);
10785
+ this.audioDescriptionManager.reinitialize();
10786
+ }
10787
+ if (this.signLanguageManager) {
10788
+ this.signLanguageManager.updateSources(config.signLanguageSrc, config.signLanguageSources);
10789
+ }
9219
10790
  if (wasAudioDescriptionEnabled) {
9220
10791
  this.disableAudioDescription();
9221
10792
  }
@@ -9624,8 +11195,12 @@
9624
11195
  }
9625
11196
  return null;
9626
11197
  }
9627
- // Audio Description
11198
+ // Audio Description (delegated to AudioDescriptionManager)
9628
11199
  async enableAudioDescription() {
11200
+ return this.audioDescriptionManager.enable();
11201
+ }
11202
+ // Legacy method body preserved for reference - can be removed after testing
11203
+ async _legacyEnableAudioDescription() {
9629
11204
  const hasSourceElementsWithDesc = this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
9630
11205
  const hasTracksWithDesc = this.audioDescriptionCaptionTracks.length > 0;
9631
11206
  if (!this.audioDescriptionSrc && !hasSourceElementsWithDesc && !hasTracksWithDesc) {
@@ -10358,6 +11933,10 @@
10358
11933
  this.emit("audiodescriptionenabled");
10359
11934
  }
10360
11935
  async disableAudioDescription() {
11936
+ return this.audioDescriptionManager.disable();
11937
+ }
11938
+ // Legacy method body preserved for reference - can be removed after testing
11939
+ async _legacyDisableAudioDescription() {
10361
11940
  if (!this.originalSrc) {
10362
11941
  return;
10363
11942
  }
@@ -10632,64 +12211,14 @@
10632
12211
  this.emit("audiodescriptiondisabled");
10633
12212
  }
10634
12213
  async toggleAudioDescription() {
10635
- const descriptionTrack = this.findTextTrack("descriptions");
10636
- const hasAudioDescriptionSrc = this.audioDescriptionSrc || this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
10637
- if (descriptionTrack && hasAudioDescriptionSrc) {
10638
- if (this.state.audioDescriptionEnabled) {
10639
- this._audioDescriptionDesiredState = false;
10640
- descriptionTrack.mode = "hidden";
10641
- await this.disableAudioDescription();
10642
- } else {
10643
- this._audioDescriptionDesiredState = true;
10644
- await this.enableAudioDescription();
10645
- const enableDescriptionTrack = () => {
10646
- this.invalidateTrackCache();
10647
- const descTrack = this.findTextTrack("descriptions");
10648
- if (descTrack) {
10649
- if (descTrack.mode === "disabled") {
10650
- descTrack.mode = "hidden";
10651
- this.setManagedTimeout(() => {
10652
- descTrack.mode = "showing";
10653
- }, 50);
10654
- } else {
10655
- descTrack.mode = "showing";
10656
- }
10657
- } else if (this.element.readyState < 2) {
10658
- this.setManagedTimeout(enableDescriptionTrack, 100);
10659
- }
10660
- };
10661
- if (this.element.readyState >= 1) {
10662
- this.setManagedTimeout(enableDescriptionTrack, 200);
10663
- } else {
10664
- this.element.addEventListener("loadedmetadata", () => {
10665
- this.setManagedTimeout(enableDescriptionTrack, 200);
10666
- }, { once: true });
10667
- }
10668
- }
10669
- } else if (descriptionTrack) {
10670
- if (descriptionTrack.mode === "showing") {
10671
- this._audioDescriptionDesiredState = false;
10672
- descriptionTrack.mode = "hidden";
10673
- this.state.audioDescriptionEnabled = false;
10674
- this.emit("audiodescriptiondisabled");
10675
- } else {
10676
- this._audioDescriptionDesiredState = true;
10677
- descriptionTrack.mode = "showing";
10678
- this.state.audioDescriptionEnabled = true;
10679
- this.emit("audiodescriptionenabled");
10680
- }
10681
- } else if (hasAudioDescriptionSrc) {
10682
- if (this.state.audioDescriptionEnabled) {
10683
- this._audioDescriptionDesiredState = false;
10684
- await this.disableAudioDescription();
10685
- } else {
10686
- this._audioDescriptionDesiredState = true;
10687
- await this.enableAudioDescription();
10688
- }
10689
- }
12214
+ return this.audioDescriptionManager.toggle();
10690
12215
  }
10691
- // Sign Language
12216
+ // Sign Language (delegated to SignLanguageManager)
10692
12217
  enableSignLanguage() {
12218
+ return this.signLanguageManager.enable();
12219
+ }
12220
+ // Legacy method body preserved for reference - can be removed after testing
12221
+ _legacyEnableSignLanguage() {
10693
12222
  var _a;
10694
12223
  const hasMultipleSources = Object.keys(this.signLanguageSources).length > 0;
10695
12224
  const hasSingleSource = !!this.signLanguageSrc;
@@ -10942,23 +12471,16 @@
10942
12471
  }, 150);
10943
12472
  }
10944
12473
  disableSignLanguage() {
10945
- if (this.signLanguageSettingsMenuVisible) {
10946
- this.hideSignLanguageSettingsMenu({ focusButton: false });
10947
- }
10948
- if (this.signLanguageWrapper) {
10949
- this.signLanguageWrapper.style.display = "none";
10950
- }
10951
- this.state.signLanguageEnabled = false;
10952
- this.emit("signlanguagedisabled");
12474
+ return this.signLanguageManager.disable();
10953
12475
  }
10954
12476
  toggleSignLanguage() {
10955
- if (this.state.signLanguageEnabled) {
10956
- this.disableSignLanguage();
10957
- } else {
10958
- this.enableSignLanguage();
10959
- }
12477
+ return this.signLanguageManager.toggle();
10960
12478
  }
10961
12479
  setupSignLanguageInteraction() {
12480
+ return this.signLanguageManager._setupInteraction();
12481
+ }
12482
+ // Legacy method preserved for reference
12483
+ _legacySetupSignLanguageInteraction() {
10962
12484
  if (!this.signLanguageWrapper) return;
10963
12485
  const isMobile2 = window.innerWidth < 768;
10964
12486
  const isFullscreen = this.state.fullscreen;
@@ -11096,6 +12618,10 @@
11096
12618
  return langNames[langCode] || langCode.toUpperCase();
11097
12619
  }
11098
12620
  switchSignLanguage(langCode) {
12621
+ return this.signLanguageManager.switchLanguage(langCode);
12622
+ }
12623
+ // Legacy method preserved for reference
12624
+ _legacySwitchSignLanguage(langCode) {
11099
12625
  if (!this.signLanguageSources[langCode] || !this.signLanguageVideo) {
11100
12626
  return;
11101
12627
  }
@@ -11111,6 +12637,10 @@
11111
12637
  this.emit("signlanguagelanguagechanged", langCode);
11112
12638
  }
11113
12639
  showSignLanguageSettingsMenu() {
12640
+ return this.signLanguageManager.showSettingsMenu();
12641
+ }
12642
+ // Legacy method preserved for reference
12643
+ _legacyShowSignLanguageSettingsMenu() {
11114
12644
  this.signLanguageSettingsMenuJustOpened = true;
11115
12645
  setTimeout(() => {
11116
12646
  this.signLanguageSettingsMenuJustOpened = false;
@@ -11254,25 +12784,7 @@
11254
12784
  focusFirstMenuItem(this.signLanguageSettingsMenu, ".".concat(this.options.classPrefix, "-sign-language-settings-item"));
11255
12785
  }
11256
12786
  hideSignLanguageSettingsMenu({ focusButton = true } = {}) {
11257
- if (this.signLanguageSettingsMenu) {
11258
- this.signLanguageSettingsMenu.style.display = "none";
11259
- this.signLanguageSettingsMenuVisible = false;
11260
- this.signLanguageSettingsMenuJustOpened = false;
11261
- if (this.signLanguageSettingsMenuKeyHandler) {
11262
- this.signLanguageSettingsMenu.removeEventListener("keydown", this.signLanguageSettingsMenuKeyHandler);
11263
- this.signLanguageSettingsMenuKeyHandler = null;
11264
- }
11265
- const menuItems = Array.from(this.signLanguageSettingsMenu.querySelectorAll(".".concat(this.options.classPrefix, "-sign-language-settings-item")));
11266
- menuItems.forEach((item) => {
11267
- item.setAttribute("tabindex", "-1");
11268
- });
11269
- if (this.signLanguageSettingsButton) {
11270
- this.signLanguageSettingsButton.setAttribute("aria-expanded", "false");
11271
- if (focusButton) {
11272
- this.signLanguageSettingsButton.focus({ preventScroll: true });
11273
- }
11274
- }
11275
- }
12787
+ return this.signLanguageManager.hideSettingsMenu({ focusButton });
11276
12788
  }
11277
12789
  positionSignLanguageSettingsMenuImmediate() {
11278
12790
  if (!this.signLanguageSettingsMenu || !this.signLanguageSettingsButton) return;
@@ -11376,6 +12888,13 @@
11376
12888
  }
11377
12889
  }
11378
12890
  constrainSignLanguagePosition() {
12891
+ return this.signLanguageManager.constrainPosition();
12892
+ }
12893
+ saveSignLanguagePreferences() {
12894
+ return this.signLanguageManager.savePreferences();
12895
+ }
12896
+ // Legacy methods preserved for reference - can be removed after testing
12897
+ _legacyConstrainSignLanguagePosition() {
11379
12898
  if (!this.signLanguageWrapper || !this.videoWrapper) return;
11380
12899
  if (this.signLanguageDraggable && this.signLanguageDraggable.manuallyPositioned) {
11381
12900
  return;
@@ -11425,7 +12944,7 @@
11425
12944
  this.signLanguageWrapper.style.bottom = "auto";
11426
12945
  this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter((c) => c.startsWith("vidply-sign-position-")));
11427
12946
  }
11428
- saveSignLanguagePreferences() {
12947
+ _legacySaveSignLanguagePreferences() {
11429
12948
  if (!this.signLanguageWrapper) return;
11430
12949
  this.storage.saveSignLanguagePreferences({
11431
12950
  size: {
@@ -11435,58 +12954,7 @@
11435
12954
  });
11436
12955
  }
11437
12956
  cleanupSignLanguage() {
11438
- if (this.signLanguageSettingsMenuVisible) {
11439
- this.hideSignLanguageSettingsMenu({ focusButton: false });
11440
- }
11441
- if (this.signLanguageDocumentClickHandler && this.signLanguageDocumentClickHandlerAdded) {
11442
- document.removeEventListener("mousedown", this.signLanguageDocumentClickHandler, true);
11443
- this.signLanguageDocumentClickHandlerAdded = false;
11444
- this.signLanguageDocumentClickHandler = null;
11445
- }
11446
- if (this.signLanguageSettingsHandlers) {
11447
- if (this.signLanguageSettingsButton) {
11448
- this.signLanguageSettingsButton.removeEventListener("click", this.signLanguageSettingsHandlers.settingsClick);
11449
- this.signLanguageSettingsButton.removeEventListener("keydown", this.signLanguageSettingsHandlers.settingsKeydown);
11450
- }
11451
- this.signLanguageSettingsHandlers = null;
11452
- }
11453
- if (this.signLanguageHandlers) {
11454
- this.off("play", this.signLanguageHandlers.play);
11455
- this.off("pause", this.signLanguageHandlers.pause);
11456
- this.off("timeupdate", this.signLanguageHandlers.timeupdate);
11457
- this.off("ratechange", this.signLanguageHandlers.ratechange);
11458
- if (this.signLanguageHandlers.captionChange) {
11459
- this.off("captionsenabled", this.signLanguageHandlers.captionChange);
11460
- }
11461
- this.signLanguageHandlers = null;
11462
- }
11463
- if (this.signLanguageInteractionHandlers) {
11464
- if (this.signLanguageHeader && this.signLanguageInteractionHandlers.headerKeyHandler) {
11465
- this.signLanguageHeader.removeEventListener("keydown", this.signLanguageInteractionHandlers.headerKeyHandler);
11466
- }
11467
- if (this.signLanguageWrapper && this.signLanguageInteractionHandlers.customKeyHandler) {
11468
- this.signLanguageWrapper.removeEventListener("keydown", this.signLanguageInteractionHandlers.customKeyHandler);
11469
- }
11470
- }
11471
- if (this.signLanguageDraggable) {
11472
- if (this.signLanguageDraggable.pointerResizeMode) {
11473
- this.signLanguageDraggable.disablePointerResizeMode();
11474
- }
11475
- this.signLanguageDraggable.destroy();
11476
- this.signLanguageDraggable = null;
11477
- }
11478
- this.signLanguageInteractionHandlers = null;
11479
- if (this.signLanguageWrapper && this.signLanguageWrapper.parentNode) {
11480
- if (this.signLanguageVideo) {
11481
- this.signLanguageVideo.pause();
11482
- this.signLanguageVideo.src = "";
11483
- }
11484
- this.signLanguageWrapper.parentNode.removeChild(this.signLanguageWrapper);
11485
- }
11486
- this.signLanguageWrapper = null;
11487
- this.signLanguageVideo = null;
11488
- this.signLanguageSettingsButton = null;
11489
- this.signLanguageSettingsMenu = null;
12957
+ return this.signLanguageManager.cleanup();
11490
12958
  }
11491
12959
  // Settings
11492
12960
  // Settings dialog removed - using individual control buttons instead
@@ -12721,7 +14189,7 @@
12721
14189
  id: "".concat(this.uniqueId, "-keyboard-instructions")
12722
14190
  }
12723
14191
  });
12724
- instructions.textContent = "Playlist navigation: Use Up and Down arrow keys to move between tracks. Press Page Up or Page Down to skip 5 tracks. Press Home to go to first track, End to go to last track. Press Enter or Space to play the selected track.";
14192
+ instructions.textContent = i18n.t("playlist.keyboardInstructions");
12725
14193
  this.playlistPanel.appendChild(instructions);
12726
14194
  const list = DOMUtils.createElement("ul", {
12727
14195
  className: "vidply-playlist-list",
@@ -12877,7 +14345,7 @@
12877
14345
  if (index < buttons.length - 1) {
12878
14346
  newIndex = index + 1;
12879
14347
  } else {
12880
- announcement = "End of playlist. ".concat(buttons.length, " of ").concat(buttons.length, ".");
14348
+ announcement = i18n.t("playlist.endOfPlaylist", { current: buttons.length, total: buttons.length });
12881
14349
  }
12882
14350
  break;
12883
14351
  case "ArrowUp":
@@ -12886,7 +14354,7 @@
12886
14354
  if (index > 0) {
12887
14355
  newIndex = index - 1;
12888
14356
  } else {
12889
- announcement = "Beginning of playlist. 1 of " + buttons.length + ".";
14357
+ announcement = i18n.t("playlist.beginningOfPlaylist", { total: buttons.length });
12890
14358
  }
12891
14359
  break;
12892
14360
  case "PageDown":
@@ -12894,7 +14362,7 @@
12894
14362
  e.stopPropagation();
12895
14363
  newIndex = Math.min(index + 5, buttons.length - 1);
12896
14364
  if (newIndex === buttons.length - 1 && index !== newIndex) {
12897
- announcement = "Jumped to last track. ".concat(newIndex + 1, " of ").concat(buttons.length, ".");
14365
+ announcement = i18n.t("playlist.jumpedToLastTrack", { current: newIndex + 1, total: buttons.length });
12898
14366
  }
12899
14367
  break;
12900
14368
  case "PageUp":
@@ -12902,7 +14370,7 @@
12902
14370
  e.stopPropagation();
12903
14371
  newIndex = Math.max(index - 5, 0);
12904
14372
  if (newIndex === 0 && index !== newIndex) {
12905
- announcement = "Jumped to first track. 1 of ".concat(buttons.length, ".");
14373
+ announcement = i18n.t("playlist.jumpedToFirstTrack", { total: buttons.length });
12906
14374
  }
12907
14375
  break;
12908
14376
  case "Home":
@@ -12910,7 +14378,7 @@
12910
14378
  e.stopPropagation();
12911
14379
  newIndex = 0;
12912
14380
  if (index !== 0) {
12913
- announcement = "First track. 1 of ".concat(buttons.length, ".");
14381
+ announcement = i18n.t("playlist.firstTrack", { total: buttons.length });
12914
14382
  }
12915
14383
  break;
12916
14384
  case "End":
@@ -12918,7 +14386,7 @@
12918
14386
  e.stopPropagation();
12919
14387
  newIndex = buttons.length - 1;
12920
14388
  if (index !== buttons.length - 1) {
12921
- announcement = "Last track. ".concat(buttons.length, " of ").concat(buttons.length, ".");
14389
+ announcement = i18n.t("playlist.lastTrack", { current: buttons.length, total: buttons.length });
12922
14390
  }
12923
14391
  break;
12924
14392
  }