@banou/media-player 0.10.16 → 0.10.18

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
@@ -158,6 +158,10 @@ an offer to skip an opening or ending, volume on a log curve, mute, playback spe
158
158
  selection, subtitle track selection, picture in picture, fullscreen, and keyboard shortcuts. Nothing
159
159
  is persisted: volume, speed and track choices start at their defaults every load.
160
160
 
161
+ The shortcuts (Space or `k`, `f`, `m`, the arrow keys) listen on the whole page. Pass `shortcuts="player"` for a
162
+ player among other content: a key then acts only from inside the player (a click on the picture puts the focus
163
+ there) or while it is fullscreen, and Space and the arrows keep scrolling the page everywhere else.
164
+
161
165
  ### Chapters
162
166
 
163
167
  Chapters come from the container, and the seekbar draws them as segments with a break at each
@@ -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-C9vSEJYW.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-C4Gj_4zs.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 };
@@ -8,5 +8,5 @@ export declare const updateSourceBuffer: (sourceBuffer: SourceBuffer, mediaSourc
8
8
  appendBuffer: (buffer: ArrayBuffer | Uint8Array) => Promise<void>;
9
9
  unbufferRange: (start: number, end: number) => Promise<void>;
10
10
  updateTimestampOffset: (timestampOffset: number) => Promise<void>;
11
- endOfStream: () => Promise<void>;
11
+ endOfStream: (allowed?: () => boolean) => Promise<void>;
12
12
  };
@@ -26,8 +26,8 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
26
26
  updateTimestampOffset: (t) => r(async () => {
27
27
  e.timestampOffset = t;
28
28
  }),
