mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-21 04:31:20 -08:00
refactor(core): CORE-09 isolate source operations and settle cancelled switches
This commit is contained in:
1 parent
874dac9f22
commit
44f4e083a9
29 files changed
+4512
-255
No files matched your search
@@ -0,0 +1,21 @@
|
||||
import type ResourceScope from '../lifecycle/scope'
|
||||
import type { SourceEvent, SourceEvents, SourceListener } from './types'
|
||||
|
||||
export function listenSource(scope: ResourceScope, art: SourceEvents, name: SourceEvent, callback: SourceListener, once = true): SourceListener {
|
||||
let fired = false
|
||||
let release: () => void
|
||||
const guarded: SourceListener = (event) => {
|
||||
if (scope.closed || fired)
|
||||
return
|
||||
if (once) {
|
||||
fired = true
|
||||
release()
|
||||
}
|
||||
callback(event)
|
||||
}
|
||||
art[once ? 'once' : 'on'](name, guarded)
|
||||
release = scope.add(() => {
|
||||
art.off(name, guarded)
|
||||
})
|
||||
return guarded
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import type ResourceScope from '../lifecycle/scope'
|
||||
import type { UrlTarget } from './types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
|
||||
export interface SourceOperation {
|
||||
scope: ResourceScope
|
||||
assigned: boolean
|
||||
acceptingEvents: boolean
|
||||
onAssigned?: () => void
|
||||
onError?: (error: unknown) => void
|
||||
active: () => boolean
|
||||
}
|
||||
|
||||
const current = new WeakMap<object, SourceOperation>()
|
||||
const assignments = new WeakMap<object, SourceOperation>()
|
||||
|
||||
export function captureSource(owner: object): () => boolean {
|
||||
const operation = current.get(owner)
|
||||
return () => current.get(owner) === operation && (!operation || operation.active())
|
||||
}
|
||||
|
||||
export function beginSource(owner: object): SourceOperation {
|
||||
const previous = current.get(owner)
|
||||
const scope = getScope(owner).child()
|
||||
const operation: SourceOperation = {
|
||||
scope,
|
||||
assigned: false,
|
||||
acceptingEvents: false,
|
||||
active: () => !isClosing(owner) && !scope.closed && current.get(owner) === operation,
|
||||
}
|
||||
current.set(owner, operation)
|
||||
scope.add(() => {
|
||||
if (current.get(owner) === operation)
|
||||
current.delete(owner)
|
||||
operation.onAssigned = undefined
|
||||
operation.onError = undefined
|
||||
})
|
||||
previous?.scope.dispose()
|
||||
return operation
|
||||
}
|
||||
|
||||
export function takeAssignment(owner: object): SourceOperation {
|
||||
const operation = assignments.get(owner)
|
||||
assignments.delete(owner)
|
||||
return operation || beginSource(owner)
|
||||
}
|
||||
|
||||
export function finishAssignment(operation: SourceOperation): void {
|
||||
operation.assigned = true
|
||||
const callback = operation.onAssigned
|
||||
operation.onAssigned = undefined
|
||||
if (operation.active())
|
||||
callback?.()
|
||||
}
|
||||
|
||||
export function assignUrl(owner: UrlTarget, operation: SourceOperation, url: string): void {
|
||||
assignments.set(owner, operation)
|
||||
operation.acceptingEvents = true
|
||||
try {
|
||||
owner.url = url
|
||||
}
|
||||
finally {
|
||||
// Also support a host-provided URL setter without a core URL mixin.
|
||||
if (assignments.get(owner) === operation) {
|
||||
assignments.delete(owner)
|
||||
finishAssignment(operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function failSource(operation: SourceOperation, error: unknown): void {
|
||||
if (!operation.active())
|
||||
return
|
||||
if (operation.onError)
|
||||
operation.onError(error)
|
||||
else
|
||||
console.warn('Failed to initialize ArtPlayer source:', error)
|
||||
operation.scope.dispose()
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import type { SourceEvent, SourceListener, SwitchHost } from './types'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { listenSource } from './listen'
|
||||
import { assignUrl, beginSource } from './operation'
|
||||
|
||||
export function switchSource(art: SwitchHost, url: string, currentTime: number): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (isClosing(art) || url === art.url) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const operation = beginSource(art)
|
||||
const scope = operation.scope.child()
|
||||
let settled = false
|
||||
const settle = (failed = false, error?: unknown) => {
|
||||
if (settled)
|
||||
return
|
||||
settled = true
|
||||
operation.onError = undefined
|
||||
operation.onAssigned = undefined
|
||||
scope.dispose()
|
||||
if (failed)
|
||||
reject(error)
|
||||
else
|
||||
resolve()
|
||||
if (failed)
|
||||
operation.scope.dispose()
|
||||
}
|
||||
scope.add(() => {
|
||||
settle()
|
||||
})
|
||||
const active = () => !settled && operation.active()
|
||||
const fail = (error: unknown) => settle(true, error)
|
||||
operation.onError = fail
|
||||
try {
|
||||
const { playing, aspectRatio, playbackRate } = art
|
||||
art.pause()
|
||||
if (!active())
|
||||
return
|
||||
|
||||
const readiness = scope.child()
|
||||
const handlers = {
|
||||
'video:error': fail,
|
||||
'video:loadedmetadata': () => {
|
||||
if (!active())
|
||||
return
|
||||
try {
|
||||
art.currentTime = currentTime
|
||||
}
|
||||
catch (error) {
|
||||
fail(error)
|
||||
}
|
||||
},
|
||||
'video:canplay': () => {
|
||||
if (!active())
|
||||
return
|
||||
// Detach readiness handlers before awaiting a possibly pending play.
|
||||
readiness.dispose()
|
||||
const resume = async () => {
|
||||
art.playbackRate = playbackRate
|
||||
if (!active())
|
||||
return
|
||||
art.aspectRatio = aspectRatio
|
||||
if (!active())
|
||||
return
|
||||
if (playing) {
|
||||
// The public play promise still rejects; this internal resume is best effort.
|
||||
try {
|
||||
await art.play()
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
if (!active())
|
||||
return
|
||||
art.notice.show = ''
|
||||
settle()
|
||||
}
|
||||
void resume().catch(fail)
|
||||
},
|
||||
}
|
||||
const capture = scope.child()
|
||||
const queued: [SourceEvent, unknown][] = []
|
||||
const names = Object.keys(handlers) as SourceEvent[]
|
||||
for (const name of names) {
|
||||
listenSource(capture, art, name, (event) => {
|
||||
if (operation.acceptingEvents)
|
||||
queued.push([name, event])
|
||||
}, false)
|
||||
}
|
||||
assignUrl(art, operation, url)
|
||||
if (!active())
|
||||
return
|
||||
art.notice.show = ''
|
||||
if (!active())
|
||||
return
|
||||
const activate = () => {
|
||||
capture.dispose()
|
||||
if (!url) {
|
||||
settle()
|
||||
return
|
||||
}
|
||||
const dispatch = {} as Record<SourceEvent, SourceListener>
|
||||
for (const name of names)
|
||||
dispatch[name] = listenSource(readiness, art, name, handlers[name])
|
||||
for (const [name, event] of queued) {
|
||||
if (!active() || readiness.closed)
|
||||
break
|
||||
dispatch[name](event)
|
||||
}
|
||||
queued.length = 0
|
||||
}
|
||||
if (operation.assigned)
|
||||
activate()
|
||||
else
|
||||
operation.onAssigned = activate
|
||||
}
|
||||
catch (error) {
|
||||
fail(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export type SourceEvent = 'video:error' | 'video:loadedmetadata' | 'video:canplay'
|
||||
export type SourceListener = (event?: unknown) => void
|
||||
|
||||
export interface SourceEvents {
|
||||
on: (name: SourceEvent, callback: SourceListener) => unknown
|
||||
once: (name: SourceEvent, callback: SourceListener) => unknown
|
||||
off: (name: SourceEvent, callback: SourceListener) => unknown
|
||||
}
|
||||
|
||||
export interface SourceMedia {
|
||||
src: string | null
|
||||
}
|
||||
|
||||
export interface UrlTarget {
|
||||
url: string | null
|
||||
}
|
||||
|
||||
export interface UrlHost<Media extends SourceMedia, Host> extends UrlTarget, SourceEvents {
|
||||
template: { $video: Media }
|
||||
option: {
|
||||
url: string
|
||||
type: string
|
||||
customType: Record<string, ((this: Host, video: Media, url: string, art: Host) => unknown) | undefined>
|
||||
}
|
||||
loading: { set show(value: boolean) }
|
||||
isReady: boolean
|
||||
emit: (name: 'restart', url: string) => unknown
|
||||
}
|
||||
|
||||
export interface SwitchHost extends UrlTarget, SourceEvents {
|
||||
currentTime: number
|
||||
readonly playing: boolean
|
||||
aspectRatio: string
|
||||
playbackRate: number
|
||||
pause: () => unknown
|
||||
play: () => unknown
|
||||
notice: { set show(value: string) }
|
||||
}
|
||||
|
||||
export interface SwitchMethods {
|
||||
switchUrl: (url: string) => Promise<void>
|
||||
switchQuality: (url: string) => Promise<void>
|
||||
set switch(url: string)
|
||||
}
|
||||
Reference in new issue
Block a user