vidply 1.0.2 → 1.0.4
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.css +24 -0
- package/dist/vidply.esm.js +374 -396
- package/dist/vidply.esm.js.map +3 -3
- package/dist/vidply.esm.min.js +7 -6
- package/dist/vidply.esm.min.meta.json +11 -45
- package/dist/vidply.js +374 -396
- package/dist/vidply.js.map +3 -3
- package/dist/vidply.min.css +1 -1
- package/dist/vidply.min.js +7 -6
- package/dist/vidply.min.meta.json +11 -45
- package/package.json +57 -54
- package/src/controls/CaptionManager.js +216 -218
- package/src/controls/ControlBar.js +2026 -1870
- package/src/controls/KeyboardManager.js +50 -13
- package/src/core/Player.js +1017 -991
- package/src/features/PlaylistManager.js +611 -437
- package/src/styles/vidply.css +24 -0
package/src/core/Player.js
CHANGED
|
@@ -3,1104 +3,1130 @@
|
|
|
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
|
-
|
|
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();
|
|
292
205
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
+
}
|
|
305
288
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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';
|
|
315
315
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
316
|
+
|
|
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
|
+
});
|
|
340
385
|
}
|
|
341
386
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
});
|
|
387
|
+
createPlayButtonOverlay() {
|
|
388
|
+
// Create complete SVG play button from Icons.js
|
|
389
|
+
this.playButtonOverlay = createPlayOverlay();
|
|
346
390
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
+
});
|
|
364
414
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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();
|
|
370
444
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
+
// Reinitialize transcript manager to pick up new tracks
|
|
520
|
+
if (this.transcriptManager) {
|
|
521
|
+
const wasVisible = this.transcriptManager.isVisible;
|
|
522
|
+
this.transcriptManager.destroy();
|
|
523
|
+
this.transcriptManager = new TranscriptManager(this);
|
|
524
|
+
|
|
525
|
+
// Restore visibility state if transcript was open
|
|
526
|
+
if (wasVisible) {
|
|
527
|
+
this.transcriptManager.showTranscript();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Update control bar to show/hide feature buttons based on new tracks
|
|
532
|
+
if (this.controlBar) {
|
|
533
|
+
this.updateControlBar();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
this.emit('sourcechange', config);
|
|
537
|
+
this.log('Media loaded successfully');
|
|
538
|
+
|
|
539
|
+
} catch (error) {
|
|
540
|
+
this.handleError(error);
|
|
541
|
+
}
|
|
375
542
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Check if we need to change renderer type
|
|
546
|
+
* @param {string} src - New source URL
|
|
547
|
+
* @returns {boolean}
|
|
548
|
+
*/
|
|
549
|
+
/**
|
|
550
|
+
* Update control bar to refresh button visibility based on available features
|
|
551
|
+
*/
|
|
552
|
+
updateControlBar() {
|
|
553
|
+
if (!this.controlBar) return;
|
|
554
|
+
|
|
555
|
+
const controlBar = this.controlBar;
|
|
556
|
+
|
|
557
|
+
// Clear existing controls content
|
|
558
|
+
controlBar.element.innerHTML = '';
|
|
559
|
+
|
|
560
|
+
// Recreate controls with updated feature detection
|
|
561
|
+
controlBar.createControls();
|
|
562
|
+
|
|
563
|
+
// Reattach events for the new controls
|
|
564
|
+
controlBar.attachEvents();
|
|
565
|
+
controlBar.setupAutoHide();
|
|
380
566
|
}
|
|
381
567
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
this.playButtonOverlay.addEventListener('click', () => {
|
|
398
|
-
this.toggle();
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
// Add to video wrapper
|
|
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');
|
|
568
|
+
shouldChangeRenderer(src) {
|
|
569
|
+
if (!this.renderer) return true;
|
|
570
|
+
|
|
571
|
+
const isYouTube = src.includes('youtube.com') || src.includes('youtu.be');
|
|
572
|
+
const isVimeo = src.includes('vimeo.com');
|
|
573
|
+
const isHLS = src.includes('.m3u8');
|
|
574
|
+
|
|
575
|
+
const currentRendererName = this.renderer.constructor.name;
|
|
576
|
+
|
|
577
|
+
if (isYouTube && currentRendererName !== 'YouTubeRenderer') return true;
|
|
578
|
+
if (isVimeo && currentRendererName !== 'VimeoRenderer') return true;
|
|
579
|
+
if (isHLS && currentRendererName !== 'HLSRenderer') return true;
|
|
580
|
+
if (!isYouTube && !isVimeo && !isHLS && currentRendererName !== 'HTML5Renderer') return true;
|
|
581
|
+
|
|
582
|
+
return false;
|
|
426
583
|
}
|
|
427
584
|
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
|
|
585
|
+
// Playback controls
|
|
586
|
+
play() {
|
|
587
|
+
if (this.renderer) {
|
|
588
|
+
this.renderer.play();
|
|
589
|
+
}
|
|
431
590
|
}
|
|
432
591
|
|
|
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;
|
|
592
|
+
pause() {
|
|
593
|
+
if (this.renderer) {
|
|
594
|
+
this.renderer.pause();
|
|
595
|
+
}
|
|
444
596
|
}
|
|
445
597
|
|
|
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) {
|
|
598
|
+
stop() {
|
|
465
599
|
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);
|
|
600
|
+
this.seek(0);
|
|
529
601
|
}
|
|
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
602
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
603
|
+
toggle() {
|
|
604
|
+
if (this.state.playing) {
|
|
605
|
+
this.pause();
|
|
606
|
+
} else {
|
|
607
|
+
this.play();
|
|
608
|
+
}
|
|
558
609
|
}
|
|
559
|
-
}
|
|
560
610
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
stop() {
|
|
568
|
-
this.pause();
|
|
569
|
-
this.seek(0);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
toggle() {
|
|
573
|
-
if (this.state.playing) {
|
|
574
|
-
this.pause();
|
|
575
|
-
} else {
|
|
576
|
-
this.play();
|
|
611
|
+
seek(time) {
|
|
612
|
+
if (this.renderer) {
|
|
613
|
+
this.renderer.seek(time);
|
|
614
|
+
}
|
|
577
615
|
}
|
|
578
|
-
}
|
|
579
616
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
this.renderer.seek(time);
|
|
617
|
+
seekForward(interval = this.options.seekInterval) {
|
|
618
|
+
this.seek(Math.min(this.state.currentTime + interval, this.state.duration));
|
|
583
619
|
}
|
|
584
|
-
}
|
|
585
620
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
621
|
+
seekBackward(interval = this.options.seekInterval) {
|
|
622
|
+
this.seek(Math.max(this.state.currentTime - interval, 0));
|
|
623
|
+
}
|
|
589
624
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
625
|
+
// Volume controls
|
|
626
|
+
setVolume(volume) {
|
|
627
|
+
const newVolume = Math.max(0, Math.min(1, volume));
|
|
628
|
+
if (this.renderer) {
|
|
629
|
+
this.renderer.setVolume(newVolume);
|
|
630
|
+
}
|
|
631
|
+
this.state.volume = newVolume;
|
|
593
632
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (this.renderer) {
|
|
598
|
-
this.renderer.setVolume(newVolume);
|
|
633
|
+
if (newVolume > 0 && this.state.muted) {
|
|
634
|
+
this.state.muted = false;
|
|
635
|
+
}
|
|
599
636
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
this.state.muted = false;
|
|
637
|
+
|
|
638
|
+
getVolume() {
|
|
639
|
+
return this.state.volume;
|
|
604
640
|
}
|
|
605
|
-
}
|
|
606
641
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
642
|
+
mute() {
|
|
643
|
+
if (this.renderer) {
|
|
644
|
+
this.renderer.setMuted(true);
|
|
645
|
+
}
|
|
646
|
+
this.state.muted = true;
|
|
647
|
+
this.emit('volumechange');
|
|
648
|
+
}
|
|
610
649
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
650
|
+
unmute() {
|
|
651
|
+
if (this.renderer) {
|
|
652
|
+
this.renderer.setMuted(false);
|
|
653
|
+
}
|
|
654
|
+
this.state.muted = false;
|
|
655
|
+
this.emit('volumechange');
|
|
614
656
|
}
|
|
615
|
-
this.state.muted = true;
|
|
616
|
-
}
|
|
617
657
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
658
|
+
toggleMute() {
|
|
659
|
+
if (this.state.muted) {
|
|
660
|
+
this.unmute();
|
|
661
|
+
} else {
|
|
662
|
+
this.mute();
|
|
663
|
+
}
|
|
621
664
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
665
|
+
|
|
666
|
+
// Playback speed
|
|
667
|
+
setPlaybackSpeed(speed) {
|
|
668
|
+
const newSpeed = Math.max(0.25, Math.min(2, speed));
|
|
669
|
+
if (this.renderer) {
|
|
670
|
+
this.renderer.setPlaybackSpeed(newSpeed);
|
|
671
|
+
}
|
|
672
|
+
this.state.playbackSpeed = newSpeed;
|
|
673
|
+
this.emit('playbackspeedchange', newSpeed);
|
|
630
674
|
}
|
|
631
|
-
}
|
|
632
675
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const newSpeed = Math.max(0.25, Math.min(2, speed));
|
|
636
|
-
if (this.renderer) {
|
|
637
|
-
this.renderer.setPlaybackSpeed(newSpeed);
|
|
676
|
+
getPlaybackSpeed() {
|
|
677
|
+
return this.state.playbackSpeed;
|
|
638
678
|
}
|
|
639
|
-
this.state.playbackSpeed = newSpeed;
|
|
640
|
-
this.emit('playbackspeedchange', newSpeed);
|
|
641
|
-
}
|
|
642
679
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
680
|
+
// Fullscreen
|
|
681
|
+
enterFullscreen() {
|
|
682
|
+
const elem = this.container;
|
|
683
|
+
|
|
684
|
+
if (elem.requestFullscreen) {
|
|
685
|
+
elem.requestFullscreen();
|
|
686
|
+
} else if (elem.webkitRequestFullscreen) {
|
|
687
|
+
elem.webkitRequestFullscreen();
|
|
688
|
+
} else if (elem.mozRequestFullScreen) {
|
|
689
|
+
elem.mozRequestFullScreen();
|
|
690
|
+
} else if (elem.msRequestFullscreen) {
|
|
691
|
+
elem.msRequestFullscreen();
|
|
692
|
+
}
|
|
646
693
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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();
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
this.state.fullscreen = true;
|
|
662
|
-
this.container.classList.add(`${this.options.classPrefix}-fullscreen`);
|
|
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();
|
|
694
|
+
this.state.fullscreen = true;
|
|
695
|
+
this.container.classList.add(`${this.options.classPrefix}-fullscreen`);
|
|
696
|
+
this.emit('fullscreenchange', true);
|
|
675
697
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
698
|
+
|
|
699
|
+
exitFullscreen() {
|
|
700
|
+
if (document.exitFullscreen) {
|
|
701
|
+
document.exitFullscreen();
|
|
702
|
+
} else if (document.webkitExitFullscreen) {
|
|
703
|
+
document.webkitExitFullscreen();
|
|
704
|
+
} else if (document.mozCancelFullScreen) {
|
|
705
|
+
document.mozCancelFullScreen();
|
|
706
|
+
} else if (document.msExitFullscreen) {
|
|
707
|
+
document.msExitFullscreen();
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
this.state.fullscreen = false;
|
|
711
|
+
this.container.classList.remove(`${this.options.classPrefix}-fullscreen`);
|
|
712
|
+
this.emit('fullscreenchange', false);
|
|
687
713
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
this.emit('pipchange', true);
|
|
714
|
+
|
|
715
|
+
toggleFullscreen() {
|
|
716
|
+
if (this.state.fullscreen) {
|
|
717
|
+
this.exitFullscreen();
|
|
718
|
+
} else {
|
|
719
|
+
this.enterFullscreen();
|
|
720
|
+
}
|
|
696
721
|
}
|
|
697
|
-
}
|
|
698
722
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
723
|
+
// Picture-in-Picture
|
|
724
|
+
enterPiP() {
|
|
725
|
+
if (this.element.requestPictureInPicture) {
|
|
726
|
+
this.element.requestPictureInPicture();
|
|
727
|
+
this.state.pip = true;
|
|
728
|
+
this.emit('pipchange', true);
|
|
729
|
+
}
|
|
704
730
|
}
|
|
705
|
-
}
|
|
706
731
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
732
|
+
exitPiP() {
|
|
733
|
+
if (document.pictureInPictureElement) {
|
|
734
|
+
document.exitPictureInPicture();
|
|
735
|
+
this.state.pip = false;
|
|
736
|
+
this.emit('pipchange', false);
|
|
737
|
+
}
|
|
712
738
|
}
|
|
713
|
-
}
|
|
714
739
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
740
|
+
togglePiP() {
|
|
741
|
+
if (this.state.pip) {
|
|
742
|
+
this.exitPiP();
|
|
743
|
+
} else {
|
|
744
|
+
this.enterPiP();
|
|
745
|
+
}
|
|
720
746
|
}
|
|
721
|
-
}
|
|
722
747
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
748
|
+
// Captions
|
|
749
|
+
enableCaptions() {
|
|
750
|
+
if (this.captionManager) {
|
|
751
|
+
this.captionManager.enable();
|
|
752
|
+
this.state.captionsEnabled = true;
|
|
753
|
+
}
|
|
727
754
|
}
|
|
728
|
-
}
|
|
729
755
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
756
|
+
disableCaptions() {
|
|
757
|
+
if (this.captionManager) {
|
|
758
|
+
this.captionManager.disable();
|
|
759
|
+
this.state.captionsEnabled = false;
|
|
760
|
+
}
|
|
735
761
|
}
|
|
736
|
-
}
|
|
737
762
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
763
|
+
toggleCaptions() {
|
|
764
|
+
if (this.state.captionsEnabled) {
|
|
765
|
+
this.disableCaptions();
|
|
766
|
+
} else {
|
|
767
|
+
this.enableCaptions();
|
|
768
|
+
}
|
|
743
769
|
}
|
|
744
770
|
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
|
|
771
|
+
// Audio Description
|
|
772
|
+
async enableAudioDescription() {
|
|
773
|
+
if (!this.audioDescriptionSrc) {
|
|
774
|
+
console.warn('VidPly: No audio description source provided');
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
748
777
|
|
|
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();
|
|
766
|
-
}
|
|
778
|
+
// Store current playback state
|
|
779
|
+
const currentTime = this.state.currentTime;
|
|
780
|
+
const wasPlaying = this.state.playing;
|
|
767
781
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
}
|
|
782
|
+
// Switch to audio-described version
|
|
783
|
+
this.element.src = this.audioDescriptionSrc;
|
|
771
784
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
785
|
+
// Wait for new source to load
|
|
786
|
+
await new Promise((resolve) => {
|
|
787
|
+
const onLoadedMetadata = () => {
|
|
788
|
+
this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
|
|
789
|
+
resolve();
|
|
790
|
+
};
|
|
791
|
+
this.element.addEventListener('loadedmetadata', onLoadedMetadata);
|
|
792
|
+
});
|
|
776
793
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
const wasPlaying = this.state.playing;
|
|
794
|
+
// Restore playback position
|
|
795
|
+
this.seek(currentTime);
|
|
780
796
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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();
|
|
797
|
+
if (wasPlaying) {
|
|
798
|
+
this.play();
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
this.state.audioDescriptionEnabled = true;
|
|
802
|
+
this.emit('audiodescriptionenabled');
|
|
798
803
|
}
|
|
799
804
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
805
|
+
async disableAudioDescription() {
|
|
806
|
+
if (!this.originalSrc) {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
803
809
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
} else {
|
|
808
|
-
await this.enableAudioDescription();
|
|
809
|
-
}
|
|
810
|
-
}
|
|
810
|
+
// Store current playback state
|
|
811
|
+
const currentTime = this.state.currentTime;
|
|
812
|
+
const wasPlaying = this.state.playing;
|
|
811
813
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
if (!this.signLanguageSrc) {
|
|
815
|
-
console.warn('No sign language video source provided');
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
814
|
+
// Switch back to original version
|
|
815
|
+
this.element.src = this.originalSrc;
|
|
818
816
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
817
|
+
// Wait for new source to load
|
|
818
|
+
await new Promise((resolve) => {
|
|
819
|
+
const onLoadedMetadata = () => {
|
|
820
|
+
this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
|
|
821
|
+
resolve();
|
|
822
|
+
};
|
|
823
|
+
this.element.addEventListener('loadedmetadata', onLoadedMetadata);
|
|
824
|
+
});
|
|
826
825
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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();
|
|
826
|
+
// Restore playback position
|
|
827
|
+
this.seek(currentTime);
|
|
828
|
+
|
|
829
|
+
if (wasPlaying) {
|
|
830
|
+
this.play();
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
this.state.audioDescriptionEnabled = false;
|
|
834
|
+
this.emit('audiodescriptiondisabled');
|
|
842
835
|
}
|
|
843
836
|
|
|
844
|
-
|
|
845
|
-
|
|
837
|
+
async toggleAudioDescription() {
|
|
838
|
+
if (this.state.audioDescriptionEnabled) {
|
|
839
|
+
await this.disableAudioDescription();
|
|
840
|
+
} else {
|
|
841
|
+
await this.enableAudioDescription();
|
|
842
|
+
}
|
|
843
|
+
}
|
|
846
844
|
|
|
847
|
-
//
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
845
|
+
// Sign Language
|
|
846
|
+
enableSignLanguage() {
|
|
847
|
+
if (!this.signLanguageSrc) {
|
|
848
|
+
console.warn('No sign language video source provided');
|
|
849
|
+
return;
|
|
852
850
|
}
|
|
853
|
-
|
|
854
|
-
pause: () => {
|
|
851
|
+
|
|
855
852
|
if (this.signLanguageVideo) {
|
|
856
|
-
|
|
853
|
+
// Already exists, just show it
|
|
854
|
+
this.signLanguageVideo.style.display = 'block';
|
|
855
|
+
this.state.signLanguageEnabled = true;
|
|
856
|
+
this.emit('signlanguageenabled');
|
|
857
|
+
return;
|
|
857
858
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
859
|
+
|
|
860
|
+
// Create sign language video element
|
|
861
|
+
this.signLanguageVideo = document.createElement('video');
|
|
862
|
+
this.signLanguageVideo.className = 'vidply-sign-language-video';
|
|
863
|
+
this.signLanguageVideo.src = this.signLanguageSrc;
|
|
864
|
+
this.signLanguageVideo.setAttribute('aria-label', i18n.t('player.signLanguage'));
|
|
865
|
+
|
|
866
|
+
// Set position based on options
|
|
867
|
+
const position = this.options.signLanguagePosition || 'bottom-right';
|
|
868
|
+
this.signLanguageVideo.classList.add(`vidply-sign-position-${position}`);
|
|
869
|
+
|
|
870
|
+
// Sync with main video
|
|
871
|
+
this.signLanguageVideo.muted = true; // Sign language video should be muted
|
|
872
|
+
this.signLanguageVideo.currentTime = this.state.currentTime;
|
|
873
|
+
if (!this.state.paused) {
|
|
874
|
+
this.signLanguageVideo.play();
|
|
862
875
|
}
|
|
863
|
-
|
|
864
|
-
|
|
876
|
+
|
|
877
|
+
// Add to video wrapper (so it overlays the video, not the entire container)
|
|
878
|
+
this.videoWrapper.appendChild(this.signLanguageVideo);
|
|
879
|
+
|
|
880
|
+
// Create bound handlers to store references for cleanup
|
|
881
|
+
this.signLanguageHandlers = {
|
|
882
|
+
play: () => {
|
|
883
|
+
if (this.signLanguageVideo) {
|
|
884
|
+
this.signLanguageVideo.play();
|
|
885
|
+
}
|
|
886
|
+
},
|
|
887
|
+
pause: () => {
|
|
888
|
+
if (this.signLanguageVideo) {
|
|
889
|
+
this.signLanguageVideo.pause();
|
|
890
|
+
}
|
|
891
|
+
},
|
|
892
|
+
timeupdate: () => {
|
|
893
|
+
if (this.signLanguageVideo && Math.abs(this.signLanguageVideo.currentTime - this.state.currentTime) > 0.5) {
|
|
894
|
+
this.signLanguageVideo.currentTime = this.state.currentTime;
|
|
895
|
+
}
|
|
896
|
+
},
|
|
897
|
+
ratechange: () => {
|
|
898
|
+
if (this.signLanguageVideo) {
|
|
899
|
+
this.signLanguageVideo.playbackRate = this.state.playbackSpeed;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
// Sync playback
|
|
905
|
+
this.on('play', this.signLanguageHandlers.play);
|
|
906
|
+
this.on('pause', this.signLanguageHandlers.pause);
|
|
907
|
+
this.on('timeupdate', this.signLanguageHandlers.timeupdate);
|
|
908
|
+
this.on('ratechange', this.signLanguageHandlers.ratechange);
|
|
909
|
+
|
|
910
|
+
this.state.signLanguageEnabled = true;
|
|
911
|
+
this.emit('signlanguageenabled');
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
disableSignLanguage() {
|
|
865
915
|
if (this.signLanguageVideo) {
|
|
866
|
-
|
|
916
|
+
this.signLanguageVideo.style.display = 'none';
|
|
867
917
|
}
|
|
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';
|
|
918
|
+
this.state.signLanguageEnabled = false;
|
|
919
|
+
this.emit('signlanguagedisabled');
|
|
884
920
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
} else {
|
|
893
|
-
this.enableSignLanguage();
|
|
921
|
+
|
|
922
|
+
toggleSignLanguage() {
|
|
923
|
+
if (this.state.signLanguageEnabled) {
|
|
924
|
+
this.disableSignLanguage();
|
|
925
|
+
} else {
|
|
926
|
+
this.enableSignLanguage();
|
|
927
|
+
}
|
|
894
928
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
929
|
+
|
|
930
|
+
cleanupSignLanguage() {
|
|
931
|
+
// Remove event listeners
|
|
932
|
+
if (this.signLanguageHandlers) {
|
|
933
|
+
this.off('play', this.signLanguageHandlers.play);
|
|
934
|
+
this.off('pause', this.signLanguageHandlers.pause);
|
|
935
|
+
this.off('timeupdate', this.signLanguageHandlers.timeupdate);
|
|
936
|
+
this.off('ratechange', this.signLanguageHandlers.ratechange);
|
|
937
|
+
this.signLanguageHandlers = null;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Remove video element
|
|
941
|
+
if (this.signLanguageVideo && this.signLanguageVideo.parentNode) {
|
|
942
|
+
this.signLanguageVideo.pause();
|
|
943
|
+
this.signLanguageVideo.src = '';
|
|
944
|
+
this.signLanguageVideo.parentNode.removeChild(this.signLanguageVideo);
|
|
945
|
+
this.signLanguageVideo = null;
|
|
946
|
+
}
|
|
905
947
|
}
|
|
906
948
|
|
|
907
|
-
//
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
this.signLanguageVideo.parentNode.removeChild(this.signLanguageVideo);
|
|
912
|
-
this.signLanguageVideo = null;
|
|
949
|
+
// Settings
|
|
950
|
+
// Settings dialog removed - using individual control buttons instead
|
|
951
|
+
showSettings() {
|
|
952
|
+
console.warn('[VidPly] Settings dialog has been removed. Use individual control buttons (speed, captions, etc.)');
|
|
913
953
|
}
|
|
914
|
-
}
|
|
915
954
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
if (this.settingsDialog) {
|
|
919
|
-
this.settingsDialog.show();
|
|
955
|
+
hideSettings() {
|
|
956
|
+
// No-op - settings dialog removed
|
|
920
957
|
}
|
|
921
|
-
}
|
|
922
958
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
959
|
+
// Utility methods
|
|
960
|
+
getCurrentTime() {
|
|
961
|
+
return this.state.currentTime;
|
|
926
962
|
}
|
|
927
|
-
}
|
|
928
963
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
}
|
|
964
|
+
getDuration() {
|
|
965
|
+
return this.state.duration;
|
|
966
|
+
}
|
|
933
967
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
968
|
+
isPlaying() {
|
|
969
|
+
return this.state.playing;
|
|
970
|
+
}
|
|
937
971
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
972
|
+
isPaused() {
|
|
973
|
+
return this.state.paused;
|
|
974
|
+
}
|
|
941
975
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
976
|
+
isEnded() {
|
|
977
|
+
return this.state.ended;
|
|
978
|
+
}
|
|
945
979
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
980
|
+
isMuted() {
|
|
981
|
+
return this.state.muted;
|
|
982
|
+
}
|
|
949
983
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
984
|
+
isFullscreen() {
|
|
985
|
+
return this.state.fullscreen;
|
|
986
|
+
}
|
|
953
987
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
988
|
+
// Error handling
|
|
989
|
+
handleError(error) {
|
|
990
|
+
this.log('Error:', error, 'error');
|
|
991
|
+
this.emit('error', error);
|
|
957
992
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
this.emit('error', error);
|
|
962
|
-
|
|
963
|
-
if (this.options.onError) {
|
|
964
|
-
this.options.onError.call(this, error);
|
|
993
|
+
if (this.options.onError) {
|
|
994
|
+
this.options.onError.call(this, error);
|
|
995
|
+
}
|
|
965
996
|
}
|
|
966
|
-
}
|
|
967
997
|
|
|
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
|
-
}
|
|
998
|
+
// Logging
|
|
999
|
+
log(message, type = 'log') {
|
|
1000
|
+
if (this.options.debug) {
|
|
1001
|
+
console[type](`[VidPly]`, message);
|
|
992
1002
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Setup responsive handlers
|
|
1006
|
+
setupResponsiveHandlers() {
|
|
1007
|
+
// Use ResizeObserver for efficient resize tracking
|
|
1008
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
1009
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
1010
|
+
for (const entry of entries) {
|
|
1011
|
+
const width = entry.contentRect.width;
|
|
1012
|
+
|
|
1013
|
+
// Update control bar for viewport
|
|
1014
|
+
if (this.controlBar && typeof this.controlBar.updateControlsForViewport === 'function') {
|
|
1015
|
+
this.controlBar.updateControlsForViewport(width);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Update transcript positioning
|
|
1019
|
+
if (this.transcriptManager && this.transcriptManager.isVisible) {
|
|
1020
|
+
this.transcriptManager.positionTranscript();
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
this.resizeObserver.observe(this.container);
|
|
1026
|
+
} else {
|
|
1027
|
+
// Fallback to window resize event
|
|
1028
|
+
this.resizeHandler = () => {
|
|
1029
|
+
const width = this.container.clientWidth;
|
|
1030
|
+
|
|
1031
|
+
if (this.controlBar && typeof this.controlBar.updateControlsForViewport === 'function') {
|
|
1032
|
+
this.controlBar.updateControlsForViewport(width);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (this.transcriptManager && this.transcriptManager.isVisible) {
|
|
1036
|
+
this.transcriptManager.positionTranscript();
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
window.addEventListener('resize', this.resizeHandler);
|
|
1003
1041
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1042
|
+
|
|
1043
|
+
// Also listen for orientation changes on mobile
|
|
1044
|
+
if (window.matchMedia) {
|
|
1045
|
+
this.orientationHandler = (e) => {
|
|
1046
|
+
// Wait for layout to settle
|
|
1047
|
+
setTimeout(() => {
|
|
1048
|
+
if (this.transcriptManager && this.transcriptManager.isVisible) {
|
|
1049
|
+
this.transcriptManager.positionTranscript();
|
|
1050
|
+
}
|
|
1051
|
+
}, 100);
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
const orientationQuery = window.matchMedia('(orientation: portrait)');
|
|
1055
|
+
if (orientationQuery.addEventListener) {
|
|
1056
|
+
orientationQuery.addEventListener('change', this.orientationHandler);
|
|
1057
|
+
} else if (orientationQuery.addListener) {
|
|
1058
|
+
// Fallback for older browsers
|
|
1059
|
+
orientationQuery.addListener(this.orientationHandler);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
this.orientationQuery = orientationQuery;
|
|
1007
1063
|
}
|
|
1008
|
-
};
|
|
1009
|
-
|
|
1010
|
-
window.addEventListener('resize', this.resizeHandler);
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
// Also listen for orientation changes on mobile
|
|
1014
|
-
if (window.matchMedia) {
|
|
1015
|
-
this.orientationHandler = (e) => {
|
|
1016
|
-
// Wait for layout to settle
|
|
1017
|
-
setTimeout(() => {
|
|
1018
|
-
if (this.transcriptManager && this.transcriptManager.isVisible) {
|
|
1019
|
-
this.transcriptManager.positionTranscript();
|
|
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;
|
|
1033
1064
|
}
|
|
1034
|
-
}
|
|
1035
1065
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
this.container.parentNode.removeChild(this.container);
|
|
1066
|
+
// Cleanup
|
|
1067
|
+
destroy() {
|
|
1068
|
+
this.log('Destroying player');
|
|
1069
|
+
|
|
1070
|
+
if (this.renderer) {
|
|
1071
|
+
this.renderer.destroy();
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (this.controlBar) {
|
|
1075
|
+
this.controlBar.destroy();
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (this.captionManager) {
|
|
1079
|
+
this.captionManager.destroy();
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (this.keyboardManager) {
|
|
1083
|
+
this.keyboardManager.destroy();
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (this.transcriptManager) {
|
|
1087
|
+
this.transcriptManager.destroy();
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Cleanup sign language video and listeners
|
|
1091
|
+
this.cleanupSignLanguage();
|
|
1092
|
+
|
|
1093
|
+
// Cleanup play overlay button
|
|
1094
|
+
if (this.playButtonOverlay && this.playButtonOverlay.parentNode) {
|
|
1095
|
+
this.playButtonOverlay.remove();
|
|
1096
|
+
this.playButtonOverlay = null;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Cleanup resize observer
|
|
1100
|
+
if (this.resizeObserver) {
|
|
1101
|
+
this.resizeObserver.disconnect();
|
|
1102
|
+
this.resizeObserver = null;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Cleanup window resize handler
|
|
1106
|
+
if (this.resizeHandler) {
|
|
1107
|
+
window.removeEventListener('resize', this.resizeHandler);
|
|
1108
|
+
this.resizeHandler = null;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// Cleanup orientation change handler
|
|
1112
|
+
if (this.orientationQuery && this.orientationHandler) {
|
|
1113
|
+
if (this.orientationQuery.removeEventListener) {
|
|
1114
|
+
this.orientationQuery.removeEventListener('change', this.orientationHandler);
|
|
1115
|
+
} else if (this.orientationQuery.removeListener) {
|
|
1116
|
+
this.orientationQuery.removeListener(this.orientationHandler);
|
|
1117
|
+
}
|
|
1118
|
+
this.orientationQuery = null;
|
|
1119
|
+
this.orientationHandler = null;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Remove container
|
|
1123
|
+
if (this.container && this.container.parentNode) {
|
|
1124
|
+
this.container.parentNode.insertBefore(this.element, this.container);
|
|
1125
|
+
this.container.parentNode.removeChild(this.container);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
this.removeAllListeners();
|
|
1100
1129
|
}
|
|
1101
|
-
|
|
1102
|
-
this.removeAllListeners();
|
|
1103
|
-
}
|
|
1104
1130
|
}
|
|
1105
1131
|
|
|
1106
1132
|
// Static instances tracker for pause others functionality
|