mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-17 01:56:16 -08:00
fix(dash): [PKG-DASH-05] synchronize SDK events and own refresh lifecycle
This commit is contained in:
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
|
||||
|
||||
Reference in new issue
Block a user