refactor(core): CORE-09 isolate source operations and settle cancelled switches

This commit is contained in:
Harvey Zhao committed 2026-09-11 02:34:26 +08:00
1 parent 874dac9f22
commit 44f4e083a9
29 files changed
+4512 -255

No files matched your search

+12 -1
View File
@@ -1,5 +1,6 @@
import type { PlayHost } from '../media/hosts'
import type { PlaybackMethods } from '../media/types'
import { captureSource } from '../source/operation'
import { def } from '../utils'
export default function playMix<Media extends Pick<PlaybackMethods, 'play'>>(art: PlayHost<Media>): asserts art is PlayHost<Media> & { play: () => Promise<Awaited<ReturnType<Media['play']>>> } {
@@ -13,12 +14,22 @@ export default function playMix<Media extends Pick<PlaybackMethods, 'play'>>(art
def(art, 'play', {
async value() {
const active = captureSource(art)
const result = await $video.play()
notice.show = i18n.get('Play')
if (!active())
return result
const message = i18n.get('Play')
if (!active())
return result
notice.show = message
if (!active())
return result
art.emit('play')
if (option.mutex) {
for (let index = 0; index < instances.length; index++) {
if (!active())
break
const instance = instances[index]!
if (instance !== art) {
instance.pause()
@@ -1,63 +0,0 @@
import { def, silencePromise } from '../utils'
export default function switchMix(art) {
function switchUrl(url, currentTime) {
return new Promise((resolve, reject) => {
if (url === art.url) {
resolve()
return
}
const { playing, aspectRatio, playbackRate } = art
art.pause()
art.url = url
art.notice.show = ''
const handlers = {}
handlers.error = (error) => {
art.off('video:canplay', handlers.canplay)
art.off('video:loadedmetadata', handlers.metadata)
reject(error)
}
handlers.metadata = () => {
art.currentTime = currentTime
}
handlers.canplay = async () => {
art.off('video:error', handlers.error)
art.playbackRate = playbackRate
art.aspectRatio = aspectRatio
if (playing) {
await silencePromise(art.play())
}
art.notice.show = ''
resolve()
}
art.once('video:error', handlers.error)
art.once('video:loadedmetadata', handlers.metadata)
art.once('video:canplay', handlers.canplay)
})
}
def(art, 'switchQuality', {
value: (url) => {
return switchUrl(url, art.currentTime)
},
})
def(art, 'switchUrl', {
value: (url) => {
return switchUrl(url, 0)
},
})
def(art, 'switch', {
set: art.switchUrl,
})
}
@@ -0,0 +1,10 @@
import type { SwitchHost, SwitchMethods } from '../source/types'
import { switchSource } from '../source/switch'
import { def } from '../utils'
export default function switchMix(art: SwitchHost): asserts art is SwitchHost & SwitchMethods {
const switchUrl = (url: string) => switchSource(art, url, 0)
def(art, 'switchQuality', { value: (url: string) => switchSource(art, url, art.currentTime) })
def(art, 'switchUrl', { value: switchUrl })
def(art, 'switch', { set: switchUrl })
}
-48
View File
@@ -1,48 +0,0 @@
import { getScope } from '../lifecycle/instance'
import { wait } from '../lifecycle/resources'
import { def, getExt } from '../utils'
export default function urlMix(art) {
const {
option,
template: { $video },
} = art
def(art, 'url', {
get() {
return $video.src
},
async set(newUrl) {
if (newUrl) {
const oldUrl = art.url
const typeName = option.type || getExt(newUrl)
const typeCallback = option.customType[typeName]
if (typeName && typeCallback) {
if (!await wait(getScope(art)) || getScope(art).closed)
return
art.loading.show = true
typeCallback.call(art, $video, newUrl, art)
}
else {
URL.revokeObjectURL(oldUrl)
$video.src = newUrl
}
if (oldUrl !== art.url) {
art.option.url = newUrl
if (art.isReady && oldUrl) {
art.once('video:canplay', () => {
art.emit('restart', newUrl)
})
}
}
}
else {
if (!await wait(getScope(art)) || getScope(art).closed)
return
art.loading.show = true
}
},
})
}
+87
View File
@@ -0,0 +1,87 @@
import type { SourceMedia, UrlHost } from '../source/types'
import { isClosing } from '../lifecycle/instance'
import { wait } from '../lifecycle/resources'
import { listenSource } from '../source/listen'
import { failSource, finishAssignment, takeAssignment } from '../source/operation'
import { def, getExt } from '../utils'
export default function urlMix<Media extends SourceMedia, Host extends UrlHost<Media, Host>>(art: Host): void {
const { option, template: { $video } } = art
def(art, 'url', {
get() {
return $video.src
},
async set(newUrl: string) {
if (isClosing(art))
return
const operation = takeAssignment(art)
operation.acceptingEvents = false
try {
if (!newUrl) {
if (await wait(operation.scope) && operation.active())
art.loading.show = true
return
}
const oldUrl = art.url
const typeName = option.type || getExt(newUrl)
const typeCallback = option.customType[typeName]
if (typeName && typeCallback) {
if (!await wait(operation.scope) || !operation.active())
return
art.loading.show = true
}
if (!operation.active())
return
// Proxies can synchronously dispatch readiness from their src setter.
const capture = operation.scope.child()
let ready = false
let failed = false
listenSource(capture, art, 'video:canplay', () => {
ready = !failed
})
listenSource(capture, art, 'video:error', () => {
failed = true
})
try {
operation.acceptingEvents = true
if (typeName && typeCallback) {
const result = typeCallback.call(art, $video, newUrl, art)
// Preserve immediate customType invocation semantics while owning rejections.
void Promise.resolve(result).catch(error => failSource(operation, error))
}
else {
if (oldUrl)
URL.revokeObjectURL(oldUrl)
if (!operation.active())
return
$video.src = newUrl
}
}
finally {
capture.dispose()
}
if (!operation.active())
return
if (oldUrl !== art.url) {
art.option.url = newUrl
if (operation.active() && art.isReady && oldUrl) {
const restart = () => {
if (operation.active())
art.emit('restart', newUrl)
}
if (ready)
restart()
else if (!failed)
listenSource(operation.scope, art, 'video:canplay', restart)
}
}
}
catch (error) {
failSource(operation, error)
}
finally {
finishAssignment(operation)
}
},
})
}