mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-11 13:37:44 -08:00
209 lines
13 KiB
JavaScript
209 lines
13 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs'
|
|
import { hash } from '../../refactor/scripts/releases.mjs'
|
|
import { mbCandidate } from '../helpers/mediabunny.js'
|
|
import { expect, test } from './fixtures.js'
|
|
|
|
const implementation = await mbCandidate()
|
|
|
|
test.beforeEach(async ({ browserName }, testInfo) => {
|
|
await testInfo.attach('mediabunny-available-inputs', { contentType: 'application/json', body: JSON.stringify([implementation].map(item => ({ browserName, name: item.name, sha256: hash(item.code), provenance: item.provenance || { kind: item.name } }))) })
|
|
})
|
|
|
|
const manifest = JSON.parse(fs.readFileSync(new URL('./media/hls/manifest.json', import.meta.url)))
|
|
const media = new Map(Object.entries(manifest.files).map(([name, expected]) => {
|
|
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url))
|
|
assert.equal(hash(bytes), expected.sha256)
|
|
return [name, bytes]
|
|
}))
|
|
|
|
for (const core of ['published', 'candidate']) {
|
|
for (const scenario of ['menus', 'topology', 'stale-source', 'stale-destroy', 'latest']) {
|
|
test(`MediaBunny ${implementation.name}: ${core} core native HLS ${scenario}`, async ({ page, browserName }, testInfo) => {
|
|
await page.route('**/mb-ui-hls/**', async (route) => {
|
|
const name = new URL(route.request().url()).pathname.split('/').at(-1)
|
|
const body = media.get(name)
|
|
await route.fulfill({ status: body ? 200 : 404, body: body || 'Missing fixture', contentType: name.endsWith('.m3u8') ? 'application/vnd.apple.mpegurl' : 'video/mp2t' })
|
|
})
|
|
await page.goto(`/test/player.html?core=${core}`)
|
|
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports;
|
|
${implementation.code}; window.mbFactory = module.exports.default || module.exports; })();` })
|
|
await page.evaluate(() => {
|
|
window.mbEvents = []
|
|
window.mbErrors = []
|
|
window.mbWrites = []
|
|
window.mbOption = { m3u8: { quality: { control: true, setting: true }, audio: { control: true, setting: true } } }
|
|
window.mbFlush = () => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
|
window.art = new window.Artplayer({ container: '.player', url: '/mb-ui-hls/master.m3u8', muted: true, setting: true, proxy: window.mbFactory(window.mbOption) })
|
|
window.mbCanvas = window.art.video
|
|
for (const event of ['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'seeked', 'error'])
|
|
window.art.on(`video:${event}`, () => window.mbEvents.push(event))
|
|
document.querySelector('#play').onclick = () => window.art.play().catch(error => window.mbErrors.push(error.message))
|
|
})
|
|
const capabilities = await page.evaluate(() => ({ AudioContext: typeof AudioContext, webkitAudioContext: typeof window.webkitAudioContext, AudioDecoder: typeof AudioDecoder, VideoDecoder: typeof VideoDecoder, coreVersion: window.Artplayer.version }))
|
|
let outcome = 'unverified'
|
|
let result
|
|
try {
|
|
if (capabilities.AudioContext === 'undefined' && capabilities.webkitAudioContext === 'undefined') {
|
|
expect(browserName).toBe('webkit')
|
|
await expect.poll(() => page.evaluate(() => window.mbCanvas.error?.code)).toBe(4)
|
|
expect(await page.evaluate(() => window.art.isReady)).toBe(false)
|
|
expect(await page.locator('.art-control-mediabunny-quality').count()).toBe(0)
|
|
outcome = 'unsupported-capability-control'
|
|
return
|
|
}
|
|
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
|
await expect(page.locator('.art-control-mediabunny-quality .art-selector-value')).toHaveText('180P')
|
|
await expect(page.locator('.art-control-mediabunny-audio .art-selector-value')).toHaveText('English')
|
|
await page.evaluate(() => {
|
|
for (const name of ['controls', 'setting']) {
|
|
const registry = window.art[name]
|
|
const update = registry.update.bind(registry)
|
|
registry.update = (option) => {
|
|
if (option.name?.startsWith('mediabunny-'))
|
|
window.mbWrites.push({ surface: name, name: option.name })
|
|
return update(option)
|
|
}
|
|
}
|
|
})
|
|
if (scenario === 'menus') {
|
|
await page.click('#play')
|
|
await expect.poll(() => page.evaluate(() => window.mbCanvas.currentTime)).toBeGreaterThan(0.2)
|
|
await page.evaluate(() => {
|
|
window.art.pause()
|
|
window.mbEvents.length = 0
|
|
})
|
|
const quality = page.locator('.art-control-mediabunny-quality')
|
|
await quality.hover()
|
|
await quality.locator('.art-selector-item').filter({ hasText: /^90P$/ }).click()
|
|
await expect.poll(() => page.evaluate(() => window.mbCanvas.getM3u8State().then(state => state?.currentLevel?.height))).toBe(90)
|
|
await expect(page.locator('.art-control-mediabunny-quality .art-selector-value')).toHaveText('90P')
|
|
await expect.poll(() => page.evaluate(() => Object.fromEntries(['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'seeked'].map(name => [name, window.mbEvents.filter(event => event === name).length])))).toEqual({ loadedmetadata: 1, loadeddata: 1, canplay: 1, canplaythrough: 1, seeked: 1 })
|
|
await page.evaluate(() => {
|
|
const menu = window.art.setting.find('mediabunny-audio')
|
|
window.art.setting.show = true
|
|
window.art.setting.render(menu.selector)
|
|
menu.selector.find(item => item.html === 'French').$item.click()
|
|
})
|
|
await expect.poll(() => page.evaluate(() => window.mbCanvas.getM3u8State().then(state => state?.currentAudio?.lang))).toBe('fr')
|
|
await expect(page.locator('.art-control-mediabunny-audio .art-selector-value')).toHaveText('French')
|
|
await expect.poll(() => page.evaluate(() => window.mbEvents.filter(event => event === 'seeked').length)).toBe(2)
|
|
await page.evaluate(() => {
|
|
window.art.setting.show = false
|
|
})
|
|
await quality.hover()
|
|
await quality.locator('.art-selector-item').filter({ hasText: /^Auto$/ }).click()
|
|
await expect.poll(() => page.evaluate(() => window.mbCanvas.getM3u8State().then(state => state?.videoMode))).toBe('auto')
|
|
await expect(page.locator('.art-control-mediabunny-quality .art-selector-value')).toHaveText('180P')
|
|
await expect.poll(() => page.evaluate(() => Object.fromEntries(['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'seeked'].map(name => [name, window.mbEvents.filter(event => event === name).length])))).toEqual({ loadedmetadata: 3, loadeddata: 3, canplay: 3, canplaythrough: 3, seeked: 3 })
|
|
result = await page.evaluate(async () => {
|
|
const state = await window.mbCanvas.getM3u8State()
|
|
const quality = window.art.controls.cache.get('mediabunny-quality').option
|
|
const audio = window.art.setting.find('mediabunny-audio')
|
|
return { currentLevel: state.currentLevel.id, qualitySelected: quality.selector.filter(item => item.default).map(item => item.value), currentAudio: state.currentAudio.id, audioSelected: audio.selector.filter(item => item.default).map(item => item.value), paired: state.currentLevel.track.canBePairedWith(state.currentAudio.track), errors: window.mbErrors }
|
|
})
|
|
expect(result.qualitySelected).toEqual([result.currentLevel])
|
|
expect(result.audioSelected).toEqual([result.currentAudio])
|
|
expect(result.paired).toBe(true)
|
|
expect(result.errors).toEqual([])
|
|
}
|
|
else if (scenario === 'topology') {
|
|
await page.evaluate(() => {
|
|
window.mbOption.m3u8.quality.control = false
|
|
window.mbOption.m3u8.audio.control = false
|
|
window.art.emit('restart')
|
|
})
|
|
await expect(page.locator('.art-control-mediabunny-quality')).toHaveCount(0)
|
|
await expect(page.locator('.art-control-mediabunny-audio')).toHaveCount(0)
|
|
expect(await page.evaluate(() => Boolean(window.art.setting.find('mediabunny-quality') && window.art.setting.find('mediabunny-audio')))).toBe(true)
|
|
await page.evaluate(async () => {
|
|
window.mbOption.m3u8.quality.control = true
|
|
window.mbOption.m3u8.audio.control = true
|
|
await window.mbCanvas.engine.load('/mb-ui-hls/video-only.m3u8')
|
|
})
|
|
await expect(page.locator('.art-control-mediabunny-quality')).toHaveCount(1)
|
|
await expect(page.locator('.art-control-mediabunny-audio')).toHaveCount(0)
|
|
expect(await page.evaluate(() => Boolean(window.art.setting.find('mediabunny-audio')))).toBe(false)
|
|
await page.evaluate(() => window.mbCanvas.engine.load('/test/pattern.mp4'))
|
|
await expect(page.locator('[class*="art-control-mediabunny-"]')).toHaveCount(0)
|
|
expect(await page.evaluate(() => Boolean(window.art.setting.find('mediabunny-quality') || window.art.setting.find('mediabunny-audio')))).toBe(false)
|
|
result = { settingsSurviveControlDisable: true, audioRemovedForVideoOnly: true, allRemovedForMp4: true }
|
|
}
|
|
else if (scenario.startsWith('stale-')) {
|
|
await page.evaluate(() => {
|
|
const engine = window.mbCanvas.engine
|
|
const getState = engine.getHlsState.bind(engine)
|
|
let first = true
|
|
engine.getHlsState = async () => {
|
|
const state = await getState()
|
|
if (first) {
|
|
first = false
|
|
return new Promise((resolve) => {
|
|
window.mbRelease = () => resolve(state)
|
|
})
|
|
}
|
|
return state
|
|
}
|
|
window.art.emit('restart')
|
|
})
|
|
await expect.poll(() => page.evaluate(() => typeof window.mbRelease)).toBe('function')
|
|
result = await page.evaluate(async (scenario) => {
|
|
if (scenario === 'stale-source')
|
|
await window.mbCanvas.engine.load('/test/pattern.mp4')
|
|
else window.art.destroy(false)
|
|
const before = window.mbWrites.length
|
|
window.mbRelease()
|
|
await window.mbFlush()
|
|
return { before, after: window.mbWrites.length, menus: document.querySelectorAll('[class*="art-control-mediabunny-"]').length }
|
|
}, scenario)
|
|
expect(result.after).toBe(result.before)
|
|
expect(result.menus).toBe(0)
|
|
}
|
|
else {
|
|
await page.evaluate(async () => {
|
|
const engine = window.mbCanvas.engine
|
|
const input = engine.input
|
|
const getTracks = input.getVideoTracks.bind(input)
|
|
const tracks = await getTracks()
|
|
window.mbDesired = tracks.find(track => track.id !== engine.media.videoTrack.id).id
|
|
let calls = 0
|
|
input.getVideoTracks = () => {
|
|
calls++
|
|
if (calls === 1)
|
|
return new Promise((resolve) => { window.mbReleaseOlder = () => resolve(tracks) })
|
|
if (calls === 2)
|
|
return new Promise((resolve) => { window.mbReleaseNewer = () => resolve(tracks) })
|
|
return getTracks()
|
|
}
|
|
window.mbOlder = window.mbCanvas.switchM3u8Quality(engine.media.videoTrack.id)
|
|
window.mbNewer = window.mbCanvas.switchM3u8Quality(window.mbDesired)
|
|
window.mbReleaseOlder()
|
|
await window.mbOlder
|
|
window.mbReleaseNewer()
|
|
await window.mbNewer
|
|
})
|
|
result = await page.evaluate(async () => {
|
|
const state = await window.mbCanvas.getM3u8State()
|
|
return { desired: window.mbDesired, selected: state.currentLevel.id, ready: window.mbCanvas.readyState, events: window.mbEvents }
|
|
})
|
|
expect(result.selected).toBe(result.desired)
|
|
expect(result.ready).toBe(4)
|
|
}
|
|
outcome = 'native-hls-ui-and-selection-observed'
|
|
}
|
|
finally {
|
|
const state = await page.evaluate(() => {
|
|
window.mbRelease?.()
|
|
window.mbReleaseOlder?.()
|
|
window.mbReleaseNewer?.()
|
|
window.mbContext = window.mbCanvas.engine.audio.audioContext
|
|
window.art.destroy(false)
|
|
return { events: window.mbEvents, errors: window.mbErrors, writes: window.mbWrites, error: window.mbCanvas.error, destroyed: window.mbCanvas.engine.destroyed }
|
|
})
|
|
await expect.poll(() => page.evaluate(() => window.mbContext?.state || 'not-created')).toMatch(/^(?:closed|not-created)$/)
|
|
await testInfo.attach('mediabunny-hls-ui', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), core, scenario, capabilities, outcome, result, state, scope: 'Actual old/candidate core, SDK HLS parsing and native decoding; rendered control/settings clicks, topology and controlled state/query delays. Input provenance identifies source or installed bundle; not long-run/device acceptance.' }) })
|
|
}
|
|
})
|
|
}
|
|
}
|