mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-22 05:11:20 -08:00
fix(jassub): [PKG-JASSUB-07] repair vendor lifecycle and subtitle frame clock
This commit is contained in:
1 parent
5e445b1af6
commit
0f3c068aae
29 files changed
+14064
-267
No files matched your search
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"task": "PKG-JASSUB-07",
|
||||
"baselineCommit": "5e445b1af655a1cafbfa3abca62d45207c042936",
|
||||
"file": "packages/artplayer-plugin-jassub/src/jassub.es.js",
|
||||
"originalSha256LF": "b973508ade00b5b9cea2f299d49f4943003372729be7350b6938e44fddb32038",
|
||||
"patchedSha256LF": "1fdcf24d206e8711348dddcb5fe7b656007a9b1d81a992bb497c6d10bd0a2cdb",
|
||||
"patchFile": "refactor/baselines/jassub-vendor.patch",
|
||||
"patchSha256LF": "fe6d429418a676f5d1b9c4daf9cd6a20be27c3a1b18f2a3d691a943188f7bfce",
|
||||
"origin": "Original wrapper matches frozen npm jassub 1.8.8 after format normalization. This is an explicit local patch, not a claim of unchanged upstream bytes.",
|
||||
"changes": [
|
||||
"constructor rollback and cancellation of late initialization",
|
||||
"idempotent destruction and ownership of actual canvas parent",
|
||||
"per-video generations and cancellation for render/color-space callbacks",
|
||||
"numeric fallback playbackRate adapter",
|
||||
"query settlement and cleanup before callbacks, including destroy cancellation",
|
||||
"per-video polyfill IDs/maps and zero-quality-counter media-time fallback capped at 30 Hz per continuous chain"
|
||||
],
|
||||
"unchanged": "Worker JS, WASM, fonts, resource URLs, root declarations and actual public instance identity remain unchanged. Original provenance manifests are not rewritten."
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,373 @@
|
||||
diff --git a/packages/artplayer-plugin-jassub/src/jassub.es.js b/packages/artplayer-plugin-jassub/src/jassub.es.js
|
||||
index f5a8f292c..904b40888 100644
|
||||
--- a/packages/artplayer-plugin-jassub/src/jassub.es.js
|
||||
+++ b/packages/artplayer-plugin-jassub/src/jassub.es.js
|
||||
@@ -1,25 +1,50 @@
|
||||
/* eslint-disable */
|
||||
-typeof HTMLVideoElement < "u" && !("requestVideoFrameCallback" in HTMLVideoElement.prototype) && "getVideoPlaybackQuality" in HTMLVideoElement.prototype && (HTMLVideoElement.prototype._rvfcpolyfillmap = {}, HTMLVideoElement.prototype.requestVideoFrameCallback = function (c) {
|
||||
- const e = performance.now(), t = this.getVideoPlaybackQuality(), s = this.mozPresentedFrames || this.mozPaintedFrames || t.totalVideoFrames - t.droppedVideoFrames, r = (n, a) => {
|
||||
- const i = this.getVideoPlaybackQuality(), h = this.mozPresentedFrames || this.mozPaintedFrames || i.totalVideoFrames - i.droppedVideoFrames;
|
||||
- if (h > s) {
|
||||
- const d = this.mozFrameDelay || i.totalFrameDelay - t.totalFrameDelay || 0, l = a - n;
|
||||
- c(a, {
|
||||
- presentationTime: a + d * 1e3,
|
||||
- expectedDisplayTime: a + l,
|
||||
- width: this.videoWidth,
|
||||
- height: this.videoHeight,
|
||||
- mediaTime: Math.max(0, this.currentTime || 0) + l / 1e3,
|
||||
- presentedFrames: h,
|
||||
- processingDuration: d
|
||||
- }), delete this._rvfcpolyfillmap[e];
|
||||
- } else
|
||||
- this._rvfcpolyfillmap[e] = requestAnimationFrame((d) => r(a, d));
|
||||
+if (typeof HTMLVideoElement < "u" && !("requestVideoFrameCallback" in HTMLVideoElement.prototype) && "getVideoPlaybackQuality" in HTMLVideoElement.prototype) {
|
||||
+ HTMLVideoElement.prototype.requestVideoFrameCallback = function (callback) {
|
||||
+ if (!Object.prototype.hasOwnProperty.call(this, "_rvfcpolyfillmap"))
|
||||
+ this._rvfcpolyfillmap = {};
|
||||
+ const id = this._rvfcpolyfillid = (this._rvfcpolyfillid || 0) + 1;
|
||||
+ const started = performance.now(), initial = this.getVideoPlaybackQuality(), initialTime = this.currentTime;
|
||||
+ const initialFrames = this.mozPresentedFrames || this.mozPaintedFrames || initial.totalVideoFrames - initial.droppedVideoFrames;
|
||||
+ const poll = (previous, now) => {
|
||||
+ if (!(id in this._rvfcpolyfillmap)) return;
|
||||
+ const quality = this.getVideoPlaybackQuality();
|
||||
+ const frames = this.mozPresentedFrames || this.mozPaintedFrames || quality.totalVideoFrames - quality.droppedVideoFrames;
|
||||
+ const advanced = frames > initialFrames;
|
||||
+ // Some WebKit backends expose quality counters but keep both at zero.
|
||||
+ // Approximate media-time delivery only there, capped at 30 Hz; never invent frame counts.
|
||||
+ const mediaFallback = initialFrames === 0 && frames === 0
|
||||
+ && initial.totalVideoFrames === 0 && initial.droppedVideoFrames === 0
|
||||
+ && quality.totalVideoFrames === 0 && quality.droppedVideoFrames === 0
|
||||
+ && Number.isFinite(this.currentTime) && this.currentTime !== initialTime
|
||||
+ && this.readyState >= 2 && !this.seeking && now - started >= 1000 / 30;
|
||||
+ if (advanced || mediaFallback) {
|
||||
+ const delay = this.mozFrameDelay || quality.totalFrameDelay - initial.totalFrameDelay || 0;
|
||||
+ const elapsed = now - previous;
|
||||
+ delete this._rvfcpolyfillmap[id];
|
||||
+ callback(now, {
|
||||
+ presentationTime: now + delay * 1e3,
|
||||
+ expectedDisplayTime: now + elapsed,
|
||||
+ width: this.videoWidth,
|
||||
+ height: this.videoHeight,
|
||||
+ mediaTime: mediaFallback && !advanced ? Math.max(0, this.currentTime) : Math.max(0, this.currentTime || 0) + elapsed / 1e3,
|
||||
+ presentedFrames: frames,
|
||||
+ processingDuration: delay
|
||||
+ });
|
||||
+ } else {
|
||||
+ this._rvfcpolyfillmap[id] = requestAnimationFrame((time) => poll(now, time));
|
||||
+ }
|
||||
+ };
|
||||
+ this._rvfcpolyfillmap[id] = requestAnimationFrame((time) => poll(started, time));
|
||||
+ return id;
|
||||
};
|
||||
- return this._rvfcpolyfillmap[e] = requestAnimationFrame((n) => r(e, n)), e;
|
||||
-}, HTMLVideoElement.prototype.cancelVideoFrameCallback = function (c) {
|
||||
- cancelAnimationFrame(this._rvfcpolyfillmap[c]), delete this._rvfcpolyfillmap[c];
|
||||
-});
|
||||
+ HTMLVideoElement.prototype.cancelVideoFrameCallback = function (id) {
|
||||
+ const callbacks = this._rvfcpolyfillmap;
|
||||
+ if (!callbacks || !(id in callbacks)) return;
|
||||
+ cancelAnimationFrame(callbacks[id]);
|
||||
+ delete callbacks[id];
|
||||
+ };
|
||||
+}
|
||||
const _ = {
|
||||
bt709: "BT709",
|
||||
// these might not be exactly correct? oops?
|
||||
@@ -74,7 +99,11 @@ class o extends EventTarget {
|
||||
* @param {Number} [options.libassGlyphLimit] libass glyph cache memory limit in MiB (approximate).
|
||||
*/
|
||||
constructor(e) {
|
||||
- if (super(), !globalThis.Worker)
|
||||
+ super();
|
||||
+ this._requests = new Set();
|
||||
+ let t;
|
||||
+ try {
|
||||
+ if (!globalThis.Worker)
|
||||
throw this.destroy("Worker not supported");
|
||||
if (!e)
|
||||
throw this.destroy("No options provided");
|
||||
@@ -82,14 +111,15 @@ class o extends EventTarget {
|
||||
new Promise((s) => {
|
||||
this._init = s;
|
||||
});
|
||||
- const t = o._test();
|
||||
+ t = o._test();
|
||||
if (this._onDemandRender = "requestVideoFrameCallback" in HTMLVideoElement.prototype && (e.onDemandRender ?? !0), this._offscreenRender = "transferControlToOffscreen" in HTMLCanvasElement.prototype && !e.canvas && (e.offscreenRender ?? !0), this.timeOffset = e.timeOffset || 0, this._video = e.video, this._videoHeight = 0, this._videoWidth = 0, this._videoColorSpace = null, this._canvas = e.canvas, this._video && !this._canvas)
|
||||
this._canvasParent = document.createElement("div"), this._canvasParent.className = "JASSUB", this._canvasParent.style.position = "relative", this._canvas = this._createCanvas(), this._video.insertAdjacentElement("afterend", this._canvasParent);
|
||||
else if (!this._canvas)
|
||||
throw this.destroy("Don't know where to render: you should give video or canvas in options.");
|
||||
if (this._bufferCanvas = document.createElement("canvas"), this._bufferCtx = this._bufferCanvas.getContext("2d"), !this._bufferCtx)
|
||||
throw this.destroy("Canvas rendering not supported");
|
||||
- this._canvasctrl = this._offscreenRender ? this._canvas.transferControlToOffscreen() : this._canvas, this._ctx = !this._offscreenRender && this._canvasctrl.getContext("2d"), this._lastRenderTime = 0, this.debug = !!e.debug, this.prescaleFactor = e.prescaleFactor || 1, this.prescaleHeightLimit = e.prescaleHeightLimit || 1080, this.maxRenderHeight = e.maxRenderHeight || 0, this._boundResize = this.resize.bind(this), this._boundTimeUpdate = this._timeupdate.bind(this), this._boundSetRate = this.setRate.bind(this), this._boundUpdateColorSpace = this._updateColorSpace.bind(this), this._video && this.setVideo(e.video), this._onDemandRender && (this.busy = !1, this._lastDemandTime = null), this._worker = new Worker(e.workerUrl || "jassub-worker.js"), this._worker.onmessage = (s) => this._onmessage(s), this._worker.onerror = (s) => this._error(s), t.then(() => {
|
||||
+ this._canvasctrl = this._offscreenRender ? this._canvas.transferControlToOffscreen() : this._canvas, this._ctx = !this._offscreenRender && this._canvasctrl.getContext("2d"), this._lastRenderTime = 0, this.debug = !!e.debug, this.prescaleFactor = e.prescaleFactor || 1, this.prescaleHeightLimit = e.prescaleHeightLimit || 1080, this.maxRenderHeight = e.maxRenderHeight || 0, this._boundResize = this.resize.bind(this), this._boundTimeUpdate = this._timeupdate.bind(this), this._boundSetRate = () => { if (!this._destroyed) this.setRate(this._video.playbackRate); }, this._boundUpdateColorSpace = this._updateColorSpace.bind(this), this._video && this.setVideo(e.video), this._onDemandRender && (this.busy = !1, this._lastDemandTime = null), this._worker = new Worker(e.workerUrl || "jassub-worker.js"), this._worker.onmessage = (s) => this._onmessage(s), this._worker.onerror = (s) => this._error(s), t.then(() => {
|
||||
+ if (this._destroyed) return;
|
||||
this._worker.postMessage({
|
||||
target: "init",
|
||||
wasmUrl: o._supportsSIMD && e.modernWasmUrl ? e.modernWasmUrl : e.wasmUrl ?? "jassub-worker.wasm",
|
||||
@@ -114,7 +144,14 @@ class o extends EventTarget {
|
||||
useLocalFonts: typeof queryLocalFonts < "u" && (e.useLocalFonts ?? !0),
|
||||
hasBitmapBug: o._hasBitmapBug
|
||||
}), this._offscreenRender === !0 && this.sendMessage("offscreenCanvas", null, [this._canvasctrl]);
|
||||
+ }).catch((error) => {
|
||||
+ if (!this._destroyed) this.destroy(error);
|
||||
});
|
||||
+ } catch (error) {
|
||||
+ t?.catch(() => {});
|
||||
+ try { this.destroy(); } catch {}
|
||||
+ throw error;
|
||||
+ }
|
||||
}
|
||||
_createCanvas() {
|
||||
return this._canvas = document.createElement("canvas"), this._canvas.style.display = "block", this._canvas.style.position = "absolute", this._canvas.style.pointerEvents = "none", this._canvasParent.appendChild(this._canvas), this._canvas;
|
||||
@@ -182,6 +219,7 @@ class o extends EventTarget {
|
||||
* @param {Boolean} [force=false]
|
||||
*/
|
||||
resize(e = 0, t = 0, s = 0, r = 0, n = this._video?.paused) {
|
||||
+ if (this._destroyed) return;
|
||||
if ((!e || !t) && this._video) {
|
||||
const a = this._getVideoPosition();
|
||||
let i = null;
|
||||
@@ -192,7 +230,7 @@ class o extends EventTarget {
|
||||
i = this._computeCanvasSize(a.width || 0, a.height || 0);
|
||||
e = i.width, t = i.height, this._canvasParent && (s = a.y - (this._canvasParent.getBoundingClientRect().top - this._video.getBoundingClientRect().top), r = a.x), this._canvas.style.width = a.width + "px", this._canvas.style.height = a.height + "px";
|
||||
}
|
||||
- this._canvas.style.top = s + "px", this._canvas.style.left = r + "px", n && this.busy === !1 ? this.busy = !0 : n = !1, this.sendMessage("canvas", { width: e, height: t, videoWidth: this._videoWidth || this._video.videoWidth, videoHeight: this._videoHeight || this._video.videoHeight, force: n });
|
||||
+ this._canvas.style.top = s + "px", this._canvas.style.left = r + "px", n && this.busy === !1 ? this.busy = !0 : n = !1, this.sendMessage("canvas", { width: e, height: t, videoWidth: this._videoWidth || this._video?.videoWidth || e, videoHeight: this._videoHeight || this._video?.videoHeight || t, force: n });
|
||||
}
|
||||
_getVideoPosition(e = this._video.videoWidth, t = this._video.videoHeight) {
|
||||
const s = e / t, { offsetWidth: r, offsetHeight: n } = this._video, a = r / n;
|
||||
@@ -212,6 +250,7 @@ class o extends EventTarget {
|
||||
return { width: e, height: t };
|
||||
}
|
||||
_timeupdate({ type: e }) {
|
||||
+ if (this._destroyed) return;
|
||||
const s = {
|
||||
seeking: !0,
|
||||
waiting: !0,
|
||||
@@ -224,7 +263,34 @@ class o extends EventTarget {
|
||||
* @param {HTMLVideoElement} video
|
||||
*/
|
||||
setVideo(e) {
|
||||
- e instanceof HTMLVideoElement ? (this._removeListeners(), this._video = e, this._onDemandRender ? this._video.requestVideoFrameCallback(this._handleRVFC.bind(this)) : (this._playstate = e.paused, e.addEventListener("timeupdate", this._boundTimeUpdate, !1), e.addEventListener("progress", this._boundTimeUpdate, !1), e.addEventListener("waiting", this._boundTimeUpdate, !1), e.addEventListener("seeking", this._boundTimeUpdate, !1), e.addEventListener("playing", this._boundTimeUpdate, !1), e.addEventListener("ratechange", this._boundSetRate, !1), e.addEventListener("resize", this._boundResize, !1)), "VideoFrame" in window && (e.addEventListener("loadedmetadata", this._boundUpdateColorSpace, !1), e.readyState > 2 && this._updateColorSpace()), e.videoWidth > 0 && this.resize(), typeof ResizeObserver < "u" && (this._ro || (this._ro = new ResizeObserver(() => this.resize())), this._ro.observe(e))) : this._error("Video element invalid!");
|
||||
+ if (this._destroyed) return;
|
||||
+ if (!(e instanceof HTMLVideoElement)) {
|
||||
+ this._error("Video element invalid!");
|
||||
+ return;
|
||||
+ }
|
||||
+ this._removeListeners();
|
||||
+ this._video = e;
|
||||
+ this._videoWidth = this._videoHeight = 0;
|
||||
+ this._videoColorSpace = null;
|
||||
+ if (this._canvasParent && this._canvasParent.parentNode !== e.parentNode)
|
||||
+ e.insertAdjacentElement("afterend", this._canvasParent);
|
||||
+ if (this._onDemandRender) this._requestFrame();
|
||||
+ else {
|
||||
+ this._playstate = e.paused;
|
||||
+ for (const name of ["timeupdate", "progress", "waiting", "seeking", "playing"])
|
||||
+ e.addEventListener(name, this._boundTimeUpdate, false);
|
||||
+ e.addEventListener("ratechange", this._boundSetRate, false);
|
||||
+ e.addEventListener("resize", this._boundResize, false);
|
||||
+ }
|
||||
+ if ("VideoFrame" in window) {
|
||||
+ e.addEventListener("loadedmetadata", this._boundUpdateColorSpace, false);
|
||||
+ if (e.readyState > 2) this._updateColorSpace();
|
||||
+ }
|
||||
+ if (e.videoWidth > 0) this.resize();
|
||||
+ if (typeof ResizeObserver < "u") {
|
||||
+ this._ro || (this._ro = new ResizeObserver(() => this.resize()));
|
||||
+ this._ro.observe(e);
|
||||
+ }
|
||||
}
|
||||
runBenchmark() {
|
||||
this.sendMessage("runBenchmark");
|
||||
@@ -316,7 +382,7 @@ class o extends EventTarget {
|
||||
getEvents(e) {
|
||||
this._fetchFromWorker({
|
||||
target: "getEvents"
|
||||
- }, (t, { events: s }) => {
|
||||
+ }, (t, { events: s } = {}) => {
|
||||
e(t, s);
|
||||
});
|
||||
}
|
||||
@@ -391,7 +457,7 @@ class o extends EventTarget {
|
||||
getStyles(e) {
|
||||
this._fetchFromWorker({
|
||||
target: "getStyles"
|
||||
- }, (t, { styles: s }) => {
|
||||
+ }, (t, { styles: s } = {}) => {
|
||||
e(t, s);
|
||||
});
|
||||
}
|
||||
@@ -433,12 +499,22 @@ class o extends EventTarget {
|
||||
}
|
||||
}
|
||||
_unbusy() {
|
||||
+ if (this._destroyed) return;
|
||||
this._lastDemandTime ? this._demandRender(this._lastDemandTime) : this.busy = !1;
|
||||
}
|
||||
_handleRVFC(e, { mediaTime: t, width: s, height: r }) {
|
||||
if (this._destroyed)
|
||||
return null;
|
||||
- this.busy ? this._lastDemandTime = { mediaTime: t, width: s, height: r } : (this.busy = !0, this._demandRender({ mediaTime: t, width: s, height: r })), this._video.requestVideoFrameCallback(this._handleRVFC.bind(this));
|
||||
+ this.busy ? this._lastDemandTime = { mediaTime: t, width: s, height: r } : (this.busy = !0, this._demandRender({ mediaTime: t, width: s, height: r })), this._requestFrame();
|
||||
+ }
|
||||
+ _requestFrame() {
|
||||
+ if (this._destroyed || !this._video || this._frameCallback != null) return;
|
||||
+ const video = this._video, generation = this._videoGeneration;
|
||||
+ this._frameCallback = video.requestVideoFrameCallback((time, metadata) => {
|
||||
+ if (this._destroyed || generation !== this._videoGeneration || video !== this._video) return;
|
||||
+ this._frameCallback = null;
|
||||
+ this._handleRVFC(time, metadata);
|
||||
+ });
|
||||
}
|
||||
_demandRender({ mediaTime: e, width: t, height: s }) {
|
||||
this._lastDemandTime = null, (t !== this._videoWidth || s !== this._videoHeight) && (this._videoWidth = t, this._videoHeight = s, this.resize()), this.sendMessage("demand", { time: e + this.timeOffset });
|
||||
@@ -456,12 +532,20 @@ class o extends EventTarget {
|
||||
this._canvas.remove(), this._createCanvas(), this._canvasctrl = this._canvas.transferControlToOffscreen(), this._ctx = !1, this.sendMessage("offscreenCanvas", null, [this._canvasctrl]), this.resize(0, 0, 0, 0, !0);
|
||||
}
|
||||
_updateColorSpace() {
|
||||
- this._video.requestVideoFrameCallback(() => {
|
||||
+ if (this._destroyed || this._colorCallback != null) return;
|
||||
+ const video = this._video, generation = this._videoGeneration;
|
||||
+ this._colorCallback = video.requestVideoFrameCallback(() => {
|
||||
+ if (this._destroyed || generation !== this._videoGeneration || video !== this._video) return;
|
||||
+ this._colorCallback = null;
|
||||
+ let frame;
|
||||
try {
|
||||
- const e = new VideoFrame(this._video);
|
||||
- this._videoColorSpace = _[e.colorSpace.matrix], e.close(), this.sendMessage("getColorSpace");
|
||||
+ frame = new VideoFrame(video);
|
||||
+ this._videoColorSpace = _[frame.colorSpace.matrix];
|
||||
+ this.sendMessage("getColorSpace");
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
+ } finally {
|
||||
+ frame?.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -495,6 +579,7 @@ class o extends EventTarget {
|
||||
return e;
|
||||
}
|
||||
_ready() {
|
||||
+ if (this._destroyed) return;
|
||||
this._init(), this.dispatchEvent(new CustomEvent("ready"));
|
||||
}
|
||||
/**
|
||||
@@ -504,7 +589,10 @@ class o extends EventTarget {
|
||||
* @param {Transferable[]} [transferable] Array of transferables.
|
||||
*/
|
||||
async sendMessage(e, t = {}, s) {
|
||||
- await this._loaded, s ? this._worker.postMessage({
|
||||
+ if (this._destroyed) return;
|
||||
+ await this._loaded;
|
||||
+ if (this._destroyed) return;
|
||||
+ s ? this._worker.postMessage({
|
||||
target: e,
|
||||
transferable: s,
|
||||
...t
|
||||
@@ -514,23 +602,51 @@ class o extends EventTarget {
|
||||
});
|
||||
}
|
||||
_fetchFromWorker(e, t) {
|
||||
+ if (this._destroyed) {
|
||||
+ t(new Error("JASSUB instance destroyed"));
|
||||
+ return;
|
||||
+ }
|
||||
+ const worker = this._worker;
|
||||
+ let timer, settled = false;
|
||||
+ const cleanup = () => {
|
||||
+ worker.removeEventListener("message", onMessage);
|
||||
+ worker.removeEventListener("error", onError);
|
||||
+ clearTimeout(timer);
|
||||
+ this._requests.delete(cancel);
|
||||
+ };
|
||||
+ const finish = (error, data) => {
|
||||
+ if (settled) return;
|
||||
+ settled = true;
|
||||
+ cleanup();
|
||||
+ t(error, data);
|
||||
+ };
|
||||
+ const onMessage = ({ data }) => { if (data.target === e.target) finish(null, data); };
|
||||
+ const onError = (error) => finish(error);
|
||||
+ const cancel = () => {
|
||||
+ if (settled) return;
|
||||
+ settled = true;
|
||||
+ cleanup();
|
||||
+ return () => t(new Error("JASSUB instance destroyed"));
|
||||
+ };
|
||||
try {
|
||||
- const s = e.target, r = setTimeout(() => {
|
||||
- a(new Error("Error: Timeout while try to fetch " + s));
|
||||
- }, 5e3), n = ({ data: i }) => {
|
||||
- i.target === s && (t(null, i), this._worker.removeEventListener("message", n), this._worker.removeEventListener("error", a), clearTimeout(r));
|
||||
- }, a = (i) => {
|
||||
- t(i), this._worker.removeEventListener("message", n), this._worker.removeEventListener("error", a), clearTimeout(r);
|
||||
- };
|
||||
- this._worker.addEventListener("message", n), this._worker.addEventListener("error", a), this._worker.postMessage(e);
|
||||
+ this._requests.add(cancel);
|
||||
+ worker.addEventListener("message", onMessage);
|
||||
+ worker.addEventListener("error", onError);
|
||||
+ timer = setTimeout(() => onError(new Error("Error: Timeout while try to fetch " + e.target)), 5e3);
|
||||
+ worker.postMessage(e);
|
||||
} catch (s) {
|
||||
- this._error(s);
|
||||
+ if (settled) throw s;
|
||||
+ finish(s);
|
||||
}
|
||||
}
|
||||
_console({ content: e, command: t }) {
|
||||
console[t].apply(console, JSON.parse(e));
|
||||
}
|
||||
_onmessage({ data: e }) {
|
||||
+ if (this._destroyed) {
|
||||
+ for (const image of e.images || []) image.image?.close?.();
|
||||
+ return;
|
||||
+ }
|
||||
this["_" + e.target] && this["_" + e.target](e);
|
||||
}
|
||||
_error(e) {
|
||||
@@ -538,6 +654,12 @@ class o extends EventTarget {
|
||||
return this.dispatchEvent(s), console.error(t), t;
|
||||
}
|
||||
_removeListeners() {
|
||||
+ this._videoGeneration = (this._videoGeneration || 0) + 1;
|
||||
+ for (const id of [this._frameCallback, this._colorCallback])
|
||||
+ if (id != null) this._video?.cancelVideoFrameCallback?.(id);
|
||||
+ this._frameCallback = this._colorCallback = null;
|
||||
+ this._lastDemandTime = null;
|
||||
+ if (this._onDemandRender) this.busy = false;
|
||||
this._video && (this._ro && this._ro.unobserve(this._video), this._ctx && (this._ctx.filter = "none"), this._video.removeEventListener("timeupdate", this._boundTimeUpdate), this._video.removeEventListener("progress", this._boundTimeUpdate), this._video.removeEventListener("waiting", this._boundTimeUpdate), this._video.removeEventListener("seeking", this._boundTimeUpdate), this._video.removeEventListener("playing", this._boundTimeUpdate), this._video.removeEventListener("ratechange", this._boundSetRate), this._video.removeEventListener("resize", this._boundResize), this._video.removeEventListener("loadedmetadata", this._boundUpdateColorSpace));
|
||||
}
|
||||
/**
|
||||
@@ -545,7 +667,29 @@ class o extends EventTarget {
|
||||
* @param {String|Error} [err] Error to throw when destroying.
|
||||
*/
|
||||
destroy(e) {
|
||||
- return e && (e = this._error(e)), this._video && this._canvasParent && this._video.parentNode?.removeChild(this._canvasParent), this._destroyed = !0, this._removeListeners(), this.sendMessage("destroy"), this._worker?.terminate(), e;
|
||||
+ if (this._destroyed) return e && this._error(e);
|
||||
+ this._destroyed = true;
|
||||
+ let failure, failed = false;
|
||||
+ const attempt = (callback) => {
|
||||
+ try { callback(); } catch (error) { if (!failed) { failure = error; failed = true; } }
|
||||
+ };
|
||||
+ if (e) attempt(() => { e = this._error(e); });
|
||||
+ attempt(() => this._removeListeners());
|
||||
+ attempt(() => this._ro?.disconnect());
|
||||
+ attempt(() => this._canvasParent?.remove());
|
||||
+ this._init?.();
|
||||
+ const callbacks = [];
|
||||
+ for (const cancel of [...this._requests || []]) attempt(() => {
|
||||
+ const notify = cancel();
|
||||
+ if (notify) callbacks.push(notify);
|
||||
+ });
|
||||
+ for (const notify of callbacks) attempt(notify);
|
||||
+ if (this._worker) {
|
||||
+ this._worker.onmessage = this._worker.onerror = null;
|
||||
+ attempt(() => this._worker.terminate());
|
||||
+ }
|
||||
+ if (failed) throw failure;
|
||||
+ return e;
|
||||
}
|
||||
}
|
||||
export {
|
||||
@@ -0,0 +1,115 @@
|
||||
# PKG-JASSUB-07 生命周期与字幕时钟修复
|
||||
|
||||
## 变更与兼容边界
|
||||
|
||||
本任务修改已登记的第三方 wrapper `src/jassub.es.js`,保留 vendor JS 例外,
|
||||
不把第三方生成代码机械迁为 TS。自有入口和 registration 继续由两个严格 TS
|
||||
模块负责。原 jassub 1.8.8 来源记录保留,新增可反向校验的
|
||||
[补丁](../baselines/jassub-vendor.patch)及[来源指纹](../baselines/jassub-vendor-patch.json),
|
||||
third-party.json 同时注明原始与本地修改后的身份,防止后续更新覆盖修复。
|
||||
Worker JS、WASM、字体、资源 URL 和旧根声明均未修改。没有新增依赖或锁文件变更。
|
||||
|
||||
公开工厂仍同步返回原生 JASSUB 实例;resize/setVideo/setTrack/destroy 的同步
|
||||
返回值、字幕协议和实例方法保留。本次修复 API-05/08/10/12 的已复现缺陷:
|
||||
|
||||
- 构造任一阶段抛错时,回收此前已创建的 DOM、监听器、帧回调和 Worker,重新
|
||||
抛出原始异常;异步能力检测不再向已销毁实例发送初始化消息。
|
||||
- destroy 首先进入终态,随后尽力完成所有清理,重复调用安全;删除实际归属的
|
||||
字幕容器,保留调用者 canvas/video。排队但尚未发送的消息取消,其 Promise
|
||||
正常结束,不再向已经 terminate 的 Worker 发消息。
|
||||
- setVideo 转移自建容器并释放旧视频订阅。render/color-space 回调分别记录归属
|
||||
和代次,即使旧回调已经排队,也不能重新启动旧循环或传入旧字幕时间。
|
||||
- 事件时钟的 ratechange 读取当前视频的数值 playbackRate,直接 setRate(number)
|
||||
保持原样,不再把 Event 对象发送给 Worker。
|
||||
- getEvents/getStyles 的 timeout/Worker error 缺少数据时不再先解构 undefined。
|
||||
每请求监听器与定时器在回调前释放,过期事件不重复通知。销毁先清理全部请求,
|
||||
再各通知一次取消错误;某个用户回调抛出异常仍完成其他清理和 Worker 终止,
|
||||
最后重新抛出第一个异常。正常查询仍返回真实 WASM 数据。
|
||||
|
||||
原生 CSP 错误在三个浏览器中均观察到 trusted plain Event,并不保证是 ErrorEvent。
|
||||
因此新增可选 `/runtime` 的准确回调类型使用 `Error | Event | null`;旧根声明完全
|
||||
保持,不要求旧入口消费者修改。此入口的迁移说明和 sendMessage 取消语义在
|
||||
包内 types/README.md 更新。Worker 加载错误沿已有 error 通道送达,调用者仍可
|
||||
显式销毁;本任务不承诺自动重试、全量错误恢复或新的查询请求 ID 协议。
|
||||
|
||||
## Windows WebKit 默认字幕停滞
|
||||
|
||||
冻结 npm 1.1.0 的真实播放中,currentTime 已到 6.49 秒且 readyState=4,
|
||||
totalVideoFrames/droppedVideoFrames 始终为零。浏览器没有原生 RVFC,旧 polyfill
|
||||
注册一次但不交付,Worker 仅产生空绘制,没有任何字幕 demand。
|
||||
|
||||
修复只针对 polyfill 中初始和当前质量计数均为零的情况:媒体时间必须有限且
|
||||
确实变化、readyState >= 2、没有正在 seek;按当前媒体时间交付近似回调,并限制
|
||||
每条连续注册链不超过 30 Hz。暂停后完成 seek 可交付,静止暂停不持续交付。
|
||||
presentedFrames 仍如实为零,不虚构解码帧。原生 RVFC 和正常增长的质量计数路径
|
||||
保留,后者不受该近似限速影响。独立视频的 map 和单调 ID 同时修复相同时间戳
|
||||
覆盖回调、跨视频取消和回调异常后未清理的问题。
|
||||
|
||||
这是有运行证据的局部兼容修复,不是全局强制 onDemandRender=false。实际默认
|
||||
onDemandRender=true 的播放、拖动、网页全屏、自定义 canvas 都已验证;所有这些
|
||||
绘制验证显式 offscreenRender=false,不能替代默认 offscreen、设备和 GPU 验收。
|
||||
|
||||
## 验证与中间失败
|
||||
|
||||
[验证记录](../baselines/jassub-vendor-validation.json)保留输入 hash、原始日志位置、
|
||||
安装报告和浏览器 JSON 摘要;本地 cache 不作为唯一永久证据。
|
||||
|
||||
| 层次 | 本次结果及范围 |
|
||||
| --- | --- |
|
||||
| 生命周期回归 | 同一 21 项在冻结旧产物全部失败,候选全部通过 |
|
||||
| Polyfill 回归 | 旧产物 13 通过/11 失败,候选 24 全通过;涵盖 144 Hz 模拟时钟、原生与正常计数不变、多视频和暂停 seek |
|
||||
| 联合测试 | 185 项通过,含旧行为基线、候选修复、真实 WASM 查询、类型、来源和可逆补丁校验 |
|
||||
| 源码与分发 | 源码、main、legacy 各 68 项通过;正常三格式构建及 docs 副本一致 |
|
||||
| 严格类型 | 全仓 406 个生产 TS 文件及既有消费者流程通过 |
|
||||
| 实际安装 | 15 个编译器/包矩阵通过,准确入口每模式 14 个负例、两种无 interop 消费通过;核心 68/JASSUB 15 个安装成员与最终工作区复核一致 |
|
||||
| 真实浏览器 | Windows Playwright Chromium 153.0.8010.12、Firefox 155、WebKit 26.6,main 默认 9、main 生命周期 12、legacy 21、main 自定义 canvas 9,共 51 项通过,无跳过或重试通过 |
|
||||
|
||||
浏览器播放组合包含候选核心、实际 5.4.0 和 5.3.1-beta.1;生命周期错误测试使用
|
||||
候选核心。原生 Worker/WASM/font/video 真实运行,验证字幕像素、切视频、数值倍率、
|
||||
重复销毁、非法 Worker URL 的同步 SyntaxError 回滚,以及 CSP 异步错误原对象
|
||||
向待处理查询交付一次。没有冒充 Chrome 扩展交互或物理 Safari/iOS/Android 验收。
|
||||
|
||||
保留三个测试编写过程中发现的问题及修正依据:
|
||||
|
||||
1. WebKit 首次播放前 seek 会回到零。fixture 先真实播放,再暂停/seek,随后仍检查
|
||||
播放进度和非透明字幕像素,未删除 WebKit 或放松成功条件。
|
||||
2. CSP 不是同步 SecurityError,回调对象也不保证 ErrorEvent。拆出实际非法 URL
|
||||
构造异常测试,并保留 CSP 异步错误及严格对象身份检查。
|
||||
3. 旧“同步返回值与 resize”测试在立即 destroy 后才检查排队消息。现在销毁前确认
|
||||
resize 尺寸、原同步返回值;额外旧红/新绿测试确认 ready 后排队消息在销毁时取消。
|
||||
初次候选 66/67 的失败日志保留,最终源码及两格式各 68 通过。
|
||||
|
||||
受控 DOM helper 的 appendChild 同步修正为先从旧父节点移除,与真实 DOM 移动
|
||||
语义一致;跨容器结论同时有真实浏览器验证。没有以 mock 取代真实播放。
|
||||
|
||||
main gzip 从 5,434 增至 6,045 字节(+611),legacy 从 5,716 增至 6,385(+669)。
|
||||
新增成本用于资源归属、清理和时钟回退;完整原始/gzip/Brotli 数值存于验证记录,
|
||||
不因此调整其他全项目性能门槛。安装验证复用 04 runner,报告保留原任务标识,
|
||||
07 包装记录明确其最终候选来源。
|
||||
|
||||
## 复跑、维护与后续
|
||||
|
||||
使用 .node-version 的 Node 和 Yarn Classic 1.22.22:
|
||||
|
||||
```sh
|
||||
yarn test:jassub
|
||||
yarn typecheck
|
||||
yarn test:jassub-types-package
|
||||
yarn build artplayer-plugin-jassub
|
||||
node --test refactor/scripts/jassub-vendor-patch.test.mjs
|
||||
```
|
||||
|
||||
test:unit 纳入生命周期/polyfill;test:jassub 还纳入补丁身份校验,baseline glob
|
||||
自动覆盖该来源测试。候选源码与 main/legacy 的环境变量用法见包内 ARCHITECTURE.md。
|
||||
浏览器设置 ARTPLAYER_JASSUB_ARTIFACT 为正常构建,再运行
|
||||
`yarn test:browser test/browser/jassub-native.spec.js test/browser/jassub-lifecycle.spec.js --workers=1`;
|
||||
自定义 canvas 另设 ARTPLAYER_JASSUB_CUSTOM_CANVAS=true。浏览器共享报告每次运行后先归档。
|
||||
|
||||
六项本任务风险已用对应证据关闭。05 继续完整组合、真实设备、默认 offscreen 和
|
||||
错误恢复;06 与 SITE-01 保留 VENDOR-04/05 的通知和字体分发门槛。公开类型/导出
|
||||
的更广泛准入风险不在本补丁任务一并关闭。仍需全部独立 major、CI/CD 与三轮发布
|
||||
复盘,不推送或发布。
|
||||
|
||||
回退本任务 commit 并正常构建即可恢复原 wrapper 与声明;先停止/销毁现有实例,
|
||||
不对运行中的 Worker 热替换。上游升级应重新核对原始来源、逐项移植或确认上游
|
||||
已修复,再运行红绿、类型、安装和原生验证,不能只替换文件并更新指纹。
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 232 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 63 / doing 14 / blocked 0 / done 155 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 62 / doing 14 / blocked 0 / done 156 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -235,7 +235,7 @@
|
||||
| PKG-JASSUB-04 | artplayer-plugin-jassub<br>迁移自有源码和公开类型 | PKG-JASSUB-03, ENG-04, ENG-06, CORE-07 | JASSUB option/instance 的兼容类型包装,vendor JS 例外记录 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done |
|
||||
| PKG-JASSUB-05 | artplayer-plugin-jassub<br>验证新旧核心和组合 | PKG-JASSUB-04, CORE-22, PKG-JASSUB-07 | 真实 worker/WASM 字幕渲染与全屏、旧核心测试 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
|
||||
| PKG-JASSUB-06 | artplayer-plugin-jassub<br>验证分发并同步文档 | PKG-JASSUB-05, ENG-07 | jassub.js、外部资源路径、许可和离线失败记录;接续 VENDOR-04/05 完整组件/字体通知、LGPL 源码分发核对及六份未明确字体的分发处置 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录;来源身份已由01冻结,完整通知和未明确字体的发布处理必须闭环,不能以来源相同替代许可结论 | H | todo |
|
||||
| PKG-JASSUB-07 | artplayer-plugin-jassub<br>修复 vendor 生命周期与字幕时钟 | PKG-JASSUB-03, PKG-JASSUB-02 | 独立记录 vendor 适配差异;幂等销毁、跨容器 setVideo、构造回滚、逐帧归属和倍率/能力边界;查询 timeout/Worker error 的缺失数据回调及每请求监听器/定时器清理(JASSUB-QUERY-01) | 保持公开真实 instance、同步方法与资源路径;保留原 vendor 来源指纹并记录每项实际补丁;候选修复正例和真实字幕/失败/时钟验证通过,未证实设备不得冒充通过 | H | todo |
|
||||
| PKG-JASSUB-07 | artplayer-plugin-jassub<br>修复 vendor 生命周期与字幕时钟 | PKG-JASSUB-03, PKG-JASSUB-02 | 独立记录 vendor 适配差异;幂等销毁、跨容器 setVideo、构造回滚、逐帧归属和倍率/能力边界;查询 timeout/Worker error 的缺失数据回调及每请求监听器/定时器清理(JASSUB-QUERY-01) | 保持公开真实 instance、同步方法与资源路径;保留原 vendor 来源指纹并记录每项实际补丁;候选修复正例和真实字幕/失败/时钟验证通过,未证实设备不得冒充通过 | H | done |
|
||||
|
||||
## 5 包迁移:artplayer-plugin-danmuku-mask
|
||||
|
||||
@@ -536,6 +536,7 @@
|
||||
- PKG-JASSUB-02: [记录](changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md) [记录](baselines/jassub-failures-validation.json)
|
||||
- PKG-JASSUB-03: [记录](changes/2026-09-14-PKG-JASSUB-03-registration.md) [记录](baselines/jassub-registration-validation.json)
|
||||
- PKG-JASSUB-04: [记录](changes/2026-09-14-PKG-JASSUB-04-types.md) [记录](baselines/jassub-types-validation.json)
|
||||
- PKG-JASSUB-07: [记录](changes/2026-09-14-PKG-JASSUB-07-vendor.md) [记录](baselines/jassub-vendor-validation.json) [记录](baselines/jassub-vendor-patch.json)
|
||||
- PKG-MASK-01: [记录](baselines/danmuku-mask-release.json) [记录](baselines/danmuku-mask-registry.json) [记录](baselines/danmuku-mask-contract.md) [记录](baselines/danmuku-mask-contract-validation.json) [记录](changes/2026-09-13-PKG-MASK-01-contract.md)
|
||||
- PKG-MASK-02: [记录](changes/2026-09-13-PKG-MASK-02-failures.md) [记录](baselines/danmuku-mask-failures-validation.json)
|
||||
- PKG-MASK-03: [记录](changes/2026-09-13-PKG-MASK-03-lifecycle.md) [记录](baselines/danmuku-mask-lifecycle-validation.json)
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-JASSUB-07 vendor 生命周期与默认字幕时钟完成
|
||||
|
||||
独立补丁保留原上游身份,修复幂等销毁、构造回滚、跨视频帧归属、数值倍率和
|
||||
查询异常清理;Windows WebKit 全零质量计数的默认逐帧停滞有旧失败及候选修复。
|
||||
联合 185 项、源码/main/legacy 各 68 项、406 生产 TS 检查通过;实际安装 15 个
|
||||
类型矩阵,核心 68/JASSUB 15 成员与候选相符。真实浏览器 51 项通过,包括默认
|
||||
onDemand 播放/seek、自定义 canvas、跨父节点换视频、原生构造与 CSP 错误。
|
||||
[变更与限制](changes/2026-09-14-PKG-JASSUB-07-vendor.md)和
|
||||
[验证记录](baselines/jassub-vendor-validation.json)保留测试时序/CSP/WebKit 的中间
|
||||
失败及修正,不冒充设备、默认 offscreen 或完整发布验收。关闭六项已证实风险,
|
||||
05/06、VENDOR-04/05 及全局三轮复盘仍保留。下一步继续分包兼容验收与未完成迁移。
|
||||
|
||||
## PKG-JASSUB-04 自有 TS 与准确类型入口完成
|
||||
|
||||
两个自有模块已迁为严格 TS,编译后的 JS 和三种正常构建产物保持上一任务行为。
|
||||
|
||||
@@ -242,10 +242,10 @@
|
||||
| DANMUKU-TIMING-01 | resolved / 已复现 | Hidden Worker wait is deducted from visible lifetime and can recycle a comment immediately after visible | PKG-DANMUKU-11 |
|
||||
| DANMUKU-SAMPLING-01 | resolved / 已复现 | Serial asynchronous placement stops native frame eligibility sampling and delays existing visible expiry | PKG-DANMUKU-12 |
|
||||
| MASK-SCHEDULING-01 | open / 已复现 | Native Mask startup can coincide with a missed narrow Danmuku timestamp window | PKG-MASK-05 |
|
||||
| JASSUB-DESTROY-01 | open / 已复现 | Repeated destruction or cross-parent setVideo can throw before JASSUB cleanup | PKG-JASSUB-02, PKG-JASSUB-03, PKG-JASSUB-07 |
|
||||
| JASSUB-DESTROY-01 | resolved / 已复现 | Repeated destruction or cross-parent setVideo can throw before JASSUB cleanup | PKG-JASSUB-02, PKG-JASSUB-03, PKG-JASSUB-07 |
|
||||
| JASSUB-CANVAS-01 | resolved / 已复现 | Custom canvas registration throws after JASSUB Worker allocation | PKG-JASSUB-02, PKG-JASSUB-03 |
|
||||
| JASSUB-CONSTRUCTION-01 | open / 已复现 | Worker construction failure leaves previously allocated JASSUB DOM and listeners | PKG-JASSUB-02, PKG-JASSUB-07 |
|
||||
| JASSUB-VIDEO-FRAME-01 | open / 已复现 | Old video frame callbacks can drive replacement subtitle time and duplicate chains | PKG-JASSUB-02, PKG-JASSUB-07 |
|
||||
| JASSUB-RATE-01 | open / 已复现 | Fallback ratechange sends an Event instead of numeric playbackRate | PKG-JASSUB-02, PKG-JASSUB-07 |
|
||||
| JASSUB-NATIVE-FRAME-01 | open / 已复现 | Windows WebKit default frame-clock path does not produce subtitle demands in actual playback | PKG-JASSUB-02, PKG-JASSUB-07, PKG-JASSUB-05 |
|
||||
| JASSUB-QUERY-01 | open / 已复现 | Subtitle query errors throw before calling the consumer and leave request listeners attached | PKG-JASSUB-07, PKG-JASSUB-05 |
|
||||
| JASSUB-CONSTRUCTION-01 | resolved / 已复现 | Worker construction failure leaves previously allocated JASSUB DOM and listeners | PKG-JASSUB-02, PKG-JASSUB-07 |
|
||||
| JASSUB-VIDEO-FRAME-01 | resolved / 已复现 | Old video frame callbacks can drive replacement subtitle time and duplicate chains | PKG-JASSUB-02, PKG-JASSUB-07 |
|
||||
| JASSUB-RATE-01 | resolved / 已复现 | Fallback ratechange sends an Event instead of numeric playbackRate | PKG-JASSUB-02, PKG-JASSUB-07 |
|
||||
| JASSUB-NATIVE-FRAME-01 | resolved / 已复现 | Windows WebKit default frame-clock path does not produce subtitle demands in actual playback | PKG-JASSUB-02, PKG-JASSUB-07, PKG-JASSUB-05 |
|
||||
| JASSUB-QUERY-01 | resolved / 已复现 | Subtitle query errors throw before calling the consumer and leave request listeners attached | PKG-JASSUB-07, PKG-JASSUB-05 |
|
||||
+60
-12
@@ -5430,7 +5430,7 @@
|
||||
"id": "JASSUB-DESTROY-01",
|
||||
"title": "Repeated destruction or cross-parent setVideo can throw before JASSUB cleanup",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-JASSUB-02",
|
||||
"PKG-JASSUB-03",
|
||||
@@ -5444,7 +5444,15 @@
|
||||
"test/jassub-registration.test.js"
|
||||
],
|
||||
"compatibleResolution": "Use actual resource ownership and tolerate direct instance teardown before host teardown. Task03 fixes host cleanup after direct teardown, reentry and retry/rollback. Direct vendor repetition and cross-parent cleanup remain task07 and this risk stays open.",
|
||||
"closureCriteria": "Candidate direct/repeated/host teardown and cross-parent video replacement release container, Worker and listeners without throwing or removing user nodes."
|
||||
"closureCriteria": "Candidate direct/repeated/host teardown and cross-parent video replacement release container, Worker and listeners without throwing or removing user nodes.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-07-vendor.md",
|
||||
"refactor/baselines/jassub-vendor-validation.json",
|
||||
"test/jassub-vendor-lifecycle.test.js",
|
||||
"test/jassub-frame-polyfill.test.js",
|
||||
"test/browser/jassub-lifecycle.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Controlled old-red/candidate-green repeated direct/host teardown and native cross-parent replacement release the actual container, video listeners and Worker once, preserving user nodes."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-CANVAS-01",
|
||||
@@ -5475,7 +5483,7 @@
|
||||
"id": "JASSUB-CONSTRUCTION-01",
|
||||
"title": "Worker construction failure leaves previously allocated JASSUB DOM and listeners",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-JASSUB-02",
|
||||
"PKG-JASSUB-07"
|
||||
@@ -5485,13 +5493,21 @@
|
||||
"refactor/baselines/jassub-failures-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Roll back each construction phase and rethrow the original error without assuming new returned an instance.",
|
||||
"closureCriteria": "Controlled and native construction failure do not retain owned DOM/listeners/callbacks; original error identity and successful construction remain intact."
|
||||
"closureCriteria": "Controlled and native construction failure do not retain owned DOM/listeners/callbacks; original error identity and successful construction remain intact.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-07-vendor.md",
|
||||
"refactor/baselines/jassub-vendor-validation.json",
|
||||
"test/jassub-vendor-lifecycle.test.js",
|
||||
"test/jassub-frame-polyfill.test.js",
|
||||
"test/browser/jassub-lifecycle.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Original controlled SecurityError identity survives rollback; actual invalid Worker URL produces native SyntaxError and leaves no owned DOM/subscriptions. Late initialization and application messages are cancelled after destruction."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-VIDEO-FRAME-01",
|
||||
"title": "Old video frame callbacks can drive replacement subtitle time and duplicate chains",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-JASSUB-02",
|
||||
"PKG-JASSUB-07"
|
||||
@@ -5501,13 +5517,21 @@
|
||||
"refactor/baselines/jassub-failures-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Track video generation and callback ownership across setVideo and destroy.",
|
||||
"closureCriteria": "Actually queued old callbacks cannot deliver stale mediaTime or rearm; replacement video owns one loop, with native evidence."
|
||||
"closureCriteria": "Actually queued old callbacks cannot deliver stale mediaTime or rearm; replacement video owns one loop, with native evidence.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-07-vendor.md",
|
||||
"refactor/baselines/jassub-vendor-validation.json",
|
||||
"test/jassub-vendor-lifecycle.test.js",
|
||||
"test/jassub-frame-polyfill.test.js",
|
||||
"test/browser/jassub-lifecycle.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Tests execute callbacks actually removed from a pending queue after setVideo/destroy and reject stale time or rearming. Separate real playback after video replacement renders subtitles and tears down successfully in three browser engines and both main/legacy."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-RATE-01",
|
||||
"title": "Fallback ratechange sends an Event instead of numeric playbackRate",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-JASSUB-02",
|
||||
"PKG-JASSUB-07"
|
||||
@@ -5517,13 +5541,21 @@
|
||||
"refactor/baselines/jassub-failures-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Adapt event subscription to read the current video rate while retaining direct setRate(number).",
|
||||
"closureCriteria": "Real ratechange and direct calls send numeric values, preserve timing, and avoid stale listeners after source replacement."
|
||||
"closureCriteria": "Real ratechange and direct calls send numeric values, preserve timing, and avoid stale listeners after source replacement.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-07-vendor.md",
|
||||
"refactor/baselines/jassub-vendor-validation.json",
|
||||
"test/jassub-vendor-lifecycle.test.js",
|
||||
"test/jassub-frame-polyfill.test.js",
|
||||
"test/browser/jassub-lifecycle.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Controlled and actual browser ratechange send the numeric current playbackRate; cross-video listener cleanup and the existing direct setRate contract remain covered."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-NATIVE-FRAME-01",
|
||||
"title": "Windows WebKit default frame-clock path does not produce subtitle demands in actual playback",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-JASSUB-02",
|
||||
"PKG-JASSUB-07",
|
||||
@@ -5534,13 +5566,21 @@
|
||||
"refactor/baselines/jassub-failures-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Investigate native frame delivery and establish a compatible capability response; explicit event-clock success is diagnostic only.",
|
||||
"closureCriteria": "Default native behavior is verified or an explicit reviewed environment limitation is recorded; no silent global fallback or substitution of diagnostic mode for default acceptance."
|
||||
"closureCriteria": "Default native behavior is verified or an explicit reviewed environment limitation is recorded; no silent global fallback or substitution of diagnostic mode for default acceptance.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-07-vendor.md",
|
||||
"refactor/baselines/jassub-vendor-validation.json",
|
||||
"test/jassub-vendor-lifecycle.test.js",
|
||||
"test/jassub-frame-polyfill.test.js",
|
||||
"test/browser/jassub-lifecycle.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Native Windows WebKit zero-counter starvation is reproduced on the published default clock. A narrow media-time fallback only in the polyfill with all-zero quality counters restores default onDemand playback and seek; candidate default and custom-canvas pixel assertions pass. Native RVFC and normally increasing counters retain their paths. Default offscreen and physical devices remain task05."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-QUERY-01",
|
||||
"title": "Subtitle query errors throw before calling the consumer and leave request listeners attached",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-JASSUB-07",
|
||||
"PKG-JASSUB-05"
|
||||
@@ -5550,7 +5590,15 @@
|
||||
"test/jassub-runtime.test.js"
|
||||
],
|
||||
"compatibleResolution": "Handle absent worker error payloads, release per-request listeners and timers before invoking user callbacks, and preserve successful synchronous query registration and callback data.",
|
||||
"closureCriteria": "Actual vendor timeout and Worker-error paths invoke the consumer exactly once with the original error, leave no request listeners/timers, and successful real-WASM events/styles remain unchanged. Browser error/recovery acceptance remains PKG-JASSUB-05."
|
||||
"closureCriteria": "Actual vendor timeout and Worker-error paths invoke the consumer exactly once with the original error, leave no request listeners/timers, and successful real-WASM events/styles remain unchanged. Browser error/recovery acceptance remains PKG-JASSUB-05.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-07-vendor.md",
|
||||
"refactor/baselines/jassub-vendor-validation.json",
|
||||
"test/jassub-vendor-lifecycle.test.js",
|
||||
"test/jassub-frame-polyfill.test.js",
|
||||
"test/browser/jassub-lifecycle.spec.js"
|
||||
],
|
||||
"resolutionRationale": "Actual wrapper timeout and Worker error invoke once with the original error and clean all request listeners/timers first, including throwing user callbacks. Native CSP plain Event identity is preserved; real WASM queries still succeed. Destroy cancellation settles all pending queries after cleanup; complete error recovery remains task05."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Vendor patch provenance is part of the baseline runner.
|
||||
import test from 'node:test'
|
||||
import { hash } from './releases.mjs'
|
||||
|
||||
test('JASSUB local vendor patch retains its exact original, changed source and reversible patch identity', () => {
|
||||
const record = JSON.parse(fs.readFileSync('refactor/baselines/jassub-vendor-patch.json', 'utf8'))
|
||||
const original = execFileSync('git', ['show', `${record.baselineCommit}:${record.file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
|
||||
const current = fs.readFileSync(record.file, 'utf8').replaceAll('\r\n', '\n')
|
||||
const patch = fs.readFileSync(record.patchFile, 'utf8').replaceAll('\r\n', '\n')
|
||||
assert.equal(hash(original), record.originalSha256LF)
|
||||
assert.equal(hash(current), record.patchedSha256LF)
|
||||
assert.equal(hash(patch), record.patchSha256LF)
|
||||
assert.notEqual(record.originalSha256LF, record.patchedSha256LF)
|
||||
execFileSync('git', ['apply', '--check', '--reverse', '--ignore-space-change', record.patchFile], { stdio: 'pipe' })
|
||||
const registry = JSON.parse(fs.readFileSync('refactor/third-party.json', 'utf8'))
|
||||
const group = registry.vendored.find(item => item.id === 'jassub-code-and-workers')
|
||||
assert.equal(group.localPatch.originalSha256LF, record.originalSha256LF)
|
||||
assert.equal(group.fingerprints.find(item => item.path === record.file).sha256, record.patchedSha256LF)
|
||||
})
|
||||
+6
-2
@@ -2532,11 +2532,15 @@
|
||||
"PKG-JASSUB-03",
|
||||
"PKG-JASSUB-02"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "独立记录 vendor 适配差异;幂等销毁、跨容器 setVideo、构造回滚、逐帧归属和倍率/能力边界;查询 timeout/Worker error 的缺失数据回调及每请求监听器/定时器清理(JASSUB-QUERY-01)",
|
||||
"acceptance": "保持公开真实 instance、同步方法与资源路径;保留原 vendor 来源指纹并记录每项实际补丁;候选修复正例和真实字幕/失败/时钟验证通过,未证实设备不得冒充通过",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-14-PKG-JASSUB-07-vendor.md",
|
||||
"baselines/jassub-vendor-validation.json",
|
||||
"baselines/jassub-vendor-patch.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-MASK-01",
|
||||
|
||||
@@ -353,9 +353,10 @@
|
||||
],
|
||||
"owners": [
|
||||
"PKG-JASSUB-01",
|
||||
"PKG-JASSUB-06"
|
||||
"PKG-JASSUB-06",
|
||||
"PKG-JASSUB-07"
|
||||
],
|
||||
"sourceStatus": "Wrapper/worker JS match npm 1.8.8 as previously frozen. Both local WASM binaries exactly match Pages nightly 747e392d7f6fe7c38bbcdfe05c9ada46b0b62daf linked to source 6b19a04ddfbad8f9bfd3237395788dd76218841b, with pinned build files and seven submodules. No independent rebuild. See baselines/jassub-provenance.json.",
|
||||
"sourceStatus": "Original wrapper matched npm 1.8.8; PKG-JASSUB-07 now carries an explicitly recorded local lifecycle/clock/query patch (baselines/jassub-vendor-patch.json). Worker JS is unchanged. Both WASM binaries still exactly match Pages nightly 747e392d7f6fe7c38bbcdfe05c9ada46b0b62daf/source 6b19a04ddfbad8f9bfd3237395788dd76218841b. Original wrapper hash, unchanged binaries/fonts, pinned build and submodules remain separately recorded; no independent WASM rebuild.",
|
||||
"licenseStatus": "Matching source LICENSE and nightly COPYRIGHT identified and byte-verified; full component notice assembly, LGPL source/distribution review and full FreeType license remain PKG-JASSUB-06 gates. VENDOR-04 remains open.",
|
||||
"upstreamSources": [
|
||||
"https://github.com/ThaUnknown/jassub/blob/main/LICENSE",
|
||||
@@ -385,7 +386,7 @@
|
||||
{
|
||||
"path": "packages/artplayer-plugin-jassub/src/jassub.es.js",
|
||||
"algorithm": "sha256-lf",
|
||||
"sha256": "b973508ade00b5b9cea2f299d49f4943003372729be7350b6938e44fddb32038"
|
||||
"sha256": "1fdcf24d206e8711348dddcb5fe7b656007a9b1d81a992bb497c6d10bd0a2cdb"
|
||||
},
|
||||
{
|
||||
"path": "packages/artplayer-plugin-jassub/worker/jassub-worker-modern.wasm",
|
||||
@@ -402,7 +403,13 @@
|
||||
"algorithm": "sha256",
|
||||
"sha256": "73880d99ba9260ac39fc4476491cf254ca784c76f3af2fb03f3f9be8065ad3c0"
|
||||
}
|
||||
]
|
||||
],
|
||||
"localPatch": {
|
||||
"task": "PKG-JASSUB-07",
|
||||
"record": "refactor/baselines/jassub-vendor-patch.json",
|
||||
"originalSha256LF": "b973508ade00b5b9cea2f299d49f4943003372729be7350b6938e44fddb32038",
|
||||
"patchedSha256LF": "1fdcf24d206e8711348dddcb5fe7b656007a9b1d81a992bb497c6d10bd0a2cdb"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "jassub-font-assets",
|
||||
|
||||
Reference in new issue
Block a user