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