up
This commit is contained in:
@@ -41,6 +41,7 @@ interface Runtime {
|
||||
abort: AbortController | null
|
||||
media: HTMLMediaElement | null
|
||||
sourceId: string | null
|
||||
fadeStartedAt: number | null
|
||||
}
|
||||
const runtime = (): Runtime => ({
|
||||
operation: -1,
|
||||
@@ -48,6 +49,7 @@ const runtime = (): Runtime => ({
|
||||
abort: null,
|
||||
media: null,
|
||||
sourceId: null,
|
||||
fadeStartedAt: null,
|
||||
})
|
||||
|
||||
// DOM и асинхронные операции остаются здесь; решения о плейлисте принимает main.
|
||||
@@ -86,15 +88,71 @@ export class MediaPlayer {
|
||||
this.state = state
|
||||
for (const channel of ['main', 'music'] as const) {
|
||||
const r = this.channels[channel]
|
||||
const c = state.channels[channel]
|
||||
if (r.media) {
|
||||
r.media.volume = state.channels[channel].volume / 100
|
||||
const fadePause = this.canFadePause(channel, state)
|
||||
if (!fadePause) r.fadeStartedAt = null
|
||||
r.media.volume = (c.volume / 100) * this.fadeGain(r)
|
||||
r.media.muted = state.muted
|
||||
if (state.channels[channel].desired !== 'playing') r.media.pause()
|
||||
if (c.desired !== 'playing' && !fadePause) r.media.pause()
|
||||
}
|
||||
if (r.operation !== state.channels[channel].operation) void this.applyChannel(channel, state)
|
||||
}
|
||||
this.warmNext()
|
||||
}
|
||||
private canFadePause(channel: Channel, snapshot: State) {
|
||||
const r = this.channels[channel]
|
||||
const c = snapshot.channels[channel]
|
||||
return (
|
||||
channel === 'music' &&
|
||||
c.desired === 'paused' &&
|
||||
r.sourceId === (c.source ? `${c.source.id}:${c.source.url}` : null) &&
|
||||
r.seekOperation === c.seekOperation &&
|
||||
!!r.media &&
|
||||
!r.media.paused &&
|
||||
!r.media.ended &&
|
||||
!r.media.error &&
|
||||
!snapshot.muted &&
|
||||
c.volume > 0
|
||||
)
|
||||
}
|
||||
private fadeGain(r: Runtime) {
|
||||
return r.fadeStartedAt === null
|
||||
? 1
|
||||
: Math.min(1, Math.max(0, 1 - (Date.now() - r.fadeStartedAt) / 1000))
|
||||
}
|
||||
private fadeMusic(media: HTMLMediaElement, r: Runtime, signal: AbortSignal) {
|
||||
r.fadeStartedAt ??= Date.now()
|
||||
return new Promise<void>((resolve) => {
|
||||
const cancel = () => {
|
||||
clearInterval(timer)
|
||||
signal.removeEventListener('abort', cancel)
|
||||
resolve()
|
||||
}
|
||||
const tick = () => {
|
||||
const volume = (this.state?.channels.music.volume ?? 0) / 100
|
||||
const gain = this.fadeGain(r)
|
||||
media.volume = volume * gain
|
||||
if (
|
||||
gain === 0 ||
|
||||
media.paused ||
|
||||
media.ended ||
|
||||
media.error ||
|
||||
media.muted ||
|
||||
volume === 0
|
||||
) {
|
||||
media.pause()
|
||||
media.volume = volume
|
||||
r.fadeStartedAt = null
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
const timer = setInterval(tick, 20)
|
||||
signal.addEventListener('abort', cancel, { once: true })
|
||||
if (signal.aborted) cancel()
|
||||
else tick()
|
||||
})
|
||||
}
|
||||
private source(media: HTMLMediaElement, url: string) {
|
||||
if (media.getAttribute('src') === url && !media.error) return
|
||||
media.pause()
|
||||
@@ -118,6 +176,7 @@ export class MediaPlayer {
|
||||
private async applyChannel(channel: Channel, snapshot: State) {
|
||||
const c = snapshot.channels[channel]
|
||||
const r = this.channels[channel]
|
||||
const fadePause = this.canFadePause(channel, snapshot)
|
||||
r.abort?.abort()
|
||||
const abort = new AbortController()
|
||||
r.abort = abort
|
||||
@@ -145,8 +204,11 @@ export class MediaPlayer {
|
||||
if (r.media && r.media !== media) r.media.pause()
|
||||
}
|
||||
r.media = media
|
||||
media.pause()
|
||||
media.volume = c.volume / 100
|
||||
if (!fadePause) {
|
||||
media.pause()
|
||||
r.fadeStartedAt = null
|
||||
}
|
||||
media.volume = (c.volume / 100) * this.fadeGain(r)
|
||||
media.muted = snapshot.muted
|
||||
let failed = false
|
||||
const current = () =>
|
||||
@@ -206,6 +268,8 @@ export class MediaPlayer {
|
||||
}
|
||||
if (!current() || failed) return
|
||||
r.seekOperation = c.seekOperation
|
||||
if (fadePause) await this.fadeMusic(media, r, abort.signal)
|
||||
if (!current() || failed) return
|
||||
if (c.desired === 'playing') {
|
||||
const playing = media.play()
|
||||
// Даже позднее завершение play не должно оживлять уже отменённый канал.
|
||||
|
||||
Reference in New Issue
Block a user