mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-18 02:21:20 -08:00
refactor(dash): [PKG-DASH-04] migrate strict TypeScript and compatible declarations
This commit is contained in:
1 parent
7de04573b6
commit
6840544e0e
38 files changed
+1744
-102
No files matched your search
+18
-12
@@ -1,20 +1,25 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { AudioTrack, Option, QualityLevel, Result } from '../types/artplayer-plugin-dash-control'
|
||||
import type { AudioItem, Cleanup, EventName, Host, QualityItem, Valid } from './types'
|
||||
import $audio from './audio.svg?raw'
|
||||
import { audioModel, qualityModel } from './mapping'
|
||||
import { createMenu } from './menu'
|
||||
import $quality from './quality.svg?raw'
|
||||
|
||||
export default function artplayerPluginDashControl(option = {}) {
|
||||
return (art) => {
|
||||
export default function artplayerPluginDashControl<Level extends object = QualityLevel, Track extends object = AudioTrack>(option: Option<Level, Track> = {}) {
|
||||
return (player: Artplayer): Result => {
|
||||
// art.dash is caller-owned; update keeps the historical SDK/media validation boundary.
|
||||
const art = player as unknown as Host<Level, Track>
|
||||
const { $video } = art.template
|
||||
const { errorHandle } = art.constructor.utils
|
||||
let closed = false
|
||||
let revision = 0
|
||||
const quality = createMenu(art, 'dash-quality', $quality)
|
||||
const audio = createMenu(art, 'dash-audio', $audio)
|
||||
const subscriptions = []
|
||||
const quality = createMenu<QualityItem>(art, 'dash-quality', $quality)
|
||||
const audio = createMenu<AudioItem<Track>>(art, 'dash-audio', $audio)
|
||||
const subscriptions: [EventName, Cleanup][] = []
|
||||
|
||||
function clear(current = () => true) {
|
||||
let failure
|
||||
function clear(current: Valid = () => true): void {
|
||||
let failure: unknown
|
||||
for (const cleanup of [quality.clear, audio.clear]) {
|
||||
if (!current())
|
||||
break
|
||||
@@ -29,11 +34,11 @@ export default function artplayerPluginDashControl(option = {}) {
|
||||
throw failure
|
||||
}
|
||||
|
||||
function update() {
|
||||
function update(): void {
|
||||
if (closed || art.isDestroy)
|
||||
return
|
||||
const version = ++revision
|
||||
const dash = art.dash
|
||||
const dash = art.dash!
|
||||
const current = () => !closed && !art.isDestroy && version === revision && art.dash === dash
|
||||
const valid = () => current() && dash.getVideoElement() === $video && current()
|
||||
try {
|
||||
@@ -66,12 +71,12 @@ export default function artplayerPluginDashControl(option = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
function destroy(): void {
|
||||
if (closed)
|
||||
return
|
||||
closed = true
|
||||
revision++
|
||||
let failure
|
||||
let failure: unknown
|
||||
const actions = [clear, ...subscriptions.splice(0).map(([name, callback]) => () => art.off(name, callback))]
|
||||
for (const action of actions) {
|
||||
try {
|
||||
@@ -86,7 +91,8 @@ export default function artplayerPluginDashControl(option = {}) {
|
||||
}
|
||||
|
||||
try {
|
||||
for (const entry of [['ready', update], ['restart', update], ['destroy', destroy]]) {
|
||||
const entries: [EventName, Cleanup][] = [['ready', update], ['restart', update], ['destroy', destroy]]
|
||||
for (const entry of entries) {
|
||||
if (closed)
|
||||
break
|
||||
subscriptions.push(entry)
|
||||
+17
-12
@@ -1,7 +1,9 @@
|
||||
import type { Config } from '../types/artplayer-plugin-dash-control'
|
||||
import type { AudioFields, AudioItem, Dash, Label, MenuModel, QualityFields, QualityItem, SelectorItem, Valid } from './types'
|
||||
import { qualityAdapter } from './sdk'
|
||||
|
||||
function uniqueLabels(items) {
|
||||
const seen = new Map()
|
||||
function uniqueLabels<Item extends SelectorItem>(items: Item[]): Item[] {
|
||||
const seen = new Map<Label, Item>()
|
||||
return items.filter((item) => {
|
||||
if (item.html === undefined)
|
||||
return true
|
||||
@@ -16,13 +18,13 @@ function uniqueLabels(items) {
|
||||
})
|
||||
}
|
||||
|
||||
export function qualityModel(dash, config, active) {
|
||||
export function qualityModel<Level extends object, Track extends object>(dash: Dash<Level, Track>, config: Config<Level>, active: Valid): MenuModel<QualityItem> | null {
|
||||
const adapter = qualityAdapter(dash)
|
||||
const levels = adapter.levels()
|
||||
if (!active() || !levels?.length)
|
||||
return null
|
||||
const auto = config.auto || 'Auto'
|
||||
const getName = config.getName || (level => `${level.height}p`)
|
||||
const getName = config.getName || ((level: QualityFields) => `${level.height}p`)
|
||||
const selected = adapter.current(levels)
|
||||
if (!active())
|
||||
return null
|
||||
@@ -32,7 +34,7 @@ export function qualityModel(dash, config, active) {
|
||||
const html = !automatic && selected ? getName(selected) : auto
|
||||
if (!active())
|
||||
return null
|
||||
const items = []
|
||||
const items: QualityItem[] = []
|
||||
for (const [index, level] of levels.entries()) {
|
||||
const label = getName(level)
|
||||
if (!active())
|
||||
@@ -40,34 +42,37 @@ export function qualityModel(dash, config, active) {
|
||||
items.push({ html: label, ...adapter.item(level, index, selected, automatic) })
|
||||
}
|
||||
const selector = uniqueLabels(items)
|
||||
.sort((left, right) => right.value - left.value)
|
||||
// The synthetic Auto item is appended after sorting SDK numeric keys.
|
||||
.sort((left, right) => (right.value as number) - (left.value as number))
|
||||
selector.push({ html: auto, value: 'auto', default: automatic })
|
||||
return { html, title: config.title || 'Quality', selector, select: adapter.select }
|
||||
}
|
||||
|
||||
function selectedTrack(tracks, current) {
|
||||
function selectedTrack<Track extends AudioFields>(tracks: Track[], current: Track): Track | undefined {
|
||||
if (tracks.includes(current))
|
||||
return current
|
||||
if (current.id == null && current.index == null)
|
||||
return undefined
|
||||
const matching = tracks.filter(track => ['id', 'index', 'lang'].every(key => current[key] == null || track[key] === current[key]))
|
||||
const fields = ['id', 'index', 'lang'] as const
|
||||
const matching = tracks.filter(track => fields.every(key => current[key] == null || track[key] === current[key]))
|
||||
return matching.length === 1 ? matching[0] : undefined
|
||||
}
|
||||
|
||||
export function audioModel(dash, config, active) {
|
||||
export function audioModel<Level extends object, Track extends object>(dash: Dash<Level, Track>, config: Config<Track>, active: Valid): MenuModel<AudioItem<Track>> | null {
|
||||
const tracks = dash.getTracksFor('audio')
|
||||
if (!active() || !tracks?.length)
|
||||
return null
|
||||
const auto = config.auto || 'Auto'
|
||||
const getName = config.getName || (track => track.lang || track.id)
|
||||
const current = dash.getCurrentTrackFor('audio') || tracks[0]
|
||||
const getName = config.getName || ((track: AudioFields) => track.lang || track.id)
|
||||
// The non-empty list check above protects the fallback item.
|
||||
const current = dash.getCurrentTrackFor('audio') || tracks[0]!
|
||||
if (!active())
|
||||
return null
|
||||
const html = current ? getName(current) : auto
|
||||
if (!active())
|
||||
return null
|
||||
const selected = selectedTrack(tracks, current)
|
||||
const items = []
|
||||
const items: AudioItem<Track>[] = []
|
||||
for (const track of tracks) {
|
||||
const label = getName(track)
|
||||
if (!active())
|
||||
+10
-8
@@ -1,8 +1,10 @@
|
||||
export function createMenu(art, name, icon) {
|
||||
let current
|
||||
const owned = new Map()
|
||||
import type { DisplayConfig, Label, MenuHost, MenuModel, SelectorItem, Valid } from './types'
|
||||
|
||||
function remove(surface, callback = owned.get(surface)) {
|
||||
export function createMenu<Item extends SelectorItem>(art: MenuHost, name: string, icon: string) {
|
||||
let current: object | undefined
|
||||
const owned = new Map<'controls' | 'setting', (item: Item) => Label>()
|
||||
|
||||
function remove(surface: 'controls' | 'setting', callback = owned.get(surface)): void {
|
||||
if (!callback || owned.get(surface) !== callback)
|
||||
return
|
||||
owned.delete(surface)
|
||||
@@ -14,9 +16,9 @@ export function createMenu(art, name, icon) {
|
||||
registry.remove(name)
|
||||
}
|
||||
|
||||
function clear() {
|
||||
function clear(): void {
|
||||
current = undefined
|
||||
let failure
|
||||
let failure: unknown
|
||||
for (const [surface, callback] of [...owned]) {
|
||||
try {
|
||||
remove(surface, callback)
|
||||
@@ -29,7 +31,7 @@ export function createMenu(art, name, icon) {
|
||||
throw failure
|
||||
}
|
||||
|
||||
function update(config, model, active) {
|
||||
function update(config: DisplayConfig, model: MenuModel<Item> | null, active: Valid): void {
|
||||
if (!model) {
|
||||
clear()
|
||||
return
|
||||
@@ -37,7 +39,7 @@ export function createMenu(art, name, icon) {
|
||||
const state = {}
|
||||
current = state
|
||||
const valid = () => state === current && active()
|
||||
const onSelect = (item) => {
|
||||
const onSelect = (item: Item): Label => {
|
||||
if (!valid())
|
||||
return item.html
|
||||
model.select(item, valid)
|
||||
+9
-6
@@ -1,11 +1,14 @@
|
||||
export function qualityAdapter(dash) {
|
||||
import type { Dash, QualityAdapter } from './types'
|
||||
|
||||
export function qualityAdapter<Level extends object, Track extends object>(dash: Dash<Level, Track>): QualityAdapter<Level> {
|
||||
// Select one SDK method family. Invalid/incomplete SDKs keep synchronous method errors.
|
||||
const modern = typeof dash.getRepresentationsByType === 'function'
|
||||
return {
|
||||
levels: () => modern ? dash.getRepresentationsByType('video') : dash.getBitrateInfoListFor('video'),
|
||||
levels: () => modern ? dash.getRepresentationsByType!('video') : dash.getBitrateInfoListFor!('video'),
|
||||
current(levels) {
|
||||
if (modern)
|
||||
return dash.getCurrentRepresentationForType('video')
|
||||
const quality = dash.getQualityFor('video')
|
||||
return dash.getCurrentRepresentationForType!('video')
|
||||
const quality = dash.getQualityFor!('video')
|
||||
return levels.find(level => level.qualityIndex === quality)
|
||||
},
|
||||
item(level, index, selected, automatic) {
|
||||
@@ -18,9 +21,9 @@ export function qualityAdapter(dash) {
|
||||
dash.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: automatic } } } })
|
||||
if (!automatic && valid()) {
|
||||
if (modern)
|
||||
dash.setRepresentationForTypeById('video', item.id)
|
||||
dash.setRepresentationForTypeById!('video', item.id)
|
||||
else
|
||||
dash.setQualityFor('video', item.value)
|
||||
dash.setQualityFor!('video', item.value as number | undefined)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { Config } from '../types/artplayer-plugin-dash-control'
|
||||
|
||||
export type Label = string | number | undefined
|
||||
export type Identifier = string | number
|
||||
export type Valid = () => boolean
|
||||
export type Cleanup = () => void
|
||||
export type EventName = 'ready' | 'restart' | 'destroy'
|
||||
|
||||
export interface QualityFields {
|
||||
height?: number
|
||||
id?: Identifier
|
||||
qualityIndex?: number
|
||||
}
|
||||
|
||||
export interface AudioFields {
|
||||
id?: Identifier
|
||||
index?: number
|
||||
lang?: string
|
||||
}
|
||||
|
||||
export interface Dash<Level extends object = object, Track extends object = object> {
|
||||
getVideoElement: () => object | null
|
||||
getSettings: () => { streaming: { abr: { autoSwitchBitrate: { video: boolean } } } }
|
||||
updateSettings: (settings: { streaming: { abr: { autoSwitchBitrate: { video: boolean } } } }) => unknown
|
||||
getRepresentationsByType?: (type: 'video') => (Level & QualityFields)[] | null | undefined
|
||||
getCurrentRepresentationForType?: (type: 'video') => (Level & QualityFields) | null | undefined
|
||||
setRepresentationForTypeById?: (type: 'video', id: Identifier | undefined) => unknown
|
||||
getBitrateInfoListFor?: (type: 'video') => (Level & QualityFields)[] | null | undefined
|
||||
getQualityFor?: (type: 'video') => number
|
||||
setQualityFor?: (type: 'video', index: number | undefined) => unknown
|
||||
getTracksFor: (type: 'audio') => (Track & AudioFields)[] | null | undefined
|
||||
getCurrentTrackFor: (type: 'audio') => (Track & AudioFields) | null | undefined
|
||||
setCurrentTrack: (track: Track & AudioFields) => unknown
|
||||
}
|
||||
|
||||
export interface SelectorItem {
|
||||
html: Label
|
||||
default: boolean
|
||||
}
|
||||
|
||||
export interface QualityItem extends SelectorItem {
|
||||
value: number | 'auto' | undefined
|
||||
id?: Identifier
|
||||
}
|
||||
|
||||
export interface AudioItem<Track extends object> extends SelectorItem {
|
||||
value: Track & AudioFields
|
||||
}
|
||||
|
||||
export interface QualityAdapter<Level extends object> {
|
||||
levels: () => (Level & QualityFields)[] | null | undefined
|
||||
current: (levels: (Level & QualityFields)[]) => (Level & QualityFields) | null | undefined
|
||||
item: (level: Level & QualityFields, index: number, selected: (Level & QualityFields) | null | undefined, automatic: boolean) => Omit<QualityItem, 'html'>
|
||||
select: (item: QualityItem, valid: Valid) => void
|
||||
}
|
||||
|
||||
export interface MenuModel<Item extends SelectorItem> {
|
||||
html: Label
|
||||
title: string
|
||||
selector: Item[]
|
||||
select: (item: Item, valid: Valid) => unknown
|
||||
}
|
||||
|
||||
export interface MenuOption<Item extends SelectorItem> {
|
||||
name: string
|
||||
html: Label
|
||||
selector: Item[]
|
||||
onSelect: (item: Item) => Label
|
||||
position?: 'right'
|
||||
style?: { padding: string }
|
||||
tooltip?: Label
|
||||
icon?: string
|
||||
width?: number
|
||||
}
|
||||
|
||||
export interface Registry {
|
||||
update: <Item extends SelectorItem>(option: MenuOption<Item>) => unknown
|
||||
remove: (name: string) => unknown
|
||||
check: (item: SelectorItem) => unknown
|
||||
cache?: { get?: (name: string) => { option?: { onSelect?: unknown } } | undefined }
|
||||
find?: (name: string) => { onSelect?: unknown } | undefined
|
||||
}
|
||||
|
||||
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 } }
|
||||
isDestroy?: boolean
|
||||
dash?: Dash<Level, Track>
|
||||
on: (name: EventName, callback: Cleanup) => unknown
|
||||
off: (name: EventName, callback: Cleanup) => unknown
|
||||
}
|
||||
|
||||
export type DisplayConfig = Pick<Config, 'control' | 'setting'>
|
||||
Reference in new issue
Block a user