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
@@ -16,6 +16,9 @@ import {DraggableResizable} from '../utils/DraggableResizable.js';
16
16
  import {createMenuItem, attachMenuKeyboardNavigation, focusFirstMenuItem} from '../utils/MenuUtils.js';
17
17
  import {createLabeledSelect, preventDragOnElement} from '../utils/FormUtils.js';
18
18
  import {debounce, isMobile, rafWithTimeout} from '../utils/PerformanceUtils.js';
19
+ import {AudioDescriptionManager} from './AudioDescriptionManager.js';
20
+ import {SignLanguageManager} from './SignLanguageManager.js';
21
+ import {captureVideoFrame} from '../utils/VideoFrameCapture.js';
19
22
 
20
23
  // Static counter for unique player instances
21
24
  let playerInstanceCounter = 0;
@@ -252,6 +255,47 @@ export class Player extends EventEmitter {
252
255
  this.metadataCueChangeHandler = null;
253
256
  this.metadataAlertHandlers = new Map();
254
257
 
258
+ // Feature managers (modular refactoring)
259
+ this.audioDescriptionManager = new AudioDescriptionManager(this);
260
+ this.signLanguageManager = new SignLanguageManager(this);
261
+
262
+ // Backward-compatible property aliases for SignLanguageManager
263
+ // These allow existing code to reference this.signLanguageWrapper, etc.
264
+ Object.defineProperties(this, {
265
+ signLanguageWrapper: {
266
+ get: () => this.signLanguageManager.wrapper,
267
+ set: (v) => { this.signLanguageManager.wrapper = v; }
268
+ },
269
+ signLanguageVideo: {
270
+ get: () => this.signLanguageManager.video,
271
+ set: (v) => { this.signLanguageManager.video = v; }
272
+ },
273
+ signLanguageHeader: {
274
+ get: () => this.signLanguageManager.header,
275
+ set: (v) => { this.signLanguageManager.header = v; }
276
+ },
277
+ signLanguageSettingsButton: {
278
+ get: () => this.signLanguageManager.settingsButton,
279
+ set: (v) => { this.signLanguageManager.settingsButton = v; }
280
+ },
281
+ signLanguageSettingsMenu: {
282
+ get: () => this.signLanguageManager.settingsMenu,
283
+ set: (v) => { this.signLanguageManager.settingsMenu = v; }
284
+ },
285
+ signLanguageSettingsMenuVisible: {
286
+ get: () => this.signLanguageManager.settingsMenuVisible,
287
+ set: (v) => { this.signLanguageManager.settingsMenuVisible = v; }
288
+ },
289
+ signLanguageDraggable: {
290
+ get: () => this.signLanguageManager.draggable,
291
+ set: (v) => { this.signLanguageManager.draggable = v; }
292
+ },
293
+ currentSignLanguage: {
294
+ get: () => this.signLanguageManager.currentLanguage,
295
+ set: (v) => { this.signLanguageManager.currentLanguage = v; }
296
+ }
297
+ });
298
+
255
299
  // Initialize
256
300
  this.init();
257
301
  }
