@openplayerjs/player 3.0.2 → 3.1.1

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/index.js CHANGED
@@ -168,13 +168,13 @@ function getActiveMedia(core) {
168
168
  try {
169
169
  const hasOverlayMgr = typeof getOverlayManager === 'function';
170
170
  if (!hasOverlayMgr)
171
- return core.media;
171
+ return core.surface;
172
172
  const active = getOverlayManager(core)?.active;
173
173
  const v = active?.fullscreenVideoEl;
174
- return v && typeof v.play === 'function' ? v : core.media;
174
+ return v && typeof v.play === 'function' ? v : core.surface;
175
175
  }
176
176
  catch {
177
- return core.media;
177
+ return core.surface;
178
178
  }
179
179
  }
180
180
  async function togglePlayback(core) {
@@ -736,6 +736,7 @@ function createUI(core, media, controls, options = {}) {
736
736
  const KEYBOARD_SHOW_MS = 6500;
737
737
  let hideTimer;
738
738
  let lastInteraction = 'pointer';
739
+ let menuOpen = false;
739
740
  const controlsHaveFocus = () => controlsRoot.contains(document.activeElement);
740
741
  const showControls = () => {
741
742
  wrapper.classList.remove('op-controls--hidden');
@@ -743,9 +744,10 @@ function createUI(core, media, controls, options = {}) {
743
744
  if (hideTimer)
744
745
  window.clearTimeout(hideTimer);
745
746
  controlsRoot.setAttribute('aria-hidden', 'false');
747
+ core.events.emit('ui:controls:show');
746
748
  };
747
749
  const hideControls = () => {
748
- if (core.media.paused || core.media.ended)
750
+ if (core.surface.paused || core.surface.ended)
749
751
  return;
750
752
  if (controlsHaveFocus()) {
751
753
  if (lastInteraction === 'keyboard') {
@@ -758,11 +760,14 @@ function createUI(core, media, controls, options = {}) {
758
760
  wrapper.classList.add('op-controls--hidden');
759
761
  mediaContainer.classList.add('op-media--controls-hidden');
760
762
  controlsRoot.setAttribute('aria-hidden', 'true');
763
+ core.events.emit('ui:controls:hide');
761
764
  };
762
765
  const scheduleHide = (ms) => {
763
766
  if (alwaysVisible)
764
767
  return;
765
- if (core.media.paused || core.media.ended)
768
+ if (menuOpen)
769
+ return;
770
+ if (core.surface.paused || core.surface.ended)
766
771
  return;
767
772
  if (hideTimer)
768
773
  window.clearTimeout(hideTimer);
@@ -853,7 +858,7 @@ function createUI(core, media, controls, options = {}) {
853
858
  // Clicks on interactive elements (buttons, links) are handled by those elements.
854
859
  if (target && target !== wrapper && target.closest('button, [role="button"], a'))
855
860
  return;
856
- const isPlaying = !core.media.paused && !core.media.ended;
861
+ const isPlaying = !core.surface.paused && !core.surface.ended;
857
862
  if (isPlaying) {
858
863
  overlay?.flashPause(350);
859
864
  core.pause();
@@ -865,6 +870,15 @@ function createUI(core, media, controls, options = {}) {
865
870
  const offPlaying = core.events.on('playing', () => scheduleHide(POINTER_SHOW_MS));
866
871
  const offPause = core.events.on('pause', () => showControls());
867
872
  const offEnded = core.events.on('ended', () => showControls());
873
+ const offMenuOpen = core.events.on('ui:menu:open', () => {
874
+ menuOpen = true;
875
+ if (hideTimer)
876
+ window.clearTimeout(hideTimer);
877
+ });
878
+ const offMenuClose = core.events.on('ui:menu:close', () => {
879
+ menuOpen = false;
880
+ scheduleHide();
881
+ });
868
882
  const offDestroy = core.events.on('player:destroy', () => {
869
883
  try {
870
884
  createdControls.forEach((c) => c.destroy?.());
@@ -888,6 +902,8 @@ function createUI(core, media, controls, options = {}) {
888
902
  offPlaying?.();
889
903
  offPause?.();
890
904
  offEnded?.();
905
+ offMenuOpen?.();
906
+ offMenuClose?.();
891
907
  offAddElement();
892
908
  offAddControl();
893
909
  offDestroy();
@@ -1219,7 +1235,7 @@ class CurrentTimeControl extends BaseControl {
1219
1235
  const update = () => {
1220
1236
  if (this.activeOverlay) {
1221
1237
  el.setAttribute('aria-hidden', 'false');
1222
- el.innerText = formatTime(this.activeOverlay.value);
1238
+ el.innerText = formatTime(Math.ceil(this.activeOverlay.value));
1223
1239
  return;
1224
1240
  }
1225
1241
  if (core.isLive) {
@@ -1484,6 +1500,8 @@ class PlayControl extends BaseControl {
1484
1500
  const labels = resolveUIConfig(core).labels;
1485
1501
  const playLabel = labels.play;
1486
1502
  const pauseLabel = labels.pause;
1503
+ const restartLabel = labels.restart;
1504
+ let isEnded = false;
1487
1505
  const btn = document.createElement('button');
1488
1506
  btn.tabIndex = 0;
1489
1507
  btn.type = 'button';
@@ -1492,20 +1510,24 @@ class PlayControl extends BaseControl {
1492
1510
  btn.setAttribute('aria-pressed', 'false');
1493
1511
  this.listen(btn, 'click', async (e) => {
1494
1512
  const me = e;
1495
- await togglePlayback(core);
1496
1513
  me.preventDefault();
1497
1514
  me.stopPropagation();
1515
+ if (isEnded) {
1516
+ const media = getActiveMedia(core);
1517
+ media.currentTime = 0;
1518
+ }
1519
+ await togglePlayback(core);
1498
1520
  }, EVENT_OPTIONS);
1499
1521
  const setPlaying = (playing) => {
1500
1522
  btn.classList.toggle('op-controls__playpause--pause', playing);
1523
+ btn.classList.toggle('op-controls__playpause--replay', isEnded && !playing);
1501
1524
  btn.setAttribute('aria-pressed', playing ? 'true' : 'false');
1502
- setA11yLabel(btn, playing ? pauseLabel : playLabel);
1525
+ setA11yLabel(btn, isEnded && !playing ? restartLabel : playing ? pauseLabel : playLabel);
1503
1526
  };
1504
- this.onPlayer('play', () => setPlaying(true));
1527
+ this.onPlayer('play', () => { isEnded = false; setPlaying(true); });
1528
+ this.onPlayer('playing', () => { isEnded = false; setPlaying(true); });
1505
1529
  this.onPlayer('pause', () => setPlaying(false));
1506
- this.onPlayer('playing', () => setPlaying(true));
1507
- this.onPlayer('pause', () => setPlaying(false));
1508
- this.onPlayer('ended', () => setPlaying(false));
1530
+ this.onPlayer('ended', () => { isEnded = true; setPlaying(false); });
1509
1531
  return btn;
1510
1532
  }
1511
1533
  }
@@ -1607,12 +1629,12 @@ class ProgressControl extends BaseControl {
1607
1629
  const getDuration = () => {
1608
1630
  if (this.activeOverlay)
1609
1631
  return this.activeOverlay.duration;
1610
- return core.media?.duration ?? core.duration;
1632
+ return core.surface?.duration ?? core.duration;
1611
1633
  };
1612
1634
  const getValue = () => {
1613
1635
  if (this.activeOverlay)
1614
1636
  return this.activeOverlay.value;
1615
- return core.media?.currentTime ?? core.currentTime;
1637
+ return core.surface?.currentTime ?? core.currentTime;
1616
1638
  };
1617
1639
  const getMode = () => (this.activeOverlay ? this.activeOverlay.mode : 'normal');
1618
1640
  const updateUI = () => {
@@ -1634,7 +1656,7 @@ class ProgressControl extends BaseControl {
1634
1656
  if (this.activeOverlay)
1635
1657
  setSeekEnabled(this.activeOverlay.canSeek);
1636
1658
  else
1637
- setSeekEnabled(!core.isLive && core.media?.duration !== Infinity);
1659
+ setSeekEnabled(!core.isLive && core.surface?.duration !== Infinity);
1638
1660
  if (slider.classList.contains('op-progress--pressed'))
1639
1661
  return;
1640
1662
  const d = Number.isFinite(duration) && duration > 0 ? duration : 0;
@@ -1741,7 +1763,7 @@ class ProgressControl extends BaseControl {
1741
1763
  slider.classList.remove('loading');
1742
1764
  if (slider.classList.contains('error'))
1743
1765
  slider.classList.remove('error');
1744
- if (!core.isLive && core.media.duration !== Infinity) {
1766
+ if (!core.isLive && core.surface.duration !== Infinity) {
1745
1767
  progress.removeAttribute('aria-valuenow');
1746
1768
  progress.removeAttribute('aria-valuetext');
1747
1769
  }
@@ -1873,6 +1895,7 @@ class SettingsControl extends BaseControl {
1873
1895
  this.panel = document.createElement('div');
1874
1896
  this.panel.className = 'op-menu';
1875
1897
  this.panel.setAttribute('role', 'menu');
1898
+ this.panel.tabIndex = -1;
1876
1899
  this.panel.style.display = 'none';
1877
1900
  this.view = document.createElement('div');
1878
1901
  this.view.className = 'op-menu__submenu';
@@ -1899,11 +1922,38 @@ class SettingsControl extends BaseControl {
1899
1922
  if (ke.key === 'Escape')
1900
1923
  this.close();
1901
1924
  }, EVENT_OPTIONS);
1902
- this.dispose.add(this.overlayMgr.bus.on('overlay:changed', () => {
1903
- this.activeSubmenuId = null;
1904
- // Always re-compute availability so the control can hide during ads
1905
- // and re-appear when content resumes, even if the menu isn't open.
1906
- this.render();
1925
+ // Close the menu when focus moves outside the player (matching YouTube behaviour).
1926
+ this.listen(document, 'focusout', () => {
1927
+ if (!this.isOpen)
1928
+ return;
1929
+ window.setTimeout(() => {
1930
+ const playerEl = this.button.closest('.op-player');
1931
+ if (playerEl && !playerEl.contains(document.activeElement))
1932
+ this.close();
1933
+ }, 0);
1934
+ }, EVENT_OPTIONS);
1935
+ let knownOverlayId = null;
1936
+ this.dispose.add(this.overlayMgr.bus.on('overlay:changed', (ov) => {
1937
+ const newId = ov?.id ?? null;
1938
+ // Only reset submenu navigation when the overlay identity changes
1939
+ // (e.g. ad starts or ends). update() fires overlay:changed on every
1940
+ // timeupdate — we must not reset the user's submenu position then.
1941
+ if (newId !== knownOverlayId) {
1942
+ knownOverlayId = newId;
1943
+ this.activeSubmenuId = null;
1944
+ // Dismiss the menu on any context switch (ad↔content) so the user
1945
+ // never sees a stale track list from the previous context. It is safe
1946
+ // to rebuild here because the ad / content transition has completed.
1947
+ if (this.isOpen)
1948
+ this.close();
1949
+ else
1950
+ this.render();
1951
+ }
1952
+ else if (!this.isOpen) {
1953
+ // Periodic timeupdate ticks: only re-render visibility when closed
1954
+ // to avoid destroying buttons the user is actively clicking.
1955
+ this.render();
1956
+ }
1907
1957
  }));
1908
1958
  getSettingsRegistry(this.core).register({
1909
1959
  id: 'speed',
@@ -1912,7 +1962,7 @@ class SettingsControl extends BaseControl {
1912
1962
  const ov = this.overlayMgr.active;
1913
1963
  if (ov?.id === 'ads')
1914
1964
  return null;
1915
- const rates = [0.5, 0.75, 1, 1.25, 1.5, 2];
1965
+ const rates = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 2.5];
1916
1966
  const current = core.playbackRate || 1;
1917
1967
  return {
1918
1968
  id: 'speed',
@@ -1946,14 +1996,22 @@ class SettingsControl extends BaseControl {
1946
1996
  this.button.setAttribute('aria-expanded', 'true');
1947
1997
  this.panel.style.display = 'block';
1948
1998
  this.render();
1999
+ this.core.events.emit('ui:menu:open');
1949
2000
  }
1950
2001
  close() {
1951
2002
  this.isOpen = false;
1952
2003
  this.activeSubmenuId = null;
1953
2004
  this.button.setAttribute('aria-expanded', 'false');
1954
2005
  this.panel.style.display = 'none';
2006
+ this.core.events.emit('ui:menu:close');
1955
2007
  }
1956
2008
  render() {
2009
+ // If focus is inside the menu container, move it to the panel before clearing
2010
+ // the DOM so the browser doesn't relocate focus to <body>, which would cause
2011
+ // the focusout handler to close the menu mid-navigation.
2012
+ if (this.isOpen && this.root.contains(document.activeElement)) {
2013
+ this.panel.focus({ preventScroll: true });
2014
+ }
1957
2015
  const reg = getSettingsRegistry(this.core);
1958
2016
  const providers = reg.list();
1959
2017
  const available = providers
@@ -2164,7 +2222,7 @@ class VolumeControl extends BaseControl {
2164
2222
  core.volume = v;
2165
2223
  core.muted = v === 0;
2166
2224
  const el = getActiveMedia(core);
2167
- if (el && el !== core.media) {
2225
+ if (el && el !== core.surface) {
2168
2226
  try {
2169
2227
  el.volume = v;
2170
2228
  el.muted = v === 0;
@@ -2184,7 +2242,7 @@ class VolumeControl extends BaseControl {
2184
2242
  lastVolume = core.volume;
2185
2243
  core.volume = 0;
2186
2244
  core.muted = true;
2187
- if (el && el !== core.media) {
2245
+ if (el && el !== core.surface) {
2188
2246
  try {
2189
2247
  el.volume = 0;
2190
2248
  el.muted = true;
@@ -2200,7 +2258,7 @@ class VolumeControl extends BaseControl {
2200
2258
  const restore = lastVolume > 0 ? lastVolume : 1;
2201
2259
  core.volume = restore;
2202
2260
  core.muted = false;
2203
- if (el && el !== core.media) {
2261
+ if (el && el !== core.surface) {
2204
2262
  try {
2205
2263
  el.volume = restore;
2206
2264
  el.muted = false;
@@ -2224,7 +2282,7 @@ class VolumeControl extends BaseControl {
2224
2282
  updateSlider(muted ? 0 : vol);
2225
2283
  updateBtn(muted ? 0 : vol);
2226
2284
  const el = getActiveMedia(core);
2227
- if (el && el !== core.media) {
2285
+ if (el && el !== core.surface) {
2228
2286
  try {
2229
2287
  el.muted = muted;
2230
2288
  if (!muted)
@@ -2245,7 +2303,7 @@ class VolumeControl extends BaseControl {
2245
2303
  updateBtn(muted ? 0 : vol);
2246
2304
  btn.setAttribute('aria-pressed', muted ? 'true' : 'false');
2247
2305
  const el = getActiveMedia(core);
2248
- if (el && el !== core.media) {
2306
+ if (el && el !== core.surface) {
2249
2307
  try {
2250
2308
  el.muted = muted;
2251
2309
  if (!muted)