vidply 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -22
- package/README.md +608 -593
- package/dist/vidply.css +2422 -1807
- package/dist/vidply.esm.js +1480 -93
- package/dist/vidply.esm.js.map +3 -3
- package/dist/vidply.esm.min.js +3 -3
- package/dist/vidply.esm.min.meta.json +48 -25
- package/dist/vidply.js +1480 -93
- package/dist/vidply.js.map +3 -3
- package/dist/vidply.min.css +1 -1
- package/dist/vidply.min.js +3 -3
- package/dist/vidply.min.meta.json +48 -25
- package/package.json +2 -2
- package/src/controls/CaptionManager.js +278 -248
- package/src/controls/ControlBar.js +2033 -2026
- package/src/controls/KeyboardManager.js +233 -233
- package/src/controls/SettingsDialog.js +417 -417
- package/src/controls/TranscriptManager.js +1803 -728
- package/src/core/Player.js +1616 -1134
- package/src/i18n/i18n.js +66 -66
- package/src/i18n/translations.js +616 -561
- package/src/icons/Icons.js +187 -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 +2422 -1807
- package/src/utils/DOMUtils.js +154 -154
- package/src/utils/EventEmitter.js +53 -53
- package/src/utils/StorageManager.js +156 -0
- package/src/utils/TimeUtils.js +87 -87
package/src/core/Player.js
CHANGED
|
@@ -1,1134 +1,1616 @@
|
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
tracks.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.element.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
'
|
|
125
|
-
'
|
|
126
|
-
'
|
|
127
|
-
'
|
|
128
|
-
'
|
|
129
|
-
'
|
|
130
|
-
'
|
|
131
|
-
'
|
|
132
|
-
'
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
this.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (
|
|
242
|
-
this.
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
//
|
|
259
|
-
if (this.options.
|
|
260
|
-
this.
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
//
|
|
272
|
-
this.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (this.options.
|
|
281
|
-
this.
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
this.
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
this.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
this.
|
|
352
|
-
|
|
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
|
-
this.
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
this.
|
|
401
|
-
|
|
402
|
-
this.
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
this.
|
|
411
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
//
|
|
520
|
-
if (this.
|
|
521
|
-
|
|
522
|
-
this.
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
if (
|
|
634
|
-
this.
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
this.state.muted
|
|
655
|
-
|
|
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
|
-
if (
|
|
685
|
-
|
|
686
|
-
} else
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
this.
|
|
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 (
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
if (
|
|
742
|
-
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
this.
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
|
|
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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
this.
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
this.
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
if
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
if (
|
|
942
|
-
this.
|
|
943
|
-
|
|
944
|
-
this.
|
|
945
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
import {StorageManager} from '../utils/StorageManager.js';
|
|
19
|
+
|
|
20
|
+
export class Player extends EventEmitter {
|
|
21
|
+
constructor(element, options = {}) {
|
|
22
|
+
super();
|
|
23
|
+
|
|
24
|
+
this.element = typeof element === 'string' ? document.querySelector(element) : element;
|
|
25
|
+
if (!this.element) {
|
|
26
|
+
throw new Error('VidPly: Element not found');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Auto-create media element if a non-media element is provided
|
|
30
|
+
if (this.element.tagName !== 'VIDEO' && this.element.tagName !== 'AUDIO') {
|
|
31
|
+
const mediaType = options.mediaType || 'video';
|
|
32
|
+
const mediaElement = document.createElement(mediaType);
|
|
33
|
+
|
|
34
|
+
// Copy attributes from the div to the media element
|
|
35
|
+
Array.from(this.element.attributes).forEach(attr => {
|
|
36
|
+
if (attr.name !== 'id' && attr.name !== 'class' && !attr.name.startsWith('data-')) {
|
|
37
|
+
mediaElement.setAttribute(attr.name, attr.value);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Copy any track elements from the div
|
|
42
|
+
const tracks = this.element.querySelectorAll('track');
|
|
43
|
+
tracks.forEach(track => {
|
|
44
|
+
mediaElement.appendChild(track.cloneNode(true));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Clear the div and insert the media element
|
|
48
|
+
this.element.innerHTML = '';
|
|
49
|
+
this.element.appendChild(mediaElement);
|
|
50
|
+
|
|
51
|
+
// Update element reference to the actual media element
|
|
52
|
+
this.element = mediaElement;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Default options
|
|
56
|
+
this.options = {
|
|
57
|
+
// Display
|
|
58
|
+
width: null,
|
|
59
|
+
height: null,
|
|
60
|
+
poster: null,
|
|
61
|
+
responsive: true,
|
|
62
|
+
fillContainer: false,
|
|
63
|
+
|
|
64
|
+
// Playback
|
|
65
|
+
autoplay: false,
|
|
66
|
+
loop: false,
|
|
67
|
+
muted: false,
|
|
68
|
+
volume: 0.8,
|
|
69
|
+
playbackSpeed: 1.0,
|
|
70
|
+
preload: 'metadata',
|
|
71
|
+
startTime: 0,
|
|
72
|
+
playsInline: true, // Enable inline playback on iOS (prevents native fullscreen)
|
|
73
|
+
|
|
74
|
+
// Controls
|
|
75
|
+
controls: true,
|
|
76
|
+
hideControlsDelay: 3000,
|
|
77
|
+
playPauseButton: true,
|
|
78
|
+
progressBar: true,
|
|
79
|
+
currentTime: true,
|
|
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: false,
|
|
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'],
|
|
128
|
+
'seek-backward': ['ArrowLeft'],
|
|
129
|
+
'mute': ['m'],
|
|
130
|
+
'fullscreen': ['f'],
|
|
131
|
+
'captions': ['c'],
|
|
132
|
+
'caption-style-menu': ['a'],
|
|
133
|
+
'speed-up': ['>'],
|
|
134
|
+
'speed-down': ['<'],
|
|
135
|
+
'speed-menu': ['s'],
|
|
136
|
+
'quality-menu': ['q'],
|
|
137
|
+
'chapters-menu': ['j'],
|
|
138
|
+
'transcript-toggle': ['t']
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// Accessibility
|
|
142
|
+
ariaLabels: {},
|
|
143
|
+
screenReaderAnnouncements: true,
|
|
144
|
+
highContrast: false,
|
|
145
|
+
focusHighlight: true,
|
|
146
|
+
|
|
147
|
+
// Languages
|
|
148
|
+
language: 'en',
|
|
149
|
+
languages: ['en'],
|
|
150
|
+
|
|
151
|
+
// Advanced
|
|
152
|
+
debug: false,
|
|
153
|
+
classPrefix: 'vidply',
|
|
154
|
+
iconType: 'svg',
|
|
155
|
+
pauseOthersOnPlay: true,
|
|
156
|
+
|
|
157
|
+
// Callbacks
|
|
158
|
+
onReady: null,
|
|
159
|
+
onPlay: null,
|
|
160
|
+
onPause: null,
|
|
161
|
+
onEnded: null,
|
|
162
|
+
onTimeUpdate: null,
|
|
163
|
+
onVolumeChange: null,
|
|
164
|
+
onError: null,
|
|
165
|
+
|
|
166
|
+
...options
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Storage manager
|
|
170
|
+
this.storage = new StorageManager('vidply');
|
|
171
|
+
|
|
172
|
+
// Load saved player preferences
|
|
173
|
+
const savedPrefs = this.storage.getPlayerPreferences();
|
|
174
|
+
if (savedPrefs) {
|
|
175
|
+
if (savedPrefs.volume !== undefined) this.options.volume = savedPrefs.volume;
|
|
176
|
+
if (savedPrefs.playbackSpeed !== undefined) this.options.playbackSpeed = savedPrefs.playbackSpeed;
|
|
177
|
+
if (savedPrefs.muted !== undefined) this.options.muted = savedPrefs.muted;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// State
|
|
181
|
+
this.state = {
|
|
182
|
+
ready: false,
|
|
183
|
+
playing: false,
|
|
184
|
+
paused: true,
|
|
185
|
+
ended: false,
|
|
186
|
+
buffering: false,
|
|
187
|
+
seeking: false,
|
|
188
|
+
muted: this.options.muted,
|
|
189
|
+
volume: this.options.volume,
|
|
190
|
+
currentTime: 0,
|
|
191
|
+
duration: 0,
|
|
192
|
+
playbackSpeed: this.options.playbackSpeed,
|
|
193
|
+
fullscreen: false,
|
|
194
|
+
pip: false,
|
|
195
|
+
captionsEnabled: this.options.captionsDefault,
|
|
196
|
+
currentCaption: null,
|
|
197
|
+
controlsVisible: true,
|
|
198
|
+
audioDescriptionEnabled: false,
|
|
199
|
+
signLanguageEnabled: false
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Store original source for toggling
|
|
203
|
+
this.originalSrc = null;
|
|
204
|
+
this.audioDescriptionSrc = this.options.audioDescriptionSrc;
|
|
205
|
+
this.signLanguageSrc = this.options.signLanguageSrc;
|
|
206
|
+
this.signLanguageVideo = null;
|
|
207
|
+
|
|
208
|
+
// Components
|
|
209
|
+
this.container = null;
|
|
210
|
+
this.renderer = null;
|
|
211
|
+
this.controlBar = null;
|
|
212
|
+
this.captionManager = null;
|
|
213
|
+
this.keyboardManager = null;
|
|
214
|
+
this.settingsDialog = null;
|
|
215
|
+
|
|
216
|
+
// Initialize
|
|
217
|
+
this.init();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async init() {
|
|
221
|
+
try {
|
|
222
|
+
this.log('Initializing VidPly player');
|
|
223
|
+
|
|
224
|
+
// Auto-detect language from HTML lang attribute if not explicitly set
|
|
225
|
+
if (!this.options.language || this.options.language === 'en') {
|
|
226
|
+
const htmlLang = this.detectHtmlLanguage();
|
|
227
|
+
if (htmlLang) {
|
|
228
|
+
this.options.language = htmlLang;
|
|
229
|
+
this.log(`Auto-detected language from HTML: ${htmlLang}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Set language
|
|
234
|
+
i18n.setLanguage(this.options.language);
|
|
235
|
+
|
|
236
|
+
// Create container
|
|
237
|
+
this.createContainer();
|
|
238
|
+
|
|
239
|
+
// Detect and initialize renderer (only if source exists)
|
|
240
|
+
const src = this.element.src || this.element.querySelector('source')?.src;
|
|
241
|
+
if (src) {
|
|
242
|
+
await this.initializeRenderer();
|
|
243
|
+
} else {
|
|
244
|
+
this.log('No initial source - waiting for playlist or manual load');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Create controls
|
|
248
|
+
if (this.options.controls) {
|
|
249
|
+
this.controlBar = new ControlBar(this);
|
|
250
|
+
this.videoWrapper.appendChild(this.controlBar.element);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Initialize captions
|
|
254
|
+
if (this.options.captions) {
|
|
255
|
+
this.captionManager = new CaptionManager(this);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Initialize transcript
|
|
259
|
+
if (this.options.transcript || this.options.transcriptButton) {
|
|
260
|
+
this.transcriptManager = new TranscriptManager(this);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Initialize keyboard controls
|
|
264
|
+
if (this.options.keyboard) {
|
|
265
|
+
this.keyboardManager = new KeyboardManager(this);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Setup responsive handlers
|
|
269
|
+
this.setupResponsiveHandlers();
|
|
270
|
+
|
|
271
|
+
// Set initial state
|
|
272
|
+
if (this.options.startTime > 0) {
|
|
273
|
+
this.seek(this.options.startTime);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (this.options.muted) {
|
|
277
|
+
this.mute();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (this.options.volume !== 0.8) {
|
|
281
|
+
this.setVolume(this.options.volume);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Mark as ready
|
|
285
|
+
this.state.ready = true;
|
|
286
|
+
this.emit('ready');
|
|
287
|
+
|
|
288
|
+
if (this.options.onReady) {
|
|
289
|
+
this.options.onReady.call(this);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Autoplay if enabled
|
|
293
|
+
if (this.options.autoplay) {
|
|
294
|
+
this.play();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
this.log('Player initialized successfully');
|
|
298
|
+
} catch (error) {
|
|
299
|
+
this.handleError(error);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Detect language from HTML lang attribute
|
|
305
|
+
* @returns {string|null} Language code if available in translations, null otherwise
|
|
306
|
+
*/
|
|
307
|
+
detectHtmlLanguage() {
|
|
308
|
+
// Try to get lang from html element
|
|
309
|
+
const htmlLang = document.documentElement.lang || document.documentElement.getAttribute('lang');
|
|
310
|
+
|
|
311
|
+
if (!htmlLang) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Normalize the language code (e.g., "en-US" -> "en", "de-DE" -> "de")
|
|
316
|
+
const normalizedLang = htmlLang.toLowerCase().split('-')[0];
|
|
317
|
+
|
|
318
|
+
// Check if this language is available in our translations
|
|
319
|
+
const availableLanguages = ['en', 'de', 'es', 'fr', 'ja'];
|
|
320
|
+
|
|
321
|
+
if (availableLanguages.includes(normalizedLang)) {
|
|
322
|
+
return normalizedLang;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Language not available, will fallback to English
|
|
326
|
+
this.log(`Language "${htmlLang}" not available, using English as fallback`);
|
|
327
|
+
return 'en';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
createContainer() {
|
|
331
|
+
// Create main container
|
|
332
|
+
this.container = DOMUtils.createElement('div', {
|
|
333
|
+
className: `${this.options.classPrefix}-player`,
|
|
334
|
+
attributes: {
|
|
335
|
+
'role': 'region',
|
|
336
|
+
'aria-label': i18n.t('player.label'),
|
|
337
|
+
'tabindex': '0'
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Add media type class
|
|
342
|
+
const mediaType = this.element.tagName.toLowerCase();
|
|
343
|
+
this.container.classList.add(`${this.options.classPrefix}-${mediaType}`);
|
|
344
|
+
|
|
345
|
+
// Add responsive class
|
|
346
|
+
if (this.options.responsive) {
|
|
347
|
+
this.container.classList.add(`${this.options.classPrefix}-responsive`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Create video wrapper (for proper positioning of controls)
|
|
351
|
+
this.videoWrapper = DOMUtils.createElement('div', {
|
|
352
|
+
className: `${this.options.classPrefix}-video-wrapper`
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Wrap original element
|
|
356
|
+
this.element.parentNode.insertBefore(this.container, this.element);
|
|
357
|
+
this.container.appendChild(this.videoWrapper);
|
|
358
|
+
this.videoWrapper.appendChild(this.element);
|
|
359
|
+
|
|
360
|
+
// Hide native controls and set dimensions
|
|
361
|
+
this.element.controls = false;
|
|
362
|
+
this.element.removeAttribute('controls');
|
|
363
|
+
this.element.setAttribute('tabindex', '-1'); // Remove from tab order
|
|
364
|
+
this.element.style.width = '100%';
|
|
365
|
+
this.element.style.height = '100%';
|
|
366
|
+
|
|
367
|
+
// Enable inline playback on iOS (prevents native fullscreen)
|
|
368
|
+
// This allows custom controls to work on iOS devices
|
|
369
|
+
if (this.element.tagName === 'VIDEO' && this.options.playsInline) {
|
|
370
|
+
this.element.setAttribute('playsinline', '');
|
|
371
|
+
this.element.setAttribute('webkit-playsinline', ''); // For older iOS versions
|
|
372
|
+
this.element.playsInline = true; // Property version
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Set dimensions
|
|
376
|
+
if (this.options.width) {
|
|
377
|
+
this.container.style.width = typeof this.options.width === 'number'
|
|
378
|
+
? `${this.options.width}px`
|
|
379
|
+
: this.options.width;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (this.options.height) {
|
|
383
|
+
this.container.style.height = typeof this.options.height === 'number'
|
|
384
|
+
? `${this.options.height}px`
|
|
385
|
+
: this.options.height;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Set poster
|
|
389
|
+
if (this.options.poster && this.element.tagName === 'VIDEO') {
|
|
390
|
+
this.element.poster = this.options.poster;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Create centered play button overlay (only for video)
|
|
394
|
+
if (this.element.tagName === 'VIDEO') {
|
|
395
|
+
this.createPlayButtonOverlay();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Make video/audio element clickable to toggle play/pause
|
|
399
|
+
this.element.style.cursor = 'pointer';
|
|
400
|
+
this.element.addEventListener('click', (e) => {
|
|
401
|
+
// Prevent if clicking on native controls (shouldn't happen but just in case)
|
|
402
|
+
if (e.target === this.element) {
|
|
403
|
+
this.toggle();
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
createPlayButtonOverlay() {
|
|
409
|
+
// Create complete SVG play button from Icons.js
|
|
410
|
+
this.playButtonOverlay = createPlayOverlay();
|
|
411
|
+
|
|
412
|
+
// Add click handler
|
|
413
|
+
this.playButtonOverlay.addEventListener('click', () => {
|
|
414
|
+
this.toggle();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Add to video wrapper
|
|
418
|
+
this.videoWrapper.appendChild(this.playButtonOverlay);
|
|
419
|
+
|
|
420
|
+
// Show/hide based on play state
|
|
421
|
+
this.on('play', () => {
|
|
422
|
+
this.playButtonOverlay.style.opacity = '0';
|
|
423
|
+
this.playButtonOverlay.style.pointerEvents = 'none';
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
this.on('pause', () => {
|
|
427
|
+
this.playButtonOverlay.style.opacity = '1';
|
|
428
|
+
this.playButtonOverlay.style.pointerEvents = 'auto';
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
this.on('ended', () => {
|
|
432
|
+
this.playButtonOverlay.style.opacity = '1';
|
|
433
|
+
this.playButtonOverlay.style.pointerEvents = 'auto';
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async initializeRenderer() {
|
|
438
|
+
const src = this.element.src || this.element.querySelector('source')?.src;
|
|
439
|
+
|
|
440
|
+
if (!src) {
|
|
441
|
+
throw new Error('No media source found');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Store original source for audio description toggling
|
|
445
|
+
if (!this.originalSrc) {
|
|
446
|
+
this.originalSrc = src;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Detect media type
|
|
450
|
+
let renderer;
|
|
451
|
+
|
|
452
|
+
if (src.includes('youtube.com') || src.includes('youtu.be')) {
|
|
453
|
+
renderer = YouTubeRenderer;
|
|
454
|
+
} else if (src.includes('vimeo.com')) {
|
|
455
|
+
renderer = VimeoRenderer;
|
|
456
|
+
} else if (src.includes('.m3u8')) {
|
|
457
|
+
renderer = HLSRenderer;
|
|
458
|
+
} else {
|
|
459
|
+
renderer = HTML5Renderer;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
this.log(`Using ${renderer.name} renderer`);
|
|
463
|
+
this.renderer = new renderer(this);
|
|
464
|
+
await this.renderer.init();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Load new media source (for playlists)
|
|
469
|
+
* @param {Object} config - Media configuration
|
|
470
|
+
* @param {string} config.src - Media source URL
|
|
471
|
+
* @param {string} config.type - Media MIME type
|
|
472
|
+
* @param {string} [config.poster] - Poster image URL
|
|
473
|
+
* @param {Array} [config.tracks] - Text tracks (captions, chapters, etc.)
|
|
474
|
+
*/
|
|
475
|
+
async load(config) {
|
|
476
|
+
try {
|
|
477
|
+
this.log('Loading new media:', config.src);
|
|
478
|
+
|
|
479
|
+
// Pause current playback
|
|
480
|
+
if (this.renderer) {
|
|
481
|
+
this.pause();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Clear existing text tracks
|
|
485
|
+
const existingTracks = this.element.querySelectorAll('track');
|
|
486
|
+
existingTracks.forEach(track => track.remove());
|
|
487
|
+
|
|
488
|
+
// Update media element
|
|
489
|
+
this.element.src = config.src;
|
|
490
|
+
|
|
491
|
+
if (config.type) {
|
|
492
|
+
this.element.type = config.type;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (config.poster && this.element.tagName === 'VIDEO') {
|
|
496
|
+
this.element.poster = config.poster;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Add new text tracks
|
|
500
|
+
if (config.tracks && config.tracks.length > 0) {
|
|
501
|
+
config.tracks.forEach(trackConfig => {
|
|
502
|
+
const track = document.createElement('track');
|
|
503
|
+
track.src = trackConfig.src;
|
|
504
|
+
track.kind = trackConfig.kind || 'captions';
|
|
505
|
+
track.srclang = trackConfig.srclang || 'en';
|
|
506
|
+
track.label = trackConfig.label || trackConfig.srclang;
|
|
507
|
+
|
|
508
|
+
if (trackConfig.default) {
|
|
509
|
+
track.default = true;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
this.element.appendChild(track);
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Check if we need to change renderer type
|
|
517
|
+
const shouldChangeRenderer = this.shouldChangeRenderer(config.src);
|
|
518
|
+
|
|
519
|
+
// Destroy old renderer if changing types
|
|
520
|
+
if (shouldChangeRenderer && this.renderer) {
|
|
521
|
+
this.renderer.destroy();
|
|
522
|
+
this.renderer = null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Initialize or reinitialize renderer
|
|
526
|
+
if (!this.renderer || shouldChangeRenderer) {
|
|
527
|
+
await this.initializeRenderer();
|
|
528
|
+
} else {
|
|
529
|
+
// Just reload the current renderer with the updated element
|
|
530
|
+
this.renderer.media = this.element; // Update media reference
|
|
531
|
+
this.element.load();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Reinitialize caption manager to pick up new tracks
|
|
535
|
+
if (this.captionManager) {
|
|
536
|
+
this.captionManager.destroy();
|
|
537
|
+
this.captionManager = new CaptionManager(this);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Reinitialize transcript manager to pick up new tracks
|
|
541
|
+
if (this.transcriptManager) {
|
|
542
|
+
const wasVisible = this.transcriptManager.isVisible;
|
|
543
|
+
this.transcriptManager.destroy();
|
|
544
|
+
this.transcriptManager = new TranscriptManager(this);
|
|
545
|
+
|
|
546
|
+
// Restore visibility state if transcript was open
|
|
547
|
+
if (wasVisible) {
|
|
548
|
+
this.transcriptManager.showTranscript();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Update control bar to show/hide feature buttons based on new tracks
|
|
553
|
+
if (this.controlBar) {
|
|
554
|
+
this.updateControlBar();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
this.emit('sourcechange', config);
|
|
558
|
+
this.log('Media loaded successfully');
|
|
559
|
+
|
|
560
|
+
} catch (error) {
|
|
561
|
+
this.handleError(error);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Check if we need to change renderer type
|
|
567
|
+
* @param {string} src - New source URL
|
|
568
|
+
* @returns {boolean}
|
|
569
|
+
*/
|
|
570
|
+
/**
|
|
571
|
+
* Update control bar to refresh button visibility based on available features
|
|
572
|
+
*/
|
|
573
|
+
updateControlBar() {
|
|
574
|
+
if (!this.controlBar) return;
|
|
575
|
+
|
|
576
|
+
const controlBar = this.controlBar;
|
|
577
|
+
|
|
578
|
+
// Clear existing controls content
|
|
579
|
+
controlBar.element.innerHTML = '';
|
|
580
|
+
|
|
581
|
+
// Recreate controls with updated feature detection
|
|
582
|
+
controlBar.createControls();
|
|
583
|
+
|
|
584
|
+
// Reattach events for the new controls
|
|
585
|
+
controlBar.attachEvents();
|
|
586
|
+
controlBar.setupAutoHide();
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
shouldChangeRenderer(src) {
|
|
590
|
+
if (!this.renderer) return true;
|
|
591
|
+
|
|
592
|
+
const isYouTube = src.includes('youtube.com') || src.includes('youtu.be');
|
|
593
|
+
const isVimeo = src.includes('vimeo.com');
|
|
594
|
+
const isHLS = src.includes('.m3u8');
|
|
595
|
+
|
|
596
|
+
const currentRendererName = this.renderer.constructor.name;
|
|
597
|
+
|
|
598
|
+
if (isYouTube && currentRendererName !== 'YouTubeRenderer') return true;
|
|
599
|
+
if (isVimeo && currentRendererName !== 'VimeoRenderer') return true;
|
|
600
|
+
if (isHLS && currentRendererName !== 'HLSRenderer') return true;
|
|
601
|
+
if (!isYouTube && !isVimeo && !isHLS && currentRendererName !== 'HTML5Renderer') return true;
|
|
602
|
+
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Playback controls
|
|
607
|
+
play() {
|
|
608
|
+
if (this.renderer) {
|
|
609
|
+
this.renderer.play();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
pause() {
|
|
614
|
+
if (this.renderer) {
|
|
615
|
+
this.renderer.pause();
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
stop() {
|
|
620
|
+
this.pause();
|
|
621
|
+
this.seek(0);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
toggle() {
|
|
625
|
+
if (this.state.playing) {
|
|
626
|
+
this.pause();
|
|
627
|
+
} else {
|
|
628
|
+
this.play();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
seek(time) {
|
|
633
|
+
if (this.renderer) {
|
|
634
|
+
this.renderer.seek(time);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
seekForward(interval = this.options.seekInterval) {
|
|
639
|
+
this.seek(Math.min(this.state.currentTime + interval, this.state.duration));
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
seekBackward(interval = this.options.seekInterval) {
|
|
643
|
+
this.seek(Math.max(this.state.currentTime - interval, 0));
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Volume controls
|
|
647
|
+
setVolume(volume) {
|
|
648
|
+
const newVolume = Math.max(0, Math.min(1, volume));
|
|
649
|
+
if (this.renderer) {
|
|
650
|
+
this.renderer.setVolume(newVolume);
|
|
651
|
+
}
|
|
652
|
+
this.state.volume = newVolume;
|
|
653
|
+
|
|
654
|
+
if (newVolume > 0 && this.state.muted) {
|
|
655
|
+
this.state.muted = false;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
this.savePlayerPreferences();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
getVolume() {
|
|
662
|
+
return this.state.volume;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
mute() {
|
|
666
|
+
if (this.renderer) {
|
|
667
|
+
this.renderer.setMuted(true);
|
|
668
|
+
}
|
|
669
|
+
this.state.muted = true;
|
|
670
|
+
this.savePlayerPreferences();
|
|
671
|
+
this.emit('volumechange');
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
unmute() {
|
|
675
|
+
if (this.renderer) {
|
|
676
|
+
this.renderer.setMuted(false);
|
|
677
|
+
}
|
|
678
|
+
this.state.muted = false;
|
|
679
|
+
this.savePlayerPreferences();
|
|
680
|
+
this.emit('volumechange');
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
toggleMute() {
|
|
684
|
+
if (this.state.muted) {
|
|
685
|
+
this.unmute();
|
|
686
|
+
} else {
|
|
687
|
+
this.mute();
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Playback speed
|
|
692
|
+
setPlaybackSpeed(speed) {
|
|
693
|
+
const newSpeed = Math.max(0.25, Math.min(2, speed));
|
|
694
|
+
if (this.renderer) {
|
|
695
|
+
this.renderer.setPlaybackSpeed(newSpeed);
|
|
696
|
+
}
|
|
697
|
+
this.state.playbackSpeed = newSpeed;
|
|
698
|
+
this.savePlayerPreferences();
|
|
699
|
+
this.emit('playbackspeedchange', newSpeed);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
getPlaybackSpeed() {
|
|
703
|
+
return this.state.playbackSpeed;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Save player preferences to localStorage
|
|
707
|
+
savePlayerPreferences() {
|
|
708
|
+
this.storage.savePlayerPreferences({
|
|
709
|
+
volume: this.state.volume,
|
|
710
|
+
muted: this.state.muted,
|
|
711
|
+
playbackSpeed: this.state.playbackSpeed
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Fullscreen
|
|
716
|
+
enterFullscreen() {
|
|
717
|
+
const elem = this.container;
|
|
718
|
+
|
|
719
|
+
if (elem.requestFullscreen) {
|
|
720
|
+
elem.requestFullscreen();
|
|
721
|
+
} else if (elem.webkitRequestFullscreen) {
|
|
722
|
+
elem.webkitRequestFullscreen();
|
|
723
|
+
} else if (elem.mozRequestFullScreen) {
|
|
724
|
+
elem.mozRequestFullScreen();
|
|
725
|
+
} else if (elem.msRequestFullscreen) {
|
|
726
|
+
elem.msRequestFullscreen();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
this.state.fullscreen = true;
|
|
730
|
+
this.container.classList.add(`${this.options.classPrefix}-fullscreen`);
|
|
731
|
+
this.emit('fullscreenchange', true);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
exitFullscreen() {
|
|
735
|
+
if (document.exitFullscreen) {
|
|
736
|
+
document.exitFullscreen();
|
|
737
|
+
} else if (document.webkitExitFullscreen) {
|
|
738
|
+
document.webkitExitFullscreen();
|
|
739
|
+
} else if (document.mozCancelFullScreen) {
|
|
740
|
+
document.mozCancelFullScreen();
|
|
741
|
+
} else if (document.msExitFullscreen) {
|
|
742
|
+
document.msExitFullscreen();
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
this.state.fullscreen = false;
|
|
746
|
+
this.container.classList.remove(`${this.options.classPrefix}-fullscreen`);
|
|
747
|
+
this.emit('fullscreenchange', false);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
toggleFullscreen() {
|
|
751
|
+
if (this.state.fullscreen) {
|
|
752
|
+
this.exitFullscreen();
|
|
753
|
+
} else {
|
|
754
|
+
this.enterFullscreen();
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Picture-in-Picture
|
|
759
|
+
enterPiP() {
|
|
760
|
+
if (this.element.requestPictureInPicture) {
|
|
761
|
+
this.element.requestPictureInPicture();
|
|
762
|
+
this.state.pip = true;
|
|
763
|
+
this.emit('pipchange', true);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
exitPiP() {
|
|
768
|
+
if (document.pictureInPictureElement) {
|
|
769
|
+
document.exitPictureInPicture();
|
|
770
|
+
this.state.pip = false;
|
|
771
|
+
this.emit('pipchange', false);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
togglePiP() {
|
|
776
|
+
if (this.state.pip) {
|
|
777
|
+
this.exitPiP();
|
|
778
|
+
} else {
|
|
779
|
+
this.enterPiP();
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Captions
|
|
784
|
+
enableCaptions() {
|
|
785
|
+
if (this.captionManager) {
|
|
786
|
+
this.captionManager.enable();
|
|
787
|
+
this.state.captionsEnabled = true;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
disableCaptions() {
|
|
792
|
+
if (this.captionManager) {
|
|
793
|
+
this.captionManager.disable();
|
|
794
|
+
this.state.captionsEnabled = false;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
toggleCaptions() {
|
|
799
|
+
if (this.state.captionsEnabled) {
|
|
800
|
+
this.disableCaptions();
|
|
801
|
+
} else {
|
|
802
|
+
this.enableCaptions();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Audio Description
|
|
807
|
+
async enableAudioDescription() {
|
|
808
|
+
if (!this.audioDescriptionSrc) {
|
|
809
|
+
console.warn('VidPly: No audio description source provided');
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Store current playback state
|
|
814
|
+
const currentTime = this.state.currentTime;
|
|
815
|
+
const wasPlaying = this.state.playing;
|
|
816
|
+
|
|
817
|
+
// Switch to audio-described version
|
|
818
|
+
this.element.src = this.audioDescriptionSrc;
|
|
819
|
+
|
|
820
|
+
// Wait for new source to load
|
|
821
|
+
await new Promise((resolve) => {
|
|
822
|
+
const onLoadedMetadata = () => {
|
|
823
|
+
this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
|
|
824
|
+
resolve();
|
|
825
|
+
};
|
|
826
|
+
this.element.addEventListener('loadedmetadata', onLoadedMetadata);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// Restore playback position
|
|
830
|
+
this.seek(currentTime);
|
|
831
|
+
|
|
832
|
+
if (wasPlaying) {
|
|
833
|
+
this.play();
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
this.state.audioDescriptionEnabled = true;
|
|
837
|
+
this.emit('audiodescriptionenabled');
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async disableAudioDescription() {
|
|
841
|
+
if (!this.originalSrc) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Store current playback state
|
|
846
|
+
const currentTime = this.state.currentTime;
|
|
847
|
+
const wasPlaying = this.state.playing;
|
|
848
|
+
|
|
849
|
+
// Switch back to original version
|
|
850
|
+
this.element.src = this.originalSrc;
|
|
851
|
+
|
|
852
|
+
// Wait for new source to load
|
|
853
|
+
await new Promise((resolve) => {
|
|
854
|
+
const onLoadedMetadata = () => {
|
|
855
|
+
this.element.removeEventListener('loadedmetadata', onLoadedMetadata);
|
|
856
|
+
resolve();
|
|
857
|
+
};
|
|
858
|
+
this.element.addEventListener('loadedmetadata', onLoadedMetadata);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
// Restore playback position
|
|
862
|
+
this.seek(currentTime);
|
|
863
|
+
|
|
864
|
+
if (wasPlaying) {
|
|
865
|
+
this.play();
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
this.state.audioDescriptionEnabled = false;
|
|
869
|
+
this.emit('audiodescriptiondisabled');
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
async toggleAudioDescription() {
|
|
873
|
+
// Check if we have description tracks or audio-described video
|
|
874
|
+
const textTracks = Array.from(this.element.textTracks || []);
|
|
875
|
+
const descriptionTrack = textTracks.find(track => track.kind === 'descriptions');
|
|
876
|
+
|
|
877
|
+
if (descriptionTrack) {
|
|
878
|
+
// Toggle description track
|
|
879
|
+
if (descriptionTrack.mode === 'showing') {
|
|
880
|
+
descriptionTrack.mode = 'hidden';
|
|
881
|
+
this.state.audioDescriptionEnabled = false;
|
|
882
|
+
this.emit('audiodescriptiondisabled');
|
|
883
|
+
} else {
|
|
884
|
+
descriptionTrack.mode = 'showing';
|
|
885
|
+
this.state.audioDescriptionEnabled = true;
|
|
886
|
+
this.emit('audiodescriptionenabled');
|
|
887
|
+
}
|
|
888
|
+
} else if (this.audioDescriptionSrc) {
|
|
889
|
+
// Use audio-described video source
|
|
890
|
+
if (this.state.audioDescriptionEnabled) {
|
|
891
|
+
await this.disableAudioDescription();
|
|
892
|
+
} else {
|
|
893
|
+
await this.enableAudioDescription();
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Sign Language
|
|
899
|
+
enableSignLanguage() {
|
|
900
|
+
if (!this.signLanguageSrc) {
|
|
901
|
+
console.warn('No sign language video source provided');
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (this.signLanguageWrapper) {
|
|
906
|
+
// Already exists, just show it
|
|
907
|
+
this.signLanguageWrapper.style.display = 'block';
|
|
908
|
+
this.state.signLanguageEnabled = true;
|
|
909
|
+
this.emit('signlanguageenabled');
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Create wrapper container
|
|
914
|
+
this.signLanguageWrapper = document.createElement('div');
|
|
915
|
+
this.signLanguageWrapper.className = 'vidply-sign-language-wrapper';
|
|
916
|
+
this.signLanguageWrapper.setAttribute('tabindex', '0');
|
|
917
|
+
this.signLanguageWrapper.setAttribute('aria-label', 'Sign Language Video - Press D to drag with keyboard, R to resize');
|
|
918
|
+
|
|
919
|
+
// Create sign language video element
|
|
920
|
+
this.signLanguageVideo = document.createElement('video');
|
|
921
|
+
this.signLanguageVideo.className = 'vidply-sign-language-video';
|
|
922
|
+
this.signLanguageVideo.src = this.signLanguageSrc;
|
|
923
|
+
this.signLanguageVideo.setAttribute('aria-label', i18n.t('player.signLanguage'));
|
|
924
|
+
this.signLanguageVideo.muted = true; // Sign language video should be muted
|
|
925
|
+
|
|
926
|
+
// Create resize handles
|
|
927
|
+
const resizeHandles = ['nw', 'ne', 'sw', 'se'].map(dir => {
|
|
928
|
+
const handle = document.createElement('div');
|
|
929
|
+
handle.className = `vidply-sign-resize-handle vidply-sign-resize-${dir}`;
|
|
930
|
+
handle.setAttribute('data-direction', dir);
|
|
931
|
+
handle.setAttribute('aria-label', `Resize ${dir.toUpperCase()}`);
|
|
932
|
+
return handle;
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
// Append video and handles to wrapper
|
|
936
|
+
this.signLanguageWrapper.appendChild(this.signLanguageVideo);
|
|
937
|
+
resizeHandles.forEach(handle => this.signLanguageWrapper.appendChild(handle));
|
|
938
|
+
|
|
939
|
+
// Set width FIRST to ensure proper dimensions
|
|
940
|
+
const saved = this.storage.getSignLanguagePreferences();
|
|
941
|
+
if (saved && saved.size && saved.size.width) {
|
|
942
|
+
this.signLanguageWrapper.style.width = saved.size.width;
|
|
943
|
+
} else {
|
|
944
|
+
this.signLanguageWrapper.style.width = '280px'; // Default width
|
|
945
|
+
}
|
|
946
|
+
// Height is always auto to maintain aspect ratio
|
|
947
|
+
this.signLanguageWrapper.style.height = 'auto';
|
|
948
|
+
|
|
949
|
+
// Position is always calculated fresh - use option or default to bottom-right
|
|
950
|
+
this.signLanguageDesiredPosition = this.options.signLanguagePosition || 'bottom-right';
|
|
951
|
+
|
|
952
|
+
// Add to main player container (NOT videoWrapper) to avoid overflow:hidden clipping
|
|
953
|
+
this.container.appendChild(this.signLanguageWrapper);
|
|
954
|
+
|
|
955
|
+
// Set position immediately after appending
|
|
956
|
+
requestAnimationFrame(() => {
|
|
957
|
+
this.constrainSignLanguagePosition();
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
// Sync with main video
|
|
961
|
+
this.signLanguageVideo.currentTime = this.state.currentTime;
|
|
962
|
+
if (!this.state.paused) {
|
|
963
|
+
this.signLanguageVideo.play();
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Setup drag and resize
|
|
967
|
+
this.setupSignLanguageInteraction();
|
|
968
|
+
|
|
969
|
+
// Create bound handlers to store references for cleanup
|
|
970
|
+
this.signLanguageHandlers = {
|
|
971
|
+
play: () => {
|
|
972
|
+
if (this.signLanguageVideo) {
|
|
973
|
+
this.signLanguageVideo.play();
|
|
974
|
+
}
|
|
975
|
+
},
|
|
976
|
+
pause: () => {
|
|
977
|
+
if (this.signLanguageVideo) {
|
|
978
|
+
this.signLanguageVideo.pause();
|
|
979
|
+
}
|
|
980
|
+
},
|
|
981
|
+
timeupdate: () => {
|
|
982
|
+
if (this.signLanguageVideo && Math.abs(this.signLanguageVideo.currentTime - this.state.currentTime) > 0.5) {
|
|
983
|
+
this.signLanguageVideo.currentTime = this.state.currentTime;
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
ratechange: () => {
|
|
987
|
+
if (this.signLanguageVideo) {
|
|
988
|
+
this.signLanguageVideo.playbackRate = this.state.playbackSpeed;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
// Sync playback
|
|
994
|
+
this.on('play', this.signLanguageHandlers.play);
|
|
995
|
+
this.on('pause', this.signLanguageHandlers.pause);
|
|
996
|
+
this.on('timeupdate', this.signLanguageHandlers.timeupdate);
|
|
997
|
+
this.on('ratechange', this.signLanguageHandlers.ratechange);
|
|
998
|
+
|
|
999
|
+
this.state.signLanguageEnabled = true;
|
|
1000
|
+
this.emit('signlanguageenabled');
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
disableSignLanguage() {
|
|
1004
|
+
if (this.signLanguageWrapper) {
|
|
1005
|
+
this.signLanguageWrapper.style.display = 'none';
|
|
1006
|
+
}
|
|
1007
|
+
this.state.signLanguageEnabled = false;
|
|
1008
|
+
this.emit('signlanguagedisabled');
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
toggleSignLanguage() {
|
|
1012
|
+
if (this.state.signLanguageEnabled) {
|
|
1013
|
+
this.disableSignLanguage();
|
|
1014
|
+
} else {
|
|
1015
|
+
this.enableSignLanguage();
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
setupSignLanguageInteraction() {
|
|
1020
|
+
if (!this.signLanguageWrapper) return;
|
|
1021
|
+
|
|
1022
|
+
let isDragging = false;
|
|
1023
|
+
let isResizing = false;
|
|
1024
|
+
let resizeDirection = null;
|
|
1025
|
+
let startX = 0;
|
|
1026
|
+
let startY = 0;
|
|
1027
|
+
let startLeft = 0;
|
|
1028
|
+
let startTop = 0;
|
|
1029
|
+
let startWidth = 0;
|
|
1030
|
+
let startHeight = 0;
|
|
1031
|
+
let dragMode = false;
|
|
1032
|
+
let resizeMode = false;
|
|
1033
|
+
|
|
1034
|
+
// Mouse drag on video element
|
|
1035
|
+
const onMouseDownVideo = (e) => {
|
|
1036
|
+
if (e.target !== this.signLanguageVideo) return;
|
|
1037
|
+
e.preventDefault();
|
|
1038
|
+
isDragging = true;
|
|
1039
|
+
startX = e.clientX;
|
|
1040
|
+
startY = e.clientY;
|
|
1041
|
+
const rect = this.signLanguageWrapper.getBoundingClientRect();
|
|
1042
|
+
startLeft = rect.left;
|
|
1043
|
+
startTop = rect.top;
|
|
1044
|
+
this.signLanguageWrapper.classList.add('vidply-sign-dragging');
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
// Mouse resize on handles
|
|
1048
|
+
const onMouseDownHandle = (e) => {
|
|
1049
|
+
if (!e.target.classList.contains('vidply-sign-resize-handle')) return;
|
|
1050
|
+
e.preventDefault();
|
|
1051
|
+
e.stopPropagation();
|
|
1052
|
+
isResizing = true;
|
|
1053
|
+
resizeDirection = e.target.getAttribute('data-direction');
|
|
1054
|
+
startX = e.clientX;
|
|
1055
|
+
startY = e.clientY;
|
|
1056
|
+
const rect = this.signLanguageWrapper.getBoundingClientRect();
|
|
1057
|
+
startLeft = rect.left;
|
|
1058
|
+
startTop = rect.top;
|
|
1059
|
+
startWidth = rect.width;
|
|
1060
|
+
startHeight = rect.height;
|
|
1061
|
+
this.signLanguageWrapper.classList.add('vidply-sign-resizing');
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
const onMouseMove = (e) => {
|
|
1065
|
+
if (isDragging) {
|
|
1066
|
+
const deltaX = e.clientX - startX;
|
|
1067
|
+
const deltaY = e.clientY - startY;
|
|
1068
|
+
|
|
1069
|
+
// Get videoWrapper and container dimensions
|
|
1070
|
+
const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
|
|
1071
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
1072
|
+
const wrapperRect = this.signLanguageWrapper.getBoundingClientRect();
|
|
1073
|
+
|
|
1074
|
+
// Calculate videoWrapper position relative to container
|
|
1075
|
+
const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
|
|
1076
|
+
const videoWrapperTop = videoWrapperRect.top - containerRect.top;
|
|
1077
|
+
|
|
1078
|
+
// Calculate new position (in client coordinates)
|
|
1079
|
+
let newLeft = startLeft + deltaX - containerRect.left;
|
|
1080
|
+
let newTop = startTop + deltaY - containerRect.top;
|
|
1081
|
+
|
|
1082
|
+
const controlsHeight = 95; // Height of controls when visible
|
|
1083
|
+
|
|
1084
|
+
// Constrain to videoWrapper bounds (ensuring it stays above controls)
|
|
1085
|
+
newLeft = Math.max(videoWrapperLeft, Math.min(newLeft, videoWrapperLeft + videoWrapperRect.width - wrapperRect.width));
|
|
1086
|
+
newTop = Math.max(videoWrapperTop, Math.min(newTop, videoWrapperTop + videoWrapperRect.height - wrapperRect.height - controlsHeight));
|
|
1087
|
+
|
|
1088
|
+
this.signLanguageWrapper.style.left = `${newLeft}px`;
|
|
1089
|
+
this.signLanguageWrapper.style.top = `${newTop}px`;
|
|
1090
|
+
this.signLanguageWrapper.style.right = 'auto';
|
|
1091
|
+
this.signLanguageWrapper.style.bottom = 'auto';
|
|
1092
|
+
// Remove position classes
|
|
1093
|
+
this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter(c => c.startsWith('vidply-sign-position-')));
|
|
1094
|
+
} else if (isResizing) {
|
|
1095
|
+
const deltaX = e.clientX - startX;
|
|
1096
|
+
|
|
1097
|
+
// Get videoWrapper and container dimensions
|
|
1098
|
+
const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
|
|
1099
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
1100
|
+
|
|
1101
|
+
let newWidth = startWidth;
|
|
1102
|
+
let newLeft = startLeft - containerRect.left;
|
|
1103
|
+
|
|
1104
|
+
// Only resize width, let height auto-adjust to maintain aspect ratio
|
|
1105
|
+
if (resizeDirection.includes('e')) {
|
|
1106
|
+
newWidth = Math.max(150, startWidth + deltaX);
|
|
1107
|
+
// Constrain width to not exceed videoWrapper right edge
|
|
1108
|
+
const maxWidth = (videoWrapperRect.right - startLeft);
|
|
1109
|
+
newWidth = Math.min(newWidth, maxWidth);
|
|
1110
|
+
}
|
|
1111
|
+
if (resizeDirection.includes('w')) {
|
|
1112
|
+
const proposedWidth = Math.max(150, startWidth - deltaX);
|
|
1113
|
+
const proposedLeft = startLeft + (startWidth - proposedWidth) - containerRect.left;
|
|
1114
|
+
// Constrain to not go beyond videoWrapper left edge
|
|
1115
|
+
const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
|
|
1116
|
+
if (proposedLeft >= videoWrapperLeft) {
|
|
1117
|
+
newWidth = proposedWidth;
|
|
1118
|
+
newLeft = proposedLeft;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
this.signLanguageWrapper.style.width = `${newWidth}px`;
|
|
1123
|
+
this.signLanguageWrapper.style.height = 'auto'; // Let video maintain aspect ratio
|
|
1124
|
+
if (resizeDirection.includes('w')) {
|
|
1125
|
+
this.signLanguageWrapper.style.left = `${newLeft}px`;
|
|
1126
|
+
}
|
|
1127
|
+
this.signLanguageWrapper.style.right = 'auto';
|
|
1128
|
+
this.signLanguageWrapper.style.bottom = 'auto';
|
|
1129
|
+
// Remove position classes
|
|
1130
|
+
this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter(c => c.startsWith('vidply-sign-position-')));
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
const onMouseUp = () => {
|
|
1135
|
+
if (isDragging || isResizing) {
|
|
1136
|
+
this.saveSignLanguagePreferences();
|
|
1137
|
+
}
|
|
1138
|
+
isDragging = false;
|
|
1139
|
+
isResizing = false;
|
|
1140
|
+
resizeDirection = null;
|
|
1141
|
+
this.signLanguageWrapper.classList.remove('vidply-sign-dragging', 'vidply-sign-resizing');
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
// Keyboard controls
|
|
1145
|
+
const onKeyDown = (e) => {
|
|
1146
|
+
// Toggle drag mode with D key
|
|
1147
|
+
if (e.key === 'd' || e.key === 'D') {
|
|
1148
|
+
dragMode = !dragMode;
|
|
1149
|
+
resizeMode = false;
|
|
1150
|
+
this.signLanguageWrapper.classList.toggle('vidply-sign-keyboard-drag', dragMode);
|
|
1151
|
+
this.signLanguageWrapper.classList.remove('vidply-sign-keyboard-resize');
|
|
1152
|
+
e.preventDefault();
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Toggle resize mode with R key
|
|
1157
|
+
if (e.key === 'r' || e.key === 'R') {
|
|
1158
|
+
resizeMode = !resizeMode;
|
|
1159
|
+
dragMode = false;
|
|
1160
|
+
this.signLanguageWrapper.classList.toggle('vidply-sign-keyboard-resize', resizeMode);
|
|
1161
|
+
this.signLanguageWrapper.classList.remove('vidply-sign-keyboard-drag');
|
|
1162
|
+
e.preventDefault();
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Escape to exit modes
|
|
1167
|
+
if (e.key === 'Escape') {
|
|
1168
|
+
dragMode = false;
|
|
1169
|
+
resizeMode = false;
|
|
1170
|
+
this.signLanguageWrapper.classList.remove('vidply-sign-keyboard-drag', 'vidply-sign-keyboard-resize');
|
|
1171
|
+
e.preventDefault();
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Arrow keys for drag/resize
|
|
1176
|
+
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
|
1177
|
+
const step = e.shiftKey ? 10 : 5;
|
|
1178
|
+
const rect = this.signLanguageWrapper.getBoundingClientRect();
|
|
1179
|
+
|
|
1180
|
+
// Get videoWrapper and container bounds
|
|
1181
|
+
const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
|
|
1182
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
1183
|
+
|
|
1184
|
+
// Calculate videoWrapper position relative to container
|
|
1185
|
+
const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
|
|
1186
|
+
const videoWrapperTop = videoWrapperRect.top - containerRect.top;
|
|
1187
|
+
|
|
1188
|
+
if (dragMode) {
|
|
1189
|
+
// Get current position relative to container
|
|
1190
|
+
let left = rect.left - containerRect.left;
|
|
1191
|
+
let top = rect.top - containerRect.top;
|
|
1192
|
+
|
|
1193
|
+
if (e.key === 'ArrowLeft') left -= step;
|
|
1194
|
+
if (e.key === 'ArrowRight') left += step;
|
|
1195
|
+
if (e.key === 'ArrowUp') top -= step;
|
|
1196
|
+
if (e.key === 'ArrowDown') top += step;
|
|
1197
|
+
|
|
1198
|
+
const controlsHeight = 95; // Height of controls when visible
|
|
1199
|
+
|
|
1200
|
+
// Constrain to videoWrapper bounds (ensuring it stays above controls)
|
|
1201
|
+
left = Math.max(videoWrapperLeft, Math.min(left, videoWrapperLeft + videoWrapperRect.width - rect.width));
|
|
1202
|
+
top = Math.max(videoWrapperTop, Math.min(top, videoWrapperTop + videoWrapperRect.height - rect.height - controlsHeight));
|
|
1203
|
+
|
|
1204
|
+
this.signLanguageWrapper.style.left = `${left}px`;
|
|
1205
|
+
this.signLanguageWrapper.style.top = `${top}px`;
|
|
1206
|
+
this.signLanguageWrapper.style.right = 'auto';
|
|
1207
|
+
this.signLanguageWrapper.style.bottom = 'auto';
|
|
1208
|
+
// Remove position classes
|
|
1209
|
+
this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter(c => c.startsWith('vidply-sign-position-')));
|
|
1210
|
+
this.saveSignLanguagePreferences();
|
|
1211
|
+
e.preventDefault();
|
|
1212
|
+
} else if (resizeMode) {
|
|
1213
|
+
let width = rect.width;
|
|
1214
|
+
|
|
1215
|
+
// Only adjust width, height will auto-adjust to maintain aspect ratio
|
|
1216
|
+
if (e.key === 'ArrowLeft') width -= step;
|
|
1217
|
+
if (e.key === 'ArrowRight') width += step;
|
|
1218
|
+
// Up/Down also adjusts width for simplicity
|
|
1219
|
+
if (e.key === 'ArrowUp') width += step;
|
|
1220
|
+
if (e.key === 'ArrowDown') width -= step;
|
|
1221
|
+
|
|
1222
|
+
// Constrain width
|
|
1223
|
+
width = Math.max(150, width);
|
|
1224
|
+
// Don't let it exceed videoWrapper width
|
|
1225
|
+
width = Math.min(width, videoWrapperRect.width);
|
|
1226
|
+
|
|
1227
|
+
this.signLanguageWrapper.style.width = `${width}px`;
|
|
1228
|
+
this.signLanguageWrapper.style.height = 'auto';
|
|
1229
|
+
this.saveSignLanguagePreferences();
|
|
1230
|
+
e.preventDefault();
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
// Attach event listeners
|
|
1236
|
+
this.signLanguageVideo.addEventListener('mousedown', onMouseDownVideo);
|
|
1237
|
+
const handles = this.signLanguageWrapper.querySelectorAll('.vidply-sign-resize-handle');
|
|
1238
|
+
handles.forEach(handle => handle.addEventListener('mousedown', onMouseDownHandle));
|
|
1239
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
1240
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
1241
|
+
this.signLanguageWrapper.addEventListener('keydown', onKeyDown);
|
|
1242
|
+
|
|
1243
|
+
// Store for cleanup
|
|
1244
|
+
this.signLanguageInteractionHandlers = {
|
|
1245
|
+
mouseDownVideo: onMouseDownVideo,
|
|
1246
|
+
mouseDownHandle: onMouseDownHandle,
|
|
1247
|
+
mouseMove: onMouseMove,
|
|
1248
|
+
mouseUp: onMouseUp,
|
|
1249
|
+
keyDown: onKeyDown,
|
|
1250
|
+
handles
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
constrainSignLanguagePosition() {
|
|
1255
|
+
if (!this.signLanguageWrapper || !this.videoWrapper) return;
|
|
1256
|
+
|
|
1257
|
+
// Ensure width is set
|
|
1258
|
+
if (!this.signLanguageWrapper.style.width || this.signLanguageWrapper.style.width === '') {
|
|
1259
|
+
this.signLanguageWrapper.style.width = '280px'; // Default width
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Get videoWrapper position relative to the player CONTAINER (where sign language video is attached)
|
|
1263
|
+
const videoWrapperRect = this.videoWrapper.getBoundingClientRect();
|
|
1264
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
1265
|
+
const wrapperRect = this.signLanguageWrapper.getBoundingClientRect();
|
|
1266
|
+
|
|
1267
|
+
// Calculate videoWrapper's position and dimensions relative to container
|
|
1268
|
+
const videoWrapperLeft = videoWrapperRect.left - containerRect.left;
|
|
1269
|
+
const videoWrapperTop = videoWrapperRect.top - containerRect.top;
|
|
1270
|
+
const videoWrapperWidth = videoWrapperRect.width;
|
|
1271
|
+
const videoWrapperHeight = videoWrapperRect.height;
|
|
1272
|
+
|
|
1273
|
+
// Use estimated height if video hasn't loaded yet (16:9 aspect ratio)
|
|
1274
|
+
let wrapperWidth = wrapperRect.width || 280;
|
|
1275
|
+
let wrapperHeight = wrapperRect.height || ((280 * 9) / 16); // Estimate based on 16:9 aspect ratio
|
|
1276
|
+
|
|
1277
|
+
let left, top;
|
|
1278
|
+
const margin = 16; // Margin from edges
|
|
1279
|
+
const controlsHeight = 95; // Height of controls when visible
|
|
1280
|
+
|
|
1281
|
+
// Always calculate fresh position based on desired location (relative to videoWrapper)
|
|
1282
|
+
const position = this.signLanguageDesiredPosition || 'bottom-right';
|
|
1283
|
+
|
|
1284
|
+
switch (position) {
|
|
1285
|
+
case 'bottom-right':
|
|
1286
|
+
left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
|
|
1287
|
+
top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
|
|
1288
|
+
break;
|
|
1289
|
+
case 'bottom-left':
|
|
1290
|
+
left = videoWrapperLeft + margin;
|
|
1291
|
+
top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
|
|
1292
|
+
break;
|
|
1293
|
+
case 'top-right':
|
|
1294
|
+
left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
|
|
1295
|
+
top = videoWrapperTop + margin;
|
|
1296
|
+
break;
|
|
1297
|
+
case 'top-left':
|
|
1298
|
+
left = videoWrapperLeft + margin;
|
|
1299
|
+
top = videoWrapperTop + margin;
|
|
1300
|
+
break;
|
|
1301
|
+
default:
|
|
1302
|
+
left = videoWrapperLeft + videoWrapperWidth - wrapperWidth - margin;
|
|
1303
|
+
top = videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// Constrain to videoWrapper bounds (ensuring it stays above controls)
|
|
1307
|
+
left = Math.max(videoWrapperLeft, Math.min(left, videoWrapperLeft + videoWrapperWidth - wrapperWidth));
|
|
1308
|
+
top = Math.max(videoWrapperTop, Math.min(top, videoWrapperTop + videoWrapperHeight - wrapperHeight - controlsHeight));
|
|
1309
|
+
|
|
1310
|
+
// Apply constrained position
|
|
1311
|
+
this.signLanguageWrapper.style.left = `${left}px`;
|
|
1312
|
+
this.signLanguageWrapper.style.top = `${top}px`;
|
|
1313
|
+
this.signLanguageWrapper.style.right = 'auto';
|
|
1314
|
+
this.signLanguageWrapper.style.bottom = 'auto';
|
|
1315
|
+
// Remove position classes if any were applied
|
|
1316
|
+
this.signLanguageWrapper.classList.remove(...Array.from(this.signLanguageWrapper.classList).filter(c => c.startsWith('vidply-sign-position-')));
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
saveSignLanguagePreferences() {
|
|
1320
|
+
if (!this.signLanguageWrapper) return;
|
|
1321
|
+
|
|
1322
|
+
// Only save width - position is always calculated fresh to bottom-right
|
|
1323
|
+
this.storage.saveSignLanguagePreferences({
|
|
1324
|
+
size: {
|
|
1325
|
+
width: this.signLanguageWrapper.style.width
|
|
1326
|
+
// Height is auto - maintained by aspect ratio
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
cleanupSignLanguage() {
|
|
1332
|
+
// Remove event listeners
|
|
1333
|
+
if (this.signLanguageHandlers) {
|
|
1334
|
+
this.off('play', this.signLanguageHandlers.play);
|
|
1335
|
+
this.off('pause', this.signLanguageHandlers.pause);
|
|
1336
|
+
this.off('timeupdate', this.signLanguageHandlers.timeupdate);
|
|
1337
|
+
this.off('ratechange', this.signLanguageHandlers.ratechange);
|
|
1338
|
+
this.signLanguageHandlers = null;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Remove interaction handlers
|
|
1342
|
+
if (this.signLanguageInteractionHandlers) {
|
|
1343
|
+
if (this.signLanguageVideo) {
|
|
1344
|
+
this.signLanguageVideo.removeEventListener('mousedown', this.signLanguageInteractionHandlers.mouseDownVideo);
|
|
1345
|
+
}
|
|
1346
|
+
if (this.signLanguageInteractionHandlers.handles) {
|
|
1347
|
+
this.signLanguageInteractionHandlers.handles.forEach(handle => {
|
|
1348
|
+
handle.removeEventListener('mousedown', this.signLanguageInteractionHandlers.mouseDownHandle);
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
document.removeEventListener('mousemove', this.signLanguageInteractionHandlers.mouseMove);
|
|
1352
|
+
document.removeEventListener('mouseup', this.signLanguageInteractionHandlers.mouseUp);
|
|
1353
|
+
if (this.signLanguageWrapper) {
|
|
1354
|
+
this.signLanguageWrapper.removeEventListener('keydown', this.signLanguageInteractionHandlers.keyDown);
|
|
1355
|
+
}
|
|
1356
|
+
this.signLanguageInteractionHandlers = null;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Remove video and wrapper elements
|
|
1360
|
+
if (this.signLanguageWrapper && this.signLanguageWrapper.parentNode) {
|
|
1361
|
+
if (this.signLanguageVideo) {
|
|
1362
|
+
this.signLanguageVideo.pause();
|
|
1363
|
+
this.signLanguageVideo.src = '';
|
|
1364
|
+
}
|
|
1365
|
+
this.signLanguageWrapper.parentNode.removeChild(this.signLanguageWrapper);
|
|
1366
|
+
this.signLanguageWrapper = null;
|
|
1367
|
+
this.signLanguageVideo = null;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// Settings
|
|
1372
|
+
// Settings dialog removed - using individual control buttons instead
|
|
1373
|
+
showSettings() {
|
|
1374
|
+
console.warn('[VidPly] Settings dialog has been removed. Use individual control buttons (speed, captions, etc.)');
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
hideSettings() {
|
|
1378
|
+
// No-op - settings dialog removed
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// Utility methods
|
|
1382
|
+
getCurrentTime() {
|
|
1383
|
+
return this.state.currentTime;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
getDuration() {
|
|
1387
|
+
return this.state.duration;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
isPlaying() {
|
|
1391
|
+
return this.state.playing;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
isPaused() {
|
|
1395
|
+
return this.state.paused;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
isEnded() {
|
|
1399
|
+
return this.state.ended;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
isMuted() {
|
|
1403
|
+
return this.state.muted;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
isFullscreen() {
|
|
1407
|
+
return this.state.fullscreen;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// Error handling
|
|
1411
|
+
handleError(error) {
|
|
1412
|
+
this.log('Error:', error, 'error');
|
|
1413
|
+
this.emit('error', error);
|
|
1414
|
+
|
|
1415
|
+
if (this.options.onError) {
|
|
1416
|
+
this.options.onError.call(this, error);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// Logging
|
|
1421
|
+
log(message, type = 'log') {
|
|
1422
|
+
if (this.options.debug) {
|
|
1423
|
+
console[type](`[VidPly]`, message);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// Setup responsive handlers
|
|
1428
|
+
setupResponsiveHandlers() {
|
|
1429
|
+
// Use ResizeObserver for efficient resize tracking
|
|
1430
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
1431
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
1432
|
+
for (const entry of entries) {
|
|
1433
|
+
const width = entry.contentRect.width;
|
|
1434
|
+
|
|
1435
|
+
// Update control bar for viewport
|
|
1436
|
+
if (this.controlBar && typeof this.controlBar.updateControlsForViewport === 'function') {
|
|
1437
|
+
this.controlBar.updateControlsForViewport(width);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// Update transcript positioning
|
|
1441
|
+
if (this.transcriptManager && this.transcriptManager.isVisible) {
|
|
1442
|
+
this.transcriptManager.positionTranscript();
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
this.resizeObserver.observe(this.container);
|
|
1448
|
+
} else {
|
|
1449
|
+
// Fallback to window resize event
|
|
1450
|
+
this.resizeHandler = () => {
|
|
1451
|
+
const width = this.container.clientWidth;
|
|
1452
|
+
|
|
1453
|
+
if (this.controlBar && typeof this.controlBar.updateControlsForViewport === 'function') {
|
|
1454
|
+
this.controlBar.updateControlsForViewport(width);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
if (this.transcriptManager && this.transcriptManager.isVisible) {
|
|
1458
|
+
this.transcriptManager.positionTranscript();
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1461
|
+
|
|
1462
|
+
window.addEventListener('resize', this.resizeHandler);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// Also listen for orientation changes on mobile
|
|
1466
|
+
if (window.matchMedia) {
|
|
1467
|
+
this.orientationHandler = (e) => {
|
|
1468
|
+
// Wait for layout to settle
|
|
1469
|
+
setTimeout(() => {
|
|
1470
|
+
if (this.transcriptManager && this.transcriptManager.isVisible) {
|
|
1471
|
+
this.transcriptManager.positionTranscript();
|
|
1472
|
+
}
|
|
1473
|
+
}, 100);
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
const orientationQuery = window.matchMedia('(orientation: portrait)');
|
|
1477
|
+
if (orientationQuery.addEventListener) {
|
|
1478
|
+
orientationQuery.addEventListener('change', this.orientationHandler);
|
|
1479
|
+
} else if (orientationQuery.addListener) {
|
|
1480
|
+
// Fallback for older browsers
|
|
1481
|
+
orientationQuery.addListener(this.orientationHandler);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
this.orientationQuery = orientationQuery;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Listen for native fullscreen change events (e.g., when user presses ESC)
|
|
1488
|
+
this.fullscreenChangeHandler = () => {
|
|
1489
|
+
const isFullscreen = !!(
|
|
1490
|
+
document.fullscreenElement ||
|
|
1491
|
+
document.webkitFullscreenElement ||
|
|
1492
|
+
document.mozFullScreenElement ||
|
|
1493
|
+
document.msFullscreenElement
|
|
1494
|
+
);
|
|
1495
|
+
|
|
1496
|
+
// Only update if state has changed
|
|
1497
|
+
if (this.state.fullscreen !== isFullscreen) {
|
|
1498
|
+
this.state.fullscreen = isFullscreen;
|
|
1499
|
+
|
|
1500
|
+
if (isFullscreen) {
|
|
1501
|
+
this.container.classList.add(`${this.options.classPrefix}-fullscreen`);
|
|
1502
|
+
} else {
|
|
1503
|
+
this.container.classList.remove(`${this.options.classPrefix}-fullscreen`);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
this.emit('fullscreenchange', isFullscreen);
|
|
1507
|
+
|
|
1508
|
+
// Update fullscreen button icon
|
|
1509
|
+
if (this.controlBar) {
|
|
1510
|
+
this.controlBar.updateFullscreenButton();
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// Reposition sign language video after fullscreen transition
|
|
1514
|
+
if (this.signLanguageWrapper && this.signLanguageWrapper.style.display !== 'none') {
|
|
1515
|
+
// Use setTimeout to ensure layout has updated after fullscreen transition
|
|
1516
|
+
// Longer delay to account for CSS transition animations and layout recalculation
|
|
1517
|
+
setTimeout(() => {
|
|
1518
|
+
// Use requestAnimationFrame to ensure the browser has fully rendered the layout
|
|
1519
|
+
requestAnimationFrame(() => {
|
|
1520
|
+
// Clear saved size and reset to default for the new container size
|
|
1521
|
+
this.storage.saveSignLanguagePreferences({ size: null });
|
|
1522
|
+
this.signLanguageDesiredPosition = 'bottom-right';
|
|
1523
|
+
// Reset to default width for the new container
|
|
1524
|
+
this.signLanguageWrapper.style.width = isFullscreen ? '400px' : '280px';
|
|
1525
|
+
this.constrainSignLanguagePosition();
|
|
1526
|
+
});
|
|
1527
|
+
}, 500);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
// Add listeners for all vendor-prefixed fullscreenchange events
|
|
1533
|
+
document.addEventListener('fullscreenchange', this.fullscreenChangeHandler);
|
|
1534
|
+
document.addEventListener('webkitfullscreenchange', this.fullscreenChangeHandler);
|
|
1535
|
+
document.addEventListener('mozfullscreenchange', this.fullscreenChangeHandler);
|
|
1536
|
+
document.addEventListener('MSFullscreenChange', this.fullscreenChangeHandler);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// Cleanup
|
|
1540
|
+
destroy() {
|
|
1541
|
+
this.log('Destroying player');
|
|
1542
|
+
|
|
1543
|
+
if (this.renderer) {
|
|
1544
|
+
this.renderer.destroy();
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
if (this.controlBar) {
|
|
1548
|
+
this.controlBar.destroy();
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
if (this.captionManager) {
|
|
1552
|
+
this.captionManager.destroy();
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
if (this.keyboardManager) {
|
|
1556
|
+
this.keyboardManager.destroy();
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
if (this.transcriptManager) {
|
|
1560
|
+
this.transcriptManager.destroy();
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// Cleanup sign language video and listeners
|
|
1564
|
+
this.cleanupSignLanguage();
|
|
1565
|
+
|
|
1566
|
+
// Cleanup play overlay button
|
|
1567
|
+
if (this.playButtonOverlay && this.playButtonOverlay.parentNode) {
|
|
1568
|
+
this.playButtonOverlay.remove();
|
|
1569
|
+
this.playButtonOverlay = null;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// Cleanup resize observer
|
|
1573
|
+
if (this.resizeObserver) {
|
|
1574
|
+
this.resizeObserver.disconnect();
|
|
1575
|
+
this.resizeObserver = null;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// Cleanup window resize handler
|
|
1579
|
+
if (this.resizeHandler) {
|
|
1580
|
+
window.removeEventListener('resize', this.resizeHandler);
|
|
1581
|
+
this.resizeHandler = null;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// Cleanup orientation change handler
|
|
1585
|
+
if (this.orientationQuery && this.orientationHandler) {
|
|
1586
|
+
if (this.orientationQuery.removeEventListener) {
|
|
1587
|
+
this.orientationQuery.removeEventListener('change', this.orientationHandler);
|
|
1588
|
+
} else if (this.orientationQuery.removeListener) {
|
|
1589
|
+
this.orientationQuery.removeListener(this.orientationHandler);
|
|
1590
|
+
}
|
|
1591
|
+
this.orientationQuery = null;
|
|
1592
|
+
this.orientationHandler = null;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// Cleanup fullscreen change handler
|
|
1596
|
+
if (this.fullscreenChangeHandler) {
|
|
1597
|
+
document.removeEventListener('fullscreenchange', this.fullscreenChangeHandler);
|
|
1598
|
+
document.removeEventListener('webkitfullscreenchange', this.fullscreenChangeHandler);
|
|
1599
|
+
document.removeEventListener('mozfullscreenchange', this.fullscreenChangeHandler);
|
|
1600
|
+
document.removeEventListener('MSFullscreenChange', this.fullscreenChangeHandler);
|
|
1601
|
+
this.fullscreenChangeHandler = null;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// Remove container
|
|
1605
|
+
if (this.container && this.container.parentNode) {
|
|
1606
|
+
this.container.parentNode.insertBefore(this.element, this.container);
|
|
1607
|
+
this.container.parentNode.removeChild(this.container);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
this.removeAllListeners();
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// Static instances tracker for pause others functionality
|
|
1615
|
+
Player.instances = [];
|
|
1616
|
+
|