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.
- package/README.md +19 -4
- package/dist/vidply.css +640 -25
- package/dist/vidply.esm.js +1447 -93
- package/dist/vidply.esm.js.map +3 -3
- package/dist/vidply.esm.min.js +6 -6
- package/dist/vidply.esm.min.meta.json +38 -15
- package/dist/vidply.js +1447 -93
- package/dist/vidply.js.map +3 -3
- package/dist/vidply.min.css +1 -1
- package/dist/vidply.min.js +6 -6
- package/dist/vidply.min.meta.json +38 -15
- package/package.json +1 -1
- package/src/controls/CaptionManager.js +30 -0
- package/src/controls/ControlBar.js +11 -4
- package/src/controls/SettingsDialog.js +3 -3
- package/src/controls/TranscriptManager.js +1147 -72
- package/src/core/Player.js +450 -20
- package/src/i18n/translations.js +70 -15
- package/src/icons/Icons.js +8 -4
- package/src/styles/vidply.css +640 -25
- package/src/utils/StorageManager.js +156 -0
package/src/core/Player.js
CHANGED
|
@@ -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:
|
|
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
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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.
|
|
905
|
+
if (this.signLanguageWrapper) {
|
|
862
906
|
// Already exists, just show it
|
|
863
|
-
this.
|
|
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
|
-
//
|
|
876
|
-
const
|
|
877
|
-
|
|
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
|
-
//
|
|
887
|
-
this.
|
|
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.
|
|
925
|
-
this.
|
|
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
|
|
950
|
-
if (this.
|
|
951
|
-
this.signLanguageVideo
|
|
952
|
-
|
|
953
|
-
|
|
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
|
|