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
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import {
7
7
  HTML5Renderer
8
- } from "./vidply.chunk-BCOFCT6U.js";
8
+ } from "./vidply.chunk-W2LSBD6Y.js";
9
9
  import {
10
10
  DOMUtils,
11
11
  DraggableResizable,
@@ -21,7 +21,7 @@ import {
21
21
  focusFirstMenuItem,
22
22
  i18n,
23
23
  preventDragOnElement
24
- } from "./vidply.chunk-SRM7VNHG.js";
24
+ } from "./vidply.chunk-GS2JX5RQ.js";
25
25
 
26
26
  // src/utils/EventEmitter.js
27
27
  var EventEmitter = class {
@@ -64,6 +64,108 @@ var EventEmitter = class {
64
64
  }
65
65
  };
66
66
 
67
+ // src/utils/PerformanceUtils.js
68
+ function debounce(func, wait = 100) {
69
+ let timeout;
70
+ return function executedFunction(...args) {
71
+ const later = () => {
72
+ clearTimeout(timeout);
73
+ func(...args);
74
+ };
75
+ clearTimeout(timeout);
76
+ timeout = setTimeout(later, wait);
77
+ };
78
+ }
79
+ function isMobile(breakpoint = 768) {
80
+ return window.innerWidth < breakpoint;
81
+ }
82
+ function rafWithTimeout(callback, timeout = 100) {
83
+ let called = false;
84
+ const execute = () => {
85
+ if (!called) {
86
+ called = true;
87
+ callback();
88
+ }
89
+ };
90
+ requestAnimationFrame(execute);
91
+ setTimeout(execute, timeout);
92
+ }
93
+
94
+ // src/utils/VideoFrameCapture.js
95
+ async function captureVideoFrame(video, time, options = {}) {
96
+ if (!video || video.tagName !== "VIDEO") {
97
+ return null;
98
+ }
99
+ const {
100
+ restoreState = true,
101
+ quality = 0.9,
102
+ maxWidth,
103
+ maxHeight
104
+ } = options;
105
+ const wasPlaying = !video.paused;
106
+ const originalTime = video.currentTime;
107
+ const originalMuted = video.muted;
108
+ if (restoreState) {
109
+ video.muted = true;
110
+ }
111
+ return new Promise((resolve) => {
112
+ const captureFrame = () => {
113
+ try {
114
+ let width = video.videoWidth || 640;
115
+ let height = video.videoHeight || 360;
116
+ if (maxWidth && width > maxWidth) {
117
+ const ratio = maxWidth / width;
118
+ width = maxWidth;
119
+ height = Math.round(height * ratio);
120
+ }
121
+ if (maxHeight && height > maxHeight) {
122
+ const ratio = maxHeight / height;
123
+ height = maxHeight;
124
+ width = Math.round(width * ratio);
125
+ }
126
+ const canvas = document.createElement("canvas");
127
+ canvas.width = width;
128
+ canvas.height = height;
129
+ const ctx = canvas.getContext("2d");
130
+ ctx.drawImage(video, 0, 0, width, height);
131
+ const dataURL = canvas.toDataURL("image/jpeg", quality);
132
+ if (restoreState) {
133
+ video.currentTime = originalTime;
134
+ video.muted = originalMuted;
135
+ if (wasPlaying && !video.paused) {
136
+ video.play().catch(() => {
137
+ });
138
+ }
139
+ }
140
+ resolve(dataURL);
141
+ } catch (error) {
142
+ if (restoreState) {
143
+ video.currentTime = originalTime;
144
+ video.muted = originalMuted;
145
+ if (wasPlaying && !video.paused) {
146
+ video.play().catch(() => {
147
+ });
148
+ }
149
+ }
150
+ resolve(null);
151
+ }
152
+ };
153
+ const onSeeked = () => {
154
+ video.removeEventListener("seeked", onSeeked);
155
+ requestAnimationFrame(() => {
156
+ requestAnimationFrame(captureFrame);
157
+ });
158
+ };
159
+ const timeDiff = Math.abs(video.currentTime - time);
160
+ if (timeDiff < 0.1 && video.readyState >= 2) {
161
+ captureFrame();
162
+ } else {
163
+ video.addEventListener("seeked", onSeeked);
164
+ video.currentTime = time;
165
+ }
166
+ });
167
+ }
168
+
67
169
  // src/controls/ControlBar.js
