diff --git a/package.json b/package.json
index 36fc168e3..d5ff76b98 100644
--- a/package.json
+++ b/package.json
@@ -90,7 +90,7 @@
"test:vtt-thumbnail-types-package": "node refactor/scripts/vtt-thumbnail-package-types.mjs",
"test:multiple-subtitles": "node --test test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js refactor/scripts/multiple-subtitles-contract.test.mjs refactor/scripts/multiple-subtitles-runtime-types.test.mjs refactor/scripts/multiple-subtitles-types.test.mjs",
"test:multiple-subtitles-types-package": "node refactor/scripts/multiple-subtitles-package-types.mjs",
- "test:jassub": "node --test test/jassub.test.js refactor/scripts/jassub-contract.test.mjs refactor/scripts/jassub-provenance.test.mjs",
+ "test:jassub": "node --test test/jassub.test.js refactor/scripts/jassub-contract.test.mjs refactor/scripts/jassub-provenance.test.mjs refactor/scripts/jassub-failures.test.mjs",
"check:ci": "node refactor/scripts/ci-workflow.mjs",
"test:ci": "node --test test/ci-summary.test.js refactor/scripts/ci-workflow.test.mjs refactor/scripts/impact.test.mjs test/package-runtime.test.js",
"test:package:runtime": "node scripts/package-runtime.mjs --canonical",
diff --git a/packages/artplayer-plugin-jassub/ARCHITECTURE.md b/packages/artplayer-plugin-jassub/ARCHITECTURE.md
index b48aa5089..5ec8746cf 100644
--- a/packages/artplayer-plugin-jassub/ARCHITECTURE.md
+++ b/packages/artplayer-plugin-jassub/ARCHITECTURE.md
@@ -42,5 +42,16 @@ asset comparisons and fails on any request error, without substituting cached ev
Set ARTPLAYER_JASSUB_CANDIDATE=1 for current source behavior; ARTPLAYER_JASSUB_ARTIFACT can point
at a main/legacy artifact. Tests use controlled DOM, Worker and SIMD detection, with actual
vendor JavaScript. They do not render ASS or execute worker WASM. Real browser, failures,
-repeated destroy and resource cleanup remain later steps. See
+repeated destroy and resource cleanup remain later steps. Frozen failure tests now
+cover repeated teardown, cross-parent setVideo, custom canvas, Worker construction
+failure, stale video frames and incorrect fallback ratechange payloads. They assert
+the old failures, not candidate fixes. PKG-JASSUB-03 owns adapter cleanup; 07 owns
+separately tracked vendor lifecycle/clock changes and 05 verifies native behavior.
+
+`yarn test:browser test/browser/jassub-native.spec.js --workers=1` loads the actual
+published wrapper, Worker, WASM and default font with offscreenRender=false.
+Chromium/Firefox render and seek correctly; Windows WebKit's default frame-clock
+path remains a recorded failure. The onDemandRender=false diagnostic does not
+waive the default behavior gate. Native failure recovery, default offscreen mode,
+full devices and sustained resource release remain unverified. See
`../../refactor/baselines/jassub-contract.md` for precise contract and provenance evidence.
diff --git a/refactor/baselines/jassub-failures-validation.json b/refactor/baselines/jassub-failures-validation.json
new file mode 100644
index 000000000..70b85ddf0
--- /dev/null
+++ b/refactor/baselines/jassub-failures-validation.json
@@ -0,0 +1,3657 @@
+{
+ "task": "PKG-JASSUB-02",
+ "sourceCommit": "0cf56b64d2711947ee7ea40a5a7f47fdb13e4521",
+ "node": "v24.21.0",
+ "platform": "win32",
+ "tests": {
+ "passed": 120,
+ "failed": 0,
+ "skipped": 0,
+ "historicalFailureAndContainment": 63,
+ "meaning": "Historical problem observations and existing protections, not candidate fixes.",
+ "log": {
+ "file": "refactor/.cache/jassub02-failures-final.log",
+ "sha256": "1c9650a5e2d118713f40f10ca62280b70e77a6de2af65901d890b7581b40531c"
+ }
+ },
+ "sources": [
+ {
+ "file": "test/jassub.test.js",
+ "sha256": "8c6fbb488912ac91eeb0beaf05b77cddbfc3c1d4cc663ab3196e237d689225c0"
+ },
+ {
+ "file": "refactor/scripts/jassub-failures.test.mjs",
+ "sha256": "073f3fd419ff46177ffac091a7bcd6bfb3b1ee4f7da2cbf8991db9692880bc83"
+ },
+ {
+ "file": "test/helpers/jassub.js",
+ "sha256": "16e7506ca140c61526e16bcc748c8f1d559f522950bebfbbbe1014317d7a283d"
+ },
+ {
+ "file": "test/browser/jassub-native.spec.js",
+ "sha256": "5a048e6e2f851312f44e69474437b46a73c03210c4b4212b804e3b1437b9f0b0"
+ },
+ {
+ "file": "test/browser/server.mjs",
+ "sha256": "8f7eed79508a1038f54d77d68d704e602499de3bf1d24c7aae8400caef6e560d"
+ }
+ ],
+ "observations": [
+ {
+ "implementation": "published-1.1.0-main",
+ "case": "rate-event-payload",
+ "mediaRate": 1.5,
+ "forwardedType": "ratechange",
+ "directNumericRatePreserved": true
+ },
+ {
+ "implementation": "published-1.1.0-main",
+ "case": "repeated-destroy",
+ "throws": true,
+ "workerTerminations": 1
+ },
+ {
+ "implementation": "published-1.1.0-main",
+ "case": "set-video-parent",
+ "workerTerminations": 0,
+ "listenersRetained": 7,
+ "originalContainerRetained": true
+ },
+ {
+ "implementation": "published-1.1.0-main",
+ "case": "custom-canvas",
+ "workerTerminations": 0,
+ "listenersRetained": 7,
+ "hostDestroyHandlerRegistered": false,
+ "initSent": true
+ },
+ {
+ "implementation": "published-1.1.0-main",
+ "case": "worker-construction-failure",
+ "originalErrorPreserved": true,
+ "insertedContainerRetained": true,
+ "listenersRetained": 7
+ },
+ {
+ "implementation": "published-1.1.0-main",
+ "case": "old-video-frame",
+ "replacementCurrentTime": 9,
+ "staleDemandTime": 123,
+ "replacementPendingCallbacks": 2,
+ "cancelledCallbacks": 0
+ },
+ {
+ "implementation": "published-1.1.0-main",
+ "case": "destroy-before-ready",
+ "workerTerminations": 1,
+ "listenersRetained": 0,
+ "postAfterTermination": [
+ "init"
+ ],
+ "loadedSettledWithinFixtureFlush": false
+ },
+ {
+ "implementation": "published-1.1.0-main",
+ "case": "worker-error",
+ "exactErrorForwarded": true,
+ "automaticallyTerminated": false,
+ "explicitHostCleanup": true
+ },
+ {
+ "implementation": "published-1.1.0-main",
+ "case": "frame-after-destroy",
+ "activelyCancelled": false,
+ "callbackDoesNoWork": true,
+ "callbackRearms": false
+ },
+ {
+ "implementation": "published-1.1.0-legacy",
+ "case": "rate-event-payload",
+ "mediaRate": 1.5,
+ "forwardedType": "ratechange",
+ "directNumericRatePreserved": true
+ },
+ {
+ "implementation": "published-1.1.0-legacy",
+ "case": "repeated-destroy",
+ "throws": true,
+ "workerTerminations": 1
+ },
+ {
+ "implementation": "published-1.1.0-legacy",
+ "case": "set-video-parent",
+ "workerTerminations": 0,
+ "listenersRetained": 7,
+ "originalContainerRetained": true
+ },
+ {
+ "implementation": "published-1.1.0-legacy",
+ "case": "custom-canvas",
+ "workerTerminations": 0,
+ "listenersRetained": 7,
+ "hostDestroyHandlerRegistered": false,
+ "initSent": true
+ },
+ {
+ "implementation": "published-1.1.0-legacy",
+ "case": "worker-construction-failure",
+ "originalErrorPreserved": true,
+ "insertedContainerRetained": true,
+ "listenersRetained": 7
+ },
+ {
+ "implementation": "published-1.1.0-legacy",
+ "case": "old-video-frame",
+ "replacementCurrentTime": 9,
+ "staleDemandTime": 123,
+ "replacementPendingCallbacks": 2,
+ "cancelledCallbacks": 0
+ },
+ {
+ "implementation": "published-1.1.0-legacy",
+ "case": "destroy-before-ready",
+ "workerTerminations": 1,
+ "listenersRetained": 0,
+ "postAfterTermination": [
+ "init"
+ ],
+ "loadedSettledWithinFixtureFlush": false
+ },
+ {
+ "implementation": "published-1.1.0-legacy",
+ "case": "worker-error",
+ "exactErrorForwarded": true,
+ "automaticallyTerminated": false,
+ "explicitHostCleanup": true
+ },
+ {
+ "implementation": "published-1.1.0-legacy",
+ "case": "frame-after-destroy",
+ "activelyCancelled": false,
+ "callbackDoesNoWork": true,
+ "callbackRearms": false
+ },
+ {
+ "implementation": "published-1.0.0-main",
+ "case": "rate-event-payload",
+ "mediaRate": 1.5,
+ "forwardedType": "ratechange",
+ "directNumericRatePreserved": true
+ },
+ {
+ "implementation": "published-1.0.0-main",
+ "case": "repeated-destroy",
+ "throws": true,
+ "workerTerminations": 1
+ },
+ {
+ "implementation": "published-1.0.0-main",
+ "case": "set-video-parent",
+ "workerTerminations": 0,
+ "listenersRetained": 7,
+ "originalContainerRetained": true
+ },
+ {
+ "implementation": "published-1.0.0-main",
+ "case": "custom-canvas",
+ "workerTerminations": 0,
+ "listenersRetained": 7,
+ "hostDestroyHandlerRegistered": false,
+ "initSent": true
+ },
+ {
+ "implementation": "published-1.0.0-main",
+ "case": "worker-construction-failure",
+ "originalErrorPreserved": true,
+ "insertedContainerRetained": true,
+ "listenersRetained": 7
+ },
+ {
+ "implementation": "published-1.0.0-main",
+ "case": "old-video-frame",
+ "replacementCurrentTime": 9,
+ "staleDemandTime": 123,
+ "replacementPendingCallbacks": 2,
+ "cancelledCallbacks": 0
+ },
+ {
+ "implementation": "published-1.0.0-main",
+ "case": "destroy-before-ready",
+ "workerTerminations": 1,
+ "listenersRetained": 0,
+ "postAfterTermination": [
+ "init"
+ ],
+ "loadedSettledWithinFixtureFlush": false
+ },
+ {
+ "implementation": "published-1.0.0-main",
+ "case": "worker-error",
+ "exactErrorForwarded": true,
+ "automaticallyTerminated": false,
+ "explicitHostCleanup": true
+ },
+ {
+ "implementation": "published-1.0.0-main",
+ "case": "frame-after-destroy",
+ "activelyCancelled": false,
+ "callbackDoesNoWork": true,
+ "callbackRearms": false
+ },
+ {
+ "implementation": "published-1.0.0-legacy",
+ "case": "rate-event-payload",
+ "mediaRate": 1.5,
+ "forwardedType": "ratechange",
+ "directNumericRatePreserved": true
+ },
+ {
+ "implementation": "published-1.0.0-legacy",
+ "case": "repeated-destroy",
+ "throws": true,
+ "workerTerminations": 1
+ },
+ {
+ "implementation": "published-1.0.0-legacy",
+ "case": "set-video-parent",
+ "workerTerminations": 0,
+ "listenersRetained": 7,
+ "originalContainerRetained": true
+ },
+ {
+ "implementation": "published-1.0.0-legacy",
+ "case": "custom-canvas",
+ "workerTerminations": 0,
+ "listenersRetained": 7,
+ "hostDestroyHandlerRegistered": false,
+ "initSent": true
+ },
+ {
+ "implementation": "published-1.0.0-legacy",
+ "case": "worker-construction-failure",
+ "originalErrorPreserved": true,
+ "insertedContainerRetained": true,
+ "listenersRetained": 7
+ },
+ {
+ "implementation": "published-1.0.0-legacy",
+ "case": "old-video-frame",
+ "replacementCurrentTime": 9,
+ "staleDemandTime": 123,
+ "replacementPendingCallbacks": 2,
+ "cancelledCallbacks": 0
+ },
+ {
+ "implementation": "published-1.0.0-legacy",
+ "case": "destroy-before-ready",
+ "workerTerminations": 1,
+ "listenersRetained": 0,
+ "postAfterTermination": [
+ "init"
+ ],
+ "loadedSettledWithinFixtureFlush": false
+ },
+ {
+ "implementation": "published-1.0.0-legacy",
+ "case": "worker-error",
+ "exactErrorForwarded": true,
+ "automaticallyTerminated": false,
+ "explicitHostCleanup": true
+ },
+ {
+ "implementation": "published-1.0.0-legacy",
+ "case": "frame-after-destroy",
+ "activelyCancelled": false,
+ "callbackDoesNoWork": true,
+ "callbackRearms": false
+ },
+ {
+ "implementation": "frozen-workspace-source",
+ "case": "rate-event-payload",
+ "mediaRate": 1.5,
+ "forwardedType": "ratechange",
+ "directNumericRatePreserved": true
+ },
+ {
+ "implementation": "frozen-workspace-source",
+ "case": "repeated-destroy",
+ "throws": true,
+ "workerTerminations": 1
+ },
+ {
+ "implementation": "frozen-workspace-source",
+ "case": "set-video-parent",
+ "workerTerminations": 0,
+ "listenersRetained": 7,
+ "originalContainerRetained": true
+ },
+ {
+ "implementation": "frozen-workspace-source",
+ "case": "custom-canvas",
+ "workerTerminations": 0,
+ "listenersRetained": 7,
+ "hostDestroyHandlerRegistered": false,
+ "initSent": true
+ },
+ {
+ "implementation": "frozen-workspace-source",
+ "case": "worker-construction-failure",
+ "originalErrorPreserved": true,
+ "insertedContainerRetained": true,
+ "listenersRetained": 7
+ },
+ {
+ "implementation": "frozen-workspace-source",
+ "case": "old-video-frame",
+ "replacementCurrentTime": 9,
+ "staleDemandTime": 123,
+ "replacementPendingCallbacks": 2,
+ "cancelledCallbacks": 0
+ },
+ {
+ "implementation": "frozen-workspace-source",
+ "case": "destroy-before-ready",
+ "workerTerminations": 1,
+ "listenersRetained": 0,
+ "postAfterTermination": [
+ "init"
+ ],
+ "loadedSettledWithinFixtureFlush": false
+ },
+ {
+ "implementation": "frozen-workspace-source",
+ "case": "worker-error",
+ "exactErrorForwarded": true,
+ "automaticallyTerminated": false,
+ "explicitHostCleanup": true
+ },
+ {
+ "implementation": "frozen-workspace-source",
+ "case": "frame-after-destroy",
+ "activelyCancelled": false,
+ "callbackDoesNoWork": true,
+ "callbackRearms": false
+ },
+ {
+ "implementation": "frozen-workspace-js",
+ "case": "rate-event-payload",
+ "mediaRate": 1.5,
+ "forwardedType": "ratechange",
+ "directNumericRatePreserved": true
+ },
+ {
+ "implementation": "frozen-workspace-js",
+ "case": "repeated-destroy",
+ "throws": true,
+ "workerTerminations": 1
+ },
+ {
+ "implementation": "frozen-workspace-js",
+ "case": "set-video-parent",
+ "workerTerminations": 0,
+ "listenersRetained": 7,
+ "originalContainerRetained": true
+ },
+ {
+ "implementation": "frozen-workspace-js",
+ "case": "custom-canvas",
+ "workerTerminations": 0,
+ "listenersRetained": 7,
+ "hostDestroyHandlerRegistered": false,
+ "initSent": true
+ },
+ {
+ "implementation": "frozen-workspace-js",
+ "case": "worker-construction-failure",
+ "originalErrorPreserved": true,
+ "insertedContainerRetained": true,
+ "listenersRetained": 7
+ },
+ {
+ "implementation": "frozen-workspace-js",
+ "case": "old-video-frame",
+ "replacementCurrentTime": 9,
+ "staleDemandTime": 123,
+ "replacementPendingCallbacks": 2,
+ "cancelledCallbacks": 0
+ },
+ {
+ "implementation": "frozen-workspace-js",
+ "case": "destroy-before-ready",
+ "workerTerminations": 1,
+ "listenersRetained": 0,
+ "postAfterTermination": [
+ "init"
+ ],
+ "loadedSettledWithinFixtureFlush": false
+ },
+ {
+ "implementation": "frozen-workspace-js",
+ "case": "worker-error",
+ "exactErrorForwarded": true,
+ "automaticallyTerminated": false,
+ "explicitHostCleanup": true
+ },
+ {
+ "implementation": "frozen-workspace-js",
+ "case": "frame-after-destroy",
+ "activelyCancelled": false,
+ "callbackDoesNoWork": true,
+ "callbackRearms": false
+ },
+ {
+ "implementation": "frozen-workspace-legacy.js",
+ "case": "rate-event-payload",
+ "mediaRate": 1.5,
+ "forwardedType": "ratechange",
+ "directNumericRatePreserved": true
+ },
+ {
+ "implementation": "frozen-workspace-legacy.js",
+ "case": "repeated-destroy",
+ "throws": true,
+ "workerTerminations": 1
+ },
+ {
+ "implementation": "frozen-workspace-legacy.js",
+ "case": "set-video-parent",
+ "workerTerminations": 0,
+ "listenersRetained": 7,
+ "originalContainerRetained": true
+ },
+ {
+ "implementation": "frozen-workspace-legacy.js",
+ "case": "custom-canvas",
+ "workerTerminations": 0,
+ "listenersRetained": 7,
+ "hostDestroyHandlerRegistered": false,
+ "initSent": true
+ },
+ {
+ "implementation": "frozen-workspace-legacy.js",
+ "case": "worker-construction-failure",
+ "originalErrorPreserved": true,
+ "insertedContainerRetained": true,
+ "listenersRetained": 7
+ },
+ {
+ "implementation": "frozen-workspace-legacy.js",
+ "case": "old-video-frame",
+ "replacementCurrentTime": 9,
+ "staleDemandTime": 123,
+ "replacementPendingCallbacks": 2,
+ "cancelledCallbacks": 0
+ },
+ {
+ "implementation": "frozen-workspace-legacy.js",
+ "case": "destroy-before-ready",
+ "workerTerminations": 1,
+ "listenersRetained": 0,
+ "postAfterTermination": [
+ "init"
+ ],
+ "loadedSettledWithinFixtureFlush": false
+ },
+ {
+ "implementation": "frozen-workspace-legacy.js",
+ "case": "worker-error",
+ "exactErrorForwarded": true,
+ "automaticallyTerminated": false,
+ "explicitHostCleanup": true
+ },
+ {
+ "implementation": "frozen-workspace-legacy.js",
+ "case": "frame-after-destroy",
+ "activelyCancelled": false,
+ "callbackDoesNoWork": true,
+ "callbackRearms": false
+ }
+ ],
+ "reports": [
+ {
+ "name": "first",
+ "file": "refactor/.cache/jassub02-native-first.json",
+ "sha256": "aaee75320045b5604c70b3c6aa2157c5edce930818c4855ae2cf45eabc362d80",
+ "stats": {
+ "startTime": "2026-09-13T16:45:55.577Z",
+ "duration": 64639.776,
+ "expected": 0,
+ "skipped": 0,
+ "unexpected": 3,
+ "flaky": 0
+ },
+ "cases": [
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with candidate core",
+ "engine": "chromium",
+ "status": "timedOut",
+ "retry": 0,
+ "errors": [
+ "\u001b[31mTest timeout of 20000ms exceeded.\u001b[39m",
+ "Error: Unhandled browser errors\n\n\u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoEqual\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // deep equality\u001b[22m\n\n\u001b[32m- Expected - 1\u001b[39m\n\u001b[31m+ Received + 3\u001b[39m\n\n\u001b[32m- Array []\u001b[39m\n\u001b[31m+ Array [\u001b[39m\n\u001b[31m+ \"Failed to execute 'compile' on 'WebAssembly': Incorrect response MIME type. Expected 'application/wasm'.\",\u001b[39m\n\u001b[31m+ ]\u001b[39m\n\n at fixtures.js:29\n\n 27 | }).catch(() => null)\n 28 | await testInfo.attach('browser-evidence', { contentType: 'application/json', body: JSON.stringify({ browser: browser.version(), platform: process.platform, manifest, state, errors, consoleErrors, failedRequests }, null, 2) })\n> 29 | expect(errors, 'Unhandled browser errors').toEqual([])\n | ^\n 30 | }, { auto: true }],\n 31 | })\n 32 |\n at Object.base.extend.diagnostics.auto (D:\\github\\ArtPlayer\\test\\browser\\fixtures.js:29:48)",
+ "Error: page.evaluate: Target page, context or browser has been closed\n\n 125 | }\n 126 | finally {\n> 127 | const state = await page.evaluate(() => window.jassubNative)\n | ^\n 128 | await testInfo.attach('actual-jassub-evidence', { body: JSON.stringify({ core, release: { version: release.version, integrity: release.integrity, member, sha256: hash(code) }, subtitleSha256: hash(subtitles), resources, external, state, limitation: 'Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance.' }, null, 2), contentType: 'application/json' })\n 129 | await page.evaluate(() => {\n 130 | if (window.art && !window.art.isDestroy)\n at D:\\github\\ArtPlayer\\test\\browser\\jassub-native.spec.js:127:32"
+ ],
+ "browser": {
+ "version": "153.0.8010.12",
+ "platform": "win32",
+ "errors": [
+ "Failed to execute 'compile' on 'WebAssembly': Incorrect response MIME type. Expected 'application/wasm'."
+ ],
+ "consoleErrors": [],
+ "failedRequests": [
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ }
+ ]
+ },
+ "evidence": null,
+ "screenshots": []
+ },
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with published core",
+ "engine": "chromium",
+ "status": "timedOut",
+ "retry": 0,
+ "errors": [
+ "\u001b[31mTest timeout of 20000ms exceeded.\u001b[39m",
+ "Error: Unhandled browser errors\n\n\u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoEqual\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // deep equality\u001b[22m\n\n\u001b[32m- Expected - 1\u001b[39m\n\u001b[31m+ Received + 3\u001b[39m\n\n\u001b[32m- Array []\u001b[39m\n\u001b[31m+ Array [\u001b[39m\n\u001b[31m+ \"Failed to execute 'compile' on 'WebAssembly': Incorrect response MIME type. Expected 'application/wasm'.\",\u001b[39m\n\u001b[31m+ ]\u001b[39m\n\n at fixtures.js:29\n\n 27 | }).catch(() => null)\n 28 | await testInfo.attach('browser-evidence', { contentType: 'application/json', body: JSON.stringify({ browser: browser.version(), platform: process.platform, manifest, state, errors, consoleErrors, failedRequests }, null, 2) })\n> 29 | expect(errors, 'Unhandled browser errors').toEqual([])\n | ^\n 30 | }, { auto: true }],\n 31 | })\n 32 |\n at Object.base.extend.diagnostics.auto (D:\\github\\ArtPlayer\\test\\browser\\fixtures.js:29:48)",
+ "Error: page.evaluate: Target page, context or browser has been closed\n\n 125 | }\n 126 | finally {\n> 127 | const state = await page.evaluate(() => window.jassubNative)\n | ^\n 128 | await testInfo.attach('actual-jassub-evidence', { body: JSON.stringify({ core, release: { version: release.version, integrity: release.integrity, member, sha256: hash(code) }, subtitleSha256: hash(subtitles), resources, external, state, limitation: 'Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance.' }, null, 2), contentType: 'application/json' })\n 129 | await page.evaluate(() => {\n 130 | if (window.art && !window.art.isDestroy)\n at D:\\github\\ArtPlayer\\test\\browser\\jassub-native.spec.js:127:32"
+ ],
+ "browser": {
+ "version": "153.0.8010.12",
+ "platform": "win32",
+ "errors": [
+ "Failed to execute 'compile' on 'WebAssembly': Incorrect response MIME type. Expected 'application/wasm'."
+ ],
+ "consoleErrors": [],
+ "failedRequests": [
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ }
+ ]
+ },
+ "evidence": null,
+ "screenshots": []
+ },
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with published-5.3.1-beta.1 core",
+ "engine": "chromium",
+ "status": "timedOut",
+ "retry": 0,
+ "errors": [
+ "\u001b[31mTest timeout of 20000ms exceeded.\u001b[39m",
+ "Error: Unhandled browser errors\n\n\u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoEqual\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // deep equality\u001b[22m\n\n\u001b[32m- Expected - 1\u001b[39m\n\u001b[31m+ Received + 3\u001b[39m\n\n\u001b[32m- Array []\u001b[39m\n\u001b[31m+ Array [\u001b[39m\n\u001b[31m+ \"Failed to execute 'compile' on 'WebAssembly': Incorrect response MIME type. Expected 'application/wasm'.\",\u001b[39m\n\u001b[31m+ ]\u001b[39m\n\n at fixtures.js:29\n\n 27 | }).catch(() => null)\n 28 | await testInfo.attach('browser-evidence', { contentType: 'application/json', body: JSON.stringify({ browser: browser.version(), platform: process.platform, manifest, state, errors, consoleErrors, failedRequests }, null, 2) })\n> 29 | expect(errors, 'Unhandled browser errors').toEqual([])\n | ^\n 30 | }, { auto: true }],\n 31 | })\n 32 |\n at Object.base.extend.diagnostics.auto (D:\\github\\ArtPlayer\\test\\browser\\fixtures.js:29:48)",
+ "Error: page.evaluate: Target page, context or browser has been closed\n\n 125 | }\n 126 | finally {\n> 127 | const state = await page.evaluate(() => window.jassubNative)\n | ^\n 128 | await testInfo.attach('actual-jassub-evidence', { body: JSON.stringify({ core, release: { version: release.version, integrity: release.integrity, member, sha256: hash(code) }, subtitleSha256: hash(subtitles), resources, external, state, limitation: 'Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance.' }, null, 2), contentType: 'application/json' })\n 129 | await page.evaluate(() => {\n 130 | if (window.art && !window.art.isDestroy)\n at D:\\github\\ArtPlayer\\test\\browser\\jassub-native.spec.js:127:32"
+ ],
+ "browser": {
+ "version": "153.0.8010.12",
+ "platform": "win32",
+ "errors": [
+ "Failed to execute 'compile' on 'WebAssembly': Incorrect response MIME type. Expected 'application/wasm'."
+ ],
+ "consoleErrors": [],
+ "failedRequests": [
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ }
+ ]
+ },
+ "evidence": null,
+ "screenshots": []
+ }
+ ]
+ },
+ {
+ "name": "mime",
+ "file": "refactor/.cache/jassub02-native-mime.json",
+ "sha256": "888fcfe6554158ce71f376b0072ed029905c32ce90167ac64524ced8b928079d",
+ "stats": {
+ "startTime": "2026-09-13T16:48:39.077Z",
+ "duration": 7125.699,
+ "expected": 3,
+ "skipped": 0,
+ "unexpected": 0,
+ "flaky": 0
+ },
+ "cases": [
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with candidate core",
+ "engine": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "errors": [],
+ "browser": {
+ "version": "153.0.8010.12",
+ "platform": "win32",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": [
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ }
+ ]
+ },
+ "evidence": {
+ "core": "candidate",
+ "release": {
+ "version": "1.1.0",
+ "integrity": "sha512-vQwEtZMmkwr5utbxfUAeE/gRccl2nfcz+SYP2Subn/L8cVIFdMXWMgicm7ZkPVtAoBbCav7p0yP4pwXkCKMzAg==",
+ "member": "package/dist/artplayer-plugin-jassub.js",
+ "sha256": "271d5284e5246fbd04575526510b3e381e5e60d414c030acecce92fcf2e9b10a"
+ },
+ "subtitleSha256": "12994d1b6a07c45fd9776cb8709afc37725a8003bd90e4e829b7cc44fbadc0ec",
+ "resources": [
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker.js",
+ "status": 200,
+ "contentType": "text/javascript"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker-modern.wasm",
+ "status": 200,
+ "contentType": "application/wasm"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ }
+ ],
+ "external": [],
+ "state": {
+ "workers": [
+ {
+ "url": "/assets/jassub/jassub-worker.js",
+ "terminated": 1
+ }
+ ],
+ "snapshots": [
+ {
+ "phase": "before-seek",
+ "visible": 2676,
+ "signature": 1690974404,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 0.497676,
+ "connected": true
+ },
+ {
+ "phase": "after-seek",
+ "visible": 6216,
+ "signature": 3526987101,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 10.014073,
+ "connected": true
+ },
+ {
+ "phase": "fullscreen",
+ "visible": 22298,
+ "signature": 2781790832,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.117091,
+ "connected": true
+ }
+ ],
+ "errors": []
+ },
+ "limitation": "Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance."
+ },
+ "screenshots": [
+ {
+ "name": "actual-jassub-page",
+ "sha256": "b14c548fa54a855a50dd491ff263052e44c081e366ad847e7caa9273dc7c6a0e"
+ }
+ ]
+ },
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with published core",
+ "engine": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "errors": [],
+ "browser": {
+ "version": "153.0.8010.12",
+ "platform": "win32",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": [
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ }
+ ]
+ },
+ "evidence": {
+ "core": "published",
+ "release": {
+ "version": "1.1.0",
+ "integrity": "sha512-vQwEtZMmkwr5utbxfUAeE/gRccl2nfcz+SYP2Subn/L8cVIFdMXWMgicm7ZkPVtAoBbCav7p0yP4pwXkCKMzAg==",
+ "member": "package/dist/artplayer-plugin-jassub.js",
+ "sha256": "271d5284e5246fbd04575526510b3e381e5e60d414c030acecce92fcf2e9b10a"
+ },
+ "subtitleSha256": "12994d1b6a07c45fd9776cb8709afc37725a8003bd90e4e829b7cc44fbadc0ec",
+ "resources": [
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker.js",
+ "status": 200,
+ "contentType": "text/javascript"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker-modern.wasm",
+ "status": 200,
+ "contentType": "application/wasm"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ }
+ ],
+ "external": [],
+ "state": {
+ "workers": [
+ {
+ "url": "/assets/jassub/jassub-worker.js",
+ "terminated": 1
+ }
+ ],
+ "snapshots": [
+ {
+ "phase": "before-seek",
+ "visible": 2676,
+ "signature": 1690974404,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 0.503139,
+ "connected": true
+ },
+ {
+ "phase": "after-seek",
+ "visible": 6216,
+ "signature": 3526987101,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 10.013048,
+ "connected": true
+ },
+ {
+ "phase": "fullscreen",
+ "visible": 22298,
+ "signature": 2781790832,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.114327,
+ "connected": true
+ }
+ ],
+ "errors": []
+ },
+ "limitation": "Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance."
+ },
+ "screenshots": [
+ {
+ "name": "actual-jassub-page",
+ "sha256": "ecfe0b2a8b5b3b4609b8a478db467f91c45fef9c57ec1ebda03e0500cf75fe1e"
+ }
+ ]
+ },
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with published-5.3.1-beta.1 core",
+ "engine": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "errors": [],
+ "browser": {
+ "version": "153.0.8010.12",
+ "platform": "win32",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": [
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ }
+ ]
+ },
+ "evidence": {
+ "core": "published-5.3.1-beta.1",
+ "release": {
+ "version": "1.1.0",
+ "integrity": "sha512-vQwEtZMmkwr5utbxfUAeE/gRccl2nfcz+SYP2Subn/L8cVIFdMXWMgicm7ZkPVtAoBbCav7p0yP4pwXkCKMzAg==",
+ "member": "package/dist/artplayer-plugin-jassub.js",
+ "sha256": "271d5284e5246fbd04575526510b3e381e5e60d414c030acecce92fcf2e9b10a"
+ },
+ "subtitleSha256": "12994d1b6a07c45fd9776cb8709afc37725a8003bd90e4e829b7cc44fbadc0ec",
+ "resources": [
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker.js",
+ "status": 200,
+ "contentType": "text/javascript"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker-modern.wasm",
+ "status": 200,
+ "contentType": "application/wasm"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ }
+ ],
+ "external": [],
+ "state": {
+ "workers": [
+ {
+ "url": "/assets/jassub/jassub-worker.js",
+ "terminated": 1
+ }
+ ],
+ "snapshots": [
+ {
+ "phase": "before-seek",
+ "visible": 2676,
+ "signature": 1690974404,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 0.523336,
+ "connected": true
+ },
+ {
+ "phase": "after-seek",
+ "visible": 6216,
+ "signature": 3526987101,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 10.012859,
+ "connected": true
+ },
+ {
+ "phase": "fullscreen",
+ "visible": 22298,
+ "signature": 2781790832,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.101009,
+ "connected": true
+ }
+ ],
+ "errors": []
+ },
+ "limitation": "Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance."
+ },
+ "screenshots": [
+ {
+ "name": "actual-jassub-page",
+ "sha256": "187d1a46c31c16f80c027d779ca78c961f29a250f6bd49dc2ede1174e67cfbae"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "other",
+ "file": "refactor/.cache/jassub02-native-other.json",
+ "sha256": "f5a4a3ff6bde205cf0abc15b8d99e1c0375443cbcf8eb425f5591bf4a9f6c1ca",
+ "stats": {
+ "startTime": "2026-09-13T16:49:10.751Z",
+ "duration": 46204.432,
+ "expected": 3,
+ "skipped": 0,
+ "unexpected": 3,
+ "flaky": 0
+ },
+ "cases": [
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with candidate core",
+ "engine": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "errors": [],
+ "browser": {
+ "version": "155.0",
+ "platform": "win32",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ "evidence": {
+ "core": "candidate",
+ "release": {
+ "version": "1.1.0",
+ "integrity": "sha512-vQwEtZMmkwr5utbxfUAeE/gRccl2nfcz+SYP2Subn/L8cVIFdMXWMgicm7ZkPVtAoBbCav7p0yP4pwXkCKMzAg==",
+ "member": "package/dist/artplayer-plugin-jassub.js",
+ "sha256": "271d5284e5246fbd04575526510b3e381e5e60d414c030acecce92fcf2e9b10a"
+ },
+ "subtitleSha256": "12994d1b6a07c45fd9776cb8709afc37725a8003bd90e4e829b7cc44fbadc0ec",
+ "resources": [
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker.js",
+ "status": 200,
+ "contentType": "text/javascript"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker-modern.wasm",
+ "status": 200,
+ "contentType": "application/wasm"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ }
+ ],
+ "external": [],
+ "state": {
+ "workers": [
+ {
+ "url": "/assets/jassub/jassub-worker.js",
+ "terminated": 1
+ }
+ ],
+ "snapshots": [
+ {
+ "phase": "before-seek",
+ "visible": 2676,
+ "signature": 1690974404,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 0.567509,
+ "connected": true
+ },
+ {
+ "phase": "after-seek",
+ "visible": 6216,
+ "signature": 3526987101,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 10.000023,
+ "connected": true
+ },
+ {
+ "phase": "fullscreen",
+ "visible": 22298,
+ "signature": 2781790832,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.040388,
+ "connected": true
+ }
+ ],
+ "errors": []
+ },
+ "limitation": "Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance."
+ },
+ "screenshots": [
+ {
+ "name": "actual-jassub-page",
+ "sha256": "184959dcee6e1aaecbecf12c760003455109f1b9b1e74dda4c4bd6226b2c39cf"
+ }
+ ]
+ },
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with published core",
+ "engine": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "errors": [],
+ "browser": {
+ "version": "155.0",
+ "platform": "win32",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": [
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "NS_ERROR_PARSED_DATA_CACHED"
+ }
+ }
+ ]
+ },
+ "evidence": {
+ "core": "published",
+ "release": {
+ "version": "1.1.0",
+ "integrity": "sha512-vQwEtZMmkwr5utbxfUAeE/gRccl2nfcz+SYP2Subn/L8cVIFdMXWMgicm7ZkPVtAoBbCav7p0yP4pwXkCKMzAg==",
+ "member": "package/dist/artplayer-plugin-jassub.js",
+ "sha256": "271d5284e5246fbd04575526510b3e381e5e60d414c030acecce92fcf2e9b10a"
+ },
+ "subtitleSha256": "12994d1b6a07c45fd9776cb8709afc37725a8003bd90e4e829b7cc44fbadc0ec",
+ "resources": [
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker.js",
+ "status": 200,
+ "contentType": "text/javascript"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker-modern.wasm",
+ "status": 200,
+ "contentType": "application/wasm"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ }
+ ],
+ "external": [],
+ "state": {
+ "workers": [
+ {
+ "url": "/assets/jassub/jassub-worker.js",
+ "terminated": 1
+ }
+ ],
+ "snapshots": [
+ {
+ "phase": "before-seek",
+ "visible": 2676,
+ "signature": 1690974404,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 0.350295,
+ "connected": true
+ },
+ {
+ "phase": "after-seek",
+ "visible": 6216,
+ "signature": 3526987101,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 10.00002,
+ "connected": true
+ },
+ {
+ "phase": "fullscreen",
+ "visible": 6216,
+ "signature": 3526987101,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.040321,
+ "connected": true
+ }
+ ],
+ "errors": []
+ },
+ "limitation": "Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance."
+ },
+ "screenshots": [
+ {
+ "name": "actual-jassub-page",
+ "sha256": "8e128bda82ccf0bd36d19a1bd0ee8275590734805fabbd780f03b9e3a8ae617b"
+ }
+ ]
+ },
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with published-5.3.1-beta.1 core",
+ "engine": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "errors": [],
+ "browser": {
+ "version": "155.0",
+ "platform": "win32",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ "evidence": {
+ "core": "published-5.3.1-beta.1",
+ "release": {
+ "version": "1.1.0",
+ "integrity": "sha512-vQwEtZMmkwr5utbxfUAeE/gRccl2nfcz+SYP2Subn/L8cVIFdMXWMgicm7ZkPVtAoBbCav7p0yP4pwXkCKMzAg==",
+ "member": "package/dist/artplayer-plugin-jassub.js",
+ "sha256": "271d5284e5246fbd04575526510b3e381e5e60d414c030acecce92fcf2e9b10a"
+ },
+ "subtitleSha256": "12994d1b6a07c45fd9776cb8709afc37725a8003bd90e4e829b7cc44fbadc0ec",
+ "resources": [
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker.js",
+ "status": 200,
+ "contentType": "text/javascript"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker-modern.wasm",
+ "status": 200,
+ "contentType": "application/wasm"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ }
+ ],
+ "external": [],
+ "state": {
+ "workers": [
+ {
+ "url": "/assets/jassub/jassub-worker.js",
+ "terminated": 1
+ }
+ ],
+ "snapshots": [
+ {
+ "phase": "before-seek",
+ "visible": 2676,
+ "signature": 1690974404,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 0.570445,
+ "connected": true
+ },
+ {
+ "phase": "after-seek",
+ "visible": 6216,
+ "signature": 3526987101,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 10.000021,
+ "connected": true
+ },
+ {
+ "phase": "fullscreen",
+ "visible": 22298,
+ "signature": 2781790832,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.161301,
+ "connected": true
+ }
+ ],
+ "errors": []
+ },
+ "limitation": "Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance."
+ },
+ "screenshots": [
+ {
+ "name": "actual-jassub-page",
+ "sha256": "220205864406f061da34aafee3c3be1eba23ec8d811f8fbe9c87f578f7e99f64"
+ }
+ ]
+ },
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with candidate core",
+ "engine": "webkit",
+ "status": "failed",
+ "retry": 0,
+ "errors": [
+ "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeGreaterThan\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected: > \u001b[32m100\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 97 | }, subtitles)\n 98 | await page.locator('#play').click()\n> 99 | await expect.poll(() => page.evaluate(() => window.jassubPixels().visible)).toBeGreaterThan(100)\n | ^\n 100 | const before = await page.evaluate(() => window.jassubPixels('before-seek'))\n 101 | expect(before.connected).toBe(true)\n 102 | await page.locator('#pause').click()\n at D:\\github\\ArtPlayer\\test\\browser\\jassub-native.spec.js:99:83"
+ ],
+ "browser": {
+ "version": "26.6",
+ "platform": "win32",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ "evidence": {
+ "core": "candidate",
+ "release": {
+ "version": "1.1.0",
+ "integrity": "sha512-vQwEtZMmkwr5utbxfUAeE/gRccl2nfcz+SYP2Subn/L8cVIFdMXWMgicm7ZkPVtAoBbCav7p0yP4pwXkCKMzAg==",
+ "member": "package/dist/artplayer-plugin-jassub.js",
+ "sha256": "271d5284e5246fbd04575526510b3e381e5e60d414c030acecce92fcf2e9b10a"
+ },
+ "subtitleSha256": "12994d1b6a07c45fd9776cb8709afc37725a8003bd90e4e829b7cc44fbadc0ec",
+ "resources": [
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker.js",
+ "status": 200,
+ "contentType": "text/javascript"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker-modern.wasm",
+ "status": 200,
+ "contentType": "application/wasm"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ }
+ ],
+ "external": [],
+ "state": {
+ "workers": [
+ {
+ "url": "/assets/jassub/jassub-worker.js",
+ "terminated": 0
+ }
+ ],
+ "snapshots": [],
+ "errors": []
+ },
+ "limitation": "Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance."
+ },
+ "screenshots": []
+ },
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with published core",
+ "engine": "webkit",
+ "status": "failed",
+ "retry": 0,
+ "errors": [
+ "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeGreaterThan\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected: > \u001b[32m100\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 97 | }, subtitles)\n 98 | await page.locator('#play').click()\n> 99 | await expect.poll(() => page.evaluate(() => window.jassubPixels().visible)).toBeGreaterThan(100)\n | ^\n 100 | const before = await page.evaluate(() => window.jassubPixels('before-seek'))\n 101 | expect(before.connected).toBe(true)\n 102 | await page.locator('#pause').click()\n at D:\\github\\ArtPlayer\\test\\browser\\jassub-native.spec.js:99:83"
+ ],
+ "browser": {
+ "version": "26.6",
+ "platform": "win32",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ "evidence": {
+ "core": "published",
+ "release": {
+ "version": "1.1.0",
+ "integrity": "sha512-vQwEtZMmkwr5utbxfUAeE/gRccl2nfcz+SYP2Subn/L8cVIFdMXWMgicm7ZkPVtAoBbCav7p0yP4pwXkCKMzAg==",
+ "member": "package/dist/artplayer-plugin-jassub.js",
+ "sha256": "271d5284e5246fbd04575526510b3e381e5e60d414c030acecce92fcf2e9b10a"
+ },
+ "subtitleSha256": "12994d1b6a07c45fd9776cb8709afc37725a8003bd90e4e829b7cc44fbadc0ec",
+ "resources": [
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker.js",
+ "status": 200,
+ "contentType": "text/javascript"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker-modern.wasm",
+ "status": 200,
+ "contentType": "application/wasm"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ }
+ ],
+ "external": [],
+ "state": {
+ "workers": [
+ {
+ "url": "/assets/jassub/jassub-worker.js",
+ "terminated": 0
+ }
+ ],
+ "snapshots": [],
+ "errors": []
+ },
+ "limitation": "Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance."
+ },
+ "screenshots": []
+ },
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with published-5.3.1-beta.1 core",
+ "engine": "webkit",
+ "status": "failed",
+ "retry": 0,
+ "errors": [
+ "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeGreaterThan\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected: > \u001b[32m100\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 97 | }, subtitles)\n 98 | await page.locator('#play').click()\n> 99 | await expect.poll(() => page.evaluate(() => window.jassubPixels().visible)).toBeGreaterThan(100)\n | ^\n 100 | const before = await page.evaluate(() => window.jassubPixels('before-seek'))\n 101 | expect(before.connected).toBe(true)\n 102 | await page.locator('#pause').click()\n at D:\\github\\ArtPlayer\\test\\browser\\jassub-native.spec.js:99:83"
+ ],
+ "browser": {
+ "version": "26.6",
+ "platform": "win32",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ "evidence": {
+ "core": "published-5.3.1-beta.1",
+ "release": {
+ "version": "1.1.0",
+ "integrity": "sha512-vQwEtZMmkwr5utbxfUAeE/gRccl2nfcz+SYP2Subn/L8cVIFdMXWMgicm7ZkPVtAoBbCav7p0yP4pwXkCKMzAg==",
+ "member": "package/dist/artplayer-plugin-jassub.js",
+ "sha256": "271d5284e5246fbd04575526510b3e381e5e60d414c030acecce92fcf2e9b10a"
+ },
+ "subtitleSha256": "12994d1b6a07c45fd9776cb8709afc37725a8003bd90e4e829b7cc44fbadc0ec",
+ "resources": [
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker.js",
+ "status": 200,
+ "contentType": "text/javascript"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker-modern.wasm",
+ "status": 200,
+ "contentType": "application/wasm"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ }
+ ],
+ "external": [],
+ "state": {
+ "workers": [
+ {
+ "url": "/assets/jassub/jassub-worker.js",
+ "terminated": 0
+ }
+ ],
+ "snapshots": [],
+ "errors": []
+ },
+ "limitation": "Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance."
+ },
+ "screenshots": []
+ }
+ ]
+ },
+ {
+ "name": "event-clock",
+ "file": "refactor/.cache/jassub02-native-event-clock.json",
+ "sha256": "a40530587be362c2b37abcfda70c52967b1b8b892a2aecb7b8c7565603ceaf65",
+ "stats": {
+ "startTime": "2026-09-13T16:51:44.674Z",
+ "duration": 6249.097000000001,
+ "expected": 1,
+ "skipped": 0,
+ "unexpected": 0,
+ "flaky": 0
+ },
+ "cases": [
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with candidate core",
+ "engine": "webkit",
+ "status": "passed",
+ "retry": 0,
+ "errors": [],
+ "browser": {
+ "version": "26.6",
+ "platform": "win32",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ "evidence": {
+ "core": "candidate",
+ "onDemandRender": false,
+ "release": {
+ "version": "1.1.0",
+ "integrity": "sha512-vQwEtZMmkwr5utbxfUAeE/gRccl2nfcz+SYP2Subn/L8cVIFdMXWMgicm7ZkPVtAoBbCav7p0yP4pwXkCKMzAg==",
+ "member": "package/dist/artplayer-plugin-jassub.js",
+ "sha256": "271d5284e5246fbd04575526510b3e381e5e60d414c030acecce92fcf2e9b10a"
+ },
+ "subtitleSha256": "12994d1b6a07c45fd9776cb8709afc37725a8003bd90e4e829b7cc44fbadc0ec",
+ "resources": [
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker.js",
+ "status": 200,
+ "contentType": "text/javascript"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker-modern.wasm",
+ "status": 200,
+ "contentType": "application/wasm"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ }
+ ],
+ "external": [],
+ "state": {
+ "workers": [
+ {
+ "url": "/assets/jassub/jassub-worker.js",
+ "terminated": 1,
+ "sent": [
+ {
+ "target": "init",
+ "width": 300,
+ "height": 150
+ },
+ {
+ "target": "canvas",
+ "width": 1280,
+ "height": 720
+ },
+ {
+ "target": "video"
+ },
+ {
+ "target": "canvas",
+ "width": 1280,
+ "height": 720
+ },
+ {
+ "target": "video"
+ },
+ {
+ "target": "video"
+ },
+ {
+ "target": "video"
+ },
+ {
+ "target": "video"
+ },
+ {
+ "target": "video"
+ },
+ {
+ "target": "video"
+ },
+ {
+ "target": "video"
+ },
+ {
+ "target": "video"
+ },
+ {
+ "target": "canvas",
+ "width": 2560,
+ "height": 1440
+ },
+ {
+ "target": "video"
+ },
+ {
+ "target": "video"
+ },
+ {
+ "target": "video"
+ },
+ {
+ "target": "video"
+ },
+ {
+ "target": "destroy"
+ }
+ ],
+ "received": [
+ {
+ "target": "ready"
+ },
+ {
+ "target": "verifyColorSpace"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 1280,
+ "height": 720
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 1280,
+ "height": 720
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 2560,
+ "height": 1440
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ }
+ ]
+ }
+ ],
+ "snapshots": [
+ {
+ "phase": "before-seek",
+ "visible": 13152,
+ "signature": 1263898348,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 0.3775417,
+ "connected": true
+ },
+ {
+ "phase": "after-seek",
+ "visible": 30818,
+ "signature": 827736580,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 10.0411875,
+ "connected": true
+ },
+ {
+ "phase": "fullscreen",
+ "visible": 119170,
+ "signature": 1826006850,
+ "width": 2560,
+ "height": 1440,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.2653959,
+ "connected": true
+ }
+ ],
+ "errors": [],
+ "lastPixel": {
+ "phase": "fullscreen",
+ "visible": 119170,
+ "signature": 1826006850,
+ "width": 2560,
+ "height": 1440,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.2653959,
+ "connected": true
+ }
+ },
+ "limitation": "Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance."
+ },
+ "screenshots": [
+ {
+ "name": "actual-jassub-page",
+ "sha256": "ca726d3de3838bf7f2327a37846fd31aa5db04a6b70c274e06f7e4a3bea38deb"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "frame-clock",
+ "file": "refactor/.cache/jassub02-native-frame-clock.json",
+ "sha256": "12f747cffb9c950d6804bc795f6c5155ff98bb2f145b9942d6cd4f8eb0ca620f",
+ "stats": {
+ "startTime": "2026-09-13T16:52:08.322Z",
+ "duration": 11462.328,
+ "expected": 0,
+ "skipped": 0,
+ "unexpected": 1,
+ "flaky": 0
+ },
+ "cases": [
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with candidate core",
+ "engine": "webkit",
+ "status": "failed",
+ "retry": 0,
+ "errors": [
+ "Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBeGreaterThan\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m)\u001b[22m\n\nExpected: > \u001b[32m100\u001b[39m\nReceived: \u001b[31m0\u001b[39m\n\nCall Log:\n- Timeout 7000ms exceeded while waiting on the predicate\n\n 107 | }, { subContent: subtitles, onDemandRender })\n 108 | await page.locator('#play').click()\n> 109 | await expect.poll(() => page.evaluate(() => window.jassubPixels().visible)).toBeGreaterThan(100)\n | ^\n 110 | const before = await page.evaluate(() => window.jassubPixels('before-seek'))\n 111 | expect(before.connected).toBe(true)\n 112 | await page.locator('#pause').click()\n at D:\\github\\ArtPlayer\\test\\browser\\jassub-native.spec.js:109:83"
+ ],
+ "browser": {
+ "version": "26.6",
+ "platform": "win32",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ "evidence": {
+ "core": "candidate",
+ "onDemandRender": true,
+ "release": {
+ "version": "1.1.0",
+ "integrity": "sha512-vQwEtZMmkwr5utbxfUAeE/gRccl2nfcz+SYP2Subn/L8cVIFdMXWMgicm7ZkPVtAoBbCav7p0yP4pwXkCKMzAg==",
+ "member": "package/dist/artplayer-plugin-jassub.js",
+ "sha256": "271d5284e5246fbd04575526510b3e381e5e60d414c030acecce92fcf2e9b10a"
+ },
+ "subtitleSha256": "12994d1b6a07c45fd9776cb8709afc37725a8003bd90e4e829b7cc44fbadc0ec",
+ "resources": [
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker.js",
+ "status": 200,
+ "contentType": "text/javascript"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker-modern.wasm",
+ "status": 200,
+ "contentType": "application/wasm"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ }
+ ],
+ "external": [],
+ "state": {
+ "workers": [
+ {
+ "url": "/assets/jassub/jassub-worker.js",
+ "terminated": 0,
+ "sent": [
+ {
+ "target": "init",
+ "width": 300,
+ "height": 150
+ },
+ {
+ "target": "canvas",
+ "width": 1280,
+ "height": 720
+ },
+ {
+ "target": "canvas",
+ "width": 1280,
+ "height": 720
+ }
+ ],
+ "received": [
+ {
+ "target": "ready"
+ },
+ {
+ "target": "verifyColorSpace"
+ },
+ {
+ "target": "render",
+ "images": 0,
+ "width": 1280,
+ "height": 720
+ }
+ ]
+ }
+ ],
+ "snapshots": [],
+ "errors": [],
+ "lastPixel": {
+ "visible": 0,
+ "signature": 0,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 6.4992094,
+ "connected": true
+ }
+ },
+ "limitation": "Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance."
+ },
+ "screenshots": []
+ }
+ ]
+ },
+ {
+ "name": "final",
+ "file": "refactor/.cache/jassub02-native-final.json",
+ "sha256": "acfc4af7f5384166dbc6d5434018c5f46e32c3d582395745a7bf390bea3cf026",
+ "stats": {
+ "startTime": "2026-09-13T16:54:43.778Z",
+ "duration": 14993.602,
+ "expected": 6,
+ "skipped": 0,
+ "unexpected": 0,
+ "flaky": 0
+ },
+ "cases": [
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with candidate core",
+ "engine": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "errors": [],
+ "browser": {
+ "version": "153.0.8010.12",
+ "platform": "win32",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": [
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ }
+ ]
+ },
+ "evidence": {
+ "core": "candidate",
+ "onDemandRender": true,
+ "release": {
+ "version": "1.1.0",
+ "integrity": "sha512-vQwEtZMmkwr5utbxfUAeE/gRccl2nfcz+SYP2Subn/L8cVIFdMXWMgicm7ZkPVtAoBbCav7p0yP4pwXkCKMzAg==",
+ "member": "package/dist/artplayer-plugin-jassub.js",
+ "sha256": "271d5284e5246fbd04575526510b3e381e5e60d414c030acecce92fcf2e9b10a"
+ },
+ "subtitleSha256": "12994d1b6a07c45fd9776cb8709afc37725a8003bd90e4e829b7cc44fbadc0ec",
+ "resources": [
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker.js",
+ "status": 200,
+ "contentType": "text/javascript"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker-modern.wasm",
+ "status": 200,
+ "contentType": "application/wasm"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ }
+ ],
+ "external": [],
+ "state": {
+ "workers": [
+ {
+ "url": "/assets/jassub/jassub-worker.js",
+ "terminated": 1,
+ "sent": [
+ {
+ "target": "init",
+ "width": 300,
+ "height": 150
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "demand",
+ "time": 0.4
+ },
+ {
+ "target": "getColorSpace"
+ },
+ {
+ "target": "demand",
+ "time": 0.44
+ },
+ {
+ "target": "demand",
+ "time": 0.48
+ },
+ {
+ "target": "demand",
+ "time": 0.52
+ },
+ {
+ "target": "demand",
+ "time": 0.56
+ },
+ {
+ "target": "demand",
+ "time": 10
+ },
+ {
+ "target": "canvas",
+ "width": 1280,
+ "height": 720
+ },
+ {
+ "target": "demand",
+ "time": 10.04
+ },
+ {
+ "target": "demand",
+ "time": 10.08
+ },
+ {
+ "target": "demand",
+ "time": 10.12
+ },
+ {
+ "target": "demand",
+ "time": 10.16
+ },
+ {
+ "target": "demand",
+ "time": 10.2
+ },
+ {
+ "target": "destroy"
+ }
+ ],
+ "received": [
+ {
+ "target": "ready"
+ },
+ {
+ "target": "verifyColorSpace"
+ },
+ {
+ "target": "render",
+ "images": 0,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "verifyColorSpace"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 1280,
+ "height": 720
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ }
+ ]
+ }
+ ],
+ "snapshots": [
+ {
+ "phase": "before-seek",
+ "visible": 2676,
+ "signature": 1690974404,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 0.507385,
+ "connected": true
+ },
+ {
+ "phase": "after-seek",
+ "visible": 6216,
+ "signature": 3526987101,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 10.013288,
+ "connected": true
+ },
+ {
+ "phase": "fullscreen",
+ "visible": 22298,
+ "signature": 2781790832,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.116258,
+ "connected": true
+ }
+ ],
+ "errors": [],
+ "lastPixel": {
+ "phase": "fullscreen",
+ "visible": 22298,
+ "signature": 2781790832,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.116258,
+ "connected": true
+ }
+ },
+ "limitation": "Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance."
+ },
+ "screenshots": [
+ {
+ "name": "actual-jassub-page",
+ "sha256": "b14c548fa54a855a50dd491ff263052e44c081e366ad847e7caa9273dc7c6a0e"
+ }
+ ]
+ },
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with published core",
+ "engine": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "errors": [],
+ "browser": {
+ "version": "153.0.8010.12",
+ "platform": "win32",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": [
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ }
+ ]
+ },
+ "evidence": {
+ "core": "published",
+ "onDemandRender": true,
+ "release": {
+ "version": "1.1.0",
+ "integrity": "sha512-vQwEtZMmkwr5utbxfUAeE/gRccl2nfcz+SYP2Subn/L8cVIFdMXWMgicm7ZkPVtAoBbCav7p0yP4pwXkCKMzAg==",
+ "member": "package/dist/artplayer-plugin-jassub.js",
+ "sha256": "271d5284e5246fbd04575526510b3e381e5e60d414c030acecce92fcf2e9b10a"
+ },
+ "subtitleSha256": "12994d1b6a07c45fd9776cb8709afc37725a8003bd90e4e829b7cc44fbadc0ec",
+ "resources": [
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker.js",
+ "status": 200,
+ "contentType": "text/javascript"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker-modern.wasm",
+ "status": 200,
+ "contentType": "application/wasm"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ }
+ ],
+ "external": [],
+ "state": {
+ "workers": [
+ {
+ "url": "/assets/jassub/jassub-worker.js",
+ "terminated": 1,
+ "sent": [
+ {
+ "target": "init",
+ "width": 300,
+ "height": 150
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "demand",
+ "time": 0.4
+ },
+ {
+ "target": "getColorSpace"
+ },
+ {
+ "target": "demand",
+ "time": 0.44
+ },
+ {
+ "target": "demand",
+ "time": 0.48
+ },
+ {
+ "target": "demand",
+ "time": 0.52
+ },
+ {
+ "target": "demand",
+ "time": 0.56
+ },
+ {
+ "target": "demand",
+ "time": 10
+ },
+ {
+ "target": "canvas",
+ "width": 1280,
+ "height": 720
+ },
+ {
+ "target": "demand",
+ "time": 10.04
+ },
+ {
+ "target": "demand",
+ "time": 10.08
+ },
+ {
+ "target": "demand",
+ "time": 10.12
+ },
+ {
+ "target": "demand",
+ "time": 10.16
+ },
+ {
+ "target": "demand",
+ "time": 10.2
+ },
+ {
+ "target": "demand",
+ "time": 10.24
+ },
+ {
+ "target": "destroy"
+ }
+ ],
+ "received": [
+ {
+ "target": "ready"
+ },
+ {
+ "target": "verifyColorSpace"
+ },
+ {
+ "target": "render",
+ "images": 0,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "verifyColorSpace"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 1280,
+ "height": 720
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ }
+ ]
+ }
+ ],
+ "snapshots": [
+ {
+ "phase": "before-seek",
+ "visible": 2676,
+ "signature": 1690974404,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 0.505015,
+ "connected": true
+ },
+ {
+ "phase": "after-seek",
+ "visible": 6216,
+ "signature": 3526987101,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 10.014939,
+ "connected": true
+ },
+ {
+ "phase": "fullscreen",
+ "visible": 22298,
+ "signature": 2781790832,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.114424,
+ "connected": true
+ }
+ ],
+ "errors": [],
+ "lastPixel": {
+ "phase": "fullscreen",
+ "visible": 22298,
+ "signature": 2781790832,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.114424,
+ "connected": true
+ }
+ },
+ "limitation": "Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance."
+ },
+ "screenshots": [
+ {
+ "name": "actual-jassub-page",
+ "sha256": "b14c548fa54a855a50dd491ff263052e44c081e366ad847e7caa9273dc7c6a0e"
+ }
+ ]
+ },
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with published-5.3.1-beta.1 core",
+ "engine": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "errors": [],
+ "browser": {
+ "version": "153.0.8010.12",
+ "platform": "win32",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": [
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "net::ERR_ABORTED"
+ }
+ }
+ ]
+ },
+ "evidence": {
+ "core": "published-5.3.1-beta.1",
+ "onDemandRender": true,
+ "release": {
+ "version": "1.1.0",
+ "integrity": "sha512-vQwEtZMmkwr5utbxfUAeE/gRccl2nfcz+SYP2Subn/L8cVIFdMXWMgicm7ZkPVtAoBbCav7p0yP4pwXkCKMzAg==",
+ "member": "package/dist/artplayer-plugin-jassub.js",
+ "sha256": "271d5284e5246fbd04575526510b3e381e5e60d414c030acecce92fcf2e9b10a"
+ },
+ "subtitleSha256": "12994d1b6a07c45fd9776cb8709afc37725a8003bd90e4e829b7cc44fbadc0ec",
+ "resources": [
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker.js",
+ "status": 200,
+ "contentType": "text/javascript"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker-modern.wasm",
+ "status": 200,
+ "contentType": "application/wasm"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ }
+ ],
+ "external": [],
+ "state": {
+ "workers": [
+ {
+ "url": "/assets/jassub/jassub-worker.js",
+ "terminated": 1,
+ "sent": [
+ {
+ "target": "init",
+ "width": 300,
+ "height": 150
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "demand",
+ "time": 0.4
+ },
+ {
+ "target": "getColorSpace"
+ },
+ {
+ "target": "demand",
+ "time": 0.44
+ },
+ {
+ "target": "demand",
+ "time": 0.48
+ },
+ {
+ "target": "demand",
+ "time": 0.52
+ },
+ {
+ "target": "demand",
+ "time": 0.56
+ },
+ {
+ "target": "demand",
+ "time": 10
+ },
+ {
+ "target": "canvas",
+ "width": 1280,
+ "height": 720
+ },
+ {
+ "target": "demand",
+ "time": 10.04
+ },
+ {
+ "target": "demand",
+ "time": 10.08
+ },
+ {
+ "target": "demand",
+ "time": 10.12
+ },
+ {
+ "target": "demand",
+ "time": 10.16
+ },
+ {
+ "target": "demand",
+ "time": 10.2
+ },
+ {
+ "target": "demand",
+ "time": 10.24
+ },
+ {
+ "target": "destroy"
+ }
+ ],
+ "received": [
+ {
+ "target": "ready"
+ },
+ {
+ "target": "verifyColorSpace"
+ },
+ {
+ "target": "render",
+ "images": 0,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "verifyColorSpace"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 1280,
+ "height": 720
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ }
+ ]
+ }
+ ],
+ "snapshots": [
+ {
+ "phase": "before-seek",
+ "visible": 2676,
+ "signature": 1690974404,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 0.505061,
+ "connected": true
+ },
+ {
+ "phase": "after-seek",
+ "visible": 6216,
+ "signature": 3526987101,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 10.014032,
+ "connected": true
+ },
+ {
+ "phase": "fullscreen",
+ "visible": 22298,
+ "signature": 2781790832,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.101751,
+ "connected": true
+ }
+ ],
+ "errors": [],
+ "lastPixel": {
+ "phase": "fullscreen",
+ "visible": 22298,
+ "signature": 2781790832,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.101751,
+ "connected": true
+ }
+ },
+ "limitation": "Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance."
+ },
+ "screenshots": [
+ {
+ "name": "actual-jassub-page",
+ "sha256": "187d1a46c31c16f80c027d779ca78c961f29a250f6bd49dc2ede1174e67cfbae"
+ }
+ ]
+ },
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with candidate core",
+ "engine": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "errors": [],
+ "browser": {
+ "version": "155.0",
+ "platform": "win32",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ "evidence": {
+ "core": "candidate",
+ "onDemandRender": true,
+ "release": {
+ "version": "1.1.0",
+ "integrity": "sha512-vQwEtZMmkwr5utbxfUAeE/gRccl2nfcz+SYP2Subn/L8cVIFdMXWMgicm7ZkPVtAoBbCav7p0yP4pwXkCKMzAg==",
+ "member": "package/dist/artplayer-plugin-jassub.js",
+ "sha256": "271d5284e5246fbd04575526510b3e381e5e60d414c030acecce92fcf2e9b10a"
+ },
+ "subtitleSha256": "12994d1b6a07c45fd9776cb8709afc37725a8003bd90e4e829b7cc44fbadc0ec",
+ "resources": [
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker.js",
+ "status": 200,
+ "contentType": "text/javascript"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker-modern.wasm",
+ "status": 200,
+ "contentType": "application/wasm"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ }
+ ],
+ "external": [],
+ "state": {
+ "workers": [
+ {
+ "url": "/assets/jassub/jassub-worker.js",
+ "terminated": 1,
+ "sent": [
+ {
+ "target": "init",
+ "width": 300,
+ "height": 150
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "demand",
+ "time": 0.44
+ },
+ {
+ "target": "getColorSpace"
+ },
+ {
+ "target": "demand",
+ "time": 0.48
+ },
+ {
+ "target": "demand",
+ "time": 0.52
+ },
+ {
+ "target": "demand",
+ "time": 0.56
+ },
+ {
+ "target": "demand",
+ "time": 0.6
+ },
+ {
+ "target": "demand",
+ "time": 10
+ },
+ {
+ "target": "demand",
+ "time": 10
+ },
+ {
+ "target": "canvas",
+ "width": 1280,
+ "height": 720
+ },
+ {
+ "target": "demand",
+ "time": 10.04
+ },
+ {
+ "target": "demand",
+ "time": 10.08
+ },
+ {
+ "target": "demand",
+ "time": 10.12
+ },
+ {
+ "target": "destroy"
+ }
+ ],
+ "received": [
+ {
+ "target": "ready"
+ },
+ {
+ "target": "verifyColorSpace"
+ },
+ {
+ "target": "render",
+ "images": 0,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "verifyColorSpace"
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 1280,
+ "height": 720
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ }
+ ]
+ }
+ ],
+ "snapshots": [
+ {
+ "phase": "before-seek",
+ "visible": 2676,
+ "signature": 1690974404,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 0.5638,
+ "connected": true
+ },
+ {
+ "phase": "after-seek",
+ "visible": 6216,
+ "signature": 3526987101,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 10.00002,
+ "connected": true
+ },
+ {
+ "phase": "fullscreen",
+ "visible": 22298,
+ "signature": 2781790832,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.040412,
+ "connected": true
+ }
+ ],
+ "errors": [],
+ "lastPixel": {
+ "phase": "fullscreen",
+ "visible": 22298,
+ "signature": 2781790832,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.040412,
+ "connected": true
+ }
+ },
+ "limitation": "Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance."
+ },
+ "screenshots": [
+ {
+ "name": "actual-jassub-page",
+ "sha256": "dcfe4e47ed962c6a6b98380878819a43be1581adb21722733b81fcac88897c2c"
+ }
+ ]
+ },
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with published core",
+ "engine": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "errors": [],
+ "browser": {
+ "version": "155.0",
+ "platform": "win32",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ "evidence": {
+ "core": "published",
+ "onDemandRender": true,
+ "release": {
+ "version": "1.1.0",
+ "integrity": "sha512-vQwEtZMmkwr5utbxfUAeE/gRccl2nfcz+SYP2Subn/L8cVIFdMXWMgicm7ZkPVtAoBbCav7p0yP4pwXkCKMzAg==",
+ "member": "package/dist/artplayer-plugin-jassub.js",
+ "sha256": "271d5284e5246fbd04575526510b3e381e5e60d414c030acecce92fcf2e9b10a"
+ },
+ "subtitleSha256": "12994d1b6a07c45fd9776cb8709afc37725a8003bd90e4e829b7cc44fbadc0ec",
+ "resources": [
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker.js",
+ "status": 200,
+ "contentType": "text/javascript"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker-modern.wasm",
+ "status": 200,
+ "contentType": "application/wasm"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ }
+ ],
+ "external": [],
+ "state": {
+ "workers": [
+ {
+ "url": "/assets/jassub/jassub-worker.js",
+ "terminated": 1,
+ "sent": [
+ {
+ "target": "init",
+ "width": 300,
+ "height": 150
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "demand",
+ "time": 0.44
+ },
+ {
+ "target": "getColorSpace"
+ },
+ {
+ "target": "demand",
+ "time": 0.48
+ },
+ {
+ "target": "demand",
+ "time": 0.52
+ },
+ {
+ "target": "demand",
+ "time": 0.56
+ },
+ {
+ "target": "demand",
+ "time": 0.64
+ },
+ {
+ "target": "demand",
+ "time": 10
+ },
+ {
+ "target": "demand",
+ "time": 10
+ },
+ {
+ "target": "demand",
+ "time": 10.04
+ },
+ {
+ "target": "canvas",
+ "width": 1280,
+ "height": 720
+ },
+ {
+ "target": "demand",
+ "time": 10.08
+ },
+ {
+ "target": "demand",
+ "time": 10.12
+ },
+ {
+ "target": "demand",
+ "time": 10.16
+ },
+ {
+ "target": "demand",
+ "time": 10.2
+ },
+ {
+ "target": "destroy"
+ }
+ ],
+ "received": [
+ {
+ "target": "ready"
+ },
+ {
+ "target": "verifyColorSpace"
+ },
+ {
+ "target": "render",
+ "images": 0,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "verifyColorSpace"
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 1280,
+ "height": 720
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ }
+ ]
+ }
+ ],
+ "snapshots": [
+ {
+ "phase": "before-seek",
+ "visible": 2676,
+ "signature": 1690974404,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 0.532466,
+ "connected": true
+ },
+ {
+ "phase": "after-seek",
+ "visible": 6216,
+ "signature": 3526987101,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 10.000023,
+ "connected": true
+ },
+ {
+ "phase": "fullscreen",
+ "visible": 22298,
+ "signature": 2781790832,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.161754,
+ "connected": true
+ }
+ ],
+ "errors": [],
+ "lastPixel": {
+ "phase": "fullscreen",
+ "visible": 22298,
+ "signature": 2781790832,
+ "width": 1280,
+ "height": 720,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.161754,
+ "connected": true
+ }
+ },
+ "limitation": "Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance."
+ },
+ "screenshots": [
+ {
+ "name": "actual-jassub-page",
+ "sha256": "65fbdabc25f645a591068987a59b4b9ecb073738c87d7d83876a6b6b6a053c3e"
+ }
+ ]
+ },
+ {
+ "title": "JASSUB published 1.1.0 renders actual WASM subtitles through seek and layout with published-5.3.1-beta.1 core",
+ "engine": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "errors": [],
+ "browser": {
+ "version": "155.0",
+ "platform": "win32",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": [
+ {
+ "url": "http://127.0.0.1:8084/assets/sample/steve-jobs.mp4",
+ "resourceType": "media",
+ "failure": {
+ "errorText": "NS_ERROR_PARSED_DATA_CACHED"
+ }
+ }
+ ]
+ },
+ "evidence": {
+ "core": "published-5.3.1-beta.1",
+ "onDemandRender": true,
+ "release": {
+ "version": "1.1.0",
+ "integrity": "sha512-vQwEtZMmkwr5utbxfUAeE/gRccl2nfcz+SYP2Subn/L8cVIFdMXWMgicm7ZkPVtAoBbCav7p0yP4pwXkCKMzAg==",
+ "member": "package/dist/artplayer-plugin-jassub.js",
+ "sha256": "271d5284e5246fbd04575526510b3e381e5e60d414c030acecce92fcf2e9b10a"
+ },
+ "subtitleSha256": "12994d1b6a07c45fd9776cb8709afc37725a8003bd90e4e829b7cc44fbadc0ec",
+ "resources": [
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker.js",
+ "status": 200,
+ "contentType": "text/javascript"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/jassub-worker-modern.wasm",
+ "status": 200,
+ "contentType": "application/wasm"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ },
+ {
+ "url": "http://127.0.0.1:8084/assets/jassub/default.woff2",
+ "status": 200,
+ "contentType": "font/woff2"
+ }
+ ],
+ "external": [],
+ "state": {
+ "workers": [
+ {
+ "url": "/assets/jassub/jassub-worker.js",
+ "terminated": 1,
+ "sent": [
+ {
+ "target": "init",
+ "width": 300,
+ "height": 150
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "canvas",
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "demand",
+ "time": 0.44
+ },
+ {
+ "target": "getColorSpace"
+ },
+ {
+ "target": "demand",
+ "time": 0.52
+ },
+ {
+ "target": "demand",
+ "time": 0.56
+ },
+ {
+ "target": "demand",
+ "time": 0.6
+ },
+ {
+ "target": "demand",
+ "time": 10
+ },
+ {
+ "target": "demand",
+ "time": 10
+ },
+ {
+ "target": "canvas",
+ "width": 1280,
+ "height": 720
+ },
+ {
+ "target": "demand",
+ "time": 10.04
+ },
+ {
+ "target": "demand",
+ "time": 10.08
+ },
+ {
+ "target": "demand",
+ "time": 10.12
+ },
+ {
+ "target": "destroy"
+ }
+ ],
+ "received": [
+ {
+ "target": "ready"
+ },
+ {
+ "target": "verifyColorSpace"
+ },
+ {
+ "target": "render",
+ "images": 0,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "verifyColorSpace"
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 640,
+ "height": 360
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "render",
+ "images": 2,
+ "width": 1280,
+ "height": 720
+ },
+ {
+ "target": "unbusy"
+ },
+ {
+ "target": "unbusy"
+ }
+ ]
+ }
+ ],
+ "snapshots": [
+ {
+ "phase": "before-seek",
+ "visible": 2676,
+ "signature": 1690974404,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 0.571282,
+ "connected": true
+ },
+ {
+ "phase": "after-seek",
+ "visible": 6216,
+ "signature": 3526987101,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 640,
+ "cssHeight": 360,
+ "time": 10.000023,
+ "connected": true
+ },
+ {
+ "phase": "fullscreen",
+ "visible": 6216,
+ "signature": 3526987101,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.045326,
+ "connected": true
+ }
+ ],
+ "errors": [],
+ "lastPixel": {
+ "phase": "fullscreen",
+ "visible": 6216,
+ "signature": 3526987101,
+ "width": 640,
+ "height": 360,
+ "cssWidth": 1280,
+ "cssHeight": 720,
+ "time": 10.045326,
+ "connected": true
+ }
+ },
+ "limitation": "Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance."
+ },
+ "screenshots": [
+ {
+ "name": "actual-jassub-page",
+ "sha256": "832a7ac308c27290f7ec6484e502f593271e1c1580bd4fe2dc44ecfab48bc2d2"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "limitations": [
+ "WebKit default frame-clock tests remain failing; explicit event-clock diagnosis is separate.",
+ "Offscreen rendering is explicitly disabled for pixel readback.",
+ "No native failure recovery, CSP, ratechange, default OffscreenCanvas, devices or sustained heap evidence.",
+ "Controlled removeChild uses assertion errors; Event rate payload is not an actual browser structured-clone failure proof.",
+ "Historical vendor remains unchanged; candidate repair is later work."
+ ]
+}
diff --git a/refactor/changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md b/refactor/changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md
new file mode 100644
index 000000000..bc4e03c96
--- /dev/null
+++ b/refactor/changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md
@@ -0,0 +1,72 @@
+# PKG-JASSUB-02 行为、错误与原生渲染基线
+
+本步骤建立迁移前基线,生产源码、第三方文件和资源未修改。新增的历史失败
+断言证明问题存在,不能当作修复通过;后续 03/07 负责修复,05 完成组合验收。
+
+## 受控行为
+
+七份真实发布/冻结实现各九个失败或保护场景,共 63 项;既有工厂、入口和
+来源测试加上事件时钟与切换字幕轨道,`yarn test:jassub` 联合 120 项通过。
+实际运行 vendor JS,DOM/Worker/RVFC 为受控平台,以下六类问题均已复现:
+
+| 问题 | 具体结果与后续责任 |
+| --- | --- |
+| 直接 destroy 再宿主 destroy,或直接重复 destroy | 二次移除容器抛错;03 修宿主重复调用,07 保证公开实例重复销毁 |
+| setVideo 切到其他父容器再 destroy | 从新视频父节点删除旧容器失败,Worker 未终止、七监听器保留;07 |
+| 自定义 canvas | vendor 已创建 Worker,adapter 访问不存在的 _canvasParent.style 抛错,尚未注册宿主清理;03 |
+| Worker 构造抛 SecurityError | 原错误保留,但先前创建的容器和七监听器遗留;07 |
+| setVideo 后旧 RVFC 到达 | 实际已排队的旧回调以 time=123 驱动 currentTime=9 的新视频,再建立第二条回调链;07 |
+| 事件模式 ratechange | 直接 setRate(2) 正常;媒体 ratechange 却将 Event 当作 rate 传给 Worker,未使用 playbackRate=1.5;07 |
+
+测试还保护已有正常行为:ready 前 destroy 会终止 Worker 并去监听器,虽然测试
+能观察到终止后仍尝试发 init;Worker error 原样分发,宿主销毁仍能清理;
+destroy 后遗留的单个 RVFC 到达会退出且不再排队。不能将这些观察夸大成
+“终止后 Worker 仍实际执行”或“销毁后永远循环”。受控 removeChild 抛的是
+ERR_ASSERTION,不冒充原生 DOMException;rate 错误负载尚非真实 clone 异常证据。
+
+正常事件用例验证时间偏移、playing/seeking 状态,以及同步 setTrackByUrl、
+setTrack、freeTrack 的实际 Worker 消息顺序。保留公开 instance 与返回值。
+
+## 真实浏览器
+
+新 `jassub-native.spec.js` 加载真实 npm 1.1.0 主产物、原生 Worker、本地固定 WASM
+和 Liberation 字体,使用两条自编 ASS 字幕及实际视频。明确设置
+offscreenRender=false 以读取真实字幕 canvas 像素,其他默认路径未冒充验收。
+原生 Worker 子类只记录 postMessage/message/terminate,仍委托浏览器原实现。
+断言初始非透明像素、seek 后不同字幕像素、网页全屏尺寸和正常单次销毁。
+
+Chromium/Firefox 各三个核心组合通过:候选、实际 npm 5.4.0、实际 5.3.1-beta.1。
+截图确认字幕已由真实 WASM 绘制。首次 Chromium 三例失败是测试服务器未提供
+application/wasm,补齐 MIME 后通过;没有改 SDK、替换编译器或放宽等待时间。
+
+Windows WebKit 26.6 的三个默认 onDemandRender 场景均没有可见字幕。保留的
+进一步观测显示:资源 200、Worker ready,但媒体已到 6.4992094 秒仍只有 init/
+canvas 消息,没有 demand 请求;canvas 为零可见像素。显式设置旧选项
+onDemandRender=false 的候选核心诊断通过。这个诊断不能替代默认路径验收,
+也不足以断言所有 Safari/设备不支持 RVFC。新增 JASSUB-NATIVE-FRAME-01,后续
+07/05 继续验证原因与兼容处理。默认 WebKit 用例保持失败,不加 skip、重试或
+扩大超时。02 的完成只表示已建立成功/失败基线,不表示全浏览器绿色。
+
+## 维护与验收边界
+
+```sh
+yarn test:jassub
+yarn test:browser test/browser/jassub-native.spec.js --workers=1
+```
+
+历史失败测试进入既有 baseline glob 和 test:jassub。设置
+ARTPLAYER_JASSUB_FAILURE_REPORT 可输出详细受控观察;默认测试不写报告。
+原生诊断可设置 ARTPLAYER_JASSUB_ON_DEMAND=false,报告保存该选择。按不同运行
+分别归档 report/results,等待进程终态后再跑下一轮。机器报告包含成功、失败和
+诊断结果,见 [验证](../baselines/jassub-failures-validation.json)。
+
+自有 adapter 修复与 vendor 方法修复必须分开。新增 PKG-JASSUB-07,在 03 之后、
+05 之前处理有证据的 vendor 生命周期/时钟问题;保留原文件及来源基线,任何
+实际补丁都必须单列差异与候选正例,不得把未修改 vendor 宣称为已修复。
+完整设备、默认 OffscreenCanvas、真实资源失败、CSP、倍率、长时播放/内存和
+VideoFrame/ResizeObserver 回收仍属后续验证。源码/类型/发布门槛全部保留。
+
+本次新增测试及测试服务器 MIME 修正,定向 lint、计划和风险检查通过;完整
+浏览器流程仍有上述 WebKit 失败,不宣称 CI 或后续组合验收通过。
+没有新依赖、锁文件或生产产物变化。独立本地提交,不推送或发布;回退本步骤
+仅移除新基线/测试与 MIME 支持,生产行为保持原样。
diff --git a/refactor/plan.md b/refactor/plan.md
index 131c17f7c..b371a788d 100644
--- a/refactor/plan.md
+++ b/refactor/plan.md
@@ -2,9 +2,9 @@
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
-基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 231 项,范围 22 个包及工作区/示例。
+基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 232 项,范围 22 个包及工作区/示例。
-状态:todo 65 / doing 14 / blocked 0 / done 152 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
+状态:todo 65 / doing 14 / blocked 0 / done 153 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -27,7 +27,7 @@
| artplayer-plugin-dash-control | 1.1.0 | PKG-DASH-01, PKG-DASH-02, PKG-DASH-03, PKG-DASH-04, PKG-DASH-05, PKG-DASH-06 |
| artplayer-plugin-document-pip | 1.1.0 | PKG-DPIP-01, PKG-DPIP-02, PKG-DPIP-03, PKG-DPIP-04, PKG-DPIP-05, PKG-DPIP-06 |
| artplayer-plugin-hls-control | 1.1.0 | PKG-HLS-01, PKG-HLS-02, PKG-HLS-03, PKG-HLS-04, PKG-HLS-SDK-01, PKG-HLS-05, PKG-HLS-06 |
-| artplayer-plugin-jassub | 1.1.0 | PKG-JASSUB-01, PKG-JASSUB-02, PKG-JASSUB-03, PKG-JASSUB-04, PKG-JASSUB-05, PKG-JASSUB-06 |
+| artplayer-plugin-jassub | 1.1.0 | PKG-JASSUB-01, PKG-JASSUB-02, PKG-JASSUB-03, PKG-JASSUB-04, PKG-JASSUB-05, PKG-JASSUB-06, PKG-JASSUB-07 |
| artplayer-plugin-multiple-subtitles | 1.2.0 | PKG-MULTI-SUB-01, PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-04, PKG-MULTI-SUB-07, PKG-MULTI-SUB-05, PKG-MULTI-SUB-06 |
| artplayer-plugin-vast | 1.2.0 | PKG-VAST-01, PKG-VAST-02, PKG-VAST-03, PKG-VAST-04, PKG-VAST-05, PKG-VAST-06 |
| artplayer-plugin-vtt-thumbnail | 1.1.0 | PKG-VTT-THUMB-01, PKG-VTT-THUMB-02, PKG-VTT-THUMB-03, PKG-VTT-THUMB-04, PKG-VTT-THUMB-05, PKG-VTT-THUMB-06 |
@@ -230,11 +230,12 @@
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
| --- | --- | --- | --- | --- | --- | --- |
| PKG-JASSUB-01 | artplayer-plugin-jassub
核对包契约与历史用法 | BASE-05 | 选项透传、result.instance、vendor 来源、worker/WASM/font 路径 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结;接续 VENDOR-04/05,核对 wrapper/worker/WASM/font 的独立来源与通知,保持资源路径和选项透传 | H | done |
-| PKG-JASSUB-02 | artplayer-plugin-jassub
建立特有行为与错误测试 | PKG-JASSUB-01, ENG-03, ENG-05 | ASS 字体、时钟/seek/倍率/resize、加载失败和销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | todo |
+| PKG-JASSUB-02 | artplayer-plugin-jassub
建立特有行为与错误测试 | PKG-JASSUB-01, ENG-03, ENG-05 | ASS 字体、时钟/seek/倍率/resize、加载失败和销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-JASSUB-03 | artplayer-plugin-jassub
整理内部职责与资源 | PKG-JASSUB-02, CORE-15, CORE-16 | 仅整理自有 adapter/销毁;保留第三方文件及来源 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
| PKG-JASSUB-04 | artplayer-plugin-jassub
迁移自有源码和公开类型 | PKG-JASSUB-03, ENG-04, ENG-06, CORE-07 | JASSUB option/instance 的兼容类型包装,vendor JS 例外记录 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
-| PKG-JASSUB-05 | artplayer-plugin-jassub
验证新旧核心和组合 | PKG-JASSUB-04, CORE-22 | 真实 worker/WASM 字幕渲染与全屏、旧核心测试 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
+| PKG-JASSUB-05 | artplayer-plugin-jassub
验证新旧核心和组合 | PKG-JASSUB-04, CORE-22, PKG-JASSUB-07 | 真实 worker/WASM 字幕渲染与全屏、旧核心测试 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-JASSUB-06 | artplayer-plugin-jassub
验证分发并同步文档 | PKG-JASSUB-05, ENG-07 | jassub.js、外部资源路径、许可和离线失败记录;接续 VENDOR-04/05 完整组件/字体通知、LGPL 源码分发核对及六份未明确字体的分发处置 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录;来源身份已由01冻结,完整通知和未明确字体的发布处理必须闭环,不能以来源相同替代许可结论 | H | todo |
+| PKG-JASSUB-07 | artplayer-plugin-jassub
修复 vendor 生命周期与字幕时钟 | PKG-JASSUB-03, PKG-JASSUB-02 | 独立记录 vendor 适配差异;幂等销毁、跨容器 setVideo、构造回滚、逐帧归属和倍率/能力边界 | 保持公开真实 instance、同步方法与资源路径;保留原 vendor 来源指纹并记录每项实际补丁;候选修复正例和真实字幕/失败/时钟验证通过,未证实设备不得冒充通过 | H | todo |
## 5 包迁移:artplayer-plugin-danmuku-mask
@@ -532,6 +533,7 @@
- PKG-MULTI-SUB-04: [记录](baselines/multiple-subtitles-runtime-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-runtime-types.md) [记录](baselines/multiple-subtitles-public-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-public-types.md) [记录](type-compatibility-policy.md) [记录](baselines/multiple-subtitles-approved-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-approved-types.md)
- PKG-MULTI-SUB-07: [记录](baselines/multiple-subtitles-timestamps.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md)
- PKG-JASSUB-01: [记录](baselines/jassub-release.json) [记录](baselines/jassub-vendor.json) [记录](baselines/jassub-font-metadata.json) [记录](baselines/jassub-contract.md) [记录](baselines/jassub-contract-validation.json) [记录](changes/2026-09-13-PKG-JASSUB-01-baseline.md) [记录](baselines/jassub-provenance.json) [记录](baselines/jassub-provenance-validation.json) [记录](changes/2026-09-14-PKG-JASSUB-01-provenance.md)
+- PKG-JASSUB-02: [记录](changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md) [记录](baselines/jassub-failures-validation.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)
diff --git a/refactor/progress.md b/refactor/progress.md
index 64199d837..7962784aa 100644
--- a/refactor/progress.md
+++ b/refactor/progress.md
@@ -1,5 +1,14 @@
# 进度与证据
+## PKG-JASSUB-02 历史缺陷与原生渲染基线完成
+
+联合 120 项通过,其中 63 项确认旧失败/已有保护;没有宣称修复。Chromium/Firefox
+实际 npm 插件和 WASM/字体六个核心组合通过,WebKit 默认逐帧路径仍失败,显式
+事件时钟诊断单独通过。测试 server 的 WASM MIME 错误已修正并保存首次失败。
+[基线记录](changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md)明确新增六项风险;
+03 处理自有 adapter,新增 07 专门修 vendor 生命周期/时钟且纳入 05 前置。生产文件
+未改,02 仅完成基线职责,全部兼容与发布门槛保留。
+
## PKG-JASSUB-01 来源及契约核对完成
两份 WASM 与 11 份字体的上游 blob 已精确匹配,nightly 对应源码、七子模块和
diff --git a/refactor/risk-table.md b/refactor/risk-table.md
index beeaccceb..c6f44f5a1 100644
--- a/refactor/risk-table.md
+++ b/refactor/risk-table.md
@@ -242,3 +242,9 @@
| 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-CANVAS-01 | open / 已复现 | 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 |
diff --git a/refactor/risks.json b/refactor/risks.json
index 807f9c7f4..1942c4a1a 100644
--- a/refactor/risks.json
+++ b/refactor/risks.json
@@ -5425,6 +5425,104 @@
],
"compatibleResolution": "Retain startup sampling evidence and distinguish it from post-model-ready delivery; investigate actual startup scheduling without silently adding catch-up semantics.",
"closureCriteria": "Identify the startup scheduling cause and assess a compatible mitigation or explicitly reviewed limitation using actual model/media evidence; post-ready success alone cannot close this risk."
+ },
+ {
+ "id": "JASSUB-DESTROY-01",
+ "title": "Repeated destruction or cross-parent setVideo can throw before JASSUB cleanup",
+ "confirmation": "reproduced",
+ "status": "open",
+ "owners": [
+ "PKG-JASSUB-02",
+ "PKG-JASSUB-03",
+ "PKG-JASSUB-07"
+ ],
+ "evidence": [
+ "refactor/changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md",
+ "refactor/baselines/jassub-failures-validation.json"
+ ],
+ "compatibleResolution": "Use actual resource ownership and tolerate direct instance teardown before host teardown.",
+ "closureCriteria": "Candidate direct/repeated/host teardown and cross-parent video replacement release container, Worker and listeners without throwing or removing user nodes."
+ },
+ {
+ "id": "JASSUB-CANVAS-01",
+ "title": "Custom canvas registration throws after JASSUB Worker allocation",
+ "confirmation": "reproduced",
+ "status": "open",
+ "owners": [
+ "PKG-JASSUB-02",
+ "PKG-JASSUB-03"
+ ],
+ "evidence": [
+ "refactor/changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md",
+ "refactor/baselines/jassub-failures-validation.json"
+ ],
+ "compatibleResolution": "Guard adapter-created parent styling; retain the supplied canvas and roll back failed registration.",
+ "closureCriteria": "Custom canvas registration succeeds and cleanup releases owned work while preserving caller DOM; public instance and option override behavior remain compatible."
+ },
+ {
+ "id": "JASSUB-CONSTRUCTION-01",
+ "title": "Worker construction failure leaves previously allocated JASSUB DOM and listeners",
+ "confirmation": "reproduced",
+ "status": "open",
+ "owners": [
+ "PKG-JASSUB-02",
+ "PKG-JASSUB-07"
+ ],
+ "evidence": [
+ "refactor/changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md",
+ "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."
+ },
+ {
+ "id": "JASSUB-VIDEO-FRAME-01",
+ "title": "Old video frame callbacks can drive replacement subtitle time and duplicate chains",
+ "confirmation": "reproduced",
+ "status": "open",
+ "owners": [
+ "PKG-JASSUB-02",
+ "PKG-JASSUB-07"
+ ],
+ "evidence": [
+ "refactor/changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md",
+ "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."
+ },
+ {
+ "id": "JASSUB-RATE-01",
+ "title": "Fallback ratechange sends an Event instead of numeric playbackRate",
+ "confirmation": "reproduced",
+ "status": "open",
+ "owners": [
+ "PKG-JASSUB-02",
+ "PKG-JASSUB-07"
+ ],
+ "evidence": [
+ "refactor/changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md",
+ "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."
+ },
+ {
+ "id": "JASSUB-NATIVE-FRAME-01",
+ "title": "Windows WebKit default frame-clock path does not produce subtitle demands in actual playback",
+ "confirmation": "reproduced",
+ "status": "open",
+ "owners": [
+ "PKG-JASSUB-02",
+ "PKG-JASSUB-07",
+ "PKG-JASSUB-05"
+ ],
+ "evidence": [
+ "refactor/changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md",
+ "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."
}
]
}
diff --git a/refactor/scripts/jassub-failures.test.mjs b/refactor/scripts/jassub-failures.test.mjs
new file mode 100644
index 000000000..081d5a125
--- /dev/null
+++ b/refactor/scripts/jassub-failures.test.mjs
@@ -0,0 +1,239 @@
+import assert from 'node:assert/strict'
+import fs from 'node:fs'
+import process from 'node:process'
+// eslint-disable-next-line test/no-import-node-test -- Frozen failure observations use the repository runner.
+import test from 'node:test'
+import { jassubEnvironment, jassubHistorical } from '../../test/helpers/jassub.js'
+
+const observations = []
+const implementations = await jassubHistorical()
+
+function environment(implementation) {
+ // Expose only VM platform constructors for controlled faults; vendor methods stay unchanged.
+ const code = `${implementation.code}\nObject.defineProperty(module.exports.default || module.exports, '__reviewPlatform', { value: { createCanvas: () => document.createElement('canvas'), setGlobal: (name, value) => { globalThis[name] = value }, ErrorEvent, Video: HTMLVideoElement } });`
+ const env = jassubEnvironment({ ...implementation, code })
+ env.platform = env.factory.__reviewPlatform
+ return env
+}
+
+function trackListeners(video) {
+ const listeners = new Map()
+ const add = video.addEventListener
+ const remove = video.removeEventListener
+ video.addEventListener = function (type, callback, ...options) {
+ if (!listeners.has(type))
+ listeners.set(type, new Set())
+ listeners.get(type).add(callback)
+ return add.call(this, type, callback, ...options)
+ }
+ video.removeEventListener = function (type, callback, ...options) {
+ listeners.get(type)?.delete(callback)
+ return remove.call(this, type, callback, ...options)
+ }
+ return () => [...listeners.values()].reduce((count, entries) => count + entries.size, 0)
+}
+
+function frames(env) {
+ const pending = new Map()
+ const cancelled = []
+ let next = 0
+ env.platform.Video.prototype.requestVideoFrameCallback = function (callback) {
+ pending.set(++next, { video: this, callback })
+ return next
+ }
+ env.platform.Video.prototype.cancelVideoFrameCallback = function (id) {
+ cancelled.push(id)
+ pending.delete(id)
+ }
+ return {
+ pending,
+ cancelled,
+ deliver(video, metadata) {
+ const entry = [...pending].find(([, row]) => row.video === video)
+ assert(entry, 'must deliver an actually queued frame callback')
+ const [id, row] = entry
+ pending.delete(id)
+ row.callback(0, metadata)
+ },
+ }
+}
+
+for (const implementation of implementations) {
+ test(`JASSUB failure baseline ${implementation.name}: fallback ratechange forwards the Event instead of the numeric media rate`, async () => {
+ const env = environment(implementation)
+ const { instance } = env.factory()(env.art)
+ await env.ready()
+ instance.setRate(2)
+ await env.flush()
+ assert.equal(env.workers[0].messages.at(-1).rate, 2)
+ env.art.video.playbackRate = 1.5
+ const event = new Event('ratechange')
+ env.art.video.dispatchEvent(event)
+ await env.flush()
+ const message = env.workers[0].messages.at(-1)
+ assert.equal(message.target, 'video')
+ assert.equal(message.rate, event)
+ assert.notEqual(message.rate, env.art.video.playbackRate)
+ observations.push({ implementation: implementation.name, case: 'rate-event-payload', mediaRate: 1.5, forwardedType: message.rate.type, directNumericRatePreserved: true })
+ instance.destroy()
+ })
+
+ test(`JASSUB failure baseline ${implementation.name}: direct destroy followed by art destroy throws on the removed container`, async () => {
+ const env = environment(implementation)
+ const active = trackListeners(env.art.video)
+ const { instance } = env.factory()(env.art)
+ await env.ready()
+ assert.equal(active(), 7)
+ instance.destroy()
+ assert.equal(active(), 0)
+ assert.equal(env.workers[0].terminated, 1)
+ assert.equal(env.parent.children.length, 1)
+ assert.throws(() => env.emit('destroy'), { code: 'ERR_ASSERTION' })
+ assert.throws(() => instance.destroy(), { code: 'ERR_ASSERTION' })
+ assert.equal(env.workers[0].terminated, 1)
+ observations.push({ implementation: implementation.name, case: 'repeated-destroy', throws: true, workerTerminations: 1 })
+ })
+
+ test(`JASSUB failure baseline ${implementation.name}: setVideo across parents makes destroy throw before releasing the worker`, async () => {
+ const env = environment(implementation)
+ const { instance } = env.factory()(env.art)
+ await env.ready()
+ const secondParent = new env.parent.constructor()
+ const secondVideo = secondParent.appendChild(new env.platform.Video())
+ const active = trackListeners(secondVideo)
+ instance.setVideo(secondVideo)
+ assert.equal(active(), 7)
+ assert.throws(() => instance.destroy(), { code: 'ERR_ASSERTION' })
+ assert.notEqual(instance._destroyed, true)
+ assert.equal(env.workers[0].terminated, 0)
+ assert.equal(env.parent.children.length, 2)
+ assert.equal(active(), 7)
+ observations.push({ implementation: implementation.name, case: 'set-video-parent', workerTerminations: 0, listenersRetained: active(), originalContainerRetained: true })
+ instance.setVideo(env.art.video)
+ instance.destroy()
+ })
+
+ test(`JASSUB failure baseline ${implementation.name}: a supplied canvas makes the adapter throw after allocating the worker`, async () => {
+ const env = environment(implementation)
+ const active = trackListeners(env.art.video)
+ const canvas = env.parent.appendChild(env.platform.createCanvas())
+ assert.throws(() => env.factory({ canvas })(env.art), { name: 'TypeError', message: /reading 'style'/ })
+ await env.flush()
+ assert.equal(env.workers.length, 1)
+ assert.equal(env.workers[0].terminated, 0)
+ assert.equal(env.workers[0].messages[0].target, 'init')
+ assert.equal(env.listeners.has('destroy'), false)
+ assert.equal(canvas.parentNode, env.parent)
+ assert.equal(active(), 7)
+ observations.push({ implementation: implementation.name, case: 'custom-canvas', workerTerminations: 0, listenersRetained: active(), hostDestroyHandlerRegistered: false, initSent: true })
+ env.workers[0].terminate()
+ })
+
+ test(`JASSUB failure baseline ${implementation.name}: Worker construction failure leaves the already inserted container and video listeners`, async () => {
+ const env = environment(implementation)
+ const active = trackListeners(env.art.video)
+ const failure = new DOMException('Controlled Worker construction failure', 'SecurityError')
+ env.platform.setGlobal('Worker', class {
+ constructor() { throw failure }
+ })
+ assert.throws(() => env.factory()(env.art), error => error === failure)
+ await env.flush()
+ assert.equal(env.workers.length, 0)
+ assert.equal(env.listeners.has('destroy'), false)
+ assert.equal(env.parent.children.length, 2)
+ assert.equal(env.parent.children[1].className, 'JASSUB')
+ assert.equal(active(), 7)
+ observations.push({ implementation: implementation.name, case: 'worker-construction-failure', originalErrorPreserved: true, insertedContainerRetained: true, listenersRetained: active() })
+ })
+
+ test(`JASSUB failure baseline ${implementation.name}: a previous video's queued frame drives the replacement video and creates a second loop`, async () => {
+ const env = environment(implementation)
+ const clock = frames(env)
+ const oldVideo = env.art.video
+ const { instance } = env.factory()(env.art)
+ await env.ready()
+ assert.equal(clock.pending.size, 1)
+ const newVideo = env.createVideo()
+ newVideo.currentTime = 9
+ instance.setVideo(newVideo)
+ await env.flush()
+ assert.equal(clock.pending.size, 2)
+ assert.equal(clock.cancelled.length, 0)
+ clock.deliver(oldVideo, { mediaTime: 123, width: 640, height: 360 })
+ await env.flush()
+ assert.equal(env.workers[0].messages.filter(item => item.target === 'demand').at(-1).time, 123)
+ assert.equal([...clock.pending.values()].filter(row => row.video === newVideo).length, 2)
+ observations.push({ implementation: implementation.name, case: 'old-video-frame', replacementCurrentTime: newVideo.currentTime, staleDemandTime: 123, replacementPendingCallbacks: 2, cancelledCallbacks: 0 })
+ instance.destroy()
+ })
+
+ test(`JASSUB containment baseline ${implementation.name}: destroy before ready terminates and detaches, but initialization continues after termination`, async () => {
+ const env = environment(implementation)
+ const active = trackListeners(env.art.video)
+ const { instance } = env.factory()(env.art)
+ const worker = env.workers[0]
+ const post = worker.postMessage
+ const afterTermination = []
+ worker.postMessage = function (message) {
+ if (this.terminated)
+ afterTermination.push(message.target)
+ return post.call(this, message)
+ }
+ let settled = false
+ instance._loaded.then(() => {
+ settled = true
+ })
+ instance.setTrack('[Script Info]')
+ instance.destroy()
+ await env.flush()
+ assert.equal(worker.terminated, 1)
+ assert.equal(env.parent.children.length, 1)
+ assert.equal(active(), 0)
+ assert.deepEqual(afterTermination, ['init'])
+ assert.equal(settled, false)
+ assert.equal(worker.messages.some(item => item.target === 'setTrack'), false)
+ observations.push({ implementation: implementation.name, case: 'destroy-before-ready', workerTerminations: 1, listenersRetained: 0, postAfterTermination: afterTermination, loadedSettledWithinFixtureFlush: settled })
+ })
+
+ test(`JASSUB containment baseline ${implementation.name}: Worker errors reach the instance and explicit host destroy still releases resources`, async () => {
+ const env = environment(implementation)
+ const active = trackListeners(env.art.video)
+ const { instance } = env.factory()(env.art)
+ const failure = new Error('Controlled Worker script load failure')
+ const errors = []
+ instance.addEventListener('error', event => errors.push(event.error))
+ env.workers[0].onerror(new env.platform.ErrorEvent('error', { error: failure }))
+ await env.flush()
+ assert.deepEqual(errors, [failure])
+ assert.equal(env.errors.at(-1), failure)
+ assert.equal(env.workers[0].terminated, 0)
+ assert.equal(active(), 7)
+ env.emit('destroy')
+ assert.equal(env.workers[0].terminated, 1)
+ assert.equal(env.parent.children.length, 1)
+ assert.equal(active(), 0)
+ observations.push({ implementation: implementation.name, case: 'worker-error', exactErrorForwarded: true, automaticallyTerminated: false, explicitHostCleanup: true })
+ })
+
+ test(`JASSUB containment baseline ${implementation.name}: the outstanding frame after destroy does no work and does not rearm`, async () => {
+ const env = environment(implementation)
+ const clock = frames(env)
+ const { instance } = env.factory()(env.art)
+ await env.ready()
+ instance.destroy()
+ await env.flush()
+ assert.equal(clock.pending.size, 1)
+ assert.equal(clock.cancelled.length, 0)
+ const before = env.workers[0].messages.length
+ clock.deliver(env.art.video, { mediaTime: 123, width: 640, height: 360 })
+ await env.flush()
+ assert.equal(env.workers[0].messages.length, before)
+ assert.equal(clock.pending.size, 0)
+ observations.push({ implementation: implementation.name, case: 'frame-after-destroy', activelyCancelled: false, callbackDoesNoWork: true, callbackRearms: false })
+ })
+}
+
+test.after(() => {
+ if (process.env.ARTPLAYER_JASSUB_FAILURE_REPORT)
+ fs.writeFileSync(process.env.ARTPLAYER_JASSUB_FAILURE_REPORT, `${JSON.stringify(observations, null, 2)}\n`)
+})
diff --git a/refactor/tasks.json b/refactor/tasks.json
index 4006817c0..a824ab884 100644
--- a/refactor/tasks.json
+++ b/refactor/tasks.json
@@ -2434,11 +2434,14 @@
"ENG-03",
"ENG-05"
],
- "status": "todo",
+ "status": "done",
"risk": "H",
"deliverable": "ASS 字体、时钟/seek/倍率/resize、加载失败和销毁",
"acceptance": "旧版本行为可重跑,成功/失败/切源/销毁有必要断言",
- "evidence": []
+ "evidence": [
+ "changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md",
+ "baselines/jassub-failures-validation.json"
+ ]
},
{
"id": "PKG-JASSUB-03",
@@ -2486,7 +2489,8 @@
],
"dependsOn": [
"PKG-JASSUB-04",
- "CORE-22"
+ "CORE-22",
+ "PKG-JASSUB-07"
],
"status": "todo",
"risk": "H",
@@ -2511,6 +2515,23 @@
"acceptance": "tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录;来源身份已由01冻结,完整通知和未明确字体的发布处理必须闭环,不能以来源相同替代许可结论",
"evidence": []
},
+ {
+ "id": "PKG-JASSUB-07",
+ "phase": "5 包迁移:artplayer-plugin-jassub",
+ "title": "修复 vendor 生命周期与字幕时钟",
+ "scope": [
+ "artplayer-plugin-jassub"
+ ],
+ "dependsOn": [
+ "PKG-JASSUB-03",
+ "PKG-JASSUB-02"
+ ],
+ "status": "todo",
+ "risk": "H",
+ "deliverable": "独立记录 vendor 适配差异;幂等销毁、跨容器 setVideo、构造回滚、逐帧归属和倍率/能力边界",
+ "acceptance": "保持公开真实 instance、同步方法与资源路径;保留原 vendor 来源指纹并记录每项实际补丁;候选修复正例和真实字幕/失败/时钟验证通过,未证实设备不得冒充通过",
+ "evidence": []
+ },
{
"id": "PKG-MASK-01",
"phase": "5 包迁移:artplayer-plugin-danmuku-mask",
diff --git a/test/browser/README.md b/test/browser/README.md
index 10571dd0a..90a0aaa5c 100644
--- a/test/browser/README.md
+++ b/test/browser/README.md
@@ -169,3 +169,12 @@ The startup time=6 row remains diagnostic because initial model work can coincid
with a missed narrow timestamp window. A passing post-ready combination is not
proof of startup losslessness, private GPU/WASM closure, OS fullscreen or devices.
See refactor/changes/2026-09-14-PKG-MASK-05-native-checkpoint.md for exact limits.
+
+`jassub-native.spec.js` runs actual published JASSUB 1.1.0, its native Worker,
+fixed local WASM/font and authored ASS cues with offscreenRender=false. It checks
+real canvas pixels before/after seek, CSS web fullscreen and normal destruction.
+Windows WebKit currently fails the default frame-clock path; the explicitly
+selected ARTPLAYER_JASSUB_ON_DEMAND=false diagnostic passes on the candidate core.
+This is a retained historical failure, not a waived browser gate. The server must
+serve WASM as application/wasm for native streaming compilation. Archive failed
+and successful runs independently. See the PKG-JASSUB-02 change record.
diff --git a/test/browser/jassub-native.spec.js b/test/browser/jassub-native.spec.js
new file mode 100644
index 000000000..570ed4481
--- /dev/null
+++ b/test/browser/jassub-native.spec.js
@@ -0,0 +1,150 @@
+import fs from 'node:fs'
+import process from 'node:process'
+import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
+import { expect, test } from './fixtures.js'
+
+const baseline = JSON.parse(fs.readFileSync('refactor/baselines/jassub-release.json', 'utf8'))
+const release = baseline.release
+const archive = await ensureArchive(release)
+const member = `package/${release.manifest.main.replace(/^\.\//, '')}`
+const code = readMember(archive, member).toString()
+const onDemandRender = process.env.ARTPLAYER_JASSUB_ON_DEMAND !== 'false'
+const subtitles = `[Script Info]
+ScriptType: v4.00+
+PlayResX: 640
+PlayResY: 360
+[V4+ Styles]
+Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
+Style: Default,Liberation Sans,36,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,20,1
+[Events]
+Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
+Dialogue: 0,0:00:00.00,0:00:04.00,Default,,0,0,0,,Before seek
+Dialogue: 0,0:00:05.00,0:00:30.00,Default,,0,0,0,,After seek with actual WASM
+`
+
+for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
+ test(`JASSUB published ${release.version} renders actual WASM subtitles through seek and layout with ${core} core`, async ({ page, context }, testInfo) => {
+ const resources = []
+ const external = []
+ const origin = new URL(testInfo.project.use.baseURL).origin
+ await context.route('**/*', (route) => {
+ if (new URL(route.request().url()).origin !== origin) {
+ external.push(route.request().url())
+ return route.abort()
+ }
+ return route.continue()
+ })
+ context.on('response', (response) => {
+ if (response.url().includes('/assets/jassub/'))
+ resources.push({ url: response.url(), status: response.status(), contentType: response.headers()['content-type'] })
+ })
+ await page.goto(`/test/player.html?core=${core}`)
+ await page.addScriptTag({ content: code })
+ await page.evaluate(() => {
+ window.jassubNative = { workers: [], snapshots: [], errors: [] }
+ const NativeWorker = window.Worker
+ window.Worker = class extends NativeWorker {
+ constructor(...args) {
+ super(...args)
+ this.observation = { url: String(args[0]), terminated: 0, sent: [], received: [] }
+ window.jassubNative.workers.push(this.observation)
+ this.addEventListener('message', ({ data }) => this.observation.received.push({ target: data.target, images: data.images?.length, width: data.width, height: data.height }))
+ }
+
+ postMessage(message, ...rest) {
+ this.observation.sent.push({ target: message.target, time: message.time, width: message.width, height: message.height })
+ return super.postMessage(message, ...rest)
+ }
+
+ terminate() {
+ this.observation.terminated++
+ return super.terminate()
+ }
+ }
+ window.createPlayer('/assets/sample/steve-jobs.mp4')
+ })
+ try {
+ await expect.poll(() => page.evaluate(() => window.art.duration)).toBeGreaterThan(30)
+ await page.locator('#play').click()
+ await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
+ await page.locator('#pause').click()
+ await page.evaluate(async ({ subContent, onDemandRender }) => {
+ const register = window.artplayerPluginJassub.default || window.artplayerPluginJassub
+ window.art.plugins.add(register({
+ subContent,
+ workerUrl: '/assets/jassub/jassub-worker.js',
+ wasmUrl: '/assets/jassub/jassub-worker.wasm',
+ modernWasmUrl: '/assets/jassub/jassub-worker-modern.wasm',
+ availableFonts: { 'liberation sans': '/assets/jassub/default.woff2' },
+ fonts: ['/assets/jassub/default.woff2'],
+ offscreenRender: false,
+ onDemandRender,
+ }))
+ window.jassub = window.art.plugins.artplayerPluginJassub.instance
+ window.jassub.addEventListener('error', event => window.jassubNative.errors.push(String(event.error)))
+ await new Promise((resolve, reject) => {
+ window.jassub.addEventListener('ready', resolve, { once: true })
+ window.jassub.addEventListener('error', event => reject(event.error), { once: true })
+ })
+ window.jassubPixels = (phase) => {
+ const canvas = window.jassub._canvas
+ const rect = canvas.getBoundingClientRect()
+ const pixels = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data
+ let visible = 0
+ let signature = 0
+ for (let i = 3; i < pixels.length; i += 4) {
+ if (pixels[i] > 0) {
+ visible++
+ signature = (signature + i * pixels[i]) >>> 0
+ }
+ }
+ const state = { phase, visible, signature, width: canvas.width, height: canvas.height, cssWidth: rect.width, cssHeight: rect.height, time: window.art.currentTime, connected: canvas.isConnected }
+ window.jassubNative.lastPixel = state
+ if (phase)
+ window.jassubNative.snapshots.push(state)
+ return state
+ }
+ }, { subContent: subtitles, onDemandRender })
+ await page.locator('#play').click()
+ await expect.poll(() => page.evaluate(() => window.jassubPixels().visible)).toBeGreaterThan(100)
+ const before = await page.evaluate(() => window.jassubPixels('before-seek'))
+ expect(before.connected).toBe(true)
+ await page.locator('#pause').click()
+ await page.evaluate(async () => {
+ const seeked = new Promise(resolve => window.art.video.addEventListener('seeked', resolve, { once: true }))
+ window.art.currentTime = 10
+ await seeked
+ })
+ await page.locator('#play').click()
+ await expect.poll(() => page.evaluate(() => window.jassubPixels().signature)).not.toBe(before.signature)
+ await expect.poll(() => page.evaluate(() => window.jassubPixels().visible)).toBeGreaterThan(100)
+ const after = await page.evaluate(() => window.jassubPixels('after-seek'))
+ expect(after.time).toBeGreaterThanOrEqual(10)
+ expect(after.signature).not.toBe(before.signature)
+ await page.evaluate(() => {
+ window.art.fullscreenWeb = true
+ })
+ await expect.poll(() => page.evaluate(() => window.jassubPixels().cssWidth)).toBeGreaterThan(before.cssWidth)
+ await expect.poll(() => page.evaluate(() => window.jassubPixels().visible)).toBeGreaterThan(100)
+ await page.evaluate(() => window.jassubPixels('fullscreen'))
+ await testInfo.attach('actual-jassub-page', { body: await page.screenshot(), contentType: 'image/png' })
+ await page.evaluate(() => window.art.destroy(false))
+ expect(await page.evaluate(() => document.querySelectorAll('.JASSUB').length)).toBe(0)
+ expect(await page.evaluate(() => window.jassubNative.workers.map(worker => worker.terminated))).toEqual([1])
+ expect(await page.evaluate(() => window.jassubNative.errors)).toEqual([])
+ expect(external).toEqual([])
+ expect(resources.some(item => item.url.endsWith('.wasm') && item.status === 200 && item.contentType === 'application/wasm')).toBe(true)
+ expect(resources.some(item => item.url.endsWith('.woff2') && item.status === 200)).toBe(true)
+ }
+ finally {
+ const state = await page.evaluate(() => window.jassubNative).catch(error => ({ unavailable: error.message }))
+ await testInfo.attach('actual-jassub-evidence', { body: JSON.stringify({ core, onDemandRender, release: { version: release.version, integrity: release.integrity, member, sha256: hash(code) }, subtitleSha256: hash(subtitles), resources, external, state, limitation: 'Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance.' }, null, 2), contentType: 'application/json' })
+ if (!page.isClosed()) {
+ await page.evaluate(() => {
+ if (window.art && !window.art.isDestroy)
+ window.art.destroy()
+ })
+ }
+ }
+ })
+}
diff --git a/test/browser/server.mjs b/test/browser/server.mjs
index eb88dc8e8..75378159b 100644
--- a/test/browser/server.mjs
+++ b/test/browser/server.mjs
@@ -83,7 +83,7 @@ add('/test/audio-tone.m4a', fs.readFileSync(path.join(workspace, 'test/browser/m
add('/test/thumbnail-pattern.mp4', fs.readFileSync(path.join(workspace, 'test/browser/media/thumbnail-pattern.mp4')), { kind: 'generated-media', file: 'test/browser/media/thumbnail-pattern.mp4' })
add('/test/auto-thumbnail-timeline.mp4', fs.readFileSync(path.join(workspace, 'test/browser/media/auto-thumbnail-timeline.mp4')), { kind: 'generated-media', file: 'test/browser/media/auto-thumbnail-timeline.mp4' })
-const mime = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.mp4': 'video/mp4', '.m4a': 'audio/mp4', '.webm': 'video/webm', '.vtt': 'text/vtt', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg' }
+const mime = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.mp4': 'video/mp4', '.m4a': 'audio/mp4', '.webm': 'video/webm', '.vtt': 'text/vtt', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.wasm': 'application/wasm', '.woff2': 'font/woff2' }
add('/test/declaration-cues.vtt', fs.readFileSync(path.join(workspace, 'test/browser/media/declaration-cues.vtt')), { kind: 'test-subtitles', file: 'test/browser/media/declaration-cues.vtt' })
add('/test/thumbnail-grid.svg', fs.readFileSync(path.join(workspace, 'test/browser/media/thumbnail-grid.svg')), { kind: 'test-thumbnail-grid', file: 'test/browser/media/thumbnail-grid.svg' })
add('/test/legacy-safe-area.js', fs.readFileSync(path.join(workspace, 'test/helpers/legacy-safe-area.js')), { kind: 'frozen-own-source', file: 'test/helpers/legacy-safe-area.js', commit: 'ccf77c4e' })
diff --git a/test/jassub.test.js b/test/jassub.test.js
index 15ad75d4c..5043b96ac 100644
--- a/test/jassub.test.js
+++ b/test/jassub.test.js
@@ -76,4 +76,30 @@ for (const implementation of implementations) {
assert.equal(init.availableFonts['liberation sans'], './default.woff2')
result.instance.destroy()
})
+ test(`JASSUB ${implementation.name}: fallback clock keeps offsets and synchronous track replacement preserves message order`, async () => {
+ const env = jassubEnvironment(implementation)
+ const { instance } = env.factory({ timeOffset: -0.25 })(env.art)
+ await env.ready()
+ env.art.video.currentTime = 12
+ env.art.video.paused = false
+ env.art.video.dispatchEvent(new Event('playing'))
+ await env.flush()
+ const playing = env.workers[0].messages.at(-1)
+ assert.equal(playing.target, 'video')
+ assert.equal(playing.currentTime, 11.75)
+ assert.equal(playing.isPaused, false)
+ env.art.video.dispatchEvent(new Event('seeking'))
+ await env.flush()
+ assert.equal(env.workers[0].messages.at(-1).isPaused, true)
+ const start = env.workers[0].messages.length
+ assert.equal(instance.setTrackByUrl('/replacement.ass'), undefined)
+ assert.equal(instance.setTrack('[Script Info]\nTitle: Replacement'), undefined)
+ assert.equal(instance.freeTrack(), undefined)
+ await env.flush()
+ const messages = env.workers[0].messages.slice(start)
+ assert.deepEqual(messages.map(message => message.target), ['setTrackByUrl', 'setTrack', 'freeTrack'])
+ assert.equal(messages[0].url, '/replacement.ass')
+ assert.equal(messages[1].content, '[Script Info]\nTitle: Replacement')
+ instance.destroy()
+ })
}