mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-24 23:51:20 -08:00
refactor: remove architecture documentation and improve code readability
- Deleted ARCHITECTURE.md file to streamline project documentation. - Refactored AudioEngine, EventTarget, MediaBunnyEngine, VideoEngine, VideoShim, and index.js for improved readability by adding consistent line breaks and spacing. - Enhanced code structure by ensuring single-responsibility principles and clearer flow in methods. - Updated TypeScript definitions for better clarity on options available in the artplayer-proxy-mediabunny package.
This commit is contained in:
1 parent
be44c1f8dd
commit
9d760a2f59
10 files changed
+164
-275
No files matched your search
@@ -14,36 +14,37 @@ import {
|
||||
export default class AudioEngine {
|
||||
constructor(events) {
|
||||
this.events = events
|
||||
|
||||
|
||||
// MediaBunny instances
|
||||
this.input = null
|
||||
this.audioSink = null
|
||||
this.audioIterator = null
|
||||
|
||||
|
||||
// Web Audio API
|
||||
this.audioContext = null
|
||||
this.gainNode = null
|
||||
|
||||
|
||||
// Playback state
|
||||
this.audioContextStartTime = 0
|
||||
this.playbackTimeAtStart = 0
|
||||
this.latestScheduledEndTime = 0
|
||||
this.duration = Number.NaN
|
||||
this.paused = true
|
||||
|
||||
|
||||
// Audio settings
|
||||
this.volume = 0.7
|
||||
this.muted = false
|
||||
this.playbackRate = 1
|
||||
|
||||
|
||||
// Async control
|
||||
this.asyncId = 0
|
||||
this.queuedNodes = new Set()
|
||||
}
|
||||
|
||||
get currentTime() {
|
||||
if (this.paused) return this.playbackTimeAtStart
|
||||
|
||||
if (this.paused)
|
||||
return this.playbackTimeAtStart
|
||||
|
||||
return (
|
||||
(this.audioContext.currentTime - this.audioContextStartTime) * this.playbackRate
|
||||
+ this.playbackTimeAtStart
|
||||
@@ -51,8 +52,10 @@ export default class AudioEngine {
|
||||
}
|
||||
|
||||
normalizeSource(src) {
|
||||
if (typeof src === 'string') return new UrlSource(src)
|
||||
if (src instanceof Blob) return new BlobSource(src)
|
||||
if (typeof src === 'string')
|
||||
return new UrlSource(src)
|
||||
if (src instanceof Blob)
|
||||
return new BlobSource(src)
|
||||
if (typeof ReadableStream !== 'undefined' && src instanceof ReadableStream) {
|
||||
return new ReadableStreamSource(src)
|
||||
}
|
||||
@@ -60,13 +63,15 @@ export default class AudioEngine {
|
||||
}
|
||||
|
||||
ensureAudioContext(sampleRate) {
|
||||
if (this.audioContext) return
|
||||
if (this.audioContext)
|
||||
return
|
||||
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext
|
||||
|
||||
|
||||
try {
|
||||
this.audioContext = new AudioContext({ sampleRate })
|
||||
} catch {
|
||||
}
|
||||
catch {
|
||||
this.audioContext = new AudioContext()
|
||||
}
|
||||
|
||||
@@ -76,7 +81,8 @@ export default class AudioEngine {
|
||||
}
|
||||
|
||||
updateGain() {
|
||||
if (!this.gainNode) return
|
||||
if (!this.gainNode)
|
||||
return
|
||||
const v = this.muted ? 0 : this.volume
|
||||
this.gainNode.gain.value = v * v
|
||||
}
|
||||
@@ -102,7 +108,8 @@ export default class AudioEngine {
|
||||
this.audioContextStartTime = 0
|
||||
|
||||
const source = this.normalizeSource(src)
|
||||
if (!source) return
|
||||
if (!source)
|
||||
return
|
||||
|
||||
this.input = new Input({
|
||||
source,
|
||||
@@ -110,7 +117,8 @@ export default class AudioEngine {
|
||||
})
|
||||
|
||||
this.duration = await this.input.computeDuration()
|
||||
if (id !== this.asyncId) return
|
||||
if (id !== this.asyncId)
|
||||
return
|
||||
|
||||
const audioTrack = await this.input.getPrimaryAudioTrack()
|
||||
if (!audioTrack) {
|
||||
@@ -134,13 +142,15 @@ export default class AudioEngine {
|
||||
}
|
||||
|
||||
async runIterator(localId) {
|
||||
if (!this.audioSink) return
|
||||
if (!this.audioSink)
|
||||
return
|
||||
|
||||
await this.stopIterator()
|
||||
this.audioIterator = this.audioSink.buffers(this.currentTime)
|
||||
|
||||
while (true) {
|
||||
if (localId !== this.asyncId || this.paused) return
|
||||
if (localId !== this.asyncId || this.paused)
|
||||
return
|
||||
|
||||
const nextPromise = this.audioIterator.next()
|
||||
|
||||
@@ -150,10 +160,10 @@ export default class AudioEngine {
|
||||
clearInterval(checkStarvation)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
this.audioContext.state === 'running' &&
|
||||
this.audioContext.currentTime >= this.latestScheduledEndTime - 0.2
|
||||
this.audioContext.state === 'running'
|
||||
&& this.audioContext.currentTime >= this.latestScheduledEndTime - 0.2
|
||||
) {
|
||||
this.audioContext.suspend()
|
||||
this.events.emit('waiting')
|
||||
@@ -163,14 +173,17 @@ export default class AudioEngine {
|
||||
let result
|
||||
try {
|
||||
result = await nextPromise
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Audio iterator error:', e)
|
||||
break
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
clearInterval(checkStarvation)
|
||||
}
|
||||
|
||||
if (localId !== this.asyncId || this.paused) return
|
||||
if (localId !== this.asyncId || this.paused)
|
||||
return
|
||||
|
||||
// Resume if was suspended
|
||||
if (this.audioContext.state === 'suspended') {
|
||||
@@ -179,7 +192,8 @@ export default class AudioEngine {
|
||||
this.events.emit('playing')
|
||||
}
|
||||
|
||||
if (result.done) break
|
||||
if (result.done)
|
||||
break
|
||||
|
||||
const { buffer, timestamp } = result.value
|
||||
|
||||
@@ -189,9 +203,9 @@ export default class AudioEngine {
|
||||
node.connect(this.gainNode)
|
||||
node.playbackRate.value = this.playbackRate
|
||||
|
||||
const startAt =
|
||||
this.audioContextStartTime +
|
||||
(timestamp - this.playbackTimeAtStart) / this.playbackRate
|
||||
const startAt
|
||||
= this.audioContextStartTime
|
||||
+ (timestamp - this.playbackTimeAtStart) / this.playbackRate
|
||||
|
||||
const duration = buffer.duration
|
||||
const endAt = startAt + duration / this.playbackRate
|
||||
@@ -202,10 +216,11 @@ export default class AudioEngine {
|
||||
|
||||
if (startAt >= this.audioContext.currentTime) {
|
||||
node.start(startAt)
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
node.start(
|
||||
this.audioContext.currentTime,
|
||||
(this.audioContext.currentTime - startAt) * this.playbackRate
|
||||
(this.audioContext.currentTime - startAt) * this.playbackRate,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -215,7 +230,8 @@ export default class AudioEngine {
|
||||
}
|
||||
|
||||
async play() {
|
||||
if (!this.paused) return
|
||||
if (!this.paused)
|
||||
return
|
||||
|
||||
if (!this.audioContext) {
|
||||
this.ensureAudioContext()
|
||||
@@ -234,7 +250,8 @@ export default class AudioEngine {
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (this.paused) return
|
||||
if (this.paused)
|
||||
return
|
||||
|
||||
this.playbackTimeAtStart = this.currentTime
|
||||
this.paused = true
|
||||
@@ -261,7 +278,8 @@ export default class AudioEngine {
|
||||
}
|
||||
|
||||
setPlaybackRate(rate) {
|
||||
if (rate === this.playbackRate) return
|
||||
if (rate === this.playbackRate)
|
||||
return
|
||||
|
||||
if (!this.paused) {
|
||||
this.playbackTimeAtStart = this.currentTime
|
||||
|
||||
@@ -16,8 +16,9 @@ export default class EventTarget {
|
||||
|
||||
removeEventListener(type, fn) {
|
||||
const list = this.listeners.get(type)
|
||||
if (!list) return
|
||||
|
||||
if (!list)
|
||||
return
|
||||
|
||||
const index = list.indexOf(fn)
|
||||
if (index >= 0) {
|
||||
list.splice(index, 1)
|
||||
@@ -27,7 +28,7 @@ export default class EventTarget {
|
||||
emit(type, detail) {
|
||||
const evt = new Event(type)
|
||||
evt.detail = detail
|
||||
|
||||
|
||||
const list = this.listeners.get(type)
|
||||
if (list) {
|
||||
list.forEach(fn => fn(evt))
|
||||
|
||||
@@ -9,7 +9,7 @@ export default class MediaBunnyEngine {
|
||||
constructor({ canvas, ctx, events, option = {} }) {
|
||||
this.events = events
|
||||
this.option = option
|
||||
|
||||
|
||||
// Create audio and video engines
|
||||
this.audio = new AudioEngine(events)
|
||||
this.video = new VideoEngine({
|
||||
@@ -41,13 +41,13 @@ export default class MediaBunnyEngine {
|
||||
|
||||
async load(src) {
|
||||
const id = ++this.loadSeq
|
||||
|
||||
|
||||
this.pause()
|
||||
this.ended = false
|
||||
this.error = null
|
||||
this.networkState = 2 // NETWORK_LOADING
|
||||
this.readyState = 0 // HAVE_NOTHING
|
||||
|
||||
|
||||
setTimeout(() => this.events.emit('waiting'), 0)
|
||||
setTimeout(() => this.events.emit('loadstart'), 0)
|
||||
|
||||
@@ -60,9 +60,11 @@ export default class MediaBunnyEngine {
|
||||
this.performLoad(src, id),
|
||||
loadTimeout > 0 ? this.createTimeout(loadTimeout) : Promise.resolve(),
|
||||
])
|
||||
} catch (err) {
|
||||
if (id !== this.loadSeq) return
|
||||
|
||||
}
|
||||
catch (err) {
|
||||
if (id !== this.loadSeq)
|
||||
return
|
||||
|
||||
this.loadSeq++
|
||||
this.error = { code: 4, message: err.message }
|
||||
this.networkState = 3 // NETWORK_NO_SOURCE
|
||||
@@ -79,33 +81,40 @@ export default class MediaBunnyEngine {
|
||||
this.readyState = 1 // HAVE_METADATA
|
||||
this.events.emit('loadedmetadata')
|
||||
this.events.emit('durationchange')
|
||||
this.events.emit('progress')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
this.video.load(src, () => {
|
||||
if (id !== this.loadSeq) return
|
||||
if (id !== this.loadSeq)
|
||||
return
|
||||
videoMetadataLoaded = true
|
||||
checkMetadata()
|
||||
}),
|
||||
this.audio.load(src, () => {
|
||||
if (id !== this.loadSeq) return
|
||||
if (id !== this.loadSeq)
|
||||
return
|
||||
audioMetadataLoaded = true
|
||||
checkMetadata()
|
||||
}),
|
||||
])
|
||||
|
||||
if (id !== this.loadSeq) return
|
||||
if (id !== this.loadSeq)
|
||||
return
|
||||
|
||||
this.readyState = 4 // HAVE_ENOUGH_DATA
|
||||
this.networkState = 1 // NETWORK_IDLE
|
||||
this.events.emit('loadeddata')
|
||||
this.events.emit('canplay')
|
||||
this.events.emit('canplaythrough')
|
||||
} catch (err) {
|
||||
if (id !== this.loadSeq) return
|
||||
|
||||
this.events.emit('progress')
|
||||
}
|
||||
catch (err) {
|
||||
if (id !== this.loadSeq)
|
||||
return
|
||||
|
||||
this.error = { code: 4, message: err.message }
|
||||
this.networkState = 3
|
||||
this.events.emit('error')
|
||||
@@ -120,7 +129,8 @@ export default class MediaBunnyEngine {
|
||||
}
|
||||
|
||||
async play() {
|
||||
if (!this.paused) return
|
||||
if (!this.paused)
|
||||
return
|
||||
|
||||
if (this.ended) {
|
||||
this.ended = false
|
||||
@@ -128,44 +138,45 @@ export default class MediaBunnyEngine {
|
||||
}
|
||||
|
||||
this.paused = false
|
||||
|
||||
|
||||
await this.audio.play()
|
||||
this.video.start(this.audio)
|
||||
|
||||
|
||||
this.events.emit('play')
|
||||
this.events.emit('playing')
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (this.paused) return
|
||||
if (this.paused)
|
||||
return
|
||||
|
||||
this.paused = true
|
||||
|
||||
|
||||
this.audio.pause()
|
||||
this.video.stop()
|
||||
|
||||
|
||||
this.events.emit('pause')
|
||||
}
|
||||
|
||||
async seek(time) {
|
||||
const shouldResume = !this.paused
|
||||
|
||||
|
||||
this.ended = false
|
||||
this.seeking = true
|
||||
|
||||
|
||||
this.events.emit('seeking')
|
||||
this.events.emit('waiting')
|
||||
|
||||
|
||||
this.pause()
|
||||
|
||||
|
||||
await Promise.all([
|
||||
this.audio.seek(time),
|
||||
this.video.seek(time),
|
||||
])
|
||||
|
||||
|
||||
this.seeking = false
|
||||
this.events.emit('seeked')
|
||||
|
||||
|
||||
if (shouldResume && !this.ended) {
|
||||
await this.play()
|
||||
}
|
||||
|
||||
@@ -55,8 +55,10 @@ export default class VideoEngine {
|
||||
}
|
||||
|
||||
normalizeSource(src) {
|
||||
if (typeof src === 'string') return new UrlSource(src)
|
||||
if (src instanceof Blob) return new BlobSource(src)
|
||||
if (typeof src === 'string')
|
||||
return new UrlSource(src)
|
||||
if (src instanceof Blob)
|
||||
return new BlobSource(src)
|
||||
if (typeof ReadableStream !== 'undefined' && src instanceof ReadableStream) {
|
||||
return new ReadableStreamSource(src)
|
||||
}
|
||||
@@ -64,8 +66,9 @@ export default class VideoEngine {
|
||||
}
|
||||
|
||||
async preflight(url) {
|
||||
if (!this.preflightRange || typeof url !== 'string') return true
|
||||
|
||||
if (!this.preflightRange || typeof url !== 'string')
|
||||
return true
|
||||
|
||||
try {
|
||||
const res = await fetch(url, { method: 'HEAD' })
|
||||
const acceptRanges = res.headers.get('accept-ranges')
|
||||
@@ -74,15 +77,17 @@ export default class VideoEngine {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} catch (e) {
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('Preflight check failed:', e)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
drawPoster() {
|
||||
if (!this.poster || this.posterDrawn) return
|
||||
|
||||
if (!this.poster || this.posterDrawn)
|
||||
return
|
||||
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
@@ -110,7 +115,8 @@ export default class VideoEngine {
|
||||
this.clear()
|
||||
this.posterDrawn = false
|
||||
|
||||
if (!(await this.preflight(src))) return
|
||||
if (!(await this.preflight(src)))
|
||||
return
|
||||
|
||||
const source = this.normalizeSource(src)
|
||||
if (!source) {
|
||||
@@ -124,7 +130,8 @@ export default class VideoEngine {
|
||||
})
|
||||
|
||||
this.duration = await this.input.computeDuration()
|
||||
if (id !== this.asyncId) return
|
||||
if (id !== this.asyncId)
|
||||
return
|
||||
|
||||
const videoTrack = await this.input.getPrimaryVideoTrack()
|
||||
if (!videoTrack) {
|
||||
@@ -170,7 +177,8 @@ export default class VideoEngine {
|
||||
async resetIterator(time) {
|
||||
await this.stopIterator()
|
||||
|
||||
if (!this.videoSink) return
|
||||
if (!this.videoSink)
|
||||
return
|
||||
|
||||
this.videoIterator = this.videoSink.canvases(time)
|
||||
|
||||
@@ -182,17 +190,20 @@ export default class VideoEngine {
|
||||
if (first) {
|
||||
this.ctx.drawImage(first.canvas, 0, 0)
|
||||
this.events.emit('loadeddata')
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.drawPoster()
|
||||
}
|
||||
}
|
||||
|
||||
async updateNextFrame(localId) {
|
||||
if (!this.videoIterator) return
|
||||
if (!this.videoIterator)
|
||||
return
|
||||
|
||||
while (true) {
|
||||
const frame = (await this.videoIterator.next()).value ?? null
|
||||
if (!frame || localId !== this.asyncId) return
|
||||
if (!frame || localId !== this.asyncId)
|
||||
return
|
||||
|
||||
const t = this.audioClock.currentTime
|
||||
const tolerance = this.dropLateFrames
|
||||
@@ -207,12 +218,13 @@ export default class VideoEngine {
|
||||
if (frame.timestamp <= t + tolerance) {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
this.ctx.drawImage(frame.canvas, 0, 0)
|
||||
|
||||
|
||||
if (!this.dropLateFrames && frame.timestamp > t) {
|
||||
this.nextFrame = null
|
||||
return
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.nextFrame = frame
|
||||
return
|
||||
}
|
||||
@@ -220,7 +232,8 @@ export default class VideoEngine {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.audioClock) return
|
||||
if (!this.audioClock)
|
||||
return
|
||||
|
||||
const t = this.audioClock.currentTime
|
||||
const now = Date.now()
|
||||
@@ -247,15 +260,16 @@ export default class VideoEngine {
|
||||
this.ctx.drawImage(this.nextFrame.canvas, 0, 0)
|
||||
this.nextFrame = null
|
||||
this.updateNextFrame(this.asyncId)
|
||||
|
||||
|
||||
if (this.stalled) {
|
||||
this.events.emit('canplay')
|
||||
this.events.emit('playing')
|
||||
this.stalled = false
|
||||
}
|
||||
} else if (!this.nextFrame) {
|
||||
}
|
||||
else if (!this.nextFrame) {
|
||||
this.updateNextFrame(this.asyncId)
|
||||
|
||||
|
||||
if (!this.nextFrame && Number.isFinite(this.duration) && t < this.duration && !this.stalled) {
|
||||
this.stalled = true
|
||||
this.events.emit('waiting')
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import EventTarget from './EventTarget.js'
|
||||
/**
|
||||
* Video Element Shim
|
||||
* Simulates HTMLVideoElement interface for MediaBunny
|
||||
*/
|
||||
import MediaBunnyEngine from './MediaBunnyEngine.js'
|
||||
import EventTarget from './EventTarget.js'
|
||||
|
||||
function clamp(v, min, max) {
|
||||
return Math.max(min, Math.min(max, Number(v) || 0))
|
||||
@@ -17,7 +17,7 @@ export default class VideoShim {
|
||||
|
||||
// Event system
|
||||
this.events = new EventTarget()
|
||||
|
||||
|
||||
// MediaBunny engine
|
||||
this.engine = new MediaBunnyEngine({ canvas, ctx, events: this.events, option })
|
||||
|
||||
@@ -36,7 +36,8 @@ export default class VideoShim {
|
||||
// Auto-load source
|
||||
if (option.source) {
|
||||
this.src = option.source
|
||||
} else if (art.option?.url) {
|
||||
}
|
||||
else if (art.option?.url) {
|
||||
this.src = art.option.url
|
||||
}
|
||||
}
|
||||
@@ -66,7 +67,8 @@ export default class VideoShim {
|
||||
|
||||
set src(v) {
|
||||
this._src = v
|
||||
if (v) this.engine.load(v)
|
||||
if (v)
|
||||
this.engine.load(v)
|
||||
}
|
||||
|
||||
get currentSrc() {
|
||||
@@ -148,8 +150,9 @@ export default class VideoShim {
|
||||
|
||||
set playbackRate(v) {
|
||||
const rate = Number(v)
|
||||
if (Number.isNaN(rate) || rate <= 0) return
|
||||
|
||||
if (Number.isNaN(rate) || rate <= 0)
|
||||
return
|
||||
|
||||
this._playbackRate = rate
|
||||
this.engine.setPlaybackRate(rate)
|
||||
this.events.emit('ratechange')
|
||||
@@ -187,7 +190,8 @@ export default class VideoShim {
|
||||
}
|
||||
|
||||
load() {
|
||||
if (this._src) this.engine.load(this._src)
|
||||
if (this._src)
|
||||
this.engine.load(this._src)
|
||||
}
|
||||
|
||||
// Video dimensions
|
||||
@@ -290,13 +294,17 @@ export default class VideoShim {
|
||||
setAttribute(name, value) {
|
||||
if (name === 'src') {
|
||||
this.src = value
|
||||
} else if (name === 'autoplay') {
|
||||
}
|
||||
else if (name === 'autoplay') {
|
||||
this.autoplay = value
|
||||
} else if (name === 'loop') {
|
||||
}
|
||||
else if (name === 'loop') {
|
||||
this.loop = value
|
||||
} else if (name === 'muted') {
|
||||
}
|
||||
else if (name === 'muted') {
|
||||
this.muted = true
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.canvas.setAttribute(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@ export default function artplayerProxyMediabunny(option = {}) {
|
||||
|
||||
// Add shim properties to canvas
|
||||
for (const prop of propertyNames) {
|
||||
if (prop === 'constructor') continue
|
||||
if (prop === 'constructor')
|
||||
continue
|
||||
if (!(prop in canvas)) {
|
||||
Object.defineProperty(canvas, prop, {
|
||||
get() {
|
||||
@@ -61,7 +62,8 @@ export default function artplayerProxyMediabunny(option = {}) {
|
||||
// Handle resize
|
||||
function resize() {
|
||||
const player = art.template?.$player
|
||||
if (!player || art.option.autoSize) return
|
||||
if (!player || art.option.autoSize)
|
||||
return
|
||||
|
||||
Object.assign(canvas.style, {
|
||||
width: '100%',
|
||||
|
||||
Reference in new issue
Block a user