@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 +3 -0
- package/dist/engine/index.js +1 -1
- package/dist/engine/subtitles.d.ts +6 -1
- package/dist/{engine-CDdjRm_U.js → engine-B8CKGIng.js} +181 -217
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/lib/engine/subtitles.ts +72 -48
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
|
package/dist/engine/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, r as c, s as l, t as u, u as d } from "../engine-
|
|
1
|
+
import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, r as c, s as l, t as u, u as d } from "../engine-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
|
-
/**
|
|
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
|
|
3
|
+
import { parse as r } from "ass-compiler";
|
|
4
4
|
//#region src/lib/engine/source-buffer.ts
|
|
5
|
-
var
|
|
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
|
-
})),
|
|
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
|
-
}),
|
|
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),
|
|
25
|
-
unbufferRange: (t, n) => r(() => t >= n ? Promise.resolve() : (e.remove(t, n),
|
|
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
|
-
},
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
45
|
+
styles: f(e.content)
|
|
81
46
|
} : (console.warn(`subtitle stream ${e.streamIndex} has no Events format, ignoring the track`), null);
|
|
82
|
-
},
|
|
83
|
-
let [n, i] = t.content.split(","), a = Number(n), o = `Dialogue: ${i},${
|
|
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
|
-
},
|
|
101
|
-
let { video: t, canvas: r, workerUrl: i, wasmUrl
|
|
102
|
-
|
|
103
|
-
}, 100),
|
|
104
|
-
|
|
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:
|
|
74
|
+
subContent: e.content,
|
|
109
75
|
workerUrl: i,
|
|
110
76
|
modernWasmUrl: a,
|
|
111
77
|
...o ? { wasmUrl: o } : {},
|
|
112
|
-
fonts:
|
|
78
|
+
fonts: d.map(([, e]) => e),
|
|
113
79
|
availableFonts: {
|
|
114
|
-
...Object.fromEntries(
|
|
80
|
+
...Object.fromEntries(d),
|
|
115
81
|
...s ? { "liberation sans": s } : {}
|
|
116
82
|
}
|
|
117
|
-
}), t.removeEventListener("ratechange",
|
|
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
|
-
|
|
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 (
|
|
127
|
-
let e =
|
|
91
|
+
if (f.has(t.streamIndex)) continue;
|
|
92
|
+
let e = m(t);
|
|
128
93
|
if (!e) continue;
|
|
129
|
-
|
|
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?.([...
|
|
98
|
+
}), v?.([...p]), _ === void 0 && (_ = t.streamIndex), u || b(e);
|
|
134
99
|
} else {
|
|
135
|
-
let e =
|
|
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 =
|
|
104
|
+
let r = h(e, t);
|
|
140
105
|
if (n.has(r.index)) continue;
|
|
141
|
-
n.set(r.index, r), _ === t.streamIndex && c
|
|
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 === _ || !
|
|
147
|
-
_ = n,
|
|
148
|
-
let r =
|
|
111
|
+
if (n === _ || !u) return;
|
|
112
|
+
_ = n, u.freeTrack();
|
|
113
|
+
let r = f.get(n);
|
|
149
114
|
if (r) {
|
|
150
|
-
|
|
151
|
-
for (let e of
|
|
152
|
-
|
|
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: () => [...
|
|
120
|
+
getStreams: () => [...p],
|
|
157
121
|
getSelectedStream: () => _,
|
|
158
122
|
setOnStreams: (e) => {
|
|
159
123
|
v = e;
|
|
160
124
|
},
|
|
161
125
|
destroy: () => {
|
|
162
|
-
clearInterval(
|
|
126
|
+
clearInterval(ee), t.removeEventListener("ratechange", y), u?.destroy(), u = void 0;
|
|
163
127
|
}
|
|
164
128
|
};
|
|
165
|
-
},
|
|
129
|
+
}, _ = class extends Error {
|
|
166
130
|
mediaElement = !0;
|
|
167
131
|
constructor(e, t) {
|
|
168
132
|
super(e, t), this.name = "MediaElementError";
|
|
169
133
|
}
|
|
170
|
-
},
|
|
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
|
-
},
|
|
176
|
-
let { videoElement: n, canvasElement: r, read:
|
|
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:
|
|
181
|
-
length:
|
|
182
|
-
audioStreamIndex:
|
|
183
|
-
read:
|
|
184
|
-
}),
|
|
185
|
-
if (!
|
|
186
|
-
|
|
187
|
-
for (let e of [...
|
|
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
|
-
|
|
156
|
+
P.push(() => j(N));
|
|
193
157
|
try {
|
|
194
|
-
let e = await
|
|
195
|
-
|
|
196
|
-
let
|
|
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:
|
|
167
|
+
onStreams: re
|
|
204
168
|
});
|
|
205
|
-
if (
|
|
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
|
|
209
|
-
|
|
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
|
|
179
|
+
let j = await new Promise((t, n) => {
|
|
216
180
|
let r = setTimeout(() => {
|
|
217
181
|
i(), n(/* @__PURE__ */ Error("The MediaSource never opened"));
|
|
218
|
-
},
|
|
219
|
-
clearTimeout(r),
|
|
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 =
|
|
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 && (
|
|
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
|
-
|
|
233
|
-
}), { appendBuffer:
|
|
234
|
-
await
|
|
235
|
-
let
|
|
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), !(
|
|
238
|
-
},
|
|
239
|
-
if (
|
|
240
|
-
let t = n.currentTime, r = e ?
|
|
241
|
-
for (let { start: e, end: n } of
|
|
242
|
-
},
|
|
243
|
-
let e =
|
|
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 =
|
|
246
|
-
return n && n.end >= t +
|
|
247
|
-
},
|
|
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
|
|
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 =
|
|
252
|
-
|
|
253
|
-
},
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
261
|
-
if (
|
|
262
|
-
n.byteLength && (await
|
|
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
|
-
|
|
228
|
+
J(e, F || t !== U);
|
|
265
229
|
} finally {
|
|
266
|
-
t ===
|
|
230
|
+
t === U && (R = !1);
|
|
267
231
|
}
|
|
268
|
-
},
|
|
269
|
-
let e =
|
|
232
|
+
}, ge = async () => {
|
|
233
|
+
let e = W;
|
|
270
234
|
if (e) try {
|
|
271
|
-
await
|
|
235
|
+
await I(e), he(), W === e && (W = null, G = 0);
|
|
272
236
|
} catch (t) {
|
|
273
|
-
if (
|
|
237
|
+
if (W !== e) return;
|
|
274
238
|
if (t?.name === "QuotaExceededError") {
|
|
275
|
-
let t = await
|
|
276
|
-
if (
|
|
239
|
+
let t = await de(!0).then(() => !0, () => !1);
|
|
240
|
+
if (W !== e) return;
|
|
277
241
|
if (t) {
|
|
278
|
-
|
|
242
|
+
V = !1, await Y(pe()?.end ?? n.currentTime);
|
|
279
243
|
return;
|
|
280
244
|
}
|
|
281
245
|
}
|
|
282
|
-
if (
|
|
283
|
-
throw
|
|
246
|
+
if (G += 1, G < C) return;
|
|
247
|
+
throw W = null, t;
|
|
284
248
|
}
|
|
285
|
-
},
|
|
286
|
-
if (
|
|
287
|
-
let e = ++
|
|
288
|
-
|
|
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 (!
|
|
291
|
-
let { data: e, subtitles: t, finished: r } = await
|
|
292
|
-
if (n() || (r && (
|
|
293
|
-
|
|
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
|
|
259
|
+
await ge(), !n() && q && (q = !1, ne?.());
|
|
296
260
|
} catch (e) {
|
|
297
|
-
|
|
261
|
+
J(e, n());
|
|
298
262
|
} finally {
|
|
299
|
-
e ===
|
|
263
|
+
e === H && (L = !1);
|
|
300
264
|
}
|
|
301
|
-
},
|
|
302
|
-
let t =
|
|
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
|
-
},
|
|
305
|
-
if (!
|
|
306
|
-
|
|
268
|
+
}, be = async (e) => {
|
|
269
|
+
if (!F && !(X(e) >= E)) {
|
|
270
|
+
V = !1, z++, B = e;
|
|
307
271
|
try {
|
|
308
|
-
|
|
309
|
-
let t =
|
|
310
|
-
for (let n = 0; n <
|
|
311
|
-
let t =
|
|
312
|
-
if (await
|
|
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
|
-
|
|
279
|
+
z--, B === e && (B = void 0);
|
|
316
280
|
}
|
|
317
281
|
}
|
|
318
|
-
},
|
|
319
|
-
let t = performance.now(), n = t -
|
|
320
|
-
if (n >=
|
|
321
|
-
|
|
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 ||
|
|
328
|
-
},
|
|
291
|
+
Z = null, !(e === null || F) && (xe = performance.now(), M?.(e));
|
|
292
|
+
}, O - n));
|
|
329
293
|
};
|
|
330
|
-
|
|
294
|
+
P.push(() => {
|
|
331
295
|
Q && clearTimeout(Q);
|
|
332
296
|
});
|
|
333
297
|
let Ce = 0, $;
|
|
334
|
-
|
|
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 <
|
|
341
|
-
if (Ce = i, !
|
|
342
|
-
if (
|
|
343
|
-
$ = void 0,
|
|
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,
|
|
311
|
+
if ($ = void 0, F) return;
|
|
348
312
|
let e = n.currentTime;
|
|
349
|
-
|
|
350
|
-
},
|
|
313
|
+
ve(e) || (V = !1, Y(e));
|
|
314
|
+
}, k);
|
|
351
315
|
}
|
|
352
316
|
};
|
|
353
|
-
n.addEventListener("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
|
-
|
|
322
|
+
J(new _(`the media element failed: ${e?.message ?? "unknown"}`, { cause: e }), !1, !0);
|
|
359
323
|
};
|
|
360
|
-
n.addEventListener("error", Ee),
|
|
324
|
+
n.addEventListener("error", Ee), P.push(() => n.removeEventListener("error", Ee));
|
|
361
325
|
let De = setInterval(() => {
|
|
362
|
-
Te || n.error || (
|
|
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
|
|
365
|
-
destroy:
|
|
366
|
-
prepareSeek:
|
|
328
|
+
return P.push(() => clearInterval(De)), {
|
|
329
|
+
destroy: ae,
|
|
330
|
+
prepareSeek: be,
|
|
367
331
|
videoElement: n,
|
|
368
|
-
selectSubtitleStream: (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
|
|
339
|
+
throw ae(), e;
|
|
376
340
|
}
|
|
377
|
-
}, ne = 5,
|
|
378
|
-
let { publicPath: n, workerUrl: r, length: i, read: a, onThumbnails: o, width: s =
|
|
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 /
|
|
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) +
|
|
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")),
|
|
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 >=
|
|
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 = [],
|
|
410
|
+
l = [], j(c);
|
|
447
411
|
}
|
|
448
412
|
};
|
|
449
413
|
} catch (e) {
|
|
450
|
-
throw
|
|
414
|
+
throw j(c), e;
|
|
451
415
|
}
|
|
452
|
-
},
|
|
453
|
-
let { video: t, canvas: n } = e, r = e.mode ??
|
|
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
|
-
},
|
|
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
|
-
},
|
|
427
|
+
}, y = () => {
|
|
464
428
|
"mediaSession" in navigator && (navigator.mediaSession.playbackState = t.paused ? "paused" : "playing");
|
|
465
|
-
},
|
|
429
|
+
}, b = () => {
|
|
466
430
|
c || l || !t.paused || t.play().catch(() => {});
|
|
467
|
-
},
|
|
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
|
-
|
|
436
|
+
y(), a?.mirror.play().catch(() => {});
|
|
473
437
|
}, C = () => {
|
|
474
|
-
|
|
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,
|
|
444
|
+
setTimeout(e, se);
|
|
481
445
|
})).catch(() => {}).finally(() => {
|
|
482
446
|
e.pause(), setTimeout(() => {
|
|
483
447
|
l = !1;
|
|
484
|
-
},
|
|
448
|
+
}, se);
|
|
485
449
|
});
|
|
486
|
-
},
|
|
450
|
+
}, T = [["play", () => {
|
|
487
451
|
t.play().catch(() => {});
|
|
488
|
-
}], ["pause", () => t.pause()]],
|
|
489
|
-
if ("mediaSession" in navigator) for (let [t, n] of
|
|
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
|
-
},
|
|
493
|
-
if (
|
|
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",
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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" ?
|
|
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
|
-
},
|
|
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",
|
|
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
|
-
|
|
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
|
|
515
|
+
await A();
|
|
552
516
|
} catch (t) {
|
|
553
|
-
console.warn("subtitle compositing for picture in picture failed", t),
|
|
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(() => {}),
|
|
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 {
|
|
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-
|
|
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,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
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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,
|
|
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
|
|
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
|
|
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:
|
|
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)
|
|
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(
|
|
191
|
-
for (const
|
|
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
|
|