@banou/media-player 0.8.21 → 0.10.0

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 CHANGED
@@ -22,9 +22,9 @@ import MediaPlayer from '@banou/media-player'
22
22
  publicPath="/"
23
23
  libavWorkerUrl="/libav-worker.js"
24
24
  jassubWorkerUrl={jassubWorkerUrl}
25
- jassubWasmUrl="/jassub-worker-modern.wasm"
26
- jassubLegacyWasmUrl="/jassub-worker.wasm"
27
- defaultFontUrl="/default.woff2"
25
+ jassubWasmUrl={jassubWasmUrl}
26
+ jassubLegacyWasmUrl={jassubLegacyWasmUrl}
27
+ defaultFontUrl={defaultFontUrl}
28
28
  title="episode.mkv"
29
29
  autoplay
30
30
  />
@@ -58,15 +58,23 @@ 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.
64
71
 
65
72
  ### The assets your app has to serve
66
73
 
67
- Nothing is bundled: the workers and the wasm are fetched at runtime from urls you provide, so they have
68
- to be copied out of `node_modules` and hosted. `src/asset-urls.ts` is a worked example, and the
69
- `copy-assets` script is what puts them in `public/`.
74
+ The workers and the wasm are fetched at runtime from urls you provide, never from anything this
75
+ package resolves for itself. libav's are plain files to copy out of `node_modules`, which is what the
76
+ `copy-assets` script does. jassub's cannot be copied and have to be built by your bundler; see below.
77
+ `src/asset-urls.ts` is the worked example for both.
70
78
 
71
79
  `publicPath` is the directory **libav's two wasm files** are served from, and both have to be there:
72
80
 
