@banou/media-player 0.8.21 → 0.9.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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { l as e, n as t, o as n, r, s as i, t as a, u as o } from "./engine-B8CKGIng.js";
1
+ import { l as e, n as t, o as n, r, s as i, t as a, u as o } from "./engine-DZrYu66u.js";
2
2
  import { Children as s, Fragment as c, isValidElement as l, useCallback as u, useEffect as d, useMemo as f, useRef as p, useState as m } from "react";
3
3
  import { css as h, keyframes as g } from "@emotion/react";
4
4
  import { createPlayer as _, useContainerAttach as v, useMediaAttach as y } from "@videojs/react";
@@ -179,7 +179,7 @@ var ee = {
179
179
  try {
180
180
  let n = await i({
181
181
  videoElement: e,
182
- canvasElement: t,
182
+ subtitleContainer: t,
183
183
  read: (e, t) => A.current(e, t),
184
184
  length: s,
185
185
  publicPath: c,
@@ -287,7 +287,7 @@ var ee = {
287
287
  if (!e || !n || !s) return;
288
288
  let t = a({
289
289
  video: e,
290
- canvas: n,
290
+ subtitles: n,
291
291
  mode: s,
292
292
  onBurnedInChange: o
293
293
  });
@@ -337,12 +337,18 @@ var ee = {
337
337
  regular: "\n font-weight: 400;\n font-size: calc(0.8 * var(--mp-unit));\n line-height: calc(1.1 * var(--mp-unit));\n @media (min-width: 960px) {\n font-size: calc(1 * var(--mp-unit));\n line-height: calc(1.4 * var(--mp-unit));\n }\n "
338
338
  }
339
339
  }, ge = h`
340
- top: unset !important;
341
- left: unset !important;
342
- width: 100%;
343
- height: 100%;
344
- margin: auto;
345
- pointer-events: none;
340
+ display: flex;
341
+ justify-content: center;
342
+ align-items: center;
343
+
344
+ canvas {
345
+ top: unset !important;
346
+ left: unset !important;
347
+ width: 100%;
348
+ height: 100%;
349
+ margin: auto;
350
+ pointer-events: none;
351
+ }
346
352
  `, _e = h`
347
353
  position: absolute;
348
354
  top: 0;
@@ -402,7 +408,7 @@ var ee = {
402
408
  ${H.bLarge.regular}
403
409
  text-shadow: 0 0 4px rgba(0, 0, 0, 1);
404
410
  pointer-events: none;
405
- `, be = (e) => e instanceof Error ? e.message : typeof e == "string" ? e : "Playback failed", xe = ({ onCanvasRef: e }) => {
411
+ `, be = (e) => e instanceof Error ? e.message : typeof e == "string" ? e : "Playback failed", xe = ({ onSubtitleRef: e }) => {
406
412
  let t = V((e) => e.title), n = V((e) => e.hideUI), r = V((e) => e.playbackError), i = V((e) => e.ready), a = V((e) => e.size), o = V((e) => e.waiting);
407
413
  return /* @__PURE__ */ w(S, { children: [
408
414
  t ? /* @__PURE__ */ C("div", {
@@ -419,7 +425,8 @@ var ee = {
419
425
  css: ye,
420
426
  children: be(r)
421
427
  }) : void 0,
422
- /* @__PURE__ */ C("canvas", {
428
+ /* @__PURE__ */ C("div", {
429
+ className: "subtitles",
423
430
  ref: e,
424
431
  css: ge
425
432
  })
@@ -2088,6 +2095,19 @@ ${X}
2088
2095
  z-index: 2;
2089
2096
  }
2090
2097
 
2098
+ /* The subtitle layer sits UNDER the title and the spinner, where the canvas itself used to sit.
2099
+ As a plain child div it is matched by the rule above and raised to the overlay items' level,
2100
+ which paints subtitles over the title's gradient, over the spinner and over the error text.
2101
+ Naming the type as well is what takes it back: :not() carries the specificity of its argument,
2102
+ so the rule above is (0,2,1) and a bare .subtitles at (0,2,0) loses to it whatever the source
2103
+ order, while div.subtitles ties at (0,2,1) and second place in the same block then wins.
2104
+ subtitle-layering.browser.test.tsx measures the computed value rather than trusting this. */
2105
+ & > div.subtitles {
2106
+ inset: 0;
2107
+ z-index: 1;
2108
+ pointer-events: none;
2109
+ }
2110
+
2091
2111
  canvas {
2092
2112
  height: 100%;
2093
2113
  width: 100%;
@@ -2112,7 +2132,7 @@ ${X}
2112
2132
  key: r,
2113
2133
  item: e
2114
2134
  }];
2115
- }), ut = ({ ref: e, onVideoRef: t, onCanvasRef: n, overlay: r, controls: i, children: a }) => {
2135
+ }), ut = ({ ref: e, onVideoRef: t, onSubtitleRef: n, overlay: r, controls: i, children: a }) => {
2116
2136
  let o = V(), s = V((e) => e.hideUI), c = V((e) => e.setHideUI), l = p(void 0), u = p("mouse"), f = p({
2117
2137
  x: -1,
2118
2138
  y: -1
@@ -2155,7 +2175,7 @@ ${X}
2155
2175
  },
2156
2176
  className: s ? "hide" : "",
2157
2177
  children: [
2158
- /* @__PURE__ */ C(xe, { onCanvasRef: n }),
2178
+ /* @__PURE__ */ C(xe, { onSubtitleRef: n }),
2159
2179
  lt(r).map(({ key: e, item: t }) => /* @__PURE__ */ C("div", {
2160
2180
  css: st,
2161
2181
  style: { ...s ? {
@@ -2265,7 +2285,7 @@ ${X}
2265
2285
  }, [I, z]), /* @__PURE__ */ C(ut, {
2266
2286
  ref: g,
2267
2287
  onVideoRef: n ? void 0 : b,
2268
- onCanvasRef: S,
2288
+ onSubtitleRef: S,
2269
2289
  overlay: e.overlay,
2270
2290
  controls: e.controls,
2271
2291
  children: t
@@ -3,12 +3,12 @@ export type ChromeProps = {
3
3
  ref?: Ref<HTMLDivElement> | ((element: HTMLDivElement | null) => void);
4
4
  /** Absent means render no video element: the media belongs to someone else and arrives as children. */
5
5
  onVideoRef?: (element: HTMLVideoElement | null) => void;
6
- onCanvasRef: (element: HTMLCanvasElement | null) => void;
6
+ onSubtitleRef: (element: HTMLDivElement | null) => void;
7
7
  /** The app's own content, over the video and outside the click-to-pause region, unlike `children`. */
8
8
  overlay?: ReactNode;
9
9
  /** False draws no control bar at all, leaving the picture, the title and the overlay. */
10
10
  controls?: boolean;
11
11
  children?: ReactNode;
12
12
  };
13
- export declare const Chrome: ({ ref, onVideoRef, onCanvasRef, overlay, controls, children }: ChromeProps) => import("@emotion/react/jsx-runtime").JSX.Element;
13
+ export declare const Chrome: ({ ref, onVideoRef, onSubtitleRef, overlay, controls, children }: ChromeProps) => import("@emotion/react/jsx-runtime").JSX.Element;
14
14
  export default Chrome;
@@ -1,4 +1,4 @@
1
- export declare const Overlay: ({ onCanvasRef }: {
2
- onCanvasRef: (element: HTMLCanvasElement | null) => void;
1
+ export declare const Overlay: ({ onSubtitleRef }: {
2
+ onSubtitleRef: (element: HTMLDivElement | null) => void;
3
3
  }) => import("@emotion/react/jsx-runtime").JSX.Element;
4
4
  export default Overlay;
@@ -13,5 +13,10 @@ export type PictureInPicture = {
13
13
  * hidden mirror. Where it does not, and the engine is Gecko, the same composite becomes the picture
14
14
  * in the page so that the BROWSER'S own picture in picture control carries the subtitles with it,
15
15
  * which it otherwise cannot: it takes a video element, and the subtitles live on a canvas above one.
16
+ *
17
+ * The second argument is the subtitle LAYER, not that canvas. From jassub 2 the canvas belongs to the
18
+ * renderer and is replaced on every pipeline rebuild, so only the layer lives long enough to hold.
16
19
  */
17
- export declare const usePictureInPicture: (video: HTMLVideoElement | null, canvas: HTMLCanvasElement | null) => PictureInPicture;
20
+ export declare const usePictureInPicture: (video: HTMLVideoElement | null,
21
+ /** The subtitle layer, not the canvas: see `PictureInPictureOptions.subtitles`. */
22
+ subtitles: HTMLElement | null) => PictureInPicture;
@@ -3,6 +3,6 @@ import type { MediaPlayerLocalOptions } from '../video-player';
3
3
  * Owns the engine for the life of a source: start, teardown, and every piece of state the pipeline
4
4
  * discovers. Nothing is mirrored in React state, so the store is the only place any of it lives.
5
5
  */
6
- export declare const usePlayback: (video: HTMLVideoElement | null, canvas: HTMLCanvasElement | null,
6
+ export declare const usePlayback: (video: HTMLVideoElement | null, subtitles: HTMLElement | null,
7
7
  /** null when the media is remote: there are no bytes, so there is no pipeline to run. */
8
8
  options: MediaPlayerLocalOptions | null) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@banou/media-player",
3
- "version": "0.8.21",
3
+ "version": "0.9.0",
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",
@@ -34,9 +34,8 @@
34
34
  "build-lib": "shx rm -rf dist/ && vp build -c vite.lib.config.ts && npm run types",
35
35
  "prepublishOnly": "npm run build-lib",
36
36
  "types": "tsc -p tsconfig.lib.json",
37
- "copy-assets": "shx mkdir -p public && npm run copy-libav && npm run copy-libass",
37
+ "copy-assets": "shx mkdir -p public && npm run copy-libav",
38
38
  "copy-libav": "shx cp node_modules/libav-wasm/build/worker.js public/libav-worker.js && shx cp node_modules/libav-wasm/build/libav.wasm public/libav.wasm && shx cp node_modules/libav-wasm/build/libav-jspi.wasm public/libav-jspi.wasm",
39
- "copy-libass": "shx cp node_modules/jassub/dist/jassub-worker.js public/jassub-worker.js && shx cp node_modules/jassub/dist/jassub-worker.wasm public/jassub-worker.wasm && shx cp node_modules/jassub/dist/jassub-worker-modern.wasm public/jassub-worker-modern.wasm && shx cp node_modules/jassub/dist/default.woff2 public/default.woff2",
40
39
  "test": "npm run fixtures && vp test run",
41
40
  "fixtures": "node scripts/fixture.mjs"
42
41
  },
@@ -45,7 +44,7 @@
45
44
  "@videojs/core": "10.0.0-beta.26",
46
45
  "@videojs/react": "10.0.0-beta.26",
47
46
  "ass-compiler": "^0.1.16",
48
- "jassub": "^1.8.6",
47
+ "jassub": "^2.5.16",
49
48
  "libav-wasm": "^0.8.4",
50
49
  "osra": "^0.6.12",
51
50
  "react-feather": "^2.0.10",
@@ -27,8 +27,15 @@ export type PictureInPictureMode = 'window' | 'burn-in'
27
27
 
28
28
  export type PictureInPictureOptions = {
29
29
  video: HTMLVideoElement
30
- /** The subtitle canvas. jassub sizes it to the video's content rect, so it maps 1:1. */
31
- canvas: HTMLCanvasElement
30
+ /**
31
+ * The subtitle LAYER, not the canvas inside it.
32
+ *
33
+ * From jassub 2 the canvas is created and removed by the subtitle renderer, one per pipeline
34
+ * build, so an element captured here would stop being the one being painted the moment the audio
35
+ * track changed. The layer is what React owns for the life of the player, so hiding and restoring
36
+ * it stays correct across a rebuild, and the canvas is looked up per frame.
37
+ */
38
+ subtitles: HTMLElement
32
39
  maxWidth?: number
33
40
  /** Where the mirror is mounted. It must be in the document. Defaults to the video's parent. */
34
41
  container?: HTMLElement
@@ -87,7 +94,7 @@ export const pictureInPictureMode = (): PictureInPictureMode | null => {
87
94
 
88
95
  /** Takes over the document's Media Session play/pause handlers while a session is open. */
89
96
  export const createPictureInPicture = (options: PictureInPictureOptions): PictureInPictureController => {
90
- const { video, canvas } = options
97
+ const { video, subtitles } = options
91
98
  const mode = options.mode ?? pictureInPictureMode() ?? 'window'
92
99
  const maxWidth = options.maxWidth ?? (mode === 'burn-in' ? BURN_IN_MAX_WIDTH : DEFAULT_MAX_WIDTH)
93
100
 
@@ -112,9 +119,12 @@ export const createPictureInPicture = (options: PictureInPictureOptions): Pictur
112
119
 
113
120
  const draw = (composite: HTMLCanvasElement, context: CanvasRenderingContext2D) => {
114
121
  context.drawImage(video, 0, 0, composite.width, composite.height)
115
- // drawImage throws on a zero-sized source, which the canvas is until jassub has painted
116
- if (canvas.width > 0 && canvas.height > 0) {
117
- context.drawImage(canvas, 0, 0, composite.width, composite.height)
122
+ // Resolved per frame: the renderer replaces this element on every pipeline rebuild. A canvas
123
+ // jassub has transferred reports the size of the last frame its worker committed, so the zero
124
+ // check still keeps drawImage off a source with no dimensions to read.
125
+ const surface = subtitles.querySelector('canvas')
126
+ if (surface && surface.width > 0 && surface.height > 0) {
127
+ context.drawImage(surface, 0, 0, composite.width, composite.height)
118
128
  }
119
129
  }
120
130
 
@@ -254,18 +264,23 @@ export const createPictureInPicture = (options: PictureInPictureOptions): Pictur
254
264
  * The real element is dimmed, never `display: none`: jassub sizes the subtitle canvas from its
255
265
  * `offsetWidth`/`offsetHeight`, and a collapsed box silently yields a composite with no subtitles
256
266
  * in it, which is exactly the thing being asked for.
267
+ *
268
+ * The LAYER is what gets hidden, rather than the canvas inside it. The canvas is replaced whenever
269
+ * the pipeline is rebuilt, so hiding that would leave the replacement visible over the composite
270
+ * (doubled subtitles) and would restore `display` onto an element that is no longer in the tree
271
+ * (subtitles gone for the rest of the session). The layer outlives both.
257
272
  */
258
273
  const present = (mirror: HTMLVideoElement) => {
259
274
  const videoOpacity = video.style.opacity
260
- const canvasDisplay = canvas.style.display
275
+ const subtitlesDisplay = subtitles.style.display
261
276
  mirror.style.cssText =
262
277
  'position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#000;z-index:1'
263
278
  video.style.opacity = '0'
264
- canvas.style.display = 'none'
279
+ subtitles.style.display = 'none'
265
280
  restore = () => {
266
281
  restore = undefined
267
282
  video.style.opacity = videoOpacity
268
- canvas.style.display = canvasDisplay
283
+ subtitles.style.display = subtitlesDisplay
269
284
  }
270
285
  }
271
286
 
@@ -13,7 +13,14 @@ export type MediaIndex = { pos: number, timestamp: number }
13
13
 
14
14
  export type PlaybackOptions = {
15
15
  videoElement: HTMLVideoElement
16
- canvasElement: HTMLCanvasElement
16
+ /**
17
+ * The subtitle layer. The renderer creates its own canvas inside it, one per jassub instance.
18
+ *
19
+ * Renamed from `canvasElement` in the jassub 2 migration, deliberately: passing a canvas here now
20
+ * means jassub transfers it to a worker and deletes it on teardown, so a caller that kept handing
21
+ * over an element it owns has to see a type error rather than a runtime one.
22
+ */
23
+ subtitleContainer: HTMLElement
17
24
  read: (offset: number, size: number) => Promise<ArrayBuffer>
18
25
  length: number
19
26
  /** serves BOTH `libav.wasm` and `libav-jspi.wasm`; libav-wasm picks one on `WebAssembly.Suspending` */
@@ -114,7 +121,7 @@ export const terminateRemuxer = (remuxer: { worker: Worker, destroy: () => Promi
114
121
 
115
122
  export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackController> => {
116
123
  const {
117
- videoElement, canvasElement, read, length, publicPath, libavWorkerUrl,
124
+ videoElement, subtitleContainer, read, length, publicPath, libavWorkerUrl,
118
125
  jassubWorkerUrl, jassubWasmUrl, jassubLegacyWasmUrl, defaultFontUrl, bufferSize = DEFAULT_BUFFER_SIZE,
119
126
  audioStreamIndex, onReady, onError, onRecovered, onSeek, onSubtitleStreams,
120
127
  onAudioStreams,
@@ -152,7 +159,7 @@ export const startPlayback = async (options: PlaybackOptions): Promise<PlaybackC
152
159
 
153
160
  const subtitles = createSubtitleRenderer({
154
161
  video: videoElement,
155
- canvas: canvasElement,
162
+ container: subtitleContainer,
156
163
  workerUrl: jassubWorkerUrl,
157
164
  wasmUrl: jassubWasmUrl,
158
165
  legacyWasmUrl: jassubLegacyWasmUrl,