mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-09 20:26:16 -08:00
test(danmuku-mask): [PKG-MASK-02] reproduce historical lifecycle and failure paths
This commit is contained in:
1 parent
d81b881085
commit
ccf3d6ed14
10 files changed
+952
-18
No files matched your search
+3
-2
@@ -51,7 +51,7 @@
|
||||
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
|
||||
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js test/asr-distribution.test.js",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
"test:unit": "node --test test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js 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 test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js test/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js test/chromecast.test.js test/chromecast-failures.test.js test/chromecast-runtime.test.js",
|
||||
"test:unit": "node --test test/danmuku-mask-failures.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js 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 test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js test/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js test/chromecast.test.js test/chromecast-failures.test.js test/chromecast-runtime.test.js",
|
||||
"test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs",
|
||||
"test": "yarn test:node && yarn test:baseline",
|
||||
"test:browser": "playwright test",
|
||||
@@ -98,7 +98,8 @@
|
||||
"test:asr-types-package": "node refactor/scripts/asr-package-types.mjs",
|
||||
"test:asr-demo": "node refactor/scripts/asr-local-demo.mjs",
|
||||
"test:chromecast": "node --test test/chromecast.test.js test/chromecast-failures.test.js test/chromecast-runtime.test.js",
|
||||
"test:chromecast-types-package": "node refactor/scripts/chromecast-package-types.mjs"
|
||||
"test:chromecast-types-package": "node refactor/scripts/chromecast-package-types.mjs",
|
||||
"test:danmuku-mask": "node --test test/danmuku-mask-failures.test.js refactor/scripts/danmuku-mask-contract.test.mjs"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
{
|
||||
"task": "PKG-MASK-02",
|
||||
"sourceCommit": "b0cfbfe3a09a84a295a257e45e4577847588d1cb",
|
||||
"node": "v24.21.0",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "frozen-source",
|
||||
"inputSha256": "2397900688f86bc74bc86a4e870f69478a651bd8df02bba543cdab832773fd6e",
|
||||
"transformedSha256": "5baf9875cfebd4bafad8091471f663f9a914e8ba208420e87b8005874f837228",
|
||||
"source": true,
|
||||
"replacements": []
|
||||
},
|
||||
{
|
||||
"name": "published-1.1.0-main",
|
||||
"archiveSha256": "96682e46cb140a84b164818dd118480d7be42b24afaab390027a25e52e8ae31c",
|
||||
"member": "package/dist/artplayer-plugin-danmuku-mask.js",
|
||||
"inputSha256": "07cebbfbf77f8ec4b2df6a77d7d7300345b86f2993e220ff14e1aae530e4fb31",
|
||||
"transformedSha256": "07cebbfbf77f8ec4b2df6a77d7d7300345b86f2993e220ff14e1aae530e4fb31",
|
||||
"probeSha256": "472b81399c806d2fbfc4abe40e3955017bec498ad1e3966fd894c4776150015f",
|
||||
"counts": {
|
||||
"setBackend": 2,
|
||||
"createSegmenter": 1,
|
||||
"toBinaryMask": 1,
|
||||
"drawMask": 1
|
||||
},
|
||||
"replacements": [
|
||||
{
|
||||
"start": 901335,
|
||||
"end": 901337,
|
||||
"target": "setBackend",
|
||||
"originalSha256": "3c42d9ac9807bbf91745b921d4887371b41b4a52e4e78f0dd127358e967dd4e0"
|
||||
},
|
||||
{
|
||||
"start": 901437,
|
||||
"end": 901439,
|
||||
"target": "setBackend",
|
||||
"originalSha256": "3c42d9ac9807bbf91745b921d4887371b41b4a52e4e78f0dd127358e967dd4e0"
|
||||
},
|
||||
{
|
||||
"start": 901764,
|
||||
"end": 902205,
|
||||
"target": "createSegmenter",
|
||||
"originalSha256": "3d8107811f464ec31d18f7b813a77e95492db7c324f5be67634fc550a4dc9fff"
|
||||
},
|
||||
{
|
||||
"start": 902561,
|
||||
"end": 903452,
|
||||
"target": "toBinaryMask",
|
||||
"originalSha256": "1fa52455c5c212f2b17b7de7b35bec4296492ba60b447d24ec8c7aef3e6734a7"
|
||||
},
|
||||
{
|
||||
"start": 903502,
|
||||
"end": 903504,
|
||||
"target": "drawMask",
|
||||
"originalSha256": "d1ee0b45da3fe74f3ebe628dc12fbd399eda5f374bd184104528a95e0ab5a757"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "published-1.1.0-legacy",
|
||||
"archiveSha256": "96682e46cb140a84b164818dd118480d7be42b24afaab390027a25e52e8ae31c",
|
||||
"member": "package/dist/artplayer-plugin-danmuku-mask.legacy.js",
|
||||
"inputSha256": "a678a3c0f6d3920bee3b66af9943b3612935028bff61c96677b8eaf8ef7cc8f0",
|
||||
"transformedSha256": "a678a3c0f6d3920bee3b66af9943b3612935028bff61c96677b8eaf8ef7cc8f0",
|
||||
"probeSha256": "c2fef32f4c07c8c52382b638f15b4539ee4d13b425734042b98881d5fe4ab1c9",
|
||||
"counts": {
|
||||
"setBackend": 2,
|
||||
"createSegmenter": 1,
|
||||
"toBinaryMask": 1,
|
||||
"drawMask": 1
|
||||
},
|
||||
"replacements": [
|
||||
{
|
||||
"start": 904198,
|
||||
"end": 904200,
|
||||
"target": "setBackend",
|
||||
"originalSha256": "0041fea20826cea952ac6411b2584c7d09c42dee7be4631e9fe6eadba3963b64"
|
||||
},
|
||||
{
|
||||
"start": 904300,
|
||||
"end": 904302,
|
||||
"target": "setBackend",
|
||||
"originalSha256": "0041fea20826cea952ac6411b2584c7d09c42dee7be4631e9fe6eadba3963b64"
|
||||
},
|
||||
{
|
||||
"start": 904629,
|
||||
"end": 905070,
|
||||
"target": "createSegmenter",
|
||||
"originalSha256": "d98df21622f139a06fde9e766a6bb24dd2919b03e2d4df908b42cedf302289ad"
|
||||
},
|
||||
{
|
||||
"start": 905453,
|
||||
"end": 906344,
|
||||
"target": "toBinaryMask",
|
||||
"originalSha256": "f77e943d57ef12cabd9b2c4cfefb2f88be20fb2169d865d6a1bf81b6ab04720a"
|
||||
},
|
||||
{
|
||||
"start": 906394,
|
||||
"end": 906396,
|
||||
"target": "drawMask",
|
||||
"originalSha256": "69d61997a241e97931db9dd1cfcef218041a752485f5f7956b09766287682da3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "published-1.1.0-module",
|
||||
"archiveSha256": "96682e46cb140a84b164818dd118480d7be42b24afaab390027a25e52e8ae31c",
|
||||
"member": "package/dist/artplayer-plugin-danmuku-mask.mjs",
|
||||
"inputSha256": "48f7a449dc9555ab4060a578902b5431701f81b2f093a7cb0526042a8c417400",
|
||||
"transformedSha256": "01b79edcfc8abf85006acb0e1b642636ce9845313fde1d8fb9d39c62a918939f",
|
||||
"probeSha256": "67d5c6dd94e27ce752b772481b66cb2bb0f275630a4ed2ca4642f1f1fde095f2",
|
||||
"counts": {
|
||||
"setBackend": 2,
|
||||
"createSegmenter": 1,
|
||||
"toBinaryMask": 1,
|
||||
"drawMask": 1
|
||||
},
|
||||
"replacements": [
|
||||
{
|
||||
"start": 1725698,
|
||||
"end": 1725708,
|
||||
"target": "setBackend",
|
||||
"originalSha256": "eeb426cebcc7b4b1377849d09fd1d50b3c61d85684675799a258f4e3aae8823a"
|
||||
},
|
||||
{
|
||||
"start": 1725846,
|
||||
"end": 1725856,
|
||||
"target": "setBackend",
|
||||
"originalSha256": "eeb426cebcc7b4b1377849d09fd1d50b3c61d85684675799a258f4e3aae8823a"
|
||||
},
|
||||
{
|
||||
"start": 1726444,
|
||||
"end": 1726446,
|
||||
"target": "createSegmenter",
|
||||
"originalSha256": "8e5202705183bd3a20a29e224499b0f77a8273ee33cd93cca71043c57ad4bdfc"
|
||||
},
|
||||
{
|
||||
"start": 1727942,
|
||||
"end": 1727944,
|
||||
"target": "toBinaryMask",
|
||||
"originalSha256": "799267762dffea91ee7fb9d426a552458f46166208b09d5e8d82b9c318a05d2f"
|
||||
},
|
||||
{
|
||||
"start": 1728116,
|
||||
"end": 1728118,
|
||||
"target": "drawMask",
|
||||
"originalSha256": "38cebf4836a918cb5a42873deff4065fe36689cbfd0a3e862ede3ebff766807e"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "published-1.0.0-main",
|
||||
"archiveSha256": "8a88f52ae843bed1be01a2e60b82cf886d33582f990a5016d49b1409c3493c4b",
|
||||
"member": "package/dist/artplayer-plugin-danmuku-mask.js",
|
||||
"inputSha256": "940cc240fcdb208427cb25d54180f9b84e0bfa96b5a4d3ba68fa9fb2ad8ffa16",
|
||||
"transformedSha256": "940cc240fcdb208427cb25d54180f9b84e0bfa96b5a4d3ba68fa9fb2ad8ffa16",
|
||||
"probeSha256": "93edf238803b68dc3b3192fdf9c85c12ad13224892e7aaca364394064b1fcc57",
|
||||
"counts": {
|
||||
"setBackend": 2,
|
||||
"createSegmenter": 1,
|
||||
"toBinaryMask": 1,
|
||||
"drawMask": 1
|
||||
},
|
||||
"replacements": [
|
||||
{
|
||||
"start": 2702,
|
||||
"end": 2714,
|
||||
"target": "setBackend",
|
||||
"originalSha256": "7dc39bab39429669a8e1ca50452330036048b7797dd1c10191c52e01b82c788e"
|
||||
},
|
||||
{
|
||||
"start": 2814,
|
||||
"end": 2826,
|
||||
"target": "setBackend",
|
||||
"originalSha256": "7dc39bab39429669a8e1ca50452330036048b7797dd1c10191c52e01b82c788e"
|
||||
},
|
||||
{
|
||||
"start": 3190,
|
||||
"end": 3207,
|
||||
"target": "createSegmenter",
|
||||
"originalSha256": "6b1027a187ae2bfb9960a09621abdf3133be7b2c94244f9b8913c149e5ca6e50"
|
||||
},
|
||||
{
|
||||
"start": 3507,
|
||||
"end": 3521,
|
||||
"target": "toBinaryMask",
|
||||
"originalSha256": "1aa8e213bdfcac558beb18b6c619a17d310ca974e61c97de388317a74f2f64d1"
|
||||
},
|
||||
{
|
||||
"start": 3613,
|
||||
"end": 3623,
|
||||
"target": "drawMask",
|
||||
"originalSha256": "588b57876050ec1c883c40e546f66bf01a3116a3081053d961a09a37fd6da20b"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "published-1.0.0-legacy",
|
||||
"archiveSha256": "8a88f52ae843bed1be01a2e60b82cf886d33582f990a5016d49b1409c3493c4b",
|
||||
"member": "package/dist/artplayer-plugin-danmuku-mask.legacy.js",
|
||||
"inputSha256": "49da39c368e036f894e623277ce837efa9b7524a040d8ae01e2245b3459c3c9f",
|
||||
"transformedSha256": "49da39c368e036f894e623277ce837efa9b7524a040d8ae01e2245b3459c3c9f",
|
||||
"probeSha256": "fa9ef8f14ce6bc0ec13557f3cc8ccb9cedf4b58a0b93db4983bdbc2106852791",
|
||||
"counts": {
|
||||
"setBackend": 2,
|
||||
"createSegmenter": 1,
|
||||
"toBinaryMask": 1,
|
||||
"drawMask": 1
|
||||
},
|
||||
"replacements": [
|
||||
{
|
||||
"start": 3164,
|
||||
"end": 3176,
|
||||
"target": "setBackend",
|
||||
"originalSha256": "ff79b48a47dfadebba910487f17a3ff7e32347f91af5e66b4f9360227ecc996b"
|
||||
},
|
||||
{
|
||||
"start": 3322,
|
||||
"end": 3334,
|
||||
"target": "setBackend",
|
||||
"originalSha256": "ff79b48a47dfadebba910487f17a3ff7e32347f91af5e66b4f9360227ecc996b"
|
||||
},
|
||||
{
|
||||
"start": 3743,
|
||||
"end": 3760,
|
||||
"target": "createSegmenter",
|
||||
"originalSha256": "d509804e369c2ff85b0463b30708c279397274ebaa58438e1275b9e98ec0a8dc"
|
||||
},
|
||||
{
|
||||
"start": 4567,
|
||||
"end": 4581,
|
||||
"target": "toBinaryMask",
|
||||
"originalSha256": "a4d2ab45b3dc07af8a0474981caa8dd3932885b722e467b18c55647c93602b92"
|
||||
},
|
||||
{
|
||||
"start": 4654,
|
||||
"end": 4664,
|
||||
"target": "drawMask",
|
||||
"originalSha256": "fe3835d5b191fc5d5efb0f08289855ac690bbc17f1553848c1aab292c3e4274c"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "published-1.0.0-module",
|
||||
"archiveSha256": "8a88f52ae843bed1be01a2e60b82cf886d33582f990a5016d49b1409c3493c4b",
|
||||
"member": "package/dist/artplayer-plugin-danmuku-mask.mjs",
|
||||
"inputSha256": "92ae895c45c3dd39110c3513eb5cb6d99b9249d96b37b7fd1a6437e97febdc8f",
|
||||
"transformedSha256": "dade379e7fe355271e74904022b95f6c8965e33228921224f50eb232a651fec8",
|
||||
"probeSha256": "9b3bacea7a45879c1ba841becf25bf8fea264cfaa2637506eb323048942cf4a7",
|
||||
"counts": {
|
||||
"setBackend": 2,
|
||||
"createSegmenter": 1,
|
||||
"toBinaryMask": 1,
|
||||
"drawMask": 1
|
||||
},
|
||||
"replacements": [
|
||||
{
|
||||
"start": 4608,
|
||||
"end": 4620,
|
||||
"target": "setBackend",
|
||||
"originalSha256": "7dc39bab39429669a8e1ca50452330036048b7797dd1c10191c52e01b82c788e"
|
||||
},
|
||||
{
|
||||
"start": 4748,
|
||||
"end": 4760,
|
||||
"target": "setBackend",
|
||||
"originalSha256": "7dc39bab39429669a8e1ca50452330036048b7797dd1c10191c52e01b82c788e"
|
||||
},
|
||||
{
|
||||
"start": 5215,
|
||||
"end": 5232,
|
||||
"target": "createSegmenter",
|
||||
"originalSha256": "6b1027a187ae2bfb9960a09621abdf3133be7b2c94244f9b8913c149e5ca6e50"
|
||||
},
|
||||
{
|
||||
"start": 5768,
|
||||
"end": 5782,
|
||||
"target": "toBinaryMask",
|
||||
"originalSha256": "1aa8e213bdfcac558beb18b6c619a17d310ca974e61c97de388317a74f2f64d1"
|
||||
},
|
||||
{
|
||||
"start": 5908,
|
||||
"end": 5918,
|
||||
"target": "drawMask",
|
||||
"originalSha256": "588b57876050ec1c883c40e546f66bf01a3116a3081053d961a09a37fd6da20b"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"scope": "Frozen owned source and actual archive-derived lifecycle probes. Archive call sites are replaced only at five controlled SDK callee boundaries; module bytes first transformed to CJS. This is not unmodified bundle/model or native ESM/browser/device acceptance.",
|
||||
"candidateAcceptance": false,
|
||||
"expectedTests": 54,
|
||||
"isolatedFatalChildren": 4,
|
||||
"checks": {
|
||||
"nodeTests": {
|
||||
"passed": 54,
|
||||
"failed": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"lint": "passed",
|
||||
"isolatedFatalChildren": {
|
||||
"count": 4,
|
||||
"expectedExit": 1,
|
||||
"mode": "--unhandled-rejections=strict",
|
||||
"parentGlobalRejectionHandler": false
|
||||
}
|
||||
},
|
||||
"evidence": {
|
||||
"file": "refactor/.cache/danmuku-mask-failures-final.log",
|
||||
"sha256": "c47c1a61a0c802aaa795b5a9fd0c4a84e38e5dbaa69243e4e684b41ed21fbf40"
|
||||
},
|
||||
"toolVersions": {
|
||||
"typescript": "5.9.3",
|
||||
"esbuild": "0.27.7"
|
||||
},
|
||||
"testFiles": [
|
||||
{
|
||||
"file": "test/helpers/danmuku-mask.js",
|
||||
"sha256": "82a5d6990af9bf5910b073b65ab73fdc60a2071e25d2d5f059b1ba35d0b3cd00"
|
||||
},
|
||||
{
|
||||
"file": "test/danmuku-mask-failures.test.js",
|
||||
"sha256": "035c3123b6f01c99e6bd41b825bdd4ed5aa7a5ce7c94285e734ece6c23710bfe"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
# PKG-MASK-02 历史错误与生命周期观察
|
||||
|
||||
本任务新增历史测试、专属 helper 和证据,并接入根测试脚本;未修改生产或生成产物,未执行模型,
|
||||
没有浏览器服务器、推送或发布。PKG-MASK-03 仍依赖 PKG-DANMUKU-07;该依赖尚未
|
||||
满足,生产重构按任务依赖继续推进。本任务不代表 SDK-08 完成。
|
||||
|
||||
## 输入和可重放接缝
|
||||
|
||||
源码来自 `b0cfbfe3a09a84a295a257e45e4577847588d1cb`;真实 npm 1.0.0/1.1.0
|
||||
tarball 来自 MASK-01 的 SHA512/SHA256 冻结。源文件与六个 main/legacy/module
|
||||
成员共七个输入,标识见 [机器报告](../baselines/danmuku-mask-failures-validation.json)。
|
||||
|
||||
两种探针必须区分:
|
||||
|
||||
- MASK-01 已用**未改写的归档字节**验证 CommonJS/script 导出、注册和 stop,未启动模型。
|
||||
- MASK-02 的历史生命周期探针对归档导出工厂内部 **五个 SDK 调用表达式的 callee**
|
||||
做受控替换:setBackend(webgl/cpu) 两处、createSegmenter/toBinaryMask/drawMask
|
||||
各一处。inline SDK 函数表达式作为整体替换,原插件异步流程、生命周期、错误处理、
|
||||
canvas 操作与旧版 async helper 保留。module 先以 esbuild 转为 CJS,因此不算
|
||||
原生 ESM 验收。冻结源码仅转换模块形式,通过 require seam 注入 SDK 替身。
|
||||
|
||||
helper 首先校验真实归档,再从实际导出函数定位唯一原文字段,用 TypeScript AST
|
||||
寻找接缝。固定替换数量不是模糊匹配后继续运行:任何数量不符直接失败。报告保存
|
||||
原成员哈希、转 CJS 后哈希、探针哈希、每个 callee 的范围/旧表达式哈希。首项测试
|
||||
逐项比较报告,变化必须显式审查。实际 SDK 顶层注册可执行,但没有模型初始化、
|
||||
模型网络、GPU 或媒体推理。探针 fetch 被拒绝;错误探针不得以网络错误替代目标错误。
|
||||
|
||||
## 实际观察
|
||||
|
||||
七个输入都复现以下三个历史缺陷,共 21 项:
|
||||
|
||||
| 触发 | 历史结果 |
|
||||
| --- | --- |
|
||||
| 初始化 pending → stop → 完成初始化 | stop 的 maskImage=none 被后续 mask 写入覆盖,重新安排 RAF |
|
||||
| 推理 pending → destroy → 推理完成 | 销毁后仍画 mask/排 RAF;segmenter 未 dispose,ready/destroy listener 留存 |
|
||||
| 初始化完成后再次 start → stop | 两条 RAF 链,stop 只取消最后记录的一条;剩余链下一轮重新写 mask |
|
||||
|
||||
详细错误矩阵集中在冻结源码与 npm 1.1.0 main,各 16 项,避免为所有格式机械重复:
|
||||
|
||||
| 触发 | 历史结果 / 关联风险 |
|
||||
| --- | --- |
|
||||
| 初始化 pending 时 stop 或 destroy | 后续模型仍存活,不 dispose;MASK-LIFETIME-01 |
|
||||
| 推理 pending 时 stop | 晚到结果覆盖 none 并排新 RAF;MASK-LIFETIME-01 |
|
||||
| 同时两次 start | 创建两个模型、两条 RAF,destroy 后仍剩一条,两个模型均未 dispose;MASK-START-01 |
|
||||
| segmentPeople rejection | 每帧记一次错误并重试;改为成功后能再画 mask;MASK-BACKEND-01 |
|
||||
| setBackend(webgl) fulfilled false | 不尝试 CPU,继续初始化;MASK-BACKEND-01 |
|
||||
| WebGL rejection、CPU fulfilled true/false | 均尝试 CPU、记录一次 warning、继续初始化,忽略返回 boolean;不是 MediaPipe CPU 推理证据 |
|
||||
| CPU rejection,经显式 start 调用 | Promise 以同一错误拒绝,尚未创建模型或排 RAF |
|
||||
| createSegmenter rejection | 记录错误,但 start resolve;以后 RAF 只轮询,既不推理也不自动重新初始化 |
|
||||
| 缺少 $danmuku | start 在模型创建后以 TypeError 拒绝;stop 也抛错,模型未 dispose;MASK-DOM-01 |
|
||||
| getContext 返回 null | start resolve;读图像时报 TypeError 并排下一帧,旧 mask 不被清除 |
|
||||
| createElement(canvas) 抛错 | start 以同一错误拒绝;已初始化模型在 destroy 后仍未 dispose |
|
||||
| getImageData 抛受控 SecurityError | 错误被记录并重试,既有 mask 留存;这不是原生 CORS 复现 |
|
||||
| RAF 返回 0;destroy 后再次 ready | truthiness 判断漏取消 0,保留 ready listener 可再启动一条链 |
|
||||
| ready 内 CPU rejection | 被事件调度丢弃的 start Promise 产生未处理拒绝 |
|
||||
| 缺少 $video | segmentBody 在 try 之外读 paused,产生未处理拒绝 |
|
||||
|
||||
加上接缝身份验证,共 **54 项历史测试**。未处理拒绝的四项各启独立 Node 子进程,
|
||||
使用 `--unhandled-rejections=strict`,验证子进程 exit=1 和指定错误;父进程没有安装
|
||||
unhandledRejection handler,子进程也没有吞错 handler。子进程只能以指定错误失败,
|
||||
AssertionError 或禁止网络错误均不作为通过。测试进程本身没有实际 RAF 定时器;
|
||||
故意留存的历史帧只在该测试的 Map 内,不会泄漏到真实 UI。
|
||||
|
||||
## 验证及后续约束
|
||||
|
||||
固定 Node 24.21.0:
|
||||
|
||||
```sh
|
||||
node --test test/danmuku-mask-failures.test.js
|
||||
node node_modules/eslint/bin/eslint.js test/helpers/danmuku-mask.js test/danmuku-mask-failures.test.js
|
||||
yarn test:danmuku-mask
|
||||
```
|
||||
|
||||
54 项通过、0 fail、0 skip;专属 lint 通过。日志
|
||||
`refactor/.cache/danmuku-mask-failures-final.log`,机器报告记录其哈希和输入身份。
|
||||
这里只证明旧缺陷可重跑,**这些结果不能变成候选验收期望**。后续候选测试应独立
|
||||
验证异步失效、模型单一归属、RAF 单链、适当错误传播/事件处理和销毁清理;不要靠
|
||||
将本套测试切换到候选 source 来保留已知问题。SDK-08、真实模型/设备/视频/浏览器
|
||||
与新旧核心组合仍由 05 验收,通知/资源最终分发由 06 核实。
|
||||
|
||||
父代理独立复跑54项通过,加入现有test:unit使CI自动执行;专属
|
||||
test:danmuku-mask同时覆盖01的六项契约。未安装依赖或更改Yarn锁文件。
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 228 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 73 / doing 16 / blocked 0 / done 139 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 73 / doing 15 / blocked 0 / done 140 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -241,7 +241,7 @@
|
||||
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| PKG-MASK-01 | artplayer-plugin-danmuku-mask<br>核对包契约与历史用法 | BASE-05 | 模型参数、start/stop、默认下载路径及 mask 样式 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结;接续 SDK-08,分别核实 Yarn 解析版本、未固定版本的模型 solutionPath 和资源来源 | H | done |
|
||||
| PKG-MASK-02 | artplayer-plugin-danmuku-mask<br>建立特有行为与错误测试 | PKG-MASK-01, ENG-03, ENG-05 | 加载期间停止/销毁、重复启动、推理失败、WebGL/CPU 边界 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | doing |
|
||||
| PKG-MASK-02 | artplayer-plugin-danmuku-mask<br>建立特有行为与错误测试 | PKG-MASK-01, ENG-03, ENG-05 | 加载期间停止/销毁、重复启动、推理失败、WebGL/CPU 边界 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
|
||||
| PKG-MASK-03 | artplayer-plugin-danmuku-mask<br>整理内部职责与资源 | PKG-MASK-02, CORE-18, PKG-DANMUKU-07 | 模型加载/推理/画布输出分离,阻止重叠推理与过期写入 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
|
||||
| PKG-MASK-04 | artplayer-plugin-danmuku-mask<br>迁移自有源码和公开类型 | PKG-MASK-03, ENG-04, ENG-06, CORE-07 | 模型 adapter、canvas 和选项的精确类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
|
||||
| PKG-MASK-05 | artplayer-plugin-danmuku-mask<br>验证新旧核心和组合 | PKG-MASK-04, CORE-22 | 真实模型和 danmuku/seek/全屏组合,GPU 资源释放 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
|
||||
@@ -530,6 +530,7 @@
|
||||
- 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)
|
||||
- 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-ASR-01: [记录](changes/2026-09-13-PKG-ASR-01-contracts.md) [记录](baselines/asr-contract.md) [记录](baselines/asr-release.json)
|
||||
- PKG-ASR-02: [记录](changes/2026-09-13-PKG-ASR-02-audio-baseline.md) [记录](baselines/asr-audio-validation.json)
|
||||
- PKG-ASR-03: [记录](changes/2026-09-13-PKG-ASR-03-audio-ownership.md) [记录](baselines/asr-ownership-validation.json)
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-MASK-02 生命周期与错误基线完成
|
||||
|
||||
54项冻结历史测试及父代理复核通过,覆盖源码和两旧版本三格式的关键生命周期;
|
||||
完整失败矩阵聚焦源码/最新main,四个未处理拒绝在独立进程精确取证。归档SDK接缝
|
||||
五处callee转换哈希可重放,不能当作真实模型通过。新测试接入test:unit和专属
|
||||
yarn test:danmuku-mask;四类风险由源码观察提升为已复现,保持开放待修复。
|
||||
见[变更](changes/2026-09-13-PKG-MASK-02-failures.md)与[证据](baselines/danmuku-mask-failures-validation.json)。
|
||||
228项:140 done、15 doing、73 todo。Mask03仍等待Danmuku07
|
||||
稳定依赖;Danmuku02受控失败和真实浏览器基线继续并行。没有推送或发布。
|
||||
|
||||
|
||||
## PKG-DANMUKU-01 弹幕契约与归档完成
|
||||
|
||||
冻结81稳定版目录、15真实归档和28源码/文档/图标输入。44项测试及父代理复核通过;
|
||||
|
||||
@@ -221,11 +221,11 @@
|
||||
| CAST-LIFE-01 | resolved / 已复现 | Cast listeners and continuations survive player destruction and update another player icon | PKG-CAST-03, PKG-CAST-05 |
|
||||
| CAST-STATE-01 | resolved / 已复现 | Cast terminal session states leave the normalized callback and icon connected | PKG-CAST-03, PKG-CAST-05 |
|
||||
| CAST-ERROR-01 | resolved / 已复现 | Cast media loading rejection escapes on a detached Promise while the click has already fulfilled | PKG-CAST-03, PKG-CAST-05 |
|
||||
| MASK-LIFETIME-01 | open / 源码/产物事实 | Mask asynchronous initialization and inference continue after stop or destroy | PKG-MASK-02, PKG-MASK-03 |
|
||||
| MASK-START-01 | open / 源码/产物事实 | Concurrent Mask start calls can initialize multiple models and create independent RAF chains | PKG-MASK-02, PKG-MASK-03 |
|
||||
| MASK-LIFETIME-01 | open / 已复现 | Mask asynchronous initialization and inference continue after stop or destroy | PKG-MASK-02, PKG-MASK-03 |
|
||||
| MASK-START-01 | open / 已复现 | Concurrent Mask start calls can initialize multiple models and create independent RAF chains | PKG-MASK-02, PKG-MASK-03 |
|
||||
| MASK-MODEL-01 | open / 源码/产物事实 | Published Mask options and installed MediaPipe adapter consume different model settings | PKG-MASK-02, PKG-MASK-04, PKG-MASK-05 |
|
||||
| MASK-BACKEND-01 | open / 源码/产物事实 | Mask TensorFlow backend fallback does not establish MediaPipe fallback and failed initialization keeps scheduling | PKG-MASK-02, PKG-MASK-03, PKG-MASK-05 |
|
||||
| MASK-DOM-01 | open / 源码/产物事实 | Mask registration and frame processing assume Danmuku DOM, readable frames and a 2D context | PKG-MASK-02, PKG-MASK-03, PKG-MASK-05 |
|
||||
| MASK-BACKEND-01 | open / 已复现 | Mask TensorFlow backend fallback does not establish MediaPipe fallback and failed initialization keeps scheduling | PKG-MASK-02, PKG-MASK-03, PKG-MASK-05 |
|
||||
| MASK-DOM-01 | open / 已复现 | Mask registration and frame processing assume Danmuku DOM, readable frames and a 2D context | PKG-MASK-02, PKG-MASK-03, PKG-MASK-05 |
|
||||
| MASK-NOTICE-01 | open / 源码/产物事实 | Published Mask bundles lack a separate third-party notice despite bundled SDK code | PKG-MASK-06 |
|
||||
| DANMUKU-TYPE-01 | open / 已复现 | Danmuku historical declared results, optional mount and initial Promise input disagree with runtime | PKG-DANMUKU-03, PKG-DANMUKU-06 |
|
||||
| DANMUKU-INPUT-01 | open / 已复现 | Danmuku replaces time zero with current time plus 0.5 and mutates supplied input objects | PKG-DANMUKU-02, PKG-DANMUKU-03 |
|
||||
|
||||
+20
-8
@@ -4906,7 +4906,7 @@
|
||||
{
|
||||
"id": "MASK-LIFETIME-01",
|
||||
"title": "Mask asynchronous initialization and inference continue after stop or destroy",
|
||||
"confirmation": "source-observed",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-MASK-02",
|
||||
@@ -4915,7 +4915,10 @@
|
||||
"evidence": [
|
||||
"refactor/baselines/danmuku-mask-contract.md",
|
||||
"refactor/baselines/danmuku-mask-release.json",
|
||||
"refactor/baselines/danmuku-mask-contract-validation.json"
|
||||
"refactor/baselines/danmuku-mask-contract-validation.json",
|
||||
"refactor/changes/2026-09-13-PKG-MASK-02-failures.md",
|
||||
"refactor/baselines/danmuku-mask-failures-validation.json",
|
||||
"test/danmuku-mask-failures.test.js"
|
||||
],
|
||||
"compatibleResolution": "Invalidate stale work while keeping restartable stop distinct from final destroy; release model and subscriptions.",
|
||||
"closureCriteria": "Pending initialization/inference cannot write masks or schedule work after stop/destroy; errors settle and owned resources are released."
|
||||
@@ -4923,7 +4926,7 @@
|
||||
{
|
||||
"id": "MASK-START-01",
|
||||
"title": "Concurrent Mask start calls can initialize multiple models and create independent RAF chains",
|
||||
"confirmation": "source-observed",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-MASK-02",
|
||||
@@ -4932,7 +4935,10 @@
|
||||
"evidence": [
|
||||
"refactor/baselines/danmuku-mask-contract.md",
|
||||
"refactor/baselines/danmuku-mask-release.json",
|
||||
"refactor/baselines/danmuku-mask-contract-validation.json"
|
||||
"refactor/baselines/danmuku-mask-contract-validation.json",
|
||||
"refactor/changes/2026-09-13-PKG-MASK-02-failures.md",
|
||||
"refactor/baselines/danmuku-mask-failures-validation.json",
|
||||
"test/danmuku-mask-failures.test.js"
|
||||
],
|
||||
"compatibleResolution": "Share pending initialization and own at most one inference loop per live registration.",
|
||||
"closureCriteria": "Repeated and concurrent starts, stop then start, failures and multiple instances have deterministic resource and result assertions."
|
||||
@@ -4958,7 +4964,7 @@
|
||||
{
|
||||
"id": "MASK-BACKEND-01",
|
||||
"title": "Mask TensorFlow backend fallback does not establish MediaPipe fallback and failed initialization keeps scheduling",
|
||||
"confirmation": "source-observed",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-MASK-02",
|
||||
@@ -4968,7 +4974,10 @@
|
||||
"evidence": [
|
||||
"refactor/baselines/danmuku-mask-contract.md",
|
||||
"refactor/baselines/danmuku-mask-release.json",
|
||||
"refactor/baselines/danmuku-mask-contract-validation.json"
|
||||
"refactor/baselines/danmuku-mask-contract-validation.json",
|
||||
"refactor/changes/2026-09-13-PKG-MASK-02-failures.md",
|
||||
"refactor/baselines/danmuku-mask-failures-validation.json",
|
||||
"test/danmuku-mask-failures.test.js"
|
||||
],
|
||||
"compatibleResolution": "Model backend outcomes explicitly, settle initialization failures and document the actual MediaPipe capability boundary.",
|
||||
"closureCriteria": "Fulfilled false, rejection, CPU failure, createSegmenter failure and later retry are tested; real inference fallback is separately evidenced."
|
||||
@@ -4976,7 +4985,7 @@
|
||||
{
|
||||
"id": "MASK-DOM-01",
|
||||
"title": "Mask registration and frame processing assume Danmuku DOM, readable frames and a 2D context",
|
||||
"confirmation": "source-observed",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-MASK-02",
|
||||
@@ -4986,7 +4995,10 @@
|
||||
"evidence": [
|
||||
"refactor/baselines/danmuku-mask-contract.md",
|
||||
"refactor/baselines/danmuku-mask-release.json",
|
||||
"refactor/baselines/danmuku-mask-contract-validation.json"
|
||||
"refactor/baselines/danmuku-mask-contract-validation.json",
|
||||
"refactor/changes/2026-09-13-PKG-MASK-02-failures.md",
|
||||
"refactor/baselines/danmuku-mask-failures-validation.json",
|
||||
"test/danmuku-mask-failures.test.js"
|
||||
],
|
||||
"compatibleResolution": "Handle missing dependencies and frame/context failures without stale writes or uncontrolled loops while preserving plugin ordering.",
|
||||
"closureCriteria": "Missing Danmuku/context, CORS failure, source changes and ready/destroy timing have controlled and real browser evidence."
|
||||
|
||||
+5
-2
@@ -2542,11 +2542,14 @@
|
||||
"ENG-03",
|
||||
"ENG-05"
|
||||
],
|
||||
"status": "doing",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "加载期间停止/销毁、重复启动、推理失败、WebGL/CPU 边界",
|
||||
"acceptance": "旧版本行为可重跑,成功/失败/切源/销毁有必要断言",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-13-PKG-MASK-02-failures.md",
|
||||
"baselines/danmuku-mask-failures-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-MASK-03",
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Historical runner includes fatal isolated Node rejection probes.
|
||||
import test from 'node:test'
|
||||
import { danmukuMaskEnvironment, danmukuMaskHistorical, flushMask } from './helpers/danmuku-mask.js'
|
||||
|
||||
// These assertions describe frozen historical defects, never candidate acceptance.
|
||||
const implementations = await danmukuMaskHistorical()
|
||||
const detailed = implementations.filter(item => ['frozen-source', 'published-1.1.0-main'].includes(item.name))
|
||||
|
||||
test('Historical SDK probes replace exactly five call sites and retain verified input identities', () => {
|
||||
const expected = JSON.parse(fs.readFileSync('refactor/baselines/danmuku-mask-failures-validation.json', 'utf8'))
|
||||
const actual = implementations.map(({ code: _code, ...identity }) => identity)
|
||||
assert.deepEqual(actual, expected.inputs, 'SDK seam transformations must remain exactly reproducible')
|
||||
assert.equal(implementations.length, 7)
|
||||
for (const item of implementations) {
|
||||
assert.match(item.inputSha256, /^[a-f0-9]{64}$/u)
|
||||
assert.match(item.transformedSha256, /^[a-f0-9]{64}$/u)
|
||||
if (!item.source) {
|
||||
assert.match(item.archiveSha256, /^[a-f0-9]{64}$/u)
|
||||
assert.match(item.probeSha256, /^[a-f0-9]{64}$/u)
|
||||
assert.deepEqual(item.counts, { setBackend: 2, createSegmenter: 1, toBinaryMask: 1, drawMask: 1 })
|
||||
assert.equal(item.replacements.length, 5)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for (const implementation of implementations) {
|
||||
test(`${implementation.name}: historical stop during initialization is undone by late initialization`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { pendingInit: true })
|
||||
const pending = env.result.start()
|
||||
await flushMask()
|
||||
assert.equal(env.initGates.length, 1)
|
||||
env.result.stop()
|
||||
assert.equal(env.style.maskImage, 'none')
|
||||
env.initGates[0].resolve()
|
||||
await pending
|
||||
await flushMask()
|
||||
assert.equal(env.draws.length, 1)
|
||||
assert.equal(env.style.maskImage, 'url(data:image/png;base64,historical-mask)')
|
||||
assert.equal(env.frames.size, 1)
|
||||
env.result.stop()
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical inference completion after destroy writes a mask and revives RAF`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { pendingSegment: true })
|
||||
await env.result.start()
|
||||
await flushMask()
|
||||
assert.equal(env.segmentGates.length, 1)
|
||||
env.destroy()
|
||||
env.segmentGates[0].resolve([{}])
|
||||
await flushMask()
|
||||
assert.equal(env.draws.length, 1)
|
||||
assert.equal(env.style.maskImage, 'url(data:image/png;base64,historical-mask)')
|
||||
assert.equal(env.frames.size, 1)
|
||||
assert.equal(env.created[0].disposed, 0)
|
||||
assert.deepEqual([...env.listeners].map(([name, callbacks]) => [name, callbacks.size]), [['ready', 1], ['destroy', 1]])
|
||||
env.result.stop()
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical repeated start leaves a second RAF chain after stop`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation)
|
||||
await env.result.start()
|
||||
await flushMask()
|
||||
await env.result.start()
|
||||
await flushMask()
|
||||
assert.equal(env.created.length, 1)
|
||||
assert.equal(env.frames.size, 2)
|
||||
env.result.stop()
|
||||
assert.equal(env.frames.size, 1)
|
||||
await env.frame()
|
||||
assert.equal(env.frames.size, 1)
|
||||
assert.equal(env.style.maskImage, 'url(data:image/png;base64,historical-mask)')
|
||||
env.result.stop()
|
||||
})
|
||||
}
|
||||
|
||||
for (const implementation of detailed) {
|
||||
for (const action of ['stop', 'destroy']) {
|
||||
test(`${implementation.name}: historical ${action} during initialization does not dispose the late model`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { pendingInit: true })
|
||||
const pending = env.result.start()
|
||||
await flushMask()
|
||||
if (action === 'stop')
|
||||
env.result.stop()
|
||||
else env.destroy()
|
||||
env.initGates[0].resolve()
|
||||
await pending
|
||||
await flushMask()
|
||||
assert.equal(env.created[0].disposed, 0)
|
||||
assert.equal(env.frames.size, 1)
|
||||
assert.equal(env.style.maskMode, 'alpha')
|
||||
env.result.stop()
|
||||
})
|
||||
}
|
||||
|
||||
test(`${implementation.name}: historical stop during inference cannot prevent its pending write`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { pendingSegment: true })
|
||||
await env.result.start()
|
||||
env.result.stop()
|
||||
assert.deepEqual(env.writes, ['none'])
|
||||
env.segmentGates[0].resolve([{}])
|
||||
await flushMask()
|
||||
assert.deepEqual(env.writes, ['none', 'url(data:image/png;base64,historical-mask)'])
|
||||
assert.equal(env.frames.size, 1)
|
||||
env.result.stop()
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical concurrent starts initialize two models and lose disposal ownership`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { pendingInit: true })
|
||||
const first = env.result.start()
|
||||
const second = env.result.start()
|
||||
await flushMask()
|
||||
assert.equal(env.created.length, 2)
|
||||
for (const gate of env.initGates) gate.resolve()
|
||||
await Promise.all([first, second])
|
||||
await flushMask()
|
||||
assert.equal(env.frames.size, 2)
|
||||
env.destroy()
|
||||
assert.equal(env.frames.size, 1)
|
||||
assert.deepEqual(env.created.map(instance => instance.disposed), [0, 0])
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical inference rejection logs once per frame and keeps retrying`, async () => {
|
||||
const error = new Error('controlled inference failure')
|
||||
const env = danmukuMaskEnvironment(implementation, { segmentError: error })
|
||||
await env.result.start()
|
||||
await flushMask()
|
||||
assert.equal(env.frames.size, 1)
|
||||
assert.equal(env.logs.length, 1)
|
||||
assert.deepEqual(env.logs[0], ['error', 'Error in segmentBody:', error])
|
||||
assert.equal(env.draws.length, 0)
|
||||
await env.frame()
|
||||
assert.equal(env.logs.length, 2)
|
||||
assert.equal(env.frames.size, 1)
|
||||
env.settings.segmentError = null
|
||||
await env.frame()
|
||||
assert.equal(env.draws.length, 1)
|
||||
env.result.stop()
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical fulfilled false WebGL selection does not trigger CPU fallback`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { backendResult: { webgl: false } })
|
||||
await env.result.start()
|
||||
await flushMask()
|
||||
assert.deepEqual(env.backends, ['webgl'])
|
||||
assert.equal(env.created.length, 1)
|
||||
assert.equal(env.logs.length, 0)
|
||||
env.result.stop()
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical rejected WebGL selection tries CPU and ignores its fulfilled boolean`, async () => {
|
||||
for (const cpu of [true, false]) {
|
||||
const env = danmukuMaskEnvironment(implementation, { backendError: { webgl: new Error('no WebGL') }, backendResult: { cpu } })
|
||||
await env.result.start()
|
||||
await flushMask()
|
||||
assert.deepEqual(env.backends, ['webgl', 'cpu'])
|
||||
assert.deepEqual(env.logs, [['warn', 'WebGL backend not available, falling back to CPU', 'no WebGL']])
|
||||
assert.equal(env.created.length, 1)
|
||||
env.result.stop()
|
||||
}
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical CPU rejection rejects explicit start before creating a model`, async () => {
|
||||
const error = new Error('controlled CPU failure')
|
||||
const env = danmukuMaskEnvironment(implementation, { backendError: { webgl: new Error('no WebGL'), cpu: error } })
|
||||
await assert.rejects(env.result.start(), failure => failure === error)
|
||||
assert.deepEqual(env.backends, ['webgl', 'cpu'])
|
||||
assert.equal(env.created.length, 0)
|
||||
assert.equal(env.frames.size, 0)
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical model initialization rejection resolves start and polls without a model`, async () => {
|
||||
const error = new Error('controlled model init failure')
|
||||
const env = danmukuMaskEnvironment(implementation, { initError: error })
|
||||
assert.equal(await env.result.start(), undefined)
|
||||
assert.deepEqual(env.logs, [['error', 'Error initializing segmenter:', error]])
|
||||
assert.equal(env.frames.size, 1)
|
||||
await env.frame()
|
||||
assert.equal(env.created.length, 1)
|
||||
assert.equal(env.inferences.length, 0)
|
||||
env.result.stop()
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical missing danmuku rejects start after allocating the model and makes stop throw`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { noDanmuku: true })
|
||||
await assert.rejects(env.result.start(), error => error.name === 'TypeError' && error.message.includes('style'))
|
||||
assert.throws(() => env.result.stop(), error => error.name === 'TypeError' && error.message.includes('style'))
|
||||
assert.equal(env.created.length, 1)
|
||||
assert.equal(env.created[0].disposed, 0)
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical unavailable 2D context logs read failures and keeps the frame loop`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { noContext: true })
|
||||
await env.result.start()
|
||||
await flushMask()
|
||||
assert.equal(env.logs.length, 1)
|
||||
assert.equal(env.logs[0][2].name, 'TypeError')
|
||||
assert.match(env.logs[0][2].message, /getImageData/u)
|
||||
assert.equal(env.frames.size, 1)
|
||||
assert.equal(env.writes.length, 0)
|
||||
env.result.stop()
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical canvas allocation failure rejects start but leaves the initialized model owned`, async () => {
|
||||
const error = new Error('controlled canvas allocation failure')
|
||||
const env = danmukuMaskEnvironment(implementation, { canvasError: error })
|
||||
await assert.rejects(env.result.start(), failure => failure === error)
|
||||
assert.equal(env.created.length, 1)
|
||||
assert.equal(env.created[0].disposed, 0)
|
||||
assert.equal(env.frames.size, 0)
|
||||
env.destroy()
|
||||
assert.equal(env.created[0].disposed, 0)
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical canvas read security failure is logged and retried without clearing the previous mask`, async () => {
|
||||
const error = new Error('controlled canvas taint')
|
||||
error.name = 'SecurityError'
|
||||
const env = danmukuMaskEnvironment(implementation, { readError: error })
|
||||
await env.result.start()
|
||||
await flushMask()
|
||||
assert.equal(env.logs[0][2], error)
|
||||
assert.equal(env.style.maskImage, 'url(prior-mask)')
|
||||
await env.frame()
|
||||
assert.equal(env.logs.length, 2)
|
||||
env.result.stop()
|
||||
})
|
||||
|
||||
test(`${implementation.name}: historical zero RAF id survives stop and retained ready listeners restart after destroy`, async () => {
|
||||
const env = danmukuMaskEnvironment(implementation, { firstFrame: 0, paused: true })
|
||||
await env.result.start()
|
||||
assert.deepEqual([...env.frames.keys()], [0])
|
||||
env.destroy()
|
||||
assert.deepEqual([...env.frames.keys()], [0])
|
||||
assert.deepEqual(env.cancelled, [])
|
||||
env.emit('ready')
|
||||
await flushMask()
|
||||
assert.equal(env.frames.size, 2)
|
||||
assert.equal(env.created[0].disposed, 0)
|
||||
})
|
||||
|
||||
for (const scenario of ['ready-backend', 'missing-video']) {
|
||||
test(`${implementation.name}: historical ${scenario} unhandled rejection is observed only in a fatal isolated child`, () => {
|
||||
const script = `import { danmukuMaskHistorical, danmukuMaskEnvironment, flushMask } from './test/helpers/danmuku-mask.js';
|
||||
const implementation = (await danmukuMaskHistorical()).find(item => item.name === ${JSON.stringify(implementation.name)});
|
||||
const settings = ${scenario === 'ready-backend' ? '{backendError: {webgl: new Error("no WebGL"), cpu: new Error("MASK02_READY_BACKEND_REJECTION")}}' : '{noVideo: true}'};
|
||||
const env = danmukuMaskEnvironment(implementation, settings);
|
||||
${scenario === 'ready-backend' ? 'env.emit("ready");' : 'await env.result.start();'}
|
||||
await flushMask();`
|
||||
const child = spawnSync(process.execPath, ['--unhandled-rejections=strict', '--input-type=module', '-e', script], { encoding: 'utf8', timeout: 15000 })
|
||||
assert.equal(child.error, undefined)
|
||||
assert.equal(child.status, 1)
|
||||
assert.match(child.stderr, scenario === 'ready-backend' ? /MASK02_READY_BACKEND_REJECTION/u : /Cannot read properties of undefined \(reading 'paused'\)/u)
|
||||
assert.doesNotMatch(child.stderr, /AssertionError|Model network access is forbidden/u)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import vm from 'node:vm'
|
||||
import { transform } from 'esbuild'
|
||||
import ts from 'typescript'
|
||||
import { verifyDanmukuMaskContract } from '../../refactor/scripts/danmuku-mask-contract.mjs'
|
||||
import { hash, readMember } from '../../refactor/scripts/releases.mjs'
|
||||
|
||||
const sourceFile = 'packages/artplayer-plugin-danmuku-mask/src/index.js'
|
||||
let historical
|
||||
export function danmukuMaskHistorical() {
|
||||
historical ||= loadHistorical()
|
||||
return historical
|
||||
}
|
||||
|
||||
function evaluate(code, additions = {}) {
|
||||
const module = { exports: {} }
|
||||
const context = {
|
||||
module,
|
||||
exports: module.exports,
|
||||
console: { log() {}, warn() {}, error() {} },
|
||||
fetch() { throw new Error('Model network access is forbidden in historical probes') },
|
||||
...additions,
|
||||
}
|
||||
context.global = context
|
||||
context.self = context
|
||||
vm.runInNewContext(code, context, { timeout: 5000 })
|
||||
return module.exports.default || module.exports
|
||||
}
|
||||
|
||||
function instrument(code, factory, label) {
|
||||
const text = factory.toString()
|
||||
const start = code.indexOf(text)
|
||||
assert(start >= 0 && !code.includes(text, start + 1), 'Exported factory must map uniquely to the evaluated bytes')
|
||||
const prefix = 'const factory = ('
|
||||
const parsed = ts.createSourceFile('factory.js', `${prefix}${text});`, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS)
|
||||
assert.equal(parsed.parseDiagnostics.length, 0)
|
||||
const replacements = []
|
||||
const counts = { setBackend: 0, createSegmenter: 0, toBinaryMask: 0, drawMask: 0 }
|
||||
const configs = new Set()
|
||||
function findConfigs(node) {
|
||||
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isObjectLiteralExpression(node.initializer)) {
|
||||
const properties = node.initializer.properties.map(property => property.name?.getText(parsed))
|
||||
if (properties.includes('runtime') && properties.includes('modelType'))
|
||||
configs.add(node.name.text)
|
||||
}
|
||||
ts.forEachChild(node, findConfigs)
|
||||
}
|
||||
findConfigs(parsed)
|
||||
function visit(node) {
|
||||
if (ts.isCallExpression(node)) {
|
||||
const args = node.arguments
|
||||
const callee = node.expression.getText(parsed)
|
||||
let target
|
||||
if (args.length === 1 && ts.isStringLiteral(args[0]) && ['webgl', 'cpu'].includes(args[0].text))
|
||||
target = 'setBackend'
|
||||
else if (callee === 'createSegmenter' || callee.endsWith('.createSegmenter') || callee.includes('is not a supported model name.') || (args.length === 2 && ts.isIdentifier(args[1]) && configs.has(args[1].text)))
|
||||
target = 'createSegmenter'
|
||||
else if (callee === 'toBinaryMask' || callee.endsWith('.toBinaryMask') || callee.includes('mask.toImageData') || (args.length === 5 && args[3].getText(parsed).endsWith('.drawContour') && args[4].getText(parsed).endsWith('.foregroundThreshold')))
|
||||
target = 'toBinaryMask'
|
||||
else if (args.length === 5 && args[3].getText(parsed).endsWith('.opacity') && args[4].getText(parsed).endsWith('.maskBlurAmount'))
|
||||
target = 'drawMask'
|
||||
if (target) {
|
||||
counts[target]++
|
||||
replacements.push({ start: start + node.expression.getStart(parsed) - prefix.length, end: start + node.expression.end - prefix.length, target, original: callee })
|
||||
// Inline SDK bodies are replaced as one expression, never traversed as owned lifecycle code.
|
||||
return
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(parsed)
|
||||
assert.deepEqual(counts, { setBackend: 2, createSegmenter: 1, toBinaryMask: 1, drawMask: 1 }, label)
|
||||
let result = code
|
||||
for (const replacement of [...replacements].sort((a, b) => b.start - a.start))
|
||||
result = `${result.slice(0, replacement.start)}globalThis.__maskProbe.${replacement.target}${result.slice(replacement.end)}`
|
||||
return { code: result, counts, replacements: replacements.map(({ start, end, target, original }) => ({ start, end, target, originalSha256: hash(original) })) }
|
||||
}
|
||||
|
||||
async function loadHistorical() {
|
||||
const contract = await verifyDanmukuMaskContract()
|
||||
const implementations = []
|
||||
const source = contract.sources.get(sourceFile)
|
||||
const sourceCode = (await transform(source, { format: 'cjs', target: 'es2020' })).code
|
||||
implementations.push({ name: 'frozen-source', code: sourceCode, inputSha256: hash(source), transformedSha256: hash(sourceCode), source: true, replacements: [] })
|
||||
for (const release of [contract.baseline.release, ...contract.baseline.previous]) {
|
||||
const archive = contract.archives.get(release.version)
|
||||
for (const entry of ['main', 'legacy', 'module']) {
|
||||
const member = `package/${release.manifest[entry].replace(/^\.\//u, '')}`
|
||||
const input = readMember(archive, member).toString()
|
||||
const code = entry === 'module' ? (await transform(input, { format: 'cjs', target: 'es2020' })).code : input
|
||||
const modified = instrument(code, evaluate(code), `${release.version}/${entry}`)
|
||||
implementations.push({ name: `published-${release.version}-${entry}`, archiveSha256: release.sha256, member, inputSha256: hash(input), transformedSha256: hash(code), probeSha256: hash(modified.code), ...modified })
|
||||
}
|
||||
}
|
||||
return implementations
|
||||
}
|
||||
|
||||
export const flushMask = () => new Promise(resolve => setImmediate(resolve))
|
||||
function deferred() {
|
||||
let resolve
|
||||
let reject
|
||||
const promise = new Promise((accept, fail) => {
|
||||
resolve = accept
|
||||
reject = fail
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
export function danmukuMaskEnvironment(implementation, settings = {}) {
|
||||
const env = {
|
||||
frames: new Map(),
|
||||
listeners: new Map(),
|
||||
logs: [],
|
||||
backends: [],
|
||||
created: [],
|
||||
inferences: [],
|
||||
masks: [],
|
||||
draws: [],
|
||||
initGates: [],
|
||||
segmentGates: [],
|
||||
writes: [],
|
||||
cancelled: [],
|
||||
settings,
|
||||
}
|
||||
let nextFrame = settings.firstFrame ?? 1
|
||||
const style = { maskMode: 'luminance', maskImage: 'url(prior-mask)' }
|
||||
Object.defineProperty(style, 'maskImage', {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => env.writes.at(-1) ?? 'url(prior-mask)',
|
||||
set: value => env.writes.push(value),
|
||||
})
|
||||
const context = {
|
||||
getImageData() {
|
||||
if (settings.readError)
|
||||
throw settings.readError
|
||||
return { data: new Uint8ClampedArray([255, 255, 255, 255]) }
|
||||
},
|
||||
putImageData() {},
|
||||
}
|
||||
env.canvas = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
getContext: () => settings.noContext ? null : context,
|
||||
toDataURL: () => 'data:image/png;base64,historical-mask',
|
||||
}
|
||||
env.video = settings.noVideo ? undefined : { paused: settings.paused ?? false, ended: false, videoWidth: 2, videoHeight: 1 }
|
||||
env.style = style
|
||||
env.art = {
|
||||
isDestroy: false,
|
||||
template: { $video: env.video, $danmuku: settings.noDanmuku ? undefined : { style } },
|
||||
on(name, callback) {
|
||||
if (!env.listeners.has(name))
|
||||
env.listeners.set(name, new Set())
|
||||
env.listeners.get(name).add(callback)
|
||||
},
|
||||
off(name, callback) { env.listeners.get(name)?.delete(callback) },
|
||||
}
|
||||
const probe = {
|
||||
SupportedModels: { MediaPipeSelfieSegmentation: 'selfie' },
|
||||
async setBackend(name) {
|
||||
env.backends.push(name)
|
||||
if (settings.backendError?.[name])
|
||||
throw settings.backendError[name]
|
||||
return settings.backendResult?.[name] ?? true
|
||||
},
|
||||
async createSegmenter(model, config) {
|
||||
const instance = {
|
||||
model,
|
||||
config,
|
||||
disposed: 0,
|
||||
dispose() { instance.disposed++ },
|
||||
async segmentPeople(video) {
|
||||
env.inferences.push({ instance, video })
|
||||
if (settings.segmentError)
|
||||
throw settings.segmentError
|
||||
if (settings.pendingSegment) {
|
||||
const gate = deferred()
|
||||
env.segmentGates.push(gate)
|
||||
return gate.promise
|
||||
}
|
||||
return settings.empty ? [] : [{}]
|
||||
},
|
||||
}
|
||||
env.created.push(instance)
|
||||
if (settings.initError)
|
||||
throw settings.initError
|
||||
if (settings.pendingInit) {
|
||||
const gate = deferred()
|
||||
env.initGates.push(gate)
|
||||
await gate.promise
|
||||
}
|
||||
return instance
|
||||
},
|
||||
async toBinaryMask(...args) {
|
||||
env.masks.push(args)
|
||||
return {}
|
||||
},
|
||||
async drawMask(...args) { env.draws.push(args) },
|
||||
}
|
||||
const factory = evaluate(implementation.code, {
|
||||
__maskProbe: probe,
|
||||
require(name) {
|
||||
assert(['@tensorflow-models/body-segmentation', '@tensorflow/tfjs-core', '@tensorflow/tfjs-backend-webgl', '@tensorflow/tfjs-backend-cpu'].includes(name))
|
||||
return probe
|
||||
},
|
||||
console: { log() {}, warn: (...args) => env.logs.push(['warn', ...args]), error: (...args) => env.logs.push(['error', ...args]) },
|
||||
document: { createElement(tag) {
|
||||
assert.equal(tag, 'canvas')
|
||||
if (settings.canvasError)
|
||||
throw settings.canvasError
|
||||
return env.canvas
|
||||
} },
|
||||
requestAnimationFrame(callback) {
|
||||
const id = nextFrame++
|
||||
env.frames.set(id, callback)
|
||||
return id
|
||||
},
|
||||
cancelAnimationFrame(id) {
|
||||
env.cancelled.push(id)
|
||||
env.frames.delete(id)
|
||||
},
|
||||
})
|
||||
env.result = factory(settings.option)(env.art)
|
||||
env.emit = (name) => {
|
||||
for (const callback of [...(env.listeners.get(name) || [])]) callback()
|
||||
}
|
||||
env.destroy = () => {
|
||||
env.art.isDestroy = true
|
||||
env.emit('destroy')
|
||||
}
|
||||
env.frame = async () => {
|
||||
const frames = [...env.frames.values()]
|
||||
env.frames.clear()
|
||||
for (const callback of frames) await callback()
|
||||
await flushMask()
|
||||
}
|
||||
return env
|
||||
}
|
||||
Reference in new issue
Block a user