mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-29 02:51:20 -08:00
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:
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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user