fix(danmuku): [PKG-DANMUKU-START-01] retain the initial playback sampling interval

This commit is contained in:
Harvey Zhao committed 2026-09-16 00:14:48 +08:00
1 parent b5f8b5e28e
commit 267b21226a
19 files changed
+9004 -26

No files matched your search

+1 -1
View File
@@ -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,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
!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,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
!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
+16 -1
View File
@@ -652,6 +652,10 @@ class SamplingWindow {
this.waiting = /* @__PURE__ */ new Set();
this.visibility = () => this.clear();
}
begin(owner) {
if (this.time === void 0)
this.capture(owner, [], owner.art.currentTime);
}
capture(owner, rows, time) {
const doc = owner.$player.ownerDocument;
if (doc !== this.document) {
@@ -789,7 +793,18 @@ class Scheduler {
}
schedule() {
const owner = this.owner;
if (!this.running || this.closed || this.fault || owner.art.isDestroy || owner.isStop || this.frame !== null)
if (!this.running || this.closed || this.fault || owner.art.isDestroy || owner.isStop)
return;
try {
const video = owner.art.video;
const started = owner.art.playing || video?.paused === false && !video.ended && video.readyState > 2;
if (started && !owner.isHide)
this.sampling.begin(owner);
} catch (error) {
this.fail(error);
return;
}
if (this.frame !== null)
return;
this.frame = window.requestAnimationFrame(() => {
this.frame = null;
@@ -648,6 +648,10 @@ var artplayerPluginDanmuku = (function() {
this.waiting = /* @__PURE__ */ new Set();
this.visibility = () => this.clear();
}
begin(owner) {
if (this.time === void 0)
this.capture(owner, [], owner.art.currentTime);
}
capture(owner, rows, time) {
const doc = owner.$player.ownerDocument;
if (doc !== this.document) {
@@ -785,7 +789,18 @@ var artplayerPluginDanmuku = (function() {
}
schedule() {
const owner = this.owner;
if (!this.running || this.closed || this.fault || owner.art.isDestroy || owner.isStop || this.frame !== null)
if (!this.running || this.closed || this.fault || owner.art.isDestroy || owner.isStop)
return;
try {
const video = owner.art.video;
const started = owner.art.playing || video?.paused === false && !video.ended && video.readyState > 2;
if (started && !owner.isHide)
this.sampling.begin(owner);
} catch (error) {
this.fail(error);
return;
}
if (this.frame !== null)
return;
this.frame = window.requestAnimationFrame(() => {
this.frame = null;
@@ -196,7 +196,16 @@ existing track algorithm and geometry fields until separately reviewed.
RAF sampling and visible lifetime maintenance continue while `beforeVisible` or
the Worker is pending. Public `readys` remains the instantaneous +/- 0.1 second
query. The sampling window also recovers rows crossed between consecutive
query. A playing, visible scheduler seeds its first observation before requesting
RAF, so a delayed first callback cannot discard the initial playback interval.
This records time and existing waiting identities only; it never calls the public
readys getter or dispatches beforeVisible/visible outside RAF. Repeated play/playing
or start calls retain that anchor until a lifecycle boundary clears it. Native
unpaused, non-ended media with current data can seed time zero even though the
unchanged public playing getter requires currentTime > 0. Dispatch still waits
for the existing playing condition; paused, hidden and seeking periods do not
become catch-up intervals.
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.
@@ -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,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
!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,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
!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
@@ -652,6 +652,10 @@ class SamplingWindow {
this.waiting = /* @__PURE__ */ new Set();
this.visibility = () => this.clear();
}
begin(owner) {
if (this.time === void 0)
this.capture(owner, [], owner.art.currentTime);
}
capture(owner, rows, time) {
const doc = owner.$player.ownerDocument;
if (doc !== this.document) {
@@ -789,7 +793,18 @@ class Scheduler {
}
schedule() {
const owner = this.owner;
if (!this.running || this.closed || this.fault || owner.art.isDestroy || owner.isStop || this.frame !== null)
if (!this.running || this.closed || this.fault || owner.art.isDestroy || owner.isStop)
return;
try {
const video = owner.art.video;
const started = owner.art.playing || video?.paused === false && !video.ended && video.readyState > 2;
if (started && !owner.isHide)
this.sampling.begin(owner);
} catch (error) {
this.fail(error);
return;
}
if (this.frame !== null)
return;
this.frame = window.requestAnimationFrame(() => {
this.frame = null;
@@ -9,6 +9,11 @@ export default class SamplingWindow {
private document: Document | undefined
private visibility = () => this.clear()
begin(owner: Danmuku) {
if (this.time === undefined)
this.capture(owner, [], owner.art.currentTime)
}
capture(owner: Danmuku, rows: DanmuItem[], time: number) {
const doc = owner.$player.ownerDocument
if (doc !== this.document) {
@@ -105,7 +105,20 @@ export default class Scheduler {
schedule() {
const owner = this.owner
if (!this.running || this.closed || this.fault || owner.art.isDestroy || owner.isStop || this.frame !== null)
if (!this.running || this.closed || this.fault || owner.art.isDestroy || owner.isStop)
return
try {
// The public playing getter requires currentTime > 0; native play can start at zero.
const video = owner.art.video
const started = owner.art.playing || (video?.paused === false && !video.ended && video.readyState > 2)
if (started && !owner.isHide)
this.sampling.begin(owner)
}
catch (error) {
this.fail(error)
return
}
if (this.frame !== null)
return
this.frame = window.requestAnimationFrame(() => {
this.frame = null
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,67 @@
# PKG-DANMUKU-START-01 首帧之前的采样区间
状态:本缺陷修复完成。起点 b5f8b5e28e33cfb44e7aa93089b6dfbf247142f0。
关闭 DANMUKU-INSTALLED-GAP-01;PKG-DANMUKU-08 的其他组合与设备门槛仍保留。
这属于实施与回归验证,没有启动用户要求自行指导的正式复盘。
## 原因和修复
完整安装包 WebKit 回归的原始轨迹中,800ms CPU 阻塞期间媒体从约0.461前进到
1.261;第一次 readys 查询已经在阻塞之后,first/middle 留在 wait 且未进入
beforeVisible。原 sampling-window 直到 RAF 内第一次 capture 才保存时间及
等待队列,因此没有先前区间可供补回。此前的连续两帧间隔测试未覆盖这个入口。
新增受控测试在播放开始后、第一帧前推进800ms,稳定复现两行都未显示。
新增真实浏览器控制在 start 事件中进行1000ms CPU 工作,保留原生媒体时钟、
RAF、readys 返回值和队列状态;修复前 Chromium/WebKit 同样漏掉 first/middle。
Firefox 在这次负例的第一帧前没有前进媒体时间,不能称其复现了相同缺陷。
`SamplingWindow.begin` 只在没有起点时保存媒体时间及既有等待行;Scheduler
在确认开始播放且可见时、请求 RAF 前调用它。它不额外查询公开 readys,也不在
RAF 外执行 beforeVisible 或 visible。重复 play/playing/start 不向前覆盖起点。
原取消、seek、stop、hide、document visibility/adoption 清空规则保持。
新插入的过期行、已采样的 false 回调仍不会因跨窗口而被额外重试。
进一步补测发现第0秒也必须覆盖:旧公开 playing 要求 currentTime > 0。
因此内部起点还接受已解除暂停、未结束且 readyState > 2 的真实媒体状态。
这不修改公开 playing;实际显示调度仍使用原 playing 条件。第0秒受控用例
在此补充前失败、补充后通过,三浏览器同时验证了真实零点播放入口。
没有改变公开声明、事件负载、回调串行顺序、轨道算法、Worker 协议、显示寿命、
播放速度或依赖。维护文档明确起点与公开 getter 的区别。
## 验证证据
详见 [机器证据](../baselines/danmuku-start-validation.json),包含原失败的精确输入、
首个采样、红绿结果、安装来源、源码/产物指纹和实际模型负载计数。
- Node24.21.0 / Yarn1.22.22。最终 `yarn test:danmuku` 278/278,通过旧契约、
热力图、renderer、输入、Worker、失败/取消和调度回归;25.322秒。
严格生产 tsc 和修改范围 lint 通过。
- 安装后的 main/legacy 各61/61调度测试通过,运行安装包内真正的 Worker 代码;
包含三小时模拟时钟、池复用、暂停/seek、串行异步回调及首帧/零点红绿。
- 最终源码三引擎时序24/24,61.623秒;安装产物24/24,57.128秒;均无跳过、
失败或重试。包括原800ms CPU窗口、首次RAF前阻塞、第0秒启动和异步回调间隔。
原 readys 查询保持瞬时窗口,候选 first/middle/sentinel 按序显示。
旧版本用例仍是历史观察或精确缺陷对照,不代表旧版已经修复。
- 最终候选核心下的真实 MediaPipe Mask 组合三引擎3/3,52.106秒。每引擎
120行全部显示,模型输出分别73/88/49次,含暂停、seek和网页全屏。
零点补充前还验证了三个新旧核心×三个浏览器9/9;它是中间版本证据,未冒充
最终代码的完整九组合。没有降低模型尺寸、负载或替换 SDK 内部计算。
- 最终隔离安装为 `.cache/packages/run-n5mxDm`,显式包含核心、Chapter和Danmuku。
此通用消费者检查的36项运行时、5旧类型/8精确类型模式属于核心/Chapter;
Danmuku 另由安装后的调度及浏览器检查证明。它不是完整20包CI矩阵。
main/legacy/ESM 在工作区、docs/compiled和安装目录的字节逐一相等。
最后构建曾出现一次 Windows copyfile UNKNOWN/-4094,磁盘空间充足且文件属性
正常;下一次相同构建成功。保留失败日志,根因未证实,没有为其修改构建逻辑。
## 剩余边界与下一步
原完整 WebKit 的13失败报告原样保留;本提交只关闭其中弹幕首采样缺陷。
Windows WebKit26.6不是物理Safari,真实设备、其余组合问题和远端CI尚不能
由这些本机结果替代。Chromium153.0.8010.12、Firefox155.0的版本见逐案例附件。
下一步继续剩余包验收与版本准备。每包大版本升级仍按已有政策单独实施;
完成全部实施后等待用户指导复盘,不能据此推送或发布。
回退本任务时须一起回退源码、生成产物、测试及状态;原始失败证据继续保留。
+8 -6
View File
@@ -2,9 +2,9 @@
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 282 项,范围 22 个包及工作区/示例。
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 283 项,范围 22 个包及工作区/示例。
状态:todo 43 / doing 20 / blocked 0 / done 219 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 43 / doing 20 / blocked 0 / done 220 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -22,7 +22,7 @@
| artplayer-plugin-auto-thumbnail | 1.1.0 | PKG-AUTO-THUMB-01, PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-04, PKG-AUTO-THUMB-05, PKG-AUTO-THUMB-06, PKG-AUTO-THUMB-07, PKG-AUTO-THUMB-08, PKG-AUTO-THUMB-09, PKG-AUTO-THUMB-10, PKG-AUTO-THUMB-11, PKG-AUTO-THUMB-12, PKG-AUTO-THUMB-13 |
| artplayer-plugin-chapter | 1.1.0 | PILOT-01, PKG-CHAPTER-01, PKG-CHAPTER-02, PKG-CHAPTER-03, PKG-CHAPTER-04, PKG-CHAPTER-05, PKG-CHAPTER-06, PKG-CHAPTER-HOVER-01 |
| artplayer-plugin-chromecast | 1.1.0 | PKG-CAST-01, PKG-CAST-02, PKG-CAST-03, PKG-CAST-04, PKG-CAST-05, PKG-CAST-06 |
| artplayer-plugin-danmuku | 5.3.0 | PKG-DANMUKU-01, PKG-DANMUKU-02, PKG-DANMUKU-03, PKG-DANMUKU-04, PKG-DANMUKU-05, PKG-DANMUKU-06, PKG-DANMUKU-07, PKG-DANMUKU-08, PKG-DANMUKU-09, SITE-DANMUKU-01, PKG-DANMUKU-10, PKG-DANMUKU-11, PKG-DANMUKU-12, PKG-DANMUKU-MASK-LOAD-01 |
| artplayer-plugin-danmuku | 5.3.0 | PKG-DANMUKU-01, PKG-DANMUKU-02, PKG-DANMUKU-03, PKG-DANMUKU-04, PKG-DANMUKU-05, PKG-DANMUKU-06, PKG-DANMUKU-07, PKG-DANMUKU-08, PKG-DANMUKU-09, SITE-DANMUKU-01, PKG-DANMUKU-10, PKG-DANMUKU-11, PKG-DANMUKU-12, PKG-DANMUKU-MASK-LOAD-01, PKG-DANMUKU-START-01 |
| artplayer-plugin-danmuku-mask | 1.1.0 | PKG-MASK-01, PKG-MASK-02, PKG-MASK-03, PKG-MASK-04, PKG-MASK-05, PKG-MASK-06, ENG-LINT-01, PKG-DANMUKU-MASK-LOAD-01 |
| artplayer-plugin-dash-control | 1.1.0 | PKG-DASH-01, PKG-DASH-02, PKG-DASH-03, PKG-DASH-04, PKG-DASH-SEEK-01, PKG-DASH-MENU-01, PKG-DASH-05, PKG-DASH-06, SITE-DASH-01, PKG-DASH-CLEANUP-01 |
| artplayer-plugin-document-pip | 1.1.0 | PKG-DPIP-01, PKG-DPIP-02, PKG-DPIP-03, PKG-DPIP-04, PKG-DPIP-05, PKG-DPIP-06 |
@@ -345,11 +345,12 @@
| PKG-DANMUKU-05 | artplayer-plugin-danmuku<br>整理 DOM 渲染、设置、热力图与 worker | PKG-DANMUKU-04, CORE-14, CORE-18 | renderer/setting/heatmap/worker 职责及资源归属 | mount/icons/设置和 worker 协议保持,销毁无后台工作 | H | done |
| PKG-DANMUKU-06 | artplayer-plugin-danmuku<br>迁移 TS 与公开声明 | PKG-DANMUKU-05, ENG-04, ENG-06 | 自有模块、worker 消息、option/item/result 类型 | 旧参数、扩展字段和事件消费通过,vendored 边界清楚 | H | done |
| PKG-DANMUKU-07 | artplayer-plugin-danmuku<br>弹幕能力稳定性验收 | PKG-DANMUKU-06, PKG-DANMUKU-10, PKG-DANMUKU-11, PKG-DANMUKU-12 | 负载/渲染/资源对比及 mask 可依赖的稳定边界 | 在试点核心中旧插件 API 与关键帧路径通过 | H | done |
| PKG-DANMUKU-08 | artplayer-plugin-danmuku<br>完成新旧核心与组合验收 | PKG-DANMUKU-07, CORE-22 | 最终核心/旧核心、mask/fullscreen/PiP 组合报告 | 性能无未解释退化,反复装卸无累计资源 | H | doing |
| PKG-DANMUKU-09 | artplayer-plugin-danmuku<br>完成分发、示例与文档 | PKG-DANMUKU-08, ENG-07, PKG-DANMUKU-MASK-LOAD-01 | danmuku.js、README、声明、worker 与产物 | tarball 和静态 icons 等旧调用通过,有独立回退版本 | H | todo |
| PKG-DANMUKU-08 | artplayer-plugin-danmuku<br>完成新旧核心与组合验收 | PKG-DANMUKU-07, CORE-22, PKG-DANMUKU-START-01 | 最终核心/旧核心、mask/fullscreen/PiP 组合报告 | 性能无未解释退化,反复装卸无累计资源 | H | doing |
| PKG-DANMUKU-09 | artplayer-plugin-danmuku<br>完成分发、示例与文档 | PKG-DANMUKU-08, ENG-07, PKG-DANMUKU-MASK-LOAD-01, PKG-DANMUKU-START-01 | danmuku.js、README、声明、worker 与产物 | tarball 和静态 icons 等旧调用通过,有独立回退版本 | H | todo |
| PKG-DANMUKU-10 | artplayer-plugin-danmuku<br>修复 issue958 密集热力图过高与曲线裁平 | PKG-DANMUKU-05 | 自动密度缩放与曲线边界、历史自定义坐标兼容及真实浏览器红绿证据 | 16000条三种分布在两核心三引擎和source/main/legacy不遮挡;显式轴和自定义points保持旧解释,有单独commit | M | done |
| PKG-DANMUKU-11 | artplayer-plugin-danmuku<br>修复 Worker 隐藏等待吞掉可见寿命 | PKG-DANMUKU-06 | 成功放置时的可见计时与延迟回复回归 | 真实 Worker 延迟下完整显示寿命;保留暂停/回收/取消和公开契约 | M | done |
| PKG-DANMUKU-12 | artplayer-plugin-danmuku<br>保持异步放置期间的原生帧采样与寿命维护 | PKG-DANMUKU-11 | 每帧采样与串行dispatcher、代际buffer及回归 | 异步等待不丢已到时采样、不延迟既有寿命维护;保留顺序、取消与公开readys窗口 | H | done |
| PKG-DANMUKU-START-01 | artplayer-plugin-danmuku<br>修复首帧延迟前丢失的弹幕采样区间 | PKG-DANMUKU-MASK-LOAD-01 | 播放起始采样修复、受控红绿及源码/安装产物真实浏览器证据 | 解释并修复安装 WebKit 首次 readys 在 CPU 间隔后才运行的漏显;保持公开查询、串行回调和 seek/暂停/隐藏取消边界 | H | done |
## 5 包迁移:artplayer-proxy-canvas
@@ -650,7 +651,7 @@
- PKG-DANMUKU-05: [记录](changes/2026-09-13-PKG-DANMUKU-05-resources.md) [记录](baselines/danmuku-resources-validation.json)
- PKG-DANMUKU-06: [记录](changes/2026-09-13-PKG-DANMUKU-06-types.md) [记录](baselines/danmuku-types-validation.json)
- PKG-DANMUKU-07: [记录](changes/2026-09-13-PKG-DANMUKU-07-stability.md) [记录](baselines/danmuku-stability-validation.json) [记录](danmuku-stability.md)
- PKG-DANMUKU-08: [记录](changes/2026-09-14-PKG-DANMUKU-08-native-combinations.md) [记录](baselines/danmuku-combination-checkpoint.json) [记录](changes/2026-09-14-PKG-DANMUKU-08-fullscreen-lifetimes.md) [记录](baselines/danmuku-fullscreen-validation.json) [记录](baselines/danmuku-old-core-load-validation.json) [记录](changes/2026-09-14-PKG-DANMUKU-08-combined-load.md) [记录](baselines/danmuku-combined-load-validation.json) [记录](changes/2026-09-15-CI-01-installed-webkit.md) [记录](baselines/ci-installed-webkit-validation.json)
- PKG-DANMUKU-08: [记录](changes/2026-09-14-PKG-DANMUKU-08-native-combinations.md) [记录](baselines/danmuku-combination-checkpoint.json) [记录](changes/2026-09-14-PKG-DANMUKU-08-fullscreen-lifetimes.md) [记录](baselines/danmuku-fullscreen-validation.json) [记录](baselines/danmuku-old-core-load-validation.json) [记录](changes/2026-09-14-PKG-DANMUKU-08-combined-load.md) [记录](baselines/danmuku-combined-load-validation.json) [记录](changes/2026-09-15-CI-01-installed-webkit.md) [记录](baselines/ci-installed-webkit-validation.json) [记录](changes/2026-09-16-PKG-DANMUKU-START-01-first-sample.md) [记录](baselines/danmuku-start-validation.json)
- PKG-CANVAS-01: [记录](changes/2026-09-12-PKG-CANVAS-01-contract.md) [记录](baselines/canvas-contract.md) [记录](baselines/canvas-release.json) [记录](canvas-validation.md) [记录](baselines/canvas-contract-validation.json)
- PKG-CANVAS-02: [记录](changes/2026-09-12-PKG-CANVAS-02-tests.md) [记录](baselines/canvas-behavior-validation.json) [记录](canvas-validation.md)
- PKG-CANVAS-03: [记录](changes/2026-09-12-PKG-CANVAS-03-lifecycle.md) [记录](baselines/canvas-lifecycle-validation.json) [记录](canvas-validation.md)
@@ -740,3 +741,4 @@
- SITE-EDITOR-VAST-01: [记录](changes/2026-09-15-SITE-EDITOR-VAST-01-consumer.md) [记录](baselines/editor-vast-consumer-validation.json) [记录](baselines/ci-source-firefox-validation.json) [记录](vast-type-decision.md)
- CI-JASSUB-SOURCE-01: [记录](changes/2026-09-15-CI-JASSUB-SOURCE-01-native-inputs.md) [记录](baselines/jassub-source-selection-validation.json)
- DOC-REVIEW-01: [记录](changes/2026-09-15-DOC-REVIEW-01-user-guidance.md) [记录](release-reviews.md) [记录](ai-workflow.md)
- PKG-DANMUKU-START-01: [记录](baselines/ci-installed-webkit-validation.json) [记录](changes/2026-09-16-PKG-DANMUKU-START-01-first-sample.md) [记录](baselines/danmuku-start-validation.json)
+13
View File
@@ -1,5 +1,18 @@
# 进度与证据
## PKG-DANMUKU-START-01 首帧漏显修复完成
已解释原安装 WebKit 的首个 readys 在CPU间隔之后才运行的漏显:私有采样窗口
原本没有播放起点。现于实际播放开始、第一帧前建立起点,补齐第0秒的原生播放
边界;公开 readys/playing 与回调调度规则保持。原失败轨迹及红绿均已归档。
278项弹幕测试、严格类型、安装main/legacy各61项通过;最终源码/安装三浏览器
时序各24项通过。最终候选核心的真实Mask负载3项均120/120显示;中间版本
三个核心×三个引擎9项通过的范围单独注明。见[记录](changes/2026-09-16-PKG-DANMUKU-START-01-first-sample.md)、
[证据](baselines/danmuku-start-validation.json)。
DANMUKU-INSTALLED-GAP-01 resolved;完整CI原13失败报告保留,其余风险没有关闭。
当前220 done、20 doing、43 todo,共283项。下一步继续包验收与版本准备;
实施后等待用户指导复盘,不推送或发布。
## PKG-TOOL-THUMB-04 Thumbnail 策略与源码迁移完成
用户批准已落实:默认保留 npm 3.5.31,显式 workspace-4.4 保留未发布工作区行为。
+1 -1
View File
@@ -290,4 +290,4 @@
| SITE-HTML-INVENTORY-01 | resolved / 已复现 | New documentation HTML is missing from historical demo route coverage, failing the complete local CI check | SITE-ROUTES-01 |
| SITE-EDITOR-VAST-01 | resolved / 已复现 | Monaco browser consumer still assumes the workspace VAST namespace after the approved npm-root restoration | SITE-EDITOR-VAST-01 |
| CI-JASSUB-SOURCE-01 | resolved / 已复现 | Default native JASSUB source runs select only the published plugin and omit the current source | CI-JASSUB-SOURCE-01 |
| DANMUKU-INSTALLED-GAP-01 | open / 已复现 | Windows WebKit installed Danmuku candidate misses two rows after a native main-thread gap | PKG-DANMUKU-08 |
| DANMUKU-INSTALLED-GAP-01 | resolved / 已复现 | Windows WebKit installed Danmuku candidate misses two rows after a native main-thread gap | PKG-DANMUKU-08, PKG-DANMUKU-START-01 |
+12 -4
View File
@@ -6853,18 +6853,26 @@
"id": "DANMUKU-INSTALLED-GAP-01",
"title": "Windows WebKit installed Danmuku candidate misses two rows after a native main-thread gap",
"confirmation": "reproduced",
"status": "open",
"status": "resolved",
"owners": [
"PKG-DANMUKU-08"
"PKG-DANMUKU-08",
"PKG-DANMUKU-START-01"
],
"evidence": [
"test/browser/danmuku-timing-diagnostic.spec.js",
"refactor/changes/2026-09-15-CI-01-installed-webkit.md",
"refactor/baselines/ci-installed-webkit-validation.json"
"refactor/baselines/ci-installed-webkit-validation.json",
"refactor/changes/2026-09-16-PKG-DANMUKU-START-01-first-sample.md",
"refactor/baselines/danmuku-start-validation.json"
],
"compatibleResolution": "Determine why the current installed candidate misses the first eligibility interval; preserve old API and timestamp semantics and add a deterministic source/installed regression. Do not treat the candidate failure as expected or lower assertions.",
"closureCriteria": "Explain captured800ms CPU gap, first empty readys sample and unprocessed first/middle rows; validate fix across source, installed formats and affected browsers while preserving existing sampling, seek, worker, lifecycle and load regressions.",
"workspaceState": "At bb2c12df3 with run-cRduve, first/middle remain wait and never enter beforeVisible; sentinel alone is visible. Chromium and Firefox complete runs pass this case. Root cause unproven and original13-failure WebKit report retained."
"workspaceState": "Final source and installed timing 24/24 each, 278 source tests and installed main/legacy 61/61 each pass. Final candidate-core model load 120/120 rows on all three engines; earlier pre-zero-guard nine-core/engine combinations are labeled intermediate evidence. Original full installed CI failures remain recorded; no physical-device or full-matrix completion claimed.",
"resolutionEvidence": [
"refactor/changes/2026-09-16-PKG-DANMUKU-START-01-first-sample.md",
"refactor/baselines/danmuku-start-validation.json"
],
"resolutionRationale": "The first sampling anchor was created only inside RAF. Reproduced in controlled first-frame and time-zero tests plus real Chromium/WebKit start-listener CPU stalls. Private playback-start anchoring preserves public queries and invalidation. Final source/installed three-engine timing, both installed runtime formats, scheduler regression and actual model load pass."
}
]
}
+27 -3
View File
@@ -3642,7 +3642,8 @@
],
"dependsOn": [
"PKG-DANMUKU-07",
"CORE-22"
"CORE-22",
"PKG-DANMUKU-START-01"
],
"status": "doing",
"risk": "H",
@@ -3657,7 +3658,9 @@
"changes/2026-09-14-PKG-DANMUKU-08-combined-load.md",
"baselines/danmuku-combined-load-validation.json",
"changes/2026-09-15-CI-01-installed-webkit.md",
"baselines/ci-installed-webkit-validation.json"
"baselines/ci-installed-webkit-validation.json",
"changes/2026-09-16-PKG-DANMUKU-START-01-first-sample.md",
"baselines/danmuku-start-validation.json"
]
},
{
@@ -3670,7 +3673,8 @@
"dependsOn": [
"PKG-DANMUKU-08",
"ENG-07",
"PKG-DANMUKU-MASK-LOAD-01"
"PKG-DANMUKU-MASK-LOAD-01",
"PKG-DANMUKU-START-01"
],
"status": "todo",
"risk": "H",
@@ -6104,6 +6108,26 @@
"release-reviews.md",
"ai-workflow.md"
]
},
{
"id": "PKG-DANMUKU-START-01",
"phase": "5 包迁移:artplayer-plugin-danmuku",
"title": "修复首帧延迟前丢失的弹幕采样区间",
"scope": [
"artplayer-plugin-danmuku"
],
"dependsOn": [
"PKG-DANMUKU-MASK-LOAD-01"
],
"status": "done",
"risk": "H",
"deliverable": "播放起始采样修复、受控红绿及源码/安装产物真实浏览器证据",
"acceptance": "解释并修复安装 WebKit 首次 readys 在 CPU 间隔后才运行的漏显;保持公开查询、串行回调和 seek/暂停/隐藏取消边界",
"evidence": [
"baselines/ci-installed-webkit-validation.json",
"changes/2026-09-16-PKG-DANMUKU-START-01-first-sample.md",
"baselines/danmuku-start-validation.json"
]
}
]
}
+22 -3
View File
@@ -19,7 +19,7 @@ test.beforeAll(async () => {
})
for (const implementation of ['published', 'candidate']) {
for (const scenario of ['main-thread-gap', 'async-visibility-gap']) {
for (const scenario of ['main-thread-gap', 'initial-frame-gap', 'initial-zero-frame-gap', 'async-visibility-gap']) {
test(`${implementation}: native ${scenario} records actual frame and eligibility gaps`, async ({ page }, testInfo) => {
await page.goto('/test/player.html?core=candidate')
const code = implementations.get(implementation)
@@ -29,6 +29,14 @@ for (const implementation of ['published', 'candidate']) {
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
await page.click('#pause')
if (scenario === 'initial-zero-frame-gap') {
await page.evaluate(async () => {
const art = window.art
const seeked = new Promise(resolve => art.video.addEventListener('seeked', resolve, { once: true }))
art.currentTime = 0
await seeked
})
}
await page.evaluate(async (scenario) => {
const art = window.art
const evidence = window.timingEvidence = { frames: [], eligibility: [], visible: [], callbacks: [], block: null }
@@ -68,6 +76,15 @@ for (const implementation of ['published', 'candidate']) {
return ready
} })
await plugin.load(rows)
if (scenario.startsWith('initial-')) {
art.once('artplayerPluginDanmuku:start', () => {
const start = performance.now()
const before = art.currentTime
// Hold the real start listener after scheduling but before the first native RAF.
while (performance.now() - start < 1000) { /* Controlled CPU load. */ }
evidence.block = { start, end: performance.now(), before, after: art.currentTime }
})
}
let blocked = false
function observe(wall) {
if (art.playing) {
@@ -101,8 +118,8 @@ for (const implementation of ['published', 'candidate']) {
expect(observation.frames.length).toBeGreaterThan(2)
expect(observation.eligibility.length).toBeGreaterThan(2)
expect(observation.visible.some(row => row.id === 'sentinel')).toBe(true)
if (scenario === 'main-thread-gap')
expect(observation.block.end - observation.block.start).toBeGreaterThanOrEqual(800)
if (scenario !== 'async-visibility-gap')
expect(observation.block.end - observation.block.start).toBeGreaterThanOrEqual(scenario.startsWith('initial-') ? 1000 : 800)
else
expect(observation.callbacks.find(row => row.id === 'first').end - observation.callbacks.find(row => row.id === 'first').start).toBeGreaterThanOrEqual(550)
if (scenario === 'async-visibility-gap') {
@@ -117,6 +134,8 @@ for (const implementation of ['published', 'candidate']) {
expect(observation.missing).toEqual([])
expect(observation.visible.map(row => row.id)).toEqual(['first', 'middle', 'sentinel'])
}
if (scenario.startsWith('initial-'))
expect(observation.eligibility[0].wall).toBeGreaterThanOrEqual(observation.block.end)
await page.evaluate(() => window.art.destroy())
})
}
+34
View File
@@ -103,6 +103,40 @@ test(`${prefix}: non-overlap-disabled placement does not queue a Worker round tr
assert.deepEqual([...owner.queue].map(row => row.$ref.offsetTop), [owner.marginTop, owner.marginTop + 20])
})
test(`${prefix}: playback anchors eligibility before a delayed first animation frame`, async (t) => {
const { env, plugin, owner } = await fixture({ antiOverlap: false })
t.after(() => env.destroy())
await plugin.load([{ text: 'first', time: 10.2, mode: 1 }, { text: 'middle', time: 10.5, mode: 1 }])
env.art.playing = true
env.art.emit('video:play')
assert.equal(output(env, 'visible').length, 0, 'Starting must not dispatch outside RAF')
env.tick(800)
env.art.currentTime = 10.8
// A repeated playing event must not move an existing start anchor forward.
env.art.emit('video:playing')
assert.equal(owner.readys.length, 0, 'The public getter stays a point-in-time query')
await env.frame()
await env.flush()
assert.deepEqual(output(env, 'visible').map(event => event.args[0].text), ['first', 'middle'])
})
test(`${prefix}: native playback at zero anchors before the legacy playing getter becomes true`, async (t) => {
const { env, plugin, owner } = await fixture({ antiOverlap: false })
t.after(() => env.destroy())
env.art.currentTime = 0
env.art.video = { paused: false, ended: false, readyState: 4, seeking: false }
await plugin.load([{ text: 'first', time: 0.2, mode: 1 }, { text: 'middle', time: 0.5, mode: 1 }])
env.art.emit('video:play')
assert.equal(output(env, 'visible').length, 0)
env.tick(800)
env.art.currentTime = 0.8
env.art.playing = true
assert.equal(owner.readys.length, 0)
await env.frame()
await env.flush()
assert.deepEqual(output(env, 'visible').map(event => event.args[0].text), ['first', 'middle'])
})
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 })