mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
refactor(mask): [PKG-MASK-03] isolate model runs and release stale work
This commit is contained in:
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
|
||||
@@ -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
@@ -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.
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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齐备。
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user