fix(core): [CORE-24] preserve source playback intent and explicit pauses

This commit is contained in:
Harvey Zhao committed 2026-09-12 03:26:26 +08:00
1 parent b5e96664ee
commit 0218196df9
22 files changed
+12895 -36

No files matched your search

@@ -6,6 +6,9 @@ export interface SourceOperation {
scope: ResourceScope
assigned: boolean
acceptingEvents: boolean
pauseRevision: number
resumePlaying?: boolean
internalPause?: boolean
onAssigned?: () => void
onError?: (error: unknown) => void
active: () => boolean
@@ -15,6 +18,25 @@ const current = new WeakMap<object, SourceOperation>()
const assignments = new WeakMap<object, SourceOperation>()
const sources = new WeakMap<object, ResourceScope>()
export function sourcePlaybackIntent(owner: object): boolean | undefined {
const operation = current.get(owner)
return operation?.active() ? operation.resumePlaying : undefined
}
export function requestSourcePlayback(owner: object, playing: boolean): void {
const operation = current.get(owner)
if (!operation?.active() || operation.resumePlaying === undefined)
return
if (!playing && operation.internalPause) {
operation.internalPause = false
}
else {
operation.resumePlaying = playing
if (!playing)
operation.pauseRevision++
}
}
export function getSourceScope(owner: object): ResourceScope {
return sources.get(owner) || getScope(owner)
}
@@ -26,6 +48,13 @@ export function captureSource(owner: object): () => boolean {
&& sources.get(owner) === source && (!source || !source.closed)
}
export function captureSourcePlayback(owner: object): () => boolean {
const active = captureSource(owner)
const operation = current.get(owner)
const revision = operation?.pauseRevision
return () => active() && operation?.pauseRevision === revision
}
export function beginSource(owner: object): SourceOperation {
const previous = sources.get(owner)
const source = getScope(owner).child()
@@ -39,6 +68,7 @@ export function beginSource(owner: object): SourceOperation {
scope,
assigned: false,
acceptingEvents: false,
pauseRevision: 0,
active: () => !isClosing(owner) && !scope.closed && current.get(owner) === operation,
}
current.set(owner, operation)
+20 -9
View File
@@ -1,7 +1,7 @@
import type { SourceEvent, SourceListener, SwitchHost } from './types'
import type { SourceListener, SwitchHost } from './types'
import { isClosing } from '../lifecycle/instance'
import { listenSource } from './listen'
import { assignUrl, beginSource } from './operation'
import { assignUrl, beginSource, sourcePlaybackIntent } from './operation'
import { positionRestoration } from './restore-position'
export function switchSource(art: SwitchHost, url: string, currentTime: number): Promise<void> {
@@ -10,13 +10,16 @@ export function switchSource(art: SwitchHost, url: string, currentTime: number):
resolve()
return
}
const playing = sourcePlaybackIntent(art) ?? art.playing
const operation = beginSource(art)
operation.resumePlaying = playing
const scope = operation.scope.child()
let settled = false
const settle = (failed = false, error?: unknown) => {
if (settled)
return
settled = true
operation.resumePlaying = undefined
operation.onError = undefined
operation.onAssigned = undefined
scope.dispose()
@@ -34,8 +37,14 @@ export function switchSource(art: SwitchHost, url: string, currentTime: number):
const fail = (error: unknown) => settle(true, error)
operation.onError = fail
try {
const { playing, aspectRatio, playbackRate } = art
art.pause()
const { aspectRatio, playbackRate } = art
operation.internalPause = true
try {
art.pause()
}
finally {
operation.internalPause = false
}
if (!active())
return
@@ -45,10 +54,11 @@ export function switchSource(art: SwitchHost, url: string, currentTime: number):
let rateRestored = false
const position = positionRestoration(art, currentTime, active)
const resume = async () => {
const pauseRevision = operation.pauseRevision
art.aspectRatio = aspectRatio
if (!active())
return
if (playing) {
if (operation.resumePlaying) {
// The public play promise still rejects; this internal resume is best effort.
try {
await art.play()
@@ -57,7 +67,8 @@ export function switchSource(art: SwitchHost, url: string, currentTime: number):
}
if (!active())
return
art.notice.show = ''
if (operation.pauseRevision === pauseRevision)
art.notice.show = ''
settle()
}
const resumeWhenReady = () => {
@@ -103,8 +114,8 @@ export function switchSource(art: SwitchHost, url: string, currentTime: number):
'seek': position.manual,
}
const capture = scope.child()
const queued: [SourceEvent, unknown][] = []
const names = Object.keys(handlers) as SourceEvent[]
const queued: [keyof typeof handlers, unknown][] = []
const names = Object.keys(handlers) as (keyof typeof handlers)[]
for (const name of names) {
listenSource(capture, art, name, (event) => {
if (operation.acceptingEvents)
@@ -123,7 +134,7 @@ export function switchSource(art: SwitchHost, url: string, currentTime: number):
settle()
return
}
const dispatch = {} as Record<SourceEvent, SourceListener>
const dispatch = {} as Record<keyof typeof handlers, SourceListener>
for (const name of names)
dispatch[name] = listenSource(readiness, art, name, handlers[name], name !== 'video:seeked')
for (const [name, event] of queued) {