mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-25 08:01:20 -08:00
refactor(mediabunny): [PKG-MB-07] own HLS selection intent and menu lifecycle
This commit is contained in:
1 parent
ccee9157a7
commit
8a2b56e829
27 files changed
+4217
-680
No files matched your search
@@ -199,14 +199,15 @@ export default class MediaBunnyEngine implements EnginePort {
|
||||
|
||||
async getHlsState() {
|
||||
const media = this.media
|
||||
const source = this.loadSeq
|
||||
if (this.destroyed)
|
||||
return null
|
||||
try {
|
||||
const state = await getHlsState(media)
|
||||
return !this.destroyed && this.media === media ? state : null
|
||||
return !this.destroyed && this.loadSeq === source && this.media === media ? state : null
|
||||
}
|
||||
catch (error) {
|
||||
if (this.destroyed || this.media !== media)
|
||||
if (this.destroyed || this.loadSeq !== source || this.media !== media)
|
||||
return null
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface EngineOptions {
|
||||
|
||||
// Stable coordinator-facing surface used by the public shim.
|
||||
export interface EnginePort {
|
||||
readonly loadSeq: number
|
||||
readonly destroyed: boolean
|
||||
readonly currentTime: number
|
||||
readonly duration: number
|
||||
readonly videoWidth: number
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { Input } from 'mediabunny'
|
||||
import type { PlaybackMedia } from './media'
|
||||
|
||||
export type SelectedTracks = Pick<PlaybackMedia, 'videoTrack' | 'audioTrack' | 'videoMode' | 'audioMode'>
|
||||
|
||||
interface SelectionHost {
|
||||
input: Input | null
|
||||
media: PlaybackMedia | null
|
||||
@@ -10,65 +9,86 @@ interface SelectionHost {
|
||||
loadSeq: number
|
||||
replaceTracks: (tracks: SelectedTracks) => Promise<void>
|
||||
}
|
||||
type ResolveTracks = (input: Input, media: PlaybackMedia, value: unknown, current: () => boolean) => Promise<SelectedTracks | null>
|
||||
|
||||
export async function selectQuality(host: SelectionHost, value: unknown): Promise<void> {
|
||||
const { input, media, loadSeq } = host
|
||||
if (!media?.isHls || !input || host.destroyed)
|
||||
const requests = new WeakMap<SelectionHost, object>()
|
||||
async function select(host: SelectionHost, value: unknown, resolve: ResolveTracks): Promise<void> {
|
||||
const { input, loadSeq } = host
|
||||
if (!host.media?.isHls || !input || host.destroyed)
|
||||
return
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && host.media === media
|
||||
let selection: SelectedTracks
|
||||
try {
|
||||
const tracks = await input.getVideoTracks()
|
||||
if (!current())
|
||||
const token = {}
|
||||
requests.set(host, token)
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && requests.get(host) === token
|
||||
while (current()) {
|
||||
const media: PlaybackMedia | null = host.media
|
||||
if (!media?.isHls)
|
||||
return
|
||||
const videoTrack = value === 'auto' ? await input.getPrimaryVideoTrack() : tracks.find(track => track.id === value) ?? media.videoTrack
|
||||
if (!current() || !videoTrack)
|
||||
return
|
||||
let audioTrack = media.audioTrack
|
||||
let audioMode = media.audioMode
|
||||
if (!audioTrack || !videoTrack.canBePairedWith(audioTrack)) {
|
||||
audioTrack = await videoTrack.getPrimaryPairableAudioTrack()
|
||||
audioMode = 'auto'
|
||||
let selection: SelectedTracks | null
|
||||
try {
|
||||
selection = await resolve(input, media, value, current)
|
||||
}
|
||||
catch (error) {
|
||||
if (!current())
|
||||
return
|
||||
if (host.media === media)
|
||||
throw error
|
||||
continue
|
||||
}
|
||||
selection = { videoTrack, audioTrack, videoMode: value === 'auto' ? 'auto' : 'manual', audioMode }
|
||||
}
|
||||
catch (error) {
|
||||
if (!current())
|
||||
return
|
||||
throw error
|
||||
if (host.media !== media)
|
||||
continue
|
||||
if (selection) {
|
||||
try {
|
||||
await host.replaceTracks(selection)
|
||||
}
|
||||
catch (error) {
|
||||
if (current())
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if (current())
|
||||
await host.replaceTracks(selection)
|
||||
}
|
||||
|
||||
export async function selectAudio(host: SelectionHost, value: unknown): Promise<void> {
|
||||
const { input, media, loadSeq } = host
|
||||
if (!media?.isHls || !input || host.destroyed)
|
||||
return
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && host.media === media
|
||||
let selection: SelectedTracks
|
||||
try {
|
||||
const tracks = await input.getAudioTracks()
|
||||
if (!current())
|
||||
return
|
||||
const audioTrack = value === 'auto'
|
||||
? media.videoTrack ? await media.videoTrack.getPrimaryPairableAudioTrack() : await input.getPrimaryAudioTrack()
|
||||
: tracks.find(track => track.id === value) ?? media.audioTrack
|
||||
if (!current() || !audioTrack)
|
||||
return
|
||||
let videoTrack = media.videoTrack
|
||||
let videoMode = media.videoMode
|
||||
if (!videoTrack || !audioTrack.canBePairedWith(videoTrack)) {
|
||||
videoTrack = await audioTrack.getPrimaryPairableVideoTrack()
|
||||
videoMode = 'auto'
|
||||
}
|
||||
selection = { videoTrack, audioTrack, videoMode, audioMode: value === 'auto' ? 'auto' : 'manual' }
|
||||
async function quality(input: Input, media: PlaybackMedia, value: unknown, current: () => boolean): Promise<SelectedTracks | null> {
|
||||
const tracks = await input.getVideoTracks()
|
||||
if (!current())
|
||||
return null
|
||||
const videoTrack = value === 'auto' ? await input.getPrimaryVideoTrack() : tracks.find(track => track.id === value) ?? media.videoTrack
|
||||
if (!current() || !videoTrack)
|
||||
return null
|
||||
let audioTrack = media.audioTrack
|
||||
let audioMode = media.audioMode
|
||||
if (!audioTrack || !videoTrack.canBePairedWith(audioTrack)) {
|
||||
audioTrack = await videoTrack.getPrimaryPairableAudioTrack()
|
||||
audioMode = 'auto'
|
||||
}
|
||||
catch (error) {
|
||||
if (!current())
|
||||
return
|
||||
throw error
|
||||
}
|
||||
if (current())
|
||||
await host.replaceTracks(selection)
|
||||
return { videoTrack, audioTrack, videoMode: value === 'auto' ? 'auto' : 'manual', audioMode }
|
||||
}
|
||||
|
||||
async function audio(input: Input, media: PlaybackMedia, value: unknown, current: () => boolean): Promise<SelectedTracks | null> {
|
||||
const tracks = await input.getAudioTracks()
|
||||
if (!current())
|
||||
return null
|
||||
const audioTrack = value === 'auto'
|
||||
? media.videoTrack ? await media.videoTrack.getPrimaryPairableAudioTrack() : await input.getPrimaryAudioTrack()
|
||||
: tracks.find(track => track.id === value) ?? media.audioTrack
|
||||
if (!current() || !audioTrack)
|
||||
return null
|
||||
let videoTrack = media.videoTrack
|
||||
let videoMode = media.videoMode
|
||||
if (!videoTrack || !audioTrack.canBePairedWith(videoTrack)) {
|
||||
videoTrack = await audioTrack.getPrimaryPairableVideoTrack()
|
||||
videoMode = 'auto'
|
||||
}
|
||||
return { videoTrack, audioTrack, videoMode, audioMode: value === 'auto' ? 'auto' : 'manual' }
|
||||
}
|
||||
|
||||
export function selectQuality(host: SelectionHost, value: unknown): Promise<void> {
|
||||
return select(host, value, quality)
|
||||
}
|
||||
|
||||
export function selectAudio(host: SelectionHost, value: unknown): Promise<void> {
|
||||
return select(host, value, audio)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { DisplayConfig, Label, MenuHost, MenuModel, SelectorItem } from './m3u8-types'
|
||||
|
||||
export function releaseAll(actions: (() => void)[]): void {
|
||||
const failures: unknown[] = []
|
||||
for (const action of actions) {
|
||||
try {
|
||||
action()
|
||||
}
|
||||
catch (error) { failures.push(error) }
|
||||
}
|
||||
if (failures.length)
|
||||
throw failures[0]
|
||||
}
|
||||
|
||||
export default function createMenu(art: MenuHost, name: string, icon: string) {
|
||||
const owned = { control: false, setting: false }
|
||||
let model: MenuModel | null = null
|
||||
function remove(surface: 'control' | 'setting'): void {
|
||||
if (!owned[surface])
|
||||
return
|
||||
owned[surface] = false
|
||||
if (surface === 'control') {
|
||||
if (art.controls.cache.has(name))
|
||||
art.controls.remove(name)
|
||||
}
|
||||
else if (art.setting.find(name)) {
|
||||
art.setting.remove(name)
|
||||
}
|
||||
}
|
||||
function clear(): void {
|
||||
model = null
|
||||
releaseAll([() => remove('control'), () => remove('setting')])
|
||||
}
|
||||
function update(next: MenuModel | null, config: DisplayConfig, active: () => boolean, select: (item: SelectorItem, title: string) => Promise<Label>): void {
|
||||
if (!next) {
|
||||
clear()
|
||||
return
|
||||
}
|
||||
model = next
|
||||
const valid = () => model === next && active()
|
||||
const onSelect = async (item: SelectorItem): Promise<Label> => {
|
||||
if (!valid() || !next.selector.some(entry => entry.value === item.value))
|
||||
return item.html
|
||||
return select(item, next.title)
|
||||
}
|
||||
if (config.control) {
|
||||
owned.control = true
|
||||
art.controls.update({ name, position: 'right', html: next.html, style: { padding: '0 10px' }, selector: next.selector, onSelect })
|
||||
}
|
||||
else {
|
||||
remove('control')
|
||||
}
|
||||
if (!valid())
|
||||
return
|
||||
if (config.setting) {
|
||||
owned.setting = true
|
||||
art.setting.update({ name, tooltip: next.html, html: next.title, icon, width: 200, selector: next.selector, onSelect })
|
||||
}
|
||||
else {
|
||||
remove('setting')
|
||||
}
|
||||
}
|
||||
return { update, clear }
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { ProxyOptions } from './engine-types'
|
||||
import type { Label, MenuModel, SelectorItem, State } from './m3u8-types'
|
||||
|
||||
type Options = NonNullable<ProxyOptions['m3u8']>
|
||||
|
||||
function unique(items: SelectorItem[]): SelectorItem[] {
|
||||
const positions = new Map<Label, number>()
|
||||
const output: SelectorItem[] = []
|
||||
for (const item of items) {
|
||||
const position = positions.get(item.html)
|
||||
if (item.html !== undefined && position !== undefined) {
|
||||
if (item.default)
|
||||
output[position] = item
|
||||
}
|
||||
else {
|
||||
positions.set(item.html, output.length)
|
||||
output.push(item)
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
export function qualityModel(state: State | null, config: NonNullable<Options['quality']>, active: () => boolean): MenuModel | null {
|
||||
if ((!config.control && !config.setting) || !state?.levels.length)
|
||||
return null
|
||||
const auto = config.auto || 'Auto'
|
||||
const getName = config.getName || (level => level.name || `${level.height}P`)
|
||||
const html = state.currentLevel ? getName(state.currentLevel) : auto
|
||||
const items: SelectorItem[] = []
|
||||
for (const level of state.levels) {
|
||||
if (!active())
|
||||
return null
|
||||
items.push({ html: getName(level), value: level.id, default: state.currentLevel?.id === level.id })
|
||||
}
|
||||
const heights = new Map(state.levels.map(level => [level.id, level.height]))
|
||||
const selector = unique(items).sort((left, right) => (right.value === 'auto' ? 0 : heights.get(right.value) || 0) - (left.value === 'auto' ? 0 : heights.get(left.value) || 0))
|
||||
selector.push({ html: auto, value: 'auto', default: !state.currentLevel })
|
||||
return { title: config.title || 'Quality', html, selector }
|
||||
}
|
||||
|
||||
export function audioModel(state: State | null, config: NonNullable<Options['audio']>, active: () => boolean): MenuModel | null {
|
||||
if ((!config.control && !config.setting) || !state || state.audios.length < 2)
|
||||
return null
|
||||
const auto = config.auto || 'Auto'
|
||||
const getName = config.getName || (track => track.name || track.lang || track.language)
|
||||
const html = state.currentAudio ? getName(state.currentAudio) : auto
|
||||
const items: SelectorItem[] = []
|
||||
for (const track of state.audios) {
|
||||
if (!active())
|
||||
return null
|
||||
items.push({ html: getName(track), value: track.id, default: state.currentAudio?.id === track.id })
|
||||
}
|
||||
const selector = unique(items)
|
||||
selector.push({ html: auto, value: 'auto', default: !state.currentAudio })
|
||||
return { title: config.title || 'Audio', html, selector }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { HlsState, ProxyOptions } from './engine-types'
|
||||
|
||||
export type State = NonNullable<HlsState>
|
||||
export type Level = State['levels'][number]
|
||||
export type Audio = State['audios'][number]
|
||||
export type Label = string | null | undefined
|
||||
export interface DisplayConfig {
|
||||
control?: boolean
|
||||
setting?: boolean
|
||||
}
|
||||
export interface SelectorItem {
|
||||
html: Label
|
||||
value: number | 'auto'
|
||||
default: boolean
|
||||
}
|
||||
export interface MenuModel {
|
||||
title: string
|
||||
html: Label
|
||||
selector: SelectorItem[]
|
||||
}
|
||||
export interface MenuOption {
|
||||
name: string
|
||||
html: Label
|
||||
selector: SelectorItem[]
|
||||
onSelect: (item: SelectorItem) => Promise<Label>
|
||||
position?: 'right'
|
||||
style?: { padding: string }
|
||||
tooltip?: Label
|
||||
icon?: string
|
||||
width?: number
|
||||
}
|
||||
interface Registry {
|
||||
update: (menu: MenuOption) => unknown
|
||||
remove: (name: string) => unknown
|
||||
}
|
||||
export interface MenuHost {
|
||||
controls: Registry & { cache: { has: (name: string) => boolean } }
|
||||
setting: Registry & { find: (name: string) => unknown }
|
||||
notice: { show: string }
|
||||
on: (name: string, callback: () => unknown) => unknown
|
||||
off: (name: string, callback: () => unknown) => unknown
|
||||
}
|
||||
export interface MenuShim {
|
||||
engine: { readonly loadSeq: number, readonly destroyed: boolean }
|
||||
getM3u8State: () => Promise<HlsState>
|
||||
switchM3u8Quality: (value: unknown) => Promise<void>
|
||||
switchM3u8Audio: (value: unknown) => Promise<void>
|
||||
}
|
||||
export interface SetupOptions {
|
||||
art: MenuHost
|
||||
shim: MenuShim
|
||||
option: ProxyOptions
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
import $audio from '../../artplayer-plugin-hls-control/src/audio.svg?raw'
|
||||
import $quality from '../../artplayer-plugin-hls-control/src/quality.svg?raw'
|
||||
|
||||
function uniqBy(array, property) {
|
||||
const seen = new Map()
|
||||
return array.filter((item) => {
|
||||
const key = item[property]
|
||||
if (key === undefined) {
|
||||
return true
|
||||
}
|
||||
return !seen.has(key) && seen.set(key, 1)
|
||||
})
|
||||
}
|
||||
|
||||
export function setupM3u8Controls({ art, shim, option }) {
|
||||
if (!option.m3u8)
|
||||
return
|
||||
|
||||
function removeControl(name) {
|
||||
if (art.controls.cache.has(name)) {
|
||||
art.controls.remove(name)
|
||||
}
|
||||
}
|
||||
|
||||
function removeSetting(name) {
|
||||
if (art.setting.find(name)) {
|
||||
art.setting.remove(name)
|
||||
}
|
||||
}
|
||||
|
||||
function clearQuality() {
|
||||
removeControl('mediabunny-quality')
|
||||
removeSetting('mediabunny-quality')
|
||||
}
|
||||
|
||||
function clearAudio() {
|
||||
removeControl('mediabunny-audio')
|
||||
removeSetting('mediabunny-audio')
|
||||
}
|
||||
|
||||
function canRender(config = {}) {
|
||||
return config.control || config.setting
|
||||
}
|
||||
|
||||
async function updateQuality(state) {
|
||||
const config = option.m3u8?.quality || {}
|
||||
if (!canRender(config) || !state?.levels.length) {
|
||||
clearQuality()
|
||||
return
|
||||
}
|
||||
|
||||
const auto = config.auto || 'Auto'
|
||||
const title = config.title || 'Quality'
|
||||
const getName = config.getName || (level => level.name || `${level.height}P`)
|
||||
const defaultHtml = state.currentLevel
|
||||
? getName(state.currentLevel)
|
||||
: auto
|
||||
|
||||
const selector = uniqBy(
|
||||
state.levels.map((item) => {
|
||||
return {
|
||||
html: getName(item),
|
||||
value: item.id,
|
||||
default: state.currentLevel?.id === item.id,
|
||||
}
|
||||
}),
|
||||
'html',
|
||||
).sort((a, b) => {
|
||||
const left = state.levels.find(item => item.id === a.value)
|
||||
const right = state.levels.find(item => item.id === b.value)
|
||||
return (right?.height || 0) - (left?.height || 0)
|
||||
})
|
||||
|
||||
selector.push({
|
||||
html: auto,
|
||||
value: 'auto',
|
||||
default: !state.currentLevel,
|
||||
})
|
||||
|
||||
const onSelect = async (item) => {
|
||||
await shim.switchM3u8Quality(item.value)
|
||||
art.notice.show = `${title}: ${item.html}`
|
||||
await update()
|
||||
return item.html
|
||||
}
|
||||
|
||||
if (config.control) {
|
||||
art.controls.update({
|
||||
name: 'mediabunny-quality',
|
||||
position: 'right',
|
||||
html: defaultHtml,
|
||||
style: { padding: '0 10px' },
|
||||
selector,
|
||||
onSelect,
|
||||
})
|
||||
}
|
||||
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: 'mediabunny-quality',
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $quality,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function updateAudio(state) {
|
||||
const config = option.m3u8?.audio || {}
|
||||
if (!canRender(config) || !state?.audios.length || state.audios.length < 2) {
|
||||
clearAudio()
|
||||
return
|
||||
}
|
||||
|
||||
const auto = config.auto || 'Auto'
|
||||
const title = config.title || 'Audio'
|
||||
const getName = config.getName || (track => track.name || track.lang || track.language)
|
||||
const defaultHtml = state.currentAudio
|
||||
? getName(state.currentAudio)
|
||||
: auto
|
||||
|
||||
const selector = uniqBy(
|
||||
state.audios.map((item) => {
|
||||
return {
|
||||
html: getName(item),
|
||||
value: item.id,
|
||||
default: state.currentAudio?.id === item.id,
|
||||
}
|
||||
}),
|
||||
'html',
|
||||
)
|
||||
|
||||
selector.push({
|
||||
html: auto,
|
||||
value: 'auto',
|
||||
default: !state.currentAudio,
|
||||
})
|
||||
|
||||
const onSelect = async (item) => {
|
||||
await shim.switchM3u8Audio(item.value)
|
||||
art.notice.show = `${title}: ${item.html}`
|
||||
await update()
|
||||
return item.html
|
||||
}
|
||||
|
||||
if (config.control) {
|
||||
art.controls.update({
|
||||
name: 'mediabunny-audio',
|
||||
position: 'right',
|
||||
html: defaultHtml,
|
||||
style: { padding: '0 10px' },
|
||||
selector,
|
||||
onSelect,
|
||||
})
|
||||
}
|
||||
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: 'mediabunny-audio',
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $audio,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function update() {
|
||||
const state = await shim.getM3u8State()
|
||||
if (!state) {
|
||||
clearQuality()
|
||||
clearAudio()
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
updateQuality(state),
|
||||
updateAudio(state),
|
||||
])
|
||||
}
|
||||
|
||||
art.on('video:loadedmetadata', update)
|
||||
art.on('restart', update)
|
||||
|
||||
return { update }
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { Label, SelectorItem, SetupOptions } from './m3u8-types'
|
||||
import $audio from '../../artplayer-plugin-hls-control/src/audio.svg?raw'
|
||||
import $quality from '../../artplayer-plugin-hls-control/src/quality.svg?raw'
|
||||
import createMenu, { releaseAll } from './m3u8-menu'
|
||||
import { audioModel, qualityModel } from './m3u8-model'
|
||||
|
||||
export function setupM3u8Controls({ art, shim, option }: SetupOptions) {
|
||||
if (!option.m3u8)
|
||||
return
|
||||
let closed = false
|
||||
let revision = 0
|
||||
let selection = 0
|
||||
const quality = createMenu(art, 'mediabunny-quality', $quality)
|
||||
const audio = createMenu(art, 'mediabunny-audio', $audio)
|
||||
const current = (source: number) => !closed && !shim.engine.destroyed && shim.engine.loadSeq === source
|
||||
|
||||
async function update(): Promise<void> {
|
||||
if (closed || shim.engine.destroyed)
|
||||
return
|
||||
const version = ++revision
|
||||
const source = shim.engine.loadSeq
|
||||
const active = () => version === revision && current(source)
|
||||
try {
|
||||
const state = await shim.getM3u8State()
|
||||
if (!active())
|
||||
return
|
||||
const qualityConfig = option.m3u8?.quality || {}
|
||||
const qualityView = qualityModel(state, qualityConfig, active)
|
||||
if (!active())
|
||||
return
|
||||
quality.update(qualityView, qualityConfig, () => current(source), (item, title) => select('quality', item, title))
|
||||
if (!active())
|
||||
return
|
||||
const audioConfig = option.m3u8?.audio || {}
|
||||
const audioView = audioModel(state, audioConfig, active)
|
||||
if (active())
|
||||
audio.update(audioView, audioConfig, () => current(source), (item, title) => select('audio', item, title))
|
||||
}
|
||||
catch (error) {
|
||||
if (active())
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function select(kind: 'quality' | 'audio', item: SelectorItem, title: string): Promise<Label> {
|
||||
const intent = ++selection
|
||||
const source = shim.engine.loadSeq
|
||||
revision++
|
||||
const active = () => intent === selection && current(source)
|
||||
try {
|
||||
if (kind === 'quality')
|
||||
await shim.switchM3u8Quality(item.value)
|
||||
else
|
||||
await shim.switchM3u8Audio(item.value)
|
||||
if (active()) {
|
||||
await update()
|
||||
if (active())
|
||||
art.notice.show = `${title}: ${item.html}`
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (active()) {
|
||||
await refresh()
|
||||
if (active())
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return item.html
|
||||
}
|
||||
|
||||
function refresh(): Promise<void> {
|
||||
return update().catch(error => console.warn('MediaBunny HLS menu update:', error))
|
||||
}
|
||||
function invalidate(): void {
|
||||
revision++
|
||||
selection++
|
||||
try {
|
||||
releaseAll([quality.clear, audio.clear])
|
||||
}
|
||||
catch (error) { console.warn('MediaBunny HLS menu cleanup:', error) }
|
||||
}
|
||||
function destroy(): void {
|
||||
if (closed)
|
||||
return
|
||||
closed = true
|
||||
invalidate()
|
||||
try {
|
||||
releaseAll([
|
||||
() => art.off('video:loadedmetadata', refresh),
|
||||
() => art.off('restart', refresh),
|
||||
() => art.off('video:loadstart', invalidate),
|
||||
() => art.off('video:error', invalidate),
|
||||
() => art.off('destroy', destroy),
|
||||
])
|
||||
}
|
||||
catch (error) { console.warn('MediaBunny HLS listener cleanup:', error) }
|
||||
}
|
||||
art.on('video:loadedmetadata', refresh)
|
||||
art.on('restart', refresh)
|
||||
art.on('video:loadstart', invalidate)
|
||||
art.on('video:error', invalidate)
|
||||
art.on('destroy', destroy)
|
||||
return { update }
|
||||
}
|
||||
Reference in new issue
Block a user