ts-video-player 0.1.8 → 0.1.9

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.
@@ -16,7 +16,7 @@ import {
16
16
  detectPipAvailability2,
17
17
  enterPiP2,
18
18
  exitPiP2
19
- } from "./chunk-rhykj0qy.js";
19
+ } from "./chunk-pvq6e7gz.js";
20
20
 
21
21
  // src/core/media-session.ts
22
22
  function isMediaSessionSupported2() {
@@ -2848,6 +2848,8 @@ class Player2 {
2848
2848
  toggleCaptions: () => this.toggleCaptions(),
2849
2849
  seekBackward: () => this.seekBy(-5),
2850
2850
  seekForward: () => this.seekBy(5),
2851
+ seekBackwardLong: () => this.seekBy(-10),
2852
+ seekForwardLong: () => this.seekBy(10),
2851
2853
  volumeUp: () => this.setVolume(Math.min(1, this.state.volume + 0.1)),
2852
2854
  volumeDown: () => this.setVolume(Math.max(0, this.state.volume - 0.1)),
2853
2855
  speedUp: () => this.setPlaybackRate(Math.min(4, this.state.playbackRate + 0.25)),
@@ -3088,7 +3090,7 @@ class Player2 {
3088
3090
  this.announce(label(this._el, "playing"));
3089
3091
  });
3090
3092
  provider.on("pause", () => {
3091
- this._store.set("paused", true);
3093
+ this._store.batch({ paused: true, playing: false, playbackState: "paused" });
3092
3094
  this._events.emit("pause");
3093
3095
  this.announce(label(this._el, "paused"));
3094
3096
  });
@@ -3357,7 +3359,7 @@ class VideoPlayerElement2 extends HTMLElementBase {
3357
3359
  this._disconnectId++;
3358
3360
  if (this._player)
3359
3361
  return;
3360
- const shadow = this.shadowRoot ?? this.attachShadow({ mode: "open" });
3362
+ const shadow = this.shadowRoot ?? this.attachShadow({ mode: "open", delegatesFocus: true });
3361
3363
  this._container = document.createElement("div");
3362
3364
  this._container.setAttribute("part", "container");
3363
3365
  this._container.style.cssText = "position:relative;width:100%;height:100%;";
@@ -3643,6 +3645,8 @@ class MediaPlayButton2 extends HTMLElementBase {
3643
3645
  background: transparent; border: none; color: inherit; cursor: pointer;
3644
3646
  }
3645
3647
  button:hover { opacity: 0.8; }
3648
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
3649
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
3646
3650
  </style>
3647
3651
  <button part="button" type="button" aria-label="${label(this, "play")}">
3648
3652
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
@@ -3704,6 +3708,8 @@ class MediaMuteButton2 extends HTMLElementBase {
3704
3708
  background: transparent; border: none; color: inherit; cursor: pointer;
3705
3709
  }
3706
3710
  button:hover { opacity: 0.8; }
3711
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
3712
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
3707
3713
  </style>
3708
3714
  <button part="button" type="button" aria-label="${label(this, "mute")}">
3709
3715
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
@@ -3753,6 +3759,8 @@ class MediaFullscreenButton2 extends HTMLElementBase {
3753
3759
  }
3754
3760
  button:hover { opacity: 0.8; }
3755
3761
  button:disabled { opacity: 0.4; cursor: not-allowed; }
3762
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
3763
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
3756
3764
  </style>
3757
3765
  <button part="button" type="button" aria-label="${label(this, "enterFullscreen")}">
3758
3766
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
@@ -3808,6 +3816,8 @@ class MediaPipButton2 extends HTMLElementBase {
3808
3816
  }
3809
3817
  button:hover { opacity: 0.8; }
3810
3818
  button:disabled { opacity: 0.4; cursor: not-allowed; }
3819
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
3820
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
3811
3821
  </style>
3812
3822
  <button part="button" type="button" aria-label="${label(this, "enterPiP")}">
3813
3823
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
@@ -4068,6 +4078,8 @@ class MediaProgressBar2 extends HTMLElementBase {
4068
4078
  border-top-color: rgba(0,0,0,0.85);
4069
4079
  }
4070
4080
  .container:hover .tooltip { opacity: 1; }
4081
+ /* A finger needs more than the 20px a mouse does. */
4082
+ @media (pointer: coarse) { .container { height: 36px; } .thumb { width: 18px; height: 18px; } }
4071
4083
  </style>
4072
4084
  <div class="container" part="container" role="slider"
4073
4085
  aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"
@@ -4216,6 +4228,8 @@ class MediaVolumeSlider2 extends HTMLElementBase {
4216
4228
  transition: transform 0.1s;
4217
4229
  }
4218
4230
  .container:hover .thumb { transform: translate(-50%, -50%) scale(1.2); }
4231
+ /* A finger needs more than the 20px a mouse does. */
4232
+ @media (pointer: coarse) { .container { height: 36px; } .thumb { width: 18px; height: 18px; } }
4219
4233
  </style>
4220
4234
  <div class="container" part="container" role="slider"
4221
4235
  aria-valuemin="0" aria-valuemax="100" aria-valuenow="100"
@@ -4412,6 +4426,8 @@ class MediaSettingsMenu2 extends HTMLElementBase {
4412
4426
  .item[aria-checked="true"]::before { content: '\\2713'; }
4413
4427
  .item[aria-checked="false"]::before { content: ''; min-width: 12px; display: inline-block; }
4414
4428
  .back { font-weight: bold; border-bottom: 1px solid rgba(255,255,255,0.2); }
4429
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
4430
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
4415
4431
  </style>
4416
4432
  <button class="trigger" part="trigger" type="button"
4417
4433
  aria-label="${label(this, "settings")}" aria-haspopup="true" aria-expanded="false">
@@ -4641,6 +4657,8 @@ class MediaSeekButton2 extends HTMLElementBase {
4641
4657
  button:hover { opacity: .8; }
4642
4658
  button:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
4643
4659
  button:disabled { cursor: default; opacity: .45; }
4660
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
4661
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
4644
4662
  </style>
4645
4663
  <button part="button" type="button"></button>
4646
4664
  `;
@@ -4686,6 +4704,8 @@ class MediaPlaybackRateButton2 extends HTMLElementBase {
4686
4704
  button { min-width: 42px; height: 40px; padding: 0 8px; border: 0; background: transparent; color: inherit; cursor: pointer; font: inherit; font-variant-numeric: tabular-nums; }
4687
4705
  button:hover { opacity: .8; }
4688
4706
  button:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
4707
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
4708
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
4689
4709
  </style>
4690
4710
  <button part="button" type="button" aria-label="${label(this, "speed")}"></button>
4691
4711
  `;
@@ -4736,6 +4756,8 @@ class MediaAirplayButton2 extends HTMLElementBase {
4736
4756
  button { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; padding: 8px; background: transparent; border: 0; color: inherit; cursor: pointer; }
4737
4757
  button:hover { opacity: 0.8; }
4738
4758
  :host([connected]) button { color: var(--media-airplay-active-color, #38bdf8); }
4759
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
4760
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
4739
4761
  </style>
4740
4762
  <button part="button" type="button" aria-label="${label(this, "airPlayPlay")}">
4741
4763
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon"><path d="${AIRPLAY_ICON}"/></svg>
@@ -4795,6 +4817,8 @@ class MediaLiveButton2 extends HTMLElementBase {
4795
4817
  button { display: inline-flex; align-items: center; gap: 6px; min-height: 40px; padding: 8px; background: transparent; border: 0; color: inherit; cursor: pointer; font: inherit; font-size: 12px; font-weight: 700; }
4796
4818
  i { width: 8px; height: 8px; border-radius: 9999px; background: #ef4444; }
4797
4819
  :host(:not([at-live-edge])) button { opacity: 0.72; }
4820
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
4821
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
4798
4822
  </style>
4799
4823
  <button part="button" type="button" aria-label="${label(this, "seekToLive")}"><i aria-hidden="true"></i><span>${label(this, "live")}</span></button>
4800
4824
  `;
@@ -4891,6 +4915,8 @@ class MediaRemotePlaybackButton2 extends HTMLElementBase {
4891
4915
  button { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; padding: 8px; background: transparent; border: 0; color: inherit; cursor: pointer; }
4892
4916
  button:hover { opacity: 0.8; }
4893
4917
  :host([connected]) button { color: var(--media-remote-active-color, #38bdf8); }
4918
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
4919
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
4894
4920
  </style>
4895
4921
  <button part="button" type="button" aria-label="${label(this, "remotePlayback")}">
4896
4922
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon"><path d="${REMOTE_ICON}"/></svg>
@@ -8,7 +8,7 @@ import {
8
8
  probeVolumeAvailability2,
9
9
  detectFullscreenAvailability2,
10
10
  detectPipAvailability2
11
- } from "./chunk-rhykj0qy.js";
11
+ } from "./chunk-pvq6e7gz.js";
12
12
 
13
13
  // src/features/playback.ts
14
14
  var playback2 = {
@@ -383,7 +383,9 @@ class MediaEventsNormalizer2 {
383
383
  }
384
384
  function onFullscreenChange2(handler) {
385
385
  const events = ["fullscreenchange", "webkitfullscreenchange", "mozfullscreenchange", "MSFullscreenChange"];
386
- const listener = () => {
386
+ const listener = (event) => {
387
+ if (event && typeof CustomEvent !== "undefined" && event instanceof CustomEvent)
388
+ return;
387
389
  const fullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement);
388
390
  handler(fullscreen);
389
391
  };
@@ -411,8 +413,8 @@ var DEFAULT_KEYBOARD_SHORTCUTS2 = {
411
413
  c: "toggleCaptions",
412
414
  ArrowLeft: "seekBackward",
413
415
  ArrowRight: "seekForward",
414
- j: "seekBackward",
415
- l: "seekForward",
416
+ j: "seekBackwardLong",
417
+ l: "seekForwardLong",
416
418
  ArrowUp: "volumeUp",
417
419
  ArrowDown: "volumeDown",
418
420
  ">": "speedUp",
@@ -463,6 +465,18 @@ function createKeyboardHandler2(actions, options = {}) {
463
465
  case "seekForward":
464
466
  actions.seekForward();
465
467
  break;
468
+ case "seekBackwardLong":
469
+ if (actions.seekBackwardLong)
470
+ actions.seekBackwardLong();
471
+ else
472
+ actions.seekBackward();
473
+ break;
474
+ case "seekForwardLong":
475
+ if (actions.seekForwardLong)
476
+ actions.seekForwardLong();
477
+ else
478
+ actions.seekForward();
479
+ break;
466
480
  case "volumeUp":
467
481
  actions.volumeUp();
468
482
  break;
@@ -78,6 +78,9 @@ export interface KeyboardActions {
78
78
  toggleCaptions(): void;
79
79
  seekBackward(): void;
80
80
  seekForward(): void;
81
+ /** J and L: ten seconds, as YouTube's keys go (the arrows are five). Falls back to seekBackward/seekForward. */
82
+ seekBackwardLong?(): void;
83
+ seekForwardLong?(): void;
81
84
  volumeUp(): void;
82
85
  volumeDown(): void;
83
86
  speedUp(): void;
@@ -23,7 +23,7 @@ import {
23
23
  MediaErrorDisplay2,
24
24
  MediaRemotePlaybackButton2,
25
25
  registerElements2
26
- } from "../chunk-hvjcmbfn.js";
26
+ } from "../chunk-fvxawgp5.js";
27
27
  export {
28
28
  MediaAirplayButton2 as MediaAirplayButton,
29
29
  MediaErrorDisplay2 as MediaErrorDisplay,
@@ -11,7 +11,7 @@ import {
11
11
  videoFeatures2,
12
12
  audioFeatures2,
13
13
  minimalFeatures2
14
- } from "../chunk-ds1nj487.js";
14
+ } from "../chunk-ndbfytd6.js";
15
15
  export {
16
16
  audioFeatures2 as audioFeatures,
17
17
  captions2 as captions,
package/dist/index.js CHANGED
@@ -63,7 +63,7 @@ import {
63
63
  applyPosition2,
64
64
  MediaSettingsMenu2,
65
65
  registerElements2
66
- } from "./chunk-hvjcmbfn.js";
66
+ } from "./chunk-fvxawgp5.js";
67
67
  import {
68
68
  playback2,
69
69
  volume2,
@@ -77,7 +77,7 @@ import {
77
77
  videoFeatures2,
78
78
  audioFeatures2,
79
79
  minimalFeatures2
80
- } from "./chunk-ds1nj487.js";
80
+ } from "./chunk-ndbfytd6.js";
81
81
  import {
82
82
  shallowEqual2,
83
83
  createSignal2,
@@ -118,7 +118,7 @@ import {
118
118
  enterPiP2,
119
119
  exitPiP2,
120
120
  detectAllFeatures2
121
- } from "./chunk-rhykj0qy.js";
121
+ } from "./chunk-pvq6e7gz.js";
122
122
  // src/core/audio-gain.ts
123
123
  class AudioGain {
124
124
  media;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-video-player",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "A modern, framework-agnostic video player with deep stx integration",
5
5
  "author": "Chris Breuer <chris@stacksjs.org>",
6
6
  "license": "MIT",
@@ -208,7 +208,12 @@ export class MediaEventsNormalizer {
208
208
  export function onFullscreenChange(handler: (fullscreen: boolean) => void): () => void {
209
209
  const events = ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange']
210
210
 
211
- const listener = () => {
211
+ const listener = (event?: Event) => {
212
+ // The browser's own change only. <video-player> re-dispatches the player's
213
+ // events on itself, bubbling and composed, under the same names; its
214
+ // "fullscreenchange" reached this document listener, which announced it
215
+ // again, and the two fed each other until the stack overflowed.
216
+ if (event && typeof CustomEvent !== 'undefined' && event instanceof CustomEvent) return
212
217
  const fullscreen = !!(
213
218
  document.fullscreenElement ||
214
219
  (document as any).webkitFullscreenElement ||
@@ -261,8 +266,8 @@ export const DEFAULT_KEYBOARD_SHORTCUTS: Record<string, string> = {
261
266
  c: 'toggleCaptions',
262
267
  ArrowLeft: 'seekBackward',
263
268
  ArrowRight: 'seekForward',
264
- j: 'seekBackward',
265
- l: 'seekForward',
269
+ j: 'seekBackwardLong',
270
+ l: 'seekForwardLong',
266
271
  ArrowUp: 'volumeUp',
267
272
  ArrowDown: 'volumeDown',
268
273
  '>': 'speedUp',
@@ -292,6 +297,9 @@ export interface KeyboardActions {
292
297
  toggleCaptions(): void
293
298
  seekBackward(): void
294
299
  seekForward(): void
300
+ /** J and L: ten seconds, as YouTube's keys go (the arrows are five). Falls back to seekBackward/seekForward. */
301
+ seekBackwardLong?(): void
302
+ seekForwardLong?(): void
295
303
  volumeUp(): void
296
304
  volumeDown(): void
297
305
  speedUp(): void
@@ -351,6 +359,14 @@ export function createKeyboardHandler(
351
359
  case 'seekForward':
352
360
  actions.seekForward()
353
361
  break
362
+ case 'seekBackwardLong':
363
+ if (actions.seekBackwardLong) actions.seekBackwardLong()
364
+ else actions.seekBackward()
365
+ break
366
+ case 'seekForwardLong':
367
+ if (actions.seekForwardLong) actions.seekForwardLong()
368
+ else actions.seekForward()
369
+ break
354
370
  case 'volumeUp':
355
371
  actions.volumeUp()
356
372
  break
@@ -27,6 +27,8 @@ export class MediaAirplayButton extends HTMLElementBase {
27
27
  button { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; padding: 8px; background: transparent; border: 0; color: inherit; cursor: pointer; }
28
28
  button:hover { opacity: 0.8; }
29
29
  :host([connected]) button { color: var(--media-airplay-active-color, #38bdf8); }
30
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
31
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
30
32
  </style>
31
33
  <button part="button" type="button" aria-label="${label(this, 'airPlayPlay')}">
32
34
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon"><path d="${AIRPLAY_ICON}"/></svg>
@@ -28,6 +28,8 @@ export class MediaFullscreenButton extends HTMLElementBase {
28
28
  }
29
29
  button:hover { opacity: 0.8; }
30
30
  button:disabled { opacity: 0.4; cursor: not-allowed; }
31
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
32
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
31
33
  </style>
32
34
  <button part="button" type="button" aria-label="${label(this, 'enterFullscreen')}">
33
35
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
@@ -20,6 +20,8 @@ export class MediaLiveButton extends HTMLElementBase {
20
20
  button { display: inline-flex; align-items: center; gap: 6px; min-height: 40px; padding: 8px; background: transparent; border: 0; color: inherit; cursor: pointer; font: inherit; font-size: 12px; font-weight: 700; }
21
21
  i { width: 8px; height: 8px; border-radius: 9999px; background: #ef4444; }
22
22
  :host(:not([at-live-edge])) button { opacity: 0.72; }
23
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
24
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
23
25
  </style>
24
26
  <button part="button" type="button" aria-label="${label(this, 'seekToLive')}"><i aria-hidden="true"></i><span>${label(this, 'live')}</span></button>
25
27
  `
@@ -33,6 +33,8 @@ export class MediaMuteButton extends HTMLElementBase {
33
33
  background: transparent; border: none; color: inherit; cursor: pointer;
34
34
  }
35
35
  button:hover { opacity: 0.8; }
36
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
37
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
36
38
  </style>
37
39
  <button part="button" type="button" aria-label="${label(this, 'mute')}">
38
40
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
@@ -27,6 +27,8 @@ export class MediaPipButton extends HTMLElementBase {
27
27
  }
28
28
  button:hover { opacity: 0.8; }
29
29
  button:disabled { opacity: 0.4; cursor: not-allowed; }
30
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
31
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
30
32
  </style>
31
33
  <button part="button" type="button" aria-label="${label(this, 'enterPiP')}">
32
34
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
@@ -27,6 +27,8 @@ export class MediaPlayButton extends HTMLElementBase {
27
27
  background: transparent; border: none; color: inherit; cursor: pointer;
28
28
  }
29
29
  button:hover { opacity: 0.8; }
30
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
31
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
30
32
  </style>
31
33
  <button part="button" type="button" aria-label="${label(this, 'play')}">
32
34
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
@@ -18,6 +18,8 @@ export class MediaPlaybackRateButton extends HTMLElementBase {
18
18
  button { min-width: 42px; height: 40px; padding: 0 8px; border: 0; background: transparent; color: inherit; cursor: pointer; font: inherit; font-variant-numeric: tabular-nums; }
19
19
  button:hover { opacity: .8; }
20
20
  button:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
21
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
22
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
21
23
  </style>
22
24
  <button part="button" type="button" aria-label="${label(this, 'speed')}"></button>
23
25
  `
@@ -65,6 +65,8 @@ export class MediaProgressBar extends HTMLElementBase {
65
65
  border-top-color: rgba(0,0,0,0.85);
66
66
  }
67
67
  .container:hover .tooltip { opacity: 1; }
68
+ /* A finger needs more than the 20px a mouse does. */
69
+ @media (pointer: coarse) { .container { height: 36px; } .thumb { width: 18px; height: 18px; } }
68
70
  </style>
69
71
  <div class="container" part="container" role="slider"
70
72
  aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"
@@ -32,6 +32,8 @@ export class MediaRemotePlaybackButton extends HTMLElementBase {
32
32
  button { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; padding: 8px; background: transparent; border: 0; color: inherit; cursor: pointer; }
33
33
  button:hover { opacity: 0.8; }
34
34
  :host([connected]) button { color: var(--media-remote-active-color, #38bdf8); }
35
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
36
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
35
37
  </style>
36
38
  <button part="button" type="button" aria-label="${label(this, 'remotePlayback')}">
37
39
  <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon"><path d="${REMOTE_ICON}"/></svg>
@@ -17,6 +17,8 @@ export class MediaSeekButton extends HTMLElementBase {
17
17
  button:hover { opacity: .8; }
18
18
  button:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
19
19
  button:disabled { cursor: default; opacity: .45; }
20
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
21
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
20
22
  </style>
21
23
  <button part="button" type="button"></button>
22
24
  `
@@ -54,6 +54,8 @@ export class MediaSettingsMenu extends HTMLElementBase {
54
54
  .item[aria-checked="true"]::before { content: '\\2713'; }
55
55
  .item[aria-checked="false"]::before { content: ''; min-width: 12px; display: inline-block; }
56
56
  .back { font-weight: bold; border-bottom: 1px solid rgba(255,255,255,0.2); }
57
+ /* 44px, the smallest a finger hits reliably, on a touch screen. */
58
+ @media (pointer: coarse) { button { min-width: 44px; min-height: 44px; } }
57
59
  </style>
58
60
  <button class="trigger" part="trigger" type="button"
59
61
  aria-label="${say(this, 'settings')}" aria-haspopup="true" aria-expanded="false">
@@ -42,6 +42,8 @@ export class MediaVolumeSlider extends HTMLElementBase {
42
42
  transition: transform 0.1s;
43
43
  }
44
44
  .container:hover .thumb { transform: translate(-50%, -50%) scale(1.2); }
45
+ /* A finger needs more than the 20px a mouse does. */
46
+ @media (pointer: coarse) { .container { height: 36px; } .thumb { width: 18px; height: 18px; } }
45
47
  </style>
46
48
  <div class="container" part="container" role="slider"
47
49
  aria-valuemin="0" aria-valuemax="100" aria-valuenow="100"
@@ -31,7 +31,9 @@ export class VideoPlayerElement extends HTMLElementBase {
31
31
  if (this._player) return
32
32
 
33
33
  // Create shadow DOM
34
- const shadow = this.shadowRoot ?? this.attachShadow({ mode: 'open' })
34
+ // delegatesFocus: focusing <video-player> (a dialog's first focus, an
35
+ // `autofocus`) puts focus on the player itself, where its keys work.
36
+ const shadow = this.shadowRoot ?? this.attachShadow({ mode: 'open', delegatesFocus: true })
35
37
 
36
38
  // Container
37
39
  this._container = document.createElement('div')
package/src/player.ts CHANGED
@@ -263,6 +263,8 @@ export class Player implements IPlayer {
263
263
  toggleCaptions: () => this.toggleCaptions(),
264
264
  seekBackward: () => this.seekBy(-5),
265
265
  seekForward: () => this.seekBy(5),
266
+ seekBackwardLong: () => this.seekBy(-10),
267
+ seekForwardLong: () => this.seekBy(10),
266
268
  volumeUp: () => this.setVolume(Math.min(1, this.state.volume + 0.1)),
267
269
  volumeDown: () => this.setVolume(Math.max(0, this.state.volume - 0.1)),
268
270
  speedUp: () => this.setPlaybackRate(Math.min(4, this.state.playbackRate + 0.25)),
@@ -599,7 +601,10 @@ export class Player implements IPlayer {
599
601
  })
600
602
 
601
603
  provider.on('pause', () => {
602
- this._store.set('paused', true)
604
+ // Not playing any more either: the controls hide only while playing,
605
+ // and a paused player that still said `playing` hid them over a still
606
+ // frame, and told everyone reading state.playing it was still going.
607
+ this._store.batch({ paused: true, playing: false, playbackState: 'paused' })
603
608
  this._events.emit('pause')
604
609
  this.announce(label(this._el, 'paused'))
605
610
  })