mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-24 14:41:20 -08:00
refactor(hls): [PKG-HLS-03] separate menus and SDK lifecycle
This commit is contained in:
1 parent
33f6d19b77
commit
ba93b98be5
24 files changed
+5849
-382
No files matched your search
@@ -1,152 +1,113 @@
|
||||
import $audio from './audio.svg?raw'
|
||||
import { audioModel, qualityModel } from './mapping'
|
||||
import { createMenu } from './menu'
|
||||
import $quality from './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)
|
||||
})
|
||||
}
|
||||
import { subscribeHls } from './sdk-events'
|
||||
|
||||
export default function artplayerPluginHlsControl(option = {}) {
|
||||
return (art) => {
|
||||
const { $video } = art.template
|
||||
const { errorHandle } = art.constructor.utils
|
||||
let closed = false
|
||||
let revision = 0
|
||||
let engine
|
||||
let unsubscribe = () => {}
|
||||
let selecting = 0
|
||||
let pending
|
||||
const retired = new WeakSet()
|
||||
const active = hls => !closed && !retired.has(hls) && art.hls === hls && hls.media === $video
|
||||
const quality = createMenu(art, 'hls-quality', 'currentLevel', $quality, active, select)
|
||||
const audio = createMenu(art, 'hls-audio', 'audioTrack', $audio, active, select)
|
||||
|
||||
function updateQuality(hls) {
|
||||
if (!hls.levels.length)
|
||||
return
|
||||
|
||||
const config = option.quality || {}
|
||||
const auto = config.auto || 'Auto'
|
||||
const title = config.title || 'Quality'
|
||||
const getName = config.getName || (level => level.name || `${level.height}P`)
|
||||
const defaultLevel = hls.levels[hls.currentLevel]
|
||||
const defaultHtml = defaultLevel ? getName(defaultLevel) : auto
|
||||
|
||||
const selector = uniqBy(
|
||||
hls.levels.map((item, index) => {
|
||||
return {
|
||||
html: getName(item, index),
|
||||
value: index,
|
||||
default: hls.currentLevel === index,
|
||||
}
|
||||
}),
|
||||
'html',
|
||||
).sort((a, b) => b.value - a.value)
|
||||
|
||||
selector.push({
|
||||
html: auto,
|
||||
value: -1,
|
||||
default: hls.currentLevel === -1,
|
||||
})
|
||||
|
||||
const onSelect = (item) => {
|
||||
hls.currentLevel = item.value
|
||||
art.notice.show = `${title}: ${item.html}`
|
||||
if (config.control)
|
||||
art.controls.check(item)
|
||||
if (config.setting)
|
||||
art.setting.check(item)
|
||||
return item.html
|
||||
function select(callback) {
|
||||
selecting++
|
||||
let succeeded = false
|
||||
try {
|
||||
const result = callback()
|
||||
succeeded = true
|
||||
return result
|
||||
}
|
||||
|
||||
if (config.control) {
|
||||
art.controls.update({
|
||||
name: 'hls-quality',
|
||||
position: 'right',
|
||||
html: defaultHtml,
|
||||
style: { padding: '0 10px' },
|
||||
selector,
|
||||
onSelect,
|
||||
})
|
||||
}
|
||||
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: 'hls-quality',
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $quality,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect,
|
||||
})
|
||||
finally {
|
||||
selecting--
|
||||
if (!selecting) {
|
||||
const hls = pending
|
||||
pending = undefined
|
||||
if (succeeded && hls)
|
||||
refresh(hls, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateAudio(hls) {
|
||||
if (!hls.audioTracks.length)
|
||||
function refresh(hls, force) {
|
||||
if (!active(hls))
|
||||
return
|
||||
|
||||
const config = option.audio || {}
|
||||
const auto = config.auto || 'Auto'
|
||||
const title = config.title || 'Audio'
|
||||
const getName = config.getName || (track => track.name || track.lang || track.language)
|
||||
const defaultTrack = hls.audioTracks[hls.audioTrack]
|
||||
const defaultHtml = defaultTrack ? getName(defaultTrack) : auto
|
||||
|
||||
const selector = uniqBy(
|
||||
hls.audioTracks.map((item, index) => {
|
||||
return {
|
||||
html: getName(item, index),
|
||||
value: item.id,
|
||||
default: hls.audioTrack === item.id,
|
||||
}
|
||||
}),
|
||||
'html',
|
||||
)
|
||||
|
||||
const onSelect = (item) => {
|
||||
hls.audioTrack = item.value
|
||||
art.notice.show = `${title}: ${item.html}`
|
||||
if (config.control)
|
||||
art.controls.check(item)
|
||||
if (config.setting)
|
||||
art.setting.check(item)
|
||||
return item.html
|
||||
}
|
||||
|
||||
if (config.control) {
|
||||
art.controls.update({
|
||||
name: 'hls-audio',
|
||||
position: 'right',
|
||||
html: defaultHtml,
|
||||
style: { padding: '0 10px' },
|
||||
selector,
|
||||
onSelect,
|
||||
})
|
||||
}
|
||||
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: 'hls-audio',
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $audio,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect,
|
||||
})
|
||||
}
|
||||
const version = ++revision
|
||||
const config = option.quality || {}
|
||||
const model = qualityModel(hls, config)
|
||||
if (!active(hls) || version !== revision)
|
||||
return
|
||||
quality.update(hls, config, model, force)
|
||||
if (!active(hls) || version !== revision)
|
||||
return
|
||||
const audioConfig = option.audio || {}
|
||||
const audioView = audioModel(hls, audioConfig)
|
||||
if (active(hls) && version === revision)
|
||||
audio.update(hls, audioConfig, audioView, force)
|
||||
}
|
||||
|
||||
function update() {
|
||||
errorHandle(art.hls?.media === $video, 'Cannot find instance of HLS from "art.hls"')
|
||||
updateQuality(art.hls)
|
||||
updateAudio(art.hls)
|
||||
if (closed)
|
||||
return
|
||||
const hls = art.hls
|
||||
errorHandle(hls?.media === $video, 'Cannot find instance of HLS from "art.hls"')
|
||||
if (retired.has(hls))
|
||||
return
|
||||
if (engine !== hls) {
|
||||
unsubscribe()
|
||||
engine = hls
|
||||
try {
|
||||
unsubscribe = subscribeHls(hls, () => {
|
||||
if (selecting)
|
||||
pending = hls
|
||||
else
|
||||
refresh(hls, false)
|
||||
}, () => {
|
||||
retired.add(hls)
|
||||
unsubscribe()
|
||||
if (engine === hls) {
|
||||
engine = undefined
|
||||
if (!closed) {
|
||||
quality.clear()
|
||||
audio.clear()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
engine = undefined
|
||||
throw error
|
||||
}
|
||||
}
|
||||
refresh(hls, true)
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (closed)
|
||||
return
|
||||
closed = true
|
||||
revision++
|
||||
pending = undefined
|
||||
quality.invalidate()
|
||||
audio.invalidate()
|
||||
unsubscribe()
|
||||
art.off('ready', update)
|
||||
art.off('restart', update)
|
||||
art.off('destroy', destroy)
|
||||
}
|
||||
|
||||
art.on('ready', update)
|
||||
art.on('restart', update)
|
||||
|
||||
return {
|
||||
name: 'artplayerPluginHlsControl',
|
||||
update,
|
||||
}
|
||||
art.on('destroy', destroy)
|
||||
return { name: 'artplayerPluginHlsControl', update }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
function uniqueLabels(items) {
|
||||
const seen = new Map()
|
||||
return items.filter((item) => {
|
||||
if (item.html === undefined)
|
||||
return true
|
||||
const first = seen.get(item.html)
|
||||
if (first) {
|
||||
if (item.default) {
|
||||
first.default = true
|
||||
first.value = item.value
|
||||
}
|
||||
return false
|
||||
}
|
||||
seen.set(item.html, item)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export function qualitySelection(hls) {
|
||||
return hls.autoLevelEnabled === true ? -1 : hls.currentLevel
|
||||
}
|
||||
|
||||
export function qualityModel(hls, config) {
|
||||
if (!hls.levels.length)
|
||||
return null
|
||||
const auto = config.auto || 'Auto'
|
||||
const getName = config.getName || (level => level.name || `${level.height}P`)
|
||||
const selected = qualitySelection(hls)
|
||||
const level = hls.levels[selected]
|
||||
const html = level ? getName(level) : auto
|
||||
const selector = uniqueLabels(hls.levels.map((item, index) => ({ html: getName(item, index), value: index, default: selected === index })))
|
||||
.sort((a, b) => b.value - a.value)
|
||||
selector.push({ html: auto, value: -1, default: selected === -1 })
|
||||
return { html, title: config.title || 'Quality', selector }
|
||||
}
|
||||
|
||||
export function audioModel(hls, config) {
|
||||
if (!hls.audioTracks.length)
|
||||
return null
|
||||
const auto = config.auto || 'Auto'
|
||||
const getName = config.getName || (track => track.name || track.lang || track.language)
|
||||
const track = hls.audioTracks[hls.audioTrack]
|
||||
const html = track ? getName(track) : auto
|
||||
const selector = uniqueLabels(hls.audioTracks.map((item, index) => ({ html: getName(item, index), value: item.id, default: hls.audioTrack === item.id })))
|
||||
return { html, title: config.title || 'Audio', selector }
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
export function createMenu(art, name, field, icon, active, select) {
|
||||
let current
|
||||
const owned = { control: false, setting: false }
|
||||
|
||||
function remove(surface) {
|
||||
if (!owned[surface])
|
||||
return
|
||||
owned[surface] = false
|
||||
art[surface === 'control' ? 'controls' : 'setting'].remove(name)
|
||||
}
|
||||
|
||||
function clear() {
|
||||
current = undefined
|
||||
remove('control')
|
||||
remove('setting')
|
||||
}
|
||||
|
||||
function check(state, item) {
|
||||
for (const entry of state.model.selector)
|
||||
entry.default = entry === item
|
||||
if (state.config.control)
|
||||
art.controls.check(item)
|
||||
if (state !== current || !active(state.hls))
|
||||
return
|
||||
if (state.config.setting)
|
||||
art.setting.check(item)
|
||||
}
|
||||
|
||||
function update(hls, config, model, force) {
|
||||
if (!model) {
|
||||
clear()
|
||||
return
|
||||
}
|
||||
const previous = current
|
||||
const selectedIndex = model.selector.findIndex(item => item.default)
|
||||
const reusable = !force && previous?.hls === hls && previous.model.title === model.title
|
||||
&& (selectedIndex !== -1 || (!previous.model.selector.some(item => item.default) && previous.model.html === model.html))
|
||||
&& owned.control === Boolean(config.control) && owned.setting === Boolean(config.setting)
|
||||
&& previous.model.selector.length === model.selector.length
|
||||
&& previous.model.selector.every((item, index) => item.html === model.selector[index].html && item.value === model.selector[index].value)
|
||||
if (reusable) {
|
||||
previous.config = config
|
||||
const target = previous.model.selector[selectedIndex]
|
||||
if (target && (!target.default || previous.model.html !== model.html)) {
|
||||
previous.model.html = model.html
|
||||
check(previous, target)
|
||||
}
|
||||
return
|
||||
}
|
||||
const state = { hls, config, model }
|
||||
current = state
|
||||
const valid = () => current === state && active(hls)
|
||||
const onSelect = item => select(() => {
|
||||
if (!valid())
|
||||
return item.html
|
||||
hls[field] = item.value
|
||||
if (!valid())
|
||||
return item.html
|
||||
model.html = item.html
|
||||
art.notice.show = `${model.title}: ${item.html}`
|
||||
if (valid())
|
||||
check(state, item)
|
||||
return item.html
|
||||
})
|
||||
if (config.control) {
|
||||
owned.control = true
|
||||
art.controls.update({ name, position: 'right', html: model.html, style: { padding: '0 10px' }, selector: model.selector, onSelect })
|
||||
}
|
||||
else {
|
||||
remove('control')
|
||||
}
|
||||
if (!valid())
|
||||
return
|
||||
if (config.setting) {
|
||||
owned.setting = true
|
||||
art.setting.update({ name, tooltip: model.html, html: model.title, icon, width: 200, selector: model.selector, onSelect })
|
||||
}
|
||||
else {
|
||||
remove('setting')
|
||||
}
|
||||
}
|
||||
|
||||
function invalidate() {
|
||||
current = undefined
|
||||
}
|
||||
return { update, clear, invalidate }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
export function subscribeHls(hls, refresh, destroyed) {
|
||||
const events = hls.constructor?.Events
|
||||
if (!events || typeof hls.on !== 'function' || typeof hls.off !== 'function')
|
||||
return () => {}
|
||||
const subscriptions = []
|
||||
let active = true
|
||||
function release() {
|
||||
if (!active)
|
||||
return
|
||||
active = false
|
||||
for (const [event, callback] of subscriptions)
|
||||
hls.off(event, callback)
|
||||
subscriptions.length = 0
|
||||
}
|
||||
try {
|
||||
for (const key of ['MANIFEST_PARSED', 'LEVELS_UPDATED', 'LEVEL_SWITCHED', 'AUDIO_TRACKS_UPDATED', 'AUDIO_TRACK_SWITCHED', 'DESTROYING']) {
|
||||
const event = events[key]
|
||||
if (typeof event !== 'string' || subscriptions.some(([name]) => name === event))
|
||||
continue
|
||||
const callback = () => {
|
||||
if (!active)
|
||||
return
|
||||
if (key === 'DESTROYING')
|
||||
destroyed()
|
||||
else
|
||||
refresh()
|
||||
}
|
||||
subscriptions.push([event, callback])
|
||||
hls.on(event, callback)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
release()
|
||||
throw error
|
||||
}
|
||||
return release
|
||||
}
|
||||
Reference in new issue
Block a user