diff --git a/docs/compiled/artplayer-plugin-danmuku.js b/docs/compiled/artplayer-plugin-danmuku.js index e1a89fc1a..dc9308ff6 100644 --- a/docs/compiled/artplayer-plugin-danmuku.js +++ b/docs/compiled/artplayer-plugin-danmuku.js @@ -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(/]*?p="(?

[^"]+)"[^>]*>(?.*?)<\/d>/gs);return Array.from(i).map(t=>{const i=t.groups.p.split(",");if(i.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(i[0]),mode:e(Number(i[1])),fontSize:Number(i[2]),color:`#${Number(i[3]).toString(16)}`,timestamp:Number(i[4]),pool:Number(i[5]),userID:i[6],rowID:Number(i[7])}}return null}).filter(Boolean)}function a({data:t}){const{xml:e,id:a}=t,n=i(e);globalThis.postMessage({danmus:n,id:a})}let n=0;function s(t,{signal:s,onCancel:o}={}){return new Promise((r,l)=>{let d,p,h,u=!1;const c=++n,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!==p){const a=p;p=void 0;try{URL.revokeObjectURL(a)}catch(i){e||(t=i),e=!0}}if(e)throw t}function g(t,e){if(u)return;u=!0;let i=arguments.length>1;s?.removeEventListener("abort",m),h?.();try{f()}catch(a){i||(e=a),i=!0}i?l(e):r(t)}function y(t,e){if(!u){console.error("Error parsing Bilibili Danmu:",e);try{g(i(t))}catch(a){g(void 0,a)}}}s?.aborted?m():(s?.addEventListener("abort",m),h=o?.(m),u?h?.():async function(){const n=s?await fetch(t,{signal:s}):await fetch(t);if(u)return;const o=await n.text();if(!u)try{const t=`\n ${e.toString()}\n ${i.toString()}\n onmessage = ${a.toString()}\n `;if(p=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),u)return void f();if(d=new Worker(p),u)return void f();d.onmessage=t=>{if(u||t.data?.id!==c)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(o,t.error||t)},d.onmessageerror=t=>y(o,t.error||t),d.postMessage({xml:o,id:c})}catch(r){y(o,r)}}().catch(t=>g(void 0,t)))})}const o=new WeakMap,r=Symbol("cancelled danmuku input");function l(t){return o.has(t)||o.set(t,{closed:!1,tasks:new Set,emitting:[]}),o.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 p(t){const e=l(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}const h='/*!\n * artplayer-plugin-danmuku.js v5.3.0\n * Github: https://github.com/zhw2590582/ArtPlayer\n * (c) 2017-2026 Harvey Zhao\n * Released under the MIT License.\n */\nfunction t({target:t,visibles:e,clientWidth:i,clientHeight:h,marginBottom:n,marginTop:o,antiOverlap:s}){const r=h-n,p=e.filter(e=>e.mode===t.mode&&e.top<=r).sort((t,e)=>t.top-e.top);if(0===p.length)return 2===t.mode?r-t.height:o;if(p.unshift({type:"top",top:0,left:0,right:0,height:o,width:i,speed:0,distance:i}),p.push({type:"bottom",top:r,left:0,right:0,height:n,width:i,speed:0,distance:i}),2===t.mode)for(let g=p.length-2;g>=0;g-=1){const e=p[g],i=p[g+1],h=e.top+e.height;if(i.top-h>=t.height)return i.top-t.height}else for(let g=1;g=t.height)return h}const a=[];for(let g=1;g{const i=Math.min(...e.map(t=>t.right)),h=Math.min(...t.map(t=>t.right));return i*e.length-h*t.length});break;case 1:case 2:a.sort((t,e)=>{const i=Math.max(...e.map(t=>t.width));return Math.max(...t.map(t=>t.width))*t.length-i*e.length})}return a[0][0].top}switch(t.mode){case 0:{const e=a.find(e=>e.every(e=>{if(ie.time}));return e&&e[0]?e[0].top:void 0}case 1:case 2:return}}onmessage=e=>{const{data:i}=e;if(!i.id||!i.type)return;const h=(0,{getDanmuTop:t}[i.type])(i);globalThis.postMessage({result:h,id:i.id})};\n',u="undefined"!==typeof self&&self.Blob&&new Blob(["URL.revokeObjectURL(import.meta.url);",h],{type:"text/javascript;charset=utf-8"});function c(t){let e;try{if(e=u&&(self.URL||self.webkitURL).createObjectURL(u),!e)throw"";const i=new Worker(e,{type:"module",name:t?.name});return i.addEventListener("error",()=>{(self.URL||self.webkitURL).revokeObjectURL(e)}),i}catch(i){return new Worker("data:text/javascript;charset=utf-8,"+encodeURIComponent(h),{type:"module",name:t?.name})}}class m{constructor(t,e){const{constructor:i,template:a}=t;this.utils=i.utils,this.validator=i.validator,this.$danmuku=a.$danmuku,this.$player=a.$player,this.art=t,this.queue=[],this.$refs=[],this.isStop=!1,this.isHide=!1,this.timer=null,this.index=0,this.worker=null,this.option=m.option,this.states={wait:[],ready:[],emit:[],stop:[]},this.start=this.start.bind(this),this.stop=this.stop.bind(this),this.reset=this.reset.bind(this),this.resize=this.resize.bind(this),this.destroy=this.destroy.bind(this),d(this),t.on("destroy",this.destroy);try{if(this.config(e,!0),!d(this))return;this.worker=new c}catch(n){throw t.off("destroy",this.destroy),p(this),n}t.on("video:play",this.start),t.on("video:playing",this.start),t.on("video:pause",this.stop),t.on("video:waiting",this.stop),t.on("resize",this.resize),this.load().catch(t=>console.warn("Failed to load initial danmuku:",t))}static get option(){return{danmuku:[],speed:5,margin:[10,"25%"],opacity:1,color:"#FFFFFF",mode:0,modes:[0,1,2],fontSize:25,antiOverlap:!0,synchronousPlayback:!1,mount:void 0,heatmap:!1,width:512,points:[],filter:()=>!0,beforeEmit:()=>!0,beforeVisible:()=>!0,visible:!0,emitter:!0,maxLength:200,lockTime:5,theme:"dark",OPACITY:{},FONT_SIZE:{},MARGIN:{},SPEED:{},COLOR:[]}}static get scheme(){return{danmuku:"array|function|string|promise",speed:"number",margin:"array",opacity:"number",color:"string",mode:"number",modes:"array",fontSize:"number|string",antiOverlap:"boolean",synchronousPlayback:"boolean",mount:"?htmldivelement|string",heatmap:"object|boolean",width:"number",points:"array",filter:"function",beforeEmit:"function",beforeVisible:"function",visible:"boolean",emitter:"boolean",maxLength:"number",lockTime:"number",theme:"string",OPACITY:"object",FONT_SIZE:"object",MARGIN:"object",SPEED:"object",COLOR:"array"}}static get cssText(){return'\n user-select: none;\n position: absolute;\n white-space: pre;\n pointer-events: none;\n perspective: 500px;\n display: inline-block;\n will-change: transform;\n font-weight: normal;\n line-height: 1.125;\n visibility: hidden;\n font-family: SimHei, "Microsoft JhengHei", Arial, Helvetica, sans-serif;\n text-shadow: rgb(0, 0, 0) 1px 0px 1px, rgb(0, 0, 0) 0px 1px 1px, rgb(0, 0, 0) 0px -1px 1px, rgb(0, 0, 0) -1px 0px 1px;\n '}get isRotate(){return this.art.plugins?.autoOrientation?.state}get marginTop(){const{clamp:t}=this.utils,e=this.option.margin[0],{clientHeight:i}=this.$player;if("number"===typeof e)return t(e,0,i);if("string"===typeof e&&e.endsWith("%")){return t(i*(Number.parseFloat(e)/100),0,i)}return m.option.margin[0]}get marginBottom(){const{clamp:t}=this.utils,e=this.option.margin[1],{clientHeight:i}=this.$player;if("number"===typeof e)return t(e,0,i);if("string"===typeof e&&e.endsWith("%")){return t(i*(Number.parseFloat(e)/100),0,i)}return m.option.margin[1]}get fontSize(){const{clamp:t}=this.utils,{clientHeight:e}=this.$player,i=this.option.fontSize;if("number"===typeof i)return Math.round(t(i,12,e));if("string"===typeof i&&i.endsWith("%")){const a=Number.parseFloat(i)/100;return Math.round(t(e*a,12,e))}return m.option.fontSize}get $ref(){const t=this.$refs.pop()||document.createElement("div");return t.style.cssText=m.cssText,t.dataset.mode="",t.dataset.id="",t.className="",t}get readys(){const{currentTime:t}=this.art,e=[];return this.filter("ready",t=>e.push(t)),this.filter("wait",i=>{t+.1>=i.time&&i.time>=t-.1&&e.push(i)}),e}get visibles(){const t=[],{clientWidth:e}=this.$player,i=this.getLeft(this.$player);return this.filter("emit",a=>{const n=a.$ref.offsetTop,s=this.getLeft(a.$ref)-i,o=a.$ref.clientHeight,r=a.$ref.clientWidth,l=s+r,d=e-l,p=l/a.$restTime,h={};h.top=n,h.left=s,h.height=o,h.width=r,h.right=d,h.speed=p,h.distance=l,h.time=a.$restTime,h.mode=a.mode,t.push(h)}),t}get speed(){return this.option.synchronousPlayback&&this.art.playbackRate?this.option.speed/Number(this.art.playbackRate):this.option.speed}async load(e){const{errorHandle:i}=this.utils,a=function(t,e){const i=l(t);if(e)for(const r of[...i.tasks])r.replace&&r.cancel();let a,n=i.closed||t.art.isDestroy;const s=new Promise(t=>a=t),o=new Set,d="function"===typeof AbortController?new AbortController:void 0,p={replace:e,signal:d?.signal,active:()=>!n&&!i.closed&&!t.art.isDestroy,onCancel:t=>p.active()?(o.add(t),()=>o.delete(t)):(t(),()=>{}),cancel(){if(!n){n=!0,a(r),d?.abort();for(const t of[...o])t();o.clear()}},wait:t=>Promise.race([t,s]),emit(e){i.emitting.push(p);try{return t.emit(e)}finally{i.emitting.pop()}},finish(){i.tasks.delete(p),o.clear()}};return n?a(r):i.tasks.add(p),p}(this,void 0===e);try{if(!a.active())return this;const n=function(e,i){return"function"===typeof e?{asynchronous:!0,value:e()}:t(e)?{asynchronous:!0,value:e}:"string"===typeof e?{asynchronous:!0,value:s(e,i)}:{asynchronous:!1,value:Array.isArray(e)?e:[]}}(e||this.option.danmuku,a),o=n.asynchronous?await a.wait(n.value):n.value;if(!a.active())return this;if(i(Array.isArray(o),"Danmuku need return an array as result"),void 0===e){if(this.reset(),!a.active())return this;this.queue=[],this.states={wait:[],ready:[],emit:[],stop:[]},this.$refs=[],this.$danmuku.textContent=""}for(let t=0;t{const i=s[e],a=o[e];return"function"===typeof i||"function"===typeof a||t(i)||t(a)?i!==a:JSON.stringify(i)!==JSON.stringify(a)}))&&!i)return this;const r=function(t,e,{defaults:i,validate:a,clamp:n,mount:s}){const o=Object.assign({},i,t,e);return a(o),o.mode=n(o.mode,0,2),o.speed=n(o.speed,1,10),o.opacity=n(o.opacity,0,1),o.lockTime=n(o.lockTime,1,60),o.maxLength=n(o.maxLength,1,1e3),o.mount=o.mount||s,o}(this.option,e,{defaults:m.option,validate:t=>this.validator(t,m.scheme),clamp:a,mount:n});return d(this)?(this.option=r,e.fontSize&&(this.reset(),!d(this))||(this.option.visible?this.show():this.hide(),d(this)&&this.art.emit("artplayerPluginDanmuku:config",this.option)),this):this}getLeft(t){const e=t.getBoundingClientRect();return this.isRotate?e.top:e.left}postMessage(t={}){return new Promise(e=>{t.id=Date.now(),this.worker.postMessage(t),this.worker.onmessage=i=>{const{data:a}=i;a.id===t.id&&e(a)}})}filter(t,e){const i=this.states[t]||[];for(let a=0;ae!==t),t.$state=e,t.$ref&&(t.$ref.dataset.state=e),this.states[e].push(t)}makeWait(t){this.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=m.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.$refs.push(t.$ref),t.$ref=null)}update(){const{setStyles:t}=this.utils;return this.timer=window.requestAnimationFrame(async()=>{if(this.art.playing&&!this.isHide){this.filter("emit",t=>{const e=(Date.now()-t.$lastStartTime)/1e3;t.$restTime-=e,t.$lastStartTime=Date.now(),t.$restTime<=0&&this.makeWait(t)});const e=this.readys;for(let i=0;i{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.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.$player;return this.filter("stop",e=>{switch(this.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.$player;return this.filter("emit",e=>{switch(this.setState(e,"stop"),e.mode){case 0:{const i=t-(this.getLeft(e.$ref)-this.getLeft(this.$player));e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition="transform 0s linear 0s";break}}}),this}stop(){return this.isStop=!0,this.suspend(),window.cancelAnimationFrame(this.timer),this.art.emit("artplayerPluginDanmuku:stop"),this}start(){return this.isStop=!1,this.continue(),this.update(),this.art.emit("artplayerPluginDanmuku:start"),this}reset(){return this.queue.forEach(t=>this.makeWait(t)),this.art.emit("artplayerPluginDanmuku:reset"),this}show(){return this.isHide=!1,this.$danmuku.style.opacity=1,this.option.visible=!0,this.art.emit("artplayerPluginDanmuku:show"),this}hide(){return this.isHide=!0,this.$danmuku.style.opacity=0,this.option.visible=!1,this.art.emit("artplayerPluginDanmuku:hide"),this}destroy(){p(this),this.stop(),this.worker?.terminate(),this.art.off("video:play",this.start),this.art.off("video:playing",this.start),this.art.off("video:pause",this.stop),this.art.off("video:waiting",this.stop),this.art.off("resize",this.resize),this.art.off("destroy",this.destroy),this.art.emit("artplayerPluginDanmuku:destroy")}}const f={map:(t,e,i,a,n)=>(t-e)*(n-a)/(i-e)+a,range(t,e,i){const a=Math.round(t/i)*i;return Array.from({length:Math.floor((e-t)/i)},(t,e)=>e*i+a)}};function g(t,e,i){const{query:a}=t.constructor.utils;t.controls.add({name:"heatmap",position:"top",html:"",style:{position:"absolute",top:"-100px",left:"0px",right:"0px",height:"100px",width:"100%",pointerEvents:"none"},mounted(n){let s=null,o=null;function r(r=[]){if(s=null,o=null,n.innerHTML="",!t.duration||t.option.isLive)return;const l={w:n.offsetWidth,h:n.offsetHeight},d={xMin:0,xMax:l.w,yMin:0,yMax:128,scale:.25,opacity:.2,minHeight:Math.floor(.05*l.h),sampling:Math.floor(l.w/100),smoothing:.2,flattening:.2};"object"===typeof i&&Object.assign(d,i);let p=[];if(Array.isArray(r)&&r.length)p=[...r];else{const i=t.duration/l.w;for(let t=0;t<=l.w;t+=d.sampling){const a=e.queue.filter(({time:e})=>e>t*i&&e<=(t+d.sampling)*i).length;p.push([t,a])}}if(0===p.length)return;const h=p[p.length-1],u=h[0],c=h[1];u!==l.w&&p.push([l.w,c]);const m=p.map(t=>t[1]),g=(Math.min(...m)+Math.max(...m))/2;for(let t=0;tg?1+d.scale:1-d.scale)+d.minHeight}const y=(t,e,i,a)=>{const n=function(t,e){const i=e[0]-t[0],a=e[1]-t[1];return{length:Math.sqrt(i**2+a**2),angle:Math.atan2(a,i)}}(e||t,i||t),s=f.map(Math.cos(n.angle)*d.flattening,0,1,1,0),o=n.angle*s+(a?Math.PI:0),r=n.length*d.smoothing;return[t[0]+Math.cos(o)*r,t[1]+Math.sin(o)*r]},v=p.map(t=>[f.map(t[0],d.xMin,d.xMax,0,l.w),f.map(t[1],d.yMin,d.yMax,l.h,0)]).reduce((t,e,i,a)=>0===i?`M ${a[a.length-1][0]},${l.h} L ${e[0]},${l.h} L ${e[0]},${e[1]}`:`${t} ${((t,e,i)=>{const a=y(i[e-1],i[e-2],t),n=y(t,i[e-1],i[e+1],!0),s=e===i.length-1?" z":"";return`C ${a[0]},${a[1]} ${n[0]},${n[1]} ${t[0]},${t[1]}${s}`})(e,i,a)}`,"");n.innerHTML=`\n \n \n \n \n \n \n \n \n \n \n \n `,s=a("#heatmap-start",n),o=a("#heatmap-stop",n),s.setAttribute("offset",100*t.played+"%"),o.setAttribute("offset",100*t.played+"%")}t.on("video:timeupdate",()=>{s&&o&&(s.setAttribute("offset",100*t.played+"%"),o.setAttribute("offset",100*t.played+"%"))}),t.on("setBar",(t,e)=>{s&&o&&"played"===t&&(s.setAttribute("offset",100*e+"%"),o.setAttribute("offset",100*e+"%"))}),t.on("ready",()=>r()),t.on("resize",()=>r()),t.on("artplayerPluginDanmuku:loaded",()=>r()),t.on("artplayerPluginDanmuku:points",t=>r(t))}})}const y='',v='',k='',$='',w='',x='',b='',S='',M='',z='',E='',C='';class T{constructor(t,e){this.art=t,this.danmuku=e,this.utils=t.constructor.utils;const{setStyle:i}=this.utils,{$controlsCenter:a}=t.template;i(a,"display","flex"),this.template={$controlsCenter:a,$mount:a,$danmuku:null,$toggle:null,$config:null,$configPanel:null,$configModes:null,$style:null,$stylePanel:null,$styleModes:null,$colors:null,$opacitySlider:null,$opacityValue:null,$marginSlider:null,$marginValue:null,$fontSizeSlider:null,$fontSizeValue:null,$speedSlider:null,$speedValue:null,$input:null,$send:null},this.slider={opacity:null,margin:null,fontSize:null,speed:null},this.emitting=!1,this.isLock=!1,this.timer=null,this.createTemplate(),this.createSliders(),this.createEvents(),this.mount(this.option.mount),t.on("resize",()=>this.resize()),t.on("fullscreen",t=>this.onFullscreen(t)),t.on("fullscreenWeb",t=>this.onFullscreen(t)),t.proxy(this.template.$config,"mouseenter",()=>{this.onMouseEnter({$control:this.template.$config,$panel:this.template.$configPanel})}),t.proxy(this.template.$style,"mouseenter",()=>{this.onMouseEnter({$control:this.template.$style,$panel:this.template.$stylePanel})})}static get icons(){return{$on:E,$off:z,$config:k,$style:C,$mode_0_off:$,$mode_0_on:w,$mode_1_off:x,$mode_1_on:b,$mode_2_off:S,$mode_2_on:M,$check_on:v,$check_off:y}}get option(){return this.danmuku.option}get outside(){return this.template.$mount!==this.template.$controlsCenter}get TEMPLATE(){const{option:t}=this;return`\n

\n ${E}${z}\n
\n
\n ${k}\n
\n
\n
\n \u6309\u7c7b\u578b\u5c4f\u853d\n
\n
\n ${$}${w}\n
\u6eda\u52a8
\n
\n
\n ${x}${b}\n
\u9876\u90e8
\n
\n
\n ${S}${M}\n
\u5e95\u90e8
\n
\n
\n
\n
\n
\n ${v}${y}\n \u9632\u6b62\u5f39\u5e55\u91cd\u53e0\n
\n
\n ${v}${y}\n \u540c\u6b65\u89c6\u9891\u901f\u5ea6\n
\n
\n
\n \u4e0d\u900f\u660e\u5ea6\n
\n
\u672a\u77e5
\n
\n
\n \u663e\u793a\u533a\u57df\n
\n
\u672a\u77e5
\n
\n
\n \u5f39\u5e55\u5b57\u53f7\n
\n
\u672a\u77e5
\n
\n
\n \u5f39\u5e55\u901f\u5ea6\n
\n
\u672a\u77e5
\n
\n
\n
\n
\n
\n
\n ${C}\n
\n
\n
\n \u6a21\u5f0f\n
\n
\n ${w}\n
\u6eda\u52a8
\n
\n
\n ${b}\n
\u9876\u90e8
\n
\n
\n ${M}\n
\u5e95\u90e8
\n
\n
\n
\n
\n \u989c\u8272\n
\n ${this.COLOR.map(t=>`
`).join("")}\n
\n
\n
\n
\n
\n \n
\u53d1\u9001
\n
\n `}get OPACITY(){return{min:0,max:100,steps:[],...this.option.OPACITY}}get FONT_SIZE(){return{min:12,max:120,steps:[],...this.option.FONT_SIZE}}get MARGIN(){return{min:0,max:3,steps:[{name:"1/4",value:[10,"75%"]},{name:"\u534a\u5c4f",value:[10,"50%"]},{name:"3/4",value:[10,"25%"]},{name:"\u6ee1\u5c4f",value:[10,10]}],...this.option.MARGIN}}get SPEED(){return{min:0,max:4,steps:[{name:"\u6781\u6162",value:10},{name:"\u8f83\u6162",value:7.5,hide:!0},{name:"\u9002\u4e2d",value:5},{name:"\u8f83\u5feb",value:2.5,hide:!0},{name:"\u6781\u5feb",value:1}],...this.option.SPEED}}get COLOR(){return this.option.COLOR.length?this.option.COLOR:["#FE0302","#FF7204","#FFAA02","#FFD302","#FFFF00","#A0EE00","#00CD00","#019899","#4266BE","#89D5FF","#CC0273","#222222","#9B9B9B","#FFFFFF"]}query(t){const{query:e}=this.utils,{$danmuku:i}=this.template;return e(t,i)}append(t,e){const{append:i}=this.utils;[...t.children].includes(e)||i(t,e)}setData(t,e){const{$player:i}=this.art.template,{$mount:a}=this.template;i.dataset[t]=e,this.outside&&(a.dataset[t]=e)}createTemplate(){const{createElement:t,tooltip:e}=this.utils,i=t("div");i.className="artplayer-plugin-danmuku",i.innerHTML=this.TEMPLATE,this.template.$danmuku=i,this.template.$toggle=this.query(".apd-toggle"),this.template.$config=this.query(".apd-config"),this.template.$configPanel=this.query(".apd-config-panel"),this.template.$configModes=this.query(".apd-config-mode .apd-modes"),this.template.$style=this.query(".apd-style"),this.template.$stylePanel=this.query(".apd-style-panel"),this.template.$styleModes=this.query(".apd-style-mode .apd-modes"),this.template.$colors=this.query(".apd-colors"),this.template.$antiOverlap=this.query(".apd-anti-overlap"),this.template.$syncVideo=this.query(".apd-sync-video"),this.template.$opacitySlider=this.query(".apd-config-opacity .apd-slider"),this.template.$opacityValue=this.query(".apd-config-opacity .apd-value"),this.template.$marginSlider=this.query(".apd-config-margin .apd-slider"),this.template.$marginValue=this.query(".apd-config-margin .apd-value"),this.template.$fontSizeSlider=this.query(".apd-config-fontSize .apd-slider"),this.template.$fontSizeValue=this.query(".apd-config-fontSize .apd-value"),this.template.$speedSlider=this.query(".apd-config-speed .apd-slider"),this.template.$speedValue=this.query(".apd-config-speed .apd-value"),this.template.$input=this.query(".apd-input"),this.template.$send=this.query(".apd-send");const{$toggle:a}=this.template;this.art.on("artplayerPluginDanmuku:show",()=>{e(a,"\u5173\u95ed\u5f39\u5e55")}),this.art.on("artplayerPluginDanmuku:hide",()=>{e(a,"\u6253\u5f00\u5f39\u5e55")})}createEvents(){const{$toggle:t,$configModes:e,$styleModes:i,$colors:a,$antiOverlap:n,$syncVideo:s,$send:o,$input:r}=this.template;this.art.proxy(t,"click",()=>{this.danmuku.config({visible:!this.option.visible}),this.reset()}),this.art.proxy(e,"click",t=>{const e=t.target.closest(".apd-mode");if(!e)return;const i=Number(e.dataset.mode);this.option.modes.includes(i)?this.danmuku.config({modes:this.option.modes.filter(t=>t!==i)}):this.danmuku.config({modes:[...this.option.modes,i]}),this.reset()}),this.art.proxy(n,"click",()=>{this.danmuku.config({antiOverlap:!this.option.antiOverlap}),this.reset()}),this.art.proxy(s,"click",()=>{this.danmuku.config({synchronousPlayback:!this.option.synchronousPlayback}),this.reset()}),this.art.proxy(i,"click",t=>{const e=t.target.closest(".apd-mode");if(!e)return;const i=Number(e.dataset.mode);this.danmuku.config({mode:i}),this.reset()}),this.art.proxy(a,"click",t=>{const e=t.target.closest(".apd-color");e&&(this.danmuku.config({color:e.dataset.color}),this.reset())}),this.art.proxy(o,"click",()=>this.emit()),this.art.proxy(r,"keypress",t=>{"Enter"===t.key&&(t.preventDefault(),this.emit())})}createSliders(){this.slider.opacity=this.createSlider({...this.OPACITY,container:this.template.$opacitySlider,findIndex:()=>Math.round(100*this.option.opacity),onChange:t=>{const{$opacityValue:e}=this.template;e.textContent=`${t}%`,this.danmuku.config({opacity:t/100})}}),this.slider.margin=this.createSlider({...this.MARGIN,container:this.template.$marginSlider,findIndex:()=>this.MARGIN.steps.findIndex(t=>t.value[0]===this.option.margin[0]&&t.value[1]===this.option.margin[1]),onChange:t=>{const e=this.MARGIN.steps[t];if(!e)return;const{$marginValue:i}=this.template;i.textContent=e.name,this.danmuku.config({margin:e.value})}}),this.slider.fontSize=this.createSlider({...this.FONT_SIZE,container:this.template.$fontSizeSlider,findIndex:()=>this.danmuku.fontSize,onChange:t=>{const{$fontSizeValue:e}=this.template;e.textContent=`${t}px`,t!==this.danmuku.fontSize&&this.danmuku.config({fontSize:t})}}),this.slider.speed=this.createSlider({...this.SPEED,container:this.template.$speedSlider,findIndex:()=>this.SPEED.steps.findIndex(t=>t.value===this.option.speed),onChange:t=>{const e=this.SPEED.steps[t];if(!e)return;const{$speedValue:i}=this.template;i.textContent=e.name,this.danmuku.config({speed:e.value})}})}createSlider({min:t,max:e,container:i,findIndex:a,onChange:n,steps:s=[]}){const{query:o,clamp:r,setStyle:l}=this.utils;l(i,"touch-action","none"),i.innerHTML=`\n
\n
\n ${s.map(()=>'
').join("")}\n
\n
\n
\n
\n
\n ${s.map(t=>t.hide?"":`
${t.name}
`).join("")}\n
\n `;const d=o(".apd-slider-dot",i),p=o(".apd-slider-progress",i);let h=!1;function u(i=a()){if(ie)return;const o=(i-t)/(e-t);d.style.left=100*o+"%",0===s.length&&(p.style.width=d.style.left),n(i)}function c(a){const{top:n,height:s,left:o,width:l}=i.getBoundingClientRect();if(this.art.isRotate){const i=r(a.clientY-n,0,s);u(Math.round(i/s*(e-t)+t))}else{const i=r(a.clientX-o,0,l);u(Math.round(i/l*(e-t)+t))}}return this.art.proxy(i,"click",t=>{c.call(this,t)}),this.art.proxy(i,"pointerdown",t=>{h=0===t.button}),this.art.on("document:pointermove",t=>{h&&c.call(this,t)}),this.art.on("document:pointerup",t=>{h&&(h=!1,c.call(this,t))}),{reset:u}}onFullscreen(t){const{$danmuku:e,$controlsCenter:i,$mount:a}=this.template;this.outside?t?this.append(i,e):this.append(a,e):this.append(i,e)}onMouseEnter({$control:t,$panel:e}){const{$player:i}=this.art.template,a=t.getBoundingClientRect(),n=e.getBoundingClientRect(),s=i.getBoundingClientRect(),o=n.width/2-a.width/2,r=s.left-(a.left-o),l=a.right+o-s.right;e.style.left=r>0?`${-o+r}px`:l>0?-o-l+"px":-o+"px"}async emit(){const{$input:t}=this.template,e=t.value.trim();if(!e.length)return;if(this.isLock)return;if(this.emitting)return;const i={text:e,mode:this.option.mode,color:this.option.color,time:this.art.currentTime};try{this.emitting=!0;const e=await this.option.beforeEmit(i);if(this.emitting=!1,!0!==e)return;i.border=!0,delete i.time,this.danmuku.emit(i),t.value="",this.lock()}catch(a){console.error("Error emitting danmuku:",a),this.emitting=!1}}lock(){const{addClass:t}=this.utils,{$send:e}=this.template;this.isLock=!0;let i=this.option.lockTime;e.textContent=i,t(e,"apd-lock");const a=()=>{this.timer=setTimeout(()=>{0===i?this.unlock():(i-=1,e.textContent=i,a())},1e3)};a()}unlock(){const{removeClass:t}=this.utils,{$send:e}=this.template;clearTimeout(this.timer),this.isLock=!1,e.textContent="\u53d1\u9001",t(e,"apd-lock")}resize(){if(this.outside)return;if(this.art.fullscreen)return;if(this.art.fullscreenWeb)return;const{$player:t,$controlsCenter:e}=this.art.template,{$danmuku:i}=this.template;this.art.widtht.dataset.color===this.option.color.toUpperCase());s&&t(s,"apd-active"),e(i,this.option.visible?"\u5173\u95ed\u5f39\u5e55":"\u6253\u5f00\u5f39\u5e55"),this.resize()}mount(t){const{errorHandle:e}=this.utils,i="string"===typeof t?document.querySelector(t):t;e(i,`Can not find the mount point: ${t}`),this.append(i,this.template.$danmuku),this.template.$mount=i,this.reset()}}if("undefined"!==typeof document){const t="artplayer-plugin-danmuku";let e=document.getElementById(t);e||(e=document.createElement("style"),e.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.appendChild(e)}):(document.head||document.documentElement).appendChild(e)),e.textContent='.artplayer-plugin-danmuku{display:flex;position:relative;z-index:99;align-items:center;justify-content:center;font-size:12px;height:32px;width:100%;color:#fff;font-weight:300;flex-shrink:0;gap:10px}.artplayer-plugin-danmuku .apd-icon{cursor:pointer;opacity:.75;transition:all .2s ease;fill:#fff}.artplayer-plugin-danmuku .apd-icon:hover{opacity:1}.artplayer-plugin-danmuku .apd-config{display:flex;position:relative}.artplayer-plugin-danmuku .apd-config .apd-config-panel{position:absolute;bottom:24px;left:0;width:320px;padding:10px;opacity:0;pointer-events:none}.artplayer-plugin-danmuku .apd-config .apd-config-panel .apd-config-panel-inner{width:100%;border-radius:3px;background-color:#000000d9;padding:10px}.artplayer-plugin-danmuku .apd-config:hover .apd-config-panel{opacity:100;pointer-events:all}.artplayer-plugin-danmuku .apd-config-mode,.artplayer-plugin-danmuku .apd-config-slider,.artplayer-plugin-danmuku .apd-config-other,.artplayer-plugin-danmuku .apd-style-mode{margin-bottom:15px}.artplayer-plugin-danmuku .apd-modes{display:flex;align-items:center;margin-top:5px;gap:20px}.artplayer-plugin-danmuku .apd-modes .apd-mode{cursor:pointer;text-align:center}.artplayer-plugin-danmuku .apd-modes .apd-mode:hover{color:#00a1d6}.artplayer-plugin-danmuku .apd-config-slider{display:flex;align-items:center;gap:12px}.artplayer-plugin-danmuku .apd-config-slider .apd-value{width:32px;text-align:right}.artplayer-plugin-danmuku .apd-slider{position:relative;flex:1;display:flex;height:20px;align-items:center;justify-content:center;cursor:pointer}.artplayer-plugin-danmuku .apd-slider .apd-slider-line{position:relative;height:2px;width:100%;overflow:hidden;border-radius:3px;background-color:#ffffff40}.artplayer-plugin-danmuku .apd-slider .apd-slider-points{position:absolute;inset:0;display:flex;align-items:center;justify-content:space-between}.artplayer-plugin-danmuku .apd-slider .apd-slider-points .apd-slider-point{width:2px;height:2px;border-radius:50%;background-color:#ffffff80}.artplayer-plugin-danmuku .apd-slider .apd-slider-progress{width:0%;height:100%;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-slider .apd-slider-dot{position:absolute;transform:translate(-6px);left:0%;width:12px;height:12px;border-radius:50%;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-slider .apd-slider-steps{display:flex;align-items:center;justify-content:space-between;position:absolute;bottom:-12px;width:calc(100% + 32px);color:#777}.artplayer-plugin-danmuku .apd-slider .apd-slider-steps .apd-slider-step{flex-shrink:0;width:36px;text-align:center;scale:.95}.artplayer-plugin-danmuku .apd-config-other{display:flex;align-items:center;gap:20px}.artplayer-plugin-danmuku .apd-config-other .apd-check-off,.artplayer-plugin-danmuku .apd-config-other .apd-check-on{width:16px;height:16px}.artplayer-plugin-danmuku .apd-config-other .apd-other{display:flex;align-items:center;cursor:pointer;gap:2px}.artplayer-plugin-danmuku .apd-config-other .apd-other:hover{color:#00a1d6}.artplayer-plugin-danmuku .apd-emitter{display:flex;flex:1;align-items:center;height:100%;background-color:#ffffff40;border-radius:5px}.artplayer-plugin-danmuku .apd-style{position:relative;display:flex;align-items:center;justify-content:center}.artplayer-plugin-danmuku .apd-style .apd-style-panel{position:absolute;bottom:24px;left:0;width:200px;padding:10px;opacity:0;pointer-events:none}.artplayer-plugin-danmuku .apd-style .apd-style-panel .apd-style-panel-inner{width:100%;border-radius:3px;background-color:#000000d9;padding:10px}.artplayer-plugin-danmuku .apd-style:hover .apd-style-panel{opacity:100;pointer-events:all}.artplayer-plugin-danmuku .apd-colors{display:flex;flex-wrap:wrap;margin-top:5px;gap:8px}.artplayer-plugin-danmuku .apd-colors .apd-color{width:16px;height:16px;border-radius:2px;cursor:pointer}.artplayer-plugin-danmuku .apd-colors .apd-color.apd-active{border:1px solid black;box-shadow:0 0 0 1px #fff}.artplayer-plugin-danmuku .apd-input{outline:none;height:100%;flex:1;min-width:0;width:auto;border:none;line-height:1;color:#fff;background-color:transparent}.artplayer-plugin-danmuku .apd-input::placeholder{color:#ffffff80}.artplayer-plugin-danmuku .apd-send{display:flex;align-items:center;justify-content:center;height:100%;width:60px;flex-shrink:0;cursor:pointer;text-shadow:none;border-top-right-radius:5px;border-bottom-right-radius:5px;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-send.apd-lock{cursor:not-allowed;color:#666;background-color:#e7e7e7}.art-controls-center .apd-emitter{width:260px;flex:none}.art-fullscreen .artplayer-plugin-danmuku,.art-fullscreen-web .artplayer-plugin-danmuku{height:38px;gap:16px}.art-fullscreen .artplayer-plugin-danmuku .apd-config-icon,.art-fullscreen-web .artplayer-plugin-danmuku .apd-config-icon,.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-off,.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-off,.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-on,.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-on{width:28px;height:28px}.art-fullscreen .artplayer-plugin-danmuku .apd-emitter,.art-fullscreen-web .artplayer-plugin-danmuku .apd-emitter{width:400px;flex:none}.art-video-player>.artplayer-plugin-danmuku{position:absolute;left:0;right:0;bottom:-40px;padding:0 10px}.art-video-player:has(>.artplayer-plugin-danmuku){margin-bottom:40px}[data-danmuku-emitter=false] .apd-emitter{display:none!important}[data-danmuku-emitter=false] .art-controls-center .artplayer-plugin-danmuku{justify-content:flex-end;gap:18px}[data-danmuku-emitter=false].art-fullscreen .art-controls-center .artplayer-plugin-danmuku,[data-danmuku-emitter=false].art-fullscreen-web .art-controls-center .artplayer-plugin-danmuku{gap:24px}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-icon{fill:#333}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-emitter{background-color:#f1f2f3}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-input{color:#000}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-input::placeholder{color:#0000004d}[data-danmuku-visible=false] .apd-toggle-off{display:block}[data-danmuku-visible=false] .apd-toggle-on,[data-danmuku-visible=true] .apd-toggle-off{display:none}[data-danmuku-visible=true] .apd-toggle-on{display:block}[data-danmuku-anti-overlap=false] .apd-anti-overlap .apd-check-on{display:none}[data-danmuku-anti-overlap=false] .apd-anti-overlap .apd-check-off,[data-danmuku-anti-overlap=true] .apd-anti-overlap .apd-check-on{display:block}[data-danmuku-anti-overlap=true] .apd-anti-overlap .apd-check-off,[data-danmuku-sync-video=false] .apd-sync-video .apd-check-on{display:none}[data-danmuku-sync-video=false] .apd-sync-video .apd-check-off,[data-danmuku-sync-video=true] .apd-sync-video .apd-check-on{display:block}[data-danmuku-sync-video=true] .apd-sync-video .apd-check-off{display:none}[data-danmuku-mode0=false] .apd-config-mode .apd-mode-0-off{display:block}[data-danmuku-mode0=false] .apd-config-mode .apd-mode-0-on{display:none}[data-danmuku-mode0=false] .art-danmuku [data-mode="0"]{opacity:0!important}[data-danmuku-mode0=true] .apd-config-mode .apd-mode-0-off{display:none}[data-danmuku-mode0=true] .apd-config-mode .apd-mode-0-on{display:block}[data-danmuku-mode="0"] .apd-style-mode [data-mode="0"]{color:#00a1d6}[data-danmuku-mode="0"] .apd-style-mode [data-mode="0"] path{fill:#00a1d6}[data-danmuku-mode1=false] .apd-config-mode .apd-mode-1-off{display:block}[data-danmuku-mode1=false] .apd-config-mode .apd-mode-1-on{display:none}[data-danmuku-mode1=false] .art-danmuku [data-mode="1"]{opacity:0!important}[data-danmuku-mode1=true] .apd-config-mode .apd-mode-1-off{display:none}[data-danmuku-mode1=true] .apd-config-mode .apd-mode-1-on{display:block}[data-danmuku-mode="1"] .apd-style-mode [data-mode="1"]{color:#00a1d6}[data-danmuku-mode="1"] .apd-style-mode [data-mode="1"] path{fill:#00a1d6}[data-danmuku-mode2=false] .apd-config-mode .apd-mode-2-off{display:block}[data-danmuku-mode2=false] .apd-config-mode .apd-mode-2-on{display:none}[data-danmuku-mode2=false] .art-danmuku [data-mode="2"]{opacity:0!important}[data-danmuku-mode2=true] .apd-config-mode .apd-mode-2-off{display:none}[data-danmuku-mode2=true] .apd-config-mode .apd-mode-2-on{display:block}[data-danmuku-mode="2"] .apd-style-mode [data-mode="2"]{color:#00a1d6}[data-danmuku-mode="2"] .apd-style-mode [data-mode="2"] path{fill:#00a1d6}'}function F(t){return e=>{const i=new m(e,t),a=e.isDestroy?void 0:new T(e,i);return!e.isDestroy&&i.option.heatmap&&g(e,i,i.option.heatmap),{name:"artplayerPluginDanmuku",emit:i.emit.bind(i),load:i.load.bind(i),config:i.config.bind(i),hide:i.hide.bind(i),show:i.show.bind(i),reset:i.reset.bind(i),mount:a?a.mount.bind(a):()=>{},get option(){return i.option},get isHide(){return i.isHide},get isStop(){return i.isStop}}}}return F.icons=T.icons,F}); +!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(/]*?p="(?

[^"]+)"[^>]*>(?.*?)<\/d>/gs);return Array.from(i).map(t=>{const i=t.groups.p.split(",");if(i.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(i[0]),mode:e(Number(i[1])),fontSize:Number(i[2]),color:`#${Number(i[3]).toString(16)}`,timestamp:Number(i[4]),pool:Number(i[5]),userID:i[6],rowID:Number(i[7])}}return null}).filter(Boolean)}function a({data:t}){const{xml:e,id:a}=t,n=i(e);globalThis.postMessage({danmus:n,id:a})}let n=0;function s(t,{signal:s,onCancel:o}={}){return new Promise((r,l)=>{let d,p,h,u=!1;const c=++n,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!==p){const a=p;p=void 0;try{URL.revokeObjectURL(a)}catch(i){e||(t=i),e=!0}}if(e)throw t}function g(t,e){if(u)return;u=!0;let i=arguments.length>1;s?.removeEventListener("abort",m),h?.();try{f()}catch(a){i||(e=a),i=!0}i?l(e):r(t)}function y(t,e){if(!u){console.error("Error parsing Bilibili Danmu:",e);try{g(i(t))}catch(a){g(void 0,a)}}}s?.aborted?m():(s?.addEventListener("abort",m),h=o?.(m),u?h?.():async function(){const n=s?await fetch(t,{signal:s}):await fetch(t);if(u)return;const o=await n.text();if(!u)try{const t=`\n ${e.toString()}\n ${i.toString()}\n onmessage = ${a.toString()}\n `;if(p=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),u)return void f();if(d=new Worker(p),u)return void f();d.onmessage=t=>{if(u||t.data?.id!==c)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(o,t.error||t)},d.onmessageerror=t=>y(o,t.error||t),d.postMessage({xml:o,id:c})}catch(r){y(o,r)}}().catch(t=>g(void 0,t)))})}const o=new WeakMap,r=Symbol("cancelled danmuku input");function l(t){return o.has(t)||o.set(t,{closed:!1,tasks:new Set,emitting:[]}),o.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 p(t){const e=l(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function h(t,e,i){const a=t.states[e]||[];for(let n=0;n{if(this.frame=null,t.timer=null,this.closed||this.fault||t.isStop||t.art.isDestroy)return;let e;const i=new Promise(t=>e=()=>t(u)),a={generation:this.generation,cancel:e,wait:t=>Promise.race([t,i]),ref:null};return this.operation=a,this.run(a).catch(t=>{this.active(a)&&this.fail(t)}).finally(()=>{this.release(a),this.operation===a&&(this.operation=null,this.schedule())})}),t.timer=this.frame)}async run(t){const e=this.owner,{setStyles:i}=e.utils;if(!e.art.playing||!this.active(t))return;e.filter("emit",t=>{const i=(Date.now()-t.$lastStartTime)/1e3;t.$restTime-=i,t.$lastStartTime=Date.now(),t.$restTime<=0&&e.makeWait(t)});const a=e.readys;for(const s of a){if(!this.active(t))return;if(this.failedItems.has(s))continue;let a;try{a=await t.wait(e.option.beforeVisible(s))}catch(n){if(!this.active(t))return;this.failedItems.add(s),this.report(n);continue}if(!this.active(t))return;if(!a)continue;const{clientWidth:o,clientHeight:r}=e.$player,l=e.$ref;if(t.danmu=s,t.ref=l,s.$ref=l,l.textContent=s.text,e.$danmuku.appendChild(l),l.style.opacity=e.option.opacity,l.style.fontSize=`${e.fontSize}px`,l.style.color=s.color,l.style.border=s.border?`1px solid ${s.color}`:null,l.style.backgroundColor=s.border?"rgb(0 0 0 / 50%)":null,i(l,s.style),!this.active(t))return;s.$lastStartTime=Date.now(),s.$restTime=e.speed;const d=o+l.clientWidth,{result:p}=await t.wait(e.postMessage({type:"getDanmuTop",target:{mode:s.mode,height:l.clientHeight,speed:d/s.$restTime},visibles:e.visibles,antiOverlap:e.option.antiOverlap,clientWidth:o,clientHeight:r,marginBottom:e.marginBottom,marginTop:e.marginTop}));if(!this.active(t)||s.$ref!==l)return;if(void 0!==p){switch(e.setState(s,"emit"),l.style.top=`${p}px`,l.style.visibility="visible",l.dataset.mode=s.mode,l.dataset.id=s.id||"",s.mode){case 0:l.style.left=`${o}px`,l.style.marginLeft="0px",l.style.transform=`translateX(${-d}px)`,l.style.transition=`transform ${s.$restTime}s linear 0s`;break;case 1:case 2:l.style.left="50%",l.style.marginLeft=`-${l.clientWidth/2}px`}t.ref=null,e.art.emit("artplayerPluginDanmuku:visible",s)}else e.setState(s,"ready"),this.release(t)}}destroy(){this.closed=!0,this.invalidate()}}let m=0;class f{constructor(t,e){this.pending=new Map,this.closed=!1,this.failed=!1,this.onFailure=e,this.worker=t();try{this.worker.onmessage=t=>{const e=this.pending.get(t.data?.id);e&&(this.pending.delete(t.data.id),e.resolve(t.data))},this.worker.onerror=t=>{t.preventDefault?.(),this.fail(t.error||t)},this.worker.onmessageerror=t=>this.fail(t.error||t)}catch(i){try{this.dispose()}catch{}throw i}}request(t){t.id=++m;const{id:e}=t;return this.closed||this.failed?Promise.resolve({id:e,result:void 0}):new Promise((i,a)=>{this.pending.set(e,{resolve:i,reject:a});try{this.worker.postMessage(t)}catch(n){this.fail(n)}})}fail(t){if(!this.closed&&!this.failed){this.failed=!0;for(const e of this.pending.values())e.reject(t);this.pending.clear();try{this.dispose()}catch{}this.onFailure(t)}}cancel(){for(const[t,e]of this.pending)e.resolve({id:t,result:void 0});this.pending.clear()}dispose(){if(this.closed)return;this.closed=!0,this.cancel();let t,e=!1;const i=i=>{try{i()}catch(a){e||(t=a),e=!0}};for(const a of["onmessage","onerror","onmessageerror"])i(()=>this.worker[a]=null);if(i(()=>this.worker.terminate()),e)throw t}}const g='/*!\n * artplayer-plugin-danmuku.js v5.3.0\n * Github: https://github.com/zhw2590582/ArtPlayer\n * (c) 2017-2026 Harvey Zhao\n * Released under the MIT License.\n */\nfunction t({target:t,visibles:e,clientWidth:i,clientHeight:h,marginBottom:n,marginTop:o,antiOverlap:s}){const r=h-n,p=e.filter(e=>e.mode===t.mode&&e.top<=r).sort((t,e)=>t.top-e.top);if(0===p.length)return 2===t.mode?r-t.height:o;if(p.unshift({type:"top",top:0,left:0,right:0,height:o,width:i,speed:0,distance:i}),p.push({type:"bottom",top:r,left:0,right:0,height:n,width:i,speed:0,distance:i}),2===t.mode)for(let g=p.length-2;g>=0;g-=1){const e=p[g],i=p[g+1],h=e.top+e.height;if(i.top-h>=t.height)return i.top-t.height}else for(let g=1;g=t.height)return h}const a=[];for(let g=1;g{const i=Math.min(...e.map(t=>t.right)),h=Math.min(...t.map(t=>t.right));return i*e.length-h*t.length});break;case 1:case 2:a.sort((t,e)=>{const i=Math.max(...e.map(t=>t.width));return Math.max(...t.map(t=>t.width))*t.length-i*e.length})}return a[0][0].top}switch(t.mode){case 0:{const e=a.find(e=>e.every(e=>{if(ie.time}));return e&&e[0]?e[0].top:void 0}case 1:case 2:return}}onmessage=e=>{const{data:i}=e;if(!i.id||!i.type)return;const h=(0,{getDanmuTop:t}[i.type])(i);globalThis.postMessage({result:h,id:i.id})};\n',y="undefined"!==typeof self&&self.Blob&&new Blob(["URL.revokeObjectURL(import.meta.url);",g],{type:"text/javascript;charset=utf-8"});function v(t){let e;try{if(e=y&&(self.URL||self.webkitURL).createObjectURL(y),!e)throw"";const i=new Worker(e,{type:"module",name:t?.name});return i.addEventListener("error",()=>{(self.URL||self.webkitURL).revokeObjectURL(e)}),i}catch(i){return new Worker("data:text/javascript;charset=utf-8,"+encodeURIComponent(g),{type:"module",name:t?.name})}}class k{constructor(t,e){const{constructor:i,template:a}=t;this.utils=i.utils,this.validator=i.validator,this.$danmuku=a.$danmuku,this.$player=a.$player,this.art=t,this.queue=[],this.$refs=[],this.isStop=!1,this.isHide=!1,this.timer=null,this.index=0,this.worker=null,this.workerClient=null,this.scheduler=new c(this),this.option=k.option,this.states={wait:[],ready:[],emit:[],stop:[]},this.start=this.start.bind(this),this.stop=this.stop.bind(this),this.reset=this.reset.bind(this),this.resize=this.resize.bind(this),this.destroy=this.destroy.bind(this),this.seek=this.seek.bind(this),d(this),t.on("destroy",this.destroy);try{if(this.config(e,!0),!d(this))return;this.createWorker()}catch(n){throw t.off("destroy",this.destroy),p(this),this.scheduler.destroy(),n}t.on("video:play",this.start),t.on("video:playing",this.start),t.on("video:pause",this.stop),t.on("video:waiting",this.stop),t.on("resize",this.resize),t.on("video:seeking",this.seek),this.load().catch(t=>console.warn("Failed to load initial danmuku:",t))}static get option(){return{danmuku:[],speed:5,margin:[10,"25%"],opacity:1,color:"#FFFFFF",mode:0,modes:[0,1,2],fontSize:25,antiOverlap:!0,synchronousPlayback:!1,mount:void 0,heatmap:!1,width:512,points:[],filter:()=>!0,beforeEmit:()=>!0,beforeVisible:()=>!0,visible:!0,emitter:!0,maxLength:200,lockTime:5,theme:"dark",OPACITY:{},FONT_SIZE:{},MARGIN:{},SPEED:{},COLOR:[]}}static get scheme(){return{danmuku:"array|function|string|promise",speed:"number",margin:"array",opacity:"number",color:"string",mode:"number",modes:"array",fontSize:"number|string",antiOverlap:"boolean",synchronousPlayback:"boolean",mount:"?htmldivelement|string",heatmap:"object|boolean",width:"number",points:"array",filter:"function",beforeEmit:"function",beforeVisible:"function",visible:"boolean",emitter:"boolean",maxLength:"number",lockTime:"number",theme:"string",OPACITY:"object",FONT_SIZE:"object",MARGIN:"object",SPEED:"object",COLOR:"array"}}static get cssText(){return'\n user-select: none;\n position: absolute;\n white-space: pre;\n pointer-events: none;\n perspective: 500px;\n display: inline-block;\n will-change: transform;\n font-weight: normal;\n line-height: 1.125;\n visibility: hidden;\n font-family: SimHei, "Microsoft JhengHei", Arial, Helvetica, sans-serif;\n text-shadow: rgb(0, 0, 0) 1px 0px 1px, rgb(0, 0, 0) 0px 1px 1px, rgb(0, 0, 0) 0px -1px 1px, rgb(0, 0, 0) -1px 0px 1px;\n '}get isRotate(){return this.art.plugins?.autoOrientation?.state}get marginTop(){const{clamp:t}=this.utils,e=this.option.margin[0],{clientHeight:i}=this.$player;if("number"===typeof e)return t(e,0,i);if("string"===typeof e&&e.endsWith("%")){return t(i*(Number.parseFloat(e)/100),0,i)}return k.option.margin[0]}get marginBottom(){const{clamp:t}=this.utils,e=this.option.margin[1],{clientHeight:i}=this.$player;if("number"===typeof e)return t(e,0,i);if("string"===typeof e&&e.endsWith("%")){return t(i*(Number.parseFloat(e)/100),0,i)}return k.option.margin[1]}get fontSize(){const{clamp:t}=this.utils,{clientHeight:e}=this.$player,i=this.option.fontSize;if("number"===typeof i)return Math.round(t(i,12,e));if("string"===typeof i&&i.endsWith("%")){const a=Number.parseFloat(i)/100;return Math.round(t(e*a,12,e))}return k.option.fontSize}get $ref(){const t=this.$refs.pop()||document.createElement("div");return t.style.cssText=k.cssText,t.dataset.mode="",t.dataset.id="",t.className="",t}get readys(){return function(t){const{currentTime:e}=t.art,i=[];return h(t,"ready",t=>i.push(t)),h(t,"wait",t=>{e+.1>=t.time&&t.time>=e-.1&&i.push(t)}),i}(this)}get visibles(){const t=[],{clientWidth:e}=this.$player,i=this.getLeft(this.$player);return this.filter("emit",a=>{const n=a.$ref.offsetTop,s=this.getLeft(a.$ref)-i,o=a.$ref.clientHeight,r=a.$ref.clientWidth,l=s+r,d=e-l,p=l/a.$restTime,h={};h.top=n,h.left=s,h.height=o,h.width=r,h.right=d,h.speed=p,h.distance=l,h.time=a.$restTime,h.mode=a.mode,t.push(h)}),t}get speed(){return this.option.synchronousPlayback&&this.art.playbackRate?this.option.speed/Number(this.art.playbackRate):this.option.speed}async load(e){const{errorHandle:i}=this.utils,a=function(t,e){const i=l(t);if(e)for(const r of[...i.tasks])r.replace&&r.cancel();let a,n=i.closed||t.art.isDestroy;const s=new Promise(t=>a=t),o=new Set,d="function"===typeof AbortController?new AbortController:void 0,p={replace:e,signal:d?.signal,active:()=>!n&&!i.closed&&!t.art.isDestroy,onCancel:t=>p.active()?(o.add(t),()=>o.delete(t)):(t(),()=>{}),cancel(){if(!n){n=!0,a(r),d?.abort();for(const t of[...o])t();o.clear()}},wait:t=>Promise.race([t,s]),emit(e){i.emitting.push(p);try{return t.emit(e)}finally{i.emitting.pop()}},finish(){i.tasks.delete(p),o.clear()}};return n?a(r):i.tasks.add(p),p}(this,void 0===e);try{if(!a.active())return this;const n=function(e,i){return"function"===typeof e?{asynchronous:!0,value:e()}:t(e)?{asynchronous:!0,value:e}:"string"===typeof e?{asynchronous:!0,value:s(e,i)}:{asynchronous:!1,value:Array.isArray(e)?e:[]}}(e||this.option.danmuku,a),o=n.asynchronous?await a.wait(n.value):n.value;if(!a.active())return this;if(i(Array.isArray(o),"Danmuku need return an array as result"),void 0===e){if(this.reset(),!a.active())return this;this.queue=[],this.states={wait:[],ready:[],emit:[],stop:[]},this.$refs=[],this.$danmuku.textContent=""}for(let t=0;t{const i=s[e],a=o[e];return"function"===typeof i||"function"===typeof a||t(i)||t(a)?i!==a:JSON.stringify(i)!==JSON.stringify(a)}))&&!i)return this;const r=function(t,e,{defaults:i,validate:a,clamp:n,mount:s}){const o=Object.assign({},i,t,e);return a(o),o.mode=n(o.mode,0,2),o.speed=n(o.speed,1,10),o.opacity=n(o.opacity,0,1),o.lockTime=n(o.lockTime,1,60),o.maxLength=n(o.maxLength,1,1e3),o.mount=o.mount||s,o}(this.option,e,{defaults:k.option,validate:t=>this.validator(t,k.scheme),clamp:a,mount:n});if(!d(this))return this;const l=this.option.beforeVisible!==r.beforeVisible;return this.option=r,l&&(this.scheduler.invalidate(),this.scheduler.schedule()),e.fontSize&&(this.reset(),!d(this))||(this.option.visible?this.show():this.hide(),d(this)&&this.art.emit("artplayerPluginDanmuku:config",this.option)),this}getLeft(t){const e=t.getBoundingClientRect();return this.isRotate?e.top:e.left}postMessage(t={}){return this.workerClient.request(t)}createWorker(){this.workerClient=new f(()=>new v,t=>this.scheduler.fail(t)),this.worker=this.workerClient.worker}filter(t,e){return h(this,t,e)}setState(t,e){!function(t,e,i){t.states[e.$state]=t.states[e.$state].filter(t=>t!==e),e.$state=i,e.$ref&&(e.$ref.dataset.state=i),t.states[i].push(e)}(this,t,e)}makeWait(t){this.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=k.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.$refs.push(t.$ref),t.$ref=null)}update(){return this.scheduler.running=!0,this.scheduler.schedule(),this}resize(){const{clientWidth:t}=this.$player;this.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.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.$player;return this.filter("stop",e=>{switch(this.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.$player;return this.filter("emit",e=>{switch(this.setState(e,"stop"),e.mode){case 0:{const i=t-(this.getLeft(e.$ref)-this.getLeft(this.$player));e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition="transform 0s linear 0s";break}}}),this}stop(){return this.isStop=!0,this.scheduler.running=!1,this.scheduler.invalidate(),this.suspend(),this.art.emit("artplayerPluginDanmuku:stop"),this}start(){if(this.scheduler.closed||this.art.isDestroy)return this;const t=++this.scheduler.starts,e=this.scheduler.generation;this.isStop=!1,this.scheduler.recover();return this.scheduler.closed||this.art.isDestroy||this.isStop||this.scheduler.fault||e!==this.scheduler.generation||t!==this.scheduler.starts||(this.continue(),this.update(),this.art.emit("artplayerPluginDanmuku:start")),this}reset(){return this.scheduler.invalidate(),this.queue.forEach(t=>this.makeWait(t)),this.art.emit("artplayerPluginDanmuku:reset"),this.scheduler.recover(),this.scheduler.schedule(),this}seek(){this.scheduler.invalidate(),this.scheduler.schedule()}show(){return this.isHide=!1,this.$danmuku.style.opacity=1,this.option.visible=!0,this.art.emit("artplayerPluginDanmuku:show"),this}hide(){return this.isHide=!0,this.scheduler.invalidate(),this.$danmuku.style.opacity=0,this.option.visible=!1,this.art.emit("artplayerPluginDanmuku:hide"),this.scheduler.schedule(),this}destroy(){p(this),this.scheduler.destroy();try{try{this.stop()}finally{this.workerClient?.dispose()}}finally{this.art.off("video:play",this.start),this.art.off("video:playing",this.start),this.art.off("video:pause",this.stop),this.art.off("video:waiting",this.stop),this.art.off("resize",this.resize),this.art.off("video:seeking",this.seek),this.art.off("destroy",this.destroy),this.art.emit("artplayerPluginDanmuku:destroy")}}}const w={map:(t,e,i,a,n)=>(t-e)*(n-a)/(i-e)+a,range(t,e,i){const a=Math.round(t/i)*i;return Array.from({length:Math.floor((e-t)/i)},(t,e)=>e*i+a)}};function x(t,e,i){const{query:a}=t.constructor.utils;t.controls.add({name:"heatmap",position:"top",html:"",style:{position:"absolute",top:"-100px",left:"0px",right:"0px",height:"100px",width:"100%",pointerEvents:"none"},mounted(n){let s=null,o=null;function r(r=[]){if(s=null,o=null,n.innerHTML="",!t.duration||t.option.isLive)return;const l={w:n.offsetWidth,h:n.offsetHeight},d={xMin:0,xMax:l.w,yMin:0,yMax:128,scale:.25,opacity:.2,minHeight:Math.floor(.05*l.h),sampling:Math.floor(l.w/100),smoothing:.2,flattening:.2};"object"===typeof i&&Object.assign(d,i);let p=[];if(Array.isArray(r)&&r.length)p=[...r];else{const i=t.duration/l.w;for(let t=0;t<=l.w;t+=d.sampling){const a=e.queue.filter(({time:e})=>e>t*i&&e<=(t+d.sampling)*i).length;p.push([t,a])}}if(0===p.length)return;const h=p[p.length-1],u=h[0],c=h[1];u!==l.w&&p.push([l.w,c]);const m=p.map(t=>t[1]),f=(Math.min(...m)+Math.max(...m))/2;for(let t=0;tf?1+d.scale:1-d.scale)+d.minHeight}const g=(t,e,i,a)=>{const n=function(t,e){const i=e[0]-t[0],a=e[1]-t[1];return{length:Math.sqrt(i**2+a**2),angle:Math.atan2(a,i)}}(e||t,i||t),s=w.map(Math.cos(n.angle)*d.flattening,0,1,1,0),o=n.angle*s+(a?Math.PI:0),r=n.length*d.smoothing;return[t[0]+Math.cos(o)*r,t[1]+Math.sin(o)*r]},y=p.map(t=>[w.map(t[0],d.xMin,d.xMax,0,l.w),w.map(t[1],d.yMin,d.yMax,l.h,0)]).reduce((t,e,i,a)=>0===i?`M ${a[a.length-1][0]},${l.h} L ${e[0]},${l.h} L ${e[0]},${e[1]}`:`${t} ${((t,e,i)=>{const a=g(i[e-1],i[e-2],t),n=g(t,i[e-1],i[e+1],!0),s=e===i.length-1?" z":"";return`C ${a[0]},${a[1]} ${n[0]},${n[1]} ${t[0]},${t[1]}${s}`})(e,i,a)}`,"");n.innerHTML=`\n \n \n \n \n \n \n \n \n \n \n \n `,s=a("#heatmap-start",n),o=a("#heatmap-stop",n),s.setAttribute("offset",100*t.played+"%"),o.setAttribute("offset",100*t.played+"%")}t.on("video:timeupdate",()=>{s&&o&&(s.setAttribute("offset",100*t.played+"%"),o.setAttribute("offset",100*t.played+"%"))}),t.on("setBar",(t,e)=>{s&&o&&"played"===t&&(s.setAttribute("offset",100*e+"%"),o.setAttribute("offset",100*e+"%"))}),t.on("ready",()=>r()),t.on("resize",()=>r()),t.on("artplayerPluginDanmuku:loaded",()=>r()),t.on("artplayerPluginDanmuku:points",t=>r(t))}})}const $='',b='',S='',M='',z='',C='',E='',D='',F='',T='',A='',L='';class P{constructor(t,e){this.art=t,this.danmuku=e,this.utils=t.constructor.utils;const{setStyle:i}=this.utils,{$controlsCenter:a}=t.template;i(a,"display","flex"),this.template={$controlsCenter:a,$mount:a,$danmuku:null,$toggle:null,$config:null,$configPanel:null,$configModes:null,$style:null,$stylePanel:null,$styleModes:null,$colors:null,$opacitySlider:null,$opacityValue:null,$marginSlider:null,$marginValue:null,$fontSizeSlider:null,$fontSizeValue:null,$speedSlider:null,$speedValue:null,$input:null,$send:null},this.slider={opacity:null,margin:null,fontSize:null,speed:null},this.emitting=!1,this.isLock=!1,this.timer=null,this.createTemplate(),this.createSliders(),this.createEvents(),this.mount(this.option.mount),t.on("resize",()=>this.resize()),t.on("fullscreen",t=>this.onFullscreen(t)),t.on("fullscreenWeb",t=>this.onFullscreen(t)),t.proxy(this.template.$config,"mouseenter",()=>{this.onMouseEnter({$control:this.template.$config,$panel:this.template.$configPanel})}),t.proxy(this.template.$style,"mouseenter",()=>{this.onMouseEnter({$control:this.template.$style,$panel:this.template.$stylePanel})})}static get icons(){return{$on:A,$off:T,$config:S,$style:L,$mode_0_off:M,$mode_0_on:z,$mode_1_off:C,$mode_1_on:E,$mode_2_off:D,$mode_2_on:F,$check_on:b,$check_off:$}}get option(){return this.danmuku.option}get outside(){return this.template.$mount!==this.template.$controlsCenter}get TEMPLATE(){const{option:t}=this;return`\n

\n ${A}${T}\n
\n
\n ${S}\n
\n
\n
\n \u6309\u7c7b\u578b\u5c4f\u853d\n
\n
\n ${M}${z}\n
\u6eda\u52a8
\n
\n
\n ${C}${E}\n
\u9876\u90e8
\n
\n
\n ${D}${F}\n
\u5e95\u90e8
\n
\n
\n
\n
\n
\n ${b}${$}\n \u9632\u6b62\u5f39\u5e55\u91cd\u53e0\n
\n
\n ${b}${$}\n \u540c\u6b65\u89c6\u9891\u901f\u5ea6\n
\n
\n
\n \u4e0d\u900f\u660e\u5ea6\n
\n
\u672a\u77e5
\n
\n
\n \u663e\u793a\u533a\u57df\n
\n
\u672a\u77e5
\n
\n
\n \u5f39\u5e55\u5b57\u53f7\n
\n
\u672a\u77e5
\n
\n
\n \u5f39\u5e55\u901f\u5ea6\n
\n
\u672a\u77e5
\n
\n
\n
\n
\n
\n
\n ${L}\n
\n
\n
\n \u6a21\u5f0f\n
\n
\n ${z}\n
\u6eda\u52a8
\n
\n
\n ${E}\n
\u9876\u90e8
\n
\n
\n ${F}\n
\u5e95\u90e8
\n
\n
\n
\n
\n \u989c\u8272\n
\n ${this.COLOR.map(t=>`
`).join("")}\n
\n
\n
\n
\n
\n \n
\u53d1\u9001
\n
\n `}get OPACITY(){return{min:0,max:100,steps:[],...this.option.OPACITY}}get FONT_SIZE(){return{min:12,max:120,steps:[],...this.option.FONT_SIZE}}get MARGIN(){return{min:0,max:3,steps:[{name:"1/4",value:[10,"75%"]},{name:"\u534a\u5c4f",value:[10,"50%"]},{name:"3/4",value:[10,"25%"]},{name:"\u6ee1\u5c4f",value:[10,10]}],...this.option.MARGIN}}get SPEED(){return{min:0,max:4,steps:[{name:"\u6781\u6162",value:10},{name:"\u8f83\u6162",value:7.5,hide:!0},{name:"\u9002\u4e2d",value:5},{name:"\u8f83\u5feb",value:2.5,hide:!0},{name:"\u6781\u5feb",value:1}],...this.option.SPEED}}get COLOR(){return this.option.COLOR.length?this.option.COLOR:["#FE0302","#FF7204","#FFAA02","#FFD302","#FFFF00","#A0EE00","#00CD00","#019899","#4266BE","#89D5FF","#CC0273","#222222","#9B9B9B","#FFFFFF"]}query(t){const{query:e}=this.utils,{$danmuku:i}=this.template;return e(t,i)}append(t,e){const{append:i}=this.utils;[...t.children].includes(e)||i(t,e)}setData(t,e){const{$player:i}=this.art.template,{$mount:a}=this.template;i.dataset[t]=e,this.outside&&(a.dataset[t]=e)}createTemplate(){const{createElement:t,tooltip:e}=this.utils,i=t("div");i.className="artplayer-plugin-danmuku",i.innerHTML=this.TEMPLATE,this.template.$danmuku=i,this.template.$toggle=this.query(".apd-toggle"),this.template.$config=this.query(".apd-config"),this.template.$configPanel=this.query(".apd-config-panel"),this.template.$configModes=this.query(".apd-config-mode .apd-modes"),this.template.$style=this.query(".apd-style"),this.template.$stylePanel=this.query(".apd-style-panel"),this.template.$styleModes=this.query(".apd-style-mode .apd-modes"),this.template.$colors=this.query(".apd-colors"),this.template.$antiOverlap=this.query(".apd-anti-overlap"),this.template.$syncVideo=this.query(".apd-sync-video"),this.template.$opacitySlider=this.query(".apd-config-opacity .apd-slider"),this.template.$opacityValue=this.query(".apd-config-opacity .apd-value"),this.template.$marginSlider=this.query(".apd-config-margin .apd-slider"),this.template.$marginValue=this.query(".apd-config-margin .apd-value"),this.template.$fontSizeSlider=this.query(".apd-config-fontSize .apd-slider"),this.template.$fontSizeValue=this.query(".apd-config-fontSize .apd-value"),this.template.$speedSlider=this.query(".apd-config-speed .apd-slider"),this.template.$speedValue=this.query(".apd-config-speed .apd-value"),this.template.$input=this.query(".apd-input"),this.template.$send=this.query(".apd-send");const{$toggle:a}=this.template;this.art.on("artplayerPluginDanmuku:show",()=>{e(a,"\u5173\u95ed\u5f39\u5e55")}),this.art.on("artplayerPluginDanmuku:hide",()=>{e(a,"\u6253\u5f00\u5f39\u5e55")})}createEvents(){const{$toggle:t,$configModes:e,$styleModes:i,$colors:a,$antiOverlap:n,$syncVideo:s,$send:o,$input:r}=this.template;this.art.proxy(t,"click",()=>{this.danmuku.config({visible:!this.option.visible}),this.reset()}),this.art.proxy(e,"click",t=>{const e=t.target.closest(".apd-mode");if(!e)return;const i=Number(e.dataset.mode);this.option.modes.includes(i)?this.danmuku.config({modes:this.option.modes.filter(t=>t!==i)}):this.danmuku.config({modes:[...this.option.modes,i]}),this.reset()}),this.art.proxy(n,"click",()=>{this.danmuku.config({antiOverlap:!this.option.antiOverlap}),this.reset()}),this.art.proxy(s,"click",()=>{this.danmuku.config({synchronousPlayback:!this.option.synchronousPlayback}),this.reset()}),this.art.proxy(i,"click",t=>{const e=t.target.closest(".apd-mode");if(!e)return;const i=Number(e.dataset.mode);this.danmuku.config({mode:i}),this.reset()}),this.art.proxy(a,"click",t=>{const e=t.target.closest(".apd-color");e&&(this.danmuku.config({color:e.dataset.color}),this.reset())}),this.art.proxy(o,"click",()=>this.emit()),this.art.proxy(r,"keypress",t=>{"Enter"===t.key&&(t.preventDefault(),this.emit())})}createSliders(){this.slider.opacity=this.createSlider({...this.OPACITY,container:this.template.$opacitySlider,findIndex:()=>Math.round(100*this.option.opacity),onChange:t=>{const{$opacityValue:e}=this.template;e.textContent=`${t}%`,this.danmuku.config({opacity:t/100})}}),this.slider.margin=this.createSlider({...this.MARGIN,container:this.template.$marginSlider,findIndex:()=>this.MARGIN.steps.findIndex(t=>t.value[0]===this.option.margin[0]&&t.value[1]===this.option.margin[1]),onChange:t=>{const e=this.MARGIN.steps[t];if(!e)return;const{$marginValue:i}=this.template;i.textContent=e.name,this.danmuku.config({margin:e.value})}}),this.slider.fontSize=this.createSlider({...this.FONT_SIZE,container:this.template.$fontSizeSlider,findIndex:()=>this.danmuku.fontSize,onChange:t=>{const{$fontSizeValue:e}=this.template;e.textContent=`${t}px`,t!==this.danmuku.fontSize&&this.danmuku.config({fontSize:t})}}),this.slider.speed=this.createSlider({...this.SPEED,container:this.template.$speedSlider,findIndex:()=>this.SPEED.steps.findIndex(t=>t.value===this.option.speed),onChange:t=>{const e=this.SPEED.steps[t];if(!e)return;const{$speedValue:i}=this.template;i.textContent=e.name,this.danmuku.config({speed:e.value})}})}createSlider({min:t,max:e,container:i,findIndex:a,onChange:n,steps:s=[]}){const{query:o,clamp:r,setStyle:l}=this.utils;l(i,"touch-action","none"),i.innerHTML=`\n
\n
\n ${s.map(()=>'
').join("")}\n
\n
\n
\n
\n
\n ${s.map(t=>t.hide?"":`
${t.name}
`).join("")}\n
\n `;const d=o(".apd-slider-dot",i),p=o(".apd-slider-progress",i);let h=!1;function u(i=a()){if(ie)return;const o=(i-t)/(e-t);d.style.left=100*o+"%",0===s.length&&(p.style.width=d.style.left),n(i)}function c(a){const{top:n,height:s,left:o,width:l}=i.getBoundingClientRect();if(this.art.isRotate){const i=r(a.clientY-n,0,s);u(Math.round(i/s*(e-t)+t))}else{const i=r(a.clientX-o,0,l);u(Math.round(i/l*(e-t)+t))}}return this.art.proxy(i,"click",t=>{c.call(this,t)}),this.art.proxy(i,"pointerdown",t=>{h=0===t.button}),this.art.on("document:pointermove",t=>{h&&c.call(this,t)}),this.art.on("document:pointerup",t=>{h&&(h=!1,c.call(this,t))}),{reset:u}}onFullscreen(t){const{$danmuku:e,$controlsCenter:i,$mount:a}=this.template;this.outside?t?this.append(i,e):this.append(a,e):this.append(i,e)}onMouseEnter({$control:t,$panel:e}){const{$player:i}=this.art.template,a=t.getBoundingClientRect(),n=e.getBoundingClientRect(),s=i.getBoundingClientRect(),o=n.width/2-a.width/2,r=s.left-(a.left-o),l=a.right+o-s.right;e.style.left=r>0?`${-o+r}px`:l>0?-o-l+"px":-o+"px"}async emit(){const{$input:t}=this.template,e=t.value.trim();if(!e.length)return;if(this.isLock)return;if(this.emitting)return;const i={text:e,mode:this.option.mode,color:this.option.color,time:this.art.currentTime};try{this.emitting=!0;const e=await this.option.beforeEmit(i);if(this.emitting=!1,!0!==e)return;i.border=!0,delete i.time,this.danmuku.emit(i),t.value="",this.lock()}catch(a){console.error("Error emitting danmuku:",a),this.emitting=!1}}lock(){const{addClass:t}=this.utils,{$send:e}=this.template;this.isLock=!0;let i=this.option.lockTime;e.textContent=i,t(e,"apd-lock");const a=()=>{this.timer=setTimeout(()=>{0===i?this.unlock():(i-=1,e.textContent=i,a())},1e3)};a()}unlock(){const{removeClass:t}=this.utils,{$send:e}=this.template;clearTimeout(this.timer),this.isLock=!1,e.textContent="\u53d1\u9001",t(e,"apd-lock")}resize(){if(this.outside)return;if(this.art.fullscreen)return;if(this.art.fullscreenWeb)return;const{$player:t,$controlsCenter:e}=this.art.template,{$danmuku:i}=this.template;this.art.widtht.dataset.color===this.option.color.toUpperCase());s&&t(s,"apd-active"),e(i,this.option.visible?"\u5173\u95ed\u5f39\u5e55":"\u6253\u5f00\u5f39\u5e55"),this.resize()}mount(t){const{errorHandle:e}=this.utils,i="string"===typeof t?document.querySelector(t):t;e(i,`Can not find the mount point: ${t}`),this.append(i,this.template.$danmuku),this.template.$mount=i,this.reset()}}if("undefined"!==typeof document){const t="artplayer-plugin-danmuku";let e=document.getElementById(t);e||(e=document.createElement("style"),e.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.appendChild(e)}):(document.head||document.documentElement).appendChild(e)),e.textContent='.artplayer-plugin-danmuku{display:flex;position:relative;z-index:99;align-items:center;justify-content:center;font-size:12px;height:32px;width:100%;color:#fff;font-weight:300;flex-shrink:0;gap:10px}.artplayer-plugin-danmuku .apd-icon{cursor:pointer;opacity:.75;transition:all .2s ease;fill:#fff}.artplayer-plugin-danmuku .apd-icon:hover{opacity:1}.artplayer-plugin-danmuku .apd-config{display:flex;position:relative}.artplayer-plugin-danmuku .apd-config .apd-config-panel{position:absolute;bottom:24px;left:0;width:320px;padding:10px;opacity:0;pointer-events:none}.artplayer-plugin-danmuku .apd-config .apd-config-panel .apd-config-panel-inner{width:100%;border-radius:3px;background-color:#000000d9;padding:10px}.artplayer-plugin-danmuku .apd-config:hover .apd-config-panel{opacity:100;pointer-events:all}.artplayer-plugin-danmuku .apd-config-mode,.artplayer-plugin-danmuku .apd-config-slider,.artplayer-plugin-danmuku .apd-config-other,.artplayer-plugin-danmuku .apd-style-mode{margin-bottom:15px}.artplayer-plugin-danmuku .apd-modes{display:flex;align-items:center;margin-top:5px;gap:20px}.artplayer-plugin-danmuku .apd-modes .apd-mode{cursor:pointer;text-align:center}.artplayer-plugin-danmuku .apd-modes .apd-mode:hover{color:#00a1d6}.artplayer-plugin-danmuku .apd-config-slider{display:flex;align-items:center;gap:12px}.artplayer-plugin-danmuku .apd-config-slider .apd-value{width:32px;text-align:right}.artplayer-plugin-danmuku .apd-slider{position:relative;flex:1;display:flex;height:20px;align-items:center;justify-content:center;cursor:pointer}.artplayer-plugin-danmuku .apd-slider .apd-slider-line{position:relative;height:2px;width:100%;overflow:hidden;border-radius:3px;background-color:#ffffff40}.artplayer-plugin-danmuku .apd-slider .apd-slider-points{position:absolute;inset:0;display:flex;align-items:center;justify-content:space-between}.artplayer-plugin-danmuku .apd-slider .apd-slider-points .apd-slider-point{width:2px;height:2px;border-radius:50%;background-color:#ffffff80}.artplayer-plugin-danmuku .apd-slider .apd-slider-progress{width:0%;height:100%;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-slider .apd-slider-dot{position:absolute;transform:translate(-6px);left:0%;width:12px;height:12px;border-radius:50%;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-slider .apd-slider-steps{display:flex;align-items:center;justify-content:space-between;position:absolute;bottom:-12px;width:calc(100% + 32px);color:#777}.artplayer-plugin-danmuku .apd-slider .apd-slider-steps .apd-slider-step{flex-shrink:0;width:36px;text-align:center;scale:.95}.artplayer-plugin-danmuku .apd-config-other{display:flex;align-items:center;gap:20px}.artplayer-plugin-danmuku .apd-config-other .apd-check-off,.artplayer-plugin-danmuku .apd-config-other .apd-check-on{width:16px;height:16px}.artplayer-plugin-danmuku .apd-config-other .apd-other{display:flex;align-items:center;cursor:pointer;gap:2px}.artplayer-plugin-danmuku .apd-config-other .apd-other:hover{color:#00a1d6}.artplayer-plugin-danmuku .apd-emitter{display:flex;flex:1;align-items:center;height:100%;background-color:#ffffff40;border-radius:5px}.artplayer-plugin-danmuku .apd-style{position:relative;display:flex;align-items:center;justify-content:center}.artplayer-plugin-danmuku .apd-style .apd-style-panel{position:absolute;bottom:24px;left:0;width:200px;padding:10px;opacity:0;pointer-events:none}.artplayer-plugin-danmuku .apd-style .apd-style-panel .apd-style-panel-inner{width:100%;border-radius:3px;background-color:#000000d9;padding:10px}.artplayer-plugin-danmuku .apd-style:hover .apd-style-panel{opacity:100;pointer-events:all}.artplayer-plugin-danmuku .apd-colors{display:flex;flex-wrap:wrap;margin-top:5px;gap:8px}.artplayer-plugin-danmuku .apd-colors .apd-color{width:16px;height:16px;border-radius:2px;cursor:pointer}.artplayer-plugin-danmuku .apd-colors .apd-color.apd-active{border:1px solid black;box-shadow:0 0 0 1px #fff}.artplayer-plugin-danmuku .apd-input{outline:none;height:100%;flex:1;min-width:0;width:auto;border:none;line-height:1;color:#fff;background-color:transparent}.artplayer-plugin-danmuku .apd-input::placeholder{color:#ffffff80}.artplayer-plugin-danmuku .apd-send{display:flex;align-items:center;justify-content:center;height:100%;width:60px;flex-shrink:0;cursor:pointer;text-shadow:none;border-top-right-radius:5px;border-bottom-right-radius:5px;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-send.apd-lock{cursor:not-allowed;color:#666;background-color:#e7e7e7}.art-controls-center .apd-emitter{width:260px;flex:none}.art-fullscreen .artplayer-plugin-danmuku,.art-fullscreen-web .artplayer-plugin-danmuku{height:38px;gap:16px}.art-fullscreen .artplayer-plugin-danmuku .apd-config-icon,.art-fullscreen-web .artplayer-plugin-danmuku .apd-config-icon,.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-off,.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-off,.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-on,.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-on{width:28px;height:28px}.art-fullscreen .artplayer-plugin-danmuku .apd-emitter,.art-fullscreen-web .artplayer-plugin-danmuku .apd-emitter{width:400px;flex:none}.art-video-player>.artplayer-plugin-danmuku{position:absolute;left:0;right:0;bottom:-40px;padding:0 10px}.art-video-player:has(>.artplayer-plugin-danmuku){margin-bottom:40px}[data-danmuku-emitter=false] .apd-emitter{display:none!important}[data-danmuku-emitter=false] .art-controls-center .artplayer-plugin-danmuku{justify-content:flex-end;gap:18px}[data-danmuku-emitter=false].art-fullscreen .art-controls-center .artplayer-plugin-danmuku,[data-danmuku-emitter=false].art-fullscreen-web .art-controls-center .artplayer-plugin-danmuku{gap:24px}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-icon{fill:#333}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-emitter{background-color:#f1f2f3}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-input{color:#000}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-input::placeholder{color:#0000004d}[data-danmuku-visible=false] .apd-toggle-off{display:block}[data-danmuku-visible=false] .apd-toggle-on,[data-danmuku-visible=true] .apd-toggle-off{display:none}[data-danmuku-visible=true] .apd-toggle-on{display:block}[data-danmuku-anti-overlap=false] .apd-anti-overlap .apd-check-on{display:none}[data-danmuku-anti-overlap=false] .apd-anti-overlap .apd-check-off,[data-danmuku-anti-overlap=true] .apd-anti-overlap .apd-check-on{display:block}[data-danmuku-anti-overlap=true] .apd-anti-overlap .apd-check-off,[data-danmuku-sync-video=false] .apd-sync-video .apd-check-on{display:none}[data-danmuku-sync-video=false] .apd-sync-video .apd-check-off,[data-danmuku-sync-video=true] .apd-sync-video .apd-check-on{display:block}[data-danmuku-sync-video=true] .apd-sync-video .apd-check-off{display:none}[data-danmuku-mode0=false] .apd-config-mode .apd-mode-0-off{display:block}[data-danmuku-mode0=false] .apd-config-mode .apd-mode-0-on{display:none}[data-danmuku-mode0=false] .art-danmuku [data-mode="0"]{opacity:0!important}[data-danmuku-mode0=true] .apd-config-mode .apd-mode-0-off{display:none}[data-danmuku-mode0=true] .apd-config-mode .apd-mode-0-on{display:block}[data-danmuku-mode="0"] .apd-style-mode [data-mode="0"]{color:#00a1d6}[data-danmuku-mode="0"] .apd-style-mode [data-mode="0"] path{fill:#00a1d6}[data-danmuku-mode1=false] .apd-config-mode .apd-mode-1-off{display:block}[data-danmuku-mode1=false] .apd-config-mode .apd-mode-1-on{display:none}[data-danmuku-mode1=false] .art-danmuku [data-mode="1"]{opacity:0!important}[data-danmuku-mode1=true] .apd-config-mode .apd-mode-1-off{display:none}[data-danmuku-mode1=true] .apd-config-mode .apd-mode-1-on{display:block}[data-danmuku-mode="1"] .apd-style-mode [data-mode="1"]{color:#00a1d6}[data-danmuku-mode="1"] .apd-style-mode [data-mode="1"] path{fill:#00a1d6}[data-danmuku-mode2=false] .apd-config-mode .apd-mode-2-off{display:block}[data-danmuku-mode2=false] .apd-config-mode .apd-mode-2-on{display:none}[data-danmuku-mode2=false] .art-danmuku [data-mode="2"]{opacity:0!important}[data-danmuku-mode2=true] .apd-config-mode .apd-mode-2-off{display:none}[data-danmuku-mode2=true] .apd-config-mode .apd-mode-2-on{display:block}[data-danmuku-mode="2"] .apd-style-mode [data-mode="2"]{color:#00a1d6}[data-danmuku-mode="2"] .apd-style-mode [data-mode="2"] path{fill:#00a1d6}'}function H(t){return e=>{const i=new k(e,t),a=e.isDestroy?void 0:new P(e,i);return!e.isDestroy&&i.option.heatmap&&x(e,i,i.option.heatmap),{name:"artplayerPluginDanmuku",emit:i.emit.bind(i),load:i.load.bind(i),config:i.config.bind(i),hide:i.hide.bind(i),show:i.show.bind(i),reset:i.reset.bind(i),mount:a?a.mount.bind(a):()=>{},get option(){return i.option},get isHide(){return i.isHide},get isStop(){return i.isStop}}}}return H.icons=P.icons,H}); diff --git a/docs/compiled/artplayer-plugin-danmuku.legacy.js b/docs/compiled/artplayer-plugin-danmuku.legacy.js index db0f66315..0052ae2a5 100644 --- a/docs/compiled/artplayer-plugin-danmuku.legacy.js +++ b/docs/compiled/artplayer-plugin-danmuku.legacy.js @@ -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,a=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,o=Math.pow,r=(e,i,a)=>i in e?t(e,i,{enumerable:!0,configurable:!0,writable:!0,value:a}):e[i]=a,l=(t,e)=>{for(var i in e||(e={}))n.call(e,i)&&r(t,i,e[i]);if(a)for(var i of a(e))s.call(e,i)&&r(t,i,e[i]);return t},d=(t,a)=>e(t,i(a)),p=(t,e,i)=>new Promise((a,n)=>{var s=t=>{try{r(i.next(t))}catch(e){n(e)}},o=t=>{try{r(i.throw(t))}catch(e){n(e)}},r=t=>t.done?a(t.value):Promise.resolve(t.value).then(s,o);r((i=i.apply(t,e)).next())});function h(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function u(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function c(t){if("string"!==typeof t)return[];const e=new RegExp(']*?p="(?

[^"]+)"[^>]*>(?.*?)<\\/d>',"gs"),i=t.matchAll(e);return Array.from(i).map(t=>{const e=t.groups.p.split(",");if(e.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(e[0]),mode:u(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,a=c(e);globalThis.postMessage({danmus:a,id:i})}let f=0;function g(t,{signal:e,onCancel:i}={}){return new Promise((a,n)=>{let s,o,r,l=!1;const d=++f,h=()=>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!==o){const a=o;o=void 0;try{URL.revokeObjectURL(a)}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",h),null==r||r();try{g()}catch(o){s||(i=o),s=!0}s?n(i):a(t)}function v(t,e){if(!l){console.error("Error parsing Bilibili Danmu:",e);try{y(c(t))}catch(i){y(void 0,i)}}}(null==e?void 0:e.aborted)?h():(null==e||e.addEventListener("abort",h),r=null==i?void 0:i(h),l?null==r||r():function(){return p(this,null,function*(){const i=e?yield fetch(t,{signal:e}):yield fetch(t);if(l)return;const a=yield i.text();if(!l)try{const t=`\n ${u.toString()}\n ${c.toString()}\n onmessage = ${m.toString()}\n `;if(o=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),l)return void g();if(s=new Worker(o),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(a,t.error||t)},s.onmessageerror=t=>v(a,t.error||t),s.postMessage({xml:a,id:d})}catch(n){v(a,n)}})}().catch(t=>y(void 0,t)))})}const y=new WeakMap,v=Symbol("cancelled danmuku input");function k(t){return y.has(t)||y.set(t,{closed:!1,tasks:new Set,emitting:[]}),y.get(t)}function x(t){const e=k(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function $(t){const e=k(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}const w='/*!\n * artplayer-plugin-danmuku.js v5.3.0\n * Github: https://github.com/zhw2590582/ArtPlayer\n * (c) 2017-2026 Harvey Zhao\n * Released under the MIT License.\n */\nfunction t({target:t,visibles:e,clientWidth:i,clientHeight:h,marginBottom:n,marginTop:o,antiOverlap:s}){const r=h-n,p=e.filter(e=>e.mode===t.mode&&e.top<=r).sort((t,e)=>t.top-e.top);if(0===p.length)return 2===t.mode?r-t.height:o;if(p.unshift({type:"top",top:0,left:0,right:0,height:o,width:i,speed:0,distance:i}),p.push({type:"bottom",top:r,left:0,right:0,height:n,width:i,speed:0,distance:i}),2===t.mode)for(let g=p.length-2;g>=0;g-=1){const e=p[g],i=p[g+1],h=e.top+e.height;if(i.top-h>=t.height)return i.top-t.height}else for(let g=1;g=t.height)return h}const a=[];for(let g=1;g{const i=Math.min(...e.map(t=>t.right)),h=Math.min(...t.map(t=>t.right));return i*e.length-h*t.length});break;case 1:case 2:a.sort((t,e)=>{const i=Math.max(...e.map(t=>t.width));return Math.max(...t.map(t=>t.width))*t.length-i*e.length})}return a[0][0].top}switch(t.mode){case 0:{const e=a.find(e=>e.every(e=>{if(ie.time}));return e&&e[0]?e[0].top:void 0}case 1:case 2:return}}onmessage=e=>{const{data:i}=e;if(!i.id||!i.type)return;const h=(0,{getDanmuTop:t}[i.type])(i);globalThis.postMessage({result:h,id:i.id})};\n',b="undefined"!==typeof self&&self.Blob&&new Blob(["URL.revokeObjectURL(import.meta.url);",w],{type:"text/javascript;charset=utf-8"});function S(t){let e;try{if(e=b&&(self.URL||self.webkitURL).createObjectURL(b),!e)throw"";const i=new Worker(e,{type:"module",name:null==t?void 0:t.name});return i.addEventListener("error",()=>{(self.URL||self.webkitURL).revokeObjectURL(e)}),i}catch(i){return new Worker("data:text/javascript;charset=utf-8,"+encodeURIComponent(w),{type:"module",name:null==t?void 0:t.name})}}class M{constructor(t,e){const{constructor:i,template:a}=t;this.utils=i.utils,this.validator=i.validator,this.$danmuku=a.$danmuku,this.$player=a.$player,this.art=t,this.queue=[],this.$refs=[],this.isStop=!1,this.isHide=!1,this.timer=null,this.index=0,this.worker=null,this.option=M.option,this.states={wait:[],ready:[],emit:[],stop:[]},this.start=this.start.bind(this),this.stop=this.stop.bind(this),this.reset=this.reset.bind(this),this.resize=this.resize.bind(this),this.destroy=this.destroy.bind(this),x(this),t.on("destroy",this.destroy);try{if(this.config(e,!0),!x(this))return;this.worker=new S}catch(n){throw t.off("destroy",this.destroy),$(this),n}t.on("video:play",this.start),t.on("video:playing",this.start),t.on("video:pause",this.stop),t.on("video:waiting",this.stop),t.on("resize",this.resize),this.load().catch(t=>console.warn("Failed to load initial danmuku:",t))}static get option(){return{danmuku:[],speed:5,margin:[10,"25%"],opacity:1,color:"#FFFFFF",mode:0,modes:[0,1,2],fontSize:25,antiOverlap:!0,synchronousPlayback:!1,mount:void 0,heatmap:!1,width:512,points:[],filter:()=>!0,beforeEmit:()=>!0,beforeVisible:()=>!0,visible:!0,emitter:!0,maxLength:200,lockTime:5,theme:"dark",OPACITY:{},FONT_SIZE:{},MARGIN:{},SPEED:{},COLOR:[]}}static get scheme(){return{danmuku:"array|function|string|promise",speed:"number",margin:"array",opacity:"number",color:"string",mode:"number",modes:"array",fontSize:"number|string",antiOverlap:"boolean",synchronousPlayback:"boolean",mount:"?htmldivelement|string",heatmap:"object|boolean",width:"number",points:"array",filter:"function",beforeEmit:"function",beforeVisible:"function",visible:"boolean",emitter:"boolean",maxLength:"number",lockTime:"number",theme:"string",OPACITY:"object",FONT_SIZE:"object",MARGIN:"object",SPEED:"object",COLOR:"array"}}static get cssText(){return'\n user-select: none;\n position: absolute;\n white-space: pre;\n pointer-events: none;\n perspective: 500px;\n display: inline-block;\n will-change: transform;\n font-weight: normal;\n line-height: 1.125;\n visibility: hidden;\n font-family: SimHei, "Microsoft JhengHei", Arial, Helvetica, sans-serif;\n text-shadow: rgb(0, 0, 0) 1px 0px 1px, rgb(0, 0, 0) 0px 1px 1px, rgb(0, 0, 0) 0px -1px 1px, rgb(0, 0, 0) -1px 0px 1px;\n '}get isRotate(){var t,e;return null==(e=null==(t=this.art.plugins)?void 0:t.autoOrientation)?void 0:e.state}get marginTop(){const{clamp:t}=this.utils,e=this.option.margin[0],{clientHeight:i}=this.$player;if("number"===typeof e)return t(e,0,i);if("string"===typeof e&&e.endsWith("%")){return t(i*(Number.parseFloat(e)/100),0,i)}return M.option.margin[0]}get marginBottom(){const{clamp:t}=this.utils,e=this.option.margin[1],{clientHeight:i}=this.$player;if("number"===typeof e)return t(e,0,i);if("string"===typeof e&&e.endsWith("%")){return t(i*(Number.parseFloat(e)/100),0,i)}return M.option.margin[1]}get fontSize(){const{clamp:t}=this.utils,{clientHeight:e}=this.$player,i=this.option.fontSize;if("number"===typeof i)return Math.round(t(i,12,e));if("string"===typeof i&&i.endsWith("%")){const a=Number.parseFloat(i)/100;return Math.round(t(e*a,12,e))}return M.option.fontSize}get $ref(){const t=this.$refs.pop()||document.createElement("div");return t.style.cssText=M.cssText,t.dataset.mode="",t.dataset.id="",t.className="",t}get readys(){const{currentTime:t}=this.art,e=[];return this.filter("ready",t=>e.push(t)),this.filter("wait",i=>{t+.1>=i.time&&i.time>=t-.1&&e.push(i)}),e}get visibles(){const t=[],{clientWidth:e}=this.$player,i=this.getLeft(this.$player);return this.filter("emit",a=>{const n=a.$ref.offsetTop,s=this.getLeft(a.$ref)-i,o=a.$ref.clientHeight,r=a.$ref.clientWidth,l=s+r,d=e-l,p=l/a.$restTime,h={};h.top=n,h.left=s,h.height=o,h.width=r,h.right=d,h.speed=p,h.distance=l,h.time=a.$restTime,h.mode=a.mode,t.push(h)}),t}get speed(){return this.option.synchronousPlayback&&this.art.playbackRate?this.option.speed/Number(this.art.playbackRate):this.option.speed}load(t){return p(this,null,function*(){const{errorHandle:e}=this.utils,i=function(t,e){const i=k(t);if(e)for(const d of[...i.tasks])d.replace&&d.cancel();let a,n=i.closed||t.art.isDestroy;const s=new Promise(t=>a=t),o=new Set,r="function"===typeof AbortController?new AbortController:void 0,l={replace:e,signal:null==r?void 0:r.signal,active:()=>!n&&!i.closed&&!t.art.isDestroy,onCancel:t=>l.active()?(o.add(t),()=>o.delete(t)):(t(),()=>{}),cancel(){if(!n){n=!0,a(v),null==r||r.abort();for(const t of[...o])t();o.clear()}},wait:t=>Promise.race([t,s]),emit(e){i.emitting.push(l);try{return t.emit(e)}finally{i.emitting.pop()}},finish(){i.tasks.delete(l),o.clear()}};return n?a(v):i.tasks.add(l),l}(this,void 0===t);try{if(!i.active())return this;const a=function(t,e){return"function"===typeof t?{asynchronous:!0,value:t()}:h(t)?{asynchronous:!0,value:t}:"string"===typeof t?{asynchronous:!0,value:g(t,e)}:{asynchronous:!1,value:Array.isArray(t)?t:[]}}(t||this.option.danmuku,i),n=a.asynchronous?yield i.wait(a.value):a.value;if(!i.active())return this;if(e(Array.isArray(n),"Danmuku need return an array as result"),void 0===t){if(this.reset(),!i.active())return this;this.queue=[],this.states={wait:[],ready:[],emit:[],stop:[]},this.$refs=[],this.$danmuku.textContent=""}for(let t=0;t{const e=n[t],i=s[t];return"function"===typeof e||"function"===typeof i||h(e)||h(i)?e!==i:JSON.stringify(e)!==JSON.stringify(i)}))&&!e)return this;const o=function(t,e,{defaults:i,validate:a,clamp:n,mount:s}){const o=Object.assign({},i,t,e);return a(o),o.mode=n(o.mode,0,2),o.speed=n(o.speed,1,10),o.opacity=n(o.opacity,0,1),o.lockTime=n(o.lockTime,1,60),o.maxLength=n(o.maxLength,1,1e3),o.mount=o.mount||s,o}(this.option,t,{defaults:M.option,validate:t=>this.validator(t,M.scheme),clamp:i,mount:a});return x(this)?(this.option=o,t.fontSize&&(this.reset(),!x(this))||(this.option.visible?this.show():this.hide(),x(this)&&this.art.emit("artplayerPluginDanmuku:config",this.option)),this):this}getLeft(t){const e=t.getBoundingClientRect();return this.isRotate?e.top:e.left}postMessage(t={}){return new Promise(e=>{t.id=Date.now(),this.worker.postMessage(t),this.worker.onmessage=i=>{const{data:a}=i;a.id===t.id&&e(a)}})}filter(t,e){const i=this.states[t]||[];for(let a=0;ae!==t),t.$state=e,t.$ref&&(t.$ref.dataset.state=e),this.states[e].push(t)}makeWait(t){this.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=M.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.$refs.push(t.$ref),t.$ref=null)}update(){const{setStyles:t}=this.utils;return this.timer=window.requestAnimationFrame(()=>p(this,null,function*(){if(this.art.playing&&!this.isHide){this.filter("emit",t=>{const e=(Date.now()-t.$lastStartTime)/1e3;t.$restTime-=e,t.$lastStartTime=Date.now(),t.$restTime<=0&&this.makeWait(t)});const e=this.readys;for(let i=0;i{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.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.$player;return this.filter("stop",e=>{switch(this.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.$player;return this.filter("emit",e=>{switch(this.setState(e,"stop"),e.mode){case 0:{const i=t-(this.getLeft(e.$ref)-this.getLeft(this.$player));e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition="transform 0s linear 0s";break}}}),this}stop(){return this.isStop=!0,this.suspend(),window.cancelAnimationFrame(this.timer),this.art.emit("artplayerPluginDanmuku:stop"),this}start(){return this.isStop=!1,this.continue(),this.update(),this.art.emit("artplayerPluginDanmuku:start"),this}reset(){return this.queue.forEach(t=>this.makeWait(t)),this.art.emit("artplayerPluginDanmuku:reset"),this}show(){return this.isHide=!1,this.$danmuku.style.opacity=1,this.option.visible=!0,this.art.emit("artplayerPluginDanmuku:show"),this}hide(){return this.isHide=!0,this.$danmuku.style.opacity=0,this.option.visible=!1,this.art.emit("artplayerPluginDanmuku:hide"),this}destroy(){var t;$(this),this.stop(),null==(t=this.worker)||t.terminate(),this.art.off("video:play",this.start),this.art.off("video:playing",this.start),this.art.off("video:pause",this.stop),this.art.off("video:waiting",this.stop),this.art.off("resize",this.resize),this.art.off("destroy",this.destroy),this.art.emit("artplayerPluginDanmuku:destroy")}}const z={map:(t,e,i,a,n)=>(t-e)*(n-a)/(i-e)+a,range(t,e,i){const a=Math.round(t/i)*i;return Array.from({length:Math.floor((e-t)/i)},(t,e)=>e*i+a)}};function E(t,e,i){const{query:a}=t.constructor.utils;t.controls.add({name:"heatmap",position:"top",html:"",style:{position:"absolute",top:"-100px",left:"0px",right:"0px",height:"100px",width:"100%",pointerEvents:"none"},mounted(n){let s=null,r=null;function l(l=[]){if(s=null,r=null,n.innerHTML="",!t.duration||t.option.isLive)return;const d={w:n.offsetWidth,h:n.offsetHeight},p={xMin:0,xMax:d.w,yMin:0,yMax:128,scale:.25,opacity:.2,minHeight:Math.floor(.05*d.h),sampling:Math.floor(d.w/100),smoothing:.2,flattening:.2};"object"===typeof i&&Object.assign(p,i);let h=[];if(Array.isArray(l)&&l.length)h=[...l];else{const i=t.duration/d.w;for(let t=0;t<=d.w;t+=p.sampling){const a=e.queue.filter(({time:e})=>e>t*i&&e<=(t+p.sampling)*i).length;h.push([t,a])}}if(0===h.length)return;const u=h[h.length-1],c=u[0],m=u[1];c!==d.w&&h.push([d.w,m]);const f=h.map(t=>t[1]),g=(Math.min(...f)+Math.max(...f))/2;for(let t=0;tg?1+p.scale:1-p.scale)+p.minHeight}const y=(t,e,i,a)=>{const n=function(t,e){const i=e[0]-t[0],a=e[1]-t[1];return{length:Math.sqrt(o(i,2)+o(a,2)),angle:Math.atan2(a,i)}}(e||t,i||t),s=z.map(Math.cos(n.angle)*p.flattening,0,1,1,0),r=n.angle*s+(a?Math.PI:0),l=n.length*p.smoothing;return[t[0]+Math.cos(r)*l,t[1]+Math.sin(r)*l]},v=h.map(t=>[z.map(t[0],p.xMin,p.xMax,0,d.w),z.map(t[1],p.yMin,p.yMax,d.h,0)]).reduce((t,e,i,a)=>0===i?`M ${a[a.length-1][0]},${d.h} L ${e[0]},${d.h} L ${e[0]},${e[1]}`:`${t} ${((t,e,i)=>{const a=y(i[e-1],i[e-2],t),n=y(t,i[e-1],i[e+1],!0),s=e===i.length-1?" z":"";return`C ${a[0]},${a[1]} ${n[0]},${n[1]} ${t[0]},${t[1]}${s}`})(e,i,a)}`,"");n.innerHTML=`\n \n \n \n \n \n \n \n \n \n \n \n `,s=a("#heatmap-start",n),r=a("#heatmap-stop",n),s.setAttribute("offset",100*t.played+"%"),r.setAttribute("offset",100*t.played+"%")}t.on("video:timeupdate",()=>{s&&r&&(s.setAttribute("offset",100*t.played+"%"),r.setAttribute("offset",100*t.played+"%"))}),t.on("setBar",(t,e)=>{s&&r&&"played"===t&&(s.setAttribute("offset",100*e+"%"),r.setAttribute("offset",100*e+"%"))}),t.on("ready",()=>l()),t.on("resize",()=>l()),t.on("artplayerPluginDanmuku:loaded",()=>l()),t.on("artplayerPluginDanmuku:points",t=>l(t))}})}const C='',T='',F='',A='',D='',P='',L='',O='',H='',V='',j='',R='';class B{constructor(t,e){this.art=t,this.danmuku=e,this.utils=t.constructor.utils;const{setStyle:i}=this.utils,{$controlsCenter:a}=t.template;i(a,"display","flex"),this.template={$controlsCenter:a,$mount:a,$danmuku:null,$toggle:null,$config:null,$configPanel:null,$configModes:null,$style:null,$stylePanel:null,$styleModes:null,$colors:null,$opacitySlider:null,$opacityValue:null,$marginSlider:null,$marginValue:null,$fontSizeSlider:null,$fontSizeValue:null,$speedSlider:null,$speedValue:null,$input:null,$send:null},this.slider={opacity:null,margin:null,fontSize:null,speed:null},this.emitting=!1,this.isLock=!1,this.timer=null,this.createTemplate(),this.createSliders(),this.createEvents(),this.mount(this.option.mount),t.on("resize",()=>this.resize()),t.on("fullscreen",t=>this.onFullscreen(t)),t.on("fullscreenWeb",t=>this.onFullscreen(t)),t.proxy(this.template.$config,"mouseenter",()=>{this.onMouseEnter({$control:this.template.$config,$panel:this.template.$configPanel})}),t.proxy(this.template.$style,"mouseenter",()=>{this.onMouseEnter({$control:this.template.$style,$panel:this.template.$stylePanel})})}static get icons(){return{$on:j,$off:V,$config:F,$style:R,$mode_0_off:A,$mode_0_on:D,$mode_1_off:P,$mode_1_on:L,$mode_2_off:O,$mode_2_on:H,$check_on:T,$check_off:C}}get option(){return this.danmuku.option}get outside(){return this.template.$mount!==this.template.$controlsCenter}get TEMPLATE(){const{option:t}=this;return`\n

\n ${j}${V}\n
\n
\n ${F}\n
\n
\n
\n \u6309\u7c7b\u578b\u5c4f\u853d\n
\n
\n ${A}${D}\n
\u6eda\u52a8
\n
\n
\n ${P}${L}\n
\u9876\u90e8
\n
\n
\n ${O}${H}\n
\u5e95\u90e8
\n
\n
\n
\n
\n
\n ${T}${C}\n \u9632\u6b62\u5f39\u5e55\u91cd\u53e0\n
\n
\n ${T}${C}\n \u540c\u6b65\u89c6\u9891\u901f\u5ea6\n
\n
\n
\n \u4e0d\u900f\u660e\u5ea6\n
\n
\u672a\u77e5
\n
\n
\n \u663e\u793a\u533a\u57df\n
\n
\u672a\u77e5
\n
\n
\n \u5f39\u5e55\u5b57\u53f7\n
\n
\u672a\u77e5
\n
\n
\n \u5f39\u5e55\u901f\u5ea6\n
\n
\u672a\u77e5
\n
\n
\n
\n
\n
\n
\n ${R}\n
\n
\n
\n \u6a21\u5f0f\n
\n
\n ${D}\n
\u6eda\u52a8
\n
\n
\n ${L}\n
\u9876\u90e8
\n
\n
\n ${H}\n
\u5e95\u90e8
\n
\n
\n
\n
\n \u989c\u8272\n
\n ${this.COLOR.map(t=>`
`).join("")}\n
\n
\n
\n
\n
\n \n
\u53d1\u9001
\n
\n `}get OPACITY(){return l({min:0,max:100,steps:[]},this.option.OPACITY)}get FONT_SIZE(){return l({min:12,max:120,steps:[]},this.option.FONT_SIZE)}get MARGIN(){return l({min:0,max:3,steps:[{name:"1/4",value:[10,"75%"]},{name:"\u534a\u5c4f",value:[10,"50%"]},{name:"3/4",value:[10,"25%"]},{name:"\u6ee1\u5c4f",value:[10,10]}]},this.option.MARGIN)}get SPEED(){return l({min:0,max:4,steps:[{name:"\u6781\u6162",value:10},{name:"\u8f83\u6162",value:7.5,hide:!0},{name:"\u9002\u4e2d",value:5},{name:"\u8f83\u5feb",value:2.5,hide:!0},{name:"\u6781\u5feb",value:1}]},this.option.SPEED)}get COLOR(){return this.option.COLOR.length?this.option.COLOR:["#FE0302","#FF7204","#FFAA02","#FFD302","#FFFF00","#A0EE00","#00CD00","#019899","#4266BE","#89D5FF","#CC0273","#222222","#9B9B9B","#FFFFFF"]}query(t){const{query:e}=this.utils,{$danmuku:i}=this.template;return e(t,i)}append(t,e){const{append:i}=this.utils;[...t.children].includes(e)||i(t,e)}setData(t,e){const{$player:i}=this.art.template,{$mount:a}=this.template;i.dataset[t]=e,this.outside&&(a.dataset[t]=e)}createTemplate(){const{createElement:t,tooltip:e}=this.utils,i=t("div");i.className="artplayer-plugin-danmuku",i.innerHTML=this.TEMPLATE,this.template.$danmuku=i,this.template.$toggle=this.query(".apd-toggle"),this.template.$config=this.query(".apd-config"),this.template.$configPanel=this.query(".apd-config-panel"),this.template.$configModes=this.query(".apd-config-mode .apd-modes"),this.template.$style=this.query(".apd-style"),this.template.$stylePanel=this.query(".apd-style-panel"),this.template.$styleModes=this.query(".apd-style-mode .apd-modes"),this.template.$colors=this.query(".apd-colors"),this.template.$antiOverlap=this.query(".apd-anti-overlap"),this.template.$syncVideo=this.query(".apd-sync-video"),this.template.$opacitySlider=this.query(".apd-config-opacity .apd-slider"),this.template.$opacityValue=this.query(".apd-config-opacity .apd-value"),this.template.$marginSlider=this.query(".apd-config-margin .apd-slider"),this.template.$marginValue=this.query(".apd-config-margin .apd-value"),this.template.$fontSizeSlider=this.query(".apd-config-fontSize .apd-slider"),this.template.$fontSizeValue=this.query(".apd-config-fontSize .apd-value"),this.template.$speedSlider=this.query(".apd-config-speed .apd-slider"),this.template.$speedValue=this.query(".apd-config-speed .apd-value"),this.template.$input=this.query(".apd-input"),this.template.$send=this.query(".apd-send");const{$toggle:a}=this.template;this.art.on("artplayerPluginDanmuku:show",()=>{e(a,"\u5173\u95ed\u5f39\u5e55")}),this.art.on("artplayerPluginDanmuku:hide",()=>{e(a,"\u6253\u5f00\u5f39\u5e55")})}createEvents(){const{$toggle:t,$configModes:e,$styleModes:i,$colors:a,$antiOverlap:n,$syncVideo:s,$send:o,$input:r}=this.template;this.art.proxy(t,"click",()=>{this.danmuku.config({visible:!this.option.visible}),this.reset()}),this.art.proxy(e,"click",t=>{const e=t.target.closest(".apd-mode");if(!e)return;const i=Number(e.dataset.mode);this.option.modes.includes(i)?this.danmuku.config({modes:this.option.modes.filter(t=>t!==i)}):this.danmuku.config({modes:[...this.option.modes,i]}),this.reset()}),this.art.proxy(n,"click",()=>{this.danmuku.config({antiOverlap:!this.option.antiOverlap}),this.reset()}),this.art.proxy(s,"click",()=>{this.danmuku.config({synchronousPlayback:!this.option.synchronousPlayback}),this.reset()}),this.art.proxy(i,"click",t=>{const e=t.target.closest(".apd-mode");if(!e)return;const i=Number(e.dataset.mode);this.danmuku.config({mode:i}),this.reset()}),this.art.proxy(a,"click",t=>{const e=t.target.closest(".apd-color");e&&(this.danmuku.config({color:e.dataset.color}),this.reset())}),this.art.proxy(o,"click",()=>this.emit()),this.art.proxy(r,"keypress",t=>{"Enter"===t.key&&(t.preventDefault(),this.emit())})}createSliders(){this.slider.opacity=this.createSlider(d(l({},this.OPACITY),{container:this.template.$opacitySlider,findIndex:()=>Math.round(100*this.option.opacity),onChange:t=>{const{$opacityValue:e}=this.template;e.textContent=`${t}%`,this.danmuku.config({opacity:t/100})}})),this.slider.margin=this.createSlider(d(l({},this.MARGIN),{container:this.template.$marginSlider,findIndex:()=>this.MARGIN.steps.findIndex(t=>t.value[0]===this.option.margin[0]&&t.value[1]===this.option.margin[1]),onChange:t=>{const e=this.MARGIN.steps[t];if(!e)return;const{$marginValue:i}=this.template;i.textContent=e.name,this.danmuku.config({margin:e.value})}})),this.slider.fontSize=this.createSlider(d(l({},this.FONT_SIZE),{container:this.template.$fontSizeSlider,findIndex:()=>this.danmuku.fontSize,onChange:t=>{const{$fontSizeValue:e}=this.template;e.textContent=`${t}px`,t!==this.danmuku.fontSize&&this.danmuku.config({fontSize:t})}})),this.slider.speed=this.createSlider(d(l({},this.SPEED),{container:this.template.$speedSlider,findIndex:()=>this.SPEED.steps.findIndex(t=>t.value===this.option.speed),onChange:t=>{const e=this.SPEED.steps[t];if(!e)return;const{$speedValue:i}=this.template;i.textContent=e.name,this.danmuku.config({speed:e.value})}}))}createSlider({min:t,max:e,container:i,findIndex:a,onChange:n,steps:s=[]}){const{query:o,clamp:r,setStyle:l}=this.utils;l(i,"touch-action","none"),i.innerHTML=`\n
\n
\n ${s.map(()=>'
').join("")}\n
\n
\n
\n
\n
\n ${s.map(t=>t.hide?"":`
${t.name}
`).join("")}\n
\n `;const d=o(".apd-slider-dot",i),p=o(".apd-slider-progress",i);let h=!1;function u(i=a()){if(ie)return;const o=(i-t)/(e-t);d.style.left=100*o+"%",0===s.length&&(p.style.width=d.style.left),n(i)}function c(a){const{top:n,height:s,left:o,width:l}=i.getBoundingClientRect();if(this.art.isRotate){const i=r(a.clientY-n,0,s);u(Math.round(i/s*(e-t)+t))}else{const i=r(a.clientX-o,0,l);u(Math.round(i/l*(e-t)+t))}}return this.art.proxy(i,"click",t=>{c.call(this,t)}),this.art.proxy(i,"pointerdown",t=>{h=0===t.button}),this.art.on("document:pointermove",t=>{h&&c.call(this,t)}),this.art.on("document:pointerup",t=>{h&&(h=!1,c.call(this,t))}),{reset:u}}onFullscreen(t){const{$danmuku:e,$controlsCenter:i,$mount:a}=this.template;this.outside?t?this.append(i,e):this.append(a,e):this.append(i,e)}onMouseEnter({$control:t,$panel:e}){const{$player:i}=this.art.template,a=t.getBoundingClientRect(),n=e.getBoundingClientRect(),s=i.getBoundingClientRect(),o=n.width/2-a.width/2,r=s.left-(a.left-o),l=a.right+o-s.right;e.style.left=r>0?`${-o+r}px`:l>0?-o-l+"px":-o+"px"}emit(){return p(this,null,function*(){const{$input:t}=this.template,e=t.value.trim();if(!e.length)return;if(this.isLock)return;if(this.emitting)return;const i={text:e,mode:this.option.mode,color:this.option.color,time:this.art.currentTime};try{this.emitting=!0;const e=yield this.option.beforeEmit(i);if(this.emitting=!1,!0!==e)return;i.border=!0,delete i.time,this.danmuku.emit(i),t.value="",this.lock()}catch(a){console.error("Error emitting danmuku:",a),this.emitting=!1}})}lock(){const{addClass:t}=this.utils,{$send:e}=this.template;this.isLock=!0;let i=this.option.lockTime;e.textContent=i,t(e,"apd-lock");const a=()=>{this.timer=setTimeout(()=>{0===i?this.unlock():(i-=1,e.textContent=i,a())},1e3)};a()}unlock(){const{removeClass:t}=this.utils,{$send:e}=this.template;clearTimeout(this.timer),this.isLock=!1,e.textContent="\u53d1\u9001",t(e,"apd-lock")}resize(){if(this.outside)return;if(this.art.fullscreen)return;if(this.art.fullscreenWeb)return;const{$player:t,$controlsCenter:e}=this.art.template,{$danmuku:i}=this.template;this.art.widtht.dataset.color===this.option.color.toUpperCase());s&&t(s,"apd-active"),e(i,this.option.visible?"\u5173\u95ed\u5f39\u5e55":"\u6253\u5f00\u5f39\u5e55"),this.resize()}mount(t){const{errorHandle:e}=this.utils,i="string"===typeof t?document.querySelector(t):t;e(i,`Can not find the mount point: ${t}`),this.append(i,this.template.$danmuku),this.template.$mount=i,this.reset()}}if("undefined"!==typeof document){const t="artplayer-plugin-danmuku";let e=document.getElementById(t);e||(e=document.createElement("style"),e.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.appendChild(e)}):(document.head||document.documentElement).appendChild(e)),e.textContent='.artplayer-plugin-danmuku{display:flex;position:relative;z-index:99;align-items:center;justify-content:center;font-size:12px;height:32px;width:100%;color:#fff;font-weight:300;flex-shrink:0;gap:10px}.artplayer-plugin-danmuku .apd-icon{cursor:pointer;opacity:.75;transition:all .2s ease;fill:#fff}.artplayer-plugin-danmuku .apd-icon:hover{opacity:1}.artplayer-plugin-danmuku .apd-config{display:flex;position:relative}.artplayer-plugin-danmuku .apd-config .apd-config-panel{position:absolute;bottom:24px;left:0;width:320px;padding:10px;opacity:0;pointer-events:none}.artplayer-plugin-danmuku .apd-config .apd-config-panel .apd-config-panel-inner{width:100%;border-radius:3px;background-color:#000000d9;padding:10px}.artplayer-plugin-danmuku .apd-config:hover .apd-config-panel{opacity:100;pointer-events:all}.artplayer-plugin-danmuku .apd-config-mode,.artplayer-plugin-danmuku .apd-config-slider,.artplayer-plugin-danmuku .apd-config-other,.artplayer-plugin-danmuku .apd-style-mode{margin-bottom:15px}.artplayer-plugin-danmuku .apd-modes{display:flex;align-items:center;margin-top:5px;gap:20px}.artplayer-plugin-danmuku .apd-modes .apd-mode{cursor:pointer;text-align:center}.artplayer-plugin-danmuku .apd-modes .apd-mode:hover{color:#00a1d6}.artplayer-plugin-danmuku .apd-config-slider{display:flex;align-items:center;gap:12px}.artplayer-plugin-danmuku .apd-config-slider .apd-value{width:32px;text-align:right}.artplayer-plugin-danmuku .apd-slider{position:relative;flex:1;display:flex;height:20px;align-items:center;justify-content:center;cursor:pointer}.artplayer-plugin-danmuku .apd-slider .apd-slider-line{position:relative;height:2px;width:100%;overflow:hidden;border-radius:3px;background-color:#ffffff40}.artplayer-plugin-danmuku .apd-slider .apd-slider-points{position:absolute;inset:0;display:flex;align-items:center;justify-content:space-between}.artplayer-plugin-danmuku .apd-slider .apd-slider-points .apd-slider-point{width:2px;height:2px;border-radius:50%;background-color:#ffffff80}.artplayer-plugin-danmuku .apd-slider .apd-slider-progress{width:0%;height:100%;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-slider .apd-slider-dot{position:absolute;transform:translate(-6px);left:0%;width:12px;height:12px;border-radius:50%;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-slider .apd-slider-steps{display:flex;align-items:center;justify-content:space-between;position:absolute;bottom:-12px;width:calc(100% + 32px);color:#777}.artplayer-plugin-danmuku .apd-slider .apd-slider-steps .apd-slider-step{flex-shrink:0;width:36px;text-align:center;scale:.95}.artplayer-plugin-danmuku .apd-config-other{display:flex;align-items:center;gap:20px}.artplayer-plugin-danmuku .apd-config-other .apd-check-off,.artplayer-plugin-danmuku .apd-config-other .apd-check-on{width:16px;height:16px}.artplayer-plugin-danmuku .apd-config-other .apd-other{display:flex;align-items:center;cursor:pointer;gap:2px}.artplayer-plugin-danmuku .apd-config-other .apd-other:hover{color:#00a1d6}.artplayer-plugin-danmuku .apd-emitter{display:flex;flex:1;align-items:center;height:100%;background-color:#ffffff40;border-radius:5px}.artplayer-plugin-danmuku .apd-style{position:relative;display:flex;align-items:center;justify-content:center}.artplayer-plugin-danmuku .apd-style .apd-style-panel{position:absolute;bottom:24px;left:0;width:200px;padding:10px;opacity:0;pointer-events:none}.artplayer-plugin-danmuku .apd-style .apd-style-panel .apd-style-panel-inner{width:100%;border-radius:3px;background-color:#000000d9;padding:10px}.artplayer-plugin-danmuku .apd-style:hover .apd-style-panel{opacity:100;pointer-events:all}.artplayer-plugin-danmuku .apd-colors{display:flex;flex-wrap:wrap;margin-top:5px;gap:8px}.artplayer-plugin-danmuku .apd-colors .apd-color{width:16px;height:16px;border-radius:2px;cursor:pointer}.artplayer-plugin-danmuku .apd-colors .apd-color.apd-active{border:1px solid black;box-shadow:0 0 0 1px #fff}.artplayer-plugin-danmuku .apd-input{outline:none;height:100%;flex:1;min-width:0;width:auto;border:none;line-height:1;color:#fff;background-color:transparent}.artplayer-plugin-danmuku .apd-input::placeholder{color:#ffffff80}.artplayer-plugin-danmuku .apd-send{display:flex;align-items:center;justify-content:center;height:100%;width:60px;flex-shrink:0;cursor:pointer;text-shadow:none;border-top-right-radius:5px;border-bottom-right-radius:5px;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-send.apd-lock{cursor:not-allowed;color:#666;background-color:#e7e7e7}.art-controls-center .apd-emitter{width:260px;flex:none}.art-fullscreen .artplayer-plugin-danmuku,.art-fullscreen-web .artplayer-plugin-danmuku{height:38px;gap:16px}.art-fullscreen .artplayer-plugin-danmuku .apd-config-icon,.art-fullscreen-web .artplayer-plugin-danmuku .apd-config-icon,.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-off,.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-off,.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-on,.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-on{width:28px;height:28px}.art-fullscreen .artplayer-plugin-danmuku .apd-emitter,.art-fullscreen-web .artplayer-plugin-danmuku .apd-emitter{width:400px;flex:none}.art-video-player>.artplayer-plugin-danmuku{position:absolute;left:0;right:0;bottom:-40px;padding:0 10px}.art-video-player:has(>.artplayer-plugin-danmuku){margin-bottom:40px}[data-danmuku-emitter=false] .apd-emitter{display:none!important}[data-danmuku-emitter=false] .art-controls-center .artplayer-plugin-danmuku{justify-content:flex-end;gap:18px}[data-danmuku-emitter=false].art-fullscreen .art-controls-center .artplayer-plugin-danmuku,[data-danmuku-emitter=false].art-fullscreen-web .art-controls-center .artplayer-plugin-danmuku{gap:24px}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-icon{fill:#333}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-emitter{background-color:#f1f2f3}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-input{color:#000}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-input::placeholder{color:#0000004d}[data-danmuku-visible=false] .apd-toggle-off{display:block}[data-danmuku-visible=false] .apd-toggle-on,[data-danmuku-visible=true] .apd-toggle-off{display:none}[data-danmuku-visible=true] .apd-toggle-on{display:block}[data-danmuku-anti-overlap=false] .apd-anti-overlap .apd-check-on{display:none}[data-danmuku-anti-overlap=false] .apd-anti-overlap .apd-check-off,[data-danmuku-anti-overlap=true] .apd-anti-overlap .apd-check-on{display:block}[data-danmuku-anti-overlap=true] .apd-anti-overlap .apd-check-off,[data-danmuku-sync-video=false] .apd-sync-video .apd-check-on{display:none}[data-danmuku-sync-video=false] .apd-sync-video .apd-check-off,[data-danmuku-sync-video=true] .apd-sync-video .apd-check-on{display:block}[data-danmuku-sync-video=true] .apd-sync-video .apd-check-off{display:none}[data-danmuku-mode0=false] .apd-config-mode .apd-mode-0-off{display:block}[data-danmuku-mode0=false] .apd-config-mode .apd-mode-0-on{display:none}[data-danmuku-mode0=false] .art-danmuku [data-mode="0"]{opacity:0!important}[data-danmuku-mode0=true] .apd-config-mode .apd-mode-0-off{display:none}[data-danmuku-mode0=true] .apd-config-mode .apd-mode-0-on{display:block}[data-danmuku-mode="0"] .apd-style-mode [data-mode="0"]{color:#00a1d6}[data-danmuku-mode="0"] .apd-style-mode [data-mode="0"] path{fill:#00a1d6}[data-danmuku-mode1=false] .apd-config-mode .apd-mode-1-off{display:block}[data-danmuku-mode1=false] .apd-config-mode .apd-mode-1-on{display:none}[data-danmuku-mode1=false] .art-danmuku [data-mode="1"]{opacity:0!important}[data-danmuku-mode1=true] .apd-config-mode .apd-mode-1-off{display:none}[data-danmuku-mode1=true] .apd-config-mode .apd-mode-1-on{display:block}[data-danmuku-mode="1"] .apd-style-mode [data-mode="1"]{color:#00a1d6}[data-danmuku-mode="1"] .apd-style-mode [data-mode="1"] path{fill:#00a1d6}[data-danmuku-mode2=false] .apd-config-mode .apd-mode-2-off{display:block}[data-danmuku-mode2=false] .apd-config-mode .apd-mode-2-on{display:none}[data-danmuku-mode2=false] .art-danmuku [data-mode="2"]{opacity:0!important}[data-danmuku-mode2=true] .apd-config-mode .apd-mode-2-off{display:none}[data-danmuku-mode2=true] .apd-config-mode .apd-mode-2-on{display:block}[data-danmuku-mode="2"] .apd-style-mode [data-mode="2"]{color:#00a1d6}[data-danmuku-mode="2"] .apd-style-mode [data-mode="2"] path{fill:#00a1d6}'}function q(t){return e=>{const i=new M(e,t),a=e.isDestroy?void 0:new B(e,i);return!e.isDestroy&&i.option.heatmap&&E(e,i,i.option.heatmap),{name:"artplayerPluginDanmuku",emit:i.emit.bind(i),load:i.load.bind(i),config:i.config.bind(i),hide:i.hide.bind(i),show:i.show.bind(i),reset:i.reset.bind(i),mount:a?a.mount.bind(a):()=>{},get option(){return i.option},get isHide(){return i.isHide},get isStop(){return i.isStop}}}}return q.icons=B.icons,q}); +!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,o=Math.pow,r=(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)&&r(t,i,e[i]);if(n)for(var i of n(e))s.call(e,i)&&r(t,i,e[i]);return t},d=(t,n)=>e(t,i(n)),p=(t,e,i)=>new Promise((n,a)=>{var s=t=>{try{r(i.next(t))}catch(e){a(e)}},o=t=>{try{r(i.throw(t))}catch(e){a(e)}},r=t=>t.done?n(t.value):Promise.resolve(t.value).then(s,o);r((i=i.apply(t,e)).next())});function h(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function u(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function c(t){if("string"!==typeof t)return[];const e=new RegExp(']*?p="(?

[^"]+)"[^>]*>(?.*?)<\\/d>',"gs"),i=t.matchAll(e);return Array.from(i).map(t=>{const e=t.groups.p.split(",");if(e.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(e[0]),mode:u(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=c(e);globalThis.postMessage({danmus:n,id:i})}let f=0;function g(t,{signal:e,onCancel:i}={}){return new Promise((n,a)=>{let s,o,r,l=!1;const d=++f,h=()=>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!==o){const n=o;o=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",h),null==r||r();try{g()}catch(o){s||(i=o),s=!0}s?a(i):n(t)}function v(t,e){if(!l){console.error("Error parsing Bilibili Danmu:",e);try{y(c(t))}catch(i){y(void 0,i)}}}(null==e?void 0:e.aborted)?h():(null==e||e.addEventListener("abort",h),r=null==i?void 0:i(h),l?null==r||r():function(){return p(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 ${u.toString()}\n ${c.toString()}\n onmessage = ${m.toString()}\n `;if(o=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),l)return void g();if(s=new Worker(o),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 k(t){return y.has(t)||y.set(t,{closed:!1,tasks:new Set,emitting:[]}),y.get(t)}function w(t){const e=k(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function x(t){const e=k(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function $(t,e,i){const n=t.states[e]||[];for(let a=0;a{if(this.frame=null,t.timer=null,this.closed||this.fault||t.isStop||t.art.isDestroy)return;let e;const i=new Promise(t=>e=()=>t(b)),n={generation:this.generation,cancel:e,wait:t=>Promise.race([t,i]),ref:null};return this.operation=n,this.run(n).catch(t=>{this.active(n)&&this.fail(t)}).finally(()=>{this.release(n),this.operation===n&&(this.operation=null,this.schedule())})}),t.timer=this.frame)}run(t){return p(this,null,function*(){const e=this.owner,{setStyles:i}=e.utils;if(!e.art.playing||!this.active(t))return;e.filter("emit",t=>{const i=(Date.now()-t.$lastStartTime)/1e3;t.$restTime-=i,t.$lastStartTime=Date.now(),t.$restTime<=0&&e.makeWait(t)});const n=e.readys;for(const s of n){if(!this.active(t))return;if(this.failedItems.has(s))continue;let n;try{n=yield t.wait(e.option.beforeVisible(s))}catch(a){if(!this.active(t))return;this.failedItems.add(s),this.report(a);continue}if(!this.active(t))return;if(!n)continue;const{clientWidth:o,clientHeight:r}=e.$player,l=e.$ref;if(t.danmu=s,t.ref=l,s.$ref=l,l.textContent=s.text,e.$danmuku.appendChild(l),l.style.opacity=e.option.opacity,l.style.fontSize=`${e.fontSize}px`,l.style.color=s.color,l.style.border=s.border?`1px solid ${s.color}`:null,l.style.backgroundColor=s.border?"rgb(0 0 0 / 50%)":null,i(l,s.style),!this.active(t))return;s.$lastStartTime=Date.now(),s.$restTime=e.speed;const d=o+l.clientWidth,{result:p}=yield t.wait(e.postMessage({type:"getDanmuTop",target:{mode:s.mode,height:l.clientHeight,speed:d/s.$restTime},visibles:e.visibles,antiOverlap:e.option.antiOverlap,clientWidth:o,clientHeight:r,marginBottom:e.marginBottom,marginTop:e.marginTop}));if(!this.active(t)||s.$ref!==l)return;if(void 0!==p){switch(e.setState(s,"emit"),l.style.top=`${p}px`,l.style.visibility="visible",l.dataset.mode=s.mode,l.dataset.id=s.id||"",s.mode){case 0:l.style.left=`${o}px`,l.style.marginLeft="0px",l.style.transform=`translateX(${-d}px)`,l.style.transition=`transform ${s.$restTime}s linear 0s`;break;case 1:case 2:l.style.left="50%",l.style.marginLeft=`-${l.clientWidth/2}px`}t.ref=null,e.art.emit("artplayerPluginDanmuku:visible",s)}else e.setState(s,"ready"),this.release(t)}})}destroy(){this.closed=!0,this.invalidate()}}let M=0;class z{constructor(t,e){this.pending=new Map,this.closed=!1,this.failed=!1,this.onFailure=e,this.worker=t();try{this.worker.onmessage=t=>{var e;const i=this.pending.get(null==(e=t.data)?void 0:e.id);i&&(this.pending.delete(t.data.id),i.resolve(t.data))},this.worker.onerror=t=>{var e;null==(e=t.preventDefault)||e.call(t),this.fail(t.error||t)},this.worker.onmessageerror=t=>this.fail(t.error||t)}catch(i){try{this.dispose()}catch(n){}throw i}}request(t){t.id=++M;const{id:e}=t;return this.closed||this.failed?Promise.resolve({id:e,result:void 0}):new Promise((i,n)=>{this.pending.set(e,{resolve:i,reject:n});try{this.worker.postMessage(t)}catch(a){this.fail(a)}})}fail(t){if(!this.closed&&!this.failed){this.failed=!0;for(const e of this.pending.values())e.reject(t);this.pending.clear();try{this.dispose()}catch(e){}this.onFailure(t)}}cancel(){for(const[t,e]of this.pending)e.resolve({id:t,result:void 0});this.pending.clear()}dispose(){if(this.closed)return;this.closed=!0,this.cancel();let t,e=!1;const i=i=>{try{i()}catch(n){e||(t=n),e=!0}};for(const n of["onmessage","onerror","onmessageerror"])i(()=>this.worker[n]=null);if(i(()=>this.worker.terminate()),e)throw t}}const C='/*!\n * artplayer-plugin-danmuku.js v5.3.0\n * Github: https://github.com/zhw2590582/ArtPlayer\n * (c) 2017-2026 Harvey Zhao\n * Released under the MIT License.\n */\nfunction t({target:t,visibles:e,clientWidth:i,clientHeight:h,marginBottom:n,marginTop:o,antiOverlap:s}){const r=h-n,p=e.filter(e=>e.mode===t.mode&&e.top<=r).sort((t,e)=>t.top-e.top);if(0===p.length)return 2===t.mode?r-t.height:o;if(p.unshift({type:"top",top:0,left:0,right:0,height:o,width:i,speed:0,distance:i}),p.push({type:"bottom",top:r,left:0,right:0,height:n,width:i,speed:0,distance:i}),2===t.mode)for(let g=p.length-2;g>=0;g-=1){const e=p[g],i=p[g+1],h=e.top+e.height;if(i.top-h>=t.height)return i.top-t.height}else for(let g=1;g=t.height)return h}const a=[];for(let g=1;g{const i=Math.min(...e.map(t=>t.right)),h=Math.min(...t.map(t=>t.right));return i*e.length-h*t.length});break;case 1:case 2:a.sort((t,e)=>{const i=Math.max(...e.map(t=>t.width));return Math.max(...t.map(t=>t.width))*t.length-i*e.length})}return a[0][0].top}switch(t.mode){case 0:{const e=a.find(e=>e.every(e=>{if(ie.time}));return e&&e[0]?e[0].top:void 0}case 1:case 2:return}}onmessage=e=>{const{data:i}=e;if(!i.id||!i.type)return;const h=(0,{getDanmuTop:t}[i.type])(i);globalThis.postMessage({result:h,id:i.id})};\n',E="undefined"!==typeof self&&self.Blob&&new Blob(["URL.revokeObjectURL(import.meta.url);",C],{type:"text/javascript;charset=utf-8"});function D(t){let e;try{if(e=E&&(self.URL||self.webkitURL).createObjectURL(E),!e)throw"";const i=new Worker(e,{type:"module",name:null==t?void 0:t.name});return i.addEventListener("error",()=>{(self.URL||self.webkitURL).revokeObjectURL(e)}),i}catch(i){return new Worker("data:text/javascript;charset=utf-8,"+encodeURIComponent(C),{type:"module",name:null==t?void 0:t.name})}}class F{constructor(t,e){const{constructor:i,template:n}=t;this.utils=i.utils,this.validator=i.validator,this.$danmuku=n.$danmuku,this.$player=n.$player,this.art=t,this.queue=[],this.$refs=[],this.isStop=!1,this.isHide=!1,this.timer=null,this.index=0,this.worker=null,this.workerClient=null,this.scheduler=new S(this),this.option=F.option,this.states={wait:[],ready:[],emit:[],stop:[]},this.start=this.start.bind(this),this.stop=this.stop.bind(this),this.reset=this.reset.bind(this),this.resize=this.resize.bind(this),this.destroy=this.destroy.bind(this),this.seek=this.seek.bind(this),w(this),t.on("destroy",this.destroy);try{if(this.config(e,!0),!w(this))return;this.createWorker()}catch(a){throw t.off("destroy",this.destroy),x(this),this.scheduler.destroy(),a}t.on("video:play",this.start),t.on("video:playing",this.start),t.on("video:pause",this.stop),t.on("video:waiting",this.stop),t.on("resize",this.resize),t.on("video:seeking",this.seek),this.load().catch(t=>console.warn("Failed to load initial danmuku:",t))}static get option(){return{danmuku:[],speed:5,margin:[10,"25%"],opacity:1,color:"#FFFFFF",mode:0,modes:[0,1,2],fontSize:25,antiOverlap:!0,synchronousPlayback:!1,mount:void 0,heatmap:!1,width:512,points:[],filter:()=>!0,beforeEmit:()=>!0,beforeVisible:()=>!0,visible:!0,emitter:!0,maxLength:200,lockTime:5,theme:"dark",OPACITY:{},FONT_SIZE:{},MARGIN:{},SPEED:{},COLOR:[]}}static get scheme(){return{danmuku:"array|function|string|promise",speed:"number",margin:"array",opacity:"number",color:"string",mode:"number",modes:"array",fontSize:"number|string",antiOverlap:"boolean",synchronousPlayback:"boolean",mount:"?htmldivelement|string",heatmap:"object|boolean",width:"number",points:"array",filter:"function",beforeEmit:"function",beforeVisible:"function",visible:"boolean",emitter:"boolean",maxLength:"number",lockTime:"number",theme:"string",OPACITY:"object",FONT_SIZE:"object",MARGIN:"object",SPEED:"object",COLOR:"array"}}static get cssText(){return'\n user-select: none;\n position: absolute;\n white-space: pre;\n pointer-events: none;\n perspective: 500px;\n display: inline-block;\n will-change: transform;\n font-weight: normal;\n line-height: 1.125;\n visibility: hidden;\n font-family: SimHei, "Microsoft JhengHei", Arial, Helvetica, sans-serif;\n text-shadow: rgb(0, 0, 0) 1px 0px 1px, rgb(0, 0, 0) 0px 1px 1px, rgb(0, 0, 0) 0px -1px 1px, rgb(0, 0, 0) -1px 0px 1px;\n '}get isRotate(){var t,e;return null==(e=null==(t=this.art.plugins)?void 0:t.autoOrientation)?void 0:e.state}get marginTop(){const{clamp:t}=this.utils,e=this.option.margin[0],{clientHeight:i}=this.$player;if("number"===typeof e)return t(e,0,i);if("string"===typeof e&&e.endsWith("%")){return t(i*(Number.parseFloat(e)/100),0,i)}return F.option.margin[0]}get marginBottom(){const{clamp:t}=this.utils,e=this.option.margin[1],{clientHeight:i}=this.$player;if("number"===typeof e)return t(e,0,i);if("string"===typeof e&&e.endsWith("%")){return t(i*(Number.parseFloat(e)/100),0,i)}return F.option.margin[1]}get fontSize(){const{clamp:t}=this.utils,{clientHeight:e}=this.$player,i=this.option.fontSize;if("number"===typeof i)return Math.round(t(i,12,e));if("string"===typeof i&&i.endsWith("%")){const n=Number.parseFloat(i)/100;return Math.round(t(e*n,12,e))}return F.option.fontSize}get $ref(){const t=this.$refs.pop()||document.createElement("div");return t.style.cssText=F.cssText,t.dataset.mode="",t.dataset.id="",t.className="",t}get readys(){return function(t){const{currentTime:e}=t.art,i=[];return $(t,"ready",t=>i.push(t)),$(t,"wait",t=>{e+.1>=t.time&&t.time>=e-.1&&i.push(t)}),i}(this)}get visibles(){const t=[],{clientWidth:e}=this.$player,i=this.getLeft(this.$player);return this.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.getLeft(n.$ref)-i,o=n.$ref.clientHeight,r=n.$ref.clientWidth,l=s+r,d=e-l,p=l/n.$restTime,h={};h.top=a,h.left=s,h.height=o,h.width=r,h.right=d,h.speed=p,h.distance=l,h.time=n.$restTime,h.mode=n.mode,t.push(h)}),t}get speed(){return this.option.synchronousPlayback&&this.art.playbackRate?this.option.speed/Number(this.art.playbackRate):this.option.speed}load(t){return p(this,null,function*(){const{errorHandle:e}=this.utils,i=function(t,e){const i=k(t);if(e)for(const d of[...i.tasks])d.replace&&d.cancel();let n,a=i.closed||t.art.isDestroy;const s=new Promise(t=>n=t),o=new Set,r="function"===typeof AbortController?new AbortController:void 0,l={replace:e,signal:null==r?void 0:r.signal,active:()=>!a&&!i.closed&&!t.art.isDestroy,onCancel:t=>l.active()?(o.add(t),()=>o.delete(t)):(t(),()=>{}),cancel(){if(!a){a=!0,n(v),null==r||r.abort();for(const t of[...o])t();o.clear()}},wait:t=>Promise.race([t,s]),emit(e){i.emitting.push(l);try{return t.emit(e)}finally{i.emitting.pop()}},finish(){i.tasks.delete(l),o.clear()}};return a?n(v):i.tasks.add(l),l}(this,void 0===t);try{if(!i.active())return this;const n=function(t,e){return"function"===typeof t?{asynchronous:!0,value:t()}:h(t)?{asynchronous:!0,value:t}:"string"===typeof t?{asynchronous:!0,value:g(t,e)}:{asynchronous:!1,value:Array.isArray(t)?t:[]}}(t||this.option.danmuku,i),a=n.asynchronous?yield i.wait(n.value):n.value;if(!i.active())return this;if(e(Array.isArray(a),"Danmuku need return an array as result"),void 0===t){if(this.reset(),!i.active())return this;this.queue=[],this.states={wait:[],ready:[],emit:[],stop:[]},this.$refs=[],this.$danmuku.textContent=""}for(let t=0;t{const e=a[t],i=s[t];return"function"===typeof e||"function"===typeof i||h(e)||h(i)?e!==i:JSON.stringify(e)!==JSON.stringify(i)}))&&!e)return this;const o=function(t,e,{defaults:i,validate:n,clamp:a,mount:s}){const o=Object.assign({},i,t,e);return n(o),o.mode=a(o.mode,0,2),o.speed=a(o.speed,1,10),o.opacity=a(o.opacity,0,1),o.lockTime=a(o.lockTime,1,60),o.maxLength=a(o.maxLength,1,1e3),o.mount=o.mount||s,o}(this.option,t,{defaults:F.option,validate:t=>this.validator(t,F.scheme),clamp:i,mount:n});if(!w(this))return this;const r=this.option.beforeVisible!==o.beforeVisible;return this.option=o,r&&(this.scheduler.invalidate(),this.scheduler.schedule()),t.fontSize&&(this.reset(),!w(this))||(this.option.visible?this.show():this.hide(),w(this)&&this.art.emit("artplayerPluginDanmuku:config",this.option)),this}getLeft(t){const e=t.getBoundingClientRect();return this.isRotate?e.top:e.left}postMessage(t={}){return this.workerClient.request(t)}createWorker(){this.workerClient=new z(()=>new D,t=>this.scheduler.fail(t)),this.worker=this.workerClient.worker}filter(t,e){return $(this,t,e)}setState(t,e){!function(t,e,i){t.states[e.$state]=t.states[e.$state].filter(t=>t!==e),e.$state=i,e.$ref&&(e.$ref.dataset.state=i),t.states[i].push(e)}(this,t,e)}makeWait(t){this.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=F.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.$refs.push(t.$ref),t.$ref=null)}update(){return this.scheduler.running=!0,this.scheduler.schedule(),this}resize(){const{clientWidth:t}=this.$player;this.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.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.$player;return this.filter("stop",e=>{switch(this.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.$player;return this.filter("emit",e=>{switch(this.setState(e,"stop"),e.mode){case 0:{const i=t-(this.getLeft(e.$ref)-this.getLeft(this.$player));e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition="transform 0s linear 0s";break}}}),this}stop(){return this.isStop=!0,this.scheduler.running=!1,this.scheduler.invalidate(),this.suspend(),this.art.emit("artplayerPluginDanmuku:stop"),this}start(){if(this.scheduler.closed||this.art.isDestroy)return this;const t=++this.scheduler.starts,e=this.scheduler.generation;this.isStop=!1,this.scheduler.recover();return this.scheduler.closed||this.art.isDestroy||this.isStop||this.scheduler.fault||e!==this.scheduler.generation||t!==this.scheduler.starts||(this.continue(),this.update(),this.art.emit("artplayerPluginDanmuku:start")),this}reset(){return this.scheduler.invalidate(),this.queue.forEach(t=>this.makeWait(t)),this.art.emit("artplayerPluginDanmuku:reset"),this.scheduler.recover(),this.scheduler.schedule(),this}seek(){this.scheduler.invalidate(),this.scheduler.schedule()}show(){return this.isHide=!1,this.$danmuku.style.opacity=1,this.option.visible=!0,this.art.emit("artplayerPluginDanmuku:show"),this}hide(){return this.isHide=!0,this.scheduler.invalidate(),this.$danmuku.style.opacity=0,this.option.visible=!1,this.art.emit("artplayerPluginDanmuku:hide"),this.scheduler.schedule(),this}destroy(){var t;x(this),this.scheduler.destroy();try{try{this.stop()}finally{null==(t=this.workerClient)||t.dispose()}}finally{this.art.off("video:play",this.start),this.art.off("video:playing",this.start),this.art.off("video:pause",this.stop),this.art.off("video:waiting",this.stop),this.art.off("resize",this.resize),this.art.off("video:seeking",this.seek),this.art.off("destroy",this.destroy),this.art.emit("artplayerPluginDanmuku:destroy")}}}const T={map:(t,e,i,n,a)=>(t-e)*(a-n)/(i-e)+n,range(t,e,i){const n=Math.round(t/i)*i;return Array.from({length:Math.floor((e-t)/i)},(t,e)=>e*i+n)}};function A(t,e,i){const{query:n}=t.constructor.utils;t.controls.add({name:"heatmap",position:"top",html:"",style:{position:"absolute",top:"-100px",left:"0px",right:"0px",height:"100px",width:"100%",pointerEvents:"none"},mounted(a){let s=null,r=null;function l(l=[]){if(s=null,r=null,a.innerHTML="",!t.duration||t.option.isLive)return;const d={w:a.offsetWidth,h:a.offsetHeight},p={xMin:0,xMax:d.w,yMin:0,yMax:128,scale:.25,opacity:.2,minHeight:Math.floor(.05*d.h),sampling:Math.floor(d.w/100),smoothing:.2,flattening:.2};"object"===typeof i&&Object.assign(p,i);let h=[];if(Array.isArray(l)&&l.length)h=[...l];else{const i=t.duration/d.w;for(let t=0;t<=d.w;t+=p.sampling){const n=e.queue.filter(({time:e})=>e>t*i&&e<=(t+p.sampling)*i).length;h.push([t,n])}}if(0===h.length)return;const u=h[h.length-1],c=u[0],m=u[1];c!==d.w&&h.push([d.w,m]);const f=h.map(t=>t[1]),g=(Math.min(...f)+Math.max(...f))/2;for(let t=0;tg?1+p.scale:1-p.scale)+p.minHeight}const y=(t,e,i,n)=>{const a=function(t,e){const i=e[0]-t[0],n=e[1]-t[1];return{length:Math.sqrt(o(i,2)+o(n,2)),angle:Math.atan2(n,i)}}(e||t,i||t),s=T.map(Math.cos(a.angle)*p.flattening,0,1,1,0),r=a.angle*s+(n?Math.PI:0),l=a.length*p.smoothing;return[t[0]+Math.cos(r)*l,t[1]+Math.sin(r)*l]},v=h.map(t=>[T.map(t[0],p.xMin,p.xMax,0,d.w),T.map(t[1],p.yMin,p.yMax,d.h,0)]).reduce((t,e,i,n)=>0===i?`M ${n[n.length-1][0]},${d.h} L ${e[0]},${d.h} L ${e[0]},${e[1]}`:`${t} ${((t,e,i)=>{const n=y(i[e-1],i[e-2],t),a=y(t,i[e-1],i[e+1],!0),s=e===i.length-1?" z":"";return`C ${n[0]},${n[1]} ${a[0]},${a[1]} ${t[0]},${t[1]}${s}`})(e,i,n)}`,"");a.innerHTML=`\n \n \n \n \n \n \n \n \n \n \n \n `,s=n("#heatmap-start",a),r=n("#heatmap-stop",a),s.setAttribute("offset",100*t.played+"%"),r.setAttribute("offset",100*t.played+"%")}t.on("video:timeupdate",()=>{s&&r&&(s.setAttribute("offset",100*t.played+"%"),r.setAttribute("offset",100*t.played+"%"))}),t.on("setBar",(t,e)=>{s&&r&&"played"===t&&(s.setAttribute("offset",100*e+"%"),r.setAttribute("offset",100*e+"%"))}),t.on("ready",()=>l()),t.on("resize",()=>l()),t.on("artplayerPluginDanmuku:loaded",()=>l()),t.on("artplayerPluginDanmuku:points",t=>l(t))}})}const P='',L='',O='',H='',V='',j='',R='',I='',q='',B='',Z='',N='';class W{constructor(t,e){this.art=t,this.danmuku=e,this.utils=t.constructor.utils;const{setStyle:i}=this.utils,{$controlsCenter:n}=t.template;i(n,"display","flex"),this.template={$controlsCenter:n,$mount:n,$danmuku:null,$toggle:null,$config:null,$configPanel:null,$configModes:null,$style:null,$stylePanel:null,$styleModes:null,$colors:null,$opacitySlider:null,$opacityValue:null,$marginSlider:null,$marginValue:null,$fontSizeSlider:null,$fontSizeValue:null,$speedSlider:null,$speedValue:null,$input:null,$send:null},this.slider={opacity:null,margin:null,fontSize:null,speed:null},this.emitting=!1,this.isLock=!1,this.timer=null,this.createTemplate(),this.createSliders(),this.createEvents(),this.mount(this.option.mount),t.on("resize",()=>this.resize()),t.on("fullscreen",t=>this.onFullscreen(t)),t.on("fullscreenWeb",t=>this.onFullscreen(t)),t.proxy(this.template.$config,"mouseenter",()=>{this.onMouseEnter({$control:this.template.$config,$panel:this.template.$configPanel})}),t.proxy(this.template.$style,"mouseenter",()=>{this.onMouseEnter({$control:this.template.$style,$panel:this.template.$stylePanel})})}static get icons(){return{$on:Z,$off:B,$config:O,$style:N,$mode_0_off:H,$mode_0_on:V,$mode_1_off:j,$mode_1_on:R,$mode_2_off:I,$mode_2_on:q,$check_on:L,$check_off:P}}get option(){return this.danmuku.option}get outside(){return this.template.$mount!==this.template.$controlsCenter}get TEMPLATE(){const{option:t}=this;return`\n

\n ${Z}${B}\n
\n
\n ${O}\n
\n
\n
\n \u6309\u7c7b\u578b\u5c4f\u853d\n
\n
\n ${H}${V}\n
\u6eda\u52a8
\n
\n
\n ${j}${R}\n
\u9876\u90e8
\n
\n
\n ${I}${q}\n
\u5e95\u90e8
\n
\n
\n
\n
\n
\n ${L}${P}\n \u9632\u6b62\u5f39\u5e55\u91cd\u53e0\n
\n
\n ${L}${P}\n \u540c\u6b65\u89c6\u9891\u901f\u5ea6\n
\n
\n
\n \u4e0d\u900f\u660e\u5ea6\n
\n
\u672a\u77e5
\n
\n
\n \u663e\u793a\u533a\u57df\n
\n
\u672a\u77e5
\n
\n
\n \u5f39\u5e55\u5b57\u53f7\n
\n
\u672a\u77e5
\n
\n
\n \u5f39\u5e55\u901f\u5ea6\n
\n
\u672a\u77e5
\n
\n
\n
\n
\n
\n
\n ${N}\n
\n
\n
\n \u6a21\u5f0f\n
\n
\n ${V}\n
\u6eda\u52a8
\n
\n
\n ${R}\n
\u9876\u90e8
\n
\n
\n ${q}\n
\u5e95\u90e8
\n
\n
\n
\n
\n \u989c\u8272\n
\n ${this.COLOR.map(t=>`
`).join("")}\n
\n
\n
\n
\n
\n \n
\u53d1\u9001
\n
\n `}get OPACITY(){return l({min:0,max:100,steps:[]},this.option.OPACITY)}get FONT_SIZE(){return l({min:12,max:120,steps:[]},this.option.FONT_SIZE)}get MARGIN(){return l({min:0,max:3,steps:[{name:"1/4",value:[10,"75%"]},{name:"\u534a\u5c4f",value:[10,"50%"]},{name:"3/4",value:[10,"25%"]},{name:"\u6ee1\u5c4f",value:[10,10]}]},this.option.MARGIN)}get SPEED(){return l({min:0,max:4,steps:[{name:"\u6781\u6162",value:10},{name:"\u8f83\u6162",value:7.5,hide:!0},{name:"\u9002\u4e2d",value:5},{name:"\u8f83\u5feb",value:2.5,hide:!0},{name:"\u6781\u5feb",value:1}]},this.option.SPEED)}get COLOR(){return this.option.COLOR.length?this.option.COLOR:["#FE0302","#FF7204","#FFAA02","#FFD302","#FFFF00","#A0EE00","#00CD00","#019899","#4266BE","#89D5FF","#CC0273","#222222","#9B9B9B","#FFFFFF"]}query(t){const{query:e}=this.utils,{$danmuku:i}=this.template;return e(t,i)}append(t,e){const{append:i}=this.utils;[...t.children].includes(e)||i(t,e)}setData(t,e){const{$player:i}=this.art.template,{$mount:n}=this.template;i.dataset[t]=e,this.outside&&(n.dataset[t]=e)}createTemplate(){const{createElement:t,tooltip:e}=this.utils,i=t("div");i.className="artplayer-plugin-danmuku",i.innerHTML=this.TEMPLATE,this.template.$danmuku=i,this.template.$toggle=this.query(".apd-toggle"),this.template.$config=this.query(".apd-config"),this.template.$configPanel=this.query(".apd-config-panel"),this.template.$configModes=this.query(".apd-config-mode .apd-modes"),this.template.$style=this.query(".apd-style"),this.template.$stylePanel=this.query(".apd-style-panel"),this.template.$styleModes=this.query(".apd-style-mode .apd-modes"),this.template.$colors=this.query(".apd-colors"),this.template.$antiOverlap=this.query(".apd-anti-overlap"),this.template.$syncVideo=this.query(".apd-sync-video"),this.template.$opacitySlider=this.query(".apd-config-opacity .apd-slider"),this.template.$opacityValue=this.query(".apd-config-opacity .apd-value"),this.template.$marginSlider=this.query(".apd-config-margin .apd-slider"),this.template.$marginValue=this.query(".apd-config-margin .apd-value"),this.template.$fontSizeSlider=this.query(".apd-config-fontSize .apd-slider"),this.template.$fontSizeValue=this.query(".apd-config-fontSize .apd-value"),this.template.$speedSlider=this.query(".apd-config-speed .apd-slider"),this.template.$speedValue=this.query(".apd-config-speed .apd-value"),this.template.$input=this.query(".apd-input"),this.template.$send=this.query(".apd-send");const{$toggle:n}=this.template;this.art.on("artplayerPluginDanmuku:show",()=>{e(n,"\u5173\u95ed\u5f39\u5e55")}),this.art.on("artplayerPluginDanmuku:hide",()=>{e(n,"\u6253\u5f00\u5f39\u5e55")})}createEvents(){const{$toggle:t,$configModes:e,$styleModes:i,$colors:n,$antiOverlap:a,$syncVideo:s,$send:o,$input:r}=this.template;this.art.proxy(t,"click",()=>{this.danmuku.config({visible:!this.option.visible}),this.reset()}),this.art.proxy(e,"click",t=>{const e=t.target.closest(".apd-mode");if(!e)return;const i=Number(e.dataset.mode);this.option.modes.includes(i)?this.danmuku.config({modes:this.option.modes.filter(t=>t!==i)}):this.danmuku.config({modes:[...this.option.modes,i]}),this.reset()}),this.art.proxy(a,"click",()=>{this.danmuku.config({antiOverlap:!this.option.antiOverlap}),this.reset()}),this.art.proxy(s,"click",()=>{this.danmuku.config({synchronousPlayback:!this.option.synchronousPlayback}),this.reset()}),this.art.proxy(i,"click",t=>{const e=t.target.closest(".apd-mode");if(!e)return;const i=Number(e.dataset.mode);this.danmuku.config({mode:i}),this.reset()}),this.art.proxy(n,"click",t=>{const e=t.target.closest(".apd-color");e&&(this.danmuku.config({color:e.dataset.color}),this.reset())}),this.art.proxy(o,"click",()=>this.emit()),this.art.proxy(r,"keypress",t=>{"Enter"===t.key&&(t.preventDefault(),this.emit())})}createSliders(){this.slider.opacity=this.createSlider(d(l({},this.OPACITY),{container:this.template.$opacitySlider,findIndex:()=>Math.round(100*this.option.opacity),onChange:t=>{const{$opacityValue:e}=this.template;e.textContent=`${t}%`,this.danmuku.config({opacity:t/100})}})),this.slider.margin=this.createSlider(d(l({},this.MARGIN),{container:this.template.$marginSlider,findIndex:()=>this.MARGIN.steps.findIndex(t=>t.value[0]===this.option.margin[0]&&t.value[1]===this.option.margin[1]),onChange:t=>{const e=this.MARGIN.steps[t];if(!e)return;const{$marginValue:i}=this.template;i.textContent=e.name,this.danmuku.config({margin:e.value})}})),this.slider.fontSize=this.createSlider(d(l({},this.FONT_SIZE),{container:this.template.$fontSizeSlider,findIndex:()=>this.danmuku.fontSize,onChange:t=>{const{$fontSizeValue:e}=this.template;e.textContent=`${t}px`,t!==this.danmuku.fontSize&&this.danmuku.config({fontSize:t})}})),this.slider.speed=this.createSlider(d(l({},this.SPEED),{container:this.template.$speedSlider,findIndex:()=>this.SPEED.steps.findIndex(t=>t.value===this.option.speed),onChange:t=>{const e=this.SPEED.steps[t];if(!e)return;const{$speedValue:i}=this.template;i.textContent=e.name,this.danmuku.config({speed:e.value})}}))}createSlider({min:t,max:e,container:i,findIndex:n,onChange:a,steps:s=[]}){const{query:o,clamp:r,setStyle:l}=this.utils;l(i,"touch-action","none"),i.innerHTML=`\n
\n
\n ${s.map(()=>'
').join("")}\n
\n
\n
\n
\n
\n ${s.map(t=>t.hide?"":`
${t.name}
`).join("")}\n
\n `;const d=o(".apd-slider-dot",i),p=o(".apd-slider-progress",i);let h=!1;function u(i=n()){if(ie)return;const o=(i-t)/(e-t);d.style.left=100*o+"%",0===s.length&&(p.style.width=d.style.left),a(i)}function c(n){const{top:a,height:s,left:o,width:l}=i.getBoundingClientRect();if(this.art.isRotate){const i=r(n.clientY-a,0,s);u(Math.round(i/s*(e-t)+t))}else{const i=r(n.clientX-o,0,l);u(Math.round(i/l*(e-t)+t))}}return this.art.proxy(i,"click",t=>{c.call(this,t)}),this.art.proxy(i,"pointerdown",t=>{h=0===t.button}),this.art.on("document:pointermove",t=>{h&&c.call(this,t)}),this.art.on("document:pointerup",t=>{h&&(h=!1,c.call(this,t))}),{reset:u}}onFullscreen(t){const{$danmuku:e,$controlsCenter:i,$mount:n}=this.template;this.outside?t?this.append(i,e):this.append(n,e):this.append(i,e)}onMouseEnter({$control:t,$panel:e}){const{$player:i}=this.art.template,n=t.getBoundingClientRect(),a=e.getBoundingClientRect(),s=i.getBoundingClientRect(),o=a.width/2-n.width/2,r=s.left-(n.left-o),l=n.right+o-s.right;e.style.left=r>0?`${-o+r}px`:l>0?-o-l+"px":-o+"px"}emit(){return p(this,null,function*(){const{$input:t}=this.template,e=t.value.trim();if(!e.length)return;if(this.isLock)return;if(this.emitting)return;const i={text:e,mode:this.option.mode,color:this.option.color,time:this.art.currentTime};try{this.emitting=!0;const e=yield this.option.beforeEmit(i);if(this.emitting=!1,!0!==e)return;i.border=!0,delete i.time,this.danmuku.emit(i),t.value="",this.lock()}catch(n){console.error("Error emitting danmuku:",n),this.emitting=!1}})}lock(){const{addClass:t}=this.utils,{$send:e}=this.template;this.isLock=!0;let i=this.option.lockTime;e.textContent=i,t(e,"apd-lock");const n=()=>{this.timer=setTimeout(()=>{0===i?this.unlock():(i-=1,e.textContent=i,n())},1e3)};n()}unlock(){const{removeClass:t}=this.utils,{$send:e}=this.template;clearTimeout(this.timer),this.isLock=!1,e.textContent="\u53d1\u9001",t(e,"apd-lock")}resize(){if(this.outside)return;if(this.art.fullscreen)return;if(this.art.fullscreenWeb)return;const{$player:t,$controlsCenter:e}=this.art.template,{$danmuku:i}=this.template;this.art.widtht.dataset.color===this.option.color.toUpperCase());s&&t(s,"apd-active"),e(i,this.option.visible?"\u5173\u95ed\u5f39\u5e55":"\u6253\u5f00\u5f39\u5e55"),this.resize()}mount(t){const{errorHandle:e}=this.utils,i="string"===typeof t?document.querySelector(t):t;e(i,`Can not find the mount point: ${t}`),this.append(i,this.template.$danmuku),this.template.$mount=i,this.reset()}}if("undefined"!==typeof document){const t="artplayer-plugin-danmuku";let e=document.getElementById(t);e||(e=document.createElement("style"),e.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.appendChild(e)}):(document.head||document.documentElement).appendChild(e)),e.textContent='.artplayer-plugin-danmuku{display:flex;position:relative;z-index:99;align-items:center;justify-content:center;font-size:12px;height:32px;width:100%;color:#fff;font-weight:300;flex-shrink:0;gap:10px}.artplayer-plugin-danmuku .apd-icon{cursor:pointer;opacity:.75;transition:all .2s ease;fill:#fff}.artplayer-plugin-danmuku .apd-icon:hover{opacity:1}.artplayer-plugin-danmuku .apd-config{display:flex;position:relative}.artplayer-plugin-danmuku .apd-config .apd-config-panel{position:absolute;bottom:24px;left:0;width:320px;padding:10px;opacity:0;pointer-events:none}.artplayer-plugin-danmuku .apd-config .apd-config-panel .apd-config-panel-inner{width:100%;border-radius:3px;background-color:#000000d9;padding:10px}.artplayer-plugin-danmuku .apd-config:hover .apd-config-panel{opacity:100;pointer-events:all}.artplayer-plugin-danmuku .apd-config-mode,.artplayer-plugin-danmuku .apd-config-slider,.artplayer-plugin-danmuku .apd-config-other,.artplayer-plugin-danmuku .apd-style-mode{margin-bottom:15px}.artplayer-plugin-danmuku .apd-modes{display:flex;align-items:center;margin-top:5px;gap:20px}.artplayer-plugin-danmuku .apd-modes .apd-mode{cursor:pointer;text-align:center}.artplayer-plugin-danmuku .apd-modes .apd-mode:hover{color:#00a1d6}.artplayer-plugin-danmuku .apd-config-slider{display:flex;align-items:center;gap:12px}.artplayer-plugin-danmuku .apd-config-slider .apd-value{width:32px;text-align:right}.artplayer-plugin-danmuku .apd-slider{position:relative;flex:1;display:flex;height:20px;align-items:center;justify-content:center;cursor:pointer}.artplayer-plugin-danmuku .apd-slider .apd-slider-line{position:relative;height:2px;width:100%;overflow:hidden;border-radius:3px;background-color:#ffffff40}.artplayer-plugin-danmuku .apd-slider .apd-slider-points{position:absolute;inset:0;display:flex;align-items:center;justify-content:space-between}.artplayer-plugin-danmuku .apd-slider .apd-slider-points .apd-slider-point{width:2px;height:2px;border-radius:50%;background-color:#ffffff80}.artplayer-plugin-danmuku .apd-slider .apd-slider-progress{width:0%;height:100%;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-slider .apd-slider-dot{position:absolute;transform:translate(-6px);left:0%;width:12px;height:12px;border-radius:50%;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-slider .apd-slider-steps{display:flex;align-items:center;justify-content:space-between;position:absolute;bottom:-12px;width:calc(100% + 32px);color:#777}.artplayer-plugin-danmuku .apd-slider .apd-slider-steps .apd-slider-step{flex-shrink:0;width:36px;text-align:center;scale:.95}.artplayer-plugin-danmuku .apd-config-other{display:flex;align-items:center;gap:20px}.artplayer-plugin-danmuku .apd-config-other .apd-check-off,.artplayer-plugin-danmuku .apd-config-other .apd-check-on{width:16px;height:16px}.artplayer-plugin-danmuku .apd-config-other .apd-other{display:flex;align-items:center;cursor:pointer;gap:2px}.artplayer-plugin-danmuku .apd-config-other .apd-other:hover{color:#00a1d6}.artplayer-plugin-danmuku .apd-emitter{display:flex;flex:1;align-items:center;height:100%;background-color:#ffffff40;border-radius:5px}.artplayer-plugin-danmuku .apd-style{position:relative;display:flex;align-items:center;justify-content:center}.artplayer-plugin-danmuku .apd-style .apd-style-panel{position:absolute;bottom:24px;left:0;width:200px;padding:10px;opacity:0;pointer-events:none}.artplayer-plugin-danmuku .apd-style .apd-style-panel .apd-style-panel-inner{width:100%;border-radius:3px;background-color:#000000d9;padding:10px}.artplayer-plugin-danmuku .apd-style:hover .apd-style-panel{opacity:100;pointer-events:all}.artplayer-plugin-danmuku .apd-colors{display:flex;flex-wrap:wrap;margin-top:5px;gap:8px}.artplayer-plugin-danmuku .apd-colors .apd-color{width:16px;height:16px;border-radius:2px;cursor:pointer}.artplayer-plugin-danmuku .apd-colors .apd-color.apd-active{border:1px solid black;box-shadow:0 0 0 1px #fff}.artplayer-plugin-danmuku .apd-input{outline:none;height:100%;flex:1;min-width:0;width:auto;border:none;line-height:1;color:#fff;background-color:transparent}.artplayer-plugin-danmuku .apd-input::placeholder{color:#ffffff80}.artplayer-plugin-danmuku .apd-send{display:flex;align-items:center;justify-content:center;height:100%;width:60px;flex-shrink:0;cursor:pointer;text-shadow:none;border-top-right-radius:5px;border-bottom-right-radius:5px;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-send.apd-lock{cursor:not-allowed;color:#666;background-color:#e7e7e7}.art-controls-center .apd-emitter{width:260px;flex:none}.art-fullscreen .artplayer-plugin-danmuku,.art-fullscreen-web .artplayer-plugin-danmuku{height:38px;gap:16px}.art-fullscreen .artplayer-plugin-danmuku .apd-config-icon,.art-fullscreen-web .artplayer-plugin-danmuku .apd-config-icon,.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-off,.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-off,.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-on,.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-on{width:28px;height:28px}.art-fullscreen .artplayer-plugin-danmuku .apd-emitter,.art-fullscreen-web .artplayer-plugin-danmuku .apd-emitter{width:400px;flex:none}.art-video-player>.artplayer-plugin-danmuku{position:absolute;left:0;right:0;bottom:-40px;padding:0 10px}.art-video-player:has(>.artplayer-plugin-danmuku){margin-bottom:40px}[data-danmuku-emitter=false] .apd-emitter{display:none!important}[data-danmuku-emitter=false] .art-controls-center .artplayer-plugin-danmuku{justify-content:flex-end;gap:18px}[data-danmuku-emitter=false].art-fullscreen .art-controls-center .artplayer-plugin-danmuku,[data-danmuku-emitter=false].art-fullscreen-web .art-controls-center .artplayer-plugin-danmuku{gap:24px}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-icon{fill:#333}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-emitter{background-color:#f1f2f3}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-input{color:#000}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-input::placeholder{color:#0000004d}[data-danmuku-visible=false] .apd-toggle-off{display:block}[data-danmuku-visible=false] .apd-toggle-on,[data-danmuku-visible=true] .apd-toggle-off{display:none}[data-danmuku-visible=true] .apd-toggle-on{display:block}[data-danmuku-anti-overlap=false] .apd-anti-overlap .apd-check-on{display:none}[data-danmuku-anti-overlap=false] .apd-anti-overlap .apd-check-off,[data-danmuku-anti-overlap=true] .apd-anti-overlap .apd-check-on{display:block}[data-danmuku-anti-overlap=true] .apd-anti-overlap .apd-check-off,[data-danmuku-sync-video=false] .apd-sync-video .apd-check-on{display:none}[data-danmuku-sync-video=false] .apd-sync-video .apd-check-off,[data-danmuku-sync-video=true] .apd-sync-video .apd-check-on{display:block}[data-danmuku-sync-video=true] .apd-sync-video .apd-check-off{display:none}[data-danmuku-mode0=false] .apd-config-mode .apd-mode-0-off{display:block}[data-danmuku-mode0=false] .apd-config-mode .apd-mode-0-on{display:none}[data-danmuku-mode0=false] .art-danmuku [data-mode="0"]{opacity:0!important}[data-danmuku-mode0=true] .apd-config-mode .apd-mode-0-off{display:none}[data-danmuku-mode0=true] .apd-config-mode .apd-mode-0-on{display:block}[data-danmuku-mode="0"] .apd-style-mode [data-mode="0"]{color:#00a1d6}[data-danmuku-mode="0"] .apd-style-mode [data-mode="0"] path{fill:#00a1d6}[data-danmuku-mode1=false] .apd-config-mode .apd-mode-1-off{display:block}[data-danmuku-mode1=false] .apd-config-mode .apd-mode-1-on{display:none}[data-danmuku-mode1=false] .art-danmuku [data-mode="1"]{opacity:0!important}[data-danmuku-mode1=true] .apd-config-mode .apd-mode-1-off{display:none}[data-danmuku-mode1=true] .apd-config-mode .apd-mode-1-on{display:block}[data-danmuku-mode="1"] .apd-style-mode [data-mode="1"]{color:#00a1d6}[data-danmuku-mode="1"] .apd-style-mode [data-mode="1"] path{fill:#00a1d6}[data-danmuku-mode2=false] .apd-config-mode .apd-mode-2-off{display:block}[data-danmuku-mode2=false] .apd-config-mode .apd-mode-2-on{display:none}[data-danmuku-mode2=false] .art-danmuku [data-mode="2"]{opacity:0!important}[data-danmuku-mode2=true] .apd-config-mode .apd-mode-2-off{display:none}[data-danmuku-mode2=true] .apd-config-mode .apd-mode-2-on{display:block}[data-danmuku-mode="2"] .apd-style-mode [data-mode="2"]{color:#00a1d6}[data-danmuku-mode="2"] .apd-style-mode [data-mode="2"] path{fill:#00a1d6}'}function _(t){return e=>{const i=new F(e,t),n=e.isDestroy?void 0:new W(e,i);return!e.isDestroy&&i.option.heatmap&&A(e,i,i.option.heatmap),{name:"artplayerPluginDanmuku",emit:i.emit.bind(i),load:i.load.bind(i),config:i.config.bind(i),hide:i.hide.bind(i),show:i.show.bind(i),reset:i.reset.bind(i),mount:n?n.mount.bind(n):()=>{},get option(){return i.option},get isHide(){return i.isHide},get isStop(){return i.isStop}}}}return _.icons=W.icons,_}); diff --git a/docs/compiled/artplayer-plugin-danmuku.mjs b/docs/compiled/artplayer-plugin-danmuku.mjs index 97dbcd0db..b6e8613cc 100644 --- a/docs/compiled/artplayer-plugin-danmuku.mjs +++ b/docs/compiled/artplayer-plugin-danmuku.mjs @@ -133,7 +133,7 @@ function onmessage({ data }) { const danmus = bilibiliDanmuParseFromXml(xml); globalThis.postMessage({ danmus, id }); } -let nextRequest = 0; +let nextRequest$1 = 0; function abortError(signal) { if (signal?.reason !== void 0) return signal.reason; @@ -147,7 +147,7 @@ function bilibiliDanmuParseFromUrl(url, { signal, onCancel } = {}) { let worker; let workerUrl; let releaseCancel; - const id = ++nextRequest; + const id = ++nextRequest$1; const cancel = () => finish(void 0, abortError(signal)); function releaseWorker() { let error; @@ -264,7 +264,7 @@ function bilibiliDanmuParseFromUrl(url, { signal, onCancel } = {}) { }); } const inputs = /* @__PURE__ */ new WeakMap(); -const cancelled = /* @__PURE__ */ Symbol("cancelled danmuku input"); +const cancelled$1 = /* @__PURE__ */ Symbol("cancelled danmuku input"); function stateFor(owner) { if (!inputs.has(owner)) inputs.set(owner, { closed: false, tasks: /* @__PURE__ */ new Set(), emitting: [] }); @@ -305,7 +305,7 @@ function beginInput(owner, replace) { if (stopped) return; stopped = true; - resolveCancel(cancelled); + resolveCancel(cancelled$1); controller?.abort(); for (const callback of [...handlers]) callback(); handlers.clear(); @@ -325,7 +325,7 @@ function beginInput(owner, replace) { } }; if (stopped) - resolveCancel(cancelled); + resolveCancel(cancelled$1); else state.tasks.add(task); return task; } @@ -343,6 +343,301 @@ function readInput(target, task) { return { asynchronous: true, value: bilibiliDanmuParseFromUrl(target, task) }; return { asynchronous: false, value: Array.isArray(target) ? target : [] }; } +function filterState(owner, state, callback) { + const danmus = owner.states[state] || []; + for (let index = 0; index < danmus.length; index++) callback(danmus[index]); + return danmus; +} +function readyItems(owner) { + const { currentTime } = owner.art; + const result = []; + filterState(owner, "ready", (danmu) => result.push(danmu)); + filterState(owner, "wait", (danmu) => { + if (currentTime + 0.1 >= danmu.time && danmu.time >= currentTime - 0.1) + result.push(danmu); + }); + return result; +} +function setItemState(owner, danmu, state) { + owner.states[danmu.$state] = owner.states[danmu.$state].filter((item) => item !== danmu); + danmu.$state = state; + if (danmu.$ref) + danmu.$ref.dataset.state = state; + owner.states[state].push(danmu); +} +const cancelled = /* @__PURE__ */ Symbol("cancelled danmuku frame"); +class Scheduler { + constructor(owner) { + this.owner = owner; + this.generation = 0; + this.starts = 0; + this.frame = null; + this.operation = null; + this.closed = false; + this.running = false; + this.fault = false; + this.failedItems = /* @__PURE__ */ new Set(); + } + active(operation) { + const owner = this.owner; + return !this.closed && !this.fault && !owner.art.isDestroy && !owner.isStop && !owner.isHide && operation.generation === this.generation; + } + release(operation) { + const { danmu, ref } = operation; + if (ref && danmu.$ref === ref) { + ref.style.cssText = this.owner.constructor.cssText; + ref.style.visibility = "hidden"; + this.owner.$refs.push(ref); + danmu.$ref = null; + } + operation.ref = null; + } + invalidate() { + this.generation++; + if (this.frame !== null) + window.cancelAnimationFrame(this.frame); + this.frame = null; + this.owner.timer = null; + if (this.operation) { + const operation = this.operation; + this.operation = null; + operation.cancel(); + this.release(operation); + } + this.owner.workerClient?.cancel(); + this.failedItems.clear(); + } + report(error) { + try { + this.owner.art.emit("artplayerPluginDanmuku:error", error); + } catch (listenerError) { + console.warn("Failed to report danmuku scheduling error:", listenerError); + } + } + fail(error) { + if (this.closed || this.fault) + return; + this.fault = true; + this.invalidate(); + this.report(error); + } + recover() { + if (this.closed || this.owner.art.isDestroy) + return; + this.failedItems.clear(); + if (this.fault) { + try { + this.owner.workerClient?.dispose(); + this.owner.createWorker(); + this.fault = false; + } catch (error) { + this.report(error); + } + } + } + schedule() { + const owner = this.owner; + if (!this.running || this.closed || this.fault || owner.art.isDestroy || owner.isStop || this.frame !== null || this.operation) + return; + this.frame = window.requestAnimationFrame(() => { + this.frame = null; + owner.timer = null; + if (this.closed || this.fault || owner.isStop || owner.art.isDestroy) + return; + let cancel; + const cancellation = new Promise((resolve) => cancel = () => resolve(cancelled)); + const operation = { generation: this.generation, cancel, wait: (value) => Promise.race([value, cancellation]), ref: null }; + this.operation = operation; + return this.run(operation).catch((error) => { + if (this.active(operation)) + this.fail(error); + }).finally(() => { + this.release(operation); + if (this.operation === operation) { + this.operation = null; + this.schedule(); + } + }); + }); + owner.timer = this.frame; + } + async run(operation) { + const owner = this.owner; + const { setStyles } = owner.utils; + if (!owner.art.playing || !this.active(operation)) + return; + owner.filter("emit", (danmu) => { + const emitTime = (Date.now() - danmu.$lastStartTime) / 1e3; + danmu.$restTime -= emitTime; + danmu.$lastStartTime = Date.now(); + if (danmu.$restTime <= 0) + owner.makeWait(danmu); + }); + const readys = owner.readys; + for (const danmu of readys) { + if (!this.active(operation)) + return; + if (this.failedItems.has(danmu)) + continue; + let state; + try { + state = await operation.wait(owner.option.beforeVisible(danmu)); + } catch (error) { + if (!this.active(operation)) + return; + this.failedItems.add(danmu); + this.report(error); + continue; + } + if (!this.active(operation)) + return; + if (!state) + continue; + const { clientWidth, clientHeight } = owner.$player; + const ref = owner.$ref; + operation.danmu = danmu; + operation.ref = ref; + danmu.$ref = ref; + ref.textContent = danmu.text; + owner.$danmuku.appendChild(ref); + ref.style.opacity = owner.option.opacity; + ref.style.fontSize = `${owner.fontSize}px`; + ref.style.color = danmu.color; + ref.style.border = danmu.border ? `1px solid ${danmu.color}` : null; + ref.style.backgroundColor = danmu.border ? "rgb(0 0 0 / 50%)" : null; + setStyles(ref, danmu.style); + if (!this.active(operation)) + return; + danmu.$lastStartTime = Date.now(); + danmu.$restTime = owner.speed; + const distance = clientWidth + ref.clientWidth; + const { result: top } = await operation.wait(owner.postMessage({ + type: "getDanmuTop", + target: { mode: danmu.mode, height: ref.clientHeight, speed: distance / danmu.$restTime }, + visibles: owner.visibles, + antiOverlap: owner.option.antiOverlap, + clientWidth, + clientHeight, + marginBottom: owner.marginBottom, + marginTop: owner.marginTop + })); + if (!this.active(operation) || danmu.$ref !== ref) + return; + if (top !== void 0) { + owner.setState(danmu, "emit"); + ref.style.top = `${top}px`; + ref.style.visibility = "visible"; + ref.dataset.mode = danmu.mode; + ref.dataset.id = danmu.id || ""; + switch (danmu.mode) { + case 0: + ref.style.left = `${clientWidth}px`; + ref.style.marginLeft = "0px"; + ref.style.transform = `translateX(${-distance}px)`; + ref.style.transition = `transform ${danmu.$restTime}s linear 0s`; + break; + case 1: + case 2: + ref.style.left = "50%"; + ref.style.marginLeft = `-${ref.clientWidth / 2}px`; + break; + } + operation.ref = null; + owner.art.emit("artplayerPluginDanmuku:visible", danmu); + } else { + owner.setState(danmu, "ready"); + this.release(operation); + } + } + } + destroy() { + this.closed = true; + this.invalidate(); + } +} +let nextRequest = 0; +class WorkerClient { + constructor(createWorker, onFailure) { + this.pending = /* @__PURE__ */ new Map(); + this.closed = false; + this.failed = false; + this.onFailure = onFailure; + this.worker = createWorker(); + try { + this.worker.onmessage = (event) => { + const request = this.pending.get(event.data?.id); + if (!request) + return; + this.pending.delete(event.data.id); + request.resolve(event.data); + }; + this.worker.onerror = (event) => { + event.preventDefault?.(); + this.fail(event.error || event); + }; + this.worker.onmessageerror = (event) => this.fail(event.error || event); + } catch (error) { + try { + this.dispose(); + } catch { + } + throw error; + } + } + request(message) { + message.id = ++nextRequest; + const { id } = message; + if (this.closed || this.failed) + return Promise.resolve({ id, result: void 0 }); + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject }); + try { + this.worker.postMessage(message); + } catch (error) { + this.fail(error); + } + }); + } + fail(error) { + if (this.closed || this.failed) + return; + this.failed = true; + for (const request of this.pending.values()) request.reject(error); + this.pending.clear(); + try { + this.dispose(); + } catch { + } + this.onFailure(error); + } + cancel() { + for (const [id, request] of this.pending) request.resolve({ id, result: void 0 }); + this.pending.clear(); + } + dispose() { + if (this.closed) + return; + this.closed = true; + this.cancel(); + let failed = false; + let error; + const attempt = (callback) => { + try { + callback(); + } catch (failure) { + if (!failed) + error = failure; + failed = true; + } + }; + for (const name of ["onmessage", "onerror", "onmessageerror"]) { + attempt(() => this.worker[name] = null); + } + attempt(() => this.worker.terminate()); + if (failed) + throw error; + } +} const jsContent = '/*!\n * artplayer-plugin-danmuku.js v5.3.0\n * Github: https://github.com/zhw2590582/ArtPlayer\n * (c) 2017-2026 Harvey Zhao\n * Released under the MIT License.\n */\nfunction getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom, marginTop, antiOverlap }) {\n const maxTop = clientHeight - marginBottom;\n const danmus = visibles.filter((item) => item.mode === target.mode && item.top <= maxTop).sort((prev, next) => prev.top - next.top);\n if (danmus.length === 0) {\n if (target.mode === 2) {\n return maxTop - target.height;\n } else {\n return marginTop;\n }\n }\n danmus.unshift({\n type: "top",\n top: 0,\n left: 0,\n right: 0,\n height: marginTop,\n width: clientWidth,\n speed: 0,\n distance: clientWidth\n });\n danmus.push({\n type: "bottom",\n top: maxTop,\n left: 0,\n right: 0,\n height: marginBottom,\n width: clientWidth,\n speed: 0,\n distance: clientWidth\n });\n if (target.mode === 2) {\n for (let index = danmus.length - 2; index >= 0; index -= 1) {\n const item = danmus[index];\n const prev = danmus[index + 1];\n const itemBottom = item.top + item.height;\n const diff = prev.top - itemBottom;\n if (diff >= target.height) {\n return prev.top - target.height;\n }\n }\n } else {\n for (let index = 1; index < danmus.length; index += 1) {\n const item = danmus[index];\n const prev = danmus[index - 1];\n const prevBottom = prev.top + prev.height;\n const diff = item.top - prevBottom;\n if (diff >= target.height) {\n return prevBottom;\n }\n }\n }\n const topMap = [];\n for (let index = 1; index < danmus.length - 1; index += 1) {\n const item = danmus[index];\n if (topMap.length) {\n const last = topMap[topMap.length - 1];\n if (last[0].top === item.top) {\n last.push(item);\n } else {\n topMap.push([item]);\n }\n } else {\n topMap.push([item]);\n }\n }\n if (antiOverlap) {\n switch (target.mode) {\n case 0: {\n const result = topMap.find((list) => {\n return list.every((danmu) => {\n if (clientWidth < danmu.distance)\n return false;\n if (target.speed < danmu.speed)\n return true;\n const overlapTime = danmu.right / (target.speed - danmu.speed);\n if (overlapTime > danmu.time)\n return true;\n return false;\n });\n });\n return result && result[0] ? result[0].top : void 0;\n }\n // 静止弹幕没有重叠问题\n case 1:\n case 2:\n return void 0;\n }\n } else {\n switch (target.mode) {\n case 0:\n topMap.sort((prev, next) => {\n const nextMinRight = Math.min(...next.map((item) => item.right));\n const prevMinRight = Math.min(...prev.map((item) => item.right));\n return nextMinRight * next.length - prevMinRight * prev.length;\n });\n break;\n case 1:\n case 2:\n topMap.sort((prev, next) => {\n const nextMaxWidth = Math.max(...next.map((item) => item.width));\n const prevMaxWidth = Math.max(...prev.map((item) => item.width));\n return prevMaxWidth * prev.length - nextMaxWidth * next.length;\n });\n break;\n }\n return topMap[0][0].top;\n }\n}\nonmessage = (event) => {\n const { data } = event;\n if (!data.id || !data.type)\n return;\n const fns = { getDanmuTop };\n const fn = fns[data.type];\n const result = fn(data);\n globalThis.postMessage({\n result,\n id: data.id\n });\n};\n'; const blob = typeof self !== "undefined" && self.Blob && new Blob(["URL.revokeObjectURL(import.meta.url);", jsContent], { type: "text/javascript;charset=utf-8" }); function WorkerWrapper(options) { @@ -383,6 +678,8 @@ class Danmuku { this.timer = null; this.index = 0; this.worker = null; + this.workerClient = null; + this.scheduler = new Scheduler(this); this.option = Danmuku.option; this.states = { wait: [], ready: [], emit: [], stop: [] }; this.start = this.start.bind(this); @@ -390,16 +687,18 @@ class Danmuku { this.reset = this.reset.bind(this); this.resize = this.resize.bind(this); this.destroy = this.destroy.bind(this); + this.seek = this.seek.bind(this); inputActive(this); art.on("destroy", this.destroy); try { this.config(option, true); if (!inputActive(this)) return; - this.worker = new WorkerWrapper(); + this.createWorker(); } catch (error) { art.off("destroy", this.destroy); cancelInputs(this); + this.scheduler.destroy(); throw error; } art.on("video:play", this.start); @@ -407,6 +706,7 @@ class Danmuku { art.on("video:pause", this.stop); art.on("video:waiting", this.stop); art.on("resize", this.resize); + art.on("video:seeking", this.seek); this.load().catch((error) => console.warn("Failed to load initial danmuku:", error)); } // 默认配置 @@ -491,15 +791,7 @@ class Danmuku { } // 获取准备好发送的弹幕 get readys() { - const { currentTime } = this.art; - const result = []; - this.filter("ready", (danmu) => result.push(danmu)); - this.filter("wait", (danmu) => { - if (currentTime + 0.1 >= danmu.time && danmu.time >= currentTime - 0.1) { - result.push(danmu); - } - }); - return result; + return readyItems(this); } // 可见的弹幕的数据,用于计算下一个弹幕的top值 get visibles() { @@ -595,7 +887,7 @@ class Danmuku { }); if (!danmu.text.trim()) return this; - if (danmu.time !== void 0) { + if (danmu.time || danmu.time === 0) { danmu.time = clamp(danmu.time, 0, Infinity); } else { danmu.time = this.art.currentTime + 0.5; @@ -647,7 +939,12 @@ class Danmuku { }); if (!inputActive(this)) return this; + const beforeVisibleChanged = this.option.beforeVisible !== next.beforeVisible; this.option = next; + if (beforeVisibleChanged) { + this.scheduler.invalidate(); + this.scheduler.schedule(); + } if (option.fontSize) { this.reset(); if (!inputActive(this)) @@ -669,33 +966,19 @@ class Danmuku { } // 复杂运算交给 Web Worker 处理 postMessage(message = {}) { - return new Promise((resolve) => { - message.id = Date.now(); - this.worker.postMessage(message); - this.worker.onmessage = (event) => { - const { data } = event; - if (data.id === message.id) { - resolve(data); - } - }; - }); + return this.workerClient.request(message); + } + createWorker() { + this.workerClient = new WorkerClient(() => new WorkerWrapper(), (error) => this.scheduler.fail(error)); + this.worker = this.workerClient.worker; } // 根据状态获取弹幕 filter(state, callback) { - const danmus = this.states[state] || []; - for (let index = 0; index < danmus.length; index++) { - callback(danmus[index]); - } - return danmus; + return filterState(this, state, callback); } // 设置弹幕状态 setState(danmu, state) { - this.states[danmu.$state] = this.states[danmu.$state].filter((item) => item !== danmu); - danmu.$state = state; - if (danmu.$ref) { - danmu.$ref.dataset.state = state; - } - this.states[state].push(danmu); + setItemState(this, danmu, state); } // 重置弹幕到wait状态,回收弹幕DOM节点 makeWait(danmu) { @@ -712,88 +995,8 @@ class Danmuku { } // 实时更新弹幕 update() { - const { setStyles } = this.utils; - this.timer = window.requestAnimationFrame(async () => { - if (this.art.playing && !this.isHide) { - this.filter("emit", (danmu) => { - const emitTime = (Date.now() - danmu.$lastStartTime) / 1e3; - danmu.$restTime -= emitTime; - danmu.$lastStartTime = Date.now(); - if (danmu.$restTime <= 0) { - this.makeWait(danmu); - } - }); - const readys = this.readys; - for (let index = 0; index < readys.length; index++) { - const danmu = readys[index]; - const state = await this.option.beforeVisible(danmu); - if (state) { - const { clientWidth, clientHeight } = this.$player; - danmu.$ref = this.$ref; - danmu.$ref.textContent = danmu.text; - this.$danmuku.appendChild(danmu.$ref); - danmu.$ref.style.opacity = this.option.opacity; - danmu.$ref.style.fontSize = `${this.fontSize}px`; - danmu.$ref.style.color = danmu.color; - danmu.$ref.style.border = danmu.border ? `1px solid ${danmu.color}` : null; - danmu.$ref.style.backgroundColor = danmu.border ? "rgb(0 0 0 / 50%)" : null; - setStyles(danmu.$ref, danmu.style); - danmu.$lastStartTime = Date.now(); - danmu.$restTime = this.speed; - const distance = clientWidth + danmu.$ref.clientWidth; - const { result: top } = await this.postMessage({ - type: "getDanmuTop", - target: { - mode: danmu.mode, - height: danmu.$ref.clientHeight, - speed: distance / danmu.$restTime - }, - // 当前弹幕信息 - visibles: this.visibles, - // 可见的弹幕的数据 - antiOverlap: this.option.antiOverlap, - clientWidth, - clientHeight, - marginBottom: this.marginBottom, - marginTop: this.marginTop - }); - if (danmu.$ref) { - if (!this.isStop && top !== void 0) { - this.setState(danmu, "emit"); - danmu.$ref.style.top = `${top}px`; - danmu.$ref.style.visibility = "visible"; - danmu.$ref.dataset.mode = danmu.mode; - danmu.$ref.dataset.id = danmu.id || ""; - switch (danmu.mode) { - // 滚动的弹幕 - case 0: { - danmu.$ref.style.left = `${clientWidth}px`; - danmu.$ref.style.marginLeft = "0px"; - danmu.$ref.style.transform = `translateX(${-distance}px)`; - danmu.$ref.style.transition = `transform ${danmu.$restTime}s linear 0s`; - break; - } - case 1: - // falls through - case 2: - danmu.$ref.style.left = "50%"; - danmu.$ref.style.marginLeft = `-${danmu.$ref.clientWidth / 2}px`; - break; - } - this.art.emit("artplayerPluginDanmuku:visible", danmu); - } else { - this.setState(danmu, "ready"); - this.$refs.push(danmu.$ref); - danmu.$ref = null; - } - } - } - } - } - if (!this.isStop) { - this.update(); - } - }); + this.scheduler.running = true; + this.scheduler.schedule(); return this; } // 重置正在显示的弹幕: stop/emit 状态的弹幕 @@ -858,23 +1061,39 @@ class Danmuku { } stop() { this.isStop = true; + this.scheduler.running = false; + this.scheduler.invalidate(); this.suspend(); - window.cancelAnimationFrame(this.timer); this.art.emit("artplayerPluginDanmuku:stop"); return this; } start() { + if (this.scheduler.closed || this.art.isDestroy) + return this; + const start = ++this.scheduler.starts; + const generation = this.scheduler.generation; this.isStop = false; + this.scheduler.recover(); + const obsolete = this.scheduler.closed || this.art.isDestroy || this.isStop || this.scheduler.fault || generation !== this.scheduler.generation || start !== this.scheduler.starts; + if (obsolete) + return this; this.continue(); this.update(); this.art.emit("artplayerPluginDanmuku:start"); return this; } reset() { + this.scheduler.invalidate(); this.queue.forEach((danmu) => this.makeWait(danmu)); this.art.emit("artplayerPluginDanmuku:reset"); + this.scheduler.recover(); + this.scheduler.schedule(); return this; } + seek() { + this.scheduler.invalidate(); + this.scheduler.schedule(); + } show() { this.isHide = false; this.$danmuku.style.opacity = 1; @@ -884,22 +1103,32 @@ class Danmuku { } hide() { this.isHide = true; + this.scheduler.invalidate(); this.$danmuku.style.opacity = 0; this.option.visible = false; this.art.emit("artplayerPluginDanmuku:hide"); + this.scheduler.schedule(); return this; } destroy() { cancelInputs(this); - this.stop(); - this.worker?.terminate(); - this.art.off("video:play", this.start); - this.art.off("video:playing", this.start); - this.art.off("video:pause", this.stop); - this.art.off("video:waiting", this.stop); - this.art.off("resize", this.resize); - this.art.off("destroy", this.destroy); - this.art.emit("artplayerPluginDanmuku:destroy"); + this.scheduler.destroy(); + try { + try { + this.stop(); + } finally { + this.workerClient?.dispose(); + } + } finally { + this.art.off("video:play", this.start); + this.art.off("video:playing", this.start); + this.art.off("video:pause", this.stop); + this.art.off("video:waiting", this.stop); + this.art.off("resize", this.resize); + this.art.off("video:seeking", this.seek); + this.art.off("destroy", this.destroy); + this.art.emit("artplayerPluginDanmuku:destroy"); + } } } const lib = { diff --git a/package.json b/package.json index 3c6d775f4..64acad491 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports", "test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js test/asr-distribution.test.js", "typecheck": "node scripts/typecheck.mjs", - "test:unit": "node --test test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js test/danmuku-mask-failures.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js test/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js test/chromecast.test.js test/chromecast-failures.test.js test/chromecast-runtime.test.js", + "test:unit": "node --test test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js test/danmuku-mask-failures.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-audio.test.js test/asr.test.js test/jassub.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js test/asr-fallback-routing.test.js test/asr-local-example.test.js test/asr-explicit-capture.test.js test/chromecast.test.js test/chromecast-failures.test.js test/chromecast-runtime.test.js", "test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs", "test": "yarn test:node && yarn test:baseline", "test:browser": "playwright test", @@ -100,7 +100,7 @@ "test:chromecast": "node --test test/chromecast.test.js test/chromecast-failures.test.js test/chromecast-runtime.test.js", "test:chromecast-types-package": "node refactor/scripts/chromecast-package-types.mjs", "test:danmuku-mask": "node --test test/danmuku-mask-failures.test.js refactor/scripts/danmuku-mask-contract.test.mjs", - "test:danmuku": "node --test test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js refactor/scripts/danmuku-contract.test.mjs" + "test:danmuku": "node --test test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js refactor/scripts/danmuku-contract.test.mjs" }, "browserslist": "last 1 Chrome version", "devDependencies": { diff --git a/packages/artplayer-plugin-danmuku/ARCHITECTURE.md b/packages/artplayer-plugin-danmuku/ARCHITECTURE.md index 807a016b2..c06bb9664 100644 --- a/packages/artplayer-plugin-danmuku/ARCHITECTURE.md +++ b/packages/artplayer-plugin-danmuku/ARCHITECTURE.md @@ -10,17 +10,21 @@ repository root. Do not infer runtime contracts from the old declaration file. | Module | Responsibility | | --- | --- | | `src/index.js` | Plugin factory, facade getters, bound methods, setting and heatmap composition | -| `src/danmuku.js` | Internal instance, public method return identity, queue commits and events; scheduling is still here pending PKG-DANMUKU-04 | +| `src/danmuku.js` | Internal instance, method return identity, configuration/load commits, event and lifecycle wiring | | `src/config.js` | Fresh defaults, validation schema, configuration comparison and normalization | | `src/input.js` | Input forms, replacement ownership, independent append operations, cancellation and synchronous callback reentry | | `src/bilibili-parser.js` | Pure legacy-compatible XML fields, mode mapping and entity decoding | | `src/bilibili.js` | Fetch and response text, one parser Worker per request, fallback, settlement and Blob URL cleanup | +| `src/scheduler.js` | One RAF or asynchronous frame, cancellation generations, preparation ownership and failure recovery; DOM preparation moves to a renderer in PKG-DANMUKU-05 | +| `src/queue.js` | Ordered state pools, eligibility and state transitions | +| `src/worker-client.js` | Track Worker lifecycle, a shared response dispatcher, unique request IDs and pending requests | | `src/worker.js` | Track placement Worker; a separate protocol from the XML parser Worker | | `src/setting.js` | Settings and emitter UI; lifecycle restructuring follows in PKG-DANMUKU-05 | | `src/heatmap.js` | Heatmap rendering; restructuring follows in PKG-DANMUKU-05 | The package is undergoing staged migration. PKG-DANMUKU-03 separates input and -configuration responsibilities in JavaScript; PKG-DANMUKU-06 converts all owned +configuration responsibilities in JavaScript; PKG-DANMUKU-04 separates scheduling +and track requests; PKG-DANMUKU-06 converts all owned modules and Worker messages to TypeScript and validates installed consumers. This intermediate structure is not the final TypeScript or release acceptance. @@ -47,8 +51,10 @@ This intermediate structure is not the final TypeScript or release acceptance. callbacks that start another replacement. - Keep input normalization observable: accepted objects receive missing mode, style and color before filtering, and the queue receives a shallow copy. - Explicit time zero is preserved; missing time defaults to current time + 0.5, - and negative time is clamped to zero. + Explicit time zero is preserved; missing time and `NaN` default to current + time + 0.5. The historical validator accepts `NaN` as a number, so preserving + zero must not accidentally remove that fallback. Positive infinity remains + positive infinity; negative values, including negative infinity, clamp to zero. - Function and Promise configuration changes use identity. Structurally equal ordinary values retain the historical JSON comparison behavior. Validate and normalize a prospective option before replacing the current option; invalid @@ -58,20 +64,77 @@ This intermediate structure is not the final TypeScript or release acceptance. empty result. Fetch/text failures must reach the public load's single error outlet and reject it; automatic initial loading observes that rejection. +## Scheduling ownership + +Keep at most one pending RAF or asynchronous preparation frame. Native `play` +and `playing` both invoke the existing start path and retain its events, but +must not create competing loops. Ready items precede wait items; wait selection +retains the current-time +/- 0.1 second window and state-pool order. Keep the +existing track algorithm and geometry fields until separately reviewed. + +Pause, reset, successful replacement input commit, hide, seeking, destroy and replacing the +`beforeVisible` callback invalidate unfinished preparation. Cancellation races +the user Promise so an unresolved callback cannot prevent later work. A frame +owns its allocated node; check that node identity before recycling it, and check +the generation after each await or callback. Existing emit/stop nodes survive +hide, seeking and callback changes; reset retains its explicit reset behavior. + +Seek does not implicitly reset already displayed or paused comments. Date.now +still measures wall-clock lifetime, excluding paused time. With synchronous +playback enabled, new comments sample speed/playbackRate when allocated; active +comments retain their assigned remaining seconds. Do not change these timing +rules as incidental cleanup. + +A rejected `beforeVisible` reports its original error once and excludes that +item from further attempts in the current run. Explicit start, reset, an +invalidation or replacement callback permits another attempt if the item is +still eligible. False results retain their ordinary next-frame evaluation. +Other items can continue after a rejection. Error-listener failures are observed +locally and logged instead of creating a detached rejected RAF Promise. + +A track Worker fault rejects its pending requests with the original error, +disposes that Worker and reports one error, then halts internal scheduling. +It does not fabricate a public stop event. A later start/reset rebuilds the +Worker; repeated failure waits for another explicit recovery opportunity. +Cancelled internal requests resolve `{ id, result: undefined }` instead of +remaining pending forever. Normal responses preserve `{ id, result }`. + +Recovery error listeners run synchronously and can call destroy, stop, reset, +hide or start again. After recovery, the outer start must recheck destruction, +stop and fault state, its cancellation generation and its start-attempt identity +before continuing paused rows, scheduling or emitting start. Generation checks +protect reset/hide and other invalidation; the attempt identity also protects a +nested start that succeeds without changing the generation. Do not let the outer +call revive stopped rows or add a second start event after a nested recovery. +Ordinary repeated starts still retain their historical events and internal +return identity. Recovery tests restore a working Worker factory before a +single nested action; they do not manufacture an infinite retry loop. + +Install the shared handler before sending requests. IDs are unique across +requests and Worker replacement, including multiple requests in one millisecond. +Late, unknown and duplicate replies do not settle a different request. + ## Verification and remaining work -`yarn test:danmuku` runs frozen historical contracts/failures and candidate input -and parser regressions. The historical helper and archives must remain frozen; +`yarn test:danmuku` runs frozen historical contracts/failures and candidate input, +parser, scheduler and Worker regressions. The historical helper and archives must remain frozen; candidate tests use `test/helpers/danmuku-candidate.js` and may select built artifacts through `ARTPLAYER_DANMUKU_ARTIFACT`. +The input tests compare `NaN` and both infinities against frozen source and real +npm main/legacy artifacts. Preserve those historical assertions alongside the +explicit-zero correction when changing numeric normalization. `test/browser/danmuku-input.spec.js` uses native video and Blob Workers against published and candidate cores. Run its source, main and legacy variants after the normal package build. Browser evidence distinguishes local XML/fault fixtures from the real Bilibili service, and Windows WebKit from Safari devices. -The older native track-overlap observation remains open until scheduler and -rendering work is verified. A passing input test is not evidence that duplicate -RAF loops, visibility continuations, settings cleanup, heatmap sampling or -long-run load behavior have been fixed. See PKG-DANMUKU-04/05/07 and the risk -register before changing completion or release status. +`test/browser/danmuku-scheduler.spec.js` verifies native callback cancellation, +three modes, strict dense geometry, one request per prepared item, native +pause/seek/rate and visible-node reuse. Preserve both geometric and protocol +assertions; unique IDs alone do not prove non-overlapping tracks. + +Settings cleanup, heatmap sampling and long-run load behavior still require +PKG-DANMUKU-05/07. Check task evidence and the risk register before changing +completion or release status; short native regressions are not package-wide +stability acceptance. diff --git a/packages/artplayer-plugin-danmuku/README.md b/packages/artplayer-plugin-danmuku/README.md index 2eb76011e..835fd40cd 100644 --- a/packages/artplayer-plugin-danmuku/README.md +++ b/packages/artplayer-plugin-danmuku/README.md @@ -20,11 +20,31 @@ reject the corresponding public `load()` Promise. Initial automatic loading observes its rejection and logs a warning. XML Worker startup/runtime failures use the existing parser locally and release Worker/Blob resources. -Explicit `time: 0` is preserved. A replacement callback passed to `config()` -takes effect, and invalid configuration leaves the current option intact. +Explicit `time: 0` is preserved. Missing time and `NaN` retain the historical +default of the current playback time plus 0.5 seconds. Positive infinity remains +unchanged; negative values, including negative infinity, are clamped to zero. +A replacement callback passed to `config()` takes effect, and invalid +configuration leaves the current option intact. Existing method return identities, getters, input normalization and XML fields remain compatible. Configuration changes do not automatically reload data. +## Scheduling failures and recovery + +Playback uses one scheduling loop. Pausing, seeking, hiding, resetting or +destroying the player cancels unfinished visibility preparation. Updating +`beforeVisible` also releases an unfinished old callback; seeking and callback +changes preserve already displayed comments. + +A rejected `beforeVisible` emits the original `artplayerPluginDanmuku:error` +once for that item in the current run, while other items continue. It is not +retried every frame. Pause/resume, reset or a replacement callback permits a +retry if the item still matches the display-time rules. A track Worker failure +reports once and halts scheduling until pause/resume or reset rebuilds it. + +If an error listener synchronously stops, destroys, resets or hides the instance, +the interrupted recovery respects that action. A nested start can recover once +without the outer call resuming it again or emitting another start event. + See [ARCHITECTURE.md](./ARCHITECTURE.md) for module ownership, compatibility constraints, tests and the remaining staged TypeScript migration. diff --git a/packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js b/packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js index e1a89fc1a..dc9308ff6 100644 --- a/packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js +++ b/packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js @@ -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(/]*?p="(?

[^"]+)"[^>]*>(?.*?)<\/d>/gs);return Array.from(i).map(t=>{const i=t.groups.p.split(",");if(i.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(i[0]),mode:e(Number(i[1])),fontSize:Number(i[2]),color:`#${Number(i[3]).toString(16)}`,timestamp:Number(i[4]),pool:Number(i[5]),userID:i[6],rowID:Number(i[7])}}return null}).filter(Boolean)}function a({data:t}){const{xml:e,id:a}=t,n=i(e);globalThis.postMessage({danmus:n,id:a})}let n=0;function s(t,{signal:s,onCancel:o}={}){return new Promise((r,l)=>{let d,p,h,u=!1;const c=++n,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!==p){const a=p;p=void 0;try{URL.revokeObjectURL(a)}catch(i){e||(t=i),e=!0}}if(e)throw t}function g(t,e){if(u)return;u=!0;let i=arguments.length>1;s?.removeEventListener("abort",m),h?.();try{f()}catch(a){i||(e=a),i=!0}i?l(e):r(t)}function y(t,e){if(!u){console.error("Error parsing Bilibili Danmu:",e);try{g(i(t))}catch(a){g(void 0,a)}}}s?.aborted?m():(s?.addEventListener("abort",m),h=o?.(m),u?h?.():async function(){const n=s?await fetch(t,{signal:s}):await fetch(t);if(u)return;const o=await n.text();if(!u)try{const t=`\n ${e.toString()}\n ${i.toString()}\n onmessage = ${a.toString()}\n `;if(p=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),u)return void f();if(d=new Worker(p),u)return void f();d.onmessage=t=>{if(u||t.data?.id!==c)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(o,t.error||t)},d.onmessageerror=t=>y(o,t.error||t),d.postMessage({xml:o,id:c})}catch(r){y(o,r)}}().catch(t=>g(void 0,t)))})}const o=new WeakMap,r=Symbol("cancelled danmuku input");function l(t){return o.has(t)||o.set(t,{closed:!1,tasks:new Set,emitting:[]}),o.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 p(t){const e=l(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}const h='/*!\n * artplayer-plugin-danmuku.js v5.3.0\n * Github: https://github.com/zhw2590582/ArtPlayer\n * (c) 2017-2026 Harvey Zhao\n * Released under the MIT License.\n */\nfunction t({target:t,visibles:e,clientWidth:i,clientHeight:h,marginBottom:n,marginTop:o,antiOverlap:s}){const r=h-n,p=e.filter(e=>e.mode===t.mode&&e.top<=r).sort((t,e)=>t.top-e.top);if(0===p.length)return 2===t.mode?r-t.height:o;if(p.unshift({type:"top",top:0,left:0,right:0,height:o,width:i,speed:0,distance:i}),p.push({type:"bottom",top:r,left:0,right:0,height:n,width:i,speed:0,distance:i}),2===t.mode)for(let g=p.length-2;g>=0;g-=1){const e=p[g],i=p[g+1],h=e.top+e.height;if(i.top-h>=t.height)return i.top-t.height}else for(let g=1;g=t.height)return h}const a=[];for(let g=1;g{const i=Math.min(...e.map(t=>t.right)),h=Math.min(...t.map(t=>t.right));return i*e.length-h*t.length});break;case 1:case 2:a.sort((t,e)=>{const i=Math.max(...e.map(t=>t.width));return Math.max(...t.map(t=>t.width))*t.length-i*e.length})}return a[0][0].top}switch(t.mode){case 0:{const e=a.find(e=>e.every(e=>{if(ie.time}));return e&&e[0]?e[0].top:void 0}case 1:case 2:return}}onmessage=e=>{const{data:i}=e;if(!i.id||!i.type)return;const h=(0,{getDanmuTop:t}[i.type])(i);globalThis.postMessage({result:h,id:i.id})};\n',u="undefined"!==typeof self&&self.Blob&&new Blob(["URL.revokeObjectURL(import.meta.url);",h],{type:"text/javascript;charset=utf-8"});function c(t){let e;try{if(e=u&&(self.URL||self.webkitURL).createObjectURL(u),!e)throw"";const i=new Worker(e,{type:"module",name:t?.name});return i.addEventListener("error",()=>{(self.URL||self.webkitURL).revokeObjectURL(e)}),i}catch(i){return new Worker("data:text/javascript;charset=utf-8,"+encodeURIComponent(h),{type:"module",name:t?.name})}}class m{constructor(t,e){const{constructor:i,template:a}=t;this.utils=i.utils,this.validator=i.validator,this.$danmuku=a.$danmuku,this.$player=a.$player,this.art=t,this.queue=[],this.$refs=[],this.isStop=!1,this.isHide=!1,this.timer=null,this.index=0,this.worker=null,this.option=m.option,this.states={wait:[],ready:[],emit:[],stop:[]},this.start=this.start.bind(this),this.stop=this.stop.bind(this),this.reset=this.reset.bind(this),this.resize=this.resize.bind(this),this.destroy=this.destroy.bind(this),d(this),t.on("destroy",this.destroy);try{if(this.config(e,!0),!d(this))return;this.worker=new c}catch(n){throw t.off("destroy",this.destroy),p(this),n}t.on("video:play",this.start),t.on("video:playing",this.start),t.on("video:pause",this.stop),t.on("video:waiting",this.stop),t.on("resize",this.resize),this.load().catch(t=>console.warn("Failed to load initial danmuku:",t))}static get option(){return{danmuku:[],speed:5,margin:[10,"25%"],opacity:1,color:"#FFFFFF",mode:0,modes:[0,1,2],fontSize:25,antiOverlap:!0,synchronousPlayback:!1,mount:void 0,heatmap:!1,width:512,points:[],filter:()=>!0,beforeEmit:()=>!0,beforeVisible:()=>!0,visible:!0,emitter:!0,maxLength:200,lockTime:5,theme:"dark",OPACITY:{},FONT_SIZE:{},MARGIN:{},SPEED:{},COLOR:[]}}static get scheme(){return{danmuku:"array|function|string|promise",speed:"number",margin:"array",opacity:"number",color:"string",mode:"number",modes:"array",fontSize:"number|string",antiOverlap:"boolean",synchronousPlayback:"boolean",mount:"?htmldivelement|string",heatmap:"object|boolean",width:"number",points:"array",filter:"function",beforeEmit:"function",beforeVisible:"function",visible:"boolean",emitter:"boolean",maxLength:"number",lockTime:"number",theme:"string",OPACITY:"object",FONT_SIZE:"object",MARGIN:"object",SPEED:"object",COLOR:"array"}}static get cssText(){return'\n user-select: none;\n position: absolute;\n white-space: pre;\n pointer-events: none;\n perspective: 500px;\n display: inline-block;\n will-change: transform;\n font-weight: normal;\n line-height: 1.125;\n visibility: hidden;\n font-family: SimHei, "Microsoft JhengHei", Arial, Helvetica, sans-serif;\n text-shadow: rgb(0, 0, 0) 1px 0px 1px, rgb(0, 0, 0) 0px 1px 1px, rgb(0, 0, 0) 0px -1px 1px, rgb(0, 0, 0) -1px 0px 1px;\n '}get isRotate(){return this.art.plugins?.autoOrientation?.state}get marginTop(){const{clamp:t}=this.utils,e=this.option.margin[0],{clientHeight:i}=this.$player;if("number"===typeof e)return t(e,0,i);if("string"===typeof e&&e.endsWith("%")){return t(i*(Number.parseFloat(e)/100),0,i)}return m.option.margin[0]}get marginBottom(){const{clamp:t}=this.utils,e=this.option.margin[1],{clientHeight:i}=this.$player;if("number"===typeof e)return t(e,0,i);if("string"===typeof e&&e.endsWith("%")){return t(i*(Number.parseFloat(e)/100),0,i)}return m.option.margin[1]}get fontSize(){const{clamp:t}=this.utils,{clientHeight:e}=this.$player,i=this.option.fontSize;if("number"===typeof i)return Math.round(t(i,12,e));if("string"===typeof i&&i.endsWith("%")){const a=Number.parseFloat(i)/100;return Math.round(t(e*a,12,e))}return m.option.fontSize}get $ref(){const t=this.$refs.pop()||document.createElement("div");return t.style.cssText=m.cssText,t.dataset.mode="",t.dataset.id="",t.className="",t}get readys(){const{currentTime:t}=this.art,e=[];return this.filter("ready",t=>e.push(t)),this.filter("wait",i=>{t+.1>=i.time&&i.time>=t-.1&&e.push(i)}),e}get visibles(){const t=[],{clientWidth:e}=this.$player,i=this.getLeft(this.$player);return this.filter("emit",a=>{const n=a.$ref.offsetTop,s=this.getLeft(a.$ref)-i,o=a.$ref.clientHeight,r=a.$ref.clientWidth,l=s+r,d=e-l,p=l/a.$restTime,h={};h.top=n,h.left=s,h.height=o,h.width=r,h.right=d,h.speed=p,h.distance=l,h.time=a.$restTime,h.mode=a.mode,t.push(h)}),t}get speed(){return this.option.synchronousPlayback&&this.art.playbackRate?this.option.speed/Number(this.art.playbackRate):this.option.speed}async load(e){const{errorHandle:i}=this.utils,a=function(t,e){const i=l(t);if(e)for(const r of[...i.tasks])r.replace&&r.cancel();let a,n=i.closed||t.art.isDestroy;const s=new Promise(t=>a=t),o=new Set,d="function"===typeof AbortController?new AbortController:void 0,p={replace:e,signal:d?.signal,active:()=>!n&&!i.closed&&!t.art.isDestroy,onCancel:t=>p.active()?(o.add(t),()=>o.delete(t)):(t(),()=>{}),cancel(){if(!n){n=!0,a(r),d?.abort();for(const t of[...o])t();o.clear()}},wait:t=>Promise.race([t,s]),emit(e){i.emitting.push(p);try{return t.emit(e)}finally{i.emitting.pop()}},finish(){i.tasks.delete(p),o.clear()}};return n?a(r):i.tasks.add(p),p}(this,void 0===e);try{if(!a.active())return this;const n=function(e,i){return"function"===typeof e?{asynchronous:!0,value:e()}:t(e)?{asynchronous:!0,value:e}:"string"===typeof e?{asynchronous:!0,value:s(e,i)}:{asynchronous:!1,value:Array.isArray(e)?e:[]}}(e||this.option.danmuku,a),o=n.asynchronous?await a.wait(n.value):n.value;if(!a.active())return this;if(i(Array.isArray(o),"Danmuku need return an array as result"),void 0===e){if(this.reset(),!a.active())return this;this.queue=[],this.states={wait:[],ready:[],emit:[],stop:[]},this.$refs=[],this.$danmuku.textContent=""}for(let t=0;t{const i=s[e],a=o[e];return"function"===typeof i||"function"===typeof a||t(i)||t(a)?i!==a:JSON.stringify(i)!==JSON.stringify(a)}))&&!i)return this;const r=function(t,e,{defaults:i,validate:a,clamp:n,mount:s}){const o=Object.assign({},i,t,e);return a(o),o.mode=n(o.mode,0,2),o.speed=n(o.speed,1,10),o.opacity=n(o.opacity,0,1),o.lockTime=n(o.lockTime,1,60),o.maxLength=n(o.maxLength,1,1e3),o.mount=o.mount||s,o}(this.option,e,{defaults:m.option,validate:t=>this.validator(t,m.scheme),clamp:a,mount:n});return d(this)?(this.option=r,e.fontSize&&(this.reset(),!d(this))||(this.option.visible?this.show():this.hide(),d(this)&&this.art.emit("artplayerPluginDanmuku:config",this.option)),this):this}getLeft(t){const e=t.getBoundingClientRect();return this.isRotate?e.top:e.left}postMessage(t={}){return new Promise(e=>{t.id=Date.now(),this.worker.postMessage(t),this.worker.onmessage=i=>{const{data:a}=i;a.id===t.id&&e(a)}})}filter(t,e){const i=this.states[t]||[];for(let a=0;ae!==t),t.$state=e,t.$ref&&(t.$ref.dataset.state=e),this.states[e].push(t)}makeWait(t){this.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=m.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.$refs.push(t.$ref),t.$ref=null)}update(){const{setStyles:t}=this.utils;return this.timer=window.requestAnimationFrame(async()=>{if(this.art.playing&&!this.isHide){this.filter("emit",t=>{const e=(Date.now()-t.$lastStartTime)/1e3;t.$restTime-=e,t.$lastStartTime=Date.now(),t.$restTime<=0&&this.makeWait(t)});const e=this.readys;for(let i=0;i{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.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.$player;return this.filter("stop",e=>{switch(this.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.$player;return this.filter("emit",e=>{switch(this.setState(e,"stop"),e.mode){case 0:{const i=t-(this.getLeft(e.$ref)-this.getLeft(this.$player));e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition="transform 0s linear 0s";break}}}),this}stop(){return this.isStop=!0,this.suspend(),window.cancelAnimationFrame(this.timer),this.art.emit("artplayerPluginDanmuku:stop"),this}start(){return this.isStop=!1,this.continue(),this.update(),this.art.emit("artplayerPluginDanmuku:start"),this}reset(){return this.queue.forEach(t=>this.makeWait(t)),this.art.emit("artplayerPluginDanmuku:reset"),this}show(){return this.isHide=!1,this.$danmuku.style.opacity=1,this.option.visible=!0,this.art.emit("artplayerPluginDanmuku:show"),this}hide(){return this.isHide=!0,this.$danmuku.style.opacity=0,this.option.visible=!1,this.art.emit("artplayerPluginDanmuku:hide"),this}destroy(){p(this),this.stop(),this.worker?.terminate(),this.art.off("video:play",this.start),this.art.off("video:playing",this.start),this.art.off("video:pause",this.stop),this.art.off("video:waiting",this.stop),this.art.off("resize",this.resize),this.art.off("destroy",this.destroy),this.art.emit("artplayerPluginDanmuku:destroy")}}const f={map:(t,e,i,a,n)=>(t-e)*(n-a)/(i-e)+a,range(t,e,i){const a=Math.round(t/i)*i;return Array.from({length:Math.floor((e-t)/i)},(t,e)=>e*i+a)}};function g(t,e,i){const{query:a}=t.constructor.utils;t.controls.add({name:"heatmap",position:"top",html:"",style:{position:"absolute",top:"-100px",left:"0px",right:"0px",height:"100px",width:"100%",pointerEvents:"none"},mounted(n){let s=null,o=null;function r(r=[]){if(s=null,o=null,n.innerHTML="",!t.duration||t.option.isLive)return;const l={w:n.offsetWidth,h:n.offsetHeight},d={xMin:0,xMax:l.w,yMin:0,yMax:128,scale:.25,opacity:.2,minHeight:Math.floor(.05*l.h),sampling:Math.floor(l.w/100),smoothing:.2,flattening:.2};"object"===typeof i&&Object.assign(d,i);let p=[];if(Array.isArray(r)&&r.length)p=[...r];else{const i=t.duration/l.w;for(let t=0;t<=l.w;t+=d.sampling){const a=e.queue.filter(({time:e})=>e>t*i&&e<=(t+d.sampling)*i).length;p.push([t,a])}}if(0===p.length)return;const h=p[p.length-1],u=h[0],c=h[1];u!==l.w&&p.push([l.w,c]);const m=p.map(t=>t[1]),g=(Math.min(...m)+Math.max(...m))/2;for(let t=0;tg?1+d.scale:1-d.scale)+d.minHeight}const y=(t,e,i,a)=>{const n=function(t,e){const i=e[0]-t[0],a=e[1]-t[1];return{length:Math.sqrt(i**2+a**2),angle:Math.atan2(a,i)}}(e||t,i||t),s=f.map(Math.cos(n.angle)*d.flattening,0,1,1,0),o=n.angle*s+(a?Math.PI:0),r=n.length*d.smoothing;return[t[0]+Math.cos(o)*r,t[1]+Math.sin(o)*r]},v=p.map(t=>[f.map(t[0],d.xMin,d.xMax,0,l.w),f.map(t[1],d.yMin,d.yMax,l.h,0)]).reduce((t,e,i,a)=>0===i?`M ${a[a.length-1][0]},${l.h} L ${e[0]},${l.h} L ${e[0]},${e[1]}`:`${t} ${((t,e,i)=>{const a=y(i[e-1],i[e-2],t),n=y(t,i[e-1],i[e+1],!0),s=e===i.length-1?" z":"";return`C ${a[0]},${a[1]} ${n[0]},${n[1]} ${t[0]},${t[1]}${s}`})(e,i,a)}`,"");n.innerHTML=`\n \n \n \n \n \n \n \n \n \n \n \n `,s=a("#heatmap-start",n),o=a("#heatmap-stop",n),s.setAttribute("offset",100*t.played+"%"),o.setAttribute("offset",100*t.played+"%")}t.on("video:timeupdate",()=>{s&&o&&(s.setAttribute("offset",100*t.played+"%"),o.setAttribute("offset",100*t.played+"%"))}),t.on("setBar",(t,e)=>{s&&o&&"played"===t&&(s.setAttribute("offset",100*e+"%"),o.setAttribute("offset",100*e+"%"))}),t.on("ready",()=>r()),t.on("resize",()=>r()),t.on("artplayerPluginDanmuku:loaded",()=>r()),t.on("artplayerPluginDanmuku:points",t=>r(t))}})}const y='',v='',k='',$='',w='',x='',b='',S='',M='',z='',E='',C='';class T{constructor(t,e){this.art=t,this.danmuku=e,this.utils=t.constructor.utils;const{setStyle:i}=this.utils,{$controlsCenter:a}=t.template;i(a,"display","flex"),this.template={$controlsCenter:a,$mount:a,$danmuku:null,$toggle:null,$config:null,$configPanel:null,$configModes:null,$style:null,$stylePanel:null,$styleModes:null,$colors:null,$opacitySlider:null,$opacityValue:null,$marginSlider:null,$marginValue:null,$fontSizeSlider:null,$fontSizeValue:null,$speedSlider:null,$speedValue:null,$input:null,$send:null},this.slider={opacity:null,margin:null,fontSize:null,speed:null},this.emitting=!1,this.isLock=!1,this.timer=null,this.createTemplate(),this.createSliders(),this.createEvents(),this.mount(this.option.mount),t.on("resize",()=>this.resize()),t.on("fullscreen",t=>this.onFullscreen(t)),t.on("fullscreenWeb",t=>this.onFullscreen(t)),t.proxy(this.template.$config,"mouseenter",()=>{this.onMouseEnter({$control:this.template.$config,$panel:this.template.$configPanel})}),t.proxy(this.template.$style,"mouseenter",()=>{this.onMouseEnter({$control:this.template.$style,$panel:this.template.$stylePanel})})}static get icons(){return{$on:E,$off:z,$config:k,$style:C,$mode_0_off:$,$mode_0_on:w,$mode_1_off:x,$mode_1_on:b,$mode_2_off:S,$mode_2_on:M,$check_on:v,$check_off:y}}get option(){return this.danmuku.option}get outside(){return this.template.$mount!==this.template.$controlsCenter}get TEMPLATE(){const{option:t}=this;return`\n

\n ${E}${z}\n
\n
\n ${k}\n
\n
\n
\n \u6309\u7c7b\u578b\u5c4f\u853d\n
\n
\n ${$}${w}\n
\u6eda\u52a8
\n
\n
\n ${x}${b}\n
\u9876\u90e8
\n
\n
\n ${S}${M}\n
\u5e95\u90e8
\n
\n
\n
\n
\n
\n ${v}${y}\n \u9632\u6b62\u5f39\u5e55\u91cd\u53e0\n
\n
\n ${v}${y}\n \u540c\u6b65\u89c6\u9891\u901f\u5ea6\n
\n
\n
\n \u4e0d\u900f\u660e\u5ea6\n
\n
\u672a\u77e5
\n
\n
\n \u663e\u793a\u533a\u57df\n
\n
\u672a\u77e5
\n
\n
\n \u5f39\u5e55\u5b57\u53f7\n
\n
\u672a\u77e5
\n
\n
\n \u5f39\u5e55\u901f\u5ea6\n
\n
\u672a\u77e5
\n
\n
\n
\n
\n
\n
\n ${C}\n
\n
\n
\n \u6a21\u5f0f\n
\n
\n ${w}\n
\u6eda\u52a8
\n
\n
\n ${b}\n
\u9876\u90e8
\n
\n
\n ${M}\n
\u5e95\u90e8
\n
\n
\n
\n
\n \u989c\u8272\n
\n ${this.COLOR.map(t=>`
`).join("")}\n
\n
\n
\n
\n
\n \n
\u53d1\u9001
\n
\n `}get OPACITY(){return{min:0,max:100,steps:[],...this.option.OPACITY}}get FONT_SIZE(){return{min:12,max:120,steps:[],...this.option.FONT_SIZE}}get MARGIN(){return{min:0,max:3,steps:[{name:"1/4",value:[10,"75%"]},{name:"\u534a\u5c4f",value:[10,"50%"]},{name:"3/4",value:[10,"25%"]},{name:"\u6ee1\u5c4f",value:[10,10]}],...this.option.MARGIN}}get SPEED(){return{min:0,max:4,steps:[{name:"\u6781\u6162",value:10},{name:"\u8f83\u6162",value:7.5,hide:!0},{name:"\u9002\u4e2d",value:5},{name:"\u8f83\u5feb",value:2.5,hide:!0},{name:"\u6781\u5feb",value:1}],...this.option.SPEED}}get COLOR(){return this.option.COLOR.length?this.option.COLOR:["#FE0302","#FF7204","#FFAA02","#FFD302","#FFFF00","#A0EE00","#00CD00","#019899","#4266BE","#89D5FF","#CC0273","#222222","#9B9B9B","#FFFFFF"]}query(t){const{query:e}=this.utils,{$danmuku:i}=this.template;return e(t,i)}append(t,e){const{append:i}=this.utils;[...t.children].includes(e)||i(t,e)}setData(t,e){const{$player:i}=this.art.template,{$mount:a}=this.template;i.dataset[t]=e,this.outside&&(a.dataset[t]=e)}createTemplate(){const{createElement:t,tooltip:e}=this.utils,i=t("div");i.className="artplayer-plugin-danmuku",i.innerHTML=this.TEMPLATE,this.template.$danmuku=i,this.template.$toggle=this.query(".apd-toggle"),this.template.$config=this.query(".apd-config"),this.template.$configPanel=this.query(".apd-config-panel"),this.template.$configModes=this.query(".apd-config-mode .apd-modes"),this.template.$style=this.query(".apd-style"),this.template.$stylePanel=this.query(".apd-style-panel"),this.template.$styleModes=this.query(".apd-style-mode .apd-modes"),this.template.$colors=this.query(".apd-colors"),this.template.$antiOverlap=this.query(".apd-anti-overlap"),this.template.$syncVideo=this.query(".apd-sync-video"),this.template.$opacitySlider=this.query(".apd-config-opacity .apd-slider"),this.template.$opacityValue=this.query(".apd-config-opacity .apd-value"),this.template.$marginSlider=this.query(".apd-config-margin .apd-slider"),this.template.$marginValue=this.query(".apd-config-margin .apd-value"),this.template.$fontSizeSlider=this.query(".apd-config-fontSize .apd-slider"),this.template.$fontSizeValue=this.query(".apd-config-fontSize .apd-value"),this.template.$speedSlider=this.query(".apd-config-speed .apd-slider"),this.template.$speedValue=this.query(".apd-config-speed .apd-value"),this.template.$input=this.query(".apd-input"),this.template.$send=this.query(".apd-send");const{$toggle:a}=this.template;this.art.on("artplayerPluginDanmuku:show",()=>{e(a,"\u5173\u95ed\u5f39\u5e55")}),this.art.on("artplayerPluginDanmuku:hide",()=>{e(a,"\u6253\u5f00\u5f39\u5e55")})}createEvents(){const{$toggle:t,$configModes:e,$styleModes:i,$colors:a,$antiOverlap:n,$syncVideo:s,$send:o,$input:r}=this.template;this.art.proxy(t,"click",()=>{this.danmuku.config({visible:!this.option.visible}),this.reset()}),this.art.proxy(e,"click",t=>{const e=t.target.closest(".apd-mode");if(!e)return;const i=Number(e.dataset.mode);this.option.modes.includes(i)?this.danmuku.config({modes:this.option.modes.filter(t=>t!==i)}):this.danmuku.config({modes:[...this.option.modes,i]}),this.reset()}),this.art.proxy(n,"click",()=>{this.danmuku.config({antiOverlap:!this.option.antiOverlap}),this.reset()}),this.art.proxy(s,"click",()=>{this.danmuku.config({synchronousPlayback:!this.option.synchronousPlayback}),this.reset()}),this.art.proxy(i,"click",t=>{const e=t.target.closest(".apd-mode");if(!e)return;const i=Number(e.dataset.mode);this.danmuku.config({mode:i}),this.reset()}),this.art.proxy(a,"click",t=>{const e=t.target.closest(".apd-color");e&&(this.danmuku.config({color:e.dataset.color}),this.reset())}),this.art.proxy(o,"click",()=>this.emit()),this.art.proxy(r,"keypress",t=>{"Enter"===t.key&&(t.preventDefault(),this.emit())})}createSliders(){this.slider.opacity=this.createSlider({...this.OPACITY,container:this.template.$opacitySlider,findIndex:()=>Math.round(100*this.option.opacity),onChange:t=>{const{$opacityValue:e}=this.template;e.textContent=`${t}%`,this.danmuku.config({opacity:t/100})}}),this.slider.margin=this.createSlider({...this.MARGIN,container:this.template.$marginSlider,findIndex:()=>this.MARGIN.steps.findIndex(t=>t.value[0]===this.option.margin[0]&&t.value[1]===this.option.margin[1]),onChange:t=>{const e=this.MARGIN.steps[t];if(!e)return;const{$marginValue:i}=this.template;i.textContent=e.name,this.danmuku.config({margin:e.value})}}),this.slider.fontSize=this.createSlider({...this.FONT_SIZE,container:this.template.$fontSizeSlider,findIndex:()=>this.danmuku.fontSize,onChange:t=>{const{$fontSizeValue:e}=this.template;e.textContent=`${t}px`,t!==this.danmuku.fontSize&&this.danmuku.config({fontSize:t})}}),this.slider.speed=this.createSlider({...this.SPEED,container:this.template.$speedSlider,findIndex:()=>this.SPEED.steps.findIndex(t=>t.value===this.option.speed),onChange:t=>{const e=this.SPEED.steps[t];if(!e)return;const{$speedValue:i}=this.template;i.textContent=e.name,this.danmuku.config({speed:e.value})}})}createSlider({min:t,max:e,container:i,findIndex:a,onChange:n,steps:s=[]}){const{query:o,clamp:r,setStyle:l}=this.utils;l(i,"touch-action","none"),i.innerHTML=`\n
\n
\n ${s.map(()=>'
').join("")}\n
\n
\n
\n
\n
\n ${s.map(t=>t.hide?"":`
${t.name}
`).join("")}\n
\n `;const d=o(".apd-slider-dot",i),p=o(".apd-slider-progress",i);let h=!1;function u(i=a()){if(ie)return;const o=(i-t)/(e-t);d.style.left=100*o+"%",0===s.length&&(p.style.width=d.style.left),n(i)}function c(a){const{top:n,height:s,left:o,width:l}=i.getBoundingClientRect();if(this.art.isRotate){const i=r(a.clientY-n,0,s);u(Math.round(i/s*(e-t)+t))}else{const i=r(a.clientX-o,0,l);u(Math.round(i/l*(e-t)+t))}}return this.art.proxy(i,"click",t=>{c.call(this,t)}),this.art.proxy(i,"pointerdown",t=>{h=0===t.button}),this.art.on("document:pointermove",t=>{h&&c.call(this,t)}),this.art.on("document:pointerup",t=>{h&&(h=!1,c.call(this,t))}),{reset:u}}onFullscreen(t){const{$danmuku:e,$controlsCenter:i,$mount:a}=this.template;this.outside?t?this.append(i,e):this.append(a,e):this.append(i,e)}onMouseEnter({$control:t,$panel:e}){const{$player:i}=this.art.template,a=t.getBoundingClientRect(),n=e.getBoundingClientRect(),s=i.getBoundingClientRect(),o=n.width/2-a.width/2,r=s.left-(a.left-o),l=a.right+o-s.right;e.style.left=r>0?`${-o+r}px`:l>0?-o-l+"px":-o+"px"}async emit(){const{$input:t}=this.template,e=t.value.trim();if(!e.length)return;if(this.isLock)return;if(this.emitting)return;const i={text:e,mode:this.option.mode,color:this.option.color,time:this.art.currentTime};try{this.emitting=!0;const e=await this.option.beforeEmit(i);if(this.emitting=!1,!0!==e)return;i.border=!0,delete i.time,this.danmuku.emit(i),t.value="",this.lock()}catch(a){console.error("Error emitting danmuku:",a),this.emitting=!1}}lock(){const{addClass:t}=this.utils,{$send:e}=this.template;this.isLock=!0;let i=this.option.lockTime;e.textContent=i,t(e,"apd-lock");const a=()=>{this.timer=setTimeout(()=>{0===i?this.unlock():(i-=1,e.textContent=i,a())},1e3)};a()}unlock(){const{removeClass:t}=this.utils,{$send:e}=this.template;clearTimeout(this.timer),this.isLock=!1,e.textContent="\u53d1\u9001",t(e,"apd-lock")}resize(){if(this.outside)return;if(this.art.fullscreen)return;if(this.art.fullscreenWeb)return;const{$player:t,$controlsCenter:e}=this.art.template,{$danmuku:i}=this.template;this.art.widtht.dataset.color===this.option.color.toUpperCase());s&&t(s,"apd-active"),e(i,this.option.visible?"\u5173\u95ed\u5f39\u5e55":"\u6253\u5f00\u5f39\u5e55"),this.resize()}mount(t){const{errorHandle:e}=this.utils,i="string"===typeof t?document.querySelector(t):t;e(i,`Can not find the mount point: ${t}`),this.append(i,this.template.$danmuku),this.template.$mount=i,this.reset()}}if("undefined"!==typeof document){const t="artplayer-plugin-danmuku";let e=document.getElementById(t);e||(e=document.createElement("style"),e.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.appendChild(e)}):(document.head||document.documentElement).appendChild(e)),e.textContent='.artplayer-plugin-danmuku{display:flex;position:relative;z-index:99;align-items:center;justify-content:center;font-size:12px;height:32px;width:100%;color:#fff;font-weight:300;flex-shrink:0;gap:10px}.artplayer-plugin-danmuku .apd-icon{cursor:pointer;opacity:.75;transition:all .2s ease;fill:#fff}.artplayer-plugin-danmuku .apd-icon:hover{opacity:1}.artplayer-plugin-danmuku .apd-config{display:flex;position:relative}.artplayer-plugin-danmuku .apd-config .apd-config-panel{position:absolute;bottom:24px;left:0;width:320px;padding:10px;opacity:0;pointer-events:none}.artplayer-plugin-danmuku .apd-config .apd-config-panel .apd-config-panel-inner{width:100%;border-radius:3px;background-color:#000000d9;padding:10px}.artplayer-plugin-danmuku .apd-config:hover .apd-config-panel{opacity:100;pointer-events:all}.artplayer-plugin-danmuku .apd-config-mode,.artplayer-plugin-danmuku .apd-config-slider,.artplayer-plugin-danmuku .apd-config-other,.artplayer-plugin-danmuku .apd-style-mode{margin-bottom:15px}.artplayer-plugin-danmuku .apd-modes{display:flex;align-items:center;margin-top:5px;gap:20px}.artplayer-plugin-danmuku .apd-modes .apd-mode{cursor:pointer;text-align:center}.artplayer-plugin-danmuku .apd-modes .apd-mode:hover{color:#00a1d6}.artplayer-plugin-danmuku .apd-config-slider{display:flex;align-items:center;gap:12px}.artplayer-plugin-danmuku .apd-config-slider .apd-value{width:32px;text-align:right}.artplayer-plugin-danmuku .apd-slider{position:relative;flex:1;display:flex;height:20px;align-items:center;justify-content:center;cursor:pointer}.artplayer-plugin-danmuku .apd-slider .apd-slider-line{position:relative;height:2px;width:100%;overflow:hidden;border-radius:3px;background-color:#ffffff40}.artplayer-plugin-danmuku .apd-slider .apd-slider-points{position:absolute;inset:0;display:flex;align-items:center;justify-content:space-between}.artplayer-plugin-danmuku .apd-slider .apd-slider-points .apd-slider-point{width:2px;height:2px;border-radius:50%;background-color:#ffffff80}.artplayer-plugin-danmuku .apd-slider .apd-slider-progress{width:0%;height:100%;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-slider .apd-slider-dot{position:absolute;transform:translate(-6px);left:0%;width:12px;height:12px;border-radius:50%;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-slider .apd-slider-steps{display:flex;align-items:center;justify-content:space-between;position:absolute;bottom:-12px;width:calc(100% + 32px);color:#777}.artplayer-plugin-danmuku .apd-slider .apd-slider-steps .apd-slider-step{flex-shrink:0;width:36px;text-align:center;scale:.95}.artplayer-plugin-danmuku .apd-config-other{display:flex;align-items:center;gap:20px}.artplayer-plugin-danmuku .apd-config-other .apd-check-off,.artplayer-plugin-danmuku .apd-config-other .apd-check-on{width:16px;height:16px}.artplayer-plugin-danmuku .apd-config-other .apd-other{display:flex;align-items:center;cursor:pointer;gap:2px}.artplayer-plugin-danmuku .apd-config-other .apd-other:hover{color:#00a1d6}.artplayer-plugin-danmuku .apd-emitter{display:flex;flex:1;align-items:center;height:100%;background-color:#ffffff40;border-radius:5px}.artplayer-plugin-danmuku .apd-style{position:relative;display:flex;align-items:center;justify-content:center}.artplayer-plugin-danmuku .apd-style .apd-style-panel{position:absolute;bottom:24px;left:0;width:200px;padding:10px;opacity:0;pointer-events:none}.artplayer-plugin-danmuku .apd-style .apd-style-panel .apd-style-panel-inner{width:100%;border-radius:3px;background-color:#000000d9;padding:10px}.artplayer-plugin-danmuku .apd-style:hover .apd-style-panel{opacity:100;pointer-events:all}.artplayer-plugin-danmuku .apd-colors{display:flex;flex-wrap:wrap;margin-top:5px;gap:8px}.artplayer-plugin-danmuku .apd-colors .apd-color{width:16px;height:16px;border-radius:2px;cursor:pointer}.artplayer-plugin-danmuku .apd-colors .apd-color.apd-active{border:1px solid black;box-shadow:0 0 0 1px #fff}.artplayer-plugin-danmuku .apd-input{outline:none;height:100%;flex:1;min-width:0;width:auto;border:none;line-height:1;color:#fff;background-color:transparent}.artplayer-plugin-danmuku .apd-input::placeholder{color:#ffffff80}.artplayer-plugin-danmuku .apd-send{display:flex;align-items:center;justify-content:center;height:100%;width:60px;flex-shrink:0;cursor:pointer;text-shadow:none;border-top-right-radius:5px;border-bottom-right-radius:5px;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-send.apd-lock{cursor:not-allowed;color:#666;background-color:#e7e7e7}.art-controls-center .apd-emitter{width:260px;flex:none}.art-fullscreen .artplayer-plugin-danmuku,.art-fullscreen-web .artplayer-plugin-danmuku{height:38px;gap:16px}.art-fullscreen .artplayer-plugin-danmuku .apd-config-icon,.art-fullscreen-web .artplayer-plugin-danmuku .apd-config-icon,.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-off,.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-off,.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-on,.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-on{width:28px;height:28px}.art-fullscreen .artplayer-plugin-danmuku .apd-emitter,.art-fullscreen-web .artplayer-plugin-danmuku .apd-emitter{width:400px;flex:none}.art-video-player>.artplayer-plugin-danmuku{position:absolute;left:0;right:0;bottom:-40px;padding:0 10px}.art-video-player:has(>.artplayer-plugin-danmuku){margin-bottom:40px}[data-danmuku-emitter=false] .apd-emitter{display:none!important}[data-danmuku-emitter=false] .art-controls-center .artplayer-plugin-danmuku{justify-content:flex-end;gap:18px}[data-danmuku-emitter=false].art-fullscreen .art-controls-center .artplayer-plugin-danmuku,[data-danmuku-emitter=false].art-fullscreen-web .art-controls-center .artplayer-plugin-danmuku{gap:24px}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-icon{fill:#333}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-emitter{background-color:#f1f2f3}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-input{color:#000}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-input::placeholder{color:#0000004d}[data-danmuku-visible=false] .apd-toggle-off{display:block}[data-danmuku-visible=false] .apd-toggle-on,[data-danmuku-visible=true] .apd-toggle-off{display:none}[data-danmuku-visible=true] .apd-toggle-on{display:block}[data-danmuku-anti-overlap=false] .apd-anti-overlap .apd-check-on{display:none}[data-danmuku-anti-overlap=false] .apd-anti-overlap .apd-check-off,[data-danmuku-anti-overlap=true] .apd-anti-overlap .apd-check-on{display:block}[data-danmuku-anti-overlap=true] .apd-anti-overlap .apd-check-off,[data-danmuku-sync-video=false] .apd-sync-video .apd-check-on{display:none}[data-danmuku-sync-video=false] .apd-sync-video .apd-check-off,[data-danmuku-sync-video=true] .apd-sync-video .apd-check-on{display:block}[data-danmuku-sync-video=true] .apd-sync-video .apd-check-off{display:none}[data-danmuku-mode0=false] .apd-config-mode .apd-mode-0-off{display:block}[data-danmuku-mode0=false] .apd-config-mode .apd-mode-0-on{display:none}[data-danmuku-mode0=false] .art-danmuku [data-mode="0"]{opacity:0!important}[data-danmuku-mode0=true] .apd-config-mode .apd-mode-0-off{display:none}[data-danmuku-mode0=true] .apd-config-mode .apd-mode-0-on{display:block}[data-danmuku-mode="0"] .apd-style-mode [data-mode="0"]{color:#00a1d6}[data-danmuku-mode="0"] .apd-style-mode [data-mode="0"] path{fill:#00a1d6}[data-danmuku-mode1=false] .apd-config-mode .apd-mode-1-off{display:block}[data-danmuku-mode1=false] .apd-config-mode .apd-mode-1-on{display:none}[data-danmuku-mode1=false] .art-danmuku [data-mode="1"]{opacity:0!important}[data-danmuku-mode1=true] .apd-config-mode .apd-mode-1-off{display:none}[data-danmuku-mode1=true] .apd-config-mode .apd-mode-1-on{display:block}[data-danmuku-mode="1"] .apd-style-mode [data-mode="1"]{color:#00a1d6}[data-danmuku-mode="1"] .apd-style-mode [data-mode="1"] path{fill:#00a1d6}[data-danmuku-mode2=false] .apd-config-mode .apd-mode-2-off{display:block}[data-danmuku-mode2=false] .apd-config-mode .apd-mode-2-on{display:none}[data-danmuku-mode2=false] .art-danmuku [data-mode="2"]{opacity:0!important}[data-danmuku-mode2=true] .apd-config-mode .apd-mode-2-off{display:none}[data-danmuku-mode2=true] .apd-config-mode .apd-mode-2-on{display:block}[data-danmuku-mode="2"] .apd-style-mode [data-mode="2"]{color:#00a1d6}[data-danmuku-mode="2"] .apd-style-mode [data-mode="2"] path{fill:#00a1d6}'}function F(t){return e=>{const i=new m(e,t),a=e.isDestroy?void 0:new T(e,i);return!e.isDestroy&&i.option.heatmap&&g(e,i,i.option.heatmap),{name:"artplayerPluginDanmuku",emit:i.emit.bind(i),load:i.load.bind(i),config:i.config.bind(i),hide:i.hide.bind(i),show:i.show.bind(i),reset:i.reset.bind(i),mount:a?a.mount.bind(a):()=>{},get option(){return i.option},get isHide(){return i.isHide},get isStop(){return i.isStop}}}}return F.icons=T.icons,F}); +!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(/]*?p="(?

[^"]+)"[^>]*>(?.*?)<\/d>/gs);return Array.from(i).map(t=>{const i=t.groups.p.split(",");if(i.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(i[0]),mode:e(Number(i[1])),fontSize:Number(i[2]),color:`#${Number(i[3]).toString(16)}`,timestamp:Number(i[4]),pool:Number(i[5]),userID:i[6],rowID:Number(i[7])}}return null}).filter(Boolean)}function a({data:t}){const{xml:e,id:a}=t,n=i(e);globalThis.postMessage({danmus:n,id:a})}let n=0;function s(t,{signal:s,onCancel:o}={}){return new Promise((r,l)=>{let d,p,h,u=!1;const c=++n,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!==p){const a=p;p=void 0;try{URL.revokeObjectURL(a)}catch(i){e||(t=i),e=!0}}if(e)throw t}function g(t,e){if(u)return;u=!0;let i=arguments.length>1;s?.removeEventListener("abort",m),h?.();try{f()}catch(a){i||(e=a),i=!0}i?l(e):r(t)}function y(t,e){if(!u){console.error("Error parsing Bilibili Danmu:",e);try{g(i(t))}catch(a){g(void 0,a)}}}s?.aborted?m():(s?.addEventListener("abort",m),h=o?.(m),u?h?.():async function(){const n=s?await fetch(t,{signal:s}):await fetch(t);if(u)return;const o=await n.text();if(!u)try{const t=`\n ${e.toString()}\n ${i.toString()}\n onmessage = ${a.toString()}\n `;if(p=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),u)return void f();if(d=new Worker(p),u)return void f();d.onmessage=t=>{if(u||t.data?.id!==c)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(o,t.error||t)},d.onmessageerror=t=>y(o,t.error||t),d.postMessage({xml:o,id:c})}catch(r){y(o,r)}}().catch(t=>g(void 0,t)))})}const o=new WeakMap,r=Symbol("cancelled danmuku input");function l(t){return o.has(t)||o.set(t,{closed:!1,tasks:new Set,emitting:[]}),o.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 p(t){const e=l(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function h(t,e,i){const a=t.states[e]||[];for(let n=0;n{if(this.frame=null,t.timer=null,this.closed||this.fault||t.isStop||t.art.isDestroy)return;let e;const i=new Promise(t=>e=()=>t(u)),a={generation:this.generation,cancel:e,wait:t=>Promise.race([t,i]),ref:null};return this.operation=a,this.run(a).catch(t=>{this.active(a)&&this.fail(t)}).finally(()=>{this.release(a),this.operation===a&&(this.operation=null,this.schedule())})}),t.timer=this.frame)}async run(t){const e=this.owner,{setStyles:i}=e.utils;if(!e.art.playing||!this.active(t))return;e.filter("emit",t=>{const i=(Date.now()-t.$lastStartTime)/1e3;t.$restTime-=i,t.$lastStartTime=Date.now(),t.$restTime<=0&&e.makeWait(t)});const a=e.readys;for(const s of a){if(!this.active(t))return;if(this.failedItems.has(s))continue;let a;try{a=await t.wait(e.option.beforeVisible(s))}catch(n){if(!this.active(t))return;this.failedItems.add(s),this.report(n);continue}if(!this.active(t))return;if(!a)continue;const{clientWidth:o,clientHeight:r}=e.$player,l=e.$ref;if(t.danmu=s,t.ref=l,s.$ref=l,l.textContent=s.text,e.$danmuku.appendChild(l),l.style.opacity=e.option.opacity,l.style.fontSize=`${e.fontSize}px`,l.style.color=s.color,l.style.border=s.border?`1px solid ${s.color}`:null,l.style.backgroundColor=s.border?"rgb(0 0 0 / 50%)":null,i(l,s.style),!this.active(t))return;s.$lastStartTime=Date.now(),s.$restTime=e.speed;const d=o+l.clientWidth,{result:p}=await t.wait(e.postMessage({type:"getDanmuTop",target:{mode:s.mode,height:l.clientHeight,speed:d/s.$restTime},visibles:e.visibles,antiOverlap:e.option.antiOverlap,clientWidth:o,clientHeight:r,marginBottom:e.marginBottom,marginTop:e.marginTop}));if(!this.active(t)||s.$ref!==l)return;if(void 0!==p){switch(e.setState(s,"emit"),l.style.top=`${p}px`,l.style.visibility="visible",l.dataset.mode=s.mode,l.dataset.id=s.id||"",s.mode){case 0:l.style.left=`${o}px`,l.style.marginLeft="0px",l.style.transform=`translateX(${-d}px)`,l.style.transition=`transform ${s.$restTime}s linear 0s`;break;case 1:case 2:l.style.left="50%",l.style.marginLeft=`-${l.clientWidth/2}px`}t.ref=null,e.art.emit("artplayerPluginDanmuku:visible",s)}else e.setState(s,"ready"),this.release(t)}}destroy(){this.closed=!0,this.invalidate()}}let m=0;class f{constructor(t,e){this.pending=new Map,this.closed=!1,this.failed=!1,this.onFailure=e,this.worker=t();try{this.worker.onmessage=t=>{const e=this.pending.get(t.data?.id);e&&(this.pending.delete(t.data.id),e.resolve(t.data))},this.worker.onerror=t=>{t.preventDefault?.(),this.fail(t.error||t)},this.worker.onmessageerror=t=>this.fail(t.error||t)}catch(i){try{this.dispose()}catch{}throw i}}request(t){t.id=++m;const{id:e}=t;return this.closed||this.failed?Promise.resolve({id:e,result:void 0}):new Promise((i,a)=>{this.pending.set(e,{resolve:i,reject:a});try{this.worker.postMessage(t)}catch(n){this.fail(n)}})}fail(t){if(!this.closed&&!this.failed){this.failed=!0;for(const e of this.pending.values())e.reject(t);this.pending.clear();try{this.dispose()}catch{}this.onFailure(t)}}cancel(){for(const[t,e]of this.pending)e.resolve({id:t,result:void 0});this.pending.clear()}dispose(){if(this.closed)return;this.closed=!0,this.cancel();let t,e=!1;const i=i=>{try{i()}catch(a){e||(t=a),e=!0}};for(const a of["onmessage","onerror","onmessageerror"])i(()=>this.worker[a]=null);if(i(()=>this.worker.terminate()),e)throw t}}const g='/*!\n * artplayer-plugin-danmuku.js v5.3.0\n * Github: https://github.com/zhw2590582/ArtPlayer\n * (c) 2017-2026 Harvey Zhao\n * Released under the MIT License.\n */\nfunction t({target:t,visibles:e,clientWidth:i,clientHeight:h,marginBottom:n,marginTop:o,antiOverlap:s}){const r=h-n,p=e.filter(e=>e.mode===t.mode&&e.top<=r).sort((t,e)=>t.top-e.top);if(0===p.length)return 2===t.mode?r-t.height:o;if(p.unshift({type:"top",top:0,left:0,right:0,height:o,width:i,speed:0,distance:i}),p.push({type:"bottom",top:r,left:0,right:0,height:n,width:i,speed:0,distance:i}),2===t.mode)for(let g=p.length-2;g>=0;g-=1){const e=p[g],i=p[g+1],h=e.top+e.height;if(i.top-h>=t.height)return i.top-t.height}else for(let g=1;g=t.height)return h}const a=[];for(let g=1;g{const i=Math.min(...e.map(t=>t.right)),h=Math.min(...t.map(t=>t.right));return i*e.length-h*t.length});break;case 1:case 2:a.sort((t,e)=>{const i=Math.max(...e.map(t=>t.width));return Math.max(...t.map(t=>t.width))*t.length-i*e.length})}return a[0][0].top}switch(t.mode){case 0:{const e=a.find(e=>e.every(e=>{if(ie.time}));return e&&e[0]?e[0].top:void 0}case 1:case 2:return}}onmessage=e=>{const{data:i}=e;if(!i.id||!i.type)return;const h=(0,{getDanmuTop:t}[i.type])(i);globalThis.postMessage({result:h,id:i.id})};\n',y="undefined"!==typeof self&&self.Blob&&new Blob(["URL.revokeObjectURL(import.meta.url);",g],{type:"text/javascript;charset=utf-8"});function v(t){let e;try{if(e=y&&(self.URL||self.webkitURL).createObjectURL(y),!e)throw"";const i=new Worker(e,{type:"module",name:t?.name});return i.addEventListener("error",()=>{(self.URL||self.webkitURL).revokeObjectURL(e)}),i}catch(i){return new Worker("data:text/javascript;charset=utf-8,"+encodeURIComponent(g),{type:"module",name:t?.name})}}class k{constructor(t,e){const{constructor:i,template:a}=t;this.utils=i.utils,this.validator=i.validator,this.$danmuku=a.$danmuku,this.$player=a.$player,this.art=t,this.queue=[],this.$refs=[],this.isStop=!1,this.isHide=!1,this.timer=null,this.index=0,this.worker=null,this.workerClient=null,this.scheduler=new c(this),this.option=k.option,this.states={wait:[],ready:[],emit:[],stop:[]},this.start=this.start.bind(this),this.stop=this.stop.bind(this),this.reset=this.reset.bind(this),this.resize=this.resize.bind(this),this.destroy=this.destroy.bind(this),this.seek=this.seek.bind(this),d(this),t.on("destroy",this.destroy);try{if(this.config(e,!0),!d(this))return;this.createWorker()}catch(n){throw t.off("destroy",this.destroy),p(this),this.scheduler.destroy(),n}t.on("video:play",this.start),t.on("video:playing",this.start),t.on("video:pause",this.stop),t.on("video:waiting",this.stop),t.on("resize",this.resize),t.on("video:seeking",this.seek),this.load().catch(t=>console.warn("Failed to load initial danmuku:",t))}static get option(){return{danmuku:[],speed:5,margin:[10,"25%"],opacity:1,color:"#FFFFFF",mode:0,modes:[0,1,2],fontSize:25,antiOverlap:!0,synchronousPlayback:!1,mount:void 0,heatmap:!1,width:512,points:[],filter:()=>!0,beforeEmit:()=>!0,beforeVisible:()=>!0,visible:!0,emitter:!0,maxLength:200,lockTime:5,theme:"dark",OPACITY:{},FONT_SIZE:{},MARGIN:{},SPEED:{},COLOR:[]}}static get scheme(){return{danmuku:"array|function|string|promise",speed:"number",margin:"array",opacity:"number",color:"string",mode:"number",modes:"array",fontSize:"number|string",antiOverlap:"boolean",synchronousPlayback:"boolean",mount:"?htmldivelement|string",heatmap:"object|boolean",width:"number",points:"array",filter:"function",beforeEmit:"function",beforeVisible:"function",visible:"boolean",emitter:"boolean",maxLength:"number",lockTime:"number",theme:"string",OPACITY:"object",FONT_SIZE:"object",MARGIN:"object",SPEED:"object",COLOR:"array"}}static get cssText(){return'\n user-select: none;\n position: absolute;\n white-space: pre;\n pointer-events: none;\n perspective: 500px;\n display: inline-block;\n will-change: transform;\n font-weight: normal;\n line-height: 1.125;\n visibility: hidden;\n font-family: SimHei, "Microsoft JhengHei", Arial, Helvetica, sans-serif;\n text-shadow: rgb(0, 0, 0) 1px 0px 1px, rgb(0, 0, 0) 0px 1px 1px, rgb(0, 0, 0) 0px -1px 1px, rgb(0, 0, 0) -1px 0px 1px;\n '}get isRotate(){return this.art.plugins?.autoOrientation?.state}get marginTop(){const{clamp:t}=this.utils,e=this.option.margin[0],{clientHeight:i}=this.$player;if("number"===typeof e)return t(e,0,i);if("string"===typeof e&&e.endsWith("%")){return t(i*(Number.parseFloat(e)/100),0,i)}return k.option.margin[0]}get marginBottom(){const{clamp:t}=this.utils,e=this.option.margin[1],{clientHeight:i}=this.$player;if("number"===typeof e)return t(e,0,i);if("string"===typeof e&&e.endsWith("%")){return t(i*(Number.parseFloat(e)/100),0,i)}return k.option.margin[1]}get fontSize(){const{clamp:t}=this.utils,{clientHeight:e}=this.$player,i=this.option.fontSize;if("number"===typeof i)return Math.round(t(i,12,e));if("string"===typeof i&&i.endsWith("%")){const a=Number.parseFloat(i)/100;return Math.round(t(e*a,12,e))}return k.option.fontSize}get $ref(){const t=this.$refs.pop()||document.createElement("div");return t.style.cssText=k.cssText,t.dataset.mode="",t.dataset.id="",t.className="",t}get readys(){return function(t){const{currentTime:e}=t.art,i=[];return h(t,"ready",t=>i.push(t)),h(t,"wait",t=>{e+.1>=t.time&&t.time>=e-.1&&i.push(t)}),i}(this)}get visibles(){const t=[],{clientWidth:e}=this.$player,i=this.getLeft(this.$player);return this.filter("emit",a=>{const n=a.$ref.offsetTop,s=this.getLeft(a.$ref)-i,o=a.$ref.clientHeight,r=a.$ref.clientWidth,l=s+r,d=e-l,p=l/a.$restTime,h={};h.top=n,h.left=s,h.height=o,h.width=r,h.right=d,h.speed=p,h.distance=l,h.time=a.$restTime,h.mode=a.mode,t.push(h)}),t}get speed(){return this.option.synchronousPlayback&&this.art.playbackRate?this.option.speed/Number(this.art.playbackRate):this.option.speed}async load(e){const{errorHandle:i}=this.utils,a=function(t,e){const i=l(t);if(e)for(const r of[...i.tasks])r.replace&&r.cancel();let a,n=i.closed||t.art.isDestroy;const s=new Promise(t=>a=t),o=new Set,d="function"===typeof AbortController?new AbortController:void 0,p={replace:e,signal:d?.signal,active:()=>!n&&!i.closed&&!t.art.isDestroy,onCancel:t=>p.active()?(o.add(t),()=>o.delete(t)):(t(),()=>{}),cancel(){if(!n){n=!0,a(r),d?.abort();for(const t of[...o])t();o.clear()}},wait:t=>Promise.race([t,s]),emit(e){i.emitting.push(p);try{return t.emit(e)}finally{i.emitting.pop()}},finish(){i.tasks.delete(p),o.clear()}};return n?a(r):i.tasks.add(p),p}(this,void 0===e);try{if(!a.active())return this;const n=function(e,i){return"function"===typeof e?{asynchronous:!0,value:e()}:t(e)?{asynchronous:!0,value:e}:"string"===typeof e?{asynchronous:!0,value:s(e,i)}:{asynchronous:!1,value:Array.isArray(e)?e:[]}}(e||this.option.danmuku,a),o=n.asynchronous?await a.wait(n.value):n.value;if(!a.active())return this;if(i(Array.isArray(o),"Danmuku need return an array as result"),void 0===e){if(this.reset(),!a.active())return this;this.queue=[],this.states={wait:[],ready:[],emit:[],stop:[]},this.$refs=[],this.$danmuku.textContent=""}for(let t=0;t{const i=s[e],a=o[e];return"function"===typeof i||"function"===typeof a||t(i)||t(a)?i!==a:JSON.stringify(i)!==JSON.stringify(a)}))&&!i)return this;const r=function(t,e,{defaults:i,validate:a,clamp:n,mount:s}){const o=Object.assign({},i,t,e);return a(o),o.mode=n(o.mode,0,2),o.speed=n(o.speed,1,10),o.opacity=n(o.opacity,0,1),o.lockTime=n(o.lockTime,1,60),o.maxLength=n(o.maxLength,1,1e3),o.mount=o.mount||s,o}(this.option,e,{defaults:k.option,validate:t=>this.validator(t,k.scheme),clamp:a,mount:n});if(!d(this))return this;const l=this.option.beforeVisible!==r.beforeVisible;return this.option=r,l&&(this.scheduler.invalidate(),this.scheduler.schedule()),e.fontSize&&(this.reset(),!d(this))||(this.option.visible?this.show():this.hide(),d(this)&&this.art.emit("artplayerPluginDanmuku:config",this.option)),this}getLeft(t){const e=t.getBoundingClientRect();return this.isRotate?e.top:e.left}postMessage(t={}){return this.workerClient.request(t)}createWorker(){this.workerClient=new f(()=>new v,t=>this.scheduler.fail(t)),this.worker=this.workerClient.worker}filter(t,e){return h(this,t,e)}setState(t,e){!function(t,e,i){t.states[e.$state]=t.states[e.$state].filter(t=>t!==e),e.$state=i,e.$ref&&(e.$ref.dataset.state=i),t.states[i].push(e)}(this,t,e)}makeWait(t){this.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=k.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.$refs.push(t.$ref),t.$ref=null)}update(){return this.scheduler.running=!0,this.scheduler.schedule(),this}resize(){const{clientWidth:t}=this.$player;this.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.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.$player;return this.filter("stop",e=>{switch(this.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.$player;return this.filter("emit",e=>{switch(this.setState(e,"stop"),e.mode){case 0:{const i=t-(this.getLeft(e.$ref)-this.getLeft(this.$player));e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition="transform 0s linear 0s";break}}}),this}stop(){return this.isStop=!0,this.scheduler.running=!1,this.scheduler.invalidate(),this.suspend(),this.art.emit("artplayerPluginDanmuku:stop"),this}start(){if(this.scheduler.closed||this.art.isDestroy)return this;const t=++this.scheduler.starts,e=this.scheduler.generation;this.isStop=!1,this.scheduler.recover();return this.scheduler.closed||this.art.isDestroy||this.isStop||this.scheduler.fault||e!==this.scheduler.generation||t!==this.scheduler.starts||(this.continue(),this.update(),this.art.emit("artplayerPluginDanmuku:start")),this}reset(){return this.scheduler.invalidate(),this.queue.forEach(t=>this.makeWait(t)),this.art.emit("artplayerPluginDanmuku:reset"),this.scheduler.recover(),this.scheduler.schedule(),this}seek(){this.scheduler.invalidate(),this.scheduler.schedule()}show(){return this.isHide=!1,this.$danmuku.style.opacity=1,this.option.visible=!0,this.art.emit("artplayerPluginDanmuku:show"),this}hide(){return this.isHide=!0,this.scheduler.invalidate(),this.$danmuku.style.opacity=0,this.option.visible=!1,this.art.emit("artplayerPluginDanmuku:hide"),this.scheduler.schedule(),this}destroy(){p(this),this.scheduler.destroy();try{try{this.stop()}finally{this.workerClient?.dispose()}}finally{this.art.off("video:play",this.start),this.art.off("video:playing",this.start),this.art.off("video:pause",this.stop),this.art.off("video:waiting",this.stop),this.art.off("resize",this.resize),this.art.off("video:seeking",this.seek),this.art.off("destroy",this.destroy),this.art.emit("artplayerPluginDanmuku:destroy")}}}const w={map:(t,e,i,a,n)=>(t-e)*(n-a)/(i-e)+a,range(t,e,i){const a=Math.round(t/i)*i;return Array.from({length:Math.floor((e-t)/i)},(t,e)=>e*i+a)}};function x(t,e,i){const{query:a}=t.constructor.utils;t.controls.add({name:"heatmap",position:"top",html:"",style:{position:"absolute",top:"-100px",left:"0px",right:"0px",height:"100px",width:"100%",pointerEvents:"none"},mounted(n){let s=null,o=null;function r(r=[]){if(s=null,o=null,n.innerHTML="",!t.duration||t.option.isLive)return;const l={w:n.offsetWidth,h:n.offsetHeight},d={xMin:0,xMax:l.w,yMin:0,yMax:128,scale:.25,opacity:.2,minHeight:Math.floor(.05*l.h),sampling:Math.floor(l.w/100),smoothing:.2,flattening:.2};"object"===typeof i&&Object.assign(d,i);let p=[];if(Array.isArray(r)&&r.length)p=[...r];else{const i=t.duration/l.w;for(let t=0;t<=l.w;t+=d.sampling){const a=e.queue.filter(({time:e})=>e>t*i&&e<=(t+d.sampling)*i).length;p.push([t,a])}}if(0===p.length)return;const h=p[p.length-1],u=h[0],c=h[1];u!==l.w&&p.push([l.w,c]);const m=p.map(t=>t[1]),f=(Math.min(...m)+Math.max(...m))/2;for(let t=0;tf?1+d.scale:1-d.scale)+d.minHeight}const g=(t,e,i,a)=>{const n=function(t,e){const i=e[0]-t[0],a=e[1]-t[1];return{length:Math.sqrt(i**2+a**2),angle:Math.atan2(a,i)}}(e||t,i||t),s=w.map(Math.cos(n.angle)*d.flattening,0,1,1,0),o=n.angle*s+(a?Math.PI:0),r=n.length*d.smoothing;return[t[0]+Math.cos(o)*r,t[1]+Math.sin(o)*r]},y=p.map(t=>[w.map(t[0],d.xMin,d.xMax,0,l.w),w.map(t[1],d.yMin,d.yMax,l.h,0)]).reduce((t,e,i,a)=>0===i?`M ${a[a.length-1][0]},${l.h} L ${e[0]},${l.h} L ${e[0]},${e[1]}`:`${t} ${((t,e,i)=>{const a=g(i[e-1],i[e-2],t),n=g(t,i[e-1],i[e+1],!0),s=e===i.length-1?" z":"";return`C ${a[0]},${a[1]} ${n[0]},${n[1]} ${t[0]},${t[1]}${s}`})(e,i,a)}`,"");n.innerHTML=`\n \n \n \n \n \n \n \n \n \n \n \n `,s=a("#heatmap-start",n),o=a("#heatmap-stop",n),s.setAttribute("offset",100*t.played+"%"),o.setAttribute("offset",100*t.played+"%")}t.on("video:timeupdate",()=>{s&&o&&(s.setAttribute("offset",100*t.played+"%"),o.setAttribute("offset",100*t.played+"%"))}),t.on("setBar",(t,e)=>{s&&o&&"played"===t&&(s.setAttribute("offset",100*e+"%"),o.setAttribute("offset",100*e+"%"))}),t.on("ready",()=>r()),t.on("resize",()=>r()),t.on("artplayerPluginDanmuku:loaded",()=>r()),t.on("artplayerPluginDanmuku:points",t=>r(t))}})}const $='',b='',S='',M='',z='',C='',E='',D='',F='',T='',A='',L='';class P{constructor(t,e){this.art=t,this.danmuku=e,this.utils=t.constructor.utils;const{setStyle:i}=this.utils,{$controlsCenter:a}=t.template;i(a,"display","flex"),this.template={$controlsCenter:a,$mount:a,$danmuku:null,$toggle:null,$config:null,$configPanel:null,$configModes:null,$style:null,$stylePanel:null,$styleModes:null,$colors:null,$opacitySlider:null,$opacityValue:null,$marginSlider:null,$marginValue:null,$fontSizeSlider:null,$fontSizeValue:null,$speedSlider:null,$speedValue:null,$input:null,$send:null},this.slider={opacity:null,margin:null,fontSize:null,speed:null},this.emitting=!1,this.isLock=!1,this.timer=null,this.createTemplate(),this.createSliders(),this.createEvents(),this.mount(this.option.mount),t.on("resize",()=>this.resize()),t.on("fullscreen",t=>this.onFullscreen(t)),t.on("fullscreenWeb",t=>this.onFullscreen(t)),t.proxy(this.template.$config,"mouseenter",()=>{this.onMouseEnter({$control:this.template.$config,$panel:this.template.$configPanel})}),t.proxy(this.template.$style,"mouseenter",()=>{this.onMouseEnter({$control:this.template.$style,$panel:this.template.$stylePanel})})}static get icons(){return{$on:A,$off:T,$config:S,$style:L,$mode_0_off:M,$mode_0_on:z,$mode_1_off:C,$mode_1_on:E,$mode_2_off:D,$mode_2_on:F,$check_on:b,$check_off:$}}get option(){return this.danmuku.option}get outside(){return this.template.$mount!==this.template.$controlsCenter}get TEMPLATE(){const{option:t}=this;return`\n

\n ${A}${T}\n
\n
\n ${S}\n
\n
\n
\n \u6309\u7c7b\u578b\u5c4f\u853d\n
\n
\n ${M}${z}\n
\u6eda\u52a8
\n
\n
\n ${C}${E}\n
\u9876\u90e8
\n
\n
\n ${D}${F}\n
\u5e95\u90e8
\n
\n
\n
\n
\n
\n ${b}${$}\n \u9632\u6b62\u5f39\u5e55\u91cd\u53e0\n
\n
\n ${b}${$}\n \u540c\u6b65\u89c6\u9891\u901f\u5ea6\n
\n
\n
\n \u4e0d\u900f\u660e\u5ea6\n
\n
\u672a\u77e5
\n
\n
\n \u663e\u793a\u533a\u57df\n
\n
\u672a\u77e5
\n
\n
\n \u5f39\u5e55\u5b57\u53f7\n
\n
\u672a\u77e5
\n
\n
\n \u5f39\u5e55\u901f\u5ea6\n
\n
\u672a\u77e5
\n
\n
\n
\n
\n
\n
\n ${L}\n
\n
\n
\n \u6a21\u5f0f\n
\n
\n ${z}\n
\u6eda\u52a8
\n
\n
\n ${E}\n
\u9876\u90e8
\n
\n
\n ${F}\n
\u5e95\u90e8
\n
\n
\n
\n
\n \u989c\u8272\n
\n ${this.COLOR.map(t=>`
`).join("")}\n
\n
\n
\n
\n
\n \n
\u53d1\u9001
\n
\n `}get OPACITY(){return{min:0,max:100,steps:[],...this.option.OPACITY}}get FONT_SIZE(){return{min:12,max:120,steps:[],...this.option.FONT_SIZE}}get MARGIN(){return{min:0,max:3,steps:[{name:"1/4",value:[10,"75%"]},{name:"\u534a\u5c4f",value:[10,"50%"]},{name:"3/4",value:[10,"25%"]},{name:"\u6ee1\u5c4f",value:[10,10]}],...this.option.MARGIN}}get SPEED(){return{min:0,max:4,steps:[{name:"\u6781\u6162",value:10},{name:"\u8f83\u6162",value:7.5,hide:!0},{name:"\u9002\u4e2d",value:5},{name:"\u8f83\u5feb",value:2.5,hide:!0},{name:"\u6781\u5feb",value:1}],...this.option.SPEED}}get COLOR(){return this.option.COLOR.length?this.option.COLOR:["#FE0302","#FF7204","#FFAA02","#FFD302","#FFFF00","#A0EE00","#00CD00","#019899","#4266BE","#89D5FF","#CC0273","#222222","#9B9B9B","#FFFFFF"]}query(t){const{query:e}=this.utils,{$danmuku:i}=this.template;return e(t,i)}append(t,e){const{append:i}=this.utils;[...t.children].includes(e)||i(t,e)}setData(t,e){const{$player:i}=this.art.template,{$mount:a}=this.template;i.dataset[t]=e,this.outside&&(a.dataset[t]=e)}createTemplate(){const{createElement:t,tooltip:e}=this.utils,i=t("div");i.className="artplayer-plugin-danmuku",i.innerHTML=this.TEMPLATE,this.template.$danmuku=i,this.template.$toggle=this.query(".apd-toggle"),this.template.$config=this.query(".apd-config"),this.template.$configPanel=this.query(".apd-config-panel"),this.template.$configModes=this.query(".apd-config-mode .apd-modes"),this.template.$style=this.query(".apd-style"),this.template.$stylePanel=this.query(".apd-style-panel"),this.template.$styleModes=this.query(".apd-style-mode .apd-modes"),this.template.$colors=this.query(".apd-colors"),this.template.$antiOverlap=this.query(".apd-anti-overlap"),this.template.$syncVideo=this.query(".apd-sync-video"),this.template.$opacitySlider=this.query(".apd-config-opacity .apd-slider"),this.template.$opacityValue=this.query(".apd-config-opacity .apd-value"),this.template.$marginSlider=this.query(".apd-config-margin .apd-slider"),this.template.$marginValue=this.query(".apd-config-margin .apd-value"),this.template.$fontSizeSlider=this.query(".apd-config-fontSize .apd-slider"),this.template.$fontSizeValue=this.query(".apd-config-fontSize .apd-value"),this.template.$speedSlider=this.query(".apd-config-speed .apd-slider"),this.template.$speedValue=this.query(".apd-config-speed .apd-value"),this.template.$input=this.query(".apd-input"),this.template.$send=this.query(".apd-send");const{$toggle:a}=this.template;this.art.on("artplayerPluginDanmuku:show",()=>{e(a,"\u5173\u95ed\u5f39\u5e55")}),this.art.on("artplayerPluginDanmuku:hide",()=>{e(a,"\u6253\u5f00\u5f39\u5e55")})}createEvents(){const{$toggle:t,$configModes:e,$styleModes:i,$colors:a,$antiOverlap:n,$syncVideo:s,$send:o,$input:r}=this.template;this.art.proxy(t,"click",()=>{this.danmuku.config({visible:!this.option.visible}),this.reset()}),this.art.proxy(e,"click",t=>{const e=t.target.closest(".apd-mode");if(!e)return;const i=Number(e.dataset.mode);this.option.modes.includes(i)?this.danmuku.config({modes:this.option.modes.filter(t=>t!==i)}):this.danmuku.config({modes:[...this.option.modes,i]}),this.reset()}),this.art.proxy(n,"click",()=>{this.danmuku.config({antiOverlap:!this.option.antiOverlap}),this.reset()}),this.art.proxy(s,"click",()=>{this.danmuku.config({synchronousPlayback:!this.option.synchronousPlayback}),this.reset()}),this.art.proxy(i,"click",t=>{const e=t.target.closest(".apd-mode");if(!e)return;const i=Number(e.dataset.mode);this.danmuku.config({mode:i}),this.reset()}),this.art.proxy(a,"click",t=>{const e=t.target.closest(".apd-color");e&&(this.danmuku.config({color:e.dataset.color}),this.reset())}),this.art.proxy(o,"click",()=>this.emit()),this.art.proxy(r,"keypress",t=>{"Enter"===t.key&&(t.preventDefault(),this.emit())})}createSliders(){this.slider.opacity=this.createSlider({...this.OPACITY,container:this.template.$opacitySlider,findIndex:()=>Math.round(100*this.option.opacity),onChange:t=>{const{$opacityValue:e}=this.template;e.textContent=`${t}%`,this.danmuku.config({opacity:t/100})}}),this.slider.margin=this.createSlider({...this.MARGIN,container:this.template.$marginSlider,findIndex:()=>this.MARGIN.steps.findIndex(t=>t.value[0]===this.option.margin[0]&&t.value[1]===this.option.margin[1]),onChange:t=>{const e=this.MARGIN.steps[t];if(!e)return;const{$marginValue:i}=this.template;i.textContent=e.name,this.danmuku.config({margin:e.value})}}),this.slider.fontSize=this.createSlider({...this.FONT_SIZE,container:this.template.$fontSizeSlider,findIndex:()=>this.danmuku.fontSize,onChange:t=>{const{$fontSizeValue:e}=this.template;e.textContent=`${t}px`,t!==this.danmuku.fontSize&&this.danmuku.config({fontSize:t})}}),this.slider.speed=this.createSlider({...this.SPEED,container:this.template.$speedSlider,findIndex:()=>this.SPEED.steps.findIndex(t=>t.value===this.option.speed),onChange:t=>{const e=this.SPEED.steps[t];if(!e)return;const{$speedValue:i}=this.template;i.textContent=e.name,this.danmuku.config({speed:e.value})}})}createSlider({min:t,max:e,container:i,findIndex:a,onChange:n,steps:s=[]}){const{query:o,clamp:r,setStyle:l}=this.utils;l(i,"touch-action","none"),i.innerHTML=`\n
\n
\n ${s.map(()=>'
').join("")}\n
\n
\n
\n
\n
\n ${s.map(t=>t.hide?"":`
${t.name}
`).join("")}\n
\n `;const d=o(".apd-slider-dot",i),p=o(".apd-slider-progress",i);let h=!1;function u(i=a()){if(ie)return;const o=(i-t)/(e-t);d.style.left=100*o+"%",0===s.length&&(p.style.width=d.style.left),n(i)}function c(a){const{top:n,height:s,left:o,width:l}=i.getBoundingClientRect();if(this.art.isRotate){const i=r(a.clientY-n,0,s);u(Math.round(i/s*(e-t)+t))}else{const i=r(a.clientX-o,0,l);u(Math.round(i/l*(e-t)+t))}}return this.art.proxy(i,"click",t=>{c.call(this,t)}),this.art.proxy(i,"pointerdown",t=>{h=0===t.button}),this.art.on("document:pointermove",t=>{h&&c.call(this,t)}),this.art.on("document:pointerup",t=>{h&&(h=!1,c.call(this,t))}),{reset:u}}onFullscreen(t){const{$danmuku:e,$controlsCenter:i,$mount:a}=this.template;this.outside?t?this.append(i,e):this.append(a,e):this.append(i,e)}onMouseEnter({$control:t,$panel:e}){const{$player:i}=this.art.template,a=t.getBoundingClientRect(),n=e.getBoundingClientRect(),s=i.getBoundingClientRect(),o=n.width/2-a.width/2,r=s.left-(a.left-o),l=a.right+o-s.right;e.style.left=r>0?`${-o+r}px`:l>0?-o-l+"px":-o+"px"}async emit(){const{$input:t}=this.template,e=t.value.trim();if(!e.length)return;if(this.isLock)return;if(this.emitting)return;const i={text:e,mode:this.option.mode,color:this.option.color,time:this.art.currentTime};try{this.emitting=!0;const e=await this.option.beforeEmit(i);if(this.emitting=!1,!0!==e)return;i.border=!0,delete i.time,this.danmuku.emit(i),t.value="",this.lock()}catch(a){console.error("Error emitting danmuku:",a),this.emitting=!1}}lock(){const{addClass:t}=this.utils,{$send:e}=this.template;this.isLock=!0;let i=this.option.lockTime;e.textContent=i,t(e,"apd-lock");const a=()=>{this.timer=setTimeout(()=>{0===i?this.unlock():(i-=1,e.textContent=i,a())},1e3)};a()}unlock(){const{removeClass:t}=this.utils,{$send:e}=this.template;clearTimeout(this.timer),this.isLock=!1,e.textContent="\u53d1\u9001",t(e,"apd-lock")}resize(){if(this.outside)return;if(this.art.fullscreen)return;if(this.art.fullscreenWeb)return;const{$player:t,$controlsCenter:e}=this.art.template,{$danmuku:i}=this.template;this.art.widtht.dataset.color===this.option.color.toUpperCase());s&&t(s,"apd-active"),e(i,this.option.visible?"\u5173\u95ed\u5f39\u5e55":"\u6253\u5f00\u5f39\u5e55"),this.resize()}mount(t){const{errorHandle:e}=this.utils,i="string"===typeof t?document.querySelector(t):t;e(i,`Can not find the mount point: ${t}`),this.append(i,this.template.$danmuku),this.template.$mount=i,this.reset()}}if("undefined"!==typeof document){const t="artplayer-plugin-danmuku";let e=document.getElementById(t);e||(e=document.createElement("style"),e.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.appendChild(e)}):(document.head||document.documentElement).appendChild(e)),e.textContent='.artplayer-plugin-danmuku{display:flex;position:relative;z-index:99;align-items:center;justify-content:center;font-size:12px;height:32px;width:100%;color:#fff;font-weight:300;flex-shrink:0;gap:10px}.artplayer-plugin-danmuku .apd-icon{cursor:pointer;opacity:.75;transition:all .2s ease;fill:#fff}.artplayer-plugin-danmuku .apd-icon:hover{opacity:1}.artplayer-plugin-danmuku .apd-config{display:flex;position:relative}.artplayer-plugin-danmuku .apd-config .apd-config-panel{position:absolute;bottom:24px;left:0;width:320px;padding:10px;opacity:0;pointer-events:none}.artplayer-plugin-danmuku .apd-config .apd-config-panel .apd-config-panel-inner{width:100%;border-radius:3px;background-color:#000000d9;padding:10px}.artplayer-plugin-danmuku .apd-config:hover .apd-config-panel{opacity:100;pointer-events:all}.artplayer-plugin-danmuku .apd-config-mode,.artplayer-plugin-danmuku .apd-config-slider,.artplayer-plugin-danmuku .apd-config-other,.artplayer-plugin-danmuku .apd-style-mode{margin-bottom:15px}.artplayer-plugin-danmuku .apd-modes{display:flex;align-items:center;margin-top:5px;gap:20px}.artplayer-plugin-danmuku .apd-modes .apd-mode{cursor:pointer;text-align:center}.artplayer-plugin-danmuku .apd-modes .apd-mode:hover{color:#00a1d6}.artplayer-plugin-danmuku .apd-config-slider{display:flex;align-items:center;gap:12px}.artplayer-plugin-danmuku .apd-config-slider .apd-value{width:32px;text-align:right}.artplayer-plugin-danmuku .apd-slider{position:relative;flex:1;display:flex;height:20px;align-items:center;justify-content:center;cursor:pointer}.artplayer-plugin-danmuku .apd-slider .apd-slider-line{position:relative;height:2px;width:100%;overflow:hidden;border-radius:3px;background-color:#ffffff40}.artplayer-plugin-danmuku .apd-slider .apd-slider-points{position:absolute;inset:0;display:flex;align-items:center;justify-content:space-between}.artplayer-plugin-danmuku .apd-slider .apd-slider-points .apd-slider-point{width:2px;height:2px;border-radius:50%;background-color:#ffffff80}.artplayer-plugin-danmuku .apd-slider .apd-slider-progress{width:0%;height:100%;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-slider .apd-slider-dot{position:absolute;transform:translate(-6px);left:0%;width:12px;height:12px;border-radius:50%;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-slider .apd-slider-steps{display:flex;align-items:center;justify-content:space-between;position:absolute;bottom:-12px;width:calc(100% + 32px);color:#777}.artplayer-plugin-danmuku .apd-slider .apd-slider-steps .apd-slider-step{flex-shrink:0;width:36px;text-align:center;scale:.95}.artplayer-plugin-danmuku .apd-config-other{display:flex;align-items:center;gap:20px}.artplayer-plugin-danmuku .apd-config-other .apd-check-off,.artplayer-plugin-danmuku .apd-config-other .apd-check-on{width:16px;height:16px}.artplayer-plugin-danmuku .apd-config-other .apd-other{display:flex;align-items:center;cursor:pointer;gap:2px}.artplayer-plugin-danmuku .apd-config-other .apd-other:hover{color:#00a1d6}.artplayer-plugin-danmuku .apd-emitter{display:flex;flex:1;align-items:center;height:100%;background-color:#ffffff40;border-radius:5px}.artplayer-plugin-danmuku .apd-style{position:relative;display:flex;align-items:center;justify-content:center}.artplayer-plugin-danmuku .apd-style .apd-style-panel{position:absolute;bottom:24px;left:0;width:200px;padding:10px;opacity:0;pointer-events:none}.artplayer-plugin-danmuku .apd-style .apd-style-panel .apd-style-panel-inner{width:100%;border-radius:3px;background-color:#000000d9;padding:10px}.artplayer-plugin-danmuku .apd-style:hover .apd-style-panel{opacity:100;pointer-events:all}.artplayer-plugin-danmuku .apd-colors{display:flex;flex-wrap:wrap;margin-top:5px;gap:8px}.artplayer-plugin-danmuku .apd-colors .apd-color{width:16px;height:16px;border-radius:2px;cursor:pointer}.artplayer-plugin-danmuku .apd-colors .apd-color.apd-active{border:1px solid black;box-shadow:0 0 0 1px #fff}.artplayer-plugin-danmuku .apd-input{outline:none;height:100%;flex:1;min-width:0;width:auto;border:none;line-height:1;color:#fff;background-color:transparent}.artplayer-plugin-danmuku .apd-input::placeholder{color:#ffffff80}.artplayer-plugin-danmuku .apd-send{display:flex;align-items:center;justify-content:center;height:100%;width:60px;flex-shrink:0;cursor:pointer;text-shadow:none;border-top-right-radius:5px;border-bottom-right-radius:5px;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-send.apd-lock{cursor:not-allowed;color:#666;background-color:#e7e7e7}.art-controls-center .apd-emitter{width:260px;flex:none}.art-fullscreen .artplayer-plugin-danmuku,.art-fullscreen-web .artplayer-plugin-danmuku{height:38px;gap:16px}.art-fullscreen .artplayer-plugin-danmuku .apd-config-icon,.art-fullscreen-web .artplayer-plugin-danmuku .apd-config-icon,.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-off,.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-off,.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-on,.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-on{width:28px;height:28px}.art-fullscreen .artplayer-plugin-danmuku .apd-emitter,.art-fullscreen-web .artplayer-plugin-danmuku .apd-emitter{width:400px;flex:none}.art-video-player>.artplayer-plugin-danmuku{position:absolute;left:0;right:0;bottom:-40px;padding:0 10px}.art-video-player:has(>.artplayer-plugin-danmuku){margin-bottom:40px}[data-danmuku-emitter=false] .apd-emitter{display:none!important}[data-danmuku-emitter=false] .art-controls-center .artplayer-plugin-danmuku{justify-content:flex-end;gap:18px}[data-danmuku-emitter=false].art-fullscreen .art-controls-center .artplayer-plugin-danmuku,[data-danmuku-emitter=false].art-fullscreen-web .art-controls-center .artplayer-plugin-danmuku{gap:24px}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-icon{fill:#333}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-emitter{background-color:#f1f2f3}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-input{color:#000}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-input::placeholder{color:#0000004d}[data-danmuku-visible=false] .apd-toggle-off{display:block}[data-danmuku-visible=false] .apd-toggle-on,[data-danmuku-visible=true] .apd-toggle-off{display:none}[data-danmuku-visible=true] .apd-toggle-on{display:block}[data-danmuku-anti-overlap=false] .apd-anti-overlap .apd-check-on{display:none}[data-danmuku-anti-overlap=false] .apd-anti-overlap .apd-check-off,[data-danmuku-anti-overlap=true] .apd-anti-overlap .apd-check-on{display:block}[data-danmuku-anti-overlap=true] .apd-anti-overlap .apd-check-off,[data-danmuku-sync-video=false] .apd-sync-video .apd-check-on{display:none}[data-danmuku-sync-video=false] .apd-sync-video .apd-check-off,[data-danmuku-sync-video=true] .apd-sync-video .apd-check-on{display:block}[data-danmuku-sync-video=true] .apd-sync-video .apd-check-off{display:none}[data-danmuku-mode0=false] .apd-config-mode .apd-mode-0-off{display:block}[data-danmuku-mode0=false] .apd-config-mode .apd-mode-0-on{display:none}[data-danmuku-mode0=false] .art-danmuku [data-mode="0"]{opacity:0!important}[data-danmuku-mode0=true] .apd-config-mode .apd-mode-0-off{display:none}[data-danmuku-mode0=true] .apd-config-mode .apd-mode-0-on{display:block}[data-danmuku-mode="0"] .apd-style-mode [data-mode="0"]{color:#00a1d6}[data-danmuku-mode="0"] .apd-style-mode [data-mode="0"] path{fill:#00a1d6}[data-danmuku-mode1=false] .apd-config-mode .apd-mode-1-off{display:block}[data-danmuku-mode1=false] .apd-config-mode .apd-mode-1-on{display:none}[data-danmuku-mode1=false] .art-danmuku [data-mode="1"]{opacity:0!important}[data-danmuku-mode1=true] .apd-config-mode .apd-mode-1-off{display:none}[data-danmuku-mode1=true] .apd-config-mode .apd-mode-1-on{display:block}[data-danmuku-mode="1"] .apd-style-mode [data-mode="1"]{color:#00a1d6}[data-danmuku-mode="1"] .apd-style-mode [data-mode="1"] path{fill:#00a1d6}[data-danmuku-mode2=false] .apd-config-mode .apd-mode-2-off{display:block}[data-danmuku-mode2=false] .apd-config-mode .apd-mode-2-on{display:none}[data-danmuku-mode2=false] .art-danmuku [data-mode="2"]{opacity:0!important}[data-danmuku-mode2=true] .apd-config-mode .apd-mode-2-off{display:none}[data-danmuku-mode2=true] .apd-config-mode .apd-mode-2-on{display:block}[data-danmuku-mode="2"] .apd-style-mode [data-mode="2"]{color:#00a1d6}[data-danmuku-mode="2"] .apd-style-mode [data-mode="2"] path{fill:#00a1d6}'}function H(t){return e=>{const i=new k(e,t),a=e.isDestroy?void 0:new P(e,i);return!e.isDestroy&&i.option.heatmap&&x(e,i,i.option.heatmap),{name:"artplayerPluginDanmuku",emit:i.emit.bind(i),load:i.load.bind(i),config:i.config.bind(i),hide:i.hide.bind(i),show:i.show.bind(i),reset:i.reset.bind(i),mount:a?a.mount.bind(a):()=>{},get option(){return i.option},get isHide(){return i.isHide},get isStop(){return i.isStop}}}}return H.icons=P.icons,H}); diff --git a/packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js b/packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js index db0f66315..0052ae2a5 100644 --- a/packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js +++ b/packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js @@ -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,a=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,o=Math.pow,r=(e,i,a)=>i in e?t(e,i,{enumerable:!0,configurable:!0,writable:!0,value:a}):e[i]=a,l=(t,e)=>{for(var i in e||(e={}))n.call(e,i)&&r(t,i,e[i]);if(a)for(var i of a(e))s.call(e,i)&&r(t,i,e[i]);return t},d=(t,a)=>e(t,i(a)),p=(t,e,i)=>new Promise((a,n)=>{var s=t=>{try{r(i.next(t))}catch(e){n(e)}},o=t=>{try{r(i.throw(t))}catch(e){n(e)}},r=t=>t.done?a(t.value):Promise.resolve(t.value).then(s,o);r((i=i.apply(t,e)).next())});function h(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function u(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function c(t){if("string"!==typeof t)return[];const e=new RegExp(']*?p="(?

[^"]+)"[^>]*>(?.*?)<\\/d>',"gs"),i=t.matchAll(e);return Array.from(i).map(t=>{const e=t.groups.p.split(",");if(e.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(e[0]),mode:u(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,a=c(e);globalThis.postMessage({danmus:a,id:i})}let f=0;function g(t,{signal:e,onCancel:i}={}){return new Promise((a,n)=>{let s,o,r,l=!1;const d=++f,h=()=>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!==o){const a=o;o=void 0;try{URL.revokeObjectURL(a)}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",h),null==r||r();try{g()}catch(o){s||(i=o),s=!0}s?n(i):a(t)}function v(t,e){if(!l){console.error("Error parsing Bilibili Danmu:",e);try{y(c(t))}catch(i){y(void 0,i)}}}(null==e?void 0:e.aborted)?h():(null==e||e.addEventListener("abort",h),r=null==i?void 0:i(h),l?null==r||r():function(){return p(this,null,function*(){const i=e?yield fetch(t,{signal:e}):yield fetch(t);if(l)return;const a=yield i.text();if(!l)try{const t=`\n ${u.toString()}\n ${c.toString()}\n onmessage = ${m.toString()}\n `;if(o=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),l)return void g();if(s=new Worker(o),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(a,t.error||t)},s.onmessageerror=t=>v(a,t.error||t),s.postMessage({xml:a,id:d})}catch(n){v(a,n)}})}().catch(t=>y(void 0,t)))})}const y=new WeakMap,v=Symbol("cancelled danmuku input");function k(t){return y.has(t)||y.set(t,{closed:!1,tasks:new Set,emitting:[]}),y.get(t)}function x(t){const e=k(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function $(t){const e=k(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}const w='/*!\n * artplayer-plugin-danmuku.js v5.3.0\n * Github: https://github.com/zhw2590582/ArtPlayer\n * (c) 2017-2026 Harvey Zhao\n * Released under the MIT License.\n */\nfunction t({target:t,visibles:e,clientWidth:i,clientHeight:h,marginBottom:n,marginTop:o,antiOverlap:s}){const r=h-n,p=e.filter(e=>e.mode===t.mode&&e.top<=r).sort((t,e)=>t.top-e.top);if(0===p.length)return 2===t.mode?r-t.height:o;if(p.unshift({type:"top",top:0,left:0,right:0,height:o,width:i,speed:0,distance:i}),p.push({type:"bottom",top:r,left:0,right:0,height:n,width:i,speed:0,distance:i}),2===t.mode)for(let g=p.length-2;g>=0;g-=1){const e=p[g],i=p[g+1],h=e.top+e.height;if(i.top-h>=t.height)return i.top-t.height}else for(let g=1;g=t.height)return h}const a=[];for(let g=1;g{const i=Math.min(...e.map(t=>t.right)),h=Math.min(...t.map(t=>t.right));return i*e.length-h*t.length});break;case 1:case 2:a.sort((t,e)=>{const i=Math.max(...e.map(t=>t.width));return Math.max(...t.map(t=>t.width))*t.length-i*e.length})}return a[0][0].top}switch(t.mode){case 0:{const e=a.find(e=>e.every(e=>{if(ie.time}));return e&&e[0]?e[0].top:void 0}case 1:case 2:return}}onmessage=e=>{const{data:i}=e;if(!i.id||!i.type)return;const h=(0,{getDanmuTop:t}[i.type])(i);globalThis.postMessage({result:h,id:i.id})};\n',b="undefined"!==typeof self&&self.Blob&&new Blob(["URL.revokeObjectURL(import.meta.url);",w],{type:"text/javascript;charset=utf-8"});function S(t){let e;try{if(e=b&&(self.URL||self.webkitURL).createObjectURL(b),!e)throw"";const i=new Worker(e,{type:"module",name:null==t?void 0:t.name});return i.addEventListener("error",()=>{(self.URL||self.webkitURL).revokeObjectURL(e)}),i}catch(i){return new Worker("data:text/javascript;charset=utf-8,"+encodeURIComponent(w),{type:"module",name:null==t?void 0:t.name})}}class M{constructor(t,e){const{constructor:i,template:a}=t;this.utils=i.utils,this.validator=i.validator,this.$danmuku=a.$danmuku,this.$player=a.$player,this.art=t,this.queue=[],this.$refs=[],this.isStop=!1,this.isHide=!1,this.timer=null,this.index=0,this.worker=null,this.option=M.option,this.states={wait:[],ready:[],emit:[],stop:[]},this.start=this.start.bind(this),this.stop=this.stop.bind(this),this.reset=this.reset.bind(this),this.resize=this.resize.bind(this),this.destroy=this.destroy.bind(this),x(this),t.on("destroy",this.destroy);try{if(this.config(e,!0),!x(this))return;this.worker=new S}catch(n){throw t.off("destroy",this.destroy),$(this),n}t.on("video:play",this.start),t.on("video:playing",this.start),t.on("video:pause",this.stop),t.on("video:waiting",this.stop),t.on("resize",this.resize),this.load().catch(t=>console.warn("Failed to load initial danmuku:",t))}static get option(){return{danmuku:[],speed:5,margin:[10,"25%"],opacity:1,color:"#FFFFFF",mode:0,modes:[0,1,2],fontSize:25,antiOverlap:!0,synchronousPlayback:!1,mount:void 0,heatmap:!1,width:512,points:[],filter:()=>!0,beforeEmit:()=>!0,beforeVisible:()=>!0,visible:!0,emitter:!0,maxLength:200,lockTime:5,theme:"dark",OPACITY:{},FONT_SIZE:{},MARGIN:{},SPEED:{},COLOR:[]}}static get scheme(){return{danmuku:"array|function|string|promise",speed:"number",margin:"array",opacity:"number",color:"string",mode:"number",modes:"array",fontSize:"number|string",antiOverlap:"boolean",synchronousPlayback:"boolean",mount:"?htmldivelement|string",heatmap:"object|boolean",width:"number",points:"array",filter:"function",beforeEmit:"function",beforeVisible:"function",visible:"boolean",emitter:"boolean",maxLength:"number",lockTime:"number",theme:"string",OPACITY:"object",FONT_SIZE:"object",MARGIN:"object",SPEED:"object",COLOR:"array"}}static get cssText(){return'\n user-select: none;\n position: absolute;\n white-space: pre;\n pointer-events: none;\n perspective: 500px;\n display: inline-block;\n will-change: transform;\n font-weight: normal;\n line-height: 1.125;\n visibility: hidden;\n font-family: SimHei, "Microsoft JhengHei", Arial, Helvetica, sans-serif;\n text-shadow: rgb(0, 0, 0) 1px 0px 1px, rgb(0, 0, 0) 0px 1px 1px, rgb(0, 0, 0) 0px -1px 1px, rgb(0, 0, 0) -1px 0px 1px;\n '}get isRotate(){var t,e;return null==(e=null==(t=this.art.plugins)?void 0:t.autoOrientation)?void 0:e.state}get marginTop(){const{clamp:t}=this.utils,e=this.option.margin[0],{clientHeight:i}=this.$player;if("number"===typeof e)return t(e,0,i);if("string"===typeof e&&e.endsWith("%")){return t(i*(Number.parseFloat(e)/100),0,i)}return M.option.margin[0]}get marginBottom(){const{clamp:t}=this.utils,e=this.option.margin[1],{clientHeight:i}=this.$player;if("number"===typeof e)return t(e,0,i);if("string"===typeof e&&e.endsWith("%")){return t(i*(Number.parseFloat(e)/100),0,i)}return M.option.margin[1]}get fontSize(){const{clamp:t}=this.utils,{clientHeight:e}=this.$player,i=this.option.fontSize;if("number"===typeof i)return Math.round(t(i,12,e));if("string"===typeof i&&i.endsWith("%")){const a=Number.parseFloat(i)/100;return Math.round(t(e*a,12,e))}return M.option.fontSize}get $ref(){const t=this.$refs.pop()||document.createElement("div");return t.style.cssText=M.cssText,t.dataset.mode="",t.dataset.id="",t.className="",t}get readys(){const{currentTime:t}=this.art,e=[];return this.filter("ready",t=>e.push(t)),this.filter("wait",i=>{t+.1>=i.time&&i.time>=t-.1&&e.push(i)}),e}get visibles(){const t=[],{clientWidth:e}=this.$player,i=this.getLeft(this.$player);return this.filter("emit",a=>{const n=a.$ref.offsetTop,s=this.getLeft(a.$ref)-i,o=a.$ref.clientHeight,r=a.$ref.clientWidth,l=s+r,d=e-l,p=l/a.$restTime,h={};h.top=n,h.left=s,h.height=o,h.width=r,h.right=d,h.speed=p,h.distance=l,h.time=a.$restTime,h.mode=a.mode,t.push(h)}),t}get speed(){return this.option.synchronousPlayback&&this.art.playbackRate?this.option.speed/Number(this.art.playbackRate):this.option.speed}load(t){return p(this,null,function*(){const{errorHandle:e}=this.utils,i=function(t,e){const i=k(t);if(e)for(const d of[...i.tasks])d.replace&&d.cancel();let a,n=i.closed||t.art.isDestroy;const s=new Promise(t=>a=t),o=new Set,r="function"===typeof AbortController?new AbortController:void 0,l={replace:e,signal:null==r?void 0:r.signal,active:()=>!n&&!i.closed&&!t.art.isDestroy,onCancel:t=>l.active()?(o.add(t),()=>o.delete(t)):(t(),()=>{}),cancel(){if(!n){n=!0,a(v),null==r||r.abort();for(const t of[...o])t();o.clear()}},wait:t=>Promise.race([t,s]),emit(e){i.emitting.push(l);try{return t.emit(e)}finally{i.emitting.pop()}},finish(){i.tasks.delete(l),o.clear()}};return n?a(v):i.tasks.add(l),l}(this,void 0===t);try{if(!i.active())return this;const a=function(t,e){return"function"===typeof t?{asynchronous:!0,value:t()}:h(t)?{asynchronous:!0,value:t}:"string"===typeof t?{asynchronous:!0,value:g(t,e)}:{asynchronous:!1,value:Array.isArray(t)?t:[]}}(t||this.option.danmuku,i),n=a.asynchronous?yield i.wait(a.value):a.value;if(!i.active())return this;if(e(Array.isArray(n),"Danmuku need return an array as result"),void 0===t){if(this.reset(),!i.active())return this;this.queue=[],this.states={wait:[],ready:[],emit:[],stop:[]},this.$refs=[],this.$danmuku.textContent=""}for(let t=0;t{const e=n[t],i=s[t];return"function"===typeof e||"function"===typeof i||h(e)||h(i)?e!==i:JSON.stringify(e)!==JSON.stringify(i)}))&&!e)return this;const o=function(t,e,{defaults:i,validate:a,clamp:n,mount:s}){const o=Object.assign({},i,t,e);return a(o),o.mode=n(o.mode,0,2),o.speed=n(o.speed,1,10),o.opacity=n(o.opacity,0,1),o.lockTime=n(o.lockTime,1,60),o.maxLength=n(o.maxLength,1,1e3),o.mount=o.mount||s,o}(this.option,t,{defaults:M.option,validate:t=>this.validator(t,M.scheme),clamp:i,mount:a});return x(this)?(this.option=o,t.fontSize&&(this.reset(),!x(this))||(this.option.visible?this.show():this.hide(),x(this)&&this.art.emit("artplayerPluginDanmuku:config",this.option)),this):this}getLeft(t){const e=t.getBoundingClientRect();return this.isRotate?e.top:e.left}postMessage(t={}){return new Promise(e=>{t.id=Date.now(),this.worker.postMessage(t),this.worker.onmessage=i=>{const{data:a}=i;a.id===t.id&&e(a)}})}filter(t,e){const i=this.states[t]||[];for(let a=0;ae!==t),t.$state=e,t.$ref&&(t.$ref.dataset.state=e),this.states[e].push(t)}makeWait(t){this.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=M.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.$refs.push(t.$ref),t.$ref=null)}update(){const{setStyles:t}=this.utils;return this.timer=window.requestAnimationFrame(()=>p(this,null,function*(){if(this.art.playing&&!this.isHide){this.filter("emit",t=>{const e=(Date.now()-t.$lastStartTime)/1e3;t.$restTime-=e,t.$lastStartTime=Date.now(),t.$restTime<=0&&this.makeWait(t)});const e=this.readys;for(let i=0;i{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.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.$player;return this.filter("stop",e=>{switch(this.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.$player;return this.filter("emit",e=>{switch(this.setState(e,"stop"),e.mode){case 0:{const i=t-(this.getLeft(e.$ref)-this.getLeft(this.$player));e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition="transform 0s linear 0s";break}}}),this}stop(){return this.isStop=!0,this.suspend(),window.cancelAnimationFrame(this.timer),this.art.emit("artplayerPluginDanmuku:stop"),this}start(){return this.isStop=!1,this.continue(),this.update(),this.art.emit("artplayerPluginDanmuku:start"),this}reset(){return this.queue.forEach(t=>this.makeWait(t)),this.art.emit("artplayerPluginDanmuku:reset"),this}show(){return this.isHide=!1,this.$danmuku.style.opacity=1,this.option.visible=!0,this.art.emit("artplayerPluginDanmuku:show"),this}hide(){return this.isHide=!0,this.$danmuku.style.opacity=0,this.option.visible=!1,this.art.emit("artplayerPluginDanmuku:hide"),this}destroy(){var t;$(this),this.stop(),null==(t=this.worker)||t.terminate(),this.art.off("video:play",this.start),this.art.off("video:playing",this.start),this.art.off("video:pause",this.stop),this.art.off("video:waiting",this.stop),this.art.off("resize",this.resize),this.art.off("destroy",this.destroy),this.art.emit("artplayerPluginDanmuku:destroy")}}const z={map:(t,e,i,a,n)=>(t-e)*(n-a)/(i-e)+a,range(t,e,i){const a=Math.round(t/i)*i;return Array.from({length:Math.floor((e-t)/i)},(t,e)=>e*i+a)}};function E(t,e,i){const{query:a}=t.constructor.utils;t.controls.add({name:"heatmap",position:"top",html:"",style:{position:"absolute",top:"-100px",left:"0px",right:"0px",height:"100px",width:"100%",pointerEvents:"none"},mounted(n){let s=null,r=null;function l(l=[]){if(s=null,r=null,n.innerHTML="",!t.duration||t.option.isLive)return;const d={w:n.offsetWidth,h:n.offsetHeight},p={xMin:0,xMax:d.w,yMin:0,yMax:128,scale:.25,opacity:.2,minHeight:Math.floor(.05*d.h),sampling:Math.floor(d.w/100),smoothing:.2,flattening:.2};"object"===typeof i&&Object.assign(p,i);let h=[];if(Array.isArray(l)&&l.length)h=[...l];else{const i=t.duration/d.w;for(let t=0;t<=d.w;t+=p.sampling){const a=e.queue.filter(({time:e})=>e>t*i&&e<=(t+p.sampling)*i).length;h.push([t,a])}}if(0===h.length)return;const u=h[h.length-1],c=u[0],m=u[1];c!==d.w&&h.push([d.w,m]);const f=h.map(t=>t[1]),g=(Math.min(...f)+Math.max(...f))/2;for(let t=0;tg?1+p.scale:1-p.scale)+p.minHeight}const y=(t,e,i,a)=>{const n=function(t,e){const i=e[0]-t[0],a=e[1]-t[1];return{length:Math.sqrt(o(i,2)+o(a,2)),angle:Math.atan2(a,i)}}(e||t,i||t),s=z.map(Math.cos(n.angle)*p.flattening,0,1,1,0),r=n.angle*s+(a?Math.PI:0),l=n.length*p.smoothing;return[t[0]+Math.cos(r)*l,t[1]+Math.sin(r)*l]},v=h.map(t=>[z.map(t[0],p.xMin,p.xMax,0,d.w),z.map(t[1],p.yMin,p.yMax,d.h,0)]).reduce((t,e,i,a)=>0===i?`M ${a[a.length-1][0]},${d.h} L ${e[0]},${d.h} L ${e[0]},${e[1]}`:`${t} ${((t,e,i)=>{const a=y(i[e-1],i[e-2],t),n=y(t,i[e-1],i[e+1],!0),s=e===i.length-1?" z":"";return`C ${a[0]},${a[1]} ${n[0]},${n[1]} ${t[0]},${t[1]}${s}`})(e,i,a)}`,"");n.innerHTML=`\n \n \n \n \n \n \n \n \n \n \n \n `,s=a("#heatmap-start",n),r=a("#heatmap-stop",n),s.setAttribute("offset",100*t.played+"%"),r.setAttribute("offset",100*t.played+"%")}t.on("video:timeupdate",()=>{s&&r&&(s.setAttribute("offset",100*t.played+"%"),r.setAttribute("offset",100*t.played+"%"))}),t.on("setBar",(t,e)=>{s&&r&&"played"===t&&(s.setAttribute("offset",100*e+"%"),r.setAttribute("offset",100*e+"%"))}),t.on("ready",()=>l()),t.on("resize",()=>l()),t.on("artplayerPluginDanmuku:loaded",()=>l()),t.on("artplayerPluginDanmuku:points",t=>l(t))}})}const C='',T='',F='',A='',D='',P='',L='',O='',H='',V='',j='',R='';class B{constructor(t,e){this.art=t,this.danmuku=e,this.utils=t.constructor.utils;const{setStyle:i}=this.utils,{$controlsCenter:a}=t.template;i(a,"display","flex"),this.template={$controlsCenter:a,$mount:a,$danmuku:null,$toggle:null,$config:null,$configPanel:null,$configModes:null,$style:null,$stylePanel:null,$styleModes:null,$colors:null,$opacitySlider:null,$opacityValue:null,$marginSlider:null,$marginValue:null,$fontSizeSlider:null,$fontSizeValue:null,$speedSlider:null,$speedValue:null,$input:null,$send:null},this.slider={opacity:null,margin:null,fontSize:null,speed:null},this.emitting=!1,this.isLock=!1,this.timer=null,this.createTemplate(),this.createSliders(),this.createEvents(),this.mount(this.option.mount),t.on("resize",()=>this.resize()),t.on("fullscreen",t=>this.onFullscreen(t)),t.on("fullscreenWeb",t=>this.onFullscreen(t)),t.proxy(this.template.$config,"mouseenter",()=>{this.onMouseEnter({$control:this.template.$config,$panel:this.template.$configPanel})}),t.proxy(this.template.$style,"mouseenter",()=>{this.onMouseEnter({$control:this.template.$style,$panel:this.template.$stylePanel})})}static get icons(){return{$on:j,$off:V,$config:F,$style:R,$mode_0_off:A,$mode_0_on:D,$mode_1_off:P,$mode_1_on:L,$mode_2_off:O,$mode_2_on:H,$check_on:T,$check_off:C}}get option(){return this.danmuku.option}get outside(){return this.template.$mount!==this.template.$controlsCenter}get TEMPLATE(){const{option:t}=this;return`\n

\n ${j}${V}\n
\n
\n ${F}\n
\n
\n
\n \u6309\u7c7b\u578b\u5c4f\u853d\n
\n
\n ${A}${D}\n
\u6eda\u52a8
\n
\n
\n ${P}${L}\n
\u9876\u90e8
\n
\n
\n ${O}${H}\n
\u5e95\u90e8
\n
\n
\n
\n
\n
\n ${T}${C}\n \u9632\u6b62\u5f39\u5e55\u91cd\u53e0\n
\n
\n ${T}${C}\n \u540c\u6b65\u89c6\u9891\u901f\u5ea6\n
\n
\n
\n \u4e0d\u900f\u660e\u5ea6\n
\n
\u672a\u77e5
\n
\n
\n \u663e\u793a\u533a\u57df\n
\n
\u672a\u77e5
\n
\n
\n \u5f39\u5e55\u5b57\u53f7\n
\n
\u672a\u77e5
\n
\n
\n \u5f39\u5e55\u901f\u5ea6\n
\n
\u672a\u77e5
\n
\n
\n
\n
\n
\n
\n ${R}\n
\n
\n
\n \u6a21\u5f0f\n
\n
\n ${D}\n
\u6eda\u52a8
\n
\n
\n ${L}\n
\u9876\u90e8
\n
\n
\n ${H}\n
\u5e95\u90e8
\n
\n
\n
\n
\n \u989c\u8272\n
\n ${this.COLOR.map(t=>`
`).join("")}\n
\n
\n
\n
\n
\n \n
\u53d1\u9001
\n
\n `}get OPACITY(){return l({min:0,max:100,steps:[]},this.option.OPACITY)}get FONT_SIZE(){return l({min:12,max:120,steps:[]},this.option.FONT_SIZE)}get MARGIN(){return l({min:0,max:3,steps:[{name:"1/4",value:[10,"75%"]},{name:"\u534a\u5c4f",value:[10,"50%"]},{name:"3/4",value:[10,"25%"]},{name:"\u6ee1\u5c4f",value:[10,10]}]},this.option.MARGIN)}get SPEED(){return l({min:0,max:4,steps:[{name:"\u6781\u6162",value:10},{name:"\u8f83\u6162",value:7.5,hide:!0},{name:"\u9002\u4e2d",value:5},{name:"\u8f83\u5feb",value:2.5,hide:!0},{name:"\u6781\u5feb",value:1}]},this.option.SPEED)}get COLOR(){return this.option.COLOR.length?this.option.COLOR:["#FE0302","#FF7204","#FFAA02","#FFD302","#FFFF00","#A0EE00","#00CD00","#019899","#4266BE","#89D5FF","#CC0273","#222222","#9B9B9B","#FFFFFF"]}query(t){const{query:e}=this.utils,{$danmuku:i}=this.template;return e(t,i)}append(t,e){const{append:i}=this.utils;[...t.children].includes(e)||i(t,e)}setData(t,e){const{$player:i}=this.art.template,{$mount:a}=this.template;i.dataset[t]=e,this.outside&&(a.dataset[t]=e)}createTemplate(){const{createElement:t,tooltip:e}=this.utils,i=t("div");i.className="artplayer-plugin-danmuku",i.innerHTML=this.TEMPLATE,this.template.$danmuku=i,this.template.$toggle=this.query(".apd-toggle"),this.template.$config=this.query(".apd-config"),this.template.$configPanel=this.query(".apd-config-panel"),this.template.$configModes=this.query(".apd-config-mode .apd-modes"),this.template.$style=this.query(".apd-style"),this.template.$stylePanel=this.query(".apd-style-panel"),this.template.$styleModes=this.query(".apd-style-mode .apd-modes"),this.template.$colors=this.query(".apd-colors"),this.template.$antiOverlap=this.query(".apd-anti-overlap"),this.template.$syncVideo=this.query(".apd-sync-video"),this.template.$opacitySlider=this.query(".apd-config-opacity .apd-slider"),this.template.$opacityValue=this.query(".apd-config-opacity .apd-value"),this.template.$marginSlider=this.query(".apd-config-margin .apd-slider"),this.template.$marginValue=this.query(".apd-config-margin .apd-value"),this.template.$fontSizeSlider=this.query(".apd-config-fontSize .apd-slider"),this.template.$fontSizeValue=this.query(".apd-config-fontSize .apd-value"),this.template.$speedSlider=this.query(".apd-config-speed .apd-slider"),this.template.$speedValue=this.query(".apd-config-speed .apd-value"),this.template.$input=this.query(".apd-input"),this.template.$send=this.query(".apd-send");const{$toggle:a}=this.template;this.art.on("artplayerPluginDanmuku:show",()=>{e(a,"\u5173\u95ed\u5f39\u5e55")}),this.art.on("artplayerPluginDanmuku:hide",()=>{e(a,"\u6253\u5f00\u5f39\u5e55")})}createEvents(){const{$toggle:t,$configModes:e,$styleModes:i,$colors:a,$antiOverlap:n,$syncVideo:s,$send:o,$input:r}=this.template;this.art.proxy(t,"click",()=>{this.danmuku.config({visible:!this.option.visible}),this.reset()}),this.art.proxy(e,"click",t=>{const e=t.target.closest(".apd-mode");if(!e)return;const i=Number(e.dataset.mode);this.option.modes.includes(i)?this.danmuku.config({modes:this.option.modes.filter(t=>t!==i)}):this.danmuku.config({modes:[...this.option.modes,i]}),this.reset()}),this.art.proxy(n,"click",()=>{this.danmuku.config({antiOverlap:!this.option.antiOverlap}),this.reset()}),this.art.proxy(s,"click",()=>{this.danmuku.config({synchronousPlayback:!this.option.synchronousPlayback}),this.reset()}),this.art.proxy(i,"click",t=>{const e=t.target.closest(".apd-mode");if(!e)return;const i=Number(e.dataset.mode);this.danmuku.config({mode:i}),this.reset()}),this.art.proxy(a,"click",t=>{const e=t.target.closest(".apd-color");e&&(this.danmuku.config({color:e.dataset.color}),this.reset())}),this.art.proxy(o,"click",()=>this.emit()),this.art.proxy(r,"keypress",t=>{"Enter"===t.key&&(t.preventDefault(),this.emit())})}createSliders(){this.slider.opacity=this.createSlider(d(l({},this.OPACITY),{container:this.template.$opacitySlider,findIndex:()=>Math.round(100*this.option.opacity),onChange:t=>{const{$opacityValue:e}=this.template;e.textContent=`${t}%`,this.danmuku.config({opacity:t/100})}})),this.slider.margin=this.createSlider(d(l({},this.MARGIN),{container:this.template.$marginSlider,findIndex:()=>this.MARGIN.steps.findIndex(t=>t.value[0]===this.option.margin[0]&&t.value[1]===this.option.margin[1]),onChange:t=>{const e=this.MARGIN.steps[t];if(!e)return;const{$marginValue:i}=this.template;i.textContent=e.name,this.danmuku.config({margin:e.value})}})),this.slider.fontSize=this.createSlider(d(l({},this.FONT_SIZE),{container:this.template.$fontSizeSlider,findIndex:()=>this.danmuku.fontSize,onChange:t=>{const{$fontSizeValue:e}=this.template;e.textContent=`${t}px`,t!==this.danmuku.fontSize&&this.danmuku.config({fontSize:t})}})),this.slider.speed=this.createSlider(d(l({},this.SPEED),{container:this.template.$speedSlider,findIndex:()=>this.SPEED.steps.findIndex(t=>t.value===this.option.speed),onChange:t=>{const e=this.SPEED.steps[t];if(!e)return;const{$speedValue:i}=this.template;i.textContent=e.name,this.danmuku.config({speed:e.value})}}))}createSlider({min:t,max:e,container:i,findIndex:a,onChange:n,steps:s=[]}){const{query:o,clamp:r,setStyle:l}=this.utils;l(i,"touch-action","none"),i.innerHTML=`\n
\n
\n ${s.map(()=>'
').join("")}\n
\n
\n
\n
\n
\n ${s.map(t=>t.hide?"":`
${t.name}
`).join("")}\n
\n `;const d=o(".apd-slider-dot",i),p=o(".apd-slider-progress",i);let h=!1;function u(i=a()){if(ie)return;const o=(i-t)/(e-t);d.style.left=100*o+"%",0===s.length&&(p.style.width=d.style.left),n(i)}function c(a){const{top:n,height:s,left:o,width:l}=i.getBoundingClientRect();if(this.art.isRotate){const i=r(a.clientY-n,0,s);u(Math.round(i/s*(e-t)+t))}else{const i=r(a.clientX-o,0,l);u(Math.round(i/l*(e-t)+t))}}return this.art.proxy(i,"click",t=>{c.call(this,t)}),this.art.proxy(i,"pointerdown",t=>{h=0===t.button}),this.art.on("document:pointermove",t=>{h&&c.call(this,t)}),this.art.on("document:pointerup",t=>{h&&(h=!1,c.call(this,t))}),{reset:u}}onFullscreen(t){const{$danmuku:e,$controlsCenter:i,$mount:a}=this.template;this.outside?t?this.append(i,e):this.append(a,e):this.append(i,e)}onMouseEnter({$control:t,$panel:e}){const{$player:i}=this.art.template,a=t.getBoundingClientRect(),n=e.getBoundingClientRect(),s=i.getBoundingClientRect(),o=n.width/2-a.width/2,r=s.left-(a.left-o),l=a.right+o-s.right;e.style.left=r>0?`${-o+r}px`:l>0?-o-l+"px":-o+"px"}emit(){return p(this,null,function*(){const{$input:t}=this.template,e=t.value.trim();if(!e.length)return;if(this.isLock)return;if(this.emitting)return;const i={text:e,mode:this.option.mode,color:this.option.color,time:this.art.currentTime};try{this.emitting=!0;const e=yield this.option.beforeEmit(i);if(this.emitting=!1,!0!==e)return;i.border=!0,delete i.time,this.danmuku.emit(i),t.value="",this.lock()}catch(a){console.error("Error emitting danmuku:",a),this.emitting=!1}})}lock(){const{addClass:t}=this.utils,{$send:e}=this.template;this.isLock=!0;let i=this.option.lockTime;e.textContent=i,t(e,"apd-lock");const a=()=>{this.timer=setTimeout(()=>{0===i?this.unlock():(i-=1,e.textContent=i,a())},1e3)};a()}unlock(){const{removeClass:t}=this.utils,{$send:e}=this.template;clearTimeout(this.timer),this.isLock=!1,e.textContent="\u53d1\u9001",t(e,"apd-lock")}resize(){if(this.outside)return;if(this.art.fullscreen)return;if(this.art.fullscreenWeb)return;const{$player:t,$controlsCenter:e}=this.art.template,{$danmuku:i}=this.template;this.art.widtht.dataset.color===this.option.color.toUpperCase());s&&t(s,"apd-active"),e(i,this.option.visible?"\u5173\u95ed\u5f39\u5e55":"\u6253\u5f00\u5f39\u5e55"),this.resize()}mount(t){const{errorHandle:e}=this.utils,i="string"===typeof t?document.querySelector(t):t;e(i,`Can not find the mount point: ${t}`),this.append(i,this.template.$danmuku),this.template.$mount=i,this.reset()}}if("undefined"!==typeof document){const t="artplayer-plugin-danmuku";let e=document.getElementById(t);e||(e=document.createElement("style"),e.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.appendChild(e)}):(document.head||document.documentElement).appendChild(e)),e.textContent='.artplayer-plugin-danmuku{display:flex;position:relative;z-index:99;align-items:center;justify-content:center;font-size:12px;height:32px;width:100%;color:#fff;font-weight:300;flex-shrink:0;gap:10px}.artplayer-plugin-danmuku .apd-icon{cursor:pointer;opacity:.75;transition:all .2s ease;fill:#fff}.artplayer-plugin-danmuku .apd-icon:hover{opacity:1}.artplayer-plugin-danmuku .apd-config{display:flex;position:relative}.artplayer-plugin-danmuku .apd-config .apd-config-panel{position:absolute;bottom:24px;left:0;width:320px;padding:10px;opacity:0;pointer-events:none}.artplayer-plugin-danmuku .apd-config .apd-config-panel .apd-config-panel-inner{width:100%;border-radius:3px;background-color:#000000d9;padding:10px}.artplayer-plugin-danmuku .apd-config:hover .apd-config-panel{opacity:100;pointer-events:all}.artplayer-plugin-danmuku .apd-config-mode,.artplayer-plugin-danmuku .apd-config-slider,.artplayer-plugin-danmuku .apd-config-other,.artplayer-plugin-danmuku .apd-style-mode{margin-bottom:15px}.artplayer-plugin-danmuku .apd-modes{display:flex;align-items:center;margin-top:5px;gap:20px}.artplayer-plugin-danmuku .apd-modes .apd-mode{cursor:pointer;text-align:center}.artplayer-plugin-danmuku .apd-modes .apd-mode:hover{color:#00a1d6}.artplayer-plugin-danmuku .apd-config-slider{display:flex;align-items:center;gap:12px}.artplayer-plugin-danmuku .apd-config-slider .apd-value{width:32px;text-align:right}.artplayer-plugin-danmuku .apd-slider{position:relative;flex:1;display:flex;height:20px;align-items:center;justify-content:center;cursor:pointer}.artplayer-plugin-danmuku .apd-slider .apd-slider-line{position:relative;height:2px;width:100%;overflow:hidden;border-radius:3px;background-color:#ffffff40}.artplayer-plugin-danmuku .apd-slider .apd-slider-points{position:absolute;inset:0;display:flex;align-items:center;justify-content:space-between}.artplayer-plugin-danmuku .apd-slider .apd-slider-points .apd-slider-point{width:2px;height:2px;border-radius:50%;background-color:#ffffff80}.artplayer-plugin-danmuku .apd-slider .apd-slider-progress{width:0%;height:100%;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-slider .apd-slider-dot{position:absolute;transform:translate(-6px);left:0%;width:12px;height:12px;border-radius:50%;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-slider .apd-slider-steps{display:flex;align-items:center;justify-content:space-between;position:absolute;bottom:-12px;width:calc(100% + 32px);color:#777}.artplayer-plugin-danmuku .apd-slider .apd-slider-steps .apd-slider-step{flex-shrink:0;width:36px;text-align:center;scale:.95}.artplayer-plugin-danmuku .apd-config-other{display:flex;align-items:center;gap:20px}.artplayer-plugin-danmuku .apd-config-other .apd-check-off,.artplayer-plugin-danmuku .apd-config-other .apd-check-on{width:16px;height:16px}.artplayer-plugin-danmuku .apd-config-other .apd-other{display:flex;align-items:center;cursor:pointer;gap:2px}.artplayer-plugin-danmuku .apd-config-other .apd-other:hover{color:#00a1d6}.artplayer-plugin-danmuku .apd-emitter{display:flex;flex:1;align-items:center;height:100%;background-color:#ffffff40;border-radius:5px}.artplayer-plugin-danmuku .apd-style{position:relative;display:flex;align-items:center;justify-content:center}.artplayer-plugin-danmuku .apd-style .apd-style-panel{position:absolute;bottom:24px;left:0;width:200px;padding:10px;opacity:0;pointer-events:none}.artplayer-plugin-danmuku .apd-style .apd-style-panel .apd-style-panel-inner{width:100%;border-radius:3px;background-color:#000000d9;padding:10px}.artplayer-plugin-danmuku .apd-style:hover .apd-style-panel{opacity:100;pointer-events:all}.artplayer-plugin-danmuku .apd-colors{display:flex;flex-wrap:wrap;margin-top:5px;gap:8px}.artplayer-plugin-danmuku .apd-colors .apd-color{width:16px;height:16px;border-radius:2px;cursor:pointer}.artplayer-plugin-danmuku .apd-colors .apd-color.apd-active{border:1px solid black;box-shadow:0 0 0 1px #fff}.artplayer-plugin-danmuku .apd-input{outline:none;height:100%;flex:1;min-width:0;width:auto;border:none;line-height:1;color:#fff;background-color:transparent}.artplayer-plugin-danmuku .apd-input::placeholder{color:#ffffff80}.artplayer-plugin-danmuku .apd-send{display:flex;align-items:center;justify-content:center;height:100%;width:60px;flex-shrink:0;cursor:pointer;text-shadow:none;border-top-right-radius:5px;border-bottom-right-radius:5px;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-send.apd-lock{cursor:not-allowed;color:#666;background-color:#e7e7e7}.art-controls-center .apd-emitter{width:260px;flex:none}.art-fullscreen .artplayer-plugin-danmuku,.art-fullscreen-web .artplayer-plugin-danmuku{height:38px;gap:16px}.art-fullscreen .artplayer-plugin-danmuku .apd-config-icon,.art-fullscreen-web .artplayer-plugin-danmuku .apd-config-icon,.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-off,.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-off,.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-on,.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-on{width:28px;height:28px}.art-fullscreen .artplayer-plugin-danmuku .apd-emitter,.art-fullscreen-web .artplayer-plugin-danmuku .apd-emitter{width:400px;flex:none}.art-video-player>.artplayer-plugin-danmuku{position:absolute;left:0;right:0;bottom:-40px;padding:0 10px}.art-video-player:has(>.artplayer-plugin-danmuku){margin-bottom:40px}[data-danmuku-emitter=false] .apd-emitter{display:none!important}[data-danmuku-emitter=false] .art-controls-center .artplayer-plugin-danmuku{justify-content:flex-end;gap:18px}[data-danmuku-emitter=false].art-fullscreen .art-controls-center .artplayer-plugin-danmuku,[data-danmuku-emitter=false].art-fullscreen-web .art-controls-center .artplayer-plugin-danmuku{gap:24px}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-icon{fill:#333}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-emitter{background-color:#f1f2f3}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-input{color:#000}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-input::placeholder{color:#0000004d}[data-danmuku-visible=false] .apd-toggle-off{display:block}[data-danmuku-visible=false] .apd-toggle-on,[data-danmuku-visible=true] .apd-toggle-off{display:none}[data-danmuku-visible=true] .apd-toggle-on{display:block}[data-danmuku-anti-overlap=false] .apd-anti-overlap .apd-check-on{display:none}[data-danmuku-anti-overlap=false] .apd-anti-overlap .apd-check-off,[data-danmuku-anti-overlap=true] .apd-anti-overlap .apd-check-on{display:block}[data-danmuku-anti-overlap=true] .apd-anti-overlap .apd-check-off,[data-danmuku-sync-video=false] .apd-sync-video .apd-check-on{display:none}[data-danmuku-sync-video=false] .apd-sync-video .apd-check-off,[data-danmuku-sync-video=true] .apd-sync-video .apd-check-on{display:block}[data-danmuku-sync-video=true] .apd-sync-video .apd-check-off{display:none}[data-danmuku-mode0=false] .apd-config-mode .apd-mode-0-off{display:block}[data-danmuku-mode0=false] .apd-config-mode .apd-mode-0-on{display:none}[data-danmuku-mode0=false] .art-danmuku [data-mode="0"]{opacity:0!important}[data-danmuku-mode0=true] .apd-config-mode .apd-mode-0-off{display:none}[data-danmuku-mode0=true] .apd-config-mode .apd-mode-0-on{display:block}[data-danmuku-mode="0"] .apd-style-mode [data-mode="0"]{color:#00a1d6}[data-danmuku-mode="0"] .apd-style-mode [data-mode="0"] path{fill:#00a1d6}[data-danmuku-mode1=false] .apd-config-mode .apd-mode-1-off{display:block}[data-danmuku-mode1=false] .apd-config-mode .apd-mode-1-on{display:none}[data-danmuku-mode1=false] .art-danmuku [data-mode="1"]{opacity:0!important}[data-danmuku-mode1=true] .apd-config-mode .apd-mode-1-off{display:none}[data-danmuku-mode1=true] .apd-config-mode .apd-mode-1-on{display:block}[data-danmuku-mode="1"] .apd-style-mode [data-mode="1"]{color:#00a1d6}[data-danmuku-mode="1"] .apd-style-mode [data-mode="1"] path{fill:#00a1d6}[data-danmuku-mode2=false] .apd-config-mode .apd-mode-2-off{display:block}[data-danmuku-mode2=false] .apd-config-mode .apd-mode-2-on{display:none}[data-danmuku-mode2=false] .art-danmuku [data-mode="2"]{opacity:0!important}[data-danmuku-mode2=true] .apd-config-mode .apd-mode-2-off{display:none}[data-danmuku-mode2=true] .apd-config-mode .apd-mode-2-on{display:block}[data-danmuku-mode="2"] .apd-style-mode [data-mode="2"]{color:#00a1d6}[data-danmuku-mode="2"] .apd-style-mode [data-mode="2"] path{fill:#00a1d6}'}function q(t){return e=>{const i=new M(e,t),a=e.isDestroy?void 0:new B(e,i);return!e.isDestroy&&i.option.heatmap&&E(e,i,i.option.heatmap),{name:"artplayerPluginDanmuku",emit:i.emit.bind(i),load:i.load.bind(i),config:i.config.bind(i),hide:i.hide.bind(i),show:i.show.bind(i),reset:i.reset.bind(i),mount:a?a.mount.bind(a):()=>{},get option(){return i.option},get isHide(){return i.isHide},get isStop(){return i.isStop}}}}return q.icons=B.icons,q}); +!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,o=Math.pow,r=(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)&&r(t,i,e[i]);if(n)for(var i of n(e))s.call(e,i)&&r(t,i,e[i]);return t},d=(t,n)=>e(t,i(n)),p=(t,e,i)=>new Promise((n,a)=>{var s=t=>{try{r(i.next(t))}catch(e){a(e)}},o=t=>{try{r(i.throw(t))}catch(e){a(e)}},r=t=>t.done?n(t.value):Promise.resolve(t.value).then(s,o);r((i=i.apply(t,e)).next())});function h(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function u(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function c(t){if("string"!==typeof t)return[];const e=new RegExp(']*?p="(?

[^"]+)"[^>]*>(?.*?)<\\/d>',"gs"),i=t.matchAll(e);return Array.from(i).map(t=>{const e=t.groups.p.split(",");if(e.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(e[0]),mode:u(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=c(e);globalThis.postMessage({danmus:n,id:i})}let f=0;function g(t,{signal:e,onCancel:i}={}){return new Promise((n,a)=>{let s,o,r,l=!1;const d=++f,h=()=>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!==o){const n=o;o=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",h),null==r||r();try{g()}catch(o){s||(i=o),s=!0}s?a(i):n(t)}function v(t,e){if(!l){console.error("Error parsing Bilibili Danmu:",e);try{y(c(t))}catch(i){y(void 0,i)}}}(null==e?void 0:e.aborted)?h():(null==e||e.addEventListener("abort",h),r=null==i?void 0:i(h),l?null==r||r():function(){return p(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 ${u.toString()}\n ${c.toString()}\n onmessage = ${m.toString()}\n `;if(o=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),l)return void g();if(s=new Worker(o),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 k(t){return y.has(t)||y.set(t,{closed:!1,tasks:new Set,emitting:[]}),y.get(t)}function w(t){const e=k(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function x(t){const e=k(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function $(t,e,i){const n=t.states[e]||[];for(let a=0;a{if(this.frame=null,t.timer=null,this.closed||this.fault||t.isStop||t.art.isDestroy)return;let e;const i=new Promise(t=>e=()=>t(b)),n={generation:this.generation,cancel:e,wait:t=>Promise.race([t,i]),ref:null};return this.operation=n,this.run(n).catch(t=>{this.active(n)&&this.fail(t)}).finally(()=>{this.release(n),this.operation===n&&(this.operation=null,this.schedule())})}),t.timer=this.frame)}run(t){return p(this,null,function*(){const e=this.owner,{setStyles:i}=e.utils;if(!e.art.playing||!this.active(t))return;e.filter("emit",t=>{const i=(Date.now()-t.$lastStartTime)/1e3;t.$restTime-=i,t.$lastStartTime=Date.now(),t.$restTime<=0&&e.makeWait(t)});const n=e.readys;for(const s of n){if(!this.active(t))return;if(this.failedItems.has(s))continue;let n;try{n=yield t.wait(e.option.beforeVisible(s))}catch(a){if(!this.active(t))return;this.failedItems.add(s),this.report(a);continue}if(!this.active(t))return;if(!n)continue;const{clientWidth:o,clientHeight:r}=e.$player,l=e.$ref;if(t.danmu=s,t.ref=l,s.$ref=l,l.textContent=s.text,e.$danmuku.appendChild(l),l.style.opacity=e.option.opacity,l.style.fontSize=`${e.fontSize}px`,l.style.color=s.color,l.style.border=s.border?`1px solid ${s.color}`:null,l.style.backgroundColor=s.border?"rgb(0 0 0 / 50%)":null,i(l,s.style),!this.active(t))return;s.$lastStartTime=Date.now(),s.$restTime=e.speed;const d=o+l.clientWidth,{result:p}=yield t.wait(e.postMessage({type:"getDanmuTop",target:{mode:s.mode,height:l.clientHeight,speed:d/s.$restTime},visibles:e.visibles,antiOverlap:e.option.antiOverlap,clientWidth:o,clientHeight:r,marginBottom:e.marginBottom,marginTop:e.marginTop}));if(!this.active(t)||s.$ref!==l)return;if(void 0!==p){switch(e.setState(s,"emit"),l.style.top=`${p}px`,l.style.visibility="visible",l.dataset.mode=s.mode,l.dataset.id=s.id||"",s.mode){case 0:l.style.left=`${o}px`,l.style.marginLeft="0px",l.style.transform=`translateX(${-d}px)`,l.style.transition=`transform ${s.$restTime}s linear 0s`;break;case 1:case 2:l.style.left="50%",l.style.marginLeft=`-${l.clientWidth/2}px`}t.ref=null,e.art.emit("artplayerPluginDanmuku:visible",s)}else e.setState(s,"ready"),this.release(t)}})}destroy(){this.closed=!0,this.invalidate()}}let M=0;class z{constructor(t,e){this.pending=new Map,this.closed=!1,this.failed=!1,this.onFailure=e,this.worker=t();try{this.worker.onmessage=t=>{var e;const i=this.pending.get(null==(e=t.data)?void 0:e.id);i&&(this.pending.delete(t.data.id),i.resolve(t.data))},this.worker.onerror=t=>{var e;null==(e=t.preventDefault)||e.call(t),this.fail(t.error||t)},this.worker.onmessageerror=t=>this.fail(t.error||t)}catch(i){try{this.dispose()}catch(n){}throw i}}request(t){t.id=++M;const{id:e}=t;return this.closed||this.failed?Promise.resolve({id:e,result:void 0}):new Promise((i,n)=>{this.pending.set(e,{resolve:i,reject:n});try{this.worker.postMessage(t)}catch(a){this.fail(a)}})}fail(t){if(!this.closed&&!this.failed){this.failed=!0;for(const e of this.pending.values())e.reject(t);this.pending.clear();try{this.dispose()}catch(e){}this.onFailure(t)}}cancel(){for(const[t,e]of this.pending)e.resolve({id:t,result:void 0});this.pending.clear()}dispose(){if(this.closed)return;this.closed=!0,this.cancel();let t,e=!1;const i=i=>{try{i()}catch(n){e||(t=n),e=!0}};for(const n of["onmessage","onerror","onmessageerror"])i(()=>this.worker[n]=null);if(i(()=>this.worker.terminate()),e)throw t}}const C='/*!\n * artplayer-plugin-danmuku.js v5.3.0\n * Github: https://github.com/zhw2590582/ArtPlayer\n * (c) 2017-2026 Harvey Zhao\n * Released under the MIT License.\n */\nfunction t({target:t,visibles:e,clientWidth:i,clientHeight:h,marginBottom:n,marginTop:o,antiOverlap:s}){const r=h-n,p=e.filter(e=>e.mode===t.mode&&e.top<=r).sort((t,e)=>t.top-e.top);if(0===p.length)return 2===t.mode?r-t.height:o;if(p.unshift({type:"top",top:0,left:0,right:0,height:o,width:i,speed:0,distance:i}),p.push({type:"bottom",top:r,left:0,right:0,height:n,width:i,speed:0,distance:i}),2===t.mode)for(let g=p.length-2;g>=0;g-=1){const e=p[g],i=p[g+1],h=e.top+e.height;if(i.top-h>=t.height)return i.top-t.height}else for(let g=1;g=t.height)return h}const a=[];for(let g=1;g{const i=Math.min(...e.map(t=>t.right)),h=Math.min(...t.map(t=>t.right));return i*e.length-h*t.length});break;case 1:case 2:a.sort((t,e)=>{const i=Math.max(...e.map(t=>t.width));return Math.max(...t.map(t=>t.width))*t.length-i*e.length})}return a[0][0].top}switch(t.mode){case 0:{const e=a.find(e=>e.every(e=>{if(ie.time}));return e&&e[0]?e[0].top:void 0}case 1:case 2:return}}onmessage=e=>{const{data:i}=e;if(!i.id||!i.type)return;const h=(0,{getDanmuTop:t}[i.type])(i);globalThis.postMessage({result:h,id:i.id})};\n',E="undefined"!==typeof self&&self.Blob&&new Blob(["URL.revokeObjectURL(import.meta.url);",C],{type:"text/javascript;charset=utf-8"});function D(t){let e;try{if(e=E&&(self.URL||self.webkitURL).createObjectURL(E),!e)throw"";const i=new Worker(e,{type:"module",name:null==t?void 0:t.name});return i.addEventListener("error",()=>{(self.URL||self.webkitURL).revokeObjectURL(e)}),i}catch(i){return new Worker("data:text/javascript;charset=utf-8,"+encodeURIComponent(C),{type:"module",name:null==t?void 0:t.name})}}class F{constructor(t,e){const{constructor:i,template:n}=t;this.utils=i.utils,this.validator=i.validator,this.$danmuku=n.$danmuku,this.$player=n.$player,this.art=t,this.queue=[],this.$refs=[],this.isStop=!1,this.isHide=!1,this.timer=null,this.index=0,this.worker=null,this.workerClient=null,this.scheduler=new S(this),this.option=F.option,this.states={wait:[],ready:[],emit:[],stop:[]},this.start=this.start.bind(this),this.stop=this.stop.bind(this),this.reset=this.reset.bind(this),this.resize=this.resize.bind(this),this.destroy=this.destroy.bind(this),this.seek=this.seek.bind(this),w(this),t.on("destroy",this.destroy);try{if(this.config(e,!0),!w(this))return;this.createWorker()}catch(a){throw t.off("destroy",this.destroy),x(this),this.scheduler.destroy(),a}t.on("video:play",this.start),t.on("video:playing",this.start),t.on("video:pause",this.stop),t.on("video:waiting",this.stop),t.on("resize",this.resize),t.on("video:seeking",this.seek),this.load().catch(t=>console.warn("Failed to load initial danmuku:",t))}static get option(){return{danmuku:[],speed:5,margin:[10,"25%"],opacity:1,color:"#FFFFFF",mode:0,modes:[0,1,2],fontSize:25,antiOverlap:!0,synchronousPlayback:!1,mount:void 0,heatmap:!1,width:512,points:[],filter:()=>!0,beforeEmit:()=>!0,beforeVisible:()=>!0,visible:!0,emitter:!0,maxLength:200,lockTime:5,theme:"dark",OPACITY:{},FONT_SIZE:{},MARGIN:{},SPEED:{},COLOR:[]}}static get scheme(){return{danmuku:"array|function|string|promise",speed:"number",margin:"array",opacity:"number",color:"string",mode:"number",modes:"array",fontSize:"number|string",antiOverlap:"boolean",synchronousPlayback:"boolean",mount:"?htmldivelement|string",heatmap:"object|boolean",width:"number",points:"array",filter:"function",beforeEmit:"function",beforeVisible:"function",visible:"boolean",emitter:"boolean",maxLength:"number",lockTime:"number",theme:"string",OPACITY:"object",FONT_SIZE:"object",MARGIN:"object",SPEED:"object",COLOR:"array"}}static get cssText(){return'\n user-select: none;\n position: absolute;\n white-space: pre;\n pointer-events: none;\n perspective: 500px;\n display: inline-block;\n will-change: transform;\n font-weight: normal;\n line-height: 1.125;\n visibility: hidden;\n font-family: SimHei, "Microsoft JhengHei", Arial, Helvetica, sans-serif;\n text-shadow: rgb(0, 0, 0) 1px 0px 1px, rgb(0, 0, 0) 0px 1px 1px, rgb(0, 0, 0) 0px -1px 1px, rgb(0, 0, 0) -1px 0px 1px;\n '}get isRotate(){var t,e;return null==(e=null==(t=this.art.plugins)?void 0:t.autoOrientation)?void 0:e.state}get marginTop(){const{clamp:t}=this.utils,e=this.option.margin[0],{clientHeight:i}=this.$player;if("number"===typeof e)return t(e,0,i);if("string"===typeof e&&e.endsWith("%")){return t(i*(Number.parseFloat(e)/100),0,i)}return F.option.margin[0]}get marginBottom(){const{clamp:t}=this.utils,e=this.option.margin[1],{clientHeight:i}=this.$player;if("number"===typeof e)return t(e,0,i);if("string"===typeof e&&e.endsWith("%")){return t(i*(Number.parseFloat(e)/100),0,i)}return F.option.margin[1]}get fontSize(){const{clamp:t}=this.utils,{clientHeight:e}=this.$player,i=this.option.fontSize;if("number"===typeof i)return Math.round(t(i,12,e));if("string"===typeof i&&i.endsWith("%")){const n=Number.parseFloat(i)/100;return Math.round(t(e*n,12,e))}return F.option.fontSize}get $ref(){const t=this.$refs.pop()||document.createElement("div");return t.style.cssText=F.cssText,t.dataset.mode="",t.dataset.id="",t.className="",t}get readys(){return function(t){const{currentTime:e}=t.art,i=[];return $(t,"ready",t=>i.push(t)),$(t,"wait",t=>{e+.1>=t.time&&t.time>=e-.1&&i.push(t)}),i}(this)}get visibles(){const t=[],{clientWidth:e}=this.$player,i=this.getLeft(this.$player);return this.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.getLeft(n.$ref)-i,o=n.$ref.clientHeight,r=n.$ref.clientWidth,l=s+r,d=e-l,p=l/n.$restTime,h={};h.top=a,h.left=s,h.height=o,h.width=r,h.right=d,h.speed=p,h.distance=l,h.time=n.$restTime,h.mode=n.mode,t.push(h)}),t}get speed(){return this.option.synchronousPlayback&&this.art.playbackRate?this.option.speed/Number(this.art.playbackRate):this.option.speed}load(t){return p(this,null,function*(){const{errorHandle:e}=this.utils,i=function(t,e){const i=k(t);if(e)for(const d of[...i.tasks])d.replace&&d.cancel();let n,a=i.closed||t.art.isDestroy;const s=new Promise(t=>n=t),o=new Set,r="function"===typeof AbortController?new AbortController:void 0,l={replace:e,signal:null==r?void 0:r.signal,active:()=>!a&&!i.closed&&!t.art.isDestroy,onCancel:t=>l.active()?(o.add(t),()=>o.delete(t)):(t(),()=>{}),cancel(){if(!a){a=!0,n(v),null==r||r.abort();for(const t of[...o])t();o.clear()}},wait:t=>Promise.race([t,s]),emit(e){i.emitting.push(l);try{return t.emit(e)}finally{i.emitting.pop()}},finish(){i.tasks.delete(l),o.clear()}};return a?n(v):i.tasks.add(l),l}(this,void 0===t);try{if(!i.active())return this;const n=function(t,e){return"function"===typeof t?{asynchronous:!0,value:t()}:h(t)?{asynchronous:!0,value:t}:"string"===typeof t?{asynchronous:!0,value:g(t,e)}:{asynchronous:!1,value:Array.isArray(t)?t:[]}}(t||this.option.danmuku,i),a=n.asynchronous?yield i.wait(n.value):n.value;if(!i.active())return this;if(e(Array.isArray(a),"Danmuku need return an array as result"),void 0===t){if(this.reset(),!i.active())return this;this.queue=[],this.states={wait:[],ready:[],emit:[],stop:[]},this.$refs=[],this.$danmuku.textContent=""}for(let t=0;t{const e=a[t],i=s[t];return"function"===typeof e||"function"===typeof i||h(e)||h(i)?e!==i:JSON.stringify(e)!==JSON.stringify(i)}))&&!e)return this;const o=function(t,e,{defaults:i,validate:n,clamp:a,mount:s}){const o=Object.assign({},i,t,e);return n(o),o.mode=a(o.mode,0,2),o.speed=a(o.speed,1,10),o.opacity=a(o.opacity,0,1),o.lockTime=a(o.lockTime,1,60),o.maxLength=a(o.maxLength,1,1e3),o.mount=o.mount||s,o}(this.option,t,{defaults:F.option,validate:t=>this.validator(t,F.scheme),clamp:i,mount:n});if(!w(this))return this;const r=this.option.beforeVisible!==o.beforeVisible;return this.option=o,r&&(this.scheduler.invalidate(),this.scheduler.schedule()),t.fontSize&&(this.reset(),!w(this))||(this.option.visible?this.show():this.hide(),w(this)&&this.art.emit("artplayerPluginDanmuku:config",this.option)),this}getLeft(t){const e=t.getBoundingClientRect();return this.isRotate?e.top:e.left}postMessage(t={}){return this.workerClient.request(t)}createWorker(){this.workerClient=new z(()=>new D,t=>this.scheduler.fail(t)),this.worker=this.workerClient.worker}filter(t,e){return $(this,t,e)}setState(t,e){!function(t,e,i){t.states[e.$state]=t.states[e.$state].filter(t=>t!==e),e.$state=i,e.$ref&&(e.$ref.dataset.state=i),t.states[i].push(e)}(this,t,e)}makeWait(t){this.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=F.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.$refs.push(t.$ref),t.$ref=null)}update(){return this.scheduler.running=!0,this.scheduler.schedule(),this}resize(){const{clientWidth:t}=this.$player;this.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.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.$player;return this.filter("stop",e=>{switch(this.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.$player;return this.filter("emit",e=>{switch(this.setState(e,"stop"),e.mode){case 0:{const i=t-(this.getLeft(e.$ref)-this.getLeft(this.$player));e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition="transform 0s linear 0s";break}}}),this}stop(){return this.isStop=!0,this.scheduler.running=!1,this.scheduler.invalidate(),this.suspend(),this.art.emit("artplayerPluginDanmuku:stop"),this}start(){if(this.scheduler.closed||this.art.isDestroy)return this;const t=++this.scheduler.starts,e=this.scheduler.generation;this.isStop=!1,this.scheduler.recover();return this.scheduler.closed||this.art.isDestroy||this.isStop||this.scheduler.fault||e!==this.scheduler.generation||t!==this.scheduler.starts||(this.continue(),this.update(),this.art.emit("artplayerPluginDanmuku:start")),this}reset(){return this.scheduler.invalidate(),this.queue.forEach(t=>this.makeWait(t)),this.art.emit("artplayerPluginDanmuku:reset"),this.scheduler.recover(),this.scheduler.schedule(),this}seek(){this.scheduler.invalidate(),this.scheduler.schedule()}show(){return this.isHide=!1,this.$danmuku.style.opacity=1,this.option.visible=!0,this.art.emit("artplayerPluginDanmuku:show"),this}hide(){return this.isHide=!0,this.scheduler.invalidate(),this.$danmuku.style.opacity=0,this.option.visible=!1,this.art.emit("artplayerPluginDanmuku:hide"),this.scheduler.schedule(),this}destroy(){var t;x(this),this.scheduler.destroy();try{try{this.stop()}finally{null==(t=this.workerClient)||t.dispose()}}finally{this.art.off("video:play",this.start),this.art.off("video:playing",this.start),this.art.off("video:pause",this.stop),this.art.off("video:waiting",this.stop),this.art.off("resize",this.resize),this.art.off("video:seeking",this.seek),this.art.off("destroy",this.destroy),this.art.emit("artplayerPluginDanmuku:destroy")}}}const T={map:(t,e,i,n,a)=>(t-e)*(a-n)/(i-e)+n,range(t,e,i){const n=Math.round(t/i)*i;return Array.from({length:Math.floor((e-t)/i)},(t,e)=>e*i+n)}};function A(t,e,i){const{query:n}=t.constructor.utils;t.controls.add({name:"heatmap",position:"top",html:"",style:{position:"absolute",top:"-100px",left:"0px",right:"0px",height:"100px",width:"100%",pointerEvents:"none"},mounted(a){let s=null,r=null;function l(l=[]){if(s=null,r=null,a.innerHTML="",!t.duration||t.option.isLive)return;const d={w:a.offsetWidth,h:a.offsetHeight},p={xMin:0,xMax:d.w,yMin:0,yMax:128,scale:.25,opacity:.2,minHeight:Math.floor(.05*d.h),sampling:Math.floor(d.w/100),smoothing:.2,flattening:.2};"object"===typeof i&&Object.assign(p,i);let h=[];if(Array.isArray(l)&&l.length)h=[...l];else{const i=t.duration/d.w;for(let t=0;t<=d.w;t+=p.sampling){const n=e.queue.filter(({time:e})=>e>t*i&&e<=(t+p.sampling)*i).length;h.push([t,n])}}if(0===h.length)return;const u=h[h.length-1],c=u[0],m=u[1];c!==d.w&&h.push([d.w,m]);const f=h.map(t=>t[1]),g=(Math.min(...f)+Math.max(...f))/2;for(let t=0;tg?1+p.scale:1-p.scale)+p.minHeight}const y=(t,e,i,n)=>{const a=function(t,e){const i=e[0]-t[0],n=e[1]-t[1];return{length:Math.sqrt(o(i,2)+o(n,2)),angle:Math.atan2(n,i)}}(e||t,i||t),s=T.map(Math.cos(a.angle)*p.flattening,0,1,1,0),r=a.angle*s+(n?Math.PI:0),l=a.length*p.smoothing;return[t[0]+Math.cos(r)*l,t[1]+Math.sin(r)*l]},v=h.map(t=>[T.map(t[0],p.xMin,p.xMax,0,d.w),T.map(t[1],p.yMin,p.yMax,d.h,0)]).reduce((t,e,i,n)=>0===i?`M ${n[n.length-1][0]},${d.h} L ${e[0]},${d.h} L ${e[0]},${e[1]}`:`${t} ${((t,e,i)=>{const n=y(i[e-1],i[e-2],t),a=y(t,i[e-1],i[e+1],!0),s=e===i.length-1?" z":"";return`C ${n[0]},${n[1]} ${a[0]},${a[1]} ${t[0]},${t[1]}${s}`})(e,i,n)}`,"");a.innerHTML=`\n \n \n \n \n \n \n \n \n \n \n \n `,s=n("#heatmap-start",a),r=n("#heatmap-stop",a),s.setAttribute("offset",100*t.played+"%"),r.setAttribute("offset",100*t.played+"%")}t.on("video:timeupdate",()=>{s&&r&&(s.setAttribute("offset",100*t.played+"%"),r.setAttribute("offset",100*t.played+"%"))}),t.on("setBar",(t,e)=>{s&&r&&"played"===t&&(s.setAttribute("offset",100*e+"%"),r.setAttribute("offset",100*e+"%"))}),t.on("ready",()=>l()),t.on("resize",()=>l()),t.on("artplayerPluginDanmuku:loaded",()=>l()),t.on("artplayerPluginDanmuku:points",t=>l(t))}})}const P='',L='',O='',H='',V='',j='',R='',I='',q='',B='',Z='',N='';class W{constructor(t,e){this.art=t,this.danmuku=e,this.utils=t.constructor.utils;const{setStyle:i}=this.utils,{$controlsCenter:n}=t.template;i(n,"display","flex"),this.template={$controlsCenter:n,$mount:n,$danmuku:null,$toggle:null,$config:null,$configPanel:null,$configModes:null,$style:null,$stylePanel:null,$styleModes:null,$colors:null,$opacitySlider:null,$opacityValue:null,$marginSlider:null,$marginValue:null,$fontSizeSlider:null,$fontSizeValue:null,$speedSlider:null,$speedValue:null,$input:null,$send:null},this.slider={opacity:null,margin:null,fontSize:null,speed:null},this.emitting=!1,this.isLock=!1,this.timer=null,this.createTemplate(),this.createSliders(),this.createEvents(),this.mount(this.option.mount),t.on("resize",()=>this.resize()),t.on("fullscreen",t=>this.onFullscreen(t)),t.on("fullscreenWeb",t=>this.onFullscreen(t)),t.proxy(this.template.$config,"mouseenter",()=>{this.onMouseEnter({$control:this.template.$config,$panel:this.template.$configPanel})}),t.proxy(this.template.$style,"mouseenter",()=>{this.onMouseEnter({$control:this.template.$style,$panel:this.template.$stylePanel})})}static get icons(){return{$on:Z,$off:B,$config:O,$style:N,$mode_0_off:H,$mode_0_on:V,$mode_1_off:j,$mode_1_on:R,$mode_2_off:I,$mode_2_on:q,$check_on:L,$check_off:P}}get option(){return this.danmuku.option}get outside(){return this.template.$mount!==this.template.$controlsCenter}get TEMPLATE(){const{option:t}=this;return`\n

\n ${Z}${B}\n
\n
\n ${O}\n
\n
\n
\n \u6309\u7c7b\u578b\u5c4f\u853d\n
\n
\n ${H}${V}\n
\u6eda\u52a8
\n
\n
\n ${j}${R}\n
\u9876\u90e8
\n
\n
\n ${I}${q}\n
\u5e95\u90e8
\n
\n
\n
\n
\n
\n ${L}${P}\n \u9632\u6b62\u5f39\u5e55\u91cd\u53e0\n
\n
\n ${L}${P}\n \u540c\u6b65\u89c6\u9891\u901f\u5ea6\n
\n
\n
\n \u4e0d\u900f\u660e\u5ea6\n
\n
\u672a\u77e5
\n
\n
\n \u663e\u793a\u533a\u57df\n
\n
\u672a\u77e5
\n
\n
\n \u5f39\u5e55\u5b57\u53f7\n
\n
\u672a\u77e5
\n
\n
\n \u5f39\u5e55\u901f\u5ea6\n
\n
\u672a\u77e5
\n
\n
\n
\n
\n
\n
\n ${N}\n
\n
\n
\n \u6a21\u5f0f\n
\n
\n ${V}\n
\u6eda\u52a8
\n
\n
\n ${R}\n
\u9876\u90e8
\n
\n
\n ${q}\n
\u5e95\u90e8
\n
\n
\n
\n
\n \u989c\u8272\n
\n ${this.COLOR.map(t=>`
`).join("")}\n
\n
\n
\n
\n
\n \n
\u53d1\u9001
\n
\n `}get OPACITY(){return l({min:0,max:100,steps:[]},this.option.OPACITY)}get FONT_SIZE(){return l({min:12,max:120,steps:[]},this.option.FONT_SIZE)}get MARGIN(){return l({min:0,max:3,steps:[{name:"1/4",value:[10,"75%"]},{name:"\u534a\u5c4f",value:[10,"50%"]},{name:"3/4",value:[10,"25%"]},{name:"\u6ee1\u5c4f",value:[10,10]}]},this.option.MARGIN)}get SPEED(){return l({min:0,max:4,steps:[{name:"\u6781\u6162",value:10},{name:"\u8f83\u6162",value:7.5,hide:!0},{name:"\u9002\u4e2d",value:5},{name:"\u8f83\u5feb",value:2.5,hide:!0},{name:"\u6781\u5feb",value:1}]},this.option.SPEED)}get COLOR(){return this.option.COLOR.length?this.option.COLOR:["#FE0302","#FF7204","#FFAA02","#FFD302","#FFFF00","#A0EE00","#00CD00","#019899","#4266BE","#89D5FF","#CC0273","#222222","#9B9B9B","#FFFFFF"]}query(t){const{query:e}=this.utils,{$danmuku:i}=this.template;return e(t,i)}append(t,e){const{append:i}=this.utils;[...t.children].includes(e)||i(t,e)}setData(t,e){const{$player:i}=this.art.template,{$mount:n}=this.template;i.dataset[t]=e,this.outside&&(n.dataset[t]=e)}createTemplate(){const{createElement:t,tooltip:e}=this.utils,i=t("div");i.className="artplayer-plugin-danmuku",i.innerHTML=this.TEMPLATE,this.template.$danmuku=i,this.template.$toggle=this.query(".apd-toggle"),this.template.$config=this.query(".apd-config"),this.template.$configPanel=this.query(".apd-config-panel"),this.template.$configModes=this.query(".apd-config-mode .apd-modes"),this.template.$style=this.query(".apd-style"),this.template.$stylePanel=this.query(".apd-style-panel"),this.template.$styleModes=this.query(".apd-style-mode .apd-modes"),this.template.$colors=this.query(".apd-colors"),this.template.$antiOverlap=this.query(".apd-anti-overlap"),this.template.$syncVideo=this.query(".apd-sync-video"),this.template.$opacitySlider=this.query(".apd-config-opacity .apd-slider"),this.template.$opacityValue=this.query(".apd-config-opacity .apd-value"),this.template.$marginSlider=this.query(".apd-config-margin .apd-slider"),this.template.$marginValue=this.query(".apd-config-margin .apd-value"),this.template.$fontSizeSlider=this.query(".apd-config-fontSize .apd-slider"),this.template.$fontSizeValue=this.query(".apd-config-fontSize .apd-value"),this.template.$speedSlider=this.query(".apd-config-speed .apd-slider"),this.template.$speedValue=this.query(".apd-config-speed .apd-value"),this.template.$input=this.query(".apd-input"),this.template.$send=this.query(".apd-send");const{$toggle:n}=this.template;this.art.on("artplayerPluginDanmuku:show",()=>{e(n,"\u5173\u95ed\u5f39\u5e55")}),this.art.on("artplayerPluginDanmuku:hide",()=>{e(n,"\u6253\u5f00\u5f39\u5e55")})}createEvents(){const{$toggle:t,$configModes:e,$styleModes:i,$colors:n,$antiOverlap:a,$syncVideo:s,$send:o,$input:r}=this.template;this.art.proxy(t,"click",()=>{this.danmuku.config({visible:!this.option.visible}),this.reset()}),this.art.proxy(e,"click",t=>{const e=t.target.closest(".apd-mode");if(!e)return;const i=Number(e.dataset.mode);this.option.modes.includes(i)?this.danmuku.config({modes:this.option.modes.filter(t=>t!==i)}):this.danmuku.config({modes:[...this.option.modes,i]}),this.reset()}),this.art.proxy(a,"click",()=>{this.danmuku.config({antiOverlap:!this.option.antiOverlap}),this.reset()}),this.art.proxy(s,"click",()=>{this.danmuku.config({synchronousPlayback:!this.option.synchronousPlayback}),this.reset()}),this.art.proxy(i,"click",t=>{const e=t.target.closest(".apd-mode");if(!e)return;const i=Number(e.dataset.mode);this.danmuku.config({mode:i}),this.reset()}),this.art.proxy(n,"click",t=>{const e=t.target.closest(".apd-color");e&&(this.danmuku.config({color:e.dataset.color}),this.reset())}),this.art.proxy(o,"click",()=>this.emit()),this.art.proxy(r,"keypress",t=>{"Enter"===t.key&&(t.preventDefault(),this.emit())})}createSliders(){this.slider.opacity=this.createSlider(d(l({},this.OPACITY),{container:this.template.$opacitySlider,findIndex:()=>Math.round(100*this.option.opacity),onChange:t=>{const{$opacityValue:e}=this.template;e.textContent=`${t}%`,this.danmuku.config({opacity:t/100})}})),this.slider.margin=this.createSlider(d(l({},this.MARGIN),{container:this.template.$marginSlider,findIndex:()=>this.MARGIN.steps.findIndex(t=>t.value[0]===this.option.margin[0]&&t.value[1]===this.option.margin[1]),onChange:t=>{const e=this.MARGIN.steps[t];if(!e)return;const{$marginValue:i}=this.template;i.textContent=e.name,this.danmuku.config({margin:e.value})}})),this.slider.fontSize=this.createSlider(d(l({},this.FONT_SIZE),{container:this.template.$fontSizeSlider,findIndex:()=>this.danmuku.fontSize,onChange:t=>{const{$fontSizeValue:e}=this.template;e.textContent=`${t}px`,t!==this.danmuku.fontSize&&this.danmuku.config({fontSize:t})}})),this.slider.speed=this.createSlider(d(l({},this.SPEED),{container:this.template.$speedSlider,findIndex:()=>this.SPEED.steps.findIndex(t=>t.value===this.option.speed),onChange:t=>{const e=this.SPEED.steps[t];if(!e)return;const{$speedValue:i}=this.template;i.textContent=e.name,this.danmuku.config({speed:e.value})}}))}createSlider({min:t,max:e,container:i,findIndex:n,onChange:a,steps:s=[]}){const{query:o,clamp:r,setStyle:l}=this.utils;l(i,"touch-action","none"),i.innerHTML=`\n
\n
\n ${s.map(()=>'
').join("")}\n
\n
\n
\n
\n
\n ${s.map(t=>t.hide?"":`
${t.name}
`).join("")}\n
\n `;const d=o(".apd-slider-dot",i),p=o(".apd-slider-progress",i);let h=!1;function u(i=n()){if(ie)return;const o=(i-t)/(e-t);d.style.left=100*o+"%",0===s.length&&(p.style.width=d.style.left),a(i)}function c(n){const{top:a,height:s,left:o,width:l}=i.getBoundingClientRect();if(this.art.isRotate){const i=r(n.clientY-a,0,s);u(Math.round(i/s*(e-t)+t))}else{const i=r(n.clientX-o,0,l);u(Math.round(i/l*(e-t)+t))}}return this.art.proxy(i,"click",t=>{c.call(this,t)}),this.art.proxy(i,"pointerdown",t=>{h=0===t.button}),this.art.on("document:pointermove",t=>{h&&c.call(this,t)}),this.art.on("document:pointerup",t=>{h&&(h=!1,c.call(this,t))}),{reset:u}}onFullscreen(t){const{$danmuku:e,$controlsCenter:i,$mount:n}=this.template;this.outside?t?this.append(i,e):this.append(n,e):this.append(i,e)}onMouseEnter({$control:t,$panel:e}){const{$player:i}=this.art.template,n=t.getBoundingClientRect(),a=e.getBoundingClientRect(),s=i.getBoundingClientRect(),o=a.width/2-n.width/2,r=s.left-(n.left-o),l=n.right+o-s.right;e.style.left=r>0?`${-o+r}px`:l>0?-o-l+"px":-o+"px"}emit(){return p(this,null,function*(){const{$input:t}=this.template,e=t.value.trim();if(!e.length)return;if(this.isLock)return;if(this.emitting)return;const i={text:e,mode:this.option.mode,color:this.option.color,time:this.art.currentTime};try{this.emitting=!0;const e=yield this.option.beforeEmit(i);if(this.emitting=!1,!0!==e)return;i.border=!0,delete i.time,this.danmuku.emit(i),t.value="",this.lock()}catch(n){console.error("Error emitting danmuku:",n),this.emitting=!1}})}lock(){const{addClass:t}=this.utils,{$send:e}=this.template;this.isLock=!0;let i=this.option.lockTime;e.textContent=i,t(e,"apd-lock");const n=()=>{this.timer=setTimeout(()=>{0===i?this.unlock():(i-=1,e.textContent=i,n())},1e3)};n()}unlock(){const{removeClass:t}=this.utils,{$send:e}=this.template;clearTimeout(this.timer),this.isLock=!1,e.textContent="\u53d1\u9001",t(e,"apd-lock")}resize(){if(this.outside)return;if(this.art.fullscreen)return;if(this.art.fullscreenWeb)return;const{$player:t,$controlsCenter:e}=this.art.template,{$danmuku:i}=this.template;this.art.widtht.dataset.color===this.option.color.toUpperCase());s&&t(s,"apd-active"),e(i,this.option.visible?"\u5173\u95ed\u5f39\u5e55":"\u6253\u5f00\u5f39\u5e55"),this.resize()}mount(t){const{errorHandle:e}=this.utils,i="string"===typeof t?document.querySelector(t):t;e(i,`Can not find the mount point: ${t}`),this.append(i,this.template.$danmuku),this.template.$mount=i,this.reset()}}if("undefined"!==typeof document){const t="artplayer-plugin-danmuku";let e=document.getElementById(t);e||(e=document.createElement("style"),e.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.appendChild(e)}):(document.head||document.documentElement).appendChild(e)),e.textContent='.artplayer-plugin-danmuku{display:flex;position:relative;z-index:99;align-items:center;justify-content:center;font-size:12px;height:32px;width:100%;color:#fff;font-weight:300;flex-shrink:0;gap:10px}.artplayer-plugin-danmuku .apd-icon{cursor:pointer;opacity:.75;transition:all .2s ease;fill:#fff}.artplayer-plugin-danmuku .apd-icon:hover{opacity:1}.artplayer-plugin-danmuku .apd-config{display:flex;position:relative}.artplayer-plugin-danmuku .apd-config .apd-config-panel{position:absolute;bottom:24px;left:0;width:320px;padding:10px;opacity:0;pointer-events:none}.artplayer-plugin-danmuku .apd-config .apd-config-panel .apd-config-panel-inner{width:100%;border-radius:3px;background-color:#000000d9;padding:10px}.artplayer-plugin-danmuku .apd-config:hover .apd-config-panel{opacity:100;pointer-events:all}.artplayer-plugin-danmuku .apd-config-mode,.artplayer-plugin-danmuku .apd-config-slider,.artplayer-plugin-danmuku .apd-config-other,.artplayer-plugin-danmuku .apd-style-mode{margin-bottom:15px}.artplayer-plugin-danmuku .apd-modes{display:flex;align-items:center;margin-top:5px;gap:20px}.artplayer-plugin-danmuku .apd-modes .apd-mode{cursor:pointer;text-align:center}.artplayer-plugin-danmuku .apd-modes .apd-mode:hover{color:#00a1d6}.artplayer-plugin-danmuku .apd-config-slider{display:flex;align-items:center;gap:12px}.artplayer-plugin-danmuku .apd-config-slider .apd-value{width:32px;text-align:right}.artplayer-plugin-danmuku .apd-slider{position:relative;flex:1;display:flex;height:20px;align-items:center;justify-content:center;cursor:pointer}.artplayer-plugin-danmuku .apd-slider .apd-slider-line{position:relative;height:2px;width:100%;overflow:hidden;border-radius:3px;background-color:#ffffff40}.artplayer-plugin-danmuku .apd-slider .apd-slider-points{position:absolute;inset:0;display:flex;align-items:center;justify-content:space-between}.artplayer-plugin-danmuku .apd-slider .apd-slider-points .apd-slider-point{width:2px;height:2px;border-radius:50%;background-color:#ffffff80}.artplayer-plugin-danmuku .apd-slider .apd-slider-progress{width:0%;height:100%;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-slider .apd-slider-dot{position:absolute;transform:translate(-6px);left:0%;width:12px;height:12px;border-radius:50%;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-slider .apd-slider-steps{display:flex;align-items:center;justify-content:space-between;position:absolute;bottom:-12px;width:calc(100% + 32px);color:#777}.artplayer-plugin-danmuku .apd-slider .apd-slider-steps .apd-slider-step{flex-shrink:0;width:36px;text-align:center;scale:.95}.artplayer-plugin-danmuku .apd-config-other{display:flex;align-items:center;gap:20px}.artplayer-plugin-danmuku .apd-config-other .apd-check-off,.artplayer-plugin-danmuku .apd-config-other .apd-check-on{width:16px;height:16px}.artplayer-plugin-danmuku .apd-config-other .apd-other{display:flex;align-items:center;cursor:pointer;gap:2px}.artplayer-plugin-danmuku .apd-config-other .apd-other:hover{color:#00a1d6}.artplayer-plugin-danmuku .apd-emitter{display:flex;flex:1;align-items:center;height:100%;background-color:#ffffff40;border-radius:5px}.artplayer-plugin-danmuku .apd-style{position:relative;display:flex;align-items:center;justify-content:center}.artplayer-plugin-danmuku .apd-style .apd-style-panel{position:absolute;bottom:24px;left:0;width:200px;padding:10px;opacity:0;pointer-events:none}.artplayer-plugin-danmuku .apd-style .apd-style-panel .apd-style-panel-inner{width:100%;border-radius:3px;background-color:#000000d9;padding:10px}.artplayer-plugin-danmuku .apd-style:hover .apd-style-panel{opacity:100;pointer-events:all}.artplayer-plugin-danmuku .apd-colors{display:flex;flex-wrap:wrap;margin-top:5px;gap:8px}.artplayer-plugin-danmuku .apd-colors .apd-color{width:16px;height:16px;border-radius:2px;cursor:pointer}.artplayer-plugin-danmuku .apd-colors .apd-color.apd-active{border:1px solid black;box-shadow:0 0 0 1px #fff}.artplayer-plugin-danmuku .apd-input{outline:none;height:100%;flex:1;min-width:0;width:auto;border:none;line-height:1;color:#fff;background-color:transparent}.artplayer-plugin-danmuku .apd-input::placeholder{color:#ffffff80}.artplayer-plugin-danmuku .apd-send{display:flex;align-items:center;justify-content:center;height:100%;width:60px;flex-shrink:0;cursor:pointer;text-shadow:none;border-top-right-radius:5px;border-bottom-right-radius:5px;background-color:#00a1d6}.artplayer-plugin-danmuku .apd-send.apd-lock{cursor:not-allowed;color:#666;background-color:#e7e7e7}.art-controls-center .apd-emitter{width:260px;flex:none}.art-fullscreen .artplayer-plugin-danmuku,.art-fullscreen-web .artplayer-plugin-danmuku{height:38px;gap:16px}.art-fullscreen .artplayer-plugin-danmuku .apd-config-icon,.art-fullscreen-web .artplayer-plugin-danmuku .apd-config-icon,.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-off,.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-off,.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-on,.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-on{width:28px;height:28px}.art-fullscreen .artplayer-plugin-danmuku .apd-emitter,.art-fullscreen-web .artplayer-plugin-danmuku .apd-emitter{width:400px;flex:none}.art-video-player>.artplayer-plugin-danmuku{position:absolute;left:0;right:0;bottom:-40px;padding:0 10px}.art-video-player:has(>.artplayer-plugin-danmuku){margin-bottom:40px}[data-danmuku-emitter=false] .apd-emitter{display:none!important}[data-danmuku-emitter=false] .art-controls-center .artplayer-plugin-danmuku{justify-content:flex-end;gap:18px}[data-danmuku-emitter=false].art-fullscreen .art-controls-center .artplayer-plugin-danmuku,[data-danmuku-emitter=false].art-fullscreen-web .art-controls-center .artplayer-plugin-danmuku{gap:24px}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-icon{fill:#333}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-emitter{background-color:#f1f2f3}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-input{color:#000}[data-danmuku-theme=light]>.artplayer-plugin-danmuku .apd-input::placeholder{color:#0000004d}[data-danmuku-visible=false] .apd-toggle-off{display:block}[data-danmuku-visible=false] .apd-toggle-on,[data-danmuku-visible=true] .apd-toggle-off{display:none}[data-danmuku-visible=true] .apd-toggle-on{display:block}[data-danmuku-anti-overlap=false] .apd-anti-overlap .apd-check-on{display:none}[data-danmuku-anti-overlap=false] .apd-anti-overlap .apd-check-off,[data-danmuku-anti-overlap=true] .apd-anti-overlap .apd-check-on{display:block}[data-danmuku-anti-overlap=true] .apd-anti-overlap .apd-check-off,[data-danmuku-sync-video=false] .apd-sync-video .apd-check-on{display:none}[data-danmuku-sync-video=false] .apd-sync-video .apd-check-off,[data-danmuku-sync-video=true] .apd-sync-video .apd-check-on{display:block}[data-danmuku-sync-video=true] .apd-sync-video .apd-check-off{display:none}[data-danmuku-mode0=false] .apd-config-mode .apd-mode-0-off{display:block}[data-danmuku-mode0=false] .apd-config-mode .apd-mode-0-on{display:none}[data-danmuku-mode0=false] .art-danmuku [data-mode="0"]{opacity:0!important}[data-danmuku-mode0=true] .apd-config-mode .apd-mode-0-off{display:none}[data-danmuku-mode0=true] .apd-config-mode .apd-mode-0-on{display:block}[data-danmuku-mode="0"] .apd-style-mode [data-mode="0"]{color:#00a1d6}[data-danmuku-mode="0"] .apd-style-mode [data-mode="0"] path{fill:#00a1d6}[data-danmuku-mode1=false] .apd-config-mode .apd-mode-1-off{display:block}[data-danmuku-mode1=false] .apd-config-mode .apd-mode-1-on{display:none}[data-danmuku-mode1=false] .art-danmuku [data-mode="1"]{opacity:0!important}[data-danmuku-mode1=true] .apd-config-mode .apd-mode-1-off{display:none}[data-danmuku-mode1=true] .apd-config-mode .apd-mode-1-on{display:block}[data-danmuku-mode="1"] .apd-style-mode [data-mode="1"]{color:#00a1d6}[data-danmuku-mode="1"] .apd-style-mode [data-mode="1"] path{fill:#00a1d6}[data-danmuku-mode2=false] .apd-config-mode .apd-mode-2-off{display:block}[data-danmuku-mode2=false] .apd-config-mode .apd-mode-2-on{display:none}[data-danmuku-mode2=false] .art-danmuku [data-mode="2"]{opacity:0!important}[data-danmuku-mode2=true] .apd-config-mode .apd-mode-2-off{display:none}[data-danmuku-mode2=true] .apd-config-mode .apd-mode-2-on{display:block}[data-danmuku-mode="2"] .apd-style-mode [data-mode="2"]{color:#00a1d6}[data-danmuku-mode="2"] .apd-style-mode [data-mode="2"] path{fill:#00a1d6}'}function _(t){return e=>{const i=new F(e,t),n=e.isDestroy?void 0:new W(e,i);return!e.isDestroy&&i.option.heatmap&&A(e,i,i.option.heatmap),{name:"artplayerPluginDanmuku",emit:i.emit.bind(i),load:i.load.bind(i),config:i.config.bind(i),hide:i.hide.bind(i),show:i.show.bind(i),reset:i.reset.bind(i),mount:n?n.mount.bind(n):()=>{},get option(){return i.option},get isHide(){return i.isHide},get isStop(){return i.isStop}}}}return _.icons=W.icons,_}); diff --git a/packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.mjs b/packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.mjs index 97dbcd0db..b6e8613cc 100644 --- a/packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.mjs +++ b/packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.mjs @@ -133,7 +133,7 @@ function onmessage({ data }) { const danmus = bilibiliDanmuParseFromXml(xml); globalThis.postMessage({ danmus, id }); } -let nextRequest = 0; +let nextRequest$1 = 0; function abortError(signal) { if (signal?.reason !== void 0) return signal.reason; @@ -147,7 +147,7 @@ function bilibiliDanmuParseFromUrl(url, { signal, onCancel } = {}) { let worker; let workerUrl; let releaseCancel; - const id = ++nextRequest; + const id = ++nextRequest$1; const cancel = () => finish(void 0, abortError(signal)); function releaseWorker() { let error; @@ -264,7 +264,7 @@ function bilibiliDanmuParseFromUrl(url, { signal, onCancel } = {}) { }); } const inputs = /* @__PURE__ */ new WeakMap(); -const cancelled = /* @__PURE__ */ Symbol("cancelled danmuku input"); +const cancelled$1 = /* @__PURE__ */ Symbol("cancelled danmuku input"); function stateFor(owner) { if (!inputs.has(owner)) inputs.set(owner, { closed: false, tasks: /* @__PURE__ */ new Set(), emitting: [] }); @@ -305,7 +305,7 @@ function beginInput(owner, replace) { if (stopped) return; stopped = true; - resolveCancel(cancelled); + resolveCancel(cancelled$1); controller?.abort(); for (const callback of [...handlers]) callback(); handlers.clear(); @@ -325,7 +325,7 @@ function beginInput(owner, replace) { } }; if (stopped) - resolveCancel(cancelled); + resolveCancel(cancelled$1); else state.tasks.add(task); return task; } @@ -343,6 +343,301 @@ function readInput(target, task) { return { asynchronous: true, value: bilibiliDanmuParseFromUrl(target, task) }; return { asynchronous: false, value: Array.isArray(target) ? target : [] }; } +function filterState(owner, state, callback) { + const danmus = owner.states[state] || []; + for (let index = 0; index < danmus.length; index++) callback(danmus[index]); + return danmus; +} +function readyItems(owner) { + const { currentTime } = owner.art; + const result = []; + filterState(owner, "ready", (danmu) => result.push(danmu)); + filterState(owner, "wait", (danmu) => { + if (currentTime + 0.1 >= danmu.time && danmu.time >= currentTime - 0.1) + result.push(danmu); + }); + return result; +} +function setItemState(owner, danmu, state) { + owner.states[danmu.$state] = owner.states[danmu.$state].filter((item) => item !== danmu); + danmu.$state = state; + if (danmu.$ref) + danmu.$ref.dataset.state = state; + owner.states[state].push(danmu); +} +const cancelled = /* @__PURE__ */ Symbol("cancelled danmuku frame"); +class Scheduler { + constructor(owner) { + this.owner = owner; + this.generation = 0; + this.starts = 0; + this.frame = null; + this.operation = null; + this.closed = false; + this.running = false; + this.fault = false; + this.failedItems = /* @__PURE__ */ new Set(); + } + active(operation) { + const owner = this.owner; + return !this.closed && !this.fault && !owner.art.isDestroy && !owner.isStop && !owner.isHide && operation.generation === this.generation; + } + release(operation) { + const { danmu, ref } = operation; + if (ref && danmu.$ref === ref) { + ref.style.cssText = this.owner.constructor.cssText; + ref.style.visibility = "hidden"; + this.owner.$refs.push(ref); + danmu.$ref = null; + } + operation.ref = null; + } + invalidate() { + this.generation++; + if (this.frame !== null) + window.cancelAnimationFrame(this.frame); + this.frame = null; + this.owner.timer = null; + if (this.operation) { + const operation = this.operation; + this.operation = null; + operation.cancel(); + this.release(operation); + } + this.owner.workerClient?.cancel(); + this.failedItems.clear(); + } + report(error) { + try { + this.owner.art.emit("artplayerPluginDanmuku:error", error); + } catch (listenerError) { + console.warn("Failed to report danmuku scheduling error:", listenerError); + } + } + fail(error) { + if (this.closed || this.fault) + return; + this.fault = true; + this.invalidate(); + this.report(error); + } + recover() { + if (this.closed || this.owner.art.isDestroy) + return; + this.failedItems.clear(); + if (this.fault) { + try { + this.owner.workerClient?.dispose(); + this.owner.createWorker(); + this.fault = false; + } catch (error) { + this.report(error); + } + } + } + schedule() { + const owner = this.owner; + if (!this.running || this.closed || this.fault || owner.art.isDestroy || owner.isStop || this.frame !== null || this.operation) + return; + this.frame = window.requestAnimationFrame(() => { + this.frame = null; + owner.timer = null; + if (this.closed || this.fault || owner.isStop || owner.art.isDestroy) + return; + let cancel; + const cancellation = new Promise((resolve) => cancel = () => resolve(cancelled)); + const operation = { generation: this.generation, cancel, wait: (value) => Promise.race([value, cancellation]), ref: null }; + this.operation = operation; + return this.run(operation).catch((error) => { + if (this.active(operation)) + this.fail(error); + }).finally(() => { + this.release(operation); + if (this.operation === operation) { + this.operation = null; + this.schedule(); + } + }); + }); + owner.timer = this.frame; + } + async run(operation) { + const owner = this.owner; + const { setStyles } = owner.utils; + if (!owner.art.playing || !this.active(operation)) + return; + owner.filter("emit", (danmu) => { + const emitTime = (Date.now() - danmu.$lastStartTime) / 1e3; + danmu.$restTime -= emitTime; + danmu.$lastStartTime = Date.now(); + if (danmu.$restTime <= 0) + owner.makeWait(danmu); + }); + const readys = owner.readys; + for (const danmu of readys) { + if (!this.active(operation)) + return; + if (this.failedItems.has(danmu)) + continue; + let state; + try { + state = await operation.wait(owner.option.beforeVisible(danmu)); + } catch (error) { + if (!this.active(operation)) + return; + this.failedItems.add(danmu); + this.report(error); + continue; + } + if (!this.active(operation)) + return; + if (!state) + continue; + const { clientWidth, clientHeight } = owner.$player; + const ref = owner.$ref; + operation.danmu = danmu; + operation.ref = ref; + danmu.$ref = ref; + ref.textContent = danmu.text; + owner.$danmuku.appendChild(ref); + ref.style.opacity = owner.option.opacity; + ref.style.fontSize = `${owner.fontSize}px`; + ref.style.color = danmu.color; + ref.style.border = danmu.border ? `1px solid ${danmu.color}` : null; + ref.style.backgroundColor = danmu.border ? "rgb(0 0 0 / 50%)" : null; + setStyles(ref, danmu.style); + if (!this.active(operation)) + return; + danmu.$lastStartTime = Date.now(); + danmu.$restTime = owner.speed; + const distance = clientWidth + ref.clientWidth; + const { result: top } = await operation.wait(owner.postMessage({ + type: "getDanmuTop", + target: { mode: danmu.mode, height: ref.clientHeight, speed: distance / danmu.$restTime }, + visibles: owner.visibles, + antiOverlap: owner.option.antiOverlap, + clientWidth, + clientHeight, + marginBottom: owner.marginBottom, + marginTop: owner.marginTop + })); + if (!this.active(operation) || danmu.$ref !== ref) + return; + if (top !== void 0) { + owner.setState(danmu, "emit"); + ref.style.top = `${top}px`; + ref.style.visibility = "visible"; + ref.dataset.mode = danmu.mode; + ref.dataset.id = danmu.id || ""; + switch (danmu.mode) { + case 0: + ref.style.left = `${clientWidth}px`; + ref.style.marginLeft = "0px"; + ref.style.transform = `translateX(${-distance}px)`; + ref.style.transition = `transform ${danmu.$restTime}s linear 0s`; + break; + case 1: + case 2: + ref.style.left = "50%"; + ref.style.marginLeft = `-${ref.clientWidth / 2}px`; + break; + } + operation.ref = null; + owner.art.emit("artplayerPluginDanmuku:visible", danmu); + } else { + owner.setState(danmu, "ready"); + this.release(operation); + } + } + } + destroy() { + this.closed = true; + this.invalidate(); + } +} +let nextRequest = 0; +class WorkerClient { + constructor(createWorker, onFailure) { + this.pending = /* @__PURE__ */ new Map(); + this.closed = false; + this.failed = false; + this.onFailure = onFailure; + this.worker = createWorker(); + try { + this.worker.onmessage = (event) => { + const request = this.pending.get(event.data?.id); + if (!request) + return; + this.pending.delete(event.data.id); + request.resolve(event.data); + }; + this.worker.onerror = (event) => { + event.preventDefault?.(); + this.fail(event.error || event); + }; + this.worker.onmessageerror = (event) => this.fail(event.error || event); + } catch (error) { + try { + this.dispose(); + } catch { + } + throw error; + } + } + request(message) { + message.id = ++nextRequest; + const { id } = message; + if (this.closed || this.failed) + return Promise.resolve({ id, result: void 0 }); + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject }); + try { + this.worker.postMessage(message); + } catch (error) { + this.fail(error); + } + }); + } + fail(error) { + if (this.closed || this.failed) + return; + this.failed = true; + for (const request of this.pending.values()) request.reject(error); + this.pending.clear(); + try { + this.dispose(); + } catch { + } + this.onFailure(error); + } + cancel() { + for (const [id, request] of this.pending) request.resolve({ id, result: void 0 }); + this.pending.clear(); + } + dispose() { + if (this.closed) + return; + this.closed = true; + this.cancel(); + let failed = false; + let error; + const attempt = (callback) => { + try { + callback(); + } catch (failure) { + if (!failed) + error = failure; + failed = true; + } + }; + for (const name of ["onmessage", "onerror", "onmessageerror"]) { + attempt(() => this.worker[name] = null); + } + attempt(() => this.worker.terminate()); + if (failed) + throw error; + } +} const jsContent = '/*!\n * artplayer-plugin-danmuku.js v5.3.0\n * Github: https://github.com/zhw2590582/ArtPlayer\n * (c) 2017-2026 Harvey Zhao\n * Released under the MIT License.\n */\nfunction getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom, marginTop, antiOverlap }) {\n const maxTop = clientHeight - marginBottom;\n const danmus = visibles.filter((item) => item.mode === target.mode && item.top <= maxTop).sort((prev, next) => prev.top - next.top);\n if (danmus.length === 0) {\n if (target.mode === 2) {\n return maxTop - target.height;\n } else {\n return marginTop;\n }\n }\n danmus.unshift({\n type: "top",\n top: 0,\n left: 0,\n right: 0,\n height: marginTop,\n width: clientWidth,\n speed: 0,\n distance: clientWidth\n });\n danmus.push({\n type: "bottom",\n top: maxTop,\n left: 0,\n right: 0,\n height: marginBottom,\n width: clientWidth,\n speed: 0,\n distance: clientWidth\n });\n if (target.mode === 2) {\n for (let index = danmus.length - 2; index >= 0; index -= 1) {\n const item = danmus[index];\n const prev = danmus[index + 1];\n const itemBottom = item.top + item.height;\n const diff = prev.top - itemBottom;\n if (diff >= target.height) {\n return prev.top - target.height;\n }\n }\n } else {\n for (let index = 1; index < danmus.length; index += 1) {\n const item = danmus[index];\n const prev = danmus[index - 1];\n const prevBottom = prev.top + prev.height;\n const diff = item.top - prevBottom;\n if (diff >= target.height) {\n return prevBottom;\n }\n }\n }\n const topMap = [];\n for (let index = 1; index < danmus.length - 1; index += 1) {\n const item = danmus[index];\n if (topMap.length) {\n const last = topMap[topMap.length - 1];\n if (last[0].top === item.top) {\n last.push(item);\n } else {\n topMap.push([item]);\n }\n } else {\n topMap.push([item]);\n }\n }\n if (antiOverlap) {\n switch (target.mode) {\n case 0: {\n const result = topMap.find((list) => {\n return list.every((danmu) => {\n if (clientWidth < danmu.distance)\n return false;\n if (target.speed < danmu.speed)\n return true;\n const overlapTime = danmu.right / (target.speed - danmu.speed);\n if (overlapTime > danmu.time)\n return true;\n return false;\n });\n });\n return result && result[0] ? result[0].top : void 0;\n }\n // 静止弹幕没有重叠问题\n case 1:\n case 2:\n return void 0;\n }\n } else {\n switch (target.mode) {\n case 0:\n topMap.sort((prev, next) => {\n const nextMinRight = Math.min(...next.map((item) => item.right));\n const prevMinRight = Math.min(...prev.map((item) => item.right));\n return nextMinRight * next.length - prevMinRight * prev.length;\n });\n break;\n case 1:\n case 2:\n topMap.sort((prev, next) => {\n const nextMaxWidth = Math.max(...next.map((item) => item.width));\n const prevMaxWidth = Math.max(...prev.map((item) => item.width));\n return prevMaxWidth * prev.length - nextMaxWidth * next.length;\n });\n break;\n }\n return topMap[0][0].top;\n }\n}\nonmessage = (event) => {\n const { data } = event;\n if (!data.id || !data.type)\n return;\n const fns = { getDanmuTop };\n const fn = fns[data.type];\n const result = fn(data);\n globalThis.postMessage({\n result,\n id: data.id\n });\n};\n'; const blob = typeof self !== "undefined" && self.Blob && new Blob(["URL.revokeObjectURL(import.meta.url);", jsContent], { type: "text/javascript;charset=utf-8" }); function WorkerWrapper(options) { @@ -383,6 +678,8 @@ class Danmuku { this.timer = null; this.index = 0; this.worker = null; + this.workerClient = null; + this.scheduler = new Scheduler(this); this.option = Danmuku.option; this.states = { wait: [], ready: [], emit: [], stop: [] }; this.start = this.start.bind(this); @@ -390,16 +687,18 @@ class Danmuku { this.reset = this.reset.bind(this); this.resize = this.resize.bind(this); this.destroy = this.destroy.bind(this); + this.seek = this.seek.bind(this); inputActive(this); art.on("destroy", this.destroy); try { this.config(option, true); if (!inputActive(this)) return; - this.worker = new WorkerWrapper(); + this.createWorker(); } catch (error) { art.off("destroy", this.destroy); cancelInputs(this); + this.scheduler.destroy(); throw error; } art.on("video:play", this.start); @@ -407,6 +706,7 @@ class Danmuku { art.on("video:pause", this.stop); art.on("video:waiting", this.stop); art.on("resize", this.resize); + art.on("video:seeking", this.seek); this.load().catch((error) => console.warn("Failed to load initial danmuku:", error)); } // 默认配置 @@ -491,15 +791,7 @@ class Danmuku { } // 获取准备好发送的弹幕 get readys() { - const { currentTime } = this.art; - const result = []; - this.filter("ready", (danmu) => result.push(danmu)); - this.filter("wait", (danmu) => { - if (currentTime + 0.1 >= danmu.time && danmu.time >= currentTime - 0.1) { - result.push(danmu); - } - }); - return result; + return readyItems(this); } // 可见的弹幕的数据,用于计算下一个弹幕的top值 get visibles() { @@ -595,7 +887,7 @@ class Danmuku { }); if (!danmu.text.trim()) return this; - if (danmu.time !== void 0) { + if (danmu.time || danmu.time === 0) { danmu.time = clamp(danmu.time, 0, Infinity); } else { danmu.time = this.art.currentTime + 0.5; @@ -647,7 +939,12 @@ class Danmuku { }); if (!inputActive(this)) return this; + const beforeVisibleChanged = this.option.beforeVisible !== next.beforeVisible; this.option = next; + if (beforeVisibleChanged) { + this.scheduler.invalidate(); + this.scheduler.schedule(); + } if (option.fontSize) { this.reset(); if (!inputActive(this)) @@ -669,33 +966,19 @@ class Danmuku { } // 复杂运算交给 Web Worker 处理 postMessage(message = {}) { - return new Promise((resolve) => { - message.id = Date.now(); - this.worker.postMessage(message); - this.worker.onmessage = (event) => { - const { data } = event; - if (data.id === message.id) { - resolve(data); - } - }; - }); + return this.workerClient.request(message); + } + createWorker() { + this.workerClient = new WorkerClient(() => new WorkerWrapper(), (error) => this.scheduler.fail(error)); + this.worker = this.workerClient.worker; } // 根据状态获取弹幕 filter(state, callback) { - const danmus = this.states[state] || []; - for (let index = 0; index < danmus.length; index++) { - callback(danmus[index]); - } - return danmus; + return filterState(this, state, callback); } // 设置弹幕状态 setState(danmu, state) { - this.states[danmu.$state] = this.states[danmu.$state].filter((item) => item !== danmu); - danmu.$state = state; - if (danmu.$ref) { - danmu.$ref.dataset.state = state; - } - this.states[state].push(danmu); + setItemState(this, danmu, state); } // 重置弹幕到wait状态,回收弹幕DOM节点 makeWait(danmu) { @@ -712,88 +995,8 @@ class Danmuku { } // 实时更新弹幕 update() { - const { setStyles } = this.utils; - this.timer = window.requestAnimationFrame(async () => { - if (this.art.playing && !this.isHide) { - this.filter("emit", (danmu) => { - const emitTime = (Date.now() - danmu.$lastStartTime) / 1e3; - danmu.$restTime -= emitTime; - danmu.$lastStartTime = Date.now(); - if (danmu.$restTime <= 0) { - this.makeWait(danmu); - } - }); - const readys = this.readys; - for (let index = 0; index < readys.length; index++) { - const danmu = readys[index]; - const state = await this.option.beforeVisible(danmu); - if (state) { - const { clientWidth, clientHeight } = this.$player; - danmu.$ref = this.$ref; - danmu.$ref.textContent = danmu.text; - this.$danmuku.appendChild(danmu.$ref); - danmu.$ref.style.opacity = this.option.opacity; - danmu.$ref.style.fontSize = `${this.fontSize}px`; - danmu.$ref.style.color = danmu.color; - danmu.$ref.style.border = danmu.border ? `1px solid ${danmu.color}` : null; - danmu.$ref.style.backgroundColor = danmu.border ? "rgb(0 0 0 / 50%)" : null; - setStyles(danmu.$ref, danmu.style); - danmu.$lastStartTime = Date.now(); - danmu.$restTime = this.speed; - const distance = clientWidth + danmu.$ref.clientWidth; - const { result: top } = await this.postMessage({ - type: "getDanmuTop", - target: { - mode: danmu.mode, - height: danmu.$ref.clientHeight, - speed: distance / danmu.$restTime - }, - // 当前弹幕信息 - visibles: this.visibles, - // 可见的弹幕的数据 - antiOverlap: this.option.antiOverlap, - clientWidth, - clientHeight, - marginBottom: this.marginBottom, - marginTop: this.marginTop - }); - if (danmu.$ref) { - if (!this.isStop && top !== void 0) { - this.setState(danmu, "emit"); - danmu.$ref.style.top = `${top}px`; - danmu.$ref.style.visibility = "visible"; - danmu.$ref.dataset.mode = danmu.mode; - danmu.$ref.dataset.id = danmu.id || ""; - switch (danmu.mode) { - // 滚动的弹幕 - case 0: { - danmu.$ref.style.left = `${clientWidth}px`; - danmu.$ref.style.marginLeft = "0px"; - danmu.$ref.style.transform = `translateX(${-distance}px)`; - danmu.$ref.style.transition = `transform ${danmu.$restTime}s linear 0s`; - break; - } - case 1: - // falls through - case 2: - danmu.$ref.style.left = "50%"; - danmu.$ref.style.marginLeft = `-${danmu.$ref.clientWidth / 2}px`; - break; - } - this.art.emit("artplayerPluginDanmuku:visible", danmu); - } else { - this.setState(danmu, "ready"); - this.$refs.push(danmu.$ref); - danmu.$ref = null; - } - } - } - } - } - if (!this.isStop) { - this.update(); - } - }); + this.scheduler.running = true; + this.scheduler.schedule(); return this; } // 重置正在显示的弹幕: stop/emit 状态的弹幕 @@ -858,23 +1061,39 @@ class Danmuku { } stop() { this.isStop = true; + this.scheduler.running = false; + this.scheduler.invalidate(); this.suspend(); - window.cancelAnimationFrame(this.timer); this.art.emit("artplayerPluginDanmuku:stop"); return this; } start() { + if (this.scheduler.closed || this.art.isDestroy) + return this; + const start = ++this.scheduler.starts; + const generation = this.scheduler.generation; this.isStop = false; + this.scheduler.recover(); + const obsolete = this.scheduler.closed || this.art.isDestroy || this.isStop || this.scheduler.fault || generation !== this.scheduler.generation || start !== this.scheduler.starts; + if (obsolete) + return this; this.continue(); this.update(); this.art.emit("artplayerPluginDanmuku:start"); return this; } reset() { + this.scheduler.invalidate(); this.queue.forEach((danmu) => this.makeWait(danmu)); this.art.emit("artplayerPluginDanmuku:reset"); + this.scheduler.recover(); + this.scheduler.schedule(); return this; } + seek() { + this.scheduler.invalidate(); + this.scheduler.schedule(); + } show() { this.isHide = false; this.$danmuku.style.opacity = 1; @@ -884,22 +1103,32 @@ class Danmuku { } hide() { this.isHide = true; + this.scheduler.invalidate(); this.$danmuku.style.opacity = 0; this.option.visible = false; this.art.emit("artplayerPluginDanmuku:hide"); + this.scheduler.schedule(); return this; } destroy() { cancelInputs(this); - this.stop(); - this.worker?.terminate(); - this.art.off("video:play", this.start); - this.art.off("video:playing", this.start); - this.art.off("video:pause", this.stop); - this.art.off("video:waiting", this.stop); - this.art.off("resize", this.resize); - this.art.off("destroy", this.destroy); - this.art.emit("artplayerPluginDanmuku:destroy"); + this.scheduler.destroy(); + try { + try { + this.stop(); + } finally { + this.workerClient?.dispose(); + } + } finally { + this.art.off("video:play", this.start); + this.art.off("video:playing", this.start); + this.art.off("video:pause", this.stop); + this.art.off("video:waiting", this.stop); + this.art.off("resize", this.resize); + this.art.off("video:seeking", this.seek); + this.art.off("destroy", this.destroy); + this.art.emit("artplayerPluginDanmuku:destroy"); + } } } const lib = { diff --git a/packages/artplayer-plugin-danmuku/src/danmuku.js b/packages/artplayer-plugin-danmuku/src/danmuku.js index 82f6de465..25a721079 100644 --- a/packages/artplayer-plugin-danmuku/src/danmuku.js +++ b/packages/artplayer-plugin-danmuku/src/danmuku.js @@ -1,5 +1,8 @@ import { defaultOption, normalizeOption, optionChanged, optionScheme } from './config' import { beginInput, cancelInputs, inputActive, readInput } from './input' +import { filterState, readyItems, setItemState } from './queue' +import Scheduler from './scheduler' +import WorkerClient from './worker-client' import DanmuWorker from './worker.js?worker&inline' export default class Danmuku { @@ -19,6 +22,8 @@ export default class Danmuku { this.timer = null // 定时器 this.index = 0 // 弹幕索引 this.worker = null + this.workerClient = null + this.scheduler = new Scheduler(this) // 格式化后的配置项 this.option = Danmuku.option @@ -32,6 +37,7 @@ export default class Danmuku { this.reset = this.reset.bind(this) this.resize = this.resize.bind(this) this.destroy = this.destroy.bind(this) + this.seek = this.seek.bind(this) // 配置事件可同步销毁播放器,先建立输入和销毁边界。 inputActive(this) @@ -42,11 +48,12 @@ export default class Danmuku { return // 创建 Web Worker, 用于计算弹幕的 top 值 - this.worker = new DanmuWorker() + this.createWorker() } catch (error) { art.off('destroy', this.destroy) cancelInputs(this) + this.scheduler.destroy() throw error } // 监听事件 @@ -55,6 +62,7 @@ export default class Danmuku { art.on('video:pause', this.stop) art.on('video:waiting', this.stop) art.on('resize', this.resize) + art.on('video:seeking', this.seek) // 开始加载弹幕 this.load().catch(error => console.warn('Failed to load initial danmuku:', error)) @@ -160,21 +168,7 @@ export default class Danmuku { // 获取准备好发送的弹幕 get readys() { - const { currentTime } = this.art - - const result = [] - - // 有的是ready状态:之前因为弹幕太多而暂停发送的弹幕 - this.filter('ready', danmu => result.push(danmu)) - - // 有的是wait状态:符合时间范围的弹幕 - this.filter('wait', (danmu) => { - if (currentTime + 0.1 >= danmu.time && danmu.time >= currentTime - 0.1) { - result.push(danmu) - } - }) - - return result + return readyItems(this) } // 可见的弹幕的数据,用于计算下一个弹幕的top值 @@ -289,7 +283,7 @@ export default class Danmuku { return this // 设置弹幕时间,如果没有则默认为当前时间加 0.5 秒 - if (danmu.time !== undefined) { + if (danmu.time || danmu.time === 0) { danmu.time = clamp(danmu.time, 0, Infinity) } else { @@ -367,7 +361,12 @@ export default class Danmuku { }) if (!inputActive(this)) return this + const beforeVisibleChanged = this.option.beforeVisible !== next.beforeVisible this.option = next + if (beforeVisibleChanged) { + this.scheduler.invalidate() + this.scheduler.schedule() + } // 动态配置有字体大小,需要重新渲染 if (option.fontSize) { @@ -398,43 +397,22 @@ export default class Danmuku { // 复杂运算交给 Web Worker 处理 postMessage(message = {}) { - return new Promise((resolve) => { - message.id = Date.now() // 生成唯一标识 - this.worker.postMessage(message) - this.worker.onmessage = (event) => { - const { data } = event - // 判断是否是当前的消息 - if (data.id === message.id) { - resolve(data) - } - } - }) + return this.workerClient.request(message) + } + + createWorker() { + this.workerClient = new WorkerClient(() => new DanmuWorker(), error => this.scheduler.fail(error)) + this.worker = this.workerClient.worker } // 根据状态获取弹幕 filter(state, callback) { - const danmus = this.states[state] || [] - for (let index = 0; index < danmus.length; index++) { - callback(danmus[index]) - } - return danmus + return filterState(this, state, callback) } // 设置弹幕状态 setState(danmu, state) { - // 从原状态池中删除 - this.states[danmu.$state] = this.states[danmu.$state].filter(item => item !== danmu) - - // 设置新状态 - danmu.$state = state - - // 设置DOM节点状态 - if (danmu.$ref) { - danmu.$ref.dataset.state = state - } - - // 添加到新状态池中 - this.states[state].push(danmu) + setItemState(this, danmu, state) } // 重置弹幕到wait状态,回收弹幕DOM节点 @@ -453,118 +431,8 @@ export default class Danmuku { // 实时更新弹幕 update() { - const { setStyles } = this.utils - - this.timer = window.requestAnimationFrame(async () => { - if (this.art.playing && !this.isHide) { - // 实时计算弹幕的剩余显示时间 - this.filter('emit', (danmu) => { - const emitTime = (Date.now() - danmu.$lastStartTime) / 1000 - danmu.$restTime -= emitTime - danmu.$lastStartTime = Date.now() - // 超过时间即重置弹幕 - if (danmu.$restTime <= 0) { - this.makeWait(danmu) - } - }) - - // 获取准备好发送的弹幕,可能包含ready和wait状态的弹幕 - const readys = this.readys - - for (let index = 0; index < readys.length; index++) { - const danmu = readys[index] - - // 弹幕发送前的过滤器 - const state = await this.option.beforeVisible(danmu) - - if (state) { - const { clientWidth, clientHeight } = this.$player - danmu.$ref = this.$ref // 获取弹幕DOM节点 - danmu.$ref.textContent = danmu.text // 设置弹幕文本 - - // 提前添加到弹幕层中,用于计算top值 - this.$danmuku.appendChild(danmu.$ref) - - // 设置初始弹幕样式 - danmu.$ref.style.opacity = this.option.opacity - danmu.$ref.style.fontSize = `${this.fontSize}px` - danmu.$ref.style.color = danmu.color - danmu.$ref.style.border = danmu.border ? `1px solid ${danmu.color}` : null - danmu.$ref.style.backgroundColor = danmu.border ? 'rgb(0 0 0 / 50%)' : null - - // 设置单独弹幕样式 - setStyles(danmu.$ref, danmu.style) - - // 记录弹幕时间戳 - danmu.$lastStartTime = Date.now() - - // 计算弹幕剩余时间 - danmu.$restTime = this.speed - - // 计算弹幕滚动的距离 - const distance = clientWidth + danmu.$ref.clientWidth - - // 计算弹幕的top值 - const { result: top } = await this.postMessage({ - type: 'getDanmuTop', - target: { - mode: danmu.mode, - height: danmu.$ref.clientHeight, - speed: distance / danmu.$restTime, - }, // 当前弹幕信息 - visibles: this.visibles, // 可见的弹幕的数据 - antiOverlap: this.option.antiOverlap, - clientWidth, - clientHeight, - marginBottom: this.marginBottom, - marginTop: this.marginTop, - }) - - if (danmu.$ref) { - if (!this.isStop && top !== undefined) { - this.setState(danmu, 'emit') // 转换为emit状态 - danmu.$ref.style.top = `${top}px` - danmu.$ref.style.visibility = 'visible' - danmu.$ref.dataset.mode = danmu.mode // CSS控制模式的显示和隐藏 - danmu.$ref.dataset.id = danmu.id || '' // 用于悬停的唯一标识 - - switch (danmu.mode) { - // 滚动的弹幕 - case 0: { - danmu.$ref.style.left = `${clientWidth}px` - danmu.$ref.style.marginLeft = '0px' - danmu.$ref.style.transform = `translateX(${-distance}px)` - danmu.$ref.style.transition = `transform ${danmu.$restTime}s linear 0s` - break - } - case 1: - // falls through - case 2: - danmu.$ref.style.left = '50%' - danmu.$ref.style.marginLeft = `-${danmu.$ref.clientWidth / 2}px` - break - default: - break - } - - this.art.emit('artplayerPluginDanmuku:visible', danmu) - } - else { - // 假如弹幕已经停止或者没有 top 值,则重置弹幕为ready状态,回收弹幕DOM节点,等待下次发送 - this.setState(danmu, 'ready') - this.$refs.push(danmu.$ref) - danmu.$ref = null - } - } - } - } - } - - // 递归调用 - if (!this.isStop) { - this.update() - } - }) + this.scheduler.running = true + this.scheduler.schedule() return this } @@ -645,14 +513,24 @@ export default class Danmuku { stop() { this.isStop = true + this.scheduler.running = false + this.scheduler.invalidate() this.suspend() - window.cancelAnimationFrame(this.timer) this.art.emit('artplayerPluginDanmuku:stop') return this } start() { + if (this.scheduler.closed || this.art.isDestroy) + return this + const start = ++this.scheduler.starts + const generation = this.scheduler.generation this.isStop = false + this.scheduler.recover() + const obsolete = this.scheduler.closed || this.art.isDestroy || this.isStop || this.scheduler.fault + || generation !== this.scheduler.generation || start !== this.scheduler.starts + if (obsolete) + return this this.continue() this.update() this.art.emit('artplayerPluginDanmuku:start') @@ -660,11 +538,19 @@ export default class Danmuku { } reset() { + this.scheduler.invalidate() this.queue.forEach(danmu => this.makeWait(danmu)) this.art.emit('artplayerPluginDanmuku:reset') + this.scheduler.recover() + this.scheduler.schedule() return this } + seek() { + this.scheduler.invalidate() + this.scheduler.schedule() + } + show() { this.isHide = false this.$danmuku.style.opacity = 1 @@ -675,22 +561,34 @@ export default class Danmuku { hide() { this.isHide = true + this.scheduler.invalidate() this.$danmuku.style.opacity = 0 this.option.visible = false this.art.emit('artplayerPluginDanmuku:hide') + this.scheduler.schedule() return this } destroy() { cancelInputs(this) - this.stop() - this.worker?.terminate() - this.art.off('video:play', this.start) - this.art.off('video:playing', this.start) - this.art.off('video:pause', this.stop) - this.art.off('video:waiting', this.stop) - this.art.off('resize', this.resize) - this.art.off('destroy', this.destroy) - this.art.emit('artplayerPluginDanmuku:destroy') + this.scheduler.destroy() + try { + try { + this.stop() + } + finally { + this.workerClient?.dispose() + } + } + finally { + this.art.off('video:play', this.start) + this.art.off('video:playing', this.start) + this.art.off('video:pause', this.stop) + this.art.off('video:waiting', this.stop) + this.art.off('resize', this.resize) + this.art.off('video:seeking', this.seek) + this.art.off('destroy', this.destroy) + this.art.emit('artplayerPluginDanmuku:destroy') + } } } diff --git a/packages/artplayer-plugin-danmuku/src/queue.js b/packages/artplayer-plugin-danmuku/src/queue.js new file mode 100644 index 000000000..083eea245 --- /dev/null +++ b/packages/artplayer-plugin-danmuku/src/queue.js @@ -0,0 +1,24 @@ +export function filterState(owner, state, callback) { + const danmus = owner.states[state] || [] + for (let index = 0; index < danmus.length; index++) callback(danmus[index]) + return danmus +} + +export function readyItems(owner) { + const { currentTime } = owner.art + const result = [] + filterState(owner, 'ready', danmu => result.push(danmu)) + filterState(owner, 'wait', (danmu) => { + if (currentTime + 0.1 >= danmu.time && danmu.time >= currentTime - 0.1) + result.push(danmu) + }) + return result +} + +export function setItemState(owner, danmu, state) { + owner.states[danmu.$state] = owner.states[danmu.$state].filter(item => item !== danmu) + danmu.$state = state + if (danmu.$ref) + danmu.$ref.dataset.state = state + owner.states[state].push(danmu) +} diff --git a/packages/artplayer-plugin-danmuku/src/scheduler.js b/packages/artplayer-plugin-danmuku/src/scheduler.js new file mode 100644 index 000000000..c15412656 --- /dev/null +++ b/packages/artplayer-plugin-danmuku/src/scheduler.js @@ -0,0 +1,208 @@ +const cancelled = Symbol('cancelled danmuku frame') + +export default class Scheduler { + constructor(owner) { + this.owner = owner + this.generation = 0 + this.starts = 0 + this.frame = null + this.operation = null + this.closed = false + this.running = false + this.fault = false + this.failedItems = new Set() + } + + active(operation) { + const owner = this.owner + return !this.closed && !this.fault && !owner.art.isDestroy && !owner.isStop + && !owner.isHide && operation.generation === this.generation + } + + release(operation) { + const { danmu, ref } = operation + if (ref && danmu.$ref === ref) { + ref.style.cssText = this.owner.constructor.cssText + ref.style.visibility = 'hidden' + this.owner.$refs.push(ref) + danmu.$ref = null + } + operation.ref = null + } + + invalidate() { + this.generation++ + if (this.frame !== null) + window.cancelAnimationFrame(this.frame) + this.frame = null + this.owner.timer = null + if (this.operation) { + const operation = this.operation + this.operation = null + operation.cancel() + this.release(operation) + } + this.owner.workerClient?.cancel() + this.failedItems.clear() + } + + report(error) { + try { + this.owner.art.emit('artplayerPluginDanmuku:error', error) + } + catch (listenerError) { + console.warn('Failed to report danmuku scheduling error:', listenerError) + } + } + + fail(error) { + if (this.closed || this.fault) + return + this.fault = true + this.invalidate() + this.report(error) + } + + recover() { + if (this.closed || this.owner.art.isDestroy) + return + this.failedItems.clear() + if (this.fault) { + try { + this.owner.workerClient?.dispose() + this.owner.createWorker() + this.fault = false + } + catch (error) { + this.report(error) + } + } + } + + schedule() { + const owner = this.owner + if (!this.running || this.closed || this.fault || owner.art.isDestroy || owner.isStop || this.frame !== null || this.operation) + return + this.frame = window.requestAnimationFrame(() => { + this.frame = null + owner.timer = null + if (this.closed || this.fault || owner.isStop || owner.art.isDestroy) + return + let cancel + const cancellation = new Promise(resolve => cancel = () => resolve(cancelled)) + const operation = { generation: this.generation, cancel, wait: value => Promise.race([value, cancellation]), ref: null } + this.operation = operation + return this.run(operation).catch((error) => { + if (this.active(operation)) + this.fail(error) + }).finally(() => { + this.release(operation) + if (this.operation === operation) { + this.operation = null + this.schedule() + } + }) + }) + owner.timer = this.frame + } + + async run(operation) { + const owner = this.owner + const { setStyles } = owner.utils + if (!owner.art.playing || !this.active(operation)) + return + owner.filter('emit', (danmu) => { + const emitTime = (Date.now() - danmu.$lastStartTime) / 1000 + danmu.$restTime -= emitTime + danmu.$lastStartTime = Date.now() + if (danmu.$restTime <= 0) + owner.makeWait(danmu) + }) + const readys = owner.readys + for (const danmu of readys) { + if (!this.active(operation)) + return + if (this.failedItems.has(danmu)) + continue + let state + try { + state = await operation.wait(owner.option.beforeVisible(danmu)) + } + catch (error) { + if (!this.active(operation)) + return + this.failedItems.add(danmu) + this.report(error) + continue + } + if (!this.active(operation)) + return + if (!state) + continue + + const { clientWidth, clientHeight } = owner.$player + const ref = owner.$ref + operation.danmu = danmu + operation.ref = ref + danmu.$ref = ref + ref.textContent = danmu.text + owner.$danmuku.appendChild(ref) + ref.style.opacity = owner.option.opacity + ref.style.fontSize = `${owner.fontSize}px` + ref.style.color = danmu.color + ref.style.border = danmu.border ? `1px solid ${danmu.color}` : null + ref.style.backgroundColor = danmu.border ? 'rgb(0 0 0 / 50%)' : null + setStyles(ref, danmu.style) + if (!this.active(operation)) + return + danmu.$lastStartTime = Date.now() + danmu.$restTime = owner.speed + const distance = clientWidth + ref.clientWidth + const { result: top } = await operation.wait(owner.postMessage({ + type: 'getDanmuTop', + target: { mode: danmu.mode, height: ref.clientHeight, speed: distance / danmu.$restTime }, + visibles: owner.visibles, + antiOverlap: owner.option.antiOverlap, + clientWidth, + clientHeight, + marginBottom: owner.marginBottom, + marginTop: owner.marginTop, + })) + if (!this.active(operation) || danmu.$ref !== ref) + return + if (top !== undefined) { + owner.setState(danmu, 'emit') + ref.style.top = `${top}px` + ref.style.visibility = 'visible' + ref.dataset.mode = danmu.mode + ref.dataset.id = danmu.id || '' + switch (danmu.mode) { + case 0: + ref.style.left = `${clientWidth}px` + ref.style.marginLeft = '0px' + ref.style.transform = `translateX(${-distance}px)` + ref.style.transition = `transform ${danmu.$restTime}s linear 0s` + break + case 1: + case 2: + ref.style.left = '50%' + ref.style.marginLeft = `-${ref.clientWidth / 2}px` + break + default: + break + } + operation.ref = null + owner.art.emit('artplayerPluginDanmuku:visible', danmu) + } + else { + owner.setState(danmu, 'ready') + this.release(operation) + } + } + } + + destroy() { + this.closed = true + this.invalidate() + } +} diff --git a/packages/artplayer-plugin-danmuku/src/worker-client.js b/packages/artplayer-plugin-danmuku/src/worker-client.js new file mode 100644 index 000000000..317274240 --- /dev/null +++ b/packages/artplayer-plugin-danmuku/src/worker-client.js @@ -0,0 +1,91 @@ +let nextRequest = 0 + +export default class WorkerClient { + constructor(createWorker, onFailure) { + this.pending = new Map() + this.closed = false + this.failed = false + this.onFailure = onFailure + this.worker = createWorker() + try { + this.worker.onmessage = (event) => { + const request = this.pending.get(event.data?.id) + if (!request) + return + this.pending.delete(event.data.id) + request.resolve(event.data) + } + this.worker.onerror = (event) => { + event.preventDefault?.() + this.fail(event.error || event) + } + this.worker.onmessageerror = event => this.fail(event.error || event) + } + catch (error) { + try { + this.dispose() + } + catch {} + throw error + } + } + + request(message) { + message.id = ++nextRequest + const { id } = message + if (this.closed || this.failed) + return Promise.resolve({ id, result: undefined }) + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject }) + try { + this.worker.postMessage(message) + } + catch (error) { + this.fail(error) + } + }) + } + + fail(error) { + if (this.closed || this.failed) + return + this.failed = true + for (const request of this.pending.values()) request.reject(error) + this.pending.clear() + try { + this.dispose() + } + catch {} + this.onFailure(error) + } + + cancel() { + for (const [id, request] of this.pending) request.resolve({ id, result: undefined }) + this.pending.clear() + } + + dispose() { + if (this.closed) + return + this.closed = true + this.cancel() + let failed = false + let error + const attempt = (callback) => { + try { + callback() + } + catch (failure) { + if (!failed) + error = failure + failed = true + } + } + for (const name of ['onmessage', 'onerror', 'onmessageerror']) { + attempt(() => this.worker[name] = null) + } + attempt(() => this.worker.terminate()) + if (failed) + throw error + } +} diff --git a/refactor/baselines/danmuku-scheduler-validation.json b/refactor/baselines/danmuku-scheduler-validation.json new file mode 100644 index 000000000..a74bf7aa7 --- /dev/null +++ b/refactor/baselines/danmuku-scheduler-validation.json @@ -0,0 +1,20614 @@ +{ + "schemaVersion": 1, + "task": "PKG-DANMUKU-04", + "status": "done", + "capturedAt": "2026-09-13T11:18:49.542Z", + "baselineCommit": "0b2efb07f", + "units": [ + { + "format": "source", + "tests": 104, + "implementationDependent": 83, + "commonSourceWorkerTests": 18, + "commonFrozenNumericTests": 3, + "file": "refactor/.cache/danmuku04-candidate-source-complete.log", + "sha256": "f05f78034322803aa28e5eedadbf31e86de97ff8af96155158336bc68d341953" + }, + { + "format": "main", + "tests": 104, + "implementationDependent": 83, + "commonSourceWorkerTests": 18, + "commonFrozenNumericTests": 3, + "file": "refactor/.cache/danmuku04-candidate-main-complete.log", + "sha256": "58f27d6d25e2f70a626b442494be833e14ca232b435b0f9d9b783a44926e3a8c" + }, + { + "format": "legacy", + "tests": 104, + "implementationDependent": 83, + "commonSourceWorkerTests": 18, + "commonFrozenNumericTests": 3, + "file": "refactor/.cache/danmuku04-candidate-legacy-complete.log", + "sha256": "b626d2a2069dfa7f9a608c7e2c83c8b910236df2ec4586117d68892ac97d6204" + } + ], + "browsers": [ + { + "format": "source", + "tests": 66, + "file": "refactor/.cache/danmuku04-browser-source-numeric-final/report.json", + "sha256": "e31066f57d89fa987012e06db83112a0ad0b3a47e668402ec877321832b9c89a", + "run": { + "file": "refactor/.cache/danmuku04-browser-source-numeric-final/run.log", + "sha256": "2e0a30a909fd07daf08bb532739b85c6635b5fa5dc39c53fac302cf989f942e1" + }, + "spec": { + "file": "refactor/.cache/danmuku04-browser-source-numeric-final/danmuku-scheduler.spec.js", + "sha256": "57a4791dba05ab8a0d5ddd2bf38730e10a70a5cd5c32b3ef4ec82440b5911e4e" + }, + "archiveResultsRoot": "refactor/.cache/danmuku04-browser-source-numeric-final/results", + "originalResultsRoot": "refactor/.cache/browser/results", + "versions": [ + "chromium: 153.0.8010.12", + "firefox: 155.0", + "webkit: 26.6" + ], + "cases": [ + { + "title": "published / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "chromium", + "durationMs": 1594, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "chromium", + "durationMs": 1331, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.907252 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/31792b31-5dc6-454d-a9e4-5c8b37ccfac0", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "chromium", + "durationMs": 1306, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.898323 + }, + { + "index": 0, + "event": "stop", + "time": 0.898323 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/7871692b-932d-4d8e-b806-fca0b9362426", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 2385, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 2369, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 2252, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 1307, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "chromium", + "durationMs": 1389, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1662, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "chromium", + "durationMs": 2290, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 602.7918090820312, + "next": 602.7918090820312, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "chromium", + "durationMs": 4241, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "chromium", + "durationMs": 1301, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "chromium", + "durationMs": 1313, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.908902 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/09c0a843-b399-472c-9aa6-50c11c27a0ff", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "chromium", + "durationMs": 1303, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.89769 + }, + { + "index": 0, + "event": "stop", + "time": 0.89769 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/fd03c655-a488-4c87-bcec-fc044f33771f", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 2506, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 1808, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 2524, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 1345, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "chromium", + "durationMs": 1373, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1576, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "chromium", + "durationMs": 2437, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 602.788818359375, + "next": 602.788818359375, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "chromium", + "durationMs": 4210, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "firefox", + "durationMs": 3172, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "firefox", + "durationMs": 1888, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.905723 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/e8b3fd31-4ba6-4a93-b5e9-f241eb4f6582", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "firefox", + "durationMs": 1686, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.874999 + }, + { + "index": 0, + "event": "stop", + "time": 0.874999 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/7b7739a5-842b-44b3-acdb-38bce3ef999d", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 2670, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 2044, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 2636, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 1792, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "firefox", + "durationMs": 1703, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 2878, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "firefox", + "durationMs": 2923, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 601.3333129882812, + "next": 601.3333129882812, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "firefox", + "durationMs": 4910, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "firefox", + "durationMs": 1806, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "firefox", + "durationMs": 1871, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.901248 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/d3ccaf64-24f7-412d-95f4-1f046a39eed1", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "firefox", + "durationMs": 1561, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.874999 + }, + { + "index": 0, + "event": "stop", + "time": 0.874999 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/fb84e1d6-1169-49e9-9a95-4248cef05bf3", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 2900, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 2247, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 3923, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 1570, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "firefox", + "durationMs": 1764, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 1708, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "firefox", + "durationMs": 2967, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 601.2999877929688, + "next": 601.2999877929688, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "firefox", + "durationMs": 4996, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "webkit", + "durationMs": 2506, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "webkit", + "durationMs": 2816, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.5477604 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/8887345c-74d7-4df2-9db3-33ad1b6b63ed", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "webkit", + "durationMs": 2744, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.4936346 + }, + { + "index": 0, + "event": "stop", + "time": 0.4937073 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/8abdf89e-4053-43e1-b126-18156c22a4e3", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 3964, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 3803, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 3585, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 2973, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "webkit", + "durationMs": 3650, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 4195, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "webkit", + "durationMs": 5271, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 609.4124755859375, + "next": 609.4124755859375, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "webkit", + "durationMs": 8351, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "webkit", + "durationMs": 2581, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "webkit", + "durationMs": 2693, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.5058696 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/377e84c9-5640-420c-9ebc-bcebd222cf0a", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "webkit", + "durationMs": 2799, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.5110266 + }, + { + "index": 0, + "event": "stop", + "time": 0.5111032 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/63387ef8-4a3a-4793-af0f-16188ec8be58", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 3630, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 2881, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 3943, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 2839, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "webkit", + "durationMs": 3184, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 3994, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "webkit", + "durationMs": 5486, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 613.9625244140625, + "next": 613.9625244140625, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "webkit", + "durationMs": 8337, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "in-memory source UMD", + "sha256": "e6658f5fa5a4fe4a81b1552731d7450262989d4f934a1ec26f260a3dcd722d61", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + } + ] + }, + { + "format": "main", + "tests": 66, + "file": "refactor/.cache/danmuku04-browser-main-numeric-final/report.json", + "sha256": "5a77e4c6e59c7f78702b120c04b64ce3740c653a7751d55ada373646ea103533", + "run": { + "file": "refactor/.cache/danmuku04-browser-main-numeric-final/run.log", + "sha256": "dffddff7b686591aac8902cdd2e8d8b5ac3c1e1c907ddecadcb5cee6a9fa5477" + }, + "spec": { + "file": "refactor/.cache/danmuku04-browser-main-numeric-final/danmuku-scheduler.spec.js", + "sha256": "57a4791dba05ab8a0d5ddd2bf38730e10a70a5cd5c32b3ef4ec82440b5911e4e" + }, + "archiveResultsRoot": "refactor/.cache/danmuku04-browser-main-numeric-final/results", + "originalResultsRoot": "refactor/.cache/browser/results", + "versions": [ + "chromium: 153.0.8010.12", + "firefox: 155.0", + "webkit: 26.6" + ], + "cases": [ + { + "title": "published / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "chromium", + "durationMs": 1409, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "chromium", + "durationMs": 1303, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.899133 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/6c9706bb-601b-42de-9d19-a9863257d58a", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "chromium", + "durationMs": 1347, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.899634 + }, + { + "index": 0, + "event": "stop", + "time": 0.899634 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/4cf3e456-5043-4049-9fdc-542c77e91532", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 2336, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 2264, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 2225, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 1295, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "chromium", + "durationMs": 1364, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1511, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "chromium", + "durationMs": 2418, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 599.8820190429688, + "next": 599.8820190429688, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "chromium", + "durationMs": 4146, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "chromium", + "durationMs": 1290, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "chromium", + "durationMs": 1270, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.897016 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/a28799c8-5f27-420f-8a8d-2c50ff8a8e95", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "chromium", + "durationMs": 1307, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.907498 + }, + { + "index": 0, + "event": "stop", + "time": 0.907498 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/23a88b13-af5c-4769-a3ab-29df34b9738b", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 2424, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 2361, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 2438, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 1325, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "chromium", + "durationMs": 1349, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1476, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "chromium", + "durationMs": 2377, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 601.33740234375, + "next": 601.33740234375, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "chromium", + "durationMs": 4140, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "firefox", + "durationMs": 2349, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "firefox", + "durationMs": 1681, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.90678 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/92bd0b6a-c128-420f-91b8-d107541e3357", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "firefox", + "durationMs": 1632, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.872461 + }, + { + "index": 0, + "event": "stop", + "time": 0.872461 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/f2c7a295-89ef-4802-877e-099982efdac9", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 2740, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 2088, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 2658, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 1205, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "firefox", + "durationMs": 1903, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 3024, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "firefox", + "durationMs": 2654, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 601.3333129882812, + "next": 601.3333129882812, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "firefox", + "durationMs": 4660, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "firefox", + "durationMs": 1697, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "firefox", + "durationMs": 1796, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.919782 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/7f9fadf7-14b3-4712-921f-57c4968893f1", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "firefox", + "durationMs": 1772, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.874999 + }, + { + "index": 0, + "event": "stop", + "time": 0.874999 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/fd38ba4c-7a2d-48a5-b0d0-65c28e092b5c", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 2838, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 2126, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 3733, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 1563, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "firefox", + "durationMs": 1622, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 1769, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "firefox", + "durationMs": 2992, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 602.75, + "next": 602.75, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "firefox", + "durationMs": 4773, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "webkit", + "durationMs": 2516, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "webkit", + "durationMs": 2160, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.5270586 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/f6a43b1c-f948-44ac-9f8c-034aac7d3a57", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "webkit", + "durationMs": 2483, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.5209417 + }, + { + "index": 0, + "event": "stop", + "time": 0.5210177 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/bba3ffdd-5710-4bd8-b5be-fd9cbd514a34", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 3801, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 2857, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 3446, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 2259, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "webkit", + "durationMs": 3023, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 3108, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "webkit", + "durationMs": 5045, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 574.0625, + "next": 574.0625, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "webkit", + "durationMs": 7930, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "webkit", + "durationMs": 2282, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "webkit", + "durationMs": 2565, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.9559543 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/8c0e38c5-06cb-4041-b2d3-b93322009b72", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "webkit", + "durationMs": 2040, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.4843741 + }, + { + "index": 0, + "event": "stop", + "time": 0.4844099 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/3ccfde56-4324-4b8b-8f99-36f1667ba9a5", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 4025, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 3258, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 3269, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 2932, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "webkit", + "durationMs": 3063, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 3197, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "webkit", + "durationMs": 4512, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 617.9000244140625, + "next": 617.9000244140625, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "webkit", + "durationMs": 7496, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + } + ] + }, + { + "format": "legacy", + "tests": 66, + "file": "refactor/.cache/danmuku04-browser-legacy-numeric-final/report.json", + "sha256": "ee02ab34e771542ba228a958b8052a1df5f2c0e54bf7468915332b1a9d833b50", + "run": { + "file": "refactor/.cache/danmuku04-browser-legacy-numeric-final/run.log", + "sha256": "41ba9761a9f953a4905c53cf1f04f5f9e1a5f900eb2f354d052c81ecd8bd58e9" + }, + "spec": { + "file": "refactor/.cache/danmuku04-browser-legacy-numeric-final/danmuku-scheduler.spec.js", + "sha256": "57a4791dba05ab8a0d5ddd2bf38730e10a70a5cd5c32b3ef4ec82440b5911e4e" + }, + "archiveResultsRoot": "refactor/.cache/danmuku04-browser-legacy-numeric-final/results", + "originalResultsRoot": "refactor/.cache/browser/results", + "versions": [ + "chromium: 153.0.8010.12", + "firefox: 155.0", + "webkit: 26.6" + ], + "cases": [ + { + "title": "published / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "chromium", + "durationMs": 1442, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "chromium", + "durationMs": 1312, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.910342 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/f95625d3-da3c-43cd-bd5e-44485457b5c8", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "chromium", + "durationMs": 1288, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.900038 + }, + { + "index": 0, + "event": "stop", + "time": 0.900038 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/6c1db542-01cf-4aba-91bb-0d59a367a38f", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 2447, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 2256, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 2224, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 1275, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "chromium", + "durationMs": 1341, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1605, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "chromium", + "durationMs": 2419, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 602.7915649414062, + "next": 602.7915649414062, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "chromium", + "durationMs": 4165, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "chromium", + "durationMs": 1267, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "chromium", + "durationMs": 1243, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.893598 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/019300e7-47a5-4891-a112-1bd126940bec", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "chromium", + "durationMs": 1289, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.904498 + }, + { + "index": 0, + "event": "stop", + "time": 0.904498 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/53f97fbd-4b03-443f-b2b0-4808abd97a28", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 2405, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 2320, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 2436, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "chromium", + "durationMs": 1289, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "chromium", + "durationMs": 1343, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1522, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "chromium", + "durationMs": 2245, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 602.7899780273438, + "next": 602.7899780273438, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "chromium", + "durationMs": 4116, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "firefox", + "durationMs": 2506, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "firefox", + "durationMs": 1743, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.874999 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/e61312b7-89bc-4e2b-acf2-de1a93430333", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "firefox", + "durationMs": 1610, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.875433 + }, + { + "index": 0, + "event": "stop", + "time": 0.875433 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/f54b5650-4ad7-4e5e-a9be-38ed500e72b4", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 2632, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 2612, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 2662, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 1548, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "firefox", + "durationMs": 1646, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 2685, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "firefox", + "durationMs": 2657, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 602.7999877929688, + "next": 602.7999877929688, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "firefox", + "durationMs": 4784, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "firefox", + "durationMs": 1654, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "firefox", + "durationMs": 1510, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.872581 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/43f7405f-e3b1-44d7-a839-608da97ba808", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "firefox", + "durationMs": 1566, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.916666 + }, + { + "index": 0, + "event": "stop", + "time": 0.916666 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/07c5a8d4-40f8-411c-8309-ac7f3feb3375", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 2299, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 2645, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 3635, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "firefox", + "durationMs": 1638, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "firefox", + "durationMs": 1578, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 1643, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "firefox", + "durationMs": 2696, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 601.3333129882812, + "next": 601.3333129882812, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "firefox", + "durationMs": 4769, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "webkit", + "durationMs": 2279, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "webkit", + "durationMs": 2510, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.5531477 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/3fb921b4-0608-4f22-9250-eb324ffb5b34", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "webkit", + "durationMs": 2413, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.5130193 + }, + { + "index": 0, + "event": "stop", + "time": 0.5130772 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/afc7addf-b473-4964-9e75-b0b9b697c06f", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 3917, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 3696, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 3504, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 2188, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "webkit", + "durationMs": 2488, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 3636, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "webkit", + "durationMs": 5298, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 572.4874877929688, + "next": 572.4874877929688, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "webkit", + "durationMs": 7204, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: accepted NaN timestamp keeps the historical current-time fallback", + "browser": "webkit", + "durationMs": 2215, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-numeric-compatibility", + "value": { + "expected": 0.5, + "inputTime": 0.5, + "queueTime": 0.5, + "isNaN": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous destroy from the error listener", + "browser": "webkit", + "durationMs": 2325, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "destroy", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": true, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.4766665 + }, + { + "index": 0, + "event": "stop", + "time": 0 + }, + { + "index": 0, + "event": "destroy", + "time": 0 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/4486a410-7b34-430f-8464-ff4b20cd45bf", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: failed recovery respects synchronous stop from the error listener", + "browser": "webkit", + "durationMs": 2206, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-recovery-reentry", + "value": { + "action": "stop", + "sameOwner": true, + "rowState": "stop", + "isStop": true, + "isDestroy": false, + "events": [ + { + "index": 0, + "event": "error", + "time": 0.5243332 + }, + { + "index": 0, + "event": "stop", + "time": 0.5243703 + } + ], + "workers": [ + { + "url": "blob:http://127.0.0.1:8084/4939d7b4-cfac-466a-b775-d95f1411d6e7", + "requests": [ + { + "type": "getDanmuTop", + "target": { + "mode": 1, + "height": 23, + "speed": 90 + }, + "visibles": [], + "antiOverlap": true, + "clientWidth": 640, + "clientHeight": 360, + "marginBottom": 10, + "marginTop": 10, + "id": 1 + } + ], + "replies": [ + { + "id": 1, + "result": 10 + } + ], + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "fault": "Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [ + "controlled track failure" + ] + } + ], + "failures": [ + "Error: controlled track failure", + "Error: controlled track reconstruction failure" + ] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 3896, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "pause", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native reset cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 2629, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "reset", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native seek cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 3577, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "seek", + "before": { + "requests": [ + 1 + ], + "visible": [ + "fresh-after-cancel" + ] + }, + "after": [ + "fresh-after-cancel" + ], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native destroy cancels pending visibility without blocking later work", + "browser": "webkit", + "durationMs": 2440, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-native-cancel", + "value": { + "action": "destroy", + "before": { + "requests": [ + 0 + ], + "visible": [] + }, + "after": [], + "cancelledSource": "An unresolved user beforeVisible Promise; native RAF, Worker and media remain active." + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native Worker renders three modes with synchronous filter and asynchronous beforeVisible", + "browser": "webkit", + "durationMs": 2818, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "modes", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 4, + "replies": 4, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 3670, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: native pause seek rate and public reset/load retain historical pool semantics", + "browser": "webkit", + "durationMs": 5009, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "playback", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-pool-and-rate", + "value": { + "frozen": { + "left": 578, + "next": 578, + "state": "stop" + }, + "reused": { + "poolReused": true, + "visibleSameNode": true, + "original": { + "id": "reused", + "state": "emit", + "visibility": "visible", + "text": "REUSED" + }, + "extraNodes": [], + "transition": "4s", + "rate": 2 + }, + "loadWithoutArgument": "replaces from original empty option; explicit array load appends" + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 3, + "replies": 3, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction", + "browser": "webkit", + "durationMs": 7436, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "settings", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 0, + "replies": 0, + "terminated": 1, + "errors": [] + }, + { + "requests": 1, + "replies": 1, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + } + ] + }, + { + "format": "dense-main", + "tests": 30, + "file": "refactor/.cache/danmuku04-browser-dense-main-numeric-final/report.json", + "sha256": "5fdf278fac810a7abf22970932a9f140316b77af23d74b8e92606bbcfe290976", + "run": { + "file": "refactor/.cache/danmuku04-browser-dense-main-numeric-final/run.log", + "sha256": "9dd7c9bcbb94ce6f1139907b88336c2b5ce01b1c1870807aa8c7fbb14f86cb0b" + }, + "spec": { + "file": "refactor/.cache/danmuku04-browser-dense-main-numeric-final/danmuku-scheduler.spec.js", + "sha256": "57a4791dba05ab8a0d5ddd2bf38730e10a70a5cd5c32b3ef4ec82440b5911e4e" + }, + "archiveResultsRoot": "refactor/.cache/danmuku04-browser-dense-main-numeric-final/results", + "originalResultsRoot": "refactor/.cache/browser/results", + "versions": [ + "chromium: 153.0.8010.12", + "firefox: 155.0", + "webkit: 26.6" + ], + "cases": [ + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1627, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1641, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1643, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1660, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1542, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1588, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1650, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1665, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1542, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1629, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 2351, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 3003, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 3303, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 3321, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 3018, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 2207, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 3000, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 2978, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 2915, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 3148, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 3719, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 3767, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 3589, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 3072, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 4284, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 3704, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 4218, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 3659, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 3778, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 3691, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + } + ] + }, + { + "format": "dense-legacy", + "tests": 30, + "file": "refactor/.cache/danmuku04-browser-dense-legacy-numeric-final/report.json", + "sha256": "ddd9e5d8a986e96f50bb6607444c5f50376aa746fd70bf64d010aafb96a3fba7", + "run": { + "file": "refactor/.cache/danmuku04-browser-dense-legacy-numeric-final/run.log", + "sha256": "68f6e7895933021f2f63458803f40affb282ec3493f50b0fcf157aaee9ab7301" + }, + "spec": { + "file": "refactor/.cache/danmuku04-browser-dense-legacy-numeric-final/danmuku-scheduler.spec.js", + "sha256": "57a4791dba05ab8a0d5ddd2bf38730e10a70a5cd5c32b3ef4ec82440b5911e4e" + }, + "archiveResultsRoot": "refactor/.cache/danmuku04-browser-dense-legacy-numeric-final/results", + "originalResultsRoot": "refactor/.cache/browser/results", + "versions": [ + "chromium: 153.0.8010.12", + "firefox: 155.0", + "webkit: 26.6" + ], + "cases": [ + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1785, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1829, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1819, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1693, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1698, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1684, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1679, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1738, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1963, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "chromium", + "durationMs": 1758, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 2987, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 3089, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 2928, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 3068, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 3033, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 3241, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 2783, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 2963, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 2982, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "firefox", + "durationMs": 3477, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 4208, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 4244, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 4385, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 4070, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 3820, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 4302, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 4235, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 3945, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "published / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 3990, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "published", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + }, + { + "title": "candidate / Candidate Danmuku candidate: six simultaneous fixed comments preserve usable track geometry and input order", + "browser": "webkit", + "durationMs": 4023, + "observations": [ + { + "name": "danmuku-browser-inputs", + "value": { + "core": "candidate", + "plugin": "artplayer-plugin-danmuku", + "format": "candidate", + "artifact": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "media": "/test/pattern.mp4", + "scenario": "density", + "worker": "Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked", + "heatmap": false + } + }, + { + "name": "danmuku-dense-tracks", + "value": [ + { + "id": "dense-0", + "mode": "1", + "state": "stop", + "text": "TRACK 0", + "x": 285, + "y": 10, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-1", + "mode": "1", + "state": "stop", + "text": "TRACK 1", + "x": 285, + "y": 33, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-2", + "mode": "1", + "state": "stop", + "text": "TRACK 2", + "x": 285, + "y": 56, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-3", + "mode": "1", + "state": "stop", + "text": "TRACK 3", + "x": 285, + "y": 79, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-4", + "mode": "1", + "state": "stop", + "text": "TRACK 4", + "x": 285, + "y": 102, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + }, + { + "id": "dense-5", + "mode": "1", + "state": "stop", + "text": "TRACK 5", + "x": 285, + "y": 125, + "width": 70, + "height": 22.5, + "visibility": "visible", + "display": "block", + "transition": "", + "opacity": "1" + } + ] + }, + { + "name": "danmuku-worker-and-lifecycle", + "value": { + "workers": [ + { + "requests": 6, + "replies": 6, + "terminated": 1, + "errors": [] + } + ], + "failures": [] + } + } + ] + } + ] + } + ], + "regressions": [ + { + "file": "refactor/.cache/danmuku04-scheduler-initial.log", + "sha256": "5c85015ab899f73f514e83b881005c4d002bdc9c5418ad0ab9fbf1bbaa85cbb3", + "tests": 18, + "passed": 6, + "failed": 12, + "cause": "Initial candidate scheduler ownership, cancellation and request regression tests before implementation." + }, + { + "file": "refactor/.cache/danmuku04-scheduler-reentry-all-red.log", + "sha256": "37194aa46c183f67a0bcda9e50394766971e1c8c2f66301a5e07a1839b8cffaf", + "tests": 32, + "passed": 27, + "failed": 5, + "cause": "Saved actual pre-guard main artifact: synchronous error listener destroy/stop/reset/hide/nested start leaves obsolete start continuation." + }, + { + "file": "refactor/.cache/danmuku04-time-nan-red.log", + "sha256": "4492a27bbe61b2718ab586a11c7617ae572a2043c8e9aeb741d145a1b1d74ed8", + "tests": 24, + "passed": 23, + "failed": 1, + "cause": "Frozen source/main/legacy preserve accepted NaN fallback. Intermediate task03 candidate regressed NaN while fixing explicit zero." + } + ], + "nativeBefore": { + "file": "refactor/.cache/danmuku04-browser-before/report.json", + "sha256": "ef2b218f71737d379a5fbc9d93d2c0e11f38a556f1e3221efa5164535e80c0e0", + "tests": 8, + "passed": 4, + "failed": 4, + "source": "Task03 actual main, Chromium, two cores; extra same-ID requests and unused duplicate DOM. This snapshot did not itself overlap; task02 had separate actual overlap evidence." + }, + "checks": [ + { + "file": "refactor/.cache/danmuku04-build-numeric-final.log", + "sha256": "f679e4dd9e03605ecfefb68e8b973acb05a0b891f0cedd9000b73f9e6fe4e8f7" + }, + { + "file": "refactor/.cache/danmuku04-lint-complete.log", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + ], + "typecheck": { + "file": "refactor/.cache/danmuku03-typecheck.log", + "sha256": "33ced90b0fff4881e9a4477aca1e8c51a5715b8170f71c860518bbdff1c4e606", + "reusedFromTask": "PKG-DANMUKU-03", + "explanation": "No production TS or public declaration changes in task04. Previous complete check covers 366 migrated TS source files and consumers, not the remaining JS." + }, + "buildRetry": { + "failure": { + "file": "refactor/.cache/danmuku04-build-copy-failure.log", + "sha256": "9cf9f9b99ce59ba7521b67d4bafcc3163e2a931ea29a79a4a8013facec8d6a1f" + }, + "serialRetry": { + "file": "refactor/.cache/danmuku04-build-final.log", + "sha256": "a4ad0ed8d61452d5af294359c5afcb5866227e28a490946ab91b3f2f8da0dc69" + }, + "identity": { + "file": "refactor/.cache/danmuku04-build-identity.log", + "sha256": "904d45eb95129f13f0ccce541fea75496be37291a2caa66cf9c2ec06f9b72cc3" + }, + "explanation": "Windows UNKNOWN copy error during a browser run; serial retry succeeded and matched already-tested main/legacy bytes. Later numeric fix received another complete normal build and its own final matrices." + }, + "sizes": [ + { + "file": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c", + "beforeBytes": 55126, + "afterBytes": 59100, + "deltaBytes": 3974, + "beforeGzipBytes": 15687, + "afterGzipBytes": 16799 + }, + { + "file": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368", + "beforeBytes": 56172, + "afterBytes": 60243, + "deltaBytes": 4071, + "beforeGzipBytes": 16050, + "afterGzipBytes": 17160 + }, + { + "file": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.mjs", + "sha256": "bb96eb930c4ed9910cf4eca65b3a0fcb85cf6c6a49429f5c9c437490a4680ce3", + "beforeBytes": 80876, + "afterBytes": 87065, + "deltaBytes": 6189, + "beforeGzipBytes": 19534, + "afterGzipBytes": 20852 + } + ], + "inputs": [ + { + "path": "package.json", + "algorithm": "sha256-lf", + "sha256": "b85c8ca62eaab7b400f613620908274c6e02551f2987752220443a00c9de703c" + }, + { + "path": "packages/artplayer-plugin-danmuku/ARCHITECTURE.md", + "algorithm": "sha256-lf", + "sha256": "5a6111ec42c5e930786b9a01369f4c1a63e43e8d536f818f23869fd05f3765cf" + }, + { + "path": "packages/artplayer-plugin-danmuku/README.md", + "algorithm": "sha256-lf", + "sha256": "6111a8d52b68ee86e43383ee8686ba0676f57741d5da733a400b29a596dba8d7" + }, + { + "path": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js", + "algorithm": "sha256-lf", + "sha256": "cb5c475202ab0770682a0395223f7248ffde7854e3b14c39fb6110d152a4d60c" + }, + { + "path": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.legacy.js", + "algorithm": "sha256-lf", + "sha256": "e6a4b144a4b83a8c6a100444ad0ed5f6dbf7157abef8d23c187b4ffa79c23368" + }, + { + "path": "packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.mjs", + "algorithm": "sha256-lf", + "sha256": "bb96eb930c4ed9910cf4eca65b3a0fcb85cf6c6a49429f5c9c437490a4680ce3" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/bilibili-parser.js", + "algorithm": "sha256-lf", + "sha256": "46720235c8b9f986ecdda436a16b03a8215e4f3dc0151c16df17dca68f0302bc" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/bilibili.js", + "algorithm": "sha256-lf", + "sha256": "39310fb20dfe5f98a492460d7a9f7d3078367ce8f70e383d89f9dc7029d297cc" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/config.js", + "algorithm": "sha256-lf", + "sha256": "33b70af031f5a5df0b93bc2dcef74b32ce4674b6a248476dc3a6f65a29deeb1c" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/danmuku.js", + "algorithm": "sha256-lf", + "sha256": "8e1d22d382ca6ec14945e77384e78f6ff8bd01506736222d747c077788ac888b" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/heatmap.js", + "algorithm": "sha256-lf", + "sha256": "3cf77ff82f62958728445eaf9c044bc4cbcf04a4ea82e753e7c44e59128c16f4" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/img/check_off.svg", + "algorithm": "sha256-lf", + "sha256": "8cb8e31da1bf7b5be8f4065920b46e1988755910218fb121dd3d9649c50156cf" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/img/check_on.svg", + "algorithm": "sha256-lf", + "sha256": "c5a34f56c56b0f696b4e3740c73e803076b5bfdfdee89714793118fc4c6baa84" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/img/config.svg", + "algorithm": "sha256-lf", + "sha256": "bbd7c2952740ac8220f396477eaca3b01f6c249866d880ece38c565cc77c5e36" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/img/mode_0_off.svg", + "algorithm": "sha256-lf", + "sha256": "fe9e5e1745e8206a4eda0d0ac30815f301c37acccec2564bbda51c1fa4bd1e1e" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/img/mode_0_on.svg", + "algorithm": "sha256-lf", + "sha256": "96720a81f0315caf6c70664de934e49d22e6dd070a9f6ca1fec2a9a96af4d138" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/img/mode_1_off.svg", + "algorithm": "sha256-lf", + "sha256": "3ccd22bf3648dc260cd1d0b6a60a9226a853764ae20efb8fe9c940cd856c81b2" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/img/mode_1_on.svg", + "algorithm": "sha256-lf", + "sha256": "2238ca27e3c7c4c687d10de13b794d8be70ae1c5ba9225ebad27f251cfe5f12a" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/img/mode_2_off.svg", + "algorithm": "sha256-lf", + "sha256": "31423e3d67c00839adc77dd871dc1da55f77c8b83b5a37b445e4b4c3f46c6d6d" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/img/mode_2_on.svg", + "algorithm": "sha256-lf", + "sha256": "96b4c9db8d5055c8777fda74cb5e0f32b3e12e62a1e0bacbf75d2088f4505e2f" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/img/off.svg", + "algorithm": "sha256-lf", + "sha256": "4e6743ee28002e16117caf47ea3bffb34ca75bcc4118744b4ee720829ae3b563" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/img/on.svg", + "algorithm": "sha256-lf", + "sha256": "cc0fb7048bff6dc3f14a17ed549209c67e9bc34368cb1571e0abd8035c7b59b7" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/img/style.svg", + "algorithm": "sha256-lf", + "sha256": "9034ea7b9a214a6d4c086c02cf85f42b1c4487cec0906e919fc91e99dfff529a" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/index.js", + "algorithm": "sha256-lf", + "sha256": "956e30c857dbb15147506924308c363dcc4a07bcf1bb97ce3fc436afa366cd9c" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/input.js", + "algorithm": "sha256-lf", + "sha256": "6740c347018290de2a8d90925148f63ec005d294426ed8d3192e69313171b8fc" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/queue.js", + "algorithm": "sha256-lf", + "sha256": "d566e312686fa486daee0c1618766c41cfb074c04bf872b7bf1bb783a7c57595" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/scheduler.js", + "algorithm": "sha256-lf", + "sha256": "cc158ab266c96323d99230183c47e60f162087daa15c8374a73b8d619e305cb6" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/setting.js", + "algorithm": "sha256-lf", + "sha256": "ffbaa808551410dc015a5f3ffe4e091f5e1b1bb45eee2d9790cf15cbf82eace2" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/style.less", + "algorithm": "sha256-lf", + "sha256": "7148a1bd32f5d3d743db4a4ae8d39dc0e1406d2bff18df42cbb20e6705d5fed9" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/worker-client.js", + "algorithm": "sha256-lf", + "sha256": "aca4c442a5722787b28d6b4b0aaa50521fe71c4c0e63e17636dc5e2268cbd925" + }, + { + "path": "packages/artplayer-plugin-danmuku/src/worker.js", + "algorithm": "sha256-lf", + "sha256": "05995a16cd728dd5ca513587a777b4be9012b8cc4b77e3a46fed1826e6769c53" + }, + { + "path": "playwright.config.js", + "algorithm": "sha256-lf", + "sha256": "df2384f5626252ec933725867e400ec22eb92bc5e6f04873059b32decf2f2d56" + }, + { + "path": "test/browser/danmuku-scheduler.spec.js", + "algorithm": "sha256-lf", + "sha256": "506cc9dc75be822ea79d51c7e8fe6a68d526b41f26af626401d1d60329cbb724" + }, + { + "path": "test/browser/fixtures.js", + "algorithm": "sha256-lf", + "sha256": "313414b8958eb5b03a2da8595fa0bbf6bfdc01484373898c6cb7179545df175d" + }, + { + "path": "test/browser/server.mjs", + "algorithm": "sha256-lf", + "sha256": "1406fac75eabc83b987e79ec7b5f97a948b27770bfd5f5290d7cb9155196f08f" + }, + { + "path": "test/danmuku-input.test.js", + "algorithm": "sha256-lf", + "sha256": "bccd24096dabd5270ba3624dc071d56a492cf44b3045c78ae261d4510ae6e712" + }, + { + "path": "test/danmuku-parser.test.js", + "algorithm": "sha256-lf", + "sha256": "ae2dae43f447c1361662f70e90009c8978e1d96bbb133b7b07baafc9d765d70e" + }, + { + "path": "test/danmuku-scheduler.test.js", + "algorithm": "sha256-lf", + "sha256": "7c1c3d81c4cc37f1ddcd18f3c35c8825061887ada6e1f395cb4d8efb73f66727" + }, + { + "path": "test/danmuku-worker-client.test.js", + "algorithm": "sha256-lf", + "sha256": "55d7754224afa5567cafd880c28d923b9ff0e0661ecfb4ea1d9c923ddb84ae3b" + }, + { + "path": "test/helpers/danmuku-candidate.js", + "algorithm": "sha256-lf", + "sha256": "215aa631baa36f2434e95fc11f178fd9a4cca71834db0dd5e93d06356674445a" + } + ], + "decisions": [ + "Single pending RAF or async frame, generation and node ownership after awaits/callbacks.", + "Unique correlated Worker IDs and pending settlement on cancellation/failure/disposal; unchanged actual track algorithm.", + "Preserve public facade and ordinary repeated start/stop events, timing, pool order, callback receiver and configuration semantics.", + "Guard recovery reentry using generation and start-attempt identity as well as lifecycle/fault state.", + "Restore legacy NaN currentTime+0.5 fallback while retaining corrected zero and old positive/negative Infinity behavior." + ], + "limitations": [ + "JS responsibility separation; full owned TS and root/runtime installed declarations remain task06.", + "Synthetic long-clock regression is not an hours-long native workload or heap benchmark; package stability acceptance remains task07.", + "Native recovery fault events and constructor failure use explicit seams; not real CSP/SDK constructor-failure coverage.", + "ESM built and dist/docs bytes match; ESM is not counted as an additional runtime-tested format.", + "Settings external DOM/timer/late-send and heatmap issues remain open for task05.", + "Windows WebKit is not physical Safari. Local fixture video does not establish arbitrary remote content compatibility.", + "Intermediate 48/60 native matrices passed before later edge-case discoveries; final acceptance is only the five numeric-final archives listed above." + ], + "releaseReady": false +} diff --git a/refactor/changes/2026-09-13-PKG-DANMUKU-04-scheduler.md b/refactor/changes/2026-09-13-PKG-DANMUKU-04-scheduler.md new file mode 100644 index 000000000..2c7d9308e --- /dev/null +++ b/refactor/changes/2026-09-13-PKG-DANMUKU-04-scheduler.md @@ -0,0 +1,84 @@ +# PKG-DANMUKU-04 时钟、队列与轨道调度 + +弹幕调度现在只有一个待执行RAF或异步准备帧。新增`src/scheduler.js`负责运行归属、 +取消、失败和恢复,`src/queue.js`负责状态池与候选顺序,`src/worker-client.js`负责 +请求关联和Worker收尾。`danmuku.js`保留公共行为出口;原`worker.js`轨道算法逐字未改。 +本步仍是JS职责拆分,完整TS和最新npm根声明/准确`/runtime`归06。 + +## 行为与兼容边界 + +- 原生play/playing可以重复进入start,仍保留旧start事件;内部只能有一条运行链。 + 队列先ready后wait,保留当前媒体时间前后0.1秒窗口、状态池顺序和原轨道几何。 +- 暂停、reset、替换输入成功提交、hide、seeking、destroy以及更换beforeVisible + 使旧准备工作失效。取消与用户Promise竞速,新工作不必等待旧回调结束。 + 每次await及用户回调后检查归属;回收前检查节点身份,避免旧续体回收新节点。 +- hide、seeking和回调更新保留已显示/暂停节点;seek不额外发reset。Date.now寿命 + 排除暂停时间;同步倍率仍只在新弹幕分配时取值,不顺便改写现有弹幕剩余寿命。 +- beforeVisible拒绝沿现有error出口发原错一次,本轮跳过该项并继续其他项;false + 仍可下一帧重试。显式start/reset或其他失效动作允许仍符合时间条件的失败项重试。 + 错误监听器自身抛错有局部观察,不产生脱离调用链的未处理RAF拒绝。 +- Worker共享回复监听在post前安装,ID在请求及Worker替换之间递增,避免同毫秒串线。 + 反序、同步、重复、未知和迟到回复不会完成其他请求。取消的内部请求解析为 + `{ id, result: undefined }`;故障以原错拒绝待处理请求,独立尝试解绑及terminate。 +- Worker故障停止内部调度但不伪造公共stop。后续start/reset可重建;重复失败等待 + 下次显式恢复。错误监听器可能同步destroy/stop/reset/hide或嵌套start,因此恢复 + 后再次检查生命周期、generation和启动序号,过期start不能继续或发迟到start事件。 +- 门面方法、getter、同步/异步返回及内部实例身份保持。配置回调保持当前option为this。 + 合法的NaN时间保持旧currentTime+0.5;零保持03明确修正后的0,正无穷保持原值, + 负无穷仍钳位0,不扩大原验证器对null等输入的接受范围。 + +## 发现的问题先修复,再保留证据 + +最初18项调度测试为6通过12失败;03实际main在两核心的8个Chromium场景为4通过4失败, +具体包括六条弹幕产生七个请求、同ID及空visible快照、节点复用时留下额外隐藏节点。 +这个03快照本身未重叠,02另有真实重叠记录;没有把不同运行的现象拼成同一次失败。 + +第一轮候选48项原生矩阵通过后,只读代理发现恢复错误监听器销毁播放器仍继续start。 +补齐destroy/stop/reset/hide/嵌套start五个回归,保存的修复前真实main在32项中27绿5红, +加入恢复所有权检查后全部转绿。随后复审发现03的`time !== undefined`在修正零时 +误改合法NaN:历史源码/main/legacy均沿旧默认时间,候选24项23绿1红。保留零的显式 +判断并恢复NaN默认路径,增加历史对照和真实浏览器可见性断言;03旧报告保持原样。 + +这些修复作为本步变更记录,新增已解决风险INPUT-02;没有把中间版本的通过当作最终 +验收。父代理负责复审、构建、浏览器和整合,子代理分别承担调度实现、确定性测试及 +独立风险复核,同一文件分工不重叠,Git由父代理统一提交。 + +## 最终验证 + +- 源码、正式main、正式legacy各104项联合通过。每组中83项随候选实现变化,18项 + 直接验证内部Worker客户端/原轨道源码,3项固定历史数值对照。不能把后21项重复 + 宣称为不同产物的额外覆盖。组成:输入24、解析26、调度33、Worker客户端21。 +- 确定性时钟覆盖三小时媒体范围、180批共720条、15次120秒暂停及暂停中seek, + 执行真实Worker算法,检查每条一次请求、发射顺序、唯一状态池及4节点全程复用。 + 这是采样生命周期的模拟时钟测试,不是实际三小时播放或帧率/内存基准。 +- 三种实现各66项原生浏览器通过,共198项:实际已发布核心5.4.0及候选5.4.1, + Chromium/Firefox/Windows WebKit。包括三模式、严格轨道几何、播放/暂停/跳转/倍率、 + 节点复用、四种pending取消、恢复重入、NaN、实际设置/发送/mount和双实例。 +- main和legacy额外各30次密集场景重复,共60次,无跳过、失败或重试后通过。 + 每次六条固定弹幕都断言几何不交叠、顺序、六个唯一请求、visible快照长度0至5、 + 回复关联及六节点;复用场景另断言同一可见节点且额外节点数组为空。 +- 普通场景要求零页面错误、零Worker错误;恢复场景显式派发受控ErrorEvent并让 + createWorker抛错,仅允许精确已知故障。实际视频/DOM/Blob Worker继续使用原生实现, + 但不能把注入故障描述成真实CSP或平台Worker构造失败。Windows WebKit不是Safari设备。 +- 仓库正常build生成三格式及docs/compiled,逐一核对字节;修改源码/测试/脚本入口lint + 通过。04未改生产TS或公开声明,引用03全仓366生产TS和消费者检查,未声称04新跑TS。 + +构建曾在浏览器运行期间遇Windows UNKNOWN复制错误;停止并行构建后正常串行重建成功, +核对main/legacy与当时已测试文件哈希一致。NaN修复后再次完整构建并运行上述最终矩阵。 +故障日志、串行重试、字节一致性证明及最终结果均保留。新增所有权逻辑使产物变大, +机器证据记录相对03的原始/gzip字节,不能声称本步缩包或性能改善。 + +证据及输入指纹见[机器报告](../baselines/danmuku-scheduler-validation.json)。原始截图和 +附件归档独立目录,报告记录原results路径到归档路径的映射,不手改原Playwright报告。 +未新增依赖或修改Yarn锁;`test:unit`和`test:danmuku`加入调度/Worker客户端测试,新增 +原生spec沿既有浏览器CI发现。中间48/60项矩阵保存为过程证据,最终引用numeric-final。 + +## 风险关闭与下一步 + +ASYNC-01由03输入/解析及04可见性/Worker归属共同满足关闭标准;TRACK-01关闭范围仅为 +已复现的重复运行链和请求串线,以及对应严格几何/节点重复回归。它们不代表任意负载、 +任意轨道算法或包级资源稳定性已经验收。CLEANUP-01保持开放:Setting外部DOM、发送 +续体和锁定计时器仍需05处理;heatmap零采样同样未修。05继续DOM渲染/设置/热力图, +06完整TS与声明,07真实长负载。完整npm安装、发布前多轮复盘仍待各自任务。 + +本任务完成后独立本地commit,无推送、标签或npm发布。 diff --git a/refactor/plan.md b/refactor/plan.md index cb05329bf..88f67b651 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -4,7 +4,7 @@ 基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 228 项,范围 22 个包及工作区/示例。 -状态:todo 71 / doing 15 / blocked 0 / done 142 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 +状态:todo 70 / doing 15 / blocked 0 / done 143 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 @@ -312,8 +312,8 @@ | PKG-DANMUKU-01 | artplayer-plugin-danmuku
核对弹幕全部公开契约 | BASE-05 | emit/load/config/hide/show/reset/mount、option/isHide/isStop/icons 与事件清单 | 源码、声明、发布包、Bilibili 输入和设置入口对照 | H | done | | PKG-DANMUKU-02 | artplayer-plugin-danmuku
建立弹幕算法与浏览器基线 | PKG-DANMUKU-01, ENG-05, ENG-08 | 密集/稀疏弹幕、过滤/异步输入、seek/倍率/重载测试 | 轨道选择、发射顺序、事件、内存和可视结果可重跑 | H | done | | PKG-DANMUKU-03 | artplayer-plugin-danmuku
整理加载、解析与配置 | PKG-DANMUKU-02, CORE-08 | bilibili/input/parser/config 的分层与取消 | 旧格式、回调、过滤、追加/替换语义保持 | M | done | -| PKG-DANMUKU-04 | artplayer-plugin-danmuku
整理时钟、队列与轨道调度 | PKG-DANMUKU-03, CORE-10 | danmuku 调度器和确定性时钟测试 | seek/暂停/倍率/长时间运行无顺序和碰撞回归 | H | doing | -| PKG-DANMUKU-05 | artplayer-plugin-danmuku
整理 DOM 渲染、设置、热力图与 worker | PKG-DANMUKU-04, CORE-14, CORE-18 | renderer/setting/heatmap/worker 职责及资源归属 | mount/icons/设置和 worker 协议保持,销毁无后台工作 | H | todo | +| PKG-DANMUKU-04 | artplayer-plugin-danmuku
整理时钟、队列与轨道调度 | PKG-DANMUKU-03, CORE-10 | danmuku 调度器和确定性时钟测试 | seek/暂停/倍率/长时间运行无顺序和碰撞回归 | H | done | +| PKG-DANMUKU-05 | artplayer-plugin-danmuku
整理 DOM 渲染、设置、热力图与 worker | PKG-DANMUKU-04, CORE-14, CORE-18 | renderer/setting/heatmap/worker 职责及资源归属 | mount/icons/设置和 worker 协议保持,销毁无后台工作 | H | doing | | PKG-DANMUKU-06 | artplayer-plugin-danmuku
迁移 TS 与公开声明 | PKG-DANMUKU-05, ENG-04, ENG-06 | 自有模块、worker 消息、option/item/result 类型 | 旧参数、扩展字段和事件消费通过,vendored 边界清楚 | H | todo | | PKG-DANMUKU-07 | artplayer-plugin-danmuku
弹幕能力稳定性验收 | PKG-DANMUKU-06 | 负载/渲染/资源对比及 mask 可依赖的稳定边界 | 在试点核心中旧插件 API 与关键帧路径通过 | H | todo | | PKG-DANMUKU-08 | artplayer-plugin-danmuku
完成新旧核心与组合验收 | PKG-DANMUKU-07, CORE-22 | 最终核心/旧核心、mask/fullscreen/PiP 组合报告 | 性能无未解释退化,反复装卸无累计资源 | H | todo | @@ -556,6 +556,7 @@ - PKG-DANMUKU-01: [记录](baselines/danmuku-release.json) [记录](baselines/danmuku-contract.md) [记录](baselines/danmuku-contract-validation.json) [记录](changes/2026-09-13-PKG-DANMUKU-01-contracts.md) - PKG-DANMUKU-02: [记录](changes/2026-09-13-PKG-DANMUKU-02-controlled-failures.md) [记录](baselines/danmuku-failures-validation.json) [记录](changes/2026-09-13-PKG-DANMUKU-02-browser-baseline.md) [记录](baselines/danmuku-browser-validation.json) - PKG-DANMUKU-03: [记录](changes/2026-09-13-PKG-DANMUKU-03-input.md) [记录](baselines/danmuku-input-validation.json) +- PKG-DANMUKU-04: [记录](changes/2026-09-13-PKG-DANMUKU-04-scheduler.md) [记录](baselines/danmuku-scheduler-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) diff --git a/refactor/progress.md b/refactor/progress.md index e30f43e5f..ceb80070e 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,16 @@ # 进度与证据 +## PKG-DANMUKU-04 时钟、队列与轨道调度完成 + +拆分scheduler/queue/worker-client,修复多RAF、同毫秒请求串线、取消后续写、 +节点重复分配、异步拒绝及恢复重入;复审修复03引入的NaN兼容回归。 +源码/main/legacy各104联合测试、三格式共198真实浏览器项及60次额外密集场景通过; +完整构建、lint通过。关闭ASYNC-01/TRACK-01,Settings/heatmap/完整TS/长负载仍待后续。 +见[变更](changes/2026-09-13-PKG-DANMUKU-04-scheduler.md)及[机器证据](baselines/danmuku-scheduler-validation.json)。 +228项:143 done、15 doing、70 todo;05进入实施。 +没有推送或发布。 + + ## PKG-DANMUKU-03 输入、解析和配置源码完成 拆分input/config/XML parser模块,修复过期替换、加载拒绝、资源清理、time0、函数配置 diff --git a/refactor/risk-table.md b/refactor/risk-table.md index 203449fa2..c21b56dd3 100644 --- a/refactor/risk-table.md +++ b/refactor/risk-table.md @@ -232,7 +232,8 @@ | DANMUKU-CONFIG-01 | resolved / 已复现 | Danmuku JSON configuration comparison ignores a replacement filter function | PKG-DANMUKU-02, PKG-DANMUKU-03 | | DANMUKU-CLEANUP-01 | open / 已复现 | Danmuku removes a different resize callback and retains the settings lock timer on destroy | PKG-DANMUKU-02, PKG-DANMUKU-05 | | DANMUKU-TYPE-02 | open / 源码/产物事实 | Danmuku id, icons, Slider steps, heatmap points and partial configuration drift from declarations | PKG-DANMUKU-05, PKG-DANMUKU-06 | -| DANMUKU-ASYNC-01 | open / 已复现 | Danmuku asynchronous input, worker replies and pending visibility have unverified failure and cancellation paths | PKG-DANMUKU-02, PKG-DANMUKU-03, PKG-DANMUKU-04, PKG-DANMUKU-05 | +| DANMUKU-ASYNC-01 | resolved / 已复现 | Danmuku asynchronous input, worker replies and pending visibility have unverified failure and cancellation paths | PKG-DANMUKU-02, PKG-DANMUKU-03, PKG-DANMUKU-04, PKG-DANMUKU-05 | | DANMUKU-HEATMAP-01 | open / 已复现 | Danmuku heatmap sampling can become zero and points/instance ownership need browser validation | PKG-DANMUKU-02, PKG-DANMUKU-05 | | DANMUKU-SOURCE-01 | open / 已复现 | Danmuku registry Git associations differ from releases and static icon provenance remains incomplete | PKG-DANMUKU-09 | -| DANMUKU-TRACK-01 | open / 已复现 | Native simultaneous fixed Danmuku comments can overlap with duplicate start loops and repeated Worker IDs | PKG-DANMUKU-04, PKG-DANMUKU-05, PKG-DANMUKU-07 | +| DANMUKU-TRACK-01 | resolved / 已复现 | Native simultaneous fixed Danmuku comments can overlap with duplicate start loops and repeated Worker IDs | PKG-DANMUKU-04, PKG-DANMUKU-05, PKG-DANMUKU-07 | +| DANMUKU-INPUT-02 | resolved / 已复现 | Intermediate explicit-zero correction regressed accepted NaN timestamp fallback | PKG-DANMUKU-04 | diff --git a/refactor/risks.json b/refactor/risks.json index b6b9f53c1..861cd6daa 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -5106,7 +5106,9 @@ "refactor/changes/2026-09-13-PKG-DANMUKU-02-browser-baseline.md", "refactor/baselines/danmuku-browser-validation.json", "refactor/changes/2026-09-13-PKG-DANMUKU-03-input.md", - "refactor/baselines/danmuku-input-validation.json" + "refactor/baselines/danmuku-input-validation.json", + "refactor/changes/2026-09-13-PKG-DANMUKU-04-scheduler.md", + "refactor/baselines/danmuku-scheduler-validation.json" ], "compatibleResolution": "Track and release exact subscription and timer ownership across settings, renderer and instances.", "closureCriteria": "Repeated lifecycle and pending settings actions leave no owned subscriptions, timers or late DOM writes." @@ -5132,7 +5134,7 @@ "id": "DANMUKU-ASYNC-01", "title": "Danmuku asynchronous input, worker replies and pending visibility have unverified failure and cancellation paths", "confirmation": "reproduced", - "status": "open", + "status": "resolved", "owners": [ "PKG-DANMUKU-02", "PKG-DANMUKU-03", @@ -5148,10 +5150,20 @@ "refactor/changes/2026-09-13-PKG-DANMUKU-02-browser-baseline.md", "refactor/baselines/danmuku-browser-validation.json", "refactor/changes/2026-09-13-PKG-DANMUKU-03-input.md", - "refactor/baselines/danmuku-input-validation.json" + "refactor/baselines/danmuku-input-validation.json", + "refactor/changes/2026-09-13-PKG-DANMUKU-04-scheduler.md", + "refactor/baselines/danmuku-scheduler-validation.json" ], "compatibleResolution": "Reproduce load ordering, Bilibili rejection and late worker/visibility paths before defining operation ownership.", - "closureCriteria": "Failures settle, obsolete results cannot alter new queues or destroyed instances, and worker resources are released with browser evidence." + "closureCriteria": "Failures settle, obsolete results cannot alter new queues or destroyed instances, and worker resources are released with browser evidence.", + "resolutionEvidence": [ + "refactor/changes/2026-09-13-PKG-DANMUKU-04-scheduler.md", + "refactor/baselines/danmuku-scheduler-validation.json", + "test/danmuku-scheduler.test.js", + "test/danmuku-worker-client.test.js", + "test/browser/danmuku-scheduler.spec.js" + ], + "resolutionRationale": "Task03 input/parser settlement and task04 worker/visibility ownership prevent stale continuation, including recovery reentry; final native matrices verify worker disposal. Settings/heatmap cleanup remains separately open." }, { "id": "DANMUKU-HEATMAP-01", @@ -5194,7 +5206,7 @@ "id": "DANMUKU-TRACK-01", "title": "Native simultaneous fixed Danmuku comments can overlap with duplicate start loops and repeated Worker IDs", "confirmation": "reproduced", - "status": "open", + "status": "resolved", "owners": [ "PKG-DANMUKU-04", "PKG-DANMUKU-05", @@ -5204,10 +5216,42 @@ "refactor/changes/2026-09-13-PKG-DANMUKU-02-browser-baseline.md", "refactor/baselines/danmuku-browser-validation.json", "test/browser/danmuku-baseline.spec.js", - "test/danmuku-failures.test.js" + "test/danmuku-failures.test.js", + "refactor/changes/2026-09-13-PKG-DANMUKU-04-scheduler.md", + "refactor/baselines/danmuku-scheduler-validation.json" ], "compatibleResolution": "Enforce one owned scheduler loop and request correlation before claiming simultaneous native anti-overlap stability; keep strict track geometry tests.", - "closureCriteria": "Deterministic duplicate-loop/worker regressions and repeated candidate native simultaneous-load geometry pass without extra nodes or stale state; a later passing old snapshot alone does not close the race." + "closureCriteria": "Deterministic duplicate-loop/worker regressions and repeated candidate native simultaneous-load geometry pass without extra nodes or stale state; a later passing old snapshot alone does not close the race.", + "resolutionEvidence": [ + "refactor/changes/2026-09-13-PKG-DANMUKU-04-scheduler.md", + "refactor/baselines/danmuku-scheduler-validation.json", + "test/danmuku-scheduler.test.js", + "test/danmuku-worker-client.test.js", + "test/browser/danmuku-scheduler.spec.js" + ], + "resolutionRationale": "Single owned loop, correlated IDs and unchanged real geometry algorithm pass deterministic regressions, all native matrices and 60 additional repeated dense cases with strict geometry, request and node counts; broad long-load acceptance remains task07." + }, + { + "id": "DANMUKU-INPUT-02", + "title": "Intermediate explicit-zero correction regressed accepted NaN timestamp fallback", + "confirmation": "reproduced", + "status": "resolved", + "owners": [ + "PKG-DANMUKU-04" + ], + "evidence": [ + "refactor/changes/2026-09-13-PKG-DANMUKU-04-scheduler.md", + "refactor/baselines/danmuku-scheduler-validation.json", + "test/danmuku-input.test.js" + ], + "compatibleResolution": "Preserve old NaN default currentTime+0.5, corrected explicit zero and old Infinity normalization without widening validation.", + "closureCriteria": "Frozen history and current candidate numeric comparisons pass, and actual native candidate renders NaN input.", + "resolutionEvidence": [ + "refactor/changes/2026-09-13-PKG-DANMUKU-04-scheduler.md", + "refactor/baselines/danmuku-scheduler-validation.json", + "test/browser/danmuku-scheduler.spec.js" + ], + "resolutionRationale": "Historical source/main/legacy establish old behavior; source/main/legacy final input and native matrices confirm the one-condition correction. Task03 frozen evidence is preserved." } ] } diff --git a/refactor/tasks.json b/refactor/tasks.json index 04410bb68..47c090a99 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -3320,11 +3320,14 @@ "PKG-DANMUKU-03", "CORE-10" ], - "status": "doing", + "status": "done", "risk": "H", "deliverable": "danmuku 调度器和确定性时钟测试", "acceptance": "seek/暂停/倍率/长时间运行无顺序和碰撞回归", - "evidence": [] + "evidence": [ + "changes/2026-09-13-PKG-DANMUKU-04-scheduler.md", + "baselines/danmuku-scheduler-validation.json" + ] }, { "id": "PKG-DANMUKU-05", @@ -3338,7 +3341,7 @@ "CORE-14", "CORE-18" ], - "status": "todo", + "status": "doing", "risk": "H", "deliverable": "renderer/setting/heatmap/worker 职责及资源归属", "acceptance": "mount/icons/设置和 worker 协议保持,销毁无后台工作", diff --git a/test/browser/danmuku-scheduler.spec.js b/test/browser/danmuku-scheduler.spec.js new file mode 100644 index 000000000..5005bbd4d --- /dev/null +++ b/test/browser/danmuku-scheduler.spec.js @@ -0,0 +1,444 @@ +import fs from 'node:fs' +import process from 'node:process' +import { hash } from '../../refactor/scripts/releases.mjs' +import { compilePackage } from '../helpers/load.js' +import { expect, test } from './fixtures.js' + +let candidate +test.beforeAll(async () => { + candidate = process.env.ARTPLAYER_DANMUKU_ARTIFACT + ? fs.readFileSync(process.env.ARTPLAYER_DANMUKU_ARTIFACT, 'utf8') + : await compilePackage('artplayer-plugin-danmuku', 'umd') +}) +async function setup(page, core, format, scenario, testInfo) { + const external = [] + const origin = new URL(testInfo.project.use.baseURL).origin + await page.route('**/*', (route) => { + const url = new URL(route.request().url()) + if (url.origin !== origin) { + external.push(url.href) + return route.abort() + } + // Same-origin blob URLs are actual inline Worker/media resources, not external requests. + return route.continue() + }) + await page.goto(`/test/player.html?core=${core}`) + await page.evaluate(() => { + window.danmukuEvidence = { workers: [], events: [], gates: [], sends: [], failures: [] } + const NativeWorker = window.Worker + window.Worker = class ObservedNativeWorker extends NativeWorker { + constructor(url, options) { + super(url, options) + this.evidence = { url: String(url), requests: [], replies: [], terminated: 0, errors: [] } + window.danmukuEvidence.workers.push(this.evidence) + this.addEventListener('message', ({ data }) => this.evidence.replies.push({ id: data.id, result: data.result ?? null })) + this.addEventListener('error', event => this.evidence.errors.push(event.message)) + } + + terminate() { + this.evidence.terminated++ + return super.terminate() + } + + postMessage(message, ...args) { + this.evidence.requests.push(structuredClone(message)) + return super.postMessage(message, ...args) + } + } + window.danmukuPlayers = [] + window.danmukuPlugins = [] + window.danmukuNodes = [] + window.danmukuVisibility = new Promise((resolve) => { + window.releaseDanmukuVisibility = resolve + }) + window.danmukuSending = new Promise((resolve) => { + window.releaseDanmukuSending = resolve + }) + }) + const implementation = { code: candidate, sha256: hash(candidate) } + await page.addScriptTag({ content: implementation.code }) + await page.evaluate(({ scenario }) => { + const first = document.querySelector('.player') + first.id = 'danmuku-player-0' + first.style.cssText = 'width:640px;height:360px' + window.createDanmukuPlayer = (index) => { + const container = index === 0 ? first : document.body.appendChild(document.createElement('div')) + container.id = `danmuku-player-${index}` + container.style.cssText = 'width:640px;height:360px' + const mount = document.createElement('div') + mount.id = `danmuku-mount-${index}` + mount.style.cssText = 'width:640px;height:36px;background:#333' + container.after(mount) + const art = new window.Artplayer({ container, url: `/test/pattern.mp4?danmuku=${index}`, muted: true, mutex: false, fullscreenWeb: true }) + window.danmukuPlayers[index] = art + for (const event of ['loaded', 'visible', 'start', 'stop', 'reset', 'destroy', 'config', 'error']) { + art.on(`artplayerPluginDanmuku:${event}`, (value) => { + window.danmukuEvidence.events.push({ index, event, time: art.currentTime, id: value?.id, text: value?.text, mode: value?.mode, count: Array.isArray(value) ? value.length : undefined }) + if (event === 'error') + window.danmukuEvidence.failures.push(String(value)) + }) + } + const option = { + danmuku: [], + heatmap: false, + speed: 8, + margin: [10, 10], + fontSize: 20, + antiOverlap: true, + synchronousPlayback: true, + lockTime: 1, + ...(scenario === 'settings' ? { mount } : {}), + filter: danmu => danmu.text !== 'FILTERED', + beforeVisible: async (danmu) => { + window.danmukuEvidence.gates.push({ index, id: danmu.id, text: danmu.text }) + if (danmu.text === 'GATED') + return window.danmukuVisibility + return danmu.text !== 'DENIED' + }, + beforeEmit: async (danmu) => { + window.danmukuEvidence.sends.push({ index, text: danmu.text, mode: danmu.mode }) + return scenario === 'settings' && index === 0 ? window.danmukuSending : true + }, + } + art.plugins.add(window.artplayerPluginDanmuku(option)) + window.danmukuPlugins[index] = art.plugins.artplayerPluginDanmuku + return art + } + window.art = window.createDanmukuPlayer(0) + document.querySelector('#play').onclick = () => window.danmukuPlayers[0].play() + document.querySelector('#pause').onclick = () => window.danmukuPlayers[0].pause() + }, { scenario }) + await expect.poll(() => page.evaluate(() => window.art.isReady && window.art.template.$video.videoWidth > 0)).toBe(true) + await testInfo.attach('danmuku-browser-inputs', { contentType: 'application/json', body: JSON.stringify({ core, plugin: 'artplayer-plugin-danmuku', format, artifact: process.env.ARTPLAYER_DANMUKU_ARTIFACT || 'in-memory source UMD', sha256: implementation.sha256, media: '/test/pattern.mp4', scenario, worker: 'Native Blob Worker, only constructor/message/termination observation; no Worker responses, media clocks, layout or RAF mocked', heatmap: false }) }) + return external +} + +async function geometry(page) { + return page.evaluate(() => (window.danmukuPlayers || []).map((art, index) => { + const player = art.template.$player.getBoundingClientRect() + return { + index, + playing: art.playing, + time: art.currentTime, + rate: art.playbackRate, + player: { x: player.x, y: player.y, width: player.width, height: player.height }, + nodes: [...art.template.$danmuku.children].map((node) => { + const rect = node.getBoundingClientRect() + const style = getComputedStyle(node) + return { id: node.dataset.id, mode: node.dataset.mode, state: node.dataset.state, text: node.textContent, x: rect.x - player.x, y: rect.y - player.y, width: rect.width, height: rect.height, visibility: style.visibility, display: style.display, transition: node.style.transition, opacity: style.opacity } + }), + } + })) +} + +const visibleIds = page => page.evaluate(() => [...new Set(window.danmukuEvidence.events.filter(item => item.event === 'visible' && item.id).map(item => item.id))]) + +test.afterEach(async ({ page }, testInfo) => { + await testInfo.attach('danmuku-layout', { contentType: 'application/json', body: JSON.stringify(await geometry(page)) }) + await testInfo.attach('danmuku-native-page', { contentType: 'image/png', body: await page.screenshot() }) + await page.evaluate(() => { + for (const art of window.danmukuPlayers || []) { + if (!art.isDestroy) + art.destroy() + } + }) + const evidence = await page.evaluate(() => ({ workers: [], failures: [], ...window.danmukuEvidence, expectedFailures: window.expectedDanmukuFailures || [], version: window.Artplayer?.version, retainedExternalSettings: [...document.querySelectorAll('[id^="danmuku-mount-"]')].map(node => node.querySelectorAll('.artplayer-plugin-danmuku').length) })) + await testInfo.attach('danmuku-worker-and-lifecycle', { contentType: 'application/json', body: JSON.stringify(evidence) }) + expect(evidence.failures).toEqual(evidence.expectedFailures) + for (const worker of evidence.workers) { + expect(worker.url).toMatch(/^blob:/u) + expect(worker.errors).toEqual(evidence.expectedFailures.length ? ['controlled track failure'] : []) + expect(worker.terminated).toBe(1) + } +}) + +for (const core of ['published', 'candidate']) { + for (const format of ['candidate']) { + const label = `${core} / Candidate Danmuku ${format}` + + test(`${label}: accepted NaN timestamp keeps the historical current-time fallback`, async ({ page }, testInfo) => { + const external = await setup(page, core, format, 'modes', testInfo) + const numeric = await page.evaluate(async () => { + const input = { id: 'nan-time', text: 'NAN FALLBACK', mode: 1, time: Number.NaN } + const expected = window.art.currentTime + 0.5 + const owner = await window.danmukuPlugins[0].emit(input) + return { expected, inputTime: input.time, queueTime: owner.queue[0].time, isNaN: Number.isNaN(input.time) } + }) + expect(numeric.isNaN).toBe(false) + expect(numeric.inputTime).toBe(numeric.expected) + expect(numeric.queueTime).toBe(numeric.expected) + await page.click('#play') + await expect.poll(() => visibleIds(page)).toContain('nan-time') + expect(external).toEqual([]) + await testInfo.attach('danmuku-native-numeric-compatibility', { contentType: 'application/json', body: JSON.stringify(numeric) }) + }) + + for (const action of ['destroy', 'stop']) { + test(`${label}: failed recovery respects synchronous ${action} from the error listener`, async ({ page }, testInfo) => { + const external = await setup(page, core, format, 'modes', testInfo) + await page.evaluate(() => window.danmukuPlugins[0].emit({ id: 'recovery-row', text: 'RECOVERY', mode: 1, time: 0.5 })) + await page.click('#play') + await expect.poll(() => visibleIds(page)).toContain('recovery-row') + const result = await page.evaluate((action) => { + const owner = window.danmukuPlugins[0].config({}) + const row = owner.queue[0] + const trackError = new Error('controlled track failure') + const rebuildError = new Error('controlled track reconstruction failure') + window.expectedDanmukuFailures = ['Error: controlled track failure', 'Error: controlled track reconstruction failure'] + owner.worker.dispatchEvent(new ErrorEvent('error', { error: trackError, message: trackError.message, cancelable: true })) + owner.stop() + owner.createWorker = () => { + throw rebuildError + } + window.art.on('artplayerPluginDanmuku:error', (error) => { + if (error === rebuildError) { + if (action === 'destroy') + window.art.destroy() + else owner.stop() + } + }) + const before = window.danmukuEvidence.events.length + const sameOwner = owner.start() === owner + return { sameOwner, rowState: row.$state, isStop: owner.isStop, isDestroy: window.art.isDestroy, events: window.danmukuEvidence.events.slice(before), workers: window.danmukuEvidence.workers } + }, action) + expect(result.sameOwner).toBe(true) + expect(result.rowState).toBe('stop') + expect(result.isStop).toBe(true) + expect(result.isDestroy).toBe(action === 'destroy') + expect(result.events.filter(event => event.event === 'start')).toEqual([]) + expect(result.workers).toHaveLength(1) + expect(result.workers[0].terminated).toBe(1) + expect(external).toEqual([]) + await testInfo.attach('danmuku-native-recovery-reentry', { contentType: 'application/json', body: JSON.stringify({ action, ...result, fault: 'Actual video/DOM/track Worker; dispatch an explicit native ErrorEvent and inject a throwing createWorker recovery seam. This does not claim a real CSP or browser Worker construction failure.' }) }) + }) + } + + for (const action of ['pause', 'reset', 'seek', 'destroy']) { + test(`${label}: native ${action} cancels pending visibility without blocking later work`, async ({ page }, testInfo) => { + const external = await setup(page, core, format, 'modes', testInfo) + await page.evaluate(() => window.danmukuPlugins[0].emit({ id: 'obsolete-gate', text: 'GATED', mode: 1, time: 0.5 })) + await page.click('#play') + await expect.poll(() => page.evaluate(() => window.danmukuEvidence.gates.some(item => item.id === 'obsolete-gate'))).toBe(true) + if (action === 'pause') { + await page.click('#pause') + await expect.poll(() => page.evaluate(() => window.danmukuPlugins[0].isStop)).toBe(true) + } + if (action === 'seek') { + await page.evaluate(async () => { + const sought = new Promise(resolve => window.art.once('video:seeked', resolve)) + window.art.currentTime = 2 + await sought + }) + } + else if (action === 'destroy') { + await page.evaluate(() => window.art.destroy()) + } + else { + await page.evaluate(() => window.danmukuPlugins[0].load()) + } + if (action !== 'destroy') { + await page.evaluate(() => window.danmukuPlugins[0].emit({ id: 'fresh-after-cancel', text: 'FRESH', mode: 1, time: window.art.currentTime + 0.5 })) + if (action === 'pause') + await page.click('#play') + // The old user Promise is still unresolved. A cancelled frame must + // release scheduler ownership so this newer item can be displayed. + await expect.poll(() => visibleIds(page)).toContain('fresh-after-cancel') + } + const before = await page.evaluate(() => ({ requests: window.danmukuEvidence.workers.map(worker => worker.requests.length), visible: window.danmukuEvidence.events.filter(event => event.event === 'visible').map(event => event.id) })) + await page.evaluate(() => window.releaseDanmukuVisibility(true)) + await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))) + expect(await visibleIds(page)).not.toContain('obsolete-gate') + if (action === 'destroy') { + expect(await page.evaluate(() => window.danmukuEvidence.workers.map(worker => worker.requests.length))).toEqual(before.requests) + expect(await page.evaluate(() => window.art.template.$danmuku.childElementCount)).toBe(0) + } + expect(external).toEqual([]) + await testInfo.attach('danmuku-native-cancel', { contentType: 'application/json', body: JSON.stringify({ action, before, after: await visibleIds(page), cancelledSource: 'An unresolved user beforeVisible Promise; native RAF, Worker and media remain active.' }) }) + }) + } + + test(`${label}: native Worker renders three modes with synchronous filter and asynchronous beforeVisible`, async ({ page }, testInfo) => { + const external = await setup(page, core, format, 'modes', testInfo) + await page.evaluate(async () => { + await window.danmukuPlugins[0].load([ + { id: 'scroll', text: 'SCROLL', mode: 0, time: 0.5 }, + { id: 'top', text: 'TOP', mode: 1, time: 0.5 }, + { id: 'bottom', text: 'BOTTOM', mode: 2, time: 0.5 }, + { id: 'filtered', text: 'FILTERED', mode: 1, time: 0.5 }, + { id: 'denied', text: 'DENIED', mode: 1, time: 0.5 }, + { id: 'gated', text: 'GATED', mode: 1, time: 0.5 }, + ]) + }) + await page.click('#play') + await expect.poll(() => visibleIds(page)).toEqual(expect.arrayContaining(['scroll', 'top', 'bottom'])) + await expect.poll(() => page.evaluate(() => window.danmukuEvidence.gates.some(item => item.id === 'gated'))).toBe(true) + expect(await visibleIds(page)).not.toContain('gated') + expect(await visibleIds(page)).not.toContain('filtered') + expect(await visibleIds(page)).not.toContain('denied') + expect(await page.evaluate(() => window.danmukuEvidence.gates.some(item => item.id === 'filtered'))).toBe(false) + await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))) + expect(await page.evaluate(() => window.danmukuEvidence.gates.filter(item => item.id === 'gated').length)).toBe(1) + await page.evaluate(() => window.releaseDanmukuVisibility(true)) + await expect.poll(() => visibleIds(page)).toContain('gated') + const [layout] = await geometry(page) + for (const id of ['top', 'bottom']) { + const item = layout.nodes.find(node => node.id === id && node.visibility === 'visible') + expect(item).toBeTruthy() + expect(Math.abs(item.x + item.width / 2 - layout.player.width / 2)).toBeLessThanOrEqual(2) + expect(item.y).toBeGreaterThanOrEqual(9) + expect(item.y + item.height).toBeLessThanOrEqual(layout.player.height - 8) + } + expect(layout.nodes.find(node => node.id === 'top').y).toBeLessThan(layout.nodes.find(node => node.id === 'bottom').y) + expect(await page.evaluate(() => window.danmukuEvidence.workers[0].replies.length)).toBeGreaterThanOrEqual(4) + expect(external).toEqual([]) + }) + + test(`${label}: six simultaneous fixed comments preserve usable track geometry and input order`, async ({ page }, testInfo) => { + const external = await setup(page, core, format, 'density', testInfo) + await page.evaluate(async () => { + await window.danmukuPlugins[0].load(Array.from({ length: 6 }, (_, index) => ({ id: `dense-${index}`, text: `TRACK ${index}`, mode: 1, time: 0.5 }))) + }) + await page.click('#play') + await expect.poll(() => visibleIds(page)).toEqual(expect.arrayContaining(Array.from({ length: 6 }, (_, index) => `dense-${index}`))) + await page.click('#pause') + await expect.poll(() => page.evaluate(() => window.danmukuPlugins[0].isStop)).toBe(true) + const [layout] = await geometry(page) + const rows = Array.from({ length: 6 }, (_, index) => layout.nodes.find(node => node.id === `dense-${index}` && node.visibility === 'visible')) + expect(rows.every(Boolean)).toBe(true) + for (let index = 0; index < rows.length; index++) { + expect(rows[index].y).toBeGreaterThanOrEqual(9) + expect(rows[index].y + rows[index].height).toBeLessThanOrEqual(layout.player.height - 8) + if (index) + expect(rows[index].y).toBeGreaterThanOrEqual(rows[index - 1].y + rows[index - 1].height - 1) + } + const order = await visibleIds(page) + expect(order).toEqual(Array.from({ length: 6 }, (_, index) => `dense-${index}`)) + const worker = await page.evaluate(() => window.danmukuEvidence.workers[0]) + expect(worker.requests).toHaveLength(6) + expect(new Set(worker.requests.map(request => request.id)).size).toBe(6) + expect(worker.requests.map(request => request.visibles.length)).toEqual([0, 1, 2, 3, 4, 5]) + expect(worker.replies.map(reply => reply.id)).toEqual(worker.requests.map(request => request.id)) + await testInfo.attach('danmuku-dense-tracks', { contentType: 'application/json', body: JSON.stringify(rows) }) + expect(external).toEqual([]) + }) + + test(`${label}: native pause seek rate and public reset/load retain historical pool semantics`, async ({ page }, testInfo) => { + const external = await setup(page, core, format, 'playback', testInfo) + await page.evaluate(async () => { + await window.danmukuPlugins[0].emit({ id: 'moving', text: 'MOVING', mode: 0, time: 0.5 }) + }) + await page.click('#play') + await expect.poll(() => visibleIds(page)).toContain('moving') + await page.click('#pause') + await expect.poll(() => page.evaluate(() => window.danmukuPlugins[0].isStop)).toBe(true) + const frozen = await page.evaluate(async () => { + const node = window.art.template.$danmuku.querySelector('[data-id="moving"]') + window.danmukuNodes = [node] + const left = node.getBoundingClientRect().left + await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))) + return { left, next: node.getBoundingClientRect().left, state: node.dataset.state } + }) + expect(frozen.state).toBe('stop') + expect(Math.abs(frozen.left - frozen.next)).toBeLessThanOrEqual(1) + await page.evaluate(async () => { + window.art.playbackRate = 2 + const sought = new Promise(resolve => window.art.once('video:seeked', resolve)) + window.art.currentTime = 2 + await sought + }) + expect(await page.evaluate(() => window.danmukuNodes[0].dataset.state)).toBe('stop') + await page.evaluate(async () => { + window.danmukuPlugins[0].reset() + await window.danmukuPlugins[0].emit({ id: 'reused', text: 'REUSED', mode: 0, time: window.art.currentTime + 0.5 }) + }) + await page.click('#play') + await expect.poll(() => visibleIds(page)).toContain('reused') + const reused = await page.evaluate(() => { + const node = window.art.template.$danmuku.querySelector('[data-id="reused"]') + const original = window.danmukuNodes[0] + const nodes = [...window.art.template.$danmuku.children] + return { + poolReused: original.textContent === 'REUSED' && nodes.includes(original), + visibleSameNode: node === original, + original: { id: original.dataset.id, state: original.dataset.state, visibility: getComputedStyle(original).visibility, text: original.textContent }, + extraNodes: nodes.filter(item => item !== original).map(item => ({ id: item.dataset.id, state: item.dataset.state, visibility: getComputedStyle(item).visibility, text: item.textContent })), + transition: node.style.transitionDuration, + rate: window.art.playbackRate, + } + }) + // The historical competing loop reused a node and then hid it while + // allocating another. A single scheduler must reuse the visible node. + expect(reused.poolReused).toBe(true) + expect(reused.visibleSameNode).toBe(true) + expect(reused.extraNodes).toEqual([]) + expect(reused.transition).toBe('4s') + expect(reused.rate).toBe(2) + await page.evaluate(async () => { + await window.danmukuPlugins[0].load([{ id: 'appended', text: 'APPENDED', mode: 1, time: window.art.currentTime + 0.5 }]) + }) + await expect.poll(() => visibleIds(page)).toContain('appended') + await page.click('#pause') + await page.evaluate(async () => { + await window.danmukuPlugins[0].load() + }) + expect(await page.locator('#danmuku-player-0 .art-danmuku > div').count()).toBe(0) + await testInfo.attach('danmuku-pool-and-rate', { contentType: 'application/json', body: JSON.stringify({ frozen, reused, loadWithoutArgument: 'replaces from original empty option; explicit array load appends' }) }) + expect(external).toEqual([]) + }) + + test(`${label}: actual settings, asynchronous send lock, external fullscreen mount and two-player destruction`, async ({ page }, testInfo) => { + const external = await setup(page, core, format, 'settings', testInfo) + const mount = page.locator('#danmuku-mount-0') + await expect(mount.locator('.artplayer-plugin-danmuku')).toHaveCount(1) + await mount.locator('.apd-config').hover() + await mount.locator('.apd-config-mode [data-mode="2"]').click() + expect(await page.evaluate(() => window.danmukuPlugins[0].option.modes)).toEqual([0, 1]) + await mount.locator('.apd-style').hover() + await mount.locator('.apd-style-mode [data-mode="1"]').click() + expect(await page.evaluate(() => window.danmukuPlugins[0].option.mode)).toBe(1) + await mount.locator('.apd-input').fill('FIRST') + await mount.locator('.apd-send').click() + await expect.poll(() => page.evaluate(() => window.danmukuEvidence.sends.length)).toBe(1) + await mount.locator('.apd-input').press('Enter') + expect(await page.evaluate(() => window.danmukuEvidence.sends.length)).toBe(1) + await page.evaluate(() => window.releaseDanmukuSending(true)) + await expect(mount.locator('.apd-input')).toHaveValue('') + await expect(mount.locator('.apd-send')).toHaveClass(/apd-lock/u) + await mount.locator('.apd-input').fill('LOCKED') + await mount.locator('.apd-input').press('Enter') + expect(await page.evaluate(() => window.danmukuEvidence.sends.length)).toBe(1) + await expect(mount.locator('.apd-send')).not.toHaveClass(/apd-lock/u) + await page.evaluate(() => { + window.danmukuEmitter = document.querySelector('#danmuku-mount-0 .artplayer-plugin-danmuku') + window.art.fullscreenWeb = true + }) + // FULLSCREEN_WEB_IN_BODY can move the player out of its original container. + await expect.poll(() => page.evaluate(() => window.art.template.$controlsCenter.contains(window.danmukuEmitter))).toBe(true) + await expect(mount.locator('.artplayer-plugin-danmuku')).toHaveCount(0) + await page.evaluate(() => { + window.art.fullscreenWeb = false + }) + await expect(mount.locator('.artplayer-plugin-danmuku')).toHaveCount(1) + expect(await page.evaluate(() => document.querySelector('#danmuku-mount-0').contains(window.danmukuEmitter))).toBe(true) + await page.evaluate(() => { + const art = window.createDanmukuPlayer(1) + const button = document.createElement('button') + button.id = 'play-second-danmuku' + button.textContent = 'Play second' + button.onclick = () => art.play() + document.body.appendChild(button) + }) + await expect.poll(() => page.evaluate(() => window.danmukuPlayers[1].isReady)).toBe(true) + await page.evaluate(() => window.danmukuPlayers[0].destroy()) + expect(await page.evaluate(() => window.danmukuEvidence.workers.map(worker => worker.terminated))).toEqual([1, 0]) + // The released plugin does not dispose external Setting DOM; retain this observation. + await expect(mount.locator('.artplayer-plugin-danmuku')).toHaveCount(1) + await page.locator('#danmuku-mount-1 .apd-input').fill('SECOND') + await page.locator('#danmuku-mount-1 .apd-send').click() + await page.click('#play-second-danmuku') + await expect.poll(() => page.evaluate(() => window.danmukuEvidence.events.some(item => item.index === 1 && item.event === 'visible' && item.text === 'SECOND'))).toBe(true) + expect(await page.evaluate(() => window.danmukuPlayers[1].isDestroy)).toBe(false) + expect(external).toEqual([]) + }) + } +} diff --git a/test/danmuku-input.test.js b/test/danmuku-input.test.js index 40d7e7442..2dae562b6 100644 --- a/test/danmuku-input.test.js +++ b/test/danmuku-input.test.js @@ -3,7 +3,7 @@ import assert from 'node:assert/strict' import test from 'node:test' import { setImmediate as nextTurn } from 'node:timers/promises' import { danmukuCandidate, danmukuCandidateEnvironment } from './helpers/danmuku-candidate.js' -import { deferred } from './helpers/danmuku.js' +import { danmukuEnvironment, danmukuHistorical, deferred } from './helpers/danmuku.js' const implementation = await danmukuCandidate() const prefix = `Danmuku ${implementation.name}` @@ -11,6 +11,23 @@ const names = env => env.events.map(event => event.name) const loaded = env => env.events.filter(event => event.name === 'artplayerPluginDanmuku:loaded') const failures = env => env.events.filter(event => event.name === 'artplayerPluginDanmuku:error') +for (const baseline of [...await danmukuHistorical(), implementation]) { + test(`Danmuku ${baseline.name}: NaN time retains the historical fallback while infinities retain their numeric behavior`, async () => { + const env = baseline === implementation ? danmukuCandidateEnvironment(baseline) : danmukuEnvironment(baseline) + const plugin = env.factory({ danmuku: [] })(env.art) + const internal = plugin.show() + for (const [input, expected] of [[Number.NaN, 10.5], [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], [Number.NEGATIVE_INFINITY, 0]]) { + const row = { text: `numeric time ${input}`, time: input } + assert.equal(await plugin.emit(row), internal) + assert.equal(row.time, expected) + assert.equal(internal.queue.at(-1).time, expected) + assert.equal(internal.queue.at(-1).text, row.text) + } + assert.equal(failures(env).length, 0) + env.destroy() + }) +} + function fixture(option = { danmuku: [] }) { const env = danmukuCandidateEnvironment(implementation) const plugin = env.factory(option)(env.art) diff --git a/test/danmuku-scheduler.test.js b/test/danmuku-scheduler.test.js new file mode 100644 index 000000000..6ef37a364 --- /dev/null +++ b/test/danmuku-scheduler.test.js @@ -0,0 +1,672 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +// eslint-disable-next-line test/no-import-node-test -- Deterministic candidate scheduling uses the Node runner. +import test from 'node:test' +import vm from 'node:vm' +import { danmukuCandidate, danmukuCandidateEnvironment } from './helpers/danmuku-candidate.js' +import { deferred } from './helpers/danmuku.js' + +const implementation = await danmukuCandidate() +const prefix = `Danmuku scheduler ${implementation.name}` +const output = (env, event) => env.events.filter(item => item.name === `artplayerPluginDanmuku:${event}`) + +function measuredNode(node) { + node.clientWidth = 100 + node.clientHeight = 20 + Object.defineProperty(node, 'offsetTop', { get: () => Number.parseFloat(node.style.top) || 0 }) + return node +} + +async function fixture(options = {}, { automatic = false } = {}) { + const env = danmukuCandidateEnvironment(implementation) + const plugin = env.factory({ danmuku: [], ...options })(env.art) + const owner = plugin.show() + const createElement = env.context.document.createElement + env.context.document.createElement = tag => measuredNode(createElement(tag)) + const layer = env.art.template.$danmuku + let content = layer.textContent + Object.defineProperty(layer, 'textContent', { + get: () => content, + set(value) { + content = value + for (const child of layer.children) + child.parentElement = null + layer.children.length = 0 + }, + }) + layer.appendChild = (child) => { + const siblings = child.parentElement?.children + const index = siblings?.indexOf(child) ?? -1 + if (index >= 0) + siblings.splice(index, 1) + layer.children.push(child) + child.parentElement = layer + return child + } + const worker = env.workers[0] + const actualBlob = env.urls.get(worker.url) + const workerCode = actualBlob + ? await actualBlob.text() + : fs.readFileSync(new URL('../packages/artplayer-plugin-danmuku/src/worker.js', import.meta.url), 'utf8') + const responses = [] + const workerContext = vm.createContext({ postMessage: data => responses.push(structuredClone(data)), URL: { revokeObjectURL() {} }, location: { href: worker.url } }) + workerContext.self = workerContext + vm.runInContext(workerCode, workerContext, { timeout: 1000 }) + function calculate(message) { + workerContext.input = structuredClone(message) + const count = responses.length + vm.runInContext('onmessage({ data: input })', workerContext, { timeout: 1000 }) + assert.equal(responses.length, count + 1, 'The actual Worker algorithm must produce the response') + return responses.at(-1) + } + function deliver(data) { + const event = { data } + worker.onmessage?.(event) + for (const callback of [...worker.listeners.get('message') || []]) + callback(event) + } + function reply(index) { + const message = worker.messages[index].message + deliver(calculate(message)) + } + const post = worker.postMessage.bind(worker) + worker.postMessage = (message) => { + post(structuredClone(message)) + if (automatic) + queueMicrotask(() => deliver(calculate(message))) + } + return { env, plugin, owner, worker, layer, calculate, deliver, reply } +} + +function geometry(mode = 1) { + return { type: 'getDanmuTop', target: { mode, height: 20, speed: 148 }, visibles: [], antiOverlap: true, clientWidth: 640, clientHeight: 360, marginTop: 10, marginBottom: 90 } +} + +test(`${prefix}: repeated play/playing and direct starts preserve events and internal return with only one queued frame`, async () => { + const { env, owner } = await fixture() + env.art.playing = true + env.art.emit('video:play') + env.art.emit('video:playing') + assert.equal(owner.start(), owner) + assert.equal(output(env, 'start').length, 3) + assert.equal(env.frames.size, 1) + assert.equal(owner.stop(), owner) + assert.equal(owner.stop(), owner) + assert.equal(output(env, 'stop').length, 2) + assert.equal(env.frames.size, 0) + env.destroy() +}) + +test(`${prefix}: additional starts while beforeVisible is pending do not schedule an overlapping frame`, async () => { + const gate = deferred() + const { env, plugin, owner } = await fixture({ beforeVisible: () => gate.promise }) + await plugin.emit({ text: 'waiting callback', time: 10 }) + env.art.playing = true + owner.start() + env.frame() + await env.flush() + owner.start() + env.art.emit('video:playing') + assert.equal(env.frames.size, 0, 'The existing asynchronous frame owns this generation') + env.destroy() + gate.resolve(true) + await env.flush() +}) + +for (const action of ['pause', 'destroy', 'reset', 'replace', 'seek', 'hide']) { + for (const stage of ['beforeVisible', 'worker']) { + test(`${prefix}: ${action} invalidates the pending ${stage} continuation without modifying an obsolete row`, async () => { + const gate = deferred() + const state = await fixture(stage === 'beforeVisible' ? { beforeVisible: () => gate.promise } : {}) + const { env, plugin, owner, worker, layer } = state + await plugin.emit({ text: 'old', time: 10, mode: 1 }) + const row = owner.queue[0] + env.art.playing = true + owner.start() + env.frame() + await env.flush() + let response + if (stage === 'worker') { + assert.equal(worker.messages.length, 1) + response = state.calculate(worker.messages[0].message) + } + if (action === 'pause') { + env.art.playing = false + owner.stop() + } + else if (action === 'destroy') { + env.destroy() + } + else if (action === 'reset') { + plugin.reset() + } + else if (action === 'replace') { + plugin.config({ danmuku: [{ text: 'new', time: 20 }] }) + await plugin.load() + } + else if (action === 'hide') { + plugin.hide() + } + else { + env.art.currentTime = 20 + env.art.emit('video:seeking') + } + const snapshot = { rowState: row.$state, rowRef: row.$ref, nodes: [...layer.children], events: output(env, 'visible').length, messages: worker.messages.length } + if (stage === 'worker') + state.deliver(response) + else + gate.resolve(true) + await env.flush() + assert.equal(row.$state, snapshot.rowState) + assert.equal(row.$ref, snapshot.rowRef) + assert.deepEqual(layer.children, snapshot.nodes) + assert.equal(output(env, 'visible').length, snapshot.events) + assert.equal(worker.messages.length, snapshot.messages) + if (action !== 'destroy') + env.destroy() + }) + } +} + +test(`${prefix}: pausing a never-settled callback does not block a fresh generation`, async () => { + const gate = deferred() + let calls = 0 + const { env, plugin, owner } = await fixture({ beforeVisible: () => ++calls === 1 ? gate.promise : true }, { automatic: true }) + await plugin.emit({ text: 'resumable', time: 10, mode: 1 }) + env.art.playing = true + owner.start() + env.frame() + await env.flush() + owner.stop() + owner.start() + await env.flush() + assert.equal(env.frames.size, 1) + env.frame() + await env.flush() + assert.equal(output(env, 'visible').length, 1) + assert.equal(calls, 2) + env.destroy() + gate.resolve(true) + await env.flush() + assert.equal(output(env, 'visible').length, 1) +}) + +test(`${prefix}: an already delivered Worker reply cannot resume its JavaScript continuation after destroy`, async () => { + const { env, plugin, owner, layer, reply } = await fixture() + await plugin.emit({ text: 'queued microtask', time: 10, mode: 1 }) + env.art.playing = true + owner.start() + env.frame() + await env.flush() + reply(0) + env.destroy() + const row = owner.queue[0] + const snapshot = { state: row.$state, ref: row.$ref, nodes: [...layer.children] } + await env.flush() + assert.equal(row.$state, snapshot.state) + assert.equal(row.$ref, snapshot.ref) + assert.deepEqual(layer.children, snapshot.nodes) + assert.equal(output(env, 'visible').length, 0) + assert.equal(env.frames.size, 0) +}) + +test(`${prefix}: beforeVisible rejection reports the original error once, skips that row until start, and allows other rows`, async () => { + const error = new Error('visibility hook rejected') + const calls = [] + let retry = false + const { env, plugin, owner } = await fixture({ + beforeVisible(row) { + calls.push(row.text) + return row.text === 'failed' && !retry ? Promise.reject(error) : true + }, + }, { automatic: true }) + await plugin.load([{ text: 'failed', time: 10, mode: 1 }, { text: 'healthy', time: 10, mode: 1 }]) + env.art.playing = true + owner.start() + env.frame() + await env.flush() + await env.flush() + assert.deepEqual(output(env, 'error').map(event => event.args[0]), [error]) + assert.deepEqual(output(env, 'visible').map(event => event.args[0].text), ['healthy']) + env.frame() + await env.flush() + assert.deepEqual(calls, ['failed', 'healthy']) + retry = true + assert.equal(owner.start(), owner) + assert.equal(env.frames.size, 1) + env.frame() + await env.flush() + assert.deepEqual(calls, ['failed', 'healthy', 'failed']) + assert.deepEqual(output(env, 'visible').map(event => event.args[0].text), ['healthy', 'failed']) + assert.equal(output(env, 'error').length, 1) + env.destroy() +}) + +test(`${prefix}: a cancelled beforeVisible rejection is observed without reporting a stale error`, async () => { + const gate = deferred() + const { env, plugin, owner } = await fixture({ beforeVisible: () => gate.promise }) + await plugin.emit({ text: 'cancelled', time: 10 }) + env.art.playing = true + owner.start() + env.frame() + await env.flush() + env.destroy() + gate.reject(new Error('rejected after destroy')) + await env.flush() + assert.equal(output(env, 'error').length, 0) + assert.equal(output(env, 'visible').length, 0) + assert.equal(env.frames.size, 0) +}) + +test(`${prefix}: beforeVisible false remains eligible for the next frame`, async () => { + let calls = 0 + const { env, plugin, owner } = await fixture({ beforeVisible: () => ++calls > 1 }, { automatic: true }) + await plugin.emit({ text: 'retry false', time: 10, mode: 1 }) + env.art.playing = true + owner.start() + env.frame() + await env.flush() + assert.equal(output(env, 'visible').length, 0) + env.frame() + await env.flush() + assert.equal(calls, 2) + assert.equal(output(env, 'visible').length, 1) + assert.equal(output(env, 'error').length, 0) + env.destroy() +}) + +test(`${prefix}: hide preserves an already visible row and show permits fresh scheduling`, async () => { + const { env, plugin, owner } = await fixture({}, { automatic: true }) + await plugin.emit({ text: 'visible before hide', time: 10, mode: 1 }) + env.art.playing = true + owner.start() + env.frame() + await env.flush() + const row = owner.queue[0] + const ref = row.$ref + const resets = output(env, 'reset').length + assert.equal(plugin.hide(), owner) + assert.equal(row.$state, 'emit') + assert.equal(row.$ref, ref) + assert.equal(output(env, 'reset').length, resets) + assert.equal(plugin.show(), owner) + await plugin.emit({ text: 'visible after show', time: 10, mode: 1 }) + env.frame() + await env.flush() + assert.equal(output(env, 'visible').length, 2) + env.destroy() +}) + +test(`${prefix}: replacing beforeVisible releases an unresolved old callback without resetting displayed rows`, async () => { + const gate = deferred() + const calls = [] + const { env, plugin, owner } = await fixture({ + beforeVisible(row) { + calls.push(`old:${row.text}`) + return row.text === 'pending' ? gate.promise : true + }, + }, { automatic: true }) + await plugin.load([{ text: 'displayed', time: 10, mode: 1 }, { text: 'pending', time: 10, mode: 1 }]) + env.art.playing = true + owner.start() + env.frame() + await env.flush() + await env.flush() + const displayed = owner.queue[0] + const ref = displayed.$ref + const resets = output(env, 'reset').length + assert.deepEqual(calls, ['old:displayed', 'old:pending']) + plugin.config({ + beforeVisible(row) { + calls.push(`new:${row.text}`) + return true + }, + }) + assert.equal(displayed.$state, 'emit') + assert.equal(displayed.$ref, ref) + assert.equal(output(env, 'reset').length, resets) + assert.equal(env.frames.size, 1) + env.frame() + await env.flush() + assert.deepEqual(calls, ['old:displayed', 'old:pending', 'new:pending']) + assert.deepEqual(output(env, 'visible').map(event => event.args[0].text), ['displayed', 'pending']) + gate.resolve(true) + await env.flush() + assert.equal(output(env, 'visible').length, 2) + env.destroy() +}) + +test(`${prefix}: configuring the same beforeVisible callback preserves the pending operation`, async () => { + const gate = deferred() + let calls = 0 + const beforeVisible = () => { + calls++ + return gate.promise + } + const { env, plugin, owner } = await fixture({ beforeVisible }, { automatic: true }) + await plugin.emit({ text: 'same callback', time: 10, mode: 1 }) + env.art.playing = true + owner.start() + env.frame() + await env.flush() + const configurations = output(env, 'config').length + plugin.config({ beforeVisible }) + assert.equal(output(env, 'config').length, configurations) + assert.equal(env.frames.size, 0) + gate.resolve(true) + await env.flush() + assert.equal(calls, 1) + assert.equal(output(env, 'visible').length, 1) + assert.equal(env.frames.size, 1) + env.destroy() +}) + +for (const action of ['destroy', 'stop']) { + test(`${prefix}: ${action} inside a Worker rebuild error listener prevents the outer start from reviving stopped rows`, async () => { + const { env, plugin, owner, worker } = await fixture({}, { automatic: true }) + await plugin.emit({ text: 'already displayed', time: 10, mode: 1 }) + env.art.playing = true + owner.start() + env.frame() + await env.flush() + const row = owner.queue[0] + assert.equal(row.$state, 'emit') + const workerError = new Error('initial worker fault') + worker.onerror({ error: workerError, preventDefault() {} }) + owner.stop() + assert.equal(row.$state, 'stop') + const rebuildError = new Error('rebuild original error') + // This is an explicit constructor-failure seam, not a native Worker assertion. + owner.createWorker = () => { + throw rebuildError + } + env.art.on('artplayerPluginDanmuku:error', (error) => { + if (error === rebuildError) { + if (action === 'destroy') + env.destroy() + else + owner.stop() + } + }) + const starts = output(env, 'start').length + assert.equal(owner.start(), owner) + await env.flush() + assert.equal(row.$state, 'stop') + assert.equal(owner.isStop, true) + assert.equal(output(env, 'start').length, starts) + assert.equal(env.frames.size, 0) + assert.deepEqual(output(env, 'error').map(event => event.args[0]), [workerError, rebuildError]) + if (action !== 'destroy') + env.destroy() + }) +} + +for (const action of ['reset', 'hide', 'start']) { + test(`${prefix}: ${action} reentry from a rebuild error owns the recovery without a second outer start`, async () => { + const { env, plugin, owner, worker } = await fixture({}, { automatic: true }) + await plugin.emit({ text: 'displayed before recovery', time: 10, mode: 1 }) + env.art.playing = true + owner.start() + env.frame() + await env.flush() + const row = owner.queue[0] + assert.equal(row.$state, 'emit') + const workerError = new Error('worker failed before reentry') + worker.onerror({ error: workerError, preventDefault() {} }) + owner.stop() + const createWorker = owner.createWorker + const rebuildError = new Error('one rebuild failure') + owner.createWorker = () => { + throw rebuildError + } + let recoveries = 0 + env.art.on('artplayerPluginDanmuku:error', (error) => { + if (error !== rebuildError) + return + recoveries++ + assert.equal(recoveries, 1, 'The recovery listener must not retry itself indefinitely') + owner.createWorker = createWorker + owner[action]() + }) + const starts = output(env, 'start').length + const resets = output(env, 'reset').length + assert.equal(owner.start(), owner) + await env.flush() + assert.equal(recoveries, 1) + assert.deepEqual(output(env, 'error').map(event => event.args[0]), [workerError, rebuildError]) + assert.equal(output(env, 'start').length, starts + (action === 'start' ? 1 : 0)) + assert.equal(output(env, 'reset').length, resets + (action === 'reset' ? 1 : 0)) + assert.equal(row.$state, action === 'reset' ? 'wait' : action === 'start' ? 'emit' : 'stop') + assert.equal(owner.isHide, action === 'hide') + assert.equal(env.frames.size, action === 'start' ? 1 : 0) + if (action === 'reset') + assert.equal(row.$ref, null) + env.destroy() + }) +} + +test(`${prefix}: requests in the same millisecond have distinct ids and reverse replies settle the right promises`, async () => { + const { env, owner, worker, reply } = await fixture() + let first + let second + owner.postMessage(geometry(1)).then(value => first = value) + owner.postMessage(geometry(2)).then(value => second = value) + assert.equal(worker.messages.length, 2) + assert.notEqual(worker.messages[0].message.id, worker.messages[1].message.id) + reply(1) + await env.flush() + assert.equal(first, undefined) + assert.equal(second.result, 250) + assert.equal(second.id, worker.messages[1].message.id) + reply(0) + await env.flush() + assert.equal(first.result, 10) + assert.equal(first.id, worker.messages[0].message.id) + env.destroy() +}) + +test(`${prefix}: a synchronous real algorithm response cannot arrive before its resolver is installed`, async () => { + const { env, owner, worker, calculate, deliver } = await fixture() + const post = worker.postMessage.bind(worker) + worker.postMessage = (message) => { + post(message) + deliver(calculate(message)) + } + let response + owner.postMessage(geometry(2)).then(value => response = value) + await env.flush() + assert.equal(response?.result, 250) + env.destroy() +}) + +test(`${prefix}: the actual algorithm places three same-time fixed rows on distinct tracks in one frame`, async () => { + const { env, plugin, owner, layer } = await fixture({}, { automatic: true }) + await plugin.load([0, 1, 2].map(index => ({ text: `fixed-${index}`, time: 10, mode: 1 }))) + env.art.playing = true + owner.start() + env.frame() + for (let step = 0; step < 3; step++) + await env.flush() + assert.deepEqual(Array.from(owner.queue, row => row.$state), ['emit', 'emit', 'emit']) + assert.deepEqual(Array.from(owner.queue, row => row.$ref.offsetTop), [10, 30, 50]) + assert.equal(layer.children.length, 3) + assert.deepEqual(output(env, 'visible').map(event => event.args[0].text), ['fixed-0', 'fixed-1', 'fixed-2']) + assert.equal(env.frames.size, 1) + env.destroy() +}) + +test(`${prefix}: a sampled three-hour simulated clock preserves batch order, pooled nodes, pause and seek state`, async (t) => { + // Advance a controlled clock at lifecycle boundaries; this is not a real-time + // endurance, frame-rate, CSS animation or browser memory measurement. + const { env, plugin, owner, worker, layer } = await fixture({}, { automatic: true }) + t.after(() => env.destroy()) + const batchCount = 180 + const batchSize = 4 + const firstTime = 10 + const horizon = 3 * 60 * 60 + const rows = Array.from({ length: batchCount * batchSize }, (_, index) => ({ + text: `long-${index}`, + time: firstTime + Math.floor(index / batchSize) * 60, + mode: 1, + })) + await plugin.load(rows) + const queue = owner.queue + const expectedOrder = Array.from(queue, row => row.text) + let advanced = 0 + const tick = (milliseconds) => { + advanced += milliseconds + env.tick(milliseconds) + } + const membership = () => { + const items = Object.entries(owner.states).flatMap(([state, items]) => { + for (const item of items) + assert.equal(item.$state, state) + return Array.from(items) + }) + assert.equal(items.length, queue.length) + assert.equal(new Set(items).size, queue.length, 'Every row belongs to exactly one state pool') + assert(items.every(item => queue.includes(item))) + } + let pooledNodes + let pauses = 0 + env.art.playing = true + env.art.emit('video:play') + env.art.emit('video:playing') + + for (let batch = 0; batch < batchCount; batch++) { + const time = firstTime + batch * 60 + tick((time - env.art.currentTime) * 1000) + env.art.currentTime = time + assert.equal(env.frames.size, 1) + const requests = worker.messages.length + await env.frame() + const active = Array.from(queue.slice(batch * batchSize, (batch + 1) * batchSize)) + assert.deepEqual(output(env, 'visible').slice(-batchSize).map(event => event.args[0]), active) + assert.equal(worker.messages.length, requests + batchSize, 'One position request per row in this batch') + assert.deepEqual(worker.messages.slice(requests).map(item => item.message.visibles.length), [0, 1, 2, 3]) + assert.deepEqual(active.map(row => row.$state), ['emit', 'emit', 'emit', 'emit']) + assert.deepEqual(active.map(row => row.$ref.offsetTop), [10, 30, 50, 70]) + if (!pooledNodes) + pooledNodes = new Set(layer.children) + assert.equal(layer.children.length, batchSize) + assert(active.every(row => pooledNodes.has(row.$ref)), 'Later batches reuse the original measured nodes') + membership() + + tick(1000) + env.art.currentTime++ + await env.frame() + assert(active.every(row => row.$restTime === 4)) + if (batch % 12 === 11) { + pauses++ + const refs = active.map(row => row.$ref) + const resetCount = output(env, 'reset').length + env.art.playing = false + env.art.emit('video:pause') + assert.equal(env.frames.size, 0) + tick(120000) + env.art.currentTime = time + 20 + env.art.emit('video:seeking') + env.art.emit('video:seeked') + assert.equal(output(env, 'reset').length, resetCount) + assert(active.every(row => row.$state === 'stop' && row.$restTime === 4)) + assert.deepEqual(active.map(row => row.$ref), refs) + env.art.playing = true + env.art.emit('video:play') + env.art.emit('video:playing') + assert.equal(env.frames.size, 1) + await env.frame() + assert(active.every(row => row.$state === 'emit' && row.$restTime === 4)) + assert.deepEqual(active.map(row => row.$ref), refs) + assert.equal(worker.messages.length, requests + batchSize, 'Resume and seek must not allocate duplicate rows') + } + + tick(5000) + env.art.currentTime += 5 + await env.frame() + assert(active.every(row => row.$state === 'wait' && row.$ref === null)) + assert.equal(owner.$refs.length, batchSize) + assert.equal(new Set(owner.$refs).size, batchSize, 'Each reusable node returns to the pool once') + assert.equal(layer.children.length, batchSize) + assert.equal(env.frames.size, 1) + membership() + } + + tick((firstTime + horizon - env.art.currentTime) * 1000) + env.art.currentTime = firstTime + horizon + await env.frame() + assert.equal(pauses, 15) + assert(advanced > horizon * 1000, 'The simulated wall clock also includes the paused intervals') + assert.equal(owner.queue, queue) + assert(queue.every(row => row.$state === 'wait' && row.$ref === null)) + assert.deepEqual(output(env, 'visible').map(event => event.args[0].text), expectedOrder) + assert.equal(worker.messages.length, rows.length) + assert.equal(new Set(worker.messages.map(item => item.message.id)).size, rows.length) + assert.equal(output(env, 'error').length, 0) + assert.equal(env.workers.length, 1) + owner.stop() + assert.equal(env.frames.size, 0) +}) + +test(`${prefix}: paused wall clock does not consume remaining lifetime and expiration returns a row to wait`, async () => { + const { env, plugin, owner } = await fixture({}, { automatic: true }) + await plugin.emit({ text: 'clock', time: 10, mode: 1 }) + env.art.playing = true + owner.start() + env.frame() + await env.flush() + const row = owner.queue[0] + assert.equal(row.$restTime, 5) + env.tick(1000) + env.art.currentTime = 11 + env.frame() + await env.flush() + assert.equal(row.$restTime, 4) + env.art.playing = false + owner.stop() + assert.equal(row.$state, 'stop') + env.tick(5000) + env.art.playing = true + owner.start() + env.frame() + await env.flush() + assert.equal(row.$restTime, 4) + env.tick(500) + env.art.currentTime = 11.5 + env.frame() + await env.flush() + assert.equal(row.$restTime, 3.5) + env.tick(4000) + env.art.currentTime = 15.5 + env.frame() + await env.flush() + assert.equal(row.$state, 'wait') + assert.equal(row.$ref, null) + assert.equal(owner.queue.length, 1) + assert.equal(output(env, 'visible').length, 1) + env.destroy() +}) + +test(`${prefix}: playbackRate is sampled for new rows while active lifetime and seek reset-event semantics stay compatible`, async () => { + const { env, plugin, owner } = await fixture({ synchronousPlayback: true }, { automatic: true }) + await plugin.load([{ text: 'already-visible', time: 10, mode: 1 }, { text: 'after-seek', time: 20, mode: 1 }]) + env.art.playbackRate = 2 + env.art.playing = true + owner.start() + env.frame() + await env.flush() + const existing = owner.queue[0] + assert.equal(existing.$restTime, 2.5) + env.art.playbackRate = 4 + const resets = output(env, 'reset').length + env.art.currentTime = 20 + env.art.emit('video:seeking') + assert.equal(existing.$state, 'emit') + assert.equal(existing.$restTime, 2.5) + assert.equal(output(env, 'reset').length, resets) + assert.equal(env.frames.size, 1) + env.frame() + await env.flush() + assert.equal(owner.queue[1].$restTime, 1.25) + assert.equal(existing.$restTime, 2.5) + env.destroy() +}) diff --git a/test/danmuku-worker-client.test.js b/test/danmuku-worker-client.test.js new file mode 100644 index 000000000..00382ffeb --- /dev/null +++ b/test/danmuku-worker-client.test.js @@ -0,0 +1,327 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +// eslint-disable-next-line test/no-import-node-test -- Controlled client faults and real worker geometry use the Node runner. +import test from 'node:test' +import vm from 'node:vm' +import { danmukuCandidate, danmukuCandidateEnvironment } from './helpers/danmuku-candidate.js' +import { loadModules } from './helpers/load.js' + +const { WorkerClient } = await loadModules({ WorkerClient: 'packages/artplayer-plugin-danmuku/src/worker-client' }) +const implementation = await danmukuCandidate() + +// This client double records protocol ownership; it does not simulate geometry. +function clientFixture(onFailure = () => {}) { + const messages = [] + const failures = [] + const worker = { + terminateCalls: 0, + postMessage(message) { messages.push({ ...message }) }, + terminate() { this.terminateCalls++ }, + } + const client = new WorkerClient(() => worker, (error) => { + failures.push(error) + onFailure(error) + }) + return { client, worker, messages, failures } +} + +function detached(worker) { + assert.equal(worker.onmessage, null) + assert.equal(worker.onerror, null) + assert.equal(worker.onmessageerror, null) +} + +test('danmuku worker client: simultaneous requests have distinct IDs and reversed replies retain their owners', async () => { + const { client, worker, messages } = clientFixture() + const inputs = [{ type: 'getDanmuTop', tag: 'first' }, { type: 'getDanmuTop', tag: 'second' }] + const first = client.request(inputs[0]) + const second = client.request(inputs[1]) + assert.notEqual(inputs[0].id, inputs[1].id) + assert.deepEqual(messages.map(item => item.id), inputs.map(item => item.id)) + assert.equal(client.pending.size, 2) + worker.onmessage({ data: { id: inputs[1].id, result: 33 } }) + assert.deepEqual(await second, { id: inputs[1].id, result: 33 }) + assert.equal(client.pending.size, 1) + worker.onmessage({ data: { id: inputs[0].id, result: 10 } }) + assert.deepEqual(await first, { id: inputs[0].id, result: 10 }) + assert.equal(client.pending.size, 0) + client.dispose() +}) + +test('danmuku worker client: a synchronous reply is observed before postMessage returns', async () => { + const { client, worker } = clientFixture() + const callback = worker.onmessage + worker.postMessage = message => callback({ data: { id: message.id, result: 72 } }) + const message = { type: 'getDanmuTop' } + assert.deepEqual(await client.request(message), { id: message.id, result: 72 }) + assert.equal(worker.onmessage, callback, 'Requests must not replace the shared dispatcher') + assert.equal(client.pending.size, 0) + client.dispose() +}) + +test('danmuku worker client: unrelated and repeated messages cannot settle another request', async () => { + const { client, worker } = clientFixture() + const a = { type: 'getDanmuTop' } + const b = { type: 'getDanmuTop' } + const first = client.request(a) + const second = client.request(b) + worker.onmessage({ data: { id: 'unrelated', result: 100 } }) + assert.equal(client.pending.size, 2) + worker.onmessage({ data: { id: a.id, result: 10 } }) + worker.onmessage({ data: { id: a.id, result: 999 } }) + assert.deepEqual(await first, { id: a.id, result: 10 }) + assert.equal(client.pending.size, 1) + worker.onmessage({ data: { id: b.id, result: 33 } }) + assert.deepEqual(await second, { id: b.id, result: 33 }) + client.dispose() +}) + +test('danmuku worker client: cancellation settles every owner, ignores late replies and permits new work', async () => { + const { client, worker, failures } = clientFixture() + const messages = [{ type: 'getDanmuTop' }, { type: 'getDanmuTop' }] + const pending = messages.map(message => client.request(message)) + client.cancel() + assert.deepEqual(await Promise.all(pending), messages.map(message => ({ id: message.id, result: undefined }))) + assert.equal(client.pending.size, 0) + assert.equal(worker.terminateCalls, 0) + const next = { type: 'getDanmuTop' } + const nextPending = client.request(next) + for (const message of messages) + worker.onmessage({ data: { id: message.id, result: 10 } }) + assert.equal(client.pending.size, 1) + worker.onmessage({ data: { id: next.id, result: 56 } }) + assert.deepEqual(await nextPending, { id: next.id, result: 56 }) + assert.deepEqual(failures, []) + client.dispose() +}) + +for (const eventName of ['error', 'messageerror']) { + test(`danmuku worker client: ${eventName} rejects all pending requests with the original failure exactly once`, async () => { + const { client, worker, messages, failures } = clientFixture() + const error = new Error(`${eventName}-original`) + const a = client.request({ type: 'getDanmuTop' }) + const b = client.request({ type: 'getDanmuTop' }) + const rejected = Promise.all([assert.rejects(a, value => value === error), assert.rejects(b, value => value === error)]) + let prevented = 0 + const handler = worker[`on${eventName}`] + handler({ error, preventDefault() { + prevented++ + } }) + handler({ error: new Error('duplicate') }) + await rejected + assert.deepEqual(failures, [error]) + assert.equal(client.pending.size, 0) + assert.equal(client.failed, true) + if (eventName === 'error') + assert.equal(prevented, 1) + const ignored = { type: 'getDanmuTop' } + assert.deepEqual(await client.request(ignored), { id: ignored.id, result: undefined }) + assert.equal(messages.length, 2) + client.dispose() + detached(worker) + assert.equal(worker.terminateCalls, 1) + }) +} + +for (const error of [new Error('post-original'), undefined, null, false]) { + test(`danmuku worker client: synchronous postMessage throw ${String(error)} retains the exact rejection value`, async () => { + const { client, worker, failures } = clientFixture() + const first = client.request({ type: 'getDanmuTop' }) + const rejected = assert.rejects(first, value => value === error) + worker.postMessage = () => { + throw error + } + await assert.rejects(client.request({ type: 'getDanmuTop' }), value => value === error) + await rejected + assert.deepEqual(failures, [error]) + assert.equal(client.pending.size, 0) + client.dispose() + }) +} + +test('danmuku worker client: dispose cancels pending work, detaches handlers and terminates exactly once', async () => { + const { client, worker, messages, failures } = clientFixture() + const message = { type: 'getDanmuTop' } + const pending = client.request(message) + const oldHandler = worker.onmessage + client.dispose() + client.dispose() + assert.deepEqual(await pending, { id: message.id, result: undefined }) + detached(worker) + assert.equal(worker.terminateCalls, 1) + oldHandler({ data: { id: message.id, result: 100 } }) + const after = { type: 'getDanmuTop' } + assert.deepEqual(await client.request(after), { id: after.id, result: undefined }) + assert.equal(messages.length, 1) + assert.deepEqual(failures, []) +}) + +test('danmuku worker client: termination failure still settles pending requests and detaches owned handlers', async () => { + const { client, worker } = clientFixture() + const error = new Error('terminate-original') + worker.terminate = () => { + worker.terminateCalls++ + throw error + } + const input = { type: 'getDanmuTop' } + const pending = client.request(input) + assert.throws(() => client.dispose(), value => value === error) + assert.deepEqual(await pending, { id: input.id, result: undefined }) + detached(worker) + client.dispose() + assert.equal(worker.terminateCalls, 1) +}) + +test('danmuku worker client: fault cleanup cannot replace the original request error', async () => { + const { client, worker, failures } = clientFixture() + const original = new Error('worker-original') + const cleanup = new Error('terminate-secondary') + worker.terminate = () => { + worker.terminateCalls++ + throw cleanup + } + const pending = client.request({ type: 'getDanmuTop' }) + const rejected = assert.rejects(pending, error => error === original) + worker.onerror({ error: original, preventDefault() {} }) + await rejected + assert.deepEqual(failures, [original]) + detached(worker) + assert.equal(worker.terminateCalls, 1) + client.dispose() + assert.equal(worker.terminateCalls, 1) +}) + +test('danmuku worker client: initial handler setup failure terminates the worker and preserves the setup error', () => { + const original = new Error('handler-setup-original') + let terminated = 0 + const worker = { + get onmessage() { return undefined }, + set onmessage(_value) { throw original }, + terminate() { + terminated++ + throw new Error('terminate-secondary') + }, + } + assert.throws(() => new WorkerClient(() => worker, () => assert.fail('No initialized client may report a runtime fault')), error => error === original) + assert.equal(terminated, 1) +}) + +test('danmuku worker client: independent clients never share pending ownership', async () => { + const first = clientFixture() + const second = clientFixture() + const a = { type: 'getDanmuTop' } + const b = { type: 'getDanmuTop' } + const pa = first.client.request(a) + const pb = second.client.request(b) + assert.notEqual(a.id, b.id) + first.client.dispose() + assert.deepEqual(await pa, { id: a.id, result: undefined }) + assert.equal(second.worker.terminateCalls, 0) + assert.equal(second.client.pending.size, 1) + second.worker.onmessage({ data: { id: b.id, result: 70 } }) + assert.deepEqual(await pb, { id: b.id, result: 70 }) + second.client.dispose() +}) + +// Unlike the client double above, this executes the actual unmodified worker +// script. Numeric geometry inputs are controlled; no browser layout is claimed. +const workerCode = fs.readFileSync(new URL('../packages/artplayer-plugin-danmuku/src/worker.js', import.meta.url), 'utf8') + +function calculateGeometry(input) { + const replies = [] + const context = vm.createContext({ input, postMessage: data => replies.push(data) }) + vm.runInContext(workerCode, context, { timeout: 1000 }) + vm.runInContext('onmessage({ data: input })', context, { timeout: 1000 }) + assert.equal(replies.length, 1) + assert.equal(replies[0].id, input.id) + return replies[0].result +} + +test('danmuku actual worker: fixed tracks preserve mode isolation, margins, gap direction and full-track deferral', () => { + const base = { id: 1, type: 'getDanmuTop', clientWidth: 100, clientHeight: 100, marginTop: 10, marginBottom: 10, antiOverlap: true } + const target = mode => ({ mode, height: 20, speed: 20 }) + assert.equal(calculateGeometry({ ...base, target: target(1), visibles: [] }), 10) + assert.equal(calculateGeometry({ ...base, target: target(2), visibles: [] }), 70) + assert.equal(calculateGeometry({ ...base, target: target(1), visibles: [{ mode: 1, top: 10, height: 20 }] }), 30) + assert.equal(calculateGeometry({ ...base, target: target(2), visibles: [{ mode: 2, top: 70, height: 20 }] }), 50) + assert.equal(calculateGeometry({ ...base, target: target(1), visibles: [{ mode: 0, top: 10, height: 80 }] }), 10) + assert.equal(calculateGeometry({ ...base, target: target(1), visibles: [{ mode: 1, top: 95, height: 20 }] }), 10) + for (const mode of [1, 2]) { + const visibles = Object.freeze([Object.freeze({ mode, top: 10, height: 40 }), Object.freeze({ mode, top: 50, height: 40 })]) + assert.equal(calculateGeometry({ ...base, target: target(mode), visibles }), undefined) + } +}) + +test('danmuku actual worker: rolling collision decisions retain distance, remaining-time and relative-speed semantics', () => { + const base = { id: 2, type: 'getDanmuTop', clientWidth: 100, clientHeight: 100, marginTop: 10, marginBottom: 10, antiOverlap: true } + const visible = { mode: 0, top: 10, height: 80, left: 40, width: 40, distance: 80, right: 20, speed: 10, time: 8 } + const calculate = (speed, changed = {}) => calculateGeometry({ ...base, target: { mode: 0, height: 20, speed }, visibles: [{ ...visible, ...changed }] }) + assert.equal(calculate(5), 10, 'A slower follower cannot catch the existing row') + assert.equal(calculate(10), 10, 'Equal speed with a positive gap remains safe') + assert.equal(calculate(11), 10, 'Catch-up happens after the existing remaining lifetime') + assert.equal(calculate(20), undefined, 'Catch-up before expiration defers the follower') + assert.equal(calculate(5, { distance: 110, right: -10 }), undefined, 'An existing row not fully inside blocks reuse') +}) + +test('danmuku actual worker: disabled antiOverlap retains the historical occupancy ranking', () => { + const base = { id: 3, type: 'getDanmuTop', clientWidth: 100, clientHeight: 100, marginTop: 10, marginBottom: 10, antiOverlap: false } + for (const mode of [0, 1, 2]) { + const visibles = [{ mode, top: 10, height: 40, width: 80, right: 10 }, { mode, top: 50, height: 40, width: 20, right: 30 }] + assert.equal(calculateGeometry({ ...base, target: { mode, height: 20, speed: 20 }, visibles }), 50) + } +}) + +test(`danmuku ${implementation.name}: owner requests made with the same clock tick remain distinct`, async (t) => { + const env = danmukuCandidateEnvironment(implementation) + const plugin = env.factory({ danmuku: [] })(env.art) + t.after(() => env.destroy()) + const owner = plugin.show() + const first = owner.postMessage({ type: 'getDanmuTop' }) + const second = owner.postMessage({ type: 'getDanmuTop' }) + const worker = env.workers[0] + assert.equal(worker.messages.length, 2) + const [a, b] = worker.messages.map(item => item.message.id) + assert.notEqual(a, b, 'The controlled Date.now value is unchanged between calls') + worker.deliver({ id: b, result: 33 }) + assert.equal((await second).result, 33) + worker.deliver({ id: a, result: 10 }) + assert.equal((await first).result, 10) +}) + +for (const recovery of ['start', 'reset']) { + test(`danmuku ${implementation.name}: a Worker fault stops the run once and ${recovery} rebuilds the client`, async (t) => { + const env = danmukuCandidateEnvironment(implementation) + const plugin = env.factory({ danmuku: [] })(env.art) + t.after(() => env.destroy()) + const owner = plugin.show() + env.art.playing = true + env.art.emit('video:play') + assert.equal(env.frames.size, 1) + const worker = env.workers[0] + const error = new Error(`${recovery}-worker-original`) + const first = owner.postMessage({ type: 'getDanmuTop' }) + const second = owner.postMessage({ type: 'getDanmuTop' }) + const rejected = Promise.all([assert.rejects(first, value => value === error), assert.rejects(second, value => value === error)]) + const fail = worker.onerror + fail({ error, preventDefault() {} }) + fail({ error: new Error('duplicate'), preventDefault() {} }) + await rejected + assert.deepEqual(env.events.filter(event => event.name === 'artplayerPluginDanmuku:error').map(event => event.args), [[error]]) + assert.equal(env.frames.size, 0) + assert.equal(worker.terminated, true) + detached(worker) + if (recovery === 'start') + env.art.emit('video:play') + else plugin.reset() + assert.equal(env.workers.length, 2) + assert.equal(env.frames.size, 1) + const fresh = env.workers[1] + assert.notEqual(fresh, worker) + assert.equal(fresh.terminated, false) + const pending = owner.postMessage({ type: 'getDanmuTop' }) + const { id } = fresh.messages[0].message + fresh.deliver({ id, result: 10 }) + assert.equal((await pending).result, 10) + assert.equal(env.events.filter(event => event.name === 'artplayerPluginDanmuku:error').length, 1) + }) +}