vidply 1.0.2 → 1.0.3
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 +6 -4
- package/dist/vidply.esm.js +157 -362
- package/dist/vidply.esm.js.map +3 -3
- package/dist/vidply.esm.min.js +3 -3
- package/dist/vidply.esm.min.meta.json +9 -43
- package/dist/vidply.js +157 -362
- package/dist/vidply.js.map +3 -3
- package/dist/vidply.min.js +3 -3
- package/dist/vidply.min.meta.json +9 -43
- package/package.json +1 -1
- package/src/controls/CaptionManager.js +216 -218
- package/src/controls/ControlBar.js +144 -26
- package/src/controls/KeyboardManager.js +50 -13
- package/src/core/Player.js +994 -1004
package/src/core/Player.js
CHANGED
|
@@ -3,1104 +3,1094 @@
|
|
|
3
3
|
* Main Player Class
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { createPlayOverlay } from '../icons/Icons.js';
|
|
19
|
-
import { i18n } from '../i18n/i18n.js';
|
|
6
|
+
import {EventEmitter} from '../utils/EventEmitter.js';
|
|
7
|
+
import {DOMUtils} from '../utils/DOMUtils.js';
|
|
8
|
+
import {ControlBar} from '../controls/ControlBar.js';
|
|
9
|
+
import {CaptionManager} from '../controls/CaptionManager.js';
|
|
10
|
+
import {KeyboardManager} from '../controls/KeyboardManager.js';
|
|
11
|
+
import {TranscriptManager} from '../controls/TranscriptManager.js';
|
|
12
|
+
import {HTML5Renderer} from '../renderers/HTML5Renderer.js';
|
|
13
|
+
import {YouTubeRenderer} from '../renderers/YouTubeRenderer.js';
|
|
14
|
+
import {VimeoRenderer} from '../renderers/VimeoRenderer.js';
|
|
15
|
+
import {HLSRenderer} from '../renderers/HLSRenderer.js';
|
|
16
|
+
import {createPlayOverlay} from '../icons/Icons.js';
|
|
17
|
+
import {i18n} from '../i18n/i18n.js';
|
|
20
18
|
|
|
21
19
|
export class Player extends EventEmitter {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
this.element = typeof element === 'string' ? document.querySelector(element) : element;
|
|
26
|
-
if (!this.element) {
|
|
27
|
-
throw new Error('VidPly: Element not found');
|
|
28
|
-
}
|
|
20
|
+
constructor(element, options = {}) {
|
|
21
|
+
super();
|
|
29
22
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const mediaElement = document.createElement(mediaType);
|
|
34
|
-
|
|
35
|
-
// Copy attributes from the div to the media element
|
|
36
|
-
Array.from(this.element.attributes).forEach(attr => {
|
|
37
|
-
if (attr.name !== 'id' && attr.name !== 'class' && !attr.name.startsWith('data-')) {
|
|
38
|
-
mediaElement.setAttribute(attr.name, attr.value);
|
|
23
|
+
this.element = typeof element === 'string' ? document.querySelector(element) : element;
|
|
24
|
+
if (!this.element) {
|
|
25
|
+
throw new Error('VidPly: Element not found');
|
|
39
26
|
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// Copy any track elements from the div
|
|
43
|
-
const tracks = this.element.querySelectorAll('track');
|
|
44
|
-
tracks.forEach(track => {
|
|
45
|
-
mediaElement.appendChild(track.cloneNode(true));
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// Clear the div and insert the media element
|
|
49
|
-
this.element.innerHTML = '';
|
|
50
|
-
this.element.appendChild(mediaElement);
|
|
51
|
-
|
|
52
|
-
// Update element reference to the actual media element
|
|
53
|
-
this.element = mediaElement;
|
|
54
|
-
}
|
|
55
27
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
duration: true,
|
|
81
|
-
volumeControl: true,
|
|
82
|
-
muteButton: true,
|
|
83
|
-
chaptersButton: true,
|
|
84
|
-
qualityButton: true,
|
|
85
|
-
captionStyleButton: true,
|
|
86
|
-
speedButton: true,
|
|
87
|
-
captionsButton: true,
|
|
88
|
-
transcriptButton: true,
|
|
89
|
-
fullscreenButton: true,
|
|
90
|
-
pipButton: true,
|
|
91
|
-
|
|
92
|
-
// Seeking
|
|
93
|
-
seekInterval: 10,
|
|
94
|
-
seekIntervalLarge: 30,
|
|
95
|
-
|
|
96
|
-
// Captions
|
|
97
|
-
captions: true,
|
|
98
|
-
captionsDefault: false,
|
|
99
|
-
captionsFontSize: '100%',
|
|
100
|
-
captionsFontFamily: 'sans-serif',
|
|
101
|
-
captionsColor: '#FFFFFF',
|
|
102
|
-
captionsBackgroundColor: '#000000',
|
|
103
|
-
captionsOpacity: 0.8,
|
|
104
|
-
|
|
105
|
-
// Audio Description
|
|
106
|
-
audioDescription: true,
|
|
107
|
-
audioDescriptionSrc: null, // URL to audio-described version
|
|
108
|
-
audioDescriptionButton: true,
|
|
109
|
-
|
|
110
|
-
// Sign Language
|
|
111
|
-
signLanguage: true,
|
|
112
|
-
signLanguageSrc: null, // URL to sign language video
|
|
113
|
-
signLanguageButton: true,
|
|
114
|
-
signLanguagePosition: 'bottom-right', // Position: 'bottom-right', 'bottom-left', 'top-right', 'top-left'
|
|
115
|
-
|
|
116
|
-
// Transcripts
|
|
117
|
-
transcript: false,
|
|
118
|
-
transcriptPosition: 'external',
|
|
119
|
-
transcriptContainer: null,
|
|
120
|
-
|
|
121
|
-
// Keyboard
|
|
122
|
-
keyboard: true,
|
|
123
|
-
keyboardShortcuts: {
|
|
124
|
-
'play-pause': [' ', 'p', 'k'],
|
|
125
|
-
'volume-up': ['ArrowUp'],
|
|
126
|
-
'volume-down': ['ArrowDown'],
|
|
127
|
-
'seek-forward': ['ArrowRight', 'f'],
|
|
128
|
-
'seek-backward': ['ArrowLeft', 'r'],
|
|
129
|
-
'seek-forward-large': ['l'],
|
|
130
|
-
'seek-backward-large': ['j'],
|
|
131
|
-
'mute': ['m'],
|
|
132
|
-
'fullscreen': ['f'],
|
|
133
|
-
'captions': ['c'],
|
|
134
|
-
'speed-up': ['>'],
|
|
135
|
-
'speed-down': ['<'],
|
|
136
|
-
'settings': ['s']
|
|
137
|
-
},
|
|
138
|
-
|
|
139
|
-
// Accessibility
|
|
140
|
-
ariaLabels: {},
|
|
141
|
-
screenReaderAnnouncements: true,
|
|
142
|
-
highContrast: false,
|
|
143
|
-
focusHighlight: true,
|
|
144
|
-
|
|
145
|
-
// Languages
|
|
146
|
-
language: 'en',
|
|
147
|
-
languages: ['en'],
|
|
148
|
-
|
|
149
|
-
// Advanced
|
|
150
|
-
debug: false,
|
|
151
|
-
classPrefix: 'vidply',
|
|
152
|
-
iconType: 'svg',
|
|
153
|
-
pauseOthersOnPlay: true,
|
|
154
|
-
|
|
155
|
-
// Callbacks
|
|
156
|
-
onReady: null,
|
|
157
|
-
onPlay: null,
|
|
158
|
-
onPause: null,
|
|
159
|
-
onEnded: null,
|
|
160
|
-
onTimeUpdate: null,
|
|
161
|
-
onVolumeChange: null,
|
|
162
|
-
onError: null,
|
|
163
|
-
|
|
164
|
-
...options
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
// State
|
|
168
|
-
this.state = {
|
|
169
|
-
ready: false,
|
|
170
|
-
playing: false,
|
|
171
|
-
paused: true,
|
|
172
|
-
ended: false,
|
|
173
|
-
buffering: false,
|
|
174
|
-
seeking: false,
|
|
175
|
-
muted: this.options.muted,
|
|
176
|
-
volume: this.options.volume,
|
|
177
|
-
currentTime: 0,
|
|
178
|
-
duration: 0,
|
|
179
|
-
playbackSpeed: this.options.playbackSpeed,
|
|
180
|
-
fullscreen: false,
|
|
181
|
-
pip: false,
|
|
182
|
-
captionsEnabled: this.options.captionsDefault,
|
|
183
|
-
currentCaption: null,
|
|
184
|
-
controlsVisible: true,
|
|
185
|
-
audioDescriptionEnabled: false,
|
|
186
|
-
signLanguageEnabled: false
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
// Store original source for toggling
|
|
190
|
-
this.originalSrc = null;
|
|
191
|
-
this.audioDescriptionSrc = this.options.audioDescriptionSrc;
|
|
192
|
-
this.signLanguageSrc = this.options.signLanguageSrc;
|
|
193
|
-
this.signLanguageVideo = null;
|
|
194
|
-
|
|
195
|
-
// Components
|
|
196
|
-
this.container = null;
|
|
197
|
-
this.renderer = null;
|
|
198
|
-
this.controlBar = null;
|
|
199
|
-
this.captionManager = null;
|
|
200
|
-
this.keyboardManager = null;
|
|
201
|
-
this.settingsDialog = null;
|
|
202
|
-
|
|
203
|
-
// Initialize
|
|
204
|
-
this.init();
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async init() {
|
|
208
|
-
try {
|
|
209
|
-
this.log('Initializing VidPly player');
|
|
210
|
-
|
|
211
|
-
// Auto-detect language from HTML lang attribute if not explicitly set
|
|
212
|
-
if (!this.options.language || this.options.language === 'en') {
|
|
213
|
-
const htmlLang = this.detectHtmlLanguage();
|
|
214
|
-
if (htmlLang) {
|
|
215
|
-
this.options.language = htmlLang;
|
|
216
|
-
this.log(`Auto-detected language from HTML: ${htmlLang}`);
|
|
28
|
+
// Auto-create media element if a non-media element is provided
|
|
29
|
+
if (this.element.tagName !== 'VIDEO' && this.element.tagName !== 'AUDIO') {
|
|
30
|
+
const mediaType = options.mediaType || 'video';
|
|
31
|
+
const mediaElement = document.createElement(mediaType);
|
|
32
|
+
|
|
33
|
+
// Copy attributes from the div to the media element
|
|
34
|
+
Array.from(this.element.attributes).forEach(attr => {
|
|
35
|
+
if (attr.name !== 'id' && attr.name !== 'class' && !attr.name.startsWith('data-')) {
|
|
36
|
+
mediaElement.setAttribute(attr.name, attr.value);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Copy any track elements from the div
|
|
41
|
+
const tracks = this.element.querySelectorAll('track');
|
|
42
|
+
tracks.forEach(track => {
|
|
43
|
+
mediaElement.appendChild(track.cloneNode(true));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Clear the div and insert the media element
|
|
47
|
+
this.element.innerHTML = '';
|
|
48
|
+
this.element.appendChild(mediaElement);
|
|
49
|
+
|
|
50
|
+
// Update element reference to the actual media element
|
|
51
|
+
this.element = mediaElement;
|
|
217
52
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
53
|
+
|
|
54
|
+
// Default options
|
|
55
|
+
this.options = {
|
|
56
|
+
// Display
|
|
57
|
+
width: null,
|
|
58
|
+
height: null,
|
|
59
|
+
poster: null,
|
|
60
|
+
responsive: true,
|
|
61
|
+
fillContainer: false,
|
|
62
|
+
|
|
63
|
+
// Playback
|
|
64
|
+
autoplay: false,
|
|
65
|
+
loop: false,
|
|
66
|
+
muted: false,
|
|
67
|
+
volume: 0.8,
|
|
68
|
+
playbackSpeed: 1.0,
|
|
69
|
+
preload: 'metadata',
|
|
70
|
+
startTime: 0,
|
|
71
|
+
|
|
72
|
+
// Controls
|
|
73
|
+
controls: true,
|
|
74
|
+
hideControlsDelay: 3000,
|
|
75
|
+
playPauseButton: true,
|
|
76
|
+
progressBar: true,
|
|
77
|
+
currentTime: true,
|
|
78
|
+
duration: true,
|
|
79
|
+
volumeControl: true,
|
|
80
|
+
muteButton: true,
|
|
81
|
+
chaptersButton: true,
|
|
82
|
+
qualityButton: true,
|
|
83
|
+
captionStyleButton: true,
|
|
84
|
+
speedButton: true,
|
|
85
|
+
captionsButton: true,
|
|
86
|
+
transcriptButton: true,
|
|
87
|
+
fullscreenButton: true,
|
|
88
|
+
pipButton: true,
|
|
89
|
+
|
|
90
|
+
// Seeking
|
|
91
|
+
seekInterval: 10,
|
|
92
|
+
seekIntervalLarge: 30,
|
|
93
|
+
|
|
94
|
+
// Captions
|
|
95
|
+
captions: true,
|
|
96
|
+
captionsDefault: false,
|
|
97
|
+
captionsFontSize: '100%',
|
|
98
|
+
captionsFontFamily: 'sans-serif',
|
|
99
|
+
captionsColor: '#FFFFFF',
|
|
100
|
+
captionsBackgroundColor: '#000000',
|
|
101
|
+
captionsOpacity: 0.8,
|
|
102
|
+
|
|
103
|
+
// Audio Description
|
|
104
|
+
audioDescription: true,
|
|
105
|
+
audioDescriptionSrc: null, // URL to audio-described version
|
|
106
|
+
audioDescriptionButton: true,
|
|
107
|
+
|
|
108
|
+
// Sign Language
|
|
109
|
+
signLanguage: true,
|
|
110
|
+
signLanguageSrc: null, // URL to sign language video
|
|
111
|
+
signLanguageButton: true,
|
|
112
|
+
signLanguagePosition: 'bottom-right', // Position: 'bottom-right', 'bottom-left', 'top-right', 'top-left'
|
|
113
|
+
|
|
114
|
+
// Transcripts
|
|
115
|
+
transcript: false,
|
|
116
|
+
transcriptPosition: 'external',
|
|
117
|
+
transcriptContainer: null,
|
|
118
|
+
|
|
119
|
+
// Keyboard
|
|
120
|
+
keyboard: true,
|
|
121
|
+
keyboardShortcuts: {
|
|
122
|
+
'play-pause': [' ', 'p', 'k'],
|
|
123
|
+
'volume-up': ['ArrowUp'],
|
|
124
|
+
'volume-down': ['ArrowDown'],
|
|
125
|
+
'seek-forward': ['ArrowRight'],
|
|
126
|
+
'seek-backward': ['ArrowLeft'],
|
|
127
|
+
'mute': ['m'],
|
|
128
|
+
'fullscreen': ['f'],
|
|
129
|
+
'captions': ['c'],
|
|
130
|
+
'caption-style-menu': ['a'],
|
|
131
|
+
'speed-up': ['>'],
|
|
132
|
+
'speed-down': ['<'],
|
|
133
|
+
'speed-menu': ['s'],
|
|
134
|
+
'quality-menu': ['q'],
|
|
135
|
+
'chapters-menu': ['j'],
|
|
136
|
+
'transcript-toggle': ['t']
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// Accessibility
|
|
140
|
+
ariaLabels: {},
|
|
141
|
+
screenReaderAnnouncements: true,
|
|
142
|
+
highContrast: false,
|
|
143
|
+
focusHighlight: true,
|
|
144
|
+
|
|
145
|
+
// Languages
|
|
146
|
+
language: 'en',
|
|
147
|
+
languages: ['en'],
|
|
148
|
+
|
|
149
|
+
// Advanced
|
|
150
|
+
debug: false,
|
|
151
|
+
classPrefix: 'vidply',
|
|
152
|
+
iconType: 'svg',
|
|
153
|
+
pauseOthersOnPlay: true,
|
|
154
|
+
|
|
155
|
+
// Callbacks
|
|
156
|
+
onReady: null,
|
|
157
|
+
onPlay: null,
|
|
158
|
+
onPause: null,
|
|
159
|
+
onEnded: null,
|
|
160
|
+
onTimeUpdate: null,
|
|
161
|
+
onVolumeChange: null,
|
|
162
|
+
onError: null,
|
|
163
|
+
|
|
164
|
+
...options
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// State
|
|
168
|
+
this.state = {
|
|
169
|
+
ready: false,
|
|
170
|
+
playing: false,
|
|
171
|
+
paused: true,
|
|
172
|
+
ended: false,
|
|
173
|
+
buffering: false,
|
|
174
|
+
seeking: false,
|
|
175
|
+
muted: this.options.muted,
|
|
176
|
+
volume: this.options.volume,
|
|
177
|
+
currentTime: 0,
|
|
178
|
+
duration: 0,
|
|
179
|
+
playbackSpeed: this.options.playbackSpeed,
|
|
180
|
+
fullscreen: false,
|
|
181
|
+
pip: false,
|
|
182
|
+
captionsEnabled: this.options.captionsDefault,
|
|
183
|
+
currentCaption: null,
|
|
184
|
+
controlsVisible: true,
|
|
185
|
+
audioDescriptionEnabled: false,
|
|
186
|
+
signLanguageEnabled: false
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Store original source for toggling
|
|
190
|
+
this.originalSrc = null;
|
|
191
|
+
this.audioDescriptionSrc = this.options.audioDescriptionSrc;
|
|
192
|
+
this.signLanguageSrc = this.options.signLanguageSrc;
|
|
193
|
+
this.signLanguageVideo = null;
|
|
194
|
+
|
|
195
|
+
// Components
|
|
196
|
+
this.container = null;
|
|
197
|
+
this.renderer = null;
|
|
198
|
+
this.controlBar = null;
|
|
199
|
+
this.captionManager = null;
|
|
200
|
+
this.keyboardManager = null;
|
|
201
|
+
this.settingsDialog = null;
|
|
202
|
+
|
|
203
|
+
// Initialize
|
|
204
|
+
this.init();
|
|
305
205
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
206
|
+
|
|
207
|
+
async init() {
|
|
208
|
+
try {
|
|
209
|
+
this.log('Initializing VidPly player');
|
|
210
|
+
|
|
211
|
+
// Auto-detect language from HTML lang attribute if not explicitly set
|
|
212
|
+
if (!this.options.language || this.options.language === 'en') {
|
|
213
|
+
const htmlLang = this.detectHtmlLanguage();
|
|
214
|
+
if (htmlLang) {
|
|
215
|
+
this.options.language = htmlLang;
|
|
216
|
+
this.log(`Auto-detected language from HTML: ${htmlLang}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Set language
|
|
221
|
+
i18n.setLanguage(this.options.language);
|
|
222
|
+
|
|
223
|
+
// Create container
|
|
224
|
+
this.createContainer();
|
|
225
|
+
|
|
226
|
+
// Detect and initialize renderer (only if source exists)
|
|
227
|
+
const src = this.element.src || this.element.querySelector('source')?.src;
|
|
228
|
+
if (src) {
|
|
229
|
+
await this.initializeRenderer();
|
|
230
|
+
} else {
|
|
231
|
+
this.log('No initial source - waiting for playlist or manual load');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Create controls
|
|
235
|
+
if (this.options.controls) {
|
|
236
|
+
this.controlBar = new ControlBar(this);
|
|
237
|
+
this.videoWrapper.appendChild(this.controlBar.element);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Initialize captions
|
|
241
|
+
if (this.options.captions) {
|
|
242
|
+
this.captionManager = new CaptionManager(this);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Initialize transcript
|
|
246
|
+
if (this.options.transcript || this.options.transcriptButton) {
|
|
247
|
+
this.transcriptManager = new TranscriptManager(this);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Initialize keyboard controls
|
|
251
|
+
if (this.options.keyboard) {
|
|
252
|
+
this.keyboardManager = new KeyboardManager(this);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Setup responsive handlers
|
|
256
|
+
this.setupResponsiveHandlers();
|
|
257
|
+
|
|
258
|
+
// Set initial state
|
|
259
|
+
if (this.options.startTime > 0) {
|
|
260
|
+
this.seek(this.options.startTime);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (this.options.muted) {
|
|
264
|
+
this.mute();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (this.options.volume !== 0.8) {
|
|
268
|
+
this.setVolume(this.options.volume);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Mark as ready
|
|
272
|
+
this.state.ready = true;
|
|
273
|
+
this.emit('ready');
|
|
274
|
+
|
|
275
|
+
if (this.options.onReady) {
|
|
276
|
+
this.options.onReady.call(this);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Autoplay if enabled
|
|
280
|
+
if (this.options.autoplay) {
|
|
281
|
+
this.play();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this.log('Player initialized successfully');
|
|
285
|
+
} catch (error) {
|
|
286
|
+
this.handleError(error);
|
|
287
|
+
}
|
|
315
288
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Detect language from HTML lang attribute
|
|
292
|
+
* @returns {string|null} Language code if available in translations, null otherwise
|
|
293
|
+
*/
|
|
294
|
+
detectHtmlLanguage() {
|
|
295
|
+
// Try to get lang from html element
|
|
296
|
+
const htmlLang = document.documentElement.lang || document.documentElement.getAttribute('lang');
|
|
297
|
+
|
|
298
|
+
if (!htmlLang) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Normalize the language code (e.g., "en-US" -> "en", "de-DE" -> "de")
|
|
303
|
+
const normalizedLang = htmlLang.toLowerCase().split('-')[0];
|
|
304
|
+
|
|
305
|
+
// Check if this language is available in our translations
|
|
306
|
+
const availableLanguages = ['en', 'de', 'es', 'fr', 'ja'];
|
|
307
|
+
|
|
308
|
+
if (availableLanguages.includes(normalizedLang)) {
|
|
309
|
+
return normalizedLang;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Language not available, will fallback to English
|
|
313
|
+
this.log(`Language "${htmlLang}" not available, using English as fallback`);
|
|
314
|
+
return 'en';
|
|
340
315
|
}
|
|
341
316
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
317
|
+
createContainer() {
|
|
318
|
+
// Create main container
|
|
319
|
+
this.container = DOMUtils.createElement('div', {
|
|
320
|
+
className: `${this.options.classPrefix}-player`,
|
|
321
|
+
attributes: {
|
|
322
|
+
'role': 'region',
|
|
323
|
+
'aria-label': i18n.t('player.label'),
|
|
324
|
+
'tabindex': '0'
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Add media type class
|
|
329
|
+
const mediaType = this.element.tagName.toLowerCase();
|
|
330
|
+
this.container.classList.add(`${this.options.classPrefix}-${mediaType}`);
|
|
331
|
+
|
|
332
|
+
// Add responsive class
|
|
333
|
+
if (this.options.responsive) {
|
|
334
|
+
this.container.classList.add(`${this.options.classPrefix}-responsive`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Create video wrapper (for proper positioning of controls)
|
|
338
|
+
this.videoWrapper = DOMUtils.createElement('div', {
|
|
339
|
+
className: `${this.options.classPrefix}-video-wrapper`
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Wrap original element
|
|
343
|
+
this.element.parentNode.insertBefore(this.container, this.element);
|
|
344
|
+
this.container.appendChild(this.videoWrapper);
|
|
345
|
+
this.videoWrapper.appendChild(this.element);
|
|
346
|
+
|
|
347
|
+
// Hide native controls and set dimensions
|
|
348
|
+
this.element.controls = false;
|
|
349
|
+
this.element.removeAttribute('controls');
|
|
350
|
+
this.element.setAttribute('tabindex', '-1'); // Remove from tab order
|
|
351
|
+
this.element.style.width = '100%';
|
|
352
|
+
this.element.style.height = '100%';
|
|
353
|
+
|
|
354
|
+
// Set dimensions
|
|
355
|
+
if (this.options.width) {
|
|
356
|
+
this.container.style.width = typeof this.options.width === 'number'
|
|
357
|
+
? `${this.options.width}px`
|
|
358
|
+
: this.options.width;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (this.options.height) {
|
|
362
|
+
this.container.style.height = typeof this.options.height === 'number'
|
|
363
|
+
? `${this.options.height}px`
|
|
364
|
+
: this.options.height;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Set poster
|
|
368
|
+
if (this.options.poster && this.element.tagName === 'VIDEO') {
|
|
369
|
+
this.element.poster = this.options.poster;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Create centered play button overlay (only for video)
|
|
373
|
+
if (this.element.tagName === 'VIDEO') {
|
|
374
|
+
this.createPlayButtonOverlay();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Make video/audio element clickable to toggle play/pause
|
|
378
|
+
this.element.style.cursor = 'pointer';
|
|
379
|
+
this.element.addEventListener('click', (e) => {
|
|
380
|
+
// Prevent if clicking on native controls (shouldn't happen but just in case)
|
|
381
|
+
if (e.target === this.element) {
|
|
382
|
+
this.toggle();
|
|
383
|
+
}
|
|
384
|
+
});
|
|
364
385
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
386
|
+
|
|
387
|
+
createPlayButtonOverlay() {
|
|
388
|
+
// Create complete SVG play button from Icons.js
|
|
389
|
+
this.playButtonOverlay = createPlayOverlay();
|
|
390
|
+
|
|
391
|
+
// Add click handler
|
|
392
|
+
this.playButtonOverlay.addEventListener('click', () => {
|
|
393
|
+
this.toggle();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Add to video wrapper
|
|
397
|
+
this.videoWrapper.appendChild(this.playButtonOverlay);
|
|
398
|
+
|
|
399
|
+
// Show/hide based on play state
|
|
400
|
+
this.on('play', () => {
|
|
401
|
+
this.playButtonOverlay.style.opacity = '0';
|
|
402
|
+
this.playButtonOverlay.style.pointerEvents = 'none';
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
this.on('pause', () => {
|
|
406
|
+
this.playButtonOverlay.style.opacity = '1';
|
|
407
|
+
this.playButtonOverlay.style.pointerEvents = 'auto';
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
this.on('ended', () => {
|
|
411
|
+
this.playButtonOverlay.style.opacity = '1';
|
|
412
|
+
this.playButtonOverlay.style.pointerEvents = 'auto';
|
|
413
|
+
});
|
|
370
414
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
415
|
+
|
|
416
|
+
async initializeRenderer() {
|
|
417
|
+
const src = this.element.src || this.element.querySelector('source')?.src;
|
|
418
|
+
|
|
419
|
+
if (!src) {
|
|
420
|
+
throw new Error('No media source found');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Store original source for audio description toggling
|
|
424
|
+
if (!this.originalSrc) {
|
|
425
|
+
this.originalSrc = src;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Detect media type
|
|
429
|
+
let renderer;
|
|
430
|
+
|
|
431
|
+
if (src.includes('youtube.com') || src.includes('youtu.be')) {
|
|
432
|
+
renderer = YouTubeRenderer;
|
|
433
|
+
} else if (src.includes('vimeo.com')) {
|
|
434
|
+
renderer = VimeoRenderer;
|
|
435
|
+
} else if (src.includes('.m3u8')) {
|
|
436
|
+
renderer = HLSRenderer;
|
|
437
|
+
} else {
|
|
438
|
+
renderer = HTML5Renderer;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
this.log(`Using ${renderer.name} renderer`);
|
|
442
|
+
this.renderer = new renderer(this);
|
|
443
|
+
await this.renderer.init();
|
|
375
444
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Load new media source (for playlists)
|
|
448
|
+
* @param {Object} config - Media configuration
|
|
449
|
+
* @param {string} config.src - Media source URL
|
|
450
|
+
* @param {string} config.type - Media MIME type
|
|
451
|
+
* @param {string} [config.poster] - Poster image URL
|
|
452
|
+
* @param {Array} [config.tracks] - Text tracks (captions, chapters, etc.)
|
|
453
|
+
*/
|
|
454
|
+
async load(config) {
|
|
455
|
+
try {
|
|
456
|
+
this.log('Loading new media:', config.src);
|
|
457
|
+
|
|
458
|
+
// Pause current playback
|
|
459
|
+
if (this.renderer) {
|
|
460
|
+
this.pause();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Clear existing text tracks
|
|
464
|
+
const existingTracks = this.element.querySelectorAll('track');
|
|
465
|
+
existingTracks.forEach(track => track.remove());
|
|
466
|
+
|
|
467
|
+
// Update media element
|
|
468
|
+
this.element.src = config.src;
|
|
469
|
+
|
|
470
|
+
if (config.type) {
|
|
471
|
+
this.element.type = config.type;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (config.poster && this.element.tagName === 'VIDEO') {
|
|
475
|
+
this.element.poster = config.poster;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Add new text tracks
|
|
479
|
+
if (config.tracks && config.tracks.length > 0) {
|
|
480
|
+
config.tracks.forEach(trackConfig => {
|
|
481
|
+
const track = document.createElement('track');
|
|
482
|
+
track.src = trackConfig.src;
|
|
483
|
+
track.kind = trackConfig.kind || 'captions';
|
|
484
|
+
track.srclang = trackConfig.srclang || 'en';
|
|
485
|
+
track.label = trackConfig.label || trackConfig.srclang;
|
|
486
|
+
|
|
487
|
+
if (trackConfig.default) {
|
|
488
|
+
track.default = true;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
this.element.appendChild(track);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Check if we need to change renderer type
|
|
496
|
+
const shouldChangeRenderer = this.shouldChangeRenderer(config.src);
|
|
497
|
+
|
|
498
|
+
// Destroy old renderer if changing types
|
|
499
|
+
if (shouldChangeRenderer && this.renderer) {
|
|
500
|
+
this.renderer.destroy();
|
|
501
|
+
this.renderer = null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Initialize or reinitialize renderer
|
|
505
|
+
if (!this.renderer || shouldChangeRenderer) {
|
|
506
|
+
await this.initializeRenderer();
|
|
507
|
+
} else {
|
|
508
|
+
// Just reload the current renderer with the updated element
|
|
509
|
+
this.renderer.media = this.element; // Update media reference
|
|
510
|
+
this.element.load();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Reinitialize caption manager to pick up new tracks
|
|
514
|
+
if (this.captionManager) {
|
|
515
|
+
this.captionManager.destroy();
|
|
516
|
+
this.captionManager = new CaptionManager(this);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
this.emit('sourcechange', config);
|
|
520
|
+
this.log('Media loaded successfully');
|
|
521
|
+
|
|
522
|
+
} catch (error) {
|
|
523
|
+
this.handleError(error);
|
|
524
|
+
}
|
|
380
525
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
this.videoWrapper.appendChild(this.playButtonOverlay);
|
|
403
|
-
|
|
404
|
-
// Show/hide based on play state
|
|
405
|
-
this.on('play', () => {
|
|
406
|
-
this.playButtonOverlay.style.opacity = '0';
|
|
407
|
-
this.playButtonOverlay.style.pointerEvents = 'none';
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
this.on('pause', () => {
|
|
411
|
-
this.playButtonOverlay.style.opacity = '1';
|
|
412
|
-
this.playButtonOverlay.style.pointerEvents = 'auto';
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
this.on('ended', () => {
|
|
416
|
-
this.playButtonOverlay.style.opacity = '1';
|
|
417
|
-
this.playButtonOverlay.style.pointerEvents = 'auto';
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
async initializeRenderer() {
|
|
422
|
-
const src = this.element.src || this.element.querySelector('source')?.src;
|
|
423
|
-
|
|
424
|
-
if (!src) {
|
|
425
|
-
throw new Error('No media source found');
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Check if we need to change renderer type
|
|
529
|
+
* @param {string} src - New source URL
|
|
530
|
+
* @returns {boolean}
|
|
531
|
+
*/
|
|
532
|
+
shouldChangeRenderer(src) {
|
|
533
|
+
if (!this.renderer) return true;
|
|
534
|
+
|
|
535
|
+
const isYouTube = src.includes('youtube.com') || src.includes('youtu.be');
|
|
536
|
+
const isVimeo = src.includes('vimeo.com');
|
|
537
|
+
const isHLS = src.includes('.m3u8');
|
|
538
|
+
|
|
539
|
+
const currentRendererName = this.renderer.constructor.name;
|
|
540
|
+
|
|
541
|
+
if (isYouTube && currentRendererName !== 'YouTubeRenderer') return true;
|
|
542
|
+
if (isVimeo && currentRendererName !== 'VimeoRenderer') return true;
|
|
543
|
+
if (isHLS && currentRendererName !== 'HLSRenderer') return true;
|
|
544
|
+
if (!isYouTube && !isVimeo && !isHLS && currentRendererName !== 'HTML5Renderer') return true;
|
|
545
|
+
|
|
546
|
+
return false;
|
|
426
547
|
}
|
|
427
548
|
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
|
|
549
|
+
// Playback controls
|
|
550
|
+
play() {
|
|
551
|
+
if (this.renderer) {
|
|
552
|
+
this.renderer.play();
|
|
553
|
+
}
|
|
431
554
|
}
|
|
432
555
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
renderer = YouTubeRenderer;
|
|
438
|
-
} else if (src.includes('vimeo.com')) {
|
|
439
|
-
renderer = VimeoRenderer;
|
|
440
|
-
} else if (src.includes('.m3u8')) {
|
|
441
|
-
renderer = HLSRenderer;
|
|
442
|
-
} else {
|
|
443
|
-
renderer = HTML5Renderer;
|
|
556
|
+
pause() {
|
|
557
|
+
if (this.renderer) {
|
|
558
|
+
this.renderer.pause();
|
|
559
|
+
}
|
|
444
560
|
}
|
|
445
561
|
|
|
446
|
-
|
|
447
|
-
this.renderer = new renderer(this);
|
|
448
|
-
await this.renderer.init();
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Load new media source (for playlists)
|
|
453
|
-
* @param {Object} config - Media configuration
|
|
454
|
-
* @param {string} config.src - Media source URL
|
|
455
|
-
* @param {string} config.type - Media MIME type
|
|
456
|
-
* @param {string} [config.poster] - Poster image URL
|
|
457
|
-
* @param {Array} [config.tracks] - Text tracks (captions, chapters, etc.)
|
|
458
|
-
*/
|
|
459
|
-
async load(config) {
|
|
460
|
-
try {
|
|
461
|
-
this.log('Loading new media:', config.src);
|
|
462
|
-
|
|
463
|
-
// Pause current playback
|
|
464
|
-
if (this.renderer) {
|
|
562
|
+
stop() {
|
|
465
563
|
this.pause();
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
// Clear existing text tracks
|
|
469
|
-
const existingTracks = this.element.querySelectorAll('track');
|
|
470
|
-
existingTracks.forEach(track => track.remove());
|
|
471
|
-
|
|
472
|
-
// Update media element
|
|
473
|
-
this.element.src = config.src;
|
|
474
|
-
|
|
475
|
-
if (config.type) {
|
|
476
|
-
this.element.type = config.type;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
if (config.poster && this.element.tagName === 'VIDEO') {
|
|
480
|
-
this.element.poster = config.poster;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Add new text tracks
|
|
484
|
-
if (config.tracks && config.tracks.length > 0) {
|
|
485
|
-
config.tracks.forEach(trackConfig => {
|
|
486
|
-
const track = document.createElement('track');
|
|
487
|
-
track.src = trackConfig.src;
|
|
488
|
-
track.kind = trackConfig.kind || 'captions';
|
|
489
|
-
track.srclang = trackConfig.srclang || 'en';
|
|
490
|
-
track.label = trackConfig.label || trackConfig.srclang;
|
|
491
|
-
|
|
492
|
-
if (trackConfig.default) {
|
|
493
|
-
track.default = true;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
this.element.appendChild(track);
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Check if we need to change renderer type
|
|
501
|
-
const shouldChangeRenderer = this.shouldChangeRenderer(config.src);
|
|
502
|
-
|
|
503
|
-
// Destroy old renderer if changing types
|
|
504
|
-
if (shouldChangeRenderer && this.renderer) {
|
|
505
|
-
this.renderer.destroy();
|
|
506
|
-
this.renderer = null;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Initialize or reinitialize renderer
|
|
510
|
-
if (!this.renderer || shouldChangeRenderer) {
|
|
511
|
-
await this.initializeRenderer();
|
|
512
|
-
} else {
|
|
513
|
-
// Just reload the current renderer with the updated element
|
|
514
|
-
this.renderer.media = this.element; // Update media reference
|
|
515
|
-
this.element.load();
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Reinitialize caption manager to pick up new tracks
|
|
519
|
-
if (this.captionManager) {
|
|
520
|
-
this.captionManager.destroy();
|
|
521
|
-
this.captionManager = new CaptionManager(this);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
this.emit('sourcechange', config);
|
|
525
|
-
this.log('Media loaded successfully');
|
|
526
|
-
|
|
527
|
-
} catch (error) {
|
|
528
|
-
this.handleError(error);
|
|
564
|
+
this.seek(0);
|
|
529
565
|
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/**
|
|
533
|
-
* Check if we need to change renderer type
|
|
534
|
-
* @param {string} src - New source URL
|
|
535
|
-
* @returns {boolean}
|
|
536
|
-
*/
|
|
537
|
-
shouldChangeRenderer(src) {
|
|
538
|
-
if (!this.renderer) return true;
|
|
539
|
-
|
|
540
|
-
const isYouTube = src.includes('youtube.com') || src.includes('youtu.be');
|
|
541
|
-
const isVimeo = src.includes('vimeo.com');
|
|
542
|
-
const isHLS = src.includes('.m3u8');
|
|
543
|
-
|
|
544
|
-
const currentRendererName = this.renderer.constructor.name;
|
|
545
|
-
|
|
546
|
-
if (isYouTube && currentRendererName !== 'YouTubeRenderer') return true;
|
|
547
|
-
if (isVimeo && currentRendererName !== 'VimeoRenderer') return true;
|
|
548
|
-
if (isHLS && currentRendererName !== 'HLSRenderer') return true;
|
|
549
|
-
if (!isYouTube && !isVimeo && !isHLS && currentRendererName !== 'HTML5Renderer') return true;
|
|
550
|
-
|
|
551
|
-
return false;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Playback controls
|
|
555
|
-
play() {
|
|
556
|
-
if (this.renderer) {
|
|
557
|
-
this.renderer.play();
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
566
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
567
|
+
toggle() {
|
|
568
|
+
if (this.state.playing) {
|
|
569
|
+
this.pause();
|
|
570
|
+
} else {
|
|
571
|
+
this.play();
|
|
572
|
+
}
|
|
564
573
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
toggle() {
|
|
573
|
-
if (this.state.playing) {
|
|
574
|
-
this.pause();
|
|
575
|
-
} else {
|
|
576
|
-
this.play();
|
|
574
|
+
|
|
575
|
+
seek(time) {
|
|
576
|
+
if (this.renderer) {
|
|
577
|
+
this.renderer.seek(time);
|
|
578
|
+
}
|
|
577
579
|
}
|
|
578
|
-
}
|
|
579
580
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
this.renderer.seek(time);
|
|
581
|
+
seekForward(interval = this.options.seekInterval) {
|
|
582
|
+
this.seek(Math.min(this.state.currentTime + interval, this.state.duration));
|
|
583
583
|
}
|
|
584
|
-
}
|
|
585
584
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
585
|
+
seekBackward(interval = this.options.seekInterval) {
|
|
586
|
+
this.seek(Math.max(this.state.currentTime - interval, 0));
|
|
587
|
+
}
|
|
589
588
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
589
|
+
// Volume controls
|
|
590
|
+
setVolume(volume) {
|
|
591
|
+
const newVolume = Math.max(0, Math.min(1, volume));
|
|
592
|
+
if (this.renderer) {
|
|
593
|
+
this.renderer.setVolume(newVolume);
|
|
594
|
+
}
|
|
595
|
+
this.state.volume = newVolume;
|
|
593
596
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (this.renderer) {
|
|
598
|
-
this.renderer.setVolume(newVolume);
|
|
599
|
-
}
|
|
600
|
-
this.state.volume = newVolume;
|
|
601
|
-
|
|
602
|
-
if (newVolume > 0 && this.state.muted) {
|
|
603
|
-
this.state.muted = false;
|
|
597
|
+
if (newVolume > 0 && this.state.muted) {
|
|
598
|
+
this.state.muted = false;
|
|
599
|
+
}
|
|
604
600
|
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
getVolume() {
|
|
608
|
-
return this.state.volume;
|
|
609
|
-
}
|
|
610
601
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
this.renderer.setMuted(true);
|
|
602
|
+
getVolume() {
|
|
603
|
+
return this.state.volume;
|
|
614
604
|
}
|
|
615
|
-
this.state.muted = true;
|
|
616
|
-
}
|
|
617
605
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
606
|
+
mute() {
|
|
607
|
+
if (this.renderer) {
|
|
608
|
+
this.renderer.setMuted(true);
|
|
609
|
+
}
|
|
610
|
+
this.state.muted = true;
|
|
611
|
+
this.emit('volumechange');
|
|
621
612
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
this.mute();
|
|
613
|
+
|
|
614
|
+
unmute() {
|
|
615
|
+
if (this.renderer) {
|
|
616
|
+
this.renderer.setMuted(false);
|
|
617
|
+
}
|
|
618
|
+
this.state.muted = false;
|
|
619
|
+
this.emit('volumechange');
|
|
630
620
|
}
|
|
631
|
-
}
|
|
632
621
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
622
|
+
toggleMute() {
|
|
623
|
+
if (this.state.muted) {
|
|
624
|
+
this.unmute();
|
|
625
|
+
} else {
|
|
626
|
+
this.mute();
|
|
627
|
+
}
|
|
638
628
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
enterFullscreen() {
|
|
649
|
-
const elem = this.container;
|
|
650
|
-
|
|
651
|
-
if (elem.requestFullscreen) {
|
|
652
|
-
elem.requestFullscreen();
|
|
653
|
-
} else if (elem.webkitRequestFullscreen) {
|
|
654
|
-
elem.webkitRequestFullscreen();
|
|
655
|
-
} else if (elem.mozRequestFullScreen) {
|
|
656
|
-
elem.mozRequestFullScreen();
|
|
657
|
-
} else if (elem.msRequestFullscreen) {
|
|
658
|
-
elem.msRequestFullscreen();
|
|
629
|
+
|
|
630
|
+
// Playback speed
|
|
631
|
+
setPlaybackSpeed(speed) {
|
|
632
|
+
const newSpeed = Math.max(0.25, Math.min(2, speed));
|
|
633
|
+
if (this.renderer) {
|
|
634
|
+
this.renderer.setPlaybackSpeed(newSpeed);
|
|
635
|
+
}
|
|
636
|
+
this.state.playbackSpeed = newSpeed;
|
|
637
|
+
this.emit('playbackspeedchange', newSpeed);
|
|
659
638
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
this.emit('fullscreenchange', true);
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
exitFullscreen() {
|
|
667
|
-
if (document.exitFullscreen) {
|
|
668
|
-
document.exitFullscreen();
|
|
669
|
-
} else if (document.webkitExitFullscreen) {
|
|
670
|
-
document.webkitExitFullscreen();
|
|
671
|
-
} else if (document.mozCancelFullScreen) {
|
|
672
|
-
document.mozCancelFullScreen();
|
|
673
|
-
} else if (document.msExitFullscreen) {
|
|
674
|
-
document.msExitFullscreen();
|
|
639
|
+
|
|
640
|
+
getPlaybackSpeed() {
|
|
641
|
+
return this.state.playbackSpeed;
|
|
675
642
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
643
|
+
|
|
644
|
+
// Fullscreen
|
|
645
|
+
enterFullscreen() {
|
|
646
|
+
const elem = this.container;
|
|
647
|
+
|
|
648
|
+
if (elem.requestFullscreen) {
|
|
649
|
+
elem.requestFullscreen();
|
|
650
|
+
} else if (elem.webkitRequestFullscreen) {
|
|
651
|
+
elem.webkitRequestFullscreen();
|
|
652
|
+
} else if (elem.mozRequestFullScreen) {
|
|
653
|
+
elem.mozRequestFullScreen();
|
|
654
|
+
} else if (elem.msRequestFullscreen) {
|
|
655
|
+
elem.msRequestFullscreen();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
this.state.fullscreen = true;
|
|
659
|
+
this.container.classList.add(`${this.options.classPrefix}-fullscreen`);
|
|
660
|
+
this.emit('fullscreenchange', true);
|
|
687
661
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
662
|
+
|
|
663
|
+
exitFullscreen() {
|
|
664
|
+
if (document.exitFullscreen) {
|
|
665
|
+
document.exitFullscreen();
|
|
666
|
+
} else if (document.webkitExitFullscreen) {
|
|
667
|
+
document.webkitExitFullscreen();
|
|
668
|
+
} else if (document.mozCancelFullScreen) {
|
|
669
|
+
document.mozCancelFullScreen();
|
|
670
|
+
} else if (document.msExitFullscreen) {
|
|
671
|
+
document.msExitFullscreen();
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
this.state.fullscreen = false;
|
|
675
|
+
this.container.classList.remove(`${this.options.classPrefix}-fullscreen`);
|
|
676
|
+
this.emit('fullscreenchange', false);
|
|
696
677
|
}
|
|
697
|
-
}
|
|
698
678
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
679
|
+
toggleFullscreen() {
|
|
680
|
+
if (this.state.fullscreen) {
|
|
681
|
+
this.exitFullscreen();
|
|
682
|
+
} else {
|
|
683
|
+
this.enterFullscreen();
|
|
684
|
+
}
|
|
704
685
|
}
|
|
705
|
-
}
|
|
706
686
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
687
|
+
// Picture-in-Picture
|
|
688
|
+
enterPiP() {
|
|
689
|
+
if (this.element.requestPictureInPicture) {
|
|
690
|
+
this.element.requestPictureInPicture();
|
|
691
|
+
this.state.pip = true;
|
|
692
|
+
this.emit('pipchange', true);
|
|
693
|
+
}
|
|
712
694
|
}
|
|
713
|
-
}
|
|
714
695
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
696
|
+
exitPiP() {
|
|
697
|
+
if (document.pictureInPictureElement) {
|
|
698
|
+
document.exitPictureInPicture();
|
|
699
|
+
this.state.pip = false;
|
|
700
|
+
this.emit('pipchange', false);
|
|
701
|
+
}
|
|
720
702
|
}
|
|
721
|
-
}
|
|
722
703
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
704
|
+
togglePiP() {
|
|
705
|
+
if (this.state.pip) {
|
|
706
|
+
this.exitPiP();
|
|
707
|
+
} else {
|
|
708
|
+
this.enterPiP();
|
|
709
|
+
}
|
|
727
710
|
}
|
|
728
|
-
}
|
|
729
711
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
712
|
+
// Captions
|
|
713
|
+
enableCaptions() {
|
|
714
|
+
if (this.captionManager) {
|
|
715
|
+
this.captionManager.enable();
|
|
716
|
+
this.state.captionsEnabled = true;
|
|
717
|
+
}
|
|
735
718
|
}
|
|
736
|
-
}
|
|
737
719
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
720
|
+
disableCaptions() {
|
|
721
|
+
if (this.captionManager) {
|
|
722
|
+
this.captionManager.disable();
|
|
723
|
+
this.state.captionsEnabled = false;
|
|
724
|
+
}
|
|
743
725
|
}
|
|
744
726
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
// Wait for new source to load
|
|
753
|
-
await new Promise((resolve) => {
|
|
754
|
-
const onLoadedMetadata = () => {
|
|
755
|
-
this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
|
|
756
|
-
resolve();
|
|
757
|
-
};
|
|
758
|
-
this.element.addEventListener('loadedmetadata', onLoadedMetadata);
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
// Restore playback position
|
|
762
|
-
this.seek(currentTime);
|
|
763
|
-
|
|
764
|
-
if (wasPlaying) {
|
|
765
|
-
this.play();
|
|
727
|
+
toggleCaptions() {
|
|
728
|
+
if (this.state.captionsEnabled) {
|
|
729
|
+
this.disableCaptions();
|
|
730
|
+
} else {
|
|
731
|
+
this.enableCaptions();
|
|
732
|
+
}
|
|
766
733
|
}
|
|
767
734
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
735
|
+
// Audio Description
|
|
736
|
+
async enableAudioDescription() {
|
|
737
|
+
if (!this.audioDescriptionSrc) {
|
|
738
|
+
console.warn('VidPly: No audio description source provided');
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
771
741
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
}
|
|
742
|
+
// Store current playback state
|
|
743
|
+
const currentTime = this.state.currentTime;
|
|
744
|
+
const wasPlaying = this.state.playing;
|
|
776
745
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
const wasPlaying = this.state.playing;
|
|
780
|
-
|
|
781
|
-
// Switch back to original version
|
|
782
|
-
this.element.src = this.originalSrc;
|
|
783
|
-
|
|
784
|
-
// Wait for new source to load
|
|
785
|
-
await new Promise((resolve) => {
|
|
786
|
-
const onLoadedMetadata = () => {
|
|
787
|
-
this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
|
|
788
|
-
resolve();
|
|
789
|
-
};
|
|
790
|
-
this.element.addEventListener('loadedmetadata', onLoadedMetadata);
|
|
791
|
-
});
|
|
792
|
-
|
|
793
|
-
// Restore playback position
|
|
794
|
-
this.seek(currentTime);
|
|
795
|
-
|
|
796
|
-
if (wasPlaying) {
|
|
797
|
-
this.play();
|
|
798
|
-
}
|
|
746
|
+
// Switch to audio-described version
|
|
747
|
+
this.element.src = this.audioDescriptionSrc;
|
|
799
748
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
749
|
+
// Wait for new source to load
|
|
750
|
+
await new Promise((resolve) => {
|
|
751
|
+
const onLoadedMetadata = () => {
|
|
752
|
+
this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
|
|
753
|
+
resolve();
|
|
754
|
+
};
|
|
755
|
+
this.element.addEventListener('loadedmetadata', onLoadedMetadata);
|
|
756
|
+
});
|
|
803
757
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
await this.disableAudioDescription();
|
|
807
|
-
} else {
|
|
808
|
-
await this.enableAudioDescription();
|
|
809
|
-
}
|
|
810
|
-
}
|
|
758
|
+
// Restore playback position
|
|
759
|
+
this.seek(currentTime);
|
|
811
760
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
console.warn('No sign language video source provided');
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
761
|
+
if (wasPlaying) {
|
|
762
|
+
this.play();
|
|
763
|
+
}
|
|
818
764
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
this.signLanguageVideo.style.display = 'block';
|
|
822
|
-
this.state.signLanguageEnabled = true;
|
|
823
|
-
this.emit('signlanguageenabled');
|
|
824
|
-
return;
|
|
765
|
+
this.state.audioDescriptionEnabled = true;
|
|
766
|
+
this.emit('audiodescriptionenabled');
|
|
825
767
|
}
|
|
826
768
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
this.signLanguageVideo.setAttribute('aria-label', i18n.t('player.signLanguage'));
|
|
832
|
-
|
|
833
|
-
// Set position based on options
|
|
834
|
-
const position = this.options.signLanguagePosition || 'bottom-right';
|
|
835
|
-
this.signLanguageVideo.classList.add(`vidply-sign-position-${position}`);
|
|
836
|
-
|
|
837
|
-
// Sync with main video
|
|
838
|
-
this.signLanguageVideo.muted = true; // Sign language video should be muted
|
|
839
|
-
this.signLanguageVideo.currentTime = this.state.currentTime;
|
|
840
|
-
if (!this.state.paused) {
|
|
841
|
-
this.signLanguageVideo.play();
|
|
842
|
-
}
|
|
769
|
+
async disableAudioDescription() {
|
|
770
|
+
if (!this.originalSrc) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
843
773
|
|
|
844
|
-
|
|
845
|
-
|
|
774
|
+
// Store current playback state
|
|
775
|
+
const currentTime = this.state.currentTime;
|
|
776
|
+
const wasPlaying = this.state.playing;
|
|
846
777
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
778
|
+
// Switch back to original version
|
|
779
|
+
this.element.src = this.originalSrc;
|
|
780
|
+
|
|
781
|
+
// Wait for new source to load
|
|
782
|
+
await new Promise((resolve) => {
|
|
783
|
+
const onLoadedMetadata = () => {
|
|
784
|
+
this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
|
|
785
|
+
resolve();
|
|
786
|
+
};
|
|
787
|
+
this.element.addEventListener('loadedmetadata', onLoadedMetadata);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
// Restore playback position
|
|
791
|
+
this.seek(currentTime);
|
|
792
|
+
|
|
793
|
+
if (wasPlaying) {
|
|
794
|
+
this.play();
|
|
852
795
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
796
|
+
|
|
797
|
+
this.state.audioDescriptionEnabled = false;
|
|
798
|
+
this.emit('audiodescriptiondisabled');
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
async toggleAudioDescription() {
|
|
802
|
+
if (this.state.audioDescriptionEnabled) {
|
|
803
|
+
await this.disableAudioDescription();
|
|
804
|
+
} else {
|
|
805
|
+
await this.enableAudioDescription();
|
|
857
806
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Sign Language
|
|
810
|
+
enableSignLanguage() {
|
|
811
|
+
if (!this.signLanguageSrc) {
|
|
812
|
+
console.warn('No sign language video source provided');
|
|
813
|
+
return;
|
|
862
814
|
}
|
|
863
|
-
|
|
864
|
-
ratechange: () => {
|
|
815
|
+
|
|
865
816
|
if (this.signLanguageVideo) {
|
|
866
|
-
|
|
817
|
+
// Already exists, just show it
|
|
818
|
+
this.signLanguageVideo.style.display = 'block';
|
|
819
|
+
this.state.signLanguageEnabled = true;
|
|
820
|
+
this.emit('signlanguageenabled');
|
|
821
|
+
return;
|
|
867
822
|
}
|
|
868
|
-
}
|
|
869
|
-
};
|
|
870
|
-
|
|
871
|
-
// Sync playback
|
|
872
|
-
this.on('play', this.signLanguageHandlers.play);
|
|
873
|
-
this.on('pause', this.signLanguageHandlers.pause);
|
|
874
|
-
this.on('timeupdate', this.signLanguageHandlers.timeupdate);
|
|
875
|
-
this.on('ratechange', this.signLanguageHandlers.ratechange);
|
|
876
|
-
|
|
877
|
-
this.state.signLanguageEnabled = true;
|
|
878
|
-
this.emit('signlanguageenabled');
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
disableSignLanguage() {
|
|
882
|
-
if (this.signLanguageVideo) {
|
|
883
|
-
this.signLanguageVideo.style.display = 'none';
|
|
884
|
-
}
|
|
885
|
-
this.state.signLanguageEnabled = false;
|
|
886
|
-
this.emit('signlanguagedisabled');
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
toggleSignLanguage() {
|
|
890
|
-
if (this.state.signLanguageEnabled) {
|
|
891
|
-
this.disableSignLanguage();
|
|
892
|
-
} else {
|
|
893
|
-
this.enableSignLanguage();
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
cleanupSignLanguage() {
|
|
898
|
-
// Remove event listeners
|
|
899
|
-
if (this.signLanguageHandlers) {
|
|
900
|
-
this.off('play', this.signLanguageHandlers.play);
|
|
901
|
-
this.off('pause', this.signLanguageHandlers.pause);
|
|
902
|
-
this.off('timeupdate', this.signLanguageHandlers.timeupdate);
|
|
903
|
-
this.off('ratechange', this.signLanguageHandlers.ratechange);
|
|
904
|
-
this.signLanguageHandlers = null;
|
|
905
|
-
}
|
|
906
823
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
824
|
+
// Create sign language video element
|
|
825
|
+
this.signLanguageVideo = document.createElement('video');
|
|
826
|
+
this.signLanguageVideo.className = 'vidply-sign-language-video';
|
|
827
|
+
this.signLanguageVideo.src = this.signLanguageSrc;
|
|
828
|
+
this.signLanguageVideo.setAttribute('aria-label', i18n.t('player.signLanguage'));
|
|
829
|
+
|
|
830
|
+
// Set position based on options
|
|
831
|
+
const position = this.options.signLanguagePosition || 'bottom-right';
|
|
832
|
+
this.signLanguageVideo.classList.add(`vidply-sign-position-${position}`);
|
|
833
|
+
|
|
834
|
+
// Sync with main video
|
|
835
|
+
this.signLanguageVideo.muted = true; // Sign language video should be muted
|
|
836
|
+
this.signLanguageVideo.currentTime = this.state.currentTime;
|
|
837
|
+
if (!this.state.paused) {
|
|
838
|
+
this.signLanguageVideo.play();
|
|
839
|
+
}
|
|
915
840
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
841
|
+
// Add to video wrapper (so it overlays the video, not the entire container)
|
|
842
|
+
this.videoWrapper.appendChild(this.signLanguageVideo);
|
|
843
|
+
|
|
844
|
+
// Create bound handlers to store references for cleanup
|
|
845
|
+
this.signLanguageHandlers = {
|
|
846
|
+
play: () => {
|
|
847
|
+
if (this.signLanguageVideo) {
|
|
848
|
+
this.signLanguageVideo.play();
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
pause: () => {
|
|
852
|
+
if (this.signLanguageVideo) {
|
|
853
|
+
this.signLanguageVideo.pause();
|
|
854
|
+
}
|
|
855
|
+
},
|
|
856
|
+
timeupdate: () => {
|
|
857
|
+
if (this.signLanguageVideo && Math.abs(this.signLanguageVideo.currentTime - this.state.currentTime) > 0.5) {
|
|
858
|
+
this.signLanguageVideo.currentTime = this.state.currentTime;
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
ratechange: () => {
|
|
862
|
+
if (this.signLanguageVideo) {
|
|
863
|
+
this.signLanguageVideo.playbackRate = this.state.playbackSpeed;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
// Sync playback
|
|
869
|
+
this.on('play', this.signLanguageHandlers.play);
|
|
870
|
+
this.on('pause', this.signLanguageHandlers.pause);
|
|
871
|
+
this.on('timeupdate', this.signLanguageHandlers.timeupdate);
|
|
872
|
+
this.on('ratechange', this.signLanguageHandlers.ratechange);
|
|
873
|
+
|
|
874
|
+
this.state.signLanguageEnabled = true;
|
|
875
|
+
this.emit('signlanguageenabled');
|
|
920
876
|
}
|
|
921
|
-
}
|
|
922
877
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
// Utility methods
|
|
930
|
-
getCurrentTime() {
|
|
931
|
-
return this.state.currentTime;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
getDuration() {
|
|
935
|
-
return this.state.duration;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
isPlaying() {
|
|
939
|
-
return this.state.playing;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
isPaused() {
|
|
943
|
-
return this.state.paused;
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
isEnded() {
|
|
947
|
-
return this.state.ended;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
isMuted() {
|
|
951
|
-
return this.state.muted;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
isFullscreen() {
|
|
955
|
-
return this.state.fullscreen;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// Error handling
|
|
959
|
-
handleError(error) {
|
|
960
|
-
this.log('Error:', error, 'error');
|
|
961
|
-
this.emit('error', error);
|
|
962
|
-
|
|
963
|
-
if (this.options.onError) {
|
|
964
|
-
this.options.onError.call(this, error);
|
|
878
|
+
disableSignLanguage() {
|
|
879
|
+
if (this.signLanguageVideo) {
|
|
880
|
+
this.signLanguageVideo.style.display = 'none';
|
|
881
|
+
}
|
|
882
|
+
this.state.signLanguageEnabled = false;
|
|
883
|
+
this.emit('signlanguagedisabled');
|
|
965
884
|
}
|
|
966
|
-
}
|
|
967
885
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// Setup responsive handlers
|
|
976
|
-
setupResponsiveHandlers() {
|
|
977
|
-
// Use ResizeObserver for efficient resize tracking
|
|
978
|
-
if (typeof ResizeObserver !== 'undefined') {
|
|
979
|
-
this.resizeObserver = new ResizeObserver((entries) => {
|
|
980
|
-
for (const entry of entries) {
|
|
981
|
-
const width = entry.contentRect.width;
|
|
982
|
-
|
|
983
|
-
// Update control bar for viewport
|
|
984
|
-
if (this.controlBar && typeof this.controlBar.updateControlsForViewport === 'function') {
|
|
985
|
-
this.controlBar.updateControlsForViewport(width);
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
// Update transcript positioning
|
|
989
|
-
if (this.transcriptManager && this.transcriptManager.isVisible) {
|
|
990
|
-
this.transcriptManager.positionTranscript();
|
|
991
|
-
}
|
|
886
|
+
toggleSignLanguage() {
|
|
887
|
+
if (this.state.signLanguageEnabled) {
|
|
888
|
+
this.disableSignLanguage();
|
|
889
|
+
} else {
|
|
890
|
+
this.enableSignLanguage();
|
|
992
891
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
cleanupSignLanguage() {
|
|
895
|
+
// Remove event listeners
|
|
896
|
+
if (this.signLanguageHandlers) {
|
|
897
|
+
this.off('play', this.signLanguageHandlers.play);
|
|
898
|
+
this.off('pause', this.signLanguageHandlers.pause);
|
|
899
|
+
this.off('timeupdate', this.signLanguageHandlers.timeupdate);
|
|
900
|
+
this.off('ratechange', this.signLanguageHandlers.ratechange);
|
|
901
|
+
this.signLanguageHandlers = null;
|
|
1003
902
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
903
|
+
|
|
904
|
+
// Remove video element
|
|
905
|
+
if (this.signLanguageVideo && this.signLanguageVideo.parentNode) {
|
|
906
|
+
this.signLanguageVideo.pause();
|
|
907
|
+
this.signLanguageVideo.src = '';
|
|
908
|
+
this.signLanguageVideo.parentNode.removeChild(this.signLanguageVideo);
|
|
909
|
+
this.signLanguageVideo = null;
|
|
1007
910
|
}
|
|
1008
|
-
};
|
|
1009
|
-
|
|
1010
|
-
window.addEventListener('resize', this.resizeHandler);
|
|
1011
911
|
}
|
|
1012
|
-
|
|
1013
|
-
//
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
}, 100);
|
|
1022
|
-
};
|
|
1023
|
-
|
|
1024
|
-
const orientationQuery = window.matchMedia('(orientation: portrait)');
|
|
1025
|
-
if (orientationQuery.addEventListener) {
|
|
1026
|
-
orientationQuery.addEventListener('change', this.orientationHandler);
|
|
1027
|
-
} else if (orientationQuery.addListener) {
|
|
1028
|
-
// Fallback for older browsers
|
|
1029
|
-
orientationQuery.addListener(this.orientationHandler);
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
this.orientationQuery = orientationQuery;
|
|
912
|
+
|
|
913
|
+
// Settings
|
|
914
|
+
// Settings dialog removed - using individual control buttons instead
|
|
915
|
+
showSettings() {
|
|
916
|
+
console.warn('[VidPly] Settings dialog has been removed. Use individual control buttons (speed, captions, etc.)');
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
hideSettings() {
|
|
920
|
+
// No-op - settings dialog removed
|
|
1033
921
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
this.log('Destroying player');
|
|
1039
|
-
|
|
1040
|
-
if (this.renderer) {
|
|
1041
|
-
this.renderer.destroy();
|
|
922
|
+
|
|
923
|
+
// Utility methods
|
|
924
|
+
getCurrentTime() {
|
|
925
|
+
return this.state.currentTime;
|
|
1042
926
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
927
|
+
|
|
928
|
+
getDuration() {
|
|
929
|
+
return this.state.duration;
|
|
1046
930
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
931
|
+
|
|
932
|
+
isPlaying() {
|
|
933
|
+
return this.state.playing;
|
|
1050
934
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
935
|
+
|
|
936
|
+
isPaused() {
|
|
937
|
+
return this.state.paused;
|
|
1054
938
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
939
|
+
|
|
940
|
+
isEnded() {
|
|
941
|
+
return this.state.ended;
|
|
1058
942
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
943
|
+
|
|
944
|
+
isMuted() {
|
|
945
|
+
return this.state.muted;
|
|
1062
946
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
// Cleanup play overlay button
|
|
1068
|
-
if (this.playButtonOverlay && this.playButtonOverlay.parentNode) {
|
|
1069
|
-
this.playButtonOverlay.remove();
|
|
1070
|
-
this.playButtonOverlay = null;
|
|
947
|
+
|
|
948
|
+
isFullscreen() {
|
|
949
|
+
return this.state.fullscreen;
|
|
1071
950
|
}
|
|
1072
|
-
|
|
1073
|
-
//
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
951
|
+
|
|
952
|
+
// Error handling
|
|
953
|
+
handleError(error) {
|
|
954
|
+
this.log('Error:', error, 'error');
|
|
955
|
+
this.emit('error', error);
|
|
956
|
+
|
|
957
|
+
if (this.options.onError) {
|
|
958
|
+
this.options.onError.call(this, error);
|
|
959
|
+
}
|
|
1077
960
|
}
|
|
1078
|
-
|
|
1079
|
-
//
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
961
|
+
|
|
962
|
+
// Logging
|
|
963
|
+
log(message, type = 'log') {
|
|
964
|
+
if (this.options.debug) {
|
|
965
|
+
console[type](`[VidPly]`, message);
|
|
966
|
+
}
|
|
1083
967
|
}
|
|
1084
|
-
|
|
1085
|
-
//
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
968
|
+
|
|
969
|
+
// Setup responsive handlers
|
|
970
|
+
setupResponsiveHandlers() {
|
|
971
|
+
// Use ResizeObserver for efficient resize tracking
|
|
972
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
973
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
974
|
+
for (const entry of entries) {
|
|
975
|
+
const width = entry.contentRect.width;
|
|
976
|
+
|
|
977
|
+
// Update control bar for viewport
|
|
978
|
+
if (this.controlBar && typeof this.controlBar.updateControlsForViewport === 'function') {
|
|
979
|
+
this.controlBar.updateControlsForViewport(width);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Update transcript positioning
|
|
983
|
+
if (this.transcriptManager && this.transcriptManager.isVisible) {
|
|
984
|
+
this.transcriptManager.positionTranscript();
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
this.resizeObserver.observe(this.container);
|
|
990
|
+
} else {
|
|
991
|
+
// Fallback to window resize event
|
|
992
|
+
this.resizeHandler = () => {
|
|
993
|
+
const width = this.container.clientWidth;
|
|
994
|
+
|
|
995
|
+
if (this.controlBar && typeof this.controlBar.updateControlsForViewport === 'function') {
|
|
996
|
+
this.controlBar.updateControlsForViewport(width);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (this.transcriptManager && this.transcriptManager.isVisible) {
|
|
1000
|
+
this.transcriptManager.positionTranscript();
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
window.addEventListener('resize', this.resizeHandler);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Also listen for orientation changes on mobile
|
|
1008
|
+
if (window.matchMedia) {
|
|
1009
|
+
this.orientationHandler = (e) => {
|
|
1010
|
+
// Wait for layout to settle
|
|
1011
|
+
setTimeout(() => {
|
|
1012
|
+
if (this.transcriptManager && this.transcriptManager.isVisible) {
|
|
1013
|
+
this.transcriptManager.positionTranscript();
|
|
1014
|
+
}
|
|
1015
|
+
}, 100);
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
const orientationQuery = window.matchMedia('(orientation: portrait)');
|
|
1019
|
+
if (orientationQuery.addEventListener) {
|
|
1020
|
+
orientationQuery.addEventListener('change', this.orientationHandler);
|
|
1021
|
+
} else if (orientationQuery.addListener) {
|
|
1022
|
+
// Fallback for older browsers
|
|
1023
|
+
orientationQuery.addListener(this.orientationHandler);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
this.orientationQuery = orientationQuery;
|
|
1027
|
+
}
|
|
1094
1028
|
}
|
|
1095
|
-
|
|
1096
|
-
//
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1029
|
+
|
|
1030
|
+
// Cleanup
|
|
1031
|
+
destroy() {
|
|
1032
|
+
this.log('Destroying player');
|
|
1033
|
+
|
|
1034
|
+
if (this.renderer) {
|
|
1035
|
+
this.renderer.destroy();
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
if (this.controlBar) {
|
|
1039
|
+
this.controlBar.destroy();
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (this.captionManager) {
|
|
1043
|
+
this.captionManager.destroy();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
if (this.keyboardManager) {
|
|
1047
|
+
this.keyboardManager.destroy();
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (this.transcriptManager) {
|
|
1051
|
+
this.transcriptManager.destroy();
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Cleanup sign language video and listeners
|
|
1055
|
+
this.cleanupSignLanguage();
|
|
1056
|
+
|
|
1057
|
+
// Cleanup play overlay button
|
|
1058
|
+
if (this.playButtonOverlay && this.playButtonOverlay.parentNode) {
|
|
1059
|
+
this.playButtonOverlay.remove();
|
|
1060
|
+
this.playButtonOverlay = null;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Cleanup resize observer
|
|
1064
|
+
if (this.resizeObserver) {
|
|
1065
|
+
this.resizeObserver.disconnect();
|
|
1066
|
+
this.resizeObserver = null;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Cleanup window resize handler
|
|
1070
|
+
if (this.resizeHandler) {
|
|
1071
|
+
window.removeEventListener('resize', this.resizeHandler);
|
|
1072
|
+
this.resizeHandler = null;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Cleanup orientation change handler
|
|
1076
|
+
if (this.orientationQuery && this.orientationHandler) {
|
|
1077
|
+
if (this.orientationQuery.removeEventListener) {
|
|
1078
|
+
this.orientationQuery.removeEventListener('change', this.orientationHandler);
|
|
1079
|
+
} else if (this.orientationQuery.removeListener) {
|
|
1080
|
+
this.orientationQuery.removeListener(this.orientationHandler);
|
|
1081
|
+
}
|
|
1082
|
+
this.orientationQuery = null;
|
|
1083
|
+
this.orientationHandler = null;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Remove container
|
|
1087
|
+
if (this.container && this.container.parentNode) {
|
|
1088
|
+
this.container.parentNode.insertBefore(this.element, this.container);
|
|
1089
|
+
this.container.parentNode.removeChild(this.container);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
this.removeAllListeners();
|
|
1100
1093
|
}
|
|
1101
|
-
|
|
1102
|
-
this.removeAllListeners();
|
|
1103
|
-
}
|
|
1104
1094
|
}
|
|
1105
1095
|
|
|
1106
1096
|
// Static instances tracker for pause others functionality
|