@openplayerjs/player 3.4.0 → 3.4.2
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/dist/.tsbuildinfo +1 -1
- package/dist/index.js +237 -82
- package/dist/index.js.map +1 -1
- package/dist/openplayer.css +1 -1
- package/dist/openplayer.js +1 -1
- package/dist/openplayer.js.map +1 -1
- package/dist/types/a11y.d.ts +15 -0
- package/dist/types/a11y.d.ts.map +1 -1
- package/dist/types/configuration.d.ts +7 -1
- package/dist/types/configuration.d.ts.map +1 -1
- package/dist/types/controls/captions.d.ts.map +1 -1
- package/dist/types/controls/duration.d.ts.map +1 -1
- package/dist/types/controls/fullscreen.d.ts.map +1 -1
- package/dist/types/controls/play.d.ts.map +1 -1
- package/dist/types/controls/progress.d.ts.map +1 -1
- package/dist/types/controls/settings.d.ts.map +1 -1
- package/dist/types/controls/time.d.ts.map +1 -1
- package/dist/types/controls/volume.d.ts.map +1 -1
- package/dist/types/events.d.ts +1 -1
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/ui.d.ts.map +1 -1
- package/package.json +3 -3
- package/dist/types/a11y-announcer.d.ts +0 -36
- package/dist/types/a11y-announcer.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -7,11 +7,15 @@ const defaultUIConfiguration = {
|
|
|
7
7
|
};
|
|
8
8
|
const defaultLabels = Object.freeze({
|
|
9
9
|
auto: 'Auto',
|
|
10
|
+
back: 'Back',
|
|
10
11
|
captions: 'CC/Subtitles',
|
|
12
|
+
captionsOff: 'Captions off',
|
|
13
|
+
captionsOn: 'Captions on',
|
|
11
14
|
click: 'Click to unmute',
|
|
12
15
|
container: 'Media player',
|
|
16
|
+
enterFullscreen: 'Enter Fullscreen',
|
|
17
|
+
exitFullscreen: 'Exit Fullscreen',
|
|
13
18
|
fullscreen: 'Fullscreen',
|
|
14
|
-
levels: 'Quality Levels',
|
|
15
19
|
live: 'Live',
|
|
16
20
|
loading: 'Loading...',
|
|
17
21
|
media: 'Media',
|
|
@@ -22,6 +26,7 @@ const defaultLabels = Object.freeze({
|
|
|
22
26
|
progressRail: 'Time Rail',
|
|
23
27
|
progressSlider: 'Time Slider',
|
|
24
28
|
restart: 'Restart',
|
|
29
|
+
seekTo: 'Seek to %s',
|
|
25
30
|
settings: 'Player Settings',
|
|
26
31
|
speed: 'Speed',
|
|
27
32
|
speedNormal: 'Normal',
|
|
@@ -30,6 +35,7 @@ const defaultLabels = Object.freeze({
|
|
|
30
35
|
unmute: 'Unmute',
|
|
31
36
|
volume: 'Volume',
|
|
32
37
|
volumeControl: 'Volume Control',
|
|
38
|
+
volumePercent: 'Volume: %s%',
|
|
33
39
|
volumeSlider: 'Volume Slider',
|
|
34
40
|
});
|
|
35
41
|
function resolveUIConfig(coreOrConfig) {
|
|
@@ -206,7 +212,7 @@ function bindCenterOverlay(core, keyTarget, bindings) {
|
|
|
206
212
|
window.addEventListener('click', onPointer, EVENT_OPTIONS);
|
|
207
213
|
window.addEventListener('pointerdown', onPointer, EVENT_OPTIONS);
|
|
208
214
|
window.addEventListener('keydown', onKeyboard, EVENT_OPTIONS);
|
|
209
|
-
|
|
215
|
+
const onKeydown = async (e) => {
|
|
210
216
|
const key = e.key;
|
|
211
217
|
onKeyboard();
|
|
212
218
|
const activeEl = document.activeElement;
|
|
@@ -246,23 +252,20 @@ function bindCenterOverlay(core, keyTarget, bindings) {
|
|
|
246
252
|
e.preventDefault();
|
|
247
253
|
e.stopPropagation();
|
|
248
254
|
break;
|
|
249
|
-
// Volume
|
|
250
255
|
case 'ArrowUp': {
|
|
251
256
|
const upVolume = Math.min(core.volume + 0.1, 1);
|
|
252
257
|
core.volume = upVolume;
|
|
253
258
|
core.muted = !(upVolume > 0);
|
|
254
259
|
if (upVolume > 0)
|
|
255
260
|
lastNonZeroVolume = upVolume;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
261
|
+
try {
|
|
262
|
+
const el = getActiveMedia(core);
|
|
263
|
+
if (el && el !== core.surface) {
|
|
259
264
|
el.volume = upVolume;
|
|
260
265
|
el.muted = !(upVolume > 0);
|
|
261
266
|
}
|
|
262
|
-
catch {
|
|
263
|
-
// ignore
|
|
264
|
-
}
|
|
265
267
|
}
|
|
268
|
+
catch { /* ignore */ }
|
|
266
269
|
e.preventDefault();
|
|
267
270
|
e.stopPropagation();
|
|
268
271
|
break;
|
|
@@ -273,16 +276,14 @@ function bindCenterOverlay(core, keyTarget, bindings) {
|
|
|
273
276
|
core.muted = !(downVolume > 0);
|
|
274
277
|
if (downVolume > 0)
|
|
275
278
|
lastNonZeroVolume = downVolume;
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
+
try {
|
|
280
|
+
const el = getActiveMedia(core);
|
|
281
|
+
if (el && el !== core.surface) {
|
|
279
282
|
el.volume = downVolume;
|
|
280
283
|
el.muted = !(downVolume > 0);
|
|
281
284
|
}
|
|
282
|
-
catch {
|
|
283
|
-
// ignore
|
|
284
|
-
}
|
|
285
285
|
}
|
|
286
|
+
catch { /* ignore */ }
|
|
286
287
|
e.preventDefault();
|
|
287
288
|
e.stopPropagation();
|
|
288
289
|
break;
|
|
@@ -305,40 +306,35 @@ function bindCenterOverlay(core, keyTarget, bindings) {
|
|
|
305
306
|
e.stopPropagation();
|
|
306
307
|
break;
|
|
307
308
|
}
|
|
308
|
-
// Mute toggle preserving last volume
|
|
309
309
|
case 'm':
|
|
310
310
|
case 'M': {
|
|
311
|
-
const el = getActiveMedia(core);
|
|
312
311
|
const nextMuted = !core.muted;
|
|
313
312
|
if (nextMuted) {
|
|
314
|
-
// store last non-zero volume before muting
|
|
315
313
|
if (core.volume > 0)
|
|
316
314
|
lastNonZeroVolume = core.volume;
|
|
317
315
|
core.volume = 0;
|
|
318
316
|
core.muted = true;
|
|
319
|
-
|
|
320
|
-
|
|
317
|
+
try {
|
|
318
|
+
const el = getActiveMedia(core);
|
|
319
|
+
if (el && el !== core.surface) {
|
|
321
320
|
el.volume = 0;
|
|
322
321
|
el.muted = true;
|
|
323
322
|
}
|
|
324
|
-
catch {
|
|
325
|
-
// ignore
|
|
326
|
-
}
|
|
327
323
|
}
|
|
324
|
+
catch { /* ignore */ }
|
|
328
325
|
}
|
|
329
326
|
else {
|
|
330
327
|
const restore = lastNonZeroVolume > 0 ? lastNonZeroVolume : 1;
|
|
331
328
|
core.volume = restore;
|
|
332
329
|
core.muted = false;
|
|
333
|
-
|
|
334
|
-
|
|
330
|
+
try {
|
|
331
|
+
const el = getActiveMedia(core);
|
|
332
|
+
if (el && el !== core.surface) {
|
|
335
333
|
el.volume = restore;
|
|
336
334
|
el.muted = false;
|
|
337
335
|
}
|
|
338
|
-
catch {
|
|
339
|
-
// ignore
|
|
340
|
-
}
|
|
341
336
|
}
|
|
337
|
+
catch { /* ignore */ }
|
|
342
338
|
}
|
|
343
339
|
e.preventDefault();
|
|
344
340
|
e.stopPropagation();
|
|
@@ -375,38 +371,55 @@ function bindCenterOverlay(core, keyTarget, bindings) {
|
|
|
375
371
|
e.stopPropagation();
|
|
376
372
|
break;
|
|
377
373
|
}
|
|
378
|
-
}
|
|
379
|
-
|
|
374
|
+
};
|
|
375
|
+
keyTarget.addEventListener('keydown', onKeydown, EVENT_OPTIONS);
|
|
376
|
+
const offWaiting = core.events.on('waiting', () => {
|
|
380
377
|
bindings?.showLoader(true);
|
|
381
378
|
bindings?.showButton(false);
|
|
382
379
|
});
|
|
383
|
-
core.events.on('seeking', () => {
|
|
380
|
+
const offSeeking = core.events.on('seeking', () => {
|
|
384
381
|
bindings?.showLoader(true);
|
|
385
382
|
bindings?.showButton(false);
|
|
386
383
|
});
|
|
387
|
-
core.events.on('seeked', () => {
|
|
384
|
+
const offSeeked = core.events.on('seeked', () => {
|
|
388
385
|
bindings?.showLoader(false);
|
|
389
386
|
// After seeking, only show the center button if we are paused.
|
|
390
387
|
// When seeking during playback (common on iOS), keeping the play button visible
|
|
391
388
|
// causes it to linger even after the loader is hidden.
|
|
392
389
|
bindings?.showButton(core.media?.paused ?? false);
|
|
393
390
|
});
|
|
394
|
-
core.events.on('play', () => {
|
|
391
|
+
const offPlay = core.events.on('play', () => {
|
|
395
392
|
bindings?.showLoader(false);
|
|
396
393
|
bindings?.showButton(false);
|
|
397
394
|
});
|
|
398
|
-
core.events.on('pause', () => {
|
|
395
|
+
const offPause = core.events.on('pause', () => {
|
|
399
396
|
bindings?.showLoader(false);
|
|
400
397
|
bindings?.showButton(true);
|
|
401
398
|
});
|
|
402
|
-
core.events.on('playing', () => {
|
|
399
|
+
const offPlaying = core.events.on('playing', () => {
|
|
403
400
|
bindings?.showLoader(false);
|
|
404
401
|
bindings?.showButton(false);
|
|
405
402
|
});
|
|
406
|
-
core.events.on('ended', () => {
|
|
403
|
+
const offEnded = core.events.on('ended', () => {
|
|
407
404
|
bindings?.showLoader(false);
|
|
408
405
|
bindings?.showButton(true);
|
|
409
406
|
});
|
|
407
|
+
return () => {
|
|
408
|
+
keyTarget.removeEventListener('click', onPointer);
|
|
409
|
+
keyTarget.removeEventListener('pointerdown', onPointer);
|
|
410
|
+
keyTarget.removeEventListener('pointerleave', onPointer);
|
|
411
|
+
keyTarget.removeEventListener('keydown', onKeydown);
|
|
412
|
+
window.removeEventListener('click', onPointer);
|
|
413
|
+
window.removeEventListener('pointerdown', onPointer);
|
|
414
|
+
window.removeEventListener('keydown', onKeyboard);
|
|
415
|
+
offWaiting();
|
|
416
|
+
offSeeking();
|
|
417
|
+
offSeeked();
|
|
418
|
+
offPlay();
|
|
419
|
+
offPause();
|
|
420
|
+
offPlaying();
|
|
421
|
+
offEnded();
|
|
422
|
+
};
|
|
410
423
|
}
|
|
411
424
|
|
|
412
425
|
let srId$1 = 0;
|
|
@@ -462,6 +475,60 @@ function setA11yLabel(el, labelText, opts) {
|
|
|
462
475
|
}
|
|
463
476
|
ensureLabelledBy(el, labelText, opts);
|
|
464
477
|
}
|
|
478
|
+
const _sharedAnnouncers = new WeakMap();
|
|
479
|
+
/**
|
|
480
|
+
* Returns the shared announcer for `wrapper`, creating it on first call.
|
|
481
|
+
* Multiple controls attached to the same wrapper share the same 2 live-region
|
|
482
|
+
* nodes. Each caller receives its own `destroy` handle; the DOM nodes are only
|
|
483
|
+
* removed once every caller has destroyed its handle (ref-counting).
|
|
484
|
+
*/
|
|
485
|
+
function getSharedAnnouncer(wrapper) {
|
|
486
|
+
let entry = _sharedAnnouncers.get(wrapper);
|
|
487
|
+
if (!entry) {
|
|
488
|
+
const { announce, destroy } = createAnnouncer(wrapper);
|
|
489
|
+
entry = { announce, refs: 0, _destroy: destroy };
|
|
490
|
+
_sharedAnnouncers.set(wrapper, entry);
|
|
491
|
+
}
|
|
492
|
+
entry.refs += 1;
|
|
493
|
+
const captured = entry;
|
|
494
|
+
const destroy = () => {
|
|
495
|
+
captured.refs -= 1;
|
|
496
|
+
if (captured.refs <= 0) {
|
|
497
|
+
captured._destroy();
|
|
498
|
+
_sharedAnnouncers.delete(wrapper);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
return { announce: entry.announce, destroy };
|
|
502
|
+
}
|
|
503
|
+
function createAnnouncer(wrapper) {
|
|
504
|
+
const regions = [];
|
|
505
|
+
let turn = 0;
|
|
506
|
+
for (let i = 0; i < 2; i++) {
|
|
507
|
+
const el = document.createElement('div');
|
|
508
|
+
el.setAttribute('role', 'status');
|
|
509
|
+
el.setAttribute('aria-live', 'polite');
|
|
510
|
+
el.setAttribute('aria-atomic', 'true');
|
|
511
|
+
el.className = 'op-player__sr-only op-player__announcer';
|
|
512
|
+
el.setAttribute('aria-hidden', 'true');
|
|
513
|
+
wrapper.appendChild(el);
|
|
514
|
+
regions.push(el);
|
|
515
|
+
}
|
|
516
|
+
const announce = (message) => {
|
|
517
|
+
// Rotate between the two regions — forces a DOM mutation even for the
|
|
518
|
+
// same string, which re-triggers announcement in most screen readers.
|
|
519
|
+
const current = regions[turn % 2];
|
|
520
|
+
const other = regions[(turn + 1) % 2];
|
|
521
|
+
turn++;
|
|
522
|
+
other.textContent = '';
|
|
523
|
+
other.setAttribute('aria-hidden', 'true');
|
|
524
|
+
current.setAttribute('aria-hidden', 'false');
|
|
525
|
+
current.textContent = message;
|
|
526
|
+
};
|
|
527
|
+
const destroy = () => {
|
|
528
|
+
regions.forEach((r) => r.remove());
|
|
529
|
+
};
|
|
530
|
+
return { announce, destroy };
|
|
531
|
+
}
|
|
465
532
|
|
|
466
533
|
function createCenterOverlayDom(core) {
|
|
467
534
|
const labels = resolveUIConfig(core).labels;
|
|
@@ -669,10 +736,11 @@ function createUI(core, media, controls, options = {}) {
|
|
|
669
736
|
mediaContainer.appendChild(overlay.button);
|
|
670
737
|
mediaContainer.appendChild(overlay.loader);
|
|
671
738
|
}
|
|
672
|
-
bindCenterOverlay(core, wrapper, overlay);
|
|
739
|
+
const teardownCenterOverlay = bindCenterOverlay(core, wrapper, overlay);
|
|
673
740
|
const controlsRoot = document.createElement('div');
|
|
674
741
|
controlsRoot.className = 'op-controls';
|
|
675
742
|
controlsRoot.setAttribute('aria-hidden', 'false');
|
|
743
|
+
controlsRoot.inert = false;
|
|
676
744
|
if (isMediaAudio) {
|
|
677
745
|
const grid = createControlGrid(controlsRoot);
|
|
678
746
|
wrapper.appendChild(mediaContainer);
|
|
@@ -705,6 +773,7 @@ function createUI(core, media, controls, options = {}) {
|
|
|
705
773
|
// ignore
|
|
706
774
|
}
|
|
707
775
|
try {
|
|
776
|
+
teardownCenterOverlay();
|
|
708
777
|
offAddElement();
|
|
709
778
|
offAddControl();
|
|
710
779
|
offDestroy();
|
|
@@ -745,6 +814,7 @@ function createUI(core, media, controls, options = {}) {
|
|
|
745
814
|
if (hideTimer)
|
|
746
815
|
window.clearTimeout(hideTimer);
|
|
747
816
|
controlsRoot.setAttribute('aria-hidden', 'false');
|
|
817
|
+
controlsRoot.inert = false;
|
|
748
818
|
core.events.emit('ui:controls:show');
|
|
749
819
|
};
|
|
750
820
|
const hideControls = () => {
|
|
@@ -761,6 +831,7 @@ function createUI(core, media, controls, options = {}) {
|
|
|
761
831
|
wrapper.classList.add('op-controls--hidden');
|
|
762
832
|
mediaContainer.classList.add('op-media--controls-hidden');
|
|
763
833
|
controlsRoot.setAttribute('aria-hidden', 'true');
|
|
834
|
+
controlsRoot.inert = true;
|
|
764
835
|
core.events.emit('ui:controls:hide');
|
|
765
836
|
};
|
|
766
837
|
const scheduleHide = (ms) => {
|
|
@@ -779,37 +850,45 @@ function createUI(core, media, controls, options = {}) {
|
|
|
779
850
|
showControls();
|
|
780
851
|
scheduleHide(POINTER_SHOW_MS);
|
|
781
852
|
};
|
|
782
|
-
|
|
853
|
+
// Track all video-path DOM listeners so they can be removed on destroy.
|
|
854
|
+
const domUnsubs = [];
|
|
855
|
+
const trackListener = (target, type, handler, options) => {
|
|
856
|
+
target.addEventListener(type, handler, options);
|
|
857
|
+
domUnsubs.push(() => target.removeEventListener(type, handler, options));
|
|
858
|
+
};
|
|
859
|
+
const onControlsFocusIn = () => {
|
|
783
860
|
lastInteraction = 'keyboard';
|
|
784
861
|
showControls();
|
|
785
862
|
scheduleHide(KEYBOARD_SHOW_MS);
|
|
786
|
-
}
|
|
787
|
-
|
|
863
|
+
};
|
|
864
|
+
const onControlsFocusOut = () => {
|
|
788
865
|
window.setTimeout(() => {
|
|
789
866
|
if (!controlsHaveFocus())
|
|
790
867
|
scheduleHide(lastInteraction === 'keyboard' ? KEYBOARD_SHOW_MS : POINTER_SHOW_MS);
|
|
791
868
|
}, 0);
|
|
792
|
-
}
|
|
869
|
+
};
|
|
870
|
+
trackListener(controlsRoot, 'focusin', onControlsFocusIn);
|
|
871
|
+
trackListener(controlsRoot, 'focusout', onControlsFocusOut);
|
|
793
872
|
const isFocusInMediaArea = () => {
|
|
794
873
|
const active = document.activeElement;
|
|
795
874
|
if (!active)
|
|
796
875
|
return false;
|
|
797
876
|
return wrapper.contains(active) && !controlsRoot.contains(active);
|
|
798
877
|
};
|
|
799
|
-
|
|
878
|
+
const onWrapperFocusIn = () => {
|
|
800
879
|
if (isFocusInMediaArea()) {
|
|
801
880
|
showControls();
|
|
802
881
|
if (hideTimer)
|
|
803
882
|
window.clearTimeout(hideTimer);
|
|
804
883
|
}
|
|
805
|
-
}
|
|
806
|
-
|
|
884
|
+
};
|
|
885
|
+
const onWrapperFocusOut = () => {
|
|
807
886
|
window.setTimeout(() => {
|
|
808
887
|
if (!wrapper.contains(document.activeElement) && !controlsHaveFocus())
|
|
809
888
|
scheduleHide();
|
|
810
889
|
}, 0);
|
|
811
|
-
}
|
|
812
|
-
|
|
890
|
+
};
|
|
891
|
+
const onWrapperKeyDown = () => {
|
|
813
892
|
lastInteraction = 'keyboard';
|
|
814
893
|
if (isFocusInMediaArea()) {
|
|
815
894
|
showControls();
|
|
@@ -820,20 +899,25 @@ function createUI(core, media, controls, options = {}) {
|
|
|
820
899
|
showControls();
|
|
821
900
|
scheduleHide(KEYBOARD_SHOW_MS);
|
|
822
901
|
}
|
|
823
|
-
}
|
|
902
|
+
};
|
|
903
|
+
trackListener(wrapper, 'focusin', onWrapperFocusIn, EVENT_OPTIONS);
|
|
904
|
+
trackListener(wrapper, 'focusout', onWrapperFocusOut, EVENT_OPTIONS);
|
|
905
|
+
trackListener(wrapper, 'keydown', onWrapperKeyDown, EVENT_OPTIONS);
|
|
824
906
|
if (mobile) {
|
|
825
|
-
|
|
907
|
+
const onWrapperPointerDown = () => {
|
|
826
908
|
lastInteraction = 'pointer';
|
|
827
909
|
showControls();
|
|
828
910
|
scheduleHide(POINTER_SHOW_MS);
|
|
829
|
-
}
|
|
911
|
+
};
|
|
912
|
+
trackListener(wrapper, 'pointerdown', onWrapperPointerDown, EVENT_OPTIONS);
|
|
830
913
|
}
|
|
831
914
|
else {
|
|
832
|
-
|
|
833
|
-
wrapper
|
|
834
|
-
|
|
835
|
-
controlsRoot
|
|
836
|
-
controlsRoot
|
|
915
|
+
const onControlsPointerLeave = () => scheduleHide(POINTER_SHOW_MS);
|
|
916
|
+
trackListener(wrapper, 'pointermove', onControlsHover, EVENT_OPTIONS);
|
|
917
|
+
trackListener(wrapper, 'pointerenter', onControlsHover, EVENT_OPTIONS);
|
|
918
|
+
trackListener(controlsRoot, 'pointerenter', onControlsHover, EVENT_OPTIONS);
|
|
919
|
+
trackListener(controlsRoot, 'pointermove', onControlsHover, EVENT_OPTIONS);
|
|
920
|
+
trackListener(controlsRoot, 'pointerleave', onControlsPointerLeave, EVENT_OPTIONS);
|
|
837
921
|
}
|
|
838
922
|
const grid = createControlGrid(controlsRoot, mainControls);
|
|
839
923
|
wrapper.appendChild(mediaContainer);
|
|
@@ -847,7 +931,7 @@ function createUI(core, media, controls, options = {}) {
|
|
|
847
931
|
});
|
|
848
932
|
const ctx = { wrapper, mediaContainer, controlsRoot, placeholder, grid };
|
|
849
933
|
maybeAutoplayUnmute(core, wrapper);
|
|
850
|
-
|
|
934
|
+
const onWrapperClick = async (e) => {
|
|
851
935
|
// Linear overlays (e.g. full-screen ads with their own video) own click handling.
|
|
852
936
|
// Non-linear ads (no fullscreenVideoEl) run alongside the content, so we allow pause.
|
|
853
937
|
if (getOverlayManager(core).active?.fullscreenVideoEl)
|
|
@@ -867,7 +951,8 @@ function createUI(core, media, controls, options = {}) {
|
|
|
867
951
|
else {
|
|
868
952
|
await core.play().catch(() => undefined);
|
|
869
953
|
}
|
|
870
|
-
}
|
|
954
|
+
};
|
|
955
|
+
trackListener(wrapper, 'click', onWrapperClick, EVENT_OPTIONS);
|
|
871
956
|
const offPlaying = core.events.on('playing', () => scheduleHide(POINTER_SHOW_MS));
|
|
872
957
|
const offPause = core.events.on('pause', () => showControls());
|
|
873
958
|
const offEnded = core.events.on('ended', () => showControls());
|
|
@@ -900,13 +985,15 @@ function createUI(core, media, controls, options = {}) {
|
|
|
900
985
|
// ignore
|
|
901
986
|
}
|
|
902
987
|
try {
|
|
988
|
+
domUnsubs.forEach((u) => u());
|
|
989
|
+
teardownCenterOverlay();
|
|
903
990
|
offPlaying?.();
|
|
904
991
|
offPause?.();
|
|
905
992
|
offEnded?.();
|
|
906
993
|
offMenuOpen?.();
|
|
907
994
|
offMenuClose?.();
|
|
908
|
-
offAddElement();
|
|
909
|
-
offAddControl();
|
|
995
|
+
offAddElement?.();
|
|
996
|
+
offAddControl?.();
|
|
910
997
|
offDestroy();
|
|
911
998
|
}
|
|
912
999
|
catch {
|
|
@@ -1144,6 +1231,8 @@ class CaptionsControl extends BaseControl {
|
|
|
1144
1231
|
const labels = resolveUIConfig(core).labels;
|
|
1145
1232
|
const label = labels.captions;
|
|
1146
1233
|
const buttonLabel = labels.toggleCaptions;
|
|
1234
|
+
const { announce, destroy } = getSharedAnnouncer(this.resolvePlayerRoot());
|
|
1235
|
+
this.dispose.add(destroy);
|
|
1147
1236
|
this.button = document.createElement('button');
|
|
1148
1237
|
this.button.type = 'button';
|
|
1149
1238
|
this.button.className = 'op-controls__captions';
|
|
@@ -1232,6 +1321,8 @@ class CaptionsControl extends BaseControl {
|
|
|
1232
1321
|
}
|
|
1233
1322
|
}
|
|
1234
1323
|
refresh();
|
|
1324
|
+
const on = this.button.getAttribute('aria-pressed') === 'true';
|
|
1325
|
+
announce(on ? (labels.captionsOn ?? `${label} on`) : (labels.captionsOff ?? `${label} off`));
|
|
1235
1326
|
me.preventDefault();
|
|
1236
1327
|
me.stopPropagation();
|
|
1237
1328
|
}, EVENT_OPTIONS);
|
|
@@ -1447,6 +1538,7 @@ class DurationControl extends BaseControl {
|
|
|
1447
1538
|
const el = document.createElement('time');
|
|
1448
1539
|
el.className = 'op-controls__duration';
|
|
1449
1540
|
el.setAttribute('aria-hidden', 'false');
|
|
1541
|
+
el.setAttribute('aria-live', 'off');
|
|
1450
1542
|
el.setAttribute('datetime', 'PT0M0S');
|
|
1451
1543
|
const update = () => {
|
|
1452
1544
|
if (this.activeOverlay) {
|
|
@@ -1547,6 +1639,8 @@ class FullscreenControl extends BaseControl {
|
|
|
1547
1639
|
const core = this.core;
|
|
1548
1640
|
const labels = resolveUIConfig(core).labels;
|
|
1549
1641
|
const btn = document.createElement('button');
|
|
1642
|
+
const { announce, destroy } = getSharedAnnouncer(this.resolvePlayerRoot());
|
|
1643
|
+
this.dispose.add(destroy);
|
|
1550
1644
|
btn.tabIndex = 0;
|
|
1551
1645
|
btn.type = 'button';
|
|
1552
1646
|
btn.className = 'op-controls__fullscreen';
|
|
@@ -1633,6 +1727,10 @@ class FullscreenControl extends BaseControl {
|
|
|
1633
1727
|
me.preventDefault();
|
|
1634
1728
|
me.stopPropagation();
|
|
1635
1729
|
}, EVENT_OPTIONS);
|
|
1730
|
+
this.onPlayer('player:fullscreenchange', () => {
|
|
1731
|
+
const key = getFullscreenElement() ? 'enterFullscreen' : 'exitFullscreen';
|
|
1732
|
+
announce(labels[key] ?? key);
|
|
1733
|
+
});
|
|
1636
1734
|
sync();
|
|
1637
1735
|
return btn;
|
|
1638
1736
|
}
|
|
@@ -1674,6 +1772,12 @@ class PlayControl extends BaseControl {
|
|
|
1674
1772
|
const playLabel = labels.play;
|
|
1675
1773
|
const pauseLabel = labels.pause;
|
|
1676
1774
|
const restartLabel = labels.restart;
|
|
1775
|
+
const { announce, destroy } = getSharedAnnouncer(this.resolvePlayerRoot());
|
|
1776
|
+
this.dispose.add(destroy);
|
|
1777
|
+
const fmt = (key, value) => {
|
|
1778
|
+
const t = labels[key] ?? key;
|
|
1779
|
+
return value != null ? t.replace('%s', value) : t;
|
|
1780
|
+
};
|
|
1677
1781
|
let isEnded = false;
|
|
1678
1782
|
const btn = document.createElement('button');
|
|
1679
1783
|
btn.tabIndex = 0;
|
|
@@ -1704,8 +1808,13 @@ class PlayControl extends BaseControl {
|
|
|
1704
1808
|
this.onPlayer('playing', () => {
|
|
1705
1809
|
isEnded = false;
|
|
1706
1810
|
setPlaying(true);
|
|
1811
|
+
if (!core.media.seeking)
|
|
1812
|
+
announce(labels['pause'] ? fmt('play') : 'Playing');
|
|
1813
|
+
});
|
|
1814
|
+
this.onPlayer('pause', () => {
|
|
1815
|
+
setPlaying(false);
|
|
1816
|
+
announce(labels['pause'] ?? 'Paused');
|
|
1707
1817
|
});
|
|
1708
|
-
this.onPlayer('pause', () => setPlaying(false));
|
|
1709
1818
|
this.onPlayer('ended', () => {
|
|
1710
1819
|
isEnded = true;
|
|
1711
1820
|
setPlaying(false);
|
|
@@ -1746,6 +1855,13 @@ class ProgressControl extends BaseControl {
|
|
|
1746
1855
|
const core = this.core;
|
|
1747
1856
|
const ui = resolveUIConfig(core);
|
|
1748
1857
|
const { allowRewind, allowSkip, labels } = ui;
|
|
1858
|
+
const labelsMap = labels;
|
|
1859
|
+
const { announce, destroy } = getSharedAnnouncer(this.resolvePlayerRoot());
|
|
1860
|
+
this.dispose.add(destroy);
|
|
1861
|
+
const fmt = (key, value) => {
|
|
1862
|
+
const t = labelsMap[key] ?? key;
|
|
1863
|
+
return value != null ? t.replace('%s', value) : t;
|
|
1864
|
+
};
|
|
1749
1865
|
const progressLabel = labels.progressSlider;
|
|
1750
1866
|
const railLabel = labels.progressRail;
|
|
1751
1867
|
const progress = document.createElement('div');
|
|
@@ -1934,6 +2050,11 @@ class ProgressControl extends BaseControl {
|
|
|
1934
2050
|
}, EVENT_OPTIONS);
|
|
1935
2051
|
this.onPlayer('durationchange', updateUI);
|
|
1936
2052
|
this.onPlayer('timeupdate', updateUI);
|
|
2053
|
+
this.onPlayer('seeked', () => {
|
|
2054
|
+
const t = formatTime(core.currentTime);
|
|
2055
|
+
const d = isFinite(core.duration) ? formatTime(core.duration) : undefined;
|
|
2056
|
+
announce(d ? `${fmt('seekTo', t)} of ${d}` : fmt('seekTo', t));
|
|
2057
|
+
});
|
|
1937
2058
|
this.onPlayer('waiting', () => {
|
|
1938
2059
|
if (!slider.classList.contains('loading'))
|
|
1939
2060
|
slider.classList.add('loading');
|
|
@@ -2219,10 +2340,11 @@ class SettingsControl extends BaseControl {
|
|
|
2219
2340
|
}
|
|
2220
2341
|
const header = document.createElement('div');
|
|
2221
2342
|
header.className = 'op-menu__header';
|
|
2343
|
+
const { labels } = resolveUIConfig(this.core);
|
|
2222
2344
|
const back = document.createElement('button');
|
|
2223
2345
|
back.type = 'button';
|
|
2224
2346
|
back.className = 'op-submenu__back';
|
|
2225
|
-
setA11yLabel(back,
|
|
2347
|
+
setA11yLabel(back, labels.back);
|
|
2226
2348
|
this.listen(back, 'click', (e) => {
|
|
2227
2349
|
const me = e;
|
|
2228
2350
|
this.activeSubmenuId = null;
|
|
@@ -2295,6 +2417,7 @@ class TimeControl extends BaseControl {
|
|
|
2295
2417
|
const delimiter = document.createElement('span');
|
|
2296
2418
|
delimiter.className = 'op-controls__time-delimiter';
|
|
2297
2419
|
delimiter.textContent = '/';
|
|
2420
|
+
delimiter.setAttribute('aria-hidden', 'true');
|
|
2298
2421
|
const container = document.createElement('span');
|
|
2299
2422
|
container.className = 'op-controls-time';
|
|
2300
2423
|
const currentTime = new CurrentTimeControl().create(core);
|
|
@@ -2331,11 +2454,23 @@ class VolumeControl extends BaseControl {
|
|
|
2331
2454
|
build() {
|
|
2332
2455
|
const core = this.core;
|
|
2333
2456
|
const labels = resolveUIConfig(core).labels;
|
|
2457
|
+
const labelsMap = labels;
|
|
2334
2458
|
const muteLabel = labels.mute;
|
|
2335
2459
|
const unmuteLabel = labels.unmute;
|
|
2336
2460
|
const volumeLabel = labels.volume;
|
|
2337
2461
|
const volumeControlLabel = labels.volumeControl;
|
|
2338
2462
|
const volumeSliderLabel = labels.volumeSlider;
|
|
2463
|
+
const { announce, destroy } = getSharedAnnouncer(this.resolvePlayerRoot());
|
|
2464
|
+
this.dispose.add(destroy);
|
|
2465
|
+
const fmt = (key, value) => {
|
|
2466
|
+
const t = labelsMap[key] ?? key;
|
|
2467
|
+
return value != null ? t.replace('%s', value) : t;
|
|
2468
|
+
};
|
|
2469
|
+
let volTimer = null;
|
|
2470
|
+
this.dispose.add(() => {
|
|
2471
|
+
if (volTimer)
|
|
2472
|
+
clearTimeout(volTimer);
|
|
2473
|
+
});
|
|
2339
2474
|
const wrapper = document.createElement('div');
|
|
2340
2475
|
wrapper.className = 'op-controls__volume';
|
|
2341
2476
|
wrapper.tabIndex = 0;
|
|
@@ -2415,15 +2550,23 @@ class VolumeControl extends BaseControl {
|
|
|
2415
2550
|
}
|
|
2416
2551
|
updateSlider(v);
|
|
2417
2552
|
updateBtn(v);
|
|
2553
|
+
if (volTimer)
|
|
2554
|
+
clearTimeout(volTimer);
|
|
2555
|
+
volTimer = setTimeout(() => {
|
|
2556
|
+
announce(fmt('volumePercent', String(Math.round(v * 100))));
|
|
2557
|
+
volTimer = null;
|
|
2558
|
+
}, 400);
|
|
2418
2559
|
}, EVENT_OPTIONS);
|
|
2419
2560
|
this.listen(btn, 'click', (e) => {
|
|
2420
2561
|
const me = e;
|
|
2421
2562
|
const el = getActiveMedia(core);
|
|
2563
|
+
let announcePct;
|
|
2422
2564
|
if (!core.muted) {
|
|
2423
2565
|
if (core.volume > 0)
|
|
2424
2566
|
lastVolume = core.volume;
|
|
2425
2567
|
core.volume = 0;
|
|
2426
2568
|
core.muted = true;
|
|
2569
|
+
announcePct = 0;
|
|
2427
2570
|
if (el && el !== core.surface) {
|
|
2428
2571
|
try {
|
|
2429
2572
|
el.volume = 0;
|
|
@@ -2440,6 +2583,7 @@ class VolumeControl extends BaseControl {
|
|
|
2440
2583
|
const restore = lastVolume > 0 ? lastVolume : 1;
|
|
2441
2584
|
core.volume = restore;
|
|
2442
2585
|
core.muted = false;
|
|
2586
|
+
announcePct = Math.round(restore * 100);
|
|
2443
2587
|
if (el && el !== core.surface) {
|
|
2444
2588
|
try {
|
|
2445
2589
|
el.volume = restore;
|
|
@@ -2452,9 +2596,38 @@ class VolumeControl extends BaseControl {
|
|
|
2452
2596
|
}
|
|
2453
2597
|
}
|
|
2454
2598
|
}
|
|
2599
|
+
if (volTimer)
|
|
2600
|
+
clearTimeout(volTimer);
|
|
2601
|
+
volTimer = null;
|
|
2602
|
+
announce(fmt('volumePercent', String(announcePct)));
|
|
2455
2603
|
me.preventDefault();
|
|
2456
2604
|
me.stopPropagation();
|
|
2457
2605
|
}, EVENT_OPTIONS);
|
|
2606
|
+
// Re-entrancy guard: writing el.volume/el.muted fires a DOM volumechange which
|
|
2607
|
+
// bridges back to core.events, re-triggering this handler. The ads plugin also
|
|
2608
|
+
// writes core.volume/core.muted from its own volumechange listener on the ad
|
|
2609
|
+
// video element, creating a cross-handler loop. The flag breaks the cycle.
|
|
2610
|
+
let syncingVolume = false;
|
|
2611
|
+
const syncActiveMedia = (muted, vol) => {
|
|
2612
|
+
if (syncingVolume)
|
|
2613
|
+
return;
|
|
2614
|
+
const el = getActiveMedia(core);
|
|
2615
|
+
if (!el || el === core.surface)
|
|
2616
|
+
return;
|
|
2617
|
+
syncingVolume = true;
|
|
2618
|
+
try {
|
|
2619
|
+
if (el.muted !== muted)
|
|
2620
|
+
el.muted = muted;
|
|
2621
|
+
if (!muted && el.volume !== vol)
|
|
2622
|
+
el.volume = vol;
|
|
2623
|
+
}
|
|
2624
|
+
catch {
|
|
2625
|
+
// ignore
|
|
2626
|
+
}
|
|
2627
|
+
finally {
|
|
2628
|
+
syncingVolume = false;
|
|
2629
|
+
}
|
|
2630
|
+
};
|
|
2458
2631
|
this.onPlayer('loadedmetadata', () => {
|
|
2459
2632
|
const muted = core.muted || core.volume === 0;
|
|
2460
2633
|
const vol = formatVolume(core.volume);
|
|
@@ -2463,19 +2636,11 @@ class VolumeControl extends BaseControl {
|
|
|
2463
2636
|
slider.value = (muted ? 0 : vol).toString();
|
|
2464
2637
|
updateSlider(muted ? 0 : vol);
|
|
2465
2638
|
updateBtn(muted ? 0 : vol);
|
|
2466
|
-
|
|
2467
|
-
if (el && el !== core.surface) {
|
|
2468
|
-
try {
|
|
2469
|
-
el.muted = muted;
|
|
2470
|
-
if (!muted)
|
|
2471
|
-
el.volume = vol;
|
|
2472
|
-
}
|
|
2473
|
-
catch {
|
|
2474
|
-
// ignore
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2639
|
+
syncActiveMedia(muted, vol);
|
|
2477
2640
|
});
|
|
2478
2641
|
this.onPlayer('volumechange', () => {
|
|
2642
|
+
if (syncingVolume)
|
|
2643
|
+
return;
|
|
2479
2644
|
const muted = core.muted || core.volume === 0;
|
|
2480
2645
|
const vol = formatVolume(core.volume);
|
|
2481
2646
|
if (vol > 0)
|
|
@@ -2484,17 +2649,7 @@ class VolumeControl extends BaseControl {
|
|
|
2484
2649
|
updateSlider(muted ? 0 : vol);
|
|
2485
2650
|
updateBtn(muted ? 0 : vol);
|
|
2486
2651
|
btn.setAttribute('aria-pressed', muted ? 'true' : 'false');
|
|
2487
|
-
|
|
2488
|
-
if (el && el !== core.surface) {
|
|
2489
|
-
try {
|
|
2490
|
-
el.muted = muted;
|
|
2491
|
-
if (!muted)
|
|
2492
|
-
el.volume = vol;
|
|
2493
|
-
}
|
|
2494
|
-
catch {
|
|
2495
|
-
// ignore
|
|
2496
|
-
}
|
|
2497
|
-
}
|
|
2652
|
+
syncActiveMedia(muted, vol);
|
|
2498
2653
|
});
|
|
2499
2654
|
const container = document.createElement('div');
|
|
2500
2655
|
container.className = 'op-controls__volume--container';
|