mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-23 13:51:20 -08:00
refactor(core): CORE-06 type media surfaces and playback hosts
This commit is contained in:
1 parent
97d320bd74
commit
ee859e4804
27 files changed
+2152
-79
No files matched your search
@@ -0,0 +1,54 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const proxyFile = new URL('../../docs/compiled/artplayer-proxy-canvas.js', import.meta.url)
|
||||
const proxyCode = fs.readFileSync(proxyFile, 'utf8')
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`${core}: existing canvas proxy keeps media identity, playback and layout`, async ({ page }, testInfo) => {
|
||||
await page.goto(`/test/player.html?core=${core}&chapter=published`)
|
||||
await page.addScriptTag({ content: proxyCode })
|
||||
await testInfo.attach('canvas-proxy-artifact', { contentType: 'application/json', body: JSON.stringify({
|
||||
kind: 'existing-workspace-artifact',
|
||||
file: 'docs/compiled/artplayer-proxy-canvas.js',
|
||||
sha256: createHash('sha256').update(proxyCode).digest('hex'),
|
||||
}) })
|
||||
await page.evaluate(() => {
|
||||
const container = document.createElement('div')
|
||||
Object.assign(container.style, { width: '640px', height: '360px' })
|
||||
document.body.append(container)
|
||||
window.draws = 0
|
||||
window.art = new window.Artplayer({
|
||||
container,
|
||||
url: '/test/pattern.mp4',
|
||||
muted: true,
|
||||
proxy: window.artplayerProxyCanvas(() => window.draws++),
|
||||
})
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
const initial = await page.evaluate(() => {
|
||||
const art = window.art
|
||||
return { canvas: art.video instanceof HTMLCanvasElement, native: art.video instanceof HTMLVideoElement, same: art.video === art.template.$video, duration: art.duration, width: art.width, rectWidth: art.rect.width }
|
||||
})
|
||||
expect(initial).toEqual({ canvas: true, native: false, same: true, duration: 8, width: 640, rectWidth: 640 })
|
||||
await page.evaluate(() => window.art.play())
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
||||
await expect.poll(() => page.evaluate(() => window.draws)).toBeGreaterThan(0)
|
||||
expect(await page.evaluate(() => window.art.playing)).toBe(true)
|
||||
await page.evaluate(() => window.art.pause())
|
||||
expect(await page.evaluate(() => window.art.playing)).toBe(false)
|
||||
await page.evaluate(() => {
|
||||
window.art.seek = 2
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeCloseTo(2, 1)
|
||||
const layout = await page.evaluate(() => {
|
||||
const art = window.art
|
||||
art.template.$container.style.width = '480px'
|
||||
return { width: art.width, x: art.x, expectedX: art.rect.left + window.pageXOffset }
|
||||
})
|
||||
expect(layout.width).toBe(480)
|
||||
expect(layout.x).toBe(layout.expectedX)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node runner.
|
||||
import { test } from 'node:test'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
|
||||
const { playMix, pauseMix, playingMix, durationMix, rectMix } = await loadModules(Object.fromEntries(['playMix', 'pauseMix', 'playingMix', 'durationMix', 'rectMix'].map(name => [name, `packages/artplayer/src/player/${name}`])))
|
||||
|
||||
test('structural playback hosts retain synchronous proxy values, async play order and receivers', async () => {
|
||||
const calls = []
|
||||
const media = {
|
||||
play() {
|
||||
assert.equal(this, media)
|
||||
calls.push('media:play')
|
||||
return 'proxy-result'
|
||||
},
|
||||
pause() {
|
||||
assert.equal(this, media)
|
||||
calls.push('media:pause')
|
||||
return 42
|
||||
},
|
||||
}
|
||||
const art = {
|
||||
template: { $video: media },
|
||||
option: { mutex: true },
|
||||
i18n: { get: key => key },
|
||||
notice: {
|
||||
get show() {
|
||||
return false
|
||||
},
|
||||
set show(message) {
|
||||
calls.push(`notice:${message}`)
|
||||
},
|
||||
},
|
||||
emit: name => calls.push(`event:${name}`),
|
||||
constructor: { instances: [] },
|
||||
}
|
||||
playMix(art)
|
||||
pauseMix(art)
|
||||
art.constructor.instances.push(art, {
|
||||
pause: () => calls.push('mutex'),
|
||||
})
|
||||
const play = art.play
|
||||
const pause = art.pause
|
||||
const result = play()
|
||||
assert(result instanceof Promise)
|
||||
assert.deepEqual(calls, ['media:play'])
|
||||
assert.equal(await result, 'proxy-result')
|
||||
assert.deepEqual(calls, ['media:play', 'notice:Play', 'event:play', 'mutex'])
|
||||
assert.equal(pause(), 42)
|
||||
assert.deepEqual(calls.slice(-3), ['media:pause', 'notice:Pause', 'event:pause'])
|
||||
for (const name of ['play', 'pause']) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(art, name)
|
||||
assert.equal(descriptor.enumerable, false)
|
||||
assert.equal(descriptor.configurable, false)
|
||||
assert.equal(descriptor.writable, false)
|
||||
}
|
||||
})
|
||||
|
||||
test('playing uses a shim boolean when present and otherwise keeps native fallback', () => {
|
||||
const video = { currentTime: 1, paused: false, ended: false, readyState: 3 }
|
||||
const art = { template: { $video: video } }
|
||||
playingMix(art)
|
||||
assert.equal(art.playing, true)
|
||||
video.playing = false
|
||||
assert.equal(art.playing, false)
|
||||
video.currentTime = 0
|
||||
video.playing = true
|
||||
assert.equal(art.playing, true)
|
||||
delete video.playing
|
||||
assert.equal(art.playing, false)
|
||||
video.currentTime = 1
|
||||
video.readyState = 2
|
||||
assert.equal(art.playing, false)
|
||||
video.readyState = 3
|
||||
video.ended = true
|
||||
assert.equal(art.playing, false)
|
||||
})
|
||||
|
||||
test('duration normalization reads the current media reference and keeps legacy infinity rules', () => {
|
||||
const art = { template: { $video: { duration: Infinity } } }
|
||||
durationMix(art)
|
||||
assert.equal(art.duration, 0)
|
||||
for (const [value, expected] of [[Number.NaN, 0], [0, 0], [-Infinity, -Infinity], [12.5, 12.5]]) {
|
||||
art.template.$video = { duration: value }
|
||||
assert.equal(art.duration, expected)
|
||||
}
|
||||
})
|
||||
|
||||
test('layout host getters stay live, preserve descriptor order and read scroll offsets', (context) => {
|
||||
const previous = Object.getOwnPropertyDescriptor(globalThis, 'window')
|
||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: { pageXOffset: 4, pageYOffset: 8 } })
|
||||
context.after(() => {
|
||||
if (previous)
|
||||
Object.defineProperty(globalThis, 'window', previous)
|
||||
else
|
||||
delete globalThis.window
|
||||
})
|
||||
let rectangle = { bottom: 40, height: 30, left: 10, right: 50, top: 10, width: 40 }
|
||||
const player = { getBoundingClientRect() {
|
||||
assert.equal(this, player)
|
||||
return rectangle
|
||||
} }
|
||||
const art = { template: { $player: player } }
|
||||
rectMix(art)
|
||||
assert.equal(art.rect, rectangle)
|
||||
assert.equal(art.x, 14)
|
||||
assert.equal(art.y, 18)
|
||||
rectangle = { ...rectangle, width: 80, left: 20 }
|
||||
assert.equal(art.width, 80)
|
||||
assert.equal(art.x, 24)
|
||||
assert.deepEqual(Object.getOwnPropertyNames(art), ['template', 'rect', 'bottom', 'height', 'left', 'right', 'top', 'width', 'x', 'y'])
|
||||
const descriptor = Object.getOwnPropertyDescriptor(art, 'width')
|
||||
assert.equal(descriptor.enumerable, false)
|
||||
assert.equal(descriptor.configurable, false)
|
||||
assert.equal(descriptor.set, undefined)
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { LayoutHost, PauseHost, PlayHost } from '../../packages/artplayer/src/media/hosts'
|
||||
import type { CanvasMedia, MediaSurface, NativeMedia } from '../../packages/artplayer/src/media/types'
|
||||
import durationMix from '../../packages/artplayer/src/player/durationMix'
|
||||
import pauseMix from '../../packages/artplayer/src/player/pauseMix'
|
||||
import playingMix from '../../packages/artplayer/src/player/playingMix'
|
||||
import playMix from '../../packages/artplayer/src/player/playMix'
|
||||
import rectMix from '../../packages/artplayer/src/player/rectMix'
|
||||
|
||||
const native: NativeMedia = document.createElement('video')
|
||||
const canvas: CanvasMedia = Object.assign(document.createElement('canvas'), {
|
||||
src: null,
|
||||
currentSrc: null,
|
||||
currentTime: 0,
|
||||
duration: 10,
|
||||
paused: true,
|
||||
ended: false,
|
||||
readyState: 0,
|
||||
videoWidth: 320,
|
||||
videoHeight: 180,
|
||||
buffered: { length: 0, start: (_index: number) => 0, end: (_index: number) => 0 },
|
||||
volume: 1,
|
||||
muted: false,
|
||||
playbackRate: 1,
|
||||
play: () => 'sync-result',
|
||||
pause: () => 42,
|
||||
load: () => {},
|
||||
})
|
||||
function useSurface(media: MediaSurface) {
|
||||
// @ts-expect-error Proxy capability is optional, including on older native browsers.
|
||||
media.requestPictureInPicture()
|
||||
media.requestPictureInPicture?.()
|
||||
void media.textTracks?.[0]?.activeCues
|
||||
// @ts-expect-error A media surface cannot be passed off as a complete native video.
|
||||
const video: HTMLVideoElement = media
|
||||
void video
|
||||
}
|
||||
useSurface(native)
|
||||
useSurface(canvas)
|
||||
// @ts-expect-error A raw canvas lacks the proxy's media implementation.
|
||||
const incomplete: MediaSurface = document.createElement('canvas')
|
||||
void incomplete
|
||||
|
||||
const shared = { i18n: { get: (key: string) => key }, notice: { show: '' }, emit: (_name: 'play' | 'pause') => {} }
|
||||
const playHost: PlayHost<{ play: () => string }> = { ...shared, template: { $video: { play: () => 'sync-result' } }, option: { mutex: false }, constructor: { instances: [] } }
|
||||
playMix(playHost)
|
||||
const result: Promise<string> = playHost.play()
|
||||
const pauseHost: PauseHost<{ pause: () => number }> = { ...shared, template: { $video: { pause: () => 42 } } }
|
||||
pauseMix(pauseHost)
|
||||
const paused: number = pauseHost.pause()
|
||||
// @ts-expect-error Pausing is not forced to return a Promise.
|
||||
const wrong: Promise<void> = pauseHost.pause()
|
||||
void [result, paused, wrong]
|
||||
|
||||
const mediaHost = { template: { $video: canvas } }
|
||||
playingMix(mediaHost)
|
||||
durationMix(mediaHost)
|
||||
const state: boolean = mediaHost.playing
|
||||
const duration: number = mediaHost.duration
|
||||
const layout: LayoutHost = { template: { $player: document.createElement('div') } }
|
||||
rectMix(layout)
|
||||
const width: number = layout.width
|
||||
// @ts-expect-error Read-only getter properties cannot be assigned.
|
||||
layout.width = 2
|
||||
void [state, duration, width]
|
||||
Reference in new issue
Block a user