mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-22 05:11:20 -08:00
feat: add danmuku plugin and i18n setup documentation - Introduced a new danmuku plugin with multiple run code options in the documentation. - Added language settings documentation for i18n, including default languages, import instructions, and examples for adding and modifying languages. - Implemented input handling for media sources in the artplayer-proxy-mediabunny package, supporting various formats including HLS. - Created m3u8 control functionalities for managing audio and video quality settings in the player. Update mediabunny.js
360 lines
8.0 KiB
JavaScript
360 lines
8.0 KiB
JavaScript
/**
|
|
* Main MediaBunny Engine
|
|
* Coordinates audio and video playback
|
|
*/
|
|
import AudioEngine from './AudioEngine.js'
|
|
import { createInput, getHlsState, resolveDuration, selectPlaybackTracks } from './input.js'
|
|
import VideoEngine from './VideoEngine.js'
|
|
|
|
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({
|
|
canvas,
|
|
ctx,
|
|
events,
|
|
timeupdateInterval: option.timeupdateInterval ?? 250,
|
|
avSyncTolerance: option.avSyncTolerance ?? 0.12,
|
|
dropLateFrames: option.dropLateFrames ?? false,
|
|
poster: option.poster ?? '',
|
|
preflightRange: option.preflightRange ?? false,
|
|
})
|
|
|
|
// Playback state
|
|
this.paused = true
|
|
this.ended = false
|
|
this.readyState = 0
|
|
this.networkState = 0
|
|
this.error = null
|
|
this.seeking = false
|
|
this.loadSeq = 0
|
|
this.input = null
|
|
this.media = null
|
|
|
|
// Listen to ended event
|
|
events.addEventListener?.('ended', () => {
|
|
this.ended = true
|
|
this.paused = true
|
|
})
|
|
}
|
|
|
|
async load(src) {
|
|
const id = ++this.loadSeq
|
|
|
|
this.pause()
|
|
this.disposeInput()
|
|
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)
|
|
|
|
const loadTimeout = Number.isFinite(this.option.loadTimeout)
|
|
? this.option.loadTimeout
|
|
: 0
|
|
|
|
try {
|
|
await Promise.race([
|
|
this.performLoad(src, id),
|
|
loadTimeout > 0 ? this.createTimeout(loadTimeout) : new Promise(() => {}),
|
|
])
|
|
}
|
|
catch (err) {
|
|
if (id !== this.loadSeq)
|
|
return
|
|
|
|
this.loadSeq++
|
|
this.error = { code: 4, message: err.message }
|
|
this.networkState = 3 // NETWORK_NO_SOURCE
|
|
this.events.emit('error')
|
|
}
|
|
}
|
|
|
|
async performLoad(src, id) {
|
|
if (!(await this.video.preflight(src)))
|
|
return
|
|
|
|
const input = createInput(src)
|
|
if (!input) {
|
|
this.video.handleNoVideoTrack()
|
|
this.audio.handleNoAudioTrack()
|
|
return
|
|
}
|
|
|
|
const media = await selectPlaybackTracks(input, src)
|
|
if (id !== this.loadSeq) {
|
|
input.dispose()
|
|
return
|
|
}
|
|
|
|
this.disposeInput()
|
|
this.input = input
|
|
this.media = media
|
|
|
|
let videoMetadataLoaded = !media.videoTrack
|
|
let audioMetadataLoaded = !media.audioTrack
|
|
|
|
const checkMetadata = () => {
|
|
if (videoMetadataLoaded && audioMetadataLoaded) {
|
|
this.readyState = 1 // HAVE_METADATA
|
|
this.events.emit('loadedmetadata')
|
|
this.events.emit('durationchange')
|
|
this.events.emit('progress')
|
|
}
|
|
}
|
|
|
|
try {
|
|
await Promise.all([
|
|
this.video.load(media, () => {
|
|
if (id !== this.loadSeq)
|
|
return
|
|
videoMetadataLoaded = true
|
|
checkMetadata()
|
|
}),
|
|
this.audio.load(media, () => {
|
|
if (id !== this.loadSeq)
|
|
return
|
|
audioMetadataLoaded = true
|
|
checkMetadata()
|
|
}),
|
|
])
|
|
|
|
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')
|
|
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')
|
|
console.error('MediaBunny load error:', err)
|
|
}
|
|
}
|
|
|
|
createTimeout(ms) {
|
|
return new Promise((_, reject) => {
|
|
setTimeout(() => reject(new Error('Load timeout')), ms)
|
|
})
|
|
}
|
|
|
|
disposeInput() {
|
|
this.input?.dispose()
|
|
this.input = null
|
|
this.media = null
|
|
}
|
|
|
|
async replaceTracks({ videoTrack, audioTrack, videoMode, audioMode }) {
|
|
if (!this.media)
|
|
return
|
|
|
|
const nextMedia = {
|
|
...this.media,
|
|
videoTrack,
|
|
audioTrack,
|
|
videoMode,
|
|
audioMode,
|
|
}
|
|
|
|
const shouldResume = !this.paused
|
|
const currentTime = this.currentTime
|
|
|
|
this.pause()
|
|
this.ended = false
|
|
this.seeking = true
|
|
this.events.emit('seeking')
|
|
this.events.emit('waiting')
|
|
|
|
nextMedia.duration = await resolveDuration(nextMedia)
|
|
this.media = nextMedia
|
|
|
|
await Promise.all([
|
|
this.video.load(nextMedia),
|
|
this.audio.load(nextMedia),
|
|
])
|
|
|
|
await Promise.all([
|
|
this.video.seek(currentTime),
|
|
this.audio.seek(currentTime),
|
|
])
|
|
|
|
this.readyState = 4
|
|
this.networkState = 1
|
|
this.seeking = false
|
|
this.events.emit('loadedmetadata')
|
|
this.events.emit('durationchange')
|
|
this.events.emit('progress')
|
|
this.events.emit('loadeddata')
|
|
this.events.emit('canplay')
|
|
this.events.emit('canplaythrough')
|
|
this.events.emit('seeked')
|
|
|
|
if (shouldResume) {
|
|
await this.play()
|
|
}
|
|
}
|
|
|
|
async selectHlsQuality(value) {
|
|
if (!this.media?.isHls || !this.input)
|
|
return
|
|
|
|
const videoTracks = await this.input.getVideoTracks()
|
|
const videoTrack = value === 'auto'
|
|
? await this.input.getPrimaryVideoTrack()
|
|
: videoTracks.find(track => track.id === value) ?? this.media.videoTrack
|
|
|
|
if (!videoTrack)
|
|
return
|
|
|
|
let audioTrack = this.media.audioTrack
|
|
let audioMode = this.media.audioMode
|
|
|
|
if (!audioTrack || !videoTrack.canBePairedWith(audioTrack)) {
|
|
audioTrack = await videoTrack.getPrimaryPairableAudioTrack()
|
|
audioMode = 'auto'
|
|
}
|
|
|
|
await this.replaceTracks({
|
|
videoTrack,
|
|
audioTrack,
|
|
videoMode: value === 'auto' ? 'auto' : 'manual',
|
|
audioMode,
|
|
})
|
|
}
|
|
|
|
async selectHlsAudio(value) {
|
|
if (!this.media?.isHls || !this.input)
|
|
return
|
|
|
|
const audioTracks = await this.input.getAudioTracks()
|
|
const audioTrack = value === 'auto'
|
|
? this.media.videoTrack
|
|
? await this.media.videoTrack.getPrimaryPairableAudioTrack()
|
|
: await this.input.getPrimaryAudioTrack()
|
|
: audioTracks.find(track => track.id === value) ?? this.media.audioTrack
|
|
|
|
if (!audioTrack)
|
|
return
|
|
|
|
let videoTrack = this.media.videoTrack
|
|
let videoMode = this.media.videoMode
|
|
|
|
if (!videoTrack || !audioTrack.canBePairedWith(videoTrack)) {
|
|
videoTrack = await audioTrack.getPrimaryPairableVideoTrack()
|
|
videoMode = 'auto'
|
|
}
|
|
|
|
await this.replaceTracks({
|
|
videoTrack,
|
|
audioTrack,
|
|
videoMode,
|
|
audioMode: value === 'auto' ? 'auto' : 'manual',
|
|
})
|
|
}
|
|
|
|
getHlsState() {
|
|
return getHlsState(this.media)
|
|
}
|
|
|
|
async play() {
|
|
if (!this.paused)
|
|
return
|
|
|
|
if (this.ended) {
|
|
this.ended = false
|
|
await this.seek(0)
|
|
}
|
|
|
|
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
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
setVolume(volume, muted) {
|
|
this.audio.setVolume(volume, muted)
|
|
}
|
|
|
|
setPlaybackRate(rate) {
|
|
this.audio.setPlaybackRate(rate)
|
|
this.video.setPlaybackRate(rate)
|
|
}
|
|
|
|
destroy() {
|
|
this.pause()
|
|
this.disposeInput()
|
|
this.audio.destroy()
|
|
this.video.destroy()
|
|
}
|
|
|
|
// Getters
|
|
get currentTime() {
|
|
return this.audio.currentTime
|
|
}
|
|
|
|
get duration() {
|
|
return this.media?.duration ?? this.audio.duration ?? this.video.duration
|
|
}
|
|
|
|
get videoWidth() {
|
|
return this.video.width
|
|
}
|
|
|
|
get videoHeight() {
|
|
return this.video.height
|
|
}
|
|
}
|