68
170
  var ControlBar = class {
69
171
  constructor(player) {
@@ -84,17 +186,13 @@ var ControlBar = class {
84
186
  this.setupAutoHide();
85
187
  this.setupOverflowDetection();
86
188
  }
87
- // Helper method to check if we're on a mobile device
88
- isMobile() {
89
- return window.innerWidth < 768;
90
- }
91
189
  // Helper method to detect touch devices
92
190
  isTouchDevice() {
93
191
  return "ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
94
192
  }
95
193
  // Smart menu positioning to avoid overflow
96
194
  positionMenu(menu, button, immediate = false) {
97
- const isMobile2 = this.isMobile();
195
+ const mobile = isMobile();
98
196
  const isOverflowMenu = menu.classList.contains(`${this.player.options.classPrefix}-overflow-menu-list`);
99
197
  const isFullscreen = this.player.state.fullscreen;
100
198
  if (isFullscreen && menu.parentElement === this.player.container) {
@@ -135,7 +233,7 @@ var ControlBar = class {
135
233
  }
136
234
  return;
137
235
  }
138
- if (isMobile2) {
236
+ if (mobile) {
139
237
  const isVolumeMenu = menu.classList.contains(`${this.player.options.classPrefix}-volume-menu`);
140
238
  const doMobilePositioning = () => {
141
239
  const parentContainer = button.parentElement;
@@ -704,13 +802,117 @@ var ControlBar = class {
704
802
  this.controls.progressTooltip = DOMUtils.createElement("div", {
705
803
  className: `${this.player.options.classPrefix}-progress-tooltip`
706
804
  });
805
+ this.controls.progressPreview = DOMUtils.createElement("div", {
806
+ className: `${this.player.options.classPrefix}-progress-preview`,
807
+ attributes: {
808
+ "aria-hidden": "true"
809
+ }
810
+ });
811
+ this.controls.progressTooltip.appendChild(this.controls.progressPreview);
812
+ this.controls.progressTooltipTime = DOMUtils.createElement("div", {
813
+ className: `${this.player.options.classPrefix}-progress-tooltip-time`
814
+ });
815
+ this.controls.progressTooltip.appendChild(this.controls.progressTooltipTime);
707
816
  progressContainer.appendChild(this.controls.buffered);
708
817
  progressContainer.appendChild(this.controls.played);
709
818
  this.controls.played.appendChild(this.controls.progressHandle);
710
819
  progressContainer.appendChild(this.controls.progressTooltip);
711
820
  this.controls.progress = progressContainer;
821
+ this.initPreviewThumbnail();
712
822
  this.setupProgressBarEvents();
713
823
  }
824
+ /**
825
+ * Initialize preview thumbnail functionality for HTML5 video
826
+ */
827
+ initPreviewThumbnail() {
828
+ this.previewThumbnailCache = /* @__PURE__ */ new Map();
829
+ this.previewVideo = null;
830
+ this.currentPreviewTime = null;
831
+ this.previewThumbnailTimeout = null;
832
+ this.previewSupported = false;
833
+ const isVideo = this.player.element && this.player.element.tagName === "VIDEO";
834
+ if (!isVideo) {
835
+ return;
836
+ }
837
+ const renderer = this.player.renderer;
838
+ const hasVideoMedia = renderer && renderer.media && renderer.media.tagName === "VIDEO";
839
+ const isHTML5Renderer = renderer && (renderer.constructor.name === "HTML5Renderer" || renderer.constructor.name === "HLSRenderer" && hasVideoMedia);
840
+ this.previewSupported = isHTML5Renderer && hasVideoMedia;
841
+ if (this.previewSupported) {
842
+ this.previewVideo = document.createElement("video");
843
+ this.previewVideo.muted = true;
844
+ this.previewVideo.preload = "metadata";
845
+ this.previewVideo.style.position = "absolute";
846
+ this.previewVideo.style.visibility = "hidden";
847
+ this.previewVideo.style.width = "1px";
848
+ this.previewVideo.style.height = "1px";
849
+ this.previewVideo.style.top = "-9999px";
850
+ const mainVideo = renderer.media || this.player.element;
851
+ if (mainVideo.src) {
852
+ this.previewVideo.src = mainVideo.src;
853
+ } else {
854
+ const source = mainVideo.querySelector("source");
855
+ if (source) {
856
+ this.previewVideo.src = source.src;
857
+ }
858
+ }
859
+ this.previewVideo.addEventListener("error", () => {
860
+ this.player.log("Preview video failed to load", "warn");
861
+ this.previewSupported = false;
862
+ });
863
+ if (this.player.container) {
864
+ this.player.container.appendChild(this.previewVideo);
865
+ }
866
+ }
867
+ }
868
+ /**
869
+ * Generate preview thumbnail for a specific time
870
+ * @param {number} time - Time in seconds
871
+ * @returns {Promise<string>} Data URL of the thumbnail
872
+ */
873
+ async generatePreviewThumbnail(time) {
874
+ if (!this.previewSupported || !this.previewVideo) {
875
+ return null;
876
+ }
877
+ const cacheKey = Math.floor(time);
878
+ if (this.previewThumbnailCache.has(cacheKey)) {
879
+ return this.previewThumbnailCache.get(cacheKey);
880
+ }
881
+ const dataURL = await captureVideoFrame(this.previewVideo, time, {
882
+ restoreState: false,
883
+ quality: 0.8,
884
+ maxWidth: 160,
885
+ maxHeight: 90
886
+ });
887
+ if (dataURL) {
888
+ if (this.previewThumbnailCache.size > 20) {
889
+ const firstKey = this.previewThumbnailCache.keys().next().value;
890
+ this.previewThumbnailCache.delete(firstKey);
891
+ }
892
+ this.previewThumbnailCache.set(cacheKey, dataURL);
893
+ }
894
+ return dataURL;
895
+ }
896
+ /**
897
+ * Update preview thumbnail display
898
+ * @param {number} time - Time in seconds
899
+ */
900
+ async updatePreviewThumbnail(time) {
901
+ if (!this.previewSupported) {
902
+ return;
903
+ }
904
+ if (this.previewThumbnailTimeout) {
905
+ clearTimeout(this.previewThumbnailTimeout);
906
+ }
907
+ this.previewThumbnailTimeout = setTimeout(async () => {
908
+ const thumbnail = await this.generatePreviewThumbnail(time);
909
+ if (thumbnail && this.controls.progressPreview) {
910
+ this.controls.progressPreview.style.backgroundImage = `url(${thumbnail})`;
911
+ this.controls.progressPreview.style.display = "block";
912
+ }
913
+ this.currentPreviewTime = time;
914
+ }, 100);
915
+ }
714
916
  setupProgressBarEvents() {
715
917
  const progress = this.controls.progress;
716
918
  const updateProgress = (clientX) => {
@@ -737,13 +939,19 @@ var ControlBar = class {
737
939
  progress.addEventListener("mousemove", (e) => {
738
940
  if (!this.isDraggingProgress) {
739
941
  const { time } = updateProgress(e.clientX);
740
- this.controls.progressTooltip.textContent = TimeUtils.formatTime(time);
741
- this.controls.progressTooltip.style.left = `${e.clientX - progress.getBoundingClientRect().left}px`;
942
+ const rect = progress.getBoundingClientRect();
943
+ const left = e.clientX - rect.left;
944
+ this.controls.progressTooltipTime.textContent = TimeUtils.formatTime(time);
945
+ this.controls.progressTooltip.style.left = `${left}px`;
742
946
  this.controls.progressTooltip.style.display = "block";
947
+ this.updatePreviewThumbnail(time);
743
948
  }
744
949
  });
745
950
  progress.addEventListener("mouseleave", () => {
746
951
  this.controls.progressTooltip.style.display = "none";
952
+ if (this.previewThumbnailTimeout) {
953
+ clearTimeout(this.previewThumbnailTimeout);
954
+ }
747
955
  });
748
956
  progress.addEventListener("keydown", (e) => {
749
957
  if (e.key === "ArrowLeft") {
@@ -2490,6 +2698,20 @@ var ControlBar = class {
2490
2698
  hide() {
2491
2699
  this.element.style.display = "none";
2492
2700
  }
2701
+ /**
2702
+ * Cleanup preview thumbnail resources
2703
+ */
2704
+ cleanupPreviewThumbnail() {
2705
+ if (this.previewThumbnailTimeout) {
2706
+ clearTimeout(this.previewThumbnailTimeout);
2707
+ this.previewThumbnailTimeout = null;
2708
+ }
2709
+ if (this.previewVideo && this.previewVideo.parentNode) {
2710
+ this.previewVideo.parentNode.removeChild(this.previewVideo);
2711
+ this.previewVideo = null;
2712
+ }
2713
+ this.previewThumbnailCache.clear();
2714
+ }
2493
2715
  destroy() {
2494
2716
  if (this.hideTimeout) {
2495
2717
  clearTimeout(this.hideTimeout);
@@ -2497,39 +2719,13 @@ var ControlBar = class {
2497
2719
  if (this.overflowResizeObserver) {
2498
2720
  this.overflowResizeObserver.disconnect();
2499
2721
  }
2722
+ this.cleanupPreviewThumbnail();
2500
2723
  if (this.element && this.element.parentNode) {
2501
2724
  this.element.parentNode.removeChild(this.element);
2502
2725
  }
2503
2726
  }
2504
2727
  };
2505
2728
 
2506
- // src/utils/PerformanceUtils.js
2507
- function debounce(func, wait = 100) {
2508
- let timeout;
2509
- return function executedFunction(...args) {
2510
- const later = () => {
2511
- clearTimeout(timeout);
2512
- func(...args);
2513
- };
2514
- clearTimeout(timeout);
2515
- timeout = setTimeout(later, wait);
2516
- };
2517
- }
2518
- function isMobile(breakpoint = 768) {
2519
- return window.innerWidth < breakpoint;
2520
- }
2521
- function rafWithTimeout(callback, timeout = 100) {
2522
- let called = false;
2523
- const execute = () => {
2524
- if (!called) {
2525
- called = true;
2526
- callback();
2527
- }
2528
- };
2529
- requestAnimationFrame(execute);
2530
- setTimeout(execute, timeout);
2531
- }
2532
-
2533
2729
  // src/controls/CaptionManager.js
2534
2730
  var CaptionManager = class {
2535
2731
  constructor(player) {
@@ -3059,40 +3255,1495 @@ var KeyboardManager = class {
3059
3255
  }
3060
3256
  };
3061
3257
 
3062
- // src/core/Player.js
3063
- var playerInstanceCounter = 0;
3064
- var Player = class _Player extends EventEmitter {
3065
- constructor(element, options = {}) {
3066
- super();
3067
- this.element = typeof element === "string" ? document.querySelector(element) : element;
3068
- if (!this.element) {
3069
- throw new Error("VidPly: Element not found");
3258
+ // src/core/AudioDescriptionManager.js
3259
+ var AudioDescriptionManager = class {
3260
+ constructor(player) {
3261
+ this.player = player;
3262
+ this.enabled = false;
3263
+ this.desiredState = false;
3264
+ this.src = player.options.audioDescriptionSrc;
3265
+ this.sourceElement = null;
3266
+ this.originalSource = null;
3267
+ this.captionTracks = [];
3268
+ }
3269
+ /**
3270
+ * Initialize audio description from source elements
3271
+ * Called during player initialization
3272
+ */
3273
+ initFromSourceElements(sourceElements, trackElements) {
3274
+ for (const sourceEl of sourceElements) {
3275
+ const descSrc = sourceEl.getAttribute("data-desc-src");
3276
+ const origSrc = sourceEl.getAttribute("data-orig-src");
3277
+ if (descSrc || origSrc) {
3278
+ if (!this.sourceElement) {
3279
+ this.sourceElement = sourceEl;
3280
+ }
3281
+ if (origSrc) {
3282
+ if (!this.originalSource) {
3283
+ this.originalSource = origSrc;
3284
+ }
3285
+ if (!this.player.originalSrc) {
3286
+ this.player.originalSrc = origSrc;
3287
+ }
3288
+ } else {
3289
+ const currentSrcAttr = sourceEl.getAttribute("src");
3290
+ if (!this.originalSource && currentSrcAttr) {
3291
+ this.originalSource = currentSrcAttr;
3292
+ }
3293
+ if (!this.player.originalSrc && currentSrcAttr) {
3294
+ this.player.originalSrc = currentSrcAttr;
3295
+ }
3296
+ }
3297
+ if (descSrc && !this.src) {
3298
+ this.src = descSrc;
3299
+ }
3300
+ }
3070
3301
  }
3071
- playerInstanceCounter++;
3072
- this.instanceId = playerInstanceCounter;
3073
- if (this.element.tagName !== "VIDEO" && this.element.tagName !== "AUDIO") {
3074
- const mediaType = options.mediaType || "video";
3075
- const mediaElement = document.createElement(mediaType);
3076
- Array.from(this.element.attributes).forEach((attr) => {
3077
- if (attr.name !== "id" && attr.name !== "class" && !attr.name.startsWith("data-")) {
3078
- mediaElement.setAttribute(attr.name, attr.value);
3302
+ trackElements.forEach((trackEl) => {
3303
+ const trackKind = trackEl.getAttribute("kind");
3304
+ const trackDescSrc = trackEl.getAttribute("data-desc-src");
3305
+ if ((trackKind === "captions" || trackKind === "subtitles" || trackKind === "chapters" || trackKind === "descriptions") && trackDescSrc) {
3306
+ this.captionTracks.push({
3307
+ trackElement: trackEl,
3308
+ originalSrc: trackEl.getAttribute("src"),
3309
+ describedSrc: trackDescSrc,
3310
+ originalTrackSrc: trackEl.getAttribute("data-orig-src") || trackEl.getAttribute("src"),
3311
+ explicit: true
3312
+ });
3313
+ this.player.log(`Found explicit described ${trackKind} track: ${trackEl.getAttribute("src")} -> ${trackDescSrc}`);
3314
+ }
3315
+ });
3316
+ }
3317
+ /**
3318
+ * Check if audio description is available
3319
+ */
3320
+ isAvailable() {
3321
+ const hasSourceElementsWithDesc = this.player.sourceElements.some(
3322
+ (el) => el.getAttribute("data-desc-src")
3323
+ );
3324
+ return !!(this.src || hasSourceElementsWithDesc || this.captionTracks.length > 0);
3325
+ }
3326
+ /**
3327
+ * Enable audio description
3328
+ */
3329
+ async enable() {
3330
+ const hasSourceElementsWithDesc = this.player.sourceElements.some(
3331
+ (el) => el.getAttribute("data-desc-src")
3332
+ );
3333
+ const hasTracksWithDesc = this.captionTracks.length > 0;
3334
+ if (!this.src && !hasSourceElementsWithDesc && !hasTracksWithDesc) {
3335
+ console.warn("VidPly: No audio description source, source elements, or tracks provided");
3336
+ return;
3337
+ }
3338
+ this.desiredState = true;
3339
+ const currentTime = this.player.state.currentTime;
3340
+ const wasPlaying = this.player.state.playing;
3341
+ const posterValue = this.player.element.poster || this.player.element.getAttribute("poster") || this.player.options.poster;
3342
+ const shouldKeepPoster = currentTime < 0.1 && !wasPlaying;
3343
+ const currentCaptionText = this._getCurrentCaptionText();
3344
+ if (this.sourceElement) {
3345
+ await this._enableWithSourceElement(currentTime, wasPlaying, posterValue, shouldKeepPoster, currentCaptionText);
3346
+ } else {
3347
+ await this._enableWithDirectSrc(currentTime, wasPlaying, posterValue, shouldKeepPoster);
3348
+ }
3349
+ }
3350
+ /**
3351
+ * Disable audio description
3352
+ */
3353
+ async disable() {
3354
+ if (!this.player.originalSrc) {
3355
+ return;
3356
+ }
3357
+ this.desiredState = false;
3358
+ const currentTime = this.player.state.currentTime;
3359
+ const wasPlaying = this.player.state.playing;
3360
+ const posterValue = this.player.element.poster || this.player.element.getAttribute("poster") || this.player.options.poster;
3361
+ const shouldKeepPoster = currentTime < 0.1 && !wasPlaying;
3362
+ const currentCaptionText = this._getCurrentCaptionText();
3363
+ if (this.sourceElement) {
3364
+ await this._disableWithSourceElement(currentTime, wasPlaying, posterValue, shouldKeepPoster, currentCaptionText);
3365
+ } else {
3366
+ await this._disableWithDirectSrc(currentTime, wasPlaying, posterValue);
3367
+ }
3368
+ }
3369
+ /**
3370
+ * Toggle audio description
3371
+ */
3372
+ async toggle() {
3373
+ const descriptionTrack = this.player.findTextTrack("descriptions");
3374
+ const hasAudioDescriptionSrc = this.isAvailable();
3375
+ if (descriptionTrack && !hasAudioDescriptionSrc) {
3376
+ if (descriptionTrack.mode === "showing") {
3377
+ descriptionTrack.mode = "hidden";
3378
+ this.enabled = false;
3379
+ this.player.emit("audiodescriptiondisabled");
3380
+ } else {
3381
+ descriptionTrack.mode = "showing";
3382
+ this.enabled = true;
3383
+ this.player.emit("audiodescriptionenabled");
3384
+ }
3385
+ } else if (descriptionTrack && hasAudioDescriptionSrc) {
3386
+ if (this.enabled) {
3387
+ this.desiredState = false;
3388
+ await this.disable();
3389
+ } else {
3390
+ descriptionTrack.mode = "showing";
3391
+ this.desiredState = true;
3392
+ await this.enable();
3393
+ }
3394
+ } else if (hasAudioDescriptionSrc) {
3395
+ if (this.enabled) {
3396
+ this.desiredState = false;
3397
+ await this.disable();
3398
+ } else {
3399
+ this.desiredState = true;
3400
+ await this.enable();
3401
+ }
3402
+ }
3403
+ }
3404
+ /**
3405
+ * Get current caption text for synchronization
3406
+ */
3407
+ _getCurrentCaptionText() {
3408
+ if (this.player.captionManager && this.player.captionManager.currentTrack && this.player.captionManager.currentCue) {
3409
+ return this.player.captionManager.currentCue.text;
3410
+ }
3411
+ return null;
3412
+ }
3413
+ /**
3414
+ * Validate that a track URL exists
3415
+ */
3416
+ async _validateTrackExists(url) {
3417
+ try {
3418
+ const response = await fetch(url, { method: "HEAD" });
3419
+ return response.ok;
3420
+ } catch {
3421
+ return false;
3422
+ }
3423
+ }
3424
+ /**
3425
+ * Swap caption tracks to described versions
3426
+ */
3427
+ async _swapCaptionTracks(toDescribed = true) {
3428
+ if (this.captionTracks.length === 0) return [];
3429
+ const swappedTracks = [];
3430
+ const validationPromises = this.captionTracks.map(async (trackInfo) => {
3431
+ if (trackInfo.trackElement && trackInfo.describedSrc) {
3432
+ if (trackInfo.explicit === true) {
3433
+ try {
3434
+ const exists = await this._validateTrackExists(
3435
+ toDescribed ? trackInfo.describedSrc : trackInfo.originalSrc
3436
+ );
3437
+ return { trackInfo, exists };
3438
+ } catch {
3439
+ return { trackInfo, exists: false };
3440
+ }
3441
+ }
3442
+ }
3443
+ return { trackInfo, exists: false };
3444
+ });
3445
+ const validationResults = await Promise.all(validationPromises);
3446
+ const tracksToSwap = validationResults.filter((result) => result.exists);
3447
+ if (tracksToSwap.length > 0) {
3448
+ const trackModes = /* @__PURE__ */ new Map();
3449
+ tracksToSwap.forEach(({ trackInfo }) => {
3450
+ const textTrack = trackInfo.trackElement.track;
3451
+ if (textTrack) {
3452
+ trackModes.set(trackInfo, {
3453
+ wasShowing: textTrack.mode === "showing",
3454
+ wasHidden: textTrack.mode === "hidden"
3455
+ });
3456
+ } else {
3457
+ trackModes.set(trackInfo, { wasShowing: false, wasHidden: false });
3079
3458
  }
3080
3459
  });
3081
- const tracks = this.element.querySelectorAll("track");
3082
- tracks.forEach((track) => {
3083
- mediaElement.appendChild(track.cloneNode(true));
3460
+ const tracksToReadd = tracksToSwap.map(({ trackInfo }) => {
3461
+ const attributes = {};
3462
+ Array.from(trackInfo.trackElement.attributes).forEach((attr) => {
3463
+ attributes[attr.name] = attr.value;
3464
+ });
3465
+ const result = {
3466
+ trackInfo,
3467
+ oldSrc: trackInfo.trackElement.getAttribute("src"),
3468
+ parent: trackInfo.trackElement.parentNode,
3469
+ nextSibling: trackInfo.trackElement.nextSibling,
3470
+ attributes
3471
+ };
3472
+ trackInfo.trackElement.remove();
3473
+ return result;
3474
+ });
3475
+ this.player.element.load();
3476
+ await new Promise((resolve) => {
3477
+ setTimeout(() => {
3478
+ tracksToReadd.forEach(({ trackInfo, parent, nextSibling, attributes }) => {
3479
+ swappedTracks.push(trackInfo);
3480
+ const newTrackElement = document.createElement("track");
3481
+ const newSrc = toDescribed ? trackInfo.describedSrc : trackInfo.originalSrc;
3482
+ newTrackElement.setAttribute("src", newSrc);
3483
+ Object.keys(attributes).forEach((attrName) => {
3484
+ if (attrName !== "src" && attrName !== "data-desc-src") {
3485
+ newTrackElement.setAttribute(attrName, attributes[attrName]);
3486
+ }
3487
+ });
3488
+ const targetParent = parent || this.player.element;
3489
+ if (nextSibling && nextSibling.parentNode) {
3490
+ targetParent.insertBefore(newTrackElement, nextSibling);
3491
+ } else {
3492
+ targetParent.appendChild(newTrackElement);
3493
+ }
3494
+ trackInfo.trackElement = newTrackElement;
3495
+ });
3496
+ this.player.invalidateTrackCache();
3497
+ const setupNewTracks = () => {
3498
+ this.player.setManagedTimeout(() => {
3499
+ swappedTracks.forEach((trackInfo) => {
3500
+ const newTextTrack = trackInfo.trackElement.track;
3501
+ if (newTextTrack) {
3502
+ const modeInfo = trackModes.get(trackInfo) || { wasShowing: false, wasHidden: false };
3503
+ newTextTrack.mode = "hidden";
3504
+ const restoreMode = () => {
3505
+ if (modeInfo.wasShowing || modeInfo.wasHidden) {
3506
+ newTextTrack.mode = "hidden";
3507
+ } else {
3508
+ newTextTrack.mode = "disabled";
3509
+ }
3510
+ };
3511
+ if (newTextTrack.readyState >= 2) {
3512
+ restoreMode();
3513
+ } else {
3514
+ newTextTrack.addEventListener("load", restoreMode, { once: true });
3515
+ newTextTrack.addEventListener("error", restoreMode, { once: true });
3516
+ }
3517
+ }
3518
+ });
3519
+ }, 300);
3520
+ };
3521
+ if (this.player.element.readyState >= 1) {
3522
+ setTimeout(setupNewTracks, 200);
3523
+ } else {
3524
+ this.player.element.addEventListener("loadedmetadata", setupNewTracks, { once: true });
3525
+ setTimeout(setupNewTracks, 2e3);
3526
+ }
3527
+ resolve();
3528
+ }, 100);
3084
3529
  });
3085
- this.element.innerHTML = "";
3086
- this.element.appendChild(mediaElement);
3087
- this.element = mediaElement;
3088
3530
  }
3089
- this._originalElement = this.element;
3090
- this.options = {
3091
- // Display
3092
- width: null,
3093
- height: null,
3094
- poster: null,
3095
- responsive: true,
3531
+ return swappedTracks;
3532
+ }
3533
+ /**
3534
+ * Update source elements to described versions
3535
+ */
3536
+ _updateSourceElements(toDescribed = true) {
3537
+ const sourceElements = this.player.sourceElements;
3538
+ const sourcesToUpdate = [];
3539
+ sourceElements.forEach((sourceEl) => {
3540
+ const descSrcAttr = sourceEl.getAttribute("data-desc-src");
3541
+ const currentSrc = sourceEl.getAttribute("src");
3542
+ if (descSrcAttr) {
3543
+ const type = sourceEl.getAttribute("type");
3544
+ let origSrc = sourceEl.getAttribute("data-orig-src") || currentSrc;
3545
+ sourcesToUpdate.push({
3546
+ src: toDescribed ? descSrcAttr : origSrc,
3547
+ type,
3548
+ origSrc,
3549
+ descSrc: descSrcAttr
3550
+ });
3551
+ } else {
3552
+ sourcesToUpdate.push({
3553
+ src: sourceEl.getAttribute("src"),
3554
+ type: sourceEl.getAttribute("type"),
3555
+ origSrc: null,
3556
+ descSrc: null
3557
+ });
3558
+ }
3559
+ });
3560
+ if (this.player.element.hasAttribute("src")) {
3561
+ this.player.element.removeAttribute("src");
3562
+ }
3563
+ sourceElements.forEach((sourceEl) => sourceEl.remove());
3564
+ sourcesToUpdate.forEach((sourceInfo) => {
3565
+ const newSource = document.createElement("source");
3566
+ newSource.setAttribute("src", sourceInfo.src);
3567
+ if (sourceInfo.type) {
3568
+ newSource.setAttribute("type", sourceInfo.type);
3569
+ }
3570
+ if (sourceInfo.origSrc) {
3571
+ newSource.setAttribute("data-orig-src", sourceInfo.origSrc);
3572
+ }
3573
+ if (sourceInfo.descSrc) {
3574
+ newSource.setAttribute("data-desc-src", sourceInfo.descSrc);
3575
+ }
3576
+ const firstTrack = this.player.element.querySelector("track");
3577
+ if (firstTrack) {
3578
+ this.player.element.insertBefore(newSource, firstTrack);
3579
+ } else {
3580
+ this.player.element.appendChild(newSource);
3581
+ }
3582
+ });
3583
+ this.player._sourceElementsDirty = true;
3584
+ this.player._sourceElementsCache = null;
3585
+ }
3586
+ /**
3587
+ * Wait for media to be ready
3588
+ */
3589
+ async _waitForMediaReady(needSeek = false) {
3590
+ await new Promise((resolve) => {
3591
+ if (this.player.element.readyState >= 1) {
3592
+ resolve();
3593
+ } else {
3594
+ const onLoad = () => {
3595
+ this.player.element.removeEventListener("loadedmetadata", onLoad);
3596
+ resolve();
3597
+ };
3598
+ this.player.element.addEventListener("loadedmetadata", onLoad);
3599
+ }
3600
+ });
3601
+ await new Promise((resolve) => setTimeout(resolve, 300));
3602
+ if (needSeek) {
3603
+ await new Promise((resolve) => {
3604
+ if (this.player.element.readyState >= 3) {
3605
+ resolve();
3606
+ } else {
3607
+ const onCanPlay = () => {
3608
+ this.player.element.removeEventListener("canplay", onCanPlay);
3609
+ this.player.element.removeEventListener("canplaythrough", onCanPlay);
3610
+ resolve();
3611
+ };
3612
+ this.player.element.addEventListener("canplay", onCanPlay, { once: true });
3613
+ this.player.element.addEventListener("canplaythrough", onCanPlay, { once: true });
3614
+ setTimeout(() => {
3615
+ this.player.element.removeEventListener("canplay", onCanPlay);
3616
+ this.player.element.removeEventListener("canplaythrough", onCanPlay);
3617
+ resolve();
3618
+ }, 3e3);
3619
+ }
3620
+ });
3621
+ }
3622
+ }
3623
+ /**
3624
+ * Restore playback state after source change
3625
+ */
3626
+ async _restorePlaybackState(currentTime, wasPlaying, shouldKeepPoster, currentCaptionText) {
3627
+ let syncTime = currentTime;
3628
+ if (currentCaptionText && this.player.captionManager && this.player.captionManager.tracks.length > 0) {
3629
+ await new Promise((resolve) => setTimeout(resolve, 500));
3630
+ const matchingTime = this.player.findMatchingCaptionTime(
3631
+ currentCaptionText,
3632
+ this.player.captionManager.tracks
3633
+ );
3634
+ if (matchingTime !== null) {
3635
+ syncTime = matchingTime;
3636
+ if (this.player.options.debug) {
3637
+ this.player.log(`[VidPly] Syncing via caption: ${currentTime}s -> ${syncTime}s`);
3638
+ }
3639
+ }
3640
+ }
3641
+ if (syncTime > 0) {
3642
+ this.player.seek(syncTime);
3643
+ await new Promise((resolve) => setTimeout(resolve, 100));
3644
+ }
3645
+ if (wasPlaying) {
3646
+ await this.player.play();
3647
+ this.player.setManagedTimeout(() => {
3648
+ this.player.hidePosterOverlay();
3649
+ }, 100);
3650
+ } else {
3651
+ this.player.pause();
3652
+ if (!shouldKeepPoster) {
3653
+ this.player.hidePosterOverlay();
3654
+ }
3655
+ }
3656
+ }
3657
+ /**
3658
+ * Enable with source element method
3659
+ */
3660
+ async _enableWithSourceElement(currentTime, wasPlaying, posterValue, shouldKeepPoster, currentCaptionText) {
3661
+ await this._swapCaptionTracks(true);
3662
+ this._updateSourceElements(true);
3663
+ if (posterValue && this.player.element.tagName === "VIDEO") {
3664
+ this.player.element.poster = posterValue;
3665
+ }
3666
+ this.player.element.load();
3667
+ await this._waitForMediaReady(currentTime > 0 || wasPlaying);
3668
+ await this._restorePlaybackState(currentTime, wasPlaying, shouldKeepPoster, currentCaptionText);
3669
+ if (!this.desiredState) return;
3670
+ this.enabled = true;
3671
+ this.player.state.audioDescriptionEnabled = true;
3672
+ this.player.emit("audiodescriptionenabled");
3673
+ this._reloadTranscript();
3674
+ }
3675
+ /**
3676
+ * Enable with direct src method
3677
+ */
3678
+ async _enableWithDirectSrc(currentTime, wasPlaying, posterValue, shouldKeepPoster) {
3679
+ await this._swapCaptionTracks(true);
3680
+ if (posterValue && this.player.element.tagName === "VIDEO") {
3681
+ this.player.element.poster = posterValue;
3682
+ }
3683
+ this.player.element.src = this.src;
3684
+ await this._waitForMediaReady(currentTime > 0 || wasPlaying);
3685
+ if (currentTime > 0) {
3686
+ this.player.seek(currentTime);
3687
+ await new Promise((resolve) => setTimeout(resolve, 100));
3688
+ }
3689
+ if (wasPlaying) {
3690
+ await this.player.play();
3691
+ } else {
3692
+ this.player.pause();
3693
+ if (!shouldKeepPoster) {
3694
+ this.player.hidePosterOverlay();
3695
+ }
3696
+ }
3697
+ if (!this.desiredState) return;
3698
+ this.enabled = true;
3699
+ this.player.state.audioDescriptionEnabled = true;
3700
+ this.player.emit("audiodescriptionenabled");
3701
+ this._reloadTranscript();
3702
+ }
3703
+ /**
3704
+ * Disable with source element method
3705
+ */
3706
+ async _disableWithSourceElement(currentTime, wasPlaying, posterValue, shouldKeepPoster, currentCaptionText) {
3707
+ await this._swapCaptionTracks(false);
3708
+ this._updateSourceElements(false);
3709
+ if (posterValue && this.player.element.tagName === "VIDEO") {
3710
+ this.player.element.poster = posterValue;
3711
+ }
3712
+ this.player.element.load();
3713
+ this.player.invalidateTrackCache();
3714
+ await this._waitForMediaReady(currentTime > 0 || wasPlaying);
3715
+ await this._restorePlaybackState(currentTime, wasPlaying, shouldKeepPoster, currentCaptionText);
3716
+ if (this.player.captionManager) {
3717
+ this.player.captionManager.destroy();
3718
+ this.player.captionManager = new CaptionManager(this.player);
3719
+ }
3720
+ if (this.desiredState) return;
3721
+ this.enabled = false;
3722
+ this.player.state.audioDescriptionEnabled = false;
3723
+ this.player.emit("audiodescriptiondisabled");
3724
+ this._reloadTranscript();
3725
+ }
3726
+ /**
3727
+ * Disable with direct src method
3728
+ */
3729
+ async _disableWithDirectSrc(currentTime, wasPlaying, posterValue) {
3730
+ await this._swapCaptionTracks(false);
3731
+ if (posterValue && this.player.element.tagName === "VIDEO") {
3732
+ this.player.element.poster = posterValue;
3733
+ }
3734
+ const originalSrcToUse = this.originalSource || this.player.originalSrc;
3735
+ this.player.element.src = originalSrcToUse;
3736
+ this.player.element.load();
3737
+ await this._waitForMediaReady(currentTime > 0 || wasPlaying);
3738
+ if (currentTime > 0) {
3739
+ this.player.seek(currentTime);
3740
+ }
3741
+ if (wasPlaying) {
3742
+ await this.player.play();
3743
+ }
3744
+ if (this.desiredState) return;
3745
+ this.enabled = false;
3746
+ this.player.state.audioDescriptionEnabled = false;
3747
+ this.player.emit("audiodescriptiondisabled");
3748
+ this._reloadTranscript();
3749
+ }
3750
+ /**
3751
+ * Reload transcript after audio description state change
3752
+ */
3753
+ _reloadTranscript() {
3754
+ if (this.player.transcriptManager && this.player.transcriptManager.isVisible) {
3755
+ this.player.setManagedTimeout(() => {
3756
+ if (this.player.transcriptManager && this.player.transcriptManager.loadTranscriptData) {
3757
+ this.player.transcriptManager.loadTranscriptData();
3758
+ }
3759
+ }, 800);
3760
+ }
3761
+ }
3762
+ /**
3763
+ * Update sources (called when playlist changes)
3764
+ */
3765
+ updateSources(audioDescriptionSrc) {
3766
+ this.src = audioDescriptionSrc || null;
3767
+ this.enabled = false;
3768
+ this.desiredState = false;
3769
+ this.sourceElement = null;
3770
+ this.originalSource = null;
3771
+ this.captionTracks = [];
3772
+ }
3773
+ /**
3774
+ * Reinitialize from current player elements (called after playlist loads new tracks)
3775
+ */
3776
+ reinitialize() {
3777
+ this.player.invalidateTrackCache();
3778
+ this.initFromSourceElements(this.player.sourceElements, this.player.trackElements);
3779
+ }
3780
+ /**
3781
+ * Cleanup
3782
+ */
3783
+ destroy() {
3784
+ this.enabled = false;
3785
+ this.desiredState = false;
3786
+ this.captionTracks = [];
3787
+ this.sourceElement = null;
3788
+ this.originalSource = null;
3789
+ }
3790
+ };
3791
+
3792
+ // src/core/SignLanguageManager.js
3793
+ var SignLanguageManager = class {
3794
+ constructor(player) {
3795
+ this.player = player;
3796
+ this.src = player.options.signLanguageSrc;
3797
+ this.sources = player.options.signLanguageSources || {};
3798
+ this.currentLanguage = null;
3799
+ this.desiredPosition = player.options.signLanguagePosition || "bottom-right";
3800
+ this.wrapper = null;
3801
+ this.header = null;
3802
+ this.video = null;
3803
+ this.selector = null;
3804
+ this.settingsButton = null;
3805
+ this.settingsMenu = null;
3806
+ this.resizeHandles = [];
3807
+ this.enabled = false;
3808
+ this.settingsMenuVisible = false;
3809
+ this.settingsMenuJustOpened = false;
3810
+ this.documentClickHandlerAdded = false;
3811
+ this.handlers = null;
3812
+ this.settingsHandlers = null;
3813
+ this.interactionHandlers = null;
3814
+ this.draggable = null;
3815
+ this.documentClickHandler = null;
3816
+ this.settingsMenuKeyHandler = null;
3817
+ this.customKeyHandler = null;
3818
+ this.dragOptionButton = null;
3819
+ this.dragOptionText = null;
3820
+ this.resizeOptionButton = null;
3821
+ this.resizeOptionText = null;
3822
+ }
3823
+ /**
3824
+ * Check if sign language is available
3825
+ */
3826
+ isAvailable() {
3827
+ return Object.keys(this.sources).length > 0 || !!this.src;
3828
+ }
3829
+ /**
3830
+ * Enable sign language video
3831
+ */
3832
+ enable() {
3833
+ const hasMultipleSources = Object.keys(this.sources).length > 0;
3834
+ const hasSingleSource = !!this.src;
3835
+ if (!hasMultipleSources && !hasSingleSource) {
3836
+ console.warn("No sign language video source provided");
3837
+ return;
3838
+ }
3839
+ if (this.wrapper) {
3840
+ this.wrapper.style.display = "block";
3841
+ this.enabled = true;
3842
+ this.player.state.signLanguageEnabled = true;
3843
+ this.player.emit("signlanguageenabled");
3844
+ this.player.setManagedTimeout(() => {
3845
+ if (this.settingsButton && document.contains(this.settingsButton)) {
3846
+ this.settingsButton.focus({ preventScroll: true });
3847
+ }
3848
+ }, 150);
3849
+ return;
3850
+ }
3851
+ let initialLang = null;
3852
+ let initialSrc = null;
3853
+ if (hasMultipleSources) {
3854
+ initialLang = this._determineInitialLanguage();
3855
+ initialSrc = this.sources[initialLang];
3856
+ this.currentLanguage = initialLang;
3857
+ } else {
3858
+ initialSrc = this.src;
3859
+ }
3860
+ this._createWrapper();
3861
+ this._createHeader(hasMultipleSources, initialLang);
3862
+ this._createVideo(initialSrc);
3863
+ this._createResizeHandles();
3864
+ this.wrapper.appendChild(this.header);
3865
+ this.wrapper.appendChild(this.video);
3866
+ this.resizeHandles.forEach((handle) => this.wrapper.appendChild(handle));
3867
+ this._applyInitialSize();
3868
+ this.player.container.appendChild(this.wrapper);
3869
+ requestAnimationFrame(() => {
3870
+ this.constrainPosition();
3871
+ });
3872
+ this.video.currentTime = this.player.state.currentTime;
3873
+ if (!this.player.state.paused) {
3874
+ this.video.play();
3875
+ }
3876
+ this._setupInteraction();
3877
+ this._setupEventHandlers(hasMultipleSources);
3878
+ this.enabled = true;
3879
+ this.player.state.signLanguageEnabled = true;
3880
+ this.player.emit("signlanguageenabled");
3881
+ this.player.setManagedTimeout(() => {
3882
+ if (this.settingsButton && document.contains(this.settingsButton)) {
3883
+ this.settingsButton.focus({ preventScroll: true });
3884
+ }
3885
+ }, 150);
3886
+ }
3887
+ /**
3888
+ * Disable sign language video
3889
+ */
3890
+ disable() {
3891
+ if (this.settingsMenuVisible) {
3892
+ this.hideSettingsMenu({ focusButton: false });
3893
+ }
3894
+ if (this.wrapper) {
3895
+ this.wrapper.style.display = "none";
3896
+ }
3897
+ this.enabled = false;
3898
+ this.player.state.signLanguageEnabled = false;
3899
+ this.player.emit("signlanguagedisabled");
3900
+ }
3901
+ /**
3902
+ * Toggle sign language video
3903
+ */
3904
+ toggle() {
3905
+ if (this.enabled) {
3906
+ this.disable();
3907
+ } else {
3908
+ this.enable();
3909
+ }
3910
+ }
3911
+ /**
3912
+ * Switch to a different sign language
3913
+ */
3914
+ switchLanguage(langCode) {
3915
+ if (!this.sources[langCode] || !this.video) {
3916
+ return;
3917
+ }
3918
+ const currentTime = this.video.currentTime;
3919
+ const wasPlaying = !this.video.paused;
3920
+ this.video.src = this.sources[langCode];
3921
+ this.currentLanguage = langCode;
3922
+ this.video.currentTime = currentTime;
3923
+ if (wasPlaying) {
3924
+ this.video.play().catch(() => {
3925
+ });
3926
+ }
3927
+ this.player.emit("signlanguagelanguagechanged", langCode);
3928
+ }
3929
+ /**
3930
+ * Get language label
3931
+ */
3932
+ getLanguageLabel(langCode) {
3933
+ const langNames = {
3934
+ "en": "English",
3935
+ "de": "Deutsch",
3936
+ "es": "Español",
3937
+ "fr": "Français",
3938
+ "it": "Italiano",
3939
+ "ja": "日本語",
3940
+ "pt": "Português",
3941
+ "ar": "العربية",
3942
+ "hi": "हिन्दी"
3943
+ };
3944
+ return langNames[langCode] || langCode.toUpperCase();
3945
+ }
3946
+ /**
3947
+ * Determine initial sign language
3948
+ */
3949
+ _determineInitialLanguage() {
3950
+ if (this.player.captionManager && this.player.captionManager.currentTrack) {
3951
+ const captionLang = this.player.captionManager.currentTrack.language?.toLowerCase().split("-")[0];
3952
+ if (captionLang && this.sources[captionLang]) {
3953
+ return captionLang;
3954
+ }
3955
+ }
3956
+ if (this.player.options.language) {
3957
+ const playerLang = this.player.options.language.toLowerCase().split("-")[0];
3958
+ if (this.sources[playerLang]) {
3959
+ return playerLang;
3960
+ }
3961
+ }
3962
+ return Object.keys(this.sources)[0];
3963
+ }
3964
+ /**
3965
+ * Create wrapper element
3966
+ */
3967
+ _createWrapper() {
3968
+ this.wrapper = document.createElement("div");
3969
+ this.wrapper.className = "vidply-sign-language-wrapper";
3970
+ this.wrapper.setAttribute("tabindex", "0");
3971
+ this.wrapper.setAttribute("aria-label", i18n.t("player.signLanguageDragResize"));
3972
+ }
3973
+ /**
3974
+ * Create header element
3975
+ */
3976
+ _createHeader(hasMultipleSources, initialLang) {
3977
+ const classPrefix = this.player.options.classPrefix;
3978
+ this.header = DOMUtils.createElement("div", {
3979
+ className: `${classPrefix}-sign-language-header`,
3980
+ attributes: { "tabindex": "0" }
3981
+ });
3982
+ const headerLeft = DOMUtils.createElement("div", {
3983
+ className: `${classPrefix}-sign-language-header-left`
3984
+ });
3985
+ const title = DOMUtils.createElement("h3", {
3986
+ textContent: i18n.t("player.signLanguageVideo")
3987
+ });
3988
+ this._createSettingsButton(headerLeft);
3989
+ if (hasMultipleSources) {
3990
+ this._createLanguageSelector(headerLeft, initialLang);
3991
+ }
3992
+ headerLeft.appendChild(title);
3993
+ const closeButton = this._createCloseButton();
3994
+ this.header.appendChild(headerLeft);
3995
+ this.header.appendChild(closeButton);
3996
+ this.settingsMenuVisible = false;
3997
+ this.settingsMenu = null;
3998
+ this.settingsMenuJustOpened = false;
3999
+ }
4000
+ /**
4001
+ * Create settings button
4002
+ */
4003
+ _createSettingsButton(container) {
4004
+ const classPrefix = this.player.options.classPrefix;
4005
+ const ariaLabel = i18n.t("player.signLanguageSettings");
4006
+ this.settingsButton = DOMUtils.createElement("button", {
4007
+ className: `${classPrefix}-sign-language-settings`,
4008
+ attributes: {
4009
+ "type": "button",
4010
+ "aria-label": ariaLabel,
4011
+ "aria-expanded": "false"
4012
+ }
4013
+ });
4014
+ this.settingsButton.appendChild(createIconElement("settings"));
4015
+ DOMUtils.attachTooltip(this.settingsButton, ariaLabel, classPrefix);
4016
+ this.settingsHandlers = {
4017
+ click: (e) => {
4018
+ e.preventDefault();
4019
+ e.stopPropagation();
4020
+ if (this.documentClickHandler) {
4021
+ this.settingsMenuJustOpened = true;
4022
+ setTimeout(() => {
4023
+ this.settingsMenuJustOpened = false;
4024
+ }, 100);
4025
+ }
4026
+ if (this.settingsMenuVisible) {
4027
+ this.hideSettingsMenu();
4028
+ } else {
4029
+ this.showSettingsMenu();
4030
+ }
4031
+ },
4032
+ keydown: (e) => {
4033
+ if (e.key === "d" || e.key === "D") {
4034
+ e.preventDefault();
4035
+ e.stopPropagation();
4036
+ this.toggleKeyboardDragMode();
4037
+ } else if (e.key === "r" || e.key === "R") {
4038
+ e.preventDefault();
4039
+ e.stopPropagation();
4040
+ this.toggleResizeMode();
4041
+ } else if (e.key === "Escape" && this.settingsMenuVisible) {
4042
+ e.preventDefault();
4043
+ e.stopPropagation();
4044
+ this.hideSettingsMenu();
4045
+ }
4046
+ }
4047
+ };
4048
+ this.settingsButton.addEventListener("click", this.settingsHandlers.click);
4049
+ this.settingsButton.addEventListener("keydown", this.settingsHandlers.keydown);
4050
+ container.appendChild(this.settingsButton);
4051
+ }
4052
+ /**
4053
+ * Create language selector
4054
+ */
4055
+ _createLanguageSelector(container, initialLang) {
4056
+ const classPrefix = this.player.options.classPrefix;
4057
+ const selectId = `${classPrefix}-sign-language-select-${Date.now()}`;
4058
+ const options = Object.keys(this.sources).map((langCode) => ({
4059
+ value: langCode,
4060
+ text: this.getLanguageLabel(langCode),
4061
+ selected: langCode === initialLang
4062
+ }));
4063
+ const { label, select } = createLabeledSelect({
4064
+ classPrefix,
4065
+ labelClass: `${classPrefix}-sign-language-label`,
4066
+ selectClass: `${classPrefix}-sign-language-select`,
4067
+ labelText: "settings.language",
4068
+ selectId,
4069
+ options,
4070
+ onChange: (e) => {
4071
+ e.stopPropagation();
4072
+ this.switchLanguage(e.target.value);
4073
+ }
4074
+ });
4075
+ this.selector = select;
4076
+ const selectorWrapper = DOMUtils.createElement("div", {
4077
+ className: `${classPrefix}-sign-language-selector-wrapper`
4078
+ });
4079
+ selectorWrapper.appendChild(label);
4080
+ selectorWrapper.appendChild(this.selector);
4081
+ preventDragOnElement(selectorWrapper);
4082
+ container.appendChild(selectorWrapper);
4083
+ }
4084
+ /**
4085
+ * Create close button
4086
+ */
4087
+ _createCloseButton() {
4088
+ const classPrefix = this.player.options.classPrefix;
4089
+ const ariaLabel = i18n.t("player.closeSignLanguage");
4090
+ const closeButton = DOMUtils.createElement("button", {
4091
+ className: `${classPrefix}-sign-language-close`,
4092
+ attributes: {
4093
+ "type": "button",
4094
+ "aria-label": ariaLabel
4095
+ }
4096
+ });
4097
+ closeButton.appendChild(createIconElement("close"));
4098
+ DOMUtils.attachTooltip(closeButton, ariaLabel, classPrefix);
4099
+ closeButton.addEventListener("click", () => {
4100
+ this.disable();
4101
+ if (this.player.controlBar?.controls?.signLanguage) {
4102
+ setTimeout(() => {
4103
+ this.player.controlBar.controls.signLanguage.focus({ preventScroll: true });
4104
+ }, 0);
4105
+ }
4106
+ });
4107
+ return closeButton;
4108
+ }
4109
+ /**
4110
+ * Create video element
4111
+ */
4112
+ _createVideo(src) {
4113
+ this.video = document.createElement("video");
4114
+ this.video.className = "vidply-sign-language-video";
4115
+ this.video.src = src;
4116
+ this.video.setAttribute("aria-label", i18n.t("player.signLanguage"));
4117
+ this.video.muted = true;
4118
+ this.video.setAttribute("playsinline", "");
4119
+ }
4120
+ /**
4121
+ * Create resize handles
4122
+ */
4123
+ _createResizeHandles() {
4124
+ const classPrefix = this.player.options.classPrefix;
4125
+ this.resizeHandles = ["n", "s", "e", "w", "ne", "nw", "se", "sw"].map((dir) => {
4126
+ const handle = DOMUtils.createElement("div", {
4127
+ className: `${classPrefix}-sign-resize-handle ${classPrefix}-sign-resize-${dir}`,
4128
+ attributes: {
4129
+ "data-direction": dir,
4130
+ "data-vidply-managed-resize": "true",
4131
+ "aria-hidden": "true"
4132
+ }
4133
+ });
4134
+ handle.style.display = "none";
4135
+ return handle;
4136
+ });
4137
+ }
4138
+ /**
4139
+ * Apply initial size
4140
+ */
4141
+ _applyInitialSize() {
4142
+ const saved = this.player.storage.getSignLanguagePreferences();
4143
+ if (saved?.size?.width) {
4144
+ this.wrapper.style.width = saved.size.width;
4145
+ } else {
4146
+ this.wrapper.style.width = "280px";
4147
+ }
4148
+ this.wrapper.style.height = "auto";
4149
+ }
4150
+ /**
4151
+ * Setup interaction (drag and resize)
4152
+ */
4153
+ _setupInteraction() {
4154
+ const isMobile2 = window.innerWidth < 768;
4155
+ const isFullscreen = this.player.state.fullscreen;
4156
+ if (isMobile2 && !isFullscreen) {
4157
+ if (this.draggable) {
4158
+ this.draggable.destroy();
4159
+ this.draggable = null;
4160
+ }
4161
+ return;
4162
+ }
4163
+ if (this.draggable) return;
4164
+ const classPrefix = this.player.options.classPrefix;
4165
+ this.draggable = new DraggableResizable(this.wrapper, {
4166
+ dragHandle: this.header,
4167
+ resizeHandles: this.resizeHandles,
4168
+ constrainToViewport: true,
4169
+ maintainAspectRatio: true,
4170
+ minWidth: 150,
4171
+ minHeight: 100,
4172
+ classPrefix: `${classPrefix}-sign`,
4173
+ keyboardDragKey: "d",
4174
+ keyboardResizeKey: "r",
4175
+ keyboardStep: 10,
4176
+ keyboardStepLarge: 50,
4177
+ pointerResizeIndicatorText: i18n.t("player.signLanguageResizeActive"),
4178
+ onPointerResizeToggle: (enabled) => {
4179
+ this.resizeHandles.forEach((handle) => {
4180
+ handle.style.display = enabled ? "block" : "none";
4181
+ });
4182
+ },
4183
+ onDragStart: (e) => {
4184
+ if (e.target.closest(`.${classPrefix}-sign-language-close`) || e.target.closest(`.${classPrefix}-sign-language-settings`) || e.target.closest(`.${classPrefix}-sign-language-select`) || e.target.closest(`.${classPrefix}-sign-language-label`) || e.target.closest(`.${classPrefix}-sign-language-settings-menu`)) {
4185
+ return false;
4186
+ }
4187
+ return true;
4188
+ }
4189
+ });
4190
+ this._setupCustomKeyHandler();
4191
+ this.interactionHandlers = {
4192
+ draggable: this.draggable,
4193
+ customKeyHandler: this.customKeyHandler
4194
+ };
4195
+ }
4196
+ /**
4197
+ * Setup custom keyboard handler
4198
+ */
4199
+ _setupCustomKeyHandler() {
4200
+ this.customKeyHandler = (e) => {
4201
+ const key = e.key.toLowerCase();
4202
+ if (this.settingsMenuVisible) return;
4203
+ if (key === "home") {
4204
+ e.preventDefault();
4205
+ e.stopPropagation();
4206
+ if (this.draggable) {
4207
+ if (this.draggable.pointerResizeMode) {
4208
+ this.draggable.disablePointerResizeMode();
4209
+ }
4210
+ this.draggable.manuallyPositioned = false;
4211
+ this.constrainPosition();
4212
+ }
4213
+ return;
4214
+ }
4215
+ if (key === "r") {
4216
+ e.preventDefault();
4217
+ e.stopPropagation();
4218
+ if (this.toggleResizeMode()) {
4219
+ this.wrapper.focus({ preventScroll: true });
4220
+ }
4221
+ return;
4222
+ }
4223
+ if (key === "escape") {
4224
+ e.preventDefault();
4225
+ e.stopPropagation();
4226
+ if (this.draggable?.pointerResizeMode) {
4227
+ this.draggable.disablePointerResizeMode();
4228
+ return;
4229
+ }
4230
+ if (this.draggable?.keyboardDragMode) {
4231
+ this.draggable.disableKeyboardDragMode();
4232
+ return;
4233
+ }
4234
+ this.disable();
4235
+ if (this.player.controlBar?.controls?.signLanguage) {
4236
+ setTimeout(() => {
4237
+ this.player.controlBar.controls.signLanguage.focus({ preventScroll: true });
4238
+ }, 0);
4239
+ }
4240
+ }
4241
+ };
4242
+ this.wrapper.addEventListener("keydown", this.customKeyHandler);
4243
+ }
4244
+ /**
4245
+ * Setup event handlers
4246
+ */
4247
+ _setupEventHandlers(hasMultipleSources) {
4248
+ this.handlers = {
4249
+ play: () => {
4250
+ if (this.video) this.video.play();
4251
+ },
4252
+ pause: () => {
4253
+ if (this.video) this.video.pause();
4254
+ },
4255
+ timeupdate: () => {
4256
+ if (this.video && Math.abs(this.video.currentTime - this.player.state.currentTime) > 0.5) {
4257
+ this.video.currentTime = this.player.state.currentTime;
4258
+ }
4259
+ },
4260
+ ratechange: () => {
4261
+ if (this.video) this.video.playbackRate = this.player.state.playbackSpeed;
4262
+ }
4263
+ };
4264
+ this.player.on("play", this.handlers.play);
4265
+ this.player.on("pause", this.handlers.pause);
4266
+ this.player.on("timeupdate", this.handlers.timeupdate);
4267
+ this.player.on("ratechange", this.handlers.ratechange);
4268
+ if (hasMultipleSources) {
4269
+ this.handlers.captionChange = () => {
4270
+ if (this.player.captionManager?.currentTrack && this.selector) {
4271
+ const captionLang = this.player.captionManager.currentTrack.language?.toLowerCase().split("-")[0];
4272
+ if (captionLang && this.sources[captionLang] && this.currentLanguage !== captionLang) {
4273
+ this.switchLanguage(captionLang);
4274
+ this.selector.value = captionLang;
4275
+ }
4276
+ }
4277
+ };
4278
+ this.player.on("captionsenabled", this.handlers.captionChange);
4279
+ }
4280
+ }
4281
+ /**
4282
+ * Constrain position within video wrapper
4283
+ */
4284
+ constrainPosition() {
4285
+ if (!this.wrapper || !this.player.videoWrapper) return;
4286
+ if (this.draggable?.manuallyPositioned) return;
4287
+ if (!this.wrapper.style.width) {
4288
+ this.wrapper.style.width = "280px";
4289
+ }
4290
+ const videoWrapperRect = this.player.videoWrapper.getBoundingClientRect();
4291
+ const containerRect = this.player.container.getBoundingClientRect();
4292
+ const wrapperRect = this.wrapper.getBoundingClientRect();
4293
+ const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
4294
+ const videoWrapperTop = videoWrapperRect.top - containerRect.top;
4295
+ const videoWrapperWidth = videoWrapperRect.width;
4296
+ const videoWrapperHeight = videoWrapperRect.height;
4297
+ let wrapperWidth = wrapperRect.width || 280;
4298
+ let wrapperHeight = wrapperRect.height || 280 * 9 / 16;
4299
+ let left, top;
4300
+ const margin = 16;
4301
+ const controlsHeight = 95;
4302
+ const position = this.desiredPosition || "bottom-right";
4303
+ switch (position) {
4304
+ case "bottom-right":
4305
+ left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
4306
+ top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
4307
+ break;
4308
+ case "bottom-left":
4309
+ left = videoWrapperLeft + margin;
4310
+ top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
4311
+ break;
4312
+ case "top-right":
4313
+ left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
4314
+ top = videoWrapperTop + margin;
4315
+ break;
4316
+ case "top-left":
4317
+ left = videoWrapperLeft + margin;
4318
+ top = videoWrapperTop + margin;
4319
+ break;
4320
+ default:
4321
+ left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
4322
+ top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
4323
+ }
4324
+ left = Math.max(videoWrapperLeft, Math.min(left, videoWrapperLeft + videoWrapperWidth - wrapperWidth));
4325
+ top = Math.max(videoWrapperTop, Math.min(top, videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight));
4326
+ this.wrapper.style.left = `${left}px`;
4327
+ this.wrapper.style.top = `${top}px`;
4328
+ this.wrapper.style.right = "auto";
4329
+ this.wrapper.style.bottom = "auto";
4330
+ }
4331
+ /**
4332
+ * Show settings menu
4333
+ */
4334
+ showSettingsMenu() {
4335
+ this.settingsMenuJustOpened = true;
4336
+ setTimeout(() => {
4337
+ this.settingsMenuJustOpened = false;
4338
+ }, 350);
4339
+ this._addDocumentClickHandler();
4340
+ if (this.settingsMenu) {
4341
+ this.settingsMenu.style.display = "block";
4342
+ this.settingsMenuVisible = true;
4343
+ this.settingsButton?.setAttribute("aria-expanded", "true");
4344
+ this._attachMenuKeyboardNavigation();
4345
+ this._positionSettingsMenu();
4346
+ this._updateDragOptionState();
4347
+ this._updateResizeOptionState();
4348
+ focusFirstMenuItem(this.settingsMenu, `.${this.player.options.classPrefix}-sign-language-settings-item`);
4349
+ return;
4350
+ }
4351
+ this._createSettingsMenu();
4352
+ }
4353
+ /**
4354
+ * Hide settings menu
4355
+ */
4356
+ hideSettingsMenu({ focusButton = true } = {}) {
4357
+ if (this.settingsMenu) {
4358
+ this.settingsMenu.style.display = "none";
4359
+ this.settingsMenuVisible = false;
4360
+ this.settingsMenuJustOpened = false;
4361
+ if (this.settingsMenuKeyHandler) {
4362
+ this.settingsMenu.removeEventListener("keydown", this.settingsMenuKeyHandler);
4363
+ this.settingsMenuKeyHandler = null;
4364
+ }
4365
+ const classPrefix = this.player.options.classPrefix;
4366
+ const menuItems = Array.from(this.settingsMenu.querySelectorAll(`.${classPrefix}-sign-language-settings-item`));
4367
+ menuItems.forEach((item) => item.setAttribute("tabindex", "-1"));
4368
+ if (this.settingsButton) {
4369
+ this.settingsButton.setAttribute("aria-expanded", "false");
4370
+ if (focusButton) {
4371
+ this.settingsButton.focus({ preventScroll: true });
4372
+ }
4373
+ }
4374
+ }
4375
+ }
4376
+ /**
4377
+ * Add document click handler
4378
+ */
4379
+ _addDocumentClickHandler() {
4380
+ if (this.documentClickHandlerAdded) return;
4381
+ this.documentClickHandler = (e) => {
4382
+ if (this.settingsMenuJustOpened) return;
4383
+ if (this.settingsButton && (this.settingsButton === e.target || this.settingsButton.contains(e.target))) {
4384
+ return;
4385
+ }
4386
+ if (this.settingsMenu && this.settingsMenu.contains(e.target)) {
4387
+ return;
4388
+ }
4389
+ if (this.settingsMenuVisible) {
4390
+ this.hideSettingsMenu();
4391
+ }
4392
+ };
4393
+ setTimeout(() => {
4394
+ document.addEventListener("mousedown", this.documentClickHandler, true);
4395
+ this.documentClickHandlerAdded = true;
4396
+ }, 300);
4397
+ }
4398
+ /**
4399
+ * Create settings menu
4400
+ */
4401
+ _createSettingsMenu() {
4402
+ const classPrefix = this.player.options.classPrefix;
4403
+ this.settingsMenu = DOMUtils.createElement("div", {
4404
+ className: `${classPrefix}-sign-language-settings-menu`,
4405
+ attributes: { "role": "menu" }
4406
+ });
4407
+ const dragOption = createMenuItem({
4408
+ classPrefix,
4409
+ itemClass: `${classPrefix}-sign-language-settings-item`,
4410
+ icon: "move",
4411
+ label: "player.enableSignDragMode",
4412
+ hasTextClass: true,
4413
+ onClick: () => {
4414
+ this.toggleKeyboardDragMode();
4415
+ this.hideSettingsMenu();
4416
+ }
4417
+ });
4418
+ dragOption.setAttribute("role", "switch");
4419
+ dragOption.setAttribute("aria-checked", "false");
4420
+ this._removeTooltipFromMenuItem(dragOption);
4421
+ this.dragOptionButton = dragOption;
4422
+ this.dragOptionText = dragOption.querySelector(`.${classPrefix}-settings-text`);
4423
+ this._updateDragOptionState();
4424
+ const resizeOption = createMenuItem({
4425
+ classPrefix,
4426
+ itemClass: `${classPrefix}-sign-language-settings-item`,
4427
+ icon: "resize",
4428
+ label: "player.enableSignResizeMode",
4429
+ hasTextClass: true,
4430
+ onClick: (event) => {
4431
+ event.preventDefault();
4432
+ event.stopPropagation();
4433
+ const enabled = this.toggleResizeMode({ focus: false });
4434
+ if (enabled) {
4435
+ this.hideSettingsMenu({ focusButton: false });
4436
+ setTimeout(() => {
4437
+ if (this.wrapper) this.wrapper.focus({ preventScroll: true });
4438
+ }, 20);
4439
+ } else {
4440
+ this.hideSettingsMenu({ focusButton: true });
4441
+ }
4442
+ }
4443
+ });
4444
+ resizeOption.setAttribute("role", "switch");
4445
+ resizeOption.setAttribute("aria-checked", "false");
4446
+ this._removeTooltipFromMenuItem(resizeOption);
4447
+ this.resizeOptionButton = resizeOption;
4448
+ this.resizeOptionText = resizeOption.querySelector(`.${classPrefix}-settings-text`);
4449
+ this._updateResizeOptionState();
4450
+ const closeOption = createMenuItem({
4451
+ classPrefix,
4452
+ itemClass: `${classPrefix}-sign-language-settings-item`,
4453
+ icon: "close",
4454
+ label: "transcript.closeMenu",
4455
+ onClick: () => this.hideSettingsMenu()
4456
+ });
4457
+ this._removeTooltipFromMenuItem(closeOption);
4458
+ this.settingsMenu.appendChild(dragOption);
4459
+ this.settingsMenu.appendChild(resizeOption);
4460
+ this.settingsMenu.appendChild(closeOption);
4461
+ this.settingsMenu.style.visibility = "hidden";
4462
+ this.settingsMenu.style.display = "block";
4463
+ if (this.settingsButton?.parentNode) {
4464
+ this.settingsButton.insertAdjacentElement("afterend", this.settingsMenu);
4465
+ } else if (this.wrapper) {
4466
+ this.wrapper.appendChild(this.settingsMenu);
4467
+ }
4468
+ this._positionSettingsMenuImmediate();
4469
+ requestAnimationFrame(() => {
4470
+ if (this.settingsMenu) {
4471
+ this.settingsMenu.style.visibility = "visible";
4472
+ }
4473
+ });
4474
+ this._attachMenuKeyboardNavigation();
4475
+ this.settingsMenuVisible = true;
4476
+ this.settingsButton?.setAttribute("aria-expanded", "true");
4477
+ this._updateDragOptionState();
4478
+ this._updateResizeOptionState();
4479
+ focusFirstMenuItem(this.settingsMenu, `.${classPrefix}-sign-language-settings-item`);
4480
+ }
4481
+ /**
4482
+ * Remove tooltip from menu item
4483
+ */
4484
+ _removeTooltipFromMenuItem(item) {
4485
+ const classPrefix = this.player.options.classPrefix;
4486
+ const tooltip = item.querySelector(`.${classPrefix}-tooltip`);
4487
+ if (tooltip) tooltip.remove();
4488
+ const buttonText = item.querySelector(`.${classPrefix}-button-text`);
4489
+ if (buttonText) buttonText.remove();
4490
+ }
4491
+ /**
4492
+ * Attach menu keyboard navigation
4493
+ */
4494
+ _attachMenuKeyboardNavigation() {
4495
+ if (this.settingsMenuKeyHandler) {
4496
+ this.settingsMenu.removeEventListener("keydown", this.settingsMenuKeyHandler);
4497
+ }
4498
+ this.settingsMenuKeyHandler = attachMenuKeyboardNavigation(
4499
+ this.settingsMenu,
4500
+ this.settingsButton,
4501
+ `.${this.player.options.classPrefix}-sign-language-settings-item`,
4502
+ () => this.hideSettingsMenu({ focusButton: true })
4503
+ );
4504
+ }
4505
+ /**
4506
+ * Position settings menu immediately
4507
+ */
4508
+ _positionSettingsMenuImmediate() {
4509
+ if (!this.settingsMenu || !this.settingsButton) return;
4510
+ const buttonRect = this.settingsButton.getBoundingClientRect();
4511
+ const menuRect = this.settingsMenu.getBoundingClientRect();
4512
+ const viewportWidth = window.innerWidth;
4513
+ const viewportHeight = window.innerHeight;
4514
+ const parentContainer = this.settingsButton.parentElement;
4515
+ if (!parentContainer) return;
4516
+ const parentRect = parentContainer.getBoundingClientRect();
4517
+ const buttonCenterX = buttonRect.left + buttonRect.width / 2 - parentRect.left;
4518
+ const buttonBottom = buttonRect.bottom - parentRect.top;
4519
+ const buttonTop = buttonRect.top - parentRect.top;
4520
+ const spaceAbove = buttonRect.top;
4521
+ const spaceBelow = viewportHeight - buttonRect.bottom;
4522
+ let menuTop = buttonBottom + 8;
4523
+ let menuBottom = null;
4524
+ if (spaceBelow < menuRect.height + 20 && spaceAbove > spaceBelow) {
4525
+ menuTop = null;
4526
+ const parentHeight = parentRect.bottom - parentRect.top;
4527
+ menuBottom = parentHeight - buttonTop + 8;
4528
+ this.settingsMenu.classList.add("vidply-menu-above");
4529
+ } else {
4530
+ this.settingsMenu.classList.remove("vidply-menu-above");
4531
+ }
4532
+ let menuLeft = buttonCenterX - menuRect.width / 2;
4533
+ let menuRight = "auto";
4534
+ let transformX = "translateX(0)";
4535
+ const menuLeftAbsolute = buttonRect.left + buttonRect.width / 2 - menuRect.width / 2;
4536
+ if (menuLeftAbsolute < 10) {
4537
+ menuLeft = 0;
4538
+ } else if (menuLeftAbsolute + menuRect.width > viewportWidth - 10) {
4539
+ menuLeft = "auto";
4540
+ menuRight = 0;
4541
+ } else {
4542
+ menuLeft = buttonCenterX;
4543
+ transformX = "translateX(-50%)";
4544
+ }
4545
+ if (menuTop !== null) {
4546
+ this.settingsMenu.style.top = `${menuTop}px`;
4547
+ this.settingsMenu.style.bottom = "auto";
4548
+ } else if (menuBottom !== null) {
4549
+ this.settingsMenu.style.top = "auto";
4550
+ this.settingsMenu.style.bottom = `${menuBottom}px`;
4551
+ }
4552
+ if (menuLeft !== "auto") {
4553
+ this.settingsMenu.style.left = `${menuLeft}px`;
4554
+ this.settingsMenu.style.right = "auto";
4555
+ } else {
4556
+ this.settingsMenu.style.left = "auto";
4557
+ this.settingsMenu.style.right = `${menuRight}px`;
4558
+ }
4559
+ this.settingsMenu.style.transform = transformX;
4560
+ }
4561
+ /**
4562
+ * Position settings menu with RAF
4563
+ */
4564
+ _positionSettingsMenu() {
4565
+ requestAnimationFrame(() => {
4566
+ setTimeout(() => {
4567
+ this._positionSettingsMenuImmediate();
4568
+ }, 10);
4569
+ });
4570
+ }
4571
+ /**
4572
+ * Toggle keyboard drag mode
4573
+ */
4574
+ toggleKeyboardDragMode() {
4575
+ if (this.draggable) {
4576
+ const wasEnabled = this.draggable.keyboardDragMode;
4577
+ this.draggable.toggleKeyboardDragMode();
4578
+ const isEnabled = this.draggable.keyboardDragMode;
4579
+ if (!wasEnabled && isEnabled) {
4580
+ this._enableMoveMode();
4581
+ }
4582
+ this._updateDragOptionState();
4583
+ }
4584
+ }
4585
+ /**
4586
+ * Enable move mode visual feedback
4587
+ */
4588
+ _enableMoveMode() {
4589
+ this.wrapper.classList.add(`${this.player.options.classPrefix}-sign-move-mode`);
4590
+ this._updateResizeOptionState();
4591
+ setTimeout(() => {
4592
+ this.wrapper.classList.remove(`${this.player.options.classPrefix}-sign-move-mode`);
4593
+ }, 2e3);
4594
+ }
4595
+ /**
4596
+ * Toggle resize mode
4597
+ */
4598
+ toggleResizeMode({ focus = true } = {}) {
4599
+ if (!this.draggable) return false;
4600
+ if (this.draggable.pointerResizeMode) {
4601
+ this.draggable.disablePointerResizeMode({ focus });
4602
+ this._updateResizeOptionState();
4603
+ return false;
4604
+ }
4605
+ this.draggable.enablePointerResizeMode({ focus });
4606
+ this._updateResizeOptionState();
4607
+ return true;
4608
+ }
4609
+ /**
4610
+ * Update drag option state
4611
+ */
4612
+ _updateDragOptionState() {
4613
+ if (!this.dragOptionButton) return;
4614
+ const isEnabled = !!this.draggable?.keyboardDragMode;
4615
+ const text = isEnabled ? i18n.t("player.disableSignDragMode") : i18n.t("player.enableSignDragMode");
4616
+ const ariaLabel = isEnabled ? i18n.t("player.disableSignDragModeAria") : i18n.t("player.enableSignDragModeAria");
4617
+ this.dragOptionButton.setAttribute("aria-checked", isEnabled ? "true" : "false");
4618
+ this.dragOptionButton.setAttribute("aria-label", ariaLabel);
4619
+ if (this.dragOptionText) {
4620
+ this.dragOptionText.textContent = text;
4621
+ }
4622
+ }
4623
+ /**
4624
+ * Update resize option state
4625
+ */
4626
+ _updateResizeOptionState() {
4627
+ if (!this.resizeOptionButton) return;
4628
+ const isEnabled = !!this.draggable?.pointerResizeMode;
4629
+ const text = isEnabled ? i18n.t("player.disableSignResizeMode") : i18n.t("player.enableSignResizeMode");
4630
+ const ariaLabel = isEnabled ? i18n.t("player.disableSignResizeModeAria") : i18n.t("player.enableSignResizeModeAria");
4631
+ this.resizeOptionButton.setAttribute("aria-checked", isEnabled ? "true" : "false");
4632
+ this.resizeOptionButton.setAttribute("aria-label", ariaLabel);
4633
+ if (this.resizeOptionText) {
4634
+ this.resizeOptionText.textContent = text;
4635
+ }
4636
+ }
4637
+ /**
4638
+ * Save preferences
4639
+ */
4640
+ savePreferences() {
4641
+ if (!this.wrapper) return;
4642
+ this.player.storage.saveSignLanguagePreferences({
4643
+ size: { width: this.wrapper.style.width }
4644
+ });
4645
+ }
4646
+ /**
4647
+ * Update sources (called when playlist changes)
4648
+ */
4649
+ updateSources(signLanguageSrc, signLanguageSources) {
4650
+ this.src = signLanguageSrc || null;
4651
+ this.sources = signLanguageSources || {};
4652
+ this.currentLanguage = null;
4653
+ }
4654
+ /**
4655
+ * Cleanup
4656
+ */
4657
+ cleanup() {
4658
+ if (this.settingsMenuVisible) {
4659
+ this.hideSettingsMenu({ focusButton: false });
4660
+ }
4661
+ if (this.documentClickHandler && this.documentClickHandlerAdded) {
4662
+ document.removeEventListener("mousedown", this.documentClickHandler, true);
4663
+ this.documentClickHandlerAdded = false;
4664
+ this.documentClickHandler = null;
4665
+ }
4666
+ if (this.settingsHandlers && this.settingsButton) {
4667
+ this.settingsButton.removeEventListener("click", this.settingsHandlers.click);
4668
+ this.settingsButton.removeEventListener("keydown", this.settingsHandlers.keydown);
4669
+ }
4670
+ this.settingsHandlers = null;
4671
+ if (this.handlers) {
4672
+ this.player.off("play", this.handlers.play);
4673
+ this.player.off("pause", this.handlers.pause);
4674
+ this.player.off("timeupdate", this.handlers.timeupdate);
4675
+ this.player.off("ratechange", this.handlers.ratechange);
4676
+ if (this.handlers.captionChange) {
4677
+ this.player.off("captionsenabled", this.handlers.captionChange);
4678
+ }
4679
+ this.handlers = null;
4680
+ }
4681
+ if (this.wrapper && this.customKeyHandler) {
4682
+ this.wrapper.removeEventListener("keydown", this.customKeyHandler);
4683
+ }
4684
+ if (this.draggable) {
4685
+ if (this.draggable.pointerResizeMode) {
4686
+ this.draggable.disablePointerResizeMode();
4687
+ }
4688
+ this.draggable.destroy();
4689
+ this.draggable = null;
4690
+ }
4691
+ this.interactionHandlers = null;
4692
+ if (this.wrapper?.parentNode) {
4693
+ if (this.video) {
4694
+ this.video.pause();
4695
+ this.video.src = "";
4696
+ }
4697
+ this.wrapper.parentNode.removeChild(this.wrapper);
4698
+ }
4699
+ this.wrapper = null;
4700
+ this.video = null;
4701
+ this.settingsButton = null;
4702
+ this.settingsMenu = null;
4703
+ }
4704
+ /**
4705
+ * Destroy
4706
+ */
4707
+ destroy() {
4708
+ this.cleanup();
4709
+ this.enabled = false;
4710
+ }
4711
+ };
4712
+
4713
+ // src/core/Player.js
4714
+ var playerInstanceCounter = 0;
4715
+ var Player = class _Player extends EventEmitter {
4716
+ constructor(element, options = {}) {
4717
+ super();
4718
+ this.element = typeof element === "string" ? document.querySelector(element) : element;
4719
+ if (!this.element) {
4720
+ throw new Error("VidPly: Element not found");
4721
+ }
4722
+ playerInstanceCounter++;
4723
+ this.instanceId = playerInstanceCounter;
4724
+ if (this.element.tagName !== "VIDEO" && this.element.tagName !== "AUDIO") {
4725
+ const mediaType = options.mediaType || "video";
4726
+ const mediaElement = document.createElement(mediaType);
4727
+ Array.from(this.element.attributes).forEach((attr) => {
4728
+ if (attr.name !== "id" && attr.name !== "class" && !attr.name.startsWith("data-")) {
4729
+ mediaElement.setAttribute(attr.name, attr.value);
4730
+ }
4731
+ });
4732
+ const tracks = this.element.querySelectorAll("track");
4733
+ tracks.forEach((track) => {
4734
+ mediaElement.appendChild(track.cloneNode(true));
4735
+ });
4736
+ this.element.innerHTML = "";
4737
+ this.element.appendChild(mediaElement);
4738
+ this.element = mediaElement;
4739
+ }
4740
+ this._originalElement = this.element;
4741
+ this.options = {
4742
+ // Display
4743
+ width: null,
4744
+ height: null,
4745
+ poster: null,
4746
+ responsive: true,
3096
4747
  fillContainer: false,
3097
4748
  // Playback
3098
4749
  autoplay: false,
@@ -3246,6 +4897,58 @@ var Player = class _Player extends EventEmitter {
3246
4897
  this.settingsDialog = null;
3247
4898
  this.metadataCueChangeHandler = null;
3248
4899
  this.metadataAlertHandlers = /* @__PURE__ */ new Map();
4900
+ this.audioDescriptionManager = new AudioDescriptionManager(this);
4901
+ this.signLanguageManager = new SignLanguageManager(this);
4902
+ Object.defineProperties(this, {
4903
+ signLanguageWrapper: {
4904
+ get: () => this.signLanguageManager.wrapper,
4905
+ set: (v) => {
4906
+ this.signLanguageManager.wrapper = v;
4907
+ }
4908
+ },
4909
+ signLanguageVideo: {
4910
+ get: () => this.signLanguageManager.video,
4911
+ set: (v) => {
4912
+ this.signLanguageManager.video = v;
4913
+ }
4914
+ },
4915
+ signLanguageHeader: {
4916
+ get: () => this.signLanguageManager.header,
4917
+ set: (v) => {
4918
+ this.signLanguageManager.header = v;
4919
+ }
4920
+ },
4921
+ signLanguageSettingsButton: {
4922
+ get: () => this.signLanguageManager.settingsButton,
4923
+ set: (v) => {
4924
+ this.signLanguageManager.settingsButton = v;
4925
+ }
4926
+ },
4927
+ signLanguageSettingsMenu: {
4928
+ get: () => this.signLanguageManager.settingsMenu,
4929
+ set: (v) => {
4930
+ this.signLanguageManager.settingsMenu = v;
4931
+ }
4932
+ },
4933
+ signLanguageSettingsMenuVisible: {
4934
+ get: () => this.signLanguageManager.settingsMenuVisible,
4935
+ set: (v) => {
4936
+ this.signLanguageManager.settingsMenuVisible = v;
4937
+ }
4938
+ },
4939
+ signLanguageDraggable: {
4940
+ get: () => this.signLanguageManager.draggable,
4941
+ set: (v) => {
4942
+ this.signLanguageManager.draggable = v;
4943
+ }
4944
+ },
4945
+ currentSignLanguage: {
4946
+ get: () => this.signLanguageManager.currentLanguage,
4947
+ set: (v) => {
4948
+ this.signLanguageManager.currentLanguage = v;
4949
+ }
4950
+ }
4951
+ });
3249
4952
  this.init();
3250
4953
  }
3251
4954
  async init() {
@@ -3339,7 +5042,7 @@ var Player = class _Player extends EventEmitter {
3339
5042
  if (!this.options.transcript && !this.options.transcriptButton) {
3340
5043
  return null;
3341
5044
  }
3342
- const module = await import("./vidply.TranscriptManager-QSF2PWUN.js");
5045
+ const module = await import("./vidply.TranscriptManager-T677KF4N.js");
3343
5046
  const Manager = module.TranscriptManager || module.default;
3344
5047
  if (!Manager) {
3345
5048
  return null;
@@ -3506,53 +5209,7 @@ var Player = class _Player extends EventEmitter {
3506
5209
  }
3507
5210
  this.currentSource = src;
3508
5211
  this._pendingSource = null;
3509
- const sourceElements = this.sourceElements;
3510
- for (const sourceEl of sourceElements) {
3511
- const descSrc = sourceEl.getAttribute("data-desc-src");
3512
- const origSrc = sourceEl.getAttribute("data-orig-src");
3513
- if (descSrc || origSrc) {
3514
- if (!this.audioDescriptionSourceElement) {
3515
- this.audioDescriptionSourceElement = sourceEl;
3516
- }
3517
- if (origSrc) {
3518
- if (!this.originalAudioDescriptionSource) {
3519
- this.originalAudioDescriptionSource = origSrc;
3520
- }
3521
- if (!this.originalSrc) {
3522
- this.originalSrc = origSrc;
3523
- }
3524
- } else {
3525
- const currentSrcAttr = sourceEl.getAttribute("src");
3526
- if (!this.originalAudioDescriptionSource && currentSrcAttr) {
3527
- this.originalAudioDescriptionSource = currentSrcAttr;
3528
- }
3529
- if (!this.originalSrc && currentSrcAttr) {
3530
- this.originalSrc = currentSrcAttr;
3531
- }
3532
- }
3533
- if (descSrc && !this.audioDescriptionSrc) {
3534
- this.audioDescriptionSrc = descSrc;
3535
- }
3536
- }
3537
- }
3538
- const trackElements = this.trackElements;
3539
- trackElements.forEach((trackEl) => {
3540
- const trackKind = trackEl.getAttribute("kind");
3541
- const trackDescSrc = trackEl.getAttribute("data-desc-src");
3542
- if (trackKind === "captions" || trackKind === "subtitles" || trackKind === "chapters") {
3543
- if (trackDescSrc) {
3544
- this.audioDescriptionCaptionTracks.push({
3545
- trackElement: trackEl,
3546
- originalSrc: trackEl.getAttribute("src"),
3547
- describedSrc: trackDescSrc,
3548
- originalTrackSrc: trackEl.getAttribute("data-orig-src") || trackEl.getAttribute("src"),
3549
- explicit: true
3550
- // Explicitly defined, so we should validate it
3551
- });
3552
- this.log(`Found explicit described ${trackKind} track: ${trackEl.getAttribute("src")} -> ${trackDescSrc}`);
3553
- }
3554
- }
3555
- });
5212
+ this.audioDescriptionManager.initFromSourceElements(this.sourceElements, this.trackElements);
3556
5213
  if (!this.originalSrc) {
3557
5214
  this.originalSrc = src;
3558
5215
  }
@@ -3564,7 +5221,7 @@ var Player = class _Player extends EventEmitter {
3564
5221
  const module = await import("./vidply.VimeoRenderer-VPH4RNES.js");
3565
5222
  rendererClass = module.VimeoRenderer || module.default;
3566
5223
  } else if (src.includes(".m3u8")) {
3567
- const module = await import("./vidply.HLSRenderer-ENLZE4QS.js");
5224
+ const module = await import("./vidply.HLSRenderer-UMPUDSYL.js");
3568
5225
  rendererClass = module.HLSRenderer || module.default;
3569
5226
  } else if (src.includes("soundcloud.com") || src.includes("api.soundcloud.com")) {
3570
5227
  const module = await import("./vidply.SoundCloudRenderer-CD7VJKNS.js");
@@ -3669,6 +5326,64 @@ var Player = class _Player extends EventEmitter {
3669
5326
  return posterPath;
3670
5327
  }
3671
5328
  }
5329
+ /**
5330
+ * Generate a poster image from video frame at specified time
5331
+ * @param {number} time - Time in seconds (default: 10)
5332
+ * @returns {Promise<string|null>} Data URL of the poster image or null if failed
5333
+ */
5334
+ async generatePosterFromVideo(time = 10) {
5335
+ if (this.element.tagName !== "VIDEO") {
5336
+ return null;
5337
+ }
5338
+ const renderer = this.renderer;
5339
+ if (!renderer || !renderer.media || renderer.media.tagName !== "VIDEO") {
5340
+ return null;
5341
+ }
5342
+ const video = renderer.media;
5343
+ if (!video.duration || video.duration < time) {
5344
+ time = Math.min(time, Math.max(1, video.duration * 0.1));
5345
+ }
5346
+ let videoToUse = video;
5347
+ if (this.controlBar && this.controlBar.previewVideo && this.controlBar.previewSupported) {
5348
+ videoToUse = this.controlBar.previewVideo;
5349
+ }
5350
+ const restoreState = videoToUse === video;
5351
+ return await captureVideoFrame(videoToUse, time, {
5352
+ restoreState,
5353
+ quality: 0.9
5354
+ });
5355
+ }
5356
+ /**
5357
+ * Auto-generate poster from video if none is provided
5358
+ */
5359
+ async autoGeneratePoster() {
5360
+ const hasPoster = this.element.getAttribute("poster") || this.element.poster || this.options.poster;
5361
+ if (hasPoster) {
5362
+ return;
5363
+ }
5364
+ if (this.element.tagName !== "VIDEO") {
5365
+ return;
5366
+ }
5367
+ if (!this.state.duration || this.state.duration === 0) {
5368
+ await new Promise((resolve) => {
5369
+ const onLoadedMetadata = () => {
5370
+ this.element.removeEventListener("loadedmetadata", onLoadedMetadata);
5371
+ resolve();
5372
+ };
5373
+ if (this.element.readyState >= 1) {
5374
+ resolve();
5375
+ } else {
5376
+ this.element.addEventListener("loadedmetadata", onLoadedMetadata);
5377
+ }
5378
+ });
5379
+ }
5380
+ const posterDataURL = await this.generatePosterFromVideo(10);
5381
+ if (posterDataURL) {
5382
+ this.element.poster = posterDataURL;
5383
+ this.log("Auto-generated poster from video frame at 10 seconds", "info");
5384
+ this.showPosterOverlay();
5385
+ }
5386
+ }
3672
5387
  showPosterOverlay() {
3673
5388
  if (!this.videoWrapper || this.element.tagName !== "VIDEO") {
3674
5389
  return;
@@ -3677,7 +5392,7 @@ var Player = class _Player extends EventEmitter {
3677
5392
  if (!poster) {
3678
5393
  return;
3679
5394
  }
3680
- const resolvedPoster = this.resolvePosterPath(poster);
5395
+ const resolvedPoster = poster.startsWith("data:") ? poster : this.resolvePosterPath(poster);
3681
5396
  this.videoWrapper.style.setProperty("--vidply-poster-image", `url("${resolvedPoster}")`);
3682
5397
  this.videoWrapper.classList.add("vidply-forced-poster");
3683
5398
  if (this._isAudioContent && this.container) {
@@ -3796,6 +5511,9 @@ var Player = class _Player extends EventEmitter {
3796
5511
  if (trackConfig.default) {
3797
5512
  track.default = true;
3798
5513
  }
5514
+ if (trackConfig.describedSrc) {
5515
+ track.setAttribute("data-desc-src", trackConfig.describedSrc);
5516
+ }
3799
5517
  const firstChild = this.element.firstChild;
3800
5518
  if (firstChild && firstChild.nodeType === Node.ELEMENT_NODE && firstChild.tagName !== "TRACK") {
3801
5519
  this.element.insertBefore(track, firstChild);
@@ -3810,6 +5528,13 @@ var Player = class _Player extends EventEmitter {
3810
5528
  this.audioDescriptionSrc = config.audioDescriptionSrc || null;
3811
5529
  this.signLanguageSrc = config.signLanguageSrc || null;
3812
5530
  this.originalSrc = config.src;
5531
+ if (this.audioDescriptionManager) {
5532
+ this.audioDescriptionManager.updateSources(config.audioDescriptionSrc);
5533
+ this.audioDescriptionManager.reinitialize();
5534
+ }
5535
+ if (this.signLanguageManager) {
5536
+ this.signLanguageManager.updateSources(config.signLanguageSrc, config.signLanguageSources);
5537
+ }
3813
5538
  if (wasAudioDescriptionEnabled) {
3814
5539
  this.disableAudioDescription();
3815
5540
  }
@@ -4217,8 +5942,12 @@ var Player = class _Player extends EventEmitter {
4217
5942
  }
4218
5943
  return null;
4219
5944
  }
4220
- // Audio Description
5945
+ // Audio Description (delegated to AudioDescriptionManager)
4221
5946
  async enableAudioDescription() {
5947
+ return this.audioDescriptionManager.enable();
5948
+ }
5949
+ // Legacy method body preserved for reference - can be removed after testing
5950
+ async _legacyEnableAudioDescription() {
4222
5951
  const hasSourceElementsWithDesc = this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
4223
5952
  const hasTracksWithDesc = this.audioDescriptionCaptionTracks.length > 0;
4224
5953
  if (!this.audioDescriptionSrc && !hasSourceElementsWithDesc && !hasTracksWithDesc) {
@@ -4951,6 +6680,10 @@ var Player = class _Player extends EventEmitter {
4951
6680
  this.emit("audiodescriptionenabled");
4952
6681
  }
4953
6682
  async disableAudioDescription() {
6683
+ return this.audioDescriptionManager.disable();
6684
+ }
6685
+ // Legacy method body preserved for reference - can be removed after testing
6686
+ async _legacyDisableAudioDescription() {
4954
6687
  if (!this.originalSrc) {
4955
6688
  return;
4956
6689
  }
@@ -5225,64 +6958,14 @@ var Player = class _Player extends EventEmitter {
5225
6958
  this.emit("audiodescriptiondisabled");
5226
6959
  }
5227
6960
  async toggleAudioDescription() {
5228
- const descriptionTrack = this.findTextTrack("descriptions");
5229
- const hasAudioDescriptionSrc = this.audioDescriptionSrc || this.sourceElements.some((el) => el.getAttribute("data-desc-src"));
5230
- if (descriptionTrack && hasAudioDescriptionSrc) {
5231
- if (this.state.audioDescriptionEnabled) {
5232
- this._audioDescriptionDesiredState = false;
5233
- descriptionTrack.mode = "hidden";
5234
- await this.disableAudioDescription();
5235
- } else {
5236
- this._audioDescriptionDesiredState = true;
5237
- await this.enableAudioDescription();
5238
- const enableDescriptionTrack = () => {
5239
- this.invalidateTrackCache();
5240
- const descTrack = this.findTextTrack("descriptions");
5241
- if (descTrack) {
5242
- if (descTrack.mode === "disabled") {
5243
- descTrack.mode = "hidden";
5244
- this.setManagedTimeout(() => {
5245
- descTrack.mode = "showing";
5246
- }, 50);
5247
- } else {
5248
- descTrack.mode = "showing";
5249
- }
5250
- } else if (this.element.readyState < 2) {
5251
- this.setManagedTimeout(enableDescriptionTrack, 100);
5252
- }
5253
- };
5254
- if (this.element.readyState >= 1) {
5255
- this.setManagedTimeout(enableDescriptionTrack, 200);
5256
- } else {
5257
- this.element.addEventListener("loadedmetadata", () => {
5258
- this.setManagedTimeout(enableDescriptionTrack, 200);
5259
- }, { once: true });
5260
- }
5261
- }
5262
- } else if (descriptionTrack) {
5263
- if (descriptionTrack.mode === "showing") {
5264
- this._audioDescriptionDesiredState = false;
5265
- descriptionTrack.mode = "hidden";
5266
- this.state.audioDescriptionEnabled = false;
5267
- this.emit("audiodescriptiondisabled");
5268
- } else {
5269
- this._audioDescriptionDesiredState = true;
5270
- descriptionTrack.mode = "showing";
5271
- this.state.audioDescriptionEnabled = true;
5272
- this.emit("audiodescriptionenabled");
5273
- }
5274
- } else if (hasAudioDescriptionSrc) {
5275
- if (this.state.audioDescriptionEnabled) {
5276
- this._audioDescriptionDesiredState = false;
5277
- await this.disableAudioDescription();
5278
- } else {
5279
- this._audioDescriptionDesiredState = true;
5280
- await this.enableAudioDescription();
5281
- }
5282
- }
6961
+ return this.audioDescriptionManager.toggle();
5283
6962
  }
5284
- // Sign Language
6963
+ // Sign Language (delegated to SignLanguageManager)
5285
6964
  enableSignLanguage() {
6965
+ return this.signLanguageManager.enable();
6966
+ }
6967
+ // Legacy method body preserved for reference - can be removed after testing
6968
+ _legacyEnableSignLanguage() {
5286
6969
  const hasMultipleSources = Object.keys(this.signLanguageSources).length > 0;
5287
6970
  const hasSingleSource = !!this.signLanguageSrc;
5288
6971
  if (!hasMultipleSources && !hasSingleSource) {
@@ -5533,23 +7216,16 @@ var Player = class _Player extends EventEmitter {
5533
7216
  }, 150);
5534
7217
  }
5535
7218
  disableSignLanguage() {
5536
- if (this.signLanguageSettingsMenuVisible) {
5537
- this.hideSignLanguageSettingsMenu({ focusButton: false });
5538
- }
5539
- if (this.signLanguageWrapper) {
5540
- this.signLanguageWrapper.style.display = "none";
5541
- }
5542
- this.state.signLanguageEnabled = false;
5543
- this.emit("signlanguagedisabled");
7219
+ return this.signLanguageManager.disable();
5544
7220
  }
5545
7221
  toggleSignLanguage() {
5546
- if (this.state.signLanguageEnabled) {
5547
- this.disableSignLanguage();
5548
- } else {
5549
- this.enableSignLanguage();
5550
- }
7222
+ return this.signLanguageManager.toggle();
5551
7223
  }
5552
7224
  setupSignLanguageInteraction() {
7225
+ return this.signLanguageManager._setupInteraction();
7226
+ }
7227
+ // Legacy method preserved for reference
7228
+ _legacySetupSignLanguageInteraction() {
5553
7229
  if (!this.signLanguageWrapper) return;
5554
7230
  const isMobile2 = window.innerWidth < 768;
5555
7231
  const isFullscreen = this.state.fullscreen;
@@ -5687,6 +7363,10 @@ var Player = class _Player extends EventEmitter {
5687
7363
  return langNames[langCode] || langCode.toUpperCase();
5688
7364
  }
5689
7365
  switchSignLanguage(langCode) {
7366
+ return this.signLanguageManager.switchLanguage(langCode);
7367
+ }
7368
+ // Legacy method preserved for reference
7369
+ _legacySwitchSignLanguage(langCode) {
5690
7370
  if (!this.signLanguageSources[langCode] || !this.signLanguageVideo) {
5691
7371
  return;
5692
7372
  }
@@ -5702,6 +7382,10 @@ var Player = class _Player extends EventEmitter {
5702
7382
  this.emit("signlanguagelanguagechanged", langCode);
5703
7383
  }
5704
7384
  showSignLanguageSettingsMenu() {
7385
+ return this.signLanguageManager.showSettingsMenu();
7386
+ }
7387
+ // Legacy method preserved for reference
7388
+ _legacyShowSignLanguageSettingsMenu() {
5705
7389
  this.signLanguageSettingsMenuJustOpened = true;
5706
7390
  setTimeout(() => {
5707
7391
  this.signLanguageSettingsMenuJustOpened = false;
@@ -5845,25 +7529,7 @@ var Player = class _Player extends EventEmitter {
5845
7529
  focusFirstMenuItem(this.signLanguageSettingsMenu, `.${this.options.classPrefix}-sign-language-settings-item`);
5846
7530
  }
5847
7531
  hideSignLanguageSettingsMenu({ focusButton = true } = {}) {
5848
- if (this.signLanguageSettingsMenu) {
5849
- this.signLanguageSettingsMenu.style.display = "none";
5850
- this.signLanguageSettingsMenuVisible = false;
5851
- this.signLanguageSettingsMenuJustOpened = false;
5852
- if (this.signLanguageSettingsMenuKeyHandler) {
5853
- this.signLanguageSettingsMenu.removeEventListener("keydown", this.signLanguageSettingsMenuKeyHandler);
5854
- this.signLanguageSettingsMenuKeyHandler = null;
5855
- }
5856
- const menuItems = Array.from(this.signLanguageSettingsMenu.querySelectorAll(`.${this.options.classPrefix}-sign-language-settings-item`));
5857
- menuItems.forEach((item) => {
5858
- item.setAttribute("tabindex", "-1");
5859
- });
5860
- if (this.signLanguageSettingsButton) {
5861
- this.signLanguageSettingsButton.setAttribute("aria-expanded", "false");
5862
- if (focusButton) {
5863
- this.signLanguageSettingsButton.focus({ preventScroll: true });
5864
- }
5865
- }
5866
- }
7532
+ return this.signLanguageManager.hideSettingsMenu({ focusButton });
5867
7533
  }
5868
7534
  positionSignLanguageSettingsMenuImmediate() {
5869
7535
  if (!this.signLanguageSettingsMenu || !this.signLanguageSettingsButton) return;
@@ -5967,6 +7633,13 @@ var Player = class _Player extends EventEmitter {
5967
7633
  }
5968
7634
  }
5969
7635
  constrainSignLanguagePosition() {
7636
+ return this.signLanguageManager.constrainPosition();
7637
+ }
7638
+ saveSignLanguagePreferences() {
7639
+ return this.signLanguageManager.savePreferences();
7640
+ }
7641
+ // Legacy methods preserved for reference - can be removed after testing
7642
+ _legacyConstrainSignLanguagePosition() {
5970
7643
  if (!this.signLanguageWrapper || !this.videoWrapper) return;
5971
7644
  if (this.signLanguageDraggable && this.signLanguageDraggable.manuallyPositioned) {
5972
7645
  return;
@@ -6016,7 +7689,7 @@ var Player = class _Player extends EventEmitter {
6016
7689
  this.signLanguageWrapper.style.bottom = "auto";
6017
7690
  this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter((c) => c.startsWith("vidply-sign-position-")));
6018
7691
  }
6019
- saveSignLanguagePreferences() {
7692
+ _legacySaveSignLanguagePreferences() {
6020
7693
  if (!this.signLanguageWrapper) return;
6021
7694
  this.storage.saveSignLanguagePreferences({
6022
7695
  size: {
@@ -6026,58 +7699,7 @@ var Player = class _Player extends EventEmitter {
6026
7699
  });
6027
7700
  }
6028
7701
  cleanupSignLanguage() {
6029
- if (this.signLanguageSettingsMenuVisible) {
6030
- this.hideSignLanguageSettingsMenu({ focusButton: false });
6031
- }
6032
- if (this.signLanguageDocumentClickHandler && this.signLanguageDocumentClickHandlerAdded) {
6033
- document.removeEventListener("mousedown", this.signLanguageDocumentClickHandler, true);
6034
- this.signLanguageDocumentClickHandlerAdded = false;
6035
- this.signLanguageDocumentClickHandler = null;
6036
- }
6037
- if (this.signLanguageSettingsHandlers) {
6038
- if (this.signLanguageSettingsButton) {
6039
- this.signLanguageSettingsButton.removeEventListener("click", this.signLanguageSettingsHandlers.settingsClick);
6040
- this.signLanguageSettingsButton.removeEventListener("keydown", this.signLanguageSettingsHandlers.settingsKeydown);
6041
- }
6042
- this.signLanguageSettingsHandlers = null;
6043
- }
6044
- if (this.signLanguageHandlers) {
6045
- this.off("play", this.signLanguageHandlers.play);
6046
- this.off("pause", this.signLanguageHandlers.pause);
6047
- this.off("timeupdate", this.signLanguageHandlers.timeupdate);
6048
- this.off("ratechange", this.signLanguageHandlers.ratechange);
6049
- if (this.signLanguageHandlers.captionChange) {
6050
- this.off("captionsenabled", this.signLanguageHandlers.captionChange);
6051
- }
6052
- this.signLanguageHandlers = null;
6053
- }
6054
- if (this.signLanguageInteractionHandlers) {
6055
- if (this.signLanguageHeader && this.signLanguageInteractionHandlers.headerKeyHandler) {
6056
- this.signLanguageHeader.removeEventListener("keydown", this.signLanguageInteractionHandlers.headerKeyHandler);
6057
- }
6058
- if (this.signLanguageWrapper && this.signLanguageInteractionHandlers.customKeyHandler) {
6059
- this.signLanguageWrapper.removeEventListener("keydown", this.signLanguageInteractionHandlers.customKeyHandler);
6060
- }
6061
- }
6062
- if (this.signLanguageDraggable) {
6063
- if (this.signLanguageDraggable.pointerResizeMode) {
6064
- this.signLanguageDraggable.disablePointerResizeMode();
6065
- }
6066
- this.signLanguageDraggable.destroy();
6067
- this.signLanguageDraggable = null;
6068
- }
6069
- this.signLanguageInteractionHandlers = null;
6070
- if (this.signLanguageWrapper && this.signLanguageWrapper.parentNode) {
6071
- if (this.signLanguageVideo) {
6072
- this.signLanguageVideo.pause();
6073
- this.signLanguageVideo.src = "";
6074
- }
6075
- this.signLanguageWrapper.parentNode.removeChild(this.signLanguageWrapper);
6076
- }
6077
- this.signLanguageWrapper = null;
6078
- this.signLanguageVideo = null;
6079
- this.signLanguageSettingsButton = null;
6080
- this.signLanguageSettingsMenu = null;
7702
+ return this.signLanguageManager.cleanup();
6081
7703
  }
6082
7704
  // Settings
6083
7705
  // Settings dialog removed - using individual control buttons instead