@@ -651,77 +695,10 @@ export class Player extends EventEmitter {
651
695
  // Clear pending source after using it
652
696
  this._pendingSource = null;
653
697
 
654
- // Check for source elements with audio description attributes
655
- const sourceElements = this.sourceElements;
656
- for (const sourceEl of sourceElements) {
657
- const descSrc = sourceEl.getAttribute('data-desc-src');
658
- const origSrc = sourceEl.getAttribute('data-orig-src');
659
-
660
- if (descSrc || origSrc) {
661
- // Found a source element with audio description attributes
662
- // Store the first one as reference, but we'll search all of them when toggling
663
- if (!this.audioDescriptionSourceElement) {
664
- this.audioDescriptionSourceElement = sourceEl;
665
- }
666
-
667
- if (origSrc) {
668
- // Store the original src from the attribute for this source
669
- if (!this.originalAudioDescriptionSource) {
670
- this.originalAudioDescriptionSource = origSrc;
671
- }
672
- // Store the original src from the first source element that has data-orig-src
673
- if (!this.originalSrc) {
674
- this.originalSrc = origSrc;
675
- }
676
- } else {
677
- // If data-orig-src is not set, use the current src attribute
678
- const currentSrcAttr = sourceEl.getAttribute('src');
679
- if (!this.originalAudioDescriptionSource && currentSrcAttr) {
680
- this.originalAudioDescriptionSource = currentSrcAttr;
681
- }
682
- if (!this.originalSrc && currentSrcAttr) {
683
- this.originalSrc = currentSrcAttr;
684
- }
685
- }
686
-
687
- // Store audio description source from data-desc-src (use first one found)
688
- if (descSrc && !this.audioDescriptionSrc) {
689
- this.audioDescriptionSrc = descSrc;
690
- }
691
- // Continue checking all source elements to ensure we capture all audio description sources
692
- }
693
- }
694
-
695
- // Check for caption/subtitle tracks with audio description versions
696
- // Only tracks with explicit data-desc-src attribute are swapped (no auto-detection to avoid 404 errors)
697
- // Description tracks (kind="descriptions") are NOT swapped - they're for transcripts
698
- const trackElements = this.trackElements;
699
- trackElements.forEach(trackEl => {
700
- const trackKind = trackEl.getAttribute('kind');
701
- const trackDescSrc = trackEl.getAttribute('data-desc-src');
702
-
703
- // Only handle caption/subtitle tracks (not description tracks)
704
- // Description tracks stay as-is since they're for transcripts
705
- // Include captions, subtitles, and chapters tracks that can be swapped for audio description
706
- if (trackKind === 'captions' || trackKind === 'subtitles' || trackKind === 'chapters') {
707
- if (trackDescSrc) {
708
- // Found a track with explicit data-desc-src - this is the described version
709
- this.audioDescriptionCaptionTracks.push({
710
- trackElement: trackEl,
711
- originalSrc: trackEl.getAttribute('src'),
712
- describedSrc: trackDescSrc,
713
- originalTrackSrc: trackEl.getAttribute('data-orig-src') || trackEl.getAttribute('src'),
714
- explicit: true // Explicitly defined, so we should validate it
715
- });
716
- this.log(`Found explicit described ${trackKind} track: ${trackEl.getAttribute('src')} -> ${trackDescSrc}`);
717
- }
718
- // Note: Auto-detection disabled to avoid 404 console errors
719
- // If you want described tracks, add data-desc-src attribute to the track element
720
- }
721
- // Description tracks (kind="descriptions") are ignored - they remain unchanged for transcripts
722
- });
723
-
724
- // Store original source for audio description toggling (fallback if not set above)
698
+ // Initialize audio description sources from elements
699
+ this.audioDescriptionManager.initFromSourceElements(this.sourceElements, this.trackElements);
700
+
701
+ // Store original source for audio description toggling (fallback if not set by manager)
725
702
  if (!this.originalSrc) {
726
703
  this.originalSrc = src;
727
704
  }
@@ -858,6 +835,95 @@ export class Player extends EventEmitter {
858
835
  }
859
836
  }
860
837
 
