@banou/media-player 0.8.1 → 0.8.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/index.js CHANGED
@@ -251,30 +251,62 @@ var ee = {
251
251
  top: 0;
252
252
  left: 0;
253
253
  width: 100%;
254
- padding: calc(1.2 * var(--mp-unit)) calc(1.6 * var(--mp-unit));
255
- /* clears a notch once the player is fullscreen; resolves to zero everywhere else */
256
- padding-top: calc(calc(1.2 * var(--mp-unit)) + env(safe-area-inset-top, 0px));
257
- ${V.headings.small}
258
- color: white;
259
- text-shadow: 0 0 4px rgba(0, 0, 0, 1);
260
254
  z-index: 2;
255
+ /* the video underneath keeps the click that toggles playback; anything in the slot that needs a
256
+ pointer takes it back for itself */
261
257
  pointer-events: none;
262
258
 
263
- /* one line with an ellipsis until there is width to wrap, since a release filename would
264
- otherwise run to four lines on a phone */
265
- white-space: nowrap;
266
- overflow: hidden;
267
- text-overflow: ellipsis;
259
+ display: flex;
260
+ align-items: flex-start;
261
+ gap: calc(1.6 * var(--mp-unit));
262
+
263
+ /* longhands per breakpoint, never the shorthand: a nested at-rule is hoisted after the rule it
264
+ sits in, so a shorthand in a media query would override the longhands above it and silently
265
+ drop the safe-area insets. Those clear a notch once the player is fullscreen and resolve to
266
+ zero everywhere else. */
267
+ padding-top: calc(calc(1.2 * var(--mp-unit)) + env(safe-area-inset-top, 0px));
268
+ padding-bottom: calc(1.2 * var(--mp-unit));
269
+ padding-left: calc(calc(1.6 * var(--mp-unit)) + env(safe-area-inset-left, 0px));
270
+ padding-right: calc(calc(1.6 * var(--mp-unit)) + env(safe-area-inset-right, 0px));
268
271
 
269
272
  @media (min-width: 768px) {
270
- padding: calc(2.4 * var(--mp-unit));
271
273
  padding-top: calc(calc(2.4 * var(--mp-unit)) + env(safe-area-inset-top, 0px));
272
- white-space: normal;
273
- overflow: visible;
274
+ padding-bottom: calc(2.4 * var(--mp-unit));
275
+ padding-left: calc(calc(2.4 * var(--mp-unit)) + env(safe-area-inset-left, 0px));
276
+ padding-right: calc(calc(2.4 * var(--mp-unit)) + env(safe-area-inset-right, 0px));
274
277
  }
275
278
 
276
279
  background: linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.3) 30%, rgba(0,0,0,0.2) 60%, rgba(0,0,0,0.1) 80%, transparent 100%);
277
- transition: opacity 0.1s cubic-bezier(.4,0,1,1);
280
+ /* visibility, not only opacity: a slot child that took pointer events back would otherwise stay
281
+ hoverable while faded out, popping a tooltip over nothing. It cascades where pointer-events
282
+ does not, and transitioning it holds the element visible for the length of the fade. */
283
+ transition: opacity 0.1s cubic-bezier(.4,0,1,1), visibility 0.1s;
284
+
285
+ .title {
286
+ ${V.headings.small}
287
+ color: white;
288
+ text-shadow: 0 0 4px rgba(0, 0, 0, 1);
289
+
290
+ /* one line with an ellipsis until there is width to wrap, since a release filename would
291
+ otherwise run to four lines on a phone */
292
+ flex: 1;
293
+ min-width: 0;
294
+ white-space: nowrap;
295
+ overflow: hidden;
296
+ text-overflow: ellipsis;
297
+
298
+ @media (min-width: 768px) {
299
+ white-space: normal;
300
+ overflow: visible;
301
+ }
302
+ }
303
+
304
+ /* pinned right with or without a title, so an app's readout does not jump sideways when a
305
+ filename arrives late */
306
+ .app-slot {
307
+ margin-left: auto;
308
+ flex: none;
309
+ }
278
310
  `, oe = u`
