@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.
@@ -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
- * Feather's slash for its own off state.
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;
@@ -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.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-feather'
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 { AlertTriangle, Check, Copy } from 'react-feather'
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
- <AlertTriangle className='alert-triangle' />
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 captions glyph, drawn here because react-feather does not have one.
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
- * 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`.
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
- * Feather's slash for its own off state.
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-feather'
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-feather'
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-feather'
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
- // 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
  }