mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-19 19:51:20 -08:00
refactor(core): CORE-09 isolate source operations and settle cancelled switches
This commit is contained in:
1 parent
874dac9f22
commit
44f4e083a9
29 files changed
+4512
-255
No files matched your search
@@ -0,0 +1,146 @@
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: rapid real-media switches identify the last restart and settle superseded work`, async ({ page }) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => window.createPlayer('/test/pattern.mp4?source=initial'))
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
const result = await page.evaluate(async () => {
|
||||
const { art } = window
|
||||
const restarts = []
|
||||
const order = []
|
||||
art.on('restart', url => restarts.push(url))
|
||||
art.on('video:canplay', () => order.push('canplay'))
|
||||
art.on('restart', () => order.push('restart'))
|
||||
let firstSettled = false
|
||||
const first = art.switchUrl('/test/pattern.mp4?source=first').then(() => {
|
||||
firstSettled = true
|
||||
})
|
||||
const second = art.switchUrl('/test/pattern.mp4?source=second')
|
||||
await Promise.resolve()
|
||||
const early = firstSettled
|
||||
await Promise.all([first, second])
|
||||
const result = { early, restarts, order, current: new URL(art.video.currentSrc).search, error: art.video.error?.code || 0, time: art.currentTime }
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result.early).toBe(core === 'candidate')
|
||||
expect(result.restarts).toEqual(core === 'candidate'
|
||||
? ['/test/pattern.mp4?source=second']
|
||||
: ['/test/pattern.mp4?source=first', '/test/pattern.mp4?source=second'])
|
||||
expect(result.order[0]).toBe('canplay')
|
||||
expect(result.current).toBe('?source=second')
|
||||
expect(result.error).toBe(0)
|
||||
expect(result.time).toBeCloseTo(0, 1)
|
||||
})
|
||||
|
||||
test(`${core}: destroy settles the candidate switch without waiting for detached media`, async ({ page }) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.evaluate(() => window.createPlayer('/test/pattern.mp4?destroy=initial'))
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
const result = await page.evaluate(async () => {
|
||||
const { art } = window
|
||||
let settled = false
|
||||
let rejected = false
|
||||
art.switchUrl('/test/pattern.mp4?destroy=pending').then(() => {
|
||||
settled = true
|
||||
}, () => {
|
||||
rejected = true
|
||||
})
|
||||
art.destroy()
|
||||
// Retain the published baseline's bounded pending observation, not an infinite-pending claim.
|
||||
await new Promise(resolve => setTimeout(resolve, 250))
|
||||
return { settled, rejected, destroyed: art.isDestroy, instances: window.Artplayer.instances.length }
|
||||
})
|
||||
expect(result).toEqual({ settled: core === 'candidate', rejected: false, destroyed: true, instances: 0 })
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate: quality switch restores position/rate and preserves real customType identities', async ({ page }) => {
|
||||
await page.goto('/test/player.html?core=candidate&chapter=published')
|
||||
await page.evaluate(() => window.createPlayer('/test/pattern.mp4?custom=initial'))
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await page.evaluate(() => {
|
||||
window.art.currentTime = 3
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.video.seeking)).toBe(false)
|
||||
const result = await page.evaluate(async () => {
|
||||
const { art } = window
|
||||
const calls = []
|
||||
art.option.type = 'controlled'
|
||||
art.option.customType.controlled = function (video, url, owner) {
|
||||
calls.push({ thisIdentity: this === art, ownerIdentity: owner === art, videoIdentity: video === art.video, count: arguments.length, url })
|
||||
video.src = url
|
||||
}
|
||||
art.playbackRate = 1.5
|
||||
art.aspectRatio = '16:9'
|
||||
const restart = []
|
||||
art.on('restart', url => restart.push(url))
|
||||
await art.switchQuality('/test/pattern.mp4?custom=next')
|
||||
const result = { calls, restart, time: art.currentTime, rate: art.playbackRate, ratio: art.aspectRatio, paused: art.video.paused, error: art.video.error?.code || 0 }
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result.calls).toEqual([{ thisIdentity: true, ownerIdentity: true, videoIdentity: true, count: 3, url: '/test/pattern.mp4?custom=next' }])
|
||||
expect(result.restart).toEqual(['/test/pattern.mp4?custom=next'])
|
||||
expect(result.time).toBeCloseTo(3, 1)
|
||||
expect(result.rate).toBe(1.5)
|
||||
expect(result.ratio).toBe('16:9')
|
||||
expect(result.paused).toBe(true)
|
||||
expect(result.error).toBe(0)
|
||||
})
|
||||
|
||||
test('candidate: real switch settles when the controlled native resume rejects', async ({ page }) => {
|
||||
await page.goto('/test/player.html?core=candidate&chapter=published')
|
||||
await page.evaluate(() => window.createPlayer('/test/pattern.mp4?resume=initial'))
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.playing)).toBe(true)
|
||||
const result = await page.evaluate(async () => {
|
||||
const { art } = window
|
||||
const error = new DOMException('controlled resume rejection', 'NotAllowedError')
|
||||
let calls = 0
|
||||
Object.defineProperty(art.video, 'play', { configurable: true, value() {
|
||||
calls++
|
||||
return Promise.reject(error)
|
||||
} })
|
||||
const value = await art.switchUrl('/test/pattern.mp4?resume=next')
|
||||
const result = { settledUndefined: value === undefined, calls, source: new URL(art.video.currentSrc).search, paused: art.video.paused }
|
||||
delete art.video.play
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result).toEqual({ settledUndefined: true, calls: 1, source: '?resume=next', paused: true })
|
||||
})
|
||||
|
||||
test('candidate: late native play completion cannot publish stale player effects', async ({ page }) => {
|
||||
await page.goto('/test/player.html?core=candidate&chapter=published')
|
||||
await page.evaluate(() => window.createPlayer('/test/pattern.mp4?late=initial'))
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.playing)).toBe(true)
|
||||
const result = await page.evaluate(async () => {
|
||||
const { art } = window
|
||||
const plays = []
|
||||
art.on('play', () => plays.push('play'))
|
||||
let resolveNative
|
||||
let started
|
||||
const invoked = new Promise(resolve => started = resolve)
|
||||
Object.defineProperty(art.video, 'play', { configurable: true, value() {
|
||||
started()
|
||||
return new Promise(resolve => resolveNative = resolve)
|
||||
} })
|
||||
const switching = art.switchUrl('/test/pattern.mp4?late=resume')
|
||||
await invoked
|
||||
art.url = '/test/pattern.mp4?late=latest'
|
||||
await switching
|
||||
art.notice.show = 'Latest source notice'
|
||||
resolveNative(42)
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
const result = { plays, notice: art.template.$noticeInner.textContent }
|
||||
delete art.video.play
|
||||
art.destroy()
|
||||
return result
|
||||
})
|
||||
expect(result).toEqual({ plays: [], notice: 'Latest source notice' })
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mock } from 'node:test'
|
||||
|
||||
export function playbackFactory({ Emitter, playMix, pauseMix, toggleMix, switchMix }) {
|
||||
export function playbackFactory({ Emitter, playMix, pauseMix, toggleMix, switchMix, beginLifecycle }) {
|
||||
return function createArt(play = () => Promise.resolve(), playing = false) {
|
||||
const art = Object.assign(new Emitter(), {
|
||||
url: 'old.mp4',
|
||||
@@ -19,6 +19,7 @@ export function playbackFactory({ Emitter, playMix, pauseMix, toggleMix, switchM
|
||||
},
|
||||
},
|
||||
})
|
||||
beginLifecycle(art)
|
||||
Object.defineProperty(art, 'playing', { get: () => !art.template.$video.paused })
|
||||
for (const mix of [playMix, pauseMix, toggleMix, switchMix])
|
||||
mix(art)
|
||||
|
||||
@@ -13,6 +13,8 @@ const modules = await loadModules({
|
||||
pauseMix: 'packages/artplayer/src/player/pauseMix',
|
||||
toggleMix: 'packages/artplayer/src/player/toggleMix',
|
||||
switchMix: 'packages/artplayer/src/player/switchMix',
|
||||
beginLifecycle: { file: 'packages/artplayer/src/lifecycle/instance', name: 'beginLifecycle' },
|
||||
getScope: { file: 'packages/artplayer/src/lifecycle/instance', name: 'getScope' },
|
||||
})
|
||||
const { silencePromise, Hotkey } = modules
|
||||
const createArt = playbackFactory(modules)
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
|
||||
import { mock, test } from 'node:test'
|
||||
import { setImmediate } from 'node:timers/promises'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
|
||||
const { Emitter, urlMix, switchMix, playMix, beginLifecycle, getScope } = await loadModules({
|
||||
Emitter: 'packages/artplayer/src/utils/emitter',
|
||||
urlMix: 'packages/artplayer/src/player/urlMix',
|
||||
switchMix: 'packages/artplayer/src/player/switchMix',
|
||||
playMix: 'packages/artplayer/src/player/playMix',
|
||||
beginLifecycle: { file: 'packages/artplayer/src/lifecycle/instance', name: 'beginLifecycle' },
|
||||
getScope: { file: 'packages/artplayer/src/lifecycle/instance', name: 'getScope' },
|
||||
})
|
||||
|
||||
function createArt(customType) {
|
||||
const art = Object.assign(new Emitter(), {
|
||||
template: { $video: { src: 'old.mp4' } },
|
||||
option: { url: 'old.mp4', type: customType ? 'custom' : '', customType: { custom: customType } },
|
||||
isReady: true,
|
||||
playing: false,
|
||||
currentTime: 37,
|
||||
playbackRate: 1.5,
|
||||
aspectRatio: '16:9',
|
||||
loading: { show: false },
|
||||
notice: { show: '' },
|
||||
pause: mock.fn(),
|
||||
play: mock.fn(() => Promise.resolve()),
|
||||
})
|
||||
beginLifecycle(art)
|
||||
urlMix(art)
|
||||
switchMix(art)
|
||||
return art
|
||||
}
|
||||
|
||||
test('superseded switches settle before readiness and only the latest restores state/restarts', async () => {
|
||||
const art = createArt()
|
||||
const restarts = []
|
||||
art.on('restart', url => restarts.push(url))
|
||||
const first = art.switchQuality('first.mp4')
|
||||
art.currentTime = 9
|
||||
const second = art.switchQuality('second.mp4')
|
||||
assert.equal(await first, undefined)
|
||||
art.currentTime = 0
|
||||
art.emit('video:loadedmetadata')
|
||||
assert.equal(art.currentTime, 9)
|
||||
art.emit('video:canplay')
|
||||
assert.equal(await second, undefined)
|
||||
assert.deepEqual(restarts, ['second.mp4'])
|
||||
assert.deepEqual(Object.keys(art.e), ['restart'])
|
||||
})
|
||||
|
||||
test('direct URL assignment cancels an outstanding switch and stale restart listeners', async () => {
|
||||
const art = createArt()
|
||||
const restarts = []
|
||||
art.on('restart', url => restarts.push(url))
|
||||
const switching = art.switchQuality('switch.mp4')
|
||||
art.url = 'direct.mp4'
|
||||
await switching
|
||||
art.currentTime = 4
|
||||
art.emit('video:loadedmetadata')
|
||||
art.emit('video:canplay')
|
||||
assert.equal(art.currentTime, 4)
|
||||
assert.deepEqual(restarts, ['direct.mp4'])
|
||||
})
|
||||
|
||||
test('scope disposal settles a pending switch and prevents post-destroy work', async () => {
|
||||
const art = createArt()
|
||||
const switching = art.switchUrl('pending.mp4')
|
||||
getScope(art).dispose()
|
||||
assert.equal(await switching, undefined)
|
||||
assert.deepEqual(Object.keys(art.e), [])
|
||||
art.url = 'closed.mp4'
|
||||
await art.switchUrl('also-closed.mp4')
|
||||
assert.equal(art.url, 'pending.mp4')
|
||||
assert.equal(art.pause.mock.callCount(), 1)
|
||||
})
|
||||
|
||||
test('same URL remains a no-op and switch setter retains the public function identity', async () => {
|
||||
const art = createArt()
|
||||
art.notice.show = 'keep'
|
||||
assert.equal(await art.switchUrl('old.mp4'), undefined)
|
||||
assert.equal(art.pause.mock.callCount(), 0)
|
||||
assert.equal(art.notice.show, 'keep')
|
||||
assert.equal(Object.getOwnPropertyDescriptor(art, 'switch').set, art.switchUrl)
|
||||
for (const name of ['url', 'switch', 'switchUrl', 'switchQuality']) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(art, name)
|
||||
assert.equal(descriptor.enumerable, false)
|
||||
assert.equal(descriptor.configurable, false)
|
||||
}
|
||||
})
|
||||
|
||||
test('a reentrant pause cannot make its superseded switch overwrite the latest URL', async () => {
|
||||
const art = createArt()
|
||||
art.pause = () => {
|
||||
art.url = 'reentrant.mp4'
|
||||
}
|
||||
await art.switchUrl('stale.mp4')
|
||||
assert.equal(art.url, 'reentrant.mp4')
|
||||
})
|
||||
|
||||
test('synchronous media setters cannot lose metadata, readiness or original errors', async () => {
|
||||
for (const fails of [false, true]) {
|
||||
const art = createArt()
|
||||
const error = new Error('synchronous source failure')
|
||||
let src = 'old.mp4'
|
||||
Object.defineProperty(art.template.$video, 'src', {
|
||||
get: () => src,
|
||||
set(value) {
|
||||
src = value
|
||||
if (fails) {
|
||||
art.emit('video:error', error)
|
||||
}
|
||||
else {
|
||||
art.currentTime = 0
|
||||
art.emit('video:loadedmetadata')
|
||||
art.emit('video:canplay')
|
||||
}
|
||||
},
|
||||
})
|
||||
const switching = art.switchQuality('sync.mp4')
|
||||
if (fails) {
|
||||
await assert.rejects(switching, actual => actual === error)
|
||||
}
|
||||
else {
|
||||
await switching
|
||||
assert.equal(art.currentTime, 37)
|
||||
}
|
||||
assert.deepEqual(Object.keys(art.e), [])
|
||||
}
|
||||
})
|
||||
|
||||
test('late resume fulfillment or rejection cannot clear a newer operation notice', async () => {
|
||||
for (const rejects of [false, true]) {
|
||||
const art = createArt()
|
||||
art.playing = true
|
||||
let finish
|
||||
art.play = () => new Promise((resolve, reject) => {
|
||||
finish = rejects ? reject : resolve
|
||||
})
|
||||
const first = art.switchUrl('first.mp4')
|
||||
art.emit('video:canplay')
|
||||
art.playing = false
|
||||
const second = art.switchUrl('second.mp4')
|
||||
await first
|
||||
art.notice.show = 'new source notice'
|
||||
finish(new Error('late play'))
|
||||
await setImmediate()
|
||||
assert.equal(art.notice.show, 'new source notice')
|
||||
art.emit('video:canplay')
|
||||
await second
|
||||
assert.deepEqual(Object.keys(art.e), [])
|
||||
}
|
||||
})
|
||||
|
||||
test('state restoration errors reject without leaking handlers or unhandled promises', async () => {
|
||||
for (const property of ['currentTime', 'playbackRate', 'aspectRatio']) {
|
||||
const art = createArt()
|
||||
const error = new Error(property)
|
||||
const switching = art.switchQuality('new.mp4')
|
||||
const previous = art[property]
|
||||
Object.defineProperty(art, property, {
|
||||
get: () => previous,
|
||||
set() {
|
||||
throw error
|
||||
},
|
||||
})
|
||||
const rejected = assert.rejects(switching, actual => actual === error)
|
||||
art.emit(property === 'currentTime' ? 'video:loadedmetadata' : 'video:canplay')
|
||||
await rejected
|
||||
assert.deepEqual(Object.keys(art.e), [])
|
||||
}
|
||||
})
|
||||
|
||||
test('customType deferral ignores the old resource and preserves callback identities', async () => {
|
||||
let called
|
||||
const art = createArt(function (video, url, owner) {
|
||||
called = [this, video, url, owner, arguments.length]
|
||||
video.src = url
|
||||
owner.emit('video:loadedmetadata')
|
||||
owner.emit('video:canplay')
|
||||
})
|
||||
const switching = art.switchQuality('custom.mp4')
|
||||
art.currentTime = 0
|
||||
art.emit('video:canplay')
|
||||
assert.equal(called, undefined)
|
||||
await switching
|
||||
assert.deepEqual(called, [art, art.template.$video, 'custom.mp4', art, 3])
|
||||
assert.equal(art.currentTime, 37)
|
||||
assert.equal(art.option.url, 'custom.mp4')
|
||||
})
|
||||
|
||||
test('customType failures reject switches with the original thrown/rejected value', async () => {
|
||||
for (const asynchronous of [false, true]) {
|
||||
const error = new Error('customType failed')
|
||||
const art = createArt(() => {
|
||||
if (asynchronous)
|
||||
return Promise.reject(error)
|
||||
throw error
|
||||
})
|
||||
await assert.rejects(art.switchUrl('failed.mp4'), actual => actual === error)
|
||||
assert.deepEqual(Object.keys(art.e), [])
|
||||
}
|
||||
})
|
||||
|
||||
test('superseded customType callbacks never start and empty URL switches settle', async () => {
|
||||
const callback = mock.fn((video, url, art) => {
|
||||
video.src = url
|
||||
art.emit('video:canplay')
|
||||
})
|
||||
const art = createArt(callback)
|
||||
const first = art.switchUrl('first.mp4')
|
||||
const second = art.switchUrl('second.mp4')
|
||||
await Promise.all([first, second])
|
||||
assert.equal(callback.mock.callCount(), 1)
|
||||
assert.equal(callback.mock.calls[0].arguments[1], 'second.mp4')
|
||||
await art.switchUrl('')
|
||||
assert.equal(art.loading.show, true)
|
||||
assert.equal(art.url, 'second.mp4')
|
||||
})
|
||||
|
||||
test('reentrant media listeners and property setters cannot restore stale state', async () => {
|
||||
for (const boundary of ['metadata', 'rate']) {
|
||||
const art = createArt()
|
||||
if (boundary === 'metadata') {
|
||||
art.once('video:loadedmetadata', () => {
|
||||
art.url = 'reentrant.mp4'
|
||||
art.currentTime = 6
|
||||
})
|
||||
}
|
||||
const switching = art.switchQuality('stale.mp4')
|
||||
art.currentTime = 0
|
||||
art.aspectRatio = 'default'
|
||||
if (boundary === 'rate') {
|
||||
Object.defineProperty(art, 'playbackRate', {
|
||||
get: () => 1,
|
||||
set() {
|
||||
art.url = 'reentrant.mp4'
|
||||
},
|
||||
})
|
||||
}
|
||||
art.emit(boundary === 'metadata' ? 'video:loadedmetadata' : 'video:canplay')
|
||||
await switching
|
||||
assert.equal(art.url, 'reentrant.mp4')
|
||||
assert.equal(art.aspectRatio, 'default')
|
||||
if (boundary === 'metadata')
|
||||
assert.equal(art.currentTime, 6)
|
||||
getScope(art).dispose()
|
||||
assert.deepEqual(Object.keys(art.e), [])
|
||||
}
|
||||
})
|
||||
|
||||
test('direct customType rejection is reported once; obsolete returned failures are ignored', async (t) => {
|
||||
const warning = t.mock.method(console, 'warn', () => {})
|
||||
const error = new Error('direct custom source')
|
||||
let rejectLate
|
||||
let started
|
||||
const invoked = new Promise(resolve => started = resolve)
|
||||
const art = createArt(() => new Promise((resolve, reject) => {
|
||||
rejectLate = reject
|
||||
started()
|
||||
}))
|
||||
art.url = 'late.mp4'
|
||||
await invoked
|
||||
art.option.type = ''
|
||||
art.url = 'current.mp4'
|
||||
rejectLate(error)
|
||||
await setImmediate()
|
||||
assert.equal(warning.mock.callCount(), 0)
|
||||
art.option.type = 'custom'
|
||||
art.option.customType.custom = () => Promise.reject(error)
|
||||
await Object.getOwnPropertyDescriptor(art, 'url').set.call(art, 'failed.mp4')
|
||||
await setImmediate()
|
||||
assert.equal(warning.mock.callCount(), 1)
|
||||
assert.deepEqual(warning.mock.calls[0].arguments, ['Failed to initialize ArtPlayer source:', error])
|
||||
assert.deepEqual(Object.keys(art.e), [])
|
||||
})
|
||||
|
||||
test('same-URL calls do not cancel the still-loading original operation', async () => {
|
||||
const art = createArt()
|
||||
let firstSettled = false
|
||||
const first = art.switchQuality('pending.mp4').then(() => firstSettled = true)
|
||||
await art.switchUrl('pending.mp4')
|
||||
assert.equal(firstSettled, false)
|
||||
art.currentTime = 0
|
||||
art.emit('video:loadedmetadata')
|
||||
art.emit('video:canplay')
|
||||
await first
|
||||
assert.equal(art.currentTime, 37)
|
||||
assert.equal(art.pause.mock.callCount(), 1)
|
||||
})
|
||||
|
||||
test('synchronous readiness replay consumes metadata once and emits restart once', async () => {
|
||||
const art = createArt()
|
||||
const restarts = []
|
||||
art.on('restart', url => restarts.push(url))
|
||||
let src = 'old.mp4'
|
||||
Object.defineProperty(art.template.$video, 'src', {
|
||||
get: () => src,
|
||||
set(value) {
|
||||
src = value
|
||||
art.emit('video:loadedmetadata')
|
||||
art.emit('video:loadedmetadata')
|
||||
},
|
||||
})
|
||||
const switching = art.switchQuality('sync.mp4')
|
||||
assert.equal(art.currentTime, 37)
|
||||
art.currentTime = 4
|
||||
art.emit('video:loadedmetadata')
|
||||
assert.equal(art.currentTime, 4)
|
||||
art.emit('video:canplay')
|
||||
art.emit('video:canplay')
|
||||
await switching
|
||||
assert.deepEqual(restarts, ['sync.mp4'])
|
||||
assert.deepEqual(Object.keys(art.e), ['restart'])
|
||||
})
|
||||
|
||||
test('the real play mixin cannot publish stale resume effects after source replacement', async () => {
|
||||
const art = createArt()
|
||||
art.i18n = { get: key => key }
|
||||
art.constructor = { instances: [] }
|
||||
let resolveNative
|
||||
art.template.$video.play = () => new Promise(resolve => resolveNative = resolve)
|
||||
playMix(art)
|
||||
art.playing = true
|
||||
const plays = []
|
||||
art.on('play', () => plays.push('play'))
|
||||
const switching = art.switchUrl('pending-resume.mp4')
|
||||
art.emit('video:canplay')
|
||||
art.url = 'latest.mp4'
|
||||
await switching
|
||||
art.notice.show = 'latest notice'
|
||||
resolveNative(42)
|
||||
await setImmediate()
|
||||
assert.equal(art.notice.show, 'latest notice')
|
||||
assert.deepEqual(plays, [])
|
||||
})
|
||||
|
||||
test('public pending play preserves its result while obsolete notification/event/mutex work stops', async () => {
|
||||
for (const destroy of [false, true]) {
|
||||
const art = createArt()
|
||||
art.i18n = { get: key => key }
|
||||
const other = { pause: mock.fn() }
|
||||
art.constructor = { instances: [other] }
|
||||
art.option.mutex = true
|
||||
art.url = 'initial.mp4'
|
||||
let resolveNative
|
||||
art.template.$video.play = () => new Promise(resolve => resolveNative = resolve)
|
||||
playMix(art)
|
||||
const plays = mock.fn()
|
||||
art.on('play', plays)
|
||||
const playing = art.play()
|
||||
if (destroy)
|
||||
getScope(art).dispose()
|
||||
else
|
||||
art.url = 'replacement.mp4'
|
||||
art.notice.show = 'preserve'
|
||||
resolveNative(42)
|
||||
assert.equal(await playing, 42)
|
||||
assert.equal(art.notice.show, 'preserve')
|
||||
assert.equal(plays.mock.callCount(), 0)
|
||||
assert.equal(other.pause.mock.callCount(), 0)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { CanvasMedia, NativeMedia } from '../../packages/artplayer/src/media/types'
|
||||
import type { SourceMedia, SwitchHost, UrlHost } from '../../packages/artplayer/src/source/types'
|
||||
import switchMix from '../../packages/artplayer/src/player/switchMix'
|
||||
import urlMix from '../../packages/artplayer/src/player/urlMix'
|
||||
|
||||
declare const native: NativeMedia
|
||||
declare const canvas: CanvasMedia
|
||||
const sourceMedia: SourceMedia[] = [native, canvas]
|
||||
void sourceMedia
|
||||
|
||||
declare const switching: SwitchHost
|
||||
switchMix(switching)
|
||||
const pending: Promise<void> = switching.switchQuality('movie.mp4')
|
||||
void pending
|
||||
switching.switch = 'movie.mp4'
|
||||
// @ts-expect-error Source URLs remain strings.
|
||||
switching.switchUrl(42)
|
||||
// @ts-expect-error Switching does not resolve with a media element or success boolean.
|
||||
const wrong: Promise<boolean> = switching.switchUrl('movie.mp4')
|
||||
void wrong
|
||||
|
||||
interface CustomHost extends UrlHost<CanvasMedia, CustomHost> {
|
||||
extension: number
|
||||
}
|
||||
declare const custom: CustomHost
|
||||
custom.option.customType.canvas = function (video, url, art) {
|
||||
this satisfies CustomHost
|
||||
const owner: CustomHost = art
|
||||
const surface: HTMLCanvasElement = video
|
||||
const source: string = url
|
||||
void [owner, surface, source]
|
||||
// @ts-expect-error A canvas proxy is not a native video element.
|
||||
const wrongMedia: HTMLVideoElement = video
|
||||
void wrongMedia
|
||||
}
|
||||
urlMix<CanvasMedia, CustomHost>(custom)
|
||||
Reference in new issue
Block a user