mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-28 10:31:20 -08:00
fix(danmuku): [PKG-DANMUKU-MASK-LOAD-01] recover stalled-frame rows and remove relaxed placement backlog
This commit is contained in:
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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user