@banou/media-player 0.9.0 → 0.10.1
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/README.md +40 -4
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.js +1 -1
- package/dist/engine/playback.d.ts +17 -0
- package/dist/engine/thumbnails.d.ts +11 -0
- package/dist/{engine-DZrYu66u.js → engine-b5_5HscR.js} +36 -19
- package/dist/index.d.ts +1 -1
- package/dist/index.js +641 -337
- package/dist/react/components/skip-chapter.d.ts +10 -0
- package/dist/react/hooks/use-thumbnails.d.ts +12 -1
- package/dist/react/player.d.ts +2 -0
- package/dist/react/source-feature.d.ts +39 -1
- package/dist/react/video-player.d.ts +13 -0
- package/dist/utils/chapters.d.ts +37 -0
- package/package.json +1 -1
- package/src/lib/engine/index.ts +1 -1
- package/src/lib/engine/playback.ts +15 -0
- package/src/lib/engine/thumbnails.ts +80 -21
- package/src/lib/index.tsx +1 -0
- package/src/lib/react/components/chrome.tsx +2 -0
- package/src/lib/react/components/progress-bar.tsx +144 -29
- package/src/lib/react/components/skip-chapter.tsx +166 -0
- package/src/lib/react/hooks/use-playback.ts +17 -2
- package/src/lib/react/hooks/use-thumbnails.ts +19 -3
- package/src/lib/react/source-feature.ts +22 -1
- package/src/lib/react/video-player.tsx +32 -3
- package/src/lib/utils/chapters.ts +302 -0
- package/src/lib/utils/source.ts +12 -1
package/README.md
CHANGED
|
@@ -58,6 +58,13 @@ player.play()
|
|
|
58
58
|
|
|
59
59
|
`useSeekThumbnails` and `usePictureInPicture` are exported for reuse outside the bundled chrome.
|
|
60
60
|
|
|
61
|
+
`useSeekThumbnails` returns `{ thumbnails, requestThumbnail }`. Previews are generated from the start
|
|
62
|
+
of the file to the end, which is right until somebody points at the seekbar: `requestThumbnail(time)`
|
|
63
|
+
moves the preview covering `time` to the front of that queue, and the walk carries on behind it from
|
|
64
|
+
wherever it had got to. Pass `undefined` when the pointer leaves. The bundled seekbar calls it for
|
|
65
|
+
you, so this is only for a chrome you draw yourself. It cannot interrupt a decode already in flight,
|
|
66
|
+
so the wait is the tail of that one rather than the whole backlog.
|
|
67
|
+
|
|
61
68
|
`downloadedRanges` paints byte spans you already hold onto the seekbar, mapped through the keyframe
|
|
62
69
|
index rather than by percentage, because a file's download progress is not its playback progress:
|
|
63
70
|
containers carry headers, fonts and attachments that occupy no time at all.
|
|
@@ -143,10 +150,39 @@ chrome at once.
|
|
|
143
150
|
|
|
144
151
|
## What it does
|
|
145
152
|
|
|
146
|
-
Play and pause, seek with a preview thumbnail and a keyframe-accurate scrub,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
every load.
|
|
153
|
+
Play and pause, seek with a preview thumbnail and a keyframe-accurate scrub, chapters on the seekbar,
|
|
154
|
+
an offer to skip an opening or ending, volume on a log curve, mute, playback speed, audio track
|
|
155
|
+
selection, subtitle track selection, picture in picture, fullscreen, and keyboard shortcuts. Nothing
|
|
156
|
+
is persisted: volume, speed and track choices start at their defaults every load.
|
|
157
|
+
|
|
158
|
+
### Chapters
|
|
159
|
+
|
|
160
|
+
Chapters come from the container, and the seekbar draws them as segments with a break at each
|
|
161
|
+
boundary. Hovering one lifts it above its neighbours and names it beside the time in the preview.
|
|
162
|
+
|
|
163
|
+
Pass `chapters` to supply them yourself, on either arm, and they win over whatever the file declared:
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
<MediaPlayer {...source} chapters={[{ start: 0, end: 88, title: 'Opening' }]} />
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
They are `{ start, end, title }` in seconds, ordered and non-overlapping, and they need not cover the
|
|
170
|
+
whole duration. A local file gets them from libav automatically, so this is for a source that knows
|
|
171
|
+
chapters the container does not.
|
|
172
|
+
|
|
173
|
+
### Skip Opening and Skip Ending
|
|
174
|
+
|
|
175
|
+
When a chapter looks like an opening or an ending, a button offers to jump past it, appearing a
|
|
176
|
+
second before the chapter and staying six seconds. It never skips on its own, so a wrong guess costs
|
|
177
|
+
a button nobody presses rather than a jump out of the episode.
|
|
178
|
+
|
|
179
|
+
The guess is made from the chapter title, against the names releases actually use (`OP`, `Opening`,
|
|
180
|
+
`ED`, `Ending`, `Credits`, and the same names carrying a song), and it defers across the whole file:
|
|
181
|
+
`Intro` is the opening where nothing else claims to be, and the cold open where an `OP` follows it.
|
|
182
|
+
A file whose chapters carry no usable name at all falls back to shape, where a chapter of about
|
|
183
|
+
ninety seconds in each half of the runtime is the opening and the ending. Nothing shorter than
|
|
184
|
+
fifteen seconds is ever offered, and a disc that is mostly themes offers nothing, because there the
|
|
185
|
+
themes are what is being watched.
|
|
150
186
|
|
|
151
187
|
### Picture in picture keeps the subtitles
|
|
152
188
|
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { startPlayback, terminateRemuxer, MediaElementError, isMediaElementError, DEFAULT_BUFFER_SIZE } from './playback';
|
|
2
|
-
export type { PlaybackOptions, PlaybackController, MediaIndex, AudioStream } from './playback';
|
|
2
|
+
export type { PlaybackOptions, PlaybackController, MediaIndex, MediaChapter, AudioStream } from './playback';
|
|
3
3
|
export { createSubtitleRenderer, SUBTITLES_OFF } from './subtitles';
|
|
4
4
|
export type { SubtitleRenderer, SubtitleRendererOptions, SubtitleStream } from './subtitles';
|
|
5
5
|
export { createThumbnailGenerator } from './thumbnails';
|
package/dist/engine/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, r as c, s as l, t as u, u as d } from "../engine-
|
|
1
|
+
import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, r as c, s as l, t as u, u as d } from "../engine-b5_5HscR.js";
|
|
2
2
|
export { i as DEFAULT_BUFFER_SIZE, e as MediaElementError, a as SUBTITLES_OFF, u as createPictureInPicture, d as createSubtitleRenderer, c as createThumbnailGenerator, n as getTimeRanges, s as isMediaElementError, o as pictureInPictureMode, l as startPlayback, t as terminateRemuxer, r as updateSourceBuffer };
|
|
@@ -6,6 +6,21 @@ export type MediaIndex = {
|
|
|
6
6
|
pos: number;
|
|
7
7
|
timestamp: number;
|
|
8
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* One named span of the timeline, in SECONDS.
|
|
11
|
+
*
|
|
12
|
+
* Chapters are not required to tile the duration: a container routinely declares a last chapter that
|
|
13
|
+
* ends fractionally before the file does, and nothing guarantees the first starts at zero. Anything
|
|
14
|
+
* drawing them has to treat the gaps as ordinary un-named time rather than assume full cover.
|
|
15
|
+
*
|
|
16
|
+
* libav also reports an `index`, dropped here the way `MediaIndex` drops it: array position already
|
|
17
|
+
* carries it, and a caller supplying their own chapters should not have to number them.
|
|
18
|
+
*/
|
|
19
|
+
export type MediaChapter = {
|
|
20
|
+
start: number;
|
|
21
|
+
end: number;
|
|
22
|
+
title: string;
|
|
23
|
+
};
|
|
9
24
|
export type PlaybackOptions = {
|
|
10
25
|
videoElement: HTMLVideoElement;
|
|
11
26
|
/**
|
|
@@ -49,6 +64,8 @@ export type PlaybackController = {
|
|
|
49
64
|
selectSubtitleStream: (streamIndex: number | undefined) => void;
|
|
50
65
|
/** Keyframe index of the input, which is what maps a downloaded byte range onto the timeline. */
|
|
51
66
|
indexes: MediaIndex[];
|
|
67
|
+
/** Named spans the container declared, empty when it declared none. */
|
|
68
|
+
chapters: MediaChapter[];
|
|
52
69
|
duration: number;
|
|
53
70
|
videoMimeType: string;
|
|
54
71
|
audioMimeType: string;
|
|
@@ -17,6 +17,17 @@ export type ThumbnailGeneratorOptions = {
|
|
|
17
17
|
export type ThumbnailGenerator = {
|
|
18
18
|
/** Report which byte ranges are readable. Called with no argument when the whole file is. */
|
|
19
19
|
update: (ranges?: [number, number][]) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Where the viewer is pointing, so that preview is decoded next. `undefined` when they stop.
|
|
22
|
+
*
|
|
23
|
+
* Only the slot covering `time` jumps the queue, and only for as long as it is still waiting, so
|
|
24
|
+
* this moves one preview forward rather than re-ordering the run. Everything behind it keeps the
|
|
25
|
+
* order it was claimed in and carries on the moment the jumped slot is done.
|
|
26
|
+
*
|
|
27
|
+
* It cannot interrupt a decode that has already started, so the wait is the tail of the one in
|
|
28
|
+
* flight and not the whole backlog.
|
|
29
|
+
*/
|
|
30
|
+
prioritize: (time: number | undefined) => void;
|
|
20
31
|
destroy: () => void;
|
|
21
32
|
};
|
|
22
33
|
export declare const createThumbnailGenerator: (options: ThumbnailGeneratorOptions) => Promise<ThumbnailGenerator>;
|
|
@@ -374,6 +374,7 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
|
|
|
374
374
|
videoElement: n,
|
|
375
375
|
selectSubtitleStream: (e) => s.selectStream(e),
|
|
376
376
|
indexes: e.indexes ?? [],
|
|
377
|
+
chapters: e.chapters ?? [],
|
|
377
378
|
duration: e.info.input.duration,
|
|
378
379
|
videoMimeType: e.info.output.videoMimeType,
|
|
379
380
|
audioMimeType: e.info.output.audioMimeType
|
|
@@ -404,7 +405,14 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
|
|
|
404
405
|
}
|
|
405
406
|
for (let [e, t] of a.entries()) t.endTime = a[e + 1]?.timestamp ?? n;
|
|
406
407
|
a.length > 1 && a.at(-1).timestamp > n - r * 2 && a.pop();
|
|
407
|
-
let l = [], u = !1, d =
|
|
408
|
+
let l = [], u = !1, d = [], f = !1, p, m = () => {
|
|
409
|
+
let e = p;
|
|
410
|
+
if (e !== void 0) {
|
|
411
|
+
let t = d.findIndex(({ timestamp: t, endTime: n }) => t <= e && e < n);
|
|
412
|
+
if (t >= 0) return t;
|
|
413
|
+
}
|
|
414
|
+
return 0;
|
|
415
|
+
}, h = () => {
|
|
408
416
|
let e = [];
|
|
409
417
|
for (let [t, n] of l.entries()) {
|
|
410
418
|
n.startTime - (e.at(-1)?.endTime ?? 0) > .01 && e.push({
|
|
@@ -425,30 +433,39 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
|
|
|
425
433
|
startTime: t,
|
|
426
434
|
endTime: n
|
|
427
435
|
}), o(e);
|
|
428
|
-
},
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
436
|
+
}, g = async (e) => {
|
|
437
|
+
if (u) return;
|
|
438
|
+
let t = await Promise.race([c.readKeyframe(e.timestamp), new Promise((e, t) => setTimeout(() => t(/* @__PURE__ */ Error("timed out")), M))]), n = await createImageBitmap(new Blob([t], { type: "image/png" })), r = new OffscreenCanvas(s, Math.max(1, Math.round(n.height * (s / n.width))));
|
|
439
|
+
r.getContext("2d").drawImage(n, 0, 0, r.width, r.height), n.close();
|
|
440
|
+
let i = await r.convertToBlob({
|
|
441
|
+
type: "image/webp",
|
|
442
|
+
quality: .7
|
|
443
|
+
});
|
|
444
|
+
u || (l = [...l, {
|
|
445
|
+
url: URL.createObjectURL(i),
|
|
446
|
+
startTime: e.timestamp,
|
|
447
|
+
endTime: e.endTime
|
|
448
|
+
}].sort((e, t) => e.startTime - t.startTime), h());
|
|
449
|
+
}, _ = () => {
|
|
450
|
+
if (f || u || !d.length) return;
|
|
451
|
+
let e = d.splice(m(), 1)[0];
|
|
452
|
+
f = !0, g(e).catch(() => {
|
|
443
453
|
e.attempts += 1, e.done = e.attempts >= ae;
|
|
454
|
+
}).finally(() => {
|
|
455
|
+
f = !1, _();
|
|
444
456
|
});
|
|
457
|
+
}, v = (e) => {
|
|
458
|
+
e.done = !0, d.push(e), _();
|
|
445
459
|
};
|
|
446
|
-
return
|
|
460
|
+
return h(), {
|
|
447
461
|
update: (e) => {
|
|
448
|
-
if (!u) for (let t of a) t.done || (!e || e.some(([e, n]) => e <= t.startByte && t.endByte <= n)) &&
|
|
462
|
+
if (!u) for (let t of a) t.done || (!e || e.some(([e, n]) => e <= t.startByte && t.endByte <= n)) && v(t);
|
|
463
|
+
},
|
|
464
|
+
prioritize: (e) => {
|
|
465
|
+
u || (p = e);
|
|
449
466
|
},
|
|
450
467
|
destroy: () => {
|
|
451
|
-
u = !0;
|
|
468
|
+
u = !0, d.length = 0;
|
|
452
469
|
for (let e of l) URL.revokeObjectURL(e.url);
|
|
453
470
|
l = [], A(c);
|
|
454
471
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,5 +10,5 @@ export { useSeekThumbnails } from './react/hooks/use-thumbnails';
|
|
|
10
10
|
export { usePictureInPicture } from './react/hooks/use-picture-in-picture';
|
|
11
11
|
export { inputToRemuxerInput } from './utils/source';
|
|
12
12
|
export type { RemuxerInput } from './utils/source';
|
|
13
|
-
export type { AudioStream, MediaIndex, PictureInPictureController, PlaybackController, PlaybackOptions, SubtitleStream, ThumbnailImage, } from './engine';
|
|
13
|
+
export type { AudioStream, MediaChapter, MediaIndex, PictureInPictureController, PlaybackController, PlaybackOptions, SubtitleStream, ThumbnailImage, } from './engine';
|
|
14
14
|
export { startPlayback, createThumbnailGenerator, createSubtitleRenderer, createPictureInPicture, SUBTITLES_OFF, } from './engine';
|