mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-26 17:01:20 -08:00
refactor(hls): [PKG-HLS-04] migrate modules and compatible types
This commit is contained in:
1 parent
ba93b98be5
commit
2f9bee4ee0
34 files changed
+4860
-88
No files matched your search
+16
-11
@@ -1,25 +1,30 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { AudioTrack, Option, QualityLevel, Result } from '../types/artplayer-plugin-hls-control'
|
||||
import type { Cleanup, Hls, Host } from './types'
|
||||
import $audio from './audio.svg?raw'
|
||||
import { audioModel, qualityModel } from './mapping'
|
||||
import { createMenu } from './menu'
|
||||
import $quality from './quality.svg?raw'
|
||||
import { subscribeHls } from './sdk-events'
|
||||
|
||||
export default function artplayerPluginHlsControl(option = {}) {
|
||||
return (art) => {
|
||||
export default function artplayerPluginHlsControl<Level extends object = QualityLevel, Track extends object = AudioTrack>(option: Option<Level, Track> = {}) {
|
||||
return (player: Artplayer): Result => {
|
||||
// The consumer owns the optional SDK; update validates its media identity before use.
|
||||
const art = player as unknown as Host<Level, Track>
|
||||
const { $video } = art.template
|
||||
const { errorHandle } = art.constructor.utils
|
||||
let closed = false
|
||||
let revision = 0
|
||||
let engine
|
||||
let unsubscribe = () => {}
|
||||
let engine: Hls<Level, Track> | undefined
|
||||
let unsubscribe: Cleanup = () => {}
|
||||
let selecting = 0
|
||||
let pending
|
||||
let pending: Hls<Level, Track> | undefined
|
||||
const retired = new WeakSet()
|
||||
const active = hls => !closed && !retired.has(hls) && art.hls === hls && hls.media === $video
|
||||
const active = (hls: Hls<Level, Track>) => !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 select(callback) {
|
||||
function select<T>(callback: () => T): T {
|
||||
selecting++
|
||||
let succeeded = false
|
||||
try {
|
||||
@@ -38,7 +43,7 @@ export default function artplayerPluginHlsControl(option = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function refresh(hls, force) {
|
||||
function refresh(hls: Hls<Level, Track>, force: boolean): void {
|
||||
if (!active(hls))
|
||||
return
|
||||
const version = ++revision
|
||||
@@ -55,10 +60,10 @@ export default function artplayerPluginHlsControl(option = {}) {
|
||||
audio.update(hls, audioConfig, audioView, force)
|
||||
}
|
||||
|
||||
function update() {
|
||||
function update(): void {
|
||||
if (closed)
|
||||
return
|
||||
const hls = art.hls
|
||||
const hls = art.hls!
|
||||
errorHandle(hls?.media === $video, 'Cannot find instance of HLS from "art.hls"')
|
||||
if (retired.has(hls))
|
||||
return
|
||||
@@ -91,7 +96,7 @@ export default function artplayerPluginHlsControl(option = {}) {
|
||||
refresh(hls, true)
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
function destroy(): void {
|
||||
if (closed)
|
||||
return
|
||||
closed = true
|
||||
+10
-7
@@ -1,5 +1,8 @@
|
||||
function uniqueLabels(items) {
|
||||
const seen = new Map()
|
||||
import type { Config } from '../types/artplayer-plugin-hls-control'
|
||||
import type { AudioFields, Hls, LevelFields, MenuModel, SelectorItem } from './types'
|
||||
|
||||
function uniqueLabels(items: SelectorItem[]): SelectorItem[] {
|
||||
const seen = new Map<string, SelectorItem>()
|
||||
return items.filter((item) => {
|
||||
if (item.html === undefined)
|
||||
return true
|
||||
@@ -16,15 +19,15 @@ function uniqueLabels(items) {
|
||||
})
|
||||
}
|
||||
|
||||
export function qualitySelection(hls) {
|
||||
export function qualitySelection(hls: Pick<Hls, 'currentLevel' | 'autoLevelEnabled'>): number {
|
||||
return hls.autoLevelEnabled === true ? -1 : hls.currentLevel
|
||||
}
|
||||
|
||||
export function qualityModel(hls, config) {
|
||||
export function qualityModel<Level extends object>(hls: Pick<Hls<Level>, 'levels' | 'currentLevel' | 'autoLevelEnabled'>, config: Config<Level>): MenuModel | null {
|
||||
if (!hls.levels.length)
|
||||
return null
|
||||
const auto = config.auto || 'Auto'
|
||||
const getName = config.getName || (level => level.name || `${level.height}P`)
|
||||
const getName = config.getName || ((level: LevelFields) => level.name || `${level.height}P`)
|
||||
const selected = qualitySelection(hls)
|
||||
const level = hls.levels[selected]
|
||||
const html = level ? getName(level) : auto
|
||||
@@ -34,11 +37,11 @@ export function qualityModel(hls, config) {
|
||||
return { html, title: config.title || 'Quality', selector }
|
||||
}
|
||||
|
||||
export function audioModel(hls, config) {
|
||||
export function audioModel<Track extends object>(hls: Pick<Hls<object, Track>, 'audioTracks' | 'audioTrack'>, config: Config<Track>): MenuModel | null {
|
||||
if (!hls.audioTracks.length)
|
||||
return null
|
||||
const auto = config.auto || 'Auto'
|
||||
const getName = config.getName || (track => track.name || track.lang || track.language)
|
||||
const getName = config.getName || ((track: AudioFields) => 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 })))
|
||||
+12
-9
@@ -1,21 +1,23 @@
|
||||
export function createMenu(art, name, field, icon, active, select) {
|
||||
let current
|
||||
import type { DisplayConfig, MenuHost, MenuModel, MenuState, SelectorItem } from './types'
|
||||
|
||||
export function createMenu<Engine extends { currentLevel: number, audioTrack: number }>(art: MenuHost, name: string, field: 'currentLevel' | 'audioTrack', icon: string, active: (hls: Engine) => boolean, select: <T>(callback: () => T) => T) {
|
||||
let current: MenuState<Engine> | undefined
|
||||
const owned = { control: false, setting: false }
|
||||
|
||||
function remove(surface) {
|
||||
function remove(surface: 'control' | 'setting'): void {
|
||||
if (!owned[surface])
|
||||
return
|
||||
owned[surface] = false
|
||||
art[surface === 'control' ? 'controls' : 'setting'].remove(name)
|
||||
}
|
||||
|
||||
function clear() {
|
||||
function clear(): void {
|
||||
current = undefined
|
||||
remove('control')
|
||||
remove('setting')
|
||||
}
|
||||
|
||||
function check(state, item) {
|
||||
function check(state: MenuState<Engine>, item: SelectorItem): void {
|
||||
for (const entry of state.model.selector)
|
||||
entry.default = entry === item
|
||||
if (state.config.control)
|
||||
@@ -26,18 +28,19 @@ export function createMenu(art, name, field, icon, active, select) {
|
||||
art.setting.check(item)
|
||||
}
|
||||
|
||||
function update(hls, config, model, force) {
|
||||
function update(hls: Engine, config: DisplayConfig, model: MenuModel | null, force: boolean): void {
|
||||
if (!model) {
|
||||
clear()
|
||||
return
|
||||
}
|
||||
const previous = current
|
||||
const selectedIndex = model.selector.findIndex(item => item.default)
|
||||
// Equal lengths below protect the same-index reads in the comparison.
|
||||
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)
|
||||
&& 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]
|
||||
@@ -50,7 +53,7 @@ export function createMenu(art, name, field, icon, active, select) {
|
||||
const state = { hls, config, model }
|
||||
current = state
|
||||
const valid = () => current === state && active(hls)
|
||||
const onSelect = item => select(() => {
|
||||
const onSelect = (item: SelectorItem) => select(() => {
|
||||
if (!valid())
|
||||
return item.html
|
||||
hls[field] = item.value
|
||||
@@ -80,7 +83,7 @@ export function createMenu(art, name, field, icon, active, select) {
|
||||
}
|
||||
}
|
||||
|
||||
function invalidate() {
|
||||
function invalidate(): void {
|
||||
current = undefined
|
||||
}
|
||||
return { update, clear, invalidate }
|
||||
+10
-6
@@ -1,19 +1,23 @@
|
||||
export function subscribeHls(hls, refresh, destroyed) {
|
||||
import type { Cleanup, Hls, SdkEvent } from './types'
|
||||
|
||||
export function subscribeHls(hls: Hls, refresh: Cleanup, destroyed: Cleanup): Cleanup {
|
||||
const events = hls.constructor?.Events
|
||||
if (!events || typeof hls.on !== 'function' || typeof hls.off !== 'function')
|
||||
return () => {}
|
||||
const subscriptions = []
|
||||
// Keep the checked SDK object so method calls retain their original receiver.
|
||||
const source = hls as Hls & Required<Pick<Hls, 'on' | 'off'>>
|
||||
const subscriptions: [string, Cleanup][] = []
|
||||
let active = true
|
||||
function release() {
|
||||
function release(): void {
|
||||
if (!active)
|
||||
return
|
||||
active = false
|
||||
for (const [event, callback] of subscriptions)
|
||||
hls.off(event, callback)
|
||||
source.off(event, callback)
|
||||
subscriptions.length = 0
|
||||
}
|
||||
try {
|
||||
for (const key of ['MANIFEST_PARSED', 'LEVELS_UPDATED', 'LEVEL_SWITCHED', 'AUDIO_TRACKS_UPDATED', 'AUDIO_TRACK_SWITCHED', 'DESTROYING']) {
|
||||
for (const key of ['MANIFEST_PARSED', 'LEVELS_UPDATED', 'LEVEL_SWITCHED', 'AUDIO_TRACKS_UPDATED', 'AUDIO_TRACK_SWITCHED', 'DESTROYING'] satisfies SdkEvent[]) {
|
||||
const event = events[key]
|
||||
if (typeof event !== 'string' || subscriptions.some(([name]) => name === event))
|
||||
continue
|
||||
@@ -26,7 +30,7 @@ export function subscribeHls(hls, refresh, destroyed) {
|
||||
refresh()
|
||||
}
|
||||
subscriptions.push([event, callback])
|
||||
hls.on(event, callback)
|
||||
source.on(event, callback)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { Config } from '../types/artplayer-plugin-hls-control'
|
||||
|
||||
export interface LevelFields {
|
||||
height?: number
|
||||
name?: string
|
||||
}
|
||||
|
||||
export interface AudioFields {
|
||||
id: number
|
||||
name?: string
|
||||
lang?: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
export type SdkEvent = 'MANIFEST_PARSED' | 'LEVELS_UPDATED' | 'LEVEL_SWITCHED' | 'AUDIO_TRACKS_UPDATED' | 'AUDIO_TRACK_SWITCHED' | 'DESTROYING'
|
||||
export type Cleanup = () => void
|
||||
|
||||
export interface Hls<Level extends object = object, Track extends object = object> {
|
||||
media: object | null
|
||||
levels: (Level & LevelFields)[]
|
||||
audioTracks: (Track & AudioFields)[]
|
||||
currentLevel: number
|
||||
audioTrack: number
|
||||
autoLevelEnabled?: boolean
|
||||
constructor?: { Events?: Partial<Record<SdkEvent, unknown>> }
|
||||
on?: (event: string, callback: Cleanup) => unknown
|
||||
off?: (event: string, callback: Cleanup) => unknown
|
||||
}
|
||||
|
||||
export interface SelectorItem {
|
||||
html: string | undefined
|
||||
value: number
|
||||
default: boolean
|
||||
}
|
||||
|
||||
export interface MenuModel {
|
||||
html: string | undefined
|
||||
title: string
|
||||
selector: SelectorItem[]
|
||||
}
|
||||
|
||||
export interface MenuOption {
|
||||
name: string
|
||||
html: string | undefined
|
||||
selector: SelectorItem[]
|
||||
onSelect: (item: SelectorItem) => string | undefined
|
||||
position?: 'right'
|
||||
style?: { padding: string }
|
||||
tooltip?: string
|
||||
icon?: string
|
||||
width?: number
|
||||
}
|
||||
|
||||
export interface Registry {
|
||||
update: (option: MenuOption) => unknown
|
||||
remove: (name: string) => unknown
|
||||
check: (item: SelectorItem) => unknown
|
||||
}
|
||||
|
||||
export interface MenuHost {
|
||||
controls: Registry
|
||||
setting: Registry
|
||||
notice: { show: string }
|
||||
}
|
||||
|
||||
export interface Host<Level extends object, Track extends object> extends MenuHost {
|
||||
template: { $video: object }
|
||||
constructor: { utils: { errorHandle: (condition: unknown, message: string) => void } }
|
||||
hls?: Hls<Level, Track>
|
||||
on: (event: 'ready' | 'restart' | 'destroy', callback: Cleanup) => unknown
|
||||
off: (event: 'ready' | 'restart' | 'destroy', callback: Cleanup) => unknown
|
||||
}
|
||||
|
||||
export type DisplayConfig = Pick<Config, 'control' | 'setting'>
|
||||
export interface MenuState<Engine> {
|
||||
hls: Engine
|
||||
config: DisplayConfig
|
||||
model: MenuModel
|
||||
}
|
||||
Reference in new issue
Block a user