This commit is contained in:
2026-10-18 19:07:29 +07:00
parent a35c09d0dc
commit 9542330125
3 changed files with 249 additions and 5 deletions
+174
View File
@@ -6,6 +6,7 @@ import { MediaPlayer } from '../src/utils/media-player.js'
import { buildScreenPackage } from './screen-package.js'
import { packageKey, type LocalPackage } from './package-store.js'
import type { Intent, Report } from '../src/types/screen.js'
import { applyRemote, parseRemote } from './remote-state.js'
function send(c: Controller, intent: Intent) {
c.command({ session: c.state.session, sequence: c.state.sequence + 1, ...intent })
@@ -291,3 +292,176 @@ test('DOM: следующий трек не предзагружается до
assert.equal(spare.paused, false)
assert.equal(playing.paused, true)
})
test('DOM: пауза музыки затихает за секунду, snapshots не возвращают громкость и позицию', async (t) => {
const f = await fixture(t)
send(f.c, { action: 'volume', channel: 'music', volume: 80 })
send(f.c, { action: 'play', channel: 'music' })
f.apply()
await setImmediate()
const media = [f.a, f.b].find((m) => !m.paused)!
media.currentTime = 44
t.mock.timers.enable({ apis: ['setInterval', 'Date'], now: 10000 })
send(f.c, { action: 'pause', channel: 'music' })
f.apply()
await setImmediate()
assert.equal(media.paused, false)
t.mock.timers.tick(500)
assert.ok(Math.abs(media.volume - 0.4) < 0.01)
assert.equal(f.c.state.channels.music.volume, 80)
f.c.state.revision++
f.apply()
assert.ok(Math.abs(media.volume - 0.4) < 0.01)
// Повтор команды тоже не продлевает затухание.
send(f.c, { action: 'pause', channel: 'music' })
f.apply()
await setImmediate()
media.currentTime = 45
t.mock.timers.tick(500)
await setImmediate()
assert.equal(media.paused, true)
assert.equal(media.volume, 0.8)
assert.equal(media.currentTime, 45)
const report = f.reports.at(-1)!
assert.equal(report.status, 'paused')
assert.equal(report.position, 45)
f.c.report(report)
assert.equal(f.c.state.channels.music.position, 45)
})
test('DOM: продолжение отменяет затухание, изменение громкости не сбрасывает его', async (t) => {
const f = await fixture(t)
send(f.c, { action: 'play', channel: 'music' })
f.apply()
await setImmediate()
const media = [f.a, f.b].find((m) => !m.paused)!
t.mock.timers.enable({ apis: ['setInterval', 'Date'], now: 10000 })
send(f.c, { action: 'pause', channel: 'music' })
f.apply()
await setImmediate()
t.mock.timers.tick(500)
send(f.c, { action: 'volume', channel: 'music', volume: 60 })
f.apply()
assert.ok(Math.abs(media.volume - 0.3) < 0.01)
send(f.c, { action: 'resume', channel: 'music' })
f.apply()
await setImmediate()
assert.equal(media.volume, 0.6)
assert.equal(media.paused, false)
t.mock.timers.tick(1500)
assert.equal(media.paused, false)
assert.equal(media.volume, 0.6)
})
for (const action of ['stop', 'next', 'error', 'session', 'destroy'] as const) {
test(`DOM: затухание отменяется при ${action}, поздняя пауза не затрагивает новый звук`, async (t) => {
const f = await fixture(t)
send(f.c, { action: 'play', channel: 'music' })
f.apply()
await setImmediate()
const media = [f.a, f.b].find((m) => !m.paused)!
t.mock.timers.enable({ apis: ['setInterval', 'Date'], now: 10000 })
send(f.c, { action: 'pause', channel: 'music' })
f.apply()
await setImmediate()
t.mock.timers.tick(400)
if (action === 'destroy') f.player.destroy()
else if (action === 'error') {
media.error = { code: 3 } as MediaError
media.dispatchEvent(new Event('error'))
} else if (action === 'session') {
f.c.selectPackage(testPackage())
f.apply()
} else {
send(f.c, { action, channel: 'music' })
f.apply()
}
await setImmediate()
assert.equal(media.paused, true)
if (action === 'next' || action === 'stop') {
send(f.c, { action: 'play', channel: 'music' })
f.apply()
await setImmediate()
const playing = [f.a, f.b].find((m) => !m.paused)!
assert.ok(playing)
t.mock.timers.tick(1500)
assert.equal(playing.paused, false)
assert.equal(playing.volume, f.c.state.channels.music.volume / 100)
} else {
const reports = f.reports.length
t.mock.timers.tick(1500)
assert.equal(f.reports.length, reports)
assert.equal(f.a.paused, true)
assert.equal(f.b.paused, true)
}
})
}
for (const silent of ['muted', 'zero', 'stopped'] as const) {
test(`DOM: беззвучная музыка ставится на паузу сразу: ${silent}`, async (t) => {
const f = await fixture(t)
send(f.c, { action: 'play', channel: 'music' })
f.apply()
await setImmediate()
const media = [f.a, f.b].find((m) => !m.paused)!
if (silent === 'muted') send(f.c, { action: 'mute', enabled: true })
if (silent === 'zero') send(f.c, { action: 'volume', channel: 'music', volume: 0 })
if (silent === 'stopped') {
send(f.c, { action: 'stop', channel: 'music' })
f.apply()
await setImmediate()
}
send(f.c, { action: 'pause', channel: 'music' })
f.apply()
await setImmediate()
assert.equal(media.paused, true)
assert.equal(f.reports.at(-1)!.status, 'paused')
})
}
for (const mode of [2, 4]) {
test(`DOM: серверная автопауза при режиме ${mode} затихает и возобновляется без остановки основного канала`, async (t) => {
const f = await fixture(t)
const music = (version: number, playback: number, positionMs: number) => ({
stateVersion: String(version), playlist: { ID: 1, tracks: [{ ID: 1 }, { ID: 2 }] },
trackIndex: 0, playback, positionMs, volume: 80, loop: true,
})
const playing = parseRemote({ mode: 1, stateVersion: '1', music: music(1, 2, 0) })
applyRemote(f.c, parseRemote({}), playing)
f.apply()
await setImmediate()
const media = [f.a, f.b].find((m) => !m.paused)!
media.currentTime = 12
t.mock.timers.enable({ apis: ['setInterval', 'Date'], now: 10000 })
const paused = parseRemote({ mode, command: 1, stateVersion: '2', music: music(2, 3, 11500) })
const seekOperation = f.c.state.channels.music.seekOperation
applyRemote(f.c, playing, paused)
assert.equal(f.c.state.channels.music.seekOperation, seekOperation)
f.apply()
await setImmediate()
assert.equal(media.paused, false)
t.mock.timers.tick(500)
assert.ok(Math.abs(media.volume - 0.4) < 0.01)
assert.equal((mode === 2 ? f.audio : f.video).paused, false)
media.currentTime = 13
t.mock.timers.tick(500)
await setImmediate()
assert.equal(media.paused, true)
assert.equal(media.currentTime, 13)
const resumed = parseRemote({ mode: 1, stateVersion: '3', music: music(3, 2, 11500) })
applyRemote(f.c, paused, resumed)
f.apply()
await setImmediate()
assert.equal(media.paused, false)
assert.equal(media.volume, 0.8)
assert.equal(media.currentTime, 11.5)
// A separate seek in the paused state still synchronizes position.
applyRemote(f.c, resumed, parseRemote({ mode: 1, stateVersion: '4', music: music(4, 3, 14000) }))
const previous = parseRemote({ mode: 1, stateVersion: '4', music: music(4, 3, 14000) })
applyRemote(f.c, previous, parseRemote({ mode: 1, stateVersion: '5', music: music(5, 3, 15000) }))
f.apply()
await setImmediate()
assert.equal(media.currentTime, 15)
assert.equal(media.paused, true)
})
}
+7 -1
View File
@@ -295,7 +295,13 @@ export function applyRemote(controller: Controller, previous: RemoteState, next:
action: m.playback === 2 ? 'resume' : m.playback === 3 ? 'pause' : 'stop',
channel: 'music',
})
if (m.playback !== 1)
const pausingSameTrack =
m.playback === 3 &&
old?.playback === 2 &&
old.playlistId === m.playlistId &&
old.trackId === m.trackId
// A pause keeps the audible position so a seek cannot cancel its fade-out.
if (m.playback !== 1 && !pausingSameTrack)
send(controller, { action: 'seek', channel: 'music', position: m.position })
}
}