test(ci): [CI-01] verify installed MediaBunny browser matrix

This commit is contained in:
Harvey Zhao committed 2026-09-15 03:07:20 +08:00
1 parent 8379ac7bbf
commit 25a0c6303d
22 files changed
+5228 -19

No files matched your search

@@ -315,3 +315,20 @@ races using the actual new/old cores and SDK with controlled query ordering. PKG
validation. PKG-MB-10 owns the demo, installed package and MPL/source notice gate.
Windows WebKit without WebCodecs/Web Audio is recorded as a capability failure,
not successful playback or a claim about all Safari installations.
## Installed browser validation
Run `yarn test:package --browser`, then pass its browser-artifacts.json as
ARTPLAYER_BROWSER_ARTIFACTS to `yarn test:browser:installed mediabunny`. The shared
roster includes this proxy and Document PiP. All ten MediaBunny browser files use
the verified installed UMD, including its bundled SDK; they never silently rebuild
a candidate when an installed map exists. Baseline and artifact overrides are
rejected in installed mode; its runner also rejects the candidate-only flag so
historical controls stay present. Source mode retains these explicit diagnostics.
Available-input attachments are an inventory, not proof that every listed input
ran. Each observation identifies the selected implementation and its provenance;
PiP combinations identify both packages. Local MP4/WebM/HLS fixtures remain test
media, not npm assets. Actual decoding, controlled races/unsupported decoder
branches, missing browser APIs and physical device evidence stay distinct. This
CI coverage does not close PKG-MB-09/10 or establish long-run/acoustic acceptance.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,44 @@
# CI-01 MediaBunny 安装产物浏览器验证
## 范围与兼容边界
公共运行时、类型、依赖、媒体夹具和测试断言不变。共同安装清单新增
artplayer-proxy-mediabunny,沿用其已冻结 npm 契约,正常打包、隔离安装后
核验文件。SDK 随 UMD 构建内联;MP4/WebM/HLS 输入仍是独立测试媒体。
十个 MediaBunny 浏览器文件共用已验证安装候选;有 map 时禁止回退源码、
旧工作区或另一个显式 artifact。安装入口拒绝仅候选的环境过滤,以保留旧版
对照。源码入口保留原有诊断方式。可用输入附件提前记录清单,不能据此声称
所有列出的版本均运行;最终观察记录实际 implementation/provenance。
Document PiP 组合同时记录 proxy 和 PiP 身份。
原生解码、WebAudio/HLS、Canvas、真实窗口能力与受控竞态分开说明。
缺失 API 的对照即使断言通过,也不算该平台播放验收;短期音画采样不等于
物理声音、长时漂移或真机验证。CI-01 与 PKG-MB-09/10 门槛保持开放。
## 验证
Windows x64 / Node 24.21.0 / Yarn 1.22.22。十五包正常构建、pack、仓库外安装、
离线冻结复装及完整成员核验通过(run-cblqI3,69.25 秒,退出 0);通用消费者
仍是 core/chapter 的 36 运行时、5 旧类型模式和 8 精确类型模式,不扩大声称。
完整 installed collection 为 49 文件/1536 项,本轮仅实跑十个 MediaBunny 文件。
Chromium 153.0.8010.12、Firefox 155.0、WebKit 26.6 各 76 项,合计 228/228
断言通过,0 skip/失败/重试,110.678 秒,实际退出 0。这里“通过”包含能力和
历史缺陷对照;WebKit 有 51 个缺解码/音频能力对照、12 个 PiP 不可用分支,
不是这些功能的成功播放。Chromium/Firefox 各 12 个原生 Document PiP 案例
验证 Canvas/窗口播放、恢复或销毁;每例分别核验 proxy 与 PiP 安装身份。
其余原生输入、HLS、音频/视频竞态及受控 decoder 分支详见逐例 outcome。
故意继承安装 map 后,source 入口清除它,Chromium 的 HLS menus、av-clock、
seek-source 三项 3/3 通过(34.899 秒),观察附件均为 source-build。
这是源码入口定向证明,不是全量源码回归。单元 257/257、工程 48/48、scoped
lint、library 类型、严格工具链和 CI 契约检查通过。最初新增 beforeEach 的空
对象参数和格式被 lint 拦截,改为记录实际 browserName 后通过;日志均保留。
报告已在进程实际结束后归档为 ci01-mb-installed-report、ci01-mb-source-report
和 ci01-mb-collection-report。输入指纹、完整逐例结果/能力分类、报告与日志
哈希见[机器证据](../baselines/ci-mediabunny-installed-validation.json)。
此前 Chapter/Audio/DASH 的失败未重跑或关闭;全十五包矩阵、远端 CI、物理
音频与长时/设备门槛仍未完成。没有新增依赖或生产源码变更,没有推送或发布。
回退本检查点恢复十四包清单及 MediaBunny 原有源码加载方式。
+1 -1
View File
@@ -509,7 +509,7 @@
- ENG-09: [记录](changes/2026-09-12-ENG-09-integration.md) [记录](baselines/engineering-integration.json)
- ENG-10: [记录](changes/2026-09-10-ENG-10-test-reliability.md) [记录](test-reliability.md)
- ENG-11: [记录](build-analysis.md) [记录](baselines/bundle-attribution.json) [记录](changes/2026-09-11-ENG-11-build-analysis.md)
- CI-01: [记录](ci-setup.md) [记录](changes/2026-09-13-CI-01-matrix-summary.md) [记录](baselines/ci-matrix-validation.json) [记录](changes/2026-09-13-CI-01-node-consumers.md) [记录](baselines/node-consumer-validation.json) [记录](changes/2026-09-14-CI-01-installed-full.md) [记录](baselines/ci-installed-full-validation.json) [记录](changes/2026-09-14-CI-01-source-chromium.md) [记录](baselines/ci-source-chromium-validation.json) [记录](changes/2026-09-15-CI-01-ads-installed.md) [记录](baselines/ci-ads-installed-validation.json) [记录](changes/2026-09-15-CI-01-subtitles-installed.md) [记录](baselines/ci-subtitles-installed-validation.json) [记录](changes/2026-09-15-CI-01-adaptive-installed.md) [记录](baselines/ci-adaptive-installed-validation.json) [记录](changes/2026-09-15-CI-01-asr-cast-installed.md) [记录](baselines/ci-asr-cast-installed-validation.json)
- CI-01: [记录](ci-setup.md) [记录](changes/2026-09-13-CI-01-matrix-summary.md) [记录](baselines/ci-matrix-validation.json) [记录](changes/2026-09-13-CI-01-node-consumers.md) [记录](baselines/node-consumer-validation.json) [记录](changes/2026-09-14-CI-01-installed-full.md) [记录](baselines/ci-installed-full-validation.json) [记录](changes/2026-09-14-CI-01-source-chromium.md) [记录](baselines/ci-source-chromium-validation.json) [记录](changes/2026-09-15-CI-01-ads-installed.md) [记录](baselines/ci-ads-installed-validation.json) [记录](changes/2026-09-15-CI-01-subtitles-installed.md) [记录](baselines/ci-subtitles-installed-validation.json) [记录](changes/2026-09-15-CI-01-adaptive-installed.md) [记录](baselines/ci-adaptive-installed-validation.json) [记录](changes/2026-09-15-CI-01-asr-cast-installed.md) [记录](baselines/ci-asr-cast-installed-validation.json) [记录](changes/2026-09-15-CI-01-mediabunny-installed.md) [记录](baselines/ci-mediabunny-installed-validation.json)
- CI-02: [记录](changes/2026-09-14-CI-02-pages-artifact.md) [记录](baselines/pages-artifact-validation.json) [记录](pages-deployment.md)
- PILOT-01: [记录](changes/2026-09-10-PILOT-01-chapter.md) [记录](baselines/pilot-validation.json)
- CORE-01: [记录](changes/2026-09-10-CORE-01-typed-utils.md) [记录](baselines/core-utils-validation.json)
+9
View File
@@ -1,5 +1,14 @@
# 进度与证据
## CI-01 MediaBunny 接入实际安装矩阵
十五包打包/隔离安装通过,新增 MediaBunny 十文件实跑 228/228 断言通过,
包括原生解码/HLS/音频、PiP 组合与受控/能力对照;WebKit 缺能力的分支不算
播放验收。源码定向 3/3、相关单元 257/257、工程 48/48 通过,见
[记录](changes/2026-09-15-CI-01-mediabunny-installed.md)。完整清单扩展到
49 文件/1536 项,本轮未全跑。197/263 完成,CI-01 与设备/发布门槛仍开放;
下一步接续 JASSUB/弹幕等剩余包,并核对独立 worker/模型资源来源。
## CI-01 统一安装包清单并接入 ASR / Chromecast
新增 --browser 从同一清单准备十四包,实际安装核验通过;消除 CI 手写列表并
+3 -1
View File
@@ -770,7 +770,9 @@
"changes/2026-09-15-CI-01-adaptive-installed.md",
"baselines/ci-adaptive-installed-validation.json",
"changes/2026-09-15-CI-01-asr-cast-installed.md",
"baselines/ci-asr-cast-installed-validation.json"
"baselines/ci-asr-cast-installed-validation.json",
"changes/2026-09-15-CI-01-mediabunny-installed.md",
"baselines/ci-mediabunny-installed-validation.json"
]
},
{
+9
View File
@@ -97,3 +97,12 @@ controlled SDK lifecycle file with actual core DOM/local media; it does not run
a Cast receiver. VAST controlled tests externalize and replace a dependency, so
they are not advertised as installed bundle coverage. Its real IMA suite retains
its independent gate. See ../../refactor/changes/2026-09-15-CI-01-asr-cast-installed.md.
MediaBunny adds ten files using the installed proxy and its bundled SDK; native
Document PiP combinations also record the verified PiP package identity. The
runner rejects ARTPLAYER_MB_BROWSER_CANDIDATE=1 in installed mode, retaining
historical controls, and the loader rejects frozen-workspace/artifact overrides.
Available-input attachments list possible inputs; per-observation provenance
identifies the selected one. Native media/decoding, controlled races, API absence
and physical/long-run verification remain separate. See
../../refactor/changes/2026-09-15-CI-01-mediabunny-installed.md for actual results.
+12 -1
View File
@@ -2,7 +2,7 @@ import type { PlaywrightTestConfig } from '@playwright/test'
import assert from 'node:assert/strict'
export type BrowserScope = 'source' | 'installed'
export const installedPackages = ['artplayer', 'artplayer-plugin-chapter', 'artplayer-plugin-ambilight', 'artplayer-proxy-canvas', 'artplayer-plugin-document-pip', 'artplayer-plugin-ads', 'artplayer-plugin-audio-track', 'artplayer-plugin-vtt-thumbnail', 'artplayer-plugin-multiple-subtitles', 'artplayer-plugin-hls-control', 'artplayer-plugin-dash-control', 'artplayer-plugin-auto-thumbnail', 'artplayer-plugin-asr', 'artplayer-plugin-chromecast']
export const installedPackages = ['artplayer', 'artplayer-plugin-chapter', 'artplayer-plugin-ambilight', 'artplayer-proxy-canvas', 'artplayer-plugin-document-pip', 'artplayer-plugin-ads', 'artplayer-plugin-audio-track', 'artplayer-plugin-vtt-thumbnail', 'artplayer-plugin-multiple-subtitles', 'artplayer-plugin-hls-control', 'artplayer-plugin-dash-control', 'artplayer-plugin-auto-thumbnail', 'artplayer-plugin-asr', 'artplayer-plugin-chromecast', 'artplayer-proxy-mediabunny']
export const installedTests = [
'playback.spec.js',
'lifecycle.spec.js',
@@ -43,6 +43,16 @@ export const installedTests = [
'asr-capture-cors.spec.js',
'asr-audio-track.spec.js',
'chromecast-lifecycle.spec.js',
'mediabunny-audio.spec.js',
'mediabunny-capability.spec.js',
'mediabunny-dpip.spec.js',
'mediabunny-entry.spec.js',
'mediabunny-hls.spec.js',
'mediabunny-inputs.spec.js',
'mediabunny-load.spec.js',
'mediabunny-shim.spec.js',
'mediabunny-video.spec.js',
'mediabunny.spec.js',
]
export function browserInvocation(scope: string, args: string[], environment: NodeJS.ProcessEnv) {
@@ -54,6 +64,7 @@ export function browserInvocation(scope: string, args: string[], environment: No
}
else {
assert(env.ARTPLAYER_BROWSER_ARTIFACTS, 'Installed browser checks require ARTPLAYER_BROWSER_ARTIFACTS')
assert(env.ARTPLAYER_MB_BROWSER_CANDIDATE !== '1', 'Installed browser checks must retain MediaBunny historical controls')
assert(!env.ARTPLAYER_DASH_DIAGNOSTIC_SDK || env.ARTPLAYER_DASH_DIAGNOSTIC_SDK === 'none', 'Installed browser checks require the unchanged DASH SDK')
assert(!['ARTPLAYER_DASH_DIAGNOSE_STALL', 'ARTPLAYER_DASH_DIAGNOSE_GETTER', 'ARTPLAYER_DASH_DIAGNOSE_METRICS'].some(name => env[name] === '1'), 'Installed browser checks cannot use DASH recovery diagnostics')
}
+1
View File
@@ -95,6 +95,7 @@ export async function checkPackages({ release = false, include = [] } = {}) {
'artplayer-plugin-auto-thumbnail': async () => (await import('../refactor/scripts/auto-thumbnail-contract.mjs')).verifyAutoThumbnailContract(),
'artplayer-plugin-asr': async () => (await import('../refactor/scripts/asr-contract.mjs')).verifyAsrContract(),
'artplayer-plugin-chromecast': async () => (await import('../refactor/scripts/chromecast-contract.mjs')).verifyChromecastContract(),
'artplayer-proxy-mediabunny': async () => (await import('../refactor/scripts/mb-contract.mjs')).verifyMbContract(),
'artplayer-plugin-audio-track': async () => (await import('../refactor/scripts/audio-contract.mjs')).verifyAudioContract(),
'artplayer-plugin-vtt-thumbnail': async () => (await import('../refactor/scripts/vtt-thumbnail-contract.mjs')).verifyVttThumbnailContract(),
'artplayer-plugin-multiple-subtitles': async () => (await import('../refactor/scripts/multiple-subtitles-contract.mjs')).verifyMultipleSubtitlesContract(),
+3
View File
@@ -33,6 +33,9 @@ test('Browser invocation rejects ambiguous scopes and output/configuration overr
test('Installed validation rejects diagnostic SDK substitutions and recovery probes', () => {
const base = { ARTPLAYER_BROWSER_ARTIFACTS: '/installed/map.json' }
const candidateOnly = { ...base, ARTPLAYER_MB_BROWSER_CANDIDATE: '1' }
assert.throws(() => browserInvocation('installed', [], candidateOnly), /retain MediaBunny historical controls/)
assert.equal(browserInvocation('source', [], candidateOnly).env.ARTPLAYER_MB_BROWSER_CANDIDATE, '1')
for (const mode of ['upstream4', 'bufferlevel4']) {
const env = { ...base, ARTPLAYER_DASH_DIAGNOSTIC_SDK: mode }
assert.throws(() => browserInvocation('installed', [], env), /unchanged DASH SDK/)
+6 -1
View File
@@ -5,6 +5,11 @@ 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 hls = new Map(Object.entries(manifest.files).map(([name, expected]) => {
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url))
@@ -248,7 +253,7 @@ for (const scenario of ['playback', 'buffer-pause', 'buffer-rate', 'resume-pause
expect(await page.evaluate(() => window.mbCanvas.engine.audio.queuedNodes.size)).toBe(0)
}
const state = await page.evaluate(() => ({ context: window.mbContext?.state, nodes: window.mbNodes.map(record => ({ stops: record.stops, disconnects: record.disconnects, starts: record.starts })), error: window.mbCanvas.error, events: window.mbEvents }))
await testInfo.attach('mediabunny-audio', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), scenario, capabilities, outcome, result, state, scope: 'Native AudioContext, BufferSourceNodes and SDK decode. Delayed resume/buffer delivery is controlled. AV samples compare decoded canvas timestamps to the audio clock over short 1x/2x playback; acoustic output, long-run drift and physical devices remain MB-09.' }) })
await testInfo.attach('mediabunny-audio', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), scenario, capabilities, outcome, result, state, scope: 'Native AudioContext, BufferSourceNodes and SDK decode. Delayed resume/buffer delivery is controlled. AV samples compare decoded canvas timestamps to the audio clock over short 1x/2x playback; acoustic output, long-run drift and physical devices remain MB-09.' }) })
}
})
}
+6 -1
View File
@@ -5,6 +5,11 @@ 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 hls = new Map(Object.entries(manifest.files).map(([name, expected]) => {
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url))
@@ -111,7 +116,7 @@ for (const core of ['published', 'candidate']) {
}
finally {
await page.evaluate(() => window.art?.destroy(false))
await testInfo.attach('mediabunny-capability', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), core, scenario, capabilities, outcome, result, scope: 'Actual SDK parsing, Canvas, Web Audio and two cores. canDecode=false is deliberately controlled for selected tracks; partial cases play the remaining native decoder. Windows WebKit missing APIs is a separate control, not playback.' }) })
await testInfo.attach('mediabunny-capability', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), core, scenario, capabilities, outcome, result, scope: 'Actual SDK parsing, Canvas, Web Audio and two cores. canDecode=false is deliberately controlled for selected tracks; partial cases play the remaining native decoder. Windows WebKit missing APIs is a separate control, not playback.' }) })
}
})
}
+7 -2
View File
@@ -6,6 +6,11 @@ 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 dpip = await dpipCandidate()
const manifest = JSON.parse(fs.readFileSync(new URL('./media/hls/manifest.json', import.meta.url)))
const hls = new Map(Object.entries(manifest.files).map(([name, expected]) => {
@@ -25,7 +30,7 @@ for (const core of ['published', 'candidate']) {
await page.goto(`/test/player.html?core=${core}`)
const supported = await page.evaluate(() => typeof window.documentPictureInPicture?.requestWindow === 'function')
if (!supported) {
await testInfo.attach('mediabunny-dpip', { contentType: 'application/json', body: JSON.stringify({ core, media, ending, implementation: implementation.name, sha256: hash(implementation.code), dpipSha256: hash(dpip.code), outcome: 'native-document-pip-unavailable', browserName, supported }) })
await testInfo.attach('mediabunny-dpip', { contentType: 'application/json', body: JSON.stringify({ core, media, ending, implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), dpipSha256: hash(dpip.code), dpipProvenance: dpip.provenance || { kind: dpip.name, sha256: hash(dpip.code) }, outcome: 'native-document-pip-unavailable', browserName, supported }) })
return
}
for (const [code, global] of [[implementation.code, 'mbFactory'], [dpip.code, 'dpipFactory']]) {
@@ -98,7 +103,7 @@ for (const core of ['published', 'candidate']) {
}
result = await page.evaluate(() => ({ events: window.mbPipEvents, placeholders: document.querySelectorAll('.artplayer-document-pip-placeholder').length, nativeWindowClosed: !window.documentPictureInPicture.window || window.documentPictureInPicture.window.closed }))
expect(result).toEqual({ events: ending === 'destroy' ? [true] : [true, false], placeholders: 0, nativeWindowClosed: true })
await testInfo.attach('mediabunny-dpip', { contentType: 'application/json', body: JSON.stringify({ core, media, ending, implementation: implementation.name, sha256: hash(implementation.code), dpipSha256: hash(dpip.code), outcome: 'native-document-pip-canvas-playback', before, result, scope: 'Actual requestWindow from a click, no iframe or API replacement; native Canvas decoding and window lifecycle. HLS includes native audio, and close cases change quality/audio inside PiP.' }) })
await testInfo.attach('mediabunny-dpip', { contentType: 'application/json', body: JSON.stringify({ core, media, ending, implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), dpipSha256: hash(dpip.code), dpipProvenance: dpip.provenance || { kind: dpip.name, sha256: hash(dpip.code) }, outcome: 'native-document-pip-canvas-playback', before, result, scope: 'Actual requestWindow from a click, no iframe or API replacement; native Canvas decoding and window lifecycle. HLS includes native audio, and close cases change quality/audio inside PiP.' }) })
}
finally {
await page.evaluate(() => {
+6 -1
View File
@@ -3,6 +3,11 @@ 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 } }))) })
})
for (const core of ['published', 'candidate']) {
for (const scenario of ['native-cleanup', 'replacement']) {
test(`MediaBunny ${implementation.name}: ${core} core native Canvas entry ${scenario}`, async ({ page }, testInfo) => {
@@ -66,7 +71,7 @@ for (const core of ['published', 'candidate']) {
}, scenario)
expect(result.before).toEqual({ identity: true, context: true, width: 640, clickCount: 1, source: null, attribute: 'native-only.mp4', volume: 0.35, style: '100%', maybe: 'maybe' })
expect(result.cleanup).toEqual({ released: [true, true, true], destroyed: true, styleAfter: '43px', alias: true })
await testInfo.attach('mediabunny-entry', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), core, scenario, result, scope: 'Actual browser Canvas descriptors and published/candidate core lifecycle. Empty source deliberately excludes decoding; existing media suites cover playback separately.' }) })
await testInfo.attach('mediabunny-entry', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), core, scenario, result, scope: 'Actual browser Canvas descriptors and published/candidate core lifecycle. Empty source deliberately excludes decoding; existing media suites cover playback separately.' }) })
})
}
}
+6 -1
View File
@@ -5,6 +5,11 @@ 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))
@@ -196,7 +201,7 @@ for (const core of ['published', 'candidate']) {
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, 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. Not installed-package or long-run/device acceptance.' }) })
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.' }) })
}
})
}
+6 -1
View File
@@ -6,6 +6,11 @@ import { tracklessMp4 } from '../helpers/trackless-mp4.js'
import { expect, test } from './fixtures.js'
const implementations = await mbBrowserImplementations()
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('mediabunny-available-inputs', { contentType: 'application/json', body: JSON.stringify(implementations.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 hls = new Map(Object.entries(manifest.files).map(([name, expected]) => {
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url))
@@ -237,7 +242,7 @@ for (const implementation of implementations) {
await expect.poll(() => page.evaluate(() => window.mbContext?.state || 'not-created')).toMatch(/^(?:closed|not-created)$/)
expect(await page.evaluate(() => window.mbPendingFrames.size)).toBe(0)
const cleanup = await page.evaluate(() => ({ audioContext: window.mbContext?.state || 'not-created', pendingFrames: window.mbPendingFrames.size, streamCancelled: window.mbStreamCancelled || false }))
await testInfo.attach('mediabunny-native-input', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), input, capabilities, outcome, state, cleanup, hlsManifest: input === 'hls' ? manifest : null, tracklessFixture: input === 'trackless' ? { parentSha256: hash(pattern), sha256: hash(trackless) } : null, scope: 'Identified proxy with native browser media and controlled ArtPlayer host using actual core utilities/config; frame timestamps sampled against the SDK audio clock, not acoustic/long-run AV-sync acceptance. Cleanup checks settled playback, not pending-operation races. Candidate trackless rejection is checked separately before decoder setup; this is not full release acceptance.' }) })
await testInfo.attach('mediabunny-native-input', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), input, capabilities, outcome, state, cleanup, hlsManifest: input === 'hls' ? manifest : null, tracklessFixture: input === 'trackless' ? { parentSha256: hash(pattern), sha256: hash(trackless) } : null, scope: 'Identified proxy with native browser media and controlled ArtPlayer host using actual core utilities/config; frame timestamps sampled against the SDK audio clock, not acoustic/long-run AV-sync acceptance. Cleanup checks settled playback, not pending-operation races. Candidate trackless rejection is checked separately before decoder setup; this is not full release acceptance.' }) })
}
})
}
+6 -1
View File
@@ -3,6 +3,11 @@ 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 } }))) })
})
for (const ending of ['destroy', 'source', 'timeout']) {
test(`MediaBunny ${implementation.name}: native pending stream cancelled by ${ending}`, async ({ page }, testInfo) => {
await page.goto('/test/player.html?core=published')
@@ -57,7 +62,7 @@ for (const ending of ['destroy', 'source', 'timeout']) {
await expect.poll(() => page.evaluate(() => window.mbReplacement.locked)).toBe(true)
await page.evaluate(() => window.mbHost.emit('destroy'))
await expect.poll(() => page.evaluate(() => window.mbCancelled)).toEqual(ending === 'source' ? ['old', 'new'] : ['old'])
await testInfo.attach('mediabunny-load-cancellation', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), ending, state, cancellations: await page.evaluate(() => window.mbCancelled), scope: 'Real browser ReadableStream and actual SDK Input disposal before first media bytes; real timeout; controlled ArtPlayer host; does not assert decoder, late frame or audio-node cleanup.' }) })
await testInfo.attach('mediabunny-load-cancellation', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), ending, state, cancellations: await page.evaluate(() => window.mbCancelled), scope: 'Real browser ReadableStream and actual SDK Input disposal before first media bytes; real timeout; controlled ArtPlayer host; does not assert decoder, late frame or audio-node cleanup.' }) })
}
finally {
await page.evaluate(() => window.mbHost.emit('destroy'))
+6 -1
View File
@@ -3,6 +3,11 @@ 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 } }))) })
})
for (const scenario of ['callback', 'destroy', 'independent']) {
test(`MediaBunny ${implementation.name}: native synthetic RAF ${scenario}`, async ({ page }, testInfo) => {
await page.goto('/test/player.html?core=published')
@@ -44,6 +49,6 @@ for (const scenario of ['callback', 'destroy', 'independent']) {
expect(result.idType).toBe('number')
expect(result.listeners).toBe(0)
expect(result.calls).toEqual(scenario === 'callback' ? [{ owner: 'first', time: 'number', mediaTime: 0, presentedFrames: 0 }] : scenario === 'independent' ? [{ owner: 'second' }] : [])
await testInfo.attach('mediabunny-shim-frames', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), scenario, result, scope: 'Real browser requestAnimationFrame and actual proxy instances; synthetic metadata is preserved, no decoding or video frame accuracy is claimed.' }) })
await testInfo.attach('mediabunny-shim-frames', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), scenario, result, scope: 'Real browser requestAnimationFrame and actual proxy instances; synthetic metadata is preserved, no decoding or video frame accuracy is claimed.' }) })
})
}
+6 -1
View File
@@ -3,6 +3,11 @@ 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 } }))) })
})
for (const scenario of ['pause-frame', 'seek-source', 'seek-newer', 'timeupdate-pause', 'seek-pool']) {
test(`MediaBunny ${implementation.name}: native video ownership ${scenario}`, async ({ page, browserName }, testInfo) => {
await page.goto('/test/player.html?core=published')
@@ -212,7 +217,7 @@ for (const scenario of ['pause-frame', 'seek-source', 'seek-newer', 'timeupdate-
expect(state.frames).toBe(0)
await expect.poll(() => page.evaluate(() => window.mbCleanupContext?.state || 'not-created')).toMatch(/^(?:closed|not-created)$/)
state.audioContext = await page.evaluate(() => window.mbCleanupContext?.state || 'not-created')
await testInfo.attach('mediabunny-video-ownership', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), scenario, outcome, result, state, scope: 'Actual SDK MP4 decoding and browser clock/RAF; frame delivery or two raw VideoSample deliveries before CanvasSink conversion control interleaving. Source/seek ownership, pause and timeupdate reentry verified; not long-run AV sync or a mock codec.' }) })
await testInfo.attach('mediabunny-video-ownership', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), scenario, outcome, result, state, scope: 'Actual SDK MP4 decoding and browser clock/RAF; frame delivery or two raw VideoSample deliveries before CanvasSink conversion control interleaving. Source/seek ownership, pause and timeupdate reentry verified; not long-run AV sync or a mock codec.' }) })
}
})
}
+5 -1
View File
@@ -4,6 +4,10 @@ import { expect, test } from './fixtures.js'
const implementations = await mbBrowserImplementations()
test.beforeEach(async ({ browserName }, testInfo) => {
await testInfo.attach('mediabunny-available-inputs', { contentType: 'application/json', body: JSON.stringify(implementations.map(item => ({ browserName, name: item.name, sha256: hash(item.code), provenance: item.provenance || { kind: item.name } }))) })
})
for (const implementation of implementations) {
test(`MediaBunny ${implementation.name}: native MP4 playback or exact Windows WebKit capability failure`, async ({ page, browserName }, testInfo) => {
await page.goto('/test/player.html?core=published')
@@ -65,7 +69,7 @@ for (const implementation of implementations) {
const canvas = window.art.video
return { events: window.mbEvents, actions: window.mbActions, ready: window.art.isReady, dimensions: [canvas.width, canvas.height, canvas.videoWidth, canvas.videoHeight], paused: canvas.paused, currentTime: canvas.currentTime, error: canvas.error, readyState: canvas.readyState, audioState: canvas.engine?.audio?.audioContext?.state }
})
await testInfo.attach('mediabunny-input', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), capabilities, outcome, state, scope: 'Identified published or candidate proxy with actual core5.4.0; native MP4 pixels/clock or explicitly unsupported Windows WebKit APIs. No HLS or long-run AV-sync acceptance.' }) })
await testInfo.attach('mediabunny-input', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, provenance: implementation.provenance, sha256: hash(implementation.code), capabilities, outcome, state, scope: 'Identified published or candidate proxy with actual core5.4.0; native MP4 pixels/clock or explicitly unsupported Windows WebKit APIs. No HLS or long-run AV-sync acceptance.' }) })
await page.evaluate(() => window.art.destroy(false))
}
})
+7 -5
View File
@@ -1,16 +1,18 @@
import fs from 'node:fs'
import assert from 'node:assert/strict'
import process from 'node:process'
import vm from 'node:vm'
import { verifyMbContract } from '../../refactor/scripts/mb-contract.mjs'
import { readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from './load.js'
import { browserCandidate } from './browser-candidate.js'
export async function mbCandidate() {
if (process.env.ARTPLAYER_BROWSER_ARTIFACTS)
assert.notEqual(process.env.ARTPLAYER_MB_BASELINE, '1', 'Installed candidate cannot use the frozen workspace')
if (process.env.ARTPLAYER_MB_BASELINE === '1')
return (await mbHistorical()).find(item => item.name === 'frozen-workspace')
if (process.env.ARTPLAYER_MB_ARTIFACT)
return { name: 'candidate-artifact', code: fs.readFileSync(process.env.ARTPLAYER_MB_ARTIFACT, 'utf8') }
return { name: 'candidate-source', code: await compilePackage('artplayer-proxy-mediabunny', 'umd') }
const candidate = await browserCandidate('artplayer-proxy-mediabunny', process.env.ARTPLAYER_MB_ARTIFACT)
const suffix = { 'installed': 'installed', 'explicit-artifact': 'artifact', 'source-build': 'source' }[candidate.provenance.kind]
return { name: `candidate-${suffix}`, ...candidate }
}
export async function mbBrowserImplementations() {
+10
View File
@@ -13,6 +13,7 @@ import { autoThumbnailCandidate } from './helpers/auto-thumbnail.js'
import { browserCandidate } from './helpers/browser-candidate.js'
import { canvasCandidate } from './helpers/canvas.js'
import { dpipCandidate } from './helpers/dpip.js'
import { mbCandidate } from './helpers/mediabunny.js'
import { multipleSubtitlesCandidate } from './helpers/multiple-subtitles.js'
import { vttThumbnailCandidate } from './helpers/vtt-thumbnail.js'
@@ -32,6 +33,7 @@ test('Additional browser packages cannot be mistaken for full release consumer a
test('Explicit installed plugin maps never fall back to source or frozen workspace', async () => {
const keys = ['ARTPLAYER_BROWSER_ARTIFACTS', 'ARTPLAYER_AMBILIGHT_BASELINE', 'ARTPLAYER_CANVAS_BASELINE', 'ARTPLAYER_DPIP_BASELINE', 'ARTPLAYER_DPIP_ARTIFACT', 'ARTPLAYER_VTT_THUMBNAIL_BASELINE', 'ARTPLAYER_VTT_THUMBNAIL_ARTIFACT', 'ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT']
keys.push('ARTPLAYER_AUTO_THUMBNAIL_BASELINE', 'ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT')
keys.push('ARTPLAYER_MB_BASELINE', 'ARTPLAYER_MB_ARTIFACT')
const previous = Object.fromEntries(keys.map(key => [key, process.env[key]]))
try {
process.env.ARTPLAYER_BROWSER_ARTIFACTS = path.resolve('refactor/.cache/absent-installed-plugin-map.json')
@@ -45,6 +47,14 @@ test('Explicit installed plugin maps never fall back to source or frozen workspa
delete process.env.ARTPLAYER_MULTIPLE_SUBTITLES_ARTIFACT
delete process.env.ARTPLAYER_AUTO_THUMBNAIL_BASELINE
delete process.env.ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT
delete process.env.ARTPLAYER_MB_BASELINE
delete process.env.ARTPLAYER_MB_ARTIFACT
await assert.rejects(mbCandidate(), { code: 'ENOENT' })
process.env.ARTPLAYER_MB_BASELINE = '1'
await assert.rejects(mbCandidate(), /cannot use the frozen workspace/)
delete process.env.ARTPLAYER_MB_BASELINE
process.env.ARTPLAYER_MB_ARTIFACT = 'override.js'
await assert.rejects(mbCandidate(), /cannot override artplayer-proxy-mediabunny artifact/)
await assert.rejects(autoThumbnailCandidate(), { code: 'ENOENT' })
process.env.ARTPLAYER_AUTO_THUMBNAIL_BASELINE = '1'
await assert.rejects(autoThumbnailCandidate(), /cannot use the frozen workspace/)