vidply 1.0.28 → 1.0.30

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 (75) hide show
  1. package/dist/dev/vidply.HLSRenderer-UMPUDSYL.js +266 -0
  2. package/dist/dev/vidply.HLSRenderer-UMPUDSYL.js.map +7 -0
  3. package/dist/dev/vidply.HTML5Renderer-FXBZQL6Y.js +12 -0
  4. package/dist/dev/vidply.HTML5Renderer-FXBZQL6Y.js.map +7 -0
  5. package/dist/dev/{vidply.TranscriptManager-QSF2PWUN.js → vidply.TranscriptManager-T677KF4N.js} +4 -5
  6. package/dist/dev/{vidply.TranscriptManager-QSF2PWUN.js.map → vidply.TranscriptManager-T677KF4N.js.map} +2 -2
  7. package/dist/dev/{vidply.chunk-SRM7VNHG.js → vidply.chunk-GS2JX5RQ.js} +136 -95
  8. package/dist/dev/vidply.chunk-GS2JX5RQ.js.map +7 -0
  9. package/dist/dev/vidply.chunk-W2LSBD6Y.js +251 -0
  10. package/dist/dev/vidply.chunk-W2LSBD6Y.js.map +7 -0
  11. package/dist/dev/vidply.esm.js +1880 -258
  12. package/dist/dev/vidply.esm.js.map +4 -4
  13. package/dist/legacy/vidply.js +2056 -365
  14. package/dist/legacy/vidply.js.map +4 -4
  15. package/dist/legacy/vidply.min.js +1 -1
  16. package/dist/legacy/vidply.min.meta.json +111 -25
  17. package/dist/prod/vidply.HLSRenderer-3CG7BZKA.min.js +6 -0
  18. package/dist/prod/vidply.HTML5Renderer-KKW3OLHM.min.js +6 -0
  19. package/dist/prod/vidply.TranscriptManager-WFZSW6NR.min.js +6 -0
  20. package/dist/prod/vidply.chunk-34RH2THY.min.js +6 -0
  21. package/dist/prod/vidply.chunk-LGTJRPUL.min.js +6 -0
  22. package/dist/prod/vidply.esm.min.js +8 -8
  23. package/dist/vidply.css +20 -1
  24. package/dist/vidply.esm.min.meta.json +120 -34
  25. package/dist/vidply.min.css +1 -1
  26. package/package.json +2 -2
  27. package/src/controls/ControlBar.js +182 -10
  28. package/src/controls/TranscriptManager.js +7 -7
  29. package/src/core/AudioDescriptionManager.js +701 -0
  30. package/src/core/Player.js +203 -256
  31. package/src/core/SignLanguageManager.js +1134 -0
  32. package/src/renderers/HTML5Renderer.js +7 -0
  33. package/src/styles/vidply.css +20 -1
  34. package/src/utils/DOMUtils.js +153 -114
  35. package/src/utils/MenuFactory.js +374 -0
  36. package/src/utils/VideoFrameCapture.js +110 -0
  37. package/dist/dev/vidply.TranscriptManager-GZKY44ON.js +0 -1744
  38. package/dist/dev/vidply.TranscriptManager-GZKY44ON.js.map +0 -7
  39. package/dist/dev/vidply.TranscriptManager-UTJBQC5B.js +0 -1744
  40. package/dist/dev/vidply.TranscriptManager-UTJBQC5B.js.map +0 -7
  41. package/dist/dev/vidply.chunk-5663PYKK.js +0 -1631
  42. package/dist/dev/vidply.chunk-5663PYKK.js.map +0 -7
  43. package/dist/dev/vidply.chunk-SRM7VNHG.js.map +0 -7
  44. package/dist/dev/vidply.chunk-UH5MTGKF.js +0 -1630
  45. package/dist/dev/vidply.chunk-UH5MTGKF.js.map +0 -7
  46. package/dist/dev/vidply.de-RXAJM5QE.js +0 -181
  47. package/dist/dev/vidply.de-RXAJM5QE.js.map +0 -7
  48. package/dist/dev/vidply.de-THBIMP4S.js +0 -180
  49. package/dist/dev/vidply.de-THBIMP4S.js.map +0 -7
  50. package/dist/dev/vidply.es-6VWDNNNL.js +0 -180
  51. package/dist/dev/vidply.es-6VWDNNNL.js.map +0 -7
  52. package/dist/dev/vidply.es-SADVLJTQ.js +0 -181
  53. package/dist/dev/vidply.es-SADVLJTQ.js.map +0 -7
  54. package/dist/dev/vidply.fr-V3VAYBBT.js +0 -181
  55. package/dist/dev/vidply.fr-V3VAYBBT.js.map +0 -7
  56. package/dist/dev/vidply.fr-WHTWCHWT.js +0 -180
  57. package/dist/dev/vidply.fr-WHTWCHWT.js.map +0 -7
  58. package/dist/dev/vidply.ja-BFQNPOFI.js +0 -180
  59. package/dist/dev/vidply.ja-BFQNPOFI.js.map +0 -7
  60. package/dist/dev/vidply.ja-KL2TLZGJ.js +0 -181
  61. package/dist/dev/vidply.ja-KL2TLZGJ.js.map +0 -7
  62. package/dist/prod/vidply.TranscriptManager-DZ2WZU3K.min.js +0 -6
  63. package/dist/prod/vidply.TranscriptManager-E5QHGFIR.min.js +0 -6
  64. package/dist/prod/vidply.TranscriptManager-UZ6DUFB6.min.js +0 -6
  65. package/dist/prod/vidply.chunk-5DWTMWEO.min.js +0 -6
  66. package/dist/prod/vidply.chunk-IBNYTGGM.min.js +0 -6
  67. package/dist/prod/vidply.chunk-MBUR3U5L.min.js +0 -6
  68. package/dist/prod/vidply.de-HGJBCLLE.min.js +0 -6
  69. package/dist/prod/vidply.de-SWFW4HYT.min.js +0 -6
  70. package/dist/prod/vidply.es-7BJ2DJAY.min.js +0 -6
  71. package/dist/prod/vidply.es-CZEBXCZN.min.js +0 -6
  72. package/dist/prod/vidply.fr-DPVR5DFY.min.js +0 -6
  73. package/dist/prod/vidply.fr-HFOL7MWA.min.js +0 -6
  74. package/dist/prod/vidply.ja-PEBVWKVH.min.js +0 -6
  75. package/dist/prod/vidply.ja-QTVU5C25.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
  }
@@ -1695,6 +1741,11 @@
1695
1741
  this.media.addEventListener("loadedmetadata", () => {
1696
1742
  this.player.state.duration = this.media.duration;
1697
1743
  this.player.emit("loadedmetadata");
1744
+ if (this.media.tagName === "VIDEO") {
1745
+ this.player.autoGeneratePoster().catch((error) => {
1746
+ this.player.log("Failed to auto-generate poster:", error, "warn");
1747
+ });
1748
+ }
1698
1749
  });
1699
1750
  this.media.addEventListener("play", () => {
1700
1751
  this.player.state.playing = true;
@@ -3268,8 +3319,7 @@
3268
3319
  descriptionTrack = textTracks.find((track) => track.kind === "descriptions");
3269
3320
  }
3270
3321
  const metadataTrack = textTracks.find((track) => track.kind === "metadata");
3271
- const hasDescriptionTrack = descriptionTrack && this.player.state.audioDescriptionEnabled;
3272
- if (!captionTrack && !hasDescriptionTrack && !metadataTrack) {
3322
+ if (!captionTrack && !descriptionTrack && !metadataTrack) {
3273
3323
  this.showNoTranscriptMessage();
3274
3324
  return;
3275
3325
  }
@@ -3307,7 +3357,7 @@
3307
3357
  allCues.push({ cue, type: "caption" });
3308
3358
  });
3309
3359
  }
