mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-28 02:01:20 -08:00
refactor(mediabunny): [PKG-MB-04] checkpoint typed shim and event cleanup
This commit is contained in:
1 parent
adba8a3d12
commit
9af253a5a9
24 files changed
+1478
-250
No files matched your search
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Event Target Implementation
|
||||
* Simple event system for video events
|
||||
*/
|
||||
export default class EventTarget {
|
||||
constructor() {
|
||||
this.listeners = new Map()
|
||||
}
|
||||
|
||||
addEventListener(type, fn) {
|
||||
if (!this.listeners.has(type)) {
|
||||
this.listeners.set(type, [])
|
||||
}
|
||||
this.listeners.get(type).push(fn)
|
||||
}
|
||||
|
||||
removeEventListener(type, fn) {
|
||||
const list = this.listeners.get(type)
|
||||
if (!list)
|
||||
return
|
||||
|
||||
const index = list.indexOf(fn)
|
||||
if (index >= 0) {
|
||||
list.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
emit(type, detail) {
|
||||
const evt = new Event(type)
|
||||
evt.detail = detail
|
||||
|
||||
const list = this.listeners.get(type)
|
||||
if (list) {
|
||||
list.forEach(fn => fn(evt))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export type MediaEvent = Event & { detail: unknown }
|
||||
export type MediaListener = (event: MediaEvent) => unknown
|
||||
|
||||
export default class EventTarget {
|
||||
declare listeners: Map<string, MediaListener[]>
|
||||
#closed = false
|
||||
|
||||
constructor() {
|
||||
this.listeners = new Map()
|
||||
}
|
||||
|
||||
addEventListener(type: string, fn: MediaListener): void {
|
||||
if (this.#closed)
|
||||
return
|
||||
if (!this.listeners.has(type))
|
||||
this.listeners.set(type, [])
|
||||
this.listeners.get(type)?.push(fn)
|
||||
}
|
||||
|
||||
removeEventListener(type: string, fn: MediaListener): void {
|
||||
const list = this.listeners.get(type)
|
||||
if (!list)
|
||||
return
|
||||
const index = list.indexOf(fn)
|
||||
if (index >= 0)
|
||||
list.splice(index, 1)
|
||||
}
|
||||
|
||||
emit(type: string, detail?: unknown): void {
|
||||
if (this.#closed)
|
||||
return
|
||||
const event = Object.assign(new Event(type), { detail })
|
||||
// Keep historical live-array forEach semantics and uncaught listener exceptions.
|
||||
this.listeners.get(type)?.forEach((fn) => {
|
||||
if (!this.#closed)
|
||||
fn(event)
|
||||
})
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.#closed = true
|
||||
this.listeners.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { EngineOptions, EnginePort } from './engine-types'
|
||||
|
||||
declare const MediaBunnyEngine: new (options: EngineOptions) => EnginePort
|
||||
export default MediaBunnyEngine
|
||||
+53
-62
@@ -1,16 +1,22 @@
|
||||
import EventTarget from './EventTarget.js'
|
||||
/**
|
||||
* Video Element Shim
|
||||
* Simulates HTMLVideoElement interface for MediaBunny
|
||||
*/
|
||||
import type { EngineOptions, EnginePort, ProxyOptions, ShimHost } from './engine-types'
|
||||
import type { MediaListener } from './EventTarget'
|
||||
import EventTarget from './EventTarget'
|
||||
import MediaBunnyEngine from './MediaBunnyEngine.js'
|
||||
|
||||
function clamp(v, min, max) {
|
||||
return Math.max(min, Math.min(max, Number(v) || 0))
|
||||
}
|
||||
import { cancelFrame, closeFrames, requestFrame } from './shim-frames'
|
||||
import { clampVolume, timeRanges } from './shim-values'
|
||||
|
||||
export default class VideoShim {
|
||||
constructor({ art, canvas, ctx, option }) {
|
||||
declare art: ShimHost
|
||||
declare canvas: HTMLCanvasElement
|
||||
declare option: ProxyOptions
|
||||
declare events: EventTarget
|
||||
declare engine: EnginePort
|
||||
declare _src: unknown
|
||||
declare _volume: number
|
||||
declare _muted: boolean
|
||||
declare _playbackRate: number
|
||||
|
||||
constructor({ art, canvas, ctx, option }: Omit<EngineOptions, 'events' | 'option'> & { art: ShimHost, option: ProxyOptions }) {
|
||||
this.art = art
|
||||
this.canvas = canvas
|
||||
this.option = option
|
||||
@@ -52,11 +58,11 @@ export default class VideoShim {
|
||||
}
|
||||
|
||||
// Event methods
|
||||
addEventListener(type, fn) {
|
||||
addEventListener(type: string, fn: MediaListener) {
|
||||
this.events.addEventListener(type, fn)
|
||||
}
|
||||
|
||||
removeEventListener(type, fn) {
|
||||
removeEventListener(type: string, fn: MediaListener) {
|
||||
this.events.removeEventListener(type, fn)
|
||||
}
|
||||
|
||||
@@ -65,7 +71,7 @@ export default class VideoShim {
|
||||
return this._src
|
||||
}
|
||||
|
||||
set src(v) {
|
||||
set src(v: unknown) {
|
||||
this._src = v
|
||||
if (v)
|
||||
this.engine.load(v)
|
||||
@@ -80,7 +86,7 @@ export default class VideoShim {
|
||||
return this.engine.currentTime
|
||||
}
|
||||
|
||||
set currentTime(t) {
|
||||
set currentTime(t: unknown) {
|
||||
this.engine.seek(Number(t) || 0)
|
||||
}
|
||||
|
||||
@@ -101,16 +107,8 @@ export default class VideoShim {
|
||||
return this.createTimeRanges(0, this.engine.duration)
|
||||
}
|
||||
|
||||
createTimeRanges(start, end) {
|
||||
const duration = this.engine.duration
|
||||
if (!duration || Number.isNaN(duration) || end <= 0) {
|
||||
return { length: 0, start: () => 0, end: () => 0 }
|
||||
}
|
||||
return {
|
||||
length: 1,
|
||||
start: () => start,
|
||||
end: () => end,
|
||||
}
|
||||
createTimeRanges(start: number, end: number): TimeRanges {
|
||||
return timeRanges(this.engine.duration, start, end)
|
||||
}
|
||||
|
||||
// Playback state
|
||||
@@ -148,7 +146,7 @@ export default class VideoShim {
|
||||
return this._playbackRate
|
||||
}
|
||||
|
||||
set playbackRate(v) {
|
||||
set playbackRate(v: unknown) {
|
||||
const rate = Number(v)
|
||||
if (Number.isNaN(rate) || rate <= 0)
|
||||
return
|
||||
@@ -163,8 +161,8 @@ export default class VideoShim {
|
||||
return this._volume
|
||||
}
|
||||
|
||||
set volume(v) {
|
||||
this._volume = clamp(v, 0, 1)
|
||||
set volume(v: unknown) {
|
||||
this._volume = clampVolume(v)
|
||||
this._muted = false
|
||||
this.engine.setVolume(this._volume, this._muted)
|
||||
this.events.emit('volumechange')
|
||||
@@ -174,7 +172,7 @@ export default class VideoShim {
|
||||
return this._muted
|
||||
}
|
||||
|
||||
set muted(v) {
|
||||
set muted(v: unknown) {
|
||||
this._muted = !!v
|
||||
this.engine.setVolume(this._volume, this._muted)
|
||||
this.events.emit('volumechange')
|
||||
@@ -198,11 +196,11 @@ export default class VideoShim {
|
||||
return this.engine.getHlsState()
|
||||
}
|
||||
|
||||
switchM3u8Quality(value) {
|
||||
switchM3u8Quality(value: unknown) {
|
||||
return this.engine.selectHlsQuality(value)
|
||||
}
|
||||
|
||||
switchM3u8Audio(value) {
|
||||
switchM3u8Audio(value: unknown) {
|
||||
return this.engine.selectHlsAudio(value)
|
||||
}
|
||||
|
||||
@@ -220,7 +218,7 @@ export default class VideoShim {
|
||||
return this.option.poster || ''
|
||||
}
|
||||
|
||||
set poster(v) {
|
||||
set poster(v: string) {
|
||||
this.option.poster = v
|
||||
}
|
||||
|
||||
@@ -228,52 +226,52 @@ export default class VideoShim {
|
||||
return this.option.autoplay || false
|
||||
}
|
||||
|
||||
set autoplay(v) {}
|
||||
set autoplay(v: unknown) {}
|
||||
|
||||
get loop() {
|
||||
return this.option.loop || false
|
||||
}
|
||||
|
||||
set loop(v) {}
|
||||
set loop(v: unknown) {}
|
||||
|
||||
get controls() {
|
||||
return false
|
||||
}
|
||||
|
||||
set controls(v) {}
|
||||
set controls(v: unknown) {}
|
||||
|
||||
get playsInline() {
|
||||
return true
|
||||
}
|
||||
|
||||
set playsInline(v) {}
|
||||
set playsInline(v: unknown) {}
|
||||
|
||||
get crossOrigin() {
|
||||
return this.option.crossOrigin || ''
|
||||
}
|
||||
|
||||
set crossOrigin(v) {}
|
||||
set crossOrigin(v: unknown) {}
|
||||
|
||||
get preload() {
|
||||
return 'auto'
|
||||
}
|
||||
|
||||
set preload(v) {}
|
||||
set preload(v: unknown) {}
|
||||
|
||||
get defaultMuted() {
|
||||
return false
|
||||
}
|
||||
|
||||
set defaultMuted(v) {}
|
||||
set defaultMuted(v: unknown) {}
|
||||
|
||||
get defaultPlaybackRate() {
|
||||
return 1
|
||||
}
|
||||
|
||||
set defaultPlaybackRate(v) {}
|
||||
set defaultPlaybackRate(v: unknown) {}
|
||||
|
||||
// Methods
|
||||
canPlayType(_type) {
|
||||
canPlayType(_type: string) {
|
||||
return 'maybe'
|
||||
}
|
||||
|
||||
@@ -281,29 +279,15 @@ export default class VideoShim {
|
||||
return this.canvas.getBoundingClientRect()
|
||||
}
|
||||
|
||||
requestVideoFrameCallback(callback) {
|
||||
const id = requestAnimationFrame((time) => {
|
||||
callback(time, {
|
||||
presentationTime: this.engine.currentTime,
|
||||
expectedDisplayTime: time + 16.6,
|
||||
width: this.engine.videoWidth,
|
||||
height: this.engine.videoHeight,
|
||||
mediaTime: this.engine.currentTime,
|
||||
presentedFrames: 0,
|
||||
processingDuration: 0,
|
||||
captureTime: time,
|
||||
receiveTime: time,
|
||||
rtpTimestamp: 0,
|
||||
})
|
||||
})
|
||||
return id
|
||||
requestVideoFrameCallback(callback: VideoFrameRequestCallback): number {
|
||||
return requestFrame(this, callback)
|
||||
}
|
||||
|
||||
cancelVideoFrameCallback(id) {
|
||||
cancelAnimationFrame(id)
|
||||
cancelVideoFrameCallback(id: number): void {
|
||||
cancelFrame(this, id)
|
||||
}
|
||||
|
||||
setAttribute(name, value) {
|
||||
setAttribute(name: string, value: unknown) {
|
||||
if (name === 'src') {
|
||||
this.src = value
|
||||
}
|
||||
@@ -317,11 +301,18 @@ export default class VideoShim {
|
||||
this.muted = true
|
||||
}
|
||||
else {
|
||||
this.canvas.setAttribute(name, value)
|
||||
Reflect.apply(this.canvas.setAttribute, this.canvas, [name, value])
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.engine.destroy()
|
||||
destroy(): void {
|
||||
if (!closeFrames(this))
|
||||
return
|
||||
try {
|
||||
this.engine.destroy()
|
||||
}
|
||||
finally {
|
||||
this.events.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type factory from '../types/artplayer-proxy-mediabunny'
|
||||
import type EventTarget from './EventTarget'
|
||||
import type { MediaEvent } from './EventTarget'
|
||||
import type { getHlsState } from './hls-state'
|
||||
|
||||
export type ProxyOptions = NonNullable<Parameters<typeof factory>[0]>
|
||||
export type HlsState = Awaited<ReturnType<typeof getHlsState>>
|
||||
|
||||
export interface ShimHost {
|
||||
constructor: { config: { events: readonly string[] } }
|
||||
option: { url?: unknown }
|
||||
emit: (type: string, event: MediaEvent) => unknown
|
||||
}
|
||||
|
||||
export interface EngineOptions {
|
||||
canvas: HTMLCanvasElement
|
||||
ctx: CanvasRenderingContext2D | null
|
||||
events: EventTarget
|
||||
option?: ProxyOptions
|
||||
}
|
||||
|
||||
// Transitional boundary for the coordinator; replaced by its strict implementation in MB-04.
|
||||
export interface EnginePort {
|
||||
readonly currentTime: number
|
||||
readonly duration: number
|
||||
readonly videoWidth: number
|
||||
readonly videoHeight: number
|
||||
paused: boolean
|
||||
ended: boolean
|
||||
seeking: boolean
|
||||
readyState: number
|
||||
networkState: number
|
||||
error: { code: number, message: string } | null
|
||||
play: () => Promise<void>
|
||||
pause: () => void
|
||||
load: (source: unknown) => Promise<void>
|
||||
seek: (time: number) => Promise<void>
|
||||
destroy: () => void
|
||||
setVolume: (volume: number, muted: boolean) => void
|
||||
setPlaybackRate: (rate: number) => void
|
||||
getHlsState: () => Promise<HlsState>
|
||||
selectHlsQuality: (value: unknown) => Promise<void>
|
||||
selectHlsAudio: (value: unknown) => Promise<void>
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { EnginePort } from './engine-types'
|
||||
|
||||
interface FrameOwner {
|
||||
engine: Pick<EnginePort, 'currentTime' | 'videoWidth' | 'videoHeight'>
|
||||
}
|
||||
|
||||
interface FrameState {
|
||||
closed: boolean
|
||||
frames: Set<number>
|
||||
}
|
||||
|
||||
// Keep resource bookkeeping off the observable shim own-property/canvas surface.
|
||||
const states = new WeakMap<FrameOwner, FrameState>()
|
||||
function state(owner: FrameOwner): FrameState {
|
||||
let value = states.get(owner)
|
||||
if (!value) {
|
||||
value = { closed: false, frames: new Set() }
|
||||
states.set(owner, value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function requestFrame(owner: FrameOwner, callback: VideoFrameRequestCallback): number {
|
||||
const resource = state(owner)
|
||||
if (resource.closed)
|
||||
return 0
|
||||
const id = requestAnimationFrame((time) => {
|
||||
resource.frames.delete(id)
|
||||
if (resource.closed)
|
||||
return
|
||||
callback(time, {
|
||||
presentationTime: owner.engine.currentTime,
|
||||
expectedDisplayTime: time + 16.6,
|
||||
width: owner.engine.videoWidth,
|
||||
height: owner.engine.videoHeight,
|
||||
mediaTime: owner.engine.currentTime,
|
||||
presentedFrames: 0,
|
||||
processingDuration: 0,
|
||||
captureTime: time,
|
||||
receiveTime: time,
|
||||
rtpTimestamp: 0,
|
||||
})
|
||||
})
|
||||
resource.frames.add(id)
|
||||
return id
|
||||
}
|
||||
|
||||
export function cancelFrame(owner: FrameOwner, id: number): void {
|
||||
state(owner).frames.delete(id)
|
||||
cancelAnimationFrame(id)
|
||||
}
|
||||
|
||||
export function closeFrames(owner: FrameOwner): boolean {
|
||||
const resource = state(owner)
|
||||
if (resource.closed)
|
||||
return false
|
||||
resource.closed = true
|
||||
for (const id of resource.frames) cancelAnimationFrame(id)
|
||||
resource.frames.clear()
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export function clampVolume(value: unknown): number {
|
||||
return Math.max(0, Math.min(1, Number(value) || 0))
|
||||
}
|
||||
|
||||
export function timeRanges(duration: number, start: number, end: number): TimeRanges {
|
||||
if (!duration || Number.isNaN(duration) || end <= 0)
|
||||
return { length: 0, start: () => 0, end: () => 0 }
|
||||
return { length: 1, start: () => start, end: () => end }
|
||||
}
|
||||
Reference in new issue
Block a user