@banou/media-player 0.10.17 → 0.10.19
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/engine/index.js +1 -1
- package/dist/engine/source-buffer.d.ts +1 -1
- package/dist/{engine-C9vSEJYW.js → engine-C4Gj_4zs.js} +136 -136
- package/dist/index.js +157 -133
- package/dist/react/components/track-menu.d.ts +1 -1
- package/dist/react/player.d.ts +1 -0
- package/dist/react/source-feature.d.ts +5 -0
- package/dist/react/video-player.d.ts +6 -0
- package/package.json +1 -1
- package/src/lib/engine/playback.ts +4 -2
- package/src/lib/engine/source-buffer.ts +6 -3
- package/src/lib/react/components/settings.tsx +37 -1
- package/src/lib/react/components/track-menu.tsx +1 -1
- package/src/lib/react/source-feature.ts +5 -0
- package/src/lib/react/video-player.tsx +11 -0
|
@@ -17,7 +17,7 @@ export declare const popoverStyle: import("@emotion/utils").SerializedStyles;
|
|
|
17
17
|
*/
|
|
18
18
|
export declare const TrackMenu: ({ title, tracks, selected, onSelect, onBack, offLabel, pending, failed }: {
|
|
19
19
|
title: string;
|
|
20
|
-
tracks: TrackChoice[];
|
|
20
|
+
tracks: readonly TrackChoice[];
|
|
21
21
|
selected: string | number | undefined;
|
|
22
22
|
onSelect: (id: string | number | undefined) => void;
|
|
23
23
|
/** Absent means the menu was opened straight from the bar, so the header is a label and no more. */
|
package/dist/react/player.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare const Player: import("@videojs/react").CreatePlayerResult<import(
|
|
|
15
15
|
audioTracks: import("./source-feature").TrackChoice[];
|
|
16
16
|
selectedAudioTrack: string | number | undefined;
|
|
17
17
|
selectAudioTrack: (id: string | number) => void | Promise<void>;
|
|
18
|
+
quality: import("./media").DelegatedSelection | null;
|
|
18
19
|
hideUI: boolean;
|
|
19
20
|
setHideUI: (hide: boolean) => void;
|
|
20
21
|
hasMedia: boolean;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MediaChapter, MediaIndex, PictureInPictureMode, ThumbnailImage } from '../engine';
|
|
2
|
+
import type { DelegatedSelection } from './media';
|
|
2
3
|
/**
|
|
3
4
|
* One row of a track menu, already named.
|
|
4
5
|
*
|
|
@@ -162,6 +163,8 @@ export type SourceState = {
|
|
|
162
163
|
audioTracks: TrackChoice[];
|
|
163
164
|
selectedAudioTrack: string | number | undefined;
|
|
164
165
|
selectAudioTrack: (id: string | number) => void | Promise<void>;
|
|
166
|
+
/** The settings menu's quality choices, from a media the player does not own. null when none are offered. */
|
|
167
|
+
quality: DelegatedSelection | null;
|
|
165
168
|
/** Chrome auto-hide. True means the controls, title and cursor are hidden. */
|
|
166
169
|
hideUI: boolean;
|
|
167
170
|
setHideUI: (hide: boolean) => void;
|
|
@@ -307,6 +310,8 @@ export declare const sourceFeature: import("@videojs/react").PlayerFeature<{
|
|
|
307
310
|
audioTracks: TrackChoice[];
|
|
308
311
|
selectedAudioTrack: string | number | undefined;
|
|
309
312
|
selectAudioTrack: (id: string | number) => void | Promise<void>;
|
|
313
|
+
/** The settings menu's quality choices, from a media the player does not own. null when none are offered. */
|
|
314
|
+
quality: DelegatedSelection | null;
|
|
310
315
|
/** Chrome auto-hide. True means the controls, title and cursor are hidden. */
|
|
311
316
|
hideUI: boolean;
|
|
312
317
|
setHideUI: (hide: boolean) => void;
|
|
@@ -193,6 +193,12 @@ export type MediaPlayerRemoteOptions = CommonOptions & {
|
|
|
193
193
|
/** The source renders these; the player draws the menu and reports the pick. */
|
|
194
194
|
subtitles?: DelegatedTracks;
|
|
195
195
|
audioTracks?: DelegatedTracks;
|
|
196
|
+
/**
|
|
197
|
+
* The source's own quality choices, as a row of the settings menu that shows the selected one.
|
|
198
|
+
* Offered only with two or more rows to choose between; `offLabel` names a row with no quality
|
|
199
|
+
* picked, such as an automatic one, whose id is null.
|
|
200
|
+
*/
|
|
201
|
+
quality?: DelegatedTracks;
|
|
196
202
|
/**
|
|
197
203
|
* Offer picture in picture, entered by the host from inside the media's own document. Off by
|
|
198
204
|
* default, and then there is no control at all. See `PassThroughPictureInPicture` for what the
|
package/package.json
CHANGED
|
@@ -709,8 +709,10 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
|
|
|
709
709
|
if (elementFailed || videoElement.error) return
|
|
710
710
|
evict().catch(() => {})
|
|
711
711
|
pump().catch((error) => reportError(error, false))
|
|
712
|
-
//
|
|
713
|
-
|
|
712
|
+
// Every remove() puts the MediaSource back to 'open', so the end has to be re-armed, though not while a remuxer
|
|
713
|
+
// seek runs: queued behind a remove() and run after a move had started one, it came before that seek's append over
|
|
714
|
+
// the end, and Firefox 151 then sometimes waited at the end for good (4 of 1060 end moves, measured 2026-10-12)
|
|
715
|
+
if (endInBuffer() && !pending && atEnd() && mediaSource.readyState === 'open') endOfStream(() => !seeking).catch(() => {})
|
|
714
716
|
}, 100)
|
|
715
717
|
teardown.push(() => clearInterval(interval))
|
|
716
718
|
|
|
@@ -50,10 +50,13 @@ export const updateSourceBuffer = (sourceBuffer: SourceBuffer, mediaSource: Medi
|
|
|
50
50
|
const updateTimestampOffset = (timestampOffset: number) =>
|
|
51
51
|
enqueue(async () => { sourceBuffer.timestampOffset = timestampOffset })
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
/**
|
|
54
|
+
* endOfStream throws mid-update and is undone by the next remove(), so the caller re-arms it.
|
|
55
|
+
* `allowed` is asked when the call reaches the front of the queue, not when it is queued.
|
|
56
|
+
*/
|
|
57
|
+
const endOfStream = (allowed = () => true) =>
|
|
55
58
|
enqueue(async () => {
|
|
56
|
-
if (mediaSource.readyState !== 'open' || sourceBuffer.updating) return
|
|
59
|
+
if (mediaSource.readyState !== 'open' || sourceBuffer.updating || !allowed()) return
|
|
57
60
|
try { mediaSource.endOfStream() } catch {}
|
|
58
61
|
})
|
|
59
62
|
|
|
@@ -107,7 +107,8 @@ ${popoverStyle}
|
|
|
107
107
|
enum PopoverContent {
|
|
108
108
|
Default,
|
|
109
109
|
PlaybackRate,
|
|
110
|
-
Audio
|
|
110
|
+
Audio,
|
|
111
|
+
Quality
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
export const SettingsAction = () => {
|
|
@@ -116,6 +117,7 @@ export const SettingsAction = () => {
|
|
|
116
117
|
const audioTracks = usePlayer((state) => state.audioTracks)
|
|
117
118
|
const selectedAudioTrack = usePlayer((state) => state.selectedAudioTrack)
|
|
118
119
|
const selectAudioTrack = usePlayer((state) => state.selectAudioTrack)
|
|
120
|
+
const quality = usePlayer((state) => state.quality)
|
|
119
121
|
const volumeNormalizer = usePlayer((state) => state.volumeNormalizer)
|
|
120
122
|
|
|
121
123
|
const [popoverContent, setPopoverContent] = useState(PopoverContent.Default)
|
|
@@ -134,6 +136,15 @@ export const SettingsAction = () => {
|
|
|
134
136
|
runSelect(id, () => selectAudioTrack(id))
|
|
135
137
|
}
|
|
136
138
|
|
|
139
|
+
const chooseQuality = (id: string | number | undefined) => {
|
|
140
|
+
if (!quality) return
|
|
141
|
+
runSelect(id ?? null, () => quality.select(id === undefined ? null : String(id)))
|
|
142
|
+
}
|
|
143
|
+
const qualityRows = quality ? quality.options.length + (quality.offLabel ? 1 : 0) : 0
|
|
144
|
+
const qualityLabel = quality?.selectedId == null
|
|
145
|
+
? quality?.offLabel
|
|
146
|
+
: quality.options.find(({ id }) => id === quality.selectedId)?.label
|
|
147
|
+
|
|
137
148
|
const changePopoverContent = (newPopoverContent: PopoverContent) => () => {
|
|
138
149
|
setPopoverContent(newPopoverContent)
|
|
139
150
|
}
|
|
@@ -179,6 +190,17 @@ export const SettingsAction = () => {
|
|
|
179
190
|
</div>
|
|
180
191
|
)
|
|
181
192
|
: null}
|
|
193
|
+
{qualityRows > 1
|
|
194
|
+
? (
|
|
195
|
+
<div onClick={changePopoverContent(PopoverContent.Quality)}>
|
|
196
|
+
<div>Quality</div>
|
|
197
|
+
<div>
|
|
198
|
+
{qualityLabel ? <span className='secondary'>{qualityLabel}</span> : null}
|
|
199
|
+
<ChevronRight />
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
)
|
|
203
|
+
: null}
|
|
182
204
|
<div onClick={changePopoverContent(PopoverContent.PlaybackRate)}>
|
|
183
205
|
<div>Playback speed</div>
|
|
184
206
|
<div>
|
|
@@ -255,6 +277,20 @@ export const SettingsAction = () => {
|
|
|
255
277
|
/>
|
|
256
278
|
)
|
|
257
279
|
}
|
|
280
|
+
{
|
|
281
|
+
open && popoverContent === PopoverContent.Quality && quality && (
|
|
282
|
+
<TrackMenu
|
|
283
|
+
title='Quality'
|
|
284
|
+
tracks={quality.options}
|
|
285
|
+
selected={quality.selectedId ?? undefined}
|
|
286
|
+
onSelect={chooseQuality}
|
|
287
|
+
onBack={changePopoverContent(PopoverContent.Default)}
|
|
288
|
+
offLabel={quality.offLabel}
|
|
289
|
+
pending={pending}
|
|
290
|
+
failed={failed}
|
|
291
|
+
/>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
258
294
|
</div>
|
|
259
295
|
)
|
|
260
296
|
}
|
|
@@ -154,7 +154,7 @@ export const popoverStyle = css`
|
|
|
154
154
|
export const TrackMenu = (
|
|
155
155
|
{ title, tracks, selected, onSelect, onBack, offLabel, pending, failed }: {
|
|
156
156
|
title: string
|
|
157
|
-
tracks: TrackChoice[]
|
|
157
|
+
tracks: readonly TrackChoice[]
|
|
158
158
|
selected: string | number | undefined
|
|
159
159
|
onSelect: (id: string | number | undefined) => void
|
|
160
160
|
/** Absent means the menu was opened straight from the bar, so the header is a label and no more. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MediaChapter, MediaIndex, PictureInPictureMode, ThumbnailImage } from '../engine'
|
|
2
|
+
import type { DelegatedSelection } from './media'
|
|
2
3
|
|
|
3
4
|
import { definePlayerFeature } from '@videojs/core/dom'
|
|
4
5
|
|
|
@@ -175,6 +176,9 @@ export type SourceState = {
|
|
|
175
176
|
selectedAudioTrack: string | number | undefined
|
|
176
177
|
selectAudioTrack: (id: string | number) => void | Promise<void>
|
|
177
178
|
|
|
179
|
+
/** The settings menu's quality choices, from a media the player does not own. null when none are offered. */
|
|
180
|
+
quality: DelegatedSelection | null
|
|
181
|
+
|
|
178
182
|
/** Chrome auto-hide. True means the controls, title and cursor are hidden. */
|
|
179
183
|
hideUI: boolean
|
|
180
184
|
setHideUI: (hide: boolean) => void
|
|
@@ -283,6 +287,7 @@ const initialState: SourceState = {
|
|
|
283
287
|
audioTracks: [],
|
|
284
288
|
selectedAudioTrack: undefined,
|
|
285
289
|
selectAudioTrack: () => {},
|
|
290
|
+
quality: null,
|
|
286
291
|
hideUI: false,
|
|
287
292
|
setHideUI: () => {},
|
|
288
293
|
hasMedia: false,
|
|
@@ -220,6 +220,12 @@ export type MediaPlayerRemoteOptions =
|
|
|
220
220
|
/** The source renders these; the player draws the menu and reports the pick. */
|
|
221
221
|
subtitles?: DelegatedTracks
|
|
222
222
|
audioTracks?: DelegatedTracks
|
|
223
|
+
/**
|
|
224
|
+
* The source's own quality choices, as a row of the settings menu that shows the selected one.
|
|
225
|
+
* Offered only with two or more rows to choose between; `offLabel` names a row with no quality
|
|
226
|
+
* picked, such as an automatic one, whose id is null.
|
|
227
|
+
*/
|
|
228
|
+
quality?: DelegatedTracks
|
|
223
229
|
/**
|
|
224
230
|
* Offer picture in picture, entered by the host from inside the media's own document. Off by
|
|
225
231
|
* default, and then there is no control at all. See `PassThroughPictureInPicture` for what the
|
|
@@ -397,6 +403,11 @@ const PlayerRoot = ({ options, children }: { options: MediaPlayerOptions, childr
|
|
|
397
403
|
})
|
|
398
404
|
}, [setSourceState, audio])
|
|
399
405
|
|
|
406
|
+
const quality = remote?.quality?.selection ?? null
|
|
407
|
+
useEffect(() => {
|
|
408
|
+
setSourceState({ quality })
|
|
409
|
+
}, [setSourceState, quality])
|
|
410
|
+
|
|
400
411
|
/*
|
|
401
412
|
* Guarded, and its own effect, for the reason the ones above are.
|
|
402
413
|
*
|