838
+ /**
839
+ * Generate a poster image from video frame at specified time
840
+ * @param {number} time - Time in seconds (default: 10)
841
+ * @returns {Promise<string|null>} Data URL of the poster image or null if failed
842
+ */
843
+ async generatePosterFromVideo(time = 10) {
844
+ // Only for HTML5 video
845
+ if (this.element.tagName !== 'VIDEO') {
846
+ return null;
847
+ }
848
+
849
+ // Check if renderer supports this (HTML5Renderer only)
850
+ const renderer = this.renderer;
851
+ if (!renderer || !renderer.media || renderer.media.tagName !== 'VIDEO') {
852
+ return null;
853
+ }
854
+
855
+ const video = renderer.media;
856
+
857
+ // Check if video has enough duration
858
+ if (!video.duration || video.duration < time) {
859
+ // Use a smaller time if video is shorter
860
+ time = Math.min(time, Math.max(1, video.duration * 0.1));
861
+ }
862
+
863
+ // Try to use preview video from ControlBar if available (avoids interfering with playback)
864
+ let videoToUse = video;
865
+ if (this.controlBar && this.controlBar.previewVideo && this.controlBar.previewSupported) {
866
+ videoToUse = this.controlBar.previewVideo;
867
+ }
868
+
869
+ // Use shared frame capture utility
870
+ // For main video, restore state; for preview video, no need
871
+ const restoreState = videoToUse === video;
872
+ return await captureVideoFrame(videoToUse, time, {
873
+ restoreState,
874
+ quality: 0.9
875
+ });
876
+ }
877
+
878
+ /**
879
+ * Auto-generate poster from video if none is provided
880
+ */
881
+ async autoGeneratePoster() {
882
+ // Check if poster already exists
883
+ const hasPoster =
884
+ this.element.getAttribute('poster') ||
885
+ this.element.poster ||
886
+ this.options.poster;
887
+
888
+ if (hasPoster) {
889
+ return;
890
+ }
891
+
892
+ // Only for HTML5 video
893
+ if (this.element.tagName !== 'VIDEO') {
894
+ return;
895
+ }
896
+
897
+ // Wait for metadata to be loaded
898
+ if (!this.state.duration || this.state.duration === 0) {
899
+ // Wait for loadedmetadata event
900
+ await new Promise((resolve) => {
901
+ const onLoadedMetadata = () => {
902
+ this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
903
+ resolve();
904
+ };
905
+
906
+ if (this.element.readyState >= 1) {
907
+ resolve();
908
+ } else {
909
+ this.element.addEventListener('loadedmetadata', onLoadedMetadata);
910
+ }
911
+ });
912
+ }
913
+
914
+ // Generate poster from second 10
915
+ const posterDataURL = await this.generatePosterFromVideo(10);
916
+
917
+ if (posterDataURL) {
918
+ // Set as poster
919
+ this.element.poster = posterDataURL;
920
+ this.log('Auto-generated poster from video frame at 10 seconds', 'info');
921
+
922
+ // Show the poster overlay
923
+ this.showPosterOverlay();
924
+ }
925
+ }
926
+
861
927
  showPosterOverlay() {
862
928
  if (!this.videoWrapper || this.element.tagName !== 'VIDEO') {
863
929
  return;
@@ -872,8 +938,10 @@ export class Player extends EventEmitter {
872
938
  return;
873
939
  }
874
940
 
875
- // Resolve relative paths to absolute URLs
876
- const resolvedPoster = this.resolvePosterPath(poster);
941
+ // Resolve relative paths to absolute URLs (skip for data URLs)
942
+ const resolvedPoster = poster.startsWith('data:')
943
+ ? poster
944
+ : this.resolvePosterPath(poster);
877
945
  this.videoWrapper.style.setProperty('--vidply-poster-image', `url("${resolvedPoster}")`);
878
946
  this.videoWrapper.classList.add('vidply-forced-poster');
879
947
 
@@ -1040,6 +1108,11 @@ export class Player extends EventEmitter {
1040
1108
  if (trackConfig.default) {
1041
1109
  track.default = true;
1042
1110
  }
1111
+
1112
+ // Support described track sources for audio description track swapping
1113
+ if (trackConfig.describedSrc) {
1114
+ track.setAttribute('data-desc-src', trackConfig.describedSrc);
1115
+ }
1043
1116
 
1044
1117
  // Insert tracks at the beginning (before any flow content) for HTML5 validity
1045
1118
  const firstChild = this.element.firstChild;
@@ -1063,6 +1136,16 @@ export class Player extends EventEmitter {
1063
1136
  // Update original source for toggling
1064
1137
  this.originalSrc = config.src;
1065
1138
 
1139
+ // Update manager sources for playlist changes
1140
+ if (this.audioDescriptionManager) {
1141
+ this.audioDescriptionManager.updateSources(config.audioDescriptionSrc);
1142
+ // Reinitialize to pick up new track elements with data-desc-src attributes
1143
+ this.audioDescriptionManager.reinitialize();
1144
+ }
1145
+ if (this.signLanguageManager) {
1146
+ this.signLanguageManager.updateSources(config.signLanguageSrc, config.signLanguageSources);
1147
+ }
1148
+
1066
1149
  // Hide accessibility features that were enabled (must happen AFTER updating sources)
1067
1150
  if (wasAudioDescriptionEnabled) {
1068
1151
  this.disableAudioDescription();
@@ -1607,8 +1690,13 @@ export class Player extends EventEmitter {
1607
1690
  return null;
1608
1691
  }
1609
1692
 
1610
- // Audio Description
1693
+ // Audio Description (delegated to AudioDescriptionManager)
1611
1694
  async enableAudioDescription() {
1695
+ return this.audioDescriptionManager.enable();
1696
+ }
1697
+
1698
+ // Legacy method body preserved for reference - can be removed after testing
1699
+ async _legacyEnableAudioDescription() {
1612
1700
  // Check if we have source elements with data-desc-src (even if audioDescriptionSrc is not set)
1613
1701
  const hasSourceElementsWithDesc = this.sourceElements.some(el => el.getAttribute('data-desc-src'));
1614
1702
  const hasTracksWithDesc = this.audioDescriptionCaptionTracks.length > 0;
@@ -2664,6 +2752,11 @@ export class Player extends EventEmitter {
2664
2752
  }
2665
2753
 
2666
2754
  async disableAudioDescription() {
2755
+ return this.audioDescriptionManager.disable();
2756
+ }
2757
+
2758
+ // Legacy method body preserved for reference - can be removed after testing
2759
+ async _legacyDisableAudioDescription() {
2667
2760
  if (!this.originalSrc) {
2668
2761
  return;
2669
2762
  }
@@ -3041,82 +3134,16 @@ export class Player extends EventEmitter {
3041
3134
  }
3042
3135
 
3043
3136
  async toggleAudioDescription() {
3044
- // Check if we have description tracks or audio-described video
3045
- const descriptionTrack = this.findTextTrack('descriptions');
3046
-
3047
- // Check if we have audio-described video source (either from options or source elements with data-desc-src)
3048
- const hasAudioDescriptionSrc = this.audioDescriptionSrc ||
3049
- this.sourceElements.some(el => el.getAttribute('data-desc-src'));
3050
-
3051
- if (descriptionTrack && hasAudioDescriptionSrc) {
3052
- // We have both: toggle description track AND swap caption tracks/sources
3053
- if (this.state.audioDescriptionEnabled) {
3054
- this._audioDescriptionDesiredState = false;
3055
- // Disable: toggle description track off and swap captions/sources back
3056
- descriptionTrack.mode = 'hidden';
3057
- await this.disableAudioDescription();
3058
- } else {
3059
- this._audioDescriptionDesiredState = true;
3060
- // Enable: swap caption tracks/sources and toggle description track on
3061
- await this.enableAudioDescription();
3062
- // Wait for tracks to be ready after source swap, then enable description track
3063
- // Use a longer timeout to ensure tracks are loaded after source swap
3064
- const enableDescriptionTrack = () => {
3065
- this.invalidateTrackCache();
3066
- const descTrack = this.findTextTrack('descriptions');
3067
- if (descTrack) {
3068
- // Set to 'hidden' first if it's in 'disabled' mode, then to 'showing'
3069
- if (descTrack.mode === 'disabled') {
3070
- descTrack.mode = 'hidden';
3071
- // Use setTimeout to ensure the browser processes the mode change
3072
- this.setManagedTimeout(() => {
3073
- descTrack.mode = 'showing';
3074
- }, 50);
3075
- } else {
3076
- descTrack.mode = 'showing';
3077
- }
3078
- } else if (this.element.readyState < 2) {
3079
- // Tracks not ready yet, wait a bit more
3080
- this.setManagedTimeout(enableDescriptionTrack, 100);
3081
- }
3082
- };
3083
- // Wait for metadata to load first
3084
- if (this.element.readyState >= 1) {
3085
- this.setManagedTimeout(enableDescriptionTrack, 200);
3086
- } else {
3087
- this.element.addEventListener('loadedmetadata', () => {
3088
- this.setManagedTimeout(enableDescriptionTrack, 200);
3089
- }, { once: true });
3090
- }
3091
- }
3092
- } else if (descriptionTrack) {
3093
- // Only description track, no audio-described video source to swap
3094
- // Toggle description track
3095
- if (descriptionTrack.mode === 'showing') {
3096
- this._audioDescriptionDesiredState = false;
3097
- descriptionTrack.mode = 'hidden';
3098
- this.state.audioDescriptionEnabled = false;
3099
- this.emit('audiodescriptiondisabled');
3100
- } else {
3101
- this._audioDescriptionDesiredState = true;
3102
- descriptionTrack.mode = 'showing';
3103
- this.state.audioDescriptionEnabled = true;
3104
- this.emit('audiodescriptionenabled');
3105
- }
3106
- } else if (hasAudioDescriptionSrc) {
3107
- // Use audio-described video source (no description track)
3108
- if (this.state.audioDescriptionEnabled) {
3109
- this._audioDescriptionDesiredState = false;
3110
- await this.disableAudioDescription();
3111
- } else {
3112
- this._audioDescriptionDesiredState = true;
3113
- await this.enableAudioDescription();
3114
- }
3115
- }
3137
+ return this.audioDescriptionManager.toggle();
3116
3138
  }
3117
3139
 
3118
- // Sign Language
3140
+ // Sign Language (delegated to SignLanguageManager)
3119
3141
  enableSignLanguage() {
3142
+ return this.signLanguageManager.enable();
3143
+ }
3144
+
3145
+ // Legacy method body preserved for reference - can be removed after testing
3146
+ _legacyEnableSignLanguage() {
3120
3147
  // Determine available sign language sources
3121
3148
  const hasMultipleSources = Object.keys(this.signLanguageSources).length > 0;
3122
3149
  const hasSingleSource = !!this.signLanguageSrc;
@@ -3452,27 +3479,19 @@ export class Player extends EventEmitter {
3452
3479
  }
3453
3480
 
3454
3481
  disableSignLanguage() {
3455
- // Hide settings menu if open
3456
- if (this.signLanguageSettingsMenuVisible) {
3457
- this.hideSignLanguageSettingsMenu({ focusButton: false });
3458
- }
3459
-
3460
- if (this.signLanguageWrapper) {
3461
- this.signLanguageWrapper.style.display = 'none';
3462
- }
3463
- this.state.signLanguageEnabled = false;
3464
- this.emit('signlanguagedisabled');
3482
+ return this.signLanguageManager.disable();
3465
3483
  }
3466
3484
 
3467
3485
  toggleSignLanguage() {
3468
- if (this.state.signLanguageEnabled) {
3469
- this.disableSignLanguage();
3470
- } else {
3471
- this.enableSignLanguage();
3472
- }
3486
+ return this.signLanguageManager.toggle();
3473
3487
  }
3474
3488
 
3475
3489
  setupSignLanguageInteraction() {
3490
+ return this.signLanguageManager._setupInteraction();
3491
+ }
3492
+
3493
+ // Legacy method preserved for reference
3494
+ _legacySetupSignLanguageInteraction() {
3476
3495
  if (!this.signLanguageWrapper) return;
3477
3496
 
3478
3497
  // Check if we're on mobile and not in fullscreen
@@ -3655,6 +3674,11 @@ export class Player extends EventEmitter {
3655
3674
  }
3656
3675
 
3657
3676
  switchSignLanguage(langCode) {
3677
+ return this.signLanguageManager.switchLanguage(langCode);
3678
+ }
3679
+
3680
+ // Legacy method preserved for reference
3681
+ _legacySwitchSignLanguage(langCode) {
3658
3682
  if (!this.signLanguageSources[langCode] || !this.signLanguageVideo) {
3659
3683
  return;
3660
3684
  }
@@ -3677,6 +3701,11 @@ export class Player extends EventEmitter {
3677
3701
  }
3678
3702
 
3679
3703
  showSignLanguageSettingsMenu() {
3704
+ return this.signLanguageManager.showSettingsMenu();
3705
+ }
3706
+
3707
+ // Legacy method preserved for reference
3708
+ _legacyShowSignLanguageSettingsMenu() {
3680
3709
  // Set flag to prevent immediate closing
3681
3710
  this.signLanguageSettingsMenuJustOpened = true;
3682
3711
  setTimeout(() => {
@@ -3869,32 +3898,7 @@ export class Player extends EventEmitter {
3869
3898
  }
3870
3899
 
3871
3900
  hideSignLanguageSettingsMenu({ focusButton = true } = {}) {
3872
- if (this.signLanguageSettingsMenu) {
3873
- this.signLanguageSettingsMenu.style.display = 'none';
3874
- this.signLanguageSettingsMenuVisible = false;
3875
- this.signLanguageSettingsMenuJustOpened = false;
3876
-
3877
- // Remove keyboard handler
3878
- if (this.signLanguageSettingsMenuKeyHandler) {
3879
- this.signLanguageSettingsMenu.removeEventListener('keydown', this.signLanguageSettingsMenuKeyHandler);
3880
- this.signLanguageSettingsMenuKeyHandler = null;
3881
- }
3882
-
3883
- // Reset tabindex on menu items
3884
- const menuItems = Array.from(this.signLanguageSettingsMenu.querySelectorAll(`.${this.options.classPrefix}-sign-language-settings-item`));
3885
- menuItems.forEach(item => {
3886
- item.setAttribute('tabindex', '-1');
3887
- });
3888
-
3889
- // Update aria-expanded
3890
- if (this.signLanguageSettingsButton) {
3891
- this.signLanguageSettingsButton.setAttribute('aria-expanded', 'false');
3892
- if (focusButton) {
3893
- // Return focus to settings button
3894
- this.signLanguageSettingsButton.focus({ preventScroll: true });
3895
- }
3896
- }
3897
- }
3901
+ return this.signLanguageManager.hideSettingsMenu({ focusButton });
3898
3902
  }
3899
3903
 
3900
3904
  positionSignLanguageSettingsMenuImmediate() {
@@ -4042,6 +4046,15 @@ export class Player extends EventEmitter {
4042
4046
  }
4043
4047
 
4044
4048
  constrainSignLanguagePosition() {
4049
+ return this.signLanguageManager.constrainPosition();
4050
+ }
4051
+
4052
+ saveSignLanguagePreferences() {
4053
+ return this.signLanguageManager.savePreferences();
4054
+ }
4055
+
4056
+ // Legacy methods preserved for reference - can be removed after testing
4057
+ _legacyConstrainSignLanguagePosition() {
4045
4058
  if (!this.signLanguageWrapper || !this.videoWrapper) return;
4046
4059
 
4047
4060
  // Don't auto-position if user has manually positioned it
@@ -4111,7 +4124,7 @@ export class Player extends EventEmitter {
4111
4124
  this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter(c => c.startsWith('vidply-sign-position-')));
4112
4125
  }
4113
4126
 
4114
- saveSignLanguagePreferences() {
4127
+ _legacySaveSignLanguagePreferences() {
4115
4128
  if (!this.signLanguageWrapper) return;
4116
4129
 
4117
4130
  // Only save width - position is always calculated fresh to bottom-right
@@ -4124,73 +4137,7 @@ export class Player extends EventEmitter {
4124
4137
  }
4125
4138
 
4126
4139
  cleanupSignLanguage() {
4127
- // Hide settings menu if open
4128
- if (this.signLanguageSettingsMenuVisible) {
4129
- this.hideSignLanguageSettingsMenu({ focusButton: false });
4130
- }
4131
-
4132
- // Remove document click handler
4133
- if (this.signLanguageDocumentClickHandler && this.signLanguageDocumentClickHandlerAdded) {
4134
- document.removeEventListener('mousedown', this.signLanguageDocumentClickHandler, true);
4135
- this.signLanguageDocumentClickHandlerAdded = false;
4136
- this.signLanguageDocumentClickHandler = null;
4137
- }
4138
-
4139
- // Remove settings menu event listeners
4140
- if (this.signLanguageSettingsHandlers) {
4141
- if (this.signLanguageSettingsButton) {
4142
- this.signLanguageSettingsButton.removeEventListener('click', this.signLanguageSettingsHandlers.settingsClick);
4143
- this.signLanguageSettingsButton.removeEventListener('keydown', this.signLanguageSettingsHandlers.settingsKeydown);
4144
- }
4145
- this.signLanguageSettingsHandlers = null;
4146
- }
4147
-
4148
- // Remove event listeners
4149
- if (this.signLanguageHandlers) {
4150
- this.off('play', this.signLanguageHandlers.play);
4151
- this.off('pause', this.signLanguageHandlers.pause);
4152
- this.off('timeupdate', this.signLanguageHandlers.timeupdate);
4153
- this.off('ratechange', this.signLanguageHandlers.ratechange);
4154
- if (this.signLanguageHandlers.captionChange) {
4155
- this.off('captionsenabled', this.signLanguageHandlers.captionChange);
4156
- }
4157
- this.signLanguageHandlers = null;
4158
- }
4159
-
4160
- // Remove event listeners
4161
- if (this.signLanguageInteractionHandlers) {
4162
- if (this.signLanguageHeader && this.signLanguageInteractionHandlers.headerKeyHandler) {
4163
- this.signLanguageHeader.removeEventListener('keydown', this.signLanguageInteractionHandlers.headerKeyHandler);
4164
- }
4165
- if (this.signLanguageWrapper && this.signLanguageInteractionHandlers.customKeyHandler) {
4166
- this.signLanguageWrapper.removeEventListener('keydown', this.signLanguageInteractionHandlers.customKeyHandler);
4167
- }
4168
- }
4169
-
4170
- // Destroy draggable utility
4171
- if (this.signLanguageDraggable) {
4172
- if (this.signLanguageDraggable.pointerResizeMode) {
4173
- this.signLanguageDraggable.disablePointerResizeMode();
4174
- }
4175
- this.signLanguageDraggable.destroy();
4176
- this.signLanguageDraggable = null;
4177
- }
4178
-
4179
- // Clear interaction handlers reference
4180
- this.signLanguageInteractionHandlers = null;
4181
-
4182
- // Remove video and wrapper elements
4183
- if (this.signLanguageWrapper && this.signLanguageWrapper.parentNode) {
4184
- if (this.signLanguageVideo) {
4185
- this.signLanguageVideo.pause();
4186
- this.signLanguageVideo.src = '';
4187
- }
4188
- this.signLanguageWrapper.parentNode.removeChild(this.signLanguageWrapper);
4189
- }
4190
- this.signLanguageWrapper = null;
4191
- this.signLanguageVideo = null;
4192
- this.signLanguageSettingsButton = null;
4193
- this.signLanguageSettingsMenu = null;
4140
+ return this.signLanguageManager.cleanup();
4194
4141
  }
4195
4142
 
4196
4143
  // Settings