@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.
@@ -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.7",
3
+ "version": "0.10.8",
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",
@@ -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
- // shape is the last resort, and only for a file whose titles said nothing whatsoever
301
- return named.some(Boolean) ? named : guessFromShape(chapters)
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
  }