279
311
  position: absolute;
280
312
  inset: 0;
@@ -305,21 +337,28 @@ var ee = {
305
337
  ${V.bLarge.regular}
306
338
  text-shadow: 0 0 4px rgba(0, 0, 0, 1);
307
339
  pointer-events: none;
308
- `, ce = (e) => e instanceof Error ? e.message : typeof e == "string" ? e : "Playback failed", le = ({ onCanvasRef: e }) => {
309
- let t = P((e) => e.title), n = P((e) => e.hideUI), r = P((e) => e.playbackError), i = P((e) => e.ready), a = P((e) => e.size), o = P((e) => e.waiting);
340
+ `, ce = (e) => e instanceof Error ? e.message : typeof e == "string" ? e : "Playback failed", le = ({ onCanvasRef: e, overlay: t }) => {
341
+ let n = P((e) => e.title), r = P((e) => e.hideUI), i = P((e) => e.playbackError), a = P((e) => e.ready), o = P((e) => e.size), s = P((e) => e.waiting);
310
342
  return /* @__PURE__ */ y(_, { children: [
311
- t ? /* @__PURE__ */ v("div", {
343
+ n || t ? /* @__PURE__ */ y("div", {
312
344
  css: ae,
313
- style: { ...n ? {
345
+ style: { ...r ? {
314
346
  opacity: "0",
347
+ visibility: "hidden",
315
348
  pointerEvents: "none"
316
349
  } : {} },
317
- children: t
350
+ children: [n ? /* @__PURE__ */ v("div", {
351
+ className: "title",
352
+ children: n
353
+ }) : void 0, t ? /* @__PURE__ */ v("div", {
354
+ className: "app-slot",
355
+ children: t
356
+ }) : void 0]
318
357
  }) : void 0,
319
- (a ? !i : o) && !r ? /* @__PURE__ */ v("div", { css: oe }) : void 0,
320
- r ? /* @__PURE__ */ v("div", {
358
+ (o ? !a : s) && !i ? /* @__PURE__ */ v("div", { css: oe }) : void 0,
359
+ i ? /* @__PURE__ */ v("div", {
321
360
  css: se,
322
- children: ce(r)
361
+ children: ce(i)
323
362
  }) : void 0,
324
363
  /* @__PURE__ */ v("canvas", {
325
364
  ref: e,
@@ -1466,8 +1505,10 @@ position: relative;
1466
1505
  },
1467
1506
  className: s ? "hide" : "",
1468
1507
  children: [
1469
- /* @__PURE__ */ v(le, { onCanvasRef: n }),
1470
- r,
1508
+ /* @__PURE__ */ v(le, {
1509
+ onCanvasRef: n,
1510
+ overlay: r
1511
+ }),
1471
1512
  /* @__PURE__ */ v(Te, {}),
1472
1513
  /* @__PURE__ */ y("div", {
1473
1514
  className: "video",
@@ -1558,6 +1599,19 @@ position: relative;
1558
1599
  */
1559
1600
  --mp-unit: 10px;
1560
1601
 
1602
+ /**
1603
+ * The other half of owning the chrome's own scale.
1604
+ *
1605
+ * Every control here is authored against a border box, which this library's dev app sets globally
1606
+ * and a host page has no reason to. Shipped without it, anything that is \`width: 100%\` AND carries
1607
+ * padding overflows its own box: the top bar ran 32px past the player and clipped the right end of
1608
+ * whatever the app had put there. Scoped to the player's subtree so it cannot reach the host's
1609
+ * layout, and only \`box-sizing\`, because zeroing margins here would reach the app's own content.
1610
+ */
1611
+ &, & *, & *::before, & *::after {
1612
+ box-sizing: border-box;
1613
+ }
1614
+
1561
1615
  display: flex;
1562
1616
  justify-content: center;
1563
1617
  background-color: #111;
@@ -4,7 +4,7 @@ export type ChromeProps = {
4
4
  /** Absent means render no video element: the media belongs to someone else and arrives as children. */
5
5
  onVideoRef?: (element: HTMLVideoElement | null) => void;
6
6
  onCanvasRef: (element: HTMLCanvasElement | null) => void;
7
- /** Above the control bar and outside the click-to-pause region, unlike `children`. */
7
+ /** The app's own content, drawn in the top bar beside the title, unlike `children`. */
8
8
  overlay?: ReactNode;
9
9
  children?: ReactNode;
10
10
  };
@@ -1,4 +1,8 @@
1
- export declare const Overlay: ({ onCanvasRef }: {
1
+ import type { ReactNode } from 'react';
2
+ export type OverlayProps = {
2
3
  onCanvasRef: (element: HTMLCanvasElement | null) => void;
3
- }) => import("@emotion/react/jsx-runtime").JSX.Element;
4
+ /** The app's own content, drawn at the right of the top bar next to the title. */
5
+ overlay?: ReactNode;
6
+ };
7
+ export declare const Overlay: ({ onCanvasRef, overlay }: OverlayProps) => import("@emotion/react/jsx-runtime").JSX.Element;
4
8
  export default Overlay;
@@ -17,8 +17,13 @@ type CommonOptions = {
17
17
  title?: string;
18
18
  autoplay?: boolean;
19
19
  /**
20
- * Drawn above the control bar and outside the click-to-pause region, for whatever the app has to
21
- * say over the video. `children` land next to the media instead, below the chrome.
20
+ * The app's own readout, drawn at the right of the top bar beside `title`, for whatever it has to
21
+ * say over the video. It shares the title's gradient and fades with the rest of the chrome, so a
22
+ * running counter does not sit over the picture once the controls have hidden themselves.
23
+ *
24
+ * The slot itself takes no pointer events, so a click still reaches the video and toggles
25
+ * playback; content that needs a pointer (a tooltip anchor, a button) sets `pointer-events: auto`
26
+ * on itself. `children` land next to the media instead, below the chrome.
22
27
  */
23
28
  overlay?: ReactNode;
24
29
  onSeek?: (fraction: number) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@banou/media-player",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
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",
@@ -49,7 +49,7 @@ export type ChromeProps = {
49
49
  /** Absent means render no video element: the media belongs to someone else and arrives as children. */
50
50
  onVideoRef?: (element: HTMLVideoElement | null) => void
51
51
  onCanvasRef: (element: HTMLCanvasElement | null) => void
52
- /** Above the control bar and outside the click-to-pause region, unlike `children`. */
52
+ /** The app's own content, drawn in the top bar beside the title, unlike `children`. */
53
53
  overlay?: ReactNode
54
54
  children?: ReactNode
55
55
  }
@@ -116,8 +116,7 @@ export const Chrome = ({ ref, onVideoRef, onCanvasRef, overlay, children }: Chro
116
116
  onMouseOut={onMouseOut}
117
117
  className={hideUI ? 'hide' : ''}
118
118
  >
119
- <Overlay onCanvasRef={onCanvasRef} />
120
- {overlay}
119
+ <Overlay onCanvasRef={onCanvasRef} overlay={overlay} />
121
120
  <ControlBar />
122
121
  <div className="video" onClick={onVideoClick}>
123
122
  {onVideoRef ? <video ref={onVideoRef} playsInline /> : null}
@@ -1,4 +1,6 @@
1
1
  /// <reference types="@emotion/react/types/css-prop" />
2
+ import type { ReactNode } from 'react'
3
+
2
4
  import { css, keyframes } from '@emotion/react'
3
5
 
4
6
  import { usePlayer } from '../player'
@@ -13,35 +15,74 @@ const style = css`
13
15
  pointer-events: none;
14
16
  `
15
17
 
16
- const titleStyle = css`
18
+ /**
19
+ * Title on the left, the app's own readout on the right, one gradient behind both.
20
+ *
21
+ * They share a row rather than sitting in two layers because they are the same band of screen: a
22
+ * filename wide enough to wrap would otherwise run under whatever the app put on the right, and two
23
+ * stacked gradients would double the darkening.
24
+ */
25
+ const topBarStyle = css`
17
26
  position: absolute;
18
27
  top: 0;
19
28
  left: 0;
20
29
  width: 100%;
21
- padding: calc(1.2 * var(--mp-unit)) calc(1.6 * var(--mp-unit));
22
- /* clears a notch once the player is fullscreen; resolves to zero everywhere else */
23
- padding-top: calc(calc(1.2 * var(--mp-unit)) + env(safe-area-inset-top, 0px));
24
- ${fonts.headings.small}
25
- color: white;
26
- text-shadow: 0 0 4px rgba(0, 0, 0, 1);
27
30
  z-index: 2;
31
+ /* the video underneath keeps the click that toggles playback; anything in the slot that needs a
32
+ pointer takes it back for itself */
28
33
  pointer-events: none;
29
34
 
30
- /* one line with an ellipsis until there is width to wrap, since a release filename would
31
- otherwise run to four lines on a phone */
32
- white-space: nowrap;
33
- overflow: hidden;
34
- text-overflow: ellipsis;
35
+ display: flex;
36
+ align-items: flex-start;
37
+ gap: calc(1.6 * var(--mp-unit));
38
+
39
+ /* longhands per breakpoint, never the shorthand: a nested at-rule is hoisted after the rule it
40
+ sits in, so a shorthand in a media query would override the longhands above it and silently
41
+ drop the safe-area insets. Those clear a notch once the player is fullscreen and resolve to
42
+ zero everywhere else. */
43
+ padding-top: calc(calc(1.2 * var(--mp-unit)) + env(safe-area-inset-top, 0px));
44
+ padding-bottom: calc(1.2 * var(--mp-unit));
45
+ padding-left: calc(calc(1.6 * var(--mp-unit)) + env(safe-area-inset-left, 0px));
46
+ padding-right: calc(calc(1.6 * var(--mp-unit)) + env(safe-area-inset-right, 0px));
35
47
 
36
48
  @media (min-width: 768px) {
37
- padding: calc(2.4 * var(--mp-unit));
38
49
  padding-top: calc(calc(2.4 * var(--mp-unit)) + env(safe-area-inset-top, 0px));
39
- white-space: normal;
40
- overflow: visible;
50
+ padding-bottom: calc(2.4 * var(--mp-unit));
51
+ padding-left: calc(calc(2.4 * var(--mp-unit)) + env(safe-area-inset-left, 0px));
52
+ padding-right: calc(calc(2.4 * var(--mp-unit)) + env(safe-area-inset-right, 0px));
41
53
  }
42
54
 
43
55
  background: linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.3) 30%, rgba(0,0,0,0.2) 60%, rgba(0,0,0,0.1) 80%, transparent 100%);
44
- transition: opacity 0.1s cubic-bezier(.4,0,1,1);
56
+ /* visibility, not only opacity: a slot child that took pointer events back would otherwise stay
57
+ hoverable while faded out, popping a tooltip over nothing. It cascades where pointer-events
58
+ does not, and transitioning it holds the element visible for the length of the fade. */
59
+ transition: opacity 0.1s cubic-bezier(.4,0,1,1), visibility 0.1s;
60
+
61
+ .title {
62
+ ${fonts.headings.small}
63
+ color: white;
64
+ text-shadow: 0 0 4px rgba(0, 0, 0, 1);
65
+
66
+ /* one line with an ellipsis until there is width to wrap, since a release filename would
67
+ otherwise run to four lines on a phone */
68
+ flex: 1;
69
+ min-width: 0;
70
+ white-space: nowrap;
71
+ overflow: hidden;
72
+ text-overflow: ellipsis;
73
+
74
+ @media (min-width: 768px) {
75
+ white-space: normal;
76
+ overflow: visible;
77
+ }
78
+ }
79
+
80
+ /* pinned right with or without a title, so an app's readout does not jump sideways when a
81
+ filename arrives late */
82
+ .app-slot {
83
+ margin-left: auto;
84
+ flex: none;
85
+ }
45
86
  `
46
87
 
47
88
  const spin = keyframes`
@@ -86,7 +127,13 @@ const errorMessage = (error: unknown) =>
86
127
  ? error.message
87
128
  : typeof error === 'string' ? error : 'Playback failed'
88
129
 
89
- export const Overlay = ({ onCanvasRef }: { onCanvasRef: (element: HTMLCanvasElement | null) => void }) => {
130
+ export type OverlayProps = {
131
+ onCanvasRef: (element: HTMLCanvasElement | null) => void
132
+ /** The app's own content, drawn at the right of the top bar next to the title. */
133
+ overlay?: ReactNode
134
+ }
135
+
136
+ export const Overlay = ({ onCanvasRef, overlay }: OverlayProps) => {
90
137
  const title = usePlayer((state) => state.title)
91
138
  const hideUI = usePlayer((state) => state.hideUI)
92
139
  const playbackError = usePlayer((state) => state.playbackError)
@@ -97,13 +144,14 @@ export const Overlay = ({ onCanvasRef }: { onCanvasRef: (element: HTMLCanvasElem
97
144
 
98
145
  return (
99
146
  <>
100
- {title
147
+ {title || overlay
101
148
  ? (
102
149
  <div
103
- css={titleStyle}
104
- style={{ ...hideUI ? { opacity: '0', pointerEvents: 'none' } : {} }}
150
+ css={topBarStyle}
151
+ style={{ ...hideUI ? { opacity: '0', visibility: 'hidden', pointerEvents: 'none' } : {} }}
105
152
  >
106
- {title}
153
+ {title ? <div className="title">{title}</div> : undefined}
154
+ {overlay ? <div className="app-slot">{overlay}</div> : undefined}
107
155
  </div>
108
156
  )
109
157
  : undefined}
@@ -27,8 +27,13 @@ type CommonOptions = {
27
27
  autoplay?: boolean
28
28
 
29
29
  /**
30
- * Drawn above the control bar and outside the click-to-pause region, for whatever the app has to
31
- * say over the video. `children` land next to the media instead, below the chrome.
30
+ * The app's own readout, drawn at the right of the top bar beside `title`, for whatever it has to
31
+ * say over the video. It shares the title's gradient and fades with the rest of the chrome, so a
32
+ * running counter does not sit over the picture once the controls have hidden themselves.
33
+ *
34
+ * The slot itself takes no pointer events, so a click still reaches the video and toggles
35
+ * playback; content that needs a pointer (a tooltip anchor, a button) sets `pointer-events: auto`
36
+ * on itself. `children` land next to the media instead, below the chrome.
32
37
  */
33
38
  overlay?: ReactNode
34
39
 
@@ -204,6 +209,19 @@ const rootStyle = css`
204
209
  */
205
210
  --mp-unit: 10px;
206
211
 
212
+ /**
213
+ * The other half of owning the chrome's own scale.
214
+ *
215
+ * Every control here is authored against a border box, which this library's dev app sets globally
216
+ * and a host page has no reason to. Shipped without it, anything that is \`width: 100%\` AND carries
217
+ * padding overflows its own box: the top bar ran 32px past the player and clipped the right end of
218
+ * whatever the app had put there. Scoped to the player's subtree so it cannot reach the host's
219
+ * layout, and only \`box-sizing\`, because zeroing margins here would reach the app's own content.
220
+ */
221
+ &, & *, & *::before, & *::after {
222
+ box-sizing: border-box;
223
+ }
224
+
207
225
  display: flex;
208
226
  justify-content: center;
209
227
  background-color: #111;