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. 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, Windows WebKit without WebCodecs/Web Audio is recorded as a capability failure,
not successful playback or a claim about all Safari installations. 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-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-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) - 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) - 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) - 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) - 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 ## CI-01 统一安装包清单并接入 ASR / Chromecast
新增 --browser 从同一清单准备十四包,实际安装核验通过;消除 CI 手写列表并 新增 --browser 从同一清单准备十四包,实际安装核验通过;消除 CI 手写列表并
+3 -1
View File
@@ -770,7 +770,9 @@
"changes/2026-09-15-CI-01-adaptive-installed.md", "changes/2026-09-15-CI-01-adaptive-installed.md",
"baselines/ci-adaptive-installed-validation.json", "baselines/ci-adaptive-installed-validation.json",
"changes/2026-09-15-CI-01-asr-cast-installed.md", "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 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 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. 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' import assert from 'node:assert/strict'
export type BrowserScope = 'source' | 'installed' 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 = [ export const installedTests = [
'playback.spec.js', 'playback.spec.js',
'lifecycle.spec.js', 'lifecycle.spec.js',
@@ -43,6 +43,16 @@ export const installedTests = [
'asr-capture-cors.spec.js', 'asr-capture-cors.spec.js',
'asr-audio-track.spec.js', 'asr-audio-track.spec.js',
'chromecast-lifecycle.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) { export function browserInvocation(scope: string, args: string[], environment: NodeJS.ProcessEnv) {
@@ -54,6 +64,7 @@ export function browserInvocation(scope: string, args: string[], environment: No
} }
else { else {
assert(env.ARTPLAYER_BROWSER_ARTIFACTS, 'Installed browser checks require ARTPLAYER_BROWSER_ARTIFACTS') 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(!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') 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-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-asr': async () => (await import('../refactor/scripts/asr-contract.mjs')).verifyAsrContract(),
'artplayer-plugin-chromecast': async () => (await import('../refactor/scripts/chromecast-contract.mjs')).verifyChromecastContract(), '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-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-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(), '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', () => { test('Installed validation rejects diagnostic SDK substitutions and recovery probes', () => {
const base = { ARTPLAYER_BROWSER_ARTIFACTS: '/installed/map.json' } 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']) { for (const mode of ['upstream4', 'bufferlevel4']) {
const env = { ...base, ARTPLAYER_DASH_DIAGNOSTIC_SDK: mode } const env = { ...base, ARTPLAYER_DASH_DIAGNOSTIC_SDK: mode }
assert.throws(() => browserInvocation('installed', [], env), /unchanged DASH SDK/) 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' import { expect, test } from './fixtures.js'
const implementation = await mbCandidate() 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 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 hls = new Map(Object.entries(manifest.files).map(([name, expected]) => {
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url)) 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) 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 })) 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' import { expect, test } from './fixtures.js'
const implementation = await mbCandidate() 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 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 hls = new Map(Object.entries(manifest.files).map(([name, expected]) => {
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url)) const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url))
@@ -111,7 +116,7 @@ for (const core of ['published', 'candidate']) {
} }
finally { finally {
await page.evaluate(() => window.art?.destroy(false)) 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' import { expect, test } from './fixtures.js'
const implementation = await mbCandidate() 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 dpip = await dpipCandidate()
const manifest = JSON.parse(fs.readFileSync(new URL('./media/hls/manifest.json', import.meta.url))) 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 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}`) await page.goto(`/test/player.html?core=${core}`)
const supported = await page.evaluate(() => typeof window.documentPictureInPicture?.requestWindow === 'function') const supported = await page.evaluate(() => typeof window.documentPictureInPicture?.requestWindow === 'function')
if (!supported) { 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 return
} }
for (const [code, global] of [[implementation.code, 'mbFactory'], [dpip.code, 'dpipFactory']]) { 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 })) 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 }) 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 { finally {
await page.evaluate(() => { await page.evaluate(() => {
+6 -1
View File
@@ -3,6 +3,11 @@ import { mbCandidate } from '../helpers/mediabunny.js'
import { expect, test } from './fixtures.js' import { expect, test } from './fixtures.js'
const implementation = await mbCandidate() 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 core of ['published', 'candidate']) {
for (const scenario of ['native-cleanup', 'replacement']) { for (const scenario of ['native-cleanup', 'replacement']) {
test(`MediaBunny ${implementation.name}: ${core} core native Canvas entry ${scenario}`, async ({ page }, testInfo) => { test(`MediaBunny ${implementation.name}: ${core} core native Canvas entry ${scenario}`, async ({ page }, testInfo) => {
@@ -66,7 +71,7 @@ for (const core of ['published', 'candidate']) {
}, scenario) }, 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.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 }) 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' import { expect, test } from './fixtures.js'
const implementation = await mbCandidate() 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 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 media = new Map(Object.entries(manifest.files).map(([name, expected]) => {
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url)) 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 } 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 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' import { expect, test } from './fixtures.js'
const implementations = await mbBrowserImplementations() 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 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 hls = new Map(Object.entries(manifest.files).map(([name, expected]) => {
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url)) 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)$/) await expect.poll(() => page.evaluate(() => window.mbContext?.state || 'not-created')).toMatch(/^(?:closed|not-created)$/)
expect(await page.evaluate(() => window.mbPendingFrames.size)).toBe(0) 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 })) 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' import { expect, test } from './fixtures.js'
const implementation = await mbCandidate() 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']) { for (const ending of ['destroy', 'source', 'timeout']) {
test(`MediaBunny ${implementation.name}: native pending stream cancelled by ${ending}`, async ({ page }, testInfo) => { test(`MediaBunny ${implementation.name}: native pending stream cancelled by ${ending}`, async ({ page }, testInfo) => {
await page.goto('/test/player.html?core=published') 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 expect.poll(() => page.evaluate(() => window.mbReplacement.locked)).toBe(true)
await page.evaluate(() => window.mbHost.emit('destroy')) await page.evaluate(() => window.mbHost.emit('destroy'))
await expect.poll(() => page.evaluate(() => window.mbCancelled)).toEqual(ending === 'source' ? ['old', 'new'] : ['old']) 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 { finally {
await page.evaluate(() => window.mbHost.emit('destroy')) 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' import { expect, test } from './fixtures.js'
const implementation = await mbCandidate() 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']) { for (const scenario of ['callback', 'destroy', 'independent']) {
test(`MediaBunny ${implementation.name}: native synthetic RAF ${scenario}`, async ({ page }, testInfo) => { test(`MediaBunny ${implementation.name}: native synthetic RAF ${scenario}`, async ({ page }, testInfo) => {
await page.goto('/test/player.html?core=published') 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.idType).toBe('number')
expect(result.listeners).toBe(0) expect(result.listeners).toBe(0)
expect(result.calls).toEqual(scenario === 'callback' ? [{ owner: 'first', time: 'number', mediaTime: 0, presentedFrames: 0 }] : scenario === 'independent' ? [{ owner: 'second' }] : []) 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' import { expect, test } from './fixtures.js'
const implementation = await mbCandidate() 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']) { 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) => { test(`MediaBunny ${implementation.name}: native video ownership ${scenario}`, async ({ page, browserName }, testInfo) => {
await page.goto('/test/player.html?core=published') 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) expect(state.frames).toBe(0)
await expect.poll(() => page.evaluate(() => window.mbCleanupContext?.state || 'not-created')).toMatch(/^(?:closed|not-created)$/) await expect.poll(() => page.evaluate(() => window.mbCleanupContext?.state || 'not-created')).toMatch(/^(?:closed|not-created)$/)
state.audioContext = await page.evaluate(() => window.mbCleanupContext?.state || '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() 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) { for (const implementation of implementations) {
test(`MediaBunny ${implementation.name}: native MP4 playback or exact Windows WebKit capability failure`, async ({ page, browserName }, testInfo) => { 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') await page.goto('/test/player.html?core=published')
@@ -65,7 +69,7 @@ for (const implementation of implementations) {
const canvas = window.art.video 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 } 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)) 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 process from 'node:process'
import vm from 'node:vm' import vm from 'node:vm'
import { verifyMbContract } from '../../refactor/scripts/mb-contract.mjs' import { verifyMbContract } from '../../refactor/scripts/mb-contract.mjs'
import { readMember } from '../../refactor/scripts/releases.mjs' import { readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from './load.js' import { browserCandidate } from './browser-candidate.js'
export async function mbCandidate() { 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') if (process.env.ARTPLAYER_MB_BASELINE === '1')
return (await mbHistorical()).find(item => item.name === 'frozen-workspace') return (await mbHistorical()).find(item => item.name === 'frozen-workspace')
if (process.env.ARTPLAYER_MB_ARTIFACT) const candidate = await browserCandidate('artplayer-proxy-mediabunny', process.env.ARTPLAYER_MB_ARTIFACT)
return { name: 'candidate-artifact', code: fs.readFileSync(process.env.ARTPLAYER_MB_ARTIFACT, 'utf8') } const suffix = { 'installed': 'installed', 'explicit-artifact': 'artifact', 'source-build': 'source' }[candidate.provenance.kind]
return { name: 'candidate-source', code: await compilePackage('artplayer-proxy-mediabunny', 'umd') } return { name: `candidate-${suffix}`, ...candidate }
} }
export async function mbBrowserImplementations() { 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 { browserCandidate } from './helpers/browser-candidate.js'
import { canvasCandidate } from './helpers/canvas.js' import { canvasCandidate } from './helpers/canvas.js'
import { dpipCandidate } from './helpers/dpip.js' import { dpipCandidate } from './helpers/dpip.js'
import { mbCandidate } from './helpers/mediabunny.js'
import { multipleSubtitlesCandidate } from './helpers/multiple-subtitles.js' import { multipleSubtitlesCandidate } from './helpers/multiple-subtitles.js'
import { vttThumbnailCandidate } from './helpers/vtt-thumbnail.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 () => { 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'] 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_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]])) const previous = Object.fromEntries(keys.map(key => [key, process.env[key]]))
try { try {
process.env.ARTPLAYER_BROWSER_ARTIFACTS = path.resolve('refactor/.cache/absent-installed-plugin-map.json') 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_MULTIPLE_SUBTITLES_ARTIFACT
delete process.env.ARTPLAYER_AUTO_THUMBNAIL_BASELINE delete process.env.ARTPLAYER_AUTO_THUMBNAIL_BASELINE
delete process.env.ARTPLAYER_AUTO_THUMBNAIL_ARTIFACT 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' }) await assert.rejects(autoThumbnailCandidate(), { code: 'ENOENT' })
process.env.ARTPLAYER_AUTO_THUMBNAIL_BASELINE = '1' process.env.ARTPLAYER_AUTO_THUMBNAIL_BASELINE = '1'
await assert.rejects(autoThumbnailCandidate(), /cannot use the frozen workspace/) await assert.rejects(autoThumbnailCandidate(), /cannot use the frozen workspace/)