3310
- if (descriptionTrack && descriptionTrack.cues && this.player.state.audioDescriptionEnabled) {
3360
+ if (descriptionTrack && descriptionTrack.cues) {
3311
3361
  Array.from(descriptionTrack.cues).forEach((cue) => {
3312
3362
  allCues.push({ cue, type: "description" });
3313
3363
  });
@@ -5501,6 +5551,110 @@
5501
5551
  init_Icons();
5502
5552
  init_i18n();
5503
5553
  init_FocusUtils();
5554
+
5555
+ // src/utils/PerformanceUtils.js
5556
+ function debounce(func, wait = 100) {
5557
+ let timeout;
5558
+ return function executedFunction(...args) {
5559
+ const later = () => {
5560
+ clearTimeout(timeout);
5561
+ func(...args);
5562
+ };
5563
+ clearTimeout(timeout);
5564
+ timeout = setTimeout(later, wait);
5565
+ };
5566
+ }
5567
+ function isMobile(breakpoint = 768) {
5568
+ return window.innerWidth < breakpoint;
5569
+ }
5570
+ function rafWithTimeout(callback, timeout = 100) {
5571
+ let called = false;
5572
+ const execute = () => {
5573
+ if (!called) {
5574
+ called = true;
5575
+ callback();
5576
+ }
5577
+ };
5578
+ requestAnimationFrame(execute);
5579
+ setTimeout(execute, timeout);
5580
+ }
5581
+
5582
+ // src/utils/VideoFrameCapture.js
5583
+ async function captureVideoFrame(video, time, options = {}) {
5584
+ if (!video || video.tagName !== "VIDEO") {
5585
+ return null;
5586
+ }
5587
+ const {
5588
+ restoreState = true,
5589
+ quality = 0.9,
5590
+ maxWidth,
5591
+ maxHeight
5592
+ } = options;
5593
+ const wasPlaying = !video.paused;
5594
+ const originalTime = video.currentTime;
5595
+ const originalMuted = video.muted;
5596
+ if (restoreState) {
5597
+ video.muted = true;
5598
+ }
5599
+ return new Promise((resolve) => {
5600
+ const captureFrame = () => {
5601
+ try {
5602
+ let width = video.videoWidth || 640;
5603
+ let height = video.videoHeight || 360;
5604
+ if (maxWidth && width > maxWidth) {
5605
+ const ratio = maxWidth / width;
5606
+ width = maxWidth;
5607
+ height = Math.round(height * ratio);
5608
+ }
5609
+ if (maxHeight && height > maxHeight) {
5610
+ const ratio = maxHeight / height;
5611
+ height = maxHeight;
5612
+ width = Math.round(width * ratio);
5613
+ }
5614
+ const canvas = document.createElement("canvas");
5615
+ canvas.width = width;
5616
+ canvas.height = height;
5617
+ const ctx = canvas.getContext("2d");
5618
+ ctx.drawImage(video, 0, 0, width, height);
5619
+ const dataURL = canvas.toDataURL("image/jpeg", quality);
5620
+ if (restoreState) {
5621
+ video.currentTime = originalTime;
5622
+ video.muted = originalMuted;
5623
+ if (wasPlaying && !video.paused) {
5624
+ video.play().catch(() => {
5625
+ });
5626
+ }
5627
+ }
5628
+ resolve(dataURL);
5629
+ } catch (error) {
5630
+ if (restoreState) {
5631
+ video.currentTime = originalTime;
5632
+ video.muted = originalMuted;
5633
+ if (wasPlaying && !video.paused) {
5634
+ video.play().catch(() => {
5635
+ });
5636
+ }
5637
+ }
5638
+ resolve(null);
5639
+ }
5640
+ };
5641
+ const onSeeked = () => {
5642
+ video.removeEventListener("seeked", onSeeked);
5643
+ requestAnimationFrame(() => {
5644
+ requestAnimationFrame(captureFrame);
5645
+ });
5646
+ };
5647
+ const timeDiff = Math.abs(video.currentTime - time);
5648
+ if (timeDiff < 0.1 && video.readyState >= 2) {
5649
+ captureFrame();
5650
+ } else {
5651
+ video.addEventListener("seeked", onSeeked);
5652
+ video.currentTime = time;
5653
+ }
5654
+ });
5655
+ }
5656
+
5657
+ // src/controls/ControlBar.js
5504
5658
  var ControlBar = class {
5505
5659
  constructor(player) {
5506
5660
  this.player = player;
@@ -5520,17 +5674,13 @@
5520
5674
  this.setupAutoHide();
5521
5675
  this.setupOverflowDetection();
5522
5676
  }
5523
- // Helper method to check if we're on a mobile device
5524
- isMobile() {
5525
- return window.innerWidth < 768;
5526
- }
5527
5677
  // Helper method to detect touch devices
5528
5678
  isTouchDevice() {
5529
5679
  return "ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
5530
5680
  }
5531
5681
  // Smart menu positioning to avoid overflow
5532
5682
  positionMenu(menu, button, immediate = false) {
5533
- const isMobile2 = this.isMobile();
5683
+ const mobile = isMobile();
5534
5684
  const isOverflowMenu = menu.classList.contains("".concat(this.player.options.classPrefix, "-overflow-menu-list"));
5535
5685
  const isFullscreen = this.player.state.fullscreen;
5536
5686
  if (isFullscreen && menu.parentElement === this.player.container) {
@@ -5571,7 +5721,7 @@
5571
5721
  }
5572
5722
  return;
5573
5723
  }
5574
- if (isMobile2) {
5724
+ if (mobile) {
5575
5725
  const isVolumeMenu = menu.classList.contains("".concat(this.player.options.classPrefix, "-volume-menu"));
5576
5726
  const doMobilePositioning = () => {
5577
5727
  const parentContainer = button.parentElement;
@@ -6141,13 +6291,117 @@
6141
6291
  this.controls.progressTooltip = DOMUtils.createElement("div", {
6142
6292
  className: "".concat(this.player.options.classPrefix, "-progress-tooltip")
6143
6293
  });
6294
+ this.controls.progressPreview = DOMUtils.createElement("div", {
6295
+ className: "".concat(this.player.options.classPrefix, "-progress-preview"),
6296
+ attributes: {
6297
+ "aria-hidden": "true"
6298
+ }
6299
+ });
6300
+ this.controls.progressTooltip.appendChild(this.controls.progressPreview);
6301
+ this.controls.progressTooltipTime = DOMUtils.createElement("div", {
6302
+ className: "".concat(this.player.options.classPrefix, "-progress-tooltip-time")
6303
+ });
6304
+ this.controls.progressTooltip.appendChild(this.controls.progressTooltipTime);
6144
6305
  progressContainer.appendChild(this.controls.buffered);
6145
6306
  progressContainer.appendChild(this.controls.played);
6146
6307
  this.controls.played.appendChild(this.controls.progressHandle);
6147
6308
  progressContainer.appendChild(this.controls.progressTooltip);
6148
6309
  this.controls.progress = progressContainer;
6310
+ this.initPreviewThumbnail();
6149
6311
  this.setupProgressBarEvents();
6150
6312
  }
6313
+ /**
6314
+ * Initialize preview thumbnail functionality for HTML5 video
6315
+ */
6316
+ initPreviewThumbnail() {
6317
+ this.previewThumbnailCache = /* @__PURE__ */ new Map();
6318
+ this.previewVideo = null;
6319
+ this.currentPreviewTime = null;
6320
+ this.previewThumbnailTimeout = null;
6321
+ this.previewSupported = false;
6322
+ const isVideo = this.player.element && this.player.element.tagName === "VIDEO";
6323
+ if (!isVideo) {
6324
+ return;
6325
+ }
6326
+ const renderer = this.player.renderer;
6327
+ const hasVideoMedia = renderer && renderer.media && renderer.media.tagName === "VIDEO";
6328
+ const isHTML5Renderer = renderer && (renderer.constructor.name === "HTML5Renderer" || renderer.constructor.name === "HLSRenderer" && hasVideoMedia);
6329
+ this.previewSupported = isHTML5Renderer && hasVideoMedia;
6330
+ if (this.previewSupported) {
6331
+ this.previewVideo = document.createElement("video");
6332
+ this.previewVideo.muted = true;
6333
+ this.previewVideo.preload = "metadata";
6334
+ this.previewVideo.style.position = "absolute";
6335
+ this.previewVideo.style.visibility = "hidden";
6336
+ this.previewVideo.style.width = "1px";
6337
+ this.previewVideo.style.height = "1px";
6338
+ this.previewVideo.style.top = "-9999px";
6339
+ const mainVideo = renderer.media || this.player.element;
6340
+ if (mainVideo.src) {
6341
+ this.previewVideo.src = mainVideo.src;
6342
+ } else {
6343
+ const source = mainVideo.querySelector("source");
6344
+ if (source) {
6345
+ this.previewVideo.src = source.src;
6346
+ }
6347
+ }
6348
+ this.previewVideo.addEventListener("error", () => {
6349
+ this.player.log("Preview video failed to load", "warn");
6350
+ this.previewSupported = false;
6351
+ });
6352
+ if (this.player.container) {
6353
+ this.player.container.appendChild(this.previewVideo);
6354
+ }
6355
+ }
6356
+ }
6357
+ /**
6358
+ * Generate preview thumbnail for a specific time
6359
+ * @param {number} time - Time in seconds
6360
+ * @returns {Promise<string>} Data URL of the thumbnail
6361
+ */
6362
+ async generatePreviewThumbnail(time) {
6363
+ if (!this.previewSupported || !this.previewVideo) {
6364
+ return null;
6365
+ }
6366
+ const cacheKey = Math.floor(time);
6367
+ if (this.previewThumbnailCache.has(cacheKey)) {
6368
+ return this.previewThumbnailCache.get(cacheKey);
6369
+ }
6370
+ const dataURL = await captureVideoFrame(this.previewVideo, time, {
6371
+ restoreState: false,
6372
+ quality: 0.8,
6373
+ maxWidth: 160,
6374
+ maxHeight: 90
6375
+ });
6376
+ if (dataURL) {
6377
+ if (this.previewThumbnailCache.size > 20) {
6378
+ const firstKey = this.previewThumbnailCache.keys().next().value;
6379
+ this.previewThumbnailCache.delete(firstKey);
6380
+ }
6381
+ this.previewThumbnailCache.set(cacheKey, dataURL);
6382
+ }
6383
+ return dataURL;
6384
+ }
6385
+ /**
6386
+ * Update preview thumbnail display
6387
+ * @param {number} time - Time in seconds
6388
+ */
6389
+ async updatePreviewThumbnail(time) {
6390
+ if (!this.previewSupported) {
6391
+ return;
6392
+ }
6393
+ if (this.previewThumbnailTimeout) {
6394
+ clearTimeout(this.previewThumbnailTimeout);
6395
+ }
6396
+ this.previewThumbnailTimeout = setTimeout(async () => {
6397
+ const thumbnail = await this.generatePreviewThumbnail(time);
6398
+ if (thumbnail && this.controls.progressPreview) {
6399
+ this.controls.progressPreview.style.backgroundImage = "url(".concat(thumbnail, ")");
6400
+ this.controls.progressPreview.style.display = "block";
6401
+ }
6402
+ this.currentPreviewTime = time;
6403
+ }, 100);
6404
+ }
6151
6405
  setupProgressBarEvents() {
6152
6406
  const progress = this.controls.progress;
6153
6407
  const updateProgress = (clientX) => {
@@ -6174,13 +6428,19 @@
6174
6428
  progress.addEventListener("mousemove", (e) => {
6175
6429
  if (!this.isDraggingProgress) {
6176
6430
  const { time } = updateProgress(e.clientX);
6177
- this.controls.progressTooltip.textContent = TimeUtils.formatTime(time);
6178
- this.controls.progressTooltip.style.left = "".concat(e.clientX - progress.getBoundingClientRect().left, "px");
6431
+ const rect = progress.getBoundingClientRect();
6432
+ const left = e.clientX - rect.left;
6433
+ this.controls.progressTooltipTime.textContent = TimeUtils.formatTime(time);
6434
+ this.controls.progressTooltip.style.left = "".concat(left, "px");
6179
6435
  this.controls.progressTooltip.style.display = "block";
6436
+ this.updatePreviewThumbnail(time);
6180
6437
  }
6181
6438
  });
6182
6439
  progress.addEventListener("mouseleave", () => {
6183
6440
  this.controls.progressTooltip.style.display = "none";
6441
+ if (this.previewThumbnailTimeout) {
6442
+ clearTimeout(this.previewThumbnailTimeout);
6443
+ }
6184
6444
  });
6185
6445
  progress.addEventListener("keydown", (e) => {
6186
6446
  if (e.key === "ArrowLeft") {
@@ -7927,6 +8187,20 @@
7927
8187
  hide() {
7928
8188
  this.element.style.display = "none";
7929
8189
  }
8190
+ /**
8191
+ * Cleanup preview thumbnail resources
8192
+ */
8193
+ cleanupPreviewThumbnail() {
8194
+ if (this.previewThumbnailTimeout) {
8195
+ clearTimeout(this.previewThumbnailTimeout);
8196
+ this.previewThumbnailTimeout = null;
8197
+ }
8198
+ if (this.previewVideo && this.previewVideo.parentNode) {
8199
+ this.previewVideo.parentNode.removeChild(this.previewVideo);
8200
+ this.previewVideo = null;
8201
+ }
8202
+ this.previewThumbnailCache.clear();
8203
+ }
7930
8204
  destroy() {
7931
8205
  if (this.hideTimeout) {
7932
8206
  clearTimeout(this.hideTimeout);
@@ -7934,6 +8208,7 @@
7934
8208
  if (this.overflowResizeObserver) {
7935
8209
  this.overflowResizeObserver.disconnect();
7936
8210
  }
8211
+ this.cleanupPreviewThumbnail();
7937
8212
  if (this.element && this.element.parentNode) {
7938
8213
  this.element.parentNode.removeChild(this.element);
7939
8214
  }
@@ -7944,35 +8219,6 @@
7944
8219
  init_DOMUtils();
7945
8220
  init_i18n();
7946
8221
  init_StorageManager();
7947
-
7948
- // src/utils/PerformanceUtils.js
7949
- function debounce(func, wait = 100) {
7950
- let timeout;
7951
- return function executedFunction(...args) {
7952
- const later = () => {
7953
- clearTimeout(timeout);
7954
- func(...args);
7955
- };
7956
- clearTimeout(timeout);
7957
- timeout = setTimeout(later, wait);
7958
- };
7959
- }
7960
- function isMobile(breakpoint = 768) {
7961
- return window.innerWidth < breakpoint;
7962
- }
7963
- function rafWithTimeout(callback, timeout = 100) {
7964
- let called = false;
7965
- const execute = () => {
7966
- if (!called) {
7967
- called = true;
7968
- callback();
7969
- }
7970
- };
7971
- requestAnimationFrame(execute);
7972
- setTimeout(execute, timeout);
7973
- }
7974
-
7975
- // src/controls/CaptionManager.js
7976
8222
  var CaptionManager = class {
7977
8223
  constructor(player) {
7978
8224
  this.player = player;
@@ -8504,49 +8750,1523 @@
8504
8750
  init_DraggableResizable();
8505
8751
  init_MenuUtils();
8506
8752
  init_FormUtils();
8507
- var playerInstanceCounter = 0;
8508
- var Player = class _Player extends EventEmitter {
8509
- constructor(element, options = {}) {
8510
- super();
8511
- this.element = typeof element === "string" ? document.querySelector(element) : element;
8512
- if (!this.element) {
8513
- throw new Error("VidPly: Element not found");
8514
- }
8515
- playerInstanceCounter++;
8516
- this.instanceId = playerInstanceCounter;
8517
- if (this.element.tagName !== "VIDEO" && this.element.tagName !== "AUDIO") {
8518
- const mediaType = options.mediaType || "video";
8519
- const mediaElement = document.createElement(mediaType);
8520
- Array.from(this.element.attributes).forEach((attr) => {
8521
- if (attr.name !== "id" && attr.name !== "class" && !attr.name.startsWith("data-")) {
8522
- mediaElement.setAttribute(attr.name, attr.value);
8753
+
8754
+ // src/core/AudioDescriptionManager.js
8755
+ var AudioDescriptionManager = class {
8756
+ constructor(player) {
8757
+ this.player = player;
8758
+ this.enabled = false;
8759
+ this.desiredState = false;
8760
+ this.src = player.options.audioDescriptionSrc;
8761
+ this.sourceElement = null;
8762
+ this.originalSource = null;
8763
+ this.captionTracks = [];
8764
+ }
8765
+ /**
8766
+ * Initialize audio description from source elements
8767
+ * Called during player initialization
8768
+ */
8769
+ initFromSourceElements(sourceElements, trackElements) {
8770
+ for (const sourceEl of sourceElements) {
8771
+ const descSrc = sourceEl.getAttribute("data-desc-src");
8772
+ const origSrc = sourceEl.getAttribute("data-orig-src");
8773
+ if (descSrc || origSrc) {
8774
+ if (!this.sourceElement) {
8775
+ this.sourceElement = sourceEl;
8523
8776
  }
8524
- });
8525
- const tracks = this.element.querySelectorAll("track");
8526
- tracks.forEach((track) => {
8527
- mediaElement.appendChild(track.cloneNode(true));
8528
- });
8529
- this.element.innerHTML = "";
8530
- this.element.appendChild(mediaElement);
8531
- this.element = mediaElement;
8777
+ if (origSrc) {
8778
+ if (!this.originalSource) {
8779
+ this.originalSource = origSrc;
8780
+ }
8781
+ if (!this.player.originalSrc) {
8782
+ this.player.originalSrc = origSrc;
8783
+ }
8784
+ } else {
8785
+ const currentSrcAttr = sourceEl.getAttribute("src");
8786
+ if (!this.originalSource && currentSrcAttr) {
8787
+ this.originalSource = currentSrcAttr;
8788
+ }
8789
+ if (!this.player.originalSrc && currentSrcAttr) {
8790
+ this.player.originalSrc = currentSrcAttr;
8791
+ }
8792
+ }
8793
+ if (descSrc && !this.src) {
8794
+ this.src = descSrc;
8795
+ }
8796
+ }
8532
8797
  }
8533
- this._originalElement = this.element;
8534
- this.options = __spreadValues({
8535
- // Display
8536
- width: null,
8537
- height: null,
8538
- poster: null,
8539
- responsive: true,
8540
- fillContainer: false,
8541
- // Playback
8542
- autoplay: false,
8543
- loop: false,
8544
- muted: false,
8545
- volume: 0.8,
8546
- playbackSpeed: 1,
8547
- preload: "metadata",
8548
- startTime: 0,
8549
- playsInline: true,
8798
+ trackElements.forEach((trackEl) => {
8799
+ const trackKind = trackEl.getAttribute("kind");
8800
+ const trackDescSrc = trackEl.getAttribute("data-desc-src");
8801
+ if ((trackKind === "captions" || trackKind === "subtitles" || trackKind === "chapters" || trackKind === "descriptions") && trackDescSrc) {
8802
+ this.captionTracks.push({
8803
+ trackElement: trackEl,
8804
+ originalSrc: trackEl.getAttribute("src"),
8805
+ describedSrc: trackDescSrc,
8806
+ originalTrackSrc: trackEl.getAttribute("data-orig-src") || trackEl.getAttribute("src"),
8807
+ explicit: true
8808
+ });
8809
+ this.player.log("Found explicit described ".concat(trackKind, " track: ").concat(trackEl.getAttribute("src"), " -> ").concat(trackDescSrc));
8810
+ }
8811
+ });
8812
+ }
8813
+ /**
8814
+ * Check if audio description is available
8815
+ */
8816
+ isAvailable() {
8817
+ const hasSourceElementsWithDesc = this.player.sourceElements.some(
8818
+ (el) => el.getAttribute("data-desc-src")
8819
+ );
8820
+ return !!(this.src || hasSourceElementsWithDesc || this.captionTracks.length > 0);
8821
+ }
8822
+ /**
8823
+ * Enable audio description
8824
+ */
8825
+ async enable() {
8826
+ const hasSourceElementsWithDesc = this.player.sourceElements.some(
8827
+ (el) => el.getAttribute("data-desc-src")
8828
+ );
8829
+ const hasTracksWithDesc = this.captionTracks.length > 0;
8830
+ if (!this.src && !hasSourceElementsWithDesc && !hasTracksWithDesc) {
8831
+ console.warn("VidPly: No audio description source, source elements, or tracks provided");
8832
+ return;
8833
+ }
8834
+ this.desiredState = true;
8835
+ const currentTime = this.player.state.currentTime;
8836
+ const wasPlaying = this.player.state.playing;
8837
+ const posterValue = this.player.element.poster || this.player.element.getAttribute("poster") || this.player.options.poster;
8838
+ const shouldKeepPoster = currentTime < 0.1 && !wasPlaying;
8839
+ const currentCaptionText = this._getCurrentCaptionText();
8840
+ if (this.sourceElement) {
8841
+ await this._enableWithSourceElement(currentTime, wasPlaying, posterValue, shouldKeepPoster, currentCaptionText);
8842
+ } else {
8843
+ await this._enableWithDirectSrc(currentTime, wasPlaying, posterValue, shouldKeepPoster);
8844
+ }
8845
+ }
8846
+ /**
8847
+ * Disable audio description
8848
+ */
8849
+ async disable() {
8850
+ if (!this.player.originalSrc) {
8851
+ return;
8852
+ }
8853
+ this.desiredState = false;
8854
+ const currentTime = this.player.state.currentTime;
8855
+ const wasPlaying = this.player.state.playing;
8856
+ const posterValue = this.player.element.poster || this.player.element.getAttribute("poster") || this.player.options.poster;
8857
+ const shouldKeepPoster = currentTime < 0.1 && !wasPlaying;
8858
+ const currentCaptionText = this._getCurrentCaptionText();
8859
+ if (this.sourceElement) {
8860
+ await this._disableWithSourceElement(currentTime, wasPlaying, posterValue, shouldKeepPoster, currentCaptionText);
8861
+ } else {
8862
+ await this._disableWithDirectSrc(currentTime, wasPlaying, posterValue);
8863
+ }
8864
+ }
8865
+ /**
8866
+ * Toggle audio description
8867
+ */
8868
+ async toggle() {
8869
+ const descriptionTrack = this.player.findTextTrack("descriptions");
8870
+ const hasAudioDescriptionSrc = this.isAvailable();
8871
+ if (descriptionTrack && !hasAudioDescriptionSrc) {
8872
+ if (descriptionTrack.mode === "showing") {
8873
+ descriptionTrack.mode = "hidden";
8874
+ this.enabled = false;
8875
+ this.player.emit("audiodescriptiondisabled");
8876
+ } else {
8877
+ descriptionTrack.mode = "showing";
8878
+ this.enabled = true;
8879
+ this.player.emit("audiodescriptionenabled");
8880
+ }
8881
+ } else if (descriptionTrack && hasAudioDescriptionSrc) {
8882
+ if (this.enabled) {
8883
+ this.desiredState = false;
8884
+ await this.disable();
8885
+ } else {
8886
+ descriptionTrack.mode = "showing";
8887
+ this.desiredState = true;
8888
+ await this.enable();
8889
+ }
8890
+ } else if (hasAudioDescriptionSrc) {
8891
+ if (this.enabled) {
8892
+ this.desiredState = false;
8893
+ await this.disable();
8894
+ } else {
8895
+ this.desiredState = true;
8896
+ await this.enable();
8897
+ }
8898
+ }
8899
+ }
8900
+ /**
8901
+ * Get current caption text for synchronization
8902
+ */
8903
+ _getCurrentCaptionText() {
8904
+ if (this.player.captionManager && this.player.captionManager.currentTrack && this.player.captionManager.currentCue) {
8905
+ return this.player.captionManager.currentCue.text;
8906
+ }
8907
+ return null;
8908
+ }
8909
+ /**
8910
+ * Validate that a track URL exists
8911
+ */
8912
+ async _validateTrackExists(url) {
8913
+ try {
8914
+ const response = await fetch(url, { method: "HEAD" });
8915
+ return response.ok;
8916
+ } catch (e) {
8917
+ return false;
8918
+ }
8919
+ }
8920
+ /**
8921
+ * Swap caption tracks to described versions
8922
+ */
8923
+ async _swapCaptionTracks(toDescribed = true) {
8924
+ if (this.captionTracks.length === 0) return [];
8925
+ const swappedTracks = [];
8926
+ const validationPromises = this.captionTracks.map(async (trackInfo) => {
8927
+ if (trackInfo.trackElement && trackInfo.describedSrc) {
8928
+ if (trackInfo.explicit === true) {
8929
+ try {
8930
+ const exists = await this._validateTrackExists(
8931
+ toDescribed ? trackInfo.describedSrc : trackInfo.originalSrc
8932
+ );
8933
+ return { trackInfo, exists };
8934
+ } catch (e) {
8935
+ return { trackInfo, exists: false };
8936
+ }
8937
+ }
8938
+ }
8939
+ return { trackInfo, exists: false };
8940
+ });
8941
+ const validationResults = await Promise.all(validationPromises);
8942
+ const tracksToSwap = validationResults.filter((result) => result.exists);
8943
+ if (tracksToSwap.length > 0) {
8944
+ const trackModes = /* @__PURE__ */ new Map();
8945
+ tracksToSwap.forEach(({ trackInfo }) => {
8946
+ const textTrack = trackInfo.trackElement.track;
8947
+ if (textTrack) {
8948
+ trackModes.set(trackInfo, {
8949
+ wasShowing: textTrack.mode === "showing",
8950
+ wasHidden: textTrack.mode === "hidden"
8951
+ });
8952
+ } else {
8953
+ trackModes.set(trackInfo, { wasShowing: false, wasHidden: false });
8954
+ }
8955
+ });
8956
+ const tracksToReadd = tracksToSwap.map(({ trackInfo }) => {
8957
+ const attributes = {};
8958
+ Array.from(trackInfo.trackElement.attributes).forEach((attr) => {
8959
+ attributes[attr.name] = attr.value;
8960
+ });
8961
+ const result = {
8962
+ trackInfo,
8963
+ oldSrc: trackInfo.trackElement.getAttribute("src"),
8964
+ parent: trackInfo.trackElement.parentNode,
8965
+ nextSibling: trackInfo.trackElement.nextSibling,
8966
+ attributes
8967
+ };
8968
+ trackInfo.trackElement.remove();
8969
+ return result;
8970
+ });
8971
+ this.player.element.load();
8972
+ await new Promise((resolve) => {
8973
+ setTimeout(() => {
8974
+ tracksToReadd.forEach(({ trackInfo, parent, nextSibling, attributes }) => {
8975
+ swappedTracks.push(trackInfo);
8976
+ const newTrackElement = document.createElement("track");
8977
+ const newSrc = toDescribed ? trackInfo.describedSrc : trackInfo.originalSrc;
8978
+ newTrackElement.setAttribute("src", newSrc);
8979
+ Object.keys(attributes).forEach((attrName) => {
8980
+ if (attrName !== "src" && attrName !== "data-desc-src") {
8981
+ newTrackElement.setAttribute(attrName, attributes[attrName]);
8982
+ }
8983
+ });
8984
+ const targetParent = parent || this.player.element;
8985
+ if (nextSibling && nextSibling.parentNode) {
8986
+ targetParent.insertBefore(newTrackElement, nextSibling);
8987
+ } else {
8988
+ targetParent.appendChild(newTrackElement);
8989
+ }
8990
+ trackInfo.trackElement = newTrackElement;
8991
+ });
8992
+ this.player.invalidateTrackCache();
8993
+ const setupNewTracks = () => {
8994
+ this.player.setManagedTimeout(() => {
8995
+ swappedTracks.forEach((trackInfo) => {
8996
+ const newTextTrack = trackInfo.trackElement.track;
8997
+ if (newTextTrack) {
8998
+ const modeInfo = trackModes.get(trackInfo) || { wasShowing: false, wasHidden: false };
8999
+ newTextTrack.mode = "hidden";
9000
+ const restoreMode = () => {
9001
+ if (modeInfo.wasShowing || modeInfo.wasHidden) {
9002
+ newTextTrack.mode = "hidden";
9003
+ } else {
9004
+ newTextTrack.mode = "disabled";
9005
+ }
9006
+ };
9007
+ if (newTextTrack.readyState >= 2) {
9008
+ restoreMode();
9009
+ } else {
9010
+ newTextTrack.addEventListener("load", restoreMode, { once: true });
9011
+ newTextTrack.addEventListener("error", restoreMode, { once: true });
9012
+ }
9013
+ }
9014
+ });
9015
+ }, 300);
9016
+ };
9017
+ if (this.player.element.readyState >= 1) {
9018
+ setTimeout(setupNewTracks, 200);
9019
+ } else {
9020
+ this.player.element.addEventListener("loadedmetadata", setupNewTracks, { once: true });
9021
+ setTimeout(setupNewTracks, 2e3);
9022
+ }
9023
+ resolve();
9024
+ }, 100);
9025
+ });
9026
+ }
9027
+ return swappedTracks;
9028
+ }
9029
+ /**
9030
+ * Update source elements to described versions
9031
+ */
9032
+ _updateSourceElements(toDescribed = true) {
9033
+ const sourceElements = this.player.sourceElements;
9034
+ const sourcesToUpdate = [];
9035
+ sourceElements.forEach((sourceEl) => {
9036
+ const descSrcAttr = sourceEl.getAttribute("data-desc-src");
9037
+ const currentSrc = sourceEl.getAttribute("src");
9038
+ if (descSrcAttr) {
9039
+ const type = sourceEl.getAttribute("type");
9040
+ let origSrc = sourceEl.getAttribute("data-orig-src") || currentSrc;
9041
+ sourcesToUpdate.push({
9042
+ src: toDescribed ? descSrcAttr : origSrc,
9043
+ type,
9044
+ origSrc,
9045
+ descSrc: descSrcAttr
9046
+ });
9047
+ } else {
9048
+ sourcesToUpdate.push({
9049
+ src: sourceEl.getAttribute("src"),
9050
+ type: sourceEl.getAttribute("type"),
9051
+ origSrc: null,
9052
+ descSrc: null
9053
+ });
9054
+ }
9055
+ });
9056
+ if (this.player.element.hasAttribute("src")) {
9057
+ this.player.element.removeAttribute("src");
9058
+ }
9059
+ sourceElements.forEach((sourceEl) => sourceEl.remove());
9060
+ sourcesToUpdate.forEach((sourceInfo) => {
9061
+ const newSource = document.createElement("source");
9062
+ newSource.setAttribute("src", sourceInfo.src);
9063
+ if (sourceInfo.type) {
9064
+ newSource.setAttribute("type", sourceInfo.type);
9065
+ }
9066
+ if (sourceInfo.origSrc) {
9067
+ newSource.setAttribute("data-orig-src", sourceInfo.origSrc);
9068
+ }
9069
+ if (sourceInfo.descSrc) {
9070
+ newSource.setAttribute("data-desc-src", sourceInfo.descSrc);
9071
+ }
9072
+ const firstTrack = this.player.element.querySelector("track");
9073
+ if (firstTrack) {
9074
+ this.player.element.insertBefore(newSource, firstTrack);
9075
+ } else {
9076
+ this.player.element.appendChild(newSource);
9077
+ }
9078
+ });
9079
+ this.player._sourceElementsDirty = true;
9080
+ this.player._sourceElementsCache = null;
9081
+ }
9082
+ /**
9083
+ * Wait for media to be ready
9084
+ */
9085
+ async _waitForMediaReady(needSeek = false) {
9086
+ await new Promise((resolve) => {
9087
+ if (this.player.element.readyState >= 1) {
9088
+ resolve();
9089
+ } else {
9090
+ const onLoad = () => {
9091
+ this.player.element.removeEventListener("loadedmetadata", onLoad);
9092
+ resolve();
9093
+ };
9094
+ this.player.element.addEventListener("loadedmetadata", onLoad);
9095
+ }
9096
+ });
9097
+ await new Promise((resolve) => setTimeout(resolve, 300));
9098
+ if (needSeek) {
9099
+ await new Promise((resolve) => {
9100
+ if (this.player.element.readyState >= 3) {
9101
+ resolve();
9102
+ } else {
9103
+ const onCanPlay = () => {
9104
+ this.player.element.removeEventListener("canplay", onCanPlay);
9105
+ this.player.element.removeEventListener("canplaythrough", onCanPlay);
9106
+ resolve();
9107
+ };
9108
+ this.player.element.addEventListener("canplay", onCanPlay, { once: true });
9109
+ this.player.element.addEventListener("canplaythrough", onCanPlay, { once: true });
9110
+ setTimeout(() => {
9111
+ this.player.element.removeEventListener("canplay", onCanPlay);
9112
+ this.player.element.removeEventListener("canplaythrough", onCanPlay);
9113
+ resolve();
9114
+ }, 3e3);
9115
+ }
9116
+ });
9117
+ }
9118
+ }
9119
+ /**
9120
+ * Restore playback state after source change
9121
+ */
9122
+ async _restorePlaybackState(currentTime, wasPlaying, shouldKeepPoster, currentCaptionText) {
9123
+ let syncTime = currentTime;
9124
+ if (currentCaptionText && this.player.captionManager && this.player.captionManager.tracks.length > 0) {
9125
+ await new Promise((resolve) => setTimeout(resolve, 500));
9126
+ const matchingTime = this.player.findMatchingCaptionTime(
9127
+ currentCaptionText,
9128
+ this.player.captionManager.tracks
9129
+ );
9130
+ if (matchingTime !== null) {
9131
+ syncTime = matchingTime;
9132
+ if (this.player.options.debug) {
9133
+ this.player.log("[VidPly] Syncing via caption: ".concat(currentTime, "s -> ").concat(syncTime, "s"));
9134
+ }
9135
+ }
9136
+ }
9137
+ if (syncTime > 0) {
9138
+ this.player.seek(syncTime);
9139
+ await new Promise((resolve) => setTimeout(resolve, 100));
9140
+ }
9141
+ if (wasPlaying) {
9142
+ await this.player.play();
9143
+ this.player.setManagedTimeout(() => {
9144
+ this.player.hidePosterOverlay();
9145
+ }, 100);
9146
+ } else {
9147
+ this.player.pause();
9148
+ if (!shouldKeepPoster) {
9149
+ this.player.hidePosterOverlay();
9150
+ }
9151
+ }
9152
+ }
9153
+ /**
9154
+ * Enable with source element method
9155
+ */
9156
+ async _enableWithSourceElement(currentTime, wasPlaying, posterValue, shouldKeepPoster, currentCaptionText) {
9157
+ await this._swapCaptionTracks(true);
9158
+ this._updateSourceElements(true);
9159
+ if (posterValue && this.player.element.tagName === "VIDEO") {
9160
+ this.player.element.poster = posterValue;
9161
+ }
9162
+ this.player.element.load();
9163
+ await this._waitForMediaReady(currentTime > 0 || wasPlaying);
9164
+ await this._restorePlaybackState(currentTime, wasPlaying, shouldKeepPoster, currentCaptionText);
9165
+ if (!this.desiredState) return;
9166
+ this.enabled = true;
9167
+ this.player.state.audioDescriptionEnabled = true;
9168
+ this.player.emit("audiodescriptionenabled");
9169
+ this._reloadTranscript();
9170
+ }
9171
+ /**
9172
+ * Enable with direct src method
9173
+ */
9174
+ async _enableWithDirectSrc(currentTime, wasPlaying, posterValue, shouldKeepPoster) {
9175
+ await this._swapCaptionTracks(true);
9176
+ if (posterValue && this.player.element.tagName === "VIDEO") {
9177
+ this.player.element.poster = posterValue;
9178
+ }
9179
+ this.player.element.src = this.src;
9180
+ await this._waitForMediaReady(currentTime > 0 || wasPlaying);
9181
+ if (currentTime > 0) {
9182
+ this.player.seek(currentTime);
9183
+ await new Promise((resolve) => setTimeout(resolve, 100));
9184
+ }
9185
+ if (wasPlaying) {
9186
+ await this.player.play();
9187
+ } else {
9188
+ this.player.pause();
9189
+ if (!shouldKeepPoster) {
9190
+ this.player.hidePosterOverlay();
9191
+ }
9192
+ }
9193
+ if (!this.desiredState) return;
9194
+ this.enabled = true;
9195
+ this.player.state.audioDescriptionEnabled = true;
9196
+ this.player.emit("audiodescriptionenabled");
9197
+ this._reloadTranscript();
9198
+ }
9199
+ /**
9200
+ * Disable with source element method
9201
+ */
9202
+ async _disableWithSourceElement(currentTime, wasPlaying, posterValue, shouldKeepPoster, currentCaptionText) {
9203
+ await this._swapCaptionTracks(false);
9204
+ this._updateSourceElements(false);
9205
+ if (posterValue && this.player.element.tagName === "VIDEO") {
9206
+ this.player.element.poster = posterValue;
9207
+ }
9208
+ this.player.element.load();
9209
+ this.player.invalidateTrackCache();
9210
+ await this._waitForMediaReady(currentTime > 0 || wasPlaying);
9211
+ await this._restorePlaybackState(currentTime, wasPlaying, shouldKeepPoster, currentCaptionText);
9212
+ if (this.player.captionManager) {
9213
+ this.player.captionManager.destroy();
9214
+ this.player.captionManager = new CaptionManager(this.player);
9215
+ }
9216
+ if (this.desiredState) return;
9217
+ this.enabled = false;
9218
+ this.player.state.audioDescriptionEnabled = false;
9219
+ this.player.emit("audiodescriptiondisabled");
9220
+ this._reloadTranscript();
9221
+ }
9222
+ /**
9223
+ * Disable with direct src method
9224
+ */
9225
+ async _disableWithDirectSrc(currentTime, wasPlaying, posterValue) {
9226
+ await this._swapCaptionTracks(false);
9227
+ if (posterValue && this.player.element.tagName === "VIDEO") {
9228
+ this.player.element.poster = posterValue;
9229
+ }
9230
+ const originalSrcToUse = this.originalSource || this.player.originalSrc;
9231
+ this.player.element.src = originalSrcToUse;
9232
+ this.player.element.load();
9233
+ await this._waitForMediaReady(currentTime > 0 || wasPlaying);
9234
+ if (currentTime > 0) {
9235
+ this.player.seek(currentTime);
9236
+ }
9237
+ if (wasPlaying) {
9238
+ await this.player.play();
9239
+ }
9240
+ if (this.desiredState) return;
9241
+ this.enabled = false;
9242
+ this.player.state.audioDescriptionEnabled = false;
9243
+ this.player.emit("audiodescriptiondisabled");
9244
+ this._reloadTranscript();
9245
+ }
9246
+ /**
9247
+ * Reload transcript after audio description state change
9248
+ */
9249
+ _reloadTranscript() {
9250
+ if (this.player.transcriptManager && this.player.transcriptManager.isVisible) {
9251
+ this.player.setManagedTimeout(() => {
9252
+ if (this.player.transcriptManager && this.player.transcriptManager.loadTranscriptData) {
9253
+ this.player.transcriptManager.loadTranscriptData();
9254
+ }
9255
+ }, 800);
9256
+ }
9257
+ }
9258
+ /**
9259
+ * Update sources (called when playlist changes)
9260
+ */
9261
+ updateSources(audioDescriptionSrc) {
9262
+ this.src = audioDescriptionSrc || null;
9263
+ this.enabled = false;
9264
+ this.desiredState = false;
9265
+ this.sourceElement = null;
9266
+ this.originalSource = null;
9267
+ this.captionTracks = [];
9268
+ }
9269
+ /**
9270
+ * Reinitialize from current player elements (called after playlist loads new tracks)
9271
+ */
9272
+ reinitialize() {
9273
+ this.player.invalidateTrackCache();
9274
+ this.initFromSourceElements(this.player.sourceElements, this.player.trackElements);
9275
+ }
9276
+ /**
9277
+ * Cleanup
9278
+ */
9279
+ destroy() {
9280
+ this.enabled = false;
9281
+ this.desiredState = false;
9282
+ this.captionTracks = [];
9283
+ this.sourceElement = null;
9284
+ this.originalSource = null;
9285
+ }
9286
+ };
9287
+
9288
+ // src/core/SignLanguageManager.js
9289
+ init_DOMUtils();
9290
+ init_Icons();
9291
+ init_i18n();
9292
+ init_DraggableResizable();
9293
+ init_MenuUtils();
9294
+ init_FormUtils();
9295
+ var SignLanguageManager = class {
9296
+ constructor(player) {
9297
+ this.player = player;
9298
+ this.src = player.options.signLanguageSrc;
9299
+ this.sources = player.options.signLanguageSources || {};
9300
+ this.currentLanguage = null;
9301
+ this.desiredPosition = player.options.signLanguagePosition || "bottom-right";
9302
+ this.wrapper = null;
9303
+ this.header = null;
9304
+ this.video = null;
9305
+ this.selector = null;
9306
+ this.settingsButton = null;
9307
+ this.settingsMenu = null;
9308
+ this.resizeHandles = [];
9309
+ this.enabled = false;
9310
+ this.settingsMenuVisible = false;
9311
+ this.settingsMenuJustOpened = false;
9312
+ this.documentClickHandlerAdded = false;
9313
+ this.handlers = null;
9314
+ this.settingsHandlers = null;
9315
+ this.interactionHandlers = null;
9316
+ this.draggable = null;
9317
+ this.documentClickHandler = null;
9318
+ this.settingsMenuKeyHandler = null;
9319
+ this.customKeyHandler = null;
9320
+ this.dragOptionButton = null;
9321
+ this.dragOptionText = null;
9322
+ this.resizeOptionButton = null;
9323
+ this.resizeOptionText = null;
9324
+ }
9325
+ /**
9326
+ * Check if sign language is available
9327
+ */
9328
+ isAvailable() {
9329
+ return Object.keys(this.sources).length > 0 || !!this.src;
9330
+ }
9331
+ /**
9332
+ * Enable sign language video
9333
+ */
9334
+ enable() {
9335
+ const hasMultipleSources = Object.keys(this.sources).length > 0;
9336
+ const hasSingleSource = !!this.src;
9337
+ if (!hasMultipleSources && !hasSingleSource) {
9338
+ console.warn("No sign language video source provided");
9339
+ return;
9340
+ }
9341
+ if (this.wrapper) {
9342
+ this.wrapper.style.display = "block";
9343
+ this.enabled = true;
9344
+ this.player.state.signLanguageEnabled = true;
9345
+ this.player.emit("signlanguageenabled");
9346
+ this.player.setManagedTimeout(() => {
9347
+ if (this.settingsButton && document.contains(this.settingsButton)) {
9348
+ this.settingsButton.focus({ preventScroll: true });
9349
+ }
9350
+ }, 150);
9351
+ return;
9352
+ }
9353
+ let initialLang = null;
9354
+ let initialSrc = null;
9355
+ if (hasMultipleSources) {
9356
+ initialLang = this._determineInitialLanguage();
9357
+ initialSrc = this.sources[initialLang];
9358
+ this.currentLanguage = initialLang;
9359
+ } else {
9360
+ initialSrc = this.src;
9361
+ }
9362
+ this._createWrapper();
9363
+ this._createHeader(hasMultipleSources, initialLang);
9364
+ this._createVideo(initialSrc);
9365
+ this._createResizeHandles();
9366
+ this.wrapper.appendChild(this.header);
9367
+ this.wrapper.appendChild(this.video);
9368
+ this.resizeHandles.forEach((handle) => this.wrapper.appendChild(handle));
9369
+ this._applyInitialSize();
9370
+ this.player.container.appendChild(this.wrapper);
9371
+ requestAnimationFrame(() => {
9372
+ this.constrainPosition();
9373
+ });
9374
+ this.video.currentTime = this.player.state.currentTime;
9375
+ if (!this.player.state.paused) {
9376
+ this.video.play();
9377
+ }
9378
+ this._setupInteraction();
9379
+ this._setupEventHandlers(hasMultipleSources);
9380
+ this.enabled = true;
9381
+ this.player.state.signLanguageEnabled = true;
9382
+ this.player.emit("signlanguageenabled");
9383
+ this.player.setManagedTimeout(() => {
9384
+ if (this.settingsButton && document.contains(this.settingsButton)) {
9385
+ this.settingsButton.focus({ preventScroll: true });
9386
+ }
9387
+ }, 150);
9388
+ }
9389
+ /**
9390
+ * Disable sign language video
9391
+ */
9392
+ disable() {
9393
+ if (this.settingsMenuVisible) {
9394
+ this.hideSettingsMenu({ focusButton: false });
9395
+ }
9396
+ if (this.wrapper) {
9397
+ this.wrapper.style.display = "none";
9398
+ }
9399
+ this.enabled = false;
9400
+ this.player.state.signLanguageEnabled = false;
9401
+ this.player.emit("signlanguagedisabled");
9402
+ }
9403
+ /**
9404
+ * Toggle sign language video
9405
+ */
9406
+ toggle() {
9407
+ if (this.enabled) {
9408
+ this.disable();
9409
+ } else {
9410
+ this.enable();
9411
+ }
9412
+ }
9413
+ /**
9414
+ * Switch to a different sign language
9415
+ */
9416
+ switchLanguage(langCode) {
9417
+ if (!this.sources[langCode] || !this.video) {
9418
+ return;
9419
+ }
9420
+ const currentTime = this.video.currentTime;
9421
+ const wasPlaying = !this.video.paused;
9422
+ this.video.src = this.sources[langCode];
9423
+ this.currentLanguage = langCode;
9424
+ this.video.currentTime = currentTime;
9425
+ if (wasPlaying) {
9426
+ this.video.play().catch(() => {
9427
+ });
9428
+ }
9429
+ this.player.emit("signlanguagelanguagechanged", langCode);
9430
+ }
9431
+ /**
9432
+ * Get language label
9433
+ */
9434
+ getLanguageLabel(langCode) {
9435
+ const langNames = {
9436
+ "en": "English",
9437
+ "de": "Deutsch",
9438
+ "es": "Español",
9439
+ "fr": "Français",
9440
+ "it": "Italiano",
9441
+ "ja": "日本語",
9442
+ "pt": "Português",
9443
+ "ar": "العربية",
9444
+ "hi": "हिन्दी"
9445
+ };
9446
+ return langNames[langCode] || langCode.toUpperCase();
9447
+ }
9448
+ /**
9449
+ * Determine initial sign language
9450
+ */
9451
+ _determineInitialLanguage() {
9452
+ var _a;
9453
+ if (this.player.captionManager && this.player.captionManager.currentTrack) {
9454
+ const captionLang = (_a = this.player.captionManager.currentTrack.language) == null ? void 0 : _a.toLowerCase().split("-")[0];
9455
+ if (captionLang && this.sources[captionLang]) {
9456
+ return captionLang;
9457
+ }
9458
+ }
9459
+ if (this.player.options.language) {
9460
+ const playerLang = this.player.options.language.toLowerCase().split("-")[0];
9461
+ if (this.sources[playerLang]) {
9462
+ return playerLang;
9463
+ }
9464
+ }
9465
+ return Object.keys(this.sources)[0];
9466
+ }
9467
+ /**
9468
+ * Create wrapper element
9469
+ */
9470
+ _createWrapper() {
9471
+ this.wrapper = document.createElement("div");
9472
+ this.wrapper.className = "vidply-sign-language-wrapper";
9473
+ this.wrapper.setAttribute("tabindex", "0");
9474
+ this.wrapper.setAttribute("aria-label", i18n.t("player.signLanguageDragResize"));
9475
+ }
9476
+ /**
9477
+ * Create header element
9478
+ */
9479
+ _createHeader(hasMultipleSources, initialLang) {
9480
+ const classPrefix = this.player.options.classPrefix;
9481
+ this.header = DOMUtils.createElement("div", {
9482
+ className: "".concat(classPrefix, "-sign-language-header"),
9483
+ attributes: { "tabindex": "0" }
9484
+ });
9485
+ const headerLeft = DOMUtils.createElement("div", {
9486
+ className: "".concat(classPrefix, "-sign-language-header-left")
9487
+ });
9488
+ const title = DOMUtils.createElement("h3", {
9489
+ textContent: i18n.t("player.signLanguageVideo")
9490
+ });
9491
+ this._createSettingsButton(headerLeft);
9492
+ if (hasMultipleSources) {
9493
+ this._createLanguageSelector(headerLeft, initialLang);
9494
+ }
9495
+ headerLeft.appendChild(title);
9496
+ const closeButton = this._createCloseButton();
9497
+ this.header.appendChild(headerLeft);
9498
+ this.header.appendChild(closeButton);
9499
+ this.settingsMenuVisible = false;
9500
+ this.settingsMenu = null;
9501
+ this.settingsMenuJustOpened = false;
9502
+ }
9503
+ /**
9504
+ * Create settings button
9505
+ */
9506
+ _createSettingsButton(container) {
9507
+ const classPrefix = this.player.options.classPrefix;
9508
+ const ariaLabel = i18n.t("player.signLanguageSettings");
9509
+ this.settingsButton = DOMUtils.createElement("button", {
9510
+ className: "".concat(classPrefix, "-sign-language-settings"),
9511
+ attributes: {
9512
+ "type": "button",
9513
+ "aria-label": ariaLabel,
9514
+ "aria-expanded": "false"
9515
+ }
9516
+ });
9517
+ this.settingsButton.appendChild(createIconElement("settings"));
9518
+ DOMUtils.attachTooltip(this.settingsButton, ariaLabel, classPrefix);
9519
+ this.settingsHandlers = {
9520
+ click: (e) => {
9521
+ e.preventDefault();
9522
+ e.stopPropagation();
9523
+ if (this.documentClickHandler) {
9524
+ this.settingsMenuJustOpened = true;
9525
+ setTimeout(() => {
9526
+ this.settingsMenuJustOpened = false;
9527
+ }, 100);
9528
+ }
9529
+ if (this.settingsMenuVisible) {
9530
+ this.hideSettingsMenu();
9531
+ } else {
9532
+ this.showSettingsMenu();
9533
+ }
9534
+ },
9535
+ keydown: (e) => {
9536
+ if (e.key === "d" || e.key === "D") {
9537
+ e.preventDefault();
9538
+ e.stopPropagation();
9539
+ this.toggleKeyboardDragMode();
9540
+ } else if (e.key === "r" || e.key === "R") {
9541
+ e.preventDefault();
9542
+ e.stopPropagation();
9543
+ this.toggleResizeMode();
9544
+ } else if (e.key === "Escape" && this.settingsMenuVisible) {
9545
+ e.preventDefault();
9546
+ e.stopPropagation();
9547
+ this.hideSettingsMenu();
9548
+ }
9549
+ }
9550
+ };
9551
+ this.settingsButton.addEventListener("click", this.settingsHandlers.click);
9552
+ this.settingsButton.addEventListener("keydown", this.settingsHandlers.keydown);
9553
+ container.appendChild(this.settingsButton);
9554
+ }
9555
+ /**
9556
+ * Create language selector
9557
+ */
9558
+ _createLanguageSelector(container, initialLang) {
9559
+ const classPrefix = this.player.options.classPrefix;
9560
+ const selectId = "".concat(classPrefix, "-sign-language-select-").concat(Date.now());
9561
+ const options = Object.keys(this.sources).map((langCode) => ({
9562
+ value: langCode,
9563
+ text: this.getLanguageLabel(langCode),
9564
+ selected: langCode === initialLang
9565
+ }));
9566
+ const { label, select } = createLabeledSelect({
9567
+ classPrefix,
9568
+ labelClass: "".concat(classPrefix, "-sign-language-label"),
9569
+ selectClass: "".concat(classPrefix, "-sign-language-select"),
9570
+ labelText: "settings.language",
9571
+ selectId,
9572
+ options,
9573
+ onChange: (e) => {
9574
+ e.stopPropagation();
9575
+ this.switchLanguage(e.target.value);
9576
+ }
9577
+ });
9578
+ this.selector = select;
9579
+ const selectorWrapper = DOMUtils.createElement("div", {
9580
+ className: "".concat(classPrefix, "-sign-language-selector-wrapper")
9581
+ });
9582
+ selectorWrapper.appendChild(label);
9583
+ selectorWrapper.appendChild(this.selector);
9584
+ preventDragOnElement(selectorWrapper);
9585
+ container.appendChild(selectorWrapper);
9586
+ }
9587
+ /**
9588
+ * Create close button
9589
+ */
9590
+ _createCloseButton() {
9591
+ const classPrefix = this.player.options.classPrefix;
9592
+ const ariaLabel = i18n.t("player.closeSignLanguage");
9593
+ const closeButton = DOMUtils.createElement("button", {
9594
+ className: "".concat(classPrefix, "-sign-language-close"),
9595
+ attributes: {
9596
+ "type": "button",
9597
+ "aria-label": ariaLabel
9598
+ }
9599
+ });
9600
+ closeButton.appendChild(createIconElement("close"));
9601
+ DOMUtils.attachTooltip(closeButton, ariaLabel, classPrefix);
9602
+ closeButton.addEventListener("click", () => {
9603
+ var _a, _b;
9604
+ this.disable();
9605
+ if ((_b = (_a = this.player.controlBar) == null ? void 0 : _a.controls) == null ? void 0 : _b.signLanguage) {
9606
+ setTimeout(() => {
9607
+ this.player.controlBar.controls.signLanguage.focus({ preventScroll: true });
9608
+ }, 0);
9609
+ }
9610
+ });
9611
+ return closeButton;
9612
+ }
9613
+ /**
9614
+ * Create video element
9615
+ */
9616
+ _createVideo(src) {
9617
+ this.video = document.createElement("video");
9618
+ this.video.className = "vidply-sign-language-video";
9619
+ this.video.src = src;
9620
+ this.video.setAttribute("aria-label", i18n.t("player.signLanguage"));
9621
+ this.video.muted = true;
9622
+ this.video.setAttribute("playsinline", "");
9623
+ }
9624
+ /**
9625
+ * Create resize handles
9626
+ */
9627
+ _createResizeHandles() {
9628
+ const classPrefix = this.player.options.classPrefix;
9629
+ this.resizeHandles = ["n", "s", "e", "w", "ne", "nw", "se", "sw"].map((dir) => {
9630
+ const handle = DOMUtils.createElement("div", {
9631
+ className: "".concat(classPrefix, "-sign-resize-handle ").concat(classPrefix, "-sign-resize-").concat(dir),
9632
+ attributes: {
9633
+ "data-direction": dir,
9634
+ "data-vidply-managed-resize": "true",
9635
+ "aria-hidden": "true"
9636
+ }
9637
+ });
9638
+ handle.style.display = "none";
9639
+ return handle;
9640
+ });
9641
+ }
9642
+ /**
9643
+ * Apply initial size
9644
+ */
9645
+ _applyInitialSize() {
9646
+ var _a;
9647
+ const saved = this.player.storage.getSignLanguagePreferences();
9648
+ if ((_a = saved == null ? void 0 : saved.size) == null ? void 0 : _a.width) {
9649
+ this.wrapper.style.width = saved.size.width;
9650
+ } else {
9651
+ this.wrapper.style.width = "280px";
9652
+ }
9653
+ this.wrapper.style.height = "auto";
9654
+ }
9655
+ /**
9656
+ * Setup interaction (drag and resize)
9657
+ */
9658
+ _setupInteraction() {
9659
+ const isMobile2 = window.innerWidth < 768;
9660
+ const isFullscreen = this.player.state.fullscreen;
9661
+ if (isMobile2 && !isFullscreen) {
9662
+ if (this.draggable) {
9663
+ this.draggable.destroy();
9664
+ this.draggable = null;
9665
+ }
9666
+ return;
9667
+ }
9668
+ if (this.draggable) return;
9669
+ const classPrefix = this.player.options.classPrefix;
9670
+ this.draggable = new DraggableResizable(this.wrapper, {
9671
+ dragHandle: this.header,
9672
+ resizeHandles: this.resizeHandles,
9673
+ constrainToViewport: true,
9674
+ maintainAspectRatio: true,
9675
+ minWidth: 150,
9676
+ minHeight: 100,
9677
+ classPrefix: "".concat(classPrefix, "-sign"),
9678
+ keyboardDragKey: "d",
9679
+ keyboardResizeKey: "r",
9680
+ keyboardStep: 10,
9681
+ keyboardStepLarge: 50,
9682
+ pointerResizeIndicatorText: i18n.t("player.signLanguageResizeActive"),
9683
+ onPointerResizeToggle: (enabled) => {
9684
+ this.resizeHandles.forEach((handle) => {
9685
+ handle.style.display = enabled ? "block" : "none";
9686
+ });
9687
+ },
9688
+ onDragStart: (e) => {
9689
+ 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"))) {
9690
+ return false;
9691
+ }
9692
+ return true;
9693
+ }
9694
+ });
9695
+ this._setupCustomKeyHandler();
9696
+ this.interactionHandlers = {
9697
+ draggable: this.draggable,
9698
+ customKeyHandler: this.customKeyHandler
9699
+ };
9700
+ }
9701
+ /**
9702
+ * Setup custom keyboard handler
9703
+ */
9704
+ _setupCustomKeyHandler() {
9705
+ this.customKeyHandler = (e) => {
9706
+ var _a, _b, _c, _d;
9707
+ const key = e.key.toLowerCase();
9708
+ if (this.settingsMenuVisible) return;
9709
+ if (key === "home") {
9710
+ e.preventDefault();
9711
+ e.stopPropagation();
9712
+ if (this.draggable) {
9713
+ if (this.draggable.pointerResizeMode) {
9714
+ this.draggable.disablePointerResizeMode();
9715
+ }
9716
+ this.draggable.manuallyPositioned = false;
9717
+ this.constrainPosition();
9718
+ }
9719
+ return;
9720
+ }
9721
+ if (key === "r") {
9722
+ e.preventDefault();
9723
+ e.stopPropagation();
9724
+ if (this.toggleResizeMode()) {
9725
+ this.wrapper.focus({ preventScroll: true });
9726
+ }
9727
+ return;
9728
+ }
9729
+ if (key === "escape") {
9730
+ e.preventDefault();
9731
+ e.stopPropagation();
9732
+ if ((_a = this.draggable) == null ? void 0 : _a.pointerResizeMode) {
9733
+ this.draggable.disablePointerResizeMode();
9734
+ return;
9735
+ }
9736
+ if ((_b = this.draggable) == null ? void 0 : _b.keyboardDragMode) {
9737
+ this.draggable.disableKeyboardDragMode();
9738
+ return;
9739
+ }
9740
+ this.disable();
9741
+ if ((_d = (_c = this.player.controlBar) == null ? void 0 : _c.controls) == null ? void 0 : _d.signLanguage) {
9742
+ setTimeout(() => {
9743
+ this.player.controlBar.controls.signLanguage.focus({ preventScroll: true });
9744
+ }, 0);
9745
+ }
9746
+ }
9747
+ };
9748
+ this.wrapper.addEventListener("keydown", this.customKeyHandler);
9749
+ }
9750
+ /**
9751
+ * Setup event handlers
9752
+ */
9753
+ _setupEventHandlers(hasMultipleSources) {
9754
+ this.handlers = {
9755
+ play: () => {
9756
+ if (this.video) this.video.play();
9757
+ },
9758
+ pause: () => {
9759
+ if (this.video) this.video.pause();
9760
+ },
9761
+ timeupdate: () => {
9762
+ if (this.video && Math.abs(this.video.currentTime - this.player.state.currentTime) > 0.5) {
9763
+ this.video.currentTime = this.player.state.currentTime;
9764
+ }
9765
+ },
9766
+ ratechange: () => {
9767
+ if (this.video) this.video.playbackRate = this.player.state.playbackSpeed;
9768
+ }
9769
+ };
9770
+ this.player.on("play", this.handlers.play);
9771
+ this.player.on("pause", this.handlers.pause);
9772
+ this.player.on("timeupdate", this.handlers.timeupdate);
9773
+ this.player.on("ratechange", this.handlers.ratechange);
9774
+ if (hasMultipleSources) {
9775
+ this.handlers.captionChange = () => {
9776
+ var _a, _b;
9777
+ if (((_a = this.player.captionManager) == null ? void 0 : _a.currentTrack) && this.selector) {
9778
+ const captionLang = (_b = this.player.captionManager.currentTrack.language) == null ? void 0 : _b.toLowerCase().split("-")[0];
9779
+ if (captionLang && this.sources[captionLang] && this.currentLanguage !== captionLang) {
9780
+ this.switchLanguage(captionLang);
9781
+ this.selector.value = captionLang;
9782
+ }
9783
+ }
9784
+ };
9785
+ this.player.on("captionsenabled", this.handlers.captionChange);
9786
+ }
9787
+ }
9788
+ /**
9789
+ * Constrain position within video wrapper
9790
+ */
9791
+ constrainPosition() {
9792
+ var _a;
9793
+ if (!this.wrapper || !this.player.videoWrapper) return;
9794
+ if ((_a = this.draggable) == null ? void 0 : _a.manuallyPositioned) return;
9795
+ if (!this.wrapper.style.width) {
9796
+ this.wrapper.style.width = "280px";
9797
+ }
9798
+ const videoWrapperRect = this.player.videoWrapper.getBoundingClientRect();
9799
+ const containerRect = this.player.container.getBoundingClientRect();
9800
+ const wrapperRect = this.wrapper.getBoundingClientRect();
9801
+ const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
9802
+ const videoWrapperTop = videoWrapperRect.top - containerRect.top;
9803
+ const videoWrapperWidth = videoWrapperRect.width;
9804
+ const videoWrapperHeight = videoWrapperRect.height;
9805
+ let wrapperWidth = wrapperRect.width || 280;
9806
+ let wrapperHeight = wrapperRect.height || 280 * 9 / 16;
9807
+ let left, top;
9808
+ const margin = 16;
9809
+ const controlsHeight = 95;
9810
+ const position = this.desiredPosition || "bottom-right";
9811
+ switch (position) {
9812
+ case "bottom-right":
9813
+ left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
9814
+ top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
9815
+ break;
9816
+ case "bottom-left":
9817
+ left = videoWrapperLeft + margin;
9818
+ top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
9819
+ break;
9820
+ case "top-right":
9821
+ left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
9822
+ top = videoWrapperTop + margin;
9823
+ break;
9824
+ case "top-left":
9825
+ left = videoWrapperLeft + margin;
9826
+ top = videoWrapperTop + margin;
9827
+ break;
9828
+ default:
9829
+ left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
9830
+ top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
9831
+ }
9832
+ left = Math.max(videoWrapperLeft, Math.min(left, videoWrapperLeft + videoWrapperWidth - wrapperWidth));
9833
+ top = Math.max(videoWrapperTop, Math.min(top, videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight));
9834
+ this.wrapper.style.left = "".concat(left, "px");
9835
+ this.wrapper.style.top = "".concat(top, "px");
9836
+ this.wrapper.style.right = "auto";
9837
+ this.wrapper.style.bottom = "auto";
9838
+ }
9839
+ /**
9840
+ * Show settings menu
9841
+ */
9842
+ showSettingsMenu() {
9843
+ var _a;
9844
+ this.settingsMenuJustOpened = true;
9845
+ setTimeout(() => {
9846
+ this.settingsMenuJustOpened = false;
9847
+ }, 350);
9848
+ this._addDocumentClickHandler();
9849
+ if (this.settingsMenu) {
9850
+ this.settingsMenu.style.display = "block";
9851
+ this.settingsMenuVisible = true;
9852
+ (_a = this.settingsButton) == null ? void 0 : _a.setAttribute("aria-expanded", "true");
9853
+ this._attachMenuKeyboardNavigation();
9854
+ this._positionSettingsMenu();
9855
+ this._updateDragOptionState();
9856
+ this._updateResizeOptionState();
9857
+ focusFirstMenuItem(this.settingsMenu, ".".concat(this.player.options.classPrefix, "-sign-language-settings-item"));
9858
+ return;
9859
+ }
9860
+ this._createSettingsMenu();
9861
+ }
9862
+ /**
9863
+ * Hide settings menu
9864
+ */
9865
+ hideSettingsMenu({ focusButton = true } = {}) {
9866
+ if (this.settingsMenu) {
9867
+ this.settingsMenu.style.display = "none";
9868
+ this.settingsMenuVisible = false;
9869
+ this.settingsMenuJustOpened = false;
9870
+ if (this.settingsMenuKeyHandler) {
9871
+ this.settingsMenu.removeEventListener("keydown", this.settingsMenuKeyHandler);
9872
+ this.settingsMenuKeyHandler = null;
9873
+ }
9874
+ const classPrefix = this.player.options.classPrefix;
9875
+ const menuItems = Array.from(this.settingsMenu.querySelectorAll(".".concat(classPrefix, "-sign-language-settings-item")));
9876
+ menuItems.forEach((item) => item.setAttribute("tabindex", "-1"));
9877
+ if (this.settingsButton) {
9878
+ this.settingsButton.setAttribute("aria-expanded", "false");
9879
+ if (focusButton) {
9880
+ this.settingsButton.focus({ preventScroll: true });
9881
+ }
9882
+ }
9883
+ }
9884
+ }
9885
+ /**
9886
+ * Add document click handler
9887
+ */
9888
+ _addDocumentClickHandler() {
9889
+ if (this.documentClickHandlerAdded) return;
9890
+ this.documentClickHandler = (e) => {
9891
+ if (this.settingsMenuJustOpened) return;
9892
+ if (this.settingsButton && (this.settingsButton === e.target || this.settingsButton.contains(e.target))) {
9893
+ return;
9894
+ }
9895
+ if (this.settingsMenu && this.settingsMenu.contains(e.target)) {
9896
+ return;
9897
+ }
9898
+ if (this.settingsMenuVisible) {
9899
+ this.hideSettingsMenu();
9900
+ }
9901
+ };
9902
+ setTimeout(() => {
9903
+ document.addEventListener("mousedown", this.documentClickHandler, true);
9904
+ this.documentClickHandlerAdded = true;
9905
+ }, 300);
9906
+ }
9907
+ /**
9908
+ * Create settings menu
9909
+ */
9910
+ _createSettingsMenu() {
9911
+ var _a, _b;
9912
+ const classPrefix = this.player.options.classPrefix;
9913
+ this.settingsMenu = DOMUtils.createElement("div", {
9914
+ className: "".concat(classPrefix, "-sign-language-settings-menu"),
9915
+ attributes: { "role": "menu" }
9916
+ });
9917
+ const dragOption = createMenuItem({
9918
+ classPrefix,
9919
+ itemClass: "".concat(classPrefix, "-sign-language-settings-item"),
9920
+ icon: "move",
9921
+ label: "player.enableSignDragMode",
9922
+ hasTextClass: true,
9923
+ onClick: () => {
9924
+ this.toggleKeyboardDragMode();
9925
+ this.hideSettingsMenu();
9926
+ }
9927
+ });
9928
+ dragOption.setAttribute("role", "switch");
9929
+ dragOption.setAttribute("aria-checked", "false");
9930
+ this._removeTooltipFromMenuItem(dragOption);
9931
+ this.dragOptionButton = dragOption;
9932
+ this.dragOptionText = dragOption.querySelector(".".concat(classPrefix, "-settings-text"));
9933
+ this._updateDragOptionState();
9934
+ const resizeOption = createMenuItem({
9935
+ classPrefix,
9936
+ itemClass: "".concat(classPrefix, "-sign-language-settings-item"),
9937
+ icon: "resize",
9938
+ label: "player.enableSignResizeMode",
9939
+ hasTextClass: true,
9940
+ onClick: (event) => {
9941
+ event.preventDefault();
9942
+ event.stopPropagation();
9943
+ const enabled = this.toggleResizeMode({ focus: false });
9944
+ if (enabled) {
9945
+ this.hideSettingsMenu({ focusButton: false });
9946
+ setTimeout(() => {
9947
+ if (this.wrapper) this.wrapper.focus({ preventScroll: true });
9948
+ }, 20);
9949
+ } else {
9950
+ this.hideSettingsMenu({ focusButton: true });
9951
+ }
9952
+ }
9953
+ });
9954
+ resizeOption.setAttribute("role", "switch");
9955
+ resizeOption.setAttribute("aria-checked", "false");
9956
+ this._removeTooltipFromMenuItem(resizeOption);
9957
+ this.resizeOptionButton = resizeOption;
9958
+ this.resizeOptionText = resizeOption.querySelector(".".concat(classPrefix, "-settings-text"));
9959
+ this._updateResizeOptionState();
9960
+ const closeOption = createMenuItem({
9961
+ classPrefix,
9962
+ itemClass: "".concat(classPrefix, "-sign-language-settings-item"),
9963
+ icon: "close",
9964
+ label: "transcript.closeMenu",
9965
+ onClick: () => this.hideSettingsMenu()
9966
+ });
9967
+ this._removeTooltipFromMenuItem(closeOption);
9968
+ this.settingsMenu.appendChild(dragOption);
9969
+ this.settingsMenu.appendChild(resizeOption);
9970
+ this.settingsMenu.appendChild(closeOption);
9971
+ this.settingsMenu.style.visibility = "hidden";
9972
+ this.settingsMenu.style.display = "block";
9973
+ if ((_a = this.settingsButton) == null ? void 0 : _a.parentNode) {
9974
+ this.settingsButton.insertAdjacentElement("afterend", this.settingsMenu);
9975
+ } else if (this.wrapper) {
9976
+ this.wrapper.appendChild(this.settingsMenu);
9977
+ }
9978
+ this._positionSettingsMenuImmediate();
9979
+ requestAnimationFrame(() => {
9980
+ if (this.settingsMenu) {
9981
+ this.settingsMenu.style.visibility = "visible";
9982
+ }
9983
+ });
9984
+ this._attachMenuKeyboardNavigation();
9985
+ this.settingsMenuVisible = true;
9986
+ (_b = this.settingsButton) == null ? void 0 : _b.setAttribute("aria-expanded", "true");
9987
+ this._updateDragOptionState();
9988
+ this._updateResizeOptionState();
9989
+ focusFirstMenuItem(this.settingsMenu, ".".concat(classPrefix, "-sign-language-settings-item"));
9990
+ }
9991
+ /**
9992
+ * Remove tooltip from menu item
9993
+ */
9994
+ _removeTooltipFromMenuItem(item) {
9995
+ const classPrefix = this.player.options.classPrefix;
9996
+ const tooltip = item.querySelector(".".concat(classPrefix, "-tooltip"));
9997
+ if (tooltip) tooltip.remove();
9998
+ const buttonText = item.querySelector(".".concat(classPrefix, "-button-text"));
9999
+ if (buttonText) buttonText.remove();
10000
+ }
10001
+ /**
10002
+ * Attach menu keyboard navigation
10003
+ */
10004
+ _attachMenuKeyboardNavigation() {
10005
+ if (this.settingsMenuKeyHandler) {
10006
+ this.settingsMenu.removeEventListener("keydown", this.settingsMenuKeyHandler);
10007
+ }
10008
+ this.settingsMenuKeyHandler = attachMenuKeyboardNavigation(
10009
+ this.settingsMenu,
10010
+ this.settingsButton,
10011
+ ".".concat(this.player.options.classPrefix, "-sign-language-settings-item"),
10012
+ () => this.hideSettingsMenu({ focusButton: true })
10013
+ );
10014
+ }
10015
+ /**
10016
+ * Position settings menu immediately
10017
+ */
10018
+ _positionSettingsMenuImmediate() {
10019
+ if (!this.settingsMenu || !this.settingsButton) return;
10020
+ const buttonRect = this.settingsButton.getBoundingClientRect();
10021
+ const menuRect = this.settingsMenu.getBoundingClientRect();
10022
+ const viewportWidth = window.innerWidth;
10023
+ const viewportHeight = window.innerHeight;
10024
+ const parentContainer = this.settingsButton.parentElement;
10025
+ if (!parentContainer) return;
10026
+ const parentRect = parentContainer.getBoundingClientRect();
10027
+ const buttonCenterX = buttonRect.left + buttonRect.width / 2 - parentRect.left;
10028
+ const buttonBottom = buttonRect.bottom - parentRect.top;
10029
+ const buttonTop = buttonRect.top - parentRect.top;
10030
+ const spaceAbove = buttonRect.top;
10031
+ const spaceBelow = viewportHeight - buttonRect.bottom;
10032
+ let menuTop = buttonBottom + 8;
10033
+ let menuBottom = null;
10034
+ if (spaceBelow < menuRect.height + 20 && spaceAbove > spaceBelow) {
10035
+ menuTop = null;
10036
+ const parentHeight = parentRect.bottom - parentRect.top;
10037
+ menuBottom = parentHeight - buttonTop + 8;
10038
+ this.settingsMenu.classList.add("vidply-menu-above");
10039
+ } else {
10040
+ this.settingsMenu.classList.remove("vidply-menu-above");
10041
+ }
10042
+ let menuLeft = buttonCenterX - menuRect.width / 2;
10043
+ let menuRight = "auto";
10044
+ let transformX = "translateX(0)";
10045
+ const menuLeftAbsolute = buttonRect.left + buttonRect.width / 2 - menuRect.width / 2;
10046
+ if (menuLeftAbsolute < 10) {
10047
+ menuLeft = 0;
10048
+ } else if (menuLeftAbsolute + menuRect.width > viewportWidth - 10) {
10049
+ menuLeft = "auto";
10050
+ menuRight = 0;
10051
+ } else {
10052
+ menuLeft = buttonCenterX;
10053
+ transformX = "translateX(-50%)";
10054
+ }
10055
+ if (menuTop !== null) {
10056
+ this.settingsMenu.style.top = "".concat(menuTop, "px");
10057
+ this.settingsMenu.style.bottom = "auto";
10058
+ } else if (menuBottom !== null) {
10059
+ this.settingsMenu.style.top = "auto";
10060
+ this.settingsMenu.style.bottom = "".concat(menuBottom, "px");
10061
+ }
10062
+ if (menuLeft !== "auto") {
10063
+ this.settingsMenu.style.left = "".concat(menuLeft, "px");
10064
+ this.settingsMenu.style.right = "auto";
10065
+ } else {
10066
+ this.settingsMenu.style.left = "auto";
10067
+ this.settingsMenu.style.right = "".concat(menuRight, "px");
10068
+ }
10069
+ this.settingsMenu.style.transform = transformX;
10070
+ }
10071
+ /**
10072
+ * Position settings menu with RAF
10073
+ */
10074
+ _positionSettingsMenu() {
10075
+ requestAnimationFrame(() => {
10076
+ setTimeout(() => {
10077
+ this._positionSettingsMenuImmediate();
10078
+ }, 10);
10079
+ });
10080
+ }
10081
+ /**
10082
+ * Toggle keyboard drag mode
10083
+ */
10084
+ toggleKeyboardDragMode() {
10085
+ if (this.draggable) {
10086
+ const wasEnabled = this.draggable.keyboardDragMode;
10087
+ this.draggable.toggleKeyboardDragMode();
10088
+ const isEnabled = this.draggable.keyboardDragMode;
10089
+ if (!wasEnabled && isEnabled) {
10090
+ this._enableMoveMode();
10091
+ }
10092
+ this._updateDragOptionState();
10093
+ }
10094
+ }
10095
+ /**
10096
+ * Enable move mode visual feedback
10097
+ */
10098
+ _enableMoveMode() {
10099
+ this.wrapper.classList.add("".concat(this.player.options.classPrefix, "-sign-move-mode"));
10100
+ this._updateResizeOptionState();
10101
+ setTimeout(() => {
10102
+ this.wrapper.classList.remove("".concat(this.player.options.classPrefix, "-sign-move-mode"));
10103
+ }, 2e3);
10104
+ }
10105
+ /**
10106
+ * Toggle resize mode
10107
+ */
10108
+ toggleResizeMode({ focus = true } = {}) {
10109
+ if (!this.draggable) return false;
10110
+ if (this.draggable.pointerResizeMode) {
10111
+ this.draggable.disablePointerResizeMode({ focus });
10112
+ this._updateResizeOptionState();
10113
+ return false;
10114
+ }
10115
+ this.draggable.enablePointerResizeMode({ focus });
10116
+ this._updateResizeOptionState();
10117
+ return true;
10118
+ }
10119
+ /**
10120
+ * Update drag option state
10121
+ */
10122
+ _updateDragOptionState() {
10123
+ var _a;
10124
+ if (!this.dragOptionButton) return;
10125
+ const isEnabled = !!((_a = this.draggable) == null ? void 0 : _a.keyboardDragMode);
10126
+ const text = isEnabled ? i18n.t("player.disableSignDragMode") : i18n.t("player.enableSignDragMode");
10127
+ const ariaLabel = isEnabled ? i18n.t("player.disableSignDragModeAria") : i18n.t("player.enableSignDragModeAria");
10128
+ this.dragOptionButton.setAttribute("aria-checked", isEnabled ? "true" : "false");
10129
+ this.dragOptionButton.setAttribute("aria-label", ariaLabel);
10130
+ if (this.dragOptionText) {
10131
+ this.dragOptionText.textContent = text;
10132
+ }
10133
+ }
10134
+ /**
10135
+ * Update resize option state
10136
+ */
10137
+ _updateResizeOptionState() {
10138
+ var _a;
10139
+ if (!this.resizeOptionButton) return;
10140
+ const isEnabled = !!((_a = this.draggable) == null ? void 0 : _a.pointerResizeMode);
10141
+ const text = isEnabled ? i18n.t("player.disableSignResizeMode") : i18n.t("player.enableSignResizeMode");
10142
+ const ariaLabel = isEnabled ? i18n.t("player.disableSignResizeModeAria") : i18n.t("player.enableSignResizeModeAria");
10143
+ this.resizeOptionButton.setAttribute("aria-checked", isEnabled ? "true" : "false");
10144
+ this.resizeOptionButton.setAttribute("aria-label", ariaLabel);
10145
+ if (this.resizeOptionText) {
10146
+ this.resizeOptionText.textContent = text;
10147
+ }
10148
+ }
10149
+ /**
10150
+ * Save preferences
10151
+ */
10152
+ savePreferences() {
10153
+ if (!this.wrapper) return;
10154
+ this.player.storage.saveSignLanguagePreferences({
10155
+ size: { width: this.wrapper.style.width }
10156
+ });
10157
+ }
10158
+ /**
10159
+ * Update sources (called when playlist changes)
10160
+ */
10161
+ updateSources(signLanguageSrc, signLanguageSources) {
10162
+ this.src = signLanguageSrc || null;
10163
+ this.sources = signLanguageSources || {};
10164
+ this.currentLanguage = null;
10165
+ }
10166
+ /**
10167
+ * Cleanup
10168
+ */
10169
+ cleanup() {
10170
+ var _a;
10171
+ if (this.settingsMenuVisible) {
10172
+ this.hideSettingsMenu({ focusButton: false });
10173
+ }
10174
+ if (this.documentClickHandler && this.documentClickHandlerAdded) {
10175
+ document.removeEventListener("mousedown", this.documentClickHandler, true);
10176
+ this.documentClickHandlerAdded = false;
10177
+ this.documentClickHandler = null;
10178
+ }
10179
+ if (this.settingsHandlers && this.settingsButton) {
10180
+ this.settingsButton.removeEventListener("click", this.settingsHandlers.click);
10181
+ this.settingsButton.removeEventListener("keydown", this.settingsHandlers.keydown);
10182
+ }
10183
+ this.settingsHandlers = null;
10184
+ if (this.handlers) {
10185
+ this.player.off("play", this.handlers.play);
10186
+ this.player.off("pause", this.handlers.pause);
10187
+ this.player.off("timeupdate", this.handlers.timeupdate);
10188
+ this.player.off("ratechange", this.handlers.ratechange);
10189
+ if (this.handlers.captionChange) {
10190
+ this.player.off("captionsenabled", this.handlers.captionChange);
10191
+ }
10192
+ this.handlers = null;
10193
+ }
10194
+ if (this.wrapper && this.customKeyHandler) {
10195
+ this.wrapper.removeEventListener("keydown", this.customKeyHandler);
10196
+ }
10197
+ if (this.draggable) {
10198
+ if (this.draggable.pointerResizeMode) {
10199
+ this.draggable.disablePointerResizeMode();
10200
+ }
10201
+ this.draggable.destroy();
10202
+ this.draggable = null;
10203
+ }
10204
+ this.interactionHandlers = null;
10205
+ if ((_a = this.wrapper) == null ? void 0 : _a.parentNode) {
10206
+ if (this.video) {
10207
+ this.video.pause();
10208
+ this.video.src = "";
10209
+ }
10210
+ this.wrapper.parentNode.removeChild(this.wrapper);
10211
+ }
10212
+ this.wrapper = null;
10213
+ this.video = null;
10214
+ this.settingsButton = null;
10215
+ this.settingsMenu = null;
10216
+ }
10217
+ /**
10218
+ * Destroy
10219
+ */
10220
+ destroy() {
10221
+ this.cleanup();
10222
+ this.enabled = false;
10223
+ }
10224
+ };
10225
+
10226
+ // src/core/Player.js
10227
+ var playerInstanceCounter = 0;
10228
+ var Player = class _Player extends EventEmitter {
10229
+ constructor(element, options = {}) {
10230
+ super();
10231
+ this.element = typeof element === "string" ? document.querySelector(element) : element;
10232
+ if (!this.element) {
10233
+ throw new Error("VidPly: Element not found");
10234
+ }
10235
+ playerInstanceCounter++;
10236
+ this.instanceId = playerInstanceCounter;
10237
+ if (this.element.tagName !== "VIDEO" && this.element.tagName !== "AUDIO") {
10238
+ const mediaType = options.mediaType || "video";
10239
+ const mediaElement = document.createElement(mediaType);
10240
+ Array.from(this.element.attributes).forEach((attr) => {
10241
+ if (attr.name !== "id" && attr.name !== "class" && !attr.name.startsWith("data-")) {
10242
+ mediaElement.setAttribute(attr.name, attr.value);
10243
+ }
10244
+ });
10245
+ const tracks = this.element.querySelectorAll("track");
10246
+ tracks.forEach((track) => {
10247
+ mediaElement.appendChild(track.cloneNode(true));
10248
+ });
10249
+ this.element.innerHTML = "";
10250
+ this.element.appendChild(mediaElement);
10251
+ this.element = mediaElement;
10252
+ }
10253
+ this._originalElement = this.element;
10254
+ this.options = __spreadValues({
10255
+ // Display
10256
+ width: null,
10257
+ height: null,
10258
+ poster: null,
10259
+ responsive: true,
10260
+ fillContainer: false,
10261
+ // Playback
10262
+ autoplay: false,
10263
+ loop: false,
10264
+ muted: false,
10265
+ volume: 0.8,
10266
+ playbackSpeed: 1,
10267
+ preload: "metadata",
10268
+ startTime: 0,
10269
+ playsInline: true,
8550
10270
  // Enable inline playback on iOS (prevents native fullscreen)
8551
10271
  // Controls
8552
10272
  controls: true,
@@ -8689,6 +10409,58 @@
8689
10409
  this.settingsDialog = null;
8690
10410
  this.metadataCueChangeHandler = null;
8691
10411
  this.metadataAlertHandlers = /* @__PURE__ */ new Map();
10412
+ this.audioDescriptionManager = new AudioDescriptionManager(this);
10413
+ this.signLanguageManager = new SignLanguageManager(this);
10414
+ Object.defineProperties(this, {
10415
+ signLanguageWrapper: {
10416
+ get: () => this.signLanguageManager.wrapper,
10417
+ set: (v) => {
10418
+ this.signLanguageManager.wrapper = v;
10419
+ }
10420
+ },
10421
+ signLanguageVideo: {
10422
+ get: () => this.signLanguageManager.video,
10423
+ set: (v) => {
10424
+ this.signLanguageManager.video = v;
10425
+ }
10426
+ },
10427
+ signLanguageHeader: {
10428
+ get: () => this.signLanguageManager.header,
10429
+ set: (v) => {
10430
+ this.signLanguageManager.header = v;
10431
+ }
10432
+ },
10433
+ signLanguageSettingsButton: {
10434
+ get: () => this.signLanguageManager.settingsButton,
10435
+ set: (v) => {
10436
+ this.signLanguageManager.settingsButton = v;
10437
+ }
10438
+ },
10439
+ signLanguageSettingsMenu: {
10440
+ get: () => this.signLanguageManager.settingsMenu,
10441
+ set: (v) => {
10442
+ this.signLanguageManager.settingsMenu = v;
10443
+ }
10444
+ },
10445
+ signLanguageSettingsMenuVisible: {
10446
+ get: () => this.signLanguageManager.settingsMenuVisible,
10447
+ set: (v) => {
10448
+ this.signLanguageManager.settingsMenuVisible = v;
10449
+ }
10450
+ },
10451
+ signLanguageDraggable: {
10452
+ get: () => this.signLanguageManager.draggable,
10453
+ set: (v) => {
10454
+ this.signLanguageManager.draggable = v;
10455
+ }
10456
+ },
10457
+ currentSignLanguage: {
10458
+ get: () => this.signLanguageManager.currentLanguage,
10459
+ set: (v) => {
10460
+ this.signLanguageManager.currentLanguage = v;
10461
+ }
10462
+ }
10463
+ });
8692
10464
  this.init();
8693
10465
  }
8694
10466
  async init() {
@@ -8951,53 +10723,7 @@
8951
10723
  }
8952
10724
  this.currentSource = src;
8953
10725
  this._pendingSource = null;
8954
- const sourceElements = this.sourceElements;
8955
- for (const sourceEl of sourceElements) {
8956
- const descSrc = sourceEl.getAttribute("data-desc-src");
8957
- const origSrc = sourceEl.getAttribute("data-orig-src");
8958
- if (descSrc || origSrc) {
8959
- if (!this.audioDescriptionSourceElement) {
8960
- this.audioDescriptionSourceElement = sourceEl;
8961
- }
8962
- if (origSrc) {
8963
- if (!this.originalAudioDescriptionSource) {
8964
- this.originalAudioDescriptionSource = origSrc;
8965
- }
8966
- if (!this.originalSrc) {
8967
- this.originalSrc = origSrc;
8968
- }
8969
- } else {
8970
- const currentSrcAttr = sourceEl.getAttribute("src");
8971
- if (!this.originalAudioDescriptionSource && currentSrcAttr) {
8972
- this.originalAudioDescriptionSource = currentSrcAttr;
8973
- }
8974
- if (!this.originalSrc && currentSrcAttr) {
8975
- this.originalSrc = currentSrcAttr;
8976
- }
8977
- }
8978
- if (descSrc && !this.audioDescriptionSrc) {
8979
- this.audioDescriptionSrc = descSrc;
8980
- }
8981
- }
8982
- }
8983
- const trackElements = this.trackElements;
8984
- trackElements.forEach((trackEl) => {
8985
- const trackKind = trackEl.getAttribute("kind");
8986
- const trackDescSrc = trackEl.getAttribute("data-desc-src");
8987
- if (trackKind === "captions" || trackKind === "subtitles" || trackKind === "chapters") {
8988
- if (trackDescSrc) {
8989
- this.audioDescriptionCaptionTracks.push({
8990
- trackElement: trackEl,
8991
- originalSrc: trackEl.getAttribute("src"),
8992
- describedSrc: trackDescSrc,
8993
- originalTrackSrc: trackEl.getAttribute("data-orig-src") || trackEl.getAttribute("src"),
8994
- explicit: true
8995
- // Explicitly defined, so we should validate it
8996
- });
8997
- this.log("Found explicit described ".concat(trackKind, " track: ").concat(trackEl.getAttribute("src"), " -> ").concat(trackDescSrc));
8998
- }
8999
- }
9000
- });
10726
+ this.audioDescriptionManager.initFromSourceElements(this.sourceElements, this.trackElements);
9001
10727
  if (!this.originalSrc) {
9002
10728
  this.originalSrc = src;
9003
10729
  }
@@ -9114,6 +10840,64 @@
9114
10840
  return posterPath;
9115
10841
  }
9116
10842
  }
10843
+ /**
10844
+ * Generate a poster image from video frame at specified time
10845
+ * @param {number} time - Time in seconds (default: 10)
10846
+ * @returns {Promise<string|null>} Data URL of the poster image or null if failed
10847
+ */
10848
+ async generatePosterFromVideo(time = 10) {
10849
+ if (this.element.tagName !== "VIDEO") {
10850
+ return null;
10851
+ }
10852
+ const renderer = this.renderer;
10853
+ if (!renderer || !renderer.media || renderer.media.tagName !== "VIDEO") {
10854
+ return null;
10855
+ }
10856
+ const video = renderer.media;
10857
+ if (!video.duration || video.duration < time) {
10858
+ time = Math.min(time, Math.max(1, video.duration * 0.1));
10859
+ }
10860
+ let videoToUse = video;
10861
+ if (this.controlBar && this.controlBar.previewVideo && this.controlBar.previewSupported) {
10862
+ videoToUse = this.controlBar.previewVideo;
10863
+ }
10864
+ const restoreState = videoToUse === video;
10865
+ return await captureVideoFrame(videoToUse, time, {
10866
+ restoreState,
10867
+ quality: 0.9
10868
+ });
10869
+ }
10870
+ /**
10871
+ * Auto-generate poster from video if none is provided
10872
+ */
10873
+ async autoGeneratePoster() {
10874
+ const hasPoster = this.element.getAttribute("poster") || this.element.poster || this.options.poster;
10875
+ if (hasPoster) {
10876
+ return;
10877
+ }
10878
+ if (this.element.tagName !== "VIDEO") {
10879
+ return;
10880
+ }
10881
+ if (!this.state.duration || this.state.duration === 0) {
10882
+ await new Promise((resolve) => {
10883
+ const onLoadedMetadata = () => {
10884
+ this.element.removeEventListener("loadedmetadata", onLoadedMetadata);
10885
+ resolve();
10886
+ };
10887
+ if (this.element.readyState >= 1) {
10888
+ resolve();
10889
+ } else {
10890
+ this.element.addEventListener("loadedmetadata", onLoadedMetadata);
10891
+ }
10892
+ });
10893
+ }
10894
+ const posterDataURL = await this.generatePosterFromVideo(10);
10895
+ if (posterDataURL) {
10896
+ this.element.poster = posterDataURL;
10897
+ this.log("Auto-generated poster from video frame at 10 seconds", "info");
10898
+ this.showPosterOverlay();
10899
+ }
10900
+ }
9117
10901
  showPosterOverlay() {
9118
10902
  if (!this.videoWrapper || this.element.tagName !== "VIDEO") {
9119
10903
  return;
@@ -9122,7 +10906,7 @@
9122
10906
  if (!poster) {
9123
10907
  return;
9124
10908
  }
9125
- const resolvedPoster = this.resolvePosterPath(poster);
10909
+ const resolvedPoster = poster.startsWith("data:") ? poster : this.resolvePosterPath(poster);
9126
10910
  this.videoWrapper.style.setProperty("--vidply-poster-image", 'url("'.concat(resolvedPoster, '")'));
9127
10911
  this.videoWrapper.classList.add("vidply-forced-poster");
9128
10912
  if (this._isAudioContent && this.container) {
@@ -9242,6 +11026,9 @@
9242
11026
  if (trackConfig.default) {
9243
11027
  track.default = true;
9244
11028
  }
11029
+ if (trackConfig.describedSrc) {
11030
+ track.setAttribute("data-desc-src", trackConfig.describedSrc);
11031
+ }
9245
11032
  const firstChild = this.element.firstChild;
9246
11033
  if (firstChild && firstChild.nodeType === Node.ELEMENT_NODE && firstChild.tagName !== "TRACK") {
9247
11034
  this.element.insertBefore(track, firstChild);
@@ -9256,6 +11043,13 @@
9256
11043
  this.audioDescriptionSrc = config.audioDescriptionSrc || null;
9257
11044
  this.signLanguageSrc = config.signLanguageSrc || null;
9258
11045
  this.originalSrc = config.src;
11046
+ if (this.audioDescriptionManager) {
11047
+ this.audioDescriptionManager.updateSources(config.audioDescriptionSrc);
11048
+ this.audioDescriptionManager.reinitialize();
11049
+ }
11050
+ if (this.signLanguageManager) {
11051
+ this.signLanguageManager.updateSources(config.signLanguageSrc, config.signLanguageSources);
11052
+ }
9259
11053
  if (wasAudioDescriptionEnabled) {
9260
11054
  this.disableAudioDescription();
9261
11055
  }
@@ -9664,8 +11458,12 @@
9664
11458
  }
9665
11459
  return null;
9666
11460
  }
9667
- // Audio Description
11461
+ // Audio Description (delegated to AudioDescriptionManager)
9668
11462
  async enableAudioDescription() {
11463
+ return this.audioDescriptionManager.enable();
11464
+ }
11465
+ // Legacy method body preserved for reference - can be removed after testing
11466
+ async _legacyEnableAudioDescription() {
9669
11467
  const hasSourceElementsWithDesc = this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
9670
11468
  const hasTracksWithDesc = this.audioDescriptionCaptionTracks.length > 0;
9671
11469
  if (!this.audioDescriptionSrc && !hasSourceElementsWithDesc && !hasTracksWithDesc) {
@@ -10398,6 +12196,10 @@
10398
12196
  this.emit("audiodescriptionenabled");
10399
12197
  }
10400
12198
  async disableAudioDescription() {
12199
+ return this.audioDescriptionManager.disable();
12200
+ }
12201
+ // Legacy method body preserved for reference - can be removed after testing
12202
+ async _legacyDisableAudioDescription() {
10401
12203
  if (!this.originalSrc) {
10402
12204
  return;
10403
12205
  }
@@ -10672,64 +12474,14 @@
10672
12474
  this.emit("audiodescriptiondisabled");
10673
12475
  }
10674
12476
  async toggleAudioDescription() {
10675
- const descriptionTrack = this.findTextTrack("descriptions");
10676
- const hasAudioDescriptionSrc = this.audioDescriptionSrc || this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
10677
- if (descriptionTrack && hasAudioDescriptionSrc) {
10678
- if (this.state.audioDescriptionEnabled) {
10679
- this._audioDescriptionDesiredState = false;
10680
- descriptionTrack.mode = "hidden";
10681
- await this.disableAudioDescription();
10682
- } else {
10683
- this._audioDescriptionDesiredState = true;
10684
- await this.enableAudioDescription();
10685
- const enableDescriptionTrack = () => {
10686
- this.invalidateTrackCache();
10687
- const descTrack = this.findTextTrack("descriptions");
10688
- if (descTrack) {
10689
- if (descTrack.mode === "disabled") {
10690
- descTrack.mode = "hidden";
10691
- this.setManagedTimeout(() => {
10692
- descTrack.mode = "showing";
10693
- }, 50);
10694
- } else {
10695
- descTrack.mode = "showing";
10696
- }
10697
- } else if (this.element.readyState < 2) {
10698
- this.setManagedTimeout(enableDescriptionTrack, 100);
10699
- }
10700
- };
10701
- if (this.element.readyState >= 1) {
10702
- this.setManagedTimeout(enableDescriptionTrack, 200);
10703
- } else {
10704
- this.element.addEventListener("loadedmetadata", () => {
10705
- this.setManagedTimeout(enableDescriptionTrack, 200);
10706
- }, { once: true });
10707
- }
10708
- }
10709
- } else if (descriptionTrack) {
10710
- if (descriptionTrack.mode === "showing") {
10711
- this._audioDescriptionDesiredState = false;
10712
- descriptionTrack.mode = "hidden";
10713
- this.state.audioDescriptionEnabled = false;
10714
- this.emit("audiodescriptiondisabled");
10715
- } else {
10716
- this._audioDescriptionDesiredState = true;
10717
- descriptionTrack.mode = "showing";
10718
- this.state.audioDescriptionEnabled = true;
10719
- this.emit("audiodescriptionenabled");
10720
- }
10721
- } else if (hasAudioDescriptionSrc) {
10722
- if (this.state.audioDescriptionEnabled) {
10723
- this._audioDescriptionDesiredState = false;
10724
- await this.disableAudioDescription();
10725
- } else {
10726
- this._audioDescriptionDesiredState = true;
10727
- await this.enableAudioDescription();
10728
- }
10729
- }
12477
+ return this.audioDescriptionManager.toggle();
10730
12478
  }
10731
- // Sign Language
12479
+ // Sign Language (delegated to SignLanguageManager)
10732
12480
  enableSignLanguage() {
12481
+ return this.signLanguageManager.enable();
12482
+ }
12483
+ // Legacy method body preserved for reference - can be removed after testing
12484
+ _legacyEnableSignLanguage() {
10733
12485
  var _a;
10734
12486
  const hasMultipleSources = Object.keys(this.signLanguageSources).length > 0;
10735
12487
  const hasSingleSource = !!this.signLanguageSrc;
@@ -10982,23 +12734,16 @@
10982
12734
  }, 150);
10983
12735
  }
10984
12736
  disableSignLanguage() {
10985
- if (this.signLanguageSettingsMenuVisible) {
10986
- this.hideSignLanguageSettingsMenu({ focusButton: false });
10987
- }
10988
- if (this.signLanguageWrapper) {
10989
- this.signLanguageWrapper.style.display = "none";
10990
- }
10991
- this.state.signLanguageEnabled = false;
10992
- this.emit("signlanguagedisabled");
12737
+ return this.signLanguageManager.disable();
10993
12738
  }
10994
12739
  toggleSignLanguage() {
10995
- if (this.state.signLanguageEnabled) {
10996
- this.disableSignLanguage();
10997
- } else {
10998
- this.enableSignLanguage();
10999
- }
12740
+ return this.signLanguageManager.toggle();
11000
12741
  }
11001
12742
  setupSignLanguageInteraction() {
12743
+ return this.signLanguageManager._setupInteraction();
12744
+ }
12745
+ // Legacy method preserved for reference
12746
+ _legacySetupSignLanguageInteraction() {
11002
12747
  if (!this.signLanguageWrapper) return;
11003
12748
  const isMobile2 = window.innerWidth < 768;
11004
12749
  const isFullscreen = this.state.fullscreen;
@@ -11136,6 +12881,10 @@
11136
12881
  return langNames[langCode] || langCode.toUpperCase();
11137
12882
  }
11138
12883
  switchSignLanguage(langCode) {
12884
+ return this.signLanguageManager.switchLanguage(langCode);
12885
+ }
12886
+ // Legacy method preserved for reference
12887
+ _legacySwitchSignLanguage(langCode) {
11139
12888
  if (!this.signLanguageSources[langCode] || !this.signLanguageVideo) {
11140
12889
  return;
11141
12890
  }
@@ -11151,6 +12900,10 @@
11151
12900
  this.emit("signlanguagelanguagechanged", langCode);
11152
12901
  }
11153
12902
  showSignLanguageSettingsMenu() {
12903
+ return this.signLanguageManager.showSettingsMenu();
12904
+ }
12905
+ // Legacy method preserved for reference
12906
+ _legacyShowSignLanguageSettingsMenu() {
11154
12907
  this.signLanguageSettingsMenuJustOpened = true;
11155
12908
  setTimeout(() => {
11156
12909
  this.signLanguageSettingsMenuJustOpened = false;
@@ -11294,25 +13047,7 @@
11294
13047
  focusFirstMenuItem(this.signLanguageSettingsMenu, ".".concat(this.options.classPrefix, "-sign-language-settings-item"));
11295
13048
  }
11296
13049
  hideSignLanguageSettingsMenu({ focusButton = true } = {}) {
11297
- if (this.signLanguageSettingsMenu) {
11298
- this.signLanguageSettingsMenu.style.display = "none";
11299
- this.signLanguageSettingsMenuVisible = false;
11300
- this.signLanguageSettingsMenuJustOpened = false;
11301
- if (this.signLanguageSettingsMenuKeyHandler) {
11302
- this.signLanguageSettingsMenu.removeEventListener("keydown", this.signLanguageSettingsMenuKeyHandler);
11303
- this.signLanguageSettingsMenuKeyHandler = null;
11304
- }
11305
- const menuItems = Array.from(this.signLanguageSettingsMenu.querySelectorAll(".".concat(this.options.classPrefix, "-sign-language-settings-item")));
11306
- menuItems.forEach((item) => {
11307
- item.setAttribute("tabindex", "-1");
11308
- });
11309
- if (this.signLanguageSettingsButton) {
11310
- this.signLanguageSettingsButton.setAttribute("aria-expanded", "false");
11311
- if (focusButton) {
11312
- this.signLanguageSettingsButton.focus({ preventScroll: true });
11313
- }
11314
- }
11315
- }
13050
+ return this.signLanguageManager.hideSettingsMenu({ focusButton });
11316
13051
  }
11317
13052
  positionSignLanguageSettingsMenuImmediate() {
11318
13053
  if (!this.signLanguageSettingsMenu || !this.signLanguageSettingsButton) return;
@@ -11416,6 +13151,13 @@
11416
13151
  }
11417
13152
  }
11418
13153
  constrainSignLanguagePosition() {
13154
+ return this.signLanguageManager.constrainPosition();
13155
+ }
13156
+ saveSignLanguagePreferences() {
13157
+ return this.signLanguageManager.savePreferences();
13158
+ }
13159
+ // Legacy methods preserved for reference - can be removed after testing
13160
+ _legacyConstrainSignLanguagePosition() {
11419
13161
  if (!this.signLanguageWrapper || !this.videoWrapper) return;
11420
13162
  if (this.signLanguageDraggable && this.signLanguageDraggable.manuallyPositioned) {
11421
13163
  return;
@@ -11465,7 +13207,7 @@
11465
13207
  this.signLanguageWrapper.style.bottom = "auto";
11466
13208
  this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter((c) => c.startsWith("vidply-sign-position-")));
11467
13209
  }
11468
- saveSignLanguagePreferences() {
13210
+ _legacySaveSignLanguagePreferences() {
11469
13211
  if (!this.signLanguageWrapper) return;
11470
13212
  this.storage.saveSignLanguagePreferences({
11471
13213
  size: {
@@ -11475,58 +13217,7 @@
11475
13217
  });
11476
13218
  }
11477
13219
  cleanupSignLanguage() {
11478
- if (this.signLanguageSettingsMenuVisible) {
11479
- this.hideSignLanguageSettingsMenu({ focusButton: false });
11480
- }
11481
- if (this.signLanguageDocumentClickHandler && this.signLanguageDocumentClickHandlerAdded) {
11482
- document.removeEventListener("mousedown", this.signLanguageDocumentClickHandler, true);
11483
- this.signLanguageDocumentClickHandlerAdded = false;
11484
- this.signLanguageDocumentClickHandler = null;
11485
- }
11486
- if (this.signLanguageSettingsHandlers) {
11487
- if (this.signLanguageSettingsButton) {
11488
- this.signLanguageSettingsButton.removeEventListener("click", this.signLanguageSettingsHandlers.settingsClick);
11489
- this.signLanguageSettingsButton.removeEventListener("keydown", this.signLanguageSettingsHandlers.settingsKeydown);
11490
- }
11491
- this.signLanguageSettingsHandlers = null;
11492
- }
11493
- if (this.signLanguageHandlers) {
11494
- this.off("play", this.signLanguageHandlers.play);
11495
- this.off("pause", this.signLanguageHandlers.pause);
11496
- this.off("timeupdate", this.signLanguageHandlers.timeupdate);
11497
- this.off("ratechange", this.signLanguageHandlers.ratechange);
11498
- if (this.signLanguageHandlers.captionChange) {
11499
- this.off("captionsenabled", this.signLanguageHandlers.captionChange);
11500
- }
11501
- this.signLanguageHandlers = null;
11502
- }
11503
- if (this.signLanguageInteractionHandlers) {
11504
- if (this.signLanguageHeader && this.signLanguageInteractionHandlers.headerKeyHandler) {
11505
- this.signLanguageHeader.removeEventListener("keydown", this.signLanguageInteractionHandlers.headerKeyHandler);
11506
- }
11507
- if (this.signLanguageWrapper && this.signLanguageInteractionHandlers.customKeyHandler) {
11508
- this.signLanguageWrapper.removeEventListener("keydown", this.signLanguageInteractionHandlers.customKeyHandler);
11509
- }
11510
- }
11511
- if (this.signLanguageDraggable) {
11512
- if (this.signLanguageDraggable.pointerResizeMode) {
11513
- this.signLanguageDraggable.disablePointerResizeMode();
11514
- }
11515
- this.signLanguageDraggable.destroy();
11516
- this.signLanguageDraggable = null;
11517
- }
11518
- this.signLanguageInteractionHandlers = null;
11519
- if (this.signLanguageWrapper && this.signLanguageWrapper.parentNode) {
11520
- if (this.signLanguageVideo) {
11521
- this.signLanguageVideo.pause();
11522
- this.signLanguageVideo.src = "";
11523
- }
11524
- this.signLanguageWrapper.parentNode.removeChild(this.signLanguageWrapper);
11525
- }
11526
- this.signLanguageWrapper = null;
11527
- this.signLanguageVideo = null;
11528
- this.signLanguageSettingsButton = null;
11529
- this.signLanguageSettingsMenu = null;
13220
+ return this.signLanguageManager.cleanup();
11530
13221
  }
11531
13222
  // Settings
11532
13223
  // Settings dialog removed - using individual control buttons instead