mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-13 07:46:17 -08:00
docs(hls): [SITE-HLS-01] add bilingual guides and fix example SDK ownership
This commit is contained in:
1 parent
2802cbacc5
commit
4ea0ba94ae
68 files changed
+5308
-1677
No files matched your search
@@ -478,3 +478,14 @@ playback passes; native attachment can affect timing and permission failures lea
|
||||
All dumps stay in ignored local cache. `firefox-minidump.mjs` reads exception/module/thread
|
||||
metadata but does not resolve a native stack. See
|
||||
[the native evidence and commands](../../refactor/changes/2026-09-15-PKG-HLS-SDK-01-native-exception.md).
|
||||
|
||||
## HLS documentation examples
|
||||
|
||||
`document-hls.spec.js` extracts the single Run Code from each bilingual HLS guide
|
||||
and requires exact parity with `docs/assets/example/hls.control.js`. It loads frozen
|
||||
Hls.js 1.5.17/1.7.2 and the actual plugin distribution with published/candidate core.
|
||||
The external example URL is routed to hash-checked local HLS media; playback, source
|
||||
replacement and one destroy call per SDK are asserted. Windows WebKit exercises only
|
||||
unsupported setup/notice/cleanup. Worker configuration stays enabled, but this suite
|
||||
does not observe worker output or establish native-HLS playback. The separate
|
||||
`document-site.spec.js` checks both guide links and Run Code library/source parameters.
|
||||
@@ -0,0 +1,91 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { extractExamples } from '../../scripts/docs-smoke/parser.ts'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const matrix = JSON.parse(fs.readFileSync('refactor/baselines/hls-sdk-matrix.json'))
|
||||
const plugin = fs.readFileSync('packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.js')
|
||||
const exampleSource = fs.readFileSync('docs/assets/example/hls.control.js', 'utf8').trim()
|
||||
const media = new Map()
|
||||
const sdks = new Map()
|
||||
|
||||
test.beforeAll(async () => {
|
||||
for (const release of matrix.releases) {
|
||||
const bytes = readMember(await ensureArchive(release), 'package/dist/hls.min.js')
|
||||
assert.equal(hash(bytes), release.files['package/dist/hls.min.js'])
|
||||
sdks.set(release.version, bytes)
|
||||
}
|
||||
const manifest = JSON.parse(fs.readFileSync('test/browser/media/hls/manifest.json'))
|
||||
for (const [name, expected] of Object.entries(manifest.files)) {
|
||||
const bytes = fs.readFileSync(`test/browser/media/hls/${name}`)
|
||||
assert.equal(hash(bytes), expected.sha256)
|
||||
media.set(name, bytes)
|
||||
}
|
||||
})
|
||||
|
||||
for (const document of ['plugin/hls-control.md', 'en/plugin/hls-control.md']) {
|
||||
const examples = extractExamples(fs.readFileSync(`packages/artplayer-vitepress/docs/${document}`, 'utf8'), document)
|
||||
assert.equal(examples.length, 1)
|
||||
assert.equal(examples[0].code, exampleSource, 'Documented setup must remain identical to the actual demo')
|
||||
for (const release of matrix.releases) {
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`documented HLS setup and source lifecycle: ${document}, SDK ${release.version}, ${core} core`, async ({ page }, testInfo) => {
|
||||
await page.route('https://playertest.longtailvideo.com/adaptive/elephants_dream_v4/**', async (route) => {
|
||||
const filename = new URL(route.request().url()).pathname.split('/').at(-1)
|
||||
const name = filename === 'index.m3u8' ? 'master.m3u8' : filename
|
||||
const bytes = media.get(name)
|
||||
await route.fulfill({ status: bytes ? 200 : 404, body: bytes || 'Missing HLS fixture', contentType: name.endsWith('.m3u8') ? 'application/vnd.apple.mpegurl' : 'video/mp2t', headers: { 'Access-Control-Allow-Origin': '*' } })
|
||||
})
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.addScriptTag({ content: sdks.get(release.version).toString() })
|
||||
await page.addScriptTag({ content: plugin.toString() })
|
||||
await page.evaluate(() => {
|
||||
document.querySelector('.player').classList.add('artplayer-app')
|
||||
window.exampleEngines = []
|
||||
const destroy = window.Hls.prototype.destroy
|
||||
const attach = window.Hls.prototype.attachMedia
|
||||
window.Hls.prototype.attachMedia = function (...args) {
|
||||
window.exampleEngines.push({ engine: this, destroyed: 0 })
|
||||
return attach.apply(this, args)
|
||||
}
|
||||
window.Hls.prototype.destroy = function (...args) {
|
||||
window.exampleEngines.find(item => item.engine === this).destroyed++
|
||||
return destroy.apply(this, args)
|
||||
}
|
||||
})
|
||||
await page.addScriptTag({ content: `${examples[0].code}\nwindow.art = art;` })
|
||||
const supported = await page.evaluate(() => window.Hls.isSupported())
|
||||
await testInfo.attach('documented-hls-inputs', { contentType: 'application/json', body: JSON.stringify({ document, core, sdk: release, sdkSHA256: hash(sdks.get(release.version)), pluginSHA256: hash(plugin), sourceSHA256: hash(exampleSource), supported, scope: supported ? 'real SDK default worker, local routed stream, playback and replacement cleanup' : 'actual unsupported setup; native fallback is only unit-tested' }) })
|
||||
if (!supported) {
|
||||
expect(await page.evaluate(() => window.exampleEngines.length)).toBe(0)
|
||||
expect(await page.evaluate(() => Boolean(window.art.plugins.artplayerPluginHlsControl))).toBe(false)
|
||||
// Windows WebKit has neither the tested MSE path nor native HLS support.
|
||||
expect(await page.evaluate(() => window.art.video.canPlayType('application/vnd.apple.mpegurl'))).toBe('')
|
||||
await expect(page.locator('.art-notice')).toContainText('Unsupported playback format: m3u8')
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
return
|
||||
}
|
||||
await expect.poll(() => page.evaluate(() => window.art.video.readyState >= 2)).toBe(true)
|
||||
await page.evaluate(() => {
|
||||
window.art.muted = true
|
||||
})
|
||||
await page.evaluate(() => window.art.play())
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
|
||||
expect(await page.evaluate(() => window.art.video.getVideoPlaybackQuality().totalVideoFrames)).toBeGreaterThan(2)
|
||||
expect(await page.evaluate(() => window.art.hls.config.enableWorker)).toBe(true)
|
||||
await expect(page.locator('.art-control-hls-quality')).toBeVisible()
|
||||
await expect(page.locator('.art-control-hls-audio')).toBeVisible()
|
||||
await page.evaluate(() => window.art.switchUrl(`${window.art.option.url}?reload=1`))
|
||||
await expect.poll(() => page.evaluate(() => window.art.video.readyState >= 2)).toBe(true)
|
||||
await page.evaluate(() => window.art.play())
|
||||
const start = await page.evaluate(() => ({ time: window.art.currentTime, frames: window.art.video.getVideoPlaybackQuality().totalVideoFrames }))
|
||||
await expect.poll(() => page.evaluate(start => window.art.currentTime > start.time + 0.3 && window.art.video.getVideoPlaybackQuality().totalVideoFrames > start.frames + 2, start)).toBe(true)
|
||||
expect(await page.evaluate(() => window.exampleEngines.map(item => item.destroyed))).toEqual([1, 0])
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
expect(await page.evaluate(() => window.exampleEngines.map(item => item.destroyed))).toEqual([1, 1])
|
||||
expect(await page.locator('.player').evaluate(element => element.childElementCount)).toBe(0)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,5 +61,19 @@ for (const language of ['zh', 'en']) {
|
||||
expect(exampleUrl.searchParams.get('code')).toContain('artplayerPluginDanmuku({')
|
||||
expect(failedAssets).toEqual([])
|
||||
await example.close()
|
||||
await page.locator(`.VPSidebar a[href="${prefix}/plugin/hls-control.html"]`).click()
|
||||
await expect(page.locator('h1')).toContainText(language === 'en' ? 'HLS Control' : 'HLS 控制')
|
||||
const hlsPopup = context.waitForEvent('page')
|
||||
await page.locator('.run-code, [classname="run-code"]').first().click()
|
||||
const hlsExample = await hlsPopup
|
||||
await hlsExample.waitForLoadState('domcontentloaded')
|
||||
const hlsTarget = new URL(hlsExample.url())
|
||||
expect(hlsTarget.searchParams.get('libs').split('\n')).toEqual([
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.5.17/hls.min.js',
|
||||
'./uncompiled/artplayer-plugin-hls-control/index.js',
|
||||
])
|
||||
expect(hlsTarget.searchParams.get('code')).toContain('art.on(\'destroy\', destroyHls)')
|
||||
expect(failedAssets).toEqual([])
|
||||
await hlsExample.close()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Verify the actual demo source against a controlled SDK lifecycle.
|
||||
import { test } from 'node:test'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
|
||||
const source = fs.readFileSync(new URL('../docs/assets/example/hls.control.js', import.meta.url), 'utf8')
|
||||
|
||||
function loadExample(supported) {
|
||||
const engines = []
|
||||
const hooks = []
|
||||
let pluginCalls = 0
|
||||
class Hls {
|
||||
static isSupported() { return supported }
|
||||
constructor() {
|
||||
this.destroyCount = 0
|
||||
engines.push(this)
|
||||
}
|
||||
|
||||
loadSource(url) { this.url = url }
|
||||
attachMedia(video) { this.media = video }
|
||||
destroy() { this.destroyCount++ }
|
||||
}
|
||||
class Artplayer {
|
||||
constructor(option) {
|
||||
this.option = option
|
||||
this.notice = {}
|
||||
}
|
||||
|
||||
on(event, callback) { hooks.push({ event, callback }) }
|
||||
}
|
||||
const sandbox = { Artplayer, Hls, artplayerPluginHlsControl: () => {
|
||||
pluginCalls++
|
||||
return () => ({})
|
||||
} }
|
||||
runInNewContext(`${source}\nglobalThis.exampleArt = art`, sandbox)
|
||||
const art = sandbox.exampleArt
|
||||
const video = { canPlayType: () => 'maybe', src: '' }
|
||||
return { art, video, engines, hooks, get pluginCalls() {
|
||||
return pluginCalls
|
||||
}, load: url => art.option.customType.m3u8(video, url, art) }
|
||||
}
|
||||
|
||||
test('HLS example destroys every replaced engine once and keeps one final cleanup hook', () => {
|
||||
const example = loadExample(true)
|
||||
example.load('one.m3u8')
|
||||
example.load('two.m3u8')
|
||||
example.load('three.m3u8')
|
||||
assert.equal(example.engines.length, 3)
|
||||
assert.equal(example.art.hls, example.engines[2])
|
||||
assert.equal(example.art.hls.media, example.video)
|
||||
assert.deepEqual(example.engines.map(engine => engine.destroyCount), [1, 1, 0])
|
||||
assert.equal(example.hooks.filter(hook => hook.event === 'destroy').length, 1)
|
||||
for (const hook of example.hooks.filter(hook => hook.event === 'destroy'))
|
||||
hook.callback()
|
||||
assert.deepEqual(example.engines.map(engine => engine.destroyCount), [1, 1, 1])
|
||||
})
|
||||
|
||||
test('native HLS fallback does not install controls requiring an Hls.js instance', () => {
|
||||
const example = loadExample(false)
|
||||
example.load('native.m3u8')
|
||||
assert.equal(example.video.src, 'native.m3u8')
|
||||
assert.equal(example.art.option.plugins.length, 0)
|
||||
assert.equal(example.pluginCalls, 0)
|
||||
assert.equal(example.engines.length, 0)
|
||||
for (const hook of example.hooks.filter(hook => hook.event === 'destroy'))
|
||||
hook.callback()
|
||||
})
|
||||
|
||||
test('unsupported HLS reports the existing notice without creating an SDK', () => {
|
||||
const example = loadExample(false)
|
||||
example.video.canPlayType = () => ''
|
||||
example.load('unsupported.m3u8')
|
||||
assert.equal(example.art.notice.show, 'Unsupported playback format: m3u8')
|
||||
assert.equal(example.video.src, '')
|
||||
assert.equal(example.engines.length, 0)
|
||||
})
|
||||
Reference in new issue
Block a user