mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-26 08:51:20 -08:00
refactor(thumbnail): [PKG-TOOL-THUMB-04] type runtime modules and retain emitter attribution
This commit is contained in:
1 parent
efd9f42fb9
commit
15485b6420
37 files changed
+8446
-152
No files matched your search
@@ -0,0 +1,245 @@
|
||||
import type ArtplayerToolThumbnail from './index'
|
||||
import type { Cleanup, ExtractionJob, LifecycleState } from './types'
|
||||
import { cancellation, cleanupAll, stateFor } from './lifecycle'
|
||||
import { replaceThumbnail } from './source'
|
||||
import { clamp } from './utils'
|
||||
|
||||
function prepare(tool: ArtplayerToolThumbnail, job: ExtractionJob) {
|
||||
const { video } = job
|
||||
const { width, number, begin, end } = tool.option
|
||||
const height = (video.videoHeight / video.videoWidth) * width
|
||||
const guard = (condition: unknown, message: string) => {
|
||||
try {
|
||||
tool.errorHandle(condition, message)
|
||||
}
|
||||
catch (error) {
|
||||
job.reported = error
|
||||
job.wasReported = true
|
||||
throw error
|
||||
}
|
||||
}
|
||||
tool.option.height = height
|
||||
tool.option.begin = clamp(begin, 0, video.duration)
|
||||
tool.option.end = clamp(end || video.duration, begin, video.duration)
|
||||
guard(tool.option.end > tool.option.begin, 'End time must be greater than the start time')
|
||||
guard(Number.isFinite(video.duration), 'Video duration must be finite')
|
||||
tool.duration = tool.option.end - tool.option.begin
|
||||
tool.density = number / tool.duration
|
||||
guard(tool.file && video, 'Please select the video file first')
|
||||
guard(!tool.processing, 'There is currently a task in progress, please wait a moment...')
|
||||
guard(tool.density <= 1, `The preview density cannot be greater than 1, but got ${tool.density}`)
|
||||
const points = tool.creatScreenshotDate()
|
||||
const canvas = tool.creatCanvas()
|
||||
const context = canvas.getContext('2d')!
|
||||
tool.emit('canvas', canvas)
|
||||
return { width, height, number, points, canvas, context }
|
||||
}
|
||||
|
||||
function createJob(tool: ArtplayerToolThumbnail, state: LifecycleState) {
|
||||
let epoch = state.epoch
|
||||
const video = tool.video
|
||||
let resolve!: () => void
|
||||
let reject!: (error: unknown) => void
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
let settled = false
|
||||
let running = false
|
||||
let job: ExtractionJob
|
||||
let frameCleanup = () => {}
|
||||
const listeners: Cleanup[] = []
|
||||
const promise = new Promise<void>((yes, no) => {
|
||||
resolve = yes
|
||||
reject = no
|
||||
})
|
||||
// Consumers still receive the rejecting promise; ignored cancellation is owned.
|
||||
promise.catch(() => {})
|
||||
const live = () => !settled && !state.closed && state.epoch === epoch && state.job === job
|
||||
function finish() {
|
||||
if (settled)
|
||||
return false
|
||||
settled = true
|
||||
if (state.job === job) {
|
||||
state.job = null
|
||||
tool.processing = false
|
||||
}
|
||||
cleanupAll([() => clearTimeout(timer), frameCleanup, ...listeners.splice(0)])
|
||||
return true
|
||||
}
|
||||
function fail(error: unknown, report = true) {
|
||||
if (settled)
|
||||
return
|
||||
try {
|
||||
finish()
|
||||
}
|
||||
catch (cleanupError) {
|
||||
error ||= cleanupError
|
||||
}
|
||||
if (report) {
|
||||
try {
|
||||
tool.emit('error', (error as { message?: string } | null | undefined)?.message)
|
||||
}
|
||||
catch (listenerError) {
|
||||
error = listenerError
|
||||
}
|
||||
}
|
||||
reject(error)
|
||||
}
|
||||
function complete() {
|
||||
if (!live())
|
||||
return
|
||||
try {
|
||||
finish()
|
||||
tool.emit('done')
|
||||
resolve()
|
||||
}
|
||||
catch (error) {
|
||||
let failure = error
|
||||
try {
|
||||
tool.emit('error', (error as { message?: string } | null | undefined)?.message)
|
||||
}
|
||||
catch (listenerError) {
|
||||
failure = listenerError
|
||||
}
|
||||
reject(failure)
|
||||
}
|
||||
}
|
||||
function extract(plan: ReturnType<typeof prepare>) {
|
||||
if (!live())
|
||||
return
|
||||
running = true
|
||||
tool.processing = true
|
||||
let index = 0
|
||||
const next = () => {
|
||||
if (!live())
|
||||
return
|
||||
if (index === plan.points.length) {
|
||||
complete()
|
||||
return
|
||||
}
|
||||
const point = plan.points[index]!
|
||||
let drawing = false
|
||||
let encoded = false
|
||||
const previous = video.oncanplay
|
||||
const draw = () => {
|
||||
if (!live() || drawing || video.seeking || (video.readyState !== undefined && video.readyState < 2))
|
||||
return
|
||||
drawing = true
|
||||
try {
|
||||
frameCleanup()
|
||||
plan.context.drawImage(video, point.x, point.y, plan.width, plan.height)
|
||||
if (!live())
|
||||
return
|
||||
plan.canvas.toBlob((blob) => {
|
||||
if (!live() || encoded)
|
||||
return
|
||||
encoded = true
|
||||
try {
|
||||
if (!blob)
|
||||
throw new Error('Unable to create thumbnail image')
|
||||
const url = replaceThumbnail(tool, blob, live)
|
||||
if (!url)
|
||||
return
|
||||
tool.emit('update', url, (index + 1) / plan.number)
|
||||
index++
|
||||
Promise.resolve().then(next)
|
||||
}
|
||||
catch (error) {
|
||||
fail(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
fail(error)
|
||||
}
|
||||
}
|
||||
frameCleanup = () => {
|
||||
if (video.oncanplay === draw)
|
||||
video.oncanplay = previous
|
||||
video.removeEventListener('seeked', draw)
|
||||
}
|
||||
try {
|
||||
video.oncanplay = draw
|
||||
video.addEventListener('seeked', draw)
|
||||
video.currentTime = point.time
|
||||
if (video.readyState >= 2 && !video.seeking)
|
||||
Promise.resolve().then(draw)
|
||||
}
|
||||
catch (error) {
|
||||
fail(error)
|
||||
}
|
||||
}
|
||||
Promise.resolve().then(next)
|
||||
}
|
||||
function ready(synchronous = false) {
|
||||
if (!live() || running)
|
||||
return
|
||||
clearTimeout(timer)
|
||||
if (!state.loading && video.duration) {
|
||||
running = true
|
||||
try {
|
||||
const plan = prepare(tool, job)
|
||||
extract(plan)
|
||||
}
|
||||
catch (error) {
|
||||
fail(error, !synchronous && !(job.wasReported && error === job.reported))
|
||||
if (synchronous)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
else {
|
||||
timer = setTimeout(() => ready(), 1000)
|
||||
if (!live())
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
job = {
|
||||
video,
|
||||
promise,
|
||||
ready,
|
||||
fail,
|
||||
cancel(reason) { fail(cancellation(reason), false) },
|
||||
adopt(sourceEpoch) {
|
||||
if (settled || running || video !== tool.video)
|
||||
return false
|
||||
epoch = sourceEpoch
|
||||
return true
|
||||
},
|
||||
}
|
||||
state.job = job
|
||||
try {
|
||||
for (const [name, callback] of [
|
||||
['error', () => {
|
||||
if (live())
|
||||
fail(new Error(`Unable to load video: media error ${video.error?.code || 0}`))
|
||||
}],
|
||||
['loadedmetadata', () => ready()],
|
||||
['durationchange', () => ready()],
|
||||
] as const) {
|
||||
listeners.push(() => video.removeEventListener(name, callback))
|
||||
video.addEventListener(name, callback)
|
||||
if (!live()) {
|
||||
cleanupAll([() => video.removeEventListener(name, callback), ...listeners.splice(0)])
|
||||
return promise
|
||||
}
|
||||
}
|
||||
if (video.error)
|
||||
fail(new Error(`Unable to load video: media error ${video.error.code}`))
|
||||
else
|
||||
ready(true)
|
||||
}
|
||||
catch (error) {
|
||||
fail(error, false)
|
||||
throw error
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
export function startExtraction(tool: ArtplayerToolThumbnail): Promise<void> {
|
||||
const state = stateFor(tool)
|
||||
if (state.closed) {
|
||||
const promise = Promise.reject(cancellation('destroyed'))
|
||||
promise.catch(() => {})
|
||||
return promise
|
||||
}
|
||||
tool.errorHandle(!state.job, 'There is currently a task in progress, please wait a moment...')
|
||||
return createJob(tool, state)
|
||||
}
|
||||
Reference in new issue
Block a user