mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-25 08:01:20 -08:00
refactor(auto-thumbnail): [PKG-AUTO-THUMB-03] type internal extraction contracts
This commit is contained in:
1 parent
00e81309ca
commit
6c989deccd
20 files changed
+3968
-48
No files matched your search
+2
-1
@@ -1,8 +1,9 @@
|
||||
import type { ExtractionConfig, ExtractionJob } from './types'
|
||||
import createFrameReader from './frames'
|
||||
import { sheetSize } from './options'
|
||||
import createVideo from './video'
|
||||
|
||||
export default function extract(job, config) {
|
||||
export default function extract(job: ExtractionJob, config: ExtractionConfig) {
|
||||
const video = createVideo(job)
|
||||
if (!job.active())
|
||||
return
|
||||
+19
-5
@@ -1,9 +1,20 @@
|
||||
import type { ExtractionJob } from './types'
|
||||
import { cleanupAll } from './session'
|
||||
|
||||
export default function createFrameReader(job, video) {
|
||||
interface PendingFrame {
|
||||
frame: number | null
|
||||
timer: ReturnType<typeof setTimeout> | null
|
||||
sought: boolean
|
||||
presented: boolean
|
||||
retries: number
|
||||
sequence: number
|
||||
started: boolean
|
||||
}
|
||||
|
||||
export default function createFrameReader(job: ExtractionJob, video: HTMLVideoElement) {
|
||||
const presentedFrames = typeof video.requestVideoFrameCallback === 'function'
|
||||
&& typeof video.cancelVideoFrameCallback === 'function'
|
||||
let pending
|
||||
let pending: PendingFrame | undefined
|
||||
|
||||
function clear() {
|
||||
const previous = pending
|
||||
@@ -13,7 +24,10 @@ export default function createFrameReader(job, video) {
|
||||
if (!previous)
|
||||
return
|
||||
const result = cleanupAll([
|
||||
() => clearTimeout(previous.timer),
|
||||
() => {
|
||||
if (previous.timer !== null)
|
||||
clearTimeout(previous.timer)
|
||||
},
|
||||
() => {
|
||||
if (previous.frame !== null)
|
||||
video.cancelVideoFrameCallback(previous.frame)
|
||||
@@ -24,11 +38,11 @@ export default function createFrameReader(job, video) {
|
||||
}
|
||||
job.own(clear)
|
||||
|
||||
return (target, draw) => {
|
||||
return (target: number, draw: () => void) => {
|
||||
clear()
|
||||
if (!job.active())
|
||||
return
|
||||
const current = { frame: null, timer: null, sought: false, presented: !presentedFrames, retries: 0, sequence: 0, started: false }
|
||||
const current: PendingFrame = { frame: null, timer: null, sought: false, presented: !presentedFrames, retries: 0, sequence: 0, started: false }
|
||||
pending = current
|
||||
const active = () => job.active() && pending === current
|
||||
const finish = job.guard(() => {
|
||||
+7
-5
@@ -1,14 +1,15 @@
|
||||
import type { Cleanup, ThumbnailHost, ThumbnailOptions } from './types'
|
||||
import extract from './extraction'
|
||||
import { readOptions } from './options'
|
||||
import createSession, { cleanupAll } from './session'
|
||||
|
||||
export default function artplayerPluginAutoThumbnail(option) {
|
||||
return async (art) => {
|
||||
const report = error => console.warn('ArtPlayer auto-thumbnail failed:', error)
|
||||
export default function artplayerPluginAutoThumbnail(option: ThumbnailOptions) {
|
||||
return async (art: ThumbnailHost) => {
|
||||
const report = (error: unknown) => console.warn('ArtPlayer auto-thumbnail failed:', error)
|
||||
const session = createSession((config) => {
|
||||
art.thumbnails = config
|
||||
}, report)
|
||||
const subscriptions = []
|
||||
const subscriptions: Cleanup[] = []
|
||||
const onMetadata = () => {
|
||||
const job = session.start()
|
||||
if (job) {
|
||||
@@ -26,7 +27,8 @@ export default function artplayerPluginAutoThumbnail(option) {
|
||||
report(result.failure)
|
||||
}
|
||||
try {
|
||||
for (const [name, callback] of [['destroy', onDestroy], ['restart', session.cancel], ['video:loadedmetadata', onMetadata]]) {
|
||||
const events: [string, Cleanup][] = [['destroy', onDestroy], ['restart', session.cancel], ['video:loadedmetadata', onMetadata]]
|
||||
for (const [name, callback] of events) {
|
||||
if (session.closed)
|
||||
break
|
||||
subscriptions.push(() => art.off(name, callback))
|
||||
+4
-2
@@ -1,4 +1,6 @@
|
||||
export function readOptions(option, getFallbackUrl) {
|
||||
import type { ExtractionConfig, ThumbnailOptions } from './types'
|
||||
|
||||
export function readOptions(option: ThumbnailOptions, getFallbackUrl: () => string): ExtractionConfig {
|
||||
const config = {
|
||||
url: option.url || getFallbackUrl(),
|
||||
width: option.width || 160,
|
||||
@@ -10,7 +12,7 @@ export function readOptions(option, getFallbackUrl) {
|
||||
return config
|
||||
}
|
||||
|
||||
export function sheetSize(config, video) {
|
||||
export function sheetSize(config: ExtractionConfig, video: Pick<HTMLVideoElement, 'duration' | 'videoWidth' | 'videoHeight'>) {
|
||||
if (!Number.isFinite(video.duration) || video.duration <= 0 || !Number.isFinite(video.videoWidth) || video.videoWidth <= 0 || !Number.isFinite(video.videoHeight) || video.videoHeight <= 0)
|
||||
throw new TypeError('Auto-thumbnail requires finite media duration and dimensions')
|
||||
const height = Math.floor(config.width * video.videoHeight / video.videoWidth)
|
||||
+13
-11
@@ -1,5 +1,7 @@
|
||||
export function cleanupAll(actions) {
|
||||
let failure
|
||||
import type { Cleanup, ExtractionJob, ThumbnailSheet } from './types'
|
||||
|
||||
export function cleanupAll(actions: Cleanup[]) {
|
||||
let failure: unknown
|
||||
let failed = false
|
||||
for (const action of actions) {
|
||||
try {
|
||||
@@ -15,14 +17,14 @@ export function cleanupAll(actions) {
|
||||
return { failed, failure }
|
||||
}
|
||||
|
||||
export default function createSession(publish, report) {
|
||||
export default function createSession(publish: (sheet: ThumbnailSheet) => void, report: (error: unknown) => void) {
|
||||
let closed = false
|
||||
let current
|
||||
let lastUrl
|
||||
const urls = new Set()
|
||||
let current: ExtractionJob | undefined
|
||||
let lastUrl: string | undefined
|
||||
const urls = new Set<string>()
|
||||
|
||||
function release(url) {
|
||||
if (urls.delete(url))
|
||||
function release(url: string | undefined) {
|
||||
if (url !== undefined && urls.delete(url))
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
@@ -47,9 +49,9 @@ export default function createSession(publish, report) {
|
||||
return
|
||||
// Install the replacement before cleanup, which can reenter through host APIs.
|
||||
const previous = current
|
||||
const actions = []
|
||||
const actions: Cleanup[] = []
|
||||
let disposed = false
|
||||
const job = {
|
||||
const job: ExtractionJob = {
|
||||
active: () => !closed && !disposed && current === job,
|
||||
own(action) {
|
||||
if (disposed) {
|
||||
@@ -107,7 +109,7 @@ export default function createSession(publish, report) {
|
||||
}
|
||||
catch (error) {
|
||||
if (lastUrl === url)
|
||||
lastUrl = urls.has(previousUrl) ? previousUrl : undefined
|
||||
lastUrl = previousUrl !== undefined && urls.has(previousUrl) ? previousUrl : undefined
|
||||
const result = cleanupAll([() => release(url)])
|
||||
if (result.failed)
|
||||
report(result.failure)
|
||||
@@ -0,0 +1,43 @@
|
||||
// Internal contracts only. Public declarations retain their historical entrypoint.
|
||||
export interface ThumbnailOptions {
|
||||
url?: string
|
||||
width?: number
|
||||
number?: number
|
||||
scale?: number
|
||||
height?: number
|
||||
}
|
||||
|
||||
export interface ExtractionConfig {
|
||||
url: string
|
||||
width: number
|
||||
number: number
|
||||
scale: number
|
||||
}
|
||||
|
||||
export interface SheetConfig {
|
||||
height: number
|
||||
column: number
|
||||
number: number
|
||||
width: number
|
||||
scale: number
|
||||
}
|
||||
|
||||
export interface ThumbnailSheet extends SheetConfig { url: string }
|
||||
export type Cleanup = () => void
|
||||
|
||||
export interface ExtractionJob {
|
||||
active: () => boolean
|
||||
own: (action: Cleanup) => void
|
||||
dispose: Cleanup
|
||||
fail: (error: unknown) => void
|
||||
guard: <Args extends unknown[], Result>(callback: (...args: Args) => Result) => (...args: Args) => Result | undefined
|
||||
publish: (blob: Blob | null, config: SheetConfig) => void
|
||||
}
|
||||
|
||||
export interface ThumbnailHost {
|
||||
option: { url: string }
|
||||
// A host may already hold a partial user configuration; generated sheets are complete.
|
||||
thumbnails: { url: string } & Partial<SheetConfig>
|
||||
on: (event: string, callback: Cleanup) => unknown
|
||||
off: (event: string, callback: Cleanup) => unknown
|
||||
}
|
||||
+4
-2
@@ -1,10 +1,12 @@
|
||||
export default function createVideo(job) {
|
||||
import type { ExtractionJob } from './types'
|
||||
|
||||
export default function createVideo(job: ExtractionJob) {
|
||||
const video = document.createElement('video')
|
||||
job.own(() => video.remove())
|
||||
job.own(() => video.load())
|
||||
job.own(() => video.removeAttribute('src'))
|
||||
job.own(() => video.pause())
|
||||
for (const property of ['onloadedmetadata', 'onloadeddata', 'onseeked', 'onerror'])
|
||||
for (const property of ['onloadedmetadata', 'onloadeddata', 'onseeked', 'onerror'] as const)
|
||||
job.own(() => { video[property] = null })
|
||||
if (!job.active())
|
||||
return video
|
||||
Reference in new issue
Block a user