@banou/media-player 0.8.15 → 0.8.17

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
@@ -1,3 +1,5 @@
1
+
2
+
1
3
  # @banou/media-player
2
4
 
3
5
  A React video player for files the browser cannot open on its own. It takes a `read(offset, size)` and
@@ -21,6 +23,7 @@ import MediaPlayer from '@banou/media-player'
21
23
  libavWorkerUrl="/libav-worker.js"
22
24
  jassubWorkerUrl={jassubWorkerUrl}
23
25
  jassubWasmUrl="/jassub-worker-modern.wasm"
26
+ jassubLegacyWasmUrl="/jassub-worker.wasm"
24
27
  defaultFontUrl="/default.woff2"
25
28
  title="episode.mkv"
26
29
  autoplay
@@ -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-CDdjRm_U.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-B8CKGIng.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 };
@@ -20,7 +20,12 @@ export type SubtitleRendererOptions = {
20
20
  * which throws. So leaving this unset does not degrade to the slower build, it fails outright.
21
21
  */
22
22
  legacyWasmUrl?: string;
23
- /** Fallback face for `liberation sans`. Without it jassub falls back to whatever the wasm build embeds. */
23
+ /**
24
+ * Fallback face for `liberation sans`. Without it jassub falls back to whatever the wasm build embeds.
25
+ *
26
+ * May be relative: it is resolved against the document before the worker sees it, for the reason
27
+ * given on `workerFetched`.
28
+ */
24
29
  defaultFontUrl?: string;
25
30
  onStreams?: (streams: SubtitleStream[]) => void;
26
31
  };
@@ -1,12 +1,12 @@
1
1
  import { makeRemuxer as e, makeThumbnailer as t } from "libav-wasm";
2
2
  import n from "jassub";
3
- import { parse as r, stringify as i } from "ass-compiler";
3
+ import { parse as r } from "ass-compiler";
4
4
  //#region src/lib/engine/source-buffer.ts
5
- var a = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
5
+ var i = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
6
6
  index: n,
7
7
  start: e.buffered.start(n),
8
8
  end: e.buffered.end(n)
9
- })), o = (e) => new Promise((t, n) => {
9
+ })), a = (e) => new Promise((t, n) => {
10
10
  let r = () => {
11
11
  e.removeEventListener("updateend", i), e.removeEventListener("abort", i), e.removeEventListener("error", a);
12
12
  }, i = () => {
@@ -15,14 +15,14 @@ var a = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
15
15
  r(), n(Error("The browser rejected a media segment", { cause: e }));
16
16
  };
17
17
  e.addEventListener("updateend", i, { once: !0 }), e.addEventListener("abort", i, { once: !0 }), e.addEventListener("error", a, { once: !0 });
18
- }), s = (e, t) => {
18
+ }), o = (e, t) => {
19
19
  let n = Promise.resolve(), r = (e) => {
20
20
  let t = n.then(e, e);
21
21
  return n = t.catch(() => {}), t;
22
22
  };
23
23
  return {
24
- appendBuffer: (t) => r(() => (e.appendBuffer(t), o(e))),
25
- unbufferRange: (t, n) => r(() => t >= n ? Promise.resolve() : (e.remove(t, n), o(e))),
24
+ appendBuffer: (t) => r(() => (e.appendBuffer(t), a(e))),
25
+ unbufferRange: (t, n) => r(() => t >= n ? Promise.resolve() : (e.remove(t, n), a(e))),
26
26
  updateTimestampOffset: (t) => r(async () => {
27
27
  e.timestampOffset = t;
28
28
  }),
@@ -32,55 +32,20 @@ var a = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
32
32
  } catch {}
33
33
  })
34
34
  };
