@banou/media-player 0.8.3 → 0.8.5
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 +407 -203
- package/dist/react/components/chrome.d.ts +3 -1
- package/dist/react/components/icons.d.ts +4 -0
- package/dist/react/components/subtitles.d.ts +9 -0
- package/dist/react/components/track-menu.d.ts +47 -0
- package/dist/react/player.d.ts +3 -2
- package/dist/react/source-feature.d.ts +33 -4
- package/dist/react/video-player.d.ts +30 -1
- package/package.json +1 -1
- package/src/lib/react/components/chrome.tsx +4 -2
- package/src/lib/react/components/control-bar.tsx +4 -2
- package/src/lib/react/components/icons.tsx +38 -0
- package/src/lib/react/components/progress-bar.tsx +20 -4
- package/src/lib/react/components/settings.tsx +26 -206
- package/src/lib/react/components/subtitles.tsx +80 -0
- package/src/lib/react/components/track-menu.tsx +277 -0
- package/src/lib/react/source-feature.ts +20 -2
- package/src/lib/react/video-player.tsx +77 -3
|
@@ -6,7 +6,9 @@ export type ChromeProps = {
|
|
|
6
6
|
onCanvasRef: (element: HTMLCanvasElement | null) => void;
|
|
7
7
|
/** The app's own content, over the video and outside the click-to-pause region, unlike `children`. */
|
|
8
8
|
overlay?: ReactNode;
|
|
9
|
+
/** False draws no control bar at all, leaving the picture, the title and the overlay. */
|
|
10
|
+
controls?: boolean;
|
|
9
11
|
children?: ReactNode;
|
|
10
12
|
};
|
|
11
|
-
export declare const Chrome: ({ ref, onVideoRef, onCanvasRef, overlay, children }: ChromeProps) => import("@emotion/react/jsx-runtime").JSX.Element;
|
|
13
|
+
export declare const Chrome: ({ ref, onVideoRef, onCanvasRef, overlay, controls, children }: ChromeProps) => import("@emotion/react/jsx-runtime").JSX.Element;
|
|
12
14
|
export default Chrome;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SVGProps } from 'react';
|
|
2
|
+
export declare const Captions: (props: SVGProps<SVGSVGElement>) => import("@emotion/react/jsx-runtime").JSX.Element;
|
|
3
|
+
/** The slash is Feather's own convention for an off state, the line `MicOff` and `BellOff` draw. */
|
|
4
|
+
export declare const CaptionsOff: (props: SVGProps<SVGSVGElement>) => import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subtitles, one click from the control bar.
|
|
3
|
+
*
|
|
4
|
+
* It used to be a row inside the settings menu, which put the most-reached control in the player two
|
|
5
|
+
* clicks deep behind a gear, next to playback speed. Audio stays in there: switching it is rare, and
|
|
6
|
+
* on a source that owns its own player it is slow enough to be a considered act.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SubtitlesAction: () => import("@emotion/react/jsx-runtime").JSX.Element | null;
|
|
9
|
+
export default SubtitlesAction;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { TrackChoice } from '../source-feature';
|
|
2
|
+
/**
|
|
3
|
+
* The popover surface and the track rows, shared by every menu in the control bar.
|
|
4
|
+
*
|
|
5
|
+
* Carries NO `position` of its own: the menu anchors to the control bar, which is the width of the
|
|
6
|
+
* player box, so each caller keeps `position: static` on its own wrapper. Anchoring to the button
|
|
7
|
+
* instead is what let the menu hang outside the box, where the root's `overflow: hidden` made it
|
|
8
|
+
* unreachable rather than merely ugly.
|
|
9
|
+
*/
|
|
10
|
+
export declare const popoverStyle: import("@emotion/utils").SerializedStyles;
|
|
11
|
+
/**
|
|
12
|
+
* One track picker, used by the subtitles button and by the settings menu's audio page.
|
|
13
|
+
*
|
|
14
|
+
* `pending` and `failed` describe the switch the viewer just asked for, not the menu: while a source
|
|
15
|
+
* is working, every row is inert and the one being switched to says so, because the selection has not
|
|
16
|
+
* moved yet and a tick next to it would be a lie.
|
|
17
|
+
*/
|
|
18
|
+
export declare const TrackMenu: ({ title, tracks, selected, onSelect, onBack, offLabel, pending, failed }: {
|
|
19
|
+
title: string;
|
|
20
|
+
tracks: TrackChoice[];
|
|
21
|
+
selected: string | number | undefined;
|
|
22
|
+
onSelect: (id: string | number | undefined) => void;
|
|
23
|
+
/** Absent means the menu was opened straight from the bar, so the header is a label and no more. */
|
|
24
|
+
onBack?: () => void;
|
|
25
|
+
/** Absent means the menu offers no way off at all, which is what audio wants. */
|
|
26
|
+
offLabel?: string;
|
|
27
|
+
/** The id currently being switched to, where `null` is the off row and undefined means idle. */
|
|
28
|
+
pending?: string | number | null;
|
|
29
|
+
failed?: boolean;
|
|
30
|
+
}) => import("@emotion/react/jsx-runtime").JSX.Element;
|
|
31
|
+
/**
|
|
32
|
+
* The open/closed and in-flight state every track menu in the bar needs.
|
|
33
|
+
*
|
|
34
|
+
* One hook rather than two, because `runSelect` is what decides when the menu may close: splitting
|
|
35
|
+
* the popover state from the selection state would wire them in a circle.
|
|
36
|
+
*/
|
|
37
|
+
export declare const useTrackMenu: ({ onReset }?: {
|
|
38
|
+
onReset?: () => void;
|
|
39
|
+
}) => {
|
|
40
|
+
open: boolean;
|
|
41
|
+
toggle: () => void;
|
|
42
|
+
close: () => void;
|
|
43
|
+
containerRef: import("react").RefObject<HTMLDivElement | null>;
|
|
44
|
+
pending: string | number | null | undefined;
|
|
45
|
+
failed: boolean;
|
|
46
|
+
runSelect: (id: string | number | null, select: () => void | Promise<void>) => void;
|
|
47
|
+
};
|
package/dist/react/player.d.ts
CHANGED
|
@@ -7,10 +7,11 @@ export declare const Player: import("@videojs/react").CreatePlayerResult<import(
|
|
|
7
7
|
thumbnailAt?: (time: number) => import("..").ThumbnailImage | undefined;
|
|
8
8
|
subtitleTracks: import("./source-feature").TrackChoice[];
|
|
9
9
|
selectedSubtitleTrack: string | number | undefined;
|
|
10
|
-
selectSubtitleTrack: (id: string | number | undefined) => void
|
|
10
|
+
selectSubtitleTrack: (id: string | number | undefined) => void | Promise<void>;
|
|
11
|
+
subtitleOffLabel?: string;
|
|
11
12
|
audioTracks: import("./source-feature").TrackChoice[];
|
|
12
13
|
selectedAudioTrack: string | number | undefined;
|
|
13
|
-
selectAudioTrack: (id: string | number) => void
|
|
14
|
+
selectAudioTrack: (id: string | number) => void | Promise<void>;
|
|
14
15
|
hideUI: boolean;
|
|
15
16
|
setHideUI: (hide: boolean) => void;
|
|
16
17
|
togglePictureInPicture: (() => void) | null;
|
|
@@ -10,6 +10,13 @@ import type { MediaIndex, ThumbnailImage } from '../engine';
|
|
|
10
10
|
export type TrackChoice = {
|
|
11
11
|
id: string | number;
|
|
12
12
|
label: string;
|
|
13
|
+
/**
|
|
14
|
+
* Offered but not selectable, so the menu shows it and refuses the click.
|
|
15
|
+
*
|
|
16
|
+
* Hiding it instead would be worse: a source that lists a dub it cannot currently serve is telling
|
|
17
|
+
* the viewer the dub exists, and a menu that silently omits it looks like the source has nothing.
|
|
18
|
+
*/
|
|
19
|
+
disabled?: boolean;
|
|
13
20
|
};
|
|
14
21
|
/**
|
|
15
22
|
* A byte span of the file the consumer has in hand, mapped onto the timeline through the keyframe
|
|
@@ -40,13 +47,24 @@ export type SourceState = {
|
|
|
40
47
|
* Falls back to scanning `thumbnails` when absent, which is what the engine's generator fills.
|
|
41
48
|
*/
|
|
42
49
|
thumbnailAt?: (time: number) => ThumbnailImage | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Both selectors may answer with a promise, and the menu waits on it.
|
|
52
|
+
*
|
|
53
|
+
* The engine switches a track by pointing the pipeline at another stream, which is immediate and
|
|
54
|
+
* cannot fail, so locally these return nothing. A source that owns its own player is the opposite
|
|
55
|
+
* case: the switch is a round trip through somebody else's UI and takes seconds, and it can lose.
|
|
56
|
+
* A menu that closed on the click would report a selection that has not happened, and a rejection
|
|
57
|
+
* with nothing awaiting it is an unhandled rejection rather than an error the viewer ever sees.
|
|
58
|
+
*/
|
|
43
59
|
subtitleTracks: TrackChoice[];
|
|
44
60
|
/** undefined means subtitles are off. */
|
|
45
61
|
selectedSubtitleTrack: string | number | undefined;
|
|
46
|
-
selectSubtitleTrack: (id: string | number | undefined) => void
|
|
62
|
+
selectSubtitleTrack: (id: string | number | undefined) => void | Promise<void>;
|
|
63
|
+
/** What the row that turns subtitles off is called, when the source would rather name it itself. */
|
|
64
|
+
subtitleOffLabel?: string;
|
|
47
65
|
audioTracks: TrackChoice[];
|
|
48
66
|
selectedAudioTrack: string | number | undefined;
|
|
49
|
-
selectAudioTrack: (id: string | number) => void
|
|
67
|
+
selectAudioTrack: (id: string | number) => void | Promise<void>;
|
|
50
68
|
/** Chrome auto-hide. True means the controls, title and cursor are hidden. */
|
|
51
69
|
hideUI: boolean;
|
|
52
70
|
setHideUI: (hide: boolean) => void;
|
|
@@ -87,13 +105,24 @@ export declare const sourceFeature: import("@videojs/react").PlayerFeature<{
|
|
|
87
105
|
* Falls back to scanning `thumbnails` when absent, which is what the engine's generator fills.
|
|
88
106
|
*/
|
|
89
107
|
thumbnailAt?: (time: number) => ThumbnailImage | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* Both selectors may answer with a promise, and the menu waits on it.
|
|
110
|
+
*
|
|
111
|
+
* The engine switches a track by pointing the pipeline at another stream, which is immediate and
|
|
112
|
+
* cannot fail, so locally these return nothing. A source that owns its own player is the opposite
|
|
113
|
+
* case: the switch is a round trip through somebody else's UI and takes seconds, and it can lose.
|
|
114
|
+
* A menu that closed on the click would report a selection that has not happened, and a rejection
|
|
115
|
+
* with nothing awaiting it is an unhandled rejection rather than an error the viewer ever sees.
|
|
116
|
+
*/
|
|
90
117
|
subtitleTracks: TrackChoice[];
|
|
91
118
|
/** undefined means subtitles are off. */
|
|
92
119
|
selectedSubtitleTrack: string | number | undefined;
|
|
93
|
-
selectSubtitleTrack: (id: string | number | undefined) => void
|
|
120
|
+
selectSubtitleTrack: (id: string | number | undefined) => void | Promise<void>;
|
|
121
|
+
/** What the row that turns subtitles off is called, when the source would rather name it itself. */
|
|
122
|
+
subtitleOffLabel?: string;
|
|
94
123
|
audioTracks: TrackChoice[];
|
|
95
124
|
selectedAudioTrack: string | number | undefined;
|
|
96
|
-
selectAudioTrack: (id: string | number) => void
|
|
125
|
+
selectAudioTrack: (id: string | number) => void | Promise<void>;
|
|
97
126
|
/** Chrome auto-hide. True means the controls, title and cursor are hidden. */
|
|
98
127
|
hideUI: boolean;
|
|
99
128
|
setHideUI: (hide: boolean) => void;
|
|
@@ -14,8 +14,28 @@ export type MediaPlayerSource = {
|
|
|
14
14
|
};
|
|
15
15
|
/** Shared by both arms: nothing here depends on who owns the media. */
|
|
16
16
|
type CommonOptions = {
|
|
17
|
+
/**
|
|
18
|
+
* Drawn across the top of the picture, in a layer of its own.
|
|
19
|
+
*
|
|
20
|
+
* That layer cannot see an `overlay` item, so an app that also places something along the top has
|
|
21
|
+
* two elements competing for one band of screen: on a narrow viewport the title ellipsizes against
|
|
22
|
+
* the FULL width and then runs underneath whatever is painted over it. An app in that position
|
|
23
|
+
* should leave this unset and draw the title as part of its own overlay row, where the two can
|
|
24
|
+
* share a flex line and the filename can be the one that gives way.
|
|
25
|
+
*/
|
|
17
26
|
title?: string;
|
|
18
27
|
autoplay?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Draw the control bar. Defaults to true.
|
|
30
|
+
*
|
|
31
|
+
* False leaves the picture, the title and the overlay and takes away only the chrome, for a host
|
|
32
|
+
* that has to put its own interactive UI over the media for a while. A source whose sign-in form
|
|
33
|
+
* lives inside its own document is the case this exists for: the form is the thing the viewer has
|
|
34
|
+
* to reach, and a control bar for a media that has not loaded yet sits on top of it and eats the
|
|
35
|
+
* clicks. Hiding it from the host side is not an option, since `setHideUI` is installed by the
|
|
36
|
+
* store's `attach` and does nothing at all until a media arrives.
|
|
37
|
+
*/
|
|
38
|
+
controls?: boolean;
|
|
19
39
|
/**
|
|
20
40
|
* The app's own content over the video: a download readout, a badge, a logo, anything the player
|
|
21
41
|
* itself has no opinion about.
|
|
@@ -85,7 +105,16 @@ export type MediaPlayerLocalOptions = CommonOptions & MediaPlayerSource & {
|
|
|
85
105
|
* works: the key session belongs to whoever owns the element.
|
|
86
106
|
*/
|
|
87
107
|
export type MediaPlayerRemoteOptions = CommonOptions & {
|
|
88
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Null means the media has not been found yet: the chrome renders and attaches when it arrives.
|
|
110
|
+
*
|
|
111
|
+
* A source usually has to mount its own document before it can hand over an element, and that
|
|
112
|
+
* document is passed as `children`, so it can only exist once this component has rendered. The
|
|
113
|
+
* key is still REQUIRED even when the value is null, because its presence is what selects this
|
|
114
|
+
* arm: omitting it falls through to the local arm, which draws an idle `<video>` over whatever
|
|
115
|
+
* the children put there.
|
|
116
|
+
*/
|
|
117
|
+
media: PlayerMedia | null;
|
|
89
118
|
read?: never;
|
|
90
119
|
size?: never;
|
|
91
120
|
/** The source's own storyboard, since there are no bytes to generate previews from. */
|
package/package.json
CHANGED
|
@@ -95,10 +95,12 @@ export type ChromeProps = {
|
|
|
95
95
|
onCanvasRef: (element: HTMLCanvasElement | null) => void
|
|
96
96
|
/** The app's own content, over the video and outside the click-to-pause region, unlike `children`. */
|
|
97
97
|
overlay?: ReactNode
|
|
98
|
+
/** False draws no control bar at all, leaving the picture, the title and the overlay. */
|
|
99
|
+
controls?: boolean
|
|
98
100
|
children?: ReactNode
|
|
99
101
|
}
|
|
100
102
|
|
|
101
|
-
export const Chrome = ({ ref, onVideoRef, onCanvasRef, overlay, children }: ChromeProps) => {
|
|
103
|
+
export const Chrome = ({ ref, onVideoRef, onCanvasRef, overlay, controls, children }: ChromeProps) => {
|
|
102
104
|
const player = usePlayer()
|
|
103
105
|
const hideUI = usePlayer((state) => state.hideUI)
|
|
104
106
|
const setHideUI = usePlayer((state) => state.setHideUI)
|
|
@@ -170,7 +172,7 @@ export const Chrome = ({ ref, onVideoRef, onCanvasRef, overlay, children }: Chro
|
|
|
170
172
|
{item}
|
|
171
173
|
</div>
|
|
172
174
|
))}
|
|
173
|
-
<ControlBar />
|
|
175
|
+
{controls === false ? null : <ControlBar />}
|
|
174
176
|
<div className="video" onClick={onVideoClick}>
|
|
175
177
|
{onVideoRef ? <video ref={onVideoRef} playsInline /> : null}
|
|
176
178
|
{children}
|
|
@@ -11,6 +11,7 @@ import { TooltipDisplay } from './tooltip-display'
|
|
|
11
11
|
import { ProgressBar } from './progress-bar'
|
|
12
12
|
import pictureInPicture from '../../assets/picture-in-picture.svg'
|
|
13
13
|
import SettingsAction from './settings'
|
|
14
|
+
import SubtitlesAction from './subtitles'
|
|
14
15
|
import colors from '../../utils/colors'
|
|
15
16
|
import Sound from './sound'
|
|
16
17
|
|
|
@@ -69,7 +70,7 @@ const style = css`
|
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
.play, .sound, .time, .settings, .picture-in-picture, .full-screen {
|
|
73
|
+
.play, .sound, .time, .subtitles, .settings, .picture-in-picture, .full-screen {
|
|
73
74
|
display: flex;
|
|
74
75
|
align-items: center;
|
|
75
76
|
|
|
@@ -87,7 +88,7 @@ const style = css`
|
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
.play, .sound, .settings, .picture-in-picture, .full-screen {
|
|
91
|
+
.play, .sound, .subtitles, .settings, .picture-in-picture, .full-screen {
|
|
91
92
|
border-radius: 4px;
|
|
92
93
|
|
|
93
94
|
padding: 8px;
|
|
@@ -258,6 +259,7 @@ export const ControlBar = () => {
|
|
|
258
259
|
</div>
|
|
259
260
|
</div>
|
|
260
261
|
<div className='right'>
|
|
262
|
+
<SubtitlesAction />
|
|
261
263
|
<SettingsAction />
|
|
262
264
|
{togglePictureInPicture
|
|
263
265
|
? (
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { SVGProps } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The captions glyph, drawn here because react-feather does not have one.
|
|
5
|
+
*
|
|
6
|
+
* Checked against react-feather 2.0.10's 287 icons: there is no captions, subtitles or CC glyph, and
|
|
7
|
+
* nothing it does have reads as one (`Type` is a text-formatting T, `MessageSquare` is a chat bubble,
|
|
8
|
+
* `FileText` is a document). Drawn on Feather's own 24 grid at stroke-width 2 with round caps and
|
|
9
|
+
* carrying no size or colour of its own, so the control bar's `svg` rules size and stroke it exactly
|
|
10
|
+
* as they do `Play` and `Settings`.
|
|
11
|
+
*/
|
|
12
|
+
const iconProps = {
|
|
13
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
14
|
+
width: 24,
|
|
15
|
+
height: 24,
|
|
16
|
+
viewBox: '0 0 24 24',
|
|
17
|
+
fill: 'none',
|
|
18
|
+
stroke: 'currentColor',
|
|
19
|
+
strokeWidth: 2,
|
|
20
|
+
strokeLinecap: 'round',
|
|
21
|
+
strokeLinejoin: 'round',
|
|
22
|
+
} as const
|
|
23
|
+
|
|
24
|
+
export const Captions = (props: SVGProps<SVGSVGElement>) => (
|
|
25
|
+
<svg {...iconProps} {...props}>
|
|
26
|
+
<rect x='3' y='5' width='18' height='14' rx='2' ry='2' />
|
|
27
|
+
<path d='M7 15h4m4 0h2M7 11h2m4 0h4' />
|
|
28
|
+
</svg>
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
/** The slash is Feather's own convention for an off state, the line `MicOff` and `BellOff` draw. */
|
|
32
|
+
export const CaptionsOff = (props: SVGProps<SVGSVGElement>) => (
|
|
33
|
+
<svg {...iconProps} {...props}>
|
|
34
|
+
<rect x='3' y='5' width='18' height='14' rx='2' ry='2' />
|
|
35
|
+
<path d='M7 15h4m4 0h2M7 11h2m4 0h4' />
|
|
36
|
+
<line x1='2' y1='2' x2='22' y2='22' />
|
|
37
|
+
</svg>
|
|
38
|
+
)
|
|
@@ -52,14 +52,28 @@ const style = css`
|
|
|
52
52
|
display: flex;
|
|
53
53
|
justify-content: center;
|
|
54
54
|
|
|
55
|
+
/* The seekbar's other hover affordance, the thumbnail below, gets a rounded box and a drop
|
|
56
|
+
shadow. A source that ships no thumbnails, which is every source whose media the player does
|
|
57
|
+
not own, leaves this readout as the entire preview, so it carries the same treatment instead
|
|
58
|
+
of sitting as bare text on the picture. The ink comes from the player root. */
|
|
59
|
+
background-color: rgba(28, 28, 28, .95);
|
|
60
|
+
border-radius: calc(.4 * var(--mp-unit));
|
|
61
|
+
padding: 0 calc(.6 * var(--mp-unit));
|
|
62
|
+
box-shadow: 0 0 calc(1 * var(--mp-unit)) rgba(0, 0, 0, .5);
|
|
63
|
+
|
|
55
64
|
text-shadow: 0 0 4px rgba(0, 0, 0, 1);
|
|
56
65
|
${fonts.bMedium.bold}
|
|
57
66
|
|
|
58
67
|
position: absolute;
|
|
59
|
-
top:
|
|
60
|
-
|
|
68
|
+
/* Anchored on its bottom edge rather than its top: now that it has a background its height
|
|
69
|
+
follows the font size, and a top-anchored box would grow downward into the track. */
|
|
70
|
+
bottom: calc(1.2 * var(--mp-unit));
|
|
71
|
+
/* Sized to its content, replacing a fixed 5 unit box that \`1:04:09\` overflows. Transparent, that
|
|
72
|
+
overflow was invisible; filled, it would not be. */
|
|
73
|
+
width: max-content;
|
|
74
|
+
white-space: nowrap;
|
|
75
|
+
transform: translateX(-50%);
|
|
61
76
|
|
|
62
|
-
margin-left: calc(-2.5 * var(--mp-unit));
|
|
63
77
|
pointer-events: none;
|
|
64
78
|
}
|
|
65
79
|
|
|
@@ -297,7 +311,9 @@ export const ProgressBar = () => {
|
|
|
297
311
|
? (
|
|
298
312
|
<div
|
|
299
313
|
className="cursor-time"
|
|
300
|
-
|
|
314
|
+
/* the inset grew with the pill: content sized and centred, its half width is now the
|
|
315
|
+
padding plus the text, so the old 18px let a filled box hang past both ends */
|
|
316
|
+
style={{ left: `clamp(calc(3 * var(--mp-unit)), ${timePercentage(progressBarHoverTime)}%, calc(100% - calc(3 * var(--mp-unit))))` }}
|
|
301
317
|
>
|
|
302
318
|
{cusorTimeString}
|
|
303
319
|
</div>
|