mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-25 16:11:20 -08:00
refactor(mediabunny): [PKG-MB-05] own video iterators, frame pools and rendering
This commit is contained in:
1 parent
5d0b128483
commit
9d037dcbf9
27 files changed
+3652
-595
No files matched your search
@@ -0,0 +1,316 @@
|
||||
import type { Input, WrappedCanvas } from 'mediabunny'
|
||||
import type { VideoOptions, VideoPort } from './engine-ports'
|
||||
import type EventTarget from './EventTarget'
|
||||
import type { PlaybackMedia } from './media'
|
||||
import type { FrameIterator } from './video-frames'
|
||||
import { CanvasSink } from 'mediabunny'
|
||||
import { preflightRange } from './preflight'
|
||||
import { readCanvas, releaseIterator } from './video-frames'
|
||||
import Poster from './video-poster'
|
||||
import Renderer from './video-renderer'
|
||||
|
||||
export default class VideoEngine implements VideoPort {
|
||||
declare canvas: HTMLCanvasElement
|
||||
declare ctx: CanvasRenderingContext2D | null
|
||||
declare events: EventTarget
|
||||
declare timeupdateInterval: number
|
||||
declare avSyncTolerance: number
|
||||
declare dropLateFrames: boolean
|
||||
declare poster: string
|
||||
declare preflightRange: boolean
|
||||
declare input: Input | null
|
||||
declare videoSink: CanvasSink | null
|
||||
declare videoIterator: FrameIterator | null
|
||||
declare nextFrame: WrappedCanvas | null
|
||||
declare rafId: number
|
||||
declare asyncId: number
|
||||
declare width: number
|
||||
declare height: number
|
||||
declare duration: number
|
||||
declare audioClock: { readonly currentTime: number } | null
|
||||
declare lastTimeUpdate: number
|
||||
declare stalled: boolean
|
||||
declare playbackRate: number
|
||||
declare posterDrawn: boolean
|
||||
declare isFetching: boolean
|
||||
#generation = 0
|
||||
#iteration = 0
|
||||
#loading: Promise<void> | null = null
|
||||
#createSink: (() => CanvasSink) | null = null
|
||||
#usedSink: CanvasSink | null = null
|
||||
#destroyed = false
|
||||
#preparing = false
|
||||
#poster = new Poster()
|
||||
#renderer: Renderer
|
||||
#onError: ((error: unknown) => void) | undefined
|
||||
|
||||
constructor({ canvas, ctx, events, timeupdateInterval = 250, avSyncTolerance = 0.12, dropLateFrames = false, poster = '', preflightRange = false, onError }: VideoOptions) {
|
||||
this.canvas = canvas
|
||||
this.ctx = ctx
|
||||
this.events = events
|
||||
this.timeupdateInterval = timeupdateInterval
|
||||
this.avSyncTolerance = avSyncTolerance
|
||||
this.dropLateFrames = dropLateFrames
|
||||
this.poster = poster
|
||||
this.preflightRange = preflightRange
|
||||
this.input = null
|
||||
this.videoSink = null
|
||||
this.videoIterator = null
|
||||
this.nextFrame = null
|
||||
this.rafId = 0
|
||||
this.asyncId = 0
|
||||
this.width = 0
|
||||
this.height = 0
|
||||
this.duration = Number.NaN
|
||||
this.audioClock = null
|
||||
this.lastTimeUpdate = 0
|
||||
this.stalled = false
|
||||
this.playbackRate = 1
|
||||
this.posterDrawn = false
|
||||
this.isFetching = false
|
||||
this.#onError = onError
|
||||
this.#renderer = new Renderer(this, () => !this.#preparing, frame => this.#draw(frame, true), error => this.#report(error))
|
||||
}
|
||||
|
||||
#current(generation: number): boolean {
|
||||
return !this.#destroyed && this.#generation === generation
|
||||
}
|
||||
|
||||
#context(): CanvasRenderingContext2D {
|
||||
if (!this.ctx)
|
||||
throw new Error('Canvas 2D context is unavailable.')
|
||||
return this.ctx
|
||||
}
|
||||
|
||||
#report(error: unknown): void {
|
||||
if (this.#destroyed)
|
||||
return
|
||||
this.#release()
|
||||
if (this.#onError)
|
||||
this.#onError(error)
|
||||
else
|
||||
this.events.emit('error', error)
|
||||
}
|
||||
|
||||
#release(): void {
|
||||
void this.stopIterator().catch(error => console.warn('MediaBunny iterator cleanup error:', error))
|
||||
}
|
||||
|
||||
#draw(frame: WrappedCanvas, clear: boolean): void {
|
||||
this.#poster.cancel()
|
||||
if (clear)
|
||||
this.clear()
|
||||
this.#context().drawImage(frame.canvas, 0, 0)
|
||||
}
|
||||
|
||||
async preflight(source: unknown, signal?: AbortSignal, current?: () => boolean): Promise<boolean> {
|
||||
return preflightRange(source, this.preflightRange, this.events, signal, current)
|
||||
}
|
||||
|
||||
drawPoster(): void {
|
||||
if (this.#destroyed || !this.poster || this.posterDrawn)
|
||||
return
|
||||
const generation = this.#generation
|
||||
this.#poster.draw(this.poster, () => this.#current(generation), (image) => {
|
||||
this.clear()
|
||||
this.canvas.width = image.naturalWidth || this.canvas.width
|
||||
this.canvas.height = image.naturalHeight || this.canvas.height
|
||||
this.#context().drawImage(image, 0, 0, this.canvas.width, this.canvas.height)
|
||||
this.posterDrawn = true
|
||||
}, error => this.#report(error))
|
||||
}
|
||||
|
||||
async stopIterator(): Promise<void> {
|
||||
const iterator = this.videoIterator
|
||||
this.videoIterator = null
|
||||
this.nextFrame = null
|
||||
await releaseIterator(iterator)
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.#context().clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
}
|
||||
|
||||
cancelPending(): void {
|
||||
this.#generation++
|
||||
this.#iteration++
|
||||
this.stop()
|
||||
this.#release()
|
||||
this.#poster.cancel()
|
||||
this.#preparing = false
|
||||
this.#loading = null
|
||||
this.#createSink = null
|
||||
this.#usedSink = null
|
||||
this.videoSink = null
|
||||
this.input = null
|
||||
}
|
||||
|
||||
async load(media: PlaybackMedia, onMetadata?: () => void): Promise<void> {
|
||||
if (this.#destroyed)
|
||||
return
|
||||
this.cancelPending()
|
||||
const generation = this.#generation
|
||||
const iteration = this.#iteration
|
||||
this.#preparing = true
|
||||
const pending = Promise.resolve().then(() => this.#load(media, generation, iteration, onMetadata))
|
||||
this.#loading = pending
|
||||
try {
|
||||
await pending
|
||||
}
|
||||
finally {
|
||||
if (this.#loading === pending)
|
||||
this.#loading = null
|
||||
}
|
||||
}
|
||||
|
||||
async #load(media: PlaybackMedia, generation: number, iteration: number, onMetadata?: () => void): Promise<void> {
|
||||
if (!this.#current(generation))
|
||||
return
|
||||
try {
|
||||
this.clear()
|
||||
this.posterDrawn = false
|
||||
const { input, videoTrack, duration } = media
|
||||
this.input = input
|
||||
this.duration = duration
|
||||
if (!videoTrack) {
|
||||
this.handleNoVideoTrack()
|
||||
onMetadata?.()
|
||||
return
|
||||
}
|
||||
const decodable = videoTrack.codec !== null && await videoTrack.canDecode()
|
||||
if (!this.#current(generation))
|
||||
return
|
||||
if (!decodable) {
|
||||
this.handleNoVideoTrack()
|
||||
onMetadata?.()
|
||||
return
|
||||
}
|
||||
const transparent = await videoTrack.canBeTransparent()
|
||||
if (!this.#current(generation))
|
||||
return
|
||||
this.#createSink = () => new CanvasSink(videoTrack, { poolSize: 2, fit: 'contain', alpha: transparent })
|
||||
this.videoSink = this.#createSink()
|
||||
this.width = videoTrack.displayWidth
|
||||
this.height = videoTrack.displayHeight
|
||||
this.canvas.width = this.width
|
||||
this.canvas.height = this.height
|
||||
onMetadata?.()
|
||||
if (this.#current(generation))
|
||||
await this.#reset(0, generation, iteration)
|
||||
}
|
||||
catch (error) {
|
||||
if (this.#current(generation)) {
|
||||
this.#release()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (this.#current(generation) && this.#iteration === iteration)
|
||||
this.#preparing = false
|
||||
}
|
||||
}
|
||||
|
||||
handleNoVideoTrack(): void {
|
||||
if (this.#destroyed)
|
||||
return
|
||||
this.videoSink = null
|
||||
this.width = 0
|
||||
this.height = 0
|
||||
this.canvas.width = 0
|
||||
this.canvas.height = 0
|
||||
this.clear()
|
||||
this.drawPoster()
|
||||
}
|
||||
|
||||
async #reset(time: number, generation: number, iteration: number): Promise<void> {
|
||||
if (!this.#current(generation) || this.#iteration !== iteration)
|
||||
return
|
||||
this.#release()
|
||||
if (this.#createSink && this.videoSink === this.#usedSink)
|
||||
this.videoSink = this.#createSink()
|
||||
const sink = this.videoSink
|
||||
if (!sink || !this.#current(generation))
|
||||
return
|
||||
this.#usedSink = sink
|
||||
const iterator = sink.canvases(time)
|
||||
this.videoIterator = iterator
|
||||
const current = () => this.#current(generation) && this.#iteration === iteration && this.videoIterator === iterator
|
||||
const first = await readCanvas(iterator)
|
||||
if (!current())
|
||||
return
|
||||
const second = await readCanvas(iterator)
|
||||
if (!current())
|
||||
return
|
||||
this.nextFrame = second
|
||||
if (first)
|
||||
this.#draw(first, false)
|
||||
else
|
||||
this.drawPoster()
|
||||
}
|
||||
|
||||
async resetIterator(time: number): Promise<void> {
|
||||
if (this.#destroyed)
|
||||
return
|
||||
const generation = this.#generation
|
||||
const iteration = ++this.#iteration
|
||||
const loading = this.#loading
|
||||
this.stop()
|
||||
this.#poster.cancel()
|
||||
this.#preparing = true
|
||||
try {
|
||||
if (loading)
|
||||
await loading
|
||||
await this.#reset(time, generation, iteration)
|
||||
}
|
||||
catch (error) {
|
||||
if (this.#current(generation) && this.#iteration === iteration) {
|
||||
this.#release()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (this.#current(generation) && this.#iteration === iteration)
|
||||
this.#preparing = false
|
||||
}
|
||||
}
|
||||
|
||||
async updateNextFrame(localId: number): Promise<void> {
|
||||
await this.#renderer.update(localId)
|
||||
}
|
||||
|
||||
render(): void {
|
||||
this.#renderer.render()
|
||||
}
|
||||
|
||||
start(audio: { readonly currentTime: number }): void {
|
||||
this.#renderer.start(audio)
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.#renderer.stop()
|
||||
}
|
||||
|
||||
async seek(time: number): Promise<void> {
|
||||
await this.resetIterator(time)
|
||||
}
|
||||
|
||||
setPlaybackRate(rate: number): void {
|
||||
this.playbackRate = Math.max(0.1, Number(rate) || 1)
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.#destroyed)
|
||||
return
|
||||
this.#destroyed = true
|
||||
this.#generation++
|
||||
this.#renderer.destroy()
|
||||
this.#poster.cancel()
|
||||
this.#release()
|
||||
this.posterDrawn = false
|
||||
this.#createSink = null
|
||||
this.#usedSink = null
|
||||
this.videoSink = null
|
||||
this.input = null
|
||||
this.audioClock = null
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user