mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-12 15:26:16 -08:00
refactor(danmuku): [PKG-DANMUKU-06] migrate runtime and preserve public types
This commit is contained in:
1 parent
abc8008da7
commit
24b883d862
64 files changed
+14020
-633
No files matched your search
+209
-244
@@ -1,114 +1,96 @@
|
||||
export type Mode = 0 | 1 | 2
|
||||
export type Danmuku
|
||||
= | Danmu[]
|
||||
| string // URL
|
||||
| (() => Promise<Danmu[]>)
|
||||
| Promise<Danmu[]>
|
||||
|
||||
export interface Slider {
|
||||
min?: number
|
||||
max?: number
|
||||
steps?: {
|
||||
name?: string
|
||||
value?: number | string
|
||||
show?: boolean
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface Danmu {
|
||||
/**
|
||||
* 弹幕文本
|
||||
*/
|
||||
text: string
|
||||
|
||||
/**
|
||||
* 弹幕发送模式: 0: 滚动,1: 顶部,2: 底部
|
||||
*/
|
||||
mode?: Mode
|
||||
|
||||
/**
|
||||
* 弹幕颜色
|
||||
*/
|
||||
color?: string
|
||||
|
||||
/**
|
||||
* 弹幕出现的时间,单位为秒
|
||||
*/
|
||||
time?: number
|
||||
|
||||
/**
|
||||
* 弹幕是否有描边, 默认为 false
|
||||
*/
|
||||
border?: boolean
|
||||
|
||||
/**
|
||||
* 弹幕自定义样式
|
||||
*/
|
||||
style?: Partial<CSSStyleDeclaration>
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
/**
|
||||
* 弹幕数据: 函数,数组,Promise,URL
|
||||
*/
|
||||
danmuku: Danmuku
|
||||
|
||||
/**
|
||||
* 弹幕持续时间,范围在[1 ~ 10]
|
||||
*/
|
||||
speed?: number
|
||||
|
||||
/**
|
||||
* 弹幕上下边距,支持像素数字和百分比
|
||||
*/
|
||||
margin?: [number | `${number}%`, number | `${number}%`]
|
||||
|
||||
/**
|
||||
* 弹幕透明度,范围在[0 ~ 1]
|
||||
*/
|
||||
opacity?: number
|
||||
|
||||
/**
|
||||
* 默认弹幕颜色,可以被单独弹幕项覆盖
|
||||
*/
|
||||
color?: string
|
||||
|
||||
/**
|
||||
* 弹幕模式: 0: 滚动,1: 顶部,2: 底部
|
||||
*/
|
||||
mode?: Mode
|
||||
|
||||
/**
|
||||
* 弹幕可见的模式
|
||||
*/
|
||||
modes?: Mode[]
|
||||
|
||||
/**
|
||||
* 弹幕字体大小,支持像素数字和百分比
|
||||
*/
|
||||
fontSize?: number | `${number}%`
|
||||
|
||||
/**
|
||||
* 弹幕是否防重叠
|
||||
*/
|
||||
antiOverlap?: boolean
|
||||
|
||||
/**
|
||||
* 是否同步播放速度
|
||||
*/
|
||||
synchronousPlayback?: boolean
|
||||
|
||||
/**
|
||||
* 弹幕发射器挂载点, 默认为播放器控制栏中部
|
||||
*/
|
||||
mount?: HTMLDivElement | string
|
||||
|
||||
/**
|
||||
* 是否开启弹幕热度图
|
||||
*/
|
||||
heatmap?:
|
||||
| boolean
|
||||
| {
|
||||
// Generated from the package public declaration by yarn build:ts. Do not edit.
|
||||
/* eslint-disable ts/no-redeclare -- Callable and public type namespace intentionally merge. */
|
||||
declare namespace artplayerPluginDanmukuDefinitions {
|
||||
export type Mode = 0 | 1 | 2
|
||||
export type Danmuku = Danmu[] | string // URL
|
||||
| (() => Promise<Danmu[]>) | Promise<Danmu[]>
|
||||
export interface Slider {
|
||||
min?: number
|
||||
max?: number
|
||||
steps?: {
|
||||
name?: string
|
||||
value?: number | string
|
||||
show?: boolean
|
||||
}[]
|
||||
}
|
||||
export interface Danmu {
|
||||
/**
|
||||
* 弹幕文本
|
||||
*/
|
||||
text: string
|
||||
/**
|
||||
* 弹幕发送模式: 0: 滚动,1: 顶部,2: 底部
|
||||
*/
|
||||
mode?: Mode
|
||||
/**
|
||||
* 弹幕颜色
|
||||
*/
|
||||
color?: string
|
||||
/**
|
||||
* 弹幕出现的时间,单位为秒
|
||||
*/
|
||||
time?: number
|
||||
/**
|
||||
* 弹幕是否有描边, 默认为 false
|
||||
*/
|
||||
border?: boolean
|
||||
/**
|
||||
* 弹幕自定义样式
|
||||
*/
|
||||
style?: Partial<CSSStyleDeclaration>
|
||||
}
|
||||
export interface Option {
|
||||
/**
|
||||
* 弹幕数据: 函数,数组,Promise,URL
|
||||
*/
|
||||
danmuku: Danmuku
|
||||
/**
|
||||
* 弹幕持续时间,范围在[1 ~ 10]
|
||||
*/
|
||||
speed?: number
|
||||
/**
|
||||
* 弹幕上下边距,支持像素数字和百分比
|
||||
*/
|
||||
margin?: [
|
||||
number | `${number}%`,
|
||||
number | `${number}%`,
|
||||
]
|
||||
/**
|
||||
* 弹幕透明度,范围在[0 ~ 1]
|
||||
*/
|
||||
opacity?: number
|
||||
/**
|
||||
* 默认弹幕颜色,可以被单独弹幕项覆盖
|
||||
*/
|
||||
color?: string
|
||||
/**
|
||||
* 弹幕模式: 0: 滚动,1: 顶部,2: 底部
|
||||
*/
|
||||
mode?: Mode
|
||||
/**
|
||||
* 弹幕可见的模式
|
||||
*/
|
||||
modes?: Mode[]
|
||||
/**
|
||||
* 弹幕字体大小,支持像素数字和百分比
|
||||
*/
|
||||
fontSize?: number | `${number}%`
|
||||
/**
|
||||
* 弹幕是否防重叠
|
||||
*/
|
||||
antiOverlap?: boolean
|
||||
/**
|
||||
* 是否同步播放速度
|
||||
*/
|
||||
synchronousPlayback?: boolean
|
||||
/**
|
||||
* 弹幕发射器挂载点, 默认为播放器控制栏中部
|
||||
*/
|
||||
mount?: HTMLDivElement | string
|
||||
/**
|
||||
* 是否开启弹幕热度图
|
||||
*/
|
||||
heatmap?: boolean | {
|
||||
xMin?: number
|
||||
xMax?: number
|
||||
yMin?: number
|
||||
@@ -120,140 +102,123 @@ export interface Option {
|
||||
smoothing?: number
|
||||
flattening?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 当播放器宽度小于此值时,弹幕发射器置于播放器底部
|
||||
*/
|
||||
width?: number
|
||||
|
||||
/**
|
||||
* 热力图数据
|
||||
*/
|
||||
points?: { time: number, value: number }[]
|
||||
|
||||
/**
|
||||
* 弹幕载入前的过滤器,只支持返回布尔值
|
||||
*/
|
||||
filter?: (danmu: Danmu) => boolean
|
||||
|
||||
/**
|
||||
* 弹幕发送前的过滤器,支持返回 Promise
|
||||
*/
|
||||
beforeEmit?: (danmu: Danmu) => boolean | Promise<boolean>
|
||||
|
||||
/**
|
||||
* 弹幕显示前的过滤器,支持返回 Promise
|
||||
*/
|
||||
beforeVisible?: (danmu: Danmu) => boolean | Promise<boolean>
|
||||
|
||||
/**
|
||||
* 弹幕是否可见
|
||||
*/
|
||||
visible?: boolean
|
||||
|
||||
/**
|
||||
* 是否开启弹幕发射器
|
||||
*/
|
||||
emitter?: boolean
|
||||
|
||||
/**
|
||||
* 弹幕输入框最大长度, 范围在[1 ~ 1000]
|
||||
*/
|
||||
maxLength?: number
|
||||
|
||||
/**
|
||||
* 输入框锁定时间,范围在[1 ~ 60]
|
||||
*/
|
||||
lockTime?: number
|
||||
|
||||
/**
|
||||
* 弹幕主题,只在自定义挂载时生效
|
||||
*/
|
||||
theme?: 'light' | 'dark'
|
||||
|
||||
/**
|
||||
* 不透明度配置项
|
||||
*/
|
||||
OPACITY?: Slider
|
||||
|
||||
/**
|
||||
* 弹幕速度配置项
|
||||
*/
|
||||
SPEED?: Slider
|
||||
|
||||
/**
|
||||
* 显示区域配置项
|
||||
*/
|
||||
MARGIN?: Slider
|
||||
|
||||
/**
|
||||
* 弹幕字号配置项
|
||||
*/
|
||||
FONT_SIZE?: Slider
|
||||
|
||||
/**
|
||||
* 颜色列表配置项
|
||||
*/
|
||||
COLOR?: string[]
|
||||
/**
|
||||
* 当播放器宽度小于此值时,弹幕发射器置于播放器底部
|
||||
*/
|
||||
width?: number
|
||||
/**
|
||||
* 热力图数据
|
||||
*/
|
||||
points?: {
|
||||
time: number
|
||||
value: number
|
||||
}[]
|
||||
/**
|
||||
* 弹幕载入前的过滤器,只支持返回布尔值
|
||||
*/
|
||||
filter?: (danmu: Danmu) => boolean
|
||||
/**
|
||||
* 弹幕发送前的过滤器,支持返回 Promise
|
||||
*/
|
||||
beforeEmit?: (danmu: Danmu) => boolean | Promise<boolean>
|
||||
/**
|
||||
* 弹幕显示前的过滤器,支持返回 Promise
|
||||
*/
|
||||
beforeVisible?: (danmu: Danmu) => boolean | Promise<boolean>
|
||||
/**
|
||||
* 弹幕是否可见
|
||||
*/
|
||||
visible?: boolean
|
||||
/**
|
||||
* 是否开启弹幕发射器
|
||||
*/
|
||||
emitter?: boolean
|
||||
/**
|
||||
* 弹幕输入框最大长度, 范围在[1 ~ 1000]
|
||||
*/
|
||||
maxLength?: number
|
||||
/**
|
||||
* 输入框锁定时间,范围在[1 ~ 60]
|
||||
*/
|
||||
lockTime?: number
|
||||
/**
|
||||
* 弹幕主题,只在自定义挂载时生效
|
||||
*/
|
||||
theme?: 'light' | 'dark'
|
||||
/**
|
||||
* 不透明度配置项
|
||||
*/
|
||||
OPACITY?: Slider
|
||||
/**
|
||||
* 弹幕速度配置项
|
||||
*/
|
||||
SPEED?: Slider
|
||||
/**
|
||||
* 显示区域配置项
|
||||
*/
|
||||
MARGIN?: Slider
|
||||
/**
|
||||
* 弹幕字号配置项
|
||||
*/
|
||||
FONT_SIZE?: Slider
|
||||
/**
|
||||
* 颜色列表配置项
|
||||
*/
|
||||
COLOR?: string[]
|
||||
}
|
||||
export interface Result {
|
||||
name: 'artplayerPluginDanmuku'
|
||||
/**
|
||||
* 发送一条实时弹幕
|
||||
*/
|
||||
emit: (danmu: Danmu) => Result
|
||||
/**
|
||||
* 重载弹幕源,或者切换新弹幕
|
||||
*/
|
||||
load: (danmuku?: Danmuku) => Promise<Result>
|
||||
/**
|
||||
* 实时改变弹幕配置
|
||||
*/
|
||||
config: (option: Option) => Result
|
||||
/**
|
||||
* 隐藏弹幕层
|
||||
*/
|
||||
hide: () => Result
|
||||
/**
|
||||
* 显示弹幕层
|
||||
*/
|
||||
show: () => Result
|
||||
/**
|
||||
* 挂载弹幕输入框
|
||||
*/
|
||||
mount: (el?: HTMLDivElement | string) => void
|
||||
/**
|
||||
* 重置弹幕
|
||||
*/
|
||||
reset: () => Result
|
||||
/**
|
||||
* 弹幕配置
|
||||
*/
|
||||
option: Option
|
||||
/**
|
||||
* 是否隐藏弹幕层
|
||||
*/
|
||||
isHide: boolean
|
||||
/**
|
||||
* 是否弹幕层停止状态
|
||||
*/
|
||||
isStop: boolean
|
||||
}
|
||||
export const artplayerPluginDanmuku: (option: Option) => (art: Artplayer) => Result
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
name: 'artplayerPluginDanmuku'
|
||||
|
||||
/**
|
||||
* 发送一条实时弹幕
|
||||
*/
|
||||
emit: (danmu: Danmu) => Result
|
||||
|
||||
/**
|
||||
* 重载弹幕源,或者切换新弹幕
|
||||
*/
|
||||
load: (danmuku?: Danmuku) => Promise<Result>
|
||||
|
||||
/**
|
||||
* 实时改变弹幕配置
|
||||
*/
|
||||
config: (option: Option) => Result
|
||||
|
||||
/**
|
||||
* 隐藏弹幕层
|
||||
*/
|
||||
hide: () => Result
|
||||
|
||||
/**
|
||||
* 显示弹幕层
|
||||
*/
|
||||
show: () => Result
|
||||
|
||||
/**
|
||||
* 挂载弹幕输入框
|
||||
*/
|
||||
mount: (el?: HTMLDivElement | string) => void
|
||||
|
||||
/**
|
||||
* 重置弹幕
|
||||
*/
|
||||
reset: () => Result
|
||||
|
||||
/**
|
||||
* 弹幕配置
|
||||
*/
|
||||
option: Option
|
||||
|
||||
/**
|
||||
* 是否隐藏弹幕层
|
||||
*/
|
||||
isHide: boolean
|
||||
|
||||
/**
|
||||
* 是否弹幕层停止状态
|
||||
*/
|
||||
isStop: boolean
|
||||
declare const artplayerPluginDanmuku: typeof artplayerPluginDanmukuDefinitions.artplayerPluginDanmuku
|
||||
declare namespace artplayerPluginDanmuku {
|
||||
export type Mode = artplayerPluginDanmukuDefinitions.Mode
|
||||
export type Danmuku = artplayerPluginDanmukuDefinitions.Danmuku
|
||||
export type Slider = artplayerPluginDanmukuDefinitions.Slider
|
||||
export type Danmu = artplayerPluginDanmukuDefinitions.Danmu
|
||||
export type Option = artplayerPluginDanmukuDefinitions.Option
|
||||
export type Result = artplayerPluginDanmukuDefinitions.Result
|
||||
}
|
||||
|
||||
declare const artplayerPluginDanmuku: (option: Option) => (art: Artplayer) => Result
|
||||
|
||||
export default artplayerPluginDanmuku
|
||||
|
||||
export = artplayerPluginDanmuku
|
||||
export as namespace artplayerPluginDanmuku;
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";function t(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function e(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function i(t){if("string"!==typeof t)return[];const i=t.matchAll(/<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\/d>/gs);return Array.from(i).map(t=>{const i=t.groups.p.split(",");if(i.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(i[0]),mode:e(Number(i[1])),fontSize:Number(i[2]),color:`#${Number(i[3]).toString(16)}`,timestamp:Number(i[4]),pool:Number(i[5]),userID:i[6],rowID:Number(i[7])}}return null}).filter(Boolean)}function n({data:t}){const{xml:e,id:n}=t,a=i(e);globalThis.postMessage({danmus:a,id:n})}let a=0;function s(t,{signal:s,onCancel:r}={}){return new Promise((o,l)=>{let d,c,p,h=!1;const u=++a,m=()=>g(void 0,function(t){if(void 0!==t?.reason)return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(s));function f(){let t,e=!1;try{if(d){const t=d;d=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==c){const n=c;c=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function g(t,e){if(h)return;h=!0;let i=arguments.length>1;s?.removeEventListener("abort",m),p?.();try{f()}catch(n){i||(e=n),i=!0}i?l(e):o(t)}function y(t,e){if(!h){console.error("Error parsing Bilibili Danmu:",e);try{g(i(t))}catch(n){g(void 0,n)}}}s?.aborted?m():(s?.addEventListener("abort",m),p=r?.(m),h?p?.():async function(){const a=s?await fetch(t,{signal:s}):await fetch(t);if(h)return;const r=await a.text();if(!h)try{const t=`\n ${e.toString()}\n ${i.toString()}\n onmessage = ${n.toString()}\n `;if(c=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),h)return void f();if(d=new Worker(c),h)return void f();d.onmessage=t=>{if(h||t.data?.id!==u)return;const{danmus:e}=t.data;Array.isArray(e)?g(e):g(void 0,new Error("Invalid Bilibili Danmu worker response"))},d.onerror=t=>{t.preventDefault?.(),y(r,t.error||t)},d.onmessageerror=t=>y(r,t.error||t),d.postMessage({xml:r,id:u})}catch(o){y(r,o)}}().catch(t=>g(void 0,t)))})}const r=new WeakMap,o=Symbol("cancelled danmuku input");function l(t){return r.has(t)||r.set(t,{closed:!1,tasks:new Set,emitting:[]}),r.get(t)}function d(t){const e=l(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function c(t){const e=l(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function p(t,e,i){const n=t.states[e]||[];for(let a=0;a<n.length;a++)i(n[a]);return n}class h{constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=s+o,d=e-l,c=l/n.$restTime,p={};p.top=a,p.left=s,p.height=r,p.width=o,p.right=d,p.speed=c,p.distance=l,p.time=n.$restTime,p.mode=n.mode,t.push(p)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emit",e=>{switch(e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.left=`${t}px`,e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}})}continue(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("stop",e=>{switch(this.owner.setState(e,"emit"),e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}}),this}suspend(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("emit",e=>{switch(this.owner.setState(e,"stop"),e.mode){case 0:{const i=t-(this.owner.getLeft(e.$ref)-this.owner.getLeft(this.owner.$player));e.$ref.style.transform=`translLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";function t(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function e(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function i(t){if("string"!==typeof t)return[];const i=t.matchAll(/<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\/d>/gs);return Array.from(i).map(t=>{const i=t.groups.p.split(",");if(i.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(i[0]),mode:e(Number(i[1])),fontSize:Number(i[2]),color:`#${Number(i[3]).toString(16)}`,timestamp:Number(i[4]),pool:Number(i[5]),userID:i[6],rowID:Number(i[7])}}return null}).filter(Boolean)}function n({data:t}){const{xml:e,id:n}=t,a=i(e);globalThis.postMessage({danmus:a,id:n})}let a=0;function s(t,{signal:s,onCancel:r}={}){return new Promise((o,l)=>{let d,c,p,h=!1;const u=++a,m=()=>g(void 0,function(t){if(void 0!==t?.reason)return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(s));function f(){let t,e=!1;try{if(d){const t=d;d=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==c){const n=c;c=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function g(t,e){if(h)return;h=!0;let i=arguments.length>1;s?.removeEventListener("abort",m),p?.();try{f()}catch(n){i||(e=n),i=!0}i?l(e):o(t)}function y(t,e){if(!h){console.error("Error parsing Bilibili Danmu:",e);try{g(i(t))}catch(n){g(void 0,n)}}}s?.aborted?m():(s?.addEventListener("abort",m),p=r?.(m),h?p?.():async function(){const a=s?await fetch(t,{signal:s}):await fetch(t);if(h)return;const r=await a.text();if(!h)try{const t=`\n ${e.toString()}\n ${i.toString()}\n onmessage = ${n.toString()}\n `;if(c=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),h)return void f();if(d=new Worker(c),h)return void f();d.onmessage=t=>{if(h||t.data?.id!==u)return;const{danmus:e}=t.data;Array.isArray(e)?g(e):g(void 0,new Error("Invalid Bilibili Danmu worker response"))},d.onerror=t=>{t.preventDefault?.(),y(r,t.error||t)},d.onmessageerror=t=>y(r,t.error||t),d.postMessage({xml:r,id:u})}catch(o){y(r,o)}}().catch(t=>g(void 0,t)))})}const r=new WeakMap,o=Symbol("cancelled danmuku input");function l(t){return r.has(t)||r.set(t,{closed:!1,tasks:new Set,emitting:[]}),r.get(t)}function d(t){const e=l(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function c(t){const e=l(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function p(t,e,i){const n=t.states[e]||[];for(let a=0;a<n.length;a++)i(n[a]);return n}class h{constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=s+o,d=e-l,c=l/n.$restTime,p={};p.top=a,p.left=s,p.height=r,p.width=o,p.right=d,p.speed=c,p.distance=l,p.time=n.$restTime,p.mode=n.mode,t.push(p)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emit",e=>{switch(e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.left=`${t}px`,e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}})}continue(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("stop",e=>{switch(this.owner.setState(e,"emit"),e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}}),this}suspend(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("emit",e=>{switch(this.owner.setState(e,"stop"),e.mode){case 0:{const i=t-(this.owner.getLeft(e.$ref)-this.owner.getLeft(this.owner.$player));e.$ref.style.transform=`translLine truncated
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.defineProperties,i=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,r=Math.pow,o=(e,i,n)=>i in e?t(e,i,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[i]=n,l=(t,e)=>{for(var i in e||(e={}))a.call(e,i)&&o(t,i,e[i]);if(n)for(var i of n(e))s.call(e,i)&&o(t,i,e[i]);return t},d=(t,n)=>e(t,i(n)),c=(t,e,i)=>new Promise((n,a)=>{var s=t=>{try{o(i.next(t))}catch(e){a(e)}},r=t=>{try{o(i.throw(t))}catch(e){a(e)}},o=t=>t.done?n(t.value):Promise.resolve(t.value).then(s,r);o((i=i.apply(t,e)).next())});function p(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function h(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function u(t){if("string"!==typeof t)return[];const e=new RegExp('<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\\/d>',"gs"),i=t.matchAll(e);return Array.from(i).map(t=>{const e=t.groups.p.split(",");if(e.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(e[0]),mode:h(Number(e[1])),fontSize:Number(e[2]),color:`#${Number(e[3]).toString(16)}`,timestamp:Number(e[4]),pool:Number(e[5]),userID:e[6],rowID:Number(e[7])}}return null}).filter(Boolean)}function m({data:t}){const{xml:e,id:i}=t,n=u(e);globalThis.postMessage({danmus:n,id:i})}let f=0;function g(t,{signal:e,onCancel:i}={}){return new Promise((n,a)=>{let s,r,o,l=!1;const d=++f,p=()=>y(void 0,function(t){if(void 0!==(null==t?void 0:t.reason))return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(e));function g(){let t,e=!1;try{if(s){const t=s;s=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==r){const n=r;r=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function y(t,i){if(l)return;l=!0;let s=arguments.length>1;null==e||e.removeEventListener("abort",p),null==o||o();try{g()}catch(r){s||(i=r),s=!0}s?a(i):n(t)}function v(t,e){if(!l){console.error("Error parsing Bilibili Danmu:",e);try{y(u(t))}catch(i){y(void 0,i)}}}(null==e?void 0:e.aborted)?p():(null==e||e.addEventListener("abort",p),o=null==i?void 0:i(p),l?null==o||o():function(){return c(this,null,function*(){const i=e?yield fetch(t,{signal:e}):yield fetch(t);if(l)return;const n=yield i.text();if(!l)try{const t=`\n ${h.toString()}\n ${u.toString()}\n onmessage = ${m.toString()}\n `;if(r=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),l)return void g();if(s=new Worker(r),l)return void g();s.onmessage=t=>{var e;if(l||(null==(e=t.data)?void 0:e.id)!==d)return;const{danmus:i}=t.data;Array.isArray(i)?y(i):y(void 0,new Error("Invalid Bilibili Danmu worker response"))},s.onerror=t=>{var e;null==(e=t.preventDefault)||e.call(t),v(n,t.error||t)},s.onmessageerror=t=>v(n,t.error||t),s.postMessage({xml:n,id:d})}catch(a){v(n,a)}})}().catch(t=>y(void 0,t)))})}const y=new WeakMap,v=Symbol("cancelled danmuku input");function w(t){return y.has(t)||y.set(t,{closed:!1,tasks:new Set,emitting:[]}),y.get(t)}function k(t){const e=w(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function x(t){const e=w(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function b(t,e,i){const n=t.states[e]||[];for(let a=0;a<n.length;a++)i(n[a]);return n}class ${constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=s+o,d=e-l,c=l/n.$restTime,p={};p.top=a,p.left=s,p.height=r,p.width=o,p.right=d,p.speed=c,p.distance=l,p.time=n.$restTime,p.mode=n.mode,t.push(p)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emitLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.defineProperties,i=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,r=Math.pow,o=(e,i,n)=>i in e?t(e,i,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[i]=n,l=(t,e)=>{for(var i in e||(e={}))a.call(e,i)&&o(t,i,e[i]);if(n)for(var i of n(e))s.call(e,i)&&o(t,i,e[i]);return t},d=(t,n)=>e(t,i(n)),c=(t,e,i)=>new Promise((n,a)=>{var s=t=>{try{o(i.next(t))}catch(e){a(e)}},r=t=>{try{o(i.throw(t))}catch(e){a(e)}},o=t=>t.done?n(t.value):Promise.resolve(t.value).then(s,r);o((i=i.apply(t,e)).next())});function p(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function h(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function u(t){if("string"!==typeof t)return[];const e=new RegExp('<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\\/d>',"gs"),i=t.matchAll(e);return Array.from(i).map(t=>{const e=t.groups.p.split(",");if(e.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(e[0]),mode:h(Number(e[1])),fontSize:Number(e[2]),color:`#${Number(e[3]).toString(16)}`,timestamp:Number(e[4]),pool:Number(e[5]),userID:e[6],rowID:Number(e[7])}}return null}).filter(Boolean)}function m({data:t}){const{xml:e,id:i}=t,n=u(e);globalThis.postMessage({danmus:n,id:i})}let f=0;function g(t,{signal:e,onCancel:i}={}){return new Promise((n,a)=>{let s,r,o,l=!1;const d=++f,p=()=>y(void 0,function(t){if(void 0!==(null==t?void 0:t.reason))return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(e));function g(){let t,e=!1;try{if(s){const t=s;s=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==r){const n=r;r=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function y(t,i){if(l)return;l=!0;let s=arguments.length>1;null==e||e.removeEventListener("abort",p),null==o||o();try{g()}catch(r){s||(i=r),s=!0}s?a(i):n(t)}function v(t,e){if(!l){console.error("Error parsing Bilibili Danmu:",e);try{y(u(t))}catch(i){y(void 0,i)}}}(null==e?void 0:e.aborted)?p():(null==e||e.addEventListener("abort",p),o=null==i?void 0:i(p),l?null==o||o():function(){return c(this,null,function*(){const i=e?yield fetch(t,{signal:e}):yield fetch(t);if(l)return;const n=yield i.text();if(!l)try{const t=`\n ${h.toString()}\n ${u.toString()}\n onmessage = ${m.toString()}\n `;if(r=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),l)return void g();if(s=new Worker(r),l)return void g();s.onmessage=t=>{var e;if(l||(null==(e=t.data)?void 0:e.id)!==d)return;const{danmus:i}=t.data;Array.isArray(i)?y(i):y(void 0,new Error("Invalid Bilibili Danmu worker response"))},s.onerror=t=>{var e;null==(e=t.preventDefault)||e.call(t),v(n,t.error||t)},s.onmessageerror=t=>v(n,t.error||t),s.postMessage({xml:n,id:d})}catch(a){v(n,a)}})}().catch(t=>y(void 0,t)))})}const y=new WeakMap,v=Symbol("cancelled danmuku input");function w(t){return y.has(t)||y.set(t,{closed:!1,tasks:new Set,emitting:[]}),y.get(t)}function k(t){const e=w(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function x(t){const e=w(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function b(t,e,i){const n=t.states[e]||[];for(let a=0;a<n.length;a++)i(n[a]);return n}class ${constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=s+o,d=e-l,c=l/n.$restTime,p={};p.top=a,p.left=s,p.height=r,p.width=o,p.right=d,p.speed=c,p.distance=l,p.time=n.$restTime,p.mode=n.mode,t.push(p)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emitLine truncated
|
||||
@@ -665,16 +665,18 @@ class Scheduler {
|
||||
danmu.$lastStartTime = Date.now();
|
||||
danmu.$restTime = owner.speed;
|
||||
const distance = clientWidth + ref.clientWidth;
|
||||
const { result: top } = await operation.wait(owner.postMessage({
|
||||
const reply = 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,
|
||||
// Valid margins are numeric. Preserve the old raw fallback for unsupported strings.
|
||||
marginBottom: owner.marginBottom,
|
||||
marginTop: owner.marginTop
|
||||
}));
|
||||
const top = typeof reply === "symbol" ? void 0 : reply.result;
|
||||
if (!this.active(operation) || danmu.$ref !== ref)
|
||||
return;
|
||||
if (top !== void 0) {
|
||||
@@ -948,6 +950,7 @@ class Danmuku {
|
||||
if (!task.active())
|
||||
return this;
|
||||
errorHandle(Array.isArray(danmus), "Danmuku need return an array as result");
|
||||
const rows = danmus;
|
||||
if (danmuku === void 0) {
|
||||
this.reset();
|
||||
if (!task.active())
|
||||
@@ -956,10 +959,10 @@ class Danmuku {
|
||||
this.states = { wait: [], ready: [], emit: [], stop: [] };
|
||||
this.renderer.clear();
|
||||
}
|
||||
for (let index = 0; index < danmus.length; index++) {
|
||||
for (let index = 0; index < rows.length; index++) {
|
||||
if (!task.active())
|
||||
return this;
|
||||
const danmu = danmus[index];
|
||||
const danmu = rows[index];
|
||||
await task.emit(danmu);
|
||||
}
|
||||
if (task.active())
|
||||
@@ -1074,6 +1077,7 @@ class Danmuku {
|
||||
return this.renderer.left($ref);
|
||||
}
|
||||
// 复杂运算交给 Web Worker 处理
|
||||
// Keep the historical empty default; unknown message types receive no reply.
|
||||
postMessage(message = {}) {
|
||||
return this.workerClient.request(message);
|
||||
}
|
||||
@@ -1312,8 +1316,9 @@ function heatmap(art, danmuku, option) {
|
||||
let closed = false;
|
||||
const active = () => !closed && !art.isDestroy;
|
||||
function listen(name, callback) {
|
||||
subscriptions.push([name, callback]);
|
||||
art.on(name, callback);
|
||||
const listener = callback;
|
||||
subscriptions.push([name, listener]);
|
||||
art.on(name, listener);
|
||||
}
|
||||
function dispose(removeControl) {
|
||||
if (closed)
|
||||
@@ -1351,18 +1356,19 @@ function heatmap(art, danmuku, option) {
|
||||
return;
|
||||
start = null;
|
||||
stop = null;
|
||||
element.innerHTML = "";
|
||||
const target = element;
|
||||
target.innerHTML = "";
|
||||
if (art.option.isLive)
|
||||
return;
|
||||
const shape = heatmapGeometry({ width: element.offsetWidth, height: element.offsetHeight, duration: art.duration, queue: danmuku.queue, option, points });
|
||||
const shape = heatmapGeometry({ width: target.offsetWidth, height: target.offsetHeight, duration: art.duration, queue: danmuku.queue, option, points });
|
||||
if (!shape || !active())
|
||||
return;
|
||||
const document2 = element.ownerDocument;
|
||||
const document2 = target.ownerDocument;
|
||||
if (!gradient)
|
||||
gradient = document2 ? "heatmap-solids" : `heatmap-solids-${++nextGradient}`;
|
||||
while (document2?.getElementById(gradient))
|
||||
gradient = `heatmap-solids-${++nextGradient}`;
|
||||
element.innerHTML = `
|
||||
target.innerHTML = `
|
||||
<svg viewBox="0 0 ${shape.width} ${shape.height}">
|
||||
<defs>
|
||||
<linearGradient id="${gradient}" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
@@ -1375,8 +1381,8 @@ function heatmap(art, danmuku, option) {
|
||||
<path fill="url(#${gradient})" d="${shape.path}"></path>
|
||||
</svg>
|
||||
`;
|
||||
start = query("#heatmap-start", element);
|
||||
stop = query("#heatmap-stop", element);
|
||||
start = query("#heatmap-start", target);
|
||||
stop = query("#heatmap-stop", target);
|
||||
progress(art.played);
|
||||
}
|
||||
function update(points) {
|
||||
@@ -1456,7 +1462,8 @@ class SettingLifecycle {
|
||||
dispose();
|
||||
else this.disposers.add(dispose);
|
||||
}
|
||||
write(target, key, value, apply = (value2) => target[key] = value2) {
|
||||
write(host, key, value, apply = (value2) => host[key] = value2) {
|
||||
const target = host;
|
||||
if (!this.active)
|
||||
return;
|
||||
const priority = () => typeof target.getPropertyPriority === "function" ? target.getPropertyPriority(key) : void 0;
|
||||
@@ -1847,16 +1854,17 @@ const style = ".artplayer-plugin-danmuku {\n display: flex;\n position: relati
|
||||
if (typeof document !== "undefined") {
|
||||
const id = "artplayer-plugin-danmuku";
|
||||
const pending = /* @__PURE__ */ Symbol.for("artplayer-plugin-danmuku.pending-style");
|
||||
let $style2 = document.getElementById(id) || document[pending];
|
||||
const styleDocument = document;
|
||||
let $style2 = document.getElementById(id) || styleDocument[pending];
|
||||
if (!$style2) {
|
||||
$style2 = document.createElement("style");
|
||||
$style2.id = id;
|
||||
if (document.readyState === "loading") {
|
||||
document[pending] = $style2;
|
||||
styleDocument[pending] = $style2;
|
||||
const ready = () => {
|
||||
document.removeEventListener("DOMContentLoaded", ready);
|
||||
if (document[pending] === $style2)
|
||||
delete document[pending];
|
||||
if (styleDocument[pending] === $style2)
|
||||
delete styleDocument[pending];
|
||||
const existing = document.getElementById(id);
|
||||
if (existing)
|
||||
existing.textContent = $style2.textContent;
|
||||
|
||||
@@ -661,16 +661,18 @@ var artplayerPluginDanmuku = (function() {
|
||||
danmu.$lastStartTime = Date.now();
|
||||
danmu.$restTime = owner.speed;
|
||||
const distance = clientWidth + ref.clientWidth;
|
||||
const { result: top } = await operation.wait(owner.postMessage({
|
||||
const reply = 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,
|
||||
// Valid margins are numeric. Preserve the old raw fallback for unsupported strings.
|
||||
marginBottom: owner.marginBottom,
|
||||
marginTop: owner.marginTop
|
||||
}));
|
||||
const top = typeof reply === "symbol" ? void 0 : reply.result;
|
||||
if (!this.active(operation) || danmu.$ref !== ref)
|
||||
return;
|
||||
if (top !== void 0) {
|
||||
@@ -944,6 +946,7 @@ var artplayerPluginDanmuku = (function() {
|
||||
if (!task.active())
|
||||
return this;
|
||||
errorHandle(Array.isArray(danmus), "Danmuku need return an array as result");
|
||||
const rows = danmus;
|
||||
if (danmuku === void 0) {
|
||||
this.reset();
|
||||
if (!task.active())
|
||||
@@ -952,10 +955,10 @@ var artplayerPluginDanmuku = (function() {
|
||||
this.states = { wait: [], ready: [], emit: [], stop: [] };
|
||||
this.renderer.clear();
|
||||
}
|
||||
for (let index = 0; index < danmus.length; index++) {
|
||||
for (let index = 0; index < rows.length; index++) {
|
||||
if (!task.active())
|
||||
return this;
|
||||
const danmu = danmus[index];
|
||||
const danmu = rows[index];
|
||||
await task.emit(danmu);
|
||||
}
|
||||
if (task.active())
|
||||
@@ -1070,6 +1073,7 @@ var artplayerPluginDanmuku = (function() {
|
||||
return this.renderer.left($ref);
|
||||
}
|
||||
// 复杂运算交给 Web Worker 处理
|
||||
// Keep the historical empty default; unknown message types receive no reply.
|
||||
postMessage(message = {}) {
|
||||
return this.workerClient.request(message);
|
||||
}
|
||||
@@ -1235,7 +1239,8 @@ var artplayerPluginDanmuku = (function() {
|
||||
smoothing: 0.2,
|
||||
flattening: 0.2
|
||||
};
|
||||
const options = Object.assign({}, defaults, typeof option === "object" ? option : {});
|
||||
const configured = Object.assign({}, typeof option === "object" ? option : {});
|
||||
const options = Object.assign({}, defaults, configured);
|
||||
for (const key of Object.keys(defaults)) {
|
||||
if (!Number.isFinite(options[key]))
|
||||
options[key] = defaults[key];
|
||||
@@ -1244,7 +1249,8 @@ var artplayerPluginDanmuku = (function() {
|
||||
options.sampling = defaults.sampling;
|
||||
if (options.xMin === options.xMax || options.yMin === options.yMax)
|
||||
return null;
|
||||
const points = Array.isArray(input) && input.length ? [...input] : sampleHeatmap(queue, width, duration, options.sampling);
|
||||
const hasCustomPoints = Array.isArray(input) && input.length > 0;
|
||||
const points = hasCustomPoints ? [...input] : sampleHeatmap(queue, width, duration, options.sampling);
|
||||
if (!points.length || !points.every((point) => Array.isArray(point) && Number.isFinite(point[0]) && Number.isFinite(point[1]))) {
|
||||
return null;
|
||||
}
|
||||
@@ -1265,14 +1271,21 @@ var artplayerPluginDanmuku = (function() {
|
||||
const y = point[1];
|
||||
point[1] = y * (y > yMid ? 1 + options.scale : 1 - options.scale) + options.minHeight;
|
||||
}
|
||||
const automatic = !hasCustomPoints && !Number.isFinite(configured.yMin) && !Number.isFinite(configured.yMax);
|
||||
let peak = 0;
|
||||
for (const point of points) peak = Math.max(peak, point[1]);
|
||||
const fitted = automatic && peak > defaults.yMax / 4;
|
||||
if (fitted)
|
||||
options.yMax = peak * 4;
|
||||
const boundY = (value) => fitted ? Math.min(height, Math.max(height * 0.75, value)) : value;
|
||||
const controlPoint = (current, previous, next, reverse) => {
|
||||
const geometry = line(previous || current, next || current);
|
||||
const flat = map(Math.cos(geometry.angle) * options.flattening, 0, 1, 1, 0);
|
||||
const angle = geometry.angle * flat + (reverse ? Math.PI : 0);
|
||||
const length = geometry.length * options.smoothing;
|
||||
return [current[0] + Math.cos(angle) * length, current[1] + Math.sin(angle) * length];
|
||||
return [current[0] + Math.cos(angle) * length, boundY(current[1] + Math.sin(angle) * length)];
|
||||
};
|
||||
const positions = points.map((point) => [map(point[0], options.xMin, options.xMax, 0, width), map(point[1], options.yMin, options.yMax, height, 0)]);
|
||||
const positions = points.map((point) => [map(point[0], options.xMin, options.xMax, 0, width), boundY(map(point[1], options.yMin, options.yMax, height, 0))]);
|
||||
if (!positions.every((point) => point.every(Number.isFinite)))
|
||||
return null;
|
||||
const path = positions.map((point, index, all) => {
|
||||
@@ -1299,8 +1312,9 @@ var artplayerPluginDanmuku = (function() {
|
||||
let closed = false;
|
||||
const active = () => !closed && !art.isDestroy;
|
||||
function listen(name, callback) {
|
||||
subscriptions.push([name, callback]);
|
||||
art.on(name, callback);
|
||||
const listener = callback;
|
||||
subscriptions.push([name, listener]);
|
||||
art.on(name, listener);
|
||||
}
|
||||
function dispose(removeControl) {
|
||||
if (closed)
|
||||
@@ -1338,18 +1352,19 @@ var artplayerPluginDanmuku = (function() {
|
||||
return;
|
||||
start = null;
|
||||
stop = null;
|
||||
element.innerHTML = "";
|
||||
const target = element;
|
||||
target.innerHTML = "";
|
||||
if (art.option.isLive)
|
||||
return;
|
||||
const shape = heatmapGeometry({ width: element.offsetWidth, height: element.offsetHeight, duration: art.duration, queue: danmuku.queue, option, points });
|
||||
const shape = heatmapGeometry({ width: target.offsetWidth, height: target.offsetHeight, duration: art.duration, queue: danmuku.queue, option, points });
|
||||
if (!shape || !active())
|
||||
return;
|
||||
const document2 = element.ownerDocument;
|
||||
const document2 = target.ownerDocument;
|
||||
if (!gradient)
|
||||
gradient = document2 ? "heatmap-solids" : `heatmap-solids-${++nextGradient}`;
|
||||
while (document2?.getElementById(gradient))
|
||||
gradient = `heatmap-solids-${++nextGradient}`;
|
||||
element.innerHTML = `
|
||||
target.innerHTML = `
|
||||
<svg viewBox="0 0 ${shape.width} ${shape.height}">
|
||||
<defs>
|
||||
<linearGradient id="${gradient}" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
@@ -1362,8 +1377,8 @@ var artplayerPluginDanmuku = (function() {
|
||||
<path fill="url(#${gradient})" d="${shape.path}"></path>
|
||||
</svg>
|
||||
`;
|
||||
start = query("#heatmap-start", element);
|
||||
stop = query("#heatmap-stop", element);
|
||||
start = query("#heatmap-start", target);
|
||||
stop = query("#heatmap-stop", target);
|
||||
progress(art.played);
|
||||
}
|
||||
function update(points) {
|
||||
@@ -1443,7 +1458,8 @@ var artplayerPluginDanmuku = (function() {
|
||||
dispose();
|
||||
else this.disposers.add(dispose);
|
||||
}
|
||||
write(target, key, value, apply = (value2) => target[key] = value2) {
|
||||
write(host, key, value, apply = (value2) => host[key] = value2) {
|
||||
const target = host;
|
||||
if (!this.active)
|
||||
return;
|
||||
const priority = () => typeof target.getPropertyPriority === "function" ? target.getPropertyPriority(key) : void 0;
|
||||
@@ -1834,16 +1850,17 @@ var artplayerPluginDanmuku = (function() {
|
||||
if (typeof document !== "undefined") {
|
||||
const id = "artplayer-plugin-danmuku";
|
||||
const pending = /* @__PURE__ */ Symbol.for("artplayer-plugin-danmuku.pending-style");
|
||||
let $style2 = document.getElementById(id) || document[pending];
|
||||
const styleDocument = document;
|
||||
let $style2 = document.getElementById(id) || styleDocument[pending];
|
||||
if (!$style2) {
|
||||
$style2 = document.createElement("style");
|
||||
$style2.id = id;
|
||||
if (document.readyState === "loading") {
|
||||
document[pending] = $style2;
|
||||
styleDocument[pending] = $style2;
|
||||
const ready = () => {
|
||||
document.removeEventListener("DOMContentLoaded", ready);
|
||||
if (document[pending] === $style2)
|
||||
delete document[pending];
|
||||
if (styleDocument[pending] === $style2)
|
||||
delete styleDocument[pending];
|
||||
const existing = document.getElementById(id);
|
||||
if (existing)
|
||||
existing.textContent = $style2.textContent;
|
||||
|
||||
+3
-1
@@ -100,7 +100,9 @@
|
||||
"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-setting.test.js test/danmuku-heatmap.test.js test/danmuku-renderer.test.js test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js refactor/scripts/danmuku-contract.test.mjs"
|
||||
"test:danmuku": "node --test test/danmuku-setting.test.js test/danmuku-heatmap.test.js test/danmuku-renderer.test.js test/danmuku-scheduler.test.js test/danmuku-worker-client.test.js test/danmuku-input.test.js test/danmuku-parser.test.js test/danmuku-failures.test.js refactor/scripts/danmuku-contract.test.mjs",
|
||||
"test:danmuku-types": "node --test refactor/scripts/danmuku-types.test.mjs",
|
||||
"test:danmuku-types-package": "node refactor/scripts/danmuku-package-types.mjs"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -2,4 +2,5 @@ node_modules
|
||||
.DS_Store
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
src
|
||||
src
|
||||
tsconfig.json
|
||||
@@ -2,40 +2,100 @@
|
||||
|
||||
The compatibility reference is the actual npm 5.3.0 package. Its archive,
|
||||
historical declarations, earlier exports and failure evidence are indexed in
|
||||
`refactor/baselines/danmuku-release.json` and `danmuku-contract.md` at the
|
||||
repository root. Do not infer runtime contracts from the old declaration file.
|
||||
`refactor/baselines/danmuku-release.json` and
|
||||
`refactor/baselines/danmuku-contract.md` relative to the repository root. Do not infer runtime contracts from the old declaration file.
|
||||
|
||||
## Module responsibilities
|
||||
|
||||
| Module | Responsibility |
|
||||
| --- | --- |
|
||||
| `src/index.js` | Plugin factory, facade getters, bound methods, setting and heatmap composition |
|
||||
| `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 |
|
||||
| `src/renderer.js` | Owned node allocation, preparation, geometry snapshots, placement, pause/resume styling and disposal |
|
||||
| `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 coordinator, configuration controls, mount and fullscreen layout |
|
||||
| `src/setting-template.js` | Existing template HTML and static icon values |
|
||||
| `src/setting-slider.js` | Slider index, pointer and rotation behavior |
|
||||
| `src/setting-send.js` | Pending send, original error outlet and lock countdown |
|
||||
| `src/setting-lifecycle.js` | Exact subscription/proxy disposal, cancellation and conditional host-property restoration |
|
||||
| `src/setting-style.js` | Shared document style and one pending DOMContentLoaded installation |
|
||||
| `src/heatmap.js` | Owned control, progress stops, subscriptions and per-instance gradient |
|
||||
| `src/heatmap-sampling.js` | Sorted-time bin counts with historical numeric boundaries |
|
||||
| `src/heatmap-geometry.js` | Legacy point normalization, caller-visible point writes and SVG curve calculation |
|
||||
| `src/index.ts` | Plugin factory, facade getters, bound methods, setting and heatmap composition |
|
||||
| `src/danmuku.ts` | Internal instance, method return identity, configuration/load commits, event and lifecycle wiring |
|
||||
| `src/config.ts` | Fresh defaults, validation schema, configuration comparison and normalization |
|
||||
| `src/input.ts` | Input forms, replacement ownership, independent append operations, cancellation and synchronous callback reentry |
|
||||
| `src/bilibili-parser.ts` | Pure legacy-compatible XML fields, mode mapping and entity decoding |
|
||||
| `src/bilibili.ts` | Fetch and response text, one parser Worker per request, fallback, settlement and Blob URL cleanup |
|
||||
| `src/scheduler.ts` | One RAF or asynchronous frame, cancellation generations, preparation ownership and failure recovery |
|
||||
| `src/renderer.ts` | Owned node allocation, preparation, geometry snapshots, placement, pause/resume styling and disposal |
|
||||
| `src/queue.ts` | Ordered state pools, eligibility and state transitions |
|
||||
| `src/worker-client.ts` | Track Worker lifecycle, a shared response dispatcher, unique request IDs and pending requests |
|
||||
| `src/worker.ts` | Track placement Worker; a separate protocol from the XML parser Worker |
|
||||
| `src/setting.ts` | Settings coordinator, configuration controls, mount and fullscreen layout |
|
||||
| `src/setting-template.ts` | Existing template HTML and static icon values |
|
||||
| `src/setting-slider.ts` | Slider index, pointer and rotation behavior |
|
||||
| `src/setting-send.ts` | Pending send, original error outlet and lock countdown |
|
||||
| `src/setting-lifecycle.ts` | Exact subscription/proxy disposal, cancellation and conditional host-property restoration |
|
||||
| `src/setting-style.ts` | Shared document style and one pending DOMContentLoaded installation |
|
||||
| `src/heatmap.ts` | Owned control, progress stops, subscriptions and per-instance gradient |
|
||||
| `src/heatmap-sampling.ts` | Sorted-time bin counts with historical numeric boundaries |
|
||||
| `src/heatmap-geometry.ts` | Legacy point normalization, caller-visible point writes and SVG curve calculation |
|
||||
|
||||
The package is undergoing staged migration. PKG-DANMUKU-03 separates input and
|
||||
configuration responsibilities in JavaScript; PKG-DANMUKU-04 separates scheduling
|
||||
and track requests; PKG-DANMUKU-05 separates rendering and UI resources;
|
||||
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.
|
||||
All 20 owned runtime modules use TypeScript. Their responsibilities remain
|
||||
separate from public declaration compatibility and from release acceptance.
|
||||
The following files contain types only:
|
||||
|
||||
| Module | Responsibility |
|
||||
| --- | --- |
|
||||
| `src/types.ts` | Reexports public runtime data types and defines the internal Artplayer host adapter |
|
||||
| `src/worker-types.ts` | Placement requests, replies, visible rows and virtual boundary rows |
|
||||
| `src/parser-types.ts` | XML parser results and its independent Worker protocol |
|
||||
| `src/setting-types.ts` | Settings template slots and slider integration contracts |
|
||||
| `src/worker-assets.d.ts` | Inline placement Worker module declaration |
|
||||
| `types/runtime-shared.d.ts` | Single source for accurate option, callback, input, queue, facade, owner and event payload types |
|
||||
|
||||
`src/types.ts` imports/reexports `types/runtime-shared.d.ts` with type-only
|
||||
syntax. Keep shared option/data definitions there; do not create a second copy
|
||||
in production source. Host, DOM and Worker adapter types remain internal. The
|
||||
factory composes Danmuku, Setting and heatmap. Danmuku owns queue/state pools and
|
||||
coordinates input, renderer and scheduler. Scheduler/renderer refer back to the
|
||||
owner with type-only imports; these references add no runtime module cycle.
|
||||
The XML parser and heatmap sampling/geometry remain independently testable.
|
||||
|
||||
## Declaration entrypoints and type boundaries
|
||||
|
||||
The root and `/legacy` declarations retain the exact actual npm 5.3.0 type
|
||||
source, including historical inaccuracies and extraction behavior. Edit neither
|
||||
to make runtime implementation checks pass. The additive `/runtime` entrypoint
|
||||
selects the existing ESM or CommonJS factory with accurate declarations. Its
|
||||
`runtime.d.mts` entry exposes ESM type exports; `runtime.d.cts` and the
|
||||
`typesVersions` fallback `runtime.d.ts` expose the callable CommonJS factory
|
||||
and its type namespace. CommonJS returns the function itself; importing it does
|
||||
not add a `factory.default` property.
|
||||
|
||||
The argument object is required, while its fields are optional. The registrar
|
||||
returns `RuntimeResult` synchronously. Command methods resolve/return `Owner`,
|
||||
which is a different object; `emit` is asynchronous even though its initial
|
||||
insertion work is synchronous. The facade retains its three live getters and
|
||||
requires a valid target for `mount`. Public data includes mutable point tuples,
|
||||
typed static icons, normalized filter inputs and queue entries for visibility
|
||||
callbacks. Filters and visibility callbacks accept truthy results; sending
|
||||
requires strict `true`. Their `this` is the current normalized option. Loader
|
||||
functions retain their receiver-free call. `EventMap` supplies named payload
|
||||
tuples explicitly; importing `/runtime` does not augment old core event types.
|
||||
It contains types only, with no new event bus or runtime export.
|
||||
|
||||
Keep these narrow implementation assertions documented when editing them:
|
||||
|
||||
- Native style/dataset writes retain their historical numeric and null values.
|
||||
WebIDL performs the conversion; changing source assignments to satisfy DOM
|
||||
declarations could change controlled hosts or observable setters.
|
||||
- Emit/stop state pools own allocated nodes. A frame with a non-null reference
|
||||
has its associated item, and release checks node identity. Non-null assertions
|
||||
depend on those invariants; they do not replace lifecycle validation.
|
||||
- The legacy public core type describes `constructor` as `Function`. The
|
||||
factory's host adapter narrows it to the existing Artplayer static utilities
|
||||
and validator. It introduces no new core capability or minimum core version.
|
||||
- An omitted internal `postMessage` keeps its old empty-message behavior;
|
||||
unsupported margin strings retain the raw historical fallback at the Worker
|
||||
boundary. Placement types describe valid scheduler requests, with explicit
|
||||
compatibility assertions for those exceptional paths. These internal message
|
||||
helpers are not additional methods promised by the public `Owner` interface.
|
||||
|
||||
The strict package config disables JavaScript input. Its `ES2021.String` type
|
||||
library describes the existing XML `replaceAll` calls; it neither introduces a
|
||||
new runtime dependency nor raises the existing browser requirement. Modern and
|
||||
legacy build targets remain es2020 and es2015; syntax lowering does not polyfill
|
||||
`replaceAll` or other browser APIs.
|
||||
|
||||
## Contracts to retain
|
||||
|
||||
@@ -125,6 +185,25 @@ Late, unknown and duplicate replies do not settle a different request.
|
||||
|
||||
## Verification and remaining work
|
||||
|
||||
Run package commands from the repository root with the pinned Node version and
|
||||
Yarn Classic 1.22.22:
|
||||
|
||||
| Command | What it checks or produces |
|
||||
| --- | --- |
|
||||
| `yarn test:danmuku` | Historical and candidate input, parser, Worker, scheduling, rendering, settings and heatmap regressions |
|
||||
| `yarn test:danmuku-types` | Frozen root declarations, current/legacy compiler consumers, negative cases, implementation assignability and editor types |
|
||||
| `yarn test:danmuku-types-package` | Packed isolated installs, real entrypoints and consumer module/compiler modes; creates local evidence, does not publish |
|
||||
| `yarn typecheck` | Strict production sources and the repository consumer type matrix |
|
||||
| `yarn build artplayer-plugin-danmuku` | Normal package outputs and copied docs artifacts, including the inline TS Worker |
|
||||
| `yarn dev artplayer-plugin-danmuku` | Local demo rebuild for the package |
|
||||
|
||||
Edit `src/` for runtime changes, `types/runtime-shared.d.ts` for shared accurate
|
||||
data contracts and the corresponding `runtime.d.*` entry for module shape.
|
||||
Rebuild artifacts through the package script. The frozen root declaration and
|
||||
historical test helpers are compatibility evidence, not generated repair targets.
|
||||
Use `refactor/fixtures/implementation/danmuku.ts` when a source/declaration change must
|
||||
prove assignability to the actual implementation.
|
||||
|
||||
`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
|
||||
@@ -143,9 +222,11 @@ 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.
|
||||
|
||||
Long-run native load behavior still requires PKG-DANMUKU-07. Check task evidence
|
||||
and the risk register before changing completion or release status; short native
|
||||
regressions are not package-wide stability acceptance.
|
||||
Long-run native load behavior still requires PKG-DANMUKU-07; cross-plugin/core
|
||||
combination coverage and final distribution acceptance remain PKG-DANMUKU-08/09.
|
||||
Check task evidence and the risk register before changing completion or release
|
||||
status. These commands and short native regressions do not establish npm release
|
||||
readiness; final checks and release reviews remain separate gates.
|
||||
|
||||
## Rendering and UI resources
|
||||
|
||||
|
||||
@@ -6,6 +6,38 @@ Danmuku plugin for ArtPlayer
|
||||
|
||||
[https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-plugin-danmuku/index.js&example=danmuku)
|
||||
|
||||
## TypeScript entrypoints
|
||||
|
||||
Existing imports from `artplayer-plugin-danmuku` and `/legacy` retain the npm
|
||||
5.3.0 declarations. Use `/runtime` for declarations that describe the actual
|
||||
factory, asynchronous commands and returned internal owner:
|
||||
|
||||
```ts
|
||||
import danmuku from 'artplayer-plugin-danmuku/runtime'
|
||||
import type { Point, RuntimeOption } from 'artplayer-plugin-danmuku/runtime'
|
||||
|
||||
const option: RuntimeOption = { danmuku: [], heatmap: true }
|
||||
const points: Point[] = [[0, 10], [200, 20]]
|
||||
```
|
||||
|
||||
The option object is required; each field is optional. Registration returns the
|
||||
plugin facade immediately. `await plugin.emit(row)` and `await plugin.load()`
|
||||
return the internal owner, as do synchronous `config/hide/show/reset` commands.
|
||||
That owner differs from the registered facade. `mount(target)` returns undefined
|
||||
and requires a valid element or selector. The facade's option/state getters
|
||||
remain live.
|
||||
|
||||
The accurate entrypoint also exposes icon types, callback receivers and an
|
||||
explicit `EventMap` of payload tuples. Filters and visibility callbacks receive
|
||||
the current option as `this`; `beforeEmit` does too and accepts only strict
|
||||
`true`. Importing these types does not automatically alter Artplayer's historical
|
||||
event declarations. Custom points use mutable `[number, number]` tuples and
|
||||
retain the existing inner-array modifications during rendering.
|
||||
|
||||
ESM imports select the existing ESM factory; CommonJS
|
||||
`require('artplayer-plugin-danmuku/runtime')` returns the existing callable
|
||||
factory directly. No additional runtime implementation is loaded.
|
||||
|
||||
## Loading and configuration
|
||||
|
||||
`plugin.load()` reloads the configured input and replaces the queue after input
|
||||
@@ -66,7 +98,10 @@ finite `heatmap.yMin` or `heatmap.yMax`, and custom points events, retain their
|
||||
original coordinate mapping; use these when deliberately controlling the scale.
|
||||
|
||||
See [ARCHITECTURE.md](./ARCHITECTURE.md) for module ownership, compatibility
|
||||
constraints, tests and the remaining staged TypeScript migration.
|
||||
constraints and editing/verification commands. All owned runtime modules now use
|
||||
TypeScript, with shared public runtime data types maintained in
|
||||
`types/runtime-shared.d.ts`. Long-duration load tests, combination/distribution
|
||||
acceptance and release reviews remain separate unfinished stages.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";function t(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function e(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function i(t){if("string"!==typeof t)return[];const i=t.matchAll(/<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\/d>/gs);return Array.from(i).map(t=>{const i=t.groups.p.split(",");if(i.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(i[0]),mode:e(Number(i[1])),fontSize:Number(i[2]),color:`#${Number(i[3]).toString(16)}`,timestamp:Number(i[4]),pool:Number(i[5]),userID:i[6],rowID:Number(i[7])}}return null}).filter(Boolean)}function n({data:t}){const{xml:e,id:n}=t,a=i(e);globalThis.postMessage({danmus:a,id:n})}let a=0;function s(t,{signal:s,onCancel:r}={}){return new Promise((o,l)=>{let d,c,p,h=!1;const u=++a,m=()=>g(void 0,function(t){if(void 0!==t?.reason)return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(s));function f(){let t,e=!1;try{if(d){const t=d;d=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==c){const n=c;c=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function g(t,e){if(h)return;h=!0;let i=arguments.length>1;s?.removeEventListener("abort",m),p?.();try{f()}catch(n){i||(e=n),i=!0}i?l(e):o(t)}function y(t,e){if(!h){console.error("Error parsing Bilibili Danmu:",e);try{g(i(t))}catch(n){g(void 0,n)}}}s?.aborted?m():(s?.addEventListener("abort",m),p=r?.(m),h?p?.():async function(){const a=s?await fetch(t,{signal:s}):await fetch(t);if(h)return;const r=await a.text();if(!h)try{const t=`\n ${e.toString()}\n ${i.toString()}\n onmessage = ${n.toString()}\n `;if(c=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),h)return void f();if(d=new Worker(c),h)return void f();d.onmessage=t=>{if(h||t.data?.id!==u)return;const{danmus:e}=t.data;Array.isArray(e)?g(e):g(void 0,new Error("Invalid Bilibili Danmu worker response"))},d.onerror=t=>{t.preventDefault?.(),y(r,t.error||t)},d.onmessageerror=t=>y(r,t.error||t),d.postMessage({xml:r,id:u})}catch(o){y(r,o)}}().catch(t=>g(void 0,t)))})}const r=new WeakMap,o=Symbol("cancelled danmuku input");function l(t){return r.has(t)||r.set(t,{closed:!1,tasks:new Set,emitting:[]}),r.get(t)}function d(t){const e=l(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function c(t){const e=l(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function p(t,e,i){const n=t.states[e]||[];for(let a=0;a<n.length;a++)i(n[a]);return n}class h{constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=s+o,d=e-l,c=l/n.$restTime,p={};p.top=a,p.left=s,p.height=r,p.width=o,p.right=d,p.speed=c,p.distance=l,p.time=n.$restTime,p.mode=n.mode,t.push(p)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emit",e=>{switch(e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.left=`${t}px`,e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}})}continue(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("stop",e=>{switch(this.owner.setState(e,"emit"),e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}}),this}suspend(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("emit",e=>{switch(this.owner.setState(e,"stop"),e.mode){case 0:{const i=t-(this.owner.getLeft(e.$ref)-this.owner.getLeft(this.owner.$player));e.$ref.style.transform=`translLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";function t(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function e(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function i(t){if("string"!==typeof t)return[];const i=t.matchAll(/<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\/d>/gs);return Array.from(i).map(t=>{const i=t.groups.p.split(",");if(i.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(i[0]),mode:e(Number(i[1])),fontSize:Number(i[2]),color:`#${Number(i[3]).toString(16)}`,timestamp:Number(i[4]),pool:Number(i[5]),userID:i[6],rowID:Number(i[7])}}return null}).filter(Boolean)}function n({data:t}){const{xml:e,id:n}=t,a=i(e);globalThis.postMessage({danmus:a,id:n})}let a=0;function s(t,{signal:s,onCancel:r}={}){return new Promise((o,l)=>{let d,c,p,h=!1;const u=++a,m=()=>g(void 0,function(t){if(void 0!==t?.reason)return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(s));function f(){let t,e=!1;try{if(d){const t=d;d=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==c){const n=c;c=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function g(t,e){if(h)return;h=!0;let i=arguments.length>1;s?.removeEventListener("abort",m),p?.();try{f()}catch(n){i||(e=n),i=!0}i?l(e):o(t)}function y(t,e){if(!h){console.error("Error parsing Bilibili Danmu:",e);try{g(i(t))}catch(n){g(void 0,n)}}}s?.aborted?m():(s?.addEventListener("abort",m),p=r?.(m),h?p?.():async function(){const a=s?await fetch(t,{signal:s}):await fetch(t);if(h)return;const r=await a.text();if(!h)try{const t=`\n ${e.toString()}\n ${i.toString()}\n onmessage = ${n.toString()}\n `;if(c=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),h)return void f();if(d=new Worker(c),h)return void f();d.onmessage=t=>{if(h||t.data?.id!==u)return;const{danmus:e}=t.data;Array.isArray(e)?g(e):g(void 0,new Error("Invalid Bilibili Danmu worker response"))},d.onerror=t=>{t.preventDefault?.(),y(r,t.error||t)},d.onmessageerror=t=>y(r,t.error||t),d.postMessage({xml:r,id:u})}catch(o){y(r,o)}}().catch(t=>g(void 0,t)))})}const r=new WeakMap,o=Symbol("cancelled danmuku input");function l(t){return r.has(t)||r.set(t,{closed:!1,tasks:new Set,emitting:[]}),r.get(t)}function d(t){const e=l(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function c(t){const e=l(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function p(t,e,i){const n=t.states[e]||[];for(let a=0;a<n.length;a++)i(n[a]);return n}class h{constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=s+o,d=e-l,c=l/n.$restTime,p={};p.top=a,p.left=s,p.height=r,p.width=o,p.right=d,p.speed=c,p.distance=l,p.time=n.$restTime,p.mode=n.mode,t.push(p)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emit",e=>{switch(e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.left=`${t}px`,e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}})}continue(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("stop",e=>{switch(this.owner.setState(e,"emit"),e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}}),this}suspend(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("emit",e=>{switch(this.owner.setState(e,"stop"),e.mode){case 0:{const i=t-(this.owner.getLeft(e.$ref)-this.owner.getLeft(this.owner.$player));e.$ref.style.transform=`translLine truncated
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.defineProperties,i=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,r=Math.pow,o=(e,i,n)=>i in e?t(e,i,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[i]=n,l=(t,e)=>{for(var i in e||(e={}))a.call(e,i)&&o(t,i,e[i]);if(n)for(var i of n(e))s.call(e,i)&&o(t,i,e[i]);return t},d=(t,n)=>e(t,i(n)),c=(t,e,i)=>new Promise((n,a)=>{var s=t=>{try{o(i.next(t))}catch(e){a(e)}},r=t=>{try{o(i.throw(t))}catch(e){a(e)}},o=t=>t.done?n(t.value):Promise.resolve(t.value).then(s,r);o((i=i.apply(t,e)).next())});function p(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function h(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function u(t){if("string"!==typeof t)return[];const e=new RegExp('<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\\/d>',"gs"),i=t.matchAll(e);return Array.from(i).map(t=>{const e=t.groups.p.split(",");if(e.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(e[0]),mode:h(Number(e[1])),fontSize:Number(e[2]),color:`#${Number(e[3]).toString(16)}`,timestamp:Number(e[4]),pool:Number(e[5]),userID:e[6],rowID:Number(e[7])}}return null}).filter(Boolean)}function m({data:t}){const{xml:e,id:i}=t,n=u(e);globalThis.postMessage({danmus:n,id:i})}let f=0;function g(t,{signal:e,onCancel:i}={}){return new Promise((n,a)=>{let s,r,o,l=!1;const d=++f,p=()=>y(void 0,function(t){if(void 0!==(null==t?void 0:t.reason))return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(e));function g(){let t,e=!1;try{if(s){const t=s;s=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==r){const n=r;r=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function y(t,i){if(l)return;l=!0;let s=arguments.length>1;null==e||e.removeEventListener("abort",p),null==o||o();try{g()}catch(r){s||(i=r),s=!0}s?a(i):n(t)}function v(t,e){if(!l){console.error("Error parsing Bilibili Danmu:",e);try{y(u(t))}catch(i){y(void 0,i)}}}(null==e?void 0:e.aborted)?p():(null==e||e.addEventListener("abort",p),o=null==i?void 0:i(p),l?null==o||o():function(){return c(this,null,function*(){const i=e?yield fetch(t,{signal:e}):yield fetch(t);if(l)return;const n=yield i.text();if(!l)try{const t=`\n ${h.toString()}\n ${u.toString()}\n onmessage = ${m.toString()}\n `;if(r=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),l)return void g();if(s=new Worker(r),l)return void g();s.onmessage=t=>{var e;if(l||(null==(e=t.data)?void 0:e.id)!==d)return;const{danmus:i}=t.data;Array.isArray(i)?y(i):y(void 0,new Error("Invalid Bilibili Danmu worker response"))},s.onerror=t=>{var e;null==(e=t.preventDefault)||e.call(t),v(n,t.error||t)},s.onmessageerror=t=>v(n,t.error||t),s.postMessage({xml:n,id:d})}catch(a){v(n,a)}})}().catch(t=>y(void 0,t)))})}const y=new WeakMap,v=Symbol("cancelled danmuku input");function w(t){return y.has(t)||y.set(t,{closed:!1,tasks:new Set,emitting:[]}),y.get(t)}function k(t){const e=w(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function x(t){const e=w(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function b(t,e,i){const n=t.states[e]||[];for(let a=0;a<n.length;a++)i(n[a]);return n}class ${constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=s+o,d=e-l,c=l/n.$restTime,p={};p.top=a,p.left=s,p.height=r,p.width=o,p.right=d,p.speed=c,p.distance=l,p.time=n.$restTime,p.mode=n.mode,t.push(p)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emitLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.defineProperties,i=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,r=Math.pow,o=(e,i,n)=>i in e?t(e,i,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[i]=n,l=(t,e)=>{for(var i in e||(e={}))a.call(e,i)&&o(t,i,e[i]);if(n)for(var i of n(e))s.call(e,i)&&o(t,i,e[i]);return t},d=(t,n)=>e(t,i(n)),c=(t,e,i)=>new Promise((n,a)=>{var s=t=>{try{o(i.next(t))}catch(e){a(e)}},r=t=>{try{o(i.throw(t))}catch(e){a(e)}},o=t=>t.done?n(t.value):Promise.resolve(t.value).then(s,r);o((i=i.apply(t,e)).next())});function p(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function h(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function u(t){if("string"!==typeof t)return[];const e=new RegExp('<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\\/d>',"gs"),i=t.matchAll(e);return Array.from(i).map(t=>{const e=t.groups.p.split(",");if(e.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(e[0]),mode:h(Number(e[1])),fontSize:Number(e[2]),color:`#${Number(e[3]).toString(16)}`,timestamp:Number(e[4]),pool:Number(e[5]),userID:e[6],rowID:Number(e[7])}}return null}).filter(Boolean)}function m({data:t}){const{xml:e,id:i}=t,n=u(e);globalThis.postMessage({danmus:n,id:i})}let f=0;function g(t,{signal:e,onCancel:i}={}){return new Promise((n,a)=>{let s,r,o,l=!1;const d=++f,p=()=>y(void 0,function(t){if(void 0!==(null==t?void 0:t.reason))return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(e));function g(){let t,e=!1;try{if(s){const t=s;s=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==r){const n=r;r=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function y(t,i){if(l)return;l=!0;let s=arguments.length>1;null==e||e.removeEventListener("abort",p),null==o||o();try{g()}catch(r){s||(i=r),s=!0}s?a(i):n(t)}function v(t,e){if(!l){console.error("Error parsing Bilibili Danmu:",e);try{y(u(t))}catch(i){y(void 0,i)}}}(null==e?void 0:e.aborted)?p():(null==e||e.addEventListener("abort",p),o=null==i?void 0:i(p),l?null==o||o():function(){return c(this,null,function*(){const i=e?yield fetch(t,{signal:e}):yield fetch(t);if(l)return;const n=yield i.text();if(!l)try{const t=`\n ${h.toString()}\n ${u.toString()}\n onmessage = ${m.toString()}\n `;if(r=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),l)return void g();if(s=new Worker(r),l)return void g();s.onmessage=t=>{var e;if(l||(null==(e=t.data)?void 0:e.id)!==d)return;const{danmus:i}=t.data;Array.isArray(i)?y(i):y(void 0,new Error("Invalid Bilibili Danmu worker response"))},s.onerror=t=>{var e;null==(e=t.preventDefault)||e.call(t),v(n,t.error||t)},s.onmessageerror=t=>v(n,t.error||t),s.postMessage({xml:n,id:d})}catch(a){v(n,a)}})}().catch(t=>y(void 0,t)))})}const y=new WeakMap,v=Symbol("cancelled danmuku input");function w(t){return y.has(t)||y.set(t,{closed:!1,tasks:new Set,emitting:[]}),y.get(t)}function k(t){const e=w(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function x(t){const e=w(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function b(t,e,i){const n=t.states[e]||[];for(let a=0;a<n.length;a++)i(n[a]);return n}class ${constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=s+o,d=e-l,c=l/n.$restTime,p={};p.top=a,p.left=s,p.height=r,p.width=o,p.right=d,p.speed=c,p.distance=l,p.time=n.$restTime,p.mode=n.mode,t.push(p)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emitLine truncated
|
||||
@@ -665,16 +665,18 @@ class Scheduler {
|
||||
danmu.$lastStartTime = Date.now();
|
||||
danmu.$restTime = owner.speed;
|
||||
const distance = clientWidth + ref.clientWidth;
|
||||
const { result: top } = await operation.wait(owner.postMessage({
|
||||
const reply = 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,
|
||||
// Valid margins are numeric. Preserve the old raw fallback for unsupported strings.
|
||||
marginBottom: owner.marginBottom,
|
||||
marginTop: owner.marginTop
|
||||
}));
|
||||
const top = typeof reply === "symbol" ? void 0 : reply.result;
|
||||
if (!this.active(operation) || danmu.$ref !== ref)
|
||||
return;
|
||||
if (top !== void 0) {
|
||||
@@ -948,6 +950,7 @@ class Danmuku {
|
||||
if (!task.active())
|
||||
return this;
|
||||
errorHandle(Array.isArray(danmus), "Danmuku need return an array as result");
|
||||
const rows = danmus;
|
||||
if (danmuku === void 0) {
|
||||
this.reset();
|
||||
if (!task.active())
|
||||
@@ -956,10 +959,10 @@ class Danmuku {
|
||||
this.states = { wait: [], ready: [], emit: [], stop: [] };
|
||||
this.renderer.clear();
|
||||
}
|
||||
for (let index = 0; index < danmus.length; index++) {
|
||||
for (let index = 0; index < rows.length; index++) {
|
||||
if (!task.active())
|
||||
return this;
|
||||
const danmu = danmus[index];
|
||||
const danmu = rows[index];
|
||||
await task.emit(danmu);
|
||||
}
|
||||
if (task.active())
|
||||
@@ -1074,6 +1077,7 @@ class Danmuku {
|
||||
return this.renderer.left($ref);
|
||||
}
|
||||
// 复杂运算交给 Web Worker 处理
|
||||
// Keep the historical empty default; unknown message types receive no reply.
|
||||
postMessage(message = {}) {
|
||||
return this.workerClient.request(message);
|
||||
}
|
||||
@@ -1312,8 +1316,9 @@ function heatmap(art, danmuku, option) {
|
||||
let closed = false;
|
||||
const active = () => !closed && !art.isDestroy;
|
||||
function listen(name, callback) {
|
||||
subscriptions.push([name, callback]);
|
||||
art.on(name, callback);
|
||||
const listener = callback;
|
||||
subscriptions.push([name, listener]);
|
||||
art.on(name, listener);
|
||||
}
|
||||
function dispose(removeControl) {
|
||||
if (closed)
|
||||
@@ -1351,18 +1356,19 @@ function heatmap(art, danmuku, option) {
|
||||
return;
|
||||
start = null;
|
||||
stop = null;
|
||||
element.innerHTML = "";
|
||||
const target = element;
|
||||
target.innerHTML = "";
|
||||
if (art.option.isLive)
|
||||
return;
|
||||
const shape = heatmapGeometry({ width: element.offsetWidth, height: element.offsetHeight, duration: art.duration, queue: danmuku.queue, option, points });
|
||||
const shape = heatmapGeometry({ width: target.offsetWidth, height: target.offsetHeight, duration: art.duration, queue: danmuku.queue, option, points });
|
||||
if (!shape || !active())
|
||||
return;
|
||||
const document2 = element.ownerDocument;
|
||||
const document2 = target.ownerDocument;
|
||||
if (!gradient)
|
||||
gradient = document2 ? "heatmap-solids" : `heatmap-solids-${++nextGradient}`;
|
||||
while (document2?.getElementById(gradient))
|
||||
gradient = `heatmap-solids-${++nextGradient}`;
|
||||
element.innerHTML = `
|
||||
target.innerHTML = `
|
||||
<svg viewBox="0 0 ${shape.width} ${shape.height}">
|
||||
<defs>
|
||||
<linearGradient id="${gradient}" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
@@ -1375,8 +1381,8 @@ function heatmap(art, danmuku, option) {
|
||||
<path fill="url(#${gradient})" d="${shape.path}"></path>
|
||||
</svg>
|
||||
`;
|
||||
start = query("#heatmap-start", element);
|
||||
stop = query("#heatmap-stop", element);
|
||||
start = query("#heatmap-start", target);
|
||||
stop = query("#heatmap-stop", target);
|
||||
progress(art.played);
|
||||
}
|
||||
function update(points) {
|
||||
@@ -1456,7 +1462,8 @@ class SettingLifecycle {
|
||||
dispose();
|
||||
else this.disposers.add(dispose);
|
||||
}
|
||||
write(target, key, value, apply = (value2) => target[key] = value2) {
|
||||
write(host, key, value, apply = (value2) => host[key] = value2) {
|
||||
const target = host;
|
||||
if (!this.active)
|
||||
return;
|
||||
const priority = () => typeof target.getPropertyPriority === "function" ? target.getPropertyPriority(key) : void 0;
|
||||
@@ -1847,16 +1854,17 @@ const style = ".artplayer-plugin-danmuku {\n display: flex;\n position: relati
|
||||
if (typeof document !== "undefined") {
|
||||
const id = "artplayer-plugin-danmuku";
|
||||
const pending = /* @__PURE__ */ Symbol.for("artplayer-plugin-danmuku.pending-style");
|
||||
let $style2 = document.getElementById(id) || document[pending];
|
||||
const styleDocument = document;
|
||||
let $style2 = document.getElementById(id) || styleDocument[pending];
|
||||
if (!$style2) {
|
||||
$style2 = document.createElement("style");
|
||||
$style2.id = id;
|
||||
if (document.readyState === "loading") {
|
||||
document[pending] = $style2;
|
||||
styleDocument[pending] = $style2;
|
||||
const ready = () => {
|
||||
document.removeEventListener("DOMContentLoaded", ready);
|
||||
if (document[pending] === $style2)
|
||||
delete document[pending];
|
||||
if (styleDocument[pending] === $style2)
|
||||
delete styleDocument[pending];
|
||||
const existing = document.getElementById(id);
|
||||
if (existing)
|
||||
existing.textContent = $style2.textContent;
|
||||
|
||||
@@ -27,11 +27,31 @@
|
||||
"types": "./types/artplayer-plugin-danmuku.d.ts",
|
||||
"import": "./dist/artplayer-plugin-danmuku.legacy.js",
|
||||
"require": "./dist/artplayer-plugin-danmuku.legacy.js"
|
||||
},
|
||||
"./runtime": {
|
||||
"import": {
|
||||
"types": "./types/runtime.d.mts",
|
||||
"default": "./dist/artplayer-plugin-danmuku.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./types/runtime.d.cts",
|
||||
"default": "./dist/artplayer-plugin-danmuku.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": "./dist/artplayer-plugin-danmuku.js",
|
||||
"module": "./dist/artplayer-plugin-danmuku.mjs",
|
||||
"types": "./types/artplayer-plugin-danmuku.d.ts",
|
||||
"legacy": "./dist/artplayer-plugin-danmuku.legacy.js",
|
||||
"browserslist": "last 1 Chrome version"
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"legacy": [
|
||||
"types/artplayer-plugin-danmuku.d.ts"
|
||||
],
|
||||
"runtime": [
|
||||
"types/runtime.d.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-8
@@ -1,4 +1,6 @@
|
||||
export function getMode(key) {
|
||||
import type { ParsedDanmu, ParserReply, ParserRequest } from './parser-types'
|
||||
|
||||
export function getMode(key: number): number {
|
||||
switch (key) {
|
||||
case 1:
|
||||
case 2:
|
||||
@@ -13,16 +15,16 @@ export function getMode(key) {
|
||||
}
|
||||
}
|
||||
|
||||
export function bilibiliDanmuParseFromXml(xmlString) {
|
||||
export function bilibiliDanmuParseFromXml(xmlString: unknown): ParsedDanmu[] {
|
||||
if (typeof xmlString !== 'string')
|
||||
return []
|
||||
const reg = /<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\/d>/gs
|
||||
const matches = xmlString.matchAll(reg)
|
||||
return Array.from(matches)
|
||||
.map((match) => {
|
||||
const attr = match.groups.p.split(',')
|
||||
const attr = match.groups!.p!.split(',')
|
||||
if (attr.length >= 8) {
|
||||
const text = match.groups.text
|
||||
const text = match.groups!.text!
|
||||
.trim()
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', '\'')
|
||||
@@ -38,7 +40,7 @@ export function bilibiliDanmuParseFromXml(xmlString) {
|
||||
color: `#${Number(attr[3]).toString(16)}`,
|
||||
timestamp: Number(attr[4]),
|
||||
pool: Number(attr[5]),
|
||||
userID: attr[6],
|
||||
userID: attr[6]!,
|
||||
rowID: Number(attr[7]),
|
||||
}
|
||||
}
|
||||
@@ -46,11 +48,11 @@ export function bilibiliDanmuParseFromXml(xmlString) {
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.filter(Boolean) as ParsedDanmu[]
|
||||
}
|
||||
|
||||
export function onmessage({ data }) {
|
||||
export function onmessage({ data }: MessageEvent<ParserRequest>) {
|
||||
const { xml, id } = data
|
||||
const danmus = bilibiliDanmuParseFromXml(xml)
|
||||
globalThis.postMessage({ danmus, id })
|
||||
globalThis.postMessage({ danmus, id } satisfies ParserReply)
|
||||
}
|
||||
+15
-12
@@ -1,8 +1,9 @@
|
||||
import type { ParsedDanmu, ParserCancellation, ParserReply, ParserRequest } from './parser-types'
|
||||
import { bilibiliDanmuParseFromXml, getMode, onmessage } from './bilibili-parser'
|
||||
|
||||
let nextRequest = 0
|
||||
|
||||
function abortError(signal) {
|
||||
function abortError(signal?: AbortSignal): unknown {
|
||||
if (signal?.reason !== undefined)
|
||||
return signal.reason
|
||||
const error = new Error('Bilibili Danmu loading cancelled')
|
||||
@@ -10,17 +11,17 @@ function abortError(signal) {
|
||||
return error
|
||||
}
|
||||
|
||||
export function bilibiliDanmuParseFromUrl(url, { signal, onCancel } = {}) {
|
||||
export function bilibiliDanmuParseFromUrl(url: string, { signal, onCancel }: ParserCancellation = {}): Promise<ParsedDanmu[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false
|
||||
let worker
|
||||
let workerUrl
|
||||
let releaseCancel
|
||||
let worker: Worker | undefined
|
||||
let workerUrl: string | undefined
|
||||
let releaseCancel: (() => void) | undefined
|
||||
const id = ++nextRequest
|
||||
const cancel = () => finish(undefined, abortError(signal))
|
||||
|
||||
function releaseWorker() {
|
||||
let error
|
||||
let error: unknown
|
||||
let failed = false
|
||||
try {
|
||||
if (worker) {
|
||||
@@ -52,7 +53,9 @@ export function bilibiliDanmuParseFromUrl(url, { signal, onCancel } = {}) {
|
||||
throw error
|
||||
}
|
||||
|
||||
function finish(value, error) {
|
||||
function finish(value: ParsedDanmu[]): void
|
||||
function finish(value: undefined, error: unknown): void
|
||||
function finish(value: ParsedDanmu[] | undefined, error?: unknown) {
|
||||
if (settled)
|
||||
return
|
||||
settled = true
|
||||
@@ -69,10 +72,10 @@ export function bilibiliDanmuParseFromUrl(url, { signal, onCancel } = {}) {
|
||||
}
|
||||
if (failed)
|
||||
reject(error)
|
||||
else resolve(value)
|
||||
else resolve(value!)
|
||||
}
|
||||
|
||||
function fallback(xml, error) {
|
||||
function fallback(xml: string, error: unknown) {
|
||||
if (settled)
|
||||
return
|
||||
console.error('Error parsing Bilibili Danmu:', error)
|
||||
@@ -107,7 +110,7 @@ export function bilibiliDanmuParseFromUrl(url, { signal, onCancel } = {}) {
|
||||
releaseWorker()
|
||||
return
|
||||
}
|
||||
worker.onmessage = (event) => {
|
||||
worker.onmessage = (event: MessageEvent<ParserReply>) => {
|
||||
if (settled || event.data?.id !== id)
|
||||
return
|
||||
const { danmus } = event.data
|
||||
@@ -121,8 +124,8 @@ export function bilibiliDanmuParseFromUrl(url, { signal, onCancel } = {}) {
|
||||
event.preventDefault?.()
|
||||
fallback(xml, event.error || event)
|
||||
}
|
||||
worker.onmessageerror = event => fallback(xml, event.error || event)
|
||||
worker.postMessage({ xml, id })
|
||||
worker.onmessageerror = event => fallback(xml, (event as MessageEvent & { error?: unknown }).error || event)
|
||||
worker.postMessage({ xml, id } satisfies ParserRequest)
|
||||
}
|
||||
catch (error) {
|
||||
fallback(xml, error)
|
||||
+8
-6
@@ -1,4 +1,6 @@
|
||||
export function defaultOption() {
|
||||
import type { DanmukuOption, NormalizedOption } from './types'
|
||||
|
||||
export function defaultOption(): NormalizedOption & { fontSize: number, margin: [number, string] } {
|
||||
return {
|
||||
danmuku: [],
|
||||
speed: 5,
|
||||
@@ -62,21 +64,21 @@ export function optionScheme() {
|
||||
}
|
||||
}
|
||||
|
||||
export function isPromiseInput(value) {
|
||||
export function isPromiseInput(value: unknown): value is Promise<unknown> {
|
||||
return value instanceof Promise || Object.prototype.toString.call(value) === '[object Promise]'
|
||||
}
|
||||
|
||||
export function optionChanged(previous, update) {
|
||||
export function optionChanged(previous: NormalizedOption, update: DanmukuOption) {
|
||||
return Object.keys(update).some((key) => {
|
||||
const before = previous[key]
|
||||
const after = update[key]
|
||||
const before = previous[key as keyof NormalizedOption]
|
||||
const after = update[key as keyof NormalizedOption]
|
||||
if (typeof before === 'function' || typeof after === 'function' || isPromiseInput(before) || isPromiseInput(after))
|
||||
return before !== after
|
||||
return JSON.stringify(before) !== JSON.stringify(after)
|
||||
})
|
||||
}
|
||||
|
||||
export function normalizeOption(previous, update, { defaults, validate, clamp, mount }) {
|
||||
export function normalizeOption(previous: NormalizedOption, update: DanmukuOption, { defaults, validate, clamp, mount }: { defaults: NormalizedOption, validate: (value: NormalizedOption) => unknown, clamp: (value: number, min: number, max: number) => number, mount: HTMLDivElement }): NormalizedOption {
|
||||
const next = Object.assign({}, defaults, previous, update)
|
||||
validate(next)
|
||||
next.mode = clamp(next.mode, 0, 2)
|
||||
+46
-21
@@ -1,13 +1,34 @@
|
||||
import type { DanmuInput, DanmuItem, DanmukuArt, DanmukuInput, DanmukuOption, DanmuState, NormalizedDanmu, NormalizedOption } from './types'
|
||||
|
||||
import { defaultOption, normalizeOption, optionChanged, optionScheme } from './config'
|
||||
import { beginInput, cancelInputs, inputActive, readInput } from './input'
|
||||
import { filterState, readyItems, setItemState } from './queue'
|
||||
import Renderer from './renderer'
|
||||
import Scheduler from './scheduler'
|
||||
import WorkerClient from './worker-client'
|
||||
import DanmuWorker from './worker.js?worker&inline'
|
||||
import DanmuWorker from './worker.ts?worker&inline'
|
||||
|
||||
export default class Danmuku {
|
||||
constructor(art, option) {
|
||||
declare ['constructor']: typeof Danmuku
|
||||
declare art: DanmukuArt
|
||||
declare utils: DanmukuArt['constructor']['utils']
|
||||
declare validator: DanmukuArt['constructor']['validator']
|
||||
declare $danmuku: HTMLDivElement
|
||||
declare $player: HTMLDivElement
|
||||
declare queue: DanmuItem[]
|
||||
declare $refs: HTMLDivElement[]
|
||||
declare isStop: boolean
|
||||
declare isHide: boolean
|
||||
declare timer: number | null
|
||||
declare index: number
|
||||
declare worker: Worker | null
|
||||
declare workerClient: WorkerClient | null
|
||||
declare renderer: Renderer
|
||||
declare scheduler: Scheduler
|
||||
declare option: NormalizedOption
|
||||
declare states: Record<DanmuState, DanmuItem[]>
|
||||
|
||||
constructor(art: DanmukuArt, option: DanmukuOption) {
|
||||
const { constructor, template } = art
|
||||
|
||||
this.utils = constructor.utils // 工具库
|
||||
@@ -99,8 +120,8 @@ export default class Danmuku {
|
||||
}
|
||||
|
||||
// 是否在移动端使用了自动旋屏,会影响弹幕的left和top值
|
||||
get isRotate() {
|
||||
return this.art.plugins?.autoOrientation?.state
|
||||
get isRotate(): boolean | undefined {
|
||||
return (this.art.plugins?.autoOrientation as { state?: boolean } | undefined)?.state
|
||||
}
|
||||
|
||||
// 计算上空白边距
|
||||
@@ -181,7 +202,7 @@ export default class Danmuku {
|
||||
}
|
||||
|
||||
// 加载弹幕
|
||||
async load(danmuku) {
|
||||
async load(danmuku?: DanmukuInput) {
|
||||
const { errorHandle } = this.utils
|
||||
const task = beginInput(this, danmuku === undefined)
|
||||
|
||||
@@ -196,6 +217,8 @@ export default class Danmuku {
|
||||
return this
|
||||
|
||||
errorHandle(Array.isArray(danmus), 'Danmuku need return an array as result')
|
||||
// Active input and the runtime validator exclude the cancellation sentinel.
|
||||
const rows = danmus as DanmuInput[]
|
||||
|
||||
// 假如没有传入弹幕参数,则清空弹幕,否则追加弹幕
|
||||
if (danmuku === undefined) {
|
||||
@@ -208,10 +231,10 @@ export default class Danmuku {
|
||||
}
|
||||
|
||||
// 逐个验证原始弹幕并转换到弹幕队列
|
||||
for (let index = 0; index < danmus.length; index++) {
|
||||
for (let index = 0; index < rows.length; index++) {
|
||||
if (!task.active())
|
||||
return this
|
||||
const danmu = danmus[index]
|
||||
const danmu = rows[index]!
|
||||
await task.emit(danmu)
|
||||
}
|
||||
|
||||
@@ -232,7 +255,7 @@ export default class Danmuku {
|
||||
}
|
||||
|
||||
// 把原始弹幕转换到弹幕队列
|
||||
async emit(danmu) {
|
||||
async emit(danmu: DanmuInput) {
|
||||
const { clamp } = this.utils
|
||||
if (!inputActive(this))
|
||||
return this
|
||||
@@ -279,7 +302,8 @@ export default class Danmuku {
|
||||
return this
|
||||
|
||||
// 自定义弹幕过滤函数
|
||||
if (!this.option.filter(danmu))
|
||||
// Validation/defaulting above fills these fields before the public callback.
|
||||
if (!this.option.filter(danmu as NormalizedDanmu))
|
||||
return this
|
||||
if (!inputActive(this))
|
||||
return this
|
||||
@@ -287,7 +311,7 @@ export default class Danmuku {
|
||||
// 添加自定义属性
|
||||
const item = {
|
||||
...danmu,
|
||||
$state: 'wait', // 弹幕初始状态
|
||||
$state: 'wait' as const, // 弹幕初始状态
|
||||
$index: this.index++, // 弹幕索引
|
||||
$ref: null, // 弹幕 DOM 节点
|
||||
$restTime: 0, // 弹幕剩余时间
|
||||
@@ -295,10 +319,10 @@ export default class Danmuku {
|
||||
}
|
||||
|
||||
// 转换为wait状态
|
||||
this.setState(item, 'wait')
|
||||
this.setState(item as DanmuItem, 'wait')
|
||||
|
||||
// 添加到实际弹幕队列
|
||||
this.queue.push(item)
|
||||
this.queue.push(item as DanmuItem)
|
||||
|
||||
// 弹幕有四个状态:
|
||||
// - wait: 弹幕还未开始显示,没有被添加到 DOM 中
|
||||
@@ -310,7 +334,7 @@ export default class Danmuku {
|
||||
}
|
||||
|
||||
// 动态配置
|
||||
config(option, isInit = false) {
|
||||
config(option: DanmukuOption, isInit = false) {
|
||||
const { clamp } = this.utils
|
||||
const { $controlsCenter } = this.art.template
|
||||
|
||||
@@ -359,13 +383,14 @@ export default class Danmuku {
|
||||
}
|
||||
|
||||
// 计算DOM的left值,受到旋屏影响
|
||||
getLeft($ref) {
|
||||
getLeft($ref: HTMLElement): number {
|
||||
return this.renderer.left($ref)
|
||||
}
|
||||
|
||||
// 复杂运算交给 Web Worker 处理
|
||||
postMessage(message = {}) {
|
||||
return this.workerClient.request(message)
|
||||
// Keep the historical empty default; unknown message types receive no reply.
|
||||
postMessage(message: Parameters<WorkerClient['request']>[0] = {} as Parameters<WorkerClient['request']>[0]) {
|
||||
return this.workerClient!.request(message)
|
||||
}
|
||||
|
||||
createWorker() {
|
||||
@@ -374,17 +399,17 @@ export default class Danmuku {
|
||||
}
|
||||
|
||||
// 根据状态获取弹幕
|
||||
filter(state, callback) {
|
||||
filter(state: DanmuState, callback: (danmu: DanmuItem) => void) {
|
||||
return filterState(this, state, callback)
|
||||
}
|
||||
|
||||
// 设置弹幕状态
|
||||
setState(danmu, state) {
|
||||
setState(danmu: DanmuItem, state: DanmuState) {
|
||||
setItemState(this, danmu, state)
|
||||
}
|
||||
|
||||
// 重置弹幕到wait状态,回收弹幕DOM节点
|
||||
makeWait(danmu) {
|
||||
makeWait(danmu: DanmuItem) {
|
||||
this.renderer.makeWait(danmu)
|
||||
}
|
||||
|
||||
@@ -454,7 +479,7 @@ export default class Danmuku {
|
||||
|
||||
show() {
|
||||
this.isHide = false
|
||||
this.$danmuku.style.opacity = 1
|
||||
this.$danmuku.style.opacity = 1 as unknown as string
|
||||
this.option.visible = true
|
||||
this.art.emit('artplayerPluginDanmuku:show')
|
||||
return this
|
||||
@@ -463,7 +488,7 @@ export default class Danmuku {
|
||||
hide() {
|
||||
this.isHide = true
|
||||
this.scheduler.invalidate()
|
||||
this.$danmuku.style.opacity = 0
|
||||
this.$danmuku.style.opacity = 0 as unknown as string
|
||||
this.option.visible = false
|
||||
this.art.emit('artplayerPluginDanmuku:hide')
|
||||
this.scheduler.schedule()
|
||||
+31
-12
@@ -1,19 +1,36 @@
|
||||
import type { DanmuItem, HeatmapOptions, HeatmapPoint } from './types'
|
||||
import { MAX_HEATMAP_POINTS, sampleHeatmap } from './heatmap-sampling'
|
||||
|
||||
function map(value, inMin, inMax, outMin, outMax) {
|
||||
function map(value: number, inMin: number, inMax: number, outMin: number, outMax: number): number {
|
||||
return ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin
|
||||
}
|
||||
|
||||
function line(pointA, pointB) {
|
||||
function line(pointA: HeatmapPoint, pointB: HeatmapPoint) {
|
||||
const lengthX = pointB[0] - pointA[0]
|
||||
const lengthY = pointB[1] - pointA[1]
|
||||
return { length: Math.sqrt(lengthX ** 2 + lengthY ** 2), angle: Math.atan2(lengthY, lengthX) }
|
||||
}
|
||||
|
||||
export function heatmapGeometry({ width, height, duration, queue, option, points: input = [] }) {
|
||||
interface GeometryInput {
|
||||
width: number
|
||||
height: number
|
||||
duration: number
|
||||
queue: readonly Pick<DanmuItem, 'time'>[]
|
||||
option: boolean | HeatmapOptions
|
||||
points?: HeatmapPoint[]
|
||||
}
|
||||
|
||||
export interface HeatmapGeometry {
|
||||
path: string
|
||||
opacity: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export function heatmapGeometry({ width, height, duration, queue, option, points: input = [] }: GeometryInput): HeatmapGeometry | null {
|
||||
if (![width, height, duration].every(value => Number.isFinite(value) && value > 0))
|
||||
return null
|
||||
const defaults = {
|
||||
const defaults: Required<HeatmapOptions> = {
|
||||
xMin: 0,
|
||||
xMax: width,
|
||||
yMin: 0,
|
||||
@@ -25,9 +42,9 @@ export function heatmapGeometry({ width, height, duration, queue, option, points
|
||||
smoothing: 0.2,
|
||||
flattening: 0.2,
|
||||
}
|
||||
const configured = Object.assign({}, typeof option === 'object' ? option : {})
|
||||
const configured: HeatmapOptions = Object.assign({}, typeof option === 'object' ? option : {})
|
||||
const options = Object.assign({}, defaults, configured)
|
||||
for (const key of Object.keys(defaults)) {
|
||||
for (const key of Object.keys(defaults) as (keyof HeatmapOptions)[]) {
|
||||
if (!Number.isFinite(options[key]))
|
||||
options[key] = defaults[key]
|
||||
}
|
||||
@@ -39,7 +56,8 @@ export function heatmapGeometry({ width, height, duration, queue, option, points
|
||||
const points = hasCustomPoints ? [...input] : sampleHeatmap(queue, width, duration, options.sampling)
|
||||
if (!points.length
|
||||
|| !points.every(point => Array.isArray(point) && Number.isFinite(point[0]) && Number.isFinite(point[1]))) { return null }
|
||||
const lastPoint = points[points.length - 1]
|
||||
// The nonempty check above guarantees both endpoints used below.
|
||||
const lastPoint = points[points.length - 1]!
|
||||
if (lastPoint[0] !== width) {
|
||||
if (points.length === MAX_HEATMAP_POINTS)
|
||||
return null
|
||||
@@ -65,8 +83,8 @@ export function heatmapGeometry({ width, height, duration, queue, option, points
|
||||
const fitted = automatic && peak > defaults.yMax / 4
|
||||
if (fitted)
|
||||
options.yMax = peak * 4
|
||||
const boundY = value => fitted ? Math.min(height, Math.max(height * 0.75, value)) : value
|
||||
const controlPoint = (current, previous, next, reverse) => {
|
||||
const boundY = (value: number) => fitted ? Math.min(height, Math.max(height * 0.75, value)) : value
|
||||
const controlPoint = (current: HeatmapPoint, previous: HeatmapPoint | undefined, next: HeatmapPoint | undefined, reverse?: boolean): HeatmapPoint => {
|
||||
const geometry = line(previous || current, next || current)
|
||||
const flat = map(Math.cos(geometry.angle) * options.flattening, 0, 1, 1, 0)
|
||||
const angle = geometry.angle * flat + (reverse ? Math.PI : 0)
|
||||
@@ -74,13 +92,14 @@ export function heatmapGeometry({ width, height, duration, queue, option, points
|
||||
// A cubic stays inside the convex hull of its endpoints and controls.
|
||||
return [current[0] + Math.cos(angle) * length, boundY(current[1] + Math.sin(angle) * length)]
|
||||
}
|
||||
const positions = points.map(point => [map(point[0], options.xMin, options.xMax, 0, width), boundY(map(point[1], options.yMin, options.yMax, height, 0))])
|
||||
const positions = points.map<HeatmapPoint>(point => [map(point[0], options.xMin, options.xMax, 0, width), boundY(map(point[1], options.yMin, options.yMax, height, 0))])
|
||||
if (!positions.every(point => point.every(Number.isFinite)))
|
||||
return null
|
||||
const path = positions.map((point, index, all) => {
|
||||
if (index === 0)
|
||||
return `M ${all[all.length - 1][0]},${height} L ${point[0]},${height} L ${point[0]},${point[1]}`
|
||||
const start = controlPoint(all[index - 1], all[index - 2], point)
|
||||
return `M ${all[all.length - 1]![0]},${height} L ${point[0]},${height} L ${point[0]},${point[1]}`
|
||||
// Every noninitial point has its immediately preceding control point.
|
||||
const start = controlPoint(all[index - 1]!, all[index - 2], point)
|
||||
const end = controlPoint(point, all[index - 1], all[index + 1], true)
|
||||
return `C ${start[0]},${start[1]} ${end[0]},${end[1]} ${point[0]},${point[1]}${index === all.length - 1 ? ' z' : ''}`
|
||||
}).join(' ')
|
||||
+7
-4
@@ -1,22 +1,25 @@
|
||||
import type { DanmuItem, HeatmapPoint } from './types'
|
||||
|
||||
export const MAX_HEATMAP_POINTS = 0xFFFFFFFF
|
||||
|
||||
function upperBound(values, value) {
|
||||
function upperBound(values: readonly number[], value: number): number {
|
||||
let start = 0
|
||||
let end = values.length
|
||||
while (start < end) {
|
||||
const middle = Math.floor((start + end) / 2)
|
||||
if (values[middle] <= value)
|
||||
// The binary-search interval guarantees middle is an existing index.
|
||||
if (values[middle]! <= value)
|
||||
start = middle + 1
|
||||
else end = middle
|
||||
}
|
||||
return start
|
||||
}
|
||||
|
||||
export function sampleHeatmap(queue, width, duration, sampling) {
|
||||
export function sampleHeatmap(queue: readonly Pick<DanmuItem, 'time'>[], width: number, duration: number, sampling: number): HeatmapPoint[] {
|
||||
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(duration) || duration <= 0
|
||||
|| !Number.isFinite(sampling) || sampling <= 0 || Math.floor(width / sampling) + 1 > MAX_HEATMAP_POINTS) { return [] }
|
||||
const times = queue.map(item => item.time).filter(time => typeof time === 'number' && !Number.isNaN(time)).sort((a, b) => a - b)
|
||||
const points = []
|
||||
const points: HeatmapPoint[] = []
|
||||
const gap = duration / width
|
||||
if (!Number.isFinite(gap))
|
||||
return []
|
||||
+38
-21
@@ -1,32 +1,47 @@
|
||||
import type { DanmuItem, DanmukuArt, HeatmapOptions, HeatmapPoint } from './types'
|
||||
import { heatmapGeometry } from './heatmap-geometry'
|
||||
|
||||
let nextGradient = 0
|
||||
|
||||
export default function heatmap(art, danmuku, option) {
|
||||
type HeatmapArt = Pick<DanmukuArt, 'isDestroy' | 'constructor' | 'on' | 'off' | 'controls' | 'option' | 'duration' | 'played'>
|
||||
interface HeatmapEvents {
|
||||
'destroy': []
|
||||
'video:timeupdate': []
|
||||
'setBar': [type: string, percentage: number]
|
||||
'ready': []
|
||||
'resize': []
|
||||
'artplayerPluginDanmuku:loaded': []
|
||||
'artplayerPluginDanmuku:points': [points: HeatmapPoint[]]
|
||||
}
|
||||
type HeatmapCallback = (...args: unknown[]) => unknown
|
||||
|
||||
export default function heatmap(art: HeatmapArt, danmuku: { queue: readonly Pick<DanmuItem, 'time'>[] }, option: boolean | HeatmapOptions): (() => void) | undefined {
|
||||
if (art.isDestroy)
|
||||
return
|
||||
const { query } = art.constructor.utils
|
||||
let gradient
|
||||
const subscriptions = []
|
||||
let element
|
||||
let start
|
||||
let stop
|
||||
let gradient: string | undefined
|
||||
const subscriptions: [keyof HeatmapEvents, HeatmapCallback][] = []
|
||||
let element: HTMLElement | undefined
|
||||
let start: Element | null | undefined
|
||||
let stop: Element | null | undefined
|
||||
let closed = false
|
||||
|
||||
const active = () => !closed && !art.isDestroy
|
||||
function listen(name, callback) {
|
||||
subscriptions.push([name, callback])
|
||||
art.on(name, callback)
|
||||
function listen<Name extends keyof HeatmapEvents>(name: Name, callback: (...args: HeatmapEvents[Name]) => unknown) {
|
||||
// Core's string-event overload erases payloads; this local map checks each subscription.
|
||||
const listener = callback as HeatmapCallback
|
||||
subscriptions.push([name, listener])
|
||||
art.on(name, listener)
|
||||
}
|
||||
function dispose(removeControl) {
|
||||
function dispose(removeControl: boolean) {
|
||||
if (closed)
|
||||
return
|
||||
closed = true
|
||||
start = null
|
||||
stop = null
|
||||
let failure
|
||||
let failure: unknown
|
||||
let failed = false
|
||||
const attempt = (callback) => {
|
||||
const attempt = (callback: () => unknown) => {
|
||||
try {
|
||||
callback()
|
||||
}
|
||||
@@ -44,29 +59,31 @@ export default function heatmap(art, danmuku, option) {
|
||||
if (failed)
|
||||
throw failure
|
||||
}
|
||||
function progress(value) {
|
||||
function progress(value: number) {
|
||||
if (active() && start && stop) {
|
||||
start.setAttribute('offset', `${value * 100}%`)
|
||||
stop.setAttribute('offset', `${value * 100}%`)
|
||||
}
|
||||
}
|
||||
function draw(points = []) {
|
||||
function draw(points: HeatmapPoint[] = []) {
|
||||
if (!active())
|
||||
return
|
||||
start = null
|
||||
stop = null
|
||||
element.innerHTML = ''
|
||||
// Drawing listeners are registered only after mounted assigns element; dispose closes first.
|
||||
const target = element!
|
||||
target.innerHTML = ''
|
||||
if (art.option.isLive)
|
||||
return
|
||||
const shape = heatmapGeometry({ width: element.offsetWidth, height: element.offsetHeight, duration: art.duration, queue: danmuku.queue, option, points })
|
||||
const shape = heatmapGeometry({ width: target.offsetWidth, height: target.offsetHeight, duration: art.duration, queue: danmuku.queue, option, points })
|
||||
if (!shape || !active())
|
||||
return
|
||||
const document = element.ownerDocument
|
||||
const document = target.ownerDocument
|
||||
if (!gradient)
|
||||
gradient = document ? 'heatmap-solids' : `heatmap-solids-${++nextGradient}`
|
||||
while (document?.getElementById(gradient))
|
||||
gradient = `heatmap-solids-${++nextGradient}`
|
||||
element.innerHTML = `
|
||||
target.innerHTML = `
|
||||
<svg viewBox="0 0 ${shape.width} ${shape.height}">
|
||||
<defs>
|
||||
<linearGradient id="${gradient}" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
@@ -79,11 +96,11 @@ export default function heatmap(art, danmuku, option) {
|
||||
<path fill="url(#${gradient})" d="${shape.path}"></path>
|
||||
</svg>
|
||||
`
|
||||
start = query('#heatmap-start', element)
|
||||
stop = query('#heatmap-stop', element)
|
||||
start = query('#heatmap-start', target)
|
||||
stop = query('#heatmap-stop', target)
|
||||
progress(art.played)
|
||||
}
|
||||
function update(points) {
|
||||
function update(points?: HeatmapPoint[]) {
|
||||
try {
|
||||
draw(points)
|
||||
}
|
||||
+11
-7
@@ -1,15 +1,19 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { DanmukuArt, DanmukuOption } from './types'
|
||||
|
||||
import Danmuku from './danmuku'
|
||||
import heatmap from './heatmap'
|
||||
import Setting from './setting'
|
||||
|
||||
export default function artplayerPluginDanmuku(option) {
|
||||
return (art) => {
|
||||
const danmuku = new Danmuku(art, option)
|
||||
let setting
|
||||
export default function artplayerPluginDanmuku(option: DanmukuOption) {
|
||||
return (art: Artplayer) => {
|
||||
// Old declarations type instance.constructor as Function; both supported cores expose these statics.
|
||||
const danmuku = new Danmuku(art as DanmukuArt, option)
|
||||
let setting: Setting | undefined
|
||||
try {
|
||||
setting = art.isDestroy ? undefined : new Setting(art, danmuku)
|
||||
setting = art.isDestroy ? undefined : new Setting(art as DanmukuArt, danmuku)
|
||||
if (!art.isDestroy && danmuku.option.heatmap) {
|
||||
heatmap(art, danmuku, danmuku.option.heatmap)
|
||||
heatmap(art as DanmukuArt, danmuku, danmuku.option.heatmap)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
@@ -25,7 +29,7 @@ export default function artplayerPluginDanmuku(option) {
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'artplayerPluginDanmuku',
|
||||
name: 'artplayerPluginDanmuku' as const,
|
||||
emit: danmuku.emit.bind(danmuku),
|
||||
load: danmuku.load.bind(danmuku),
|
||||
config: danmuku.config.bind(danmuku),
|
||||
+40
-15
@@ -1,22 +1,45 @@
|
||||
import type { DanmuInput, DanmukuInput } from './types'
|
||||
import { bilibiliDanmuParseFromUrl } from './bilibili'
|
||||
import { isPromiseInput } from './config'
|
||||
|
||||
const inputs = new WeakMap()
|
||||
const cancelled = Symbol('cancelled danmuku input')
|
||||
|
||||
function stateFor(owner) {
|
||||
if (!inputs.has(owner))
|
||||
inputs.set(owner, { closed: false, tasks: new Set(), emitting: [] })
|
||||
return inputs.get(owner)
|
||||
interface InputOwner {
|
||||
art: { isDestroy: boolean }
|
||||
emit: (danmu: DanmuInput) => unknown
|
||||
}
|
||||
|
||||
export function inputActive(owner) {
|
||||
export interface InputTask {
|
||||
replace: boolean
|
||||
signal: AbortSignal | undefined
|
||||
active: () => boolean
|
||||
onCancel: (callback: () => void) => () => void
|
||||
cancel: () => void
|
||||
wait: <T>(value: T) => Promise<Awaited<T> | typeof cancelled>
|
||||
emit: (danmu: DanmuInput) => unknown
|
||||
finish: () => void
|
||||
}
|
||||
|
||||
interface InputState {
|
||||
closed: boolean
|
||||
tasks: Set<InputTask>
|
||||
emitting: InputTask[]
|
||||
}
|
||||
|
||||
const inputs = new WeakMap<InputOwner, InputState>()
|
||||
export const cancelled = Symbol('cancelled danmuku input')
|
||||
|
||||
function stateFor(owner: InputOwner): InputState {
|
||||
if (!inputs.has(owner))
|
||||
inputs.set(owner, { closed: false, tasks: new Set(), emitting: [] })
|
||||
return inputs.get(owner)!
|
||||
}
|
||||
|
||||
export function inputActive(owner: InputOwner) {
|
||||
const state = stateFor(owner)
|
||||
const task = state.emitting[state.emitting.length - 1]
|
||||
return !state.closed && !owner.art.isDestroy && (!task || task.active())
|
||||
}
|
||||
|
||||
export function beginInput(owner, replace) {
|
||||
export function beginInput(owner: InputOwner, replace: boolean): InputTask {
|
||||
const state = stateFor(owner)
|
||||
if (replace) {
|
||||
for (const task of [...state.tasks]) {
|
||||
@@ -25,11 +48,11 @@ export function beginInput(owner, replace) {
|
||||
}
|
||||
}
|
||||
let stopped = state.closed || owner.art.isDestroy
|
||||
let resolveCancel
|
||||
const cancellation = new Promise(resolve => resolveCancel = resolve)
|
||||
const handlers = new Set()
|
||||
let resolveCancel!: (value: typeof cancelled) => void
|
||||
const cancellation = new Promise<typeof cancelled>(resolve => resolveCancel = resolve)
|
||||
const handlers = new Set<() => void>()
|
||||
const controller = typeof AbortController === 'function' ? new AbortController() : undefined
|
||||
const task = {
|
||||
const task: InputTask = {
|
||||
replace,
|
||||
signal: controller?.signal,
|
||||
active: () => !stopped && !state.closed && !owner.art.isDestroy,
|
||||
@@ -71,13 +94,15 @@ export function beginInput(owner, replace) {
|
||||
return task
|
||||
}
|
||||
|
||||
export function cancelInputs(owner) {
|
||||
export function cancelInputs(owner: InputOwner) {
|
||||
const state = stateFor(owner)
|
||||
state.closed = true
|
||||
for (const task of [...state.tasks]) task.cancel()
|
||||
}
|
||||
|
||||
export function readInput(target, task) {
|
||||
export function readInput(target: DanmukuInput, task: InputTask):
|
||||
| { asynchronous: true, value: DanmuInput[] | PromiseLike<DanmuInput[]> }
|
||||
| { asynchronous: false, value: DanmuInput[] } {
|
||||
if (typeof target === 'function')
|
||||
return { asynchronous: true, value: target() }
|
||||
if (isPromiseInput(target))
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { DanmuInput } from './types'
|
||||
|
||||
export interface ParsedDanmu extends DanmuInput {
|
||||
time: number
|
||||
mode: number
|
||||
fontSize: number
|
||||
color: string
|
||||
timestamp: number
|
||||
pool: number
|
||||
userID: string
|
||||
rowID: number
|
||||
}
|
||||
|
||||
export interface ParserRequest { xml: unknown, id: number }
|
||||
export interface ParserReply { danmus: ParsedDanmu[], id: number }
|
||||
|
||||
export interface ParserCancellation {
|
||||
signal?: AbortSignal
|
||||
onCancel?: (callback: () => void) => () => void
|
||||
}
|
||||
+9
-5
@@ -1,12 +1,16 @@
|
||||
export function filterState(owner, state, callback) {
|
||||
import type { DanmuItem, DanmuState } from './types'
|
||||
|
||||
interface QueueOwner { states: Record<DanmuState, DanmuItem[]>, art: { currentTime: number } }
|
||||
|
||||
export function filterState(owner: QueueOwner, state: DanmuState, callback: (danmu: DanmuItem) => void) {
|
||||
const danmus = owner.states[state] || []
|
||||
for (let index = 0; index < danmus.length; index++) callback(danmus[index])
|
||||
for (let index = 0; index < danmus.length; index++) callback(danmus[index]!)
|
||||
return danmus
|
||||
}
|
||||
|
||||
export function readyItems(owner) {
|
||||
export function readyItems(owner: QueueOwner) {
|
||||
const { currentTime } = owner.art
|
||||
const result = []
|
||||
const result: DanmuItem[] = []
|
||||
filterState(owner, 'ready', danmu => result.push(danmu))
|
||||
filterState(owner, 'wait', (danmu) => {
|
||||
if (currentTime + 0.1 >= danmu.time && danmu.time >= currentTime - 0.1)
|
||||
@@ -15,7 +19,7 @@ export function readyItems(owner) {
|
||||
return result
|
||||
}
|
||||
|
||||
export function setItemState(owner, danmu, state) {
|
||||
export function setItemState(owner: QueueOwner, danmu: DanmuItem, state: DanmuState) {
|
||||
owner.states[danmu.$state] = owner.states[danmu.$state].filter(item => item !== danmu)
|
||||
danmu.$state = state
|
||||
if (danmu.$ref)
|
||||
+43
-36
@@ -1,5 +1,12 @@
|
||||
import type Danmuku from './danmuku'
|
||||
import type { FrameOperation } from './scheduler'
|
||||
import type { DanmuItem } from './types'
|
||||
|
||||
export default class Renderer {
|
||||
constructor(owner) {
|
||||
declare owner: Danmuku
|
||||
declare nodes: Set<HTMLDivElement>
|
||||
|
||||
constructor(owner: Danmuku) {
|
||||
this.owner = owner
|
||||
this.nodes = new Set()
|
||||
}
|
||||
@@ -14,21 +21,21 @@ export default class Renderer {
|
||||
return $ref
|
||||
}
|
||||
|
||||
get visibles() {
|
||||
const result = []
|
||||
get visibles(): { top: number, left: number, height: number, width: number, right: number, speed: number, distance: number, time: number, mode: number }[] {
|
||||
const result: Renderer['visibles'] = []
|
||||
const { clientWidth } = this.owner.$player
|
||||
const clientLeft = this.owner.getLeft(this.owner.$player)
|
||||
|
||||
this.owner.filter('emit', (danmu) => {
|
||||
const top = danmu.$ref.offsetTop
|
||||
const left = this.owner.getLeft(danmu.$ref) - clientLeft
|
||||
const height = danmu.$ref.clientHeight
|
||||
const width = danmu.$ref.clientWidth
|
||||
const top = danmu.$ref!.offsetTop
|
||||
const left = this.owner.getLeft(danmu.$ref!) - clientLeft
|
||||
const height = danmu.$ref!.clientHeight
|
||||
const width = danmu.$ref!.clientWidth
|
||||
const distance = left + width
|
||||
const right = clientWidth - distance
|
||||
const speed = distance / danmu.$restTime
|
||||
|
||||
const emit = {}
|
||||
const emit = {} as Renderer['visibles'][number]
|
||||
emit.top = top
|
||||
emit.left = left
|
||||
emit.height = height
|
||||
@@ -45,21 +52,21 @@ export default class Renderer {
|
||||
return result
|
||||
}
|
||||
|
||||
left($ref) {
|
||||
left($ref: HTMLElement): number {
|
||||
const rect = $ref.getBoundingClientRect()
|
||||
return this.owner.isRotate ? rect.top : rect.left
|
||||
}
|
||||
|
||||
makeWait(danmu) {
|
||||
makeWait(danmu: DanmuItem) {
|
||||
this.owner.setState(danmu, 'wait')
|
||||
if (danmu.$ref) {
|
||||
danmu.$ref.style.cssText = this.owner.constructor.cssText
|
||||
danmu.$ref.style.visibility = 'hidden'
|
||||
danmu.$ref.style.marginLeft = '0px'
|
||||
danmu.$ref.style.transform = 'translateX(0px)'
|
||||
danmu.$ref.style.transition = 'transform 0s linear 0s'
|
||||
danmu.$ref!.style.cssText = this.owner.constructor.cssText
|
||||
danmu.$ref!.style.visibility = 'hidden'
|
||||
danmu.$ref!.style.marginLeft = '0px'
|
||||
danmu.$ref!.style.transform = 'translateX(0px)'
|
||||
danmu.$ref!.style.transition = 'transform 0s linear 0s'
|
||||
this.owner.$refs.push(danmu.$ref)
|
||||
danmu.$ref = null
|
||||
danmu!.$ref = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +77,7 @@ export default class Renderer {
|
||||
switch (danmu.mode) {
|
||||
// 滚动的弹幕
|
||||
case 0:
|
||||
danmu.$ref.style.left = `${clientWidth}px`
|
||||
danmu.$ref!.style.left = `${clientWidth}px`
|
||||
break
|
||||
default:
|
||||
break
|
||||
@@ -82,10 +89,10 @@ export default class Renderer {
|
||||
switch (danmu.mode) {
|
||||
// 滚动的弹幕
|
||||
case 0: {
|
||||
const distance = clientWidth + danmu.$ref.clientWidth
|
||||
danmu.$ref.style.left = `${clientWidth}px`
|
||||
danmu.$ref.style.transform = `translateX(${-distance}px)`
|
||||
danmu.$ref.style.transition = `transform ${danmu.$restTime}s linear 0s`
|
||||
const distance = clientWidth + danmu.$ref!.clientWidth
|
||||
danmu.$ref!.style.left = `${clientWidth}px`
|
||||
danmu.$ref!.style.transform = `translateX(${-distance}px)`
|
||||
danmu.$ref!.style.transition = `transform ${danmu.$restTime}s linear 0s`
|
||||
break
|
||||
}
|
||||
default:
|
||||
@@ -102,9 +109,9 @@ export default class Renderer {
|
||||
switch (danmu.mode) {
|
||||
// 继续滚动的弹幕
|
||||
case 0: {
|
||||
const distance = clientWidth + danmu.$ref.clientWidth
|
||||
danmu.$ref.style.transform = `translateX(${-distance}px)`
|
||||
danmu.$ref.style.transition = `transform ${danmu.$restTime}s linear 0s`
|
||||
const distance = clientWidth + danmu.$ref!.clientWidth
|
||||
danmu.$ref!.style.transform = `translateX(${-distance}px)`
|
||||
danmu.$ref!.style.transition = `transform ${danmu.$restTime}s linear 0s`
|
||||
break
|
||||
}
|
||||
default:
|
||||
@@ -122,9 +129,9 @@ export default class Renderer {
|
||||
switch (danmu.mode) {
|
||||
// 停止滚动的弹幕
|
||||
case 0: {
|
||||
const translateX = clientWidth - (this.owner.getLeft(danmu.$ref) - this.owner.getLeft(this.owner.$player))
|
||||
danmu.$ref.style.transform = `translateX(${-translateX}px)`
|
||||
danmu.$ref.style.transition = 'transform 0s linear 0s'
|
||||
const translateX = clientWidth - (this.owner.getLeft(danmu.$ref!) - this.owner.getLeft(this.owner.$player))
|
||||
danmu.$ref!.style.transform = `translateX(${-translateX}px)`
|
||||
danmu.$ref!.style.transition = 'transform 0s linear 0s'
|
||||
break
|
||||
}
|
||||
default:
|
||||
@@ -135,7 +142,7 @@ export default class Renderer {
|
||||
return this
|
||||
}
|
||||
|
||||
prepare(danmu, operation) {
|
||||
prepare(danmu: DanmuItem, operation: FrameOperation) {
|
||||
const owner = this.owner
|
||||
const ref = owner.$ref
|
||||
operation.danmu = danmu
|
||||
@@ -143,19 +150,19 @@ export default class Renderer {
|
||||
danmu.$ref = ref
|
||||
ref.textContent = danmu.text
|
||||
owner.$danmuku.appendChild(ref)
|
||||
ref.style.opacity = owner.option.opacity
|
||||
ref.style.opacity = owner.option.opacity as unknown as string
|
||||
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
|
||||
ref.style.border = danmu.border ? `1px solid ${danmu.color}` : null as unknown as string
|
||||
ref.style.backgroundColor = danmu.border ? 'rgb(0 0 0 / 50%)' : null as unknown as string
|
||||
owner.utils.setStyles(ref, danmu.style)
|
||||
return ref
|
||||
}
|
||||
|
||||
place(danmu, ref, top, distance, clientWidth) {
|
||||
place(danmu: DanmuItem, ref: HTMLDivElement, top: number, distance: number, clientWidth: number) {
|
||||
ref.style.top = `${top}px`
|
||||
ref.style.visibility = 'visible'
|
||||
ref.dataset.mode = danmu.mode
|
||||
ref.dataset.mode = danmu.mode as unknown as string
|
||||
ref.dataset.id = danmu.id || ''
|
||||
switch (danmu.mode) {
|
||||
case 0:
|
||||
@@ -174,13 +181,13 @@ export default class Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
release(operation) {
|
||||
release(operation: FrameOperation) {
|
||||
const { danmu, ref } = operation
|
||||
if (ref && danmu.$ref === ref) {
|
||||
if (ref && danmu!.$ref === ref) {
|
||||
ref.style.cssText = this.owner.constructor.cssText
|
||||
ref.style.visibility = 'hidden'
|
||||
this.owner.$refs.push(ref)
|
||||
danmu.$ref = null
|
||||
danmu!.$ref = null
|
||||
}
|
||||
operation.ref = null
|
||||
}
|
||||
+34
-11
@@ -1,7 +1,28 @@
|
||||
import type Danmuku from './danmuku'
|
||||
import type { DanmuItem } from './types'
|
||||
|
||||
const cancelled = Symbol('cancelled danmuku frame')
|
||||
|
||||
export interface FrameOperation {
|
||||
generation: number
|
||||
cancel: () => void
|
||||
wait: <T>(value: T | PromiseLike<T>) => Promise<T | typeof cancelled>
|
||||
danmu?: DanmuItem
|
||||
ref: HTMLDivElement | null
|
||||
}
|
||||
|
||||
export default class Scheduler {
|
||||
constructor(owner) {
|
||||
declare owner: Danmuku
|
||||
declare generation: number
|
||||
declare starts: number
|
||||
declare frame: number | null
|
||||
declare operation: FrameOperation | null
|
||||
declare closed: boolean
|
||||
declare running: boolean
|
||||
declare fault: boolean
|
||||
declare failedItems: Set<DanmuItem>
|
||||
|
||||
constructor(owner: Danmuku) {
|
||||
this.owner = owner
|
||||
this.generation = 0
|
||||
this.starts = 0
|
||||
@@ -13,13 +34,13 @@ export default class Scheduler {
|
||||
this.failedItems = new Set()
|
||||
}
|
||||
|
||||
active(operation) {
|
||||
active(operation: FrameOperation) {
|
||||
const owner = this.owner
|
||||
return !this.closed && !this.fault && !owner.art.isDestroy && !owner.isStop
|
||||
&& !owner.isHide && operation.generation === this.generation
|
||||
}
|
||||
|
||||
release(operation) {
|
||||
release(operation: FrameOperation) {
|
||||
this.owner.renderer.release(operation)
|
||||
}
|
||||
|
||||
@@ -39,7 +60,7 @@ export default class Scheduler {
|
||||
this.failedItems.clear()
|
||||
}
|
||||
|
||||
report(error) {
|
||||
report(error: unknown) {
|
||||
try {
|
||||
this.owner.art.emit('artplayerPluginDanmuku:error', error)
|
||||
}
|
||||
@@ -48,7 +69,7 @@ export default class Scheduler {
|
||||
}
|
||||
}
|
||||
|
||||
fail(error) {
|
||||
fail(error: unknown) {
|
||||
if (this.closed || this.fault)
|
||||
return
|
||||
this.fault = true
|
||||
@@ -81,9 +102,9 @@ export default class Scheduler {
|
||||
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 }
|
||||
let cancel!: () => void
|
||||
const cancellation = new Promise<typeof cancelled>(resolve => cancel = () => resolve(cancelled))
|
||||
const operation: FrameOperation = { 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))
|
||||
@@ -99,7 +120,7 @@ export default class Scheduler {
|
||||
owner.timer = this.frame
|
||||
}
|
||||
|
||||
async run(operation) {
|
||||
async run(operation: FrameOperation) {
|
||||
const owner = this.owner
|
||||
if (!owner.art.playing || !this.active(operation))
|
||||
return
|
||||
@@ -139,16 +160,18 @@ export default class Scheduler {
|
||||
danmu.$lastStartTime = Date.now()
|
||||
danmu.$restTime = owner.speed
|
||||
const distance = clientWidth + ref.clientWidth
|
||||
const { result: top } = await operation.wait(owner.postMessage({
|
||||
const reply = 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,
|
||||
// Valid margins are numeric. Preserve the old raw fallback for unsupported strings.
|
||||
marginBottom: owner.marginBottom as number,
|
||||
marginTop: owner.marginTop,
|
||||
}))
|
||||
const top = typeof reply === 'symbol' ? undefined : reply.result
|
||||
if (!this.active(operation) || danmu.$ref !== ref)
|
||||
return
|
||||
if (top !== undefined) {
|
||||
+49
-15
@@ -1,7 +1,35 @@
|
||||
const properties = new WeakMap()
|
||||
import type { DanmukuArt } from './types'
|
||||
|
||||
type Disposer = () => unknown
|
||||
type PropertyValue = string | number | boolean | undefined
|
||||
interface PropertyEntry {
|
||||
original: PropertyValue
|
||||
present: boolean
|
||||
priority: string | undefined
|
||||
owners: Map<SettingLifecycle, PropertyValue>
|
||||
apply: (value: PropertyValue) => unknown
|
||||
last?: PropertyValue
|
||||
lastPriority?: string
|
||||
}
|
||||
interface SettingEvents {
|
||||
'resize': []
|
||||
'fullscreen': [state: boolean]
|
||||
'fullscreenWeb': [state: boolean]
|
||||
'artplayerPluginDanmuku:show': []
|
||||
'artplayerPluginDanmuku:hide': []
|
||||
'document:pointermove': [event: PointerEvent]
|
||||
'document:pointerup': [event: PointerEvent]
|
||||
}
|
||||
const properties = new WeakMap<object, Map<string, PropertyEntry>>()
|
||||
|
||||
export default class SettingLifecycle {
|
||||
constructor(art) {
|
||||
declare art: DanmukuArt
|
||||
declare closed: boolean
|
||||
declare disposers: Set<Disposer>
|
||||
declare cancelled: Promise<void>
|
||||
declare cancel: () => void
|
||||
|
||||
constructor(art: DanmukuArt) {
|
||||
this.art = art
|
||||
this.closed = false
|
||||
this.disposers = new Set()
|
||||
@@ -12,13 +40,15 @@ export default class SettingLifecycle {
|
||||
return !this.closed && !this.art.isDestroy
|
||||
}
|
||||
|
||||
own(dispose) {
|
||||
own(dispose: Disposer) {
|
||||
if (this.closed)
|
||||
dispose()
|
||||
else this.disposers.add(dispose)
|
||||
}
|
||||
|
||||
write(target, key, value, apply = value => target[key] = value) {
|
||||
write(host: DOMStringMap | CSSStyleDeclaration, key: string, value: PropertyValue, apply: (value: PropertyValue) => unknown = value => (host as unknown as Record<string, PropertyValue>)[key] = value) {
|
||||
// This adapter retains native dataset/style assignment and its WebIDL coercion.
|
||||
const target = host as unknown as Record<string, PropertyValue> & Partial<Pick<CSSStyleDeclaration, 'getPropertyPriority' | 'setProperty'>>
|
||||
if (!this.active)
|
||||
return
|
||||
const priority = () => typeof target.getPropertyPriority === 'function' ? target.getPropertyPriority(key) : undefined
|
||||
@@ -52,7 +82,7 @@ export default class SettingLifecycle {
|
||||
entry.lastPriority = priority()
|
||||
}
|
||||
else if (entry.priority !== undefined) {
|
||||
target.setProperty(key, entry.original, entry.priority)
|
||||
target.setProperty!(key, entry.original as string, entry.priority)
|
||||
}
|
||||
else if (entry.present) {
|
||||
entry.apply(entry.original)
|
||||
@@ -75,13 +105,15 @@ export default class SettingLifecycle {
|
||||
entry.lastPriority = priority()
|
||||
}
|
||||
|
||||
on(name, callback) {
|
||||
on<Name extends keyof SettingEvents>(name: Name, callback: (this: unknown, ...args: SettingEvents[Name]) => unknown) {
|
||||
if (!this.active)
|
||||
return
|
||||
// Keep the emitter/native callback receiver while consulting this owner.
|
||||
// eslint-disable-next-line ts/no-this-alias
|
||||
const lifecycle = this
|
||||
function listener(...args) {
|
||||
function listener(this: unknown, ...args: unknown[]) {
|
||||
if (lifecycle.active)
|
||||
return callback.apply(this, args)
|
||||
return callback.apply(this, args as SettingEvents[Name])
|
||||
}
|
||||
const dispose = () => this.art.off(name, listener)
|
||||
this.own(dispose)
|
||||
@@ -96,18 +128,20 @@ export default class SettingLifecycle {
|
||||
}
|
||||
}
|
||||
|
||||
proxy(target, name, callback) {
|
||||
proxy<Name extends keyof HTMLElementEventMap>(target: HTMLElement, name: Name, callback: (this: unknown, event: HTMLElementEventMap[Name]) => unknown) {
|
||||
if (!this.active)
|
||||
return
|
||||
// Keep the emitter/native callback receiver while consulting this owner.
|
||||
// eslint-disable-next-line ts/no-this-alias
|
||||
const lifecycle = this
|
||||
function listener(...args) {
|
||||
function listener(this: unknown, ...args: unknown[]) {
|
||||
if (lifecycle.active)
|
||||
return callback.apply(this, args)
|
||||
return callback.apply(this, args as [HTMLElementEventMap[Name]])
|
||||
}
|
||||
let remove
|
||||
let remove: (() => void) | undefined
|
||||
const dispose = () => {
|
||||
if (typeof remove === 'function') {
|
||||
const registry = this.art.events
|
||||
const registry = this.art.events as DanmukuArt['events'] & { destroyEvents?: Set<() => void> }
|
||||
if (typeof registry?.remove === 'function' && registry.destroyEvents?.has(remove))
|
||||
registry.remove(remove)
|
||||
else remove()
|
||||
@@ -128,11 +162,11 @@ export default class SettingLifecycle {
|
||||
}
|
||||
}
|
||||
|
||||
wait(value) {
|
||||
wait<Value>(value: Value | PromiseLike<Value>) {
|
||||
return Promise.race([value, this.cancelled])
|
||||
}
|
||||
|
||||
release(dispose) {
|
||||
release(dispose: Disposer) {
|
||||
try {
|
||||
dispose()
|
||||
}
|
||||
+12
-9
@@ -1,17 +1,20 @@
|
||||
export async function emitSetting(setting) {
|
||||
import type Setting from './setting'
|
||||
import type { DanmuInput } from './types'
|
||||
|
||||
export async function emitSetting(setting: Setting) {
|
||||
const { lifecycle, template: { $input } } = setting
|
||||
if (!lifecycle.active)
|
||||
return
|
||||
const text = $input.value.trim()
|
||||
if (!text.length || setting.isLock || setting.emitting)
|
||||
return
|
||||
const danmu = {
|
||||
const danmu: DanmuInput = {
|
||||
text,
|
||||
mode: setting.option.mode,
|
||||
color: setting.option.color,
|
||||
time: setting.art.currentTime,
|
||||
}
|
||||
const report = (error) => {
|
||||
const report = (error: unknown) => {
|
||||
if (lifecycle.active)
|
||||
console.error('Error emitting danmuku:', error)
|
||||
}
|
||||
@@ -39,15 +42,15 @@ export async function emitSetting(setting) {
|
||||
}
|
||||
}
|
||||
|
||||
export function lockSetting(setting) {
|
||||
export function lockSetting(setting: Setting) {
|
||||
if (!setting.lifecycle.active)
|
||||
return
|
||||
const { addClass } = setting.utils
|
||||
const { $send } = setting.template
|
||||
clearTimeout(setting.timer)
|
||||
clearTimeout(setting.timer as number | undefined)
|
||||
setting.isLock = true
|
||||
let time = setting.option.lockTime
|
||||
$send.textContent = time
|
||||
$send.textContent = time as unknown as string
|
||||
addClass($send, 'apd-lock')
|
||||
const loop = () => {
|
||||
setting.timer = setTimeout(() => {
|
||||
@@ -58,7 +61,7 @@ export function lockSetting(setting) {
|
||||
}
|
||||
else {
|
||||
time -= 1
|
||||
$send.textContent = time
|
||||
$send.textContent = time as unknown as string
|
||||
loop()
|
||||
}
|
||||
}, 1000)
|
||||
@@ -66,8 +69,8 @@ export function lockSetting(setting) {
|
||||
loop()
|
||||
}
|
||||
|
||||
export function unlockSetting(setting) {
|
||||
clearTimeout(setting.timer)
|
||||
export function unlockSetting(setting: Setting) {
|
||||
clearTimeout(setting.timer as number | undefined)
|
||||
setting.timer = null
|
||||
setting.isLock = false
|
||||
if (!setting.lifecycle.active)
|
||||
+9
-6
@@ -1,10 +1,13 @@
|
||||
export default function createSlider({ min, max, container, findIndex, onChange, steps = [] }) {
|
||||
import type Setting from './setting'
|
||||
import type { SliderConfig } from './setting-types'
|
||||
|
||||
export default function createSlider(this: Setting, { min, max, container, findIndex, onChange, steps = [] }: SliderConfig) {
|
||||
const { lifecycle } = this
|
||||
if (!lifecycle.active)
|
||||
return { reset() {} }
|
||||
const { query, clamp, setStyle } = this.utils
|
||||
|
||||
setStyle(container, 'touch-action', 'none')
|
||||
setStyle(container, 'touch-action' as keyof CSSStyleDeclaration, 'none')
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="apd-slider-line">
|
||||
@@ -19,14 +22,14 @@ export default function createSlider({ min, max, container, findIndex, onChange,
|
||||
</div>
|
||||
`
|
||||
|
||||
const $dot = query('.apd-slider-dot', container)
|
||||
const $progress = query('.apd-slider-progress', container)
|
||||
const $dot = query<HTMLDivElement>('.apd-slider-dot', container)!
|
||||
const $progress = query<HTMLDivElement>('.apd-slider-progress', container)!
|
||||
|
||||
let isDroging = false
|
||||
|
||||
lifecycle.own(() => isDroging = false)
|
||||
|
||||
function reset(index) {
|
||||
function reset(index?: number) {
|
||||
if (!lifecycle.active)
|
||||
return
|
||||
if (index === undefined)
|
||||
@@ -41,7 +44,7 @@ export default function createSlider({ min, max, container, findIndex, onChange,
|
||||
onChange(index)
|
||||
}
|
||||
|
||||
function updateLeft(event) {
|
||||
function updateLeft(this: Setting, event: MouseEvent) {
|
||||
const { top, height, left, width } = container.getBoundingClientRect()
|
||||
if (this.art.isRotate) {
|
||||
const value = clamp(event.clientY - top, 0, height)
|
||||
+8
-7
@@ -3,26 +3,27 @@ import style from './style.less?inline'
|
||||
if (typeof document !== 'undefined') {
|
||||
const id = 'artplayer-plugin-danmuku'
|
||||
const pending = Symbol.for('artplayer-plugin-danmuku.pending-style')
|
||||
let $style = document.getElementById(id) || document[pending]
|
||||
const styleDocument = document as Document & { [key: symbol]: HTMLElement | undefined }
|
||||
let $style = document.getElementById(id) || styleDocument[pending]
|
||||
if (!$style) {
|
||||
$style = document.createElement('style')
|
||||
$style.id = id
|
||||
if (document.readyState === 'loading') {
|
||||
document[pending] = $style
|
||||
styleDocument[pending] = $style
|
||||
const ready = () => {
|
||||
document.removeEventListener('DOMContentLoaded', ready)
|
||||
if (document[pending] === $style)
|
||||
delete document[pending]
|
||||
if (styleDocument[pending] === $style)
|
||||
delete styleDocument[pending]
|
||||
const existing = document.getElementById(id)
|
||||
if (existing)
|
||||
existing.textContent = $style.textContent
|
||||
existing.textContent = $style!.textContent
|
||||
else
|
||||
(document.head || document.documentElement).appendChild($style)
|
||||
(document.head || document.documentElement).appendChild($style!)
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', ready, { once: true })
|
||||
}
|
||||
else {
|
||||
(document.head || document.documentElement).appendChild($style)
|
||||
(document.head || document.documentElement).appendChild($style!)
|
||||
}
|
||||
}
|
||||
$style.textContent = style
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
import type Setting from './setting'
|
||||
import $check_off from './img/check_off.svg?raw'
|
||||
import $check_on from './img/check_on.svg?raw'
|
||||
import $config from './img/config.svg?raw'
|
||||
@@ -28,7 +29,7 @@ export function settingIcons() {
|
||||
}
|
||||
}
|
||||
|
||||
export function settingTemplate() {
|
||||
export function settingTemplate(this: Setting) {
|
||||
const { option } = this
|
||||
return `
|
||||
<div class="apd-toggle">
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { SliderStep } from './types'
|
||||
|
||||
// createTemplate/createSliders fill these slots before registering active UI callbacks.
|
||||
// The initial null values and absent antiOverlap/syncVideo keys are kept for compatibility.
|
||||
export interface SettingTemplate {
|
||||
$controlsCenter: HTMLDivElement
|
||||
$mount: HTMLElement
|
||||
$danmuku: HTMLDivElement
|
||||
$toggle: HTMLDivElement
|
||||
$config: HTMLDivElement
|
||||
$configPanel: HTMLDivElement
|
||||
$configModes: HTMLDivElement
|
||||
$style: HTMLDivElement
|
||||
$stylePanel: HTMLDivElement
|
||||
$styleModes: HTMLDivElement
|
||||
$colors: HTMLDivElement
|
||||
$antiOverlap?: HTMLDivElement
|
||||
$syncVideo?: HTMLDivElement
|
||||
$opacitySlider: HTMLDivElement
|
||||
$opacityValue: HTMLDivElement
|
||||
$marginSlider: HTMLDivElement
|
||||
$marginValue: HTMLDivElement
|
||||
$fontSizeSlider: HTMLDivElement
|
||||
$fontSizeValue: HTMLDivElement
|
||||
$speedSlider: HTMLDivElement
|
||||
$speedValue: HTMLDivElement
|
||||
$input: HTMLInputElement
|
||||
$send: HTMLDivElement
|
||||
}
|
||||
|
||||
export interface SettingSlider { reset: (index?: number) => void }
|
||||
export interface SliderConfig {
|
||||
min: number
|
||||
max: number
|
||||
container: HTMLElement
|
||||
findIndex: () => number
|
||||
onChange: (index: number) => void
|
||||
steps?: SliderStep<unknown>[]
|
||||
}
|
||||
+66
-53
@@ -1,3 +1,6 @@
|
||||
import type Danmuku from './danmuku'
|
||||
import type { SettingSlider, SettingTemplate, SliderConfig } from './setting-types'
|
||||
import type { DanmukuArt, NormalizedOption, SliderOption } from './types'
|
||||
import SettingLifecycle from './setting-lifecycle'
|
||||
import { emitSetting, lockSetting, unlockSetting } from './setting-send'
|
||||
import createSlider from './setting-slider'
|
||||
@@ -5,7 +8,17 @@ import { settingIcons, settingTemplate } from './setting-template'
|
||||
import './setting-style'
|
||||
|
||||
export default class Setting {
|
||||
constructor(art, danmuku) {
|
||||
declare art: DanmukuArt
|
||||
declare danmuku: Danmuku
|
||||
declare utils: DanmukuArt['constructor']['utils']
|
||||
declare template: SettingTemplate
|
||||
declare slider: Record<'opacity' | 'margin' | 'fontSize' | 'speed', SettingSlider>
|
||||
declare emitting: boolean
|
||||
declare isLock: boolean
|
||||
declare timer: ReturnType<typeof setTimeout> | null
|
||||
declare lifecycle: SettingLifecycle
|
||||
|
||||
constructor(art: DanmukuArt, danmuku: Danmuku) {
|
||||
this.art = art
|
||||
this.danmuku = danmuku
|
||||
this.utils = art.constructor.utils
|
||||
@@ -16,32 +29,32 @@ export default class Setting {
|
||||
this.template = {
|
||||
$controlsCenter,
|
||||
$mount: $controlsCenter,
|
||||
$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,
|
||||
$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,
|
||||
opacity: null!,
|
||||
margin: null!,
|
||||
fontSize: null!,
|
||||
speed: null!,
|
||||
}
|
||||
|
||||
this.emitting = false
|
||||
@@ -56,7 +69,7 @@ export default class Setting {
|
||||
art.on('destroy', this.destroy)
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
this.lifecycle.write($controlsCenter.style, 'display', 'flex', value => setStyle($controlsCenter, 'display', value))
|
||||
this.lifecycle.write($controlsCenter.style, 'display', 'flex', value => setStyle($controlsCenter, 'display', value as string))
|
||||
|
||||
this.createTemplate()
|
||||
if (!this.lifecycle.active)
|
||||
@@ -103,7 +116,7 @@ export default class Setting {
|
||||
return settingIcons()
|
||||
}
|
||||
|
||||
get option() {
|
||||
get option(): NormalizedOption {
|
||||
return this.danmuku.option
|
||||
}
|
||||
|
||||
@@ -115,7 +128,7 @@ export default class Setting {
|
||||
return settingTemplate.call(this)
|
||||
}
|
||||
|
||||
get OPACITY() {
|
||||
get OPACITY(): Required<SliderOption> {
|
||||
return {
|
||||
min: 0,
|
||||
max: 100,
|
||||
@@ -124,7 +137,7 @@ export default class Setting {
|
||||
}
|
||||
}
|
||||
|
||||
get FONT_SIZE() {
|
||||
get FONT_SIZE(): Required<SliderOption> {
|
||||
return {
|
||||
min: 12,
|
||||
max: 120,
|
||||
@@ -133,7 +146,7 @@ export default class Setting {
|
||||
}
|
||||
}
|
||||
|
||||
get MARGIN() {
|
||||
get MARGIN(): Required<SliderOption<NormalizedOption['margin']>> {
|
||||
return {
|
||||
min: 0,
|
||||
max: 3,
|
||||
@@ -159,7 +172,7 @@ export default class Setting {
|
||||
}
|
||||
}
|
||||
|
||||
get SPEED() {
|
||||
get SPEED(): Required<SliderOption> {
|
||||
return {
|
||||
min: 0,
|
||||
max: 4,
|
||||
@@ -212,13 +225,13 @@ export default class Setting {
|
||||
]
|
||||
}
|
||||
|
||||
query(selector) {
|
||||
query<T extends HTMLElement = HTMLDivElement>(selector: string): T {
|
||||
const { query } = this.utils
|
||||
const { $danmuku } = this.template
|
||||
return query(selector, $danmuku)
|
||||
return query(selector, $danmuku) as T
|
||||
}
|
||||
|
||||
append(el, target) {
|
||||
append(el: HTMLElement, target: HTMLElement) {
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
const { append } = this.utils
|
||||
@@ -228,7 +241,7 @@ export default class Setting {
|
||||
append(el, target)
|
||||
}
|
||||
|
||||
setData(key, value) {
|
||||
setData(key: string, value: string | number | boolean) {
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
const { $player } = this.art.template
|
||||
@@ -265,7 +278,7 @@ export default class Setting {
|
||||
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.$input = this.query<HTMLInputElement>('.apd-input')
|
||||
this.template.$send = this.query('.apd-send')
|
||||
|
||||
const { $toggle } = this.template
|
||||
@@ -290,7 +303,7 @@ export default class Setting {
|
||||
})
|
||||
|
||||
this.lifecycle.proxy($configModes, 'click', (event) => {
|
||||
const $mode = event.target.closest('.apd-mode')
|
||||
const $mode = (event.target as HTMLElement).closest<HTMLElement>('.apd-mode')
|
||||
if (!$mode)
|
||||
return
|
||||
const mode = Number($mode.dataset.mode)
|
||||
@@ -307,14 +320,14 @@ export default class Setting {
|
||||
this.reset()
|
||||
})
|
||||
|
||||
this.lifecycle.proxy($antiOverlap, 'click', () => {
|
||||
this.lifecycle.proxy($antiOverlap!, 'click', () => {
|
||||
this.danmuku.config({
|
||||
antiOverlap: !this.option.antiOverlap,
|
||||
})
|
||||
this.reset()
|
||||
})
|
||||
|
||||
this.lifecycle.proxy($syncVideo, 'click', () => {
|
||||
this.lifecycle.proxy($syncVideo!, 'click', () => {
|
||||
this.danmuku.config({
|
||||
synchronousPlayback: !this.option.synchronousPlayback,
|
||||
})
|
||||
@@ -322,7 +335,7 @@ export default class Setting {
|
||||
})
|
||||
|
||||
this.lifecycle.proxy($styleModes, 'click', (event) => {
|
||||
const $mode = event.target.closest('.apd-mode')
|
||||
const $mode = (event.target as HTMLElement).closest<HTMLElement>('.apd-mode')
|
||||
if (!$mode)
|
||||
return
|
||||
const mode = Number($mode.dataset.mode)
|
||||
@@ -333,7 +346,7 @@ export default class Setting {
|
||||
})
|
||||
|
||||
this.lifecycle.proxy($colors, 'click', (event) => {
|
||||
const $color = event.target.closest('.apd-color')
|
||||
const $color = (event.target as HTMLElement).closest<HTMLElement>('.apd-color')
|
||||
if (!$color)
|
||||
return
|
||||
this.danmuku.config({
|
||||
@@ -373,7 +386,7 @@ export default class Setting {
|
||||
container: this.template.$marginSlider,
|
||||
findIndex: () => {
|
||||
return this.MARGIN.steps.findIndex(
|
||||
item => item.value[0] === this.option.margin[0] && item.value[1] === this.option.margin[1],
|
||||
item => item.value![0] === this.option.margin[0] && item.value![1] === this.option.margin[1],
|
||||
)
|
||||
},
|
||||
onChange: (index) => {
|
||||
@@ -381,7 +394,7 @@ export default class Setting {
|
||||
if (!margin)
|
||||
return
|
||||
const { $marginValue } = this.template
|
||||
$marginValue.textContent = margin.name
|
||||
$marginValue.textContent = margin.name as string
|
||||
this.danmuku.config({
|
||||
margin: margin.value,
|
||||
})
|
||||
@@ -416,7 +429,7 @@ export default class Setting {
|
||||
if (!speed)
|
||||
return
|
||||
const { $speedValue } = this.template
|
||||
$speedValue.textContent = speed.name
|
||||
$speedValue.textContent = speed.name as string
|
||||
this.danmuku.config({
|
||||
speed: speed.value,
|
||||
})
|
||||
@@ -424,11 +437,11 @@ export default class Setting {
|
||||
})
|
||||
}
|
||||
|
||||
createSlider(option) {
|
||||
createSlider(option: SliderConfig) {
|
||||
return createSlider.call(this, option)
|
||||
}
|
||||
|
||||
onFullscreen(state) {
|
||||
onFullscreen(state: boolean) {
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
const { $danmuku, $controlsCenter, $mount } = this.template
|
||||
@@ -445,7 +458,7 @@ export default class Setting {
|
||||
}
|
||||
}
|
||||
|
||||
onMouseEnter({ $control, $panel }) {
|
||||
onMouseEnter({ $control, $panel }: { $control: HTMLElement, $panel: HTMLElement }) {
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
const { $player } = this.art.template
|
||||
@@ -522,7 +535,7 @@ export default class Setting {
|
||||
this.setData('danmukuTheme', this.option.theme)
|
||||
this.setData('danmukuEmitter', this.option.emitter)
|
||||
|
||||
const colors = $colors.children
|
||||
const colors = $colors.children as HTMLCollectionOf<HTMLElement>
|
||||
const $color = Array.from(colors).find(item => item.dataset.color === this.option.color.toUpperCase())
|
||||
$color && inverseClass($color, 'apd-active')
|
||||
|
||||
@@ -531,14 +544,14 @@ export default class Setting {
|
||||
this.resize()
|
||||
}
|
||||
|
||||
mount(target) {
|
||||
mount(target: NormalizedOption['mount']) {
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
const { errorHandle } = this.utils
|
||||
const $el = typeof target === 'string' ? document.querySelector(target) : target
|
||||
errorHandle($el, `Can not find the mount point: ${target}`)
|
||||
this.append($el, this.template.$danmuku)
|
||||
this.template.$mount = $el
|
||||
const $el = typeof target === 'string' ? document.querySelector<HTMLElement>(target) : target
|
||||
errorHandle($el as unknown as boolean, `Can not find the mount point: ${target}`)
|
||||
this.append($el!, this.template.$danmuku)
|
||||
this.template.$mount = $el!
|
||||
this.reset()
|
||||
}
|
||||
|
||||
@@ -546,7 +559,7 @@ export default class Setting {
|
||||
if (this.lifecycle.closed)
|
||||
return
|
||||
this.lifecycle.close()
|
||||
clearTimeout(this.timer)
|
||||
clearTimeout(this.timer as number | undefined)
|
||||
this.timer = null
|
||||
this.emitting = false
|
||||
this.isLock = false
|
||||
@@ -0,0 +1,19 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
|
||||
// Public runtime data has one declaration source; host and Worker adapters stay internal.
|
||||
export type {
|
||||
Danmu as DanmuInput,
|
||||
Item as DanmuItem,
|
||||
Input as DanmukuInput,
|
||||
RuntimeOption as DanmukuOption,
|
||||
State as DanmuState,
|
||||
Heatmap as HeatmapOptions,
|
||||
Point as HeatmapPoint,
|
||||
NormalizedDanmu,
|
||||
NormalizedOption,
|
||||
Slider as SliderOption,
|
||||
SliderStep,
|
||||
} from '../types/runtime-shared'
|
||||
|
||||
export type DanmukuArt = Artplayer & { constructor: typeof Artplayer, isRotate?: boolean }
|
||||
export type DanmuStyle = Partial<CSSStyleDeclaration>
|
||||
@@ -0,0 +1,4 @@
|
||||
declare module '*?worker&inline' {
|
||||
const WorkerConstructor: new (options?: WorkerOptions) => Worker
|
||||
export default WorkerConstructor
|
||||
}
|
||||
+16
-8
@@ -1,14 +1,22 @@
|
||||
import type { PlacementReply, PlacementRequest } from './worker-types'
|
||||
|
||||
let nextRequest = 0
|
||||
|
||||
export default class WorkerClient {
|
||||
constructor(createWorker, onFailure) {
|
||||
declare pending: Map<number, { resolve: (value: PlacementReply) => void, reject: (error: unknown) => void }>
|
||||
declare closed: boolean
|
||||
declare failed: boolean
|
||||
declare onFailure: (error: unknown) => void
|
||||
declare worker: Worker
|
||||
|
||||
constructor(createWorker: () => Worker, onFailure: (error: unknown) => void) {
|
||||
this.pending = new Map()
|
||||
this.closed = false
|
||||
this.failed = false
|
||||
this.onFailure = onFailure
|
||||
this.worker = createWorker()
|
||||
try {
|
||||
this.worker.onmessage = (event) => {
|
||||
this.worker.onmessage = (event: MessageEvent<PlacementReply>) => {
|
||||
const request = this.pending.get(event.data?.id)
|
||||
if (!request)
|
||||
return
|
||||
@@ -19,7 +27,7 @@ export default class WorkerClient {
|
||||
event.preventDefault?.()
|
||||
this.fail(event.error || event)
|
||||
}
|
||||
this.worker.onmessageerror = event => this.fail(event.error || event)
|
||||
this.worker.onmessageerror = event => this.fail((event as MessageEvent & { error?: unknown }).error || event)
|
||||
}
|
||||
catch (error) {
|
||||
try {
|
||||
@@ -30,7 +38,7 @@ export default class WorkerClient {
|
||||
}
|
||||
}
|
||||
|
||||
request(message) {
|
||||
request(message: PlacementRequest): Promise<PlacementReply> {
|
||||
message.id = ++nextRequest
|
||||
const { id } = message
|
||||
if (this.closed || this.failed)
|
||||
@@ -46,7 +54,7 @@ export default class WorkerClient {
|
||||
})
|
||||
}
|
||||
|
||||
fail(error) {
|
||||
fail(error: unknown) {
|
||||
if (this.closed || this.failed)
|
||||
return
|
||||
this.failed = true
|
||||
@@ -70,8 +78,8 @@ export default class WorkerClient {
|
||||
this.closed = true
|
||||
this.cancel()
|
||||
let failed = false
|
||||
let error
|
||||
const attempt = (callback) => {
|
||||
let error: unknown
|
||||
const attempt = (callback: () => void) => {
|
||||
try {
|
||||
callback()
|
||||
}
|
||||
@@ -81,7 +89,7 @@ export default class WorkerClient {
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
for (const name of ['onmessage', 'onerror', 'onmessageerror']) {
|
||||
for (const name of ['onmessage', 'onerror', 'onmessageerror'] as const) {
|
||||
attempt(() => this.worker[name] = null)
|
||||
}
|
||||
attempt(() => this.worker.terminate())
|
||||
@@ -0,0 +1,32 @@
|
||||
export interface VisibleDanmu {
|
||||
mode: number
|
||||
top: number
|
||||
left: number
|
||||
right: number
|
||||
height: number
|
||||
width: number
|
||||
speed: number
|
||||
distance: number
|
||||
time: number
|
||||
}
|
||||
|
||||
export interface PlacementRequest {
|
||||
id?: number
|
||||
type: 'getDanmuTop'
|
||||
target: { mode: number, height: number, speed: number }
|
||||
visibles: VisibleDanmu[]
|
||||
clientWidth: number
|
||||
clientHeight: number
|
||||
marginBottom: number
|
||||
marginTop: number
|
||||
antiOverlap: boolean
|
||||
}
|
||||
|
||||
export interface PlacementMessage extends PlacementRequest { id: number }
|
||||
export interface PlacementReply { id: number, result: number | undefined }
|
||||
|
||||
export interface PlacementBoundary extends Omit<VisibleDanmu, 'mode' | 'time'> {
|
||||
type: 'top' | 'bottom'
|
||||
}
|
||||
|
||||
export type PlacementRow = VisibleDanmu | PlacementBoundary
|
||||
+16
-13
@@ -1,9 +1,11 @@
|
||||
function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom, marginTop, antiOverlap }) {
|
||||
import type { PlacementMessage, PlacementReply, PlacementRequest, PlacementRow, VisibleDanmu } from './worker-types'
|
||||
|
||||
function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom, marginTop, antiOverlap }: PlacementRequest): number | undefined {
|
||||
// 弹幕最大高度
|
||||
const maxTop = clientHeight - marginBottom
|
||||
|
||||
// 过滤同模式的弹幕,即每种模式各不影响
|
||||
const danmus = visibles
|
||||
const danmus: PlacementRow[] = visibles
|
||||
.filter(item => item.mode === target.mode && item.top <= maxTop)
|
||||
.sort((prev, next) => prev.top - next.top)
|
||||
|
||||
@@ -44,8 +46,8 @@ function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom
|
||||
if (target.mode === 2) {
|
||||
// 倒序查找
|
||||
for (let index = danmus.length - 2; index >= 0; index -= 1) {
|
||||
const item = danmus[index]
|
||||
const prev = danmus[index + 1]
|
||||
const item = danmus[index]!
|
||||
const prev = danmus[index + 1]!
|
||||
const itemBottom = item.top + item.height
|
||||
const diff = prev.top - itemBottom
|
||||
if (diff >= target.height) {
|
||||
@@ -56,8 +58,8 @@ function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom
|
||||
else {
|
||||
// 顺序查找
|
||||
for (let index = 1; index < danmus.length; index += 1) {
|
||||
const item = danmus[index]
|
||||
const prev = danmus[index - 1]
|
||||
const item = danmus[index]!
|
||||
const prev = danmus[index - 1]!
|
||||
const prevBottom = prev.top + prev.height
|
||||
const diff = item.top - prevBottom
|
||||
if (diff >= target.height) {
|
||||
@@ -66,12 +68,13 @@ function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom
|
||||
}
|
||||
}
|
||||
|
||||
const topMap = []
|
||||
const topMap: VisibleDanmu[][] = []
|
||||
for (let index = 1; index < danmus.length - 1; index += 1) {
|
||||
const item = danmus[index]
|
||||
// The loop excludes the two virtual boundary rows inserted above.
|
||||
const item = danmus[index] as VisibleDanmu
|
||||
if (topMap.length) {
|
||||
const last = topMap[topMap.length - 1]
|
||||
if (last[0].top === item.top) {
|
||||
const last = topMap[topMap.length - 1]!
|
||||
if (last[0]!.top === item.top) {
|
||||
last.push(item)
|
||||
}
|
||||
else {
|
||||
@@ -131,11 +134,11 @@ function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom
|
||||
break
|
||||
}
|
||||
|
||||
return topMap[0][0].top
|
||||
return topMap[0]![0]!.top
|
||||
}
|
||||
}
|
||||
|
||||
onmessage = (event) => {
|
||||
onmessage = (event: MessageEvent<PlacementMessage>) => {
|
||||
const { data } = event
|
||||
if (!data.id || !data.type)
|
||||
return
|
||||
@@ -147,5 +150,5 @@ onmessage = (event) => {
|
||||
globalThis.postMessage({
|
||||
result,
|
||||
id: data.id,
|
||||
})
|
||||
} satisfies PlacementReply)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": false,
|
||||
"lib": ["ES2020", "ES2021.String", "DOM", "DOM.Iterable"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "../../types/assets.d.ts", "../../types/option-validator.d.ts"]
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
|
||||
export type Mode = 0 | 1 | 2
|
||||
export type State = 'wait' | 'ready' | 'emit' | 'stop'
|
||||
export type Margin = [number | string, number | string]
|
||||
/** Mutable tuples: historical custom heatmap rendering adjusts the second coordinate. */
|
||||
export type Point = [number, number]
|
||||
|
||||
export interface Danmu {
|
||||
text: string
|
||||
id?: string
|
||||
mode?: number
|
||||
color?: string
|
||||
time?: number
|
||||
border?: boolean
|
||||
style?: Partial<CSSStyleDeclaration>
|
||||
[extension: string]: unknown
|
||||
}
|
||||
|
||||
/** Input fields have been filled before filter runs; queue fields do not exist yet. */
|
||||
export interface NormalizedDanmu extends Danmu {
|
||||
mode: number
|
||||
color: string
|
||||
time: number
|
||||
style: Partial<CSSStyleDeclaration>
|
||||
}
|
||||
|
||||
/** The actual mutable queue entry supplied to beforeVisible and visible/loaded events. */
|
||||
export interface Item extends NormalizedDanmu {
|
||||
$state: State
|
||||
$index: number
|
||||
$ref: HTMLDivElement | null
|
||||
$restTime: number
|
||||
$lastStartTime: number
|
||||
}
|
||||
|
||||
/** Functions run without an option receiver. Arrays are also valid synchronous results. */
|
||||
export type Input = Danmu[] | string | Promise<Danmu[]> | (() => Danmu[] | Promise<Danmu[]>)
|
||||
export interface SliderStep<Value = number> { name?: string, value?: Value, hide?: boolean, show?: boolean }
|
||||
export interface Slider<Value = number> { min?: number, max?: number, steps?: SliderStep<Value>[] }
|
||||
export interface Heatmap {
|
||||
xMin?: number
|
||||
xMax?: number
|
||||
yMin?: number
|
||||
yMax?: number
|
||||
scale?: number
|
||||
opacity?: number
|
||||
minHeight?: number
|
||||
sampling?: number
|
||||
smoothing?: number
|
||||
flattening?: number
|
||||
}
|
||||
|
||||
export interface NormalizedOption {
|
||||
danmuku: Input
|
||||
speed: number
|
||||
margin: Margin
|
||||
opacity: number
|
||||
color: string
|
||||
/** Numeric inputs are clamped; values other than 0, 1 and 2 are not emitted. */
|
||||
mode: number
|
||||
modes: number[]
|
||||
fontSize: number | string
|
||||
antiOverlap: boolean
|
||||
synchronousPlayback: boolean
|
||||
mount: HTMLDivElement | string | undefined
|
||||
heatmap: boolean | Heatmap
|
||||
width: number
|
||||
/** Stored configuration; send the points event to draw custom points. */
|
||||
points: Point[]
|
||||
/** A truthy result accepts the row. */
|
||||
filter: (this: NormalizedOption, danmu: NormalizedDanmu) => unknown
|
||||
/** Only a result equal to true sends the row; callbacks may be asynchronous. */
|
||||
beforeEmit: (this: NormalizedOption, danmu: Danmu) => unknown
|
||||
/** A truthy result displays the queue entry; callbacks may be asynchronous. */
|
||||
beforeVisible: (this: NormalizedOption, danmu: Item) => unknown
|
||||
visible: boolean
|
||||
emitter: boolean
|
||||
maxLength: number
|
||||
lockTime: number
|
||||
theme: string
|
||||
OPACITY: Slider
|
||||
FONT_SIZE: Slider
|
||||
MARGIN: Slider<Margin>
|
||||
SPEED: Slider
|
||||
COLOR: string[]
|
||||
}
|
||||
|
||||
/** The argument object is required; every option inside it is optional. */
|
||||
export type RuntimeOption = Partial<NormalizedOption>
|
||||
|
||||
/** The owner returned by commands is different from the registered plugin facade. */
|
||||
export interface Owner {
|
||||
art: Artplayer
|
||||
option: NormalizedOption
|
||||
isHide: boolean
|
||||
isStop: boolean
|
||||
queue: Item[]
|
||||
states: Record<State, Item[]>
|
||||
readonly readys: Item[]
|
||||
readonly speed: number
|
||||
readonly fontSize: number
|
||||
readonly marginTop: number
|
||||
readonly marginBottom: number | string
|
||||
readonly isRotate: boolean | undefined
|
||||
emit: (danmu: Danmu) => Promise<Owner>
|
||||
/** No argument replaces the configured source; a provided argument appends rows. */
|
||||
load: (danmuku?: Input) => Promise<Owner>
|
||||
config: (option: RuntimeOption) => Owner
|
||||
hide: () => Owner
|
||||
show: () => Owner
|
||||
reset: () => Owner
|
||||
start: () => Owner
|
||||
stop: () => Owner
|
||||
continue: () => Owner
|
||||
suspend: () => Owner
|
||||
update: () => Owner
|
||||
resize: () => void
|
||||
seek: () => void
|
||||
destroy: () => void
|
||||
}
|
||||
|
||||
export interface RuntimeResult {
|
||||
name: 'artplayerPluginDanmuku'
|
||||
emit: (danmu: Danmu) => Promise<Owner>
|
||||
load: (danmuku?: Input) => Promise<Owner>
|
||||
config: (option: RuntimeOption) => Owner
|
||||
hide: () => Owner
|
||||
show: () => Owner
|
||||
reset: () => Owner
|
||||
/** The live setting requires a valid mount; omitting it does not select a default. */
|
||||
mount: (target: HTMLDivElement | string) => void
|
||||
readonly option: NormalizedOption
|
||||
readonly isHide: boolean
|
||||
readonly isStop: boolean
|
||||
}
|
||||
|
||||
export interface Icons {
|
||||
$on: string
|
||||
$off: string
|
||||
$config: string
|
||||
$style: string
|
||||
$mode_0_off: string
|
||||
$mode_0_on: string
|
||||
$mode_1_off: string
|
||||
$mode_1_on: string
|
||||
$mode_2_off: string
|
||||
$mode_2_on: string
|
||||
$check_on: string
|
||||
$check_off: string
|
||||
}
|
||||
|
||||
export interface RuntimeFactory {
|
||||
(option: RuntimeOption): (art: Artplayer) => RuntimeResult
|
||||
icons: Icons
|
||||
}
|
||||
|
||||
/** Explicit payload map; importing /runtime does not augment historical Artplayer events. */
|
||||
export interface EventMap {
|
||||
'artplayerPluginDanmuku:points': [points: Point[]]
|
||||
'artplayerPluginDanmuku:loaded': [queue: Item[]]
|
||||
'artplayerPluginDanmuku:visible': [item: Item]
|
||||
'artplayerPluginDanmuku:config': [option: NormalizedOption]
|
||||
'artplayerPluginDanmuku:error': [error: unknown]
|
||||
'artplayerPluginDanmuku:show': []
|
||||
'artplayerPluginDanmuku:hide': []
|
||||
'artplayerPluginDanmuku:start': []
|
||||
'artplayerPluginDanmuku:stop': []
|
||||
'artplayerPluginDanmuku:reset': []
|
||||
'artplayerPluginDanmuku:destroy': []
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { RuntimeFactory } from './runtime-shared.js'
|
||||
|
||||
declare const artplayerPluginDanmuku: RuntimeFactory
|
||||
// eslint-disable-next-line ts/no-redeclare -- Ambient merge exposes types on the CommonJS factory.
|
||||
declare namespace artplayerPluginDanmuku {
|
||||
type Danmu = import('./runtime-shared.js').Danmu
|
||||
type EventMap = import('./runtime-shared.js').EventMap
|
||||
type Heatmap = import('./runtime-shared.js').Heatmap
|
||||
type Icons = import('./runtime-shared.js').Icons
|
||||
type Input = import('./runtime-shared.js').Input
|
||||
type Item = import('./runtime-shared.js').Item
|
||||
type Margin = import('./runtime-shared.js').Margin
|
||||
type Mode = import('./runtime-shared.js').Mode
|
||||
type NormalizedDanmu = import('./runtime-shared.js').NormalizedDanmu
|
||||
type NormalizedOption = import('./runtime-shared.js').NormalizedOption
|
||||
type Owner = import('./runtime-shared.js').Owner
|
||||
type Point = import('./runtime-shared.js').Point
|
||||
type RuntimeFactory = import('./runtime-shared.js').RuntimeFactory
|
||||
type RuntimeOption = import('./runtime-shared.js').RuntimeOption
|
||||
type RuntimeResult = import('./runtime-shared.js').RuntimeResult
|
||||
type Slider<Value = number> = import('./runtime-shared.js').Slider<Value>
|
||||
type SliderStep<Value = number> = import('./runtime-shared.js').SliderStep<Value>
|
||||
type State = import('./runtime-shared.js').State
|
||||
}
|
||||
export = artplayerPluginDanmuku
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { RuntimeFactory } from './runtime-shared.js'
|
||||
|
||||
declare const artplayerPluginDanmuku: RuntimeFactory
|
||||
export default artplayerPluginDanmuku
|
||||
export type { Danmu, EventMap, Heatmap, Icons, Input, Item, Margin, Mode, NormalizedDanmu, NormalizedOption, Owner, Point, RuntimeFactory, RuntimeOption, RuntimeResult, Slider, SliderStep, State } from './runtime-shared.js'
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { RuntimeFactory } from './runtime-shared.js'
|
||||
|
||||
declare const artplayerPluginDanmuku: RuntimeFactory
|
||||
// eslint-disable-next-line ts/no-redeclare -- Ambient merge exposes types on the CommonJS factory.
|
||||
declare namespace artplayerPluginDanmuku {
|
||||
type Danmu = import('./runtime-shared.js').Danmu
|
||||
type EventMap = import('./runtime-shared.js').EventMap
|
||||
type Heatmap = import('./runtime-shared.js').Heatmap
|
||||
type Icons = import('./runtime-shared.js').Icons
|
||||
type Input = import('./runtime-shared.js').Input
|
||||
type Item = import('./runtime-shared.js').Item
|
||||
type Margin = import('./runtime-shared.js').Margin
|
||||
type Mode = import('./runtime-shared.js').Mode
|
||||
type NormalizedDanmu = import('./runtime-shared.js').NormalizedDanmu
|
||||
type NormalizedOption = import('./runtime-shared.js').NormalizedOption
|
||||
type Owner = import('./runtime-shared.js').Owner
|
||||
type Point = import('./runtime-shared.js').Point
|
||||
type RuntimeFactory = import('./runtime-shared.js').RuntimeFactory
|
||||
type RuntimeOption = import('./runtime-shared.js').RuntimeOption
|
||||
type RuntimeResult = import('./runtime-shared.js').RuntimeResult
|
||||
type Slider<Value = number> = import('./runtime-shared.js').Slider<Value>
|
||||
type SliderStep<Value = number> = import('./runtime-shared.js').SliderStep<Value>
|
||||
type State = import('./runtime-shared.js').State
|
||||
}
|
||||
export = artplayerPluginDanmuku
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,99 @@
|
||||
# PKG-DANMUKU-06 自有 TypeScript 与兼容声明
|
||||
|
||||
本步将前面已拆分的20个自有运行模块迁移为严格TypeScript,明确输入、队列、
|
||||
Worker协议、UI与生命周期边界。任务完成状态以tasks.json为准。
|
||||
|
||||
## 实现与兼容边界
|
||||
|
||||
- 入口保持同步插件门面;Danmuku拥有配置与队列,scheduler负责时钟与有效操作,
|
||||
renderer负责节点,WorkerClient负责请求身份与终止,worker只计算轨道。
|
||||
input、bilibili及parser保持请求/解析/取消分层;设置和热图继续使用05/10的模块边界。
|
||||
- 新增内部types、worker-types、parser-types、setting-types及Worker资源声明。
|
||||
公开运行数据由types/runtime-shared.d.ts单一维护,内部通过type-only转发使用;
|
||||
DOM宿主与Worker协议留在src。不存在新的运行时模块环或消费者依赖。
|
||||
- 类字段使用declare,不改变原初始化顺序或新增undefined自有属性。保留同步数组
|
||||
加载前缀、Promise/owner返回身份、回调receiver、默认参数、事件与原DOM属性写入。
|
||||
取消符号明确区分异步结果;没有为了通过类型检查改变轨道算法。
|
||||
- 受控断言用于旧核心constructor声明、已初始化的设置节点、已分配的活动弹幕节点、
|
||||
DOM WebIDL的原始数值/null赋值,以及历史无效margin字符串回退。它们不替代输入
|
||||
验证,不引入全局any。旧postMessage空对象默认值仍保留,未知协议不产生回复。
|
||||
- 分包tsconfig禁用allowJs。ES2021.String仅描述既有parser.replaceAll,不新增该
|
||||
运行时要求;现代es2020与legacy es2015构建目标不变。本包没有自有遗留JS或vendor。
|
||||
模块地图、生命周期与维护指引见包内ARCHITECTURE.md。
|
||||
|
||||
## 公开类型与消费者
|
||||
|
||||
实际npm 5.3.0根声明完整保留,换行归一后SHA256为
|
||||
`006c3e2d22425b936d6d09bf5138b0ae13c4cfffd0a571d03722c17137dbe99c`。
|
||||
旧根/legacy的参数、返回提取和普通替代工厂关系不变;没有给工厂增加必填default。
|
||||
新增可选/runtime路径指向相同分发文件,只提供准确声明:
|
||||
|
||||
- 工厂参数对象仍必传,但对象内字段可选。保留弹幕扩展字段;输入可为数组、URL、
|
||||
Promise或返回数组/Promise的函数,config可以传部分字段。
|
||||
- emit/load返回Promise<Owner>,同步命令返回Owner,Owner与注册门面分别描述;
|
||||
门面的option/isHide/isStop只读getter不会使内部配置对象变为深只读。
|
||||
- filter接收填默认值后的数据,beforeVisible接收队列项;两者按truthy判断,
|
||||
beforeEmit严格比较true。回调this为实际NormalizedOption。
|
||||
- mount需要有效目标。icons描述真实可写属性;Slider与margin元组、heatmap坐标和
|
||||
配置保持实际格式。points配置目前只保存,自定义绘图由points事件触发,保留内层
|
||||
元组mutation。EventMap提供明确payload,不全局改写旧核心事件声明。
|
||||
- 旧根的同步emit结果和对象points属于历史声明错误,切换/runtime后需按真实行为
|
||||
使用await及元组;继续使用旧入口无需因本次迁移修改代码。实际运行并未被旧声明
|
||||
所描述的行为替换,具体示例见README及type-compatibility-policy.md。
|
||||
|
||||
四项类型测试覆盖真实声明归档、正反例、实现赋值和语义编辑器生成。安装测试在
|
||||
仓库外实际Yarn pack/install并离线冻结重装,逐成员核对字节,检查CJS/ESM身份。
|
||||
12个编译器/解析模式组合包括TS5.9.3与4.3.5;候选每个适用模式有4个root和14个
|
||||
runtime负例,逐个去掉expect-error确认相应错误实际出现。
|
||||
旧根在NodeNext ESM默认namespace方式的7个历史诊断与候选逐代码/行号比较,
|
||||
显式namespace适配通过;保留失败事实,不把它包装成全部旧TS用法均无错误。
|
||||
|
||||
build:ts把Danmuku纳入语义声明生成,更新docs编辑器资产;不手改生成声明。
|
||||
.npmignore排除实现tsconfig,公开类型仍随包分发。新增test:danmuku-types及
|
||||
test:danmuku-types-package脚本,前者被既有test:baseline自动发现。本步无新依赖,
|
||||
唯一yarn.lock不变,版本递增仍由统一版本任务执行。
|
||||
|
||||
## 测试与证据
|
||||
|
||||
最终机器证据见[验收报告](../baselines/danmuku-types-validation.json)。
|
||||
完整源码250项单测通过,正式main和legacy各164项联合测试通过;严格类型检查覆盖
|
||||
全仓390个生产TS文件。源码/main/legacy最终各156项真实浏览器通过,共468项,
|
||||
每轮2并发、零重试、零跳过;覆盖实际发布5.4.0与候选5.4.1核心以及Chromium、
|
||||
Firefox和Windows WebKit。正常构建、类型编辑器生成、目标lint、冻结安装及严格
|
||||
Node24.21.0/Yarn1.22.22工具链检查通过;构建产物与docs复制逐字节相同。
|
||||
源码单测、正式main/legacy联合测试包含共同内部模块与历史对照,不能把每一项都
|
||||
计为独立产物测试。Worker加载器会执行选中产物内嵌Blob或data URL的实际字节;
|
||||
只有源码受控模式才单独编译worker.ts。冻结的历史helper没有修改。
|
||||
|
||||
内置浏览器用正常yarn dev打开8082现有danmuku示例,本地视频和XML实际加载。
|
||||
Chrome/152.0.0.0,视频播放至11.711807秒且解码宽640;发送
|
||||
`TS migration browser check`后输入清空;暂停在43.744183秒,再次读取保持同值。
|
||||
真实弹幕可见,页面warn/error记录为空。原生Browser.getVersion不支持,改用受支持
|
||||
CDP只读navigator.userAgent取版本;之前失效tab的CDP焦点超时不作为页面失败。
|
||||
这只是开发示例smoke,不替代发布包、Safari真机或07的持续负载验收。
|
||||
|
||||
## 失败处置与未完成范围
|
||||
|
||||
- 第一版实际pack发现实现tsconfig泄漏,修正.npmignore后重做完整隔离安装。
|
||||
- 实现赋值fixture最初放在根test/types,导致根配置缺少分包Worker资源和既有
|
||||
ES2021.String声明;移至refactor/fixtures/implementation,并用分包配置检查。
|
||||
负例诊断行计算也修正了跨空白行匹配;保留失败日志,不弱化类型断言。
|
||||
- 最终独立复查更正架构文档仍指向旧位置的实现fixture,并对最终包内容重新执行
|
||||
隔离打包安装;不使用修正文档前的归档作为最终候选证据。
|
||||
- 产物Worker测试首次只识别Blob,实际正式构建使用百分号编码data URL。增加精确
|
||||
data URL解码后执行内嵌算法,保持未知来源断言,不用当前源码冒充产物。
|
||||
- 一轮浏览器复测提前启动下一轮,共享结果目录被覆盖,导致最后两项trace清理失败;
|
||||
下一轮同时遇到端口占用。保留日志,改为一个串行命令,上一进程结束后归档再继续。
|
||||
- 随后source矩阵154通过、2个WebKit uniform场景超时。trace显示published首次
|
||||
Play客户端13.17秒而浏览器动作仅0.39秒;candidate启动/建页/导航合计16.23秒,
|
||||
都在插件安装前。16k加载分别0.995秒与2.96秒;published超时后几何断言实际通过,
|
||||
但整项仍记失败。证据支持宿主或自动化延迟,不能确认更具体的系统原因。
|
||||
其他重型检查结束后,原断言/20秒超时不变,单worker WebKit诊断6项通过;完整
|
||||
最终矩阵单独归档,不关闭历史负载疑点,也不以重跑抹去首次失败。
|
||||
|
||||
07继续承担持续负载、渲染/资源对比及Mask稳定依赖,08承担完整组合,09承担分发
|
||||
与文档最终验收。DANMUKU-LOAD-01仍保持待验证;本步不声称npm候选或全部重构完成。
|
||||
三轮发布复盘、设备/外部SDK证据、全包独立major以及远端CI仍按原计划。
|
||||
|
||||
回退:单独revert本任务提交,恢复05/10的JS实现及原类型分发;#958独立修复不回退。
|
||||
只提交本任务实现、测试、文档和状态,不推送或发布。
|
||||
@@ -0,0 +1,27 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { Danmu, Option, Result } from 'artplayer-plugin-danmuku'
|
||||
import danmuku from 'artplayer-plugin-danmuku'
|
||||
|
||||
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false
|
||||
type Assert<T extends true> = T
|
||||
type Factory = (option: Option) => (art: Artplayer) => Result
|
||||
type SameArguments = Assert<Equal<Parameters<typeof danmuku>, [option: Option]>>
|
||||
type SameResult = Assert<Equal<ReturnType<ReturnType<typeof danmuku>>, Result>>
|
||||
type SameEmit = Assert<Equal<ReturnType<Result['emit']>, Result>>
|
||||
type SameLoad = Assert<Equal<ReturnType<Result['load']>, Promise<Result>>>
|
||||
type SamePoints = Assert<Equal<Option['points'], { time: number, value: number }[] | undefined>>
|
||||
type SameDanmuKeys = Assert<Equal<keyof Danmu, 'text' | 'mode' | 'color' | 'time' | 'border' | 'style'>>
|
||||
|
||||
declare const art: Artplayer
|
||||
declare const result: Result
|
||||
const replacement: Factory = (_option: Option) => (_art: Artplayer) => result
|
||||
const toOld: Factory = danmuku
|
||||
const fromOld: typeof danmuku = replacement
|
||||
const option: Option = { danmuku: [], points: [{ time: 1, value: 2 }], margin: [0, '25%'], beforeEmit: async () => true }
|
||||
const plugin: Result = danmuku(option)(art)
|
||||
const synchronousOldEmit: Result = plugin.emit({ text: 'old declaration', mode: 1 })
|
||||
const loaded: Promise<Result> = plugin.load(Promise.resolve([{ text: 'reload' }]))
|
||||
const configured: Result = plugin.config(option)
|
||||
const mounted: void = plugin.mount('#external')
|
||||
void [toOld, fromOld, synchronousOldEmit, loaded, configured, mounted]
|
||||
export type { SameArguments, SameDanmuKeys, SameEmit, SameLoad, SamePoints, SameResult }
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { RuntimeFactory } from 'artplayer-plugin-danmuku/runtime'
|
||||
import factory from '../../../packages/artplayer-plugin-danmuku/src'
|
||||
|
||||
// Compile with the package configuration: the implementation imports Worker assets.
|
||||
const implementation: RuntimeFactory = factory
|
||||
export { implementation }
|
||||
+4
-3
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 229 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 69 / doing 15 / blocked 0 / done 145 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 68 / doing 15 / blocked 0 / done 146 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -314,8 +314,8 @@
|
||||
| PKG-DANMUKU-03 | artplayer-plugin-danmuku<br>整理加载、解析与配置 | PKG-DANMUKU-02, CORE-08 | bilibili/input/parser/config 的分层与取消 | 旧格式、回调、过滤、追加/替换语义保持 | M | done |
|
||||
| PKG-DANMUKU-04 | artplayer-plugin-danmuku<br>整理时钟、队列与轨道调度 | PKG-DANMUKU-03, CORE-10 | danmuku 调度器和确定性时钟测试 | seek/暂停/倍率/长时间运行无顺序和碰撞回归 | H | done |
|
||||
| PKG-DANMUKU-05 | artplayer-plugin-danmuku<br>整理 DOM 渲染、设置、热力图与 worker | PKG-DANMUKU-04, CORE-14, CORE-18 | renderer/setting/heatmap/worker 职责及资源归属 | mount/icons/设置和 worker 协议保持,销毁无后台工作 | H | done |
|
||||
| PKG-DANMUKU-06 | artplayer-plugin-danmuku<br>迁移 TS 与公开声明 | PKG-DANMUKU-05, ENG-04, ENG-06 | 自有模块、worker 消息、option/item/result 类型 | 旧参数、扩展字段和事件消费通过,vendored 边界清楚 | H | doing |
|
||||
| PKG-DANMUKU-07 | artplayer-plugin-danmuku<br>弹幕能力稳定性验收 | PKG-DANMUKU-06, PKG-DANMUKU-10 | 负载/渲染/资源对比及 mask 可依赖的稳定边界 | 在试点核心中旧插件 API 与关键帧路径通过 | H | todo |
|
||||
| PKG-DANMUKU-06 | artplayer-plugin-danmuku<br>迁移 TS 与公开声明 | PKG-DANMUKU-05, ENG-04, ENG-06 | 自有模块、worker 消息、option/item/result 类型 | 旧参数、扩展字段和事件消费通过,vendored 边界清楚 | H | done |
|
||||
| PKG-DANMUKU-07 | artplayer-plugin-danmuku<br>弹幕能力稳定性验收 | PKG-DANMUKU-06, PKG-DANMUKU-10 | 负载/渲染/资源对比及 mask 可依赖的稳定边界 | 在试点核心中旧插件 API 与关键帧路径通过 | H | doing |
|
||||
| PKG-DANMUKU-08 | artplayer-plugin-danmuku<br>完成新旧核心与组合验收 | PKG-DANMUKU-07, CORE-22 | 最终核心/旧核心、mask/fullscreen/PiP 组合报告 | 性能无未解释退化,反复装卸无累计资源 | H | todo |
|
||||
| PKG-DANMUKU-09 | artplayer-plugin-danmuku<br>完成分发、示例与文档 | PKG-DANMUKU-08, ENG-07 | danmuku.js、README、声明、worker 与产物 | tarball 和静态 icons 等旧调用通过,有独立回退版本 | H | todo |
|
||||
| PKG-DANMUKU-10 | artplayer-plugin-danmuku<br>修复 issue958 密集热力图过高与曲线裁平 | PKG-DANMUKU-05 | 自动密度缩放与曲线边界、历史自定义坐标兼容及真实浏览器红绿证据 | 16000条三种分布在两核心三引擎和source/main/legacy不遮挡;显式轴和自定义points保持旧解释,有单独commit | M | done |
|
||||
@@ -559,6 +559,7 @@
|
||||
- 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-DANMUKU-05: [记录](changes/2026-09-13-PKG-DANMUKU-05-resources.md) [记录](baselines/danmuku-resources-validation.json)
|
||||
- PKG-DANMUKU-06: [记录](changes/2026-09-13-PKG-DANMUKU-06-types.md) [记录](baselines/danmuku-types-validation.json)
|
||||
- PKG-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)
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-DANMUKU-06 自有 TS 与公开声明完成
|
||||
|
||||
20个自有运行模块完成严格TS,新增内部状态/Worker/parser/UI类型边界;公开运行
|
||||
数据单一维护。实际npm5.3.0根声明完整保留,新增同实现/runtime准确描述Promise、
|
||||
owner、getter、回调和数据;语义docs编辑器声明同步生成。包架构和维护命令已更新。
|
||||
完整源码250项、main/legacy各164项联合单测、4项类型检查、12组仓库外安装消费者
|
||||
通过;全仓390生产TS严格检查、冻结Yarn安装、工具链、构建和目标lint通过。
|
||||
三格式/两核心/三引擎最终468项浏览器通过,原断言/超时不变、无重试/跳过;内置
|
||||
Chrome152完成现有docs示例实际播放、发送与暂停。Worker data URL测试加载器、
|
||||
先前报告目录冲突和WebKit超时均保留原因与失败证据,未把重跑等同历史从未失败。
|
||||
关闭DANMUKU-TYPE-01/02;LOAD-01与稳定性/组合/分发及发布复盘继续保留。
|
||||
见[变更](changes/2026-09-13-PKG-DANMUKU-06-types.md)和[机器证据](baselines/danmuku-types-validation.json)。
|
||||
229项:146 done、15 doing、68 todo;07进入负载、渲染与资源稳定性验收。
|
||||
本地独立任务提交,不推送或发布。
|
||||
|
||||
## PKG-DANMUKU-10:issue #958 密集热力图修复
|
||||
|
||||
用户新增的16000条弹幕热图过高/裁平已独立复现并修复。自动密度拟合到底部四分之一,
|
||||
|
||||
@@ -227,11 +227,11 @@
|
||||
| MASK-BACKEND-01 | open / 已复现 | Mask TensorFlow backend fallback does not establish MediaPipe fallback and failed initialization keeps scheduling | PKG-MASK-02, PKG-MASK-03, PKG-MASK-05 |
|
||||
| MASK-DOM-01 | open / 已复现 | Mask registration and frame processing assume Danmuku DOM, readable frames and a 2D context | PKG-MASK-02, PKG-MASK-03, PKG-MASK-05 |
|
||||
| MASK-NOTICE-01 | open / 源码/产物事实 | Published Mask bundles lack a separate third-party notice despite bundled SDK code | PKG-MASK-06 |
|
||||
| DANMUKU-TYPE-01 | open / 已复现 | Danmuku historical declared results, optional mount and initial Promise input disagree with runtime | PKG-DANMUKU-03, PKG-DANMUKU-06 |
|
||||
| DANMUKU-TYPE-01 | resolved / 已复现 | Danmuku historical declared results, optional mount and initial Promise input disagree with runtime | PKG-DANMUKU-03, PKG-DANMUKU-06 |
|
||||
| DANMUKU-INPUT-01 | resolved / 已复现 | Danmuku replaces time zero with current time plus 0.5 and mutates supplied input objects | PKG-DANMUKU-02, PKG-DANMUKU-03 |
|
||||
| DANMUKU-CONFIG-01 | resolved / 已复现 | Danmuku JSON configuration comparison ignores a replacement filter function | PKG-DANMUKU-02, PKG-DANMUKU-03 |
|
||||
| DANMUKU-CLEANUP-01 | resolved / 已复现 | 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-TYPE-02 | resolved / 源码/产物事实 | Danmuku id, icons, Slider steps, heatmap points and partial configuration drift from declarations | PKG-DANMUKU-05, PKG-DANMUKU-06 |
|
||||
| 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 | resolved / 已复现 | 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 |
|
||||
|
||||
+20
-6
@@ -5023,7 +5023,7 @@
|
||||
"id": "DANMUKU-TYPE-01",
|
||||
"title": "Danmuku historical declared results, optional mount and initial Promise input disagree with runtime",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-DANMUKU-03",
|
||||
"PKG-DANMUKU-06"
|
||||
@@ -5033,10 +5033,17 @@
|
||||
"refactor/baselines/danmuku-contract-validation.json",
|
||||
"refactor/baselines/danmuku-release.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-06-types.md",
|
||||
"refactor/baselines/danmuku-types-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Preserve latest published complete root type contracts and actual legal JS return identities; expose accurate types separately.",
|
||||
"closureCriteria": "Installed consumers verify latest root assignments and precise async/getter/mount contracts; initial Promise mismatch has an explicit tested resolution."
|
||||
"closureCriteria": "Installed consumers verify latest root assignments and precise async/getter/mount contracts; initial Promise mismatch has an explicit tested resolution.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-13-PKG-DANMUKU-06-types.md",
|
||||
"refactor/baselines/danmuku-types-validation.json"
|
||||
],
|
||||
"resolutionRationale": "Preserved actual npm 5.3.0 complete root/legacy declaration and installed consumer behavior; optional runtime entry describes tested async owner, getter, mount, data, icon and callback contracts. Verified actual implementation assignability and packed/offline-installed current and historical compiler consumers."
|
||||
},
|
||||
{
|
||||
"id": "DANMUKU-INPUT-01",
|
||||
@@ -5127,7 +5134,7 @@
|
||||
"id": "DANMUKU-TYPE-02",
|
||||
"title": "Danmuku id, icons, Slider steps, heatmap points and partial configuration drift from declarations",
|
||||
"confirmation": "source-observed",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-DANMUKU-05",
|
||||
"PKG-DANMUKU-06"
|
||||
@@ -5135,10 +5142,17 @@
|
||||
"evidence": [
|
||||
"refactor/baselines/danmuku-contract.md",
|
||||
"refactor/baselines/danmuku-contract-validation.json",
|
||||
"refactor/baselines/danmuku-release.json"
|
||||
"refactor/baselines/danmuku-release.json",
|
||||
"refactor/changes/2026-09-13-PKG-DANMUKU-06-types.md",
|
||||
"refactor/baselines/danmuku-types-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Validate actual accepted data and static properties, retain root compatibility and document effective fields.",
|
||||
"closureCriteria": "Positive and negative installed consumers and source behavior tests cover these fields without widening away old factory contracts."
|
||||
"closureCriteria": "Positive and negative installed consumers and source behavior tests cover these fields without widening away old factory contracts.",
|
||||
"resolutionEvidence": [
|
||||
"refactor/changes/2026-09-13-PKG-DANMUKU-06-types.md",
|
||||
"refactor/baselines/danmuku-types-validation.json"
|
||||
],
|
||||
"resolutionRationale": "Preserved actual npm 5.3.0 complete root/legacy declaration and installed consumer behavior; optional runtime entry describes tested async owner, getter, mount, data, icon and callback contracts. Verified actual implementation assignability and packed/offline-installed current and historical compiler consumers."
|
||||
},
|
||||
{
|
||||
"id": "DANMUKU-ASYNC-01",
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import ts from 'typescript'
|
||||
import compat from 'typescript-compat'
|
||||
import { checkFiles, packedFiles } from '../../scripts/package-check.mjs'
|
||||
import { consumerDirectory, removeConsumer, run, workspace, writeJson } from '../../scripts/package-consumer.mjs'
|
||||
import { verifyDanmukuContract } from './danmuku-contract.mjs'
|
||||
import { hash, readMember } from './releases.mjs'
|
||||
|
||||
const publishedFixture = 'refactor/fixtures/consumers/danmuku-published.ts'
|
||||
const rootFixture = 'test/types/danmuku-root.ts'
|
||||
const runtimeFixture = 'test/types/danmuku-runtime.ts'
|
||||
|
||||
async function main() {
|
||||
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Use yarn test:danmuku-types-package')
|
||||
const yarn = process.env.npm_execpath
|
||||
assert(yarn && fs.existsSync(yarn))
|
||||
const output = fs.mkdtempSync(path.join(workspace, 'refactor/.cache/danmuku-package-types-'))
|
||||
console.log(`Danmuku isolated package evidence: ${output}`)
|
||||
const contract = await verifyDanmukuContract()
|
||||
assert.equal(contract.baseline.release.version, '5.3.0')
|
||||
const fixtures = Object.fromEntries([publishedFixture, rootFixture, runtimeFixture].map(file => [file, fs.readFileSync(path.join(workspace, file), 'utf8')]))
|
||||
const packages = []
|
||||
for (const name of ['artplayer', 'artplayer-plugin-danmuku']) {
|
||||
const archive = path.join(output, `${name}.tgz`)
|
||||
fs.writeFileSync(path.join(output, `${name}-pack.log`), run([yarn, 'pack', '--filename', archive], path.join(workspace, 'packages', name)))
|
||||
const manifest = JSON.parse(readMember(archive, 'package/package.json'))
|
||||
const files = packedFiles(archive)
|
||||
checkFiles(manifest, files, name === contract.baseline.release.name ? Object.keys(contract.baseline.release.files) : [])
|
||||
packages.push({ name, archive, sha256: hash(fs.readFileSync(archive)), files: Object.fromEntries(files.map(member => [member, hash(readMember(archive, member))])) })
|
||||
}
|
||||
const published = { ...contract.baseline.release, archive: contract.archives.get('5.3.0'), label: 'published-5.3.0' }
|
||||
const declaration = 'package/types/artplayer-plugin-danmuku.d.ts'
|
||||
assert.equal(readMember(packages[1].archive, declaration).toString().replaceAll('\r\n', '\n'), readMember(published.archive, declaration).toString().replaceAll('\r\n', '\n'), 'Preserve the actual npm 5.3.0 root declaration')
|
||||
const matrix = []
|
||||
const historical = new Map()
|
||||
for (const plugin of [published, { ...packages[1], label: 'candidate' }]) {
|
||||
const consumer = consumerDirectory()
|
||||
try {
|
||||
writeJson(path.join(consumer, 'package.json'), { name: 'danmuku-isolated-consumer', private: true, dependencies: Object.fromEntries([packages[0], plugin].map(pkg => [pkg.name, `file:${pkg.archive.replaceAll('\\', '/')}`])) })
|
||||
fs.copyFileSync(path.join(workspace, 'yarn.lock'), path.join(consumer, 'yarn.lock'))
|
||||
fs.writeFileSync(path.join(output, `${plugin.label}-install.log`), run([yarn, 'install', '--offline', '--ignore-scripts', '--non-interactive'], consumer))
|
||||
const lock = fs.readFileSync(path.join(consumer, 'yarn.lock'))
|
||||
fs.writeFileSync(path.join(output, `${plugin.label}-frozen-install.log`), run([yarn, 'install', '--offline', '--frozen-lockfile', '--ignore-scripts', '--non-interactive'], consumer))
|
||||
assert.deepEqual(fs.readFileSync(path.join(consumer, 'yarn.lock')), lock)
|
||||
fs.writeFileSync(path.join(output, `${plugin.label}-yarn.lock`), lock)
|
||||
for (const pkg of [packages[0], plugin]) {
|
||||
const root = path.join(consumer, 'node_modules', pkg.name)
|
||||
assert.equal(fs.realpathSync(root), root, 'Installed package must not be a workspace link')
|
||||
for (const [member, expected] of Object.entries(pkg.files))
|
||||
assert.equal(hash(fs.readFileSync(path.join(root, member.slice(8)))), expected, `Installed bytes differ: ${member}`)
|
||||
}
|
||||
const runtimeFile = path.join(consumer, 'exports.cjs')
|
||||
fs.writeFileSync(runtimeFile, `const assert = require('node:assert/strict');
|
||||
const names = ${JSON.stringify(plugin.label === 'candidate' ? ['artplayer-plugin-danmuku', 'artplayer-plugin-danmuku/legacy', 'artplayer-plugin-danmuku/runtime'] : ['artplayer-plugin-danmuku', 'artplayer-plugin-danmuku/legacy'])};
|
||||
for (const name of names) { const factory = require(name); assert.equal(typeof factory, 'function'); assert.equal(typeof factory({danmuku: []}), 'function'); assert.equal(typeof factory.icons, 'object'); }
|
||||
${plugin.label === 'candidate' ? 'assert.equal(require(\'artplayer-plugin-danmuku/runtime\'), require(\'artplayer-plugin-danmuku\'));' : ''}
|
||||
Promise.all(names.map(name => import(name))).then(modules => { for (const value of modules) assert.equal(typeof value.default, 'function'); assert.equal(modules[1].default, require(names[1])); ${plugin.label === 'candidate' ? 'assert.equal(modules[2].default, modules[0].default);' : ''} }).catch(error => { console.error(error); process.exitCode = 1; });`)
|
||||
fs.writeFileSync(path.join(output, `${plugin.label}-runtime.log`), run([runtimeFile], consumer))
|
||||
const modes = [[ts, 'node10-commonjs'], [compat, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm']]
|
||||
if (plugin.label === 'candidate')
|
||||
modes.push([ts, 'node10-commonjs-no-interop'], [compat, 'node10-commonjs-no-interop'])
|
||||
for (const [compiler, mode] of modes) {
|
||||
const next = mode.startsWith('nodenext')
|
||||
const filename = path.join(consumer, `consumer.${next ? mode.endsWith('-cjs') ? 'cts' : 'mts' : 'ts'}`)
|
||||
const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: !mode.endsWith('-no-interop'), target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'], module: next ? compiler.ModuleKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleKind.ESNext : compiler.ModuleKind.CommonJS, moduleResolution: next ? compiler.ModuleResolutionKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleResolutionKind.Bundler : compiler.ModuleResolutionKind.NodeJs }
|
||||
function compile(source) {
|
||||
fs.writeFileSync(filename, source)
|
||||
const program = compiler.createProgram([filename], options)
|
||||
for (const file of program.getSourceFiles()) {
|
||||
const actual = fs.realpathSync(file.fileName)
|
||||
assert(actual.startsWith(fs.realpathSync(consumer) + path.sep) || (program.isSourceFileDefaultLibrary(file) && path.dirname(actual) === fs.realpathSync(path.dirname(compiler.sys.getExecutingFilePath()))), `Types escaped installed consumer: ${actual}`)
|
||||
}
|
||||
return compiler.getPreEmitDiagnostics(program).map(item => ({ code: item.code, line: item.file && item.start !== undefined ? item.file.getLineAndCharacterOfPosition(item.start).line + 1 : null, message: compiler.flattenDiagnosticMessageText(item.messageText, '\n').replaceAll(consumer.replaceAll('\\', '/'), '<consumer>') }))
|
||||
}
|
||||
function checkedFixture(source, label) {
|
||||
const diagnostics = compile(source)
|
||||
assert.deepEqual(diagnostics, [], `${label}: ${compiler.version} ${mode}`)
|
||||
const lines = []
|
||||
let line = 1
|
||||
for (const text of source.split('\n')) {
|
||||
if (/^[ \t]*\/\/ @ts-expect-error/.test(text))
|
||||
lines.push(line)
|
||||
else line++
|
||||
}
|
||||
assert.equal(lines.length, label === 'root/legacy' ? 4 : 14, `Keep all negative ${label} fixtures`)
|
||||
const invalid = compile(source.replaceAll(/^[ \t]*\/\/ @ts-expect-error[^\n]*\n/gm, ''))
|
||||
assert.deepEqual([...new Set(invalid.map(item => item.line))].sort((a, b) => a - b), lines, `Every ${label} negative statement must fail at its own line`)
|
||||
return { diagnostics, invalid, negativeStatements: lines.length }
|
||||
}
|
||||
const direct = compile(fixtures[publishedFixture])
|
||||
const key = `${compiler.version}:${mode}`
|
||||
if (plugin.label === 'published-5.3.0') {
|
||||
if (mode === 'nodenext-esm') {
|
||||
assert(direct.length > 0, 'Preserve evidence of the existing NodeNext default namespace mismatch')
|
||||
assert(direct.every(item => [2322, 2344, 2349].includes(item.code)), 'Only the established callable namespace mismatch is expected')
|
||||
}
|
||||
else {
|
||||
assert.deepEqual(direct, [])
|
||||
}
|
||||
historical.set(key, direct.map(({ code, line }) => ({ code, line })))
|
||||
}
|
||||
else {
|
||||
assert.deepEqual(direct.map(({ code, line }) => ({ code, line })), historical.get(key) ?? [], 'Candidate must preserve exact npm root diagnostic codes and positions')
|
||||
}
|
||||
const namespace = mode === 'nodenext-esm' ? compile(fixtures[publishedFixture].replace('import danmuku from \'artplayer-plugin-danmuku\'', 'import danmukuModule from \'artplayer-plugin-danmuku\'\nconst danmuku = danmukuModule.default')) : []
|
||||
assert.deepEqual(namespace, [], 'Explicit published NodeNext namespace remains valid')
|
||||
let root = null
|
||||
let runtime = null
|
||||
let commonjs = []
|
||||
if (plugin.label === 'candidate') {
|
||||
const rootSource = mode === 'nodenext-esm'
|
||||
? fixtures[rootFixture].replace('import danmuku from \'artplayer-plugin-danmuku\'', 'import danmukuModule from \'artplayer-plugin-danmuku\'\nconst danmuku = danmukuModule.default').replace('import legacy from \'artplayer-plugin-danmuku/legacy\'', 'import legacyModule from \'artplayer-plugin-danmuku/legacy\'\nconst legacy = legacyModule.default')
|
||||
: fixtures[rootFixture]
|
||||
root = checkedFixture(rootSource, 'root/legacy')
|
||||
const runtimeSource = mode.endsWith('-no-interop') ? fixtures[runtimeFixture].replace('import runtime from \'artplayer-plugin-danmuku/runtime\'', 'import runtime = require(\'artplayer-plugin-danmuku/runtime\')') : fixtures[runtimeFixture]
|
||||
runtime = checkedFixture(runtimeSource, 'runtime')
|
||||
if (mode !== 'nodenext-esm' && mode !== 'bundler-esm') {
|
||||
commonjs = compile(`import root = require('artplayer-plugin-danmuku'); import legacy = require('artplayer-plugin-danmuku/legacy'); import runtime = require('artplayer-plugin-danmuku/runtime');\nconst replacement: typeof root.default = (_option) => (_art) => { throw new Error('consumer'); }; const other: typeof legacy.default = replacement; root.default({danmuku: []}); legacy.default({danmuku: []}); runtime({danmuku: []}); void other;`)
|
||||
assert.deepEqual(commonjs, [], 'Installed require declarations preserve old factories and callable runtime')
|
||||
}
|
||||
}
|
||||
matrix.push({ plugin: plugin.label, compiler: compiler.version, mode, direct, namespace, root, runtime, commonjs })
|
||||
writeJson(path.join(output, 'matrix-progress.json'), matrix)
|
||||
}
|
||||
}
|
||||
finally {
|
||||
removeConsumer(consumer)
|
||||
}
|
||||
}
|
||||
writeJson(path.join(output, 'report.json'), { suite: 'danmuku-isolated-package-types', introducedBy: 'PKG-DANMUKU-06', scope: 'Actual npm 5.3.0 and packed candidate outside workspace; offline and frozen reinstall, exact installed bytes, unchanged root declaration, full factory replacement and extraction. Old synchronous emit and object points remain root typing only. Runtime types are checked separately. NodeNext root namespace failures are compared by exact codes and line positions. This is not real playback or complete publication acceptance.', fixtures: Object.fromEntries(Object.entries(fixtures).map(([file, source]) => [file, hash(source)])), packages, published: { archive: published.archive, sha256: published.sha256 }, matrix })
|
||||
console.log(`Danmuku installed types passed: ${matrix.length} cases; ${output}`)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Public declaration compatibility uses Node.
|
||||
import test from 'node:test'
|
||||
import ts from 'typescript'
|
||||
import compat from 'typescript-compat'
|
||||
import { generateCoreEditorDeclaration } from '../../scripts/editor-types.mjs'
|
||||
import { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from '../../scripts/plugin-editor-types.mjs'
|
||||
import { checkConsumer } from '../../scripts/typecheck.mjs'
|
||||
import { ensureArchive, readMember } from './releases.mjs'
|
||||
|
||||
const modes = [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]
|
||||
const read = file => fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n')
|
||||
const declaration = 'packages/artplayer-plugin-danmuku/types/artplayer-plugin-danmuku.d.ts'
|
||||
|
||||
test('Danmuku root declaration retains the exact latest npm 5.3.0 type source', async () => {
|
||||
const { release } = JSON.parse(read('refactor/baselines/danmuku-release.json'))
|
||||
const archive = await ensureArchive(release)
|
||||
assert.equal(read(declaration), readMember(archive, 'package/types/artplayer-plugin-danmuku.d.ts').toString().replaceAll('\r\n', '\n'))
|
||||
})
|
||||
|
||||
test('Danmuku root and runtime consumers retain extraction, ownership and meaningful negative checks', () => {
|
||||
for (const [compiler, mode] of modes) {
|
||||
for (const fixture of ['danmuku-root', 'danmuku-runtime']) {
|
||||
let source = read(`test/types/${fixture}.ts`)
|
||||
if (fixture === 'danmuku-root' && mode === 'nodenext-esm') {
|
||||
source = source.replace('import danmuku from \'artplayer-plugin-danmuku\'', 'import danmukuModule from \'artplayer-plugin-danmuku\'\nconst danmuku = danmukuModule.default')
|
||||
.replace('import legacy from \'artplayer-plugin-danmuku/legacy\'', 'import legacyModule from \'artplayer-plugin-danmuku/legacy\'\nconst legacy = legacyModule.default')
|
||||
}
|
||||
assert.deepEqual(checkConsumer(compiler, mode, source), [], `${fixture}: ${compiler.version}/${mode}`)
|
||||
const expected = []
|
||||
let line = 1
|
||||
for (const text of source.split('\n')) {
|
||||
if (/^\s*\/\/ @ts-expect-error/u.test(text))
|
||||
expected.push(line)
|
||||
else line++
|
||||
}
|
||||
assert.equal(expected.length, fixture === 'danmuku-root' ? 4 : 14)
|
||||
const invalid = checkConsumer(compiler, mode, source.replaceAll(/^[\t ]*\/\/ @ts-expect-error[^\n]*\n/gmu, ''))
|
||||
assert.deepEqual(invalid.map(item => item.line).sort((a, b) => a - b), expected, 'Every invalid consumer statement must fail at its own line')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('Danmuku runtime declarations are assignable from the real strict TypeScript implementation', () => {
|
||||
const filename = path.resolve('packages/artplayer-plugin-danmuku/tsconfig.json')
|
||||
const config = ts.readConfigFile(filename, ts.sys.readFile)
|
||||
assert.equal(config.error, undefined)
|
||||
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, path.dirname(filename))
|
||||
const program = ts.createProgram([...parsed.fileNames, path.resolve('refactor/fixtures/implementation/danmuku.ts')], parsed.options)
|
||||
assert.deepEqual(ts.getPreEmitDiagnostics(program).map(item => ts.flattenDiagnosticMessageText(item.messageText, '\n')), [])
|
||||
})
|
||||
|
||||
test('Danmuku editor declarations retain the callable global and exported historical names', () => {
|
||||
const generated = generatePluginEditorDeclaration(read(declaration), 'artplayerPluginDanmuku')
|
||||
const core = generateCoreEditorDeclaration()
|
||||
const consumer = `const option: artplayerPluginDanmuku.Option = {danmuku: [], points: [{time: 1, value: 2}]};
|
||||
const art = new Artplayer({container: '#player', url: '/video.mp4'});
|
||||
const result: artplayerPluginDanmuku.Result = artplayerPluginDanmuku(option)(art);
|
||||
const oldReturn: artplayerPluginDanmuku.Result = result.emit({text: 'legacy editor'}); void oldReturn;`
|
||||
for (const compiler of [ts, compat])
|
||||
assert.deepEqual(checkPluginEditorDeclaration(generated, core, consumer, compiler), [])
|
||||
})
|
||||
+6
-3
@@ -3362,11 +3362,14 @@
|
||||
"ENG-04",
|
||||
"ENG-06"
|
||||
],
|
||||
"status": "doing",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "自有模块、worker 消息、option/item/result 类型",
|
||||
"acceptance": "旧参数、扩展字段和事件消费通过,vendored 边界清楚",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-13-PKG-DANMUKU-06-types.md",
|
||||
"baselines/danmuku-types-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-DANMUKU-07",
|
||||
@@ -3379,7 +3382,7 @@
|
||||
"PKG-DANMUKU-06",
|
||||
"PKG-DANMUKU-10"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "doing",
|
||||
"risk": "H",
|
||||
"deliverable": "负载/渲染/资源对比及 mask 可依赖的稳定边界",
|
||||
"acceptance": "在试点核心中旧插件 API 与关键帧路径通过",
|
||||
|
||||
@@ -28,3 +28,10 @@ Chromecast的PKG-CAST-04沿用此规则保留最新npm 1.1.0根声明,不引
|
||||
破坏。1.0.0的export=与1.1.0的default export已有历史差异,分别保留测试证据;
|
||||
准确异步注册、回调和查询类型通过新增/runtime提供。此处记录规则的应用,
|
||||
不声称用户另行确认过Cast专属问题,也不扩大到未验证的运行时变化。
|
||||
|
||||
Danmuku的PKG-DANMUKU-06保留实际npm 5.3.0根声明的完整类型形状与字节(忽略
|
||||
换行格式),包括历史同步emit结果、必填danmuku、对象形points和NodeNext ESM的
|
||||
既有namespace限制。新增/runtime描述真实Promise/owner结果、可选配置字段、tuple
|
||||
points、静态icons和回调receiver;根/legacy/runtime复用原分发实现,不添加不存在
|
||||
的factory.default。旧入口用户不需要因本次类型迁移修改代码。切换/runtime是可选
|
||||
的准确类型入口,历史声明与运行时的区别见包README;这里没有新增旧类型破坏授权。
|
||||
+1
-1
@@ -53,7 +53,7 @@ console.log(`✨ Built ${artplayerTSoutput}`);
|
||||
const type = pluginsTS[index]
|
||||
const { name, file } = parsePluginInfo(type)
|
||||
const source = String(fs.readFileSync(type))
|
||||
const semanticPlugin = ['artplayerPluginChromecast', 'artplayerPluginAsr', 'artplayerPluginMultipleSubtitles', 'artplayerPluginVttThumbnail', 'artplayerPluginHlsControl', 'artplayerPluginAudioTrack', 'artplayerPluginDashControl', 'artplayerPluginAds', 'artplayerPluginAmbilight', 'artplayerProxyCanvas', 'artplayerProxyMediabunny', 'artplayerPluginDocumentPip', 'ArtplayerToolIframe', 'ArtplayerToolThumbnail'].includes(name)
|
||||
const semanticPlugin = ['artplayerPluginDanmuku', 'artplayerPluginChromecast', 'artplayerPluginAsr', 'artplayerPluginMultipleSubtitles', 'artplayerPluginVttThumbnail', 'artplayerPluginHlsControl', 'artplayerPluginAudioTrack', 'artplayerPluginDashControl', 'artplayerPluginAds', 'artplayerPluginAmbilight', 'artplayerProxyCanvas', 'artplayerProxyMediabunny', 'artplayerPluginDocumentPip', 'ArtplayerToolIframe', 'ArtplayerToolThumbnail'].includes(name)
|
||||
const localTypes = name === 'artplayerProxyMediabunny' ? { './media': fs.readFileSync(path.join(path.dirname(type), 'media.d.ts'), 'utf8') } : {}
|
||||
const code = semanticPlugin
|
||||
? generatePluginEditorDeclaration(source, name, localTypes)
|
||||
|
||||
@@ -70,3 +70,15 @@ volume/rate, independent instances and cleanup. Published-only tests retain defe
|
||||
See [audio validation](../refactor/audio-validation.md) for actual media tests and limitations.
|
||||
|
||||
Document PiP: `yarn test:dpip` runs 48 historical window/DOM/lifecycle cases. Native DOM iframe checks use `yarn test:browser test/browser/dpip.spec.js`; controlled window APIs do not establish native Document PiP support. See [validation notes](../refactor/dpip-validation.md).
|
||||
|
||||
Danmuku: `yarn test:danmuku` covers historical behavior and current input, scheduling,
|
||||
settings, rendering and heatmap boundaries. `ARTPLAYER_DANMUKU_ARTIFACT` selects a
|
||||
built candidate for integration cases; direct internal tests remain source tests.
|
||||
The candidate Worker helper executes the selected artifact's Blob or data URL bytes;
|
||||
only source-mode controlled imports compile `worker.ts` separately. The frozen
|
||||
published helper remains unchanged. `yarn test:danmuku-types` checks the unchanged
|
||||
npm root, the optional accurate `/runtime` entry, implementation assignability and
|
||||
semantic editor declarations. `yarn test:danmuku-types-package` packs and installs
|
||||
the packages outside the workspace, verifies exact member bytes and frozen offline
|
||||
reinstallation, and checks historical and current compiler consumers. These type
|
||||
checks do not replace the native browser suite or final distribution acceptance.
|
||||
@@ -128,3 +128,14 @@ both response modes. A passing diagnostic only proves observation plus successfu
|
||||
playback after release; inspect its progressed/waiting flags before claiming buffering.
|
||||
These capability observation timeouts do not relax the integration assertions.
|
||||
Archive the report and results (including traces) before starting another browser suite.
|
||||
|
||||
Danmuku input, scheduler, resources and heatmap-density specs accept
|
||||
`ARTPLAYER_DANMUKU_ARTIFACT` for a built main/legacy file; unset it to compile the
|
||||
current JS/TS entry. Run each format sequentially and wait for the command's final
|
||||
exit before copying reports or launching the next format. A visible last test line
|
||||
does not prove reporter/trace teardown is finished. These runs share the server port
|
||||
and output directory, so simultaneous invocations can corrupt failure evidence.
|
||||
For a timeout, inspect the retained trace before changing the test or implementation;
|
||||
browser startup and pre-plugin playback delays are different from plugin execution.
|
||||
The density regression retains 16000 rows, real playback and SVG/pixel geometry at
|
||||
two widths. A diagnostic pass does not waive the full matrix or long-load acceptance.
|
||||
@@ -2,7 +2,9 @@ import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Deterministic owned-node tests use the Node runner.
|
||||
import test from 'node:test'
|
||||
import { parseHTML } from 'linkedom'
|
||||
import Renderer from '../packages/artplayer-plugin-danmuku/src/renderer.js'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
|
||||
const { Renderer } = await loadModules({ Renderer: 'packages/artplayer-plugin-danmuku/src/renderer' })
|
||||
|
||||
function fixture(t) {
|
||||
const { document } = parseHTML('<html><body><div id="layer"><i id="foreign"></i></div></body></html>')
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
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 { danmukuCandidate, danmukuCandidateEnvironment, danmukuWorkerCode } from './helpers/danmuku-candidate.js'
|
||||
import { deferred } from './helpers/danmuku.js'
|
||||
|
||||
const implementation = await danmukuCandidate()
|
||||
@@ -44,10 +43,7 @@ async function fixture(options = {}, { automatic = false } = {}) {
|
||||
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 workerCode = await danmukuWorkerCode(implementation, env, worker)
|
||||
const responses = []
|
||||
const workerContext = vm.createContext({ postMessage: data => responses.push(structuredClone(data)), URL: { revokeObjectURL() {} }, location: { href: worker.url } })
|
||||
workerContext.self = workerContext
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
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 { danmukuCandidate, danmukuCandidateEnvironment, danmukuWorkerCode } from './helpers/danmuku-candidate.js'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
|
||||
const { WorkerClient } = await loadModules({ WorkerClient: 'packages/artplayer-plugin-danmuku/src/worker-client' })
|
||||
@@ -223,13 +222,18 @@ test('danmuku worker client: independent clients never share pending ownership',
|
||||
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')
|
||||
// Unlike the client double above, this executes the actual source or artifact
|
||||
// worker. Numeric geometry inputs are controlled; no browser layout is claimed.
|
||||
const workerEnvironment = danmukuCandidateEnvironment(implementation)
|
||||
workerEnvironment.factory({ danmuku: [] })(workerEnvironment.art)
|
||||
const actualWorker = workerEnvironment.workers[0]
|
||||
const workerCode = await danmukuWorkerCode(implementation, workerEnvironment, actualWorker)
|
||||
workerEnvironment.destroy()
|
||||
|
||||
function calculateGeometry(input) {
|
||||
const replies = []
|
||||
const context = vm.createContext({ input, postMessage: data => replies.push(data) })
|
||||
const context = vm.createContext({ input, postMessage: data => replies.push(data), URL: { revokeObjectURL() {} }, location: { href: actualWorker.url } })
|
||||
context.self = context
|
||||
vm.runInContext(workerCode, context, { timeout: 1000 })
|
||||
vm.runInContext('onmessage({ data: input })', context, { timeout: 1000 })
|
||||
assert.equal(replies.length, 1)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
@@ -6,8 +7,31 @@ import { build } from 'esbuild'
|
||||
import { ensureArchive, readMember, refactorDir } from '../../refactor/scripts/releases.mjs'
|
||||
import { getEntryFile } from '../../scripts/projects.js'
|
||||
import { danmukuEnvironment } from './danmuku.js'
|
||||
import { resolveSource } from './load.js'
|
||||
|
||||
const workspace = fileURLToPath(new URL('../../', import.meta.url))
|
||||
let sourceWorker
|
||||
|
||||
export async function danmukuWorkerCode(implementation, env, worker) {
|
||||
const blob = env.urls.get(worker.url)
|
||||
if (blob)
|
||||
return blob.text()
|
||||
// Production worker-loader emits a percent-encoded data URL. Execute those
|
||||
// exact shipped bytes instead of silently substituting the current source.
|
||||
const dataPrefix = 'data:text/javascript;charset=utf-8,'
|
||||
if (typeof worker.url === 'string' && worker.url.startsWith(dataPrefix))
|
||||
return decodeURIComponent(worker.url.slice(dataPrefix.length))
|
||||
assert.equal(implementation.name, 'candidate-source', 'Artifact Worker tests must execute the Worker embedded in that artifact')
|
||||
sourceWorker ??= build({
|
||||
entryPoints: [resolveSource('packages/artplayer-plugin-danmuku/src/worker')],
|
||||
bundle: true,
|
||||
write: false,
|
||||
format: 'iife',
|
||||
platform: 'browser',
|
||||
target: 'es2020',
|
||||
}).then(result => result.outputFiles[0].text)
|
||||
return sourceWorker
|
||||
}
|
||||
|
||||
export async function danmukuCandidate() {
|
||||
const coreRelease = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/releases.json'), 'utf8')).releases.find(item => item.name === 'artplayer')
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { Option, Result } from 'artplayer-plugin-danmuku'
|
||||
import danmuku from 'artplayer-plugin-danmuku'
|
||||
import legacy from 'artplayer-plugin-danmuku/legacy'
|
||||
|
||||
const legacyFactory: typeof danmuku = legacy
|
||||
const rootFactory: typeof legacy = danmuku
|
||||
const option: Option = { danmuku: [], points: [{ time: 1, value: 2 }] }
|
||||
danmuku(option)
|
||||
legacy(option)
|
||||
declare const result: Result
|
||||
const synchronous: Result = result.emit({ text: 'historical declaration' })
|
||||
void [legacyFactory, rootFactory, synchronous]
|
||||
|
||||
// @ts-expect-error The published root requires its option argument.
|
||||
danmuku()
|
||||
// @ts-expect-error The published root requires danmuku in Option.
|
||||
danmuku({})
|
||||
// @ts-expect-error Preserve historical object points at the root.
|
||||
danmuku({ danmuku: [], points: [[1, 2]] })
|
||||
// @ts-expect-error Root emit keeps its published synchronous Result shape.
|
||||
const asynchronous: Promise<Result> = result.emit({ text: 'old emit' })
|
||||
void asynchronous
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { Danmu, EventMap, Icons, Input, Item, NormalizedDanmu, NormalizedOption, Owner, Point, RuntimeFactory, RuntimeOption, RuntimeResult, Slider } from 'artplayer-plugin-danmuku/runtime'
|
||||
import Artplayer from 'artplayer'
|
||||
import runtime from 'artplayer-plugin-danmuku/runtime'
|
||||
|
||||
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false
|
||||
type Assert<T extends true> = T
|
||||
type FactoryArguments = Assert<Equal<Parameters<typeof runtime>, [option: RuntimeOption]>>
|
||||
type FactoryReturns = Assert<Equal<ReturnType<ReturnType<typeof runtime>>, RuntimeResult>>
|
||||
type EmitReturns = Assert<Equal<ReturnType<RuntimeResult['emit']>, Promise<Owner>>>
|
||||
type LoadReturns = Assert<Equal<ReturnType<RuntimeResult['load']>, Promise<Owner>>>
|
||||
type CommandReturns = Assert<Equal<ReturnType<RuntimeResult['config']>, Owner>>
|
||||
type OwnerHasNoFacadeKeys = Assert<Equal<Extract<'name' | 'mount', keyof Owner>, never>>
|
||||
type VisiblePayload = Assert<Equal<EventMap['artplayerPluginDanmuku:visible'], [item: Item]>>
|
||||
type LoadedPayload = Assert<Equal<EventMap['artplayerPluginDanmuku:loaded'], [queue: Item[]]>>
|
||||
|
||||
function expectOption(_option: NormalizedOption) {}
|
||||
const row: Danmu = { text: 'extended input', id: 'message-1', author: { name: 'test' }, style: { fontWeight: 'bold' } }
|
||||
const input: Input = () => [row]
|
||||
const asyncInput: Input = async () => [row]
|
||||
const point: Point = [10, 5]
|
||||
const margin: Slider<[number | string, number | string]> = { steps: [{ name: 'half', value: [10, '50%'], hide: true, show: false }] }
|
||||
const options: RuntimeOption = {
|
||||
danmuku: input,
|
||||
points: [point],
|
||||
MARGIN: margin,
|
||||
SPEED: { steps: [{ name: 'fast', value: 2.5, hide: true }] },
|
||||
filter(danmu) {
|
||||
expectOption(this)
|
||||
const normalized: NormalizedDanmu = danmu
|
||||
const time: number = danmu.time
|
||||
const extension: unknown = danmu.author
|
||||
// @ts-expect-error Queue allocation has not run when filter is invoked.
|
||||
const state: string = danmu.$state
|
||||
void [normalized, time, extension, state]
|
||||
return true
|
||||
},
|
||||
async beforeEmit(danmu) {
|
||||
expectOption(this)
|
||||
const text: string = danmu.text
|
||||
void text
|
||||
return true
|
||||
},
|
||||
async beforeVisible(item) {
|
||||
expectOption(this)
|
||||
const state: 'wait' | 'ready' | 'emit' | 'stop' = item.$state
|
||||
const index: number = item.$index
|
||||
const node: HTMLDivElement | null = item.$ref
|
||||
const extra: unknown = item.author
|
||||
void [state, index, node, extra]
|
||||
return true
|
||||
},
|
||||
}
|
||||
const art = new Artplayer({ container: '#player', url: '/video.mp4', plugins: [runtime(options)] })
|
||||
const factory: RuntimeFactory = runtime
|
||||
const result = runtime({})(art)
|
||||
const registered: 'artplayerPluginDanmuku' = result.name
|
||||
const icon: string = runtime.icons.$check_on
|
||||
const icons: Icons = runtime.icons
|
||||
runtime.icons = icons
|
||||
runtime.icons.$on = '<svg></svg>'
|
||||
const promise: Promise<Owner> = result.emit(row)
|
||||
const loading: Promise<Owner> = result.load(asyncInput)
|
||||
const replacing: Promise<Owner> = result.load()
|
||||
const owner: Owner = result.config({ opacity: 0.5 })
|
||||
const stopped: Owner = owner.stop().start().suspend().continue().reset()
|
||||
const configured: NormalizedOption = result.option
|
||||
result.option.visible = false
|
||||
result.mount(document.createElement('div'))
|
||||
result.mount('#danmuku-mount')
|
||||
const loadedPayload: EventMap['artplayerPluginDanmuku:loaded'] = [owner.queue]
|
||||
const pointsPayload: EventMap['artplayerPluginDanmuku:points'] = [[point]]
|
||||
const failedPayload: EventMap['artplayerPluginDanmuku:error'] = [new Error('failed')]
|
||||
|
||||
async function consume() {
|
||||
const emitted = await result.emit({ text: 'await owner' })
|
||||
const rows: Item[] = emitted.queue
|
||||
const next: Owner = emitted.hide().show()
|
||||
// @ts-expect-error The resolved owner does not expose the facade name.
|
||||
void emitted.name
|
||||
// @ts-expect-error The resolved owner does not own setting.mount.
|
||||
emitted.mount('#mount')
|
||||
return [rows, next]
|
||||
}
|
||||
// @ts-expect-error The factory still requires its options object.
|
||||
runtime()
|
||||
// @ts-expect-error Undefined is not a valid options object.
|
||||
runtime(undefined)
|
||||
// @ts-expect-error An emitted row requires text.
|
||||
result.emit({ id: 'missing-text' })
|
||||
// @ts-expect-error The runtime heatmap uses mutable pairs, not historical object points.
|
||||
runtime({ points: [{ time: 1, value: 2 }] })
|
||||
// @ts-expect-error Margin sliders carry pairs rather than scalar numbers.
|
||||
runtime({ MARGIN: { steps: [{ value: 10 }] } })
|
||||
// @ts-expect-error A live mount needs an explicit destination.
|
||||
result.mount()
|
||||
// @ts-expect-error Facade option is a getter, although the returned options object is mutable.
|
||||
result.option = configured
|
||||
// @ts-expect-error Facade isHide is a getter.
|
||||
result.isHide = true
|
||||
// @ts-expect-error Facade isStop is a getter.
|
||||
result.isStop = true
|
||||
// @ts-expect-error emit returns a Promise; synchronous chaining is not available.
|
||||
result.emit(row).hide()
|
||||
// @ts-expect-error The factory has icons but no default self-reference.
|
||||
void runtime.default
|
||||
|
||||
export type { CommandReturns, EmitReturns, FactoryArguments, FactoryReturns, LoadedPayload, LoadReturns, OwnerHasNoFacadeKeys, VisiblePayload }
|
||||
void [factory, registered, icon, promise, loading, replacing, stopped, loadedPayload, pointsPayload, failedPayload, consume]
|
||||
Reference in new issue
Block a user