|
@@ -121,6 +121,18 @@ test('unrecoverable outcomes never retry automatically', async () => {
|
|
|
assert.equal(h.visible().phase, 'failed')
|
|
assert.equal(h.visible().phase, 'failed')
|
|
|
} finally { await h.live.dispose() }
|
|
} finally { await h.live.dispose() }
|
|
|
}
|
|
}
|
|
|
|
|
+ // The player reports permanent browser/media errors with `code`, not `category`.
|
|
|
|
|
+ for (const code of ['VIDEO_URL_INVALID', 'VIDEO_UNSUPPORTED']) {
|
|
|
|
|
+ const clock = new Clock(), h = failingLive(clock, { fail: () => { throw Object.assign(new Error(code), { code }) } })
|
|
|
|
|
+ try {
|
|
|
|
|
+ const opening = h.live.start('A')
|
|
|
|
|
+ await new Promise(setImmediate)
|
|
|
|
|
+ await clock.advance(120000)
|
|
|
|
|
+ await opening
|
|
|
|
|
+ assert.equal(h.attempts.length, 1, `${code} must not be retried`)
|
|
|
|
|
+ assert.equal(h.visible().phase, 'failed')
|
|
|
|
|
+ } finally { await h.live.dispose() }
|
|
|
|
|
+ }
|
|
|
// A temporary 503-style failure is retried.
|
|
// A temporary 503-style failure is retried.
|
|
|
const clock = new Clock(), retryable = failingLive(clock, { fail: () => { throw Object.assign(new Error('暂时不可用'), { category: 'VIDEO_START_FAILED' }) } })
|
|
const clock = new Clock(), retryable = failingLive(clock, { fail: () => { throw Object.assign(new Error('暂时不可用'), { category: 'VIDEO_START_FAILED' }) } })
|
|
|
try {
|
|
try {
|
|
@@ -147,15 +159,24 @@ test('an exhausted budget names the reason instead of pretending it was the firs
|
|
|
} finally { await h.live.dispose() }
|
|
} finally { await h.live.dispose() }
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-test('closing cancels pending retries and releases the prepared identity', async () => {
|
|
|
|
|
|
|
+test('closing during a retry wait both cancels and releases cleanly', async () => {
|
|
|
const clock = new Clock(), h = failingLive(clock)
|
|
const clock = new Clock(), h = failingLive(clock)
|
|
|
try {
|
|
try {
|
|
|
const opening = h.live.start('A')
|
|
const opening = h.live.start('A')
|
|
|
await new Promise(setImmediate)
|
|
await new Promise(setImmediate)
|
|
|
- await h.live.stop()
|
|
|
|
|
|
|
+ // We are now inside the retry wait window: the failed attempt already released its identity.
|
|
|
|
|
+ assert.equal(h.visible().phase, 'retrying')
|
|
|
|
|
+ assert.equal(await h.live.stop(), true, 'closing in the wait window must report a clean release')
|
|
|
await clock.advance(200000)
|
|
await clock.advance(200000)
|
|
|
await opening
|
|
await opening
|
|
|
assert.equal(h.attempts.length, 1, 'no retry may run after close')
|
|
assert.equal(h.attempts.length, 1, 'no retry may run after close')
|
|
|
assert.equal(h.visible().media, null)
|
|
assert.equal(h.visible().media, null)
|
|
|
|
|
+ assert.equal(h.visible().cleanupError, '', 'a cleanly released viewer must not stay in cleanup pending')
|
|
|
|
|
+ // The same viewer must be usable again; a stuck pending set would refuse this forever.
|
|
|
|
|
+ await h.live.start('A')
|
|
|
|
|
+ await new Promise(setImmediate)
|
|
|
|
|
+ assert.equal(h.visible().cleanupError, '')
|
|
|
|
|
+ assert.notEqual(h.visible().error, '上次清理结果尚未确认,暂不能重复建流')
|
|
|
|
|
+ assert.equal(h.attempts.length, 2, 'reopening must actually attempt a new live start')
|
|
|
} finally { await h.live.dispose() }
|
|
} finally { await h.live.dispose() }
|
|
|
})
|
|
})
|