mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-29 02:51:20 -08:00
refactor(danmuku): [PKG-DANMUKU-03] own input loading parsing and configuration
This commit is contained in:
1 parent
d3c91af92c
commit
0b2efb07f1
26 files changed
+9167
-503
No files matched your search
@@ -0,0 +1,56 @@
|
||||
export function getMode(key) {
|
||||
switch (key) {
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
return 0
|
||||
case 4:
|
||||
return 2
|
||||
case 5:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
export function bilibiliDanmuParseFromXml(xmlString) {
|
||||
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(',')
|
||||
if (attr.length >= 8) {
|
||||
const text = match.groups.text
|
||||
.trim()
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', '\'')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('&', '&')
|
||||
|
||||
return {
|
||||
text,
|
||||
time: Number(attr[0]),
|
||||
mode: getMode(Number(attr[1])),
|
||||
fontSize: Number(attr[2]),
|
||||
color: `#${Number(attr[3]).toString(16)}`,
|
||||
timestamp: Number(attr[4]),
|
||||
pool: Number(attr[5]),
|
||||
userID: attr[6],
|
||||
rowID: Number(attr[7]),
|
||||
}
|
||||
}
|
||||
else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export function onmessage({ data }) {
|
||||
const { xml, id } = data
|
||||
const danmus = bilibiliDanmuParseFromXml(xml)
|
||||
globalThis.postMessage({ danmus, id })
|
||||
}
|
||||
@@ -1,93 +1,144 @@
|
||||
function getMode(key) {
|
||||
switch (key) {
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
return 0
|
||||
case 4:
|
||||
return 2
|
||||
case 5:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
import { bilibiliDanmuParseFromXml, getMode, onmessage } from './bilibili-parser'
|
||||
|
||||
let nextRequest = 0
|
||||
|
||||
function abortError(signal) {
|
||||
if (signal?.reason !== undefined)
|
||||
return signal.reason
|
||||
const error = new Error('Bilibili Danmu loading cancelled')
|
||||
error.name = 'AbortError'
|
||||
return error
|
||||
}
|
||||
|
||||
function bilibiliDanmuParseFromXml(xmlString) {
|
||||
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(',')
|
||||
if (attr.length >= 8) {
|
||||
const text = match.groups.text
|
||||
.trim()
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', '\'')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('&', '&')
|
||||
export function bilibiliDanmuParseFromUrl(url, { signal, onCancel } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false
|
||||
let worker
|
||||
let workerUrl
|
||||
let releaseCancel
|
||||
const id = ++nextRequest
|
||||
const cancel = () => finish(undefined, abortError(signal))
|
||||
|
||||
return {
|
||||
text,
|
||||
time: Number(attr[0]),
|
||||
mode: getMode(Number(attr[1])),
|
||||
fontSize: Number(attr[2]),
|
||||
color: `#${Number(attr[3]).toString(16)}`,
|
||||
timestamp: Number(attr[4]),
|
||||
pool: Number(attr[5]),
|
||||
userID: attr[6],
|
||||
rowID: Number(attr[7]),
|
||||
function releaseWorker() {
|
||||
let error
|
||||
let failed = false
|
||||
try {
|
||||
if (worker) {
|
||||
const current = worker
|
||||
worker = undefined
|
||||
current.onmessage = null
|
||||
current.onerror = null
|
||||
current.onmessageerror = null
|
||||
current.terminate()
|
||||
}
|
||||
}
|
||||
else {
|
||||
return null
|
||||
catch (failure) {
|
||||
error = failure
|
||||
failed = true
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function onmessage({ data }) {
|
||||
const { xml, id } = data
|
||||
if (!id || !xml)
|
||||
return
|
||||
const danmus = bilibiliDanmuParseFromXml(xml)
|
||||
globalThis.postMessage({ danmus, id })
|
||||
}
|
||||
|
||||
function createWorker() {
|
||||
const workerText = `
|
||||
${getMode.toString()}
|
||||
${bilibiliDanmuParseFromXml.toString()}
|
||||
onmessage = ${onmessage.toString()}
|
||||
`
|
||||
const blob = new Blob([workerText], { type: 'application/javascript' })
|
||||
return new Worker(URL.createObjectURL(blob))
|
||||
}
|
||||
|
||||
export function bilibiliDanmuParseFromUrl(url) {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve) => {
|
||||
const res = await fetch(url)
|
||||
const xml = await res.text()
|
||||
|
||||
try {
|
||||
const worker = createWorker()
|
||||
worker.onmessage = (event) => {
|
||||
const { danmus, id } = event.data
|
||||
if (!id || !danmus)
|
||||
return
|
||||
resolve(danmus)
|
||||
worker.terminate()
|
||||
if (workerUrl !== undefined) {
|
||||
const current = workerUrl
|
||||
workerUrl = undefined
|
||||
try {
|
||||
URL.revokeObjectURL(current)
|
||||
}
|
||||
catch (failure) {
|
||||
if (!failed)
|
||||
error = failure
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
worker.postMessage({ xml, id: Date.now() })
|
||||
if (failed)
|
||||
throw error
|
||||
}
|
||||
catch (error) {
|
||||
|
||||
function finish(value, error) {
|
||||
if (settled)
|
||||
return
|
||||
settled = true
|
||||
let failed = arguments.length > 1
|
||||
signal?.removeEventListener('abort', cancel)
|
||||
releaseCancel?.()
|
||||
try {
|
||||
releaseWorker()
|
||||
}
|
||||
catch (cleanupError) {
|
||||
if (!failed)
|
||||
error = cleanupError
|
||||
failed = true
|
||||
}
|
||||
if (failed)
|
||||
reject(error)
|
||||
else resolve(value)
|
||||
}
|
||||
|
||||
function fallback(xml, error) {
|
||||
if (settled)
|
||||
return
|
||||
console.error('Error parsing Bilibili Danmu:', error)
|
||||
const danmus = bilibiliDanmuParseFromXml(xml)
|
||||
resolve(danmus)
|
||||
try {
|
||||
finish(bilibiliDanmuParseFromXml(xml))
|
||||
}
|
||||
catch (failure) {
|
||||
finish(undefined, failure)
|
||||
}
|
||||
}
|
||||
|
||||
async function read() {
|
||||
const response = signal ? await fetch(url, { signal }) : await fetch(url)
|
||||
if (settled)
|
||||
return
|
||||
const xml = await response.text()
|
||||
if (settled)
|
||||
return
|
||||
try {
|
||||
const workerText = `
|
||||
${getMode.toString()}
|
||||
${bilibiliDanmuParseFromXml.toString()}
|
||||
onmessage = ${onmessage.toString()}
|
||||
`
|
||||
workerUrl = URL.createObjectURL(new Blob([workerText], { type: 'application/javascript' }))
|
||||
if (settled) {
|
||||
releaseWorker()
|
||||
return
|
||||
}
|
||||
worker = new Worker(workerUrl)
|
||||
if (settled) {
|
||||
releaseWorker()
|
||||
return
|
||||
}
|
||||
worker.onmessage = (event) => {
|
||||
if (settled || event.data?.id !== id)
|
||||
return
|
||||
const { danmus } = event.data
|
||||
if (!Array.isArray(danmus)) {
|
||||
finish(undefined, new Error('Invalid Bilibili Danmu worker response'))
|
||||
return
|
||||
}
|
||||
finish(danmus)
|
||||
}
|
||||
worker.onerror = (event) => {
|
||||
event.preventDefault?.()
|
||||
fallback(xml, event.error || event)
|
||||
}
|
||||
worker.onmessageerror = event => fallback(xml, event.error || event)
|
||||
worker.postMessage({ xml, id })
|
||||
}
|
||||
catch (error) {
|
||||
fallback(xml, error)
|
||||
}
|
||||
}
|
||||
|
||||
if (signal?.aborted) {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
signal?.addEventListener('abort', cancel)
|
||||
releaseCancel = onCancel?.(cancel)
|
||||
if (settled) {
|
||||
releaseCancel?.()
|
||||
return
|
||||
}
|
||||
read().catch(error => finish(undefined, error))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
export function defaultOption() {
|
||||
return {
|
||||
danmuku: [],
|
||||
speed: 5,
|
||||
margin: [10, '25%'],
|
||||
opacity: 1,
|
||||
color: '#FFFFFF',
|
||||
mode: 0,
|
||||
modes: [0, 1, 2],
|
||||
fontSize: 25,
|
||||
antiOverlap: true,
|
||||
synchronousPlayback: false,
|
||||
mount: undefined,
|
||||
heatmap: false,
|
||||
width: 512,
|
||||
points: [],
|
||||
filter: () => true,
|
||||
beforeEmit: () => true,
|
||||
beforeVisible: () => true,
|
||||
visible: true,
|
||||
emitter: true,
|
||||
maxLength: 200,
|
||||
lockTime: 5,
|
||||
theme: 'dark',
|
||||
OPACITY: {},
|
||||
FONT_SIZE: {},
|
||||
MARGIN: {},
|
||||
SPEED: {},
|
||||
COLOR: [],
|
||||
}
|
||||
}
|
||||
|
||||
export function optionScheme() {
|
||||
return {
|
||||
danmuku: 'array|function|string|promise',
|
||||
speed: 'number',
|
||||
margin: 'array',
|
||||
opacity: 'number',
|
||||
color: 'string',
|
||||
mode: 'number',
|
||||
modes: 'array',
|
||||
fontSize: 'number|string',
|
||||
antiOverlap: 'boolean',
|
||||
synchronousPlayback: 'boolean',
|
||||
mount: '?htmldivelement|string',
|
||||
heatmap: 'object|boolean',
|
||||
width: 'number',
|
||||
points: 'array',
|
||||
filter: 'function',
|
||||
beforeEmit: 'function',
|
||||
beforeVisible: 'function',
|
||||
visible: 'boolean',
|
||||
emitter: 'boolean',
|
||||
maxLength: 'number',
|
||||
lockTime: 'number',
|
||||
theme: 'string',
|
||||
OPACITY: 'object',
|
||||
FONT_SIZE: 'object',
|
||||
MARGIN: 'object',
|
||||
SPEED: 'object',
|
||||
COLOR: 'array',
|
||||
}
|
||||
}
|
||||
|
||||
export function isPromiseInput(value) {
|
||||
return value instanceof Promise || Object.prototype.toString.call(value) === '[object Promise]'
|
||||
}
|
||||
|
||||
export function optionChanged(previous, update) {
|
||||
return Object.keys(update).some((key) => {
|
||||
const before = previous[key]
|
||||
const after = update[key]
|
||||
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 }) {
|
||||
const next = Object.assign({}, defaults, previous, update)
|
||||
validate(next)
|
||||
next.mode = clamp(next.mode, 0, 2)
|
||||
next.speed = clamp(next.speed, 1, 10)
|
||||
next.opacity = clamp(next.opacity, 0, 1)
|
||||
next.lockTime = clamp(next.lockTime, 1, 60)
|
||||
next.maxLength = clamp(next.maxLength, 1, 1000)
|
||||
next.mount = next.mount || mount
|
||||
return next
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { bilibiliDanmuParseFromUrl } from './bilibili'
|
||||
import { defaultOption, normalizeOption, optionChanged, optionScheme } from './config'
|
||||
import { beginInput, cancelInputs, inputActive, readInput } from './input'
|
||||
import DanmuWorker from './worker.js?worker&inline'
|
||||
|
||||
export default class Danmuku {
|
||||
@@ -17,6 +18,7 @@ export default class Danmuku {
|
||||
this.isHide = false // 是否隐藏
|
||||
this.timer = null // 定时器
|
||||
this.index = 0 // 弹幕索引
|
||||
this.worker = null
|
||||
|
||||
// 格式化后的配置项
|
||||
this.option = Danmuku.option
|
||||
@@ -24,12 +26,6 @@ export default class Danmuku {
|
||||
// 弹幕状态池
|
||||
this.states = { wait: [], ready: [], emit: [], stop: [] }
|
||||
|
||||
// 初始化配置
|
||||
this.config(option, true)
|
||||
|
||||
// 创建 Web Worker, 用于计算弹幕的 top 值
|
||||
this.worker = new DanmuWorker()
|
||||
|
||||
// 绑定公用事件
|
||||
this.start = this.start.bind(this)
|
||||
this.stop = this.stop.bind(this)
|
||||
@@ -37,82 +33,41 @@ export default class Danmuku {
|
||||
this.resize = this.resize.bind(this)
|
||||
this.destroy = this.destroy.bind(this)
|
||||
|
||||
// 配置事件可同步销毁播放器,先建立输入和销毁边界。
|
||||
inputActive(this)
|
||||
art.on('destroy', this.destroy)
|
||||
try {
|
||||
this.config(option, true)
|
||||
if (!inputActive(this))
|
||||
return
|
||||
|
||||
// 创建 Web Worker, 用于计算弹幕的 top 值
|
||||
this.worker = new DanmuWorker()
|
||||
}
|
||||
catch (error) {
|
||||
art.off('destroy', this.destroy)
|
||||
cancelInputs(this)
|
||||
throw error
|
||||
}
|
||||
// 监听事件
|
||||
art.on('video:play', this.start)
|
||||
art.on('video:playing', this.start)
|
||||
art.on('video:pause', this.stop)
|
||||
art.on('video:waiting', this.stop)
|
||||
art.on('destroy', this.destroy)
|
||||
art.on('resize', this.resize)
|
||||
|
||||
// 开始加载弹幕
|
||||
this.load()
|
||||
this.load().catch(error => console.warn('Failed to load initial danmuku:', error))
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
static get option() {
|
||||
return {
|
||||
danmuku: [], // 弹幕数据
|
||||
speed: 5, // 弹幕持续时间,范围在[1 ~ 10]
|
||||
margin: [10, '25%'], // 弹幕上下边距,支持像素数字和百分比
|
||||
opacity: 1, // 弹幕透明度,范围在[0 ~ 1]
|
||||
color: '#FFFFFF', // 默认弹幕颜色,可以被单独弹幕项覆盖
|
||||
mode: 0, // 默认弹幕模式: 0: 滚动,1: 顶部,2: 底部
|
||||
modes: [0, 1, 2], // 弹幕可见的模式
|
||||
fontSize: 25, // 弹幕字体大小,支持像素数字和百分比
|
||||
antiOverlap: true, // 弹幕是否防重叠
|
||||
synchronousPlayback: false, // 是否同步播放速度
|
||||
mount: undefined, // 弹幕发射器挂载点, 默认为播放器控制栏中部
|
||||
heatmap: false, // 是否开启热力图
|
||||
width: 512, // 当播放器宽度小于此值时,弹幕发射器置于播放器底部
|
||||
points: [], // 热力图数据
|
||||
filter: () => true, // 弹幕载入前的过滤器,只支持返回布尔值
|
||||
beforeEmit: () => true, // 弹幕发送前的过滤器,支持返回 Promise
|
||||
beforeVisible: () => true, // 弹幕显示前的过滤器,支持返回 Promise
|
||||
visible: true, // 弹幕层是否可见
|
||||
emitter: true, // 是否开启弹幕发射器
|
||||
maxLength: 200, // 弹幕输入框最大长度, 范围在[1 ~ 1000]
|
||||
lockTime: 5, // 输入框锁定时间,范围在[1 ~ 60]
|
||||
theme: 'dark', // 弹幕主题,支持 dark 和 light,只在自定义挂载时生效
|
||||
OPACITY: {}, // 不透明度配置项
|
||||
FONT_SIZE: {}, // 弹幕字号配置项
|
||||
MARGIN: {}, // 显示区域配置项
|
||||
SPEED: {}, // 弹幕速度配置项
|
||||
COLOR: [], // 颜色列表配置项
|
||||
}
|
||||
return defaultOption()
|
||||
}
|
||||
|
||||
// 配置校验
|
||||
static get scheme() {
|
||||
return {
|
||||
danmuku: 'array|function|string',
|
||||
speed: 'number',
|
||||
margin: 'array',
|
||||
opacity: 'number',
|
||||
color: 'string',
|
||||
mode: 'number',
|
||||
modes: 'array',
|
||||
fontSize: 'number|string',
|
||||
antiOverlap: 'boolean',
|
||||
synchronousPlayback: 'boolean',
|
||||
mount: '?htmldivelement|string',
|
||||
heatmap: 'object|boolean',
|
||||
width: 'number',
|
||||
points: 'array',
|
||||
filter: 'function',
|
||||
beforeEmit: 'function',
|
||||
beforeVisible: 'function',
|
||||
visible: 'boolean',
|
||||
emitter: 'boolean',
|
||||
maxLength: 'number',
|
||||
lockTime: 'number',
|
||||
theme: 'string',
|
||||
OPACITY: 'object',
|
||||
FONT_SIZE: 'object',
|
||||
MARGIN: 'object',
|
||||
SPEED: 'object',
|
||||
COLOR: 'array',
|
||||
}
|
||||
return optionScheme()
|
||||
}
|
||||
|
||||
// 初始弹幕样式
|
||||
@@ -264,29 +219,25 @@ export default class Danmuku {
|
||||
// 加载弹幕
|
||||
async load(danmuku) {
|
||||
const { errorHandle } = this.utils
|
||||
|
||||
let danmus = []
|
||||
const target = danmuku || this.option.danmuku
|
||||
const task = beginInput(this, danmuku === undefined)
|
||||
|
||||
try {
|
||||
if (typeof target === 'function') {
|
||||
danmus = await target() // 异步函数获取
|
||||
}
|
||||
else if (target instanceof Promise) {
|
||||
danmus = await target // 从 Promise 对象获取
|
||||
}
|
||||
else if (typeof target === 'string') {
|
||||
danmus = await bilibiliDanmuParseFromUrl(target) // 从B站xml链接解析
|
||||
}
|
||||
else if (Array.isArray(target)) {
|
||||
danmus = target // 直接传入数组
|
||||
}
|
||||
if (!task.active())
|
||||
return this
|
||||
const target = danmuku || this.option.danmuku
|
||||
const input = readInput(target, task)
|
||||
// 数组在第一次 await 前提交,空数组的 loaded 保持同步。
|
||||
const danmus = input.asynchronous ? await task.wait(input.value) : input.value
|
||||
if (!task.active())
|
||||
return this
|
||||
|
||||
errorHandle(Array.isArray(danmus), 'Danmuku need return an array as result')
|
||||
|
||||
// 假如没有传入弹幕参数,则清空弹幕,否则追加弹幕
|
||||
if (danmuku === undefined) {
|
||||
this.reset() // 重置弹幕
|
||||
if (!task.active())
|
||||
return this
|
||||
this.queue = [] // 清空弹幕队列
|
||||
this.states = { wait: [], ready: [], emit: [], stop: [] } // 清空弹幕状态池
|
||||
this.$refs = [] // 清空弹幕DOM节点池
|
||||
@@ -295,16 +246,24 @@ export default class Danmuku {
|
||||
|
||||
// 逐个验证原始弹幕并转换到弹幕队列
|
||||
for (let index = 0; index < danmus.length; index++) {
|
||||
if (!task.active())
|
||||
return this
|
||||
const danmu = danmus[index]
|
||||
await this.emit(danmu)
|
||||
await task.emit(danmu)
|
||||
}
|
||||
|
||||
this.art.emit('artplayerPluginDanmuku:loaded', this.queue)
|
||||
if (task.active())
|
||||
this.art.emit('artplayerPluginDanmuku:loaded', this.queue)
|
||||
}
|
||||
catch (error) {
|
||||
if (!task.active())
|
||||
return this
|
||||
this.art.emit('artplayerPluginDanmuku:error', error)
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
task.finish()
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
@@ -312,6 +271,8 @@ export default class Danmuku {
|
||||
// 把原始弹幕转换到弹幕队列
|
||||
async emit(danmu) {
|
||||
const { clamp } = this.utils
|
||||
if (!inputActive(this))
|
||||
return this
|
||||
|
||||
this.validator(danmu, {
|
||||
id: '?string', // 弹幕唯一标识
|
||||
@@ -328,7 +289,7 @@ export default class Danmuku {
|
||||
return this
|
||||
|
||||
// 设置弹幕时间,如果没有则默认为当前时间加 0.5 秒
|
||||
if (danmu.time) {
|
||||
if (danmu.time !== undefined) {
|
||||
danmu.time = clamp(danmu.time, 0, Infinity)
|
||||
}
|
||||
else {
|
||||
@@ -357,6 +318,8 @@ export default class Danmuku {
|
||||
// 自定义弹幕过滤函数
|
||||
if (!this.option.filter(danmu))
|
||||
return this
|
||||
if (!inputActive(this))
|
||||
return this
|
||||
|
||||
// 添加自定义属性
|
||||
const item = {
|
||||
@@ -389,28 +352,28 @@ export default class Danmuku {
|
||||
const { $controlsCenter } = this.art.template
|
||||
|
||||
// 判断配置项是否有变化
|
||||
const changed = Object.keys(option).some(
|
||||
key => JSON.stringify(this.option[key]) !== JSON.stringify(option[key]),
|
||||
)
|
||||
const changed = optionChanged(this.option, option)
|
||||
|
||||
// 没有变化则直接返回
|
||||
if (!changed && !isInit)
|
||||
return this
|
||||
|
||||
// 更新配置项
|
||||
this.option = Object.assign({}, Danmuku.option, this.option, option)
|
||||
this.validator(this.option, Danmuku.scheme)
|
||||
|
||||
this.option.mode = clamp(this.option.mode, 0, 2)
|
||||
this.option.speed = clamp(this.option.speed, 1, 10)
|
||||
this.option.opacity = clamp(this.option.opacity, 0, 1)
|
||||
this.option.lockTime = clamp(this.option.lockTime, 1, 60)
|
||||
this.option.maxLength = clamp(this.option.maxLength, 1, 1000)
|
||||
this.option.mount = this.option.mount || $controlsCenter
|
||||
const next = normalizeOption(this.option, option, {
|
||||
defaults: Danmuku.option,
|
||||
validate: value => this.validator(value, Danmuku.scheme),
|
||||
clamp,
|
||||
mount: $controlsCenter,
|
||||
})
|
||||
if (!inputActive(this))
|
||||
return this
|
||||
this.option = next
|
||||
|
||||
// 动态配置有字体大小,需要重新渲染
|
||||
if (option.fontSize) {
|
||||
this.reset()
|
||||
if (!inputActive(this))
|
||||
return this
|
||||
}
|
||||
|
||||
// 通过配置项控制弹幕的显示和隐藏
|
||||
@@ -421,7 +384,8 @@ export default class Danmuku {
|
||||
this.hide()
|
||||
}
|
||||
|
||||
this.art.emit('artplayerPluginDanmuku:config', this.option)
|
||||
if (inputActive(this))
|
||||
this.art.emit('artplayerPluginDanmuku:config', this.option)
|
||||
|
||||
return this
|
||||
}
|
||||
@@ -718,13 +682,14 @@ export default class Danmuku {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
cancelInputs(this)
|
||||
this.stop()
|
||||
this.worker.terminate()
|
||||
this.worker?.terminate()
|
||||
this.art.off('video:play', this.start)
|
||||
this.art.off('video:playing', this.start)
|
||||
this.art.off('video:pause', this.stop)
|
||||
this.art.off('video:waiting', this.stop)
|
||||
this.art.off('resize', this.reset)
|
||||
this.art.off('resize', this.resize)
|
||||
this.art.off('destroy', this.destroy)
|
||||
this.art.emit('artplayerPluginDanmuku:destroy')
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import Setting from './setting'
|
||||
export default function artplayerPluginDanmuku(option) {
|
||||
return (art) => {
|
||||
const danmuku = new Danmuku(art, option)
|
||||
const setting = new Setting(art, danmuku)
|
||||
const setting = art.isDestroy ? undefined : new Setting(art, danmuku)
|
||||
|
||||
if (danmuku.option.heatmap) {
|
||||
if (!art.isDestroy && danmuku.option.heatmap) {
|
||||
heatmap(art, danmuku, danmuku.option.heatmap)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function artplayerPluginDanmuku(option) {
|
||||
hide: danmuku.hide.bind(danmuku),
|
||||
show: danmuku.show.bind(danmuku),
|
||||
reset: danmuku.reset.bind(danmuku),
|
||||
mount: setting.mount.bind(setting),
|
||||
mount: setting ? setting.mount.bind(setting) : () => {},
|
||||
get option() {
|
||||
return danmuku.option
|
||||
},
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
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)
|
||||
}
|
||||
|
||||
export function inputActive(owner) {
|
||||
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) {
|
||||
const state = stateFor(owner)
|
||||
if (replace) {
|
||||
for (const task of [...state.tasks]) {
|
||||
if (task.replace)
|
||||
task.cancel()
|
||||
}
|
||||
}
|
||||
let stopped = state.closed || owner.art.isDestroy
|
||||
let resolveCancel
|
||||
const cancellation = new Promise(resolve => resolveCancel = resolve)
|
||||
const handlers = new Set()
|
||||
const controller = typeof AbortController === 'function' ? new AbortController() : undefined
|
||||
const task = {
|
||||
replace,
|
||||
signal: controller?.signal,
|
||||
active: () => !stopped && !state.closed && !owner.art.isDestroy,
|
||||
onCancel(callback) {
|
||||
if (!task.active()) {
|
||||
callback()
|
||||
return () => {}
|
||||
}
|
||||
handlers.add(callback)
|
||||
return () => handlers.delete(callback)
|
||||
},
|
||||
cancel() {
|
||||
if (stopped)
|
||||
return
|
||||
stopped = true
|
||||
resolveCancel(cancelled)
|
||||
controller?.abort()
|
||||
for (const callback of [...handlers]) callback()
|
||||
handlers.clear()
|
||||
},
|
||||
wait: value => Promise.race([value, cancellation]),
|
||||
emit(danmu) {
|
||||
state.emitting.push(task)
|
||||
try {
|
||||
return owner.emit(danmu)
|
||||
}
|
||||
finally {
|
||||
state.emitting.pop()
|
||||
}
|
||||
},
|
||||
finish() {
|
||||
state.tasks.delete(task)
|
||||
handlers.clear()
|
||||
},
|
||||
}
|
||||
if (stopped)
|
||||
resolveCancel(cancelled)
|
||||
else state.tasks.add(task)
|
||||
return task
|
||||
}
|
||||
|
||||
export function cancelInputs(owner) {
|
||||
const state = stateFor(owner)
|
||||
state.closed = true
|
||||
for (const task of [...state.tasks]) task.cancel()
|
||||
}
|
||||
|
||||
export function readInput(target, task) {
|
||||
if (typeof target === 'function')
|
||||
return { asynchronous: true, value: target() }
|
||||
if (isPromiseInput(target))
|
||||
return { asynchronous: true, value: target }
|
||||
if (typeof target === 'string')
|
||||
return { asynchronous: true, value: bilibiliDanmuParseFromUrl(target, task) }
|
||||
return { asynchronous: false, value: Array.isArray(target) ? target : [] }
|
||||
}
|
||||
Reference in new issue
Block a user