refactor(thumbnail): [PKG-TOOL-THUMB-04] preserve approved published defaults

This commit is contained in:
Harvey Zhao committed 2026-09-15 23:54:24 +08:00
1 parent 490e157717
commit b5f8b5e28e
37 files changed
+11172 -136

No files matched your search

@@ -1,13 +1,15 @@
import type ArtplayerToolThumbnail from './index'
import type { Cleanup, ExtractionJob, LifecycleState } from './types'
import { cancellation, cleanupAll, stateFor } from './lifecycle'
import { thumbnailPolicy } from './policy'
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 policy = thumbnailPolicy(tool.option)
const height = policy.aspectHeight ? (video.videoHeight / video.videoWidth) * width : tool.option.height
const guard = (condition: unknown, message: string) => {
try {
tool.errorHandle(condition, message)
@@ -32,7 +34,7 @@ function prepare(tool: ArtplayerToolThumbnail, job: ExtractionJob) {
const canvas = tool.creatCanvas()
const context = canvas.getContext('2d')!
tool.emit('canvas', canvas)
return { width, height, number, points, canvas, context }
return { width, height, number, points, canvas, context, delay: policy.delay }
}
function createJob(tool: ArtplayerToolThumbnail, state: LifecycleState) {
@@ -112,15 +114,19 @@ function createJob(tool: ArtplayerToolThumbnail, state: LifecycleState) {
if (!live())
return
if (index === plan.points.length) {
complete()
if (plan.delay !== null)
timer = setTimeout(complete, Number(plan.delay) * 2)
else
complete()
return
}
const point = plan.points[index]!
let drawing = false
let encoded = false
let delayElapsed = plan.delay === null
const previous = video.oncanplay
const draw = () => {
if (!live() || drawing || video.seeking || (video.readyState !== undefined && video.readyState < 2))
if (!live() || !delayElapsed || drawing || video.seeking || (video.readyState !== undefined && video.readyState < 2))
return
drawing = true
try {
@@ -160,8 +166,17 @@ function createJob(tool: ArtplayerToolThumbnail, state: LifecycleState) {
video.oncanplay = draw
video.addEventListener('seeked', draw)
video.currentTime = point.time
if (video.readyState >= 2 && !video.seeking)
if (plan.delay !== null) {
timer = setTimeout(() => {
delayElapsed = true
draw()
}, plan.delay)
if (!live())
clearTimeout(timer)
}
else if (video.readyState >= 2 && !video.seeking) {
Promise.resolve().then(draw)
}
}
catch (error) {
fail(error)
@@ -3,6 +3,7 @@ import Emitter from './emitter'
import { startExtraction } from './extraction'
import { connectInput, releaseInput, setupInput } from './input'
import { cleanupAll, closeState, releaseMedia, stateFor } from './lifecycle'
import { thumbnailPolicy } from './policy'
import { createSheet, downloadSheet, screenshotPoints } from './sheet'
import { loadSource } from './source'
@@ -46,6 +47,7 @@ export default class ArtplayerToolThumbnail extends Emitter<ThumbnailEvents> {
static get DEFAULTS() {
return {
delay: 300,
number: 60,
width: 160,
height: 90,
@@ -94,9 +96,11 @@ export default class ArtplayerToolThumbnail extends Emitter<ThumbnailEvents> {
inputChange(event: Event) {
if (stateFor(this).closed)
return
const { resetInput } = thumbnailPolicy(this.option)
const file = this.option.fileInput.files![0]
this.loadVideo(file)
;(event.target as HTMLInputElement).value = ''
if (resetInput)
(event.target as HTMLInputElement).value = ''
}
loadVideo(file?: File | null) {
@@ -1,7 +1,7 @@
import type ArtplayerToolThumbnail from './index'
import type { InputOptions } from './types'
import { cancellation, stateFor } from './lifecycle'
import { clamp } from './utils'
import { normalizePolicy } from './policy'
type Registration = readonly [name: string, callback: EventListener]
interface InputRecord {
@@ -62,11 +62,7 @@ export function setupInput(tool: ArtplayerToolThumbnail, patch: InputOptions) {
const option = Object.assign({}, tool.option, patch)
const target = option.fileInput!
tool.errorHandle(target instanceof Element, 'The \'fileInput\' is not a Element')
for (const name of ['number', 'width', 'column', 'begin', 'end'])
tool.errorHandle(typeof option[name] === 'number', `The '${name}' is not a number`)
option.number = clamp(option.number, 10, 1000)
option.width = clamp(option.width, 10, 1000)
option.column = clamp(option.column, 1, 1000)
normalizePolicy(option, (condition, message) => tool.errorHandle(condition, message))
if (!current())
throw cancellation('input setup superseded')
@@ -0,0 +1,22 @@
import type { RuntimeOptions } from './types'
import { clamp } from './utils'
export function thumbnailPolicy(option: RuntimeOptions) {
const workspace = option.compatibility === 'workspace-4.4'
return { delay: workspace ? null : option.delay, aspectHeight: workspace, resetInput: workspace }
}
export function normalizePolicy(option: RuntimeOptions, validate: (condition: unknown, message: string) => void) {
validate(option.compatibility === undefined || option.compatibility === 'published-3.5' || option.compatibility === 'workspace-4.4', 'The compatibility option must be published-3.5 or workspace-4.4')
const workspace = thumbnailPolicy(option).aspectHeight
const fields = workspace ? ['number', 'width', 'column', 'begin', 'end'] : ['delay', 'number', 'width', 'height', 'column', 'begin', 'end']
for (const name of fields)
validate(typeof option[name] === 'number', `The '${name}' is not a number`)
option.number = clamp(option.number, 10, 1000)
option.width = clamp(option.width, 10, 1000)
option.column = clamp(option.column, 1, 1000)
if (!workspace) {
option.delay = clamp(Number(option.delay), 10, 1000)
option.height = clamp(option.height, 10, 1000)
}
}
@@ -1,6 +1,7 @@
import type ArtplayerToolThumbnail from './index'
import type { Cleanup } from './types'
import { cleanupAll, revoke, stateFor } from './lifecycle'
import { thumbnailPolicy } from './policy'
export function loadSource(tool: ArtplayerToolThumbnail, file?: File | null) {
const state = stateFor(tool)
@@ -8,6 +9,7 @@ export function loadSource(tool: ArtplayerToolThumbnail, file?: File | null) {
return
const initialEpoch = state.epoch
const video = tool.video
const { delay } = thumbnailPolicy(tool.option)
const support = video.canPlayType(file.type)
tool.errorHandle(support === 'maybe' || support === 'probably', `Playback of this file format is not supported: ${file.type}`)
if (state.closed || state.epoch !== initialEpoch)
@@ -28,11 +30,15 @@ export function loadSource(tool: ArtplayerToolThumbnail, file?: File | null) {
const listeners: Cleanup[] = []
state.sourceListeners = listeners
let pendingCleanup = () => {}
let notificationTimer: ReturnType<typeof setTimeout> | undefined
const cancelNotification = () => clearTimeout(notificationTimer)
listeners.push(cancelNotification)
let reported = false
const error = () => {
if (!current() || reported || (video.currentSrc && video.currentSrc !== url))
return
reported = true
cancelNotification()
state.loading = false
const failure = new Error(`Unable to load video: media error ${video.error?.code || 0}`)
if (state.job)
@@ -79,8 +85,20 @@ export function loadSource(tool: ArtplayerToolThumbnail, file?: File | null) {
if (previous !== url)
revoke(state.sourceUrls, previous)
}
if (current())
tool.emit('video', video)
if (current() && !reported) {
if (delay !== null) {
notificationTimer = setTimeout(() => {
cancelNotification()
if (current() && !reported)
tool.emit('video', video)
}, delay)
if (!current() || reported)
cancelNotification()
}
else {
tool.emit('video', video)
}
}
}
catch (error) {
if (current()) {
@@ -9,11 +9,15 @@ export interface SheetOptions {
export interface InputOptions extends Partial<SheetOptions> {
fileInput?: Element
delay?: number
compatibility?: 'published-3.5' | 'workspace-4.4'
[name: string]: unknown
}
export interface RuntimeOptions extends SheetOptions {
fileInput: HTMLInputElement
delay?: number
compatibility?: 'published-3.5' | 'workspace-4.4'
[name: string]: unknown
}