mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
271 lines
16 KiB
JavaScript
271 lines
16 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs'
|
|
import process from 'node:process'
|
|
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
|
|
import { compilePackage } from '../helpers/load.js'
|
|
import { expect, test } from './fixtures.js'
|
|
|
|
let sdkCode
|
|
let publishedCode
|
|
let sourceCode
|
|
let evidence
|
|
let media
|
|
test.afterEach(async ({ page }, testInfo) => {
|
|
const state = await page.evaluate(() => {
|
|
const art = window.art
|
|
return {
|
|
supported: window.Hls?.isSupported(),
|
|
mediaSource: typeof window.MediaSource,
|
|
managedMediaSource: typeof window.ManagedMediaSource,
|
|
video: art?.video.outerHTML,
|
|
readyState: art?.video.readyState,
|
|
networkState: art?.video.networkState,
|
|
sdkErrors: window.sdkErrors,
|
|
sdkEvents: window.sdkEvents,
|
|
levels: art?.hls.levels.map(level => ({ height: level.height, codec: level.videoCodec })),
|
|
tracks: art?.hls.audioTracks.map(track => ({ id: track.id, name: track.name })),
|
|
}
|
|
}).catch(error => ({ diagnosticError: error.message }))
|
|
await testInfo.attach('hls-state', { contentType: 'application/json', body: JSON.stringify(state, null, 2) })
|
|
})
|
|
test.beforeAll(async () => {
|
|
const sdk = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/hls-sdk.json', import.meta.url)))
|
|
const plugin = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/hls-control-release.json', import.meta.url)))
|
|
async function code(release, member) {
|
|
const bytes = readMember(await ensureArchive(release), member)
|
|
assert.equal(hash(bytes), release.files[member])
|
|
return bytes.toString()
|
|
}
|
|
sdkCode = await code(sdk.release, 'package/dist/hls.min.js')
|
|
publishedCode = await code(plugin.release, 'package/dist/artplayer-plugin-hls-control.js')
|
|
// This task exercises source builds. Installed artifacts are a separate HLS-06 gate.
|
|
assert(!process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'HLS source fixture cannot masquerade as an installed plugin')
|
|
sourceCode = process.env.ARTPLAYER_HLS_ARTIFACT
|
|
? fs.readFileSync(process.env.ARTPLAYER_HLS_ARTIFACT, 'utf8')
|
|
: await compilePackage('artplayer-plugin-hls-control', 'umd')
|
|
const manifest = JSON.parse(fs.readFileSync(new URL('./media/hls/manifest.json', import.meta.url)))
|
|
media = new Map()
|
|
for (const [name, expected] of Object.entries(manifest.files)) {
|
|
assert(/^[\w.-]+$/.test(name))
|
|
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url))
|
|
assert.equal(hash(bytes), expected.sha256)
|
|
assert.equal(bytes.length, expected.bytes)
|
|
media.set(name, bytes)
|
|
}
|
|
evidence = { sdk: sdk.release, plugin: plugin.release, sourceSHA256: hash(sourceCode), candidate: process.env.ARTPLAYER_HLS_ARTIFACT || 'workspace source build', media: manifest, limitations: ['SDK worker disabled', 'local deterministic media', 'not an isolated installed package', 'no physical device certification'] }
|
|
})
|
|
|
|
async function openHls(page, core, plugin, testInfo, manifest = 'master.m3u8') {
|
|
await testInfo.attach('hls-inputs', { contentType: 'application/json', body: JSON.stringify(evidence) })
|
|
await page.route('**/hls-fixture/**', async (route) => {
|
|
const name = new URL(route.request().url()).pathname.split('/').at(-1)
|
|
const bytes = media.get(name)
|
|
await route.fulfill({ status: bytes ? 200 : 503, body: bytes || 'Intentional HLS failure', contentType: name.endsWith('.m3u8') ? 'application/vnd.apple.mpegurl' : 'video/mp2t' })
|
|
})
|
|
await page.goto(`/test/player.html?core=${core}`)
|
|
await page.addScriptTag({ content: sdkCode })
|
|
await page.addScriptTag({ content: plugin === 'published' ? publishedCode : sourceCode })
|
|
await page.evaluate((manifest) => {
|
|
window.sdkErrors = []
|
|
window.sdkEvents = []
|
|
window.destroyedEngines = 0
|
|
window.art = new window.Artplayer({
|
|
container: '.player',
|
|
url: `/hls-fixture/${manifest}`,
|
|
muted: true,
|
|
setting: true,
|
|
plugins: [window.artplayerPluginHlsControl({ quality: { control: true, setting: true }, audio: { control: true, setting: true } })],
|
|
customType: { m3u8(video, url, art) {
|
|
if (art.hls)
|
|
art.hls.destroy()
|
|
const hls = new window.Hls({ enableWorker: false, manifestLoadingMaxRetry: 0 })
|
|
art.hls = hls
|
|
hls.on(window.Hls.Events.ERROR, (_, data) => window.sdkErrors.push({ type: data.type, details: data.details, fatal: data.fatal }))
|
|
for (const name of ['LEVEL_SWITCHED', 'AUDIO_TRACK_SWITCHED', 'MANIFEST_PARSED'])
|
|
hls.on(window.Hls.Events[name], (_, data) => window.sdkEvents.push({ name, level: data.level, id: data.id }))
|
|
hls.on(window.Hls.Events.DESTROYING, () => window.destroyedEngines++)
|
|
hls.loadSource(url)
|
|
hls.attachMedia(video)
|
|
} },
|
|
})
|
|
const art = window.art
|
|
art.on('destroy', () => art.hls.destroy())
|
|
document.querySelector('#play').onclick = () => art.play()
|
|
document.querySelector('#pause').onclick = () => art.pause()
|
|
}, manifest)
|
|
}
|
|
|
|
test('SDK MSE availability is explicit; unavailable hosts report failure and clean up', async ({ page, browserName }, testInfo) => {
|
|
await openHls(page, 'candidate', 'published', testInfo)
|
|
const capability = await page.evaluate(() => ({ supported: window.Hls.isSupported(), mse: typeof window.MediaSource, managed: typeof window.ManagedMediaSource }))
|
|
if (process.platform === 'win32' && browserName === 'webkit') {
|
|
expect(capability).toEqual({ supported: false, mse: 'undefined', managed: 'undefined' })
|
|
await expect.poll(() => page.evaluate(() => window.sdkErrors.filter(error => error.fatal))).toEqual([{ type: 'mediaError', details: 'manifestIncompatibleCodecsError', fatal: true }])
|
|
expect(await page.evaluate(() => window.art.isReady)).toBe(false)
|
|
}
|
|
else {
|
|
expect(capability.supported).toBe(true)
|
|
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
|
}
|
|
expect(await page.evaluate(() => {
|
|
const sdk = window.art.hls
|
|
window.art.destroy()
|
|
return { media: sdk.media, destroyed: window.destroyedEngines }
|
|
})).toEqual({ media: null, destroyed: 1 })
|
|
})
|
|
|
|
test.describe('MSE playback matrix', () => {
|
|
// Verified by the separate capability test above. Safari/MSE coverage remains HLS-05 work.
|
|
test.skip(({ browserName }) => process.platform === 'win32' && browserName === 'webkit', 'Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval')
|
|
for (const core of ['published', 'candidate']) {
|
|
for (const plugin of ['published', 'candidate']) {
|
|
test(`${core} core / ${plugin} HLS: real decoding, manual quality, Auto and audio`, async ({ page }, testInfo) => {
|
|
await openHls(page, core, plugin, testInfo)
|
|
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
|
await page.click('#play')
|
|
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeGreaterThan(0.3)
|
|
const initial = await page.evaluate(() => ({
|
|
supported: window.Hls.isSupported(),
|
|
version: window.Hls.version,
|
|
levels: window.art.hls.levels.map(level => level.height),
|
|
tracks: window.art.hls.audioTracks.map(track => ({ id: track.id, name: track.name })),
|
|
frames: window.art.video.getVideoPlaybackQuality().totalVideoFrames,
|
|
}))
|
|
expect(initial).toMatchObject({ supported: true, version: '1.5.17', levels: [90, 180], tracks: [{ id: 0, name: 'English' }, { id: 1, name: 'French' }] })
|
|
expect(initial.frames).toBeGreaterThan(0)
|
|
await page.evaluate(() => {
|
|
window.art.controls.show = true
|
|
})
|
|
const quality = page.locator('.art-control-hls-quality')
|
|
await quality.hover()
|
|
await quality.locator('.art-selector-item').filter({ hasText: /^90P$/ }).click()
|
|
await expect.poll(() => page.evaluate(() => ({ automatic: window.art.hls.autoLevelEnabled, level: window.art.hls.currentLevel, height: window.art.video.videoHeight }))).toEqual({ automatic: false, level: 0, height: 90 })
|
|
await quality.hover()
|
|
await quality.locator('.art-selector-item').filter({ hasText: /^180P$/ }).click()
|
|
await expect.poll(() => page.evaluate(() => ({ manual: window.art.hls.autoLevelEnabled, level: window.art.hls.currentLevel, height: window.art.video.videoHeight }))).toEqual({ manual: false, level: 1, height: 180 })
|
|
await quality.hover()
|
|
await quality.locator('.art-selector-item').filter({ hasText: /^Auto$/ }).click()
|
|
await expect.poll(() => page.evaluate(() => window.art.hls.autoLevelEnabled)).toBe(true)
|
|
await expect.poll(() => page.evaluate(() => window.art.hls.currentLevel)).toBeGreaterThanOrEqual(0)
|
|
await page.evaluate(() => {
|
|
window.art.controls.show = true
|
|
})
|
|
const audio = page.locator('.art-control-hls-audio')
|
|
await audio.hover()
|
|
await audio.locator('.art-selector-item').filter({ hasText: /^French$/ }).click()
|
|
await expect.poll(() => page.evaluate(() => window.sdkEvents.some(event => event.name === 'AUDIO_TRACK_SWITCHED' && event.id === 1))).toBe(true)
|
|
expect(await page.evaluate(() => window.art.hls.audioTrack)).toBe(1)
|
|
await page.click('#pause')
|
|
await expect.poll(() => page.evaluate(() => window.art.video.paused)).toBe(true)
|
|
await page.evaluate(() => {
|
|
window.art.seek = 5
|
|
})
|
|
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeCloseTo(5, 1)
|
|
await page.click('#play')
|
|
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeGreaterThan(5.2)
|
|
expect(await page.evaluate(() => window.sdkErrors.filter(error => error.fatal))).toEqual([])
|
|
const result = await page.evaluate(() => {
|
|
const video = window.art.video
|
|
const canvas = document.createElement('canvas')
|
|
canvas.width = 16
|
|
canvas.height = 16
|
|
const ctx = canvas.getContext('2d')
|
|
ctx.drawImage(video, 0, 0, 16, 16)
|
|
const pixels = ctx.getImageData(0, 0, 16, 16).data
|
|
const colors = new Set()
|
|
for (let index = 0; index < pixels.length; index += 4)
|
|
colors.add(`${pixels[index]},${pixels[index + 1]},${pixels[index + 2]}`)
|
|
const sdk = window.art.hls
|
|
window.art.destroy()
|
|
return { colors: colors.size, media: sdk.media, destroyed: window.destroyedEngines, children: document.querySelector('.player').childElementCount }
|
|
})
|
|
expect(result.colors).toBeGreaterThan(8)
|
|
expect(result).toMatchObject({ media: null, destroyed: 1, children: 0 })
|
|
})
|
|
|
|
test(`${core} core / ${plugin} HLS: failed manifest and actual SDK teardown`, async ({ page }, testInfo) => {
|
|
await openHls(page, core, plugin, testInfo, 'missing.m3u8')
|
|
await expect.poll(() => page.evaluate(() => window.sdkErrors.filter(error => error.fatal))).toEqual([{ type: 'networkError', details: 'manifestLoadError', fatal: true }])
|
|
expect(await page.locator('.art-control-hls-quality').count()).toBe(0)
|
|
await page.evaluate(() => window.art.switchUrl('/hls-fixture/master.m3u8'))
|
|
await expect.poll(() => page.evaluate(() => window.art.hls.levels.length)).toBe(2)
|
|
await page.click('#play')
|
|
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeGreaterThan(0.2)
|
|
expect(await page.evaluate(() => {
|
|
const sdk = window.art.hls
|
|
window.art.destroy()
|
|
return { destroyed: window.destroyedEngines, media: sdk.media }
|
|
})).toEqual({ destroyed: 2, media: null })
|
|
})
|
|
|
|
test(`${core} core / ${plugin} HLS: source switch drops SDK audio topology`, async ({ page }, testInfo) => {
|
|
await openHls(page, core, plugin, testInfo)
|
|
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
|
await page.click('#play')
|
|
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeGreaterThan(0.2)
|
|
await page.evaluate(() => {
|
|
window.previousHls = window.art.hls
|
|
return window.art.switchUrl('/hls-fixture/video-only.m3u8')
|
|
})
|
|
await expect.poll(() => page.evaluate(() => ({ levels: window.art.hls.levels.length, tracks: window.art.hls.audioTracks.length, height: window.art.video.videoHeight }))).toEqual({ levels: 1, tracks: 0, height: 90 })
|
|
expect(await page.evaluate(() => ({ oldMedia: window.previousHls.media, replaced: window.previousHls !== window.art.hls, destroyed: window.destroyedEngines }))).toEqual({ oldMedia: null, replaced: true, destroyed: 1 })
|
|
if (plugin === 'published') {
|
|
// Real SDK corroboration of the frozen historical empty-topology defect.
|
|
await expect(page.locator('.art-control-hls-audio')).toHaveCount(1)
|
|
}
|
|
else {
|
|
await expect(page.locator('.art-control-hls-audio')).toHaveCount(0)
|
|
}
|
|
expect(await page.evaluate(() => window.sdkErrors.filter(error => error.fatal))).toEqual([])
|
|
await page.evaluate(() => window.art.destroy())
|
|
expect(await page.evaluate(() => window.destroyedEngines)).toBe(2)
|
|
})
|
|
}
|
|
|
|
test(`${core} core / published HLS: real Auto playback loses Auto label on update`, async ({ page }, testInfo) => {
|
|
await openHls(page, core, 'published', testInfo)
|
|
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
|
await page.click('#play')
|
|
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeGreaterThan(0.2)
|
|
expect(await page.evaluate(() => window.art.hls.autoLevelEnabled)).toBe(true)
|
|
const height = await page.evaluate(() => {
|
|
const art = window.art
|
|
art.plugins.artplayerPluginHlsControl.update()
|
|
return art.hls.levels[art.hls.currentLevel].height
|
|
})
|
|
await expect(page.locator('.art-control-hls-quality .art-selector-value')).toHaveText(`${height}P`)
|
|
expect(await page.evaluate(() => window.art.hls.autoLevelEnabled)).toBe(true)
|
|
await page.evaluate(() => window.art.destroy())
|
|
})
|
|
|
|
test(`${core} core / candidate HLS: SDK events synchronize Auto and audio without replacing menus`, async ({ page }, testInfo) => {
|
|
await openHls(page, core, 'candidate', testInfo)
|
|
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
|
await page.click('#play')
|
|
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeGreaterThan(0.3)
|
|
await expect(page.locator('.art-control-hls-quality .art-selector-value')).toHaveText('Auto')
|
|
await page.evaluate(() => {
|
|
window.qualityElement = document.querySelector('.art-control-hls-quality')
|
|
window.audioElement = document.querySelector('.art-control-hls-audio')
|
|
window.art.hls.currentLevel = 0
|
|
})
|
|
await expect.poll(() => page.evaluate(() => window.art.hls.currentLevel)).toBe(0)
|
|
await expect(page.locator('.art-control-hls-quality .art-selector-value')).toHaveText('90P')
|
|
await page.evaluate(() => {
|
|
window.art.hls.currentLevel = -1
|
|
})
|
|
await expect.poll(() => page.evaluate(() => window.art.hls.currentLevel)).toBeGreaterThanOrEqual(0)
|
|
await expect(page.locator('.art-control-hls-quality .art-selector-value')).toHaveText('Auto')
|
|
await page.evaluate(() => {
|
|
window.art.hls.audioTrack = 1
|
|
})
|
|
await expect(page.locator('.art-control-hls-audio .art-selector-value')).toHaveText('French')
|
|
expect(await page.evaluate(() => ({ quality: window.qualityElement === document.querySelector('.art-control-hls-quality'), audio: window.audioElement === document.querySelector('.art-control-hls-audio') }))).toEqual({ quality: true, audio: true })
|
|
await page.evaluate(() => window.art.plugins.artplayerPluginHlsControl.update())
|
|
await expect(page.locator('.art-control-hls-quality .art-selector-value')).toHaveText('Auto')
|
|
expect(await page.evaluate(() => window.sdkErrors.filter(error => error.fatal))).toEqual([])
|
|
await page.evaluate(() => window.art.destroy())
|
|
})
|
|
}
|
|
})
|