@banou/media-player 0.10.7 → 0.10.9
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 +235 -255
- package/dist/react/components/icons.d.ts +1 -4
- package/dist/react/player.d.ts +1 -0
- package/dist/react/source-feature.d.ts +14 -0
- package/package.json +2 -2
- package/src/lib/react/components/control-bar.tsx +1 -1
- package/src/lib/react/components/errors.tsx +2 -2
- package/src/lib/react/components/icons.tsx +6 -23
- package/src/lib/react/components/progress-bar.tsx +30 -0
- package/src/lib/react/components/settings.tsx +1 -1
- package/src/lib/react/components/skip-chapter.tsx +7 -1
- package/src/lib/react/components/sound.tsx +1 -1
- package/src/lib/react/components/subtitles.tsx +1 -1
- package/src/lib/react/components/track-menu.tsx +1 -1
- package/src/lib/react/source-feature.ts +8 -0
- package/src/lib/utils/chapters.ts +7 -3
|
@@ -1,7 +1,4 @@
|
|
|
1
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;
|
|
5
2
|
/**
|
|
6
3
|
* Subtitles inside the picture: the picture-in-picture frame with caption bars in the inset.
|
|
7
4
|
*
|
|
@@ -16,6 +13,6 @@ export declare const SubtitlesInPicture: (props: SVGProps<SVGSVGElement>) => imp
|
|
|
16
13
|
* The pair exists because that control is the one button in the bar whose glyph did not move with its
|
|
17
14
|
* state, so the state was carried by an accent colour and by nothing else. A pair, not a slash: a
|
|
18
15
|
* slashed resting state reads as unavailable, and the subtitles button beside it already draws
|
|
19
|
-
*
|
|
16
|
+
* Lucide's slash for its own off state.
|
|
20
17
|
*/
|
|
21
18
|
export declare const SubtitlesOutsidePicture: (props: SVGProps<SVGSVGElement>) => import("@emotion/react/jsx-runtime").JSX.Element;
|
package/dist/react/player.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare const Player: import("@videojs/react").CreatePlayerResult<import(
|
|
|
7
7
|
thumbnails: import("..").ThumbnailImage[];
|
|
8
8
|
thumbnailAt?: (time: number) => import("..").ThumbnailImage | undefined;
|
|
9
9
|
requestThumbnail: (time: number | undefined) => void;
|
|
10
|
+
seekPreview: boolean;
|
|
10
11
|
subtitleTracks: import("./source-feature").TrackChoice[];
|
|
11
12
|
selectedSubtitleTrack: string | number | undefined;
|
|
12
13
|
selectSubtitleTrack: (id: string | number | undefined) => void | Promise<void>;
|
|
@@ -130,6 +130,13 @@ export type SourceState = {
|
|
|
130
130
|
* A no-op on a source that brings its own storyboard, and until the generator has booted.
|
|
131
131
|
*/
|
|
132
132
|
requestThumbnail: (time: number | undefined) => void;
|
|
133
|
+
/**
|
|
134
|
+
* True while the seekbar shows what is under the pointer: the time, the chapter and the thumbnail.
|
|
135
|
+
*
|
|
136
|
+
* Written by the seekbar when that opens and closes, never per move, for the reason above. Read by
|
|
137
|
+
* whatever else the chrome draws in the space above the bar, which steps aside meanwhile.
|
|
138
|
+
*/
|
|
139
|
+
seekPreview: boolean;
|
|
133
140
|
/**
|
|
134
141
|
* Both selectors may answer with a promise, and the menu waits on it.
|
|
135
142
|
*
|
|
@@ -266,6 +273,13 @@ export declare const sourceFeature: import("@videojs/react").PlayerFeature<{
|
|
|
266
273
|
* A no-op on a source that brings its own storyboard, and until the generator has booted.
|
|
267
274
|
*/
|
|
268
275
|
requestThumbnail: (time: number | undefined) => void;
|
|
276
|
+
/**
|
|
277
|
+
* True while the seekbar shows what is under the pointer: the time, the chapter and the thumbnail.
|
|
278
|
+
*
|
|
279
|
+
* Written by the seekbar when that opens and closes, never per move, for the reason above. Read by
|
|
280
|
+
* whatever else the chrome draws in the space above the bar, which steps aside meanwhile.
|
|
281
|
+
*/
|
|
282
|
+
seekPreview: boolean;
|
|
269
283
|
/**
|
|
270
284
|
* Both selectors may answer with a promise, and the menu waits on it.
|
|
271
285
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@banou/media-player",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.9",
|
|
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",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"ass-compiler": "^0.1.16",
|
|
47
47
|
"jassub": "^2.5.16",
|
|
48
48
|
"libav-wasm": "^0.8.7",
|
|
49
|
+
"lucide-react": "^0.536.0",
|
|
49
50
|
"osra": "^0.6.12",
|
|
50
|
-
"react-feather": "^2.0.10",
|
|
51
51
|
"react-tooltip": "^5.28.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference types="@emotion/react/types/css-prop" />
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
3
3
|
import { css } from '@emotion/react'
|
|
4
|
-
import { Maximize, Minimize, Pause, Play, RotateCcw } from 'react
|
|
4
|
+
import { Maximize, Minimize, Pause, Play, RotateCcw } from 'lucide-react'
|
|
5
5
|
|
|
6
6
|
import { linearToLogVolume, logToLinearVolume } from '../../utils/volume-utils'
|
|
7
7
|
import { formatMediaTime } from '../../utils/time'
|
|
@@ -3,7 +3,7 @@ import type { PlaybackErrorEntry } from '../source-feature'
|
|
|
3
3
|
|
|
4
4
|
import { useState } from 'react'
|
|
5
5
|
import { css } from '@emotion/react'
|
|
6
|
-
import {
|
|
6
|
+
import { Check, Copy, TriangleAlert } from 'lucide-react'
|
|
7
7
|
|
|
8
8
|
import { fonts } from '../../utils/fonts'
|
|
9
9
|
import { formatTime } from '../../utils/time'
|
|
@@ -173,7 +173,7 @@ export const ErrorsAction = () => {
|
|
|
173
173
|
aria-label={label}
|
|
174
174
|
aria-expanded={open}
|
|
175
175
|
>
|
|
176
|
-
<
|
|
176
|
+
<TriangleAlert className='alert-triangle' />
|
|
177
177
|
</button>
|
|
178
178
|
}
|
|
179
179
|
toolTipText={<span>{label}</span>}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import type { SVGProps } from 'react'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The
|
|
4
|
+
* The two burn-in glyphs, drawn here because Lucide has no "subtitles in the picture in picture
|
|
5
|
+
* window": its `PictureInPicture` has no caption bars and its `Captions` has no inset.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
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`.
|
|
7
|
+
* Drawn on Lucide's own 24 grid at stroke-width 2 with round caps and carrying no size or colour of
|
|
8
|
+
* their own, so the control bar's `svg` rules size and stroke them exactly as they do `Play` and
|
|
9
|
+
* `Settings`.
|
|
11
10
|
*/
|
|
12
11
|
const iconProps = {
|
|
13
12
|
xmlns: 'http://www.w3.org/2000/svg',
|
|
@@ -21,22 +20,6 @@ const iconProps = {
|
|
|
21
20
|
strokeLinejoin: 'round',
|
|
22
21
|
} as const
|
|
23
22
|
|
|
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
|
-
)
|
|
39
|
-
|
|
40
23
|
/**
|
|
41
24
|
* Subtitles inside the picture: the picture-in-picture frame with caption bars in the inset.
|
|
42
25
|
*
|
|
@@ -58,7 +41,7 @@ export const SubtitlesInPicture = (props: SVGProps<SVGSVGElement>) => (
|
|
|
58
41
|
* The pair exists because that control is the one button in the bar whose glyph did not move with its
|
|
59
42
|
* state, so the state was carried by an accent colour and by nothing else. A pair, not a slash: a
|
|
60
43
|
* slashed resting state reads as unavailable, and the subtitles button beside it already draws
|
|
61
|
-
*
|
|
44
|
+
* Lucide's slash for its own off state.
|
|
62
45
|
*/
|
|
63
46
|
export const SubtitlesOutsidePicture = (props: SVGProps<SVGSVGElement>) => (
|
|
64
47
|
<svg {...iconProps} {...props}>
|
|
@@ -248,6 +248,8 @@ export const ProgressBar = () => {
|
|
|
248
248
|
const requestThumbnail = usePlayer((state) => state.requestThumbnail)
|
|
249
249
|
const chapters = usePlayer((state) => state.chapters)
|
|
250
250
|
const seekingTo = usePlayer((state) => state.seekingTo)
|
|
251
|
+
const setSourceState = usePlayer((state) => state.setSourceState)
|
|
252
|
+
const hideUI = usePlayer((state) => state.hideUI)
|
|
251
253
|
|
|
252
254
|
const progressBarRef = useRef<HTMLDivElement>(null)
|
|
253
255
|
|
|
@@ -266,6 +268,19 @@ export const ProgressBar = () => {
|
|
|
266
268
|
requestThumbnail(time)
|
|
267
269
|
}
|
|
268
270
|
|
|
271
|
+
// the same truthiness the readout and the thumbnail below are drawn on
|
|
272
|
+
const previewing = !!progressBarHoverTime
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
if (!previewing) return
|
|
275
|
+
setSourceState({ seekPreview: true })
|
|
276
|
+
return () => setSourceState({ seekPreview: false })
|
|
277
|
+
}, [setSourceState, previewing])
|
|
278
|
+
|
|
279
|
+
// faded out with the chrome, the readout is off screen while it would still hold the offer back
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
if (hideUI) showPreviewAt(undefined)
|
|
282
|
+
}, [hideUI])
|
|
283
|
+
|
|
269
284
|
// onChange reports a bare fraction, so the device that opened the gesture is recorded on press
|
|
270
285
|
const dragPointerType = useRef<string | undefined>(undefined)
|
|
271
286
|
|
|
@@ -346,7 +361,20 @@ export const ProgressBar = () => {
|
|
|
346
361
|
return fraction * duration
|
|
347
362
|
}
|
|
348
363
|
|
|
364
|
+
/*
|
|
365
|
+
* A browser follows a tap with a mouseover and a mousemove at the tap, after the lift has closed
|
|
366
|
+
* the preview, and on a phone no mouse ever moves away to close it again: the readout stays up and
|
|
367
|
+
* the skip offer stays hidden behind it. Those are the only mouse events here with no pointer event
|
|
368
|
+
* of type mouse before them. Mouse events stay the trigger, since a host's jsdom suite hovers with
|
|
369
|
+
* them.
|
|
370
|
+
*/
|
|
371
|
+
const lastPointerType = useRef('mouse')
|
|
372
|
+
const onPointer: DOMAttributes<HTMLDivElement>['onPointerMove'] = (ev) => {
|
|
373
|
+
lastPointerType.current = ev.pointerType
|
|
374
|
+
}
|
|
375
|
+
|
|
349
376
|
const onProgressBarOver: DOMAttributes<HTMLDivElement>['onMouseMove'] = (ev) => {
|
|
377
|
+
if (lastPointerType.current === 'touch') return
|
|
350
378
|
showPreviewAt(timeAtClientX(ev.clientX))
|
|
351
379
|
}
|
|
352
380
|
|
|
@@ -495,6 +523,8 @@ export const ProgressBar = () => {
|
|
|
495
523
|
dragging ? 'dragging' : '',
|
|
496
524
|
segmented ? 'segmented' : '',
|
|
497
525
|
].filter(Boolean).join(' ')}
|
|
526
|
+
onPointerDown={onPointer}
|
|
527
|
+
onPointerMove={onPointer}
|
|
498
528
|
onMouseMove={onProgressBarOver}
|
|
499
529
|
onMouseOut={hideProgressBarTime}
|
|
500
530
|
>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference types="@emotion/react/types/css-prop" />
|
|
2
2
|
import { useState } from 'react'
|
|
3
3
|
import { css } from '@emotion/react'
|
|
4
|
-
import { ChevronLeft, ChevronRight, Settings } from 'react
|
|
4
|
+
import { ChevronLeft, ChevronRight, Settings } from 'lucide-react'
|
|
5
5
|
|
|
6
6
|
import { usePlayer } from '../player'
|
|
7
7
|
import { TooltipDisplay } from './tooltip-display'
|
|
@@ -93,6 +93,12 @@ export const SkipChapter = () => {
|
|
|
93
93
|
const chapters = usePlayer((state) => state.chapters)
|
|
94
94
|
const currentTime = usePlayer((state) => state.currentTime)
|
|
95
95
|
const requestSeek = usePlayer((state) => state.requestSeek)
|
|
96
|
+
/*
|
|
97
|
+
* The seekbar's readout rises into this corner when the pointer nears the end of the bar, and this
|
|
98
|
+
* layer is drawn over the control bar, so the offer steps aside while it is up rather than cover
|
|
99
|
+
* the chapter name. The timer below keeps running: a look at the bar does not extend the offer.
|
|
100
|
+
*/
|
|
101
|
+
const seekPreview = usePlayer((state) => state.seekPreview)
|
|
96
102
|
const player = usePlayer()
|
|
97
103
|
|
|
98
104
|
const kinds = useMemo(() => classifyChapters(chapters), [chapters])
|
|
@@ -155,7 +161,7 @@ export const SkipChapter = () => {
|
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
return (
|
|
158
|
-
<div css={style} className={show ? 'show' : ''}>
|
|
164
|
+
<div css={style} className={show && !seekPreview ? 'show' : ''}>
|
|
159
165
|
<button type='button' className='skip-chapter' onClick={skip}>
|
|
160
166
|
{LABELS[skippable.kind]}
|
|
161
167
|
</button>
|
|
@@ -3,7 +3,7 @@ import type { Ref } from 'react'
|
|
|
3
3
|
|
|
4
4
|
import { useMemo } from 'react'
|
|
5
5
|
import { css } from '@emotion/react'
|
|
6
|
-
import { Volume1, Volume2, VolumeX } from 'react
|
|
6
|
+
import { Volume1, Volume2, VolumeX } from 'lucide-react'
|
|
7
7
|
|
|
8
8
|
import { usePlayer } from '../player'
|
|
9
9
|
import { TooltipDisplay } from './tooltip-display'
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/// <reference types="@emotion/react/types/css-prop" />
|
|
2
2
|
import { css } from '@emotion/react'
|
|
3
|
+
import { Captions, CaptionsOff } from 'lucide-react'
|
|
3
4
|
|
|
4
5
|
import { usePlayer } from '../player'
|
|
5
6
|
import { TooltipDisplay } from './tooltip-display'
|
|
6
|
-
import { Captions, CaptionsOff } from './icons'
|
|
7
7
|
import { TrackMenu, popoverStyle, useTrackMenu } from './track-menu'
|
|
8
8
|
|
|
9
9
|
const style = css`
|
|
@@ -3,7 +3,7 @@ import type { TrackChoice } from '../source-feature'
|
|
|
3
3
|
|
|
4
4
|
import { useEffect, useRef, useState } from 'react'
|
|
5
5
|
import { css } from '@emotion/react'
|
|
6
|
-
import { ChevronLeft } from 'react
|
|
6
|
+
import { ChevronLeft } from 'lucide-react'
|
|
7
7
|
|
|
8
8
|
import { fonts } from '../../utils/fonts'
|
|
9
9
|
|
|
@@ -139,6 +139,13 @@ export type SourceState = {
|
|
|
139
139
|
* A no-op on a source that brings its own storyboard, and until the generator has booted.
|
|
140
140
|
*/
|
|
141
141
|
requestThumbnail: (time: number | undefined) => void
|
|
142
|
+
/**
|
|
143
|
+
* True while the seekbar shows what is under the pointer: the time, the chapter and the thumbnail.
|
|
144
|
+
*
|
|
145
|
+
* Written by the seekbar when that opens and closes, never per move, for the reason above. Read by
|
|
146
|
+
* whatever else the chrome draws in the space above the bar, which steps aside meanwhile.
|
|
147
|
+
*/
|
|
148
|
+
seekPreview: boolean
|
|
142
149
|
|
|
143
150
|
/**
|
|
144
151
|
* Both selectors may answer with a promise, and the menu waits on it.
|
|
@@ -258,6 +265,7 @@ const initialState: SourceState = {
|
|
|
258
265
|
chapters: [],
|
|
259
266
|
thumbnails: [],
|
|
260
267
|
requestThumbnail: () => {},
|
|
268
|
+
seekPreview: false,
|
|
261
269
|
subtitleTracks: [],
|
|
262
270
|
selectedSubtitleTrack: undefined,
|
|
263
271
|
selectSubtitleTrack: () => {},
|
|
@@ -151,7 +151,7 @@ const LABEL_SEPARATORS = /[-:|~/\\[\]()「」『』【】,.!?"'’]|\s+by\s+/u
|
|
|
151
151
|
/** Titles a container writes when it has nothing to say, which must never look like a marker. */
|
|
152
152
|
const isPlaceholder = (title: string): boolean =>
|
|
153
153
|
// "Chapter 07", and the timestamps one muxer writes as titles, which are 300 markers in the sample
|
|
154
|
-
/^chapter\s*\d+$/i.test(title.trim()) || /^\d{1,2}:\d{2}:\d{2}([.,]\d+)?$/.test(title.trim())
|
|
154
|
+
!title.trim() || /^chapter\s*\d+$/i.test(title.trim()) || /^\d{1,2}:\d{2}:\d{2}([.,]\d+)?$/.test(title.trim())
|
|
155
155
|
|
|
156
156
|
const normalise = (title: string): string =>
|
|
157
157
|
(title.trim().toLowerCase().split(LABEL_SEPARATORS)[0] ?? '').trim().replace(/\s+/gu, ' ')
|
|
@@ -297,6 +297,10 @@ export const classifyChapters = (chapters: MediaChapter[]): (ChapterKind | undef
|
|
|
297
297
|
return guess && !plain.includes(guess) ? guess : undefined
|
|
298
298
|
})
|
|
299
299
|
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
/*
|
|
301
|
+
* Shape is the last resort, and only for a file where no person named anything. A name that is not
|
|
302
|
+
* a theme still answers for its chapter: a theme-length Recap in the first half would otherwise be
|
|
303
|
+
* offered as the opening, and a Preview in the second as the ending.
|
|
304
|
+
*/
|
|
305
|
+
return chapters.every(({ title }) => isPlaceholder(title)) ? guessFromShape(chapters) : named
|
|
302
306
|
}
|