@@ -79,25 +87,62 @@ libav-wasm picks between them at runtime on `typeof WebAssembly.Suspending === '
79
87
  only one does not fail everywhere: it fails on exactly the browsers that pick the missing file, which
80
88
  reads as a browser bug rather than a missing asset. Serve both.
81
89
 
82
- The rest are named individually: `libavWorkerUrl` (`libav-wasm/build/worker.js`) and the optional
83
- `defaultFontUrl`. jassub also has two builds, and the same warning applies:
90
+ `libavWorkerUrl` (`libav-wasm/build/worker.js`) is named individually and still copies fine.
91
+
92
+ ### jassub's assets are imported, not copied
93
+
94
+ jassub 2's worker is an ES module that imports `abslink` and `lfa-ponyfill` by bare specifier plus
95
+ five relative siblings, and it spawns a nested worker of its own. Copying one file out of
96
+ `node_modules` produces something no browser can load, so the bundler has to build that graph. With
97
+ vite that is one query each, in your own source:
98
+
99
+ ```ts
100
+ import jassubWorkerUrl from 'jassub/dist/worker/worker.js?worker&url'
101
+ import jassubWasmUrl from 'jassub/dist/wasm/jassub-worker-modern.wasm?no-inline&url'
102
+ import jassubLegacyWasmUrl from 'jassub/dist/wasm/jassub-worker.wasm?no-inline&url'
103
+ import defaultFontUrl from 'jassub/dist/default.woff2?no-inline&url'
104
+ ```
105
+
106
+ `?no-inline` is load bearing for anything built with `build.lib` set: vite inlines every asset as a
107
+ base64 data url in library mode and ignores `assetsInlineLimit` while doing it, so without it the two
108
+ wasm builds and the font land in your entry chunk as about 5.9 MB of base64. `src/asset-urls.ts` in
109
+ this repo is the same four lines and is meant to be copied.
110
+
111
+ **If you build with `build.lib` set, build these four in a separate pass with it UNSET.** `?no-inline`
112
+ covers the assets you name, and it cannot cover the ones jassub names for itself: its emscripten glue
113
+ resolves the wasm with `new URL('./wasm/...', import.meta.url)` inside the worker, and lib mode inlines
114
+ those too. Measured on this version, one entry importing exactly the four lines above:
115
+
116
+ | build | worker chunk | base64 wasm blobs |
117
+ | --- | --- | --- |
118
+ | `build.lib` set | 2,922,207 bytes | 2 |
119
+ | no `build.lib` | 110,525 bytes | 0 |
120
+
121
+ `assetsInlineLimit: 0` and the function form both make no difference, because lib mode decides to
122
+ inline before it consults either. A second vite config with a single JS entry, no `lib`, and the same
123
+ `outDir` produces the 110 KB worker, the 30 KB pthread glue and both wasm files as real files.
124
+
125
+ `?no-inline&url` needs a type declaration, because vite's own ambient `*?url` only matches a specifier
126
+ ENDING in `?url`. See `src/vite-env.d.ts`.
127
+
128
+ The two wasm builds are picked at runtime:
84
129
 
85
130
  | option | file | when it is used |
86
131
  | --- | --- | --- |
87
- | `jassubWasmUrl` | `jassub/dist/jassub-worker-modern.wasm` | wherever WebAssembly SIMD exists |
88
- | `jassubLegacyWasmUrl` | `jassub/dist/jassub-worker.wasm` | Safari before 16.4, and anything else without SIMD |
132
+ | `jassubWasmUrl` | `jassub/dist/wasm/jassub-worker-modern.wasm` | wherever RELAXED SIMD exists |
133
+ | `jassubLegacyWasmUrl` | `jassub/dist/wasm/jassub-worker.wasm` | everywhere else |
89
134
 
90
- `jassubLegacyWasmUrl` is optional in the type and not in practice: jassub falls back to a bare
91
- `'jassub-worker.wasm'`, which it resolves against the `blob:` url its worker is built from, and that
92
- throws. Leaving it unset does not fall back to the slower build, it loses subtitles entirely.
135
+ The test is RELAXED simd, not baseline simd: jassub validates a module using `i8x16.relaxed_swizzle`.
136
+ That is a much later feature than plain SIMD, so the second file is not a museum piece for ancient
137
+ Safari the way it was under jassub 1. Serve both.
93
138
 
94
- jassub ships a classic worker script, so wrap it:
139
+ Serve the wasm as `application/wasm`: jassub instantiates it with `instantiateStreaming`, which
140
+ refuses any other content type.
95
141
 
96
- ```ts
97
- const jassubWorkerUrl = URL.createObjectURL(
98
- new Blob([`importScripts("/jassub-worker.js")`], { type: 'application/javascript' }),
99
- )
100
- ```
142
+ No cross-origin isolation is needed. jassub allocates a shared `WebAssembly.Memory` unconditionally,
143
+ which constructs on a plain origin in both engines (measured on Chrome 152 and Firefox 154), and its
144
+ own thread count is gated on `crossOriginIsolated`, so without COOP and COEP it simply runs single
145
+ threaded.
101
146
 
102
147
  The chrome brings its own scale and needs nothing from the host page's root font. Everything is sized
103
148
  against `--mp-unit`, which defaults to `10px` on the player element; set it there to rescale the whole
@@ -116,6 +161,11 @@ Subtitles are painted by jassub onto a canvas over the video, and picture in pic
116
161
  element and nothing else, so the browser has no way to composite the two: a plain
117
162
  `requestPictureInPicture()` puts the bare video in the window and leaves the subtitles on the page.
118
163
 
164
+ That canvas belongs to jassub from version 2 on: the constructor transfers it to a worker, which an
165
+ element accepts exactly once, and `destroy()` removes it from the document. So the player renders a
166
+ subtitle LAYER and the engine mounts one canvas inside it per pipeline build. Anything reaching for
167
+ the surface has to look it up through the layer each time rather than hold it.
168
+
119
169
  So the player composites them itself. Every presented frame is drawn to an offscreen canvas with the
120
170
  subtitle canvas on top, and `captureStream()` turns that into a MediaStream backing a hidden video
121
171
  element, which is the one that enters the window. The original element keeps playing and stays the
@@ -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-B8CKGIng.js";
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-YeMWShCv.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 };
@@ -14,8 +14,15 @@
14
14
  export type PictureInPictureMode = 'window' | 'burn-in';
15
15
  export type PictureInPictureOptions = {
16
16
  video: HTMLVideoElement;
17
- /** The subtitle canvas. jassub sizes it to the video's content rect, so it maps 1:1. */
18
- canvas: HTMLCanvasElement;
17
+ /**
18
+ * The subtitle LAYER, not the canvas inside it.
19
+ *
20
+ * From jassub 2 the canvas is created and removed by the subtitle renderer, one per pipeline
21
+ * build, so an element captured here would stop being the one being painted the moment the audio
22
+ * track changed. The layer is what React owns for the life of the player, so hiding and restoring
23
+ * it stays correct across a rebuild, and the canvas is looked up per frame.
24
+ */
25
+ subtitles: HTMLElement;
19
26
  maxWidth?: number;
20
27
  /** Where the mirror is mounted. It must be in the document. Defaults to the video's parent. */
21
28
  container?: HTMLElement;
@@ -8,7 +8,14 @@ export type MediaIndex = {
8
8
  };
9
9
  export type PlaybackOptions = {
10
10
  videoElement: HTMLVideoElement;
11
- canvasElement: HTMLCanvasElement;
11
+ /**
12
+ * The subtitle layer. The renderer creates its own canvas inside it, one per jassub instance.
13
+ *
14
+ * Renamed from `canvasElement` in the jassub 2 migration, deliberately: passing a canvas here now
15
+ * means jassub transfers it to a worker and deletes it on teardown, so a caller that kept handing
16
+ * over an element it owns has to see a type error rather than a runtime one.
17
+ */
18
+ subtitleContainer: HTMLElement;
12
19
  read: (offset: number, size: number) => Promise<ArrayBuffer>;
13
20
  length: number;
14
21
  /** serves BOTH `libav.wasm` and `libav-jspi.wasm`; libav-wasm picks one on `WebAssembly.Suspending` */
@@ -8,16 +8,27 @@ export type SubtitleStream = {
8
8
  export declare const SUBTITLES_OFF = -1;
9
9
  export type SubtitleRendererOptions = {
10
10
  video: HTMLVideoElement;
11
- canvas: HTMLCanvasElement;
11
+ /**
12
+ * Where the subtitle surface is mounted.
13
+ *
14
+ * A container, not the canvas itself. From jassub 2 the canvas belongs to the renderer: the
15
+ * constructor transfers it to a worker, which an element accepts exactly once for its whole life,
16
+ * and `destroy()` removes it from the document. Neither is survivable for an element someone else
17
+ * owns, and this pipeline is rebuilt in place on an audio track change and on an element recovery.
18
+ * So one canvas is created here per jassub instance, inside this container.
19
+ *
20
+ * The live canvas is `container.querySelector('canvas')`, and it is replaced on every rebuild.
21
+ */
22
+ container: HTMLElement;
12
23
  workerUrl: string;
13
24
  /** The SIMD build, `jassub-worker-modern.wasm`. Used wherever WebAssembly SIMD is available. */
14
25
  wasmUrl: string;
15
26
  /**
16
- * The non-SIMD build, `jassub-worker.wasm`, for Safari before 16.4 and anything else without SIMD.
27
+ * The non-SIMD build, `jassub-worker.wasm`, for anything without WebAssembly SIMD.
17
28
  *
18
- * Not optional in practice, only in the type. jassub picks `wasmUrl ?? 'jassub-worker.wasm'` when SIMD
19
- * is missing, and that bare relative name resolves against the blob: url the worker is built from,
20
- * which throws. So leaving this unset does not degrade to the slower build, it fails outright.
29
+ * Optional in practice as well as in the type, unlike under jassub 1: v2 defaults both wasm urls to
30
+ * its own files resolved against `import.meta.url`, so leaving this unset degrades to whatever the
31
+ * bundler emitted rather than failing outright.
21
32
  */
22
33
  legacyWasmUrl?: string;
23
34
  /**
@@ -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>;