refactor(mask): [PKG-MASK-03] isolate model runs and release stale work

This commit is contained in:
Harvey Zhao committed 2026-09-13 23:46:15 +08:00
1 parent 6c989deccd
commit aba125262a
23 files changed
+1835 -379

No files matched your search

@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(t.artplayerPluginDanmukuMask=t(),define(function(){return t.artplayerPluginDanmukuMask})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginDanmukuMask=t()}(this,function(){"use strict";function e(e,t){for(var n=0;n<t.length;n++){const r=t[n];if("string"!==typeof r&&!Array.isArray(r))for(const t in r)if("default"!==t&&!(t in e)){const n=Object.getOwnPropertyDescriptor(r,t);n&&Object.defineProperty(e,t,n.get?n:{enumerable:!0,get:()=>r[t]})}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}class t{constructor(e,t){this.backend=e,this.dataMover=t,this.data=new WeakMap,this.dataIdsCount=0}get(e){return this.data.has(e)||this.dataMover.moveData(this.backend,e),this.data.get(e)}set(e,t){this.dataIdsCount++,this.data.set(e,t)}has(e){return this.data.has(e)}delete(e){return this.dataIdsCount--,this.data.delete(e)}numDataIds(){return this.dataIdsCount}}class n{refCount(e){return r("refCount")}incRef(e){return r("incRef")}timerAvailable(){return!0}time(e){return r("time")}read(e){return r("read")}readSync(e){return r("readSync")}readToGPU(e,t){return r("readToGPU")}numDataIds(){return r("numDataIds")}disposeData(e,t){return r("disposeData")}write(e,t,n){return r("write")}move(e,t,n,a,s){return r("move")}createTensorFromGPUData(e,t,n){return r("createTensorFromGPUData")}memory(){return r("memory")}floatPrecision(){return r("floatPrecision")}epsilon(){return 32===this.floatPrecision()?1e-7:1e-4}dispose(){return r("dispose")}}function r(e){throw new Error(`'${e}' not yet implemented or not found in the registry. This kernel may not be supported by the tfjs backend you have chosen`)}function a(e,t,n){return Math.max(e,Math.min(t,n))}function s(e){return e%2===0?e:e+1}function o(e,t,n){const r=e[t];e[t]=e[n],e[n]=r}function i(e,t){if(!e)throw new Error("string"===typeof t?t:t())}function u(e,t,n=""){i(d(e,t),()=>n+` Shapes ${e} and ${t} must match`)}function l(e){i(null!=e,()=>"The input to the tensor constructor must be a non-null value.")}function c(e){if(0===e.length)return 1;let t=e[0];for(let n=1;n<e.length;n++)t*=e[n];return t}function d(e,t){if(e===t)return!0;if(null==e||null==t)return!1;if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}function p(e){return e%1===0}function h(e){const t=Math.ceil(Math.sqrt(e));return[t,Math.ceil(e/t)]}function f(e,t){return t<=e.length?e:e+" ".repeat(t-e.length)}function m(e,t=e=>0,n,r){return new Promise((a,s)=>{let o=0;const i=()=>{if(e())return void a();o++;const u=t(o);null!=n&&o>=n?s():null!=r?r(i,u):setTimeout(i,u)};i()})}function g(e,t){let n=1,r=-1;for(let s=0;s<e.length;++s)if(e[s]>=0)n*=e[s];else if(-1===e[s]){if(-1!==r)throw Error(`Shapes can only have 1 implicit size. Found -1 at dim ${r} and dim ${s}`);r=s}else if(e[s]<0)throw Error(`Shapes can not be < 0. Found ${e[s]} at dim ${s}`);if(-1===r){if(t>0&&t!==n)throw Error(`Size(${t}) must match the product of shape ${e}`);return e}if(0===n)throw Error(`Cannot infer the missing size in [${e}] when there are 0 elements`);if(t%n!==0)throw Error(`The implicit shape can't be a fractional number. Got ${t} / ${n}`);const a=e.slice();return a[r]=t/n,a}function y(e,t){const n=t.length;return i((e=null==e?t.map((e,t)=>t):[].concat(e)).every(e=>e>=-n&&e<n),()=>`All values in axis param must be in range [-${n}, ${n}) but got axis ${e}`),i(e.every(e=>p(e)),()=>`All values in axis param must be integers but got axis ${e}`),e.map(e=>e<0?n+e:e)}function b(e,t){const n=[],r=[],a=null!=t&&Array.isArray(t)&&0===t.length,s=null==t||a?null:y(t,e).sort();let o=0;for(let i=0;i<e.length;++i){if(null!=s){if(s[o]===i&&1!==e[i])throw new Error(`Can't squeeze axis ${i} since its dim '${e[i]}' is not 1`);(null==s[o]||s[o]>i)&&1===e[i]&&(n.push(e[i]),r.push(i)),s[o]<=i&&o++}1!==e[i]&&(n.push(e[i]),r.push(i))}return{newShape:n,keptDims:r}}function x(e,t){return v(e,t)}function v(e,t){let n=null;if(null==e||"float32"===e)n=new Float32Array(t);else if("int32"===e)n=new Int32Array(t);else if("bool"===e)n=new Uint8Array(t);else{if("string"!==e)throw new Error(`Unknown data type ${e}`);n=new Array(t)}return n}function w(e,t){return"complex64"!==t&&(("float32"!==t||"complex64"===e)&&(("int32"!==t||"float32"===e||"complex64"===e)&&("bool"!==t||"bool"!==e)))}function k(e){if("float32"===e||"int32"===e)return 4;if("complex64"===e)return 8;if("bool"===e)return 1;throw new Error(`Unknown dtype ${e}`)}function I(e){return"string"===typeof e||e instanceof String}function N(e){return Array.isArray(e)?N(e[0]):e instanceof Float32Array?"float32":e instanceof Int32Array||e instanceof Uint8Array||e instanceof Uint8ClampedArray?"int32":"number"===typeof e?"float32":I(e)?"string":function(e){return"boolean"===typeof e}(e)?"bool":"float32"}function S(e){return!!(e&&e.constructor&&e.call&&e.apply)}function T(e,t){for(let n=t;n<e;++n)if(e%n===0)return nLine truncated
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginDanmukuMask=t(),define(function(){return e.artplayerPluginDanmukuMask})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginDanmukuMask=t()}(this,function(){"use strict";function e(e,t){for(var n=0;n<t.length;n++){const r=t[n];if("string"!==typeof r&&!Array.isArray(r))for(const t in r)if("default"!==t&&!(t in e)){const n=Object.getOwnPropertyDescriptor(r,t);n&&Object.defineProperty(e,t,n.get?n:{enumerable:!0,get:()=>r[t]})}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}class t{constructor(e,t){this.backend=e,this.dataMover=t,this.data=new WeakMap,this.dataIdsCount=0}get(e){return this.data.has(e)||this.dataMover.moveData(this.backend,e),this.data.get(e)}set(e,t){this.dataIdsCount++,this.data.set(e,t)}has(e){return this.data.has(e)}delete(e){return this.dataIdsCount--,this.data.delete(e)}numDataIds(){return this.dataIdsCount}}class n{refCount(e){return r("refCount")}incRef(e){return r("incRef")}timerAvailable(){return!0}time(e){return r("time")}read(e){return r("read")}readSync(e){return r("readSync")}readToGPU(e,t){return r("readToGPU")}numDataIds(){return r("numDataIds")}disposeData(e,t){return r("disposeData")}write(e,t,n){return r("write")}move(e,t,n,a,s){return r("move")}createTensorFromGPUData(e,t,n){return r("createTensorFromGPUData")}memory(){return r("memory")}floatPrecision(){return r("floatPrecision")}epsilon(){return 32===this.floatPrecision()?1e-7:1e-4}dispose(){return r("dispose")}}function r(e){throw new Error(`'${e}' not yet implemented or not found in the registry. This kernel may not be supported by the tfjs backend you have chosen`)}function a(e,t,n){return Math.max(e,Math.min(t,n))}function s(e){return e%2===0?e:e+1}function o(e,t,n){const r=e[t];e[t]=e[n],e[n]=r}function i(e,t){if(!e)throw new Error("string"===typeof t?t:t())}function u(e,t,n=""){i(d(e,t),()=>n+` Shapes ${e} and ${t} must match`)}function l(e){i(null!=e,()=>"The input to the tensor constructor must be a non-null value.")}function c(e){if(0===e.length)return 1;let t=e[0];for(let n=1;n<e.length;n++)t*=e[n];return t}function d(e,t){if(e===t)return!0;if(null==e||null==t)return!1;if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}function p(e){return e%1===0}function h(e){const t=Math.ceil(Math.sqrt(e));return[t,Math.ceil(e/t)]}function f(e,t){return t<=e.length?e:e+" ".repeat(t-e.length)}function m(e,t=e=>0,n,r){return new Promise((a,s)=>{let o=0;const i=()=>{if(e())return void a();o++;const u=t(o);null!=n&&o>=n?s():null!=r?r(i,u):setTimeout(i,u)};i()})}function g(e,t){let n=1,r=-1;for(let s=0;s<e.length;++s)if(e[s]>=0)n*=e[s];else if(-1===e[s]){if(-1!==r)throw Error(`Shapes can only have 1 implicit size. Found -1 at dim ${r} and dim ${s}`);r=s}else if(e[s]<0)throw Error(`Shapes can not be < 0. Found ${e[s]} at dim ${s}`);if(-1===r){if(t>0&&t!==n)throw Error(`Size(${t}) must match the product of shape ${e}`);return e}if(0===n)throw Error(`Cannot infer the missing size in [${e}] when there are 0 elements`);if(t%n!==0)throw Error(`The implicit shape can't be a fractional number. Got ${t} / ${n}`);const a=e.slice();return a[r]=t/n,a}function y(e,t){const n=t.length;return i((e=null==e?t.map((e,t)=>t):[].concat(e)).every(e=>e>=-n&&e<n),()=>`All values in axis param must be in range [-${n}, ${n}) but got axis ${e}`),i(e.every(e=>p(e)),()=>`All values in axis param must be integers but got axis ${e}`),e.map(e=>e<0?n+e:e)}function b(e,t){const n=[],r=[],a=null!=t&&Array.isArray(t)&&0===t.length,s=null==t||a?null:y(t,e).sort();let o=0;for(let i=0;i<e.length;++i){if(null!=s){if(s[o]===i&&1!==e[i])throw new Error(`Can't squeeze axis ${i} since its dim '${e[i]}' is not 1`);(null==s[o]||s[o]>i)&&1===e[i]&&(n.push(e[i]),r.push(i)),s[o]<=i&&o++}1!==e[i]&&(n.push(e[i]),r.push(i))}return{newShape:n,keptDims:r}}function x(e,t){return v(e,t)}function v(e,t){let n=null;if(null==e||"float32"===e)n=new Float32Array(t);else if("int32"===e)n=new Int32Array(t);else if("bool"===e)n=new Uint8Array(t);else{if("string"!==e)throw new Error(`Unknown data type ${e}`);n=new Array(t)}return n}function w(e,t){return"complex64"!==t&&(("float32"!==t||"complex64"===e)&&(("int32"!==t||"float32"===e||"complex64"===e)&&("bool"!==t||"bool"!==e)))}function k(e){if("float32"===e||"int32"===e)return 4;if("complex64"===e)return 8;if("bool"===e)return 1;throw new Error(`Unknown dtype ${e}`)}function I(e){return"string"===typeof e||e instanceof String}function N(e){return Array.isArray(e)?N(e[0]):e instanceof Float32Array?"float32":e instanceof Int32Array||e instanceof Uint8Array||e instanceof Uint8ClampedArray?"int32":"number"===typeof e?"float32":I(e)?"string":function(e){return"boolean"===typeof e}(e)?"bool":"float32"}function S(e){return!!(e&&e.constructor&&e.call&&e.apply)}function T(e,t){for(let n=t;n<e;++n)if(e%n===0)return nLine truncated
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(t.artplayerPluginDanmukuMask=t(),define(function(){return t.artplayerPluginDanmukuMask})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginDanmukuMask=t()}(this,function(){"use strict";var e=(e,t,n)=>new Promise((r,a)=>{var s=e=>{try{i(n.next(e))}catch(t){a(t)}},o=e=>{try{i(n.throw(e))}catch(t){a(t)}},i=e=>e.done?r(e.value):Promise.resolve(e.value).then(s,o);i((n=n.apply(e,t)).next())});function t(e,t){for(var n=0;n<t.length;n++){const r=t[n];if("string"!==typeof r&&!Array.isArray(r))for(const t in r)if("default"!==t&&!(t in e)){const n=Object.getOwnPropertyDescriptor(r,t);n&&Object.defineProperty(e,t,n.get?n:{enumerable:!0,get:()=>r[t]})}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}class n{constructor(e,t){this.backend=e,this.dataMover=t,this.data=new WeakMap,this.dataIdsCount=0}get(e){return this.data.has(e)||this.dataMover.moveData(this.backend,e),this.data.get(e)}set(e,t){this.dataIdsCount++,this.data.set(e,t)}has(e){return this.data.has(e)}delete(e){return this.dataIdsCount--,this.data.delete(e)}numDataIds(){return this.dataIdsCount}}class r{refCount(e){return a("refCount")}incRef(e){return a("incRef")}timerAvailable(){return!0}time(e){return a("time")}read(e){return a("read")}readSync(e){return a("readSync")}readToGPU(e,t){return a("readToGPU")}numDataIds(){return a("numDataIds")}disposeData(e,t){return a("disposeData")}write(e,t,n){return a("write")}move(e,t,n,r,s){return a("move")}createTensorFromGPUData(e,t,n){return a("createTensorFromGPUData")}memory(){return a("memory")}floatPrecision(){return a("floatPrecision")}epsilon(){return 32===this.floatPrecision()?1e-7:1e-4}dispose(){return a("dispose")}}function a(e){throw new Error(`'${e}' not yet implemented or not found in the registry. This kernel may not be supported by the tfjs backend you have chosen`)}function s(e,t,n){return Math.max(e,Math.min(t,n))}function o(e){return e%2===0?e:e+1}function i(e,t,n){const r=e[t];e[t]=e[n],e[n]=r}function u(e,t){if(!e)throw new Error("string"===typeof t?t:t())}function l(e,t,n=""){u(p(e,t),()=>n+` Shapes ${e} and ${t} must match`)}function c(e){u(null!=e,()=>"The input to the tensor constructor must be a non-null value.")}function d(e){if(0===e.length)return 1;let t=e[0];for(let n=1;n<e.length;n++)t*=e[n];return t}function p(e,t){if(e===t)return!0;if(null==e||null==t)return!1;if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}function h(e){return e%1===0}function f(e){const t=Math.ceil(Math.sqrt(e));return[t,Math.ceil(e/t)]}function m(e,t){return t<=e.length?e:e+" ".repeat(t-e.length)}function g(e,t=e=>0,n,r){return new Promise((a,s)=>{let o=0;const i=()=>{if(e())return void a();o++;const u=t(o);null!=n&&o>=n?s():null!=r?r(i,u):setTimeout(i,u)};i()})}function y(e,t){let n=1,r=-1;for(let s=0;s<e.length;++s)if(e[s]>=0)n*=e[s];else if(-1===e[s]){if(-1!==r)throw Error(`Shapes can only have 1 implicit size. Found -1 at dim ${r} and dim ${s}`);r=s}else if(e[s]<0)throw Error(`Shapes can not be < 0. Found ${e[s]} at dim ${s}`);if(-1===r){if(t>0&&t!==n)throw Error(`Size(${t}) must match the product of shape ${e}`);return e}if(0===n)throw Error(`Cannot infer the missing size in [${e}] when there are 0 elements`);if(t%n!==0)throw Error(`The implicit shape can't be a fractional number. Got ${t} / ${n}`);const a=e.slice();return a[r]=t/n,a}function b(e,t){const n=t.length;return u((e=null==e?t.map((e,t)=>t):[].concat(e)).every(e=>e>=-n&&e<n),()=>`All values in axis param must be in range [-${n}, ${n}) but got axis ${e}`),u(e.every(e=>h(e)),()=>`All values in axis param must be integers but got axis ${e}`),e.map(e=>e<0?n+e:e)}function x(e,t){const n=[],r=[],a=null!=t&&Array.isArray(t)&&0===t.length,s=null==t||a?null:b(t,e).sort();let o=0;for(let i=0;i<e.length;++i){if(null!=s){if(s[o]===i&&1!==e[i])throw new Error(`Can't squeeze axis ${i} since its dim '${e[i]}' is not 1`);(null==s[o]||s[o]>i)&&1===e[i]&&(n.push(e[i]),r.push(i)),s[o]<=i&&o++}1!==e[i]&&(n.push(e[i]),r.push(i))}return{newShape:n,keptDims:r}}function v(e,t){return w(e,t)}function w(e,t){let n=null;if(null==e||"float32"===e)n=new Float32Array(t);else if("int32"===e)n=new Int32Array(t);else if("bool"===e)n=new Uint8Array(t);else{if("string"!==e)throw new Error(`Unknown data type ${e}`);n=new Array(t)}return n}function k(e,t){return"complex64"!==t&&(("float32"!==t||"complex64"===e)&&(("int32"!==t||"float32"===e||"complex64"===e)&&("bool"!==t||"bool"!==e)))}function I(e){if("float32"===e||"int32"===e)return 4;if("complex64"===e)return 8;if("bool"===e)return 1;throw new Error(`Unknown dtype ${e}`)}function N(e){return"string"===typeof e||e instanceof String}function S(e){return Array.isArray(e)?S(e[0]):e instanceof Float32Array?"float32":e instanceof Int32Array||e instanceof Uint8Array||e instanceof Uint8ClampedArray?"int32":"number"===tLine truncated
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginDanmukuMask=t(),define(function(){return e.artplayerPluginDanmukuMask})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginDanmukuMask=t()}(this,function(){"use strict";var e=(e,t,n)=>new Promise((r,a)=>{var s=e=>{try{i(n.next(e))}catch(t){a(t)}},o=e=>{try{i(n.throw(e))}catch(t){a(t)}},i=e=>e.done?r(e.value):Promise.resolve(e.value).then(s,o);i((n=n.apply(e,t)).next())});function t(e,t){for(var n=0;n<t.length;n++){const r=t[n];if("string"!==typeof r&&!Array.isArray(r))for(const t in r)if("default"!==t&&!(t in e)){const n=Object.getOwnPropertyDescriptor(r,t);n&&Object.defineProperty(e,t,n.get?n:{enumerable:!0,get:()=>r[t]})}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}class n{constructor(e,t){this.backend=e,this.dataMover=t,this.data=new WeakMap,this.dataIdsCount=0}get(e){return this.data.has(e)||this.dataMover.moveData(this.backend,e),this.data.get(e)}set(e,t){this.dataIdsCount++,this.data.set(e,t)}has(e){return this.data.has(e)}delete(e){return this.dataIdsCount--,this.data.delete(e)}numDataIds(){return this.dataIdsCount}}class r{refCount(e){return a("refCount")}incRef(e){return a("incRef")}timerAvailable(){return!0}time(e){return a("time")}read(e){return a("read")}readSync(e){return a("readSync")}readToGPU(e,t){return a("readToGPU")}numDataIds(){return a("numDataIds")}disposeData(e,t){return a("disposeData")}write(e,t,n){return a("write")}move(e,t,n,r,s){return a("move")}createTensorFromGPUData(e,t,n){return a("createTensorFromGPUData")}memory(){return a("memory")}floatPrecision(){return a("floatPrecision")}epsilon(){return 32===this.floatPrecision()?1e-7:1e-4}dispose(){return a("dispose")}}function a(e){throw new Error(`'${e}' not yet implemented or not found in the registry. This kernel may not be supported by the tfjs backend you have chosen`)}function s(e,t,n){return Math.max(e,Math.min(t,n))}function o(e){return e%2===0?e:e+1}function i(e,t,n){const r=e[t];e[t]=e[n],e[n]=r}function u(e,t){if(!e)throw new Error("string"===typeof t?t:t())}function l(e,t,n=""){u(p(e,t),()=>n+` Shapes ${e} and ${t} must match`)}function c(e){u(null!=e,()=>"The input to the tensor constructor must be a non-null value.")}function d(e){if(0===e.length)return 1;let t=e[0];for(let n=1;n<e.length;n++)t*=e[n];return t}function p(e,t){if(e===t)return!0;if(null==e||null==t)return!1;if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}function h(e){return e%1===0}function f(e){const t=Math.ceil(Math.sqrt(e));return[t,Math.ceil(e/t)]}function m(e,t){return t<=e.length?e:e+" ".repeat(t-e.length)}function g(e,t=e=>0,n,r){return new Promise((a,s)=>{let o=0;const i=()=>{if(e())return void a();o++;const u=t(o);null!=n&&o>=n?s():null!=r?r(i,u):setTimeout(i,u)};i()})}function y(e,t){let n=1,r=-1;for(let s=0;s<e.length;++s)if(e[s]>=0)n*=e[s];else if(-1===e[s]){if(-1!==r)throw Error(`Shapes can only have 1 implicit size. Found -1 at dim ${r} and dim ${s}`);r=s}else if(e[s]<0)throw Error(`Shapes can not be < 0. Found ${e[s]} at dim ${s}`);if(-1===r){if(t>0&&t!==n)throw Error(`Size(${t}) must match the product of shape ${e}`);return e}if(0===n)throw Error(`Cannot infer the missing size in [${e}] when there are 0 elements`);if(t%n!==0)throw Error(`The implicit shape can't be a fractional number. Got ${t} / ${n}`);const a=e.slice();return a[r]=t/n,a}function b(e,t){const n=t.length;return u((e=null==e?t.map((e,t)=>t):[].concat(e)).every(e=>e>=-n&&e<n),()=>`All values in axis param must be in range [-${n}, ${n}) but got axis ${e}`),u(e.every(e=>h(e)),()=>`All values in axis param must be integers but got axis ${e}`),e.map(e=>e<0?n+e:e)}function x(e,t){const n=[],r=[],a=null!=t&&Array.isArray(t)&&0===t.length,s=null==t||a?null:b(t,e).sort();let o=0;for(let i=0;i<e.length;++i){if(null!=s){if(s[o]===i&&1!==e[i])throw new Error(`Can't squeeze axis ${i} since its dim '${e[i]}' is not 1`);(null==s[o]||s[o]>i)&&1===e[i]&&(n.push(e[i]),r.push(i)),s[o]<=i&&o++}1!==e[i]&&(n.push(e[i]),r.push(i))}return{newShape:n,keptDims:r}}function v(e,t){return w(e,t)}function w(e,t){let n=null;if(null==e||"float32"===e)n=new Float32Array(t);else if("int32"===e)n=new Int32Array(t);else if("bool"===e)n=new Uint8Array(t);else{if("string"!==e)throw new Error(`Unknown data type ${e}`);n=new Array(t)}return n}function k(e,t){return"complex64"!==t&&(("float32"!==t||"complex64"===e)&&(("int32"!==t||"float32"===e||"complex64"===e)&&("bool"!==t||"bool"!==e)))}function I(e){if("float32"===e||"int32"===e)return 4;if("complex64"===e)return 8;if("bool"===e)return 1;throw new Error(`Unknown dtype ${e}`)}function N(e){return"string"===typeof e||e instanceof String}function S(e){return Array.isArray(e)?S(e[0]):e instanceof Float32Array?"float32":e instanceof Int32Array||e instanceof Uint8Array||e instanceof Uint8ClampedArray?"int32":"number"===tLine truncated
+270 -107
View File
@@ -23,6 +23,20 @@ function _mergeNamespaces(n, m) {
}
return Object.freeze(Object.defineProperty(n, Symbol.toStringTag, { value: "Module" }));
}
function maskConfig(option) {
return {
solutionPath: option.solutionPath || "https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation",
modelSelection: option.modelSelection || 1,
smoothSegmentation: option.smoothSegmentation !== void 0 ? option.smoothSegmentation : true,
minDetectionConfidence: option.minDetectionConfidence || 0.5,
minTrackingConfidence: option.minTrackingConfidence || 0.5,
selfieMode: option.selfieMode || false,
drawContour: option.drawContour || false,
foregroundThreshold: option.foregroundThreshold || 0.5,
opacity: option.opacity || 1,
maskBlurAmount: option.maskBlurAmount || 3
};
}
const EPSILON_FLOAT32$1 = 1e-7;
const EPSILON_FLOAT16$1 = 1e-4;
class DataStorage {
@@ -49040,129 +49054,278 @@ const kernelConfigs = [
for (const kernelConfig of kernelConfigs) {
registerKernel(kernelConfig);
}
function artplayerPluginDanmukuMask(option = {}) {
return (art) => {
const {
template: { $video, $danmuku }
} = art;
let segmenter = null;
let canvas = null;
let ctx = null;
let animationFrameId = null;
let isInitialized = false;
const config = {
solutionPath: option.solutionPath || "https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation",
modelSelection: option.modelSelection || 1,
smoothSegmentation: option.smoothSegmentation !== void 0 ? option.smoothSegmentation : true,
minDetectionConfidence: option.minDetectionConfidence || 0.5,
minTrackingConfidence: option.minTrackingConfidence || 0.5,
selfieMode: option.selfieMode || false,
drawContour: option.drawContour || false,
foregroundThreshold: option.foregroundThreshold || 0.5,
opacity: option.opacity || 1,
maskBlurAmount: option.maskBlurAmount || 3
async function loadSegmenter(config, active) {
try {
await setBackend("webgl");
} catch (error) {
if (!active())
return null;
console.warn("WebGL backend not available, falling back to CPU", error.message);
await setBackend("cpu");
}
if (!active())
return null;
try {
return await ke(Se.MediaPipeSelfieSegmentation, {
runtime: "mediapipe",
modelType: "general",
solutionPath: config.solutionPath,
modelSelection: config.modelSelection,
smoothSegmentation: config.smoothSegmentation,
minDetectionConfidence: config.minDetectionConfidence,
minTrackingConfidence: config.minTrackingConfidence,
selfieMode: config.selfieMode
});
} catch (error) {
if (active())
console.error("Error initializing segmenter:", error);
return null;
}
}
async function releaseSegmenter(segmenter) {
try {
await segmenter.dispose();
} catch (error) {
console.warn("Failed to dispose danmuku mask segmenter:", error);
}
}
const toBinaryMask = (...args) => Ue(...args);
const drawMask = (...args) => Ne(...args);
function makeWhiteTransparent(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
if (data[i] > 250 && data[i + 1] > 250 && data[i + 2] > 250)
data[i + 3] = 0;
}
return imageData;
}
function createOutput(layer) {
const canvas = document.createElement("canvas");
try {
const ctx = canvas.getContext("2d");
if (!ctx)
throw new Error("Danmuku mask requires a 2D canvas context");
Object.assign(layer.style, {
maskMode: "alpha",
maskSize: "contain",
maskRepeat: "no-repeat",
backgroundSize: "contain",
backgroundRepeat: "no-repeat"
});
return { canvas, ctx };
} catch (error) {
canvas.width = 0;
canvas.height = 0;
throw error;
}
}
async function renderMask(output, video, layer, segmenter, config, active) {
const { canvas, ctx } = output;
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const segmentation = await segmenter.segmentPeople(video);
if (!active() || !segmentation || !segmentation.length)
return;
const mask = await toBinaryMask(
segmentation,
{ r: 255, g: 255, b: 255, a: 255 },
{ r: 0, g: 0, b: 0, a: 255 },
config.drawContour,
config.foregroundThreshold
);
if (!active())
return;
await drawMask(canvas, video, mask, config.opacity, config.maskBlurAmount);
if (!active())
return;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
ctx.putImageData(makeWhiteTransparent(imageData), 0, 0);
const url = canvas.toDataURL();
if (active())
layer.style.maskImage = `url(${url})`;
}
function releaseOutput(output) {
if (output) {
output.canvas.width = 0;
output.canvas.height = 0;
}
}
class MaskController {
constructor(art, config, { $video, $danmuku }) {
this.art = art;
this.config = config;
this.video = $video;
this.layer = $danmuku;
this.closed = art.isDestroy;
this.run = null;
this.tail = Promise.resolve();
this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
this.destroy = this.destroy.bind(this);
this.ready = () => {
this.start().catch((error) => console.error("Failed to start danmuku mask:", error));
};
async function initTensorFlow() {
if (!this.closed) {
try {
await setBackend("webgl");
art.on("destroy", this.destroy);
if (!this.closed)
art.on("ready", this.ready);
} catch (error) {
console.warn("WebGL backend not available, falling back to CPU", error.message);
await setBackend("cpu");
}
}
async function initSegmenter() {
await initTensorFlow();
const model = Se.MediaPipeSelfieSegmentation;
const segmenterConfig = {
runtime: "mediapipe",
modelType: "general",
solutionPath: config.solutionPath,
modelSelection: config.modelSelection,
smoothSegmentation: config.smoothSegmentation,
minDetectionConfidence: config.minDetectionConfidence,
minTrackingConfidence: config.minTrackingConfidence,
selfieMode: config.selfieMode
};
try {
segmenter = await ke(model, segmenterConfig);
isInitialized = true;
} catch (error) {
console.error("Error initializing segmenter:", error);
isInitialized = false;
}
}
function setupDanmukuStyle() {
Object.assign($danmuku.style, {
maskMode: "alpha",
maskSize: "contain",
maskRepeat: "no-repeat",
backgroundSize: "contain",
backgroundRepeat: "no-repeat"
});
}
function createCanvas2() {
canvas = document.createElement("canvas");
ctx = canvas.getContext("2d");
}
function makeWhiteTransparent(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
if (data[i] > 250 && data[i + 1] > 250 && data[i + 2] > 250) {
data[i + 3] = 0;
this.closed = true;
this.halt();
try {
art.off("ready", this.ready);
} catch {
}
try {
art.off("destroy", this.destroy);
} catch {
}
throw error;
}
return imageData;
}
async function segmentBody() {
if (!isInitialized || $video.paused || $video.ended) {
animationFrameId = requestAnimationFrame(segmentBody);
return;
}
}
active(run) {
return run === this.run && run.running && !this.closed && !this.art.isDestroy;
}
release(run) {
if (run.releasing)
return run.releasing;
const segmenter = run.segmenter;
run.segmenter = null;
const output = run.output;
run.output = null;
run.releasing = (async () => {
await Promise.resolve();
try {
canvas.width = $video.videoWidth;
canvas.height = $video.videoHeight;
const segmentation = await segmenter.segmentPeople($video);
if (!segmentation || segmentation.length === 0) {
animationFrameId = requestAnimationFrame(segmentBody);
releaseOutput(output);
} finally {
if (segmenter)
await releaseSegmenter(segmenter);
}
})();
return run.releasing;
}
schedule(run) {
if (!this.active(run) || run.frame !== null || run.busy)
return;
run.frame = requestAnimationFrame(() => {
run.frame = null;
this.tick(run);
});
}
tick(run) {
if (!this.active(run) || run.busy)
return;
const video = this.video;
if (video.paused || video.ended || !(video.videoWidth > 0 && video.videoHeight > 0)) {
this.schedule(run);
return;
}
run.busy = true;
const work = (async () => {
try {
await Promise.resolve();
if (this.active(run))
await renderMask(run.output, video, this.layer, run.segmenter, this.config, () => this.active(run));
} catch (error) {
if (this.active(run))
console.error("Error in segmentBody:", error);
} finally {
run.busy = false;
if (!this.active(run))
await this.release(run);
else this.schedule(run);
}
})();
this.tail = work.catch((error) => console.warn("Failed to release danmuku mask resources:", error));
}
async start() {
if (this.closed || this.art.isDestroy)
return;
if (this.run?.running)
return this.run.started;
let cancel;
const cancelled = new Promise((resolve) => cancel = resolve);
const run = { running: true, initializing: true, busy: false, frame: null, segmenter: null, output: null, cancel, started: null };
const previous = this.tail;
this.run = run;
const initialize = (async () => {
try {
await previous;
if (!this.active(run))
return;
if (!this.video || !this.layer?.style)
throw new Error("Danmuku mask requires core video and danmuku template nodes");
run.segmenter = await loadSegmenter(this.config, () => this.active(run));
if (!this.active(run))
return;
if (!run.segmenter) {
run.running = false;
return;
}
const foregroundColor = { r: 255, g: 255, b: 255, a: 255 };
const backgroundColor = { r: 0, g: 0, b: 0, a: 255 };
const mask = await Ue(
segmentation,
foregroundColor,
backgroundColor,
config.drawContour,
config.foregroundThreshold
);
await Ne(canvas, $video, mask, config.opacity, config.maskBlurAmount);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
ctx.putImageData(makeWhiteTransparent(imageData), 0, 0);
$danmuku.style.maskImage = `url(${canvas.toDataURL()})`;
run.output = createOutput(this.layer);
} catch (error) {
console.error("Error in segmentBody:", error);
if (this.active(run)) {
run.running = false;
throw error;
}
} finally {
run.initializing = false;
if (!this.active(run))
await this.release(run);
}
animationFrameId = requestAnimationFrame(segmentBody);
if (this.active(run))
this.tick(run);
})();
this.tail = initialize.catch(() => {
});
run.started = Promise.race([initialize, cancelled]);
return run.started;
}
halt() {
const run = this.run;
if (run) {
run.running = false;
run.cancel();
if (run.frame !== null) {
cancelAnimationFrame(run.frame);
run.frame = null;
}
if (!run.initializing && !run.busy)
this.tail = this.release(run).catch((error) => console.warn("Failed to release danmuku mask resources:", error));
}
}
stop() {
this.halt();
if (this.layer?.style)
this.layer.style.maskImage = "none";
}
destroy() {
if (this.closed)
return;
this.closed = true;
try {
this.stop();
} finally {
try {
this.art.off("ready", this.ready);
} finally {
this.art.off("destroy", this.destroy);
}
}
}
}
function artplayerPluginDanmukuMask(option = {}) {
return (art) => {
const { template: { $video, $danmuku } } = art;
const controller = new MaskController(art, maskConfig(option), { $video, $danmuku });
async function startSegmentation() {
if (!isInitialized) {
await initSegmenter();
}
if (!canvas) {
createCanvas2();
}
setupDanmukuStyle();
segmentBody();
await controller.start();
}
function stopSegmentation() {
$danmuku.style.maskImage = "none";
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
controller.stop();
}
art.on("ready", startSegmentation);
art.on("destroy", stopSegmentation);
return {
name: "artplayerPluginDanmukuMask",
start: startSegmentation,
+2 -2
View File
@@ -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/danmuku-setting.test.js test/danmuku-heatmap.test.js test/danmuku-renderer.test.js test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js 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:unit": "node --test test/danmuku-setting.test.js test/danmuku-heatmap.test.js test/danmuku-renderer.test.js test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js test/danmuku-mask-failures.test.js test/danmuku-mask-lifecycle.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",
@@ -99,7 +99,7 @@
"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:danmuku-mask": "node --test test/danmuku-mask-failures.test.js refactor/scripts/danmuku-mask-contract.test.mjs",
"test:danmuku-mask": "node --test test/danmuku-mask-lifecycle.test.js test/danmuku-mask-failures.test.js refactor/scripts/danmuku-mask-contract.test.mjs",
"test:danmuku": "node --test test/danmuku-setting.test.js test/danmuku-heatmap.test.js test/danmuku-renderer.test.js test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js refactor/scripts/danmuku-contract.test.mjs",
"test:danmuku-types": "node --test refactor/scripts/danmuku-types.test.mjs",
"test:danmuku-types-package": "node refactor/scripts/danmuku-package-types.mjs"
@@ -0,0 +1,133 @@
# Danmuku Mask maintenance map
The compatibility baseline is the actual npm 1.1.0 package and the earlier
1.0.0 export shape. Sources and historical failure probes are recorded in
`refactor/baselines/danmuku-mask-contract.md`, `danmuku-mask-release.json` and
`danmuku-mask-failures-validation.json`, relative to the repository root.
## Modules and ownership
| Module | Responsibility |
| --- | --- |
| `src/index.js` | Preserve synchronous registration, capture core template nodes before option getters, expose named start/stop closures |
| `src/config.js` | Snapshot the original option defaults without changing OR/undefined semantics |
| `src/sdk.js` | Existing TF backend selection, MediaPipe adapter configuration, mask calls and model disposal outlet |
| `src/controller.js` | One active run, initialization/inference serialization, cancellation, RAF ownership, ready/destroy subscriptions |
| `src/output.js` | Private canvas/context, unchanged binary mask colors and threshold conversion, guarded maskImage commit, canvas release |
These modules remain JavaScript for PKG-MASK-03. Full owned-source TypeScript and
public declaration work belongs to PKG-MASK-04; this task does not claim it is
finished. Public declarations are unchanged.
Mask captures the core's video and `.art-danmuku` layer. The core template
provides that layer before plugins register; Danmuku does not create it.
Mask does not access the Danmuku queue, individual item nodes, Worker protocol,
facade/Owner methods or `artplayerPluginDanmuku:*` events. Danmuku07's fixed CSS
mask test establishes root-layer coexistence, not segmentation/model acceptance.
The existing demo registers Danmuku then Mask; that order alone is not a model
or template readiness guarantee.
## Start, cancellation and resource lifecycle
Registration returns `{ name, start, stop }` synchronously. The public named
`startSegmentation` closure returns `Promise<void>`; `stopSegmentation` returns
undefined. Extracted calls keep their captured controller. Concurrent/repeated
starts share the current initialization and do not add a second inference or
RAF chain. Successful start still does not wait for a completed first mask.
Each run owns its model, canvas/context, cancellation resolver and at most one
RAF or inference. A run may commit output only while it remains current, running
and attached to an undestroyed player. Check this after backend/model loading,
segmentPeople, toBinaryMask and drawMask. An old continuation must never update
the layer or reschedule its own loop after stop, destroy or replacement.
Stop marks the run inactive, resolves pending public start, cancels its exact
RAF (including id zero), and writes the historical `maskImage = 'none'`.
It leaves the other setup styles intact. Destroy additionally prevents restart
and detaches the exact ready/destroy callbacks. Registration rollback detaches
subscriptions without changing preexisting host styles. Ready observes startup
rejections locally; explicit start still rejects backend or canvas failures.
Stopping now releases the model and canvas instead of retaining them indefinitely.
A subsequent start initializes a new model. This is an intentional resource
correction; the extra restart initialization cost needs native validation.
An in-progress SDK operation cannot be aborted through the current SDK API.
Its late result is ignored, then its private canvas bitmap is reset to zero
dimensions and its accessible model is disposed. Once frame work has settled,
a pending SDK disposal must not keep the independent canvas bitmap allocated.
Restart waits for that outstanding work and
visible disposal result instead of overlapping two model runs. Repeated stop
calls share disposal and cannot bypass that wait.
If an SDK operation never settles, a restart can remain pending. Cancellation
still settles its public start when stopped again. The plugin cannot prove
termination of unabortable native/WASM work or recover resources hidden inside
a rejected SDK initialization. Do not claim otherwise from Promise cancellation.
`releaseSegmenter` calls the supported segmenter.dispose API and observes any
returned Promise. The installed body-segmentation 1.0.2 MediaPipe implementation
calls its underlying solution.close without returning that result. Consequently
awaiting dispose does not prove the native close or GPU release has completed.
Actual resource release, model requests, cross-instance TF state and devices
remain PKG-MASK-05 evidence requirements. Do not depend on private SDK fields to
make a disposal test appear stronger than its real API.
## Behavior and compatibility boundaries
- Keep registrar-time option snapshots and the historical OR defaults. Zero
modelSelection/opacity/threshold/blur still take their defaults; explicit
smoothSegmentation=false is retained. Template references are captured before
getters in the option snapshot run.
- Keep runtime=mediapipe and modelType=general. The current adapter maps general
to modelSelection=0 and ignores several additionally passed plugin options;
connecting those options would be a behavior change, not a cleanup.
- tf.setBackend('webgl') fulfilled false does not mean CPU fallback. Only rejection
tries CPU. TF backend selection does not prove MediaPipe's inference backend.
- Model creation failure still logs `Error initializing segmenter:` and resolves
start, but no longer creates an idle endless RAF loop. Explicit start retries.
CPU/backend failure still rejects explicit start; automatic ready logs the
observed rejection through `Failed to start danmuku mask:`.
- No video/layer or no 2D context fails startup before retaining an accessible
model or loop. Missing dimensions waits for readable frames. These are failure
handling corrections; normal video inference/output is unchanged.
- Active inference/read failures retain `Error in segmentBody:` and retry on the
next frame without silently clearing an earlier mask. Late failures after
cancellation are observed without restarting work or overwriting user state.
- Preserve white foreground/black background binary colors, drawMask arguments,
canvas PNG data URLs and strict RGB >250 transparency (250 stays opaque).
The canvas remains private; it is never inserted into Danmuku's node pool.
- This task introduces no public events, type augmentation, SDK dependency
updates, default CDN pinning or new model-selection semantics.
## Editing and validation
From the repository root with pinned Node and Yarn:
```sh
node --test test/danmuku-mask-lifecycle.test.js
yarn test:danmuku-mask
node --test test/danmuku-mask-failures.test.js refactor/scripts/danmuku-mask-contract.test.mjs
node node_modules/eslint/bin/eslint.js packages/artplayer-plugin-danmuku-mask/src test/helpers/danmuku-mask-candidate.js test/danmuku-mask-lifecycle.test.js
yarn build artplayer-plugin-danmuku-mask
yarn dev artplayer-plugin-danmuku-mask
```
The candidate helper bundles current owned modules and substitutes the SDK
imports with controlled implementations. Its fake RAF/video/canvas test
cancellation and output ownership; they do not prove real model quality, canvas
pixels, CSS alignment, CORS, browser scheduling, GPU memory or WASM cleanup.
Historical probes remain frozen and assert old failures, not candidate success.
Future changes to controller scheduling must cover stop/destroy during backend,
model, inference, binary-mask and draw waits; repeated/concurrent starts; restart
behind disposal; synchronous SDK reentry; RAF zero; late rejection; and listener
rollback. Change the output module only with pixel-boundary and late-commit tests.
Two registrations in the same controlled SDK/RAF environment also verify that
stopping or destroying one leaves the other model, mask and scheduled frame owned
by its original player. Native shared TensorFlow backend state remains unverified.
Native acceptance still needs actual local model loading, new/old cores, real
Danmuku composition, pause/seek/source/layout changes, multiple players, failure
recovery and post-destroy resources. The default unversioned solutionPath can
load assets independently of Yarn's SDK resolution. PKG-MASK-04/05/06 remain
separate source/type, real combination and distribution/release stages.
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(t.artplayerPluginDanmukuMask=t(),define(function(){return t.artplayerPluginDanmukuMask})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginDanmukuMask=t()}(this,function(){"use strict";function e(e,t){for(var n=0;n<t.length;n++){const r=t[n];if("string"!==typeof r&&!Array.isArray(r))for(const t in r)if("default"!==t&&!(t in e)){const n=Object.getOwnPropertyDescriptor(r,t);n&&Object.defineProperty(e,t,n.get?n:{enumerable:!0,get:()=>r[t]})}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}class t{constructor(e,t){this.backend=e,this.dataMover=t,this.data=new WeakMap,this.dataIdsCount=0}get(e){return this.data.has(e)||this.dataMover.moveData(this.backend,e),this.data.get(e)}set(e,t){this.dataIdsCount++,this.data.set(e,t)}has(e){return this.data.has(e)}delete(e){return this.dataIdsCount--,this.data.delete(e)}numDataIds(){return this.dataIdsCount}}class n{refCount(e){return r("refCount")}incRef(e){return r("incRef")}timerAvailable(){return!0}time(e){return r("time")}read(e){return r("read")}readSync(e){return r("readSync")}readToGPU(e,t){return r("readToGPU")}numDataIds(){return r("numDataIds")}disposeData(e,t){return r("disposeData")}write(e,t,n){return r("write")}move(e,t,n,a,s){return r("move")}createTensorFromGPUData(e,t,n){return r("createTensorFromGPUData")}memory(){return r("memory")}floatPrecision(){return r("floatPrecision")}epsilon(){return 32===this.floatPrecision()?1e-7:1e-4}dispose(){return r("dispose")}}function r(e){throw new Error(`'${e}' not yet implemented or not found in the registry. This kernel may not be supported by the tfjs backend you have chosen`)}function a(e,t,n){return Math.max(e,Math.min(t,n))}function s(e){return e%2===0?e:e+1}function o(e,t,n){const r=e[t];e[t]=e[n],e[n]=r}function i(e,t){if(!e)throw new Error("string"===typeof t?t:t())}function u(e,t,n=""){i(d(e,t),()=>n+` Shapes ${e} and ${t} must match`)}function l(e){i(null!=e,()=>"The input to the tensor constructor must be a non-null value.")}function c(e){if(0===e.length)return 1;let t=e[0];for(let n=1;n<e.length;n++)t*=e[n];return t}function d(e,t){if(e===t)return!0;if(null==e||null==t)return!1;if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}function p(e){return e%1===0}function h(e){const t=Math.ceil(Math.sqrt(e));return[t,Math.ceil(e/t)]}function f(e,t){return t<=e.length?e:e+" ".repeat(t-e.length)}function m(e,t=e=>0,n,r){return new Promise((a,s)=>{let o=0;const i=()=>{if(e())return void a();o++;const u=t(o);null!=n&&o>=n?s():null!=r?r(i,u):setTimeout(i,u)};i()})}function g(e,t){let n=1,r=-1;for(let s=0;s<e.length;++s)if(e[s]>=0)n*=e[s];else if(-1===e[s]){if(-1!==r)throw Error(`Shapes can only have 1 implicit size. Found -1 at dim ${r} and dim ${s}`);r=s}else if(e[s]<0)throw Error(`Shapes can not be < 0. Found ${e[s]} at dim ${s}`);if(-1===r){if(t>0&&t!==n)throw Error(`Size(${t}) must match the product of shape ${e}`);return e}if(0===n)throw Error(`Cannot infer the missing size in [${e}] when there are 0 elements`);if(t%n!==0)throw Error(`The implicit shape can't be a fractional number. Got ${t} / ${n}`);const a=e.slice();return a[r]=t/n,a}function y(e,t){const n=t.length;return i((e=null==e?t.map((e,t)=>t):[].concat(e)).every(e=>e>=-n&&e<n),()=>`All values in axis param must be in range [-${n}, ${n}) but got axis ${e}`),i(e.every(e=>p(e)),()=>`All values in axis param must be integers but got axis ${e}`),e.map(e=>e<0?n+e:e)}function b(e,t){const n=[],r=[],a=null!=t&&Array.isArray(t)&&0===t.length,s=null==t||a?null:y(t,e).sort();let o=0;for(let i=0;i<e.length;++i){if(null!=s){if(s[o]===i&&1!==e[i])throw new Error(`Can't squeeze axis ${i} since its dim '${e[i]}' is not 1`);(null==s[o]||s[o]>i)&&1===e[i]&&(n.push(e[i]),r.push(i)),s[o]<=i&&o++}1!==e[i]&&(n.push(e[i]),r.push(i))}return{newShape:n,keptDims:r}}function x(e,t){return v(e,t)}function v(e,t){let n=null;if(null==e||"float32"===e)n=new Float32Array(t);else if("int32"===e)n=new Int32Array(t);else if("bool"===e)n=new Uint8Array(t);else{if("string"!==e)throw new Error(`Unknown data type ${e}`);n=new Array(t)}return n}function w(e,t){return"complex64"!==t&&(("float32"!==t||"complex64"===e)&&(("int32"!==t||"float32"===e||"complex64"===e)&&("bool"!==t||"bool"!==e)))}function k(e){if("float32"===e||"int32"===e)return 4;if("complex64"===e)return 8;if("bool"===e)return 1;throw new Error(`Unknown dtype ${e}`)}function I(e){return"string"===typeof e||e instanceof String}function N(e){return Array.isArray(e)?N(e[0]):e instanceof Float32Array?"float32":e instanceof Int32Array||e instanceof Uint8Array||e instanceof Uint8ClampedArray?"int32":"number"===typeof e?"float32":I(e)?"string":function(e){return"boolean"===typeof e}(e)?"bool":"float32"}function S(e){return!!(e&&e.constructor&&e.call&&e.apply)}function T(e,t){for(let n=t;n<e;++n)if(e%n===0)return nLine truncated
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginDanmukuMask=t(),define(function(){return e.artplayerPluginDanmukuMask})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginDanmukuMask=t()}(this,function(){"use strict";function e(e,t){for(var n=0;n<t.length;n++){const r=t[n];if("string"!==typeof r&&!Array.isArray(r))for(const t in r)if("default"!==t&&!(t in e)){const n=Object.getOwnPropertyDescriptor(r,t);n&&Object.defineProperty(e,t,n.get?n:{enumerable:!0,get:()=>r[t]})}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}class t{constructor(e,t){this.backend=e,this.dataMover=t,this.data=new WeakMap,this.dataIdsCount=0}get(e){return this.data.has(e)||this.dataMover.moveData(this.backend,e),this.data.get(e)}set(e,t){this.dataIdsCount++,this.data.set(e,t)}has(e){return this.data.has(e)}delete(e){return this.dataIdsCount--,this.data.delete(e)}numDataIds(){return this.dataIdsCount}}class n{refCount(e){return r("refCount")}incRef(e){return r("incRef")}timerAvailable(){return!0}time(e){return r("time")}read(e){return r("read")}readSync(e){return r("readSync")}readToGPU(e,t){return r("readToGPU")}numDataIds(){return r("numDataIds")}disposeData(e,t){return r("disposeData")}write(e,t,n){return r("write")}move(e,t,n,a,s){return r("move")}createTensorFromGPUData(e,t,n){return r("createTensorFromGPUData")}memory(){return r("memory")}floatPrecision(){return r("floatPrecision")}epsilon(){return 32===this.floatPrecision()?1e-7:1e-4}dispose(){return r("dispose")}}function r(e){throw new Error(`'${e}' not yet implemented or not found in the registry. This kernel may not be supported by the tfjs backend you have chosen`)}function a(e,t,n){return Math.max(e,Math.min(t,n))}function s(e){return e%2===0?e:e+1}function o(e,t,n){const r=e[t];e[t]=e[n],e[n]=r}function i(e,t){if(!e)throw new Error("string"===typeof t?t:t())}function u(e,t,n=""){i(d(e,t),()=>n+` Shapes ${e} and ${t} must match`)}function l(e){i(null!=e,()=>"The input to the tensor constructor must be a non-null value.")}function c(e){if(0===e.length)return 1;let t=e[0];for(let n=1;n<e.length;n++)t*=e[n];return t}function d(e,t){if(e===t)return!0;if(null==e||null==t)return!1;if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}function p(e){return e%1===0}function h(e){const t=Math.ceil(Math.sqrt(e));return[t,Math.ceil(e/t)]}function f(e,t){return t<=e.length?e:e+" ".repeat(t-e.length)}function m(e,t=e=>0,n,r){return new Promise((a,s)=>{let o=0;const i=()=>{if(e())return void a();o++;const u=t(o);null!=n&&o>=n?s():null!=r?r(i,u):setTimeout(i,u)};i()})}function g(e,t){let n=1,r=-1;for(let s=0;s<e.length;++s)if(e[s]>=0)n*=e[s];else if(-1===e[s]){if(-1!==r)throw Error(`Shapes can only have 1 implicit size. Found -1 at dim ${r} and dim ${s}`);r=s}else if(e[s]<0)throw Error(`Shapes can not be < 0. Found ${e[s]} at dim ${s}`);if(-1===r){if(t>0&&t!==n)throw Error(`Size(${t}) must match the product of shape ${e}`);return e}if(0===n)throw Error(`Cannot infer the missing size in [${e}] when there are 0 elements`);if(t%n!==0)throw Error(`The implicit shape can't be a fractional number. Got ${t} / ${n}`);const a=e.slice();return a[r]=t/n,a}function y(e,t){const n=t.length;return i((e=null==e?t.map((e,t)=>t):[].concat(e)).every(e=>e>=-n&&e<n),()=>`All values in axis param must be in range [-${n}, ${n}) but got axis ${e}`),i(e.every(e=>p(e)),()=>`All values in axis param must be integers but got axis ${e}`),e.map(e=>e<0?n+e:e)}function b(e,t){const n=[],r=[],a=null!=t&&Array.isArray(t)&&0===t.length,s=null==t||a?null:y(t,e).sort();let o=0;for(let i=0;i<e.length;++i){if(null!=s){if(s[o]===i&&1!==e[i])throw new Error(`Can't squeeze axis ${i} since its dim '${e[i]}' is not 1`);(null==s[o]||s[o]>i)&&1===e[i]&&(n.push(e[i]),r.push(i)),s[o]<=i&&o++}1!==e[i]&&(n.push(e[i]),r.push(i))}return{newShape:n,keptDims:r}}function x(e,t){return v(e,t)}function v(e,t){let n=null;if(null==e||"float32"===e)n=new Float32Array(t);else if("int32"===e)n=new Int32Array(t);else if("bool"===e)n=new Uint8Array(t);else{if("string"!==e)throw new Error(`Unknown data type ${e}`);n=new Array(t)}return n}function w(e,t){return"complex64"!==t&&(("float32"!==t||"complex64"===e)&&(("int32"!==t||"float32"===e||"complex64"===e)&&("bool"!==t||"bool"!==e)))}function k(e){if("float32"===e||"int32"===e)return 4;if("complex64"===e)return 8;if("bool"===e)return 1;throw new Error(`Unknown dtype ${e}`)}function I(e){return"string"===typeof e||e instanceof String}function N(e){return Array.isArray(e)?N(e[0]):e instanceof Float32Array?"float32":e instanceof Int32Array||e instanceof Uint8Array||e instanceof Uint8ClampedArray?"int32":"number"===typeof e?"float32":I(e)?"string":function(e){return"boolean"===typeof e}(e)?"bool":"float32"}function S(e){return!!(e&&e.constructor&&e.call&&e.apply)}function T(e,t){for(let n=t;n<e;++n)if(e%n===0)return nLine truncated
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(t.artplayerPluginDanmukuMask=t(),define(function(){return t.artplayerPluginDanmukuMask})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginDanmukuMask=t()}(this,function(){"use strict";var e=(e,t,n)=>new Promise((r,a)=>{var s=e=>{try{i(n.next(e))}catch(t){a(t)}},o=e=>{try{i(n.throw(e))}catch(t){a(t)}},i=e=>e.done?r(e.value):Promise.resolve(e.value).then(s,o);i((n=n.apply(e,t)).next())});function t(e,t){for(var n=0;n<t.length;n++){const r=t[n];if("string"!==typeof r&&!Array.isArray(r))for(const t in r)if("default"!==t&&!(t in e)){const n=Object.getOwnPropertyDescriptor(r,t);n&&Object.defineProperty(e,t,n.get?n:{enumerable:!0,get:()=>r[t]})}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}class n{constructor(e,t){this.backend=e,this.dataMover=t,this.data=new WeakMap,this.dataIdsCount=0}get(e){return this.data.has(e)||this.dataMover.moveData(this.backend,e),this.data.get(e)}set(e,t){this.dataIdsCount++,this.data.set(e,t)}has(e){return this.data.has(e)}delete(e){return this.dataIdsCount--,this.data.delete(e)}numDataIds(){return this.dataIdsCount}}class r{refCount(e){return a("refCount")}incRef(e){return a("incRef")}timerAvailable(){return!0}time(e){return a("time")}read(e){return a("read")}readSync(e){return a("readSync")}readToGPU(e,t){return a("readToGPU")}numDataIds(){return a("numDataIds")}disposeData(e,t){return a("disposeData")}write(e,t,n){return a("write")}move(e,t,n,r,s){return a("move")}createTensorFromGPUData(e,t,n){return a("createTensorFromGPUData")}memory(){return a("memory")}floatPrecision(){return a("floatPrecision")}epsilon(){return 32===this.floatPrecision()?1e-7:1e-4}dispose(){return a("dispose")}}function a(e){throw new Error(`'${e}' not yet implemented or not found in the registry. This kernel may not be supported by the tfjs backend you have chosen`)}function s(e,t,n){return Math.max(e,Math.min(t,n))}function o(e){return e%2===0?e:e+1}function i(e,t,n){const r=e[t];e[t]=e[n],e[n]=r}function u(e,t){if(!e)throw new Error("string"===typeof t?t:t())}function l(e,t,n=""){u(p(e,t),()=>n+` Shapes ${e} and ${t} must match`)}function c(e){u(null!=e,()=>"The input to the tensor constructor must be a non-null value.")}function d(e){if(0===e.length)return 1;let t=e[0];for(let n=1;n<e.length;n++)t*=e[n];return t}function p(e,t){if(e===t)return!0;if(null==e||null==t)return!1;if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}function h(e){return e%1===0}function f(e){const t=Math.ceil(Math.sqrt(e));return[t,Math.ceil(e/t)]}function m(e,t){return t<=e.length?e:e+" ".repeat(t-e.length)}function g(e,t=e=>0,n,r){return new Promise((a,s)=>{let o=0;const i=()=>{if(e())return void a();o++;const u=t(o);null!=n&&o>=n?s():null!=r?r(i,u):setTimeout(i,u)};i()})}function y(e,t){let n=1,r=-1;for(let s=0;s<e.length;++s)if(e[s]>=0)n*=e[s];else if(-1===e[s]){if(-1!==r)throw Error(`Shapes can only have 1 implicit size. Found -1 at dim ${r} and dim ${s}`);r=s}else if(e[s]<0)throw Error(`Shapes can not be < 0. Found ${e[s]} at dim ${s}`);if(-1===r){if(t>0&&t!==n)throw Error(`Size(${t}) must match the product of shape ${e}`);return e}if(0===n)throw Error(`Cannot infer the missing size in [${e}] when there are 0 elements`);if(t%n!==0)throw Error(`The implicit shape can't be a fractional number. Got ${t} / ${n}`);const a=e.slice();return a[r]=t/n,a}function b(e,t){const n=t.length;return u((e=null==e?t.map((e,t)=>t):[].concat(e)).every(e=>e>=-n&&e<n),()=>`All values in axis param must be in range [-${n}, ${n}) but got axis ${e}`),u(e.every(e=>h(e)),()=>`All values in axis param must be integers but got axis ${e}`),e.map(e=>e<0?n+e:e)}function x(e,t){const n=[],r=[],a=null!=t&&Array.isArray(t)&&0===t.length,s=null==t||a?null:b(t,e).sort();let o=0;for(let i=0;i<e.length;++i){if(null!=s){if(s[o]===i&&1!==e[i])throw new Error(`Can't squeeze axis ${i} since its dim '${e[i]}' is not 1`);(null==s[o]||s[o]>i)&&1===e[i]&&(n.push(e[i]),r.push(i)),s[o]<=i&&o++}1!==e[i]&&(n.push(e[i]),r.push(i))}return{newShape:n,keptDims:r}}function v(e,t){return w(e,t)}function w(e,t){let n=null;if(null==e||"float32"===e)n=new Float32Array(t);else if("int32"===e)n=new Int32Array(t);else if("bool"===e)n=new Uint8Array(t);else{if("string"!==e)throw new Error(`Unknown data type ${e}`);n=new Array(t)}return n}function k(e,t){return"complex64"!==t&&(("float32"!==t||"complex64"===e)&&(("int32"!==t||"float32"===e||"complex64"===e)&&("bool"!==t||"bool"!==e)))}function I(e){if("float32"===e||"int32"===e)return 4;if("complex64"===e)return 8;if("bool"===e)return 1;throw new Error(`Unknown dtype ${e}`)}function N(e){return"string"===typeof e||e instanceof String}function S(e){return Array.isArray(e)?S(e[0]):e instanceof Float32Array?"float32":e instanceof Int32Array||e instanceof Uint8Array||e instanceof Uint8ClampedArray?"int32":"number"===tLine truncated
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerPluginDanmukuMask=t(),define(function(){return e.artplayerPluginDanmukuMask})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginDanmukuMask=t()}(this,function(){"use strict";var e=(e,t,n)=>new Promise((r,a)=>{var s=e=>{try{i(n.next(e))}catch(t){a(t)}},o=e=>{try{i(n.throw(e))}catch(t){a(t)}},i=e=>e.done?r(e.value):Promise.resolve(e.value).then(s,o);i((n=n.apply(e,t)).next())});function t(e,t){for(var n=0;n<t.length;n++){const r=t[n];if("string"!==typeof r&&!Array.isArray(r))for(const t in r)if("default"!==t&&!(t in e)){const n=Object.getOwnPropertyDescriptor(r,t);n&&Object.defineProperty(e,t,n.get?n:{enumerable:!0,get:()=>r[t]})}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}class n{constructor(e,t){this.backend=e,this.dataMover=t,this.data=new WeakMap,this.dataIdsCount=0}get(e){return this.data.has(e)||this.dataMover.moveData(this.backend,e),this.data.get(e)}set(e,t){this.dataIdsCount++,this.data.set(e,t)}has(e){return this.data.has(e)}delete(e){return this.dataIdsCount--,this.data.delete(e)}numDataIds(){return this.dataIdsCount}}class r{refCount(e){return a("refCount")}incRef(e){return a("incRef")}timerAvailable(){return!0}time(e){return a("time")}read(e){return a("read")}readSync(e){return a("readSync")}readToGPU(e,t){return a("readToGPU")}numDataIds(){return a("numDataIds")}disposeData(e,t){return a("disposeData")}write(e,t,n){return a("write")}move(e,t,n,r,s){return a("move")}createTensorFromGPUData(e,t,n){return a("createTensorFromGPUData")}memory(){return a("memory")}floatPrecision(){return a("floatPrecision")}epsilon(){return 32===this.floatPrecision()?1e-7:1e-4}dispose(){return a("dispose")}}function a(e){throw new Error(`'${e}' not yet implemented or not found in the registry. This kernel may not be supported by the tfjs backend you have chosen`)}function s(e,t,n){return Math.max(e,Math.min(t,n))}function o(e){return e%2===0?e:e+1}function i(e,t,n){const r=e[t];e[t]=e[n],e[n]=r}function u(e,t){if(!e)throw new Error("string"===typeof t?t:t())}function l(e,t,n=""){u(p(e,t),()=>n+` Shapes ${e} and ${t} must match`)}function c(e){u(null!=e,()=>"The input to the tensor constructor must be a non-null value.")}function d(e){if(0===e.length)return 1;let t=e[0];for(let n=1;n<e.length;n++)t*=e[n];return t}function p(e,t){if(e===t)return!0;if(null==e||null==t)return!1;if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}function h(e){return e%1===0}function f(e){const t=Math.ceil(Math.sqrt(e));return[t,Math.ceil(e/t)]}function m(e,t){return t<=e.length?e:e+" ".repeat(t-e.length)}function g(e,t=e=>0,n,r){return new Promise((a,s)=>{let o=0;const i=()=>{if(e())return void a();o++;const u=t(o);null!=n&&o>=n?s():null!=r?r(i,u):setTimeout(i,u)};i()})}function y(e,t){let n=1,r=-1;for(let s=0;s<e.length;++s)if(e[s]>=0)n*=e[s];else if(-1===e[s]){if(-1!==r)throw Error(`Shapes can only have 1 implicit size. Found -1 at dim ${r} and dim ${s}`);r=s}else if(e[s]<0)throw Error(`Shapes can not be < 0. Found ${e[s]} at dim ${s}`);if(-1===r){if(t>0&&t!==n)throw Error(`Size(${t}) must match the product of shape ${e}`);return e}if(0===n)throw Error(`Cannot infer the missing size in [${e}] when there are 0 elements`);if(t%n!==0)throw Error(`The implicit shape can't be a fractional number. Got ${t} / ${n}`);const a=e.slice();return a[r]=t/n,a}function b(e,t){const n=t.length;return u((e=null==e?t.map((e,t)=>t):[].concat(e)).every(e=>e>=-n&&e<n),()=>`All values in axis param must be in range [-${n}, ${n}) but got axis ${e}`),u(e.every(e=>h(e)),()=>`All values in axis param must be integers but got axis ${e}`),e.map(e=>e<0?n+e:e)}function x(e,t){const n=[],r=[],a=null!=t&&Array.isArray(t)&&0===t.length,s=null==t||a?null:b(t,e).sort();let o=0;for(let i=0;i<e.length;++i){if(null!=s){if(s[o]===i&&1!==e[i])throw new Error(`Can't squeeze axis ${i} since its dim '${e[i]}' is not 1`);(null==s[o]||s[o]>i)&&1===e[i]&&(n.push(e[i]),r.push(i)),s[o]<=i&&o++}1!==e[i]&&(n.push(e[i]),r.push(i))}return{newShape:n,keptDims:r}}function v(e,t){return w(e,t)}function w(e,t){let n=null;if(null==e||"float32"===e)n=new Float32Array(t);else if("int32"===e)n=new Int32Array(t);else if("bool"===e)n=new Uint8Array(t);else{if("string"!==e)throw new Error(`Unknown data type ${e}`);n=new Array(t)}return n}function k(e,t){return"complex64"!==t&&(("float32"!==t||"complex64"===e)&&(("int32"!==t||"float32"===e||"complex64"===e)&&("bool"!==t||"bool"!==e)))}function I(e){if("float32"===e||"int32"===e)return 4;if("complex64"===e)return 8;if("bool"===e)return 1;throw new Error(`Unknown dtype ${e}`)}function N(e){return"string"===typeof e||e instanceof String}function S(e){return Array.isArray(e)?S(e[0]):e instanceof Float32Array?"float32":e instanceof Int32Array||e instanceof Uint8Array||e instanceof Uint8ClampedArray?"int32":"number"===tLine truncated
@@ -23,6 +23,20 @@ function _mergeNamespaces(n, m) {
}
return Object.freeze(Object.defineProperty(n, Symbol.toStringTag, { value: "Module" }));
}
function maskConfig(option) {
return {
solutionPath: option.solutionPath || "https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation",
modelSelection: option.modelSelection || 1,
smoothSegmentation: option.smoothSegmentation !== void 0 ? option.smoothSegmentation : true,
minDetectionConfidence: option.minDetectionConfidence || 0.5,
minTrackingConfidence: option.minTrackingConfidence || 0.5,
selfieMode: option.selfieMode || false,
drawContour: option.drawContour || false,
foregroundThreshold: option.foregroundThreshold || 0.5,
opacity: option.opacity || 1,
maskBlurAmount: option.maskBlurAmount || 3
};
}
const EPSILON_FLOAT32$1 = 1e-7;
const EPSILON_FLOAT16$1 = 1e-4;
class DataStorage {
@@ -49040,129 +49054,278 @@ const kernelConfigs = [
for (const kernelConfig of kernelConfigs) {
registerKernel(kernelConfig);
}
function artplayerPluginDanmukuMask(option = {}) {
return (art) => {
const {
template: { $video, $danmuku }
} = art;
let segmenter = null;
let canvas = null;
let ctx = null;
let animationFrameId = null;
let isInitialized = false;
const config = {
solutionPath: option.solutionPath || "https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation",
modelSelection: option.modelSelection || 1,
smoothSegmentation: option.smoothSegmentation !== void 0 ? option.smoothSegmentation : true,
minDetectionConfidence: option.minDetectionConfidence || 0.5,
minTrackingConfidence: option.minTrackingConfidence || 0.5,
selfieMode: option.selfieMode || false,
drawContour: option.drawContour || false,
foregroundThreshold: option.foregroundThreshold || 0.5,
opacity: option.opacity || 1,
maskBlurAmount: option.maskBlurAmount || 3
async function loadSegmenter(config, active) {
try {
await setBackend("webgl");
} catch (error) {
if (!active())
return null;
console.warn("WebGL backend not available, falling back to CPU", error.message);
await setBackend("cpu");
}
if (!active())
return null;
try {
return await ke(Se.MediaPipeSelfieSegmentation, {
runtime: "mediapipe",
modelType: "general",
solutionPath: config.solutionPath,
modelSelection: config.modelSelection,
smoothSegmentation: config.smoothSegmentation,
minDetectionConfidence: config.minDetectionConfidence,
minTrackingConfidence: config.minTrackingConfidence,
selfieMode: config.selfieMode
});
} catch (error) {
if (active())
console.error("Error initializing segmenter:", error);
return null;
}
}
async function releaseSegmenter(segmenter) {
try {
await segmenter.dispose();
} catch (error) {
console.warn("Failed to dispose danmuku mask segmenter:", error);
}
}
const toBinaryMask = (...args) => Ue(...args);
const drawMask = (...args) => Ne(...args);
function makeWhiteTransparent(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
if (data[i] > 250 && data[i + 1] > 250 && data[i + 2] > 250)
data[i + 3] = 0;
}
return imageData;
}
function createOutput(layer) {
const canvas = document.createElement("canvas");
try {
const ctx = canvas.getContext("2d");
if (!ctx)
throw new Error("Danmuku mask requires a 2D canvas context");
Object.assign(layer.style, {
maskMode: "alpha",
maskSize: "contain",
maskRepeat: "no-repeat",
backgroundSize: "contain",
backgroundRepeat: "no-repeat"
});
return { canvas, ctx };
} catch (error) {
canvas.width = 0;
canvas.height = 0;
throw error;
}
}
async function renderMask(output, video, layer, segmenter, config, active) {
const { canvas, ctx } = output;
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const segmentation = await segmenter.segmentPeople(video);
if (!active() || !segmentation || !segmentation.length)
return;
const mask = await toBinaryMask(
segmentation,
{ r: 255, g: 255, b: 255, a: 255 },
{ r: 0, g: 0, b: 0, a: 255 },
config.drawContour,
config.foregroundThreshold
);
if (!active())
return;
await drawMask(canvas, video, mask, config.opacity, config.maskBlurAmount);
if (!active())
return;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
ctx.putImageData(makeWhiteTransparent(imageData), 0, 0);
const url = canvas.toDataURL();
if (active())
layer.style.maskImage = `url(${url})`;
}
function releaseOutput(output) {
if (output) {
output.canvas.width = 0;
output.canvas.height = 0;
}
}
class MaskController {
constructor(art, config, { $video, $danmuku }) {
this.art = art;
this.config = config;
this.video = $video;
this.layer = $danmuku;
this.closed = art.isDestroy;
this.run = null;
this.tail = Promise.resolve();
this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
this.destroy = this.destroy.bind(this);
this.ready = () => {
this.start().catch((error) => console.error("Failed to start danmuku mask:", error));
};
async function initTensorFlow() {
if (!this.closed) {
try {
await setBackend("webgl");
art.on("destroy", this.destroy);
if (!this.closed)
art.on("ready", this.ready);
} catch (error) {
console.warn("WebGL backend not available, falling back to CPU", error.message);
await setBackend("cpu");
}
}
async function initSegmenter() {
await initTensorFlow();
const model = Se.MediaPipeSelfieSegmentation;
const segmenterConfig = {
runtime: "mediapipe",
modelType: "general",
solutionPath: config.solutionPath,
modelSelection: config.modelSelection,
smoothSegmentation: config.smoothSegmentation,
minDetectionConfidence: config.minDetectionConfidence,
minTrackingConfidence: config.minTrackingConfidence,
selfieMode: config.selfieMode
};
try {
segmenter = await ke(model, segmenterConfig);
isInitialized = true;
} catch (error) {
console.error("Error initializing segmenter:", error);
isInitialized = false;
}
}
function setupDanmukuStyle() {
Object.assign($danmuku.style, {
maskMode: "alpha",
maskSize: "contain",
maskRepeat: "no-repeat",
backgroundSize: "contain",
backgroundRepeat: "no-repeat"
});
}
function createCanvas2() {
canvas = document.createElement("canvas");
ctx = canvas.getContext("2d");
}
function makeWhiteTransparent(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
if (data[i] > 250 && data[i + 1] > 250 && data[i + 2] > 250) {
data[i + 3] = 0;
this.closed = true;
this.halt();
try {
art.off("ready", this.ready);
} catch {
}
try {
art.off("destroy", this.destroy);
} catch {
}
throw error;
}
return imageData;
}
async function segmentBody() {
if (!isInitialized || $video.paused || $video.ended) {
animationFrameId = requestAnimationFrame(segmentBody);
return;
}
}
active(run) {
return run === this.run && run.running && !this.closed && !this.art.isDestroy;
}
release(run) {
if (run.releasing)
return run.releasing;
const segmenter = run.segmenter;
run.segmenter = null;
const output = run.output;
run.output = null;
run.releasing = (async () => {
await Promise.resolve();
try {
canvas.width = $video.videoWidth;
canvas.height = $video.videoHeight;
const segmentation = await segmenter.segmentPeople($video);
if (!segmentation || segmentation.length === 0) {
animationFrameId = requestAnimationFrame(segmentBody);
releaseOutput(output);
} finally {
if (segmenter)
await releaseSegmenter(segmenter);
}
})();
return run.releasing;
}
schedule(run) {
if (!this.active(run) || run.frame !== null || run.busy)
return;
run.frame = requestAnimationFrame(() => {
run.frame = null;
this.tick(run);
});
}
tick(run) {
if (!this.active(run) || run.busy)
return;
const video = this.video;
if (video.paused || video.ended || !(video.videoWidth > 0 && video.videoHeight > 0)) {
this.schedule(run);
return;
}
run.busy = true;
const work = (async () => {
try {
await Promise.resolve();
if (this.active(run))
await renderMask(run.output, video, this.layer, run.segmenter, this.config, () => this.active(run));
} catch (error) {
if (this.active(run))
console.error("Error in segmentBody:", error);
} finally {
run.busy = false;
if (!this.active(run))
await this.release(run);
else this.schedule(run);
}
})();
this.tail = work.catch((error) => console.warn("Failed to release danmuku mask resources:", error));
}
async start() {
if (this.closed || this.art.isDestroy)
return;
if (this.run?.running)
return this.run.started;
let cancel;
const cancelled = new Promise((resolve) => cancel = resolve);
const run = { running: true, initializing: true, busy: false, frame: null, segmenter: null, output: null, cancel, started: null };
const previous = this.tail;
this.run = run;
const initialize = (async () => {
try {
await previous;
if (!this.active(run))
return;
if (!this.video || !this.layer?.style)
throw new Error("Danmuku mask requires core video and danmuku template nodes");
run.segmenter = await loadSegmenter(this.config, () => this.active(run));
if (!this.active(run))
return;
if (!run.segmenter) {
run.running = false;
return;
}
const foregroundColor = { r: 255, g: 255, b: 255, a: 255 };
const backgroundColor = { r: 0, g: 0, b: 0, a: 255 };
const mask = await Ue(
segmentation,
foregroundColor,
backgroundColor,
config.drawContour,
config.foregroundThreshold
);
await Ne(canvas, $video, mask, config.opacity, config.maskBlurAmount);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
ctx.putImageData(makeWhiteTransparent(imageData), 0, 0);
$danmuku.style.maskImage = `url(${canvas.toDataURL()})`;
run.output = createOutput(this.layer);
} catch (error) {
console.error("Error in segmentBody:", error);
if (this.active(run)) {
run.running = false;
throw error;
}
} finally {
run.initializing = false;
if (!this.active(run))
await this.release(run);
}
animationFrameId = requestAnimationFrame(segmentBody);
if (this.active(run))
this.tick(run);
})();
this.tail = initialize.catch(() => {
});
run.started = Promise.race([initialize, cancelled]);
return run.started;
}
halt() {
const run = this.run;
if (run) {
run.running = false;
run.cancel();
if (run.frame !== null) {
cancelAnimationFrame(run.frame);
run.frame = null;
}
if (!run.initializing && !run.busy)
this.tail = this.release(run).catch((error) => console.warn("Failed to release danmuku mask resources:", error));
}
}
stop() {
this.halt();
if (this.layer?.style)
this.layer.style.maskImage = "none";
}
destroy() {
if (this.closed)
return;
this.closed = true;
try {
this.stop();
} finally {
try {
this.art.off("ready", this.ready);
} finally {
this.art.off("destroy", this.destroy);
}
}
}
}
function artplayerPluginDanmukuMask(option = {}) {
return (art) => {
const { template: { $video, $danmuku } } = art;
const controller = new MaskController(art, maskConfig(option), { $video, $danmuku });
async function startSegmentation() {
if (!isInitialized) {
await initSegmenter();
}
if (!canvas) {
createCanvas2();
}
setupDanmukuStyle();
segmentBody();
await controller.start();
}
function stopSegmentation() {
$danmuku.style.maskImage = "none";
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
controller.stop();
}
art.on("ready", startSegmentation);
art.on("destroy", stopSegmentation);
return {
name: "artplayerPluginDanmukuMask",
start: startSegmentation,
@@ -0,0 +1,14 @@
export default function maskConfig(option) {
return {
solutionPath: option.solutionPath || 'https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation',
modelSelection: option.modelSelection || 1,
smoothSegmentation: option.smoothSegmentation !== undefined ? option.smoothSegmentation : true,
minDetectionConfidence: option.minDetectionConfidence || 0.5,
minTrackingConfidence: option.minTrackingConfidence || 0.5,
selfieMode: option.selfieMode || false,
drawContour: option.drawContour || false,
foregroundThreshold: option.foregroundThreshold || 0.5,
opacity: option.opacity || 1,
maskBlurAmount: option.maskBlurAmount || 3,
}
}
@@ -0,0 +1,188 @@
import { createOutput, releaseOutput, renderMask } from './output'
import { loadSegmenter, releaseSegmenter } from './sdk'
export default class MaskController {
constructor(art, config, { $video, $danmuku }) {
this.art = art
this.config = config
this.video = $video
this.layer = $danmuku
this.closed = art.isDestroy
this.run = null
this.tail = Promise.resolve()
this.start = this.start.bind(this)
this.stop = this.stop.bind(this)
this.destroy = this.destroy.bind(this)
this.ready = () => {
this.start().catch(error => console.error('Failed to start danmuku mask:', error))
}
if (!this.closed) {
try {
art.on('destroy', this.destroy)
if (!this.closed)
art.on('ready', this.ready)
}
catch (error) {
this.closed = true
this.halt()
try {
art.off('ready', this.ready)
}
catch {}
try {
art.off('destroy', this.destroy)
}
catch {}
throw error
}
}
}
active(run) {
return run === this.run && run.running && !this.closed && !this.art.isDestroy
}
release(run) {
if (run.releasing)
return run.releasing
const segmenter = run.segmenter
run.segmenter = null
const output = run.output
run.output = null
run.releasing = (async () => {
await Promise.resolve()
try {
// Frame work has settled; an SDK close must not retain this independent bitmap.
releaseOutput(output)
}
finally {
if (segmenter)
await releaseSegmenter(segmenter)
}
})()
return run.releasing
}
schedule(run) {
if (!this.active(run) || run.frame !== null || run.busy)
return
run.frame = requestAnimationFrame(() => {
run.frame = null
this.tick(run)
})
}
tick(run) {
if (!this.active(run) || run.busy)
return
const video = this.video
if (video.paused || video.ended || !(video.videoWidth > 0 && video.videoHeight > 0)) {
this.schedule(run)
return
}
run.busy = true
const work = (async () => {
try {
// Reserve the work slot before SDK code can synchronously reenter start/stop.
await Promise.resolve()
if (this.active(run))
await renderMask(run.output, video, this.layer, run.segmenter, this.config, () => this.active(run))
}
catch (error) {
if (this.active(run))
console.error('Error in segmentBody:', error)
}
finally {
run.busy = false
if (!this.active(run))
await this.release(run)
else this.schedule(run)
}
})()
// A restart waits for uncancellable SDK work and disposal; it never overlaps it.
this.tail = work.catch(error => console.warn('Failed to release danmuku mask resources:', error))
}
async start() {
if (this.closed || this.art.isDestroy)
return
if (this.run?.running)
return this.run.started
let cancel
const cancelled = new Promise(resolve => cancel = resolve)
const run = { running: true, initializing: true, busy: false, frame: null, segmenter: null, output: null, cancel, started: null }
const previous = this.tail
this.run = run
const initialize = (async () => {
try {
await previous
if (!this.active(run))
return
if (!this.video || !this.layer?.style)
throw new Error('Danmuku mask requires core video and danmuku template nodes')
run.segmenter = await loadSegmenter(this.config, () => this.active(run))
if (!this.active(run))
return
if (!run.segmenter) {
run.running = false
return
}
run.output = createOutput(this.layer)
}
catch (error) {
if (this.active(run)) {
run.running = false
throw error
}
}
finally {
run.initializing = false
if (!this.active(run))
await this.release(run)
}
if (this.active(run))
this.tick(run)
})()
this.tail = initialize.catch(() => {})
// stop settles public start promptly; eventual SDK results remain observed by initialize.
run.started = Promise.race([initialize, cancelled])
return run.started
}
halt() {
const run = this.run
if (run) {
run.running = false
run.cancel()
if (run.frame !== null) {
cancelAnimationFrame(run.frame)
run.frame = null
}
if (!run.initializing && !run.busy)
this.tail = this.release(run).catch(error => console.warn('Failed to release danmuku mask resources:', error))
}
}
stop() {
this.halt()
if (this.layer?.style)
this.layer.style.maskImage = 'none'
}
destroy() {
if (this.closed)
return
this.closed = true
try {
this.stop()
}
finally {
try {
this.art.off('ready', this.ready)
}
finally {
this.art.off('destroy', this.destroy)
}
}
}
}
@@ -1,155 +1,16 @@
import * as bodySegmentation from '@tensorflow-models/body-segmentation'
import * as tf from '@tensorflow/tfjs-core'
import '@tensorflow/tfjs-backend-webgl'
import '@tensorflow/tfjs-backend-cpu'
import maskConfig from './config'
import MaskController from './controller'
export default function artplayerPluginDanmukuMask(option = {}) {
return (art) => {
const {
template: { $video, $danmuku },
} = art
let segmenter = null
let canvas = null
let ctx = null
let animationFrameId = null
let isInitialized = false
const config = {
solutionPath: option.solutionPath || 'https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation',
modelSelection: option.modelSelection || 1,
smoothSegmentation: option.smoothSegmentation !== undefined ? option.smoothSegmentation : true,
minDetectionConfidence: option.minDetectionConfidence || 0.5,
minTrackingConfidence: option.minTrackingConfidence || 0.5,
selfieMode: option.selfieMode || false,
drawContour: option.drawContour || false,
foregroundThreshold: option.foregroundThreshold || 0.5,
opacity: option.opacity || 1,
maskBlurAmount: option.maskBlurAmount || 3,
}
async function initTensorFlow() {
try {
await tf.setBackend('webgl')
}
catch (error) {
console.warn('WebGL backend not available, falling back to CPU', error.message)
await tf.setBackend('cpu')
}
}
async function initSegmenter() {
await initTensorFlow()
const model = bodySegmentation.SupportedModels.MediaPipeSelfieSegmentation
const segmenterConfig = {
runtime: 'mediapipe',
modelType: 'general',
solutionPath: config.solutionPath,
modelSelection: config.modelSelection,
smoothSegmentation: config.smoothSegmentation,
minDetectionConfidence: config.minDetectionConfidence,
minTrackingConfidence: config.minTrackingConfidence,
selfieMode: config.selfieMode,
}
try {
segmenter = await bodySegmentation.createSegmenter(model, segmenterConfig)
isInitialized = true
}
catch (error) {
console.error('Error initializing segmenter:', error)
isInitialized = false
}
}
function setupDanmukuStyle() {
Object.assign($danmuku.style, {
maskMode: 'alpha',
maskSize: 'contain',
maskRepeat: 'no-repeat',
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
})
}
function createCanvas() {
canvas = document.createElement('canvas')
ctx = canvas.getContext('2d')
}
function makeWhiteTransparent(imageData) {
const data = imageData.data
for (let i = 0; i < data.length; i += 4) {
if (data[i] > 250 && data[i + 1] > 250 && data[i + 2] > 250) {
data[i + 3] = 0
}
}
return imageData
}
async function segmentBody() {
if (!isInitialized || $video.paused || $video.ended) {
animationFrameId = requestAnimationFrame(segmentBody)
return
}
try {
canvas.width = $video.videoWidth
canvas.height = $video.videoHeight
const segmentation = await segmenter.segmentPeople($video)
if (!segmentation || segmentation.length === 0) {
animationFrameId = requestAnimationFrame(segmentBody)
return
}
const foregroundColor = { r: 255, g: 255, b: 255, a: 255 }
const backgroundColor = { r: 0, g: 0, b: 0, a: 255 }
const mask = await bodySegmentation.toBinaryMask(
segmentation,
foregroundColor,
backgroundColor,
config.drawContour,
config.foregroundThreshold,
)
await bodySegmentation.drawMask(canvas, $video, mask, config.opacity, config.maskBlurAmount)
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
ctx.putImageData(makeWhiteTransparent(imageData), 0, 0)
$danmuku.style.maskImage = `url(${canvas.toDataURL()})`
}
catch (error) {
console.error('Error in segmentBody:', error)
}
animationFrameId = requestAnimationFrame(segmentBody)
}
const { template: { $video, $danmuku } } = art
const controller = new MaskController(art, maskConfig(option), { $video, $danmuku })
async function startSegmentation() {
if (!isInitialized) {
await initSegmenter()
}
if (!canvas) {
createCanvas()
}
setupDanmukuStyle()
segmentBody()
await controller.start()
}
function stopSegmentation() {
$danmuku.style.maskImage = 'none'
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
animationFrameId = null
}
controller.stop()
}
art.on('ready', startSegmentation)
art.on('destroy', stopSegmentation)
return {
name: 'artplayerPluginDanmukuMask',
start: startSegmentation,
@@ -0,0 +1,65 @@
import { drawMask, toBinaryMask } from './sdk'
export function makeWhiteTransparent(imageData) {
const data = imageData.data
for (let i = 0; i < data.length; i += 4) {
if (data[i] > 250 && data[i + 1] > 250 && data[i + 2] > 250)
data[i + 3] = 0
}
return imageData
}
export function createOutput(layer) {
const canvas = document.createElement('canvas')
try {
const ctx = canvas.getContext('2d')
if (!ctx)
throw new Error('Danmuku mask requires a 2D canvas context')
Object.assign(layer.style, {
maskMode: 'alpha',
maskSize: 'contain',
maskRepeat: 'no-repeat',
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
})
return { canvas, ctx }
}
catch (error) {
canvas.width = 0
canvas.height = 0
throw error
}
}
export async function renderMask(output, video, layer, segmenter, config, active) {
const { canvas, ctx } = output
canvas.width = video.videoWidth
canvas.height = video.videoHeight
const segmentation = await segmenter.segmentPeople(video)
if (!active() || !segmentation || !segmentation.length)
return
const mask = await toBinaryMask(
segmentation,
{ r: 255, g: 255, b: 255, a: 255 },
{ r: 0, g: 0, b: 0, a: 255 },
config.drawContour,
config.foregroundThreshold,
)
if (!active())
return
await drawMask(canvas, video, mask, config.opacity, config.maskBlurAmount)
if (!active())
return
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
ctx.putImageData(makeWhiteTransparent(imageData), 0, 0)
const url = canvas.toDataURL()
if (active())
layer.style.maskImage = `url(${url})`
}
export function releaseOutput(output) {
if (output) {
output.canvas.width = 0
output.canvas.height = 0
}
}
@@ -0,0 +1,47 @@
import * as bodySegmentation from '@tensorflow-models/body-segmentation'
import * as tf from '@tensorflow/tfjs-core'
import '@tensorflow/tfjs-backend-webgl'
import '@tensorflow/tfjs-backend-cpu'
export async function loadSegmenter(config, active) {
try {
await tf.setBackend('webgl')
}
catch (error) {
if (!active())
return null
console.warn('WebGL backend not available, falling back to CPU', error.message)
await tf.setBackend('cpu')
}
if (!active())
return null
try {
return await bodySegmentation.createSegmenter(bodySegmentation.SupportedModels.MediaPipeSelfieSegmentation, {
runtime: 'mediapipe',
modelType: 'general',
solutionPath: config.solutionPath,
modelSelection: config.modelSelection,
smoothSegmentation: config.smoothSegmentation,
minDetectionConfidence: config.minDetectionConfidence,
minTrackingConfidence: config.minTrackingConfidence,
selfieMode: config.selfieMode,
})
}
catch (error) {
if (active())
console.error('Error initializing segmenter:', error)
return null
}
}
export async function releaseSegmenter(segmenter) {
try {
await segmenter.dispose()
}
catch (error) {
console.warn('Failed to dispose danmuku mask segmenter:', error)
}
}
export const toBinaryMask = (...args) => bodySegmentation.toBinaryMask(...args)
export const drawMask = (...args) => bodySegmentation.drawMask(...args)
@@ -0,0 +1,187 @@
{
"task": "PKG-MASK-03",
"node": "v24.21.0",
"platform": "win32",
"scope": "Source controller and output ownership with controlled SDK/RAF/canvas; normal three-format builds. No native inference or GPU cleanup acceptance.",
"source": [
{
"file": "packages/artplayer-plugin-danmuku-mask/src/config.js",
"sha256": "b055e6a3f5814207cfac452904853813a67b98c63c0c70695c3da133353d8129"
},
{
"file": "packages/artplayer-plugin-danmuku-mask/src/controller.js",
"sha256": "b6b546484e0f3ba9ff38da57d7a14f8d522adfff3e85da9d788516e7551b1bed"
},
{
"file": "packages/artplayer-plugin-danmuku-mask/src/index.js",
"sha256": "3e518e52fc1ede883d3acca6d5ee66fd6da0d343dfa5803386d23f0bbd72c90f"
},
{
"file": "packages/artplayer-plugin-danmuku-mask/src/output.js",
"sha256": "142a29ac269065122323ad10d7103d01eba8e2776cd2bcf40f988332330709e5"
},
{
"file": "packages/artplayer-plugin-danmuku-mask/src/sdk.js",
"sha256": "caa21c19aac14fd8634e2867e6d712f217e7bb47cbb36667947e436dc30b2984"
}
],
"tests": {
"command": "yarn test:danmuku-mask",
"log": "refactor/.cache/mask03-integrated-final.log",
"logSha256": "6800ef6f0988f81ee3ea80fffd1a1401b210b8e2d038c206b7a62995351bbf3d",
"passed": 96,
"candidate": 36,
"frozenDefects": 54,
"frozenContract": 6,
"failed": 0,
"skipped": 0,
"cases": [
"✔ Danmuku Mask freezes all published stable archive members and their actual Git associations (0.6332ms)",
"✔ Both published declarations retain optional options, synchronous registration, async start and void stop (45.5503ms)",
"✔ Actual main and legacy exports preserve historical CommonJS and browser global forms without starting a model (270.2566ms)",
"✔ Frozen owned source keeps its defaults, ready/destroy hooks and alpha mask pixel boundary without a real SDK (20.0664ms)",
"✔ Frozen zero numeric options use historical OR defaults while false smoothSegmentation is retained (2.3961ms)",
"✔ SDK-08 distinguishes the frozen Yarn adapter and local resource bytes from unversioned CDN resolution (0.8288ms)",
"✔ Historical SDK probes replace exactly five call sites and retain verified input identities (1.6883ms)",
"✔ frozen-source: historical stop during initialization is undone by late initialization (5.4237ms)",
"✔ frozen-source: historical inference completion after destroy writes a mask and revives RAF (1.6315ms)",
"✔ frozen-source: historical repeated start leaves a second RAF chain after stop (1.6516ms)",
"✔ published-1.1.0-main: historical stop during initialization is undone by late initialization (29.7697ms)",
"✔ published-1.1.0-main: historical inference completion after destroy writes a mask and revives RAF (4.2542ms)",
"✔ published-1.1.0-main: historical repeated start leaves a second RAF chain after stop (5.5539ms)",
"✔ published-1.1.0-legacy: historical stop during initialization is undone by late initialization (42.0313ms)",
"✔ published-1.1.0-legacy: historical inference completion after destroy writes a mask and revives RAF (10.7785ms)",
"✔ published-1.1.0-legacy: historical repeated start leaves a second RAF chain after stop (3.312ms)",
"✔ published-1.1.0-module: historical stop during initialization is undone by late initialization (29.5567ms)",
"✔ published-1.1.0-module: historical inference completion after destroy writes a mask and revives RAF (7.2118ms)",
"✔ published-1.1.0-module: historical repeated start leaves a second RAF chain after stop (8.5416ms)",
"✔ published-1.0.0-main: historical stop during initialization is undone by late initialization (62.8032ms)",
"✔ published-1.0.0-main: historical inference completion after destroy writes a mask and revives RAF (16.2368ms)",
"✔ published-1.0.0-main: historical repeated start leaves a second RAF chain after stop (11.9776ms)",
"✔ published-1.0.0-legacy: historical stop during initialization is undone by late initialization (60.0044ms)",
"✔ published-1.0.0-legacy: historical inference completion after destroy writes a mask and revives RAF (13.9692ms)",
"✔ published-1.0.0-legacy: historical repeated start leaves a second RAF chain after stop (12.9393ms)",
"✔ published-1.0.0-module: historical stop during initialization is undone by late initialization (62.9506ms)",
"✔ published-1.0.0-module: historical inference completion after destroy writes a mask and revives RAF (13.5528ms)",
"✔ published-1.0.0-module: historical repeated start leaves a second RAF chain after stop (15.421ms)",
"✔ frozen-source: historical stop during initialization does not dispose the late model (1.3067ms)",
"✔ frozen-source: historical destroy during initialization does not dispose the late model (1.0049ms)",
"✔ frozen-source: historical stop during inference cannot prevent its pending write (0.9691ms)",
"✔ frozen-source: historical concurrent starts initialize two models and lose disposal ownership (1.28ms)",
"✔ frozen-source: historical inference rejection logs once per frame and keeps retrying (1.7847ms)",
"✔ frozen-source: historical fulfilled false WebGL selection does not trigger CPU fallback (1.3902ms)",
"✔ frozen-source: historical rejected WebGL selection tries CPU and ignores its fulfilled boolean (2.1617ms)",
"✔ frozen-source: historical CPU rejection rejects explicit start before creating a model (1.8598ms)",
"✔ frozen-source: historical model initialization rejection resolves start and polls without a model (1.3122ms)",
"✔ frozen-source: historical missing danmuku rejects start after allocating the model and makes stop throw (0.9713ms)",
"✔ frozen-source: historical unavailable 2D context logs read failures and keeps the frame loop (1.1514ms)",
"✔ frozen-source: historical canvas allocation failure rejects start but leaves the initialized model owned (0.8525ms)",
"✔ frozen-source: historical canvas read security failure is logged and retried without clearing the previous mask (1.2027ms)",
"✔ frozen-source: historical zero RAF id survives stop and retained ready listeners restart after destroy (1.0528ms)",
"✔ frozen-source: historical ready-backend unhandled rejection is observed only in a fatal isolated child (2903.2536ms)",
"✔ frozen-source: historical missing-video unhandled rejection is observed only in a fatal isolated child (2990.5019ms)",
"✔ published-1.1.0-main: historical stop during initialization does not dispose the late model (3.81ms)",
"✔ published-1.1.0-main: historical destroy during initialization does not dispose the late model (3.343ms)",
"✔ published-1.1.0-main: historical stop during inference cannot prevent its pending write (14.4356ms)",
"✔ published-1.1.0-main: historical concurrent starts initialize two models and lose disposal ownership (3.56ms)",
"✔ published-1.1.0-main: historical inference rejection logs once per frame and keeps retrying (5.4085ms)",
"✔ published-1.1.0-main: historical fulfilled false WebGL selection does not trigger CPU fallback (19.8639ms)",
"✔ published-1.1.0-main: historical rejected WebGL selection tries CPU and ignores its fulfilled boolean (7.0089ms)",
"✔ published-1.1.0-main: historical CPU rejection rejects explicit start before creating a model (2.6735ms)",
"✔ published-1.1.0-main: historical model initialization rejection resolves start and polls without a model (3.21ms)",
"✔ published-1.1.0-main: historical missing danmuku rejects start after allocating the model and makes stop throw (3.5393ms)",
"✔ published-1.1.0-main: historical unavailable 2D context logs read failures and keeps the frame loop (3.0877ms)",
"✔ published-1.1.0-main: historical canvas allocation failure rejects start but leaves the initialized model owned (2.7232ms)",
"✔ published-1.1.0-main: historical canvas read security failure is logged and retried without clearing the previous mask (3.4678ms)",
"✔ published-1.1.0-main: historical zero RAF id survives stop and retained ready listeners restart after destroy (3.1487ms)",
"✔ published-1.1.0-main: historical ready-backend unhandled rejection is observed only in a fatal isolated child (3039.1198ms)",
"✔ published-1.1.0-main: historical missing-video unhandled rejection is observed only in a fatal isolated child (2950.6901ms)",
"✔ Mask candidate instances sharing SDK and RAF retain independent models, layers and cancellation (6.5051ms)",
"✔ Mask candidate retains synchronous registrar, optional options, exact public methods and Promise<void> start (3.5659ms)",
"✔ Mask candidate preserves OR defaults, false smooth setting, snapshot and exact SDK interpretation (1.6287ms)",
"✔ Mask candidate stop during webgl prevents late output and RAF revival (1.1136ms)",
"✔ Mask candidate stop during init prevents late output and RAF revival (0.9582ms)",
"✔ Mask candidate stop during segment prevents late output and RAF revival (1.0316ms)",
"✔ Mask candidate stop during mask prevents late output and RAF revival (1.185ms)",
"✔ Mask candidate stop during draw prevents late output and RAF revival (1.06ms)",
"✔ Mask candidate destroy during webgl prevents late output and RAF revival (0.8933ms)",
"✔ Mask candidate destroy during init prevents late output and RAF revival (1.1325ms)",
"✔ Mask candidate destroy during segment prevents late output and RAF revival (1.3227ms)",
"✔ Mask candidate destroy during mask prevents late output and RAF revival (1.9155ms)",
"✔ Mask candidate destroy during draw prevents late output and RAF revival (1.4494ms)",
"✔ Mask candidate concurrent and repeated starts keep one initialization and one inference chain (1.482ms)",
"✔ Mask candidate restart waits for old inference and model disposal without blocking stop settlement (1.1059ms)",
"✔ Mask candidate repeated stops cannot bypass a pending disposal when restarting (1.1706ms)",
"✔ Mask candidate synchronous SDK reentry does not overlap old and new runs (1.1659ms)",
"✔ Mask candidate cancels RAF id zero, detaches exact listeners and ignores start after destroy (1.4668ms)",
"✔ Mask candidate no readable dimensions waits without invoking inference (1.3263ms)",
"✔ Mask candidate backend false keeps old interpretation while rejection tries CPU (3.3682ms)",
"✔ Mask candidate rejects explicit backend failure and observes automatic ready failure (2.0741ms)",
"✔ Mask candidate model rejection retains resolved start and logged error without idle retry loop (1.527ms)",
"✔ Mask candidate noLayer initialization fails without retaining resources (2.1708ms)",
"✔ Mask candidate noVideo initialization fails without retaining resources (1.1812ms)",
"✔ Mask candidate noContext initialization fails without retaining resources (1.3588ms)",
"✔ Mask candidate canvasError initialization fails without retaining resources (1.4164ms)",
"✔ Mask candidate inference and canvas-read errors keep the old active retry outlet (2.6162ms)",
"✔ Mask candidate late SDK rejection after cancellation remains observed and silent (1.3766ms)",
"✔ Mask candidate already-destroyed registration does not install listeners or allocate resources (0.9332ms)",
"✔ Mask candidate subscription rollback preserves original failure and prior host styles (1.9141ms)",
"✔ Mask candidate immediate stop skips backend/model startup and public start settles (1.4292ms)",
"✔ Mask candidate cancelled backend/model rejection is observed without retry or late log (1.6941ms)",
"✔ Mask candidate disposal rejection is observed, clears canvas and allows independent restart (1.6381ms)",
"✔ Mask candidate empty segmentation retains existing mask and a single active loop (1.1239ms)",
"✔ Mask candidate cancelled inference cannot overwrite a subsequent external mask write (1.1616ms)",
"✔ Mask candidate retains template capture before evaluating option getters (1.0374ms)"
]
},
"checks": [
{
"file": "refactor/.cache/mask03-build.log",
"sha256": "4c198f39b981759a137c5c6a69f8166f497bc3946519e64c1e17e585d0db77d5"
},
{
"file": "refactor/.cache/mask03-integrated-lint.log",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
],
"regression": {
"log": "refactor/.cache/mask03-canvas-disposal-red.log",
"sha256": "1f0fd4d3f2e3a6e4a98789138ca3cfdc98c38336fe08b9f8ea39e92597fc90e9",
"cause": "Private bitmap retained while model disposal never resolved; release independent canvas first."
},
"review": {
"independent": "Read-only controller/SDK/reentry review found no concrete blocker; added before-gate model disposal assertion and shared-SDK multiple-instance case.",
"artifacts": "Candidate helper only builds source and substitutes SDK. Built main/legacy/ESM are not covered by its controlled lifecycle run."
},
"artifacts": [
{
"file": "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.js",
"copy": "docs/compiled/artplayer-plugin-danmuku-mask.js",
"sha256": "406a8c28207d386cffe1e61dc79afd8a6a9d5cbd1dc8e1661b4febbf48182f55",
"bytes": 907181
},
{
"file": "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.legacy.js",
"copy": "docs/compiled/artplayer-plugin-danmuku-mask.legacy.js",
"sha256": "62d61dcedd3e90fa480589c2f1e3168c97635232c98ffe8c25c7918a2f3c66db",
"bytes": 910266
},
{
"file": "packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.mjs",
"copy": "docs/compiled/artplayer-plugin-danmuku-mask.mjs",
"sha256": "51936576383129170125b33d2983858e0841ea507368970f6d154bc9d78583b1",
"bytes": 1732398
}
],
"publicTypes": {
"file": "packages/artplayer-plugin-danmuku-mask/types/artplayer-plugin-danmuku-mask.d.ts",
"sha256": "f2b67f82a6471b79d921a7376b24620d0ed8284a3e1882b0f75a9b38dfac3638",
"changed": false
},
"limitations": [
"SDK model operations are unabortable; restart waits for old work settlement.",
"body-segmentation 1.0.2 dispose calls MediaPipe close without returning its result; no native close completion is proven.",
"Actual models, native video/CSS, SDK fallback, GPU resources and cross-instance backend state remain PKG-MASK-05.",
"Full TypeScript/public declaration migration remains PKG-MASK-04; distribution/notice work remains PKG-MASK-06."
]
}
@@ -0,0 +1,45 @@
# PKG-MASK-03 生命周期与模块拆分
## 结构与契约
入口保留同步注册及 name/start/stop,start 为 Promise<void>,stop 同步返回 undefined。
拆为 config(原样快照默认值)、sdk(后端/模型接口)、output(私有画布与遮罩提交)、
controller(运行归属与调度)和 index(公开门面)。JS 与 TS 渐进共存;完整类型迁移
属于04,未声称本步完成TS。包内 ARCHITECTURE.md 记录状态、资源、维护入口及限制。
保留模板先捕获、OR/undefined 默认值、general/mediapipe、WebGL fulfilled false
不触发CPU fallback、二值颜色及严格RGB>250透明规则、data URL和原有样式。没有改
公开声明、依赖、版本、事件或默认模型CDN。新增 test:danmuku-mask 脚本并将候选
用例加入 test:unit,复用现有 Node/Yarn/测试依赖,不需更新锁文件。
## 缺陷修复及可观察差异
历史54项缺陷探针保留原始来源。候选对 backend/model/segment/mask/draw 每个等待点
验证 stop/destroy 后不晚写、不复活RAF、不提前dispose;并发start共享初始化,
每个注册只拥有一条推理链,stop可及时完成等待中的公共start。销毁撤销精确监听器。
stop现在释放模型和独立画布,重启重新初始化,存在真实的重启开销;重启先等待旧的
不可取消SDK工作及可观察dispose结果,不能把新旧推理并发作为加速。
初始化失败不再维持空转RAF;模型创建失败仍记录原错误并resolve,后端/画布失败
仍拒绝显式start,ready自动调用会观察拒绝。缺节点/2D上下文和0尺寸的失败边界更清楚。
审查中复现了dispose挂起时私有画布仍为300×150,已先释放独立bitmap再等待模型;
保留红测日志及最终断言。多个播放器共享受控SDK/RAF时,停止一个不改变另一个的
模型、图层或帧;这不代表真实TF全局状态已验收。
## 实际验证
Node24.21.0下 yarn test:danmuku-mask:96通过、0失败/跳过,其中36候选、54历史缺陷、
6历史契约;不能把历史探针计为候选修复验证。定向lint通过。正常构建main、legacy、
ESM成功且docs副本逐字节一致。源码helper替换SDK,只验证受控生命周期;没有声称
这些用例验证了内嵌SDK的发布产物运行。独立只读审查未发现具体阻断,指出的阶段
dispose断言已补充。机器证据见 [生命周期验证](../baselines/danmuku-mask-lifecycle-validation.json)。
## 剩余门槛与回退
SDK1.0.2公开dispose不返回底层MediaPipe close完成,因此只能证明调用和自有资源
释放,不能证明WASM/GPU结束;没有使用私有SDK字段规避边界。永不完成的SDK工作仍可
阻塞下一次start,stop仍可完成其公共Promise。MASK-LIFETIME保留真实资源证据待办并
由05接续;模型解释、实际fallback、CORS/切源/真实画面/组合及发布许可仍未关闭。
03按内部结构与受控归属完成,04/05/06依次推进;没有降低最终发布要求。
回退本任务源码、脚本和测试后正常重建三种产物;不修改历史夹具,不推送或发布。
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 231 项,范围 22 个包及工作区/示例。
状态:todo 68 / doing 14 / blocked 0 / done 149 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 67 / doing 14 / blocked 0 / done 150 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -242,7 +242,7 @@
| --- | --- | --- | --- | --- | --- | --- |
| 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 | done |
| PKG-MASK-03 | artplayer-plugin-danmuku-mask<br>整理内部职责与资源 | PKG-MASK-02, CORE-18, PKG-DANMUKU-07 | 模型加载/推理/画布输出分离,阻止重叠推理与过期写入 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
| PKG-MASK-03 | artplayer-plugin-danmuku-mask<br>整理内部职责与资源 | PKG-MASK-02, CORE-18, PKG-DANMUKU-07 | 模型加载/推理/画布输出分离,阻止重叠推理与过期写入 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| 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 |
| PKG-MASK-06 | artplayer-plugin-danmuku-mask<br>验证分发并同步文档 | PKG-MASK-05, ENG-07 | danmuku.mask.js、资源版本/许可、CPU fallback 和包体积证据 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
@@ -534,6 +534,7 @@
- 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-MASK-03: [记录](changes/2026-09-13-PKG-MASK-03-lifecycle.md) [记录](baselines/danmuku-mask-lifecycle-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)
+9
View File
@@ -1,5 +1,14 @@
# 进度与证据
## PKG-MASK-03 生命周期拆分完成
五个职责模块和单运行归属控制器落地;停止/销毁、迟到回调、并发启动、释放等待和
多实例互不干扰有36候选测试。联合历史共96通过,lint和三格式正常构建通过。
新增包架构与[变更记录](changes/2026-09-13-PKG-MASK-03-lifecycle.md),候选用例接入test:unit。
MASK-START关闭;真实GPU/WASM释放、模型/后端/DOM组合仍保留05门槛,04 TS迁移下一步。
自动缩略图内部TS检查点已独立提交6c989decc并通过提交审计;03仍doing。
## PKG-AUTO-THUMB-03 内部TS检查点(仍doing)
六个抽帧/会话/资源运行模块迁到严格TS,内部类型和实际核心宿主接入fixture齐备。
+2 -2
View File
@@ -221,8 +221,8 @@
| 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, PKG-MASK-05 |
| MASK-START-01 | resolved / 已复现 | 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 |
+27 -8
View File
@@ -4910,7 +4910,8 @@
"status": "open",
"owners": [
"PKG-MASK-02",
"PKG-MASK-03"
"PKG-MASK-03",
"PKG-MASK-05"
],
"evidence": [
"refactor/baselines/danmuku-mask-contract.md",
@@ -4918,16 +4919,19 @@
"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"
"test/danmuku-mask-failures.test.js",
"refactor/changes/2026-09-13-PKG-MASK-03-lifecycle.md",
"refactor/baselines/danmuku-mask-lifecycle-validation.json",
"test/danmuku-mask-lifecycle.test.js"
],
"compatibleResolution": "Invalidate stale work while keeping restartable stop distinct from final destroy; release model and subscriptions.",
"compatibleResolution": "Invalidate stale work while keeping restartable stop distinct from final destroy; release model and subscriptions. PKG-MASK-03 fixes controlled stale work, owned bitmaps and subscriptions; native SDK dispose completion is not exposed by body-segmentation 1.0.2 and remains a real-resource validation limit in PKG-MASK-05.",
"closureCriteria": "Pending initialization/inference cannot write masks or schedule work after stop/destroy; errors settle and owned resources are released."
},
{
"id": "MASK-START-01",
"title": "Concurrent Mask start calls can initialize multiple models and create independent RAF chains",
"confirmation": "reproduced",
"status": "open",
"status": "resolved",
"owners": [
"PKG-MASK-02",
"PKG-MASK-03"
@@ -4938,10 +4942,19 @@
"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"
"test/danmuku-mask-failures.test.js",
"refactor/changes/2026-09-13-PKG-MASK-03-lifecycle.md",
"refactor/baselines/danmuku-mask-lifecycle-validation.json",
"test/danmuku-mask-lifecycle.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."
"closureCriteria": "Repeated and concurrent starts, stop then start, failures and multiple instances have deterministic resource and result assertions.",
"resolutionEvidence": [
"refactor/changes/2026-09-13-PKG-MASK-03-lifecycle.md",
"refactor/baselines/danmuku-mask-lifecycle-validation.json",
"test/danmuku-mask-lifecycle.test.js"
],
"resolutionRationale": "Controlled shared-SDK multiple-instance, concurrent/repeated start, cancellation/restart behind inference/disposal, failure recovery and synchronous reentry prove one owned chain per registration. Actual global SDK/device state is separately retained by PKG-MASK-05."
},
{
"id": "MASK-MODEL-01",
@@ -4977,7 +4990,10 @@
"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"
"test/danmuku-mask-failures.test.js",
"refactor/changes/2026-09-13-PKG-MASK-03-lifecycle.md",
"refactor/baselines/danmuku-mask-lifecycle-validation.json",
"test/danmuku-mask-lifecycle.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."
@@ -4998,7 +5014,10 @@
"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"
"test/danmuku-mask-failures.test.js",
"refactor/changes/2026-09-13-PKG-MASK-03-lifecycle.md",
"refactor/baselines/danmuku-mask-lifecycle-validation.json",
"test/danmuku-mask-lifecycle.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
View File
@@ -2563,11 +2563,14 @@
"CORE-18",
"PKG-DANMUKU-07"
],
"status": "todo",
"status": "done",
"risk": "H",
"deliverable": "模型加载/推理/画布输出分离,阻止重叠推理与过期写入",
"acceptance": "结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过",
"evidence": []
"evidence": [
"changes/2026-09-13-PKG-MASK-03-lifecycle.md",
"baselines/danmuku-mask-lifecycle-validation.json"
]
},
{
"id": "PKG-MASK-04",
+5
View File
@@ -2,6 +2,11 @@
Use the pinned Node/Yarn toolchain from `../refactor/toolchain-setup.md`.
`yarn test:danmuku-mask` runs candidate run cancellation/resource ownership plus
frozen historical defects and public contracts. Candidate tests also run in
`test:unit`; their controlled SDK, RAF and canvas hosts do not prove native model
inference, GPU disposal or browser mask geometry. See the Mask architecture map.
`yarn test:mediabunny` runs historical lifecycle observations, candidate load cancellation,
and real SDK input parsing/track contracts; all are in `test:unit`. Candidate failures
can be reproduced against the frozen main with `ARTPLAYER_MB_BASELINE=1`. This does not
+409
View File
@@ -0,0 +1,409 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Candidate lifecycle probes share the historical Node runner.
import test from 'node:test'
import { environment, flush, maskCandidate } from './helpers/danmuku-mask-candidate.js'
const code = await maskCandidate()
const create = settings => environment(code, settings)
test('Mask candidate instances sharing SDK and RAF retain independent models, layers and cancellation', async () => {
const env = create({ hold: ['segment'] })
const listeners = new Map()
const style = { maskImage: 'url(second-prior)' }
const secondArt = {
isDestroy: false,
template: { $video: { ...env.video }, $danmuku: { style } },
on(name, callback) { listeners.set(name, callback) },
off(name, callback) {
if (listeners.get(name) === callback)
listeners.delete(name)
},
}
const second = env.factory()(secondArt)
await Promise.all([env.result.start(), second.start()])
await flush()
assert.equal(env.created.length, 2)
assert.equal(env.inferences.length, 2)
assert.notEqual(env.inferences[0].video, env.inferences[1].video)
env.result.stop()
env.gates.segment[0].resolve()
await flush()
assert.equal(env.created[0].disposed, 1)
assert.equal(env.created[1].disposed, 0)
assert.equal(style.maskImage, 'url(second-prior)')
env.gates.segment[1].resolve()
await flush()
assert.equal(env.style.maskImage, 'none')
assert.equal(style.maskImage, 'url(data:image/png;base64,candidate-mask)')
assert.equal(env.frames.size, 1)
env.destroy()
await flush()
assert.equal(env.frames.size, 1, 'Destroying the first player must preserve the other RAF')
secondArt.isDestroy = true
listeners.get('destroy')()
await flush()
assert.equal(env.frames.size, 0)
assert.equal(listeners.size, 0)
assert.equal(style.maskImage, 'none')
assert.equal(env.created[1].disposed, 1)
})
async function ended(env) {
env.destroy()
await flush()
assert.equal(env.frames.size, 0)
assert.equal([...env.listeners.values()].reduce((sum, listeners) => sum + listeners.size, 0), 0)
}
test('Mask candidate retains synchronous registrar, optional options, exact public methods and Promise<void> start', async () => {
const env = create()
assert.deepEqual(Object.keys(env.result), ['name', 'start', 'stop'])
assert.equal(env.result.name, 'artplayerPluginDanmukuMask')
assert.equal(env.result.start.name, 'startSegmentation')
assert.equal(env.result.stop.name, 'stopSegmentation')
assert.equal(await env.result.start(), undefined)
await flush()
assert.equal(env.created.length, 1)
assert.equal(env.inferences.length, 1)
assert.equal(env.frames.size, 1)
assert.equal(env.style.maskImage, 'url(data:image/png;base64,candidate-mask)')
assert.deepEqual(env.canvases[0].pixels, [255, 255, 255, 0, 250, 255, 255, 255])
assert.equal(env.result.stop(), undefined)
await ended(env)
assert.equal(env.created[0].disposed, 1)
})
test('Mask candidate preserves OR defaults, false smooth setting, snapshot and exact SDK interpretation', async () => {
const option = { modelSelection: 0, smoothSegmentation: false, opacity: 0, maskBlurAmount: 0, foregroundThreshold: 0, solutionPath: '/models' }
const env = create({ option })
option.solutionPath = '/changed'
await env.result.start()
await flush()
assert.deepEqual(env.backends, ['webgl'])
assert.deepEqual(JSON.parse(JSON.stringify(env.created[0].config)), { runtime: 'mediapipe', modelType: 'general', solutionPath: '/models', modelSelection: 1, smoothSegmentation: false, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5, selfieMode: false })
assert.equal(env.masks[0][4], 0.5)
assert.equal(env.draws[0][3], 1)
assert.equal(env.draws[0][4], 3)
await ended(env)
})
for (const action of ['stop', 'destroy']) {
for (const phase of ['webgl', 'init', 'segment', 'mask', 'draw']) {
test(`Mask candidate ${action} during ${phase} prevents late output and RAF revival`, async () => {
const env = create({ hold: [phase] })
const starting = env.result.start()
await flush()
assert.equal(env.gates[phase].length, 1)
if (action === 'stop')
env.result.stop()
else env.destroy()
assert.equal(await starting, undefined, 'Cancellation settles the public start before SDK completion')
assert.equal(env.style.maskImage, 'none')
for (const model of env.created)
assert.equal(model.disposed, 0, 'Do not dispose a model while its initialization or frame work is pending')
assert.equal(env.frames.size, 0)
env.gates[phase][0].resolve()
await flush()
assert.equal(env.frames.size, 0)
assert(env.writes.every(value => value === 'none'))
assert.equal(env.created.length, phase === 'webgl' ? 0 : 1)
for (const model of env.created) {
assert.equal(model.disposed, 1)
assert.equal(model.disposedWhileBusy, false)
}
for (const canvas of env.canvases) assert.deepEqual([canvas.width, canvas.height], [0, 0])
await ended(env)
})
}
}
test('Mask candidate concurrent and repeated starts keep one initialization and one inference chain', async () => {
const env = create({ hold: ['init'] })
const first = env.result.start()
const second = env.result.start()
await flush()
assert.equal(env.created.length, 1)
env.gates.init[0].resolve()
await Promise.all([first, second])
await flush()
await env.result.start()
assert.equal(env.created.length, 1)
assert.equal(env.inferences.length, 1)
assert.equal(env.frames.size, 1)
await env.frame()
assert.equal(env.inferences.length, 2)
assert.equal(env.maximum, 1)
await ended(env)
})
test('Mask candidate restart waits for old inference and model disposal without blocking stop settlement', async () => {
const env = create({ hold: ['segment', 'dispose'] })
await env.result.start()
await flush()
env.result.stop()
const next = env.result.start()
await flush()
assert.equal(env.created.length, 1)
env.gates.segment[0].resolve()
await flush()
assert.equal(env.gates.dispose.length, 1)
assert.equal(env.created.length, 1)
env.settings.hold = []
env.gates.dispose[0].resolve()
await next
await flush()
assert.equal(env.created.length, 2)
assert.equal(env.maximum, 1)
assert.equal(env.created[0].disposed, 1)
assert.equal(env.created[0].disposedWhileBusy, false)
assert.equal(env.frames.size, 1)
await ended(env)
})
test('Mask candidate repeated stops cannot bypass a pending disposal when restarting', async () => {
const env = create({ paused: true, hold: ['dispose'] })
await env.result.start()
env.result.stop()
env.result.stop()
const next = env.result.start()
await flush()
assert.equal(env.created.length, 1)
assert.equal(env.gates.dispose.length, 1)
assert.deepEqual([env.canvases[0].width, env.canvases[0].height], [0, 0], 'A pending model disposal must not retain an idle private canvas bitmap')
env.settings.hold = []
env.gates.dispose[0].resolve()
await next
assert.equal(env.created.length, 2)
await ended(env)
})
test('Mask candidate synchronous SDK reentry does not overlap old and new runs', async () => {
let restarted
const env = create({ hold: ['segment'], onSegment(env) {
env.settings.onSegment = null
env.result.stop()
restarted = env.result.start()
} })
await env.result.start()
await flush()
assert.equal(env.created.length, 1)
env.settings.hold = []
env.gates.segment[0].resolve()
await restarted
await flush()
assert.equal(env.maximum, 1)
assert.equal(env.created.length, 2)
await ended(env)
})
test('Mask candidate cancels RAF id zero, detaches exact listeners and ignores start after destroy', async () => {
const env = create({ firstFrame: 0, paused: true })
await env.result.start()
assert.deepEqual([...env.frames.keys()], [0])
await ended(env)
assert.deepEqual(env.cancelled, [0])
env.emit('ready')
await env.result.start()
assert.equal(env.created.length, 1)
assert.equal(env.frames.size, 0)
})
test('Mask candidate no readable dimensions waits without invoking inference', async () => {
const env = create()
env.video.videoWidth = 0
await env.result.start()
await flush()
assert.equal(env.inferences.length, 0)
assert.equal(env.frames.size, 1)
env.video.videoWidth = 2
await env.frame()
assert.equal(env.inferences.length, 1)
await ended(env)
})
test('Mask candidate backend false keeps old interpretation while rejection tries CPU', async () => {
const fulfilled = create({ backendResult: { webgl: false } })
await fulfilled.result.start()
assert.deepEqual(fulfilled.backends, ['webgl'])
await ended(fulfilled)
const rejected = create({ reject: { webgl: new Error('no WebGL') }, backendResult: { cpu: false } })
await rejected.result.start()
assert.deepEqual(rejected.backends, ['webgl', 'cpu'])
assert.equal(rejected.created.length, 1)
await ended(rejected)
})
test('Mask candidate rejects explicit backend failure and observes automatic ready failure', async () => {
const error = new Error('no CPU')
const env = create({ reject: { webgl: new Error('no WebGL'), cpu: error } })
await assert.rejects(env.result.start(), failure => failure === error)
env.emit('ready')
await flush()
assert(env.logs.some(entry => entry[1] === 'Failed to start danmuku mask:' && entry[2] === error))
assert.equal(env.frames.size, 0)
await ended(env)
})
test('Mask candidate model rejection retains resolved start and logged error without idle retry loop', async () => {
const error = new Error('bad model')
const env = create({ reject: { init: error } })
assert.equal(await env.result.start(), undefined)
assert.deepEqual(env.logs, [['error', 'Error initializing segmenter:', error]])
assert.equal(env.frames.size, 0)
env.settings.reject = {}
await env.result.start()
await flush()
assert.equal(env.frames.size, 1)
await ended(env)
})
for (const setting of ['noLayer', 'noVideo', 'noContext', 'canvasError']) {
test(`Mask candidate ${setting} initialization fails without retaining resources`, async () => {
const env = create({ [setting]: setting === 'canvasError' ? new Error('canvas allocation') : true })
await assert.rejects(env.result.start())
await flush()
assert.equal(env.frames.size, 0)
for (const model of env.created) assert.equal(model.disposed, 1)
for (const canvas of env.canvases) assert.deepEqual([canvas.width, canvas.height], [0, 0])
assert.doesNotThrow(() => env.result.stop())
await ended(env)
})
}
test('Mask candidate inference and canvas-read errors keep the old active retry outlet', async () => {
for (const settings of [{ reject: { segment: new Error('inference') } }, { readError: new Error('taint') }]) {
const env = create(settings)
await env.result.start()
await flush()
assert.equal(env.logs[0][1], 'Error in segmentBody:')
assert.equal(env.style.maskImage, 'url(prior-mask)')
await env.frame()
assert.equal(env.logs.length, 2)
assert.equal(env.frames.size, 1)
await ended(env)
}
})
test('Mask candidate late SDK rejection after cancellation remains observed and silent', async () => {
const env = create({ hold: ['segment'] })
await env.result.start()
await flush()
env.destroy()
env.gates.segment[0].reject(new Error('late inference'))
await flush()
assert.equal(env.logs.length, 0)
assert.equal(env.frames.size, 0)
assert.equal(env.created[0].disposed, 1)
})
test('Mask candidate already-destroyed registration does not install listeners or allocate resources', async () => {
const env = create({ destroyed: true })
await env.result.start()
assert.equal(env.listeners.size, 0)
assert.equal(env.created.length, 0)
})
test('Mask candidate subscription rollback preserves original failure and prior host styles', () => {
for (const event of ['ready', 'destroy']) {
let env
const failure = new Error('subscription failed')
assert.throws(() => create({
subscriptionError: event,
subscriptionFailure: failure,
onEnvironment(value) { env = value },
}), error => error === failure)
assert.equal([...env.listeners.values()].reduce((sum, callbacks) => sum + callbacks.size, 0), 0)
assert.equal(env.style.maskImage, 'url(prior-mask)')
assert.equal(env.created.length, 0)
}
})
test('Mask candidate immediate stop skips backend/model startup and public start settles', async () => {
const env = create()
const pending = env.result.start()
env.result.stop()
await pending
await flush()
assert.deepEqual(env.backends, [])
assert.equal(env.created.length, 0)
await ended(env)
})
test('Mask candidate cancelled backend/model rejection is observed without retry or late log', async () => {
for (const phase of ['webgl', 'init']) {
const env = create({ hold: [phase] })
const pending = env.result.start()
await flush()
env.result.stop()
await pending
env.gates[phase][0].reject(new Error('late SDK rejection'))
await flush()
assert.equal(env.logs.length, 0)
assert.equal(env.frames.size, 0)
assert.deepEqual(env.backends, ['webgl'])
await ended(env)
}
})
test('Mask candidate disposal rejection is observed, clears canvas and allows independent restart', async () => {
const failure = new Error('dispose failed')
const env = create({ reject: { dispose: failure } })
await env.result.start()
await flush()
env.result.stop()
await flush()
assert.deepEqual(env.logs, [['warn', 'Failed to dispose danmuku mask segmenter:', failure]])
assert.deepEqual([env.canvases[0].width, env.canvases[0].height], [0, 0])
env.settings.reject = {}
await env.result.start()
assert.equal(env.created.length, 2)
await ended(env)
})
test('Mask candidate empty segmentation retains existing mask and a single active loop', async () => {
const env = create({ empty: true })
await env.result.start()
await flush()
assert.equal(env.style.maskImage, 'url(prior-mask)')
assert.equal(env.masks.length, 0)
assert.equal(env.draws.length, 0)
assert.equal(env.frames.size, 1)
await env.frame()
assert.equal(env.maximum, 1)
assert.equal(env.frames.size, 1)
await ended(env)
})
test('Mask candidate cancelled inference cannot overwrite a subsequent external mask write', async () => {
const env = create({ hold: ['segment'] })
await env.result.start()
await flush()
env.result.stop()
env.style.maskImage = 'url(user-mask)'
env.gates.segment[0].resolve()
await flush()
assert.equal(env.style.maskImage, 'url(user-mask)')
assert.equal(env.frames.size, 0)
assert.equal(env.created[0].disposed, 1)
await ended(env)
})
test('Mask candidate retains template capture before evaluating option getters', async () => {
let captured
const option = {
get solutionPath() {
captured.art.template.$video = { paused: true, ended: false, videoWidth: 0, videoHeight: 0 }
captured.art.template.$danmuku = { style: {} }
return '/models'
},
}
const env = create({
option,
onEnvironment(value) { captured = value },
})
await env.result.start()
await flush()
assert.equal(env.inferences[0].video, env.video)
assert.equal(env.style.maskImage, 'url(data:image/png;base64,candidate-mask)')
await ended(env)
})
+144
View File
@@ -0,0 +1,144 @@
import assert from 'node:assert/strict'
import vm from 'node:vm'
import { build } from 'esbuild'
export const flush = () => new Promise(resolve => setImmediate(resolve))
export async function maskCandidate() {
const result = await build({ entryPoints: ['packages/artplayer-plugin-danmuku-mask/src/index.js'], bundle: true, write: false, format: 'cjs', platform: 'browser', external: ['@tensorflow-models/body-segmentation', '@tensorflow/tfjs-core', '@tensorflow/tfjs-backend-webgl', '@tensorflow/tfjs-backend-cpu'] })
return result.outputFiles[0].text
}
export function environment(code, settings = {}) {
const env = { settings, frames: new Map(), listeners: new Map(), logs: [], writes: [], backends: [], created: [], canvases: [], inferences: [], masks: [], draws: [], gates: {}, cancelled: [], inflight: 0, maximum: 0 }
let nextFrame = settings.firstFrame ?? 1
async function phase(name, value) {
if (settings.reject?.[name])
throw settings.reject[name]
if (settings.hold?.includes(name)) {
let resolve
let reject
const promise = new Promise((yes, no) => {
resolve = yes
reject = no
})
;(env.gates[name] ||= []).push({ resolve, reject })
await promise
}
return value
}
const style = { maskMode: 'luminance' }
Object.defineProperty(style, 'maskImage', { get: () => env.writes.at(-1) ?? 'url(prior-mask)', set: value => env.writes.push(value) })
env.video = { paused: settings.paused ?? false, ended: false, videoWidth: 2, videoHeight: 1 }
env.style = style
const probe = {
SupportedModels: { MediaPipeSelfieSegmentation: 'selfie' },
async setBackend(name) {
env.backends.push(name)
return phase(name, settings.backendResult?.[name] ?? true)
},
async createSegmenter(model, config) {
const segmenter = {
model,
config,
disposed: 0,
disposedWhileBusy: false,
async dispose() {
this.disposed++
this.disposedWhileBusy = env.inflight > 0
await phase('dispose')
},
async segmentPeople(video) {
env.inferences.push({ segmenter, video })
env.inflight++
env.maximum = Math.max(env.maximum, env.inflight)
settings.onSegment?.(env)
try {
return await phase('segment', settings.empty ? [] : [{}])
}
finally { env.inflight-- }
},
}
env.created.push(segmenter)
return phase('init', segmenter)
},
async toBinaryMask(...args) {
env.masks.push(args)
return phase('mask', {})
},
async drawMask(...args) {
env.draws.push(args)
await phase('draw')
},
}
const module = { exports: {} }
const context = {
module,
exports: module.exports,
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
const canvas = {
width: 300,
height: 150,
pixels: null,
getContext: () => settings.noContext
? null
: {
getImageData() {
if (settings.readError)
throw settings.readError
return { data: new Uint8ClampedArray([255, 255, 255, 255, 250, 255, 255, 255]) }
},
putImageData(image) { canvas.pixels = [...image.data] },
},
toDataURL: () => 'data:image/png;base64,candidate-mask',
}
env.canvases.push(canvas)
return canvas
} },
requestAnimationFrame(callback) {
const id = nextFrame++
env.frames.set(id, callback)
return id
},
cancelAnimationFrame(id) {
env.cancelled.push(id)
env.frames.delete(id)
},
}
vm.runInNewContext(code, context)
const factory = module.exports.default
env.factory = factory
env.art = {
isDestroy: settings.destroyed ?? false,
template: { $video: settings.noVideo ? null : env.video, $danmuku: settings.noLayer ? null : { style } },
on(name, callback) {
if (!env.listeners.has(name))
env.listeners.set(name, new Set())
env.listeners.get(name).add(callback)
if (settings.subscriptionError === name)
throw settings.subscriptionFailure
},
off(name, callback) { env.listeners.get(name)?.delete(callback) },
}
settings.onEnvironment?.(env)
env.result = factory(settings.option)(env.art)
env.emit = name => [...(env.listeners.get(name) || [])].forEach(callback => 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) callback()
await flush()
}
return env
}