fix(dash): [PKG-DASH-05] synchronize SDK events and own refresh lifecycle

This commit is contained in:
Harvey Zhao committed 2026-09-12 06:34:31 +08:00
1 parent 2ad00730ee
commit a9cf50bc4e
24 files changed
+3913 -40

No files matched your search

@@ -5,6 +5,7 @@ import $audio from './audio.svg?raw'
import { audioModel, qualityModel } from './mapping'
import { createMenu } from './menu'
import $quality from './quality.svg?raw'
import { observeSDK } from './sdk-events'
export default function artplayerPluginDashControl<Level extends object = QualityLevel, Track extends object = AudioTrack>(option: Option<Level, Track> = {}) {
return (player: Artplayer): Result => {
@@ -17,6 +18,14 @@ export default function artplayerPluginDashControl<Level extends object = Qualit
const quality = createMenu<QualityItem>(art, 'dash-quality', $quality)
const audio = createMenu<AudioItem<Track>>(art, 'dash-audio', $audio)
const subscriptions: [EventName, Cleanup][] = []
const observer = observeSDK<Level, Track>({
active: dash => !closed && !art.isDestroy && art.dash === dash,
refresh: update,
reset() {
const version = ++revision
clear(() => version === revision)
},
})
function clear(current: Valid = () => true): void {
let failure: unknown
@@ -45,6 +54,9 @@ export default function artplayerPluginDashControl<Level extends object = Qualit
errorHandle(dash.getVideoElement() === $video, 'Cannot find instance of DASH from "art.dash"')
if (!current())
return
observer.bind(dash)
if (!valid())
return
const qualityConfig = option.quality || {}
const qualities = qualityModel(dash, qualityConfig, valid)
if (!valid())
@@ -60,11 +72,13 @@ export default function artplayerPluginDashControl<Level extends object = Qualit
catch (error) {
if (current()) {
const cleanupVersion = ++revision
try {
clear(() => revision === cleanupVersion)
}
catch (cleanupError) {
console.warn('ArtPlayer DASH cleanup failed:', cleanupError)
for (const cleanup of [observer.release, () => clear(() => revision === cleanupVersion)]) {
try {
cleanup()
}
catch (cleanupError) {
console.warn('ArtPlayer DASH cleanup failed:', cleanupError)
}
}
}
throw error
@@ -77,7 +91,7 @@ export default function artplayerPluginDashControl<Level extends object = Qualit
closed = true
revision++
let failure: unknown
const actions = [clear, ...subscriptions.splice(0).map(([name, callback]) => () => art.off(name, callback))]
const actions = [observer.release, clear, ...subscriptions.splice(0).map(([name, callback]) => () => art.off(name, callback))]
for (const action of actions) {
try {
action()
@@ -0,0 +1,159 @@
import type { Cleanup, Dash, SDKEventName } from './types'
interface Binding<Level extends object, Track extends object> {
dash: Dash<Level, Track>
off: NonNullable<Dash<Level, Track>['off']>
callbacks: [SDKEventName, Cleanup][]
epoch: number
queued: boolean
running: boolean
suspended: boolean
automatic: boolean
}
export function observeSDK<Level extends object, Track extends object>(options: {
active: (dash: Dash<Level, Track>) => boolean
refresh: Cleanup
reset: Cleanup
}) {
let binding: Binding<Level, Track> | undefined
const active = (record: Binding<Level, Track>) => binding === record && options.active(record.dash)
const automatic = (record: Binding<Level, Track>) => record.dash.getSettings().streaming.abr.autoSwitchBitrate.video
function release(): void {
const record = binding
if (!record)
return
binding = undefined
record.epoch++
let failure: unknown
for (const [name, callback] of record.callbacks.splice(0)) {
try {
record.off.call(record.dash, name, callback)
}
catch (error) {
failure ||= error
}
}
if (failure)
throw failure
}
function fail(record: Binding<Level, Track>, error: unknown): void {
if (binding === record) {
try {
release()
}
catch (cleanupError) {
console.warn('ArtPlayer DASH subscription cleanup failed:', cleanupError)
}
if (!binding && options.active(record.dash)) {
try {
options.reset()
}
catch (cleanupError) {
console.warn('ArtPlayer DASH cleanup failed:', cleanupError)
}
}
}
console.warn('ArtPlayer DASH refresh failed:', error)
}
function schedule(record: Binding<Level, Track>): void {
if (!active(record) || record.suspended || record.queued || record.running)
return
record.queued = true
const epoch = record.epoch
void Promise.resolve().then(() => {
if (!active(record) || record.epoch !== epoch || record.suspended)
return
record.queued = false
record.running = true
try {
record.automatic = automatic(record)
if (active(record))
options.refresh()
}
catch (error) {
fail(record, error)
}
finally {
record.running = false
}
})
}
function bind(dash: Dash<Level, Track>): void {
const previous = binding
if (previous?.dash === dash) {
const value = automatic(previous)
if (active(previous)) {
previous.automatic = value
previous.suspended = false
}
return
}
release()
// An off callback may synchronously install a newer binding.
if (binding || !options.active(dash) || typeof dash.on !== 'function' || typeof dash.off !== 'function')
return
const record: Binding<Level, Track> = { dash, off: dash.off, callbacks: [], epoch: 0, queued: false, running: false, suspended: false, automatic: false }
binding = record
const on = dash.on
try {
record.automatic = automatic(record)
const changed: SDKEventName[] = ['qualityChangeRequested', 'qualityChangeRendered', 'trackChangeRendered', 'streamUpdated', 'streamInitialized']
const entries: [SDKEventName, Cleanup][] = changed.map(name => [name, () => {
if (name === 'streamUpdated' || name === 'streamInitialized')
record.suspended = false
schedule(record)
}])
entries.push(['playbackTimeUpdated', () => {
if (!active(record) || record.suspended)
return
try {
const value = automatic(record)
if (active(record) && value !== record.automatic) {
record.automatic = value
schedule(record)
}
}
catch (error) {
fail(record, error)
}
}])
entries.push(['streamTeardownComplete', () => {
if (!active(record))
return
record.epoch++
record.queued = false
record.suspended = true
try {
options.reset()
}
catch (error) {
fail(record, error)
}
}])
for (const entry of entries) {
if (!active(record))
break
record.callbacks.push(entry)
on.call(dash, ...entry)
}
}
catch (error) {
if (binding === record) {
try {
release()
}
catch (cleanupError) {
console.warn('ArtPlayer DASH subscription cleanup failed:', cleanupError)
}
}
throw error
}
}
return { bind, release }
}
@@ -5,6 +5,7 @@ export type Identifier = string | number
export type Valid = () => boolean
export type Cleanup = () => void
export type EventName = 'ready' | 'restart' | 'destroy'
export type SDKEventName = 'qualityChangeRequested' | 'qualityChangeRendered' | 'trackChangeRendered' | 'streamUpdated' | 'streamInitialized' | 'playbackTimeUpdated' | 'streamTeardownComplete'
export interface QualityFields {
height?: number
@@ -15,6 +16,8 @@ export interface QualityFields {
export type AudioFields = AudioTrack
export interface Dash<Level extends object = object, Track extends object = object> {
on?: (name: SDKEventName, callback: Cleanup) => unknown
off?: (name: SDKEventName, callback: Cleanup) => unknown
getVideoElement: () => object | null
getSettings: () => { streaming: { abr: { autoSwitchBitrate: { video: boolean } } } }
updateSettings: (settings: { streaming: { abr: { autoSwitchBitrate: { video: boolean } } } }) => unknown