mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-26 17:01:20 -08:00
test(ci): [CI-01] verify installed MediaBunny browser matrix
This commit is contained in:
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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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/)
|
||||||
|
|||||||
@@ -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.' }) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -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.' }) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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.' }) })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,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.' }) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'))
|
||||||
|
|||||||
@@ -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.' }) })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -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.' }) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -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))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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/)
|
||||||
|
|||||||
Reference in new issue
Block a user