35
- }, c = -1, l = (e) => new Date(e).toISOString().slice(11, 22), u = (e, t) => e.createStyle({
36
- ...t,
37
- treat_fontname_as_pattern: 0,
38
- Blur: 0,
39
- Justify: 0,
40
- FontName: t.Fontname,
41
- FontSize: Number(t.Fontsize),
42
- PrimaryColour: Number(t.PrimaryColour),
43
- SecondaryColour: Number(t.SecondaryColour),
44
- OutlineColour: Number(t.OutlineColour),
45
- BackColour: Number(t.BackColour),
46
- Bold: Number(t.Bold),
47
- Italic: Number(t.Italic),
48
- Underline: Number(t.Underline),
49
- StrikeOut: Number(t.StrikeOut),
50
- ScaleX: Number(t.ScaleX),
51
- ScaleY: Number(t.ScaleY),
52
- Spacing: Number(t.Spacing),
53
- Angle: Number(t.Angle),
54
- BorderStyle: Number(t.BorderStyle),
55
- Outline: Number(t.Outline),
56
- Shadow: Number(t.Shadow),
57
- Alignment: Number(t.Alignment),
58
- MarginL: Number(t.MarginL),
59
- MarginR: Number(t.MarginR),
60
- MarginV: Number(t.MarginV),
61
- Encoding: Number(t.Encoding)
62
- }), d = (e) => {
63
- let t = r(e);
64
- return i({
65
- ...t,
66
- info: {
67
- ...t.info,
68
- ScaledBorderAndShadow: "no",
69
- LayoutResX: "",
70
- LayoutResY: ""
71
- }
72
- });
73
- }, f = (e) => {
35
+ }, s = -1, c = (e, t) => e.createEvent(t), l = (e) => e === void 0 ? void 0 : new URL(e, document.baseURI).toString(), u = (e) => new Date(e).toISOString().slice(11, 22), d = 1, f = (e) => new Map(r(e).styles.style.map((e, t) => [e.Name, t + d])), p = (e, t) => {
36
+ let n = t.replace(/^\*+/, ""), r = n.toLowerCase() === "default" ? "Default" : n;
37
+ return e.styles.get(r) ?? e.styles.get("Default") ?? 0;
38
+ }, m = (e) => {
74
39
  let t = e.content.match(/\r?\n\[Events\]\r?\nFormat: (.*)/)?.[0];
75
40
  return t ? {
76
41
  type: "header",
77
42
  streamIndex: e.streamIndex,
78
43
  content: e.content,
79
44
  eventsContent: t,
80
- parsed: r(e.content)
45
+ styles: f(e.content)
81
46
  } : (console.warn(`subtitle stream ${e.streamIndex} has no Events format, ignoring the track`), null);
82
- }, p = (e, t) => {
83
- let [n, i] = t.content.split(","), a = Number(n), o = `Dialogue: ${i},${l(t.start)},${l(t.end)},${t.content.replace(`${a},${i},`, "")}`, s = r(`${e.eventsContent}\r\n${o}`).events.dialogue[0];
47
+ }, h = (e, t) => {
48
+ let [n, i] = t.content.split(","), a = Number(n), o = `Dialogue: ${i},${u(t.start)},${u(t.end)},${t.content.replace(`${a},${i},`, "")}`, s = r(`${e.eventsContent}\r\n${o}`).events.dialogue[0];
84
49
  if (!s) throw Error("dialogue event is undefined");
85
50
  return {
86
51
  type: "dialogue",
@@ -88,6 +53,7 @@ var a = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
88
53
  index: a,
89
54
  assEvent: {
90
55
  ...s,
56
+ Style: p(e, s.Style),
91
57
  Effect: s.Effect ?? "",
92
58
  Text: s.Text.raw,
93
59
  Duration: (s.End - s.Start) * 1e3,
@@ -97,285 +63,283 @@ var a = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
97
63
  _index: a
98
64
  }
99
65
  };
100
- }, m = (e) => {
101
- let { video: t, canvas: r, workerUrl: i, wasmUrl: a, legacyWasmUrl: o, defaultFontUrl: s } = e, c, l = [], m = /* @__PURE__ */ new Map(), h = [], g = /* @__PURE__ */ new Map(), _, v = e.onStreams, y = setInterval(() => {
102
- c?.setCurrentTime(t.paused, t.currentTime, t.playbackRate);
103
- }, 100), b = () => c?.setRate(t.playbackRate), x = (e) => {
104
- c = new n({
66
+ }, g = (e) => {
67
+ let { video: t, canvas: r, workerUrl: i } = e, a = l(e.wasmUrl), o = l(e.legacyWasmUrl), s = l(e.defaultFontUrl), u, d = [], f = /* @__PURE__ */ new Map(), p = [], g = /* @__PURE__ */ new Map(), _, v = e.onStreams, ee = setInterval(() => {
68
+ u?.setCurrentTime(t.paused, t.currentTime, t.playbackRate);
69
+ }, 100), y = () => u?.setRate(t.playbackRate), b = (e) => {
70
+ u = new n({
105
71
  onDemandRender: !1,
106
72
  video: t,
107
73
  canvas: r,
108
- subContent: d(e.content),
74
+ subContent: e.content,
109
75
  workerUrl: i,
110
76
  modernWasmUrl: a,
111
77
  ...o ? { wasmUrl: o } : {},
112
- fonts: l.map(([, e]) => e),
78
+ fonts: d.map(([, e]) => e),
113
79
  availableFonts: {
114
- ...Object.fromEntries(l),
80
+ ...Object.fromEntries(d),
115
81
  ...s ? { "liberation sans": s } : {}
116
82
  }
117
- }), t.removeEventListener("ratechange", c._boundSetRate), t.addEventListener("ratechange", b);
118
- for (let t of e.parsed.styles.style) u(c, t);
83
+ }), t.removeEventListener("ratechange", u._boundSetRate), t.addEventListener("ratechange", y);
119
84
  };
120
85
  return {
121
86
  pushAttachments: (e) => {
122
- l = [...l, ...e.map((e) => [e.filename, new Uint8Array(e.data)])];
87
+ d = [...d, ...e.map((e) => [e.filename, new Uint8Array(e.data)])];
123
88
  },
124
89
  pushFragments: (e) => {
125
90
  for (let t of e) if (t.type === "header") {
126
- if (m.has(t.streamIndex)) continue;
127
- let e = f(t);
91
+ if (f.has(t.streamIndex)) continue;
92
+ let e = m(t);
128
93
  if (!e) continue;
129
- m.set(t.streamIndex, e), h.push({
94
+ f.set(t.streamIndex, e), p.push({
130
95
  streamIndex: t.streamIndex,
131
96
  title: t.title,
132
97
  language: t.language
133
- }), v?.([...h]), _ === void 0 && (_ = t.streamIndex), c || x(e);
98
+ }), v?.([...p]), _ === void 0 && (_ = t.streamIndex), u || b(e);
134
99
  } else {
135
- let e = m.get(t.streamIndex);
100
+ let e = f.get(t.streamIndex);
136
101
  if (!e) continue;
137
102
  let n = g.get(t.streamIndex);
138
103
  n || (n = /* @__PURE__ */ new Map(), g.set(t.streamIndex, n));
139
- let r = p(e, t);
104
+ let r = h(e, t);
140
105
  if (n.has(r.index)) continue;
141
- n.set(r.index, r), _ === t.streamIndex && c?.createEvent(r.assEvent);
106
+ n.set(r.index, r), _ === t.streamIndex && u && c(u, r.assEvent);
142
107
  }
143
108
  },
144
109
  selectStream: (e) => {
145
110
  let n = e ?? -1;
146
- if (n === _ || !c) return;
147
- _ = n, c.freeTrack();
148
- let r = m.get(n);
111
+ if (n === _ || !u) return;
112
+ _ = n, u.freeTrack();
113
+ let r = f.get(n);
149
114
  if (r) {
150
- c.setTrack(d(r.content));
151
- for (let e of r.parsed.styles.style) u(c, e);
152
- for (let e of g.get(n)?.values() ?? []) c.createEvent(e.assEvent);
153
- c.setCurrentTime(t.paused, t.currentTime, t.playbackRate);
115
+ u.setTrack(r.content);
116
+ for (let e of g.get(n)?.values() ?? []) c(u, e.assEvent);
117
+ u.setCurrentTime(t.paused, t.currentTime, t.playbackRate);
154
118
  }
155
119
  },
156
- getStreams: () => [...h],
120
+ getStreams: () => [...p],
157
121
  getSelectedStream: () => _,
158
122
  setOnStreams: (e) => {
159
123
  v = e;
160
124
  },
161
125
  destroy: () => {
162
- clearInterval(y), t.removeEventListener("ratechange", b), c?.destroy(), c = void 0;
126
+ clearInterval(ee), t.removeEventListener("ratechange", y), u?.destroy(), u = void 0;
163
127
  }
164
128
  };
165
- }, h = class extends Error {
129
+ }, _ = class extends Error {
166
130
  mediaElement = !0;
167
131
  constructor(e, t) {
168
132
  super(e, t), this.name = "MediaElementError";
169
133
  }
170
- }, g = (e) => !!e && typeof e == "object" && e.mediaElement === !0, _ = -20, v = 60, y = 30, b = -5, x = 20, ee = 5, S = 15e3, C = 1, w = 3, te = 12, T = 200, E = 250, D = 25e5, O = (e) => {
134
+ }, v = (e) => !!e && typeof e == "object" && e.mediaElement === !0, ee = -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) => {
171
135
  let { worker: t } = e, n = setTimeout(() => t.terminate(), 2e3);
172
136
  e.destroy().catch(() => {}).finally(() => {
173
137
  clearTimeout(n), t.terminate();
174
138
  });
175
- }, k = async (t) => {
176
- let { videoElement: n, canvasElement: r, read: i, length: o, publicPath: c, libavWorkerUrl: l, jassubWorkerUrl: u, jassubWasmUrl: d, jassubLegacyWasmUrl: f, defaultFontUrl: p, bufferSize: g = D, audioStreamIndex: k, onReady: ne, onError: re, onRecovered: ie, onSeek: ae, onSubtitleStreams: oe, onAudioStreams: se } = t, A = await e({
139
+ }, te = async (t) => {
140
+ let { videoElement: n, canvasElement: r, read: a, length: s, publicPath: c, libavWorkerUrl: l, jassubWorkerUrl: u, jassubWasmUrl: d, jassubLegacyWasmUrl: f, defaultFontUrl: p, bufferSize: m = A, audioStreamIndex: h, onReady: v, onError: te, onRecovered: ne, onSeek: M, onSubtitleStreams: re, onAudioStreams: ie } = t, N = await e({
177
141
  publicPath: c,
178
142
  workerUrl: l,
179
143
  workerOptions: { type: "module" },
180
- bufferSize: g,
181
- length: o,
182
- audioStreamIndex: k,
183
- read: i
184
- }), j = [], M = !1, N = () => {
185
- if (!M) {
186
- M = !0;
187
- for (let e of [...j].reverse()) try {
144
+ bufferSize: m,
145
+ length: s,
146
+ audioStreamIndex: h,
147
+ read: a
148
+ }), P = [], F = !1, ae = () => {
149
+ if (!F) {
150
+ F = !0;
151
+ for (let e of [...P].reverse()) try {
188
152
  e();
189
153
  } catch {}
190
154
  }
191
155
  };
192
- j.push(() => O(A));
156
+ P.push(() => j(N));
193
157
  try {
194
- let e = await A.init(), t = e.audioStreams ?? [], i = t.some((e) => e.streamIndex === k) ? k : t[0]?.streamIndex ?? -1;
195
- se?.(t, i);
196
- let o = m({
158
+ let e = await N.init(), t = e.audioStreams ?? [], a = t.some((e) => e.streamIndex === h) ? h : t[0]?.streamIndex ?? -1;
159
+ ie?.(t, a);
160
+ let s = g({
197
161
  video: n,
198
162
  canvas: r,
199
163
  workerUrl: u,
200
164
  wasmUrl: d,
201
165
  legacyWasmUrl: f,
202
166
  defaultFontUrl: p,
203
- onStreams: oe
167
+ onStreams: re
204
168
  });
205
- if (j.push(() => o.destroy()), e.attachments?.length && o.pushAttachments(e.attachments), !e.info.output.videoMimeType) throw Error("The video codec in this file could not be identified");
169
+ if (P.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");
206
170
  let c = [e.info.output.videoMimeType, e.info.output.audioMimeType].filter(Boolean).join(","), l = `video/mp4; codecs="${c}"`;
207
171
  if (!MediaSource.isTypeSupported(l)) throw Error(`This browser cannot play the codecs in this file: ${c}`);
208
- let g = new MediaSource(), D = URL.createObjectURL(g);
209
- j.push(() => URL.revokeObjectURL(D)), n.src = D, j.push(() => {
172
+ let m = new MediaSource(), A = URL.createObjectURL(m);
173
+ P.push(() => URL.revokeObjectURL(A)), n.src = A, P.push(() => {
210
174
  try {
211
175
  n.pause();
212
176
  } catch {}
213
177
  n.removeAttribute("src"), n.load();
214
178
  });
215
- let O = await new Promise((t, n) => {
179
+ let j = await new Promise((t, n) => {
216
180
  let r = setTimeout(() => {
217
181
  i(), n(/* @__PURE__ */ Error("The MediaSource never opened"));
218
- }, S), i = () => {
219
- clearTimeout(r), g.removeEventListener("sourceopen", a), g.removeEventListener("sourceclose", o), g.removeEventListener("error", o);
182
+ }, w), i = () => {
183
+ clearTimeout(r), m.removeEventListener("sourceopen", a), m.removeEventListener("sourceclose", o), m.removeEventListener("error", o);
220
184
  }, a = () => {
221
185
  try {
222
- let n = g.addSourceBuffer(l);
186
+ let n = m.addSourceBuffer(l);
223
187
  n.mode = "segments";
224
188
  let r = e.info.input.duration;
225
- Number.isFinite(r) && r > 0 && (g.duration = r), i(), t(n);
189
+ Number.isFinite(r) && r > 0 && (m.duration = r), i(), t(n);
226
190
  } catch (e) {
227
191
  i(), n(e);
228
192
  }
229
193
  }, o = () => {
230
194
  i(), n(/* @__PURE__ */ Error("The MediaSource closed before a SourceBuffer could be created"));
231
195
  };
232
- g.addEventListener("sourceopen", a, { once: !0 }), g.addEventListener("sourceclose", o, { once: !0 }), g.addEventListener("error", o, { once: !0 });
233
- }), { appendBuffer: P, unbufferRange: ce, updateTimestampOffset: le, endOfStream: ue } = s(O, g);
234
- await P(e.data), e.subtitles?.length && o.pushFragments(e.subtitles), ne?.();
235
- let F = !1, I = !1, L = 0, R, de = () => R ?? n.currentTime, z = !1, B = 0, V = 0, H = null, U = 0, W = 0, G = !1, K = 0, q = (e, t, n = !1) => {
196
+ m.addEventListener("sourceopen", a, { once: !0 }), m.addEventListener("sourceclose", o, { once: !0 }), m.addEventListener("error", o, { once: !0 });
197
+ }), { appendBuffer: I, unbufferRange: oe, updateTimestampOffset: se, endOfStream: ce } = o(j, m);
198
+ await I(e.data), e.subtitles?.length && s.pushFragments(e.subtitles), v?.();
199
+ let L = !1, R = !1, z = 0, B, le = () => B ?? n.currentTime, V = !1, H = 0, U = 0, W = null, G = 0, K = 0, q = !1, ue = 0, J = (e, t, n = !1) => {
236
200
  let r = e?.message === "Cancelled";
237
- t && r || (console.error(e), !(G && !n) && (G = !0, re?.(r ? Error("Reading the video file failed", { cause: e }) : e)));
238
- }, fe = async (e = !1) => {
239
- if (L > 0 && !e) return;
240
- let t = n.currentTime, r = e ? b : _, i = e ? x : v;
241
- for (let { start: e, end: n } of a(O)) e < t + r && await ce(e, t + r), n > t + i && await ce(t + i, n);
242
- }, pe = () => {
243
- let e = a(O);
201
+ t && r || (console.error(e), !(q && !n) && (q = !0, te?.(r ? Error("Reading the video file failed", { cause: e }) : e)));
202
+ }, de = async (e = !1) => {
203
+ if (z > 0 && !e) return;
204
+ let t = n.currentTime, r = e ? x : ee, a = e ? S : y;
205
+ for (let { start: e, end: n } of i(j)) e < t + r && await oe(e, t + r), n > t + a && await oe(t + a, n);
206
+ }, fe = () => {
207
+ let e = i(j);
244
208
  if (!e.length) return !0;
245
- let t = de(), n = e.find((e) => e.start <= t + C && t < e.end);
246
- return n && n.end >= t + v ? !1 : !n || n.end < t + y;
247
- }, me = () => {
209
+ let t = le(), n = e.find((e) => e.start <= t + T && t < e.end);
210
+ return n && n.end >= t + y ? !1 : !n || n.end < t + b;
211
+ }, pe = () => {
248
212
  let e = n.currentTime;
249
- return a(O).find((t) => t.start <= e + C && e < t.end);
213
+ return i(j).find((t) => t.start <= e + T && e < t.end);
214
+ }, me = () => {
215
+ let e = pe();
216
+ return !!e && e.end >= K - .1;
250
217
  }, he = () => {
251
- let e = me();
252
- return !!e && e.end >= W - .1;
253
- }, ge = () => {
254
- let e = a(O).map((e) => e.end);
255
- e.length && (W = Math.max(W, ...e));
256
- }, J = async (e) => {
257
- I = !0, H = null, U = 0, K = e;
258
- let t = ++V;
218
+ let e = i(j).map((e) => e.end);
219
+ e.length && (K = Math.max(K, ...e));
220
+ }, Y = async (e) => {
221
+ R = !0, W = null, G = 0, ue = e;
222
+ let t = ++U;
259
223
  try {
260
- let { data: n, pts: r, subtitles: i, finished: a } = await A.seek(e);
261
- if (M || t !== V || (a && (z = !0), i?.length && o.pushFragments(i), await le(r), M || t !== V)) return;
262
- n.byteLength && (await P(n), ge());
224
+ let { data: n, pts: r, subtitles: i, finished: a } = await N.seek(e);
225
+ if (F || t !== U || (a && (V = !0), i?.length && s.pushFragments(i), await se(r), F || t !== U)) return;
226
+ n.byteLength && (await I(n), he());
263
227
  } catch (e) {
264
- q(e, M || t !== V);
228
+ J(e, F || t !== U);
265
229
  } finally {
266
- t === V && (I = !1);
230
+ t === U && (R = !1);
267
231
  }
268
- }, _e = async () => {
269
- let e = H;
232
+ }, ge = async () => {
233
+ let e = W;
270
234
  if (e) try {
271
- await P(e), ge(), H === e && (H = null, U = 0);
235
+ await I(e), he(), W === e && (W = null, G = 0);
272
236
  } catch (t) {
273
- if (H !== e) return;
237
+ if (W !== e) return;
274
238
  if (t?.name === "QuotaExceededError") {
275
- let t = await fe(!0).then(() => !0, () => !1);
276
- if (H !== e) return;
239
+ let t = await de(!0).then(() => !0, () => !1);
240
+ if (W !== e) return;
277
241
  if (t) {
278
- z = !1, await J(me()?.end ?? n.currentTime);
242
+ V = !1, await Y(pe()?.end ?? n.currentTime);
279
243
  return;
280
244
  }
281
245
  }
282
- if (U += 1, U < ee) return;
283
- throw H = null, t;
246
+ if (G += 1, G < C) return;
247
+ throw W = null, t;
284
248
  }
285
- }, ve = async () => {
286
- if (F || I || $ !== void 0 || M || !H && (z || !pe())) return;
287
- let e = ++B, t = V, n = () => M || e !== B || t !== V;
288
- F = !0;
249
+ }, _e = async () => {
250
+ if (L || R || $ !== void 0 || F || !W && (V || !fe())) return;
251
+ let e = ++H, t = U, n = () => F || e !== H || t !== U;
252
+ L = !0;
289
253
  try {
290
- if (!H) {
291
- let { data: e, subtitles: t, finished: r } = await A.read();
292
- if (n() || (r && (z = !0), t?.length && o.pushFragments(t), !e.byteLength)) return;
293
- H = e, U = 0;
254
+ if (!W) {
255
+ let { data: e, subtitles: t, finished: r } = await N.read();
256
+ if (n() || (r && (V = !0), t?.length && s.pushFragments(t), !e.byteLength)) return;
257
+ W = e, G = 0;
294
258
  }
295
- await _e(), !n() && G && (G = !1, ie?.());
259
+ await ge(), !n() && q && (q = !1, ne?.());
296
260
  } catch (e) {
297
- q(e, n());
261
+ J(e, n());
298
262
  } finally {
299
- e === B && (F = !1);
263
+ e === H && (L = !1);
300
264
  }
301
- }, ye = (e) => e >= K && !!me(), be = (e) => a(O).some((t) => t.start <= e + C && e < t.end), Y = (e) => {
302
- let t = a(O).find((t) => t.start <= e + C && e < t.end);
265
+ }, ve = (e) => e >= ue && !!pe(), ye = (e) => i(j).some((t) => t.start <= e + T && e < t.end), X = (e) => {
266
+ let t = i(j).find((t) => t.start <= e + T && e < t.end);
303
267
  return t ? t.end - e : 0;
304
- }, xe = async (e) => {
305
- if (!M && !(Y(e) >= w)) {
306
- z = !1, L++, R = e;
268
+ }, be = async (e) => {
269
+ if (!F && !(X(e) >= E)) {
270
+ V = !1, z++, B = e;
307
271
  try {
308
- be(e) || await J(e);
309
- let t = V;
310
- for (let n = 0; n < te && !(M || t !== V || z || Y(e) >= w); n++) {
311
- let t = Y(e);
312
- if (await ve(), Y(e) <= t) break;
272
+ ye(e) || await Y(e);
273
+ let t = U;
274
+ for (let n = 0; n < D && !(F || t !== U || V || X(e) >= E); n++) {
275
+ let t = X(e);
276
+ if (await _e(), X(e) <= t) break;
313
277
  }
314
278
  } finally {
315
- L--, R === e && (R = void 0);
279
+ z--, B === e && (B = void 0);
316
280
  }
317
281
  }
318
- }, X = 0, Z = null, Q, Se = (e) => {
319
- let t = performance.now(), n = t - X;
320
- if (n >= T) {
321
- X = t, Z = null, ae?.(e);
282
+ }, xe = 0, Z = null, Q, Se = (e) => {
283
+ let t = performance.now(), n = t - xe;
284
+ if (n >= O) {
285
+ xe = t, Z = null, M?.(e);
322
286
  return;
323
287
  }
324
288
  Z = e, !Q && (Q = setTimeout(() => {
325
289
  Q = void 0;
326
290
  let e = Z;
327
- Z = null, !(e === null || M) && (X = performance.now(), ae?.(e));
328
- }, T - n));
291
+ Z = null, !(e === null || F) && (xe = performance.now(), M?.(e));
292
+ }, O - n));
329
293
  };
330
- j.push(() => {
294
+ P.push(() => {
331
295
  Q && clearTimeout(Q);
332
296
  });
333
297
  let Ce = 0, $;
334
- j.push(() => {
298
+ P.push(() => {
335
299
  $ && clearTimeout($);
336
300
  });
337
301
  let we = () => {
338
302
  let t = n.currentTime, r = e.info.input.duration || n.duration;
339
303
  r > 0 && Se(Math.min(Math.max(t / r, 0), 1));
340
- let i = performance.now(), a = i - Ce < E;
341
- if (Ce = i, !ye(t) && !(I && Math.abs(t - K) < .001)) {
342
- if (z = !1, $ && clearTimeout($), !a) {
343
- $ = void 0, J(t);
304
+ let i = performance.now(), a = i - Ce < k;
305
+ if (Ce = i, !ve(t) && !(R && Math.abs(t - ue) < .001)) {
306
+ if (V = !1, $ && clearTimeout($), !a) {
307
+ $ = void 0, Y(t);
344
308
  return;
345
309
  }
346
310
  $ = setTimeout(() => {
347
- if ($ = void 0, M) return;
311
+ if ($ = void 0, F) return;
348
312
  let e = n.currentTime;
349
- ye(e) || (z = !1, J(e));
350
- }, E);
313
+ ve(e) || (V = !1, Y(e));
314
+ }, k);
351
315
  }
352
316
  };
353
- n.addEventListener("seeking", we), j.push(() => n.removeEventListener("seeking", we));
317
+ n.addEventListener("seeking", we), P.push(() => n.removeEventListener("seeking", we));
354
318
  let Te = !1, Ee = () => {
355
319
  if (Te) return;
356
320
  Te = !0;
357
321
  let e = n.error;
358
- q(new h(`the media element failed: ${e?.message ?? "unknown"}`, { cause: e }), !1, !0);
322
+ J(new _(`the media element failed: ${e?.message ?? "unknown"}`, { cause: e }), !1, !0);
359
323
  };
360
- n.addEventListener("error", Ee), j.push(() => n.removeEventListener("error", Ee));
324
+ n.addEventListener("error", Ee), P.push(() => n.removeEventListener("error", Ee));
361
325
  let De = setInterval(() => {
362
- Te || n.error || (fe().catch(() => {}), ve().catch((e) => q(e, !1)), z && !H && he() && g.readyState === "open" && ue().catch(() => {}));
326
+ Te || n.error || (de().catch(() => {}), _e().catch((e) => J(e, !1)), V && !W && me() && m.readyState === "open" && ce().catch(() => {}));
363
327
  }, 100);
364
- return j.push(() => clearInterval(De)), {
365
- destroy: N,
366
- prepareSeek: xe,
328
+ return P.push(() => clearInterval(De)), {
329
+ destroy: ae,
330
+ prepareSeek: be,
367
331
  videoElement: n,
368
- selectSubtitleStream: (e) => o.selectStream(e),
332
+ selectSubtitleStream: (e) => s.selectStream(e),
369
333
  indexes: e.indexes ?? [],
370
334
  duration: e.info.input.duration,
371
335
  videoMimeType: e.info.output.videoMimeType,
372
336
  audioMimeType: e.info.output.audioMimeType
373
337
  };
374
338
  } catch (e) {
375
- throw N(), e;
339
+ throw ae(), e;
376
340
  }
377
- }, ne = 5, re = 500, ie = 320, ae = 1e6, oe = 3, se = 1e4, A = async (e) => {
378
- let { publicPath: n, workerUrl: r, length: i, read: a, onThumbnails: o, width: s = ie } = e, c = await t({
341
+ }, ne = 5, M = 500, re = 320, ie = 1e6, N = 3, P = 1e4, F = async (e) => {
342
+ let { publicPath: n, workerUrl: r, length: i, read: a, onThumbnails: o, width: s = re } = e, c = await t({
379
343
  publicPath: n,
380
344
  workerUrl: r,
381
345
  workerOptions: { type: "module" },
@@ -383,14 +347,14 @@ var a = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
383
347
  read: a
384
348
  });
385
349
  try {
386
- let t = await c.init(), n = t.duration, r = Math.max(e.interval ?? ne, n / re), a = [];
350
+ let t = await c.init(), n = t.duration, r = Math.max(e.interval ?? ne, n / M), a = [];
387
351
  for (let [e, o] of t.indexes.entries()) {
388
352
  let s = a.at(-1);
389
353
  s && o.timestamp - s.timestamp < r || a.push({
390
354
  timestamp: o.timestamp,
391
355
  endTime: n,
392
356
  startByte: o.pos,
393
- endByte: Math.min((t.indexes[e + 1]?.pos ?? i) + ae, i),
357
+ endByte: Math.min((t.indexes[e + 1]?.pos ?? i) + ie, i),
394
358
  done: !1,
395
359
  attempts: 0
396
360
  });
@@ -421,7 +385,7 @@ var a = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
421
385
  }, p = (e) => {
422
386
  e.done = !0, d = d.then(async () => {
423
387
  if (u) return;
424
- let t = await Promise.race([c.readKeyframe(e.timestamp), new Promise((e, t) => setTimeout(() => t(/* @__PURE__ */ Error("timed out")), se))]), n = await createImageBitmap(new Blob([t], { type: "image/png" })), r = new OffscreenCanvas(s, Math.max(1, Math.round(n.height * (s / n.width))));
388
+ let t = await Promise.race([c.readKeyframe(e.timestamp), new Promise((e, t) => setTimeout(() => t(/* @__PURE__ */ Error("timed out")), P))]), n = await createImageBitmap(new Blob([t], { type: "image/png" })), r = new OffscreenCanvas(s, Math.max(1, Math.round(n.height * (s / n.width))));
425
389
  r.getContext("2d").drawImage(n, 0, 0, r.width, r.height), n.close();
426
390
  let i = await r.convertToBlob({
427
391
  type: "image/webp",
@@ -433,7 +397,7 @@ var a = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
433
397
  endTime: e.endTime
434
398
  }].sort((e, t) => e.startTime - t.startTime), f());
435
399
  }).catch(() => {
436
- e.attempts += 1, e.done = e.attempts >= oe;
400
+ e.attempts += 1, e.done = e.attempts >= N;
437
401
  });
438
402
  };
439
403
  return f(), {
@@ -443,14 +407,14 @@ var a = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
443
407
  destroy: () => {
444
408
  u = !0;
445
409
  for (let e of l) URL.revokeObjectURL(e.url);
446
- l = [], O(c);
410
+ l = [], j(c);
447
411
  }
448
412
  };
449
413
  } catch (e) {
450
- throw O(c), e;
414
+ throw j(c), e;
451
415
  }
452
- }, j = 1280, M = 1920, N = 250, P = 100, ce = () => typeof HTMLCanvasElement < "u" && "captureStream" in HTMLCanvasElement.prototype, le = () => typeof document < "u" && typeof HTMLVideoElement < "u" && typeof HTMLVideoElement.prototype.requestPictureInPicture == "function" && !!document.pictureInPictureEnabled, ue = () => typeof HTMLVideoElement < "u" && "mozCaptureStream" in HTMLVideoElement.prototype, F = () => ce() ? le() ? "window" : ue() ? "burn-in" : null : null, I = (e) => {
453
- let { video: t, canvas: n } = e, r = e.mode ?? F() ?? "window", i = e.maxWidth ?? (r === "burn-in" ? M : j), 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) => {
416
+ }, ae = 1280, I = 1920, oe = 250, se = 100, ce = () => 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, z = () => ce() ? L() ? "window" : R() ? "burn-in" : null : null, B = (e) => {
417
+ let { video: t, canvas: n } = e, r = e.mode ?? z() ?? "window", i = e.maxWidth ?? (r === "burn-in" ? I : ae), 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) => {
454
418
  r.drawImage(t, 0, 0, e.width, e.height), n.width > 0 && n.height > 0 && r.drawImage(n, 0, 0, e.width, e.height);
455
419
  }, h = () => {
456
420
  a && m(a.composite, a.context);
@@ -458,58 +422,58 @@ var a = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
458
422
  h(), v();
459
423
  }, v = () => {
460
424
  d || !a || (o = g ? t.requestVideoFrameCallback(_) : requestAnimationFrame(_));
461
- }, y = () => {
425
+ }, ee = () => {
462
426
  o !== void 0 && (g ? t.cancelVideoFrameCallback(o) : cancelAnimationFrame(o)), s !== void 0 && clearInterval(s), o = void 0, s = void 0;
463
- }, b = () => {
427
+ }, y = () => {
464
428
  "mediaSession" in navigator && (navigator.mediaSession.playbackState = t.paused ? "paused" : "playing");
465
- }, x = () => {
429
+ }, b = () => {
466
430
  c || l || !t.paused || t.play().catch(() => {});
467
- }, ee = () => {
431
+ }, x = () => {
468
432
  c || l || (t.paused || t.pause(), r !== "burn-in" && (c = !0, a?.mirror.play().catch(() => {}).finally(() => {
469
433
  c = !1;
470
434
  })));
471
435
  }, S = () => {
472
- b(), a?.mirror.play().catch(() => {});
436
+ y(), a?.mirror.play().catch(() => {});
473
437
  }, C = () => {
474
- b(), r === "burn-in" && a?.mirror.pause();
438
+ y(), r === "burn-in" && a?.mirror.pause();
475
439
  }, w = () => {
476
440
  if (r !== "burn-in" || !a || !t.paused || l) return;
477
441
  l = !0;
478
442
  let { mirror: e } = a;
479
443
  h(), e.play().then(() => new Promise((e) => {
480
- setTimeout(e, P);
444
+ setTimeout(e, se);
481
445
  })).catch(() => {}).finally(() => {
482
446
  e.pause(), setTimeout(() => {
483
447
  l = !1;
484
- }, P);
448
+ }, se);
485
449
  });
486
- }, te = [["play", () => {
450
+ }, T = [["play", () => {
487
451
  t.play().catch(() => {});
488
- }], ["pause", () => t.pause()]], T = (e) => {
489
- if ("mediaSession" in navigator) for (let [t, n] of te) try {
452
+ }], ["pause", () => t.pause()]], E = (e) => {
453
+ if ("mediaSession" in navigator) for (let [t, n] of T) try {
490
454
  navigator.mediaSession.setActionHandler(t, e ? n : null);
491
455
  } catch {}
492
- }, E = () => {
493
- if (y(), T(!1), t.removeEventListener("play", S), t.removeEventListener("pause", C), t.removeEventListener("seeked", w), !a) {
456
+ }, D = () => {
457
+ if (ee(), E(!1), t.removeEventListener("play", S), t.removeEventListener("pause", C), t.removeEventListener("seeked", w), !a) {
494
458
  f?.();
495
459
  return;
496
460
  }
497
461
  let { stream: e, mirror: n } = a;
498
- n.removeEventListener("play", x), n.removeEventListener("pause", ee), n.removeEventListener("leavepictureinpicture", D);
462
+ n.removeEventListener("play", b), n.removeEventListener("pause", x), n.removeEventListener("leavepictureinpicture", O);
499
463
  for (let t of e.getTracks()) t.stop();
500
464
  n.remove(), a = void 0, f?.();
501
- }, D = () => E(), O = (e) => {
465
+ }, O = () => D(), k = (e) => {
502
466
  let r = t.style.opacity, i = n.style.display;
503
467
  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 = () => {
504
468
  f = void 0, t.style.opacity = r, n.style.display = i;
505
469
  };
506
- }, k = async () => {
470
+ }, A = async () => {
507
471
  let n = Math.min(t.videoWidth || i, i), o = Math.round(n * ((t.videoHeight || 9) / (t.videoWidth || 16))), c = document.createElement("canvas");
508
472
  c.width = n, c.height = o;
509
473
  let l = c.getContext("2d");
510
474
  if (!l) throw Error("the picture in picture composite has no 2d context");
511
475
  let u = document.createElement("video");
512
- u.muted = !0, u.playsInline = !0, r === "burn-in" ? O(u) : u.style.cssText = "position:fixed;width:1px;height:1px;opacity:0;pointer-events:none;left:-1px;top:-1px", m(c, l);
476
+ 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);
513
477
  let f = {
514
478
  composite: c,
515
479
  context: l,
@@ -518,7 +482,7 @@ var a = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
518
482
  };
519
483
  if (a = f, u.srcObject = f.stream, (e.container ?? t.parentElement ?? document.body).appendChild(u), v(), s = setInterval(() => {
520
484
  t.paused && h();
521
- }, N), u.play().catch(() => {}), u.readyState === 0 && await new Promise((e) => {
485
+ }, oe), u.play().catch(() => {}), u.readyState === 0 && await new Promise((e) => {
522
486
  let t = () => {
523
487
  clearTimeout(n), e();
524
488
  }, n = setTimeout(t, 1e3);
@@ -527,7 +491,7 @@ var a = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
527
491
  u.remove();
528
492
  return;
529
493
  }
530
- if (u.addEventListener("play", x), u.addEventListener("pause", ee), u.addEventListener("leavepictureinpicture", D), t.addEventListener("play", S), t.addEventListener("pause", C), t.addEventListener("seeked", w), T(!0), b(), r === "burn-in") {
494
+ if (u.addEventListener("play", b), u.addEventListener("pause", x), u.addEventListener("leavepictureinpicture", O), t.addEventListener("play", S), t.addEventListener("pause", C), t.addEventListener("seeked", w), E(!0), y(), r === "burn-in") {
531
495
  t.paused && u.pause(), e.onBurnedInChange?.(!0);
532
496
  return;
533
497
  }
@@ -540,7 +504,7 @@ var a = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
540
504
  try {
541
505
  if (p()) {
542
506
  if (r === "burn-in") {
543
- E(), e.onBurnedInChange?.(!1);
507
+ D(), e.onBurnedInChange?.(!1);
544
508
  return;
545
509
  }
546
510
  await document.exitPictureInPicture().catch(() => {});
@@ -548,9 +512,9 @@ var a = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
548
512
  }
549
513
  r !== "burn-in" && document.pictureInPictureElement && await document.exitPictureInPicture().catch(() => {});
550
514
  try {
551
- await k();
515
+ await A();
552
516
  } catch (t) {
553
- console.warn("subtitle compositing for picture in picture failed", t), E(), r === "burn-in" && e.onBurnedInChange?.(!1);
517
+ console.warn("subtitle compositing for picture in picture failed", t), D(), r === "burn-in" && e.onBurnedInChange?.(!1);
554
518
  }
555
519
  } finally {
556
520
  u = !1;
@@ -559,9 +523,9 @@ var a = (e) => Array(e.buffered.length).fill(void 0).map((t, n) => ({
559
523
  },
560
524
  mode: r,
561
525
  destroy: () => {
562
- d = !0, r !== "burn-in" && p() && document.exitPictureInPicture().catch(() => {}), E(), r === "burn-in" && e.onBurnedInChange?.(!1);
526
+ d = !0, r !== "burn-in" && p() && document.exitPictureInPicture().catch(() => {}), D(), r === "burn-in" && e.onBurnedInChange?.(!1);
563
527
  }
564
528
  };
565
529
  };
566
530
  //#endregion
567
- export { h as a, O as c, a as d, s as f, D as i, c as l, F as n, g as o, A as r, k as s, I as t, m as u };
531
+ export { _ as a, j as c, i as d, o as f, A as i, s as l, z as n, v as o, F as r, te as s, B as t, g 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-CDdjRm_U.js";
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";
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@banou/media-player",
3
- "version": "0.8.15",
3
+ "version": "0.8.17",
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",
@@ -1,17 +1,25 @@
1
- import type { ParsedASS, ParsedASSStyles } from 'ass-compiler'
2
1
  import type { ASS_Event } from 'jassub'
3
2
  import type { Attachment, SubtitleFragment } from 'libav-wasm/build/worker'
4
3
 
5
4
  import JASSUB from 'jassub'
6
- import { parse, stringify } from 'ass-compiler'
5
+ import { parse } from 'ass-compiler'
7
6
 
8
7
  export type SubtitleStream = { streamIndex: number, title: string, language: string }
9
8
 
10
9
  /** -1 turns subtitles off. It frees the track and matches no header, so nothing is set. */
11
10
  export const SUBTITLES_OFF = -1
12
11
 
13
- type SubtitleHeaderPart = { type: 'header', streamIndex: number, content: string, eventsContent: string, parsed: ParsedASS }
14
- type SubtitleDialoguePart = { type: 'dialogue', streamIndex: number, index: number, assEvent: ASS_Event }
12
+ /**
13
+ * jassub's `ASS_Event` types `Style` as a string, which the library it wraps does not agree with:
14
+ * the field is written straight through to libass's `int Style`, an index into the track's style
15
+ * list. Corrected once, here, so a name cannot end up in it again.
16
+ */
17
+ type StyledEvent = Omit<ASS_Event, 'Style'> & { Style: number }
18
+
19
+ const createEvent = (jassub: JASSUB, event: StyledEvent) => jassub.createEvent(event as unknown as ASS_Event)
20
+
21
+ type SubtitleHeaderPart = { type: 'header', streamIndex: number, content: string, eventsContent: string, styles: Map<string, number> }
22
+ type SubtitleDialoguePart = { type: 'dialogue', streamIndex: number, index: number, assEvent: StyledEvent }
15
23
 
16
24
  export type SubtitleRendererOptions = {
17
25
  video: HTMLVideoElement
@@ -27,47 +35,61 @@ export type SubtitleRendererOptions = {
27
35
  * which throws. So leaving this unset does not degrade to the slower build, it fails outright.
28
36
  */
29
37
  legacyWasmUrl?: string
30
- /** Fallback face for `liberation sans`. Without it jassub falls back to whatever the wasm build embeds. */
38
+ /**
39
+ * Fallback face for `liberation sans`. Without it jassub falls back to whatever the wasm build embeds.
40
+ *
41
+ * May be relative: it is resolved against the document before the worker sees it, for the reason
42
+ * given on `workerFetched`.
43
+ */
31
44
  defaultFontUrl?: string
32
45
  onStreams?: (streams: SubtitleStream[]) => void
33
46
  }
34
47
 
48
+ /**
49
+ * A url the WORKER will fetch, resolved against the document first.
50
+ *
51
+ * jassub's worker is built from a `blob:` url, so a relative url handed to it resolves against that
52
+ * blob inside the worker and the fetch never lands. For the wasm that fails loudly. For the fallback
53
+ * font it fails in SILENCE: libass is left with no face to draw with, so the track renders nothing at
54
+ * all and the picture simply has no subtitles on it, with no error anywhere to say why.
55
+ *
56
+ * `workerUrl` is deliberately not passed through here. `new Worker()` is called on the main thread,
57
+ * where a relative url already resolves against the document.
58
+ */
59
+ const workerFetched = (url: string | undefined) =>
60
+ url === undefined ? undefined : new URL(url, document.baseURI).toString()
61
+
35
62
  const convertTimestamp = (ms: number) => new Date(ms).toISOString().slice(11, 22)
36
63
 
37
- const appendParsedStyle = (jassub: JASSUB, style: ParsedASSStyles['style'][number]) =>
38
- jassub.createStyle({
39
- ...style,
40
- treat_fontname_as_pattern: 0,
41
- Blur: 0,
42
- Justify: 0,
43
- FontName: style.Fontname,
44
- FontSize: Number(style.Fontsize),
45
- PrimaryColour: Number(style.PrimaryColour),
46
- SecondaryColour: Number(style.SecondaryColour),
47
- OutlineColour: Number(style.OutlineColour),
48
- BackColour: Number(style.BackColour),
49
- Bold: Number(style.Bold),
50
- Italic: Number(style.Italic),
51
- Underline: Number(style.Underline),
52
- StrikeOut: Number(style.StrikeOut),
53
- ScaleX: Number(style.ScaleX),
54
- ScaleY: Number(style.ScaleY),
55
- Spacing: Number(style.Spacing),
56
- Angle: Number(style.Angle),
57
- BorderStyle: Number(style.BorderStyle),
58
- Outline: Number(style.Outline),
59
- Shadow: Number(style.Shadow),
60
- Alignment: Number(style.Alignment),
61
- MarginL: Number(style.MarginL),
62
- MarginR: Number(style.MarginR),
63
- MarginV: Number(style.MarginV),
64
- Encoding: Number(style.Encoding),
65
- } as Parameters<JASSUB['createStyle']>[0])
66
-
67
- // cleared so jassub scales the script to the canvas, not to the authored resolution
68
- const renderable = (content: string) => {
69
- const parsed = parse(content)
70
- return stringify({ ...parsed, info: { ...parsed.info, ScaledBorderAndShadow: 'no', LayoutResX: '', LayoutResY: '' } })
64
+ /**
65
+ * How many styles libass keeps in front of the ones the file declares.
66
+ *
67
+ * `ass_new_track` always allocates its own "Default" at index 0 before parsing a line of the header,
68
+ * so the file's first style lands at 1. That number is what an event's `Style` field holds: libass
69
+ * reads it as an index into the track's style list and nothing else. `subtitle-scale.browser.test.tsx`
70
+ * is what pins this, by measuring the rendered text against what libass itself draws.
71
+ */
72
+ const LIBASS_OWN_STYLES = 1
73
+
74
+ const headerStyles = (content: string) =>
75
+ // a duplicated name resolves to the LAST one, the way libass's own lookup scans the list backwards
76
+ new Map(parse(content).styles.style.map((style, index) => [style.Name, index + LIBASS_OWN_STYLES]))
77
+
78
+ /**
79
+ * The index libass will resolve this event's style to.
80
+ *
81
+ * jassub types `ASS_Event.Style` as a string and writes it straight through to an `int`, so handing
82
+ * it a style NAME stores 0, which is libass's own default: Arial at size 18, with margins and a drop
83
+ * shadow of its own. Subtitles still appear, in a face and a size the file never asked for.
84
+ *
85
+ * Falling back to the header's own "Default", then to 0, is what libass does for a name it cannot
86
+ * find. The leading-`*` strip and the case fold on "Default" are its normalisation, kept so a
87
+ * `*Default` written by an older tool resolves here too.
88
+ */
89
+ const styleIndex = (header: SubtitleHeaderPart, name: string) => {
90
+ const stripped = name.replace(/^\*+/, '')
91
+ const key = stripped.toLowerCase() === 'default' ? 'Default' : stripped
92
+ return header.styles.get(key) ?? header.styles.get('Default') ?? 0
71
93
  }
72
94
 
73
95
  /**
@@ -84,7 +106,7 @@ const toHeaderPart = (fragment: SubtitleFragment & { type: 'header' }): Subtitle
84
106
  console.warn(`subtitle stream ${fragment.streamIndex} has no Events format, ignoring the track`)
85
107
  return null
86
108
  }
87
- return { type: 'header', streamIndex: fragment.streamIndex, content: fragment.content, eventsContent, parsed: parse(fragment.content) }
109
+ return { type: 'header', streamIndex: fragment.streamIndex, content: fragment.content, eventsContent, styles: headerStyles(fragment.content) }
88
110
  }
89
111
 
90
112
  const toDialoguePart = (header: SubtitleHeaderPart, fragment: SubtitleFragment & { type: 'dialogue' }): SubtitleDialoguePart => {
@@ -102,6 +124,7 @@ const toDialoguePart = (header: SubtitleHeaderPart, fragment: SubtitleFragment &
102
124
  index: dialogueIndex,
103
125
  assEvent: {
104
126
  ...event,
127
+ Style: styleIndex(header, event.Style),
105
128
  Effect: event.Effect ?? '',
106
129
  Text: event.Text.raw,
107
130
  Duration: (event.End - event.Start) * 1000,
@@ -109,14 +132,17 @@ const toDialoguePart = (header: SubtitleHeaderPart, fragment: SubtitleFragment &
109
132
  End: event.End * 1000,
110
133
  ReadOrder: dialogueIndex,
111
134
  _index: dialogueIndex,
112
- } as ASS_Event,
135
+ } as StyledEvent,
113
136
  }
114
137
  }
115
138
 
116
139
  export type SubtitleRenderer = ReturnType<typeof createSubtitleRenderer>
117
140
 
118
141
  export const createSubtitleRenderer = (options: SubtitleRendererOptions) => {
119
- const { video, canvas, workerUrl, wasmUrl, legacyWasmUrl, defaultFontUrl } = options
142
+ const { video, canvas, workerUrl } = options
143
+ const wasmUrl = workerFetched(options.wasmUrl)!
144
+ const legacyWasmUrl = workerFetched(options.legacyWasmUrl)
145
+ const defaultFontUrl = workerFetched(options.defaultFontUrl)
120
146
  let jassub: JASSUB | undefined
121
147
  let attachments: [string, Uint8Array][] = []
122
148
  const headers = new Map<number, SubtitleHeaderPart>()
@@ -136,7 +162,7 @@ export const createSubtitleRenderer = (options: SubtitleRendererOptions) => {
136
162
  onDemandRender: false,
137
163
  video,
138
164
  canvas,
139
- subContent: renderable(header.content),
165
+ subContent: header.content,
140
166
  workerUrl,
141
167
  modernWasmUrl: wasmUrl,
142
168
  ...legacyWasmUrl ? { wasmUrl: legacyWasmUrl } : {},
@@ -149,7 +175,6 @@ export const createSubtitleRenderer = (options: SubtitleRendererOptions) => {
149
175
  // jassub 1.8.x binds setRate as the ratechange listener, so the Event becomes the rate
150
176
  video.removeEventListener('ratechange', (jassub as unknown as { _boundSetRate: EventListener })._boundSetRate)
151
177
  video.addEventListener('ratechange', onRateChange)
152
- for (const style of header.parsed.styles.style) appendParsedStyle(jassub, style)
153
178
  }
154
179
 
155
180
  const pushAttachments = (incoming: Attachment[]) => {
@@ -175,7 +200,7 @@ export const createSubtitleRenderer = (options: SubtitleRendererOptions) => {
175
200
  const part = toDialoguePart(header, fragment)
176
201
  if (byIndex.has(part.index)) continue
177
202
  byIndex.set(part.index, part)
178
- if (selected === fragment.streamIndex) jassub?.createEvent(part.assEvent)
203
+ if (selected === fragment.streamIndex && jassub) createEvent(jassub, part.assEvent)
179
204
  }
180
205
  }
181
206
  }
@@ -187,9 +212,8 @@ export const createSubtitleRenderer = (options: SubtitleRendererOptions) => {
187
212
  jassub.freeTrack()
188
213
  const header = headers.get(next)
189
214
  if (!header) return
190
- jassub.setTrack(renderable(header.content))
191
- for (const style of header.parsed.styles.style) appendParsedStyle(jassub, style)
192
- for (const part of dialogues.get(next)?.values() ?? []) jassub.createEvent(part.assEvent)
215
+ jassub.setTrack(header.content)
216
+ for (const part of dialogues.get(next)?.values() ?? []) createEvent(jassub, part.assEvent)
193
217
  jassub.setCurrentTime(video.paused, video.currentTime, video.playbackRate)
194
218
  }
195
219