@banou/media-player 0.10.19 → 0.10.20

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
@@ -2351,6 +2351,7 @@ ${gt}
2351
2351
  e = Date.now() + zt;
2352
2352
  };
2353
2353
  }, [M]), d(() => () => clearInterval(S.current), []), /* @__PURE__ */ E("div", {
2354
+ className: "control-bar",
2354
2355
  css: Bt,
2355
2356
  style: { ...c ? {
2356
2357
  opacity: "0",
@@ -2681,47 +2682,51 @@ ${gt}
2681
2682
  let s = W(), c = W((e) => e.hideUI), l = W((e) => e.setHideUI), u = W((e) => e.hasMedia), f = W((e) => e.passThroughPictureInPicture), m = p(void 0), h = p("mouse"), g = p({
2682
2683
  x: -1,
2683
2684
  y: -1
2684
- }), _ = p(null);
2685
+ }), _ = p(!1), v = p(null);
2685
2686
  d(() => () => clearTimeout(m.current), []), d(() => {
2686
2687
  u || (clearTimeout(m.current), l(!1));
2687
2688
  }, [u, l]);
2688
- let v = () => {
2689
+ let y = () => {
2689
2690
  u && l(!0);
2690
- }, y = (t) => {
2691
- _.current = t, typeof e == "function" ? e(t) : e && (e.current = t);
2692
- }, b = () => {
2691
+ }, b = (t) => {
2692
+ v.current = t, typeof e == "function" ? e(t) : e && (e.current = t);
2693
+ }, x = () => {
2693
2694
  if (s.passThroughPictureInPicture?.armed) return !0;
2694
2695
  if (h.current !== "mouse") return !1;
2695
2696
  let { x: e, y: t } = g.current;
2696
- if (!_.current || e < 0) return !1;
2697
+ if (!v.current || e < 0) return !1;
2697
2698
  let n = document.elementFromPoint(e, t);
2698
- return !!n && _.current.contains(n) && !n.closest(".video");
2699
- }, x = () => {
2700
- if (b()) {
2701
- m.current = setTimeout(x, Zt);
2699
+ return !n || !v.current.contains(n) || n.closest(".video") ? !1 : !n.closest(".control-bar") || _.current || !!v.current.querySelector(".control-bar [aria-expanded=\"true\"]");
2700
+ }, S = () => {
2701
+ if (x()) {
2702
+ m.current = setTimeout(S, Zt);
2702
2703
  return;
2703
2704
  }
2704
- v();
2705
- }, S = () => {
2706
- l(!1), clearTimeout(m.current), m.current = setTimeout(x, Zt);
2707
- };
2705
+ y();
2706
+ }, C = () => {
2707
+ l(!1), clearTimeout(m.current), m.current = setTimeout(S, Zt);
2708
+ }, w = (e) => {
2709
+ h.current = e.pointerType, e.pointerType === "mouse" && (g.current = {
2710
+ x: e.clientX,
2711
+ y: e.clientY
2712
+ }, C());
2713
+ }, D = (e) => {
2714
+ h.current = e.pointerType, _.current = !0;
2715
+ }, O = () => {
2716
+ _.current = !1;
2717
+ }, k = p(!0);
2708
2718
  return /* @__PURE__ */ E("div", {
2709
2719
  css: en,
2710
- ref: y,
2711
- onPointerMove: (e) => {
2712
- h.current = e.pointerType, e.pointerType === "mouse" && (g.current = {
2713
- x: e.clientX,
2714
- y: e.clientY
2715
- }, S());
2716
- },
2717
- onPointerDown: (e) => {
2718
- h.current = e.pointerType;
2719
- },
2720
+ ref: b,
2721
+ onPointerMove: w,
2722
+ onPointerDown: D,
2723
+ onPointerUp: O,
2724
+ onPointerCancel: O,
2720
2725
  onMouseOut: (e) => {
2721
2726
  let t = e.relatedTarget;
2722
2727
  if (t && e.currentTarget.contains(t)) return;
2723
2728
  let { left: n, right: r, top: i, bottom: a } = e.currentTarget.getBoundingClientRect();
2724
- e.clientX >= n && e.clientX < r && e.clientY >= i && e.clientY < a || (clearTimeout(m.current), v());
2729
+ e.clientX >= n && e.clientX < r && e.clientY >= i && e.clientY < a || (clearTimeout(m.current), y());
2725
2730
  },
2726
2731
  className: c ? "hide" : "",
2727
2732
  tabIndex: a === "player" ? -1 : void 0,
@@ -2738,23 +2743,27 @@ ${gt}
2738
2743
  }, e)),
2739
2744
  i === !1 ? null : /* @__PURE__ */ T(Vt, {
2740
2745
  shortcuts: a,
2741
- root: _
2746
+ root: v
2742
2747
  }),
2743
2748
  /* @__PURE__ */ T(Wt, {}),
2744
2749
  /* @__PURE__ */ T(Xt, {}),
2745
- f?.armed && _.current ? /* @__PURE__ */ T("div", {
2750
+ f?.armed && v.current ? /* @__PURE__ */ T("div", {
2746
2751
  className: "pass-through",
2747
- style: { clipPath: $t(f.armed, _.current.getBoundingClientRect()) },
2752
+ style: { clipPath: $t(f.armed, v.current.getBoundingClientRect()) },
2748
2753
  onPointerOver: f.disarm
2749
2754
  }) : null,
2750
2755
  /* @__PURE__ */ E("div", {
2751
2756
  className: "video",
2752
- onClick: () => {
2757
+ onClick: (e) => {
2753
2758
  if (h.current === "mouse") {
2754
- s.togglePaused();
2759
+ if (e.detail > 1) return;
2760
+ k.current = s.paused, s.togglePaused();
2755
2761
  return;
2756
2762
  }
2757
- c ? S() : (clearTimeout(m.current), v());
2763
+ c ? C() : (clearTimeout(m.current), y());
2764
+ },
2765
+ onDoubleClick: () => {
2766
+ h.current === "mouse" && (k.current ? s.pause() : s.play().catch(() => {}), s.toggleFullscreen());
2758
2767
  },
2759
2768
  children: [t ? /* @__PURE__ */ T("video", {
2760
2769
  ref: t,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@banou/media-player",
3
- "version": "0.10.19",
3
+ "version": "0.10.20",
4
4
  "description": "A video player for containers and codecs the browser cannot play natively, remuxed on the fly",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -147,6 +147,8 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shor
147
147
  const lastPointerType = useRef<string>('mouse')
148
148
  // where the mouse last was, which is where it still is when nothing has moved
149
149
  const pointer = useRef({ x: -1, y: -1 })
150
+ // a mouse button held down with the mouse still is a drag on the seekbar or the volume, not yet over
151
+ const pressed = useRef(false)
150
152
  const root = useRef<HTMLDivElement | null>(null)
151
153
 
152
154
  useEffect(() => () => clearTimeout(autoHide.current), [])
@@ -189,9 +191,10 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shor
189
191
  * and a right click offers the browser's video menu rather than the control's. That is how a link
190
192
  * drawn in an overlay stops behaving like a link, which is what this exists to stop.
191
193
  *
192
- * Only what actually takes pointer events can be hit, so this needs no list of selectors: the
193
- * chrome's own layers opt out, an overlay item opts out until an app opts a child back in, and the
194
- * picture is excluded here because resting on the picture is exactly when hiding is right.
194
+ * Only what actually takes pointer events can be hit: the chrome's own layers opt out, and an
195
+ * overlay item opts out until an app opts a child back in. The picture and the control bar are
196
+ * excluded here because resting on them is exactly when hiding is right, unless a button is held
197
+ * on the bar or one of its menus is open, which are still in use.
195
198
  *
196
199
  * The mouse only. A finger has no resting position, and `elementFromPoint` on the last tap would
197
200
  * keep the controls up forever after one press.
@@ -205,7 +208,9 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shor
205
208
  const { x, y } = pointer.current
206
209
  if (!root.current || x < 0) return false
207
210
  const hit = document.elementFromPoint(x, y)
208
- return !!hit && root.current.contains(hit) && !hit.closest('.video')
211
+ if (!hit || !root.current.contains(hit) || hit.closest('.video')) return false
212
+ if (hit.closest('.control-bar')) return pressed.current || !!root.current.querySelector('.control-bar [aria-expanded="true"]')
213
+ return true
209
214
  }
210
215
 
211
216
  const hideUnlessTheMouseIsOnAControl = () => {
@@ -233,11 +238,20 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shor
233
238
 
234
239
  const onPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
235
240
  lastPointerType.current = event.pointerType
241
+ pressed.current = true
236
242
  }
237
243
 
244
+ const onPointerUp = () => {
245
+ pressed.current = false
246
+ }
247
+
248
+ const pausedBeforeClick = useRef(true)
249
+
238
250
  // play/pause with a mouse, show or hide the controls with a finger, which has no other way back
239
- const onVideoClick = () => {
251
+ const onVideoClick = (event: React.MouseEvent) => {
240
252
  if (lastPointerType.current === 'mouse') {
253
+ if (event.detail > 1) return
254
+ pausedBeforeClick.current = player.paused
241
255
  player.togglePaused()
242
256
  return
243
257
  }
@@ -248,6 +262,15 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shor
248
262
  }
249
263
  }
250
264
 
265
+ // The first click toggled playback, so it is put back rather than toggled again: a remote media
266
+ // reports its pause late, and a second toggle would read it as still playing and pause it twice.
267
+ const onVideoDoubleClick = () => {
268
+ if (lastPointerType.current !== 'mouse') return
269
+ if (pausedBeforeClick.current) player.pause()
270
+ else void player.play().catch(() => {})
271
+ void player.toggleFullscreen()
272
+ }
273
+
251
274
  // relatedTarget alone is not enough: the browser fires mouseout with a null one while the pointer
252
275
  // still sits over the video, so a null is confirmed against the pointer's own coordinates.
253
276
  const onMouseOut: React.DOMAttributes<HTMLDivElement>['onMouseOut'] = (ev) => {
@@ -266,6 +289,8 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shor
266
289
  ref={setRoot}
267
290
  onPointerMove={onPointerMove}
268
291
  onPointerDown={onPointerDown}
292
+ onPointerUp={onPointerUp}
293
+ onPointerCancel={onPointerUp}
269
294
  onMouseOut={onMouseOut}
270
295
  className={hideUI ? 'hide' : ''}
271
296
  // focusable, so a click on the picture puts the focus, and with it the shortcuts, inside the player
@@ -294,7 +319,7 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shor
294
319
  />
295
320
  )
296
321
  : null}
297
- <div className="video" onClick={onVideoClick}>
322
+ <div className="video" onClick={onVideoClick} onDoubleClick={onVideoDoubleClick}>
298
323
  {onVideoRef ? <video ref={onVideoRef} playsInline /> : null}
299
324
  {children}
300
325
  </div>
@@ -308,6 +308,7 @@ export const ControlBar = (
308
308
  // Armed, the whole bar takes no pointer events, or the rows around the control would still catch
309
309
  // the click at its box. The chrome's layer covers everything but the control meanwhile.
310
310
  <div
311
+ className='control-bar'
311
312
  css={style}
312
313
  style={{ ...hideUI ? { opacity: '0', pointerEvents: 'none' } : armed ? { pointerEvents: 'none' } : {} }}
313
314
  >