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

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

No files matched your search

@@ -0,0 +1,139 @@
import type { PlacementRequest, PlacementRow, VisibleDanmu } from './worker-types'
export function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom, marginTop, antiOverlap }: PlacementRequest): number | undefined {
// 弹幕最大高度
const maxTop = clientHeight - marginBottom
// 过滤同模式的弹幕,即每种模式各不影响
const danmus: PlacementRow[] = visibles
.filter(item => item.mode === target.mode && item.top <= maxTop)
.sort((prev, next) => prev.top - next.top)
// 如果没有同模式的弹幕,直接返回
if (danmus.length === 0) {
if (target.mode === 2) {
return maxTop - target.height
}
else {
return marginTop
}
}
// 上下各加一个虚拟弹幕,方便计算
danmus.unshift({
type: 'top',
top: 0,
left: 0,
right: 0,
height: marginTop,
width: clientWidth,
speed: 0,
distance: clientWidth,
})
danmus.push({
type: 'bottom',
top: maxTop,
left: 0,
right: 0,
height: marginBottom,
width: clientWidth,
speed: 0,
distance: clientWidth,
})
// 查找是否有多余的缝隙足以容纳当前弹幕
if (target.mode === 2) {
// 倒序查找
for (let index = danmus.length - 2; index >= 0; index -= 1) {
const item = danmus[index]!
const prev = danmus[index + 1]!
const itemBottom = item.top + item.height
const diff = prev.top - itemBottom
if (diff >= target.height) {
return prev.top - target.height
}
}
}
else {
// 顺序查找
for (let index = 1; index < danmus.length; index += 1) {
const item = danmus[index]!
const prev = danmus[index - 1]!
const prevBottom = prev.top + prev.height
const diff = item.top - prevBottom
if (diff >= target.height) {
return prevBottom
}
}
}
const topMap: VisibleDanmu[][] = []
for (let index = 1; index < danmus.length - 1; index += 1) {
// The loop excludes the two virtual boundary rows inserted above.
const item = danmus[index] as VisibleDanmu
if (topMap.length) {
const last = topMap[topMap.length - 1]!
if (last[0]!.top === item.top) {
last.push(item)
}
else {
topMap.push([item])
}
}
else {
topMap.push([item])
}
}
if (antiOverlap) {
switch (target.mode) {
case 0: {
const result = topMap.find((list) => {
return list.every((danmu) => {
if (clientWidth < danmu.distance)
return false
if (target.speed < danmu.speed)
return true
const overlapTime = danmu.right / (target.speed - danmu.speed)
if (overlapTime > danmu.time)
return true
return false
})
})
return result && result[0] ? result[0].top : undefined
}
// 静止弹幕没有重叠问题
case 1:
case 2:
return undefined
default:
break
}
}
else {
switch (target.mode) {
case 0:
topMap.sort((prev, next) => {
const nextMinRight = Math.min(...next.map(item => item.right))
const prevMinRight = Math.min(...prev.map(item => item.right))
return nextMinRight * next.length - prevMinRight * prev.length
})
break
case 1:
case 2:
topMap.sort((prev, next) => {
const nextMaxWidth = Math.max(...next.map(item => item.width))
const prevMaxWidth = Math.max(...prev.map(item => item.width))
return prevMaxWidth * prev.length - nextMaxWidth * next.length
})
break
default:
break
}
return topMap[0]![0]!.top
}
}
@@ -0,0 +1,49 @@
import type Danmuku from './danmuku'
import type { DanmuItem } from './types'
// Public readys remains a point-in-time query; automatic playback can bridge a stalled frame.
export default class SamplingWindow {
private time: number | undefined
private rows = new Set<DanmuItem>()
private waiting = new Set<DanmuItem>()
private document: Document | undefined
private visibility = () => this.clear()
capture(owner: Danmuku, rows: DanmuItem[], time: number) {
const doc = owner.$player.ownerDocument
if (doc !== this.document) {
this.document?.removeEventListener('visibilitychange', this.visibility)
this.document = doc
doc?.addEventListener('visibilitychange', this.visibility)
this.clear()
}
if (!Number.isFinite(time) || doc?.visibilityState === 'hidden' || owner.art.video?.seeking) {
this.clear()
return rows
}
const previousTime = this.time
const previousRows = this.rows
const previousWaiting = this.waiting
this.time = time
this.rows = new Set(rows)
this.waiting = new Set(owner.states.wait)
if (previousTime === undefined || time <= previousTime)
return rows
const missed = owner.states.wait.filter(row => previousWaiting.has(row) && !previousRows.has(row)
&& row.time >= previousTime - 0.1 && row.time < time - 0.1)
for (const row of missed) this.rows.add(row)
return [...rows.filter(row => row.$state === 'ready'), ...missed, ...rows.filter(row => row.$state !== 'ready')]
}
clear() {
this.time = undefined
this.rows.clear()
this.waiting.clear()
}
destroy() {
this.document?.removeEventListener('visibilitychange', this.visibility)
this.document = undefined
this.clear()
}
}
@@ -1,5 +1,8 @@
import type Danmuku from './danmuku'
import type { DanmuItem } from './types'
import type { PlacementRequest } from './worker-types'
import { getDanmuTop } from './placement'
import SamplingWindow from './sampling-window'
import SchedulingBuffer from './scheduling-buffer'
const cancelled = Symbol('cancelled danmuku frame')
@@ -23,6 +26,7 @@ export default class Scheduler {
declare fault: boolean
declare failedItems: Set<DanmuItem>
declare buffer: SchedulingBuffer
declare sampling: SamplingWindow
constructor(owner: Danmuku) {
this.owner = owner
@@ -35,6 +39,7 @@ export default class Scheduler {
this.fault = false
this.failedItems = new Set()
this.buffer = new SchedulingBuffer()
this.sampling = new SamplingWindow()
}
active(operation: FrameOperation) {
@@ -62,6 +67,7 @@ export default class Scheduler {
this.owner.workerClient?.cancel()
this.failedItems.clear()
this.buffer.clear()
this.sampling.clear()
}
report(error: unknown) {
@@ -116,10 +122,11 @@ export default class Scheduler {
if (danmu.$restTime <= 0)
owner.makeWait(danmu)
})
const time = owner.art.currentTime
const readys = owner.readys
if (generation !== this.generation)
return
this.buffer.capture(readys, this.failedItems)
this.buffer.capture(this.sampling.capture(owner, readys, time), this.failedItems)
}
}
catch (error) {
@@ -183,7 +190,7 @@ export default class Scheduler {
danmu.$lastStartTime = Date.now()
danmu.$restTime = owner.speed
const distance = clientWidth + ref.clientWidth
const reply = await operation.wait(owner.postMessage({
const request: PlacementRequest = {
type: 'getDanmuTop',
target: { mode: danmu.mode, height: ref.clientHeight, speed: distance / danmu.$restTime },
visibles: owner.visibles,
@@ -193,7 +200,12 @@ export default class Scheduler {
// Valid margins are numeric. Preserve the old raw fallback for unsupported strings.
marginBottom: owner.marginBottom as number,
marginTop: owner.marginTop,
}))
}
// Relaxed placement is small and deterministic; avoid a Worker round trip per row.
// Collision-sensitive placement and the internal postMessage API still use the Worker.
const reply = request.antiOverlap
? await operation.wait(owner.postMessage(request))
: { result: getDanmuTop(request) }
const top = typeof reply === 'symbol' ? undefined : reply.result
if (!this.active(operation) || danmu.$ref !== ref)
return
@@ -215,5 +227,6 @@ export default class Scheduler {
destroy() {
this.closed = true
this.invalidate()
this.sampling.destroy()
}
}
+2 -139
View File
@@ -1,142 +1,5 @@
import type { PlacementMessage, PlacementReply, PlacementRequest, PlacementRow, VisibleDanmu } from './worker-types'
function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom, marginTop, antiOverlap }: PlacementRequest): number | undefined {
// 弹幕最大高度
const maxTop = clientHeight - marginBottom
// 过滤同模式的弹幕,即每种模式各不影响
const danmus: PlacementRow[] = visibles
.filter(item => item.mode === target.mode && item.top <= maxTop)
.sort((prev, next) => prev.top - next.top)
// 如果没有同模式的弹幕,直接返回
if (danmus.length === 0) {
if (target.mode === 2) {
return maxTop - target.height
}
else {
return marginTop
}
}
// 上下各加一个虚拟弹幕,方便计算
danmus.unshift({
type: 'top',
top: 0,
left: 0,
right: 0,
height: marginTop,
width: clientWidth,
speed: 0,
distance: clientWidth,
})
danmus.push({
type: 'bottom',
top: maxTop,
left: 0,
right: 0,
height: marginBottom,
width: clientWidth,
speed: 0,
distance: clientWidth,
})
// 查找是否有多余的缝隙足以容纳当前弹幕
if (target.mode === 2) {
// 倒序查找
for (let index = danmus.length - 2; index >= 0; index -= 1) {
const item = danmus[index]!
const prev = danmus[index + 1]!
const itemBottom = item.top + item.height
const diff = prev.top - itemBottom
if (diff >= target.height) {
return prev.top - target.height
}
}
}
else {
// 顺序查找
for (let index = 1; index < danmus.length; index += 1) {
const item = danmus[index]!
const prev = danmus[index - 1]!
const prevBottom = prev.top + prev.height
const diff = item.top - prevBottom
if (diff >= target.height) {
return prevBottom
}
}
}
const topMap: VisibleDanmu[][] = []
for (let index = 1; index < danmus.length - 1; index += 1) {
// The loop excludes the two virtual boundary rows inserted above.
const item = danmus[index] as VisibleDanmu
if (topMap.length) {
const last = topMap[topMap.length - 1]!
if (last[0]!.top === item.top) {
last.push(item)
}
else {
topMap.push([item])
}
}
else {
topMap.push([item])
}
}
if (antiOverlap) {
switch (target.mode) {
case 0: {
const result = topMap.find((list) => {
return list.every((danmu) => {
if (clientWidth < danmu.distance)
return false
if (target.speed < danmu.speed)
return true
const overlapTime = danmu.right / (target.speed - danmu.speed)
if (overlapTime > danmu.time)
return true
return false
})
})
return result && result[0] ? result[0].top : undefined
}
// 静止弹幕没有重叠问题
case 1:
case 2:
return undefined
default:
break
}
}
else {
switch (target.mode) {
case 0:
topMap.sort((prev, next) => {
const nextMinRight = Math.min(...next.map(item => item.right))
const prevMinRight = Math.min(...prev.map(item => item.right))
return nextMinRight * next.length - prevMinRight * prev.length
})
break
case 1:
case 2:
topMap.sort((prev, next) => {
const nextMaxWidth = Math.max(...next.map(item => item.width))
const prevMaxWidth = Math.max(...prev.map(item => item.width))
return prevMaxWidth * prev.length - nextMaxWidth * next.length
})
break
default:
break
}
return topMap[0]![0]!.top
}
}
import type { PlacementMessage, PlacementReply } from './worker-types'
import { getDanmuTop } from './placement'
onmessage = (event: MessageEvent<PlacementMessage>) => {
const { data } = event