@banou/media-player 0.10.7 → 0.10.8
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 +311 -300
- package/dist/react/player.d.ts +1 -0
- package/dist/react/source-feature.d.ts +14 -0
- package/package.json +1 -1
- package/src/lib/react/components/progress-bar.tsx +30 -0
- package/src/lib/react/components/skip-chapter.tsx +7 -1
- package/src/lib/react/source-feature.ts +8 -0
- package/src/lib/utils/chapters.ts +7 -3
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
|
@@ -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
|
>
|
|
@@ -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>
|
|
@@ -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
|
}
|