fix(danmuku): [PKG-DANMUKU-MASK-LOAD-01] recover stalled-frame rows and remove relaxed placement backlog

This commit is contained in:
Harvey Zhao committed 2026-09-14 22:13:08 +08:00
1 parent 475d38043e
commit 314a1c3163
27 files changed
+14045 -195

No files matched your search

@@ -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("&quot;",'"').replaceAll("&apos;","'").replaceAll("&lt;","<").replaceAll("&gt;",">").replaceAll("&amp;","&"),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("&quot;",'"').replaceAll("&apos;","'").replaceAll("&lt;","<").replaceAll("&gt;",">").replaceAll("&amp;","&"),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("&quot;",'"').replaceAll("&apos;","'").replaceAll("&lt;","<").replaceAll("&gt;",">").replaceAll("&amp;","&"),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("&quot;",'"').replaceAll("&apos;","'").replaceAll("&lt;","<").replaceAll("&gt;",">").replaceAll("&amp;","&"),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()
}
}
+2 -139
View File
@@ -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