mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-14 00:06:17 -08:00
fix(danmuku): [PKG-DANMUKU-MASK-LOAD-01] recover stalled-frame rows and remove relaxed placement backlog
This commit is contained in:
1 parent
475d38043e
commit
314a1c3163
27 files changed
+14045
-195
No files matched your search
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";function t(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function e(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function i(t){if("string"!==typeof t)return[];const i=t.matchAll(/<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\/d>/gs);return Array.from(i).map(t=>{const i=t.groups.p.split(",");if(i.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(i[0]),mode:e(Number(i[1])),fontSize:Number(i[2]),color:`#${Number(i[3]).toString(16)}`,timestamp:Number(i[4]),pool:Number(i[5]),userID:i[6],rowID:Number(i[7])}}return null}).filter(Boolean)}function n({data:t}){const{xml:e,id:n}=t,a=i(e);globalThis.postMessage({danmus:a,id:n})}let a=0;function s(t,{signal:s,onCancel:r}={}){return new Promise((o,l)=>{let d,c,h,p=!1;const u=++a,m=()=>g(void 0,function(t){if(void 0!==t?.reason)return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(s));function f(){let t,e=!1;try{if(d){const t=d;d=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==c){const n=c;c=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function g(t,e){if(p)return;p=!0;let i=arguments.length>1;s?.removeEventListener("abort",m),h?.();try{f()}catch(n){i||(e=n),i=!0}i?l(e):o(t)}function y(t,e){if(!p){console.error("Error parsing Bilibili Danmu:",e);try{g(i(t))}catch(n){g(void 0,n)}}}s?.aborted?m():(s?.addEventListener("abort",m),h=r?.(m),p?h?.():async function(){const a=s?await fetch(t,{signal:s}):await fetch(t);if(p)return;const r=await a.text();if(!p)try{const t=`\n ${e.toString()}\n ${i.toString()}\n onmessage = ${n.toString()}\n `;if(c=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),p)return void f();if(d=new Worker(c),p)return void f();d.onmessage=t=>{if(p||t.data?.id!==u)return;const{danmus:e}=t.data;Array.isArray(e)?g(e):g(void 0,new Error("Invalid Bilibili Danmu worker response"))},d.onerror=t=>{t.preventDefault?.(),y(r,t.error||t)},d.onmessageerror=t=>y(r,t.error||t),d.postMessage({xml:r,id:u})}catch(o){y(r,o)}}().catch(t=>g(void 0,t)))})}const r=new WeakMap,o=Symbol("cancelled danmuku input");function l(t){return r.has(t)||r.set(t,{closed:!1,tasks:new Set,emitting:[]}),r.get(t)}function d(t){const e=l(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function c(t){const e=l(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function h(t,e,i){const n=t.states[e]||[];for(let a=0;a<n.length;a++)i(n[a]);return n}class p{constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=s+o,d=e-l,c=l/n.$restTime,h={};h.top=a,h.left=s,h.height=r,h.width=o,h.right=d,h.speed=c,h.distance=l,h.time=n.$restTime,h.mode=n.mode,t.push(h)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emit",e=>{switch(e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.left=`${t}px`,e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}})}continue(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("stop",e=>{switch(this.owner.setState(e,"emit"),e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}}),this}suspend(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("emit",e=>{switch(this.owner.setState(e,"stop"),e.mode){case 0:{const i=t-(this.owner.getLeft(e.$ref)-this.owner.getLeft(this.owner.$player));e.$ref.style.transform=`translLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";function t(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function e(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function i(t){if("string"!==typeof t)return[];const i=t.matchAll(/<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\/d>/gs);return Array.from(i).map(t=>{const i=t.groups.p.split(",");if(i.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(i[0]),mode:e(Number(i[1])),fontSize:Number(i[2]),color:`#${Number(i[3]).toString(16)}`,timestamp:Number(i[4]),pool:Number(i[5]),userID:i[6],rowID:Number(i[7])}}return null}).filter(Boolean)}function n({data:t}){const{xml:e,id:n}=t,s=i(e);globalThis.postMessage({danmus:s,id:n})}let s=0;function a(t,{signal:a,onCancel:r}={}){return new Promise((o,l)=>{let d,h,c,p=!1;const u=++s,m=()=>g(void 0,function(t){if(void 0!==t?.reason)return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(a));function f(){let t,e=!1;try{if(d){const t=d;d=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==h){const n=h;h=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function g(t,e){if(p)return;p=!0;let i=arguments.length>1;a?.removeEventListener("abort",m),c?.();try{f()}catch(n){i||(e=n),i=!0}i?l(e):o(t)}function y(t,e){if(!p){console.error("Error parsing Bilibili Danmu:",e);try{g(i(t))}catch(n){g(void 0,n)}}}a?.aborted?m():(a?.addEventListener("abort",m),c=r?.(m),p?c?.():async function(){const s=a?await fetch(t,{signal:a}):await fetch(t);if(p)return;const r=await s.text();if(!p)try{const t=`\n ${e.toString()}\n ${i.toString()}\n onmessage = ${n.toString()}\n `;if(h=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),p)return void f();if(d=new Worker(h),p)return void f();d.onmessage=t=>{if(p||t.data?.id!==u)return;const{danmus:e}=t.data;Array.isArray(e)?g(e):g(void 0,new Error("Invalid Bilibili Danmu worker response"))},d.onerror=t=>{t.preventDefault?.(),y(r,t.error||t)},d.onmessageerror=t=>y(r,t.error||t),d.postMessage({xml:r,id:u})}catch(o){y(r,o)}}().catch(t=>g(void 0,t)))})}const r=new WeakMap,o=Symbol("cancelled danmuku input");function l(t){return r.has(t)||r.set(t,{closed:!1,tasks:new Set,emitting:[]}),r.get(t)}function d(t){const e=l(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function h(t){const e=l(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function c(t,e,i){const n=t.states[e]||[];for(let s=0;s<n.length;s++)i(n[s]);return n}class p{constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const s=n.$ref.offsetTop,a=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=a+o,d=e-l,h=l/n.$restTime,c={};c.top=s,c.left=a,c.height=r,c.width=o,c.right=d,c.speed=h,c.distance=l,c.time=n.$restTime,c.mode=n.mode,t.push(c)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emit",e=>{switch(e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.left=`${t}px`,e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}})}continue(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("stop",e=>{switch(this.owner.setState(e,"emit"),e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}}),this}suspend(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("emit",e=>{switch(this.owner.setState(e,"stop"),e.mode){case 0:{const i=t-(this.owner.getLeft(e.$ref)-this.owner.getLeft(this.owner.$player));e.$ref.style.transform=`translLine truncated
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.defineProperties,i=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,r=Math.pow,o=(e,i,n)=>i in e?t(e,i,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[i]=n,l=(t,e)=>{for(var i in e||(e={}))a.call(e,i)&&o(t,i,e[i]);if(n)for(var i of n(e))s.call(e,i)&&o(t,i,e[i]);return t},d=(t,n)=>e(t,i(n)),c=(t,e,i)=>new Promise((n,a)=>{var s=t=>{try{o(i.next(t))}catch(e){a(e)}},r=t=>{try{o(i.throw(t))}catch(e){a(e)}},o=t=>t.done?n(t.value):Promise.resolve(t.value).then(s,r);o((i=i.apply(t,e)).next())});function p(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function h(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function u(t){if("string"!==typeof t)return[];const e=new RegExp('<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\\/d>',"gs"),i=t.matchAll(e);return Array.from(i).map(t=>{const e=t.groups.p.split(",");if(e.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(e[0]),mode:h(Number(e[1])),fontSize:Number(e[2]),color:`#${Number(e[3]).toString(16)}`,timestamp:Number(e[4]),pool:Number(e[5]),userID:e[6],rowID:Number(e[7])}}return null}).filter(Boolean)}function m({data:t}){const{xml:e,id:i}=t,n=u(e);globalThis.postMessage({danmus:n,id:i})}let f=0;function g(t,{signal:e,onCancel:i}={}){return new Promise((n,a)=>{let s,r,o,l=!1;const d=++f,p=()=>y(void 0,function(t){if(void 0!==(null==t?void 0:t.reason))return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(e));function g(){let t,e=!1;try{if(s){const t=s;s=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==r){const n=r;r=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function y(t,i){if(l)return;l=!0;let s=arguments.length>1;null==e||e.removeEventListener("abort",p),null==o||o();try{g()}catch(r){s||(i=r),s=!0}s?a(i):n(t)}function v(t,e){if(!l){console.error("Error parsing Bilibili Danmu:",e);try{y(u(t))}catch(i){y(void 0,i)}}}(null==e?void 0:e.aborted)?p():(null==e||e.addEventListener("abort",p),o=null==i?void 0:i(p),l?null==o||o():function(){return c(this,null,function*(){const i=e?yield fetch(t,{signal:e}):yield fetch(t);if(l)return;const n=yield i.text();if(!l)try{const t=`\n ${h.toString()}\n ${u.toString()}\n onmessage = ${m.toString()}\n `;if(r=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),l)return void g();if(s=new Worker(r),l)return void g();s.onmessage=t=>{var e;if(l||(null==(e=t.data)?void 0:e.id)!==d)return;const{danmus:i}=t.data;Array.isArray(i)?y(i):y(void 0,new Error("Invalid Bilibili Danmu worker response"))},s.onerror=t=>{var e;null==(e=t.preventDefault)||e.call(t),v(n,t.error||t)},s.onmessageerror=t=>v(n,t.error||t),s.postMessage({xml:n,id:d})}catch(a){v(n,a)}})}().catch(t=>y(void 0,t)))})}const y=new WeakMap,v=Symbol("cancelled danmuku input");function w(t){return y.has(t)||y.set(t,{closed:!1,tasks:new Set,emitting:[]}),y.get(t)}function k(t){const e=w(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function x(t){const e=w(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function b(t,e,i){const n=t.states[e]||[];for(let a=0;a<n.length;a++)i(n[a]);return n}class ${constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=s+o,d=e-l,c=l/n.$restTime,p={};p.top=a,p.left=s,p.height=r,p.width=o,p.right=d,p.speed=c,p.distance=l,p.time=n.$restTime,p.mode=n.mode,t.push(p)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emitLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.defineProperties,i=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,s=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,r=Math.pow,o=(e,i,n)=>i in e?t(e,i,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[i]=n,l=(t,e)=>{for(var i in e||(e={}))s.call(e,i)&&o(t,i,e[i]);if(n)for(var i of n(e))a.call(e,i)&&o(t,i,e[i]);return t},d=(t,n)=>e(t,i(n)),h=(t,e,i)=>new Promise((n,s)=>{var a=t=>{try{o(i.next(t))}catch(e){s(e)}},r=t=>{try{o(i.throw(t))}catch(e){s(e)}},o=t=>t.done?n(t.value):Promise.resolve(t.value).then(a,r);o((i=i.apply(t,e)).next())});function c(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function p(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function u(t){if("string"!==typeof t)return[];const e=new RegExp('<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\\/d>',"gs"),i=t.matchAll(e);return Array.from(i).map(t=>{const e=t.groups.p.split(",");if(e.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(e[0]),mode:p(Number(e[1])),fontSize:Number(e[2]),color:`#${Number(e[3]).toString(16)}`,timestamp:Number(e[4]),pool:Number(e[5]),userID:e[6],rowID:Number(e[7])}}return null}).filter(Boolean)}function m({data:t}){const{xml:e,id:i}=t,n=u(e);globalThis.postMessage({danmus:n,id:i})}let f=0;function g(t,{signal:e,onCancel:i}={}){return new Promise((n,s)=>{let a,r,o,l=!1;const d=++f,c=()=>y(void 0,function(t){if(void 0!==(null==t?void 0:t.reason))return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(e));function g(){let t,e=!1;try{if(a){const t=a;a=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==r){const n=r;r=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function y(t,i){if(l)return;l=!0;let a=arguments.length>1;null==e||e.removeEventListener("abort",c),null==o||o();try{g()}catch(r){a||(i=r),a=!0}a?s(i):n(t)}function v(t,e){if(!l){console.error("Error parsing Bilibili Danmu:",e);try{y(u(t))}catch(i){y(void 0,i)}}}(null==e?void 0:e.aborted)?c():(null==e||e.addEventListener("abort",c),o=null==i?void 0:i(c),l?null==o||o():function(){return h(this,null,function*(){const i=e?yield fetch(t,{signal:e}):yield fetch(t);if(l)return;const n=yield i.text();if(!l)try{const t=`\n ${p.toString()}\n ${u.toString()}\n onmessage = ${m.toString()}\n `;if(r=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),l)return void g();if(a=new Worker(r),l)return void g();a.onmessage=t=>{var e;if(l||(null==(e=t.data)?void 0:e.id)!==d)return;const{danmus:i}=t.data;Array.isArray(i)?y(i):y(void 0,new Error("Invalid Bilibili Danmu worker response"))},a.onerror=t=>{var e;null==(e=t.preventDefault)||e.call(t),v(n,t.error||t)},a.onmessageerror=t=>v(n,t.error||t),a.postMessage({xml:n,id:d})}catch(s){v(n,s)}})}().catch(t=>y(void 0,t)))})}const y=new WeakMap,v=Symbol("cancelled danmuku input");function w(t){return y.has(t)||y.set(t,{closed:!1,tasks:new Set,emitting:[]}),y.get(t)}function k(t){const e=w(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function x(t){const e=w(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function b(t,e,i){const n=t.states[e]||[];for(let s=0;s<n.length;s++)i(n[s]);return n}class ${constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const s=n.$ref.offsetTop,a=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=a+o,d=e-l,h=l/n.$restTime,c={};c.top=s,c.left=a,c.height=r,c.width=o,c.right=d,c.speed=h,c.distance=l,c.time=n.$restTime,c.mode=n.mode,t.push(c)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emitLine truncated
|
||||
@@ -538,6 +538,155 @@ class Renderer {
|
||||
this.nodes.clear();
|
||||
}
|
||||
}
|
||||
function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom, marginTop, antiOverlap }) {
|
||||
const maxTop = clientHeight - marginBottom;
|
||||
const danmus = visibles.filter((item) => item.mode === target.mode && item.top <= maxTop).sort((prev, next) => prev.top - next.top);
|
||||
if (danmus.length === 0) {
|
||||
if (target.mode === 2) {
|
||||
return maxTop - target.height;
|
||||
} else {
|
||||
return marginTop;
|
||||
}
|
||||
}
|
||||
danmus.unshift({
|
||||
type: "top",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: marginTop,
|
||||
width: clientWidth,
|
||||
speed: 0,
|
||||
distance: clientWidth
|
||||
});
|
||||
danmus.push({
|
||||
type: "bottom",
|
||||
top: maxTop,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: marginBottom,
|
||||
width: clientWidth,
|
||||
speed: 0,
|
||||
distance: clientWidth
|
||||
});
|
||||
if (target.mode === 2) {
|
||||
for (let index = danmus.length - 2; index >= 0; index -= 1) {
|
||||
const item = danmus[index];
|
||||
const prev = danmus[index + 1];
|
||||
const itemBottom = item.top + item.height;
|
||||
const diff = prev.top - itemBottom;
|
||||
if (diff >= target.height) {
|
||||
return prev.top - target.height;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let index = 1; index < danmus.length; index += 1) {
|
||||
const item = danmus[index];
|
||||
const prev = danmus[index - 1];
|
||||
const prevBottom = prev.top + prev.height;
|
||||
const diff = item.top - prevBottom;
|
||||
if (diff >= target.height) {
|
||||
return prevBottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
const topMap = [];
|
||||
for (let index = 1; index < danmus.length - 1; index += 1) {
|
||||
const item = danmus[index];
|
||||
if (topMap.length) {
|
||||
const last = topMap[topMap.length - 1];
|
||||
if (last[0].top === item.top) {
|
||||
last.push(item);
|
||||
} else {
|
||||
topMap.push([item]);
|
||||
}
|
||||
} else {
|
||||
topMap.push([item]);
|
||||
}
|
||||
}
|
||||
if (antiOverlap) {
|
||||
switch (target.mode) {
|
||||
case 0: {
|
||||
const result = topMap.find((list) => {
|
||||
return list.every((danmu) => {
|
||||
if (clientWidth < danmu.distance)
|
||||
return false;
|
||||
if (target.speed < danmu.speed)
|
||||
return true;
|
||||
const overlapTime = danmu.right / (target.speed - danmu.speed);
|
||||
if (overlapTime > danmu.time)
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
});
|
||||
return result && result[0] ? result[0].top : void 0;
|
||||
}
|
||||
// 静止弹幕没有重叠问题
|
||||
case 1:
|
||||
case 2:
|
||||
return void 0;
|
||||
}
|
||||
} else {
|
||||
switch (target.mode) {
|
||||
case 0:
|
||||
topMap.sort((prev, next) => {
|
||||
const nextMinRight = Math.min(...next.map((item) => item.right));
|
||||
const prevMinRight = Math.min(...prev.map((item) => item.right));
|
||||
return nextMinRight * next.length - prevMinRight * prev.length;
|
||||
});
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
topMap.sort((prev, next) => {
|
||||
const nextMaxWidth = Math.max(...next.map((item) => item.width));
|
||||
const prevMaxWidth = Math.max(...prev.map((item) => item.width));
|
||||
return prevMaxWidth * prev.length - nextMaxWidth * next.length;
|
||||
});
|
||||
break;
|
||||
}
|
||||
return topMap[0][0].top;
|
||||
}
|
||||
}
|
||||
class SamplingWindow {
|
||||
constructor() {
|
||||
this.rows = /* @__PURE__ */ new Set();
|
||||
this.waiting = /* @__PURE__ */ new Set();
|
||||
this.visibility = () => this.clear();
|
||||
}
|
||||
capture(owner, rows, time) {
|
||||
const doc = owner.$player.ownerDocument;
|
||||
if (doc !== this.document) {
|
||||
this.document?.removeEventListener("visibilitychange", this.visibility);
|
||||
this.document = doc;
|
||||
doc?.addEventListener("visibilitychange", this.visibility);
|
||||
this.clear();
|
||||
}
|
||||
if (!Number.isFinite(time) || doc?.visibilityState === "hidden" || owner.art.video?.seeking) {
|
||||
this.clear();
|
||||
return rows;
|
||||
}
|
||||
const previousTime = this.time;
|
||||
const previousRows = this.rows;
|
||||
const previousWaiting = this.waiting;
|
||||
this.time = time;
|
||||
this.rows = new Set(rows);
|
||||
this.waiting = new Set(owner.states.wait);
|
||||
if (previousTime === void 0 || time <= previousTime)
|
||||
return rows;
|
||||
const missed = owner.states.wait.filter((row) => previousWaiting.has(row) && !previousRows.has(row) && row.time >= previousTime - 0.1 && row.time < time - 0.1);
|
||||
for (const row of missed) this.rows.add(row);
|
||||
return [...rows.filter((row) => row.$state === "ready"), ...missed, ...rows.filter((row) => row.$state !== "ready")];
|
||||
}
|
||||
clear() {
|
||||
this.time = void 0;
|
||||
this.rows.clear();
|
||||
this.waiting.clear();
|
||||
}
|
||||
destroy() {
|
||||
this.document?.removeEventListener("visibilitychange", this.visibility);
|
||||
this.document = void 0;
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
class SchedulingBuffer {
|
||||
constructor() {
|
||||
this.pending = /* @__PURE__ */ new Set();
|
||||
@@ -584,6 +733,7 @@ class Scheduler {
|
||||
this.fault = false;
|
||||
this.failedItems = /* @__PURE__ */ new Set();
|
||||
this.buffer = new SchedulingBuffer();
|
||||
this.sampling = new SamplingWindow();
|
||||
}
|
||||
active(operation) {
|
||||
const owner = this.owner;
|
||||
@@ -607,6 +757,7 @@ class Scheduler {
|
||||
this.owner.workerClient?.cancel();
|
||||
this.failedItems.clear();
|
||||
this.buffer.clear();
|
||||
this.sampling.clear();
|
||||
}
|
||||
report(error) {
|
||||
try {
|
||||
@@ -655,10 +806,11 @@ class Scheduler {
|
||||
if (danmu.$restTime <= 0)
|
||||
owner.makeWait(danmu);
|
||||
});
|
||||
const time = owner.art.currentTime;
|
||||
const readys2 = owner.readys;
|
||||
if (generation !== this.generation)
|
||||
return;
|
||||
this.buffer.capture(readys2, this.failedItems);
|
||||
this.buffer.capture(this.sampling.capture(owner, readys2, time), this.failedItems);
|
||||
}
|
||||
} catch (error) {
|
||||
if (generation === this.generation)
|
||||
@@ -718,7 +870,7 @@ class Scheduler {
|
||||
danmu.$lastStartTime = Date.now();
|
||||
danmu.$restTime = owner.speed;
|
||||
const distance = clientWidth + ref.clientWidth;
|
||||
const reply = await operation.wait(owner.postMessage({
|
||||
const request = {
|
||||
type: "getDanmuTop",
|
||||
target: { mode: danmu.mode, height: ref.clientHeight, speed: distance / danmu.$restTime },
|
||||
visibles: owner.visibles,
|
||||
@@ -728,7 +880,8 @@ class Scheduler {
|
||||
// Valid margins are numeric. Preserve the old raw fallback for unsupported strings.
|
||||
marginBottom: owner.marginBottom,
|
||||
marginTop: owner.marginTop
|
||||
}));
|
||||
};
|
||||
const reply = request.antiOverlap ? await operation.wait(owner.postMessage(request)) : { result: getDanmuTop(request) };
|
||||
const top = typeof reply === "symbol" ? void 0 : reply.result;
|
||||
if (!this.active(operation) || danmu.$ref !== ref)
|
||||
return;
|
||||
@@ -747,6 +900,7 @@ class Scheduler {
|
||||
destroy() {
|
||||
this.closed = true;
|
||||
this.invalidate();
|
||||
this.sampling.destroy();
|
||||
}
|
||||
}
|
||||
let nextRequest = 0;
|
||||
|
||||
@@ -534,6 +534,155 @@ var artplayerPluginDanmuku = (function() {
|
||||
this.nodes.clear();
|
||||
}
|
||||
}
|
||||
function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom, marginTop, antiOverlap }) {
|
||||
const maxTop = clientHeight - marginBottom;
|
||||
const danmus = visibles.filter((item) => item.mode === target.mode && item.top <= maxTop).sort((prev, next) => prev.top - next.top);
|
||||
if (danmus.length === 0) {
|
||||
if (target.mode === 2) {
|
||||
return maxTop - target.height;
|
||||
} else {
|
||||
return marginTop;
|
||||
}
|
||||
}
|
||||
danmus.unshift({
|
||||
type: "top",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: marginTop,
|
||||
width: clientWidth,
|
||||
speed: 0,
|
||||
distance: clientWidth
|
||||
});
|
||||
danmus.push({
|
||||
type: "bottom",
|
||||
top: maxTop,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: marginBottom,
|
||||
width: clientWidth,
|
||||
speed: 0,
|
||||
distance: clientWidth
|
||||
});
|
||||
if (target.mode === 2) {
|
||||
for (let index = danmus.length - 2; index >= 0; index -= 1) {
|
||||
const item = danmus[index];
|
||||
const prev = danmus[index + 1];
|
||||
const itemBottom = item.top + item.height;
|
||||
const diff = prev.top - itemBottom;
|
||||
if (diff >= target.height) {
|
||||
return prev.top - target.height;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let index = 1; index < danmus.length; index += 1) {
|
||||
const item = danmus[index];
|
||||
const prev = danmus[index - 1];
|
||||
const prevBottom = prev.top + prev.height;
|
||||
const diff = item.top - prevBottom;
|
||||
if (diff >= target.height) {
|
||||
return prevBottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
const topMap = [];
|
||||
for (let index = 1; index < danmus.length - 1; index += 1) {
|
||||
const item = danmus[index];
|
||||
if (topMap.length) {
|
||||
const last = topMap[topMap.length - 1];
|
||||
if (last[0].top === item.top) {
|
||||
last.push(item);
|
||||
} else {
|
||||
topMap.push([item]);
|
||||
}
|
||||
} else {
|
||||
topMap.push([item]);
|
||||
}
|
||||
}
|
||||
if (antiOverlap) {
|
||||
switch (target.mode) {
|
||||
case 0: {
|
||||
const result = topMap.find((list) => {
|
||||
return list.every((danmu) => {
|
||||
if (clientWidth < danmu.distance)
|
||||
return false;
|
||||
if (target.speed < danmu.speed)
|
||||
return true;
|
||||
const overlapTime = danmu.right / (target.speed - danmu.speed);
|
||||
if (overlapTime > danmu.time)
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
});
|
||||
return result && result[0] ? result[0].top : void 0;
|
||||
}
|
||||
// 静止弹幕没有重叠问题
|
||||
case 1:
|
||||
case 2:
|
||||
return void 0;
|
||||
}
|
||||
} else {
|
||||
switch (target.mode) {
|
||||
case 0:
|
||||
topMap.sort((prev, next) => {
|
||||
const nextMinRight = Math.min(...next.map((item) => item.right));
|
||||
const prevMinRight = Math.min(...prev.map((item) => item.right));
|
||||
return nextMinRight * next.length - prevMinRight * prev.length;
|
||||
});
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
topMap.sort((prev, next) => {
|
||||
const nextMaxWidth = Math.max(...next.map((item) => item.width));
|
||||
const prevMaxWidth = Math.max(...prev.map((item) => item.width));
|
||||
return prevMaxWidth * prev.length - nextMaxWidth * next.length;
|
||||
});
|
||||
break;
|
||||
}
|
||||
return topMap[0][0].top;
|
||||
}
|
||||
}
|
||||
class SamplingWindow {
|
||||
constructor() {
|
||||
this.rows = /* @__PURE__ */ new Set();
|
||||
this.waiting = /* @__PURE__ */ new Set();
|
||||
this.visibility = () => this.clear();
|
||||
}
|
||||
capture(owner, rows, time) {
|
||||
const doc = owner.$player.ownerDocument;
|
||||
if (doc !== this.document) {
|
||||
this.document?.removeEventListener("visibilitychange", this.visibility);
|
||||
this.document = doc;
|
||||
doc?.addEventListener("visibilitychange", this.visibility);
|
||||
this.clear();
|
||||
}
|
||||
if (!Number.isFinite(time) || doc?.visibilityState === "hidden" || owner.art.video?.seeking) {
|
||||
this.clear();
|
||||
return rows;
|
||||
}
|
||||
const previousTime = this.time;
|
||||
const previousRows = this.rows;
|
||||
const previousWaiting = this.waiting;
|
||||
this.time = time;
|
||||
this.rows = new Set(rows);
|
||||
this.waiting = new Set(owner.states.wait);
|
||||
if (previousTime === void 0 || time <= previousTime)
|
||||
return rows;
|
||||
const missed = owner.states.wait.filter((row) => previousWaiting.has(row) && !previousRows.has(row) && row.time >= previousTime - 0.1 && row.time < time - 0.1);
|
||||
for (const row of missed) this.rows.add(row);
|
||||
return [...rows.filter((row) => row.$state === "ready"), ...missed, ...rows.filter((row) => row.$state !== "ready")];
|
||||
}
|
||||
clear() {
|
||||
this.time = void 0;
|
||||
this.rows.clear();
|
||||
this.waiting.clear();
|
||||
}
|
||||
destroy() {
|
||||
this.document?.removeEventListener("visibilitychange", this.visibility);
|
||||
this.document = void 0;
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
class SchedulingBuffer {
|
||||
constructor() {
|
||||
this.pending = /* @__PURE__ */ new Set();
|
||||
@@ -580,6 +729,7 @@ var artplayerPluginDanmuku = (function() {
|
||||
this.fault = false;
|
||||
this.failedItems = /* @__PURE__ */ new Set();
|
||||
this.buffer = new SchedulingBuffer();
|
||||
this.sampling = new SamplingWindow();
|
||||
}
|
||||
active(operation) {
|
||||
const owner = this.owner;
|
||||
@@ -603,6 +753,7 @@ var artplayerPluginDanmuku = (function() {
|
||||
this.owner.workerClient?.cancel();
|
||||
this.failedItems.clear();
|
||||
this.buffer.clear();
|
||||
this.sampling.clear();
|
||||
}
|
||||
report(error) {
|
||||
try {
|
||||
@@ -651,10 +802,11 @@ var artplayerPluginDanmuku = (function() {
|
||||
if (danmu.$restTime <= 0)
|
||||
owner.makeWait(danmu);
|
||||
});
|
||||
const time = owner.art.currentTime;
|
||||
const readys2 = owner.readys;
|
||||
if (generation !== this.generation)
|
||||
return;
|
||||
this.buffer.capture(readys2, this.failedItems);
|
||||
this.buffer.capture(this.sampling.capture(owner, readys2, time), this.failedItems);
|
||||
}
|
||||
} catch (error) {
|
||||
if (generation === this.generation)
|
||||
@@ -714,7 +866,7 @@ var artplayerPluginDanmuku = (function() {
|
||||
danmu.$lastStartTime = Date.now();
|
||||
danmu.$restTime = owner.speed;
|
||||
const distance = clientWidth + ref.clientWidth;
|
||||
const reply = await operation.wait(owner.postMessage({
|
||||
const request = {
|
||||
type: "getDanmuTop",
|
||||
target: { mode: danmu.mode, height: ref.clientHeight, speed: distance / danmu.$restTime },
|
||||
visibles: owner.visibles,
|
||||
@@ -724,7 +876,8 @@ var artplayerPluginDanmuku = (function() {
|
||||
// Valid margins are numeric. Preserve the old raw fallback for unsupported strings.
|
||||
marginBottom: owner.marginBottom,
|
||||
marginTop: owner.marginTop
|
||||
}));
|
||||
};
|
||||
const reply = request.antiOverlap ? await operation.wait(owner.postMessage(request)) : { result: getDanmuTop(request) };
|
||||
const top = typeof reply === "symbol" ? void 0 : reply.result;
|
||||
if (!this.active(operation) || danmu.$ref !== ref)
|
||||
return;
|
||||
@@ -743,6 +896,7 @@ var artplayerPluginDanmuku = (function() {
|
||||
destroy() {
|
||||
this.closed = true;
|
||||
this.invalidate();
|
||||
this.sampling.destroy();
|
||||
}
|
||||
}
|
||||
let nextRequest = 0;
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
The native combination suite includes both frozen/current Danmuku with three core
|
||||
versions and a bounded 20 rows/s load after model readiness. Model output must
|
||||
continue during that load. Current Chromium/WebKit delivery failures block the
|
||||
combination gate under PKG-DANMUKU-MASK-LOAD-01; Firefox passes do not close them.
|
||||
See `refactor/changes/2026-09-14-PKG-DANMUKU-08-combined-load.md` for exact source
|
||||
inputs, historical observations and the still-unproven performance cause.
|
||||
continue during that load. PKG-DANMUKU-MASK-LOAD-01 addresses Chromium/WebKit
|
||||
delivery loss in the Danmuku scheduler; this does not replace or patch the Mask
|
||||
model, SDK, backend, video dimensions or inference loop. The optional Chromium
|
||||
`ARTPLAYER_MASK_PROFILE=1` capture records actual SDK image conversion cost.
|
||||
Keep profiling separate from ordinary timing acceptance. See the original
|
||||
failures in `refactor/changes/2026-09-14-PKG-DANMUKU-08-combined-load.md` and the
|
||||
repair record in `refactor/changes/2026-09-14-PKG-DANMUKU-MASK-LOAD-01-scheduling.md`.
|
||||
|
||||
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
|
||||
|
||||
@@ -21,8 +21,10 @@ old-plugin combinations, combined-load performance and devices remain task 08/09
|
||||
The actual Mask combination now also tests frozen 5.3.0 and a 20 rows/s post-model
|
||||
load. `test/helpers/danmuku-combination-load.js` owns only test RAF/listeners and
|
||||
retains observations after failed assertions. Candidate complete delivery remains
|
||||
mandatory. Chromium/WebKit failures are tracked by DANMUKU-MASK-LOAD-01 and
|
||||
PKG-DANMUKU-MASK-LOAD-01; ordinary playback or Firefox passes cannot close them.
|
||||
mandatory. PKG-DANMUKU-MASK-LOAD-01 addresses the original Chromium/WebKit loss
|
||||
with continuous-playback sampling and relaxed placement. Preserve the original
|
||||
failed reports and rerun the complete model/core/format matrix after changes;
|
||||
ordinary playback or Firefox passes alone cannot close that gate.
|
||||
|
||||
`danmuku-fullscreen.spec.js` tests all old/current core and plugin pairs in real
|
||||
document and CSS web fullscreen, with three fresh player lifetimes per case.
|
||||
@@ -57,10 +59,12 @@ historical declarations, earlier exports and failure evidence are indexed in
|
||||
| `src/bilibili.ts` | Fetch and response text, one parser Worker per request, fallback, settlement and Blob URL cleanup |
|
||||
| `src/scheduler.ts` | One sampling RAF and one serial asynchronous batch, cancellation generations, preparation ownership and failure recovery |
|
||||
| `src/scheduling-buffer.ts` | Identity-based observations and current-batch reservations; ready-before-wait selection without public state mutation |
|
||||
| `src/sampling-window.ts` | Continuous media-time observations, stalled-frame recovery and document visibility ownership |
|
||||
| `src/renderer.ts` | Owned node allocation, preparation, geometry snapshots, placement, pause/resume styling and disposal |
|
||||
| `src/queue.ts` | Ordered state pools, eligibility and state transitions |
|
||||
| `src/worker-client.ts` | Track Worker lifecycle, a shared response dispatcher, unique request IDs and pending requests |
|
||||
| `src/worker.ts` | Track placement Worker; a separate protocol from the XML parser Worker |
|
||||
| `src/placement.ts` | Shared pure legacy track geometry, used by relaxed placement and the Worker |
|
||||
| `src/worker.ts` | Placement Worker message adapter; a separate protocol from the XML parser Worker |
|
||||
| `src/setting.ts` | Settings coordinator, configuration controls, mount and fullscreen layout |
|
||||
| `src/setting-template.ts` | Existing template HTML and static icon values |
|
||||
| `src/setting-slider.ts` | Slider index, pointer and rotation behavior |
|
||||
@@ -71,7 +75,7 @@ historical declarations, earlier exports and failure evidence are indexed in
|
||||
| `src/heatmap-sampling.ts` | Sorted-time bin counts with historical numeric boundaries |
|
||||
| `src/heatmap-geometry.ts` | Legacy point normalization, caller-visible point writes and SVG curve calculation |
|
||||
|
||||
All 20 owned runtime modules use TypeScript. Their responsibilities remain
|
||||
All owned runtime modules use TypeScript. Their responsibilities remain
|
||||
separate from public declaration compatibility and from release acceptance.
|
||||
The following files contain types only:
|
||||
|
||||
@@ -191,8 +195,13 @@ retains the current-time +/- 0.1 second window and state-pool order. Keep the
|
||||
existing track algorithm and geometry fields until separately reviewed.
|
||||
|
||||
RAF sampling and visible lifetime maintenance continue while `beforeVisible` or
|
||||
the Worker is pending. The scheduling buffer stores only actually observed row
|
||||
references; it does not sweep elapsed time or modify public wait/ready states.
|
||||
the Worker is pending. Public `readys` remains the instantaneous +/- 0.1 second
|
||||
query. The sampling window also recovers rows crossed between consecutive
|
||||
forward media samples, but only if they were already waiting at the earlier
|
||||
sample and were not already selected there. It does not replay newly appended
|
||||
past rows or retry a false callback merely because time crossed that row.
|
||||
The scheduling buffer stores the resulting row references without modifying
|
||||
public wait/ready states.
|
||||
The dispatcher remains serial. Reserve every row in its batch until completion,
|
||||
so a rejected placement cannot repeatedly jump ahead of later rows in that batch.
|
||||
At the next RAF, capture new observations before selecting ready rows followed by
|
||||
@@ -200,14 +209,24 @@ wait rows, retaining capture order within each group. Identity sets prevent the
|
||||
same observed row from entering both the current and pending batch. Cancellation
|
||||
clears both sets; an old operation's finally must not clear a newer batch.
|
||||
|
||||
This fixes missed eligibility during asynchronous preparation, not timestamps
|
||||
crossed while the entire main thread is blocked. The native timing diagnostic
|
||||
keeps the unchanged published miss visible beside the candidate regression and
|
||||
records CPU-blocked frame gaps without calling them successful load acceptance.
|
||||
Sampling still scans the existing state pools on each active frame. Buffer storage
|
||||
is bounded by the distinct observed queue rows, not an arbitrary drop limit; this
|
||||
does not establish a throughput or memory improvement. Measure dense queues and
|
||||
long asynchronous waits in the separate sustained-load acceptance task.
|
||||
The sampling window clears on scheduler invalidation, seeking, backward or
|
||||
invalid media time, hidden documents and document changes. It owns exactly one
|
||||
visibility listener on the player's current document, releases it on adoption,
|
||||
and removes it on destroy. Visibility changes clear catch-up history without
|
||||
cancelling a running user callback. The next visible sample starts a new window.
|
||||
This extends PKG-DANMUKU-12 to cover main-thread gaps during continuous playback;
|
||||
it is not a promise to replay comments across pause, seek or background periods.
|
||||
Sampling still scans existing state pools; retained identities are bounded by
|
||||
queue rows rather than an arbitrary drop limit.
|
||||
|
||||
After the serial `beforeVisible` callback, `antiOverlap: false` uses the shared
|
||||
placement function directly, avoiding a Worker round trip for every row while
|
||||
an actual segmentation model competes for the main thread. `antiOverlap: true`
|
||||
and the internal `postMessage` helper retain the Worker protocol. Track geometry,
|
||||
default options, visible event order, generation checks and visible lifetime
|
||||
remain unchanged. Do not duplicate the geometry in either caller or bypass the
|
||||
callback cancellation guard. Native model loads and timing diagnostics complement
|
||||
the controlled scheduling tests; they do not prove unlimited throughput.
|
||||
|
||||
Pause, reset, successful replacement input commit, hide, seeking, destroy and replacing the
|
||||
`beforeVisible` callback invalidate unfinished preparation. Cancellation races
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";function t(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function e(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function i(t){if("string"!==typeof t)return[];const i=t.matchAll(/<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\/d>/gs);return Array.from(i).map(t=>{const i=t.groups.p.split(",");if(i.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(i[0]),mode:e(Number(i[1])),fontSize:Number(i[2]),color:`#${Number(i[3]).toString(16)}`,timestamp:Number(i[4]),pool:Number(i[5]),userID:i[6],rowID:Number(i[7])}}return null}).filter(Boolean)}function n({data:t}){const{xml:e,id:n}=t,a=i(e);globalThis.postMessage({danmus:a,id:n})}let a=0;function s(t,{signal:s,onCancel:r}={}){return new Promise((o,l)=>{let d,c,h,p=!1;const u=++a,m=()=>g(void 0,function(t){if(void 0!==t?.reason)return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(s));function f(){let t,e=!1;try{if(d){const t=d;d=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==c){const n=c;c=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function g(t,e){if(p)return;p=!0;let i=arguments.length>1;s?.removeEventListener("abort",m),h?.();try{f()}catch(n){i||(e=n),i=!0}i?l(e):o(t)}function y(t,e){if(!p){console.error("Error parsing Bilibili Danmu:",e);try{g(i(t))}catch(n){g(void 0,n)}}}s?.aborted?m():(s?.addEventListener("abort",m),h=r?.(m),p?h?.():async function(){const a=s?await fetch(t,{signal:s}):await fetch(t);if(p)return;const r=await a.text();if(!p)try{const t=`\n ${e.toString()}\n ${i.toString()}\n onmessage = ${n.toString()}\n `;if(c=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),p)return void f();if(d=new Worker(c),p)return void f();d.onmessage=t=>{if(p||t.data?.id!==u)return;const{danmus:e}=t.data;Array.isArray(e)?g(e):g(void 0,new Error("Invalid Bilibili Danmu worker response"))},d.onerror=t=>{t.preventDefault?.(),y(r,t.error||t)},d.onmessageerror=t=>y(r,t.error||t),d.postMessage({xml:r,id:u})}catch(o){y(r,o)}}().catch(t=>g(void 0,t)))})}const r=new WeakMap,o=Symbol("cancelled danmuku input");function l(t){return r.has(t)||r.set(t,{closed:!1,tasks:new Set,emitting:[]}),r.get(t)}function d(t){const e=l(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function c(t){const e=l(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function h(t,e,i){const n=t.states[e]||[];for(let a=0;a<n.length;a++)i(n[a]);return n}class p{constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=s+o,d=e-l,c=l/n.$restTime,h={};h.top=a,h.left=s,h.height=r,h.width=o,h.right=d,h.speed=c,h.distance=l,h.time=n.$restTime,h.mode=n.mode,t.push(h)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emit",e=>{switch(e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.left=`${t}px`,e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}})}continue(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("stop",e=>{switch(this.owner.setState(e,"emit"),e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}}),this}suspend(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("emit",e=>{switch(this.owner.setState(e,"stop"),e.mode){case 0:{const i=t-(this.owner.getLeft(e.$ref)-this.owner.getLeft(this.owner.$player));e.$ref.style.transform=`translLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";function t(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function e(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function i(t){if("string"!==typeof t)return[];const i=t.matchAll(/<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\/d>/gs);return Array.from(i).map(t=>{const i=t.groups.p.split(",");if(i.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(i[0]),mode:e(Number(i[1])),fontSize:Number(i[2]),color:`#${Number(i[3]).toString(16)}`,timestamp:Number(i[4]),pool:Number(i[5]),userID:i[6],rowID:Number(i[7])}}return null}).filter(Boolean)}function n({data:t}){const{xml:e,id:n}=t,s=i(e);globalThis.postMessage({danmus:s,id:n})}let s=0;function a(t,{signal:a,onCancel:r}={}){return new Promise((o,l)=>{let d,h,c,p=!1;const u=++s,m=()=>g(void 0,function(t){if(void 0!==t?.reason)return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(a));function f(){let t,e=!1;try{if(d){const t=d;d=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==h){const n=h;h=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function g(t,e){if(p)return;p=!0;let i=arguments.length>1;a?.removeEventListener("abort",m),c?.();try{f()}catch(n){i||(e=n),i=!0}i?l(e):o(t)}function y(t,e){if(!p){console.error("Error parsing Bilibili Danmu:",e);try{g(i(t))}catch(n){g(void 0,n)}}}a?.aborted?m():(a?.addEventListener("abort",m),c=r?.(m),p?c?.():async function(){const s=a?await fetch(t,{signal:a}):await fetch(t);if(p)return;const r=await s.text();if(!p)try{const t=`\n ${e.toString()}\n ${i.toString()}\n onmessage = ${n.toString()}\n `;if(h=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),p)return void f();if(d=new Worker(h),p)return void f();d.onmessage=t=>{if(p||t.data?.id!==u)return;const{danmus:e}=t.data;Array.isArray(e)?g(e):g(void 0,new Error("Invalid Bilibili Danmu worker response"))},d.onerror=t=>{t.preventDefault?.(),y(r,t.error||t)},d.onmessageerror=t=>y(r,t.error||t),d.postMessage({xml:r,id:u})}catch(o){y(r,o)}}().catch(t=>g(void 0,t)))})}const r=new WeakMap,o=Symbol("cancelled danmuku input");function l(t){return r.has(t)||r.set(t,{closed:!1,tasks:new Set,emitting:[]}),r.get(t)}function d(t){const e=l(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function h(t){const e=l(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function c(t,e,i){const n=t.states[e]||[];for(let s=0;s<n.length;s++)i(n[s]);return n}class p{constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const s=n.$ref.offsetTop,a=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=a+o,d=e-l,h=l/n.$restTime,c={};c.top=s,c.left=a,c.height=r,c.width=o,c.right=d,c.speed=h,c.distance=l,c.time=n.$restTime,c.mode=n.mode,t.push(c)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emit",e=>{switch(e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.left=`${t}px`,e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}})}continue(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("stop",e=>{switch(this.owner.setState(e,"emit"),e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}}),this}suspend(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("emit",e=>{switch(this.owner.setState(e,"stop"),e.mode){case 0:{const i=t-(this.owner.getLeft(e.$ref)-this.owner.getLeft(this.owner.$player));e.$ref.style.transform=`translLine truncated
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.defineProperties,i=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,r=Math.pow,o=(e,i,n)=>i in e?t(e,i,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[i]=n,l=(t,e)=>{for(var i in e||(e={}))a.call(e,i)&&o(t,i,e[i]);if(n)for(var i of n(e))s.call(e,i)&&o(t,i,e[i]);return t},d=(t,n)=>e(t,i(n)),c=(t,e,i)=>new Promise((n,a)=>{var s=t=>{try{o(i.next(t))}catch(e){a(e)}},r=t=>{try{o(i.throw(t))}catch(e){a(e)}},o=t=>t.done?n(t.value):Promise.resolve(t.value).then(s,r);o((i=i.apply(t,e)).next())});function p(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function h(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function u(t){if("string"!==typeof t)return[];const e=new RegExp('<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\\/d>',"gs"),i=t.matchAll(e);return Array.from(i).map(t=>{const e=t.groups.p.split(",");if(e.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(e[0]),mode:h(Number(e[1])),fontSize:Number(e[2]),color:`#${Number(e[3]).toString(16)}`,timestamp:Number(e[4]),pool:Number(e[5]),userID:e[6],rowID:Number(e[7])}}return null}).filter(Boolean)}function m({data:t}){const{xml:e,id:i}=t,n=u(e);globalThis.postMessage({danmus:n,id:i})}let f=0;function g(t,{signal:e,onCancel:i}={}){return new Promise((n,a)=>{let s,r,o,l=!1;const d=++f,p=()=>y(void 0,function(t){if(void 0!==(null==t?void 0:t.reason))return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(e));function g(){let t,e=!1;try{if(s){const t=s;s=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==r){const n=r;r=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function y(t,i){if(l)return;l=!0;let s=arguments.length>1;null==e||e.removeEventListener("abort",p),null==o||o();try{g()}catch(r){s||(i=r),s=!0}s?a(i):n(t)}function v(t,e){if(!l){console.error("Error parsing Bilibili Danmu:",e);try{y(u(t))}catch(i){y(void 0,i)}}}(null==e?void 0:e.aborted)?p():(null==e||e.addEventListener("abort",p),o=null==i?void 0:i(p),l?null==o||o():function(){return c(this,null,function*(){const i=e?yield fetch(t,{signal:e}):yield fetch(t);if(l)return;const n=yield i.text();if(!l)try{const t=`\n ${h.toString()}\n ${u.toString()}\n onmessage = ${m.toString()}\n `;if(r=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),l)return void g();if(s=new Worker(r),l)return void g();s.onmessage=t=>{var e;if(l||(null==(e=t.data)?void 0:e.id)!==d)return;const{danmus:i}=t.data;Array.isArray(i)?y(i):y(void 0,new Error("Invalid Bilibili Danmu worker response"))},s.onerror=t=>{var e;null==(e=t.preventDefault)||e.call(t),v(n,t.error||t)},s.onmessageerror=t=>v(n,t.error||t),s.postMessage({xml:n,id:d})}catch(a){v(n,a)}})}().catch(t=>y(void 0,t)))})}const y=new WeakMap,v=Symbol("cancelled danmuku input");function w(t){return y.has(t)||y.set(t,{closed:!1,tasks:new Set,emitting:[]}),y.get(t)}function k(t){const e=w(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function x(t){const e=w(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function b(t,e,i){const n=t.states[e]||[];for(let a=0;a<n.length;a++)i(n[a]);return n}class ${constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=s+o,d=e-l,c=l/n.$restTime,p={};p.top=a,p.left=s,p.height=r,p.width=o,p.right=d,p.speed=c,p.distance=l,p.time=n.$restTime,p.mode=n.mode,t.push(p)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emitLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.defineProperties,i=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,s=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,r=Math.pow,o=(e,i,n)=>i in e?t(e,i,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[i]=n,l=(t,e)=>{for(var i in e||(e={}))s.call(e,i)&&o(t,i,e[i]);if(n)for(var i of n(e))a.call(e,i)&&o(t,i,e[i]);return t},d=(t,n)=>e(t,i(n)),h=(t,e,i)=>new Promise((n,s)=>{var a=t=>{try{o(i.next(t))}catch(e){s(e)}},r=t=>{try{o(i.throw(t))}catch(e){s(e)}},o=t=>t.done?n(t.value):Promise.resolve(t.value).then(a,r);o((i=i.apply(t,e)).next())});function c(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function p(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function u(t){if("string"!==typeof t)return[];const e=new RegExp('<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\\/d>',"gs"),i=t.matchAll(e);return Array.from(i).map(t=>{const e=t.groups.p.split(",");if(e.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(e[0]),mode:p(Number(e[1])),fontSize:Number(e[2]),color:`#${Number(e[3]).toString(16)}`,timestamp:Number(e[4]),pool:Number(e[5]),userID:e[6],rowID:Number(e[7])}}return null}).filter(Boolean)}function m({data:t}){const{xml:e,id:i}=t,n=u(e);globalThis.postMessage({danmus:n,id:i})}let f=0;function g(t,{signal:e,onCancel:i}={}){return new Promise((n,s)=>{let a,r,o,l=!1;const d=++f,c=()=>y(void 0,function(t){if(void 0!==(null==t?void 0:t.reason))return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(e));function g(){let t,e=!1;try{if(a){const t=a;a=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==r){const n=r;r=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function y(t,i){if(l)return;l=!0;let a=arguments.length>1;null==e||e.removeEventListener("abort",c),null==o||o();try{g()}catch(r){a||(i=r),a=!0}a?s(i):n(t)}function v(t,e){if(!l){console.error("Error parsing Bilibili Danmu:",e);try{y(u(t))}catch(i){y(void 0,i)}}}(null==e?void 0:e.aborted)?c():(null==e||e.addEventListener("abort",c),o=null==i?void 0:i(c),l?null==o||o():function(){return h(this,null,function*(){const i=e?yield fetch(t,{signal:e}):yield fetch(t);if(l)return;const n=yield i.text();if(!l)try{const t=`\n ${p.toString()}\n ${u.toString()}\n onmessage = ${m.toString()}\n `;if(r=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),l)return void g();if(a=new Worker(r),l)return void g();a.onmessage=t=>{var e;if(l||(null==(e=t.data)?void 0:e.id)!==d)return;const{danmus:i}=t.data;Array.isArray(i)?y(i):y(void 0,new Error("Invalid Bilibili Danmu worker response"))},a.onerror=t=>{var e;null==(e=t.preventDefault)||e.call(t),v(n,t.error||t)},a.onmessageerror=t=>v(n,t.error||t),a.postMessage({xml:n,id:d})}catch(s){v(n,s)}})}().catch(t=>y(void 0,t)))})}const y=new WeakMap,v=Symbol("cancelled danmuku input");function w(t){return y.has(t)||y.set(t,{closed:!1,tasks:new Set,emitting:[]}),y.get(t)}function k(t){const e=w(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function x(t){const e=w(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function b(t,e,i){const n=t.states[e]||[];for(let s=0;s<n.length;s++)i(n[s]);return n}class ${constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const s=n.$ref.offsetTop,a=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=a+o,d=e-l,h=l/n.$restTime,c={};c.top=s,c.left=a,c.height=r,c.width=o,c.right=d,c.speed=h,c.distance=l,c.time=n.$restTime,c.mode=n.mode,t.push(c)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emitLine truncated
|
||||
@@ -538,6 +538,155 @@ class Renderer {
|
||||
this.nodes.clear();
|
||||
}
|
||||
}
|
||||
function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom, marginTop, antiOverlap }) {
|
||||
const maxTop = clientHeight - marginBottom;
|
||||
const danmus = visibles.filter((item) => item.mode === target.mode && item.top <= maxTop).sort((prev, next) => prev.top - next.top);
|
||||
if (danmus.length === 0) {
|
||||
if (target.mode === 2) {
|
||||
return maxTop - target.height;
|
||||
} else {
|
||||
return marginTop;
|
||||
}
|
||||
}
|
||||
danmus.unshift({
|
||||
type: "top",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: marginTop,
|
||||
width: clientWidth,
|
||||
speed: 0,
|
||||
distance: clientWidth
|
||||
});
|
||||
danmus.push({
|
||||
type: "bottom",
|
||||
top: maxTop,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: marginBottom,
|
||||
width: clientWidth,
|
||||
speed: 0,
|
||||
distance: clientWidth
|
||||
});
|
||||
if (target.mode === 2) {
|
||||
for (let index = danmus.length - 2; index >= 0; index -= 1) {
|
||||
const item = danmus[index];
|
||||
const prev = danmus[index + 1];
|
||||
const itemBottom = item.top + item.height;
|
||||
const diff = prev.top - itemBottom;
|
||||
if (diff >= target.height) {
|
||||
return prev.top - target.height;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let index = 1; index < danmus.length; index += 1) {
|
||||
const item = danmus[index];
|
||||
const prev = danmus[index - 1];
|
||||
const prevBottom = prev.top + prev.height;
|
||||
const diff = item.top - prevBottom;
|
||||
if (diff >= target.height) {
|
||||
return prevBottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
const topMap = [];
|
||||
for (let index = 1; index < danmus.length - 1; index += 1) {
|
||||
const item = danmus[index];
|
||||
if (topMap.length) {
|
||||
const last = topMap[topMap.length - 1];
|
||||
if (last[0].top === item.top) {
|
||||
last.push(item);
|
||||
} else {
|
||||
topMap.push([item]);
|
||||
}
|
||||
} else {
|
||||
topMap.push([item]);
|
||||
}
|
||||
}
|
||||
if (antiOverlap) {
|
||||
switch (target.mode) {
|
||||
case 0: {
|
||||
const result = topMap.find((list) => {
|
||||
return list.every((danmu) => {
|
||||
if (clientWidth < danmu.distance)
|
||||
return false;
|
||||
if (target.speed < danmu.speed)
|
||||
return true;
|
||||
const overlapTime = danmu.right / (target.speed - danmu.speed);
|
||||
if (overlapTime > danmu.time)
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
});
|
||||
return result && result[0] ? result[0].top : void 0;
|
||||
}
|
||||
// 静止弹幕没有重叠问题
|
||||
case 1:
|
||||
case 2:
|
||||
return void 0;
|
||||
}
|
||||
} else {
|
||||
switch (target.mode) {
|
||||
case 0:
|
||||
topMap.sort((prev, next) => {
|
||||
const nextMinRight = Math.min(...next.map((item) => item.right));
|
||||
const prevMinRight = Math.min(...prev.map((item) => item.right));
|
||||
return nextMinRight * next.length - prevMinRight * prev.length;
|
||||
});
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
topMap.sort((prev, next) => {
|
||||
const nextMaxWidth = Math.max(...next.map((item) => item.width));
|
||||
const prevMaxWidth = Math.max(...prev.map((item) => item.width));
|
||||
return prevMaxWidth * prev.length - nextMaxWidth * next.length;
|
||||
});
|
||||
break;
|
||||
}
|
||||
return topMap[0][0].top;
|
||||
}
|
||||
}
|
||||
class SamplingWindow {
|
||||
constructor() {
|
||||
this.rows = /* @__PURE__ */ new Set();
|
||||
this.waiting = /* @__PURE__ */ new Set();
|
||||
this.visibility = () => this.clear();
|
||||
}
|
||||
capture(owner, rows, time) {
|
||||
const doc = owner.$player.ownerDocument;
|
||||
if (doc !== this.document) {
|
||||
this.document?.removeEventListener("visibilitychange", this.visibility);
|
||||
this.document = doc;
|
||||
doc?.addEventListener("visibilitychange", this.visibility);
|
||||
this.clear();
|
||||
}
|
||||
if (!Number.isFinite(time) || doc?.visibilityState === "hidden" || owner.art.video?.seeking) {
|
||||
this.clear();
|
||||
return rows;
|
||||
}
|
||||
const previousTime = this.time;
|
||||
const previousRows = this.rows;
|
||||
const previousWaiting = this.waiting;
|
||||
this.time = time;
|
||||
this.rows = new Set(rows);
|
||||
this.waiting = new Set(owner.states.wait);
|
||||
if (previousTime === void 0 || time <= previousTime)
|
||||
return rows;
|
||||
const missed = owner.states.wait.filter((row) => previousWaiting.has(row) && !previousRows.has(row) && row.time >= previousTime - 0.1 && row.time < time - 0.1);
|
||||
for (const row of missed) this.rows.add(row);
|
||||
return [...rows.filter((row) => row.$state === "ready"), ...missed, ...rows.filter((row) => row.$state !== "ready")];
|
||||
}
|
||||
clear() {
|
||||
this.time = void 0;
|
||||
this.rows.clear();
|
||||
this.waiting.clear();
|
||||
}
|
||||
destroy() {
|
||||
this.document?.removeEventListener("visibilitychange", this.visibility);
|
||||
this.document = void 0;
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
class SchedulingBuffer {
|
||||
constructor() {
|
||||
this.pending = /* @__PURE__ */ new Set();
|
||||
@@ -584,6 +733,7 @@ class Scheduler {
|
||||
this.fault = false;
|
||||
this.failedItems = /* @__PURE__ */ new Set();
|
||||
this.buffer = new SchedulingBuffer();
|
||||
this.sampling = new SamplingWindow();
|
||||
}
|
||||
active(operation) {
|
||||
const owner = this.owner;
|
||||
@@ -607,6 +757,7 @@ class Scheduler {
|
||||
this.owner.workerClient?.cancel();
|
||||
this.failedItems.clear();
|
||||
this.buffer.clear();
|
||||
this.sampling.clear();
|
||||
}
|
||||
report(error) {
|
||||
try {
|
||||
@@ -655,10 +806,11 @@ class Scheduler {
|
||||
if (danmu.$restTime <= 0)
|
||||
owner.makeWait(danmu);
|
||||
});
|
||||
const time = owner.art.currentTime;
|
||||
const readys2 = owner.readys;
|
||||
if (generation !== this.generation)
|
||||
return;
|
||||
this.buffer.capture(readys2, this.failedItems);
|
||||
this.buffer.capture(this.sampling.capture(owner, readys2, time), this.failedItems);
|
||||
}
|
||||
} catch (error) {
|
||||
if (generation === this.generation)
|
||||
@@ -718,7 +870,7 @@ class Scheduler {
|
||||
danmu.$lastStartTime = Date.now();
|
||||
danmu.$restTime = owner.speed;
|
||||
const distance = clientWidth + ref.clientWidth;
|
||||
const reply = await operation.wait(owner.postMessage({
|
||||
const request = {
|
||||
type: "getDanmuTop",
|
||||
target: { mode: danmu.mode, height: ref.clientHeight, speed: distance / danmu.$restTime },
|
||||
visibles: owner.visibles,
|
||||
@@ -728,7 +880,8 @@ class Scheduler {
|
||||
// Valid margins are numeric. Preserve the old raw fallback for unsupported strings.
|
||||
marginBottom: owner.marginBottom,
|
||||
marginTop: owner.marginTop
|
||||
}));
|
||||
};
|
||||
const reply = request.antiOverlap ? await operation.wait(owner.postMessage(request)) : { result: getDanmuTop(request) };
|
||||
const top = typeof reply === "symbol" ? void 0 : reply.result;
|
||||
if (!this.active(operation) || danmu.$ref !== ref)
|
||||
return;
|
||||
@@ -747,6 +900,7 @@ class Scheduler {
|
||||
destroy() {
|
||||
this.closed = true;
|
||||
this.invalidate();
|
||||
this.sampling.destroy();
|
||||
}
|
||||
}
|
||||
let nextRequest = 0;
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import type { PlacementRequest, PlacementRow, VisibleDanmu } from './worker-types'
|
||||
|
||||
export function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom, marginTop, antiOverlap }: PlacementRequest): number | undefined {
|
||||
// 弹幕最大高度
|
||||
const maxTop = clientHeight - marginBottom
|
||||
|
||||
// 过滤同模式的弹幕,即每种模式各不影响
|
||||
const danmus: PlacementRow[] = visibles
|
||||
.filter(item => item.mode === target.mode && item.top <= maxTop)
|
||||
.sort((prev, next) => prev.top - next.top)
|
||||
|
||||
// 如果没有同模式的弹幕,直接返回
|
||||
if (danmus.length === 0) {
|
||||
if (target.mode === 2) {
|
||||
return maxTop - target.height
|
||||
}
|
||||
else {
|
||||
return marginTop
|
||||
}
|
||||
}
|
||||
|
||||
// 上下各加一个虚拟弹幕,方便计算
|
||||
danmus.unshift({
|
||||
type: 'top',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: marginTop,
|
||||
width: clientWidth,
|
||||
speed: 0,
|
||||
distance: clientWidth,
|
||||
})
|
||||
|
||||
danmus.push({
|
||||
type: 'bottom',
|
||||
top: maxTop,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: marginBottom,
|
||||
width: clientWidth,
|
||||
speed: 0,
|
||||
distance: clientWidth,
|
||||
})
|
||||
|
||||
// 查找是否有多余的缝隙足以容纳当前弹幕
|
||||
if (target.mode === 2) {
|
||||
// 倒序查找
|
||||
for (let index = danmus.length - 2; index >= 0; index -= 1) {
|
||||
const item = danmus[index]!
|
||||
const prev = danmus[index + 1]!
|
||||
const itemBottom = item.top + item.height
|
||||
const diff = prev.top - itemBottom
|
||||
if (diff >= target.height) {
|
||||
return prev.top - target.height
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 顺序查找
|
||||
for (let index = 1; index < danmus.length; index += 1) {
|
||||
const item = danmus[index]!
|
||||
const prev = danmus[index - 1]!
|
||||
const prevBottom = prev.top + prev.height
|
||||
const diff = item.top - prevBottom
|
||||
if (diff >= target.height) {
|
||||
return prevBottom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const topMap: VisibleDanmu[][] = []
|
||||
for (let index = 1; index < danmus.length - 1; index += 1) {
|
||||
// The loop excludes the two virtual boundary rows inserted above.
|
||||
const item = danmus[index] as VisibleDanmu
|
||||
if (topMap.length) {
|
||||
const last = topMap[topMap.length - 1]!
|
||||
if (last[0]!.top === item.top) {
|
||||
last.push(item)
|
||||
}
|
||||
else {
|
||||
topMap.push([item])
|
||||
}
|
||||
}
|
||||
else {
|
||||
topMap.push([item])
|
||||
}
|
||||
}
|
||||
|
||||
if (antiOverlap) {
|
||||
switch (target.mode) {
|
||||
case 0: {
|
||||
const result = topMap.find((list) => {
|
||||
return list.every((danmu) => {
|
||||
if (clientWidth < danmu.distance)
|
||||
return false
|
||||
if (target.speed < danmu.speed)
|
||||
return true
|
||||
const overlapTime = danmu.right / (target.speed - danmu.speed)
|
||||
if (overlapTime > danmu.time)
|
||||
return true
|
||||
return false
|
||||
})
|
||||
})
|
||||
|
||||
return result && result[0] ? result[0].top : undefined
|
||||
}
|
||||
|
||||
// 静止弹幕没有重叠问题
|
||||
case 1:
|
||||
case 2:
|
||||
return undefined
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
else {
|
||||
switch (target.mode) {
|
||||
case 0:
|
||||
topMap.sort((prev, next) => {
|
||||
const nextMinRight = Math.min(...next.map(item => item.right))
|
||||
const prevMinRight = Math.min(...prev.map(item => item.right))
|
||||
return nextMinRight * next.length - prevMinRight * prev.length
|
||||
})
|
||||
break
|
||||
case 1:
|
||||
case 2:
|
||||
topMap.sort((prev, next) => {
|
||||
const nextMaxWidth = Math.max(...next.map(item => item.width))
|
||||
const prevMaxWidth = Math.max(...prev.map(item => item.width))
|
||||
return prevMaxWidth * prev.length - nextMaxWidth * next.length
|
||||
})
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return topMap[0]![0]!.top
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type Danmuku from './danmuku'
|
||||
import type { DanmuItem } from './types'
|
||||
|
||||
// Public readys remains a point-in-time query; automatic playback can bridge a stalled frame.
|
||||
export default class SamplingWindow {
|
||||
private time: number | undefined
|
||||
private rows = new Set<DanmuItem>()
|
||||
private waiting = new Set<DanmuItem>()
|
||||
private document: Document | undefined
|
||||
private visibility = () => this.clear()
|
||||
|
||||
capture(owner: Danmuku, rows: DanmuItem[], time: number) {
|
||||
const doc = owner.$player.ownerDocument
|
||||
if (doc !== this.document) {
|
||||
this.document?.removeEventListener('visibilitychange', this.visibility)
|
||||
this.document = doc
|
||||
doc?.addEventListener('visibilitychange', this.visibility)
|
||||
this.clear()
|
||||
}
|
||||
if (!Number.isFinite(time) || doc?.visibilityState === 'hidden' || owner.art.video?.seeking) {
|
||||
this.clear()
|
||||
return rows
|
||||
}
|
||||
const previousTime = this.time
|
||||
const previousRows = this.rows
|
||||
const previousWaiting = this.waiting
|
||||
this.time = time
|
||||
this.rows = new Set(rows)
|
||||
this.waiting = new Set(owner.states.wait)
|
||||
if (previousTime === undefined || time <= previousTime)
|
||||
return rows
|
||||
const missed = owner.states.wait.filter(row => previousWaiting.has(row) && !previousRows.has(row)
|
||||
&& row.time >= previousTime - 0.1 && row.time < time - 0.1)
|
||||
for (const row of missed) this.rows.add(row)
|
||||
return [...rows.filter(row => row.$state === 'ready'), ...missed, ...rows.filter(row => row.$state !== 'ready')]
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.time = undefined
|
||||
this.rows.clear()
|
||||
this.waiting.clear()
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.document?.removeEventListener('visibilitychange', this.visibility)
|
||||
this.document = undefined
|
||||
this.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import type Danmuku from './danmuku'
|
||||
import type { DanmuItem } from './types'
|
||||
import type { PlacementRequest } from './worker-types'
|
||||
import { getDanmuTop } from './placement'
|
||||
import SamplingWindow from './sampling-window'
|
||||
import SchedulingBuffer from './scheduling-buffer'
|
||||
|
||||
const cancelled = Symbol('cancelled danmuku frame')
|
||||
@@ -23,6 +26,7 @@ export default class Scheduler {
|
||||
declare fault: boolean
|
||||
declare failedItems: Set<DanmuItem>
|
||||
declare buffer: SchedulingBuffer
|
||||
declare sampling: SamplingWindow
|
||||
|
||||
constructor(owner: Danmuku) {
|
||||
this.owner = owner
|
||||
@@ -35,6 +39,7 @@ export default class Scheduler {
|
||||
this.fault = false
|
||||
this.failedItems = new Set()
|
||||
this.buffer = new SchedulingBuffer()
|
||||
this.sampling = new SamplingWindow()
|
||||
}
|
||||
|
||||
active(operation: FrameOperation) {
|
||||
@@ -62,6 +67,7 @@ export default class Scheduler {
|
||||
this.owner.workerClient?.cancel()
|
||||
this.failedItems.clear()
|
||||
this.buffer.clear()
|
||||
this.sampling.clear()
|
||||
}
|
||||
|
||||
report(error: unknown) {
|
||||
@@ -116,10 +122,11 @@ export default class Scheduler {
|
||||
if (danmu.$restTime <= 0)
|
||||
owner.makeWait(danmu)
|
||||
})
|
||||
const time = owner.art.currentTime
|
||||
const readys = owner.readys
|
||||
if (generation !== this.generation)
|
||||
return
|
||||
this.buffer.capture(readys, this.failedItems)
|
||||
this.buffer.capture(this.sampling.capture(owner, readys, time), this.failedItems)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
@@ -183,7 +190,7 @@ export default class Scheduler {
|
||||
danmu.$lastStartTime = Date.now()
|
||||
danmu.$restTime = owner.speed
|
||||
const distance = clientWidth + ref.clientWidth
|
||||
const reply = await operation.wait(owner.postMessage({
|
||||
const request: PlacementRequest = {
|
||||
type: 'getDanmuTop',
|
||||
target: { mode: danmu.mode, height: ref.clientHeight, speed: distance / danmu.$restTime },
|
||||
visibles: owner.visibles,
|
||||
@@ -193,7 +200,12 @@ export default class Scheduler {
|
||||
// Valid margins are numeric. Preserve the old raw fallback for unsupported strings.
|
||||
marginBottom: owner.marginBottom as number,
|
||||
marginTop: owner.marginTop,
|
||||
}))
|
||||
}
|
||||
// Relaxed placement is small and deterministic; avoid a Worker round trip per row.
|
||||
// Collision-sensitive placement and the internal postMessage API still use the Worker.
|
||||
const reply = request.antiOverlap
|
||||
? await operation.wait(owner.postMessage(request))
|
||||
: { result: getDanmuTop(request) }
|
||||
const top = typeof reply === 'symbol' ? undefined : reply.result
|
||||
if (!this.active(operation) || danmu.$ref !== ref)
|
||||
return
|
||||
@@ -215,5 +227,6 @@ export default class Scheduler {
|
||||
destroy() {
|
||||
this.closed = true
|
||||
this.invalidate()
|
||||
this.sampling.destroy()
|
||||
}
|
||||
}
|
||||
@@ -1,142 +1,5 @@
|
||||
import type { PlacementMessage, PlacementReply, PlacementRequest, PlacementRow, VisibleDanmu } from './worker-types'
|
||||
|
||||
function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom, marginTop, antiOverlap }: PlacementRequest): number | undefined {
|
||||
// 弹幕最大高度
|
||||
const maxTop = clientHeight - marginBottom
|
||||
|
||||
// 过滤同模式的弹幕,即每种模式各不影响
|
||||
const danmus: PlacementRow[] = visibles
|
||||
.filter(item => item.mode === target.mode && item.top <= maxTop)
|
||||
.sort((prev, next) => prev.top - next.top)
|
||||
|
||||
// 如果没有同模式的弹幕,直接返回
|
||||
if (danmus.length === 0) {
|
||||
if (target.mode === 2) {
|
||||
return maxTop - target.height
|
||||
}
|
||||
else {
|
||||
return marginTop
|
||||
}
|
||||
}
|
||||
|
||||
// 上下各加一个虚拟弹幕,方便计算
|
||||
danmus.unshift({
|
||||
type: 'top',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: marginTop,
|
||||
width: clientWidth,
|
||||
speed: 0,
|
||||
distance: clientWidth,
|
||||
})
|
||||
|
||||
danmus.push({
|
||||
type: 'bottom',
|
||||
top: maxTop,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: marginBottom,
|
||||
width: clientWidth,
|
||||
speed: 0,
|
||||
distance: clientWidth,
|
||||
})
|
||||
|
||||
// 查找是否有多余的缝隙足以容纳当前弹幕
|
||||
if (target.mode === 2) {
|
||||
// 倒序查找
|
||||
for (let index = danmus.length - 2; index >= 0; index -= 1) {
|
||||
const item = danmus[index]!
|
||||
const prev = danmus[index + 1]!
|
||||
const itemBottom = item.top + item.height
|
||||
const diff = prev.top - itemBottom
|
||||
if (diff >= target.height) {
|
||||
return prev.top - target.height
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 顺序查找
|
||||
for (let index = 1; index < danmus.length; index += 1) {
|
||||
const item = danmus[index]!
|
||||
const prev = danmus[index - 1]!
|
||||
const prevBottom = prev.top + prev.height
|
||||
const diff = item.top - prevBottom
|
||||
if (diff >= target.height) {
|
||||
return prevBottom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const topMap: VisibleDanmu[][] = []
|
||||
for (let index = 1; index < danmus.length - 1; index += 1) {
|
||||
// The loop excludes the two virtual boundary rows inserted above.
|
||||
const item = danmus[index] as VisibleDanmu
|
||||
if (topMap.length) {
|
||||
const last = topMap[topMap.length - 1]!
|
||||
if (last[0]!.top === item.top) {
|
||||
last.push(item)
|
||||
}
|
||||
else {
|
||||
topMap.push([item])
|
||||
}
|
||||
}
|
||||
else {
|
||||
topMap.push([item])
|
||||
}
|
||||
}
|
||||
|
||||
if (antiOverlap) {
|
||||
switch (target.mode) {
|
||||
case 0: {
|
||||
const result = topMap.find((list) => {
|
||||
return list.every((danmu) => {
|
||||
if (clientWidth < danmu.distance)
|
||||
return false
|
||||
if (target.speed < danmu.speed)
|
||||
return true
|
||||
const overlapTime = danmu.right / (target.speed - danmu.speed)
|
||||
if (overlapTime > danmu.time)
|
||||
return true
|
||||
return false
|
||||
})
|
||||
})
|
||||
|
||||
return result && result[0] ? result[0].top : undefined
|
||||
}
|
||||
|
||||
// 静止弹幕没有重叠问题
|
||||
case 1:
|
||||
case 2:
|
||||
return undefined
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
else {
|
||||
switch (target.mode) {
|
||||
case 0:
|
||||
topMap.sort((prev, next) => {
|
||||
const nextMinRight = Math.min(...next.map(item => item.right))
|
||||
const prevMinRight = Math.min(...prev.map(item => item.right))
|
||||
return nextMinRight * next.length - prevMinRight * prev.length
|
||||
})
|
||||
break
|
||||
case 1:
|
||||
case 2:
|
||||
topMap.sort((prev, next) => {
|
||||
const nextMaxWidth = Math.max(...next.map(item => item.width))
|
||||
const prevMaxWidth = Math.max(...prev.map(item => item.width))
|
||||
return prevMaxWidth * prev.length - nextMaxWidth * next.length
|
||||
})
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return topMap[0]![0]!.top
|
||||
}
|
||||
}
|
||||
import type { PlacementMessage, PlacementReply } from './worker-types'
|
||||
import { getDanmuTop } from './placement'
|
||||
|
||||
onmessage = (event: MessageEvent<PlacementMessage>) => {
|
||||
const { data } = event
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,103 @@
|
||||
# PKG-DANMUKU-MASK-LOAD-01 真实模型负载下的弹幕调度
|
||||
|
||||
状态:本缺陷修复验收完成;完整组合/分发任务仍独立开放。起点
|
||||
475d38043e917b412018c6851d6a7464ef7bda67。范围为弹幕内部调度及组合验证;
|
||||
Mask生产实现、MediaPipe模型/SDK、视频尺寸、负载与等待预算不变。
|
||||
|
||||
## 缺陷与诊断
|
||||
|
||||
原始真实模型矩阵18项12通过6失败,保留在
|
||||
[原始记录](2026-09-14-PKG-DANMUKU-08-combined-load.md)及
|
||||
[原始证据](../baselines/danmuku-combined-load-validation.json)。
|
||||
5项候选120行未完整显示,另1项beta核心负载前行未显示;旧插件也漏显,
|
||||
不能据此声称是重构引入的回归。
|
||||
|
||||
增加只读pending/reserved行快照和显式Chromium CDP CPU诊断入口。
|
||||
单worker原实现诊断仍失败:120行均已采样,仅68行显示,reserved中62行,
|
||||
其中52行尚未显示,确认串行放置积压。CPU采样显示SDK结果转换中的
|
||||
createImageBitmap约5.57秒,二值mask转换约1.01秒,drawImage约0.89秒;
|
||||
自有调度约15毫秒。这是一次采样的自耗时,不是精确分阶段计时或总成本保证。
|
||||
调用栈指向MediaPipe转换image/segmentationMask的原有路径;没有替换SDK私有
|
||||
函数、禁用输出、降低模型/视频尺寸或使用假时钟让负载通过。
|
||||
|
||||
## 结构与兼容边界
|
||||
|
||||
- `placement.ts`从Worker中原样提取轨道计算。Worker保留旧消息协议;
|
||||
scheduler仅在`antiOverlap: false`时直接调用同一计算,消除逐行消息往返。
|
||||
默认防重叠模式和内部postMessage仍使用Worker,没有新增公共选项或依赖。
|
||||
- `sampling-window.ts`拥有连续媒体采样及当前document的visibility监听器。
|
||||
自动调度可补回上次已在wait池、尚未被采样而被连续播放跨过的行。
|
||||
不追加播放刚插入的过期行,不因跨窗口再次调用已采样的false回调。
|
||||
pause/seek/hide/reset等失效、原生seeking、页面隐藏、反向时间和document切换
|
||||
切断追赶窗口;销毁释放监听器及保留引用。
|
||||
- 公开`readys`仍是当前时间±0.1秒查询;公开wait/ready池不被采样模块写入。
|
||||
ready优先级、批次身份保留、beforeVisible串行与取消、可见事件顺序、
|
||||
显示寿命和已有DOM/CSS/类型/分发入口保持不变。
|
||||
|
||||
这是API-04/05/08/11/12边界内的缺陷修复,扩展了PKG-DANMUKU-12只处理
|
||||
异步等待期间采样的能力。连续播放CPU卡顿后可能出现旧版漏掉的行;
|
||||
它不承诺跨暂停、seek或后台时段重放,也不承诺无限吞吐量。
|
||||
架构、维护入口和资源归属同步更新于两个包的ARCHITECTURE.md。
|
||||
|
||||
## 验证记录
|
||||
|
||||
固定Node24.21.0、YarnClassic1.22.22;没有新增依赖。
|
||||
|
||||
- 可控旧红:首批6项4通过2失败,分别复现允许重叠模式Worker积压和CPU跨窗漏显。
|
||||
- 当前弹幕Node套件275/275通过,包含串行回调、生命周期重入、公开readys不变、
|
||||
不重放追加过期行、false回调不额外重试及visibility监听器释放。
|
||||
后续增加document迁移用例:旧窗口不追赶、旧监听器移除、新窗口恢复连续采样,
|
||||
最终source/main/legacy各276/276通过。直接内部用例仍执行源码,集成用例加载
|
||||
指定产物及其实际内嵌Worker,不把所有276项都称为产物执行。
|
||||
- 首次直接`yarn tsc`错误地进入根`.bin/tsc`所指向的TS5.1.6兼容别名,
|
||||
setting-lifecycle闭包收窄产生TS18048。按既有typechecking.md指定的
|
||||
`node node_modules/typescript/bin/tsc -p packages/artplayer-plugin-danmuku/tsconfig.json --noEmit`
|
||||
使用生产编译器TS5.9.3通过。未修改该源文件或放松strict;TS4.3/5.1的
|
||||
消费者兼容与生产源码编译器是不同要求。
|
||||
- 第一轮修复后真实模型矩阵18/18退出0:9候选完整交付、9冻结旧插件行为观察;
|
||||
Chromium/Firefox/WebKit、当前/5.4.0/5.3.1-beta.1核心覆盖。每个候选120行
|
||||
完整显示且回收,模型保持运行。
|
||||
|
||||
最终结果及实际输入指纹见[机器证据](../baselines/danmuku-mask-load-validation.json)。
|
||||
|
||||
| 验证 | 结果与范围 |
|
||||
| --- | --- |
|
||||
| 实际模型源码矩阵 | 18通过,9候选完整交付、9旧插件观察;216.28秒 |
|
||||
| 首次寿命/PiP/时钟回归 | 72通过12失败;12失败全部是候选允许重叠路径零Worker请求与旧测试前提冲突。48项PiP包含32原生播放和16API不可用记录 |
|
||||
| 扩展两种放置路径后的源码寿命/时钟 | 60通过;48寿命含24候选/24旧插件,12时钟含6候选/6旧插件;114.36秒 |
|
||||
| main候选 | 39通过:9模型、24寿命、6时钟;162.43秒 |
|
||||
| legacy候选 | 同范围39通过;160.03秒 |
|
||||
| Node source/main/legacy | 每轮276通过;内部纯函数仍是源码测试,集成测试加载对应产物 |
|
||||
| 类型与实际安装 | TS5.9.3严格源码通过;4项类型/实现赋值/编辑器测试通过;实际隔离包12组消费者通过,保留精确旧声明诊断对照 |
|
||||
| 构建/字节一致性 | 弹幕与Mask三格式构建通过;Mask产物无变化;原轨道函数逐字一致;隔离包79个成员仍匹配当前文件,三份docs/compiled与dist一致 |
|
||||
|
||||
源码/main/legacy的27个候选真实模型负载均显示120个唯一行,最终120行wait、
|
||||
无节点引用、pending/reserved均为空,模型继续输出。原来负载前漏显的beta核心
|
||||
路径同样通过。全部最终浏览器报告零重试、零跳过;API不可用是独立记录。
|
||||
浏览器为Windows Chromium153.0.8010.12、Firefox155.0、WebKit26.6。
|
||||
模型下帧间隔P95仍约79–275毫秒,故这里确认交付完整性,不宣称解决模型帧率
|
||||
或GPU/WASM性能。公开类型、模型输入、负载和原等待预算未改变。
|
||||
|
||||
另一次错误调用把`.mjs`传入只接受CommonJS/UMD的同步VM测试宿主,271项
|
||||
146通过125失败,语法解析报Unexpected token export,插件尚未执行。
|
||||
保留该日志;不通过改写ESM字节伪装原生模块执行。test/README.md已明确入口范围。
|
||||
真实ESM导入由仓库外打包消费验证,完整ESM浏览器分发仍属PKG-DANMUKU-09。
|
||||
|
||||
原始日志、CPU profile和各轮浏览器报告归档在refactor/.cache/danmuku-mask-load-*。
|
||||
首次寿命回归仍假设允许重叠模式必经Worker,该前提已被本修复取消;已保留
|
||||
原失败并扩展两种模式。防重叠候选仍要求一对真实Worker请求/回复、至少1400ms
|
||||
受控延迟及至少900ms显示;允许重叠候选要求零请求/回复和至少900ms显示。
|
||||
旧插件两种模式都仍复现被等待吃掉寿命的缺陷。原寿命断言没有降低。
|
||||
CPU卡顿用例新增候选全部三行按序显示的断言,旧插件CPU漏显继续独立观察。
|
||||
|
||||
## 重跑与回退
|
||||
|
||||
实际模型:`yarn test:browser:source test/browser/danmuku-mask-native.spec.js
|
||||
--grep 'actual model and Danmuku' --workers=2`。main/legacy分别指定存在的
|
||||
ARTPLAYER_DANMUKU_ARTIFACT和ARTPLAYER_MASK_ARTIFACT文件;报告包含实际输入哈希。
|
||||
诊断仅Chromium单worker增加ARTPLAYER_MASK_PROFILE=1;正式验收不打开profile。
|
||||
每轮终止后先归档报告,再运行下一轮,不能混用源与产物证据。
|
||||
|
||||
回退本任务的独立提交会恢复原Worker逐行路径及点采样,并重新暴露记录中的漏显。
|
||||
没有公共类型或数据迁移。完整组合任务08/Mask05、设备、其他包和发布复盘仍独立
|
||||
验收;本修复不授权push、部署或npm发布。
|
||||
+3
-3
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 262 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 49 / doing 21 / blocked 0 / done 192 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 48 / doing 21 / blocked 0 / done 193 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -467,7 +467,7 @@
|
||||
|
||||
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| PKG-DANMUKU-MASK-LOAD-01 | artplayer-plugin-danmuku, artplayer-plugin-danmuku-mask<br>修复真实Mask组合下的弹幕未采样与放置积压 | PKG-DANMUKU-12, PKG-MASK-04, CORE-22 | 分阶段性能/采样诊断、兼容实现修复及新旧核心/产物回归 | 关闭真实模型组合5项密集负载失败和1项负载前漏显,保持回调顺序、显示寿命与生命周期取消;不能以降低负载或关闭模型制造通过 | H | todo |
|
||||
| PKG-DANMUKU-MASK-LOAD-01 | artplayer-plugin-danmuku, artplayer-plugin-danmuku-mask<br>修复真实Mask组合下的弹幕未采样与放置积压 | PKG-DANMUKU-12, PKG-MASK-04, CORE-22 | 分阶段性能/采样诊断、兼容实现修复及新旧核心/产物回归 | 关闭真实模型组合5项密集负载失败和1项负载前漏显,保持回调顺序、显示寿命与生命周期取消;不能以降低负载或关闭模型制造通过 | H | done |
|
||||
|
||||
## 完成证据与阻塞
|
||||
|
||||
@@ -684,4 +684,4 @@
|
||||
- CI-BROWSER-01: [记录](changes/2026-09-14-CI-BROWSER-01-scopes.md) [记录](baselines/browser-scope-validation.json)
|
||||
- PKG-CHAPTER-HOVER-01: [记录](changes/2026-09-14-PKG-CHAPTER-HOVER-01-geometry.md) [记录](baselines/chapter-hover-validation.json)
|
||||
- PKG-CANVAS-SUBTITLE-01: [记录](changes/2026-09-14-PKG-CANVAS-SUBTITLE-01-track.md) [记录](baselines/canvas-subtitles-validation.json)
|
||||
- PKG-DANMUKU-MASK-LOAD-01: [记录](changes/2026-09-14-PKG-DANMUKU-08-combined-load.md) [记录](baselines/danmuku-combined-load-validation.json)
|
||||
- PKG-DANMUKU-MASK-LOAD-01: [记录](changes/2026-09-14-PKG-DANMUKU-08-combined-load.md) [记录](baselines/danmuku-combined-load-validation.json) [记录](changes/2026-09-14-PKG-DANMUKU-MASK-LOAD-01-scheduling.md) [记录](baselines/danmuku-mask-load-validation.json)
|
||||
@@ -1,5 +1,29 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-DANMUKU-MASK-LOAD-01 真实模型负载漏显与积压修复
|
||||
|
||||
连续播放采样补回主线程卡顿跨过的既有wait行,公开readys窗口不变;允许重叠
|
||||
模式直接使用原轨道计算,默认防重叠仍走Worker。采样/纯几何/消息协议拆分,
|
||||
保留串行回调、取消、显示寿命;document迁移及destroy释放visibility监听器。
|
||||
没有更换Mask模型/SDK、缩小视频、降低20条每秒负载或扩大原等待预算。
|
||||
|
||||
模型源码18通过(9候选完整/9旧观察),main/legacy各39通过(9模型、24寿命、
|
||||
6时钟):27个候选模型用例均120行唯一显示/回收,pending/reserved为空。
|
||||
寿命测试补齐防重叠/允许重叠两条路径,源码60通过;原生PiP矩阵48项中的
|
||||
32原生播放与16API不可用保持区分。第一次寿命12失败来自旧测试强制Worker
|
||||
假设,完整报告保留;扩展用例继续验证实际延迟路径,没有降低寿命要求。
|
||||
|
||||
Node source/main/legacy各276通过,严格TS5.9.3、4项类型/编辑器测试、实际
|
||||
隔离安装12组通过。误用TS5.1.6别名及把ESM交给同步VM的失败单独记录,
|
||||
不归为产品缺陷或ESM行为通过。构建及79个最终打包成员一致性已核验。
|
||||
见[变更](changes/2026-09-14-PKG-DANMUKU-MASK-LOAD-01-scheduling.md)及
|
||||
[证据](baselines/danmuku-mask-load-validation.json)。模型帧间隔仍高,未宣称帧率提升。
|
||||
|
||||
本缺陷任务及风险关闭;262项:193 done、21 doing、48 todo。弹幕08/09、
|
||||
Mask05/06及其他包/设备/远端CI/发布复盘继续开放;ESM完整浏览器分发仍属09。
|
||||
本项按独立本地提交交付并核验审计,未push/发布。下一步继续包组合与分发收尾。
|
||||
|
||||
|
||||
## PKG-DANMUKU-08 旧插件组合与真实负载:新增必须修复项
|
||||
|
||||
扩展PiP/Mask测试加载校验后的npm弹幕5.3.0及候选源码。普通PiP矩阵36项退出0:
|
||||
|
||||
@@ -275,4 +275,4 @@
|
||||
| CANVAS-SUBTITLE-01 | resolved / 已复现 | Canvas底层video缺初始字幕轨道且普通Canvas挂载不能加载原生字幕 | PKG-CANVAS-SUBTITLE-01, PKG-CANVAS-05 |
|
||||
| DPIP-INSTALLED-PROVENANCE-01 | resolved / 已复现 | Document PiP测试加载器忽略显式安装map并回退到源码 | PKG-CANVAS-05, CI-01 |
|
||||
| DASH-MENU-01 | resolved / 已复现 | SDK 5.2.1 播放中设置质量选项在点击时变为不可见 | PKG-DASH-MENU-01, PKG-DASH-05, REVIEW-02 |
|
||||
| DANMUKU-MASK-LOAD-01 | open / 已复现 | 真实Mask模型运行时弹幕存在未采样窗口及串行放置积压 | PKG-DANMUKU-MASK-LOAD-01, PKG-DANMUKU-08, PKG-MASK-05 |
|
||||
| DANMUKU-MASK-LOAD-01 | resolved / 已复现 | 真实Mask模型运行时弹幕存在未采样窗口及串行放置积压 | PKG-DANMUKU-MASK-LOAD-01, PKG-DANMUKU-08, PKG-MASK-05 |
|
||||
+11
-2
@@ -6299,7 +6299,7 @@
|
||||
"id": "DANMUKU-MASK-LOAD-01",
|
||||
"title": "真实Mask模型运行时弹幕存在未采样窗口及串行放置积压",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-DANMUKU-MASK-LOAD-01",
|
||||
"PKG-DANMUKU-08",
|
||||
@@ -6313,7 +6313,16 @@
|
||||
],
|
||||
"compatibleResolution": "分析真实模型/绘制/Worker往返成本和未采样、待放置行,保持公共readys与回调顺序、显示寿命及生命周期取消;用独立回归验证兼容修复。",
|
||||
"closureCriteria": "原始失败保留并有可解释修复;真实模型下120行完整交付、无活动引用积压,新旧核心和main/legacy以及暂停/seek/hide/destroy回归通过;不降低负载、停模型或扩大等待伪造结果。",
|
||||
"workspaceState": "源码18项12通过6失败:5项候选密集负载不完整,1项Chromium beta核心负载前行未显示。旧插件同环境也漏显,尚未证明是重构回归或定位单一瓶颈。Firefox候选和原生PiP负载通过。"
|
||||
"workspaceState": "连续播放采样窗口补回CPU跨窗行;允许重叠放置直接复用原轨道函数,防重叠和内部消息API仍使用Worker。真实模型源码18、main39、legacy39通过;其中27个候选模型负载全部120行完整交付/回收,pending/reserved为空。",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-14-PKG-DANMUKU-MASK-LOAD-01-scheduling.md",
|
||||
"refactor/baselines/danmuku-mask-load-validation.json",
|
||||
"test/danmuku-scheduler.test.js",
|
||||
"test/browser/danmuku-mask-native.spec.js",
|
||||
"test/browser/danmuku-lifetime.spec.js",
|
||||
"test/browser/danmuku-timing-diagnostic.spec.js"
|
||||
],
|
||||
"resolutionRationale": "原5项负载漏显和1项负载前行失败保留;实际模型/SDK/视频和20条每秒输入、等待预算不变。源与两产物共27项模型负载完整交付;两种放置路径寿命、CPU/异步间隙、新旧核心及取消/文档迁移回归通过。Node source/main/legacy各276通过,源码类型、编辑器/消费者及隔离包12组通过。首次旧Worker假设失败和错误ESM宿主调用单独记录;不冒充ESM完整浏览器验收、帧率提升或全部包发布通过。"
|
||||
}
|
||||
]
|
||||
}
|
||||
+4
-2
@@ -5483,13 +5483,15 @@
|
||||
"PKG-MASK-04",
|
||||
"CORE-22"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "分阶段性能/采样诊断、兼容实现修复及新旧核心/产物回归",
|
||||
"acceptance": "关闭真实模型组合5项密集负载失败和1项负载前漏显,保持回调顺序、显示寿命与生命周期取消;不能以降低负载或关闭模型制造通过",
|
||||
"evidence": [
|
||||
"changes/2026-09-14-PKG-DANMUKU-08-combined-load.md",
|
||||
"baselines/danmuku-combined-load-validation.json"
|
||||
"baselines/danmuku-combined-load-validation.json",
|
||||
"changes/2026-09-14-PKG-DANMUKU-MASK-LOAD-01-scheduling.md",
|
||||
"baselines/danmuku-mask-load-validation.json"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
+5
-1
@@ -183,7 +183,11 @@ Document PiP: `yarn test:dpip` runs 48 historical window/DOM/lifecycle cases. Na
|
||||
|
||||
Danmuku: `yarn test:danmuku` covers historical behavior and current input, scheduling,
|
||||
settings, rendering and heatmap boundaries. `ARTPLAYER_DANMUKU_ARTIFACT` selects a
|
||||
built candidate for integration cases; direct internal tests remain source tests.
|
||||
built main/legacy candidate for integration cases; direct internal tests remain
|
||||
source tests. This controlled synchronous VM loader executes CommonJS/UMD scripts,
|
||||
not `.mjs` modules. Do not pass ESM bytes to that override or interpret its parser
|
||||
failure as a production ESM failure. Actual ESM imports are checked by the isolated
|
||||
package consumer; full native distribution coverage remains the package release gate.
|
||||
The candidate Worker helper executes the selected artifact's Blob or data URL bytes;
|
||||
only source-mode controlled imports compile `worker.ts` separately. The frozen
|
||||
published helper remains unchanged. `yarn test:danmuku-types` checks the unchanged
|
||||
|
||||
@@ -253,7 +253,14 @@ See refactor/changes/2026-09-14-PKG-MASK-05-native-checkpoint.md for exact limit
|
||||
Its combination cases now include frozen/current Danmuku and a bounded 120-row
|
||||
load while the actual model continues. `danmuku-combination-load.js` observes
|
||||
real media/RAF and cleans up its test listeners; it does not seed eligible rows.
|
||||
Candidate completeness failures remain failing under PKG-DANMUKU-MASK-LOAD-01.
|
||||
The candidate must deliver all 120 rows exactly once and recycle their references;
|
||||
original failures and the PKG-DANMUKU-MASK-LOAD-01 repair remain separate evidence.
|
||||
For a Chromium diagnostic only, set `ARTPLAYER_MASK_PROFILE=1` to attach the actual
|
||||
CDP CPU profile around the load. This never stops inference or patches the SDK.
|
||||
Ordinary format acceptance runs without profiling. Final load snapshots also
|
||||
record pending/reserved row IDs to distinguish missed sampling from placement
|
||||
backlog. Use `ARTPLAYER_DANMUKU_ARTIFACT` with `ARTPLAYER_MASK_ARTIFACT` to bind both
|
||||
candidate plugins to the exact main/legacy files being tested.
|
||||
`danmuku-dpip.spec.js` also includes two-window load cases and both plugin versions.
|
||||
Frozen-plugin retained DOM, candidate cleanup and unsupported API outcomes remain
|
||||
distinct; the old plugin's observed loss is not complete-delivery acceptance.
|
||||
|
||||
@@ -19,8 +19,8 @@ test.beforeAll(async () => {
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
for (const implementation of ['published', 'candidate']) {
|
||||
for (const mode of [0, 1]) {
|
||||
test(`${core} core / ${implementation} plugin / mode ${mode}: Worker waiting does not consume visible lifetime`, async ({ page }, testInfo) => {
|
||||
for (const [mode, antiOverlap] of [0, 1].flatMap(mode => [true, false].map(antiOverlap => [mode, antiOverlap]))) {
|
||||
test(`${core} core / ${implementation} plugin / mode ${mode} / antiOverlap ${antiOverlap}: placement preserves visible lifetime`, async ({ page }, testInfo) => {
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.evaluate(() => {
|
||||
const evidence = window.lifetimeEvidence = { posts: [], replies: [], visible: [], recycled: [] }
|
||||
@@ -50,11 +50,11 @@ for (const core of ['published', 'candidate']) {
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
||||
// Install after native playing, then start once. The published duplicate
|
||||
// play/playing loop defect is covered elsewhere and is not this timing case.
|
||||
await page.evaluate(async (mode) => {
|
||||
await page.evaluate(async ({ mode, antiOverlap }) => {
|
||||
const art = window.art
|
||||
const evidence = window.lifetimeEvidence
|
||||
art.on('artplayerPluginDanmuku:visible', row => evidence.visible.push({ wall: performance.now(), clock: Date.now(), startedAt: row.$lastStartTime, rest: row.$restTime, mode: row.mode, time: art.currentTime, transition: row.$ref.style.transition }))
|
||||
art.plugins.add(window.artplayerPluginDanmuku({ danmuku: [], speed: 1, antiOverlap: false, heatmap: false, emitter: false }))
|
||||
art.plugins.add(window.artplayerPluginDanmuku({ danmuku: [], speed: 1, antiOverlap, heatmap: false, emitter: false }))
|
||||
const plugin = art.plugins.artplayerPluginDanmuku
|
||||
const owner = plugin.config({})
|
||||
const makeWait = owner.makeWait
|
||||
@@ -65,16 +65,18 @@ for (const core of ['published', 'candidate']) {
|
||||
}
|
||||
await plugin.load([{ id: 'delayed', text: 'Full visible lifetime', time: art.currentTime + 0.4, mode }])
|
||||
owner.start()
|
||||
}, mode)
|
||||
}, { mode, antiOverlap })
|
||||
await expect.poll(() => page.evaluate(() => window.lifetimeEvidence.recycled.length)).toBe(1)
|
||||
await page.click('#pause')
|
||||
const evidence = await page.evaluate(() => ({ ...window.lifetimeEvidence, video: { width: window.art.video.videoWidth, time: window.art.currentTime } }))
|
||||
await testInfo.attach('danmuku-lifetime', { contentType: 'application/json', body: JSON.stringify({ core, implementation, mode, sha256: hash(code), evidence, scope: 'Native video and real Worker geometry with controlled 1500ms request delay; does not assert naturally occurring Worker starvation.' }) })
|
||||
expect(evidence.posts).toHaveLength(1)
|
||||
expect(evidence.replies).toHaveLength(1)
|
||||
const workerPlacement = implementation === 'published' || antiOverlap
|
||||
await testInfo.attach('danmuku-lifetime', { contentType: 'application/json', body: JSON.stringify({ core, implementation, mode, antiOverlap, workerPlacement, sha256: hash(code), evidence, scope: 'Native video; Worker path has a controlled 1500ms request delay, candidate relaxed path must bypass it. Both candidate paths require full visible lifetime. Does not assert naturally occurring Worker starvation.' }) })
|
||||
expect(evidence.posts).toHaveLength(workerPlacement ? 1 : 0)
|
||||
expect(evidence.replies).toHaveLength(workerPlacement ? 1 : 0)
|
||||
expect(evidence.visible).toHaveLength(1)
|
||||
expect(evidence.video.width).toBeGreaterThan(0)
|
||||
expect(evidence.replies[0].wall - evidence.posts[0].wall).toBeGreaterThanOrEqual(1400)
|
||||
if (workerPlacement)
|
||||
expect(evidence.replies[0].wall - evidence.posts[0].wall).toBeGreaterThanOrEqual(1400)
|
||||
const lifetime = evidence.recycled[0].wall - evidence.visible[0].wall
|
||||
if (implementation === 'published') {
|
||||
// Preserve the historical defect as explicit evidence, not the candidate contract.
|
||||
|
||||
@@ -265,7 +265,23 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
expect(restored.mode).toBe('alpha')
|
||||
await testInfo.attach('actual-combination-page', { body: await page.screenshot(), contentType: 'image/png' })
|
||||
const outputsBeforeLoad = await page.evaluate(() => window.maskCombination.times.length)
|
||||
load = await observeDanmukuLoad(page, { label: 'Mask load', requireComplete: implementation === 'candidate' })
|
||||
let profiler
|
||||
if (process.env.ARTPLAYER_MASK_PROFILE === '1') {
|
||||
assert.equal(testInfo.project.name, 'chromium', 'CDP profiling requires the Chromium diagnostic subset')
|
||||
profiler = await page.context().newCDPSession(page)
|
||||
await profiler.send('Profiler.enable')
|
||||
await profiler.send('Profiler.start')
|
||||
}
|
||||
try {
|
||||
load = await observeDanmukuLoad(page, { label: 'Mask load', requireComplete: implementation === 'candidate' })
|
||||
}
|
||||
finally {
|
||||
if (profiler) {
|
||||
const { profile } = await profiler.send('Profiler.stop')
|
||||
await testInfo.attach('mask-cpu-profile', { contentType: 'application/json', body: JSON.stringify(profile) })
|
||||
await profiler.detach()
|
||||
}
|
||||
}
|
||||
load.modelOutputs = await page.evaluate(before => window.maskCombination.times.length - before, outputsBeforeLoad)
|
||||
expect(load.modelOutputs).toBeGreaterThan(1)
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
|
||||
@@ -93,7 +93,7 @@ for (const implementation of ['published', 'candidate']) {
|
||||
const visible = new Set(evidence.visible.map(row => row.id))
|
||||
return { ...evidence, rows, missing: rows.filter(row => !visible.has(row.id)).map(row => row.id), media: { time: window.art.currentTime, width: window.art.video.videoWidth } }
|
||||
})
|
||||
await testInfo.attach('danmuku-timing-observation', { contentType: 'application/json', body: JSON.stringify({ implementation, scenario, sha256: hash(code), observation, scope: 'Diagnostic only: native timing with controlled load, original readys getter and unmodified queue states. Missing rows are reported, not waived as stability acceptance.' }) })
|
||||
await testInfo.attach('danmuku-timing-observation', { contentType: 'application/json', body: JSON.stringify({ implementation, scenario, sha256: hash(code), observation, scope: 'Native timing with controlled load, original readys getter and unmodified queue states. Candidate must recover both CPU and asynchronous gaps; published CPU loss is observed separately. This is not sustained-load acceptance.' }) })
|
||||
expect(observation.media.width).toBeGreaterThan(0)
|
||||
expect(observation.rows).toHaveLength(3)
|
||||
expect(observation.frames.length).toBeGreaterThan(2)
|
||||
@@ -111,6 +111,10 @@ for (const implementation of ['published', 'candidate']) {
|
||||
expect(observation.callbacks[index].start).toBeGreaterThanOrEqual(observation.callbacks[index - 1].end)
|
||||
}
|
||||
}
|
||||
if (implementation === 'candidate') {
|
||||
expect(observation.missing).toEqual([])
|
||||
expect(observation.visible.map(row => row.id)).toEqual(['first', 'middle', 'sentinel'])
|
||||
}
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -78,6 +78,149 @@ function geometry(mode = 1) {
|
||||
return { type: 'getDanmuTop', target: { mode, height: 20, speed: 148 }, visibles: [], antiOverlap: true, clientWidth: 640, clientHeight: 360, marginTop: 10, marginBottom: 90 }
|
||||
}
|
||||
|
||||
test(`${prefix}: non-overlap-disabled placement does not queue a Worker round trip and keeps callbacks serial`, async (t) => {
|
||||
const gate = deferred()
|
||||
const calls = []
|
||||
const { env, plugin, owner, worker } = await fixture({ antiOverlap: false, beforeVisible(row) {
|
||||
calls.push(row.text)
|
||||
return row.text === 'first' ? gate.promise : true
|
||||
} })
|
||||
t.after(() => {
|
||||
env.destroy()
|
||||
gate.resolve(false)
|
||||
})
|
||||
await plugin.load([{ text: 'first', time: 10, mode: 1 }, { text: 'second', time: 10, mode: 1 }])
|
||||
env.art.playing = true
|
||||
owner.start()
|
||||
env.frame()
|
||||
await env.flush()
|
||||
assert.deepEqual(calls, ['first'])
|
||||
gate.resolve(true)
|
||||
await env.flush()
|
||||
assert.deepEqual(calls, ['first', 'second'])
|
||||
assert.deepEqual(output(env, 'visible').map(event => event.args[0].text), ['first', 'second'])
|
||||
assert.equal(worker.messages.length, 0)
|
||||
assert.deepEqual([...owner.queue].map(row => row.$ref.offsetTop), [owner.marginTop, owner.marginTop + 20])
|
||||
})
|
||||
|
||||
for (const boundary of ['continuous', 'seek', 'stop', 'hide', 'visibility']) {
|
||||
test(`${prefix}: CPU gap recovery respects ${boundary} boundaries and keeps public readys unchanged`, async (t) => {
|
||||
const { env, plugin, owner } = await fixture({}, { automatic: true })
|
||||
t.after(() => env.destroy())
|
||||
const listeners = new Set()
|
||||
const doc = {
|
||||
visibilityState: 'visible',
|
||||
addEventListener(name, fn) {
|
||||
assert.equal(name, 'visibilitychange')
|
||||
listeners.add(fn)
|
||||
},
|
||||
removeEventListener(name, fn) {
|
||||
assert.equal(name, 'visibilitychange')
|
||||
listeners.delete(fn)
|
||||
},
|
||||
}
|
||||
owner.$player.ownerDocument = doc
|
||||
await plugin.load([{ text: 'gap', time: 10.4, mode: 1 }])
|
||||
env.art.playing = true
|
||||
owner.start()
|
||||
env.frame()
|
||||
await env.flush()
|
||||
env.tick(800)
|
||||
env.art.currentTime = 10.8
|
||||
if (boundary === 'seek')
|
||||
env.art.emit('video:seeking')
|
||||
if (boundary === 'stop') {
|
||||
owner.stop()
|
||||
owner.start()
|
||||
}
|
||||
if (boundary === 'hide') {
|
||||
owner.hide()
|
||||
owner.show()
|
||||
}
|
||||
if (boundary === 'visibility') {
|
||||
for (const listener of listeners) listener()
|
||||
}
|
||||
assert.equal(owner.readys.length, 0)
|
||||
env.frame()
|
||||
await env.flush()
|
||||
assert.deepEqual(output(env, 'visible').map(event => event.args[0].text), boundary === 'continuous' ? ['gap'] : [])
|
||||
env.destroy()
|
||||
assert.equal(listeners.size, 0)
|
||||
})
|
||||
}
|
||||
|
||||
test(`${prefix}: document adoption drops the old catch-up window and transfers visibility ownership`, async (t) => {
|
||||
const { env, plugin, owner } = await fixture({}, { automatic: true })
|
||||
t.after(() => env.destroy())
|
||||
function document() {
|
||||
const listeners = new Set()
|
||||
return {
|
||||
visibilityState: 'visible',
|
||||
listeners,
|
||||
addEventListener: (_name, fn) => listeners.add(fn),
|
||||
removeEventListener: (_name, fn) => listeners.delete(fn),
|
||||
}
|
||||
}
|
||||
const original = document()
|
||||
const adopted = document()
|
||||
owner.$player.ownerDocument = original
|
||||
await plugin.load([{ text: 'before adoption', time: 10.4, mode: 1 }, { text: 'after adoption', time: 11.2, mode: 1 }])
|
||||
env.art.playing = true
|
||||
owner.start()
|
||||
await env.frame()
|
||||
assert.equal(original.listeners.size, 1)
|
||||
owner.$player.ownerDocument = adopted
|
||||
env.tick(800)
|
||||
env.art.currentTime = 10.8
|
||||
await env.frame()
|
||||
assert.equal(original.listeners.size, 0)
|
||||
assert.equal(adopted.listeners.size, 1)
|
||||
assert.equal(output(env, 'visible').length, 0, 'Do not replay rows crossed while adopting another document')
|
||||
env.tick(800)
|
||||
env.art.currentTime = 11.6
|
||||
await env.frame()
|
||||
assert.deepEqual(output(env, 'visible').map(event => event.args[0].text), ['after adoption'])
|
||||
env.destroy()
|
||||
assert.equal(adopted.listeners.size, 0)
|
||||
})
|
||||
|
||||
test(`${prefix}: CPU gap recovery ignores newly appended past rows and does not retry an already sampled false callback`, async (t) => {
|
||||
const calls = []
|
||||
const { env, plugin, owner } = await fixture({ beforeVisible(row) {
|
||||
calls.push(row.text)
|
||||
return row.text !== 'declined'
|
||||
} }, { automatic: true })
|
||||
t.after(() => env.destroy())
|
||||
await plugin.load([{ text: 'declined', time: 10, mode: 1 }, { text: 'gap', time: 10.4, mode: 1 }])
|
||||
env.art.playing = true
|
||||
owner.start()
|
||||
env.frame()
|
||||
await env.flush()
|
||||
env.tick(800)
|
||||
env.art.currentTime = 10.8
|
||||
await plugin.load([{ text: 'appended-past', time: 10.3, mode: 1 }])
|
||||
env.frame()
|
||||
await env.flush()
|
||||
assert.deepEqual(calls, ['declined', 'gap'])
|
||||
assert.deepEqual(output(env, 'visible').map(event => event.args[0].text), ['gap'])
|
||||
})
|
||||
|
||||
for (const boundary of ['stop', 'hide', 'reset', 'destroy']) {
|
||||
test(`${prefix}: inline relaxed placement honors ${boundary} from the first visible callback`, async (t) => {
|
||||
const { env, plugin, owner, worker } = await fixture({ antiOverlap: false })
|
||||
t.after(() => env.destroy())
|
||||
await plugin.load([{ text: 'first', time: 10, mode: 1 }, { text: 'cancelled', time: 10, mode: 1 }])
|
||||
env.art.on('artplayerPluginDanmuku:visible', () => owner[boundary]())
|
||||
env.art.playing = true
|
||||
owner.start()
|
||||
env.frame()
|
||||
await env.flush()
|
||||
assert.deepEqual(output(env, 'visible').map(event => event.args[0].text), ['first'])
|
||||
assert.equal(worker.messages.length, 0)
|
||||
assert.equal(owner.queue[1].$ref, null)
|
||||
})
|
||||
}
|
||||
|
||||
test(`${prefix}: repeated play/playing and direct starts preserve events and internal return with only one queued frame`, async () => {
|
||||
const { env, owner } = await fixture()
|
||||
env.art.playing = true
|
||||
|
||||
@@ -36,7 +36,8 @@ export async function observeDanmukuLoad(page, { label, requireComplete, rate =
|
||||
art.off('artplayerPluginDanmuku:visible', visible)
|
||||
art.off('artplayerPluginDanmuku:error', error)
|
||||
const loaded = owner.queue.filter(row => ids.has(row.id))
|
||||
const result = { ...state, endWall: host.performance.now(), time: art.currentTime, loaded: loaded.length, waiting: loaded.filter(row => row.$state === 'wait').length, referenced: loaded.filter(row => row.$ref).length, nodes: art.template.$danmuku.children.length, pooled: owner.$refs.length, uniquePool: new Set(owner.$refs).size, decodedWidth: art.video.videoWidth }
|
||||
const buffered = { pending: [...owner.scheduler?.buffer.pending || []].filter(row => ids.has(row.id)).map(row => row.id), reserved: [...owner.scheduler?.buffer.reserved || []].filter(row => ids.has(row.id)).map(row => row.id) }
|
||||
const result = { ...state, endWall: host.performance.now(), time: art.currentTime, loaded: loaded.length, waiting: loaded.filter(row => row.$state === 'wait').length, referenced: loaded.filter(row => row.$ref).length, nodes: art.template.$danmuku.children.length, pooled: owner.$refs.length, uniquePool: new Set(owner.$refs).size, decodedWidth: art.video.videoWidth, buffered }
|
||||
window.danmukuCombinationLoadResult = result
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user