mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-20 04:01:20 -08:00
321 lines
6.4 KiB
TypeScript
321 lines
6.4 KiB
TypeScript
import type { MediaBunnyShim, SyntheticFrameCallback } from '../types/artplayer-proxy-mediabunny'
|
|
import type { EngineOptions, EnginePort, ProxyOptions, ShimHost } from './engine-types'
|
|
import type { MediaListener } from './EventTarget'
|
|
import EventTarget from './EventTarget'
|
|
import MediaBunnyEngine from './MediaBunnyEngine.js'
|
|
import { cancelFrame, closeFrames, requestFrame } from './shim-frames'
|
|
import { clampVolume, timeRanges } from './shim-values'
|
|
|
|
export default class VideoShim implements MediaBunnyShim {
|
|
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
|
|
|
|
// Event system
|
|
this.events = new EventTarget()
|
|
|
|
// MediaBunny engine
|
|
this.engine = new MediaBunnyEngine({ canvas, ctx, events: this.events, option })
|
|
|
|
// Internal state
|
|
this._src = null
|
|
this._volume = option.volume ?? 0.7
|
|
this._muted = !!option.muted
|
|
this._playbackRate = 1
|
|
|
|
// Apply initial volume
|
|
this.engine.setVolume(this._volume, this._muted)
|
|
|
|
// Forward events to ArtPlayer
|
|
this.setupEventForwarding()
|
|
|
|
// Auto-load source
|
|
if (option.source) {
|
|
this.src = option.source
|
|
}
|
|
else if (art.option?.url) {
|
|
this.src = art.option.url
|
|
}
|
|
}
|
|
|
|
setupEventForwarding() {
|
|
const { events: artEvents } = this.art.constructor.config
|
|
artEvents.forEach((name) => {
|
|
this.events.addEventListener(name, (e) => {
|
|
this.art.emit(`video:${e.type}`, e)
|
|
})
|
|
})
|
|
}
|
|
|
|
// Event methods
|
|
addEventListener(type: string, fn: MediaListener) {
|
|
this.events.addEventListener(type, fn)
|
|
}
|
|
|
|
removeEventListener(type: string, fn: MediaListener) {
|
|
this.events.removeEventListener(type, fn)
|
|
}
|
|
|
|
// Source
|
|
get src() {
|
|
return this._src
|
|
}
|
|
|
|
set src(v: unknown) {
|
|
this._src = v
|
|
if (v)
|
|
this.engine.load(v)
|
|
}
|
|
|
|
get currentSrc() {
|
|
return this._src
|
|
}
|
|
|
|
// Time
|
|
get currentTime(): number {
|
|
return this.engine.currentTime
|
|
}
|
|
|
|
set currentTime(t: unknown) {
|
|
// The coordinator reports active failures; the synchronous setter must observe rejection.
|
|
void Promise.resolve(this.engine.seek(Number(t) || 0)).catch(() => {})
|
|
}
|
|
|
|
get duration() {
|
|
return this.engine.duration
|
|
}
|
|
|
|
// Buffered/Played/Seekable
|
|
get buffered() {
|
|
return this.createTimeRanges(0, this.engine.duration)
|
|
}
|
|
|
|
get played() {
|
|
return this.createTimeRanges(0, this.engine.currentTime)
|
|
}
|
|
|
|
get seekable() {
|
|
return this.createTimeRanges(0, this.engine.duration)
|
|
}
|
|
|
|
createTimeRanges(start: number, end: number): TimeRanges {
|
|
return timeRanges(this.engine.duration, start, end)
|
|
}
|
|
|
|
// Playback state
|
|
get paused() {
|
|
return this.engine.paused
|
|
}
|
|
|
|
get playing() {
|
|
return !this.engine.paused && !this.engine.ended
|
|
}
|
|
|
|
get ended() {
|
|
return this.engine.ended
|
|
}
|
|
|
|
get seeking() {
|
|
return this.engine.seeking
|
|
}
|
|
|
|
// Ready state
|
|
get readyState() {
|
|
return this.engine.readyState
|
|
}
|
|
|
|
get networkState() {
|
|
return this.engine.networkState
|
|
}
|
|
|
|
get error() {
|
|
return this.engine.error
|
|
}
|
|
|
|
// Playback rate
|
|
get playbackRate(): number {
|
|
return this._playbackRate
|
|
}
|
|
|
|
set playbackRate(v: unknown) {
|
|
const rate = Number(v)
|
|
if (Number.isNaN(rate) || rate <= 0)
|
|
return
|
|
|
|
this._playbackRate = rate
|
|
this.engine.setPlaybackRate(rate)
|
|
this.events.emit('ratechange')
|
|
}
|
|
|
|
// Volume
|
|
get volume(): number {
|
|
return this._volume
|
|
}
|
|
|
|
set volume(v: unknown) {
|
|
this._volume = clampVolume(v)
|
|
this._muted = false
|
|
this.engine.setVolume(this._volume, this._muted)
|
|
this.events.emit('volumechange')
|
|
}
|
|
|
|
get muted(): boolean {
|
|
return this._muted
|
|
}
|
|
|
|
set muted(v: unknown) {
|
|
this._muted = !!v
|
|
this.engine.setVolume(this._volume, this._muted)
|
|
this.events.emit('volumechange')
|
|
}
|
|
|
|
// Playback methods
|
|
play() {
|
|
return this.engine.play()
|
|
}
|
|
|
|
pause() {
|
|
this.engine.pause()
|
|
}
|
|
|
|
load() {
|
|
if (this._src)
|
|
this.engine.load(this._src)
|
|
}
|
|
|
|
getM3u8State() {
|
|
return this.engine.getHlsState()
|
|
}
|
|
|
|
switchM3u8Quality(value: unknown) {
|
|
return this.engine.selectHlsQuality(value)
|
|
}
|
|
|
|
switchM3u8Audio(value: unknown) {
|
|
return this.engine.selectHlsAudio(value)
|
|
}
|
|
|
|
// Video dimensions
|
|
get videoWidth() {
|
|
return this.engine.videoWidth
|
|
}
|
|
|
|
get videoHeight() {
|
|
return this.engine.videoHeight
|
|
}
|
|
|
|
// Other properties
|
|
get poster() {
|
|
return this.option.poster || ''
|
|
}
|
|
|
|
set poster(v: string) {
|
|
this.option.poster = v
|
|
}
|
|
|
|
get autoplay(): boolean {
|
|
return this.option.autoplay || false
|
|
}
|
|
|
|
set autoplay(v: unknown) {}
|
|
|
|
get loop(): boolean {
|
|
return this.option.loop || false
|
|
}
|
|
|
|
set loop(v: unknown) {}
|
|
|
|
get controls(): boolean {
|
|
return false
|
|
}
|
|
|
|
set controls(v: unknown) {}
|
|
|
|
get playsInline(): boolean {
|
|
return true
|
|
}
|
|
|
|
set playsInline(v: unknown) {}
|
|
|
|
get crossOrigin(): string {
|
|
return this.option.crossOrigin || ''
|
|
}
|
|
|
|
set crossOrigin(v: unknown) {}
|
|
|
|
get preload(): string {
|
|
return 'auto'
|
|
}
|
|
|
|
set preload(v: unknown) {}
|
|
|
|
get defaultMuted(): boolean {
|
|
return false
|
|
}
|
|
|
|
set defaultMuted(v: unknown) {}
|
|
|
|
get defaultPlaybackRate(): number {
|
|
return 1
|
|
}
|
|
|
|
set defaultPlaybackRate(v: unknown) {}
|
|
|
|
// Methods
|
|
canPlayType(_type: string): 'maybe' {
|
|
return 'maybe'
|
|
}
|
|
|
|
getBoundingClientRect() {
|
|
return this.canvas.getBoundingClientRect()
|
|
}
|
|
|
|
requestVideoFrameCallback(callback: SyntheticFrameCallback): number {
|
|
return requestFrame(this, callback)
|
|
}
|
|
|
|
cancelVideoFrameCallback(id: number): void {
|
|
cancelFrame(this, id)
|
|
}
|
|
|
|
setAttribute(name: string, value: unknown) {
|
|
if (name === 'src') {
|
|
this.src = value
|
|
}
|
|
else if (name === 'autoplay') {
|
|
this.autoplay = value
|
|
}
|
|
else if (name === 'loop') {
|
|
this.loop = value
|
|
}
|
|
else if (name === 'muted') {
|
|
this.muted = true
|
|
}
|
|
else {
|
|
Reflect.apply(this.canvas.setAttribute, this.canvas, [name, value])
|
|
}
|
|
}
|
|
|
|
destroy(): void {
|
|
if (!closeFrames(this))
|
|
return
|
|
try {
|
|
this.engine.destroy()
|
|
}
|
|
finally {
|
|
this.events.destroy()
|
|
}
|
|
}
|
|
}
|