mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-18 10:41:20 -08:00
refactor(core): CORE-11 split media events and scope reconnect lifetimes
This commit is contained in:
1 parent
5e31274f1d
commit
b8039ad1f1
27 files changed
+4844
-475
No files matched your search
@@ -0,0 +1,13 @@
|
||||
import type { MediaEventHost } from './types'
|
||||
import config from '../../config'
|
||||
import { isClosing } from '../../lifecycle/instance'
|
||||
|
||||
export function forwardMediaEvents(art: MediaEventHost): void {
|
||||
const { proxy, template: { $video } } = art
|
||||
for (let index = 0; index < config.events.length; index++) {
|
||||
proxy($video, config.events[index]!, (event: Event) => {
|
||||
if (!isClosing(art))
|
||||
art.emit(`video:${event.type}`, event)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { MediaEventName, MediaEvents, MediaListener, MediaUI } from './types'
|
||||
import { getScope, isClosing } from '../../lifecycle/instance'
|
||||
|
||||
export function listenMedia(art: MediaEvents, name: MediaEventName, callback: MediaListener, once = false): void {
|
||||
const guarded: MediaListener = (event) => {
|
||||
if (!isClosing(art))
|
||||
callback(event)
|
||||
}
|
||||
art[once ? 'once' : 'on'](name, guarded)
|
||||
getScope(art).add(() => {
|
||||
art.off(name, guarded)
|
||||
})
|
||||
}
|
||||
|
||||
export function showMediaUI(art: MediaUI, changes: [keyof MediaUI, boolean][], active?: () => boolean): void {
|
||||
for (const [name, value] of changes) {
|
||||
if (isClosing(art) || (active && !active()))
|
||||
return
|
||||
art[name].show = value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { MediaEventHost, Reconnect } from './types'
|
||||
import { isClosing } from '../../lifecycle/instance'
|
||||
import { captureSource } from '../../source/operation'
|
||||
import { isMobile, setStyle, silencePromise } from '../../utils'
|
||||
import { listenMedia, showMediaUI } from './listen'
|
||||
|
||||
export function installEnded(art: MediaEventHost): void {
|
||||
const { option } = art
|
||||
listenMedia(art, 'video:ended', () => {
|
||||
if (option.loop) {
|
||||
const active = captureSource(art)
|
||||
art.seek = 0
|
||||
if (isClosing(art) || !active())
|
||||
return
|
||||
silencePromise(art.play())
|
||||
if (active())
|
||||
showMediaUI(art, [['controls', false], ['mask', false]], active)
|
||||
}
|
||||
else {
|
||||
showMediaUI(art, [['controls', true], ['mask', true]])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function installPlaybackUI(art: MediaEventHost, reconnect: Reconnect): void {
|
||||
const { template: { $poster } } = art
|
||||
listenMedia(art, 'video:loadedmetadata', () => {
|
||||
art.emit('resize')
|
||||
if (isMobile)
|
||||
showMediaUI(art, [['loading', false], ['controls', true], ['mask', true]])
|
||||
})
|
||||
listenMedia(art, 'video:loadstart', () => {
|
||||
reconnect.loadStart()
|
||||
showMediaUI(art, [['loading', true], ['mask', false], ['controls', true]])
|
||||
})
|
||||
listenMedia(art, 'video:pause', () => {
|
||||
showMediaUI(art, [['controls', true], ['mask', true]])
|
||||
})
|
||||
listenMedia(art, 'video:play', () => {
|
||||
showMediaUI(art, [['mask', false]])
|
||||
if (!isClosing(art))
|
||||
setStyle($poster, 'display', 'none')
|
||||
})
|
||||
listenMedia(art, 'video:playing', () => showMediaUI(art, [['mask', false]]))
|
||||
listenMedia(art, 'video:progress', () => {
|
||||
if (art.playing)
|
||||
showMediaUI(art, [['loading', false]])
|
||||
})
|
||||
listenMedia(art, 'video:seeked', () => showMediaUI(art, [['loading', false], ['mask', true]]))
|
||||
listenMedia(art, 'video:seeking', () => showMediaUI(art, [['loading', true], ['mask', false]]))
|
||||
listenMedia(art, 'video:timeupdate', () => showMediaUI(art, [['mask', false]]))
|
||||
listenMedia(art, 'video:waiting', () => showMediaUI(art, [['loading', true], ['mask', false]]))
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { MediaEventHost, Reconnect } from './types'
|
||||
import { isClosing } from '../../lifecycle/instance'
|
||||
import { listenMedia, showMediaUI } from './listen'
|
||||
|
||||
export function installReadiness(art: MediaEventHost, reconnect: Reconnect): void {
|
||||
listenMedia(art, 'video:canplay', () => {
|
||||
reconnect.reset()
|
||||
showMediaUI(art, [['loading', false]])
|
||||
})
|
||||
listenMedia(art, 'video:canplay', () => {
|
||||
showMediaUI(art, [['loading', false], ['controls', true], ['mask', true]])
|
||||
if (isClosing(art))
|
||||
return
|
||||
art.isReady = true
|
||||
art.emit('ready')
|
||||
}, true)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type ResourceScope from '../../lifecycle/scope'
|
||||
import type { MediaEventHost, Reconnect } from './types'
|
||||
import { isClosing } from '../../lifecycle/instance'
|
||||
import { wait } from '../../lifecycle/resources'
|
||||
import { assignUrl, beginSource, getSourceScope } from '../../source/operation'
|
||||
import { addClass, removeClass } from '../../utils'
|
||||
import { showMediaUI } from './listen'
|
||||
|
||||
export function createReconnect(art: MediaEventHost): Reconnect {
|
||||
const { option, constructor, i18n, notice, template: { $player } } = art
|
||||
let source = getSourceScope(art)
|
||||
let attempts = 0
|
||||
let pending: ResourceScope | undefined
|
||||
|
||||
const run = async (error: unknown) => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const current = getSourceScope(art)
|
||||
if (current !== source) {
|
||||
source = current
|
||||
attempts = 0
|
||||
}
|
||||
if (pending && !pending.closed)
|
||||
return
|
||||
const attempt = current.child()
|
||||
pending = attempt
|
||||
attempt.add(() => {
|
||||
if (pending === attempt)
|
||||
pending = undefined
|
||||
})
|
||||
const active = () => !isClosing(art) && !attempt.closed && current === getSourceScope(art)
|
||||
const retry = attempts < constructor.RECONNECT_TIME_MAX
|
||||
try {
|
||||
if (!retry) {
|
||||
showMediaUI(art, [['mask', true], ['loading', false], ['controls', true]], active)
|
||||
if (!active())
|
||||
return
|
||||
addClass($player, 'art-error')
|
||||
}
|
||||
if (!await wait(attempt, constructor.RECONNECT_SLEEP_TIME) || !active())
|
||||
return
|
||||
if (retry) {
|
||||
const url = option.url
|
||||
if (!active())
|
||||
return
|
||||
attempts += 1
|
||||
const attemptNumber = attempts
|
||||
// End the old timer before creating the retry's new source generation.
|
||||
attempt.dispose()
|
||||
const operation = beginSource(art)
|
||||
source = getSourceScope(art)
|
||||
assignUrl(art, operation, url)
|
||||
if (!operation.active())
|
||||
return
|
||||
const message = `${i18n.get('Reconnect')}: ${attemptNumber}`
|
||||
if (!operation.active())
|
||||
return
|
||||
notice.show = message
|
||||
if (operation.active())
|
||||
art.emit('error', error, attemptNumber)
|
||||
}
|
||||
else {
|
||||
const message = i18n.get('Video Load Failed')
|
||||
if (active())
|
||||
notice.show = message
|
||||
}
|
||||
}
|
||||
finally {
|
||||
attempt.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
reset() {
|
||||
attempts = 0
|
||||
pending?.dispose()
|
||||
removeClass($player, 'art-error')
|
||||
},
|
||||
loadStart() {
|
||||
removeClass($player, 'art-error')
|
||||
},
|
||||
schedule(error) {
|
||||
void run(error).catch((failure) => {
|
||||
console.warn('ArtPlayer reconnect failed:', failure)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
export type MediaEventName = `video:${string}`
|
||||
export type MediaListener = (event?: unknown) => void
|
||||
|
||||
export interface MediaEvents {
|
||||
on: (name: MediaEventName, callback: MediaListener) => unknown
|
||||
once: (name: MediaEventName, callback: MediaListener) => unknown
|
||||
off: (name: MediaEventName, callback: MediaListener) => unknown
|
||||
emit: {
|
||||
(name: MediaEventName, event: Event): unknown
|
||||
(name: 'ready' | 'resize'): unknown
|
||||
(name: 'error', error: unknown, attempt: number): unknown
|
||||
}
|
||||
}
|
||||
|
||||
export interface MediaUI {
|
||||
loading: { set show(value: boolean) }
|
||||
controls: { set show(value: boolean) }
|
||||
mask: { set show(value: boolean) }
|
||||
}
|
||||
|
||||
export interface MediaEventHost extends MediaEvents, MediaUI {
|
||||
template: {
|
||||
$video: EventTarget
|
||||
$player: Pick<HTMLElement, 'classList'>
|
||||
$poster: Pick<HTMLElement, 'style'>
|
||||
}
|
||||
proxy: (target: EventTarget, name: string, callback: (event: Event) => void) => unknown
|
||||
isReady: boolean
|
||||
readonly playing: boolean
|
||||
url: string | null
|
||||
set seek(time: number)
|
||||
play: () => unknown
|
||||
option: { url: string, loop: boolean }
|
||||
i18n: { get: (key: string) => string }
|
||||
notice: { set show(message: string) }
|
||||
constructor: { RECONNECT_TIME_MAX: number, RECONNECT_SLEEP_TIME: number }
|
||||
}
|
||||
|
||||
export interface Reconnect {
|
||||
reset: () => void
|
||||
loadStart: () => void
|
||||
schedule: (error: unknown) => void
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
import config from '../config'
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import { wait } from '../lifecycle/resources'
|
||||
import { addClass, isMobile, setStyle, silencePromise } from '../utils'
|
||||
|
||||
export default function eventInit(art) {
|
||||
const {
|
||||
i18n,
|
||||
notice,
|
||||
option,
|
||||
constructor,
|
||||
proxy,
|
||||
template: { $player, $video, $poster },
|
||||
} = art
|
||||
|
||||
let reconnectTime = 0
|
||||
|
||||
for (let index = 0; index < config.events.length; index++) {
|
||||
proxy($video, config.events[index], (event) => {
|
||||
art.emit(`video:${event.type}`, event)
|
||||
})
|
||||
}
|
||||
|
||||
// art.on('video:abort', () => {
|
||||
|
||||
// });
|
||||
|
||||
art.on('video:canplay', () => {
|
||||
reconnectTime = 0
|
||||
art.loading.show = false
|
||||
})
|
||||
|
||||
art.once('video:canplay', () => {
|
||||
art.loading.show = false
|
||||
art.controls.show = true
|
||||
art.mask.show = true
|
||||
art.isReady = true
|
||||
art.emit('ready')
|
||||
})
|
||||
|
||||
// art.on('video:canplaythrough', () => {
|
||||
|
||||
// });
|
||||
|
||||
// art.on('video:durationchange', () => {
|
||||
|
||||
// });
|
||||
|
||||
// art.on('video:emptied', () => {
|
||||
|
||||
// });
|
||||
|
||||
art.on('video:ended', () => {
|
||||
if (option.loop) {
|
||||
art.seek = 0
|
||||
silencePromise(art.play())
|
||||
art.controls.show = false
|
||||
art.mask.show = false
|
||||
}
|
||||
else {
|
||||
art.controls.show = true
|
||||
art.mask.show = true
|
||||
}
|
||||
})
|
||||
|
||||
art.on('video:error', async (error) => {
|
||||
if (reconnectTime < constructor.RECONNECT_TIME_MAX) {
|
||||
if (!await wait(getScope(art), constructor.RECONNECT_SLEEP_TIME) || getScope(art).closed)
|
||||
return
|
||||
reconnectTime += 1
|
||||
art.url = option.url
|
||||
notice.show = `${i18n.get('Reconnect')}: ${reconnectTime}`
|
||||
art.emit('error', error, reconnectTime)
|
||||
}
|
||||
else {
|
||||
art.mask.show = true
|
||||
art.loading.show = false
|
||||
art.controls.show = true
|
||||
addClass($player, 'art-error')
|
||||
if (!await wait(getScope(art), constructor.RECONNECT_SLEEP_TIME) || getScope(art).closed)
|
||||
return
|
||||
notice.show = i18n.get('Video Load Failed')
|
||||
}
|
||||
})
|
||||
|
||||
// art.on('video:loadeddata', () => {
|
||||
|
||||
// });
|
||||
|
||||
art.on('video:loadedmetadata', () => {
|
||||
art.emit('resize')
|
||||
if (isMobile) {
|
||||
art.loading.show = false
|
||||
art.controls.show = true
|
||||
art.mask.show = true
|
||||
}
|
||||
})
|
||||
|
||||
art.on('video:loadstart', () => {
|
||||
art.loading.show = true
|
||||
art.mask.show = false
|
||||
art.controls.show = true
|
||||
})
|
||||
|
||||
art.on('video:pause', () => {
|
||||
art.controls.show = true
|
||||
art.mask.show = true
|
||||
})
|
||||
|
||||
art.on('video:play', () => {
|
||||
art.mask.show = false
|
||||
setStyle($poster, 'display', 'none')
|
||||
})
|
||||
|
||||
art.on('video:playing', () => {
|
||||
art.mask.show = false
|
||||
})
|
||||
|
||||
art.on('video:progress', () => {
|
||||
if (art.playing) {
|
||||
art.loading.show = false
|
||||
}
|
||||
})
|
||||
|
||||
// art.on('video:ratechange', () => {
|
||||
|
||||
// });
|
||||
|
||||
art.on('video:seeked', () => {
|
||||
art.loading.show = false
|
||||
art.mask.show = true
|
||||
})
|
||||
|
||||
art.on('video:seeking', () => {
|
||||
art.loading.show = true
|
||||
art.mask.show = false
|
||||
})
|
||||
|
||||
// art.on('video:stalled', () => {
|
||||
|
||||
// });
|
||||
|
||||
// art.on('video:suspend', () => {
|
||||
|
||||
// });
|
||||
|
||||
art.on('video:timeupdate', () => {
|
||||
art.mask.show = false
|
||||
})
|
||||
|
||||
// art.on('video:volumechange', () => {
|
||||
|
||||
// });
|
||||
|
||||
art.on('video:waiting', () => {
|
||||
art.loading.show = true
|
||||
art.mask.show = false
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { MediaEventHost } from '../media/events/types'
|
||||
import { forwardMediaEvents } from '../media/events/forward'
|
||||
import { listenMedia } from '../media/events/listen'
|
||||
import { installEnded, installPlaybackUI } from '../media/events/playback'
|
||||
import { installReadiness } from '../media/events/readiness'
|
||||
import { createReconnect } from '../media/events/reconnect'
|
||||
|
||||
export default function eventInit(art: MediaEventHost): void {
|
||||
forwardMediaEvents(art)
|
||||
const reconnect = createReconnect(art)
|
||||
installReadiness(art, reconnect)
|
||||
installEnded(art)
|
||||
listenMedia(art, 'video:error', reconnect.schedule)
|
||||
installPlaybackUI(art, reconnect)
|
||||
}
|
||||
@@ -13,15 +13,28 @@ export interface SourceOperation {
|
||||
|
||||
const current = new WeakMap<object, SourceOperation>()
|
||||
const assignments = new WeakMap<object, SourceOperation>()
|
||||
const sources = new WeakMap<object, ResourceScope>()
|
||||
|
||||
export function getSourceScope(owner: object): ResourceScope {
|
||||
return sources.get(owner) || getScope(owner)
|
||||
}
|
||||
|
||||
export function captureSource(owner: object): () => boolean {
|
||||
const operation = current.get(owner)
|
||||
const source = sources.get(owner)
|
||||
return () => current.get(owner) === operation && (!operation || operation.active())
|
||||
&& sources.get(owner) === source && (!source || !source.closed)
|
||||
}
|
||||
|
||||
export function beginSource(owner: object): SourceOperation {
|
||||
const previous = current.get(owner)
|
||||
const scope = getScope(owner).child()
|
||||
const previous = sources.get(owner)
|
||||
const source = getScope(owner).child()
|
||||
const scope = source.child()
|
||||
sources.set(owner, source)
|
||||
source.add(() => {
|
||||
if (sources.get(owner) === source)
|
||||
sources.delete(owner)
|
||||
})
|
||||
const operation: SourceOperation = {
|
||||
scope,
|
||||
assigned: false,
|
||||
@@ -35,7 +48,7 @@ export function beginSource(owner: object): SourceOperation {
|
||||
operation.onAssigned = undefined
|
||||
operation.onError = undefined
|
||||
})
|
||||
previous?.scope.dispose()
|
||||
previous?.dispose()
|
||||
return operation
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user