vidply 1.0.6 → 1.0.7

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.
@@ -15,6 +15,7 @@ import {VimeoRenderer} from '../renderers/VimeoRenderer.js';
15
15
  import {HLSRenderer} from '../renderers/HLSRenderer.js';
16
16
  import {createPlayOverlay} from '../icons/Icons.js';
17
17
  import {i18n} from '../i18n/i18n.js';
18
+ import {StorageManager} from '../utils/StorageManager.js';
18
19
 
19
20
  export class Player extends EventEmitter {
20
21
  constructor(element, options = {}) {
@@ -86,7 +87,7 @@ export class Player extends EventEmitter {
86
87
  captionsButton: true,
87
88
  transcriptButton: true,
88
89
  fullscreenButton: true,
89
- pipButton: true,
90
+ pipButton: false,
90
91
 
91
92
  // Seeking
92
93
  seekInterval: 10,
@@ -165,6 +166,17 @@ export class Player extends EventEmitter {
165
166
  ...options
166
167
  };
167
168
 
169
+ // Storage manager
170
+ this.storage = new StorageManager('vidply');
171
+
172
+ // Load saved player preferences
173
+ const savedPrefs = this.storage.getPlayerPreferences();
174
+ if (savedPrefs) {
175
+ if (savedPrefs.volume !== undefined) this.options.volume = savedPrefs.volume;
176
+ if (savedPrefs.playbackSpeed !== undefined) this.options.playbackSpeed = savedPrefs.playbackSpeed;
177
+ if (savedPrefs.muted !== undefined) this.options.muted = savedPrefs.muted;
178
+ }
179
+
168
180
  // State
169
181
  this.state = {
170
182
  ready: false,
@@ -642,6 +654,8 @@ export class Player extends EventEmitter {
642
654
  if (newVolume > 0 && this.state.muted) {
643
655
  this.state.muted = false;
644
656
  }
657
+
658
+ this.savePlayerPreferences();
645
659
  }
646
660
 
647
661
  getVolume() {
@@ -653,6 +667,7 @@ export class Player extends EventEmitter {
653
667
  this.renderer.setMuted(true);
654
668
  }
655
669
  this.state.muted = true;
670
+ this.savePlayerPreferences();
656
671
  this.emit('volumechange');
657
672
  }
658
673
 
@@ -661,6 +676,7 @@ export class Player extends EventEmitter {
661
676
  this.renderer.setMuted(false);
662
677
  }
663
678
  this.state.muted = false;
679
+ this.savePlayerPreferences();
664
680
  this.emit('volumechange');
665
681
  }
666
682
 
@@ -679,12 +695,22 @@ export class Player extends EventEmitter {
679
695
  this.renderer.setPlaybackSpeed(newSpeed);
680
696
  }
681
697
  this.state.playbackSpeed = newSpeed;
698
+ this.savePlayerPreferences();
682
699
  this.emit('playbackspeedchange', newSpeed);
683
700
  }
684
701
 
685
702
  getPlaybackSpeed() {
686
703
  return this.state.playbackSpeed;
687
704
  }
705
+
706
+ // Save player preferences to localStorage
707
+ savePlayerPreferences() {
708
+ this.storage.savePlayerPreferences({
709
+ volume: this.state.volume,
710
+ muted: this.state.muted,
711
+ playbackSpeed: this.state.playbackSpeed
712
+ });
713
+ }
688
714
 
689
715
  // Fullscreen
690
716
  enterFullscreen() {
@@ -844,10 +870,28 @@ export class Player extends EventEmitter {
844
870
  }
845
871
 
846
872
  async toggleAudioDescription() {
847
- if (this.state.audioDescriptionEnabled) {
848
- await this.disableAudioDescription();
849
- } else {
850
- await this.enableAudioDescription();
873
+ // Check if we have description tracks or audio-described video
874
+ const textTracks = Array.from(this.element.textTracks || []);
875
+ const descriptionTrack = textTracks.find(track => track.kind === 'descriptions');
876
+
877
+ if (descriptionTrack) {
878
+ // Toggle description track
879
+ if (descriptionTrack.mode === 'showing') {
880
+ descriptionTrack.mode = 'hidden';
881
+ this.state.audioDescriptionEnabled = false;
882
+ this.emit('audiodescriptiondisabled');
883
+ } else {
884
+ descriptionTrack.mode = 'showing';
885
+ this.state.audioDescriptionEnabled = true;
886
+ this.emit('audiodescriptionenabled');
887
+ }
888
+ } else if (this.audioDescriptionSrc) {
889
+ // Use audio-described video source
890
+ if (this.state.audioDescriptionEnabled) {
891
+ await this.disableAudioDescription();
892
+ } else {
893
+ await this.enableAudioDescription();
894
+ }
851
895
  }
852
896
  }
853
897
 
@@ -858,33 +902,69 @@ export class Player extends EventEmitter {
858
902
  return;
859
903
  }
860
904
 
861
- if (this.signLanguageVideo) {
905
+ if (this.signLanguageWrapper) {
862
906
  // Already exists, just show it
863
- this.signLanguageVideo.style.display = 'block';
907
+ this.signLanguageWrapper.style.display = 'block';
864
908
  this.state.signLanguageEnabled = true;
865
909
  this.emit('signlanguageenabled');
866
910
  return;
867
911
  }
868
912
 
913
+ // Create wrapper container
914
+ this.signLanguageWrapper = document.createElement('div');
915
+ this.signLanguageWrapper.className = 'vidply-sign-language-wrapper';
916
+ this.signLanguageWrapper.setAttribute('tabindex', '0');
917
+ this.signLanguageWrapper.setAttribute('aria-label', 'Sign Language Video - Press D to drag with keyboard, R to resize');
918
+
869
919
  // Create sign language video element
870
920
  this.signLanguageVideo = document.createElement('video');
871
921
  this.signLanguageVideo.className = 'vidply-sign-language-video';
872
922
  this.signLanguageVideo.src = this.signLanguageSrc;
873
923
  this.signLanguageVideo.setAttribute('aria-label', i18n.t('player.signLanguage'));
924
+ this.signLanguageVideo.muted = true; // Sign language video should be muted
874
925
 
875
- // Set position based on options
876
- const position = this.options.signLanguagePosition || 'bottom-right';
877
- this.signLanguageVideo.classList.add(`vidply-sign-position-${position}`);
926
+ // Create resize handles
927
+ const resizeHandles = ['nw', 'ne', 'sw', 'se'].map(dir => {
928
+ const handle = document.createElement('div');
929
+ handle.className = `vidply-sign-resize-handle vidply-sign-resize-${dir}`;
930
+ handle.setAttribute('data-direction', dir);
931
+ handle.setAttribute('aria-label', `Resize ${dir.toUpperCase()}`);
932
+ return handle;
933
+ });
934
+
935
+ // Append video and handles to wrapper
936
+ this.signLanguageWrapper.appendChild(this.signLanguageVideo);
937
+ resizeHandles.forEach(handle => this.signLanguageWrapper.appendChild(handle));
938
+
939
+ // Set width FIRST to ensure proper dimensions
940
+ const saved = this.storage.getSignLanguagePreferences();
941
+ if (saved && saved.size && saved.size.width) {
942
+ this.signLanguageWrapper.style.width = saved.size.width;
943
+ } else {
944
+ this.signLanguageWrapper.style.width = '280px'; // Default width
945
+ }
946
+ // Height is always auto to maintain aspect ratio
947
+ this.signLanguageWrapper.style.height = 'auto';
948
+
949
+ // Position is always calculated fresh - use option or default to bottom-right
950
+ this.signLanguageDesiredPosition = this.options.signLanguagePosition || 'bottom-right';
951
+
952
+ // Add to main player container (NOT videoWrapper) to avoid overflow:hidden clipping
953
+ this.container.appendChild(this.signLanguageWrapper);
954
+
955
+ // Set position immediately after appending
956
+ requestAnimationFrame(() => {
957
+ this.constrainSignLanguagePosition();
958
+ });
878
959
 
879
960
  // Sync with main video
880
- this.signLanguageVideo.muted = true; // Sign language video should be muted
881
961
  this.signLanguageVideo.currentTime = this.state.currentTime;
882
962
  if (!this.state.paused) {
883
963
  this.signLanguageVideo.play();
884
964
  }
885
965
 
886
- // Add to video wrapper (so it overlays the video, not the entire container)
887
- this.videoWrapper.appendChild(this.signLanguageVideo);
966
+ // Setup drag and resize
967
+ this.setupSignLanguageInteraction();
888
968
 
889
969
  // Create bound handlers to store references for cleanup
890
970
  this.signLanguageHandlers = {
@@ -921,8 +1001,8 @@ export class Player extends EventEmitter {
921
1001
  }
922
1002
 
923
1003
  disableSignLanguage() {
924
- if (this.signLanguageVideo) {
925
- this.signLanguageVideo.style.display = 'none';
1004
+ if (this.signLanguageWrapper) {
1005
+ this.signLanguageWrapper.style.display = 'none';
926
1006
  }
927
1007
  this.state.signLanguageEnabled = false;
928
1008
  this.emit('signlanguagedisabled');
@@ -936,6 +1016,318 @@ export class Player extends EventEmitter {
936
1016
  }
937
1017
  }
938
1018
 
1019
+ setupSignLanguageInteraction() {
1020
+ if (!this.signLanguageWrapper) return;
1021
+
1022
+ let isDragging = false;
1023
+ let isResizing = false;
1024
+ let resizeDirection = null;
1025
+ let startX = 0;
1026
+ let startY = 0;
1027
+ let startLeft = 0;
1028
+ let startTop = 0;
1029
+ let startWidth = 0;
1030
+ let startHeight = 0;
1031
+ let dragMode = false;
1032
+ let resizeMode = false;
1033
+
1034
+ // Mouse drag on video element
1035
+ const onMouseDownVideo = (e) => {
1036
+ if (e.target !== this.signLanguageVideo) return;
1037
+ e.preventDefault();
1038
+ isDragging = true;
1039
+ startX = e.clientX;
1040
+ startY = e.clientY;
1041
+ const rect = this.signLanguageWrapper.getBoundingClientRect();
1042
+ startLeft = rect.left;
1043
+ startTop = rect.top;
1044
+ this.signLanguageWrapper.classList.add('vidply-sign-dragging');
1045
+ };
1046
+
1047
+ // Mouse resize on handles
1048
+ const onMouseDownHandle = (e) => {
1049
+ if (!e.target.classList.contains('vidply-sign-resize-handle')) return;
1050
+ e.preventDefault();
1051
+ e.stopPropagation();
1052
+ isResizing = true;
1053
+ resizeDirection = e.target.getAttribute('data-direction');
1054
+ startX = e.clientX;
1055
+ startY = e.clientY;
1056
+ const rect = this.signLanguageWrapper.getBoundingClientRect();
1057
+ startLeft = rect.left;
1058
+ startTop = rect.top;
1059
+ startWidth = rect.width;
1060
+ startHeight = rect.height;
1061
+ this.signLanguageWrapper.classList.add('vidply-sign-resizing');
1062
+ };
1063
+
1064
+ const onMouseMove = (e) => {
1065
+ if (isDragging) {
1066
+ const deltaX = e.clientX - startX;
1067
+ const deltaY = e.clientY - startY;
1068
+
1069
+ // Get videoWrapper and container dimensions
1070
+ const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
1071
+ const containerRect = this.container.getBoundingClientRect();
1072
+ const wrapperRect = this.signLanguageWrapper.getBoundingClientRect();
1073
+
1074
+ // Calculate videoWrapper position relative to container
1075
+ const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
1076
+ const videoWrapperTop = videoWrapperRect.top - containerRect.top;
1077
+
1078
+ // Calculate new position (in client coordinates)
1079
+ let newLeft = startLeft + deltaX - containerRect.left;
1080
+ let newTop = startTop + deltaY - containerRect.top;
1081
+
1082
+ const controlsHeight = 95; // Height of controls when visible
1083
+
1084
+ // Constrain to videoWrapper bounds (ensuring it stays above controls)
1085
+ newLeft = Math.max(videoWrapperLeft, Math.min(newLeft, videoWrapperLeft + videoWrapperRect.width - wrapperRect.width));
1086
+ newTop = Math.max(videoWrapperTop, Math.min(newTop, videoWrapperTop + videoWrapperRect.height - wrapperRect.height - controlsHeight));
1087
+
1088
+ this.signLanguageWrapper.style.left = `${newLeft}px`;
1089
+ this.signLanguageWrapper.style.top = `${newTop}px`;
1090
+ this.signLanguageWrapper.style.right = 'auto';
1091
+ this.signLanguageWrapper.style.bottom = 'auto';
1092
+ // Remove position classes
1093
+ this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter(c => c.startsWith('vidply-sign-position-')));
1094
+ } else if (isResizing) {
1095
+ const deltaX = e.clientX - startX;
1096
+
1097
+ // Get videoWrapper and container dimensions
1098
+ const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
1099
+ const containerRect = this.container.getBoundingClientRect();
1100
+
1101
+ let newWidth = startWidth;
1102
+ let newLeft = startLeft - containerRect.left;
1103
+
1104
+ // Only resize width, let height auto-adjust to maintain aspect ratio
1105
+ if (resizeDirection.includes('e')) {
1106
+ newWidth = Math.max(150, startWidth + deltaX);
1107
+ // Constrain width to not exceed videoWrapper right edge
1108
+ const maxWidth = (videoWrapperRect.right - startLeft);
1109
+ newWidth = Math.min(newWidth, maxWidth);
1110
+ }
1111
+ if (resizeDirection.includes('w')) {
1112
+ const proposedWidth = Math.max(150, startWidth - deltaX);
1113
+ const proposedLeft = startLeft + (startWidth - proposedWidth) - containerRect.left;
1114
+ // Constrain to not go beyond videoWrapper left edge
1115
+ const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
1116
+ if (proposedLeft >= videoWrapperLeft) {
1117
+ newWidth = proposedWidth;
1118
+ newLeft = proposedLeft;
1119
+ }
1120
+ }
1121
+
1122
+ this.signLanguageWrapper.style.width = `${newWidth}px`;
1123
+ this.signLanguageWrapper.style.height = 'auto'; // Let video maintain aspect ratio
1124
+ if (resizeDirection.includes('w')) {
1125
+ this.signLanguageWrapper.style.left = `${newLeft}px`;
1126
+ }
1127
+ this.signLanguageWrapper.style.right = 'auto';
1128
+ this.signLanguageWrapper.style.bottom = 'auto';
1129
+ // Remove position classes
1130
+ this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter(c => c.startsWith('vidply-sign-position-')));
1131
+ }
1132
+ };
1133
+
1134
+ const onMouseUp = () => {
1135
+ if (isDragging || isResizing) {
1136
+ this.saveSignLanguagePreferences();
1137
+ }
1138
+ isDragging = false;
1139
+ isResizing = false;
1140
+ resizeDirection = null;
1141
+ this.signLanguageWrapper.classList.remove('vidply-sign-dragging', 'vidply-sign-resizing');
1142
+ };
1143
+
1144
+ // Keyboard controls
1145
+ const onKeyDown = (e) => {
1146
+ // Toggle drag mode with D key
1147
+ if (e.key === 'd' || e.key === 'D') {
1148
+ dragMode = !dragMode;
1149
+ resizeMode = false;
1150
+ this.signLanguageWrapper.classList.toggle('vidply-sign-keyboard-drag', dragMode);
1151
+ this.signLanguageWrapper.classList.remove('vidply-sign-keyboard-resize');
1152
+ e.preventDefault();
1153
+ return;
1154
+ }
1155
+
1156
+ // Toggle resize mode with R key
1157
+ if (e.key === 'r' || e.key === 'R') {
1158
+ resizeMode = !resizeMode;
1159
+ dragMode = false;
1160
+ this.signLanguageWrapper.classList.toggle('vidply-sign-keyboard-resize', resizeMode);
1161
+ this.signLanguageWrapper.classList.remove('vidply-sign-keyboard-drag');
1162
+ e.preventDefault();
1163
+ return;
1164
+ }
1165
+
1166
+ // Escape to exit modes
1167
+ if (e.key === 'Escape') {
1168
+ dragMode = false;
1169
+ resizeMode = false;
1170
+ this.signLanguageWrapper.classList.remove('vidply-sign-keyboard-drag', 'vidply-sign-keyboard-resize');
1171
+ e.preventDefault();
1172
+ return;
1173
+ }
1174
+
1175
+ // Arrow keys for drag/resize
1176
+ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
1177
+ const step = e.shiftKey ? 10 : 5;
1178
+ const rect = this.signLanguageWrapper.getBoundingClientRect();
1179
+
1180
+ // Get videoWrapper and container bounds
1181
+ const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
1182
+ const containerRect = this.container.getBoundingClientRect();
1183
+
1184
+ // Calculate videoWrapper position relative to container
1185
+ const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
1186
+ const videoWrapperTop = videoWrapperRect.top - containerRect.top;
1187
+
1188
+ if (dragMode) {
1189
+ // Get current position relative to container
1190
+ let left = rect.left - containerRect.left;
1191
+ let top = rect.top - containerRect.top;
1192
+
1193
+ if (e.key === 'ArrowLeft') left -= step;
1194
+ if (e.key === 'ArrowRight') left += step;
1195
+ if (e.key === 'ArrowUp') top -= step;
1196
+ if (e.key === 'ArrowDown') top += step;
1197
+
1198
+ const controlsHeight = 95; // Height of controls when visible
1199
+
1200
+ // Constrain to videoWrapper bounds (ensuring it stays above controls)
1201
+ left = Math.max(videoWrapperLeft, Math.min(left, videoWrapperLeft + videoWrapperRect.width - rect.width));
1202
+ top = Math.max(videoWrapperTop, Math.min(top, videoWrapperTop + videoWrapperRect.height - rect.height - controlsHeight));
1203
+
1204
+ this.signLanguageWrapper.style.left = `${left}px`;
1205
+ this.signLanguageWrapper.style.top = `${top}px`;
1206
+ this.signLanguageWrapper.style.right = 'auto';
1207
+ this.signLanguageWrapper.style.bottom = 'auto';
1208
+ // Remove position classes
1209
+ this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter(c => c.startsWith('vidply-sign-position-')));
1210
+ this.saveSignLanguagePreferences();
1211
+ e.preventDefault();
1212
+ } else if (resizeMode) {
1213
+ let width = rect.width;
1214
+
1215
+ // Only adjust width, height will auto-adjust to maintain aspect ratio
1216
+ if (e.key === 'ArrowLeft') width -= step;
1217
+ if (e.key === 'ArrowRight') width += step;
1218
+ // Up/Down also adjusts width for simplicity
1219
+ if (e.key === 'ArrowUp') width += step;
1220
+ if (e.key === 'ArrowDown') width -= step;
1221
+
1222
+ // Constrain width
1223
+ width = Math.max(150, width);
1224
+ // Don't let it exceed videoWrapper width
1225
+ width = Math.min(width, videoWrapperRect.width);
1226
+
1227
+ this.signLanguageWrapper.style.width = `${width}px`;
1228
+ this.signLanguageWrapper.style.height = 'auto';
1229
+ this.saveSignLanguagePreferences();
1230
+ e.preventDefault();
1231
+ }
1232
+ }
1233
+ };
1234
+
1235
+ // Attach event listeners
1236
+ this.signLanguageVideo.addEventListener('mousedown', onMouseDownVideo);
1237
+ const handles = this.signLanguageWrapper.querySelectorAll('.vidply-sign-resize-handle');
1238
+ handles.forEach(handle => handle.addEventListener('mousedown', onMouseDownHandle));
1239
+ document.addEventListener('mousemove', onMouseMove);
1240
+ document.addEventListener('mouseup', onMouseUp);
1241
+ this.signLanguageWrapper.addEventListener('keydown', onKeyDown);
1242
+
1243
+ // Store for cleanup
1244
+ this.signLanguageInteractionHandlers = {
1245
+ mouseDownVideo: onMouseDownVideo,
1246
+ mouseDownHandle: onMouseDownHandle,
1247
+ mouseMove: onMouseMove,
1248
+ mouseUp: onMouseUp,
1249
+ keyDown: onKeyDown,
1250
+ handles
1251
+ };
1252
+ }
1253
+
1254
+ constrainSignLanguagePosition() {
1255
+ if (!this.signLanguageWrapper || !this.videoWrapper) return;
1256
+
1257
+ // Ensure width is set
1258
+ if (!this.signLanguageWrapper.style.width || this.signLanguageWrapper.style.width === '') {
1259
+ this.signLanguageWrapper.style.width = '280px'; // Default width
1260
+ }
1261
+
1262
+ // Get videoWrapper position relative to the player CONTAINER (where sign language video is attached)
1263
+ const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
1264
+ const containerRect = this.container.getBoundingClientRect();
1265
+ const wrapperRect = this.signLanguageWrapper.getBoundingClientRect();
1266
+
1267
+ // Calculate videoWrapper's position and dimensions relative to container
1268
+ const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
1269
+ const videoWrapperTop = videoWrapperRect.top - containerRect.top;
1270
+ const videoWrapperWidth = videoWrapperRect.width;
1271
+ const videoWrapperHeight = videoWrapperRect.height;
1272
+
1273
+ // Use estimated height if video hasn't loaded yet (16:9 aspect ratio)
1274
+ let wrapperWidth = wrapperRect.width || 280;
1275
+ let wrapperHeight = wrapperRect.height || ((280 * 9) / 16); // Estimate based on 16:9 aspect ratio
1276
+
1277
+ let left, top;
1278
+ const margin = 16; // Margin from edges
1279
+ const controlsHeight = 95; // Height of controls when visible
1280
+
1281
+ // Always calculate fresh position based on desired location (relative to videoWrapper)
1282
+ const position = this.signLanguageDesiredPosition || 'bottom-right';
1283
+
1284
+ switch (position) {
1285
+ case 'bottom-right':
1286
+ left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
1287
+ top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
1288
+ break;
1289
+ case 'bottom-left':
1290
+ left = videoWrapperLeft + margin;
1291
+ top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
1292
+ break;
1293
+ case 'top-right':
1294
+ left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
1295
+ top = videoWrapperTop + margin;
1296
+ break;
1297
+ case 'top-left':
1298
+ left = videoWrapperLeft + margin;
1299
+ top = videoWrapperTop + margin;
1300
+ break;
1301
+ default:
1302
+ left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
1303
+ top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
1304
+ }
1305
+
1306
+ // Constrain to videoWrapper bounds (ensuring it stays above controls)
1307
+ left = Math.max(videoWrapperLeft, Math.min(left, videoWrapperLeft + videoWrapperWidth - wrapperWidth));
1308
+ top = Math.max(videoWrapperTop, Math.min(top, videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight));
1309
+
1310
+ // Apply constrained position
1311
+ this.signLanguageWrapper.style.left = `${left}px`;
1312
+ this.signLanguageWrapper.style.top = `${top}px`;
1313
+ this.signLanguageWrapper.style.right = 'auto';
1314
+ this.signLanguageWrapper.style.bottom = 'auto';
1315
+ // Remove position classes if any were applied
1316
+ this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter(c => c.startsWith('vidply-sign-position-')));
1317
+ }
1318
+
1319
+ saveSignLanguagePreferences() {
1320
+ if (!this.signLanguageWrapper) return;
1321
+
1322
+ // Only save width - position is always calculated fresh to bottom-right
1323
+ this.storage.saveSignLanguagePreferences({
1324
+ size: {
1325
+ width: this.signLanguageWrapper.style.width
1326
+ // Height is auto - maintained by aspect ratio
1327
+ }
1328
+ });
1329
+ }
1330
+
939
1331
  cleanupSignLanguage() {
940
1332
  // Remove event listeners
941
1333
  if (this.signLanguageHandlers) {
@@ -946,11 +1338,32 @@ export class Player extends EventEmitter {
946
1338
  this.signLanguageHandlers = null;
947
1339
  }
948
1340
 
949
- // Remove video element
950
- if (this.signLanguageVideo && this.signLanguageVideo.parentNode) {
951
- this.signLanguageVideo.pause();
952
- this.signLanguageVideo.src = '';
953
- this.signLanguageVideo.parentNode.removeChild(this.signLanguageVideo);
1341
+ // Remove interaction handlers
1342
+ if (this.signLanguageInteractionHandlers) {
1343
+ if (this.signLanguageVideo) {
1344
+ this.signLanguageVideo.removeEventListener('mousedown', this.signLanguageInteractionHandlers.mouseDownVideo);
1345
+ }
1346
+ if (this.signLanguageInteractionHandlers.handles) {
1347
+ this.signLanguageInteractionHandlers.handles.forEach(handle => {
1348
+ handle.removeEventListener('mousedown', this.signLanguageInteractionHandlers.mouseDownHandle);
1349
+ });
1350
+ }
1351
+ document.removeEventListener('mousemove', this.signLanguageInteractionHandlers.mouseMove);
1352
+ document.removeEventListener('mouseup', this.signLanguageInteractionHandlers.mouseUp);
1353
+ if (this.signLanguageWrapper) {
1354
+ this.signLanguageWrapper.removeEventListener('keydown', this.signLanguageInteractionHandlers.keyDown);
1355
+ }
1356
+ this.signLanguageInteractionHandlers = null;
1357
+ }
1358
+
1359
+ // Remove video and wrapper elements
1360
+ if (this.signLanguageWrapper && this.signLanguageWrapper.parentNode) {
1361
+ if (this.signLanguageVideo) {
1362
+ this.signLanguageVideo.pause();
1363
+ this.signLanguageVideo.src = '';
1364
+ }
1365
+ this.signLanguageWrapper.parentNode.removeChild(this.signLanguageWrapper);
1366
+ this.signLanguageWrapper = null;
954
1367
  this.signLanguageVideo = null;
955
1368
  }
956
1369
  }
@@ -1096,6 +1509,23 @@ export class Player extends EventEmitter {
1096
1509
  if (this.controlBar) {
1097
1510
  this.controlBar.updateFullscreenButton();
1098
1511
  }
1512
+
1513
+ // Reposition sign language video after fullscreen transition
1514
+ if (this.signLanguageWrapper && this.signLanguageWrapper.style.display !== 'none') {
1515
+ // Use setTimeout to ensure layout has updated after fullscreen transition
1516
+ // Longer delay to account for CSS transition animations and layout recalculation
1517
+ setTimeout(() => {
1518
+ // Use requestAnimationFrame to ensure the browser has fully rendered the layout
1519
+ requestAnimationFrame(() => {
1520
+ // Clear saved size and reset to default for the new container size
1521
+ this.storage.saveSignLanguagePreferences({ size: null });
1522
+ this.signLanguageDesiredPosition = 'bottom-right';
1523
+ // Reset to default width for the new container
1524
+ this.signLanguageWrapper.style.width = isFullscreen ? '400px' : '280px';
1525
+ this.constrainSignLanguagePosition();
1526
+ });
1527
+ }, 500);
1528
+ }
1099
1529
  }
1100
1530
  };
1101
1531