Enhance property handling in artplayerProxyMediabunny

- Updated the way properties from the shim instance and its prototype are retrieved.
- Replaced the for-in loop with a Set to ensure all properties, including inherited ones, are considered.
- Excluded the 'constructor' property from being added to the canvas.

refactor: remove unused utility functions and streamline event handling

- Deleted `videoEvents.js`, `clamp.js`, and `sleep.js` as they were not utilized.
- Introduced a new `EventTarget` class to manage event listeners more efficiently.
- Enhanced `MediaBunnyEngine`, `AudioEngine`, `VideoEngine`, and `VideoShim` to utilize the new event system.
- Updated type definitions in `artplayer-proxy-mediabunny.d.ts` to include new configuration options.
- Added comprehensive architecture documentation to clarify the structure and flow of the MediaBunny engine.
- Refactored the codebase for improved readability and maintainability, including better naming conventions and JSDoc comments.
This commit is contained in:
Harvey Zhao committed 2026-01-01 17:03:44 +08:00
1 parent 59e9a32ecb
commit be44c1f8dd
25 files changed
+2299 -10309

No files matched your search

@@ -0,0 +1,287 @@
/**
* Audio Engine for MediaBunny
* Handles audio playback using Web Audio API
*/
import {
ALL_FORMATS,
AudioBufferSink,
BlobSource,
Input,
ReadableStreamSource,
UrlSource,
} from 'mediabunny'
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
return (
(this.audioContext.currentTime - this.audioContextStartTime) * this.playbackRate
+ this.playbackTimeAtStart
)
}
normalizeSource(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)
}
return src
}
ensureAudioContext(sampleRate) {
if (this.audioContext) return
const AudioContext = window.AudioContext || window.webkitAudioContext
try {
this.audioContext = new AudioContext({ sampleRate })
} catch {
this.audioContext = new AudioContext()
}
this.gainNode = this.audioContext.createGain()
this.gainNode.connect(this.audioContext.destination)
this.updateGain()
}
updateGain() {
if (!this.gainNode) return
const v = this.muted ? 0 : this.volume
this.gainNode.gain.value = v * v
}
stopQueuedNodes() {
this.queuedNodes.forEach(node => node.stop())
this.queuedNodes.clear()
}
async stopIterator() {
await this.audioIterator?.return()
this.audioIterator = null
}
async load(src, onMetadata) {
const id = ++this.asyncId
await this.stopIterator()
this.stopQueuedNodes()
this.paused = true
this.playbackTimeAtStart = 0
this.audioContextStartTime = 0
const source = this.normalizeSource(src)
if (!source) return
this.input = new Input({
source,
formats: ALL_FORMATS,
})
this.duration = await this.input.computeDuration()
if (id !== this.asyncId) return
const audioTrack = await this.input.getPrimaryAudioTrack()
if (!audioTrack) {
this.audioSink = null
this.ensureAudioContext()
onMetadata?.()
return
}
if (audioTrack.codec === null || !(await audioTrack.canDecode())) {
this.audioSink = null
this.ensureAudioContext()
onMetadata?.()
return
}
this.ensureAudioContext(audioTrack.sampleRate)
this.audioSink = new AudioBufferSink(audioTrack)
onMetadata?.()
}
async runIterator(localId) {
if (!this.audioSink) return
await this.stopIterator()
this.audioIterator = this.audioSink.buffers(this.currentTime)
while (true) {
if (localId !== this.asyncId || this.paused) return
const nextPromise = this.audioIterator.next()
// Monitor for buffer starvation
const checkStarvation = setInterval(() => {
if (localId !== this.asyncId || this.paused) {
clearInterval(checkStarvation)
return
}
if (
this.audioContext.state === 'running' &&
this.audioContext.currentTime >= this.latestScheduledEndTime - 0.2
) {
this.audioContext.suspend()
this.events.emit('waiting')
}
}, 50)
let result
try {
result = await nextPromise
} catch (e) {
console.error('Audio iterator error:', e)
break
} finally {
clearInterval(checkStarvation)
}
if (localId !== this.asyncId || this.paused) return
// Resume if was suspended
if (this.audioContext.state === 'suspended') {
await this.audioContext.resume()
this.events.emit('canplay')
this.events.emit('playing')
}
if (result.done) break
const { buffer, timestamp } = result.value
// Schedule audio buffer
const node = this.audioContext.createBufferSource()
node.buffer = buffer
node.connect(this.gainNode)
node.playbackRate.value = this.playbackRate
const startAt =
this.audioContextStartTime +
(timestamp - this.playbackTimeAtStart) / this.playbackRate
const duration = buffer.duration
const endAt = startAt + duration / this.playbackRate
if (endAt > this.latestScheduledEndTime) {
this.latestScheduledEndTime = endAt
}
if (startAt >= this.audioContext.currentTime) {
node.start(startAt)
} else {
node.start(
this.audioContext.currentTime,
(this.audioContext.currentTime - startAt) * this.playbackRate
)
}
this.queuedNodes.add(node)
node.onended = () => this.queuedNodes.delete(node)
}
}
async play() {
if (!this.paused) return
if (!this.audioContext) {
this.ensureAudioContext()
}
if (this.audioContext.state === 'suspended') {
await this.audioContext.resume()
}
this.audioContextStartTime = this.audioContext.currentTime
this.latestScheduledEndTime = this.audioContextStartTime
this.paused = false
const id = ++this.asyncId
this.runIterator(id)
}
pause() {
if (this.paused) return
this.playbackTimeAtStart = this.currentTime
this.paused = true
this.stopIterator()
this.stopQueuedNodes()
}
async seek(time) {
this.playbackTimeAtStart = Math.max(0, time)
this.audioContextStartTime = this.audioContext.currentTime
this.latestScheduledEndTime = this.audioContextStartTime
const id = ++this.asyncId
if (!this.paused) {
this.runIterator(id)
}
}
setVolume(volume, muted) {
this.volume = volume
this.muted = muted
this.updateGain()
}
setPlaybackRate(rate) {
if (rate === this.playbackRate) return
if (!this.paused) {
this.playbackTimeAtStart = this.currentTime
this.audioContextStartTime = this.audioContext.currentTime
}
this.playbackRate = rate
if (!this.paused) {
const id = ++this.asyncId
this.runIterator(id)
}
}
destroy() {
this.asyncId++
this.pause()
this.audioContext?.close()
this.audioContext = null
this.input = null
this.audioSink = null
}
}