29
- endOfStream: () => r(async () => {
30
- if (!(t.readyState !== "open" || e.updating)) try {
29
+ endOfStream: (n = () => !0) => r(async () => {
30
+ if (t.readyState === "open" && !e.updating && n()) try {
31
31
  t.endOfStream();
32
32
  } catch {}
33
33
  })
@@ -174,13 +174,13 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
174
174
  constructor(e, t) {
175
175
  super(e, t), this.name = "MediaElementError";
176
176
  }
177
- }, _ = (e) => !!e && typeof e == "object" && e.mediaElement === !0, v = -20, y = 60, b = 30, x = -5, S = 20, C = 5, w = 15e3, T = 1, E = 3, ee = 12, D = 200, O = 250, k = 25e5, A = (e) => {
177
+ }, _ = (e) => !!e && typeof e == "object" && e.mediaElement === !0, v = -20, y = 60, b = 30, x = -5, S = 20, C = 5, w = 15e3, T = 1, E = 3, D = 12, O = 200, k = 250, A = 25e5, j = (e) => {
178
178
  let { worker: t } = e, n = setTimeout(() => t.terminate(), 2e3);
179
179
  e.destroy().catch(() => {}).finally(() => {
180
180
  clearTimeout(n), t.terminate();
181
181
  });
182
- }, te = async (t) => {
183
- let { videoElement: n, subtitleContainer: r, read: a, length: s, publicPath: c, libavWorkerUrl: l, jassubWorkerUrl: u, jassubWasmUrl: d, jassubLegacyWasmUrl: f, defaultFontUrl: p, bufferSize: m = k, audioStreamIndex: _, onReady: te, onError: ne, onRecovered: re, onSeek: ie, onSubtitleStreams: ae, onAudioStreams: oe } = t, j = await e({
182
+ }, ee = async (t) => {
183
+ let { videoElement: n, subtitleContainer: r, read: a, length: s, publicPath: c, libavWorkerUrl: l, jassubWorkerUrl: u, jassubWasmUrl: d, jassubLegacyWasmUrl: f, defaultFontUrl: p, bufferSize: m = A, audioStreamIndex: _, onReady: ee, onError: te, onRecovered: ne, onSeek: re, onSubtitleStreams: ie, onAudioStreams: ae } = t, M = await e({
184
184
  publicPath: c,
185
185
  workerUrl: l,
186
186
  workerOptions: { type: "module" },
@@ -188,18 +188,18 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
188
188
  length: s,
189
189
  audioStreamIndex: _,
190
190
  read: a
191
- }), M = [], N = !1, se = () => {
192
- if (!N) {
193
- N = !0;
194
- for (let e of [...M].reverse()) try {
191
+ }), N = [], P = !1, F = () => {
192
+ if (!P) {
193
+ P = !0;
194
+ for (let e of [...N].reverse()) try {
195
195
  e();
196
196
  } catch {}
197
197
  }
198
198
  };
199
- M.push(() => A(j));
199
+ N.push(() => j(M));
200
200
  try {
201
- let e = await j.init(), t = e.audioStreams ?? [], a = t.some((e) => e.streamIndex === _) ? _ : t[0]?.streamIndex ?? -1;
202
- oe?.(t, a);
201
+ let e = await M.init(), t = e.audioStreams ?? [], a = t.some((e) => e.streamIndex === _) ? _ : t[0]?.streamIndex ?? -1;
202
+ ae?.(t, a);
203
203
  let s = h({
204
204
  video: n,
205
205
  container: r,
@@ -207,19 +207,19 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
207
207
  wasmUrl: d,
208
208
  legacyWasmUrl: f,
209
209
  defaultFontUrl: p,
210
- onStreams: ae
210
+ onStreams: ie
211
211
  });
212
- if (M.push(() => s.destroy()), e.attachments?.length && s.pushAttachments(e.attachments), !e.info.output.videoMimeType) throw Error("The video codec in this file could not be identified");
212
+ if (N.push(() => s.destroy()), e.attachments?.length && s.pushAttachments(e.attachments), !e.info.output.videoMimeType) throw Error("The video codec in this file could not be identified");
213
213
  let c = [e.info.output.videoMimeType, e.info.output.audioMimeType].filter(Boolean).join(","), l = `video/mp4; codecs="${c}"`;
214
214
  if (!MediaSource.isTypeSupported(l)) throw Error(`This browser cannot play the codecs in this file: ${c}`);
215
- let m = new MediaSource(), k = URL.createObjectURL(m);
216
- M.push(() => URL.revokeObjectURL(k)), n.src = k, M.push(() => {
215
+ let m = new MediaSource(), A = URL.createObjectURL(m);
216
+ N.push(() => URL.revokeObjectURL(A)), n.src = A, N.push(() => {
217
217
  try {
218
218
  n.pause();
219
219
  } catch {}
220
220
  n.removeAttribute("src"), n.load();
221
221
  });
222
- let A = await new Promise((t, n) => {
222
+ let j = await new Promise((t, n) => {
223
223
  let r = setTimeout(() => {
224
224
  i(), n(/* @__PURE__ */ Error("The MediaSource never opened"));
225
225
  }, w), i = () => {
@@ -237,138 +237,138 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
237
237
  i(), n(/* @__PURE__ */ Error("The MediaSource closed before a SourceBuffer could be created"));
238
238
  };
239
239
  m.addEventListener("sourceopen", a, { once: !0 }), m.addEventListener("sourceclose", o, { once: !0 }), m.addEventListener("error", o, { once: !0 });
240
- }), { appendBuffer: P, unbufferRange: ce, updateTimestampOffset: le, endOfStream: ue } = o(A, m);
241
- await P(e.data), e.subtitles?.length && s.pushFragments(e.subtitles), te?.();
242
- let F = !1, I = !1, L = 0, R, de = () => R ?? n.currentTime, z = !1, fe = !1, pe = 0, B = 0, V = null, H = 0, U = 0, W = !1, G = 0, K = !0, q = (e, t, n = !1) => {
240
+ }), { appendBuffer: I, unbufferRange: oe, updateTimestampOffset: se, endOfStream: ce } = o(j, m);
241
+ await I(e.data), e.subtitles?.length && s.pushFragments(e.subtitles), ee?.();
242
+ let L = !1, R = !1, z = 0, B, le = () => B ?? n.currentTime, V = !1, ue = !1, de = 0, H = 0, U = null, W = 0, G = 0, K = !1, fe = 0, q = !0, J = (e, t, n = !1) => {
243
243
  let r = e?.message === "Cancelled";
244
- t && r || (console.error(e), (!W || n) && (W = !0, ne?.(r ? Error("Reading the video file failed", { cause: e }) : e)));
245
- }, me = async (e = !1) => {
246
- if (L > 0 && !e) return;
247
- let t = n.currentTime, r = e ? x : v, a = e ? S : y, o = i(A), s = (e, t) => (K = !1, ce(e, t));
244
+ t && r || (console.error(e), (!K || n) && (K = !0, te?.(r ? Error("Reading the video file failed", { cause: e }) : e)));
245
+ }, pe = async (e = !1) => {
246
+ if (z > 0 && !e) return;
247
+ let t = n.currentTime, r = e ? x : v, a = e ? S : y, o = i(j), s = (e, t) => (q = !1, oe(e, t));
248
248
  o.length && o[0].start < t + r && await s(0, t + r);
249
249
  for (let { end: e } of o) e > t + a && await s(t + a, e);
250
- }, he = () => {
251
- let e = i(A);
250
+ }, me = () => {
251
+ let e = i(j);
252
252
  if (!e.length) return !0;
253
- let t = de(), n = e.find((e) => e.start <= t + T && t < e.end);
254
- return be() && ve(t) && (!n || n.start <= t) || n && n.end >= t + y ? !1 : !n || n.end < t + b;
255
- }, ge = (e) => i(A).find((t) => t.start <= e + T && e < t.end), _e = () => ge(n.currentTime), ve = (e = n.currentTime) => {
256
- let t = ge(e);
257
- return e >= U - .1 || !!t && t.end >= U - .1;
258
- }, ye = () => {
259
- let e = i(A).map((e) => e.end);
260
- e.length && (U = Math.max(U, ...e)), z && (fe = !0);
261
- }, be = () => z || fe && (i(A).at(-1)?.end ?? 0) >= U - .1, J = async (e) => {
262
- I = !0, V = null, H = 0, G = e, K = !0;
263
- let t = ++B;
253
+ let t = le(), n = e.find((e) => e.start <= t + T && t < e.end);
254
+ return ye() && _e(t) && (!n || n.start <= t) || n && n.end >= t + y ? !1 : !n || n.end < t + b;
255
+ }, he = (e) => i(j).find((t) => t.start <= e + T && e < t.end), ge = () => he(n.currentTime), _e = (e = n.currentTime) => {
256
+ let t = he(e);
257
+ return e >= G - .1 || !!t && t.end >= G - .1;
258
+ }, ve = () => {
259
+ let e = i(j).map((e) => e.end);
260
+ e.length && (G = Math.max(G, ...e)), V && (ue = !0);
261
+ }, ye = () => V || ue && (i(j).at(-1)?.end ?? 0) >= G - .1, Y = async (e) => {
262
+ R = !0, U = null, W = 0, fe = e, q = !0;
263
+ let t = ++H;
264
264
  try {
265
- let { data: n, pts: r, subtitles: i, finished: a } = await j.seek(e);
266
- if (N || t !== B || (a && (z = !0), i?.length && s.pushFragments(i), await le(r), N || t !== B)) return;
267
- n.byteLength && (await P(n), ye());
265
+ let { data: n, pts: r, subtitles: i, finished: a } = await M.seek(e);
266
+ if (P || t !== H || (a && (V = !0), i?.length && s.pushFragments(i), await se(r), P || t !== H)) return;
267
+ n.byteLength && (await I(n), ve());
268
268
  } catch (e) {
269
- t === B && (K = !1), q(e, N || t !== B);
269
+ t === H && (q = !1), J(e, P || t !== H);
270
270
  } finally {
271
- t === B && (I = !1);
271
+ t === H && (R = !1);
272
272
  }
273
- }, xe = async () => {
274
- let e = V;
273
+ }, be = async () => {
274
+ let e = U;
275
275
  if (e) try {
276
- await P(e), ye(), V === e && (V = null, H = 0);
276
+ await I(e), ve(), U === e && (U = null, W = 0);
277
277
  } catch (t) {
278
- if (V !== e) return;
278
+ if (U !== e) return;
279
279
  if (t?.name === "QuotaExceededError") {
280
- let t = await me(!0).then(() => !0, () => !1);
281
- if (V !== e) return;
280
+ let t = await pe(!0).then(() => !0, () => !1);
281
+ if (U !== e) return;
282
282
  if (t) {
283
- z = !1, await J(_e()?.end ?? n.currentTime);
283
+ V = !1, await Y(ge()?.end ?? n.currentTime);
284
284
  return;
285
285
  }
286
286
  }
287
- if (H += 1, H < C) return;
288
- throw V = null, t;
287
+ if (W += 1, W < C) return;
288
+ throw U = null, t;
289
289
  }
290
- }, Se = async () => {
291
- if (F || I || Q !== void 0 || N || !V && (z || !he())) return;
292
- let e = ++pe, t = B, n = () => N || e !== pe || t !== B;
293
- F = !0;
290
+ }, xe = async () => {
291
+ if (L || R || $ !== void 0 || P || !U && (V || !me())) return;
292
+ let e = ++de, t = H, n = () => P || e !== de || t !== H;
293
+ L = !0;
294
294
  try {
295
- if (!V) {
296
- let { data: e, subtitles: t, finished: r } = await j.read();
297
- if (n() || (r && (z = !0), t?.length && s.pushFragments(t), !e.byteLength)) return;
298
- V = e, H = 0;
295
+ if (!U) {
296
+ let { data: e, subtitles: t, finished: r } = await M.read();
297
+ if (n() || (r && (V = !0), t?.length && s.pushFragments(t), !e.byteLength)) return;
298
+ U = e, W = 0;
299
299
  }
300
- await xe(), !n() && W && (W = !1, re?.());
300
+ await be(), !n() && K && (K = !1, ne?.());
301
301
  } catch (e) {
302
- q(e, n());
302
+ J(e, n());
303
303
  } finally {
304
- e === pe && (F = !1);
304
+ e === de && (L = !1);
305
305
  }
306
- }, Ce = (e) => e >= G && we(e) && Te(e), we = (e) => i(A).some((t) => t.start <= e && e < t.end), Y = (e) => {
307
- let t = ge(e);
306
+ }, Se = (e) => e >= fe && Ce(e) && we(e), Ce = (e) => i(j).some((t) => t.start <= e && e < t.end), X = (e) => {
307
+ let t = he(e);
308
308
  return t ? t.end - e : 0;
309
- }, Te = (e) => Y(e) >= E || be() && ve(e), Ee = async (e) => {
310
- if (!N && !(we(e) && Te(e))) {
311
- z = !1, L++, R = e;
309
+ }, we = (e) => X(e) >= E || ye() && _e(e), Te = async (e) => {
310
+ if (!P && !(Ce(e) && we(e))) {
311
+ V = !1, z++, B = e;
312
312
  try {
313
- await J(e);
314
- let t = B;
315
- for (let n = 0; n < ee && !(N || t !== B || z || Y(e) >= E); n++) {
316
- let t = Y(e);
317
- if (await Se(), Y(e) <= t) break;
313
+ await Y(e);
314
+ let t = H;
315
+ for (let n = 0; n < D && !(P || t !== H || V || X(e) >= E); n++) {
316
+ let t = X(e);
317
+ if (await xe(), X(e) <= t) break;
318
318
  }
319
319
  } finally {
320
- L--, R === e && (R = void 0);
320
+ z--, B === e && (B = void 0);
321
321
  }
322
322
  }
323
- }, De = 0, X = null, Z, Oe = (e) => {
324
- let t = performance.now(), n = t - De;
325
- if (n >= D) {
326
- De = t, X = null, ie?.(e);
323
+ }, Ee = 0, Z = null, Q, De = (e) => {
324
+ let t = performance.now(), n = t - Ee;
325
+ if (n >= O) {
326
+ Ee = t, Z = null, re?.(e);
327
327
  return;
328
328
  }
329
- X = e, !Z && (Z = setTimeout(() => {
330
- Z = void 0;
331
- let e = X;
332
- X = null, !(e === null || N) && (De = performance.now(), ie?.(e));
333
- }, D - n));
329
+ Z = e, !Q && (Q = setTimeout(() => {
330
+ Q = void 0;
331
+ let e = Z;
332
+ Z = null, !(e === null || P) && (Ee = performance.now(), re?.(e));
333
+ }, O - n));
334
334
  };
335
- M.push(() => {
336
- Z && clearTimeout(Z);
337
- });
338
- let ke = 0, Q;
339
- M.push(() => {
335
+ N.push(() => {
340
336
  Q && clearTimeout(Q);
341
337
  });
342
- let Ae = () => {
338
+ let Oe = 0, $;
339
+ N.push(() => {
340
+ $ && clearTimeout($);
341
+ });
342
+ let ke = () => {
343
343
  let t = n.currentTime, r = e.info.input.duration || n.duration;
344
- r > 0 && Oe(Math.min(Math.max(t / r, 0), 1));
345
- let i = performance.now(), a = i - ke < O;
346
- if (ke = i, !Ce(t) && !(K && (I || Y(t) > 0) && Math.abs(t - G) < .001)) {
347
- if (z = !1, Q && clearTimeout(Q), !a) {
348
- Q = void 0, J(t);
344
+ r > 0 && De(Math.min(Math.max(t / r, 0), 1));
345
+ let i = performance.now(), a = i - Oe < k;
346
+ if (Oe = i, !Se(t) && !(q && (R || X(t) > 0) && Math.abs(t - fe) < .001)) {
347
+ if (V = !1, $ && clearTimeout($), !a) {
348
+ $ = void 0, Y(t);
349
349
  return;
350
350
  }
351
- Q = setTimeout(() => {
352
- if (Q = void 0, N) return;
351
+ $ = setTimeout(() => {
352
+ if ($ = void 0, P) return;
353
353
  let e = n.currentTime;
354
- Ce(e) || (z = !1, J(e));
355
- }, O);
354
+ Se(e) || (V = !1, Y(e));
355
+ }, k);
356
356
  }
357
357
  };
358
- n.addEventListener("seeking", Ae), M.push(() => n.removeEventListener("seeking", Ae));
359
- let $ = !1, je = () => {
360
- if ($) return;
361
- $ = !0;
358
+ n.addEventListener("seeking", ke), N.push(() => n.removeEventListener("seeking", ke));
359
+ let Ae = !1, je = () => {
360
+ if (Ae) return;
361
+ Ae = !0;
362
362
  let e = n.error;
363
- q(new g(`the media element failed: ${e?.message ?? "unknown"}`, { cause: e }), !1, !0);
363
+ J(new g(`the media element failed: ${e?.message ?? "unknown"}`, { cause: e }), !1, !0);
364
364
  };
365
- n.addEventListener("error", je), M.push(() => n.removeEventListener("error", je));
365
+ n.addEventListener("error", je), N.push(() => n.removeEventListener("error", je));
366
366
  let Me = setInterval(() => {
367
- $ || n.error || (me().catch(() => {}), Se().catch((e) => q(e, !1)), be() && !V && ve() && m.readyState === "open" && ue().catch(() => {}));
367
+ Ae || n.error || (pe().catch(() => {}), xe().catch((e) => J(e, !1)), ye() && !U && _e() && m.readyState === "open" && ce(() => !R).catch(() => {}));
368
368
  }, 100);
369
- return M.push(() => clearInterval(Me)), {
370
- destroy: se,
371
- prepareSeek: Ee,
369
+ return N.push(() => clearInterval(Me)), {
370
+ destroy: F,
371
+ prepareSeek: Te,
372
372
  videoElement: n,
373
373
  selectSubtitleStream: (e) => s.selectStream(e),
374
374
  indexes: e.indexes ?? [],
@@ -378,10 +378,10 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
378
378
  audioMimeType: e.info.output.audioMimeType
379
379
  };
380
380
  } catch (e) {
381
- throw se(), e;
381
+ throw F(), e;
382
382
  }
383
- }, ne = 5, re = 500, ie = 320, ae = 1e6, oe = 3, j = 1e4, M = 1.5, N = 4e6, se = 3e4, P = async (e) => {
384
- let { publicPath: n, workerUrl: r, length: i, read: a, onThumbnails: o, width: s = ie } = e, c = await t({
383
+ }, te = 5, ne = 500, re = 320, ie = 1e6, ae = 3, M = 1e4, N = 1.5, P = 4e6, F = 3e4, I = async (e) => {
384
+ let { publicPath: n, workerUrl: r, length: i, read: a, onThumbnails: o, width: s = re } = e, c = await t({
385
385
  publicPath: n,
386
386
  workerUrl: r,
387
387
  workerOptions: { type: "module" },
@@ -389,7 +389,7 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
389
389
  read: a
390
390
  });
391
391
  try {
392
- let t = await c.init(), n = t.duration, r = Math.max(e.interval ?? ne, n / re), a = [], l = (e) => {
392
+ let t = await c.init(), n = t.duration, r = Math.max(e.interval ?? te, n / ne), a = [], l = (e) => {
393
393
  let t = [];
394
394
  for (let [a, o] of e.entries()) {
395
395
  let s = t.at(-1);
@@ -397,7 +397,7 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
397
397
  timestamp: o.timestamp,
398
398
  endTime: n,
399
399
  startByte: o.pos,
400
- endByte: Math.min((e[a + 1]?.pos ?? i) + ae, i),
400
+ endByte: Math.min((e[a + 1]?.pos ?? i) + ie, i),
401
401
  done: !1,
402
402
  attempts: 0
403
403
  });
@@ -436,7 +436,7 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
436
436
  }), o(e);
437
437
  }, x = async (e) => {
438
438
  if (d) return;
439
- let t = await Promise.race([c.readKeyframe(e.timestamp), new Promise((e, t) => setTimeout(() => t(/* @__PURE__ */ Error("timed out")), j))]), n = await createImageBitmap(new Blob([t], { type: "image/png" })), r = new OffscreenCanvas(s, Math.max(1, Math.round(n.height * (s / n.width))));
439
+ 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))));
440
440
  r.getContext("2d").drawImage(n, 0, 0, r.width, r.height), n.close();
441
441
  let i = await r.convertToBlob({
442
442
  type: "image/webp",
@@ -460,7 +460,7 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
460
460
  if (!f.length) return;
461
461
  let e = f.splice(y(), 1)[0];
462
462
  p = !0, x(e).catch(() => {
463
- e.attempts += 1, e.done = e.attempts >= oe;
463
+ e.attempts += 1, e.done = e.attempts >= ae;
464
464
  }).finally(() => {
465
465
  p = !1, S();
466
466
  });
@@ -469,7 +469,7 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
469
469
  }, w = async (e) => {
470
470
  _ = !0;
471
471
  try {
472
- let t = await Promise.race([c.init(), new Promise((e, t) => setTimeout(() => t(/* @__PURE__ */ Error("timed out")), se))]);
472
+ let t = await Promise.race([c.init(), new Promise((e, t) => setTimeout(() => t(/* @__PURE__ */ Error("timed out")), F))]);
473
473
  if (d) return;
474
474
  h = e;
475
475
  let n = l(t.indexes);
@@ -495,7 +495,7 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
495
495
  if (d) return;
496
496
  v = e;
497
497
  let t = T(e);
498
- h < 0 ? h = t : (t >= Math.max(h * M, h + N) || t >= i && h < i) && (g = !0), E(e), S();
498
+ h < 0 ? h = t : (t >= Math.max(h * N, h + P) || t >= i && h < i) && (g = !0), E(e), S();
499
499
  },
500
500
  prioritize: (e) => {
501
501
  d || (m = e);
@@ -503,14 +503,14 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
503
503
  destroy: () => {
504
504
  d = !0, f.length = 0;
505
505
  for (let e of u) URL.revokeObjectURL(e.url);
506
- u = [], A(c);
506
+ u = [], j(c);
507
507
  }
508
508
  };
509
509
  } catch (e) {
510
- throw A(c), e;
510
+ throw j(c), e;
511
511
  }
512
- }, ce = 1280, le = 1920, ue = 250, F = 100, I = () => typeof HTMLCanvasElement < "u" && "captureStream" in HTMLCanvasElement.prototype, L = () => typeof document < "u" && typeof HTMLVideoElement < "u" && typeof HTMLVideoElement.prototype.requestPictureInPicture == "function" && !!document.pictureInPictureEnabled, R = () => typeof HTMLVideoElement < "u" && "mozCaptureStream" in HTMLVideoElement.prototype, de = () => I() ? L() ? "window" : R() ? "burn-in" : null : null, z = (e) => {
513
- let { video: t, subtitles: n } = e, r = e.mode ?? de() ?? "window", i = e.maxWidth ?? (r === "burn-in" ? le : ce), a, o, s, c = !1, l = !1, u = !1, d = !1, f, p = () => r === "burn-in" ? !!a : !!a && document.pictureInPictureElement === a.mirror, m = (e, r) => {
512
+ }, oe = 1280, se = 1920, ce = 250, L = 100, R = () => typeof HTMLCanvasElement < "u" && "captureStream" in HTMLCanvasElement.prototype, z = () => typeof document < "u" && typeof HTMLVideoElement < "u" && typeof HTMLVideoElement.prototype.requestPictureInPicture == "function" && !!document.pictureInPictureEnabled, B = () => typeof HTMLVideoElement < "u" && "mozCaptureStream" in HTMLVideoElement.prototype, le = () => R() ? z() ? "window" : B() ? "burn-in" : null : null, V = (e) => {
513
+ let { video: t, subtitles: n } = e, r = e.mode ?? le() ?? "window", i = e.maxWidth ?? (r === "burn-in" ? se : oe), a, o, s, c = !1, l = !1, u = !1, d = !1, f, p = () => r === "burn-in" ? !!a : !!a && document.pictureInPictureElement === a.mirror, m = (e, r) => {
514
514
  r.drawImage(t, 0, 0, e.width, e.height);
515
515
  let i = n.querySelector("canvas");
516
516
  i && i.width > 0 && i.height > 0 && r.drawImage(i, 0, 0, e.width, e.height);
@@ -539,39 +539,39 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
539
539
  l = !0;
540
540
  let { mirror: e } = a;
541
541
  h(), e.play().then(() => new Promise((e) => {
542
- setTimeout(e, F);
542
+ setTimeout(e, L);
543
543
  })).catch(() => {}).finally(() => {
544
544
  e.pause(), setTimeout(() => {
545
545
  l = !1;
546
- }, F);
546
+ }, L);
547
547
  });
548
548
  }, E = [["play", () => {
549
549
  t.play().catch(() => {});
550
- }], ["pause", () => t.pause()]], ee = (e) => {
550
+ }], ["pause", () => t.pause()]], D = (e) => {
551
551
  if ("mediaSession" in navigator) for (let [t, n] of E) try {
552
552
  navigator.mediaSession.setActionHandler(t, e ? n : null);
553
553
  } catch {}
554
- }, D = () => {
555
- if (y(), ee(!1), t.removeEventListener("play", C), t.removeEventListener("pause", w), t.removeEventListener("seeked", T), !a) {
554
+ }, O = () => {
555
+ if (y(), D(!1), t.removeEventListener("play", C), t.removeEventListener("pause", w), t.removeEventListener("seeked", T), !a) {
556
556
  f?.();
557
557
  return;
558
558
  }
559
559
  let { stream: e, mirror: n } = a;
560
- n.removeEventListener("play", x), n.removeEventListener("pause", S), n.removeEventListener("leavepictureinpicture", O);
560
+ n.removeEventListener("play", x), n.removeEventListener("pause", S), n.removeEventListener("leavepictureinpicture", k);
561
561
  for (let t of e.getTracks()) t.stop();
562
562
  n.remove(), a = void 0, f?.();
563
- }, O = () => D(), k = (e) => {
563
+ }, k = () => O(), A = (e) => {
564
564
  let r = t.style.opacity, i = n.style.display;
565
565
  e.style.cssText = "position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#000;z-index:1", t.style.opacity = "0", n.style.display = "none", f = () => {
566
566
  f = void 0, t.style.opacity = r, n.style.display = i;
567
567
  };
568
- }, A = async () => {
568
+ }, j = async () => {
569
569
  let n = Math.min(t.videoWidth || i, i), o = Math.round(n * ((t.videoHeight || 9) / (t.videoWidth || 16))), c = document.createElement("canvas");
570
570
  c.width = n, c.height = o;
571
571
  let l = c.getContext("2d");
572
572
  if (!l) throw Error("the picture in picture composite has no 2d context");
573
573
  let u = document.createElement("video");
574
- u.muted = !0, u.playsInline = !0, r === "burn-in" ? k(u) : u.style.cssText = "position:fixed;width:1px;height:1px;opacity:0;pointer-events:none;left:-1px;top:-1px", m(c, l);
574
+ u.muted = !0, u.playsInline = !0, r === "burn-in" ? A(u) : u.style.cssText = "position:fixed;width:1px;height:1px;opacity:0;pointer-events:none;left:-1px;top:-1px", m(c, l);
575
575
  let f = {
576
576
  composite: c,
577
577
  context: l,
@@ -580,7 +580,7 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
580
580
  };
581
581
  if (a = f, u.srcObject = f.stream, (e.container ?? t.parentElement ?? document.body).appendChild(u), v(), s = setInterval(() => {
582
582
  t.paused && h();
583
- }, ue), u.play().catch(() => {}), u.readyState === 0 && await new Promise((e) => {
583
+ }, ce), u.play().catch(() => {}), u.readyState === 0 && await new Promise((e) => {
584
584
  let t = () => {
585
585
  clearTimeout(n), e();
586
586
  }, n = setTimeout(t, 1e3);
@@ -589,7 +589,7 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
589
589
  u.remove();
590
590
  return;
591
591
  }
592
- if (u.addEventListener("play", x), u.addEventListener("pause", S), u.addEventListener("leavepictureinpicture", O), t.addEventListener("play", C), t.addEventListener("pause", w), t.addEventListener("seeked", T), ee(!0), b(), r === "burn-in") {
592
+ if (u.addEventListener("play", x), u.addEventListener("pause", S), u.addEventListener("leavepictureinpicture", k), t.addEventListener("play", C), t.addEventListener("pause", w), t.addEventListener("seeked", T), D(!0), b(), r === "burn-in") {
593
593
  t.paused && u.pause(), e.onBurnedInChange?.(!0);
594
594
  return;
595
595
  }
@@ -602,7 +602,7 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
602
602
  try {
603
603
  if (p()) {
604
604
  if (r === "burn-in") {
605
- D(), e.onBurnedInChange?.(!1);
605
+ O(), e.onBurnedInChange?.(!1);
606
606
  return;
607
607
  }
608
608
  await document.exitPictureInPicture().catch(() => {});
@@ -610,9 +610,9 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
610
610
  }
611
611
  r !== "burn-in" && document.pictureInPictureElement && await document.exitPictureInPicture().catch(() => {});
612
612
  try {
613
- await A();
613
+ await j();
614
614
  } catch (t) {
615
- console.warn("subtitle compositing for picture in picture failed", t), D(), r === "burn-in" && e.onBurnedInChange?.(!1);
615
+ console.warn("subtitle compositing for picture in picture failed", t), O(), r === "burn-in" && e.onBurnedInChange?.(!1);
616
616
  }
617
617
  } finally {
618
618
  u = !1;
@@ -621,9 +621,9 @@ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
621
621
  },
622
622
  mode: r,
623
623
  destroy: () => {
624
- d = !0, r !== "burn-in" && p() && document.exitPictureInPicture().catch(() => {}), D(), r === "burn-in" && e.onBurnedInChange?.(!1);
624
+ d = !0, r !== "burn-in" && p() && document.exitPictureInPicture().catch(() => {}), O(), r === "burn-in" && e.onBurnedInChange?.(!1);
625
625
  }
626
626
  };
627
627
  };
628
628
  //#endregion
629
- export { g as a, A as c, i as d, o as f, k as i, s as l, de as n, _ as o, P as r, te as s, z as t, h as u };
629
+ export { g as a, j as c, i as d, o as f, A as i, s as l, le as n, _ as o, I as r, ee as s, V as t, h as u };
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-C9vSEJYW.js";
1
+ import { l as e, n as t, o as n, r, s as i, t as a, u as o } from "./engine-C4Gj_4zs.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, useSyncExternalStore as h } from "react";
3
3
  import { css as g, keyframes as _ } from "@emotion/react";
4
4
  import { createPlayer as v, useContainerAttach as y, useMediaAttach as b } from "@videojs/react";
@@ -2252,82 +2252,90 @@ ${gt}
2252
2252
  }
2253
2253
  }
2254
2254
  }
2255
- `, Vt = () => {
2256
- let e = G(), t = G((e) => e.paused), n = G((e) => e.currentTime), r = G((e) => e.duration), i = G((e) => e.seekingTo), a = G((e) => e.fullscreen), o = G((e) => e.hideUI), s = G((e) => e.togglePictureInPicture), c = G((e) => e.pictureInPictureMode), l = G((e) => e.burnedInSubtitles), f = G((e) => e.passThroughPictureInPicture), h = G((e) => e.subtitleTracks), g = G((e) => e.requestSeek), [_, v] = m(null), y = p(null), b = p(void 0), x = c === "burn-in", S = h.length > 0, C = r > 0 && n === r, w = u(({ direction: t, stepSize: n }) => {
2257
- let r = Ne(e.volume), i = t === "up" ? n : -n, a = Math.max(0, Math.min(1, r + i)), o = e.muted;
2258
- e.setVolume(Me(a)), o && !e.muted && e.toggleMuted();
2259
- }, [e]);
2255
+ `, Vt = ({ shortcuts: e = "window", root: t }) => {
2256
+ let n = G(), r = G((e) => e.paused), i = G((e) => e.currentTime), a = G((e) => e.duration), o = G((e) => e.seekingTo), s = G((e) => e.fullscreen), c = G((e) => e.hideUI), l = G((e) => e.togglePictureInPicture), f = G((e) => e.pictureInPictureMode), h = G((e) => e.burnedInSubtitles), g = G((e) => e.passThroughPictureInPicture), _ = G((e) => e.subtitleTracks), v = G((e) => e.requestSeek), [y, b] = m(null), x = p(null), S = p(void 0), C = f === "burn-in", w = _.length > 0, D = a > 0 && i === a, O = u(({ direction: e, stepSize: t }) => {
2257
+ let r = Ne(n.volume), i = e === "up" ? t : -t, a = Math.max(0, Math.min(1, r + i)), o = n.muted;
2258
+ n.setVolume(Me(a)), o && !n.muted && n.toggleMuted();
2259
+ }, [n]), k = p(!1);
2260
2260
  d(() => {
2261
- let t = (t) => g ? g(t) : e.seek(t), n = (n) => {
2262
- let r = n.target;
2263
- if (r?.isContentEditable || /^(input|textarea|select)$/i.test(r?.tagName ?? "")) return;
2264
- let i = !0;
2265
- if (n.key === "f") e.toggleFullscreen();
2266
- else if (n.key === "k") e.togglePaused();
2267
- else if (n.key === " ") e.togglePaused();
2268
- else if (n.key === "m") e.toggleMuted();
2269
- else if (n.key === "ArrowUp") w({
2261
+ let r = (e) => v ? v(e) : n.seek(e), i = (e) => e.composedPath()[0], a = (e) => {
2262
+ k.current = !!t.current?.contains(i(e) ?? null);
2263
+ }, o = (a) => {
2264
+ let o = i(a);
2265
+ if (o?.isContentEditable || /^(input|textarea|select)$/i.test(o?.tagName ?? "")) return;
2266
+ let s = t.current?.contains(o ?? null) || o === document.body && k.current;
2267
+ if (e === "player" && !n.fullscreen && !s) return;
2268
+ let c = !0;
2269
+ if (a.key === "f") n.toggleFullscreen();
2270
+ else if (a.key === "k") n.togglePaused();
2271
+ else if (a.key === " ") n.togglePaused();
2272
+ else if (a.key === "m") n.toggleMuted();
2273
+ else if (a.key === "ArrowUp") O({
2270
2274
  direction: "up",
2271
2275
  stepSize: It
2272
2276
  });
2273
- else if (n.key === "ArrowDown") w({
2277
+ else if (a.key === "ArrowDown") O({
2274
2278
  direction: "down",
2275
2279
  stepSize: It
2276
2280
  });
2277
- else if (n.key === "ArrowRight") {
2278
- if (!e.duration) return;
2279
- t(Math.min(e.currentTime + Lt, e.duration));
2280
- } else n.key === "ArrowLeft" ? t(Math.max(e.currentTime - Lt, 0)) : i = !1;
2281
- i && n.preventDefault();
2281
+ else if (a.key === "ArrowRight") {
2282
+ if (!n.duration) return;
2283
+ r(Math.min(n.currentTime + Lt, n.duration));
2284
+ } else a.key === "ArrowLeft" ? r(Math.max(n.currentTime - Lt, 0)) : c = !1;
2285
+ c && a.preventDefault();
2286
+ };
2287
+ return window.addEventListener("keydown", o), window.addEventListener("pointerdown", a, !0), window.addEventListener("focusin", a, !0), () => {
2288
+ window.removeEventListener("keydown", o), window.removeEventListener("pointerdown", a, !0), window.removeEventListener("focusin", a, !0);
2282
2289
  };
2283
- return window.addEventListener("keydown", n), () => window.removeEventListener("keydown", n);
2284
2290
  }, [
2291
+ n,
2292
+ O,
2293
+ v,
2285
2294
  e,
2286
- w,
2287
- g
2295
+ t
2288
2296
  ]);
2289
- let D = u((e) => {
2290
- e.preventDefault(), e.deltaY < 0 ? w({
2297
+ let A = u((e) => {
2298
+ e.preventDefault(), e.deltaY < 0 ? O({
2291
2299
  direction: "up",
2292
2300
  stepSize: It
2293
- }) : w({
2301
+ }) : O({
2294
2302
  direction: "down",
2295
2303
  stepSize: It
2296
2304
  });
2297
- }, [w]);
2305
+ }, [O]);
2298
2306
  d(() => {
2299
- if (_) return _.addEventListener("wheel", D, { passive: !1 }), () => _.removeEventListener("wheel", D);
2300
- }, [_, D]);
2301
- let O = (e) => {
2302
- if (!f || f.active || f.armed || e.pointerType === "touch") return;
2307
+ if (y) return y.addEventListener("wheel", A, { passive: !1 }), () => y.removeEventListener("wheel", A);
2308
+ }, [y, A]);
2309
+ let j = (e) => {
2310
+ if (!g || g.active || g.armed || e.pointerType === "touch") return;
2303
2311
  let { left: t, top: n, width: r, height: i } = e.currentTarget.getBoundingClientRect();
2304
- f.arm({
2312
+ g.arm({
2305
2313
  left: t,
2306
2314
  top: n,
2307
2315
  width: r,
2308
2316
  height: i
2309
2317
  });
2310
- }, k = !!f?.armed;
2318
+ }, M = !!g?.armed;
2311
2319
  return d(() => {
2312
- if (!k) return;
2313
- clearInterval(b.current);
2320
+ if (!M) return;
2321
+ clearInterval(S.current);
2314
2322
  let e = Infinity, t = setInterval(() => {
2315
2323
  if (Date.now() > e) {
2316
2324
  clearInterval(t);
2317
2325
  return;
2318
2326
  }
2319
- let n = y.current, r = document.activeElement;
2327
+ let n = x.current, r = document.activeElement;
2320
2328
  n && r?.tagName === "IFRAME" && r.closest(".video")?.parentElement?.contains(n) && n.focus({ preventScroll: !0 });
2321
2329
  }, Rt);
2322
- return b.current = t, () => {
2330
+ return S.current = t, () => {
2323
2331
  e = Date.now() + zt;
2324
2332
  };
2325
- }, [k]), d(() => () => clearInterval(b.current), []), /* @__PURE__ */ E("div", {
2333
+ }, [M]), d(() => () => clearInterval(S.current), []), /* @__PURE__ */ E("div", {
2326
2334
  css: Bt,
2327
- style: { ...o ? {
2335
+ style: { ...c ? {
2328
2336
  opacity: "0",
2329
2337
  pointerEvents: "none"
2330
- } : k ? { pointerEvents: "none" } : {} },
2338
+ } : M ? { pointerEvents: "none" } : {} },
2331
2339
  children: [/* @__PURE__ */ T(dt, {}), /* @__PURE__ */ E("div", {
2332
2340
  className: "actions",
2333
2341
  children: [/* @__PURE__ */ E("div", {
@@ -2339,16 +2347,16 @@ ${gt}
2339
2347
  text: /* @__PURE__ */ T("button", {
2340
2348
  className: "play",
2341
2349
  type: "button",
2342
- onClick: () => e.togglePaused(),
2343
- "aria-label": C ? "Replay" : t ? "Play" : "Pause",
2344
- children: T(C ? L : t ? I : F, {})
2350
+ onClick: () => n.togglePaused(),
2351
+ "aria-label": D ? "Replay" : r ? "Play" : "Pause",
2352
+ children: T(D ? L : r ? I : F, {})
2345
2353
  }),
2346
- toolTipText: /* @__PURE__ */ T("span", { children: C ? "Replay (k)" : t ? "Play (k)" : "Pause (k)" })
2354
+ toolTipText: /* @__PURE__ */ T("span", { children: D ? "Replay (k)" : r ? "Play (k)" : "Pause (k)" })
2347
2355
  }),
2348
- /* @__PURE__ */ T(Ft, { ref: v }),
2356
+ /* @__PURE__ */ T(Ft, { ref: b }),
2349
2357
  /* @__PURE__ */ T("div", {
2350
2358
  className: "time",
2351
- children: Fe(i ?? n, r)
2359
+ children: Fe(o ?? i, a)
2352
2360
  })
2353
2361
  ]
2354
2362
  }), /* @__PURE__ */ E("div", {
@@ -2357,40 +2365,40 @@ ${gt}
2357
2365
  /* @__PURE__ */ T(wt, {}),
2358
2366
  /* @__PURE__ */ T(At, {}),
2359
2367
  /* @__PURE__ */ T(Ot, {}),
2360
- f ? /* @__PURE__ */ T($, {
2368
+ g ? /* @__PURE__ */ T($, {
2361
2369
  id: "picture-in-picture",
2362
2370
  tooltipPlace: "top-end",
2363
2371
  text: /* @__PURE__ */ T("button", {
2364
- ref: y,
2365
- className: f.armed ? "picture-in-picture armed" : "picture-in-picture",
2372
+ ref: x,
2373
+ className: g.armed ? "picture-in-picture armed" : "picture-in-picture",
2366
2374
  type: "button",
2367
- onPointerEnter: O,
2368
- onPointerMove: O,
2369
- onClick: f.active ? f.exit : void 0,
2375
+ onPointerEnter: j,
2376
+ onPointerMove: j,
2377
+ onClick: g.active ? g.exit : void 0,
2370
2378
  "aria-label": "Picture in picture",
2371
- "aria-pressed": f.active,
2379
+ "aria-pressed": g.active,
2372
2380
  children: /* @__PURE__ */ T("img", {
2373
2381
  src: ft,
2374
2382
  alt: ""
2375
2383
  })
2376
2384
  }),
2377
- toolTipText: /* @__PURE__ */ T("span", { children: f.active ? "Exit picture in picture" : "Picture in picture" })
2378
- }) : s ? /* @__PURE__ */ T($, {
2385
+ toolTipText: /* @__PURE__ */ T("span", { children: g.active ? "Exit picture in picture" : "Picture in picture" })
2386
+ }) : l ? /* @__PURE__ */ T($, {
2379
2387
  id: "picture-in-picture",
2380
2388
  tooltipPlace: "top-end",
2381
2389
  text: /* @__PURE__ */ T("button", {
2382
2390
  className: "picture-in-picture",
2383
2391
  type: "button",
2384
- onClick: s,
2385
- disabled: x && !S,
2386
- "aria-label": x ? "Put the subtitles in the video" : "Picture in picture",
2387
- "aria-pressed": x ? l : void 0,
2388
- children: x ? T(l ? mt : ht, {}) : /* @__PURE__ */ T("img", {
2392
+ onClick: l,
2393
+ disabled: C && !w,
2394
+ "aria-label": C ? "Put the subtitles in the video" : "Picture in picture",
2395
+ "aria-pressed": C ? h : void 0,
2396
+ children: C ? T(h ? mt : ht, {}) : /* @__PURE__ */ T("img", {
2389
2397
  src: ft,
2390
2398
  alt: ""
2391
2399
  })
2392
2400
  }),
2393
- toolTipText: /* @__PURE__ */ T("span", { children: x ? S ? l ? "Subtitles are in the picture" : "Put the subtitles in the picture" : "This file has no subtitles" : "Picture in picture" })
2401
+ toolTipText: /* @__PURE__ */ T("span", { children: C ? w ? h ? "Subtitles are in the picture" : "Put the subtitles in the picture" : "This file has no subtitles" : "Picture in picture" })
2394
2402
  }) : null,
2395
2403
  /* @__PURE__ */ T($, {
2396
2404
  id: "full-screen",
@@ -2398,12 +2406,12 @@ ${gt}
2398
2406
  text: /* @__PURE__ */ T("button", {
2399
2407
  className: "full-screen",
2400
2408
  type: "button",
2401
- onClick: () => e.toggleFullscreen(),
2409
+ onClick: () => n.toggleFullscreen(),
2402
2410
  "aria-label": "Full screen",
2403
- "aria-pressed": a,
2404
- children: T(a ? P : N, {})
2411
+ "aria-pressed": s,
2412
+ children: T(s ? P : N, {})
2405
2413
  }),
2406
- toolTipText: /* @__PURE__ */ T("span", { children: a ? "Exit full screen (f)" : "Full screen (f)" })
2414
+ toolTipText: /* @__PURE__ */ T("span", { children: s ? "Exit full screen (f)" : "Full screen (f)" })
2407
2415
  })
2408
2416
  ]
2409
2417
  })]
@@ -2594,6 +2602,9 @@ ${gt}
2594
2602
  align-items: center;
2595
2603
  height: 100%;
2596
2604
  width: 100%;
2605
+ /* focusable with shortcuts='player', and the first key after a click on the picture would otherwise
2606
+ ring the whole player as :focus-visible */
2607
+ outline: none;
2597
2608
 
2598
2609
  & > div:not(:last-of-type) {
2599
2610
  position: absolute;
@@ -2646,85 +2657,89 @@ ${gt}
2646
2657
  key: r,
2647
2658
  item: e
2648
2659
  }];
2649
- }), nn = ({ ref: e, onVideoRef: t, onSubtitleRef: n, overlay: r, controls: i, children: a }) => {
2650
- let o = G(), s = G((e) => e.hideUI), c = G((e) => e.setHideUI), l = G((e) => e.hasMedia), u = G((e) => e.passThroughPictureInPicture), f = p(void 0), m = p("mouse"), h = p({
2660
+ }), nn = ({ ref: e, onVideoRef: t, onSubtitleRef: n, overlay: r, controls: i, shortcuts: a, children: o }) => {
2661
+ let s = G(), c = G((e) => e.hideUI), l = G((e) => e.setHideUI), u = G((e) => e.hasMedia), f = G((e) => e.passThroughPictureInPicture), m = p(void 0), h = p("mouse"), g = p({
2651
2662
  x: -1,
2652
2663
  y: -1
2653
- }), g = p(null);
2654
- d(() => () => clearTimeout(f.current), []), d(() => {
2655
- l || (clearTimeout(f.current), c(!1));
2656
- }, [l, c]);
2657
- let _ = () => {
2658
- l && c(!0);
2659
- }, v = (t) => {
2660
- g.current = t, typeof e == "function" ? e(t) : e && (e.current = t);
2661
- }, y = () => {
2662
- if (o.passThroughPictureInPicture?.armed) return !0;
2663
- if (m.current !== "mouse") return !1;
2664
- let { x: e, y: t } = h.current;
2665
- if (!g.current || e < 0) return !1;
2666
- let n = document.elementFromPoint(e, t);
2667
- return !!n && g.current.contains(n) && !n.closest(".video");
2664
+ }), _ = p(null);
2665
+ d(() => () => clearTimeout(m.current), []), d(() => {
2666
+ u || (clearTimeout(m.current), l(!1));
2667
+ }, [u, l]);
2668
+ let v = () => {
2669
+ u && l(!0);
2670
+ }, y = (t) => {
2671
+ _.current = t, typeof e == "function" ? e(t) : e && (e.current = t);
2668
2672
  }, b = () => {
2669
- if (y()) {
2670
- f.current = setTimeout(b, Zt);
2673
+ if (s.passThroughPictureInPicture?.armed) return !0;
2674
+ if (h.current !== "mouse") return !1;
2675
+ let { x: e, y: t } = g.current;
2676
+ if (!_.current || e < 0) return !1;
2677
+ let n = document.elementFromPoint(e, t);
2678
+ return !!n && _.current.contains(n) && !n.closest(".video");
2679
+ }, x = () => {
2680
+ if (b()) {
2681
+ m.current = setTimeout(x, Zt);
2671
2682
  return;
2672
2683
  }
2673
- _();
2674
- }, x = () => {
2675
- c(!1), clearTimeout(f.current), f.current = setTimeout(b, Zt);
2684
+ v();
2685
+ }, S = () => {
2686
+ l(!1), clearTimeout(m.current), m.current = setTimeout(x, Zt);
2676
2687
  };
2677
2688
  return /* @__PURE__ */ E("div", {
2678
2689
  css: en,
2679
- ref: v,
2690
+ ref: y,
2680
2691
  onPointerMove: (e) => {
2681
- m.current = e.pointerType, e.pointerType === "mouse" && (h.current = {
2692
+ h.current = e.pointerType, e.pointerType === "mouse" && (g.current = {
2682
2693
  x: e.clientX,
2683
2694
  y: e.clientY
2684
- }, x());
2695
+ }, S());
2685
2696
  },
2686
2697
  onPointerDown: (e) => {
2687
- m.current = e.pointerType;
2698
+ h.current = e.pointerType;
2688
2699
  },
2689
2700
  onMouseOut: (e) => {
2690
2701
  let t = e.relatedTarget;
2691
2702
  if (t && e.currentTarget.contains(t)) return;
2692
2703
  let { left: n, right: r, top: i, bottom: a } = e.currentTarget.getBoundingClientRect();
2693
- e.clientX >= n && e.clientX < r && e.clientY >= i && e.clientY < a || (clearTimeout(f.current), _());
2704
+ e.clientX >= n && e.clientX < r && e.clientY >= i && e.clientY < a || (clearTimeout(m.current), v());
2694
2705
  },
2695
- className: s ? "hide" : "",
2706
+ className: c ? "hide" : "",
2707
+ tabIndex: a === "player" ? -1 : void 0,
2696
2708
  children: [
2697
2709
  /* @__PURE__ */ T(ke, { onSubtitleRef: n }),
2698
2710
  tn(r).map(({ key: e, item: t }) => /* @__PURE__ */ T("div", {
2699
2711
  css: Qt,
2700
- style: { ...s ? {
2712
+ style: { ...c ? {
2701
2713
  opacity: "0",
2702
2714
  visibility: "hidden",
2703
2715
  pointerEvents: "none"
2704
2716
  } : {} },
2705
2717
  children: t
2706
2718
  }, e)),
2707
- i === !1 ? null : /* @__PURE__ */ T(Vt, {}),
2719
+ i === !1 ? null : /* @__PURE__ */ T(Vt, {
2720
+ shortcuts: a,
2721
+ root: _
2722
+ }),
2708
2723
  /* @__PURE__ */ T(Wt, {}),
2709
2724
  /* @__PURE__ */ T(Xt, {}),
2710
- u?.armed && g.current ? /* @__PURE__ */ T("div", {
2725
+ f?.armed && _.current ? /* @__PURE__ */ T("div", {
2711
2726
  className: "pass-through",
2712
- style: { clipPath: $t(u.armed, g.current.getBoundingClientRect()) },
2713
- onPointerOver: u.disarm
2727
+ style: { clipPath: $t(f.armed, _.current.getBoundingClientRect()) },
2728
+ onPointerOver: f.disarm
2714
2729
  }) : null,
2715
2730
  /* @__PURE__ */ E("div", {
2716
2731
  className: "video",
2717
2732
  onClick: () => {
2718
- if (m.current === "mouse") {
2719
- o.togglePaused();
2733
+ if (h.current === "mouse") {
2734
+ s.togglePaused();
2720
2735
  return;
2721
2736
  }
2722
- s ? x() : (clearTimeout(f.current), _());
2737
+ c ? S() : (clearTimeout(m.current), v());
2723
2738
  },
2724
2739
  children: [t ? /* @__PURE__ */ T("video", {
2725
2740
  ref: t,
2726
2741
  playsInline: !0
2727
- }) : null, a]
2742
+ }) : null, o]
2728
2743
  })
2729
2744
  ]
2730
2745
  });
@@ -2839,6 +2854,7 @@ ${gt}
2839
2854
  onSubtitleRef: w,
2840
2855
  overlay: e.overlay,
2841
2856
  controls: e.controls,
2857
+ shortcuts: e.shortcuts,
2842
2858
  children: t
2843
2859
  });
2844
2860
  }, an = g`
@@ -8,7 +8,8 @@ export type ChromeProps = {
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
+ shortcuts?: 'window' | 'player';
11
12
  children?: ReactNode;
12
13
  };
13
- export declare const Chrome: ({ ref, onVideoRef, onSubtitleRef, overlay, controls, children }: ChromeProps) => import("@emotion/react/jsx-runtime").JSX.Element;
14
+ export declare const Chrome: ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shortcuts, children }: ChromeProps) => import("@emotion/react/jsx-runtime").JSX.Element;
14
15
  export default Chrome;
@@ -1,2 +1,6 @@
1
- export declare const ControlBar: () => import("@emotion/react/jsx-runtime").JSX.Element;
1
+ import type { RefObject } from 'react';
2
+ export declare const ControlBar: ({ shortcuts, root }: {
3
+ shortcuts?: 'window' | 'player';
4
+ root: RefObject<HTMLDivElement | null>;
5
+ }) => import("@emotion/react/jsx-runtime").JSX.Element;
2
6
  export default ControlBar;
@@ -59,6 +59,13 @@ type CommonOptions = {
59
59
  * store's `attach` and does nothing at all until a media arrives.
60
60
  */
61
61
  controls?: boolean;
62
+ /**
63
+ * Where the keyboard shortcuts listen. `'window'`, the default, takes keys from the whole page, which suits a
64
+ * page that is only the player. `'player'` takes a key only when the focus is inside the player (a click on the
65
+ * picture puts it there), or nothing has the focus and the last click or focus was in the player, or the player
66
+ * is fullscreen, so Space and the arrow keys keep scrolling the page and pressing its buttons everywhere else.
67
+ */
68
+ shortcuts?: 'window' | 'player';
62
69
  /**
63
70
  * The app's own content over the video: a download readout, a badge, a logo, anything the player
64
71
  * itself has no opinion about.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@banou/media-player",
3
- "version": "0.10.16",
3
+ "version": "0.10.18",
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",
@@ -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
- // every remove() puts the MediaSource back to 'open', so the end has to be re-armed
713
- if (endInBuffer() && !pending && atEnd() && mediaSource.readyState === 'open') endOfStream().catch(() => {})
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
- // endOfStream throws mid-update and is undone by the next remove(), so the caller re-arms it
54
- const endOfStream = () =>
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
 
@@ -52,6 +52,9 @@ const style = css`
52
52
  align-items: center;
53
53
  height: 100%;
54
54
  width: 100%;
55
+ /* focusable with shortcuts='player', and the first key after a click on the picture would otherwise
56
+ ring the whole player as :focus-visible */
57
+ outline: none;
55
58
 
56
59
  & > div:not(:last-of-type) {
57
60
  position: absolute;
@@ -129,10 +132,11 @@ export type ChromeProps = {
129
132
  overlay?: ReactNode
130
133
  /** False draws no control bar at all, leaving the picture, the title and the overlay. */
131
134
  controls?: boolean
135
+ shortcuts?: 'window' | 'player'
132
136
  children?: ReactNode
133
137
  }
134
138
 
135
- export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, children }: ChromeProps) => {
139
+ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, shortcuts, children }: ChromeProps) => {
136
140
  const player = usePlayer()
137
141
  const hideUI = usePlayer((state) => state.hideUI)
138
142
  const setHideUI = usePlayer((state) => state.setHideUI)
@@ -264,6 +268,8 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, chil
264
268
  onPointerDown={onPointerDown}
265
269
  onMouseOut={onMouseOut}
266
270
  className={hideUI ? 'hide' : ''}
271
+ // focusable, so a click on the picture puts the focus, and with it the shortcuts, inside the player
272
+ tabIndex={shortcuts === 'player' ? -1 : undefined}
267
273
  >
268
274
  <Overlay onSubtitleRef={onSubtitleRef} />
269
275
  {overlayItems(overlay).map(({ key, item }) => (
@@ -275,7 +281,7 @@ export const Chrome = ({ ref, onVideoRef, onSubtitleRef, overlay, controls, chil
275
281
  {item}
276
282
  </div>
277
283
  ))}
278
- {controls === false ? null : <ControlBar />}
284
+ {controls === false ? null : <ControlBar shortcuts={shortcuts} root={root} />}
279
285
  {/* Not tied to `hideUI`: it says what to do next, and it is on screen for nine seconds. */}
280
286
  <BurnInHint />
281
287
  <SkipChapter />
@@ -1,4 +1,6 @@
1
1
  /// <reference types="@emotion/react/types/css-prop" />
2
+ import type { RefObject } from 'react'
3
+
2
4
  import { useCallback, useEffect, useRef, useState } from 'react'
3
5
  import { css } from '@emotion/react'
4
6
  import { Maximize, Minimize, Pause, Play, RotateCcw } from 'lucide-react'
@@ -157,7 +159,9 @@ const style = css`
157
159
  }
158
160
  `
159
161
 
160
- export const ControlBar = () => {
162
+ export const ControlBar = (
163
+ { shortcuts = 'window', root }: { shortcuts?: 'window' | 'player', root: RefObject<HTMLDivElement | null> },
164
+ ) => {
161
165
  const player = usePlayer()
162
166
  const paused = usePlayer((state) => state.paused)
163
167
  const currentTime = usePlayer((state) => state.currentTime)
@@ -198,12 +202,21 @@ export const ControlBar = () => {
198
202
  if (wasMuted && !player.muted) player.toggleMuted()
199
203
  }, [player])
200
204
 
205
+ // whether the last press or focus on the page landed in the player: a focused control that goes away (Skip
206
+ // Opening, once pressed) leaves the focus on the body, and the shortcuts stay with the player through that
207
+ const engaged = useRef(false)
208
+
201
209
  useEffect(() => {
202
210
  const seek = (time: number) => requestSeek ? requestSeek(time) : player.seek(time)
211
+ // the path, not ev.target, which a shadow root around the player retargets to its host
212
+ const targetOf = (ev: Event) => ev.composedPath()[0] as HTMLElement | undefined
213
+ const engage = (ev: Event) => { engaged.current = !!root.current?.contains(targetOf(ev) ?? null) }
203
214
  const eventListener = (ev: KeyboardEvent) => {
204
- // on the window, so typing into a consumer's own input is the one case that opts out
205
- const target = ev.target as HTMLElement | null
215
+ // on the window, so typing into a consumer's own input opts out
216
+ const target = targetOf(ev)
206
217
  if (target?.isContentEditable || /^(input|textarea|select)$/i.test(target?.tagName ?? '')) return
218
+ const fromPlayer = root.current?.contains(target ?? null) || (target === document.body && engaged.current)
219
+ if (shortcuts === 'player' && !player.fullscreen && !fromPlayer) return
207
220
 
208
221
  let shouldPreventDefault = true
209
222
  // space would otherwise reach the browser's own shortcut
@@ -233,8 +246,14 @@ export const ControlBar = () => {
233
246
  }
234
247
  }
235
248
  window.addEventListener('keydown', eventListener)
236
- return () => window.removeEventListener('keydown', eventListener)
237
- }, [player, modifyVolume, requestSeek])
249
+ window.addEventListener('pointerdown', engage, true)
250
+ window.addEventListener('focusin', engage, true)
251
+ return () => {
252
+ window.removeEventListener('keydown', eventListener)
253
+ window.removeEventListener('pointerdown', engage, true)
254
+ window.removeEventListener('focusin', engage, true)
255
+ }
256
+ }, [player, modifyVolume, requestSeek, shortcuts, root])
238
257
 
239
258
  const handleWheel = useCallback((e: WheelEvent) => {
240
259
  e.preventDefault()
@@ -73,6 +73,14 @@ type CommonOptions = {
73
73
  */
74
74
  controls?: boolean
75
75
 
76
+ /**
77
+ * Where the keyboard shortcuts listen. `'window'`, the default, takes keys from the whole page, which suits a
78
+ * page that is only the player. `'player'` takes a key only when the focus is inside the player (a click on the
79
+ * picture puts it there), or nothing has the focus and the last click or focus was in the player, or the player
80
+ * is fullscreen, so Space and the arrow keys keep scrolling the page and pressing its buttons everywhere else.
81
+ */
82
+ shortcuts?: 'window' | 'player'
83
+
76
84
  /**
77
85
  * The app's own content over the video: a download readout, a badge, a logo, anything the player
78
86
  * itself has no opinion about.
@@ -411,6 +419,7 @@ const PlayerRoot = ({ options, children }: { options: MediaPlayerOptions, childr
411
419
  onSubtitleRef={setSubtitleLayer}
412
420
  overlay={options.overlay}
413
421
  controls={options.controls}
422
+ shortcuts={options.shortcuts}
414
423
  >
415
424
  {children}
416
425
  </Chrome>