@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 +79 -25
- package/dist/react/components/chrome.d.ts +1 -1
- package/dist/react/components/overlay.d.ts +6 -2
- package/dist/react/video-player.d.ts +7 -2
- package/package.json +1 -1
- package/src/lib/react/components/chrome.tsx +2 -3
- package/src/lib/react/components/overlay.tsx +69 -21
- package/src/lib/react/video-player.tsx +20 -2
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
|
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__ */
|
|
343
|
+
n || t ? /* @__PURE__ */ y("div", {
|
|
312
344
|
css: ae,
|
|
313
|
-
style: { ...
|
|
345
|
+
style: { ...r ? {
|
|
314
346
|
opacity: "0",
|
|
347
|
+
visibility: "hidden",
|
|
315
348
|
pointerEvents: "none"
|
|
316
349
|
} : {} },
|
|
317
|
-
children:
|
|
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
|
-
(
|
|
320
|
-
|
|
358
|
+
(o ? !a : s) && !i ? /* @__PURE__ */ v("div", { css: oe }) : void 0,
|
|
359
|
+
i ? /* @__PURE__ */ v("div", {
|
|
321
360
|
css: se,
|
|
322
|
-
children: ce(
|
|
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, {
|
|
1470
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export type OverlayProps = {
|
|
2
3
|
onCanvasRef: (element: HTMLCanvasElement | null) => void;
|
|
3
|
-
|
|
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
|
-
*
|
|
21
|
-
* say over the video.
|
|
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
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
|
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={
|
|
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
|
-
*
|
|
31
|
-
* say over the video.
|
|
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;
|