@banou/media-player 0.7.0 → 0.8.0
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/README.md +3 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +329 -285
- package/dist/react/components/chrome.d.ts +5 -2
- package/dist/react/hooks/use-playback.d.ts +4 -2
- package/dist/react/hooks/use-thumbnails.d.ts +3 -2
- package/dist/react/media.d.ts +98 -0
- package/dist/react/player.d.ts +7 -6
- package/dist/react/source-feature.d.ts +35 -13
- package/dist/react/video-player.d.ts +45 -5
- package/dist/utils/track-label.d.ts +12 -0
- package/package.json +13 -4
- package/src/lib/index.tsx +16 -1
- package/src/lib/react/components/chrome.tsx +7 -3
- package/src/lib/react/components/overlay.tsx +14 -10
- package/src/lib/react/components/progress-bar.tsx +29 -26
- package/src/lib/react/components/settings.tsx +22 -29
- package/src/lib/react/components/sound.tsx +1 -1
- package/src/lib/react/components/tooltip-display.tsx +4 -4
- package/src/lib/react/hooks/use-playback.ts +34 -22
- package/src/lib/react/hooks/use-thumbnails.ts +4 -3
- package/src/lib/react/media.ts +124 -0
- package/src/lib/react/source-feature.ts +31 -13
- package/src/lib/react/video-player.tsx +118 -14
- package/src/lib/utils/fonts.ts +66 -66
- package/src/lib/utils/track-label.ts +15 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { ReactNode, Ref } from 'react';
|
|
2
2
|
export type ChromeProps = {
|
|
3
3
|
ref?: Ref<HTMLDivElement> | ((element: HTMLDivElement | null) => void);
|
|
4
|
-
|
|
4
|
+
/** Absent means render no video element: the media belongs to someone else and arrives as children. */
|
|
5
|
+
onVideoRef?: (element: HTMLVideoElement | null) => void;
|
|
5
6
|
onCanvasRef: (element: HTMLCanvasElement | null) => void;
|
|
7
|
+
/** Above the control bar and outside the click-to-pause region, unlike `children`. */
|
|
8
|
+
overlay?: ReactNode;
|
|
6
9
|
children?: ReactNode;
|
|
7
10
|
};
|
|
8
|
-
export declare const Chrome: ({ ref, onVideoRef, onCanvasRef, children }: ChromeProps) => import("@emotion/react/jsx-runtime").JSX.Element;
|
|
11
|
+
export declare const Chrome: ({ ref, onVideoRef, onCanvasRef, overlay, children }: ChromeProps) => import("@emotion/react/jsx-runtime").JSX.Element;
|
|
9
12
|
export default Chrome;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MediaPlayerLocalOptions } from '../video-player';
|
|
2
2
|
/**
|
|
3
3
|
* Owns the engine for the life of a source: start, teardown, and every piece of state the pipeline
|
|
4
4
|
* discovers. Nothing is mirrored in React state, so the store is the only place any of it lives.
|
|
5
5
|
*/
|
|
6
|
-
export declare const usePlayback: (video: HTMLVideoElement | null, canvas: HTMLCanvasElement | null,
|
|
6
|
+
export declare const usePlayback: (video: HTMLVideoElement | null, canvas: HTMLCanvasElement | null,
|
|
7
|
+
/** null when the media is remote: there are no bytes, so there is no pipeline to run. */
|
|
8
|
+
options: MediaPlayerLocalOptions | null) => void;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { ThumbnailImage } from '../../engine';
|
|
2
2
|
import type { DownloadedRange } from '../source-feature';
|
|
3
3
|
export type UseSeekThumbnailsOptions = {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/** Optional alongside `length`/`read`: with no bytes there is nothing to generate from. */
|
|
5
|
+
publicPath: string | undefined;
|
|
6
|
+
workerUrl: string | undefined;
|
|
6
7
|
length: number | undefined;
|
|
7
8
|
read: ((offset: number, size: number) => Promise<ArrayBuffer>) | undefined;
|
|
8
9
|
/** When omitted the whole file is treated as readable, which is the case for a local file. */
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { ThumbnailImage } from '../engine';
|
|
2
|
+
/**
|
|
3
|
+
* A media object the player drives but does not own.
|
|
4
|
+
*
|
|
5
|
+
* video.js v10 never asks for an `HTMLVideoElement`. Its `PlayerTarget` is `{ media, container }` and
|
|
6
|
+
* every feature gates on property presence alone, with no `instanceof` anywhere, so a plain object
|
|
7
|
+
* carrying these fields lights the whole chrome up. Upstream relies on the same thing: `VimeoMedia`
|
|
8
|
+
* implements `Partial<Video>` over a cross-origin iframe.
|
|
9
|
+
*
|
|
10
|
+
* Declared here rather than imported from `@videojs/media` on purpose. That package is transitive
|
|
11
|
+
* under `@videojs/core`, its `Media` type moved out of `@videojs/core/dom`'s exports between betas,
|
|
12
|
+
* and its shape changed across them, so importing it would pin every consumer to one beta. The
|
|
13
|
+
* assertion at the bottom of this file keeps this type honest against the real one at build time,
|
|
14
|
+
* where `@videojs/media` does exist.
|
|
15
|
+
*
|
|
16
|
+
* OMITTING a property is how you say "not supported". The feature owning it returns out of its
|
|
17
|
+
* `attach` and stays at its defaults, which is why this is mostly optional.
|
|
18
|
+
*/
|
|
19
|
+
export type TimeRangesLike = {
|
|
20
|
+
readonly length: number;
|
|
21
|
+
start: (index: number) => number;
|
|
22
|
+
end: (index: number) => number;
|
|
23
|
+
};
|
|
24
|
+
export type PlayerMedia = EventTarget & {
|
|
25
|
+
play: () => Promise<void>;
|
|
26
|
+
pause: () => void;
|
|
27
|
+
paused: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Seek and source capability. Both are needed or the store goes quiet: `playbackFeature` refuses
|
|
30
|
+
* to attach without them, and `seek` returns without doing anything. A remote media that loads
|
|
31
|
+
* nothing still has to answer `src`/`currentSrc`/`readyState`/`load`, and `readyState` has to
|
|
32
|
+
* reach HAVE_FUTURE_DATA (3) or `waiting` stays true forever and the spinner never clears.
|
|
33
|
+
*/
|
|
34
|
+
currentTime: number;
|
|
35
|
+
duration: number;
|
|
36
|
+
seeking: boolean;
|
|
37
|
+
src?: string;
|
|
38
|
+
currentSrc?: string;
|
|
39
|
+
readyState: number;
|
|
40
|
+
load?: () => void;
|
|
41
|
+
volume?: number;
|
|
42
|
+
muted?: boolean;
|
|
43
|
+
playbackRate?: number;
|
|
44
|
+
ended?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Structural, not the DOM `TimeRanges`, because a remote source transports these as plain pairs and
|
|
47
|
+
* has nothing to hand back that passes an `instanceof`. Note that video.js decides "no buffer
|
|
48
|
+
* capability" by comparing against its own empty sentinel BY IDENTITY, so a hand-rolled empty
|
|
49
|
+
* `{ length: 0 }` reads as a genuinely empty buffer rather than as an absent capability. Omit the
|
|
50
|
+
* field entirely to mean absent.
|
|
51
|
+
*/
|
|
52
|
+
buffered?: TimeRangesLike;
|
|
53
|
+
seekable?: TimeRangesLike;
|
|
54
|
+
/** Structurally what video.js reads off a failed element, so a remote source can report one too. */
|
|
55
|
+
error?: {
|
|
56
|
+
readonly code: number;
|
|
57
|
+
readonly message: string;
|
|
58
|
+
} | null;
|
|
59
|
+
videoWidth?: number;
|
|
60
|
+
videoHeight?: number;
|
|
61
|
+
requestPictureInPicture?: () => Promise<unknown>;
|
|
62
|
+
requestFullscreen?: () => Promise<unknown>;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Where the seekbar's hover previews come from.
|
|
66
|
+
*
|
|
67
|
+
* With bytes in hand the player generates them itself with a second libav worker. A media it does not
|
|
68
|
+
* own has no bytes, but a commercial source usually publishes its own storyboard, so it can answer
|
|
69
|
+
* for a time directly. Narrowed on `at`, the field that carries the difference, rather than on a tag
|
|
70
|
+
* repeating what the shape already says.
|
|
71
|
+
*/
|
|
72
|
+
export type ExternalThumbnails = {
|
|
73
|
+
at: (time: number) => ThumbnailImage | undefined;
|
|
74
|
+
/** Shown all at once on the seekbar if the source can enumerate them; previews work without it. */
|
|
75
|
+
all?: ThumbnailImage[];
|
|
76
|
+
};
|
|
77
|
+
export declare const isExternalThumbnails: (value: unknown) => value is ExternalThumbnails;
|
|
78
|
+
/**
|
|
79
|
+
* One choice among several the player presents but does not fulfil: the source owns the switch.
|
|
80
|
+
*
|
|
81
|
+
* This is what a subtitle or audio track becomes when the media is remote. The player draws the menu
|
|
82
|
+
* and reports the pick; whoever owns the document does the work and renders the result.
|
|
83
|
+
*/
|
|
84
|
+
export type DelegatedSelection = {
|
|
85
|
+
options: readonly {
|
|
86
|
+
id: string;
|
|
87
|
+
label: string;
|
|
88
|
+
disabled?: boolean;
|
|
89
|
+
}[];
|
|
90
|
+
selectedId: string | null;
|
|
91
|
+
select: (id: string | null) => void | Promise<void>;
|
|
92
|
+
/** Label for the entry that turns the feature off, when the source offers one. */
|
|
93
|
+
offLabel?: string;
|
|
94
|
+
};
|
|
95
|
+
export type DelegatedTracks = {
|
|
96
|
+
selection: DelegatedSelection;
|
|
97
|
+
};
|
|
98
|
+
export declare const isDelegatedTracks: (value: unknown) => value is DelegatedTracks;
|
package/dist/react/player.d.ts
CHANGED
|
@@ -4,12 +4,13 @@ export declare const Player: import("@videojs/react").CreatePlayerResult<import(
|
|
|
4
4
|
downloadedRanges?: import("./source-feature").DownloadedRange[];
|
|
5
5
|
indexes: import("..").MediaIndex[];
|
|
6
6
|
thumbnails: import("..").ThumbnailImage[];
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
thumbnailAt?: (time: number) => import("..").ThumbnailImage | undefined;
|
|
8
|
+
subtitleTracks: import("./source-feature").TrackChoice[];
|
|
9
|
+
selectedSubtitleTrack: string | number | undefined;
|
|
10
|
+
selectSubtitleTrack: (id: string | number | undefined) => void;
|
|
11
|
+
audioTracks: import("./source-feature").TrackChoice[];
|
|
12
|
+
selectedAudioTrack: string | number | undefined;
|
|
13
|
+
selectAudioTrack: (id: string | number) => void;
|
|
13
14
|
hideUI: boolean;
|
|
14
15
|
setHideUI: (hide: boolean) => void;
|
|
15
16
|
togglePictureInPicture: () => void;
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MediaIndex, ThumbnailImage } from '../engine';
|
|
2
|
+
/**
|
|
3
|
+
* One row of a track menu, already named.
|
|
4
|
+
*
|
|
5
|
+
* Labelled by whoever writes it rather than by the menu, because the two writers know different
|
|
6
|
+
* things: the engine has a language tag and a title to disambiguate between, while a source that
|
|
7
|
+
* owns its own player hands over a label it has already decided on. The id is opaque here for the
|
|
8
|
+
* same reason, a libav stream index one way and a site's own track id the other.
|
|
9
|
+
*/
|
|
10
|
+
export type TrackChoice = {
|
|
11
|
+
id: string | number;
|
|
12
|
+
label: string;
|
|
13
|
+
};
|
|
2
14
|
/**
|
|
3
15
|
* A byte span of the file the consumer has in hand, mapped onto the timeline through the keyframe
|
|
4
16
|
* index, because a file's download percentage is not its playback percentage.
|
|
@@ -23,13 +35,18 @@ export type SourceState = {
|
|
|
23
35
|
/** Keyframe index of the input, which turns a downloaded byte range into a time range. */
|
|
24
36
|
indexes: MediaIndex[];
|
|
25
37
|
thumbnails: ThumbnailImage[];
|
|
26
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Answers for one time directly, when the source has a storyboard it can index but not enumerate.
|
|
40
|
+
* Falls back to scanning `thumbnails` when absent, which is what the engine's generator fills.
|
|
41
|
+
*/
|
|
42
|
+
thumbnailAt?: (time: number) => ThumbnailImage | undefined;
|
|
43
|
+
subtitleTracks: TrackChoice[];
|
|
27
44
|
/** undefined means subtitles are off. */
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
selectedSubtitleTrack: string | number | undefined;
|
|
46
|
+
selectSubtitleTrack: (id: string | number | undefined) => void;
|
|
47
|
+
audioTracks: TrackChoice[];
|
|
48
|
+
selectedAudioTrack: string | number | undefined;
|
|
49
|
+
selectAudioTrack: (id: string | number) => void;
|
|
33
50
|
/** Chrome auto-hide. True means the controls, title and cursor are hidden. */
|
|
34
51
|
hideUI: boolean;
|
|
35
52
|
setHideUI: (hide: boolean) => void;
|
|
@@ -57,13 +74,18 @@ export declare const sourceFeature: import("@videojs/react").PlayerFeature<{
|
|
|
57
74
|
/** Keyframe index of the input, which turns a downloaded byte range into a time range. */
|
|
58
75
|
indexes: MediaIndex[];
|
|
59
76
|
thumbnails: ThumbnailImage[];
|
|
60
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Answers for one time directly, when the source has a storyboard it can index but not enumerate.
|
|
79
|
+
* Falls back to scanning `thumbnails` when absent, which is what the engine's generator fills.
|
|
80
|
+
*/
|
|
81
|
+
thumbnailAt?: (time: number) => ThumbnailImage | undefined;
|
|
82
|
+
subtitleTracks: TrackChoice[];
|
|
61
83
|
/** undefined means subtitles are off. */
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
84
|
+
selectedSubtitleTrack: string | number | undefined;
|
|
85
|
+
selectSubtitleTrack: (id: string | number | undefined) => void;
|
|
86
|
+
audioTracks: TrackChoice[];
|
|
87
|
+
selectedAudioTrack: string | number | undefined;
|
|
88
|
+
selectAudioTrack: (id: string | number) => void;
|
|
67
89
|
/** Chrome auto-hide. True means the controls, title and cursor are hidden. */
|
|
68
90
|
hideUI: boolean;
|
|
69
91
|
setHideUI: (hide: boolean) => void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { DownloadedRange } from './source-feature';
|
|
3
|
+
import type { DelegatedTracks, ExternalThumbnails, PlayerMedia } from './media';
|
|
3
4
|
/**
|
|
4
5
|
* The source, read a range at a time: the player never downloads the whole file. Both fields travel
|
|
5
6
|
* together, so passing one without the other is a type error.
|
|
@@ -11,7 +12,22 @@ export type MediaPlayerSource = {
|
|
|
11
12
|
read?: undefined;
|
|
12
13
|
size?: undefined;
|
|
13
14
|
};
|
|
14
|
-
|
|
15
|
+
/** Shared by both arms: nothing here depends on who owns the media. */
|
|
16
|
+
type CommonOptions = {
|
|
17
|
+
title?: string;
|
|
18
|
+
autoplay?: boolean;
|
|
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.
|
|
22
|
+
*/
|
|
23
|
+
overlay?: ReactNode;
|
|
24
|
+
onSeek?: (fraction: number) => void;
|
|
25
|
+
onPlaybackError?: (error: unknown) => void;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Bytes in: the player owns the `<video>`, and libav feeds it a fragment at a time.
|
|
29
|
+
*/
|
|
30
|
+
export type MediaPlayerLocalOptions = CommonOptions & MediaPlayerSource & {
|
|
15
31
|
/**
|
|
16
32
|
* Where libav's wasm is served from. BOTH `libav.wasm` and `libav-jspi.wasm` have to be there:
|
|
17
33
|
* libav-wasm picks between them on `WebAssembly.Suspending`, so serving one fails only on the
|
|
@@ -30,13 +46,37 @@ export type MediaPlayerOptions = MediaPlayerSource & {
|
|
|
30
46
|
/** Fallback face for `liberation sans`, used when a subtitle track names a font the file does not carry. */
|
|
31
47
|
defaultFontUrl?: string;
|
|
32
48
|
bufferSize?: number;
|
|
33
|
-
autoplay?: boolean;
|
|
34
|
-
title?: string;
|
|
35
49
|
/** Byte spans available, painted on the seekbar and informing the thumbnail generator. */
|
|
36
50
|
downloadedRanges?: DownloadedRange[];
|
|
37
|
-
|
|
38
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Reader for the thumbnail engine, when it should differ from playback's.
|
|
53
|
+
*
|
|
54
|
+
* They are the same by default so generation shares playback's fetch order. A consumer whose
|
|
55
|
+
* reads are not free wants them apart: a torrent hands this a non-prioritising, fail-fast reader
|
|
56
|
+
* so generating previews cannot steal download order from the bytes playback is blocked on.
|
|
57
|
+
*/
|
|
58
|
+
thumbnailRead?: (offset: number, size: number) => Promise<ArrayBuffer>;
|
|
59
|
+
/** Off entirely. A second wasm worker during pipeline boot is worth avoiding on a slow source. */
|
|
60
|
+
thumbnailsEnabled?: boolean;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* A media the caller owns and the player only drives.
|
|
64
|
+
*
|
|
65
|
+
* No bytes, so no libav, no MediaSource and no thumbnail generation. For a source whose video lives
|
|
66
|
+
* in a document this one cannot reach into, which is also the only arrangement under which its DRM
|
|
67
|
+
* works: the key session belongs to whoever owns the element.
|
|
68
|
+
*/
|
|
69
|
+
export type MediaPlayerRemoteOptions = CommonOptions & {
|
|
70
|
+
media: PlayerMedia;
|
|
71
|
+
read?: never;
|
|
72
|
+
size?: never;
|
|
73
|
+
/** The source's own storyboard, since there are no bytes to generate previews from. */
|
|
74
|
+
thumbnails?: ExternalThumbnails;
|
|
75
|
+
/** The source renders these; the player draws the menu and reports the pick. */
|
|
76
|
+
subtitles?: DelegatedTracks;
|
|
77
|
+
audioTracks?: DelegatedTracks;
|
|
39
78
|
};
|
|
79
|
+
export type MediaPlayerOptions = MediaPlayerLocalOptions | MediaPlayerRemoteOptions;
|
|
40
80
|
export declare const MediaPlayer: (options: MediaPlayerOptions & {
|
|
41
81
|
children?: ReactNode;
|
|
42
82
|
}) => import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -3,6 +3,11 @@ export type LabelledTrack = {
|
|
|
3
3
|
title: string;
|
|
4
4
|
language: string;
|
|
5
5
|
};
|
|
6
|
+
/** One row of a track menu, already named. Mirrors `TrackChoice` without importing the store. */
|
|
7
|
+
export type NamedTrack = {
|
|
8
|
+
id: string | number;
|
|
9
|
+
label: string;
|
|
10
|
+
};
|
|
6
11
|
/**
|
|
7
12
|
* Human name for a track's language tag. `fallback: 'none'` is what makes an unknown tag come back
|
|
8
13
|
* undefined instead of echoing itself, so the caller can fall through to the title.
|
|
@@ -17,3 +22,10 @@ export declare const labelTracks: <T extends LabelledTrack>(tracks: T[]) => {
|
|
|
17
22
|
track: T;
|
|
18
23
|
label: string;
|
|
19
24
|
}[];
|
|
25
|
+
/**
|
|
26
|
+
* Names the engine's streams once, on the way into the store, so the menu only ever reads rows.
|
|
27
|
+
*
|
|
28
|
+
* The store's ids are opaque so a source that owns its own player can use its own track ids; the id
|
|
29
|
+
* here is the libav stream index, which is what this half of the code has to say.
|
|
30
|
+
*/
|
|
31
|
+
export declare const toNamedTracks: <T extends LabelledTrack>(tracks: T[]) => NamedTrack[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@banou/media-player",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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",
|
|
@@ -18,17 +18,23 @@
|
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"dist",
|
|
21
|
-
"src/lib"
|
|
21
|
+
"src/lib",
|
|
22
|
+
"!src/lib/**/*.test.ts",
|
|
23
|
+
"!src/lib/**/*.test.tsx",
|
|
24
|
+
"!src/lib/**/*.fixture.ts"
|
|
22
25
|
],
|
|
23
26
|
"scripts": {
|
|
24
27
|
"dev": "npm run copy-assets && vp dev --port 4560",
|
|
25
28
|
"build": "shx rm -rf build/ && npm run copy-assets && vp build",
|
|
26
29
|
"preview": "vp preview",
|
|
27
30
|
"build-lib": "shx rm -rf dist/ && vp build -c vite.lib.config.ts && npm run types",
|
|
31
|
+
"prepublishOnly": "npm run build-lib",
|
|
28
32
|
"types": "tsc -p tsconfig.lib.json",
|
|
29
33
|
"copy-assets": "shx mkdir -p public && npm run copy-libav && npm run copy-libass",
|
|
30
34
|
"copy-libav": "shx cp node_modules/libav-wasm/build/worker.js public/libav-worker.js && shx cp node_modules/libav-wasm/build/libav.wasm public/libav.wasm && shx cp node_modules/libav-wasm/build/libav-jspi.wasm public/libav-jspi.wasm",
|
|
31
|
-
"copy-libass": "shx cp node_modules/jassub/dist/jassub-worker.js public/jassub-worker.js && shx cp node_modules/jassub/dist/jassub-worker.wasm public/jassub-worker.wasm && shx cp node_modules/jassub/dist/jassub-worker-modern.wasm public/jassub-worker-modern.wasm && shx cp node_modules/jassub/dist/default.woff2 public/default.woff2"
|
|
35
|
+
"copy-libass": "shx cp node_modules/jassub/dist/jassub-worker.js public/jassub-worker.js && shx cp node_modules/jassub/dist/jassub-worker.wasm public/jassub-worker.wasm && shx cp node_modules/jassub/dist/jassub-worker-modern.wasm public/jassub-worker-modern.wasm && shx cp node_modules/jassub/dist/default.woff2 public/default.woff2",
|
|
36
|
+
"test": "npm run fixtures && vp test run",
|
|
37
|
+
"fixtures": "node scripts/fixture.mjs"
|
|
32
38
|
},
|
|
33
39
|
"author": "Banou26",
|
|
34
40
|
"dependencies": {
|
|
@@ -50,12 +56,15 @@
|
|
|
50
56
|
"@types/react": "^19.0.0",
|
|
51
57
|
"@types/react-dom": "^19.0.0",
|
|
52
58
|
"@vitejs/plugin-react": "^6.0.3",
|
|
59
|
+
"@vitest/browser-playwright": "^4.1.10",
|
|
60
|
+
"playwright": "^1.62.1",
|
|
53
61
|
"react": "^19.0.0",
|
|
54
62
|
"react-dom": "^19.0.0",
|
|
55
63
|
"shx": "^0.3.4",
|
|
56
64
|
"typescript": "^7.0.2",
|
|
57
65
|
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.4",
|
|
58
|
-
"vite-plus": "0.2.4"
|
|
66
|
+
"vite-plus": "0.2.4",
|
|
67
|
+
"vitest-browser-react": "^2.2.0"
|
|
59
68
|
},
|
|
60
69
|
"overrides": {
|
|
61
70
|
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.4"
|
package/src/lib/index.tsx
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
/// <reference types="@emotion/react/types/css-prop" />
|
|
2
2
|
|
|
3
3
|
export { MediaPlayer, default } from './react/video-player'
|
|
4
|
-
export type {
|
|
4
|
+
export type {
|
|
5
|
+
MediaPlayerOptions,
|
|
6
|
+
MediaPlayerLocalOptions,
|
|
7
|
+
MediaPlayerRemoteOptions,
|
|
8
|
+
MediaPlayerSource,
|
|
9
|
+
} from './react/video-player'
|
|
10
|
+
|
|
11
|
+
// A media the player drives but does not own, for a source whose video lives somewhere unreachable.
|
|
12
|
+
export { isDelegatedTracks, isExternalThumbnails } from './react/media'
|
|
13
|
+
export type {
|
|
14
|
+
DelegatedSelection,
|
|
15
|
+
DelegatedTracks,
|
|
16
|
+
ExternalThumbnails,
|
|
17
|
+
PlayerMedia,
|
|
18
|
+
TimeRangesLike,
|
|
19
|
+
} from './react/media'
|
|
5
20
|
|
|
6
21
|
export { Player, usePlayer } from './react/player'
|
|
7
22
|
export type { PlayerStore } from './react/player'
|
|
@@ -46,12 +46,15 @@ const style = css`
|
|
|
46
46
|
|
|
47
47
|
export type ChromeProps = {
|
|
48
48
|
ref?: Ref<HTMLDivElement> | ((element: HTMLDivElement | null) => void)
|
|
49
|
-
|
|
49
|
+
/** Absent means render no video element: the media belongs to someone else and arrives as children. */
|
|
50
|
+
onVideoRef?: (element: HTMLVideoElement | null) => void
|
|
50
51
|
onCanvasRef: (element: HTMLCanvasElement | null) => void
|
|
52
|
+
/** Above the control bar and outside the click-to-pause region, unlike `children`. */
|
|
53
|
+
overlay?: ReactNode
|
|
51
54
|
children?: ReactNode
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
export const Chrome = ({ ref, onVideoRef, onCanvasRef, children }: ChromeProps) => {
|
|
57
|
+
export const Chrome = ({ ref, onVideoRef, onCanvasRef, overlay, children }: ChromeProps) => {
|
|
55
58
|
const player = usePlayer()
|
|
56
59
|
const hideUI = usePlayer((state) => state.hideUI)
|
|
57
60
|
const setHideUI = usePlayer((state) => state.setHideUI)
|
|
@@ -114,9 +117,10 @@ export const Chrome = ({ ref, onVideoRef, onCanvasRef, children }: ChromeProps)
|
|
|
114
117
|
className={hideUI ? 'hide' : ''}
|
|
115
118
|
>
|
|
116
119
|
<Overlay onCanvasRef={onCanvasRef} />
|
|
120
|
+
{overlay}
|
|
117
121
|
<ControlBar />
|
|
118
122
|
<div className="video" onClick={onVideoClick}>
|
|
119
|
-
<video ref={onVideoRef} playsInline />
|
|
123
|
+
{onVideoRef ? <video ref={onVideoRef} playsInline /> : null}
|
|
120
124
|
{children}
|
|
121
125
|
</div>
|
|
122
126
|
</div>
|
|
@@ -18,9 +18,9 @@ const titleStyle = css`
|
|
|
18
18
|
top: 0;
|
|
19
19
|
left: 0;
|
|
20
20
|
width: 100%;
|
|
21
|
-
padding: 1.
|
|
21
|
+
padding: calc(1.2 * var(--mp-unit)) calc(1.6 * var(--mp-unit));
|
|
22
22
|
/* clears a notch once the player is fullscreen; resolves to zero everywhere else */
|
|
23
|
-
padding-top: calc(1.
|
|
23
|
+
padding-top: calc(calc(1.2 * var(--mp-unit)) + env(safe-area-inset-top, 0px));
|
|
24
24
|
${fonts.headings.small}
|
|
25
25
|
color: white;
|
|
26
26
|
text-shadow: 0 0 4px rgba(0, 0, 0, 1);
|
|
@@ -34,8 +34,8 @@ const titleStyle = css`
|
|
|
34
34
|
text-overflow: ellipsis;
|
|
35
35
|
|
|
36
36
|
@media (min-width: 768px) {
|
|
37
|
-
padding: 2.
|
|
38
|
-
padding-top: calc(2.
|
|
37
|
+
padding: calc(2.4 * var(--mp-unit));
|
|
38
|
+
padding-top: calc(calc(2.4 * var(--mp-unit)) + env(safe-area-inset-top, 0px));
|
|
39
39
|
white-space: normal;
|
|
40
40
|
overflow: visible;
|
|
41
41
|
}
|
|
@@ -59,8 +59,8 @@ const loadingStyle = css`
|
|
|
59
59
|
|
|
60
60
|
::after {
|
|
61
61
|
content: '';
|
|
62
|
-
width:
|
|
63
|
-
height:
|
|
62
|
+
width: calc(4 * var(--mp-unit));
|
|
63
|
+
height: calc(4 * var(--mp-unit));
|
|
64
64
|
border-radius: 50%;
|
|
65
65
|
border: 3px solid rgba(255, 255, 255, 0.25);
|
|
66
66
|
border-top-color: #fff;
|
|
@@ -73,7 +73,7 @@ const errorStyle = css`
|
|
|
73
73
|
position: absolute;
|
|
74
74
|
inset: auto 0 20%;
|
|
75
75
|
z-index: 3;
|
|
76
|
-
padding: 0
|
|
76
|
+
padding: 0 calc(2 * var(--mp-unit));
|
|
77
77
|
text-align: center;
|
|
78
78
|
color: #fff;
|
|
79
79
|
${fonts.bLarge.regular}
|
|
@@ -92,6 +92,8 @@ export const Overlay = ({ onCanvasRef }: { onCanvasRef: (element: HTMLCanvasElem
|
|
|
92
92
|
const playbackError = usePlayer((state) => state.playbackError)
|
|
93
93
|
const ready = usePlayer((state) => state.ready)
|
|
94
94
|
const size = usePlayer((state) => state.size)
|
|
95
|
+
// video.js's own: readyState below HAVE_FUTURE_DATA while not paused
|
|
96
|
+
const waiting = usePlayer((state) => state.waiting)
|
|
95
97
|
|
|
96
98
|
return (
|
|
97
99
|
<>
|
|
@@ -105,9 +107,11 @@ export const Overlay = ({ onCanvasRef }: { onCanvasRef: (element: HTMLCanvasElem
|
|
|
105
107
|
</div>
|
|
106
108
|
)
|
|
107
109
|
: undefined}
|
|
108
|
-
{/*
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
{/* Two different waits, one spinner. With bytes it is pre-metadata rather than buffering: the
|
|
111
|
+
store reports 0 both before metadata and for a genuinely unknown duration, so `size` is what
|
|
112
|
+
tells whether a source was handed over at all. A media this player does not own has neither
|
|
113
|
+
`size` nor `ready`, and reports the ordinary `waiting` every element does. */}
|
|
114
|
+
{(size ? !ready : waiting) && !playbackError
|
|
111
115
|
? <div css={loadingStyle} />
|
|
112
116
|
: undefined}
|
|
113
117
|
{playbackError
|