docs(dash): [SITE-DASH-01] add bilingual guides and fix example lifecycle

This commit is contained in:
Harvey Zhao committed 2026-09-15 18:09:30 +08:00
1 parent 4ea0ba94ae
commit 319dccb605
70 files changed
+4005 -1695

No files matched your search

+11
View File
@@ -489,3 +489,14 @@ replacement and one destroy call per SDK are asserted. Windows WebKit exercises
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.
## DASH documentation examples
`document-dash.spec.js` requires both guide snippets to match the actual
`docs/assets/example/dash.control.js`. That shared source runs once per frozen
dash.js 4.5.2/5.2.1, published/candidate core and browser combination. Local MPD
media is hash-checked; initial/replacement playback and one SDK destroy per instance
are asserted with the original SDK initialization settings. WebKit without MSE
checks only unsupported setup/notice/cleanup. `document-site.spec.js` also verifies
the two guide links and Run Code libraries/source; full SDK/device acceptance stays
separate. Unit nullable-language and repeated-cleanup cases run via test:unit.
+97
View File
@@ -0,0 +1,97 @@
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/dash-sdk.json')).sdks
const plugin = fs.readFileSync('packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.js')
const exampleSource = fs.readFileSync('docs/assets/example/dash.control.js', 'utf8').trim()
const documents = ['plugin/dash-control.md', 'en/plugin/dash-control.md']
for (const document of documents) {
const examples = extractExamples(fs.readFileSync(`packages/artplayer-vitepress/docs/${document}`, 'utf8'), document)
assert.equal(examples.length, 1)
assert.equal(examples[0].code, exampleSource, 'Both documented examples must match the actual demo')
}
const media = new Map()
const sdks = new Map()
test.beforeAll(async () => {
for (const sdk of matrix) {
const bytes = readMember(await ensureArchive(sdk.release), sdk.codeMember)
assert.equal(hash(bytes), sdk.release.files[sdk.codeMember])
sdks.set(sdk.release.version, bytes)
}
const manifest = JSON.parse(fs.readFileSync('test/browser/media/dash/manifest.json'))
for (const [name, expected] of Object.entries(manifest.files)) {
const bytes = fs.readFileSync(`test/browser/media/dash/${name}`)
assert.equal(hash(bytes), expected.sha256)
media.set(name, bytes)
}
})
for (const sdk of matrix) {
for (const core of ['published', 'candidate']) {
test(`documented DASH setup and source lifecycle: SDK ${sdk.release.version}, ${core} core`, async ({ page }, testInfo) => {
await page.route('https://media.axprod.net/TestVectors/v7-Clear/**', async (route) => {
const filename = new URL(route.request().url()).pathname.split('/').at(-1)
const name = filename === 'Manifest_1080p.mpd' ? 'master.mpd' : filename
const bytes = media.get(name)
await route.fulfill({ status: bytes ? 200 : 404, body: bytes || 'Missing DASH fixture', contentType: name.endsWith('.mpd') ? 'application/dash+xml' : 'video/mp4', headers: { 'Access-Control-Allow-Origin': '*' } })
})
await page.goto(`/test/player.html?core=${core}`)
await page.addScriptTag({ content: sdks.get(sdk.release.version).toString() })
await page.addScriptTag({ content: plugin.toString() })
await page.evaluate(() => {
document.querySelector('.player').classList.add('artplayer-app')
window.exampleEngines = []
const mediaPlayer = window.dashjs.MediaPlayer
window.dashjs.MediaPlayer = Object.assign(function (...args) {
const factory = mediaPlayer.apply(this, args)
const create = factory.create
factory.create = function (...args) {
const engine = create.apply(this, args)
const entry = { engine, destroyed: 0 }
window.exampleEngines.push(entry)
const destroy = engine.destroy
engine.destroy = function (...args) {
entry.destroyed++
return destroy.apply(this, args)
}
return engine
}
return factory
}, mediaPlayer)
})
await page.addScriptTag({ content: `${exampleSource}\nwindow.art = art;` })
const supported = await page.evaluate(() => window.dashjs.supportsMediaSource())
await testInfo.attach('documented-dash-inputs', { contentType: 'application/json', body: JSON.stringify({ documents, core, sdk, sdkSHA256: hash(sdks.get(sdk.release.version)), pluginSHA256: hash(plugin), sourceSHA256: hash(exampleSource), supported, scope: supported ? 'real SDK with original settings, local routed MPD, playback and replacement cleanup' : 'actual unsupported setup only' }) })
if (!supported) {
expect(await page.evaluate(() => window.exampleEngines.length)).toBe(0)
expect(await page.evaluate(() => Boolean(window.art.plugins.artplayerPluginDashControl))).toBe(false)
await expect(page.locator('.art-notice')).toContainText('Unsupported playback format: mpd')
await page.evaluate(() => window.art.destroy())
return
}
await expect.poll(() => page.evaluate(() => window.art.video.readyState >= 2)).toBe(true)
expect(await page.evaluate(() => window.art.dash.getVersion())).toBe(sdk.release.version)
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)
await expect(page.locator('.art-control-dash-quality')).toBeVisible()
await expect(page.locator('.art-control-dash-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)
})
}
}
+14
View File
@@ -75,5 +75,19 @@ for (const language of ['zh', 'en']) {
expect(hlsTarget.searchParams.get('code')).toContain('art.on(\'destroy\', destroyHls)')
expect(failedAssets).toEqual([])
await hlsExample.close()
await page.locator(`.VPSidebar a[href="${prefix}/plugin/dash-control.html"]`).click()
await expect(page.locator('h1')).toContainText(language === 'en' ? 'DASH Control' : 'DASH 控制')
const dashPopup = context.waitForEvent('page')
await page.locator('.run-code, [classname="run-code"]').first().click()
const dashExample = await dashPopup
await dashExample.waitForLoadState('domcontentloaded')
const dashTarget = new URL(dashExample.url())
expect(dashTarget.searchParams.get('libs').split('\n')).toEqual([
'https://cdnjs.cloudflare.com/ajax/libs/dashjs/5.2.1/modern/umd/dash.all.min.js',
'./uncompiled/artplayer-plugin-dash-control/index.js',
])
expect(dashTarget.searchParams.get('code')).toContain('art.on(\'destroy\', destroyDash)')
expect(failedAssets).toEqual([])
await dashExample.close()
})
}
+79
View File
@@ -0,0 +1,79 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
// eslint-disable-next-line test/no-import-node-test -- Execute the actual public example with a controlled SDK lifecycle.
import { test } from 'node:test'
import { runInNewContext } from 'node:vm'
const source = fs.readFileSync(new URL('../docs/assets/example/dash.control.js', import.meta.url), 'utf8')
function loadExample(supported) {
const engines = []
const hooks = []
const pluginOptions = []
const dashjs = {
supportsMediaSource: () => supported,
MediaPlayer: () => ({ create() {
const engine = {
destroyed: 0,
initialize(video, url, autoplay) {
Object.assign(this, { video, url, autoplay })
},
destroy() { this.destroyed++ },
}
engines.push(engine)
return engine
} }),
}
class Artplayer {
constructor(option) {
this.option = { autoplay: false, ...option }
this.notice = {}
}
on(event, callback) { hooks.push({ event, callback }) }
}
const sandbox = { Artplayer, dashjs, artplayerPluginDashControl: (option) => {
pluginOptions.push(option)
return () => ({})
} }
runInNewContext(`${source}\nglobalThis.exampleArt = art`, sandbox)
const art = sandbox.exampleArt
const video = {}
return { art, engines, hooks, pluginOptions, video, load: url => art.option.customType.mpd(video, url, art) }
}
test('DASH example releases replaced engines once and has one final cleanup hook', () => {
const example = loadExample(true)
for (const url of ['first.mpd', 'second.mpd', 'third.mpd'])
example.load(url)
assert.equal(example.art.dash, example.engines[2])
assert.equal(example.art.dash.video, example.video)
assert.equal(example.art.dash.autoplay, false)
assert.deepEqual(example.engines.map(engine => engine.destroyed), [1, 1, 0])
const hooks = example.hooks.filter(hook => hook.event === 'destroy')
assert.equal(hooks.length, 1)
for (const hook of hooks) {
hook.callback()
hook.callback()
}
assert.deepEqual(example.engines.map(engine => engine.destroyed), [1, 1, 1])
})
test('unsupported DASH creates no SDK or SDK-dependent control plugin', () => {
const example = loadExample(false)
example.load('unsupported.mpd')
assert.equal(example.art.notice.show, 'Unsupported playback format: mpd')
assert.equal(example.engines.length, 0)
assert.equal(example.art.option.plugins.length, 0)
assert.equal(example.pluginOptions.length, 0)
for (const hook of example.hooks.filter(hook => hook.event === 'destroy'))
hook.callback()
})
test('DASH audio formatter handles actual SDK nullable metadata and preserves language labels', () => {
const format = loadExample(true).pluginOptions[0].audio.getName
assert.equal(format({ lang: 'en', id: 0 }), 'EN')
assert.equal(format({ lang: null, id: 0 }), '0')
assert.equal(format({ id: 'commentary' }), 'commentary')
assert.equal(format({ lang: null, id: null }), 'Audio')
})