| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133 |
- import test from 'node:test'
- import assert from 'node:assert/strict'
- import { createVideoLiveSession } from '../../src/modules/cockpit/services/videoLiveSession.js'
- const data = (code) => ({ deviceCode: code, sessionId: `session-${code}`, mode: 'LIVE', status: 'ACTIVE', media: { native: true, url: `https://media.invalid/${code}.mp4` } })
- const prepared = (code) => ({ deviceCode: code, sessionId: `session-${code}`, sessionToken: `owner-${code}`, mode: 'LIVE', status: 'PREPARED' })
- const stopped = (session) => ({ sessionId: session.sessionId, status: 'STOPPED' })
- test('simultaneous open commands never create a video for a superseded device', async () => {
- const requests = [], updates = []
- const live = createVideoLiveSession({
- prepare: async (code) => prepared(code),
- start: async (session) => { const code = typeof session === 'string' ? session : session.deviceCode; requests.push(code); return data(code) },
- heartbeat: async () => {}, stop: async (session) => stopped(session),
- }, (patch) => updates.push(patch))
- try {
- await Promise.all([live.start('A'), live.start('B')])
- assert.deepEqual(requests, ['B'])
- assert.equal(updates.some((patch) => patch.media?.url.endsWith('/A.mp4')), false)
- } finally { await live.dispose() }
- })
- test('closing in the same turn cancels an open before it can start server media', async () => {
- const requests = []
- const live = createVideoLiveSession({
- prepare: async (code) => prepared(code),
- start: async (session) => { const code = typeof session === 'string' ? session : session.deviceCode; requests.push(code); return data(code) },
- heartbeat: async () => {}, stop: async (session) => stopped(session),
- }, () => {})
- try {
- const opening = live.start('A')
- await live.stop()
- await opening
- assert.deepEqual(requests, [])
- } finally { await live.dispose() }
- })
- class Clock {
- now = 0; next = 1; jobs = new Map()
- setTimeout(fn, delay) { const id = this.next++; this.jobs.set(id, { fn, at: this.now + delay }); return id }
- clearTimeout(id) { this.jobs.delete(id) }
- setInterval(fn, delay) { const id = this.setTimeout(fn, delay); this.jobs.get(id).interval = delay; return id }
- clearInterval(id) { this.clearTimeout(id) }
- async advance(duration) {
- const end = this.now + duration
- while (true) {
- const due = Array.from(this.jobs).filter(([, job]) => job.at <= end).sort((a, b) => a[1].at - b[1].at)[0]
- if (!due) break
- const [id, job] = due; this.now = job.at
- if (job.interval) job.at += job.interval; else this.jobs.delete(id)
- job.fn(); await new Promise(setImmediate)
- }
- this.now = end; await new Promise(setImmediate)
- }
- }
- test('STARTING is polled using the prepared identity and is never mistaken for active media', async () => {
- const clock = new Clock(), updates = [], releases = [], statuses = []
- const live = createVideoLiveSession({
- prepare: async () => prepared('A'),
- start: async () => ({ sessionId: 'session-A', status: 'STARTING' }),
- status: async (session) => { statuses.push(session.sessionId); return data('A') },
- stop: async (session) => { releases.push(session.sessionId); return stopped(session) },
- heartbeat: async () => {},
- }, (patch) => updates.push(patch), clock)
- try {
- const opening = live.start('A'); await new Promise(setImmediate)
- assert.equal(updates.some((patch) => patch.media), false)
- await clock.advance(5000); await opening
- assert.deepEqual(statuses, ['session-A'])
- assert.equal(updates.some((patch) => patch.media?.url.endsWith('/A.mp4')), true)
- assert.deepEqual(releases, [])
- } finally { await live.dispose() }
- })
- test('accepted cancellation stays unconfirmed until status attests terminal cleanup', async () => {
- const clock = new Clock(), visible = {}, statusCalls = []
- const live = createVideoLiveSession({
- prepare: async () => prepared('A'), start: async () => data('A'), heartbeat: async () => {},
- stop: async () => ({ sessionId: 'session-A', status: 'CLEANUP_PENDING' }),
- status: async (session) => { statusCalls.push(session.sessionId); return { sessionId: 'session-A', status: 'STOPPED' } },
- }, (patch) => Object.assign(visible, patch), clock)
- try {
- await live.start('A'); live.ready(); await live.stop()
- assert.equal(visible.cleanupError.includes('未确认'), true)
- await clock.advance(5000)
- assert.deepEqual(statusCalls, ['session-A'])
- assert.equal(visible.cleanupError, '')
- assert.equal(visible.media, null)
- } finally { await live.dispose() }
- })
- test('a confirmed cancel permits the new device while the old live response is still in flight', async () => {
- const updates = [], starts = []
- let arrived, finishOld
- const entered = new Promise((resolve) => { arrived = resolve })
- const late = new Promise((resolve) => { finishOld = resolve })
- const live = createVideoLiveSession({
- prepare: async (code) => prepared(code),
- start: async (session) => { starts.push(session.deviceCode); if (session.deviceCode === 'A') { arrived(); return late }; return data('B') },
- stop: async (session) => stopped(session), heartbeat: async () => {},
- }, (patch) => updates.push(patch))
- const first = live.start('A')
- try {
- await entered
- await live.start('B')
- assert.deepEqual(starts, ['A', 'B'])
- finishOld(data('A')); await first
- const displayed = updates.filter((patch) => patch.media).map((patch) => patch.media.url)
- assert.deepEqual(displayed, ['https://media.invalid/B.mp4'])
- } finally { finishOld(data('A')); await first; await live.dispose() }
- })
- test('cancel releases the prepared identity while live is still pending and a late response cannot publish media', async () => {
- const releases = [], updates = []
- let started, respond
- const entered = new Promise((resolve) => { started = resolve })
- const delayed = new Promise((resolve) => { respond = resolve })
- const live = createVideoLiveSession({
- prepare: async (code) => prepared(code),
- start: async () => { started(); return delayed },
- stop: async (session) => { releases.push({ sessionId: session.sessionId, sessionToken: session.sessionToken }); return stopped(session) },
- heartbeat: async () => {},
- }, (patch) => updates.push(patch))
- const opening = live.start('A')
- try {
- await entered
- await live.stop()
- assert.deepEqual(releases, [{ sessionId: 'session-A', sessionToken: 'owner-A' }])
- respond(data('A')); await opening
- assert.equal(updates.some((patch) => patch.media), false)
- } finally { respond(data('A')); await opening; await live.dispose() }
- })
|