mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
test(mediabunny): [PKG-MB-09] start hour-scale installed playback checks
This commit is contained in:
1 parent
5795492f0f
commit
d71b6fea38
11 files changed
+301
-13
No files matched your search
@@ -1,10 +1,12 @@
|
||||
import process from 'node:process'
|
||||
import { defineConfig } from '@playwright/test'
|
||||
import base from './playwright.config.js'
|
||||
import { soakOptions } from './test/helpers/soak-options.js'
|
||||
|
||||
export default defineConfig({
|
||||
...base,
|
||||
testDir: './test/soak',
|
||||
timeout: 270000,
|
||||
timeout: soakOptions(process.env.ARTPLAYER_MB_SOAK_SECONDS).timeoutMs,
|
||||
workers: 2,
|
||||
outputDir: 'refactor/.cache/soak/results',
|
||||
reporter: [
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "PKG-MB-09",
|
||||
"startedFrom": "5795492f0f6eff9f987b43e2f51643f0131f659d",
|
||||
"capturedAt": "2026-09-16T03:20:08.923Z",
|
||||
"status": "running-at-capture",
|
||||
"completionProven": false,
|
||||
"node": "v24.21.0",
|
||||
"yarn": "1.22.22",
|
||||
"sourceSha256LF": {
|
||||
"playwright.soak.config.js": "5be535955745716e94353c8d21458b18301bf612f15c3f78d931f4d875d44c69",
|
||||
"scripts/generate-hls-fixture.mjs": "70468ef1daa00726a985a0fcbd365f43e3ee741f883307cbf63ce24316084dad",
|
||||
"test/helpers/soak-options.js": "bcb37f317cf34a53a0070715f625cbbc88969bd6914219a95db689e543574995",
|
||||
"test/soak/mediabunny.spec.js": "f435cf9d04f58ed6786428d72745cb3545f595d0313fefac25d1ad671211d826",
|
||||
"refactor/scripts/soak-options.test.mjs": "8aa25d5308376b807621f3b3914181a7ad2535e30622f3c8773b796582ea52d5"
|
||||
},
|
||||
"prechecks": {
|
||||
"optionsTests": {
|
||||
"pass": 3,
|
||||
"fail": 0
|
||||
},
|
||||
"lintExit": 0,
|
||||
"installedListExit": 0,
|
||||
"defaultMedia": {
|
||||
"files": 32,
|
||||
"completeManifestEqual": true
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"path": "refactor/.cache/mb09-hour-hls/manifest.json",
|
||||
"sha256": "ed988797a4e8c9ccd848b67ea9be85ece0fe1df1b276e88daa80a0c49f6c126a",
|
||||
"duration": 11000,
|
||||
"files": 22008,
|
||||
"bytes": 598523989,
|
||||
"ffmpeg": "ffmpeg version 2025-11-12-git-6cdd2cbe32-full_build-www.gyan.dev Copyright (c) 2000-2025 the FFmpeg developers"
|
||||
},
|
||||
"command": "yarn test:mediabunny-soak --project chromium --project firefox",
|
||||
"environment": {
|
||||
"ARTPLAYER_MB_SOAK_SECONDS": "3600",
|
||||
"ARTPLAYER_MB_SOAK_MEDIA": "refactor/.cache/mb09-hour-hls",
|
||||
"ARTPLAYER_BROWSER_ARTIFACTS": "refactor/.cache/packages/run-kK0VCe/browser-artifacts.json"
|
||||
},
|
||||
"logs": [
|
||||
{
|
||||
"path": "refactor/.cache/mb09-hour-options.log",
|
||||
"sha256": "5d00f6c63329e399579b084fa9898fd3959249a09cbc16533fed0d6ded04ebeb"
|
||||
},
|
||||
{
|
||||
"path": "refactor/.cache/mb09-hour-lint.log",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"path": "refactor/.cache/mb09-hour-installed-list.log",
|
||||
"sha256": "5db98207480cc15e840e3c74dbe444a4c35e86048f7a8ade061b5e9fa274ecb5"
|
||||
},
|
||||
{
|
||||
"path": "refactor/.cache/mb09-hour-generate.log",
|
||||
"sha256": "7a490ee15ad659fd6303b70d5ace5767a131a7b508fa31a1c6b8ea0cdbd9772a"
|
||||
},
|
||||
{
|
||||
"path": "refactor/.cache/mb09-hour-default-generate.log",
|
||||
"sha256": "276a5b50b4f169a525ea0ad3fb1fded298c07589d34d0017ad97c3b381820f19"
|
||||
}
|
||||
],
|
||||
"liveLog": "refactor/.cache/mb09-hour-browser.log",
|
||||
"samples": [
|
||||
{
|
||||
"path": "refactor/.cache/soak/results/mediabunny-MediaBunny-sust-71bde-layback-with-published-core-chromium/progress.jsonl",
|
||||
"started": {
|
||||
"kind": "started",
|
||||
"project": "chromium",
|
||||
"core": "published",
|
||||
"implementation": "candidate-installed",
|
||||
"sha256": "662598a27e1757692ce4bd560c376bc96520d172ed67427ec519bd269896fa92",
|
||||
"manifestSha256": "ed988797a4e8c9ccd848b67ea9be85ece0fe1df1b276e88daa80a0c49f6c126a",
|
||||
"mediaDuration": 11000,
|
||||
"requestedPhaseSeconds": 3600,
|
||||
"capabilities": {
|
||||
"audio": "function",
|
||||
"videoDecoder": "function",
|
||||
"audioDecoder": "function"
|
||||
},
|
||||
"outcome": "incomplete",
|
||||
"phases": [],
|
||||
"checkpoints": []
|
||||
},
|
||||
"latest": {
|
||||
"kind": "sample",
|
||||
"phase": "continuous-1x",
|
||||
"wall": 55981.09999990463,
|
||||
"time": 56.24,
|
||||
"rate": 1,
|
||||
"active": 50,
|
||||
"queued": 50,
|
||||
"iterators": 2,
|
||||
"draws": 1326,
|
||||
"visibility": "visible",
|
||||
"paused": false,
|
||||
"error": null,
|
||||
"maxAVError": 0.02500000000000213
|
||||
},
|
||||
"observations": 11
|
||||
},
|
||||
{
|
||||
"path": "refactor/.cache/soak/results/mediabunny-MediaBunny-sust-828df-layback-with-candidate-core-chromium/progress.jsonl",
|
||||
"started": {
|
||||
"kind": "started",
|
||||
"project": "chromium",
|
||||
"core": "candidate",
|
||||
"implementation": "candidate-installed",
|
||||
"sha256": "662598a27e1757692ce4bd560c376bc96520d172ed67427ec519bd269896fa92",
|
||||
"manifestSha256": "ed988797a4e8c9ccd848b67ea9be85ece0fe1df1b276e88daa80a0c49f6c126a",
|
||||
"mediaDuration": 11000,
|
||||
"requestedPhaseSeconds": 3600,
|
||||
"capabilities": {
|
||||
"audio": "function",
|
||||
"videoDecoder": "function",
|
||||
"audioDecoder": "function"
|
||||
},
|
||||
"outcome": "incomplete",
|
||||
"phases": [],
|
||||
"checkpoints": []
|
||||
},
|
||||
"latest": {
|
||||
"kind": "sample",
|
||||
"phase": "continuous-1x",
|
||||
"wall": 55933.39999997616,
|
||||
"time": 56.18933333333334,
|
||||
"rate": 1,
|
||||
"active": 53,
|
||||
"queued": 53,
|
||||
"iterators": 2,
|
||||
"draws": 1325,
|
||||
"visibility": "visible",
|
||||
"paused": false,
|
||||
"error": null,
|
||||
"maxAVError": 0.02500000000000213
|
||||
},
|
||||
"observations": 11
|
||||
}
|
||||
],
|
||||
"limitations": [
|
||||
"This start snapshot is not a terminal browser result. Recheck the live process and report before assessing completion.",
|
||||
"Fixed installed runtime bytes are tested; shared-tool changes invalidated their original release input fingerprint.",
|
||||
"No physical-device, acoustic, background or whole-heap proof."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
# PKG-MB-09:数小时播放验收入口
|
||||
|
||||
任务保持 doing。本检查点扩展现有持续播放验收;不以入口实现替代长测结果。
|
||||
当前核心/MediaBunny 公开接口、运行时代码及发布产物不变。
|
||||
|
||||
## 执行与维护
|
||||
|
||||
`ARTPLAYER_MB_SOAK_SECONDS` 指定每个连续阶段的实际墙钟秒数,默认仍为 90,
|
||||
仅允许 90..3600 整数。3600 模式执行连续一小时 1×、一小时 2×,随后保留 seek、
|
||||
质量/音轨切换和 destroy 清理。两段连续阶段之间不 seek、循环或修改媒体时钟。
|
||||
每项超时是两段目标墙钟加原有 90 秒操作余量;默认仍是 270 秒,未放宽原有断言。
|
||||
媒体必须至少覆盖 `max(600, 3 * phaseSeconds + 60)` 秒;不足在启动浏览器前失败。
|
||||
|
||||
生成器最大素材时长由 3600 扩展到 14400 秒,仍禁止覆盖既有目录。默认 12 秒素材
|
||||
已重新生成,32 个文件、FFmpeg 参数和哈希全部与原冻结 manifest 相同。
|
||||
|
||||
```powershell
|
||||
yarn generate:media:hls refactor/.cache/mb09-hour-hls 11000
|
||||
$env:ARTPLAYER_MB_SOAK_SECONDS = '3600'
|
||||
$env:ARTPLAYER_MB_SOAK_MEDIA = (Resolve-Path refactor/.cache/mb09-hour-hls).Path
|
||||
$env:ARTPLAYER_BROWSER_ARTIFACTS = (Resolve-Path refactor/.cache/packages/run-kK0VCe/browser-artifacts.json).Path
|
||||
Remove-Item Env:ARTPLAYER_MB_ARTIFACT -ErrorAction SilentlyContinue
|
||||
yarn test:mediabunny-soak --project chromium --project firefox
|
||||
```
|
||||
|
||||
该安装映射对应已登记的 core 6.0.0 / MediaBunny 2.0.0 文件,实际来源由既有校验器
|
||||
及浏览器附件记录。共享工程变动后旧候选输入指纹失效,使用固定运行时字节测试不表示
|
||||
通过发布准入;未来运行必须明确选择当时的安装映射。
|
||||
|
||||
修复原入口对 `ARTPLAYER_MB_ARTIFACT` 的无条件要求:现在允许显式构建文件或经过
|
||||
验证的安装映射,仍禁止隐式源码编译,同时指定覆盖值仍由现有 helper 拒绝。
|
||||
长素材按需读取,只常驻文件索引;初始化核对全部哈希,每次响应前再次核对分片,
|
||||
不再把整套媒体字节复制到每个 worker 的常驻 Map。
|
||||
|
||||
每 5 秒另写逐用例 `progress.jsonl`,记录媒体/墙钟、绘制、节点/迭代器、可见性与
|
||||
累计最大 AV 偏差;进程中断时也保留采样。原有最终附件、AV 250ms 阈值、活动节点
|
||||
上限、推进速度和清理检查不变。时长参数位于 `test/helpers/soak-options.js`,
|
||||
由 Playwright 配置和用例共享,避免超时与媒体长度计算漂移。
|
||||
|
||||
## 当前验证与边界
|
||||
|
||||
- Node 24.21.0 / Yarn 1.22.22;FFmpeg 与原冻结工具版本相同。
|
||||
- 参数与启动拒绝测试 3/3:默认时长/超时保持、边界拒绝、缺少显式产物和短素材拒绝。
|
||||
- 变更文件只读 lint 通过;安装映射模式 `--list` 成功发现六个原有浏览器/核心用例。
|
||||
- 默认素材重新生成后的完整 manifest 深比较通过,32 文件及命令没有改变。
|
||||
- 11,000 秒素材生成完成,22,008 文件 / 598,523,989 字节,manifest 摘要和工具信息
|
||||
见[启动证据](../baselines/mb-hour-soak-start.json)。开始采样时 Chromium 新旧核心都在
|
||||
1× 实际推进,无媒体错误、活动节点分别 50/53、AV 最大偏差约 25ms。
|
||||
这些是约 55 秒时的中途状态,不是数小时通过结论。
|
||||
|
||||
Chromium/Firefox 新旧核心四项以两个 worker 执行,理想执行约四小时加启动/切换清理。
|
||||
不要同时启动占用 8084 的普通浏览器测试。运行前归档旧 `refactor/.cache/soak`;
|
||||
运行中依据实际进程句柄与 progress 文件观察,不把未完成报告或锁文件当成功。
|
||||
失败时保留日志和采样,不提高阈值或重试直到通过。
|
||||
|
||||
即使通过,也不证明声学输出同步、完整堆/GPU无泄漏、后台节流、物理设备或全部插件组合。
|
||||
WebKit 缺失原生 WebCodecs 的对照不算播放;MB-09 与相关风险继续保留未完成项。
|
||||
@@ -1,5 +1,9 @@
|
||||
# MediaBunny proxy 验证入口
|
||||
|
||||
数小时专项入口见[2026-09-16 检查点](changes/2026-09-16-PKG-MB-09-hour-soak.md)。
|
||||
它保留原断言和默认时长,支持已安装候选映射、按需分片读取及持久化采样;长测未终结
|
||||
前不能据此宣称通过。任务仍为 PKG-MB-09 doing。
|
||||
|
||||
开始改动前读取[冻结契约](baselines/mb-contract.md)、[接口快照](baselines/mb-surface.json)、
|
||||
[归档/源码哈希](baselines/mb-release.json)。MB-01接口基线与
|
||||
[MB-02行为基线](changes/2026-09-12-PKG-MB-02-baseline.md)分开记录;不得把接口测试称为真实媒体验收。
|
||||
|
||||
+1
-1
@@ -669,7 +669,7 @@
|
||||
- PKG-MB-06: [记录](baselines/mb-audio-source.json) [记录](changes/2026-09-12-PKG-MB-06-audio.md) [记录](baselines/mb-audio-validation.json)
|
||||
- PKG-MB-07: [记录](baselines/mb-hls-source.json) [记录](changes/2026-09-12-PKG-MB-07-hls.md) [记录](baselines/mb-hls-validation.json)
|
||||
- PKG-MB-08: [记录](baselines/mb-entry-source.json) [记录](changes/2026-09-12-PKG-MB-08-entry-checkpoint.md) [记录](baselines/mb-entry-checkpoint.json) [记录](baselines/mb-capability-source.json) [记录](changes/2026-09-12-PKG-MB-08-capability.md) [记录](baselines/mb-capability-validation.json)
|
||||
- PKG-MB-09: [记录](changes/2026-09-12-PKG-MB-09-native-pip-checkpoint.md) [记录](baselines/mb-native-pip-checkpoint.json) [记录](changes/2026-09-12-PKG-MB-09-sustained-playback-checkpoint.md) [记录](baselines/mb-sustained-validation.json) [记录](baselines/mb-sustained-media.json)
|
||||
- PKG-MB-09: [记录](changes/2026-09-12-PKG-MB-09-native-pip-checkpoint.md) [记录](baselines/mb-native-pip-checkpoint.json) [记录](changes/2026-09-12-PKG-MB-09-sustained-playback-checkpoint.md) [记录](baselines/mb-sustained-validation.json) [记录](baselines/mb-sustained-media.json) [记录](changes/2026-09-16-PKG-MB-09-hour-soak.md) [记录](baselines/mb-hour-soak-start.json)
|
||||
- PKG-IFRAME-01: [记录](changes/2026-09-12-PKG-IFRAME-01-contract.md) [记录](baselines/iframe-release.json) [记录](baselines/iframe-contract.md) [记录](baselines/iframe-contract-validation.json)
|
||||
- PKG-IFRAME-02: [记录](changes/2026-09-12-PKG-IFRAME-02-behavior.md) [记录](baselines/iframe-behavior-validation.json)
|
||||
- PKG-IFRAME-03: [记录](changes/2026-09-12-PKG-IFRAME-03-requests-checkpoint.md) [记录](baselines/iframe-requests-checkpoint.json) [记录](changes/2026-09-12-PKG-IFRAME-03-boundaries-checkpoint.md) [记录](baselines/iframe-boundaries-checkpoint.json) [记录](iframe-message-boundary.md) [记录](changes/2026-09-13-PKG-IFRAME-03-navigation.md) [记录](baselines/iframe-navigation-validation.json) [记录](iframe-document-protocol.md)
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-MB-09 数小时播放运行检查点
|
||||
|
||||
扩展现有长测为每段90..3600秒,默认时长/超时和断言不变;支持已安装候选映射,
|
||||
媒体改为按需读取且每次校验哈希,每5秒持久化进度。3项前置测试、定向lint、
|
||||
安装映射列举通过,默认12秒素材32文件/参数/哈希完全不变。
|
||||
11000秒素材生成完成,Chromium/Firefox新旧核心四组已启动、两个worker;
|
||||
[启动快照](baselines/mb-hour-soak-start.json)记录首批Chromium约55秒的正常推进,
|
||||
不是最终结果。用实际会话句柄和 `refactor/.cache/soak/results/*/progress.jsonl`
|
||||
检查持续状态,终结后保留完整报告;不要重复启动或让其他浏览器测试占用8084。
|
||||
[维护与限制](changes/2026-09-16-PKG-MB-09-hour-soak.md)。MB-09仍doing,计数不变。
|
||||
当前没有正式复盘、推送或发布。
|
||||
|
||||
## CI-NPM-03 只读 registry 与部分发布恢复判断
|
||||
|
||||
新增 `yarn release:registry`,在真实候选验证前后夹住只读 registry 查询,区分缺失、
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Soak validation runs without launching hours of browsers.
|
||||
import test from 'node:test'
|
||||
import { soakOptions } from '../../test/helpers/soak-options.js'
|
||||
|
||||
test('Default soak preserves the old duration and deadline; hour phases require enough uninterrupted media', () => {
|
||||
assert.deepEqual(soakOptions(undefined), { phaseSeconds: 90, minimumMediaSeconds: 600, timeoutMs: 270000 })
|
||||
const long = soakOptions('3600')
|
||||
assert.equal(long.phaseSeconds, 3600)
|
||||
assert.equal(long.minimumMediaSeconds, 10860)
|
||||
assert.equal(long.timeoutMs, 7290000)
|
||||
for (const value of ['', '0', '89', '90.5', '3601', 'Infinity', 'NaN', 'abc'])
|
||||
assert.throws(() => soakOptions(value), /whole seconds/)
|
||||
})
|
||||
|
||||
test('Missing explicit artifacts fail before browser work', () => {
|
||||
const env = { ...process.env, ARTPLAYER_MB_SOAK_SECONDS: '3600' }
|
||||
delete env.ARTPLAYER_MB_ARTIFACT
|
||||
delete env.ARTPLAYER_BROWSER_ARTIFACTS
|
||||
delete env.ARTPLAYER_MB_BASELINE
|
||||
const result = spawnSync(process.execPath, ['test/soak/mediabunny.spec.js'], { cwd: process.cwd(), env, encoding: 'utf8' })
|
||||
assert.notEqual(result.status, 0)
|
||||
assert.match(result.stderr, /explicit built artifact or verified installed artifact map/)
|
||||
})
|
||||
|
||||
test('Hour phases reject the existing ten-minute fixture before launching a browser', (t) => {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'artplayer-soak-options-'))
|
||||
t.after(() => {
|
||||
const resolved = fs.realpathSync(directory)
|
||||
assert.equal(path.dirname(resolved), fs.realpathSync(os.tmpdir()))
|
||||
assert(path.basename(resolved).startsWith('artplayer-soak-options-'))
|
||||
fs.rmSync(resolved, { recursive: true })
|
||||
})
|
||||
const artifact = path.join(directory, 'artifact.js')
|
||||
fs.writeFileSync(artifact, '// Not executed: media validation must reject first.\n')
|
||||
fs.writeFileSync(path.join(directory, 'manifest.json'), JSON.stringify({ duration: 600, files: {} }))
|
||||
const env = { ...process.env, ARTPLAYER_MB_SOAK_SECONDS: '3600', ARTPLAYER_MB_SOAK_MEDIA: directory, ARTPLAYER_MB_ARTIFACT: artifact }
|
||||
delete env.ARTPLAYER_BROWSER_ARTIFACTS
|
||||
delete env.ARTPLAYER_MB_BASELINE
|
||||
const result = spawnSync(process.execPath, ['test/soak/mediabunny.spec.js'], { cwd: process.cwd(), env, encoding: 'utf8' })
|
||||
assert.notEqual(result.status, 0)
|
||||
assert.match(result.stderr, /at least 10860 seconds/)
|
||||
})
|
||||
+3
-1
@@ -4021,7 +4021,9 @@
|
||||
"baselines/mb-native-pip-checkpoint.json",
|
||||
"changes/2026-09-12-PKG-MB-09-sustained-playback-checkpoint.md",
|
||||
"baselines/mb-sustained-validation.json",
|
||||
"baselines/mb-sustained-media.json"
|
||||
"baselines/mb-sustained-media.json",
|
||||
"changes/2026-09-16-PKG-MB-09-hour-soak.md",
|
||||
"baselines/mb-hour-soak-start.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ import { hash } from '../refactor/scripts/releases.mjs'
|
||||
const root = fileURLToPath(new URL('../', import.meta.url))
|
||||
const output = path.resolve(root, process.argv[2] || 'test/browser/media/hls')
|
||||
const duration = Number(process.argv[3] || 12)
|
||||
assert(Number.isInteger(duration) && duration >= 12 && duration <= 3600, 'Duration must be 12..3600 whole seconds')
|
||||
assert(Number.isInteger(duration) && duration >= 12 && duration <= 14400, 'Duration must be 12..14400 whole seconds')
|
||||
assert(!fs.existsSync(output), 'Output must be a new directory; never overwrite frozen media')
|
||||
const ffmpeg = process.env.ARTPLAYER_FFMPEG || 'ffmpeg'
|
||||
const version = execFileSync(ffmpeg, ['-version'], { encoding: 'utf8' }).split(/\r?\n/)[0]
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
export function soakOptions(value) {
|
||||
const phaseSeconds = value === undefined ? 90 : Number(value)
|
||||
assert(Number.isInteger(phaseSeconds) && phaseSeconds >= 90 && phaseSeconds <= 3600, 'ARTPLAYER_MB_SOAK_SECONDS must be 90..3600 whole seconds per phase')
|
||||
return { phaseSeconds, minimumMediaSeconds: Math.max(600, phaseSeconds * 3 + 60), timeoutMs: phaseSeconds * 2000 + 90000 }
|
||||
}
|
||||
@@ -5,31 +5,39 @@ import process from 'node:process'
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { expect, test } from '../browser/fixtures.js'
|
||||
import { mbCandidate } from '../helpers/mediabunny.js'
|
||||
import { soakOptions } from '../helpers/soak-options.js'
|
||||
|
||||
assert(process.env.ARTPLAYER_MB_ARTIFACT || process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'Soak requires an explicit built artifact or verified installed artifact map')
|
||||
const implementation = await mbCandidate()
|
||||
assert(process.env.ARTPLAYER_MB_ARTIFACT, 'Soak requires an explicit built MediaBunny artifact')
|
||||
const { phaseSeconds, minimumMediaSeconds } = soakOptions(process.env.ARTPLAYER_MB_SOAK_SECONDS)
|
||||
assert(process.env.ARTPLAYER_MB_SOAK_MEDIA, 'Generate and specify the long HLS fixture directory')
|
||||
const directory = path.resolve(process.env.ARTPLAYER_MB_SOAK_MEDIA)
|
||||
const manifestBytes = fs.readFileSync(path.join(directory, 'manifest.json'))
|
||||
const manifest = JSON.parse(manifestBytes)
|
||||
assert(manifest.duration >= 600, 'Soak needs at least ten minutes of media')
|
||||
assert(manifest.duration >= minimumMediaSeconds, `Soak needs at least ${minimumMediaSeconds} seconds of media for both uninterrupted phases`)
|
||||
const media = new Map(Object.entries(manifest.files).map(([name, expected]) => {
|
||||
assert.equal(path.basename(name), name)
|
||||
const bytes = fs.readFileSync(path.join(directory, name))
|
||||
assert.equal(hash(bytes), expected.sha256, name)
|
||||
return [name, bytes]
|
||||
return [name, { file: path.join(directory, name), sha256: expected.sha256 }]
|
||||
}))
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
test(`MediaBunny sustained HLS playback with ${core} core`, async ({ page }, testInfo) => {
|
||||
await page.route('**/mb-soak/**', async (route) => {
|
||||
const name = new URL(route.request().url()).pathname.split('/').at(-1)
|
||||
const body = media.get(name)
|
||||
const entry = media.get(name)
|
||||
const body = entry && fs.readFileSync(entry.file)
|
||||
if (body)
|
||||
assert.equal(hash(body), entry.sha256, 'Soak media changed during playback')
|
||||
await route.fulfill({ status: body ? 200 : 404, body: body || 'Missing fixture', contentType: name.endsWith('.m3u8') ? 'application/vnd.apple.mpegurl' : 'video/mp2t' })
|
||||
})
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
const capabilities = await page.evaluate(() => ({ audio: typeof window.AudioContext, videoDecoder: typeof window.VideoDecoder, audioDecoder: typeof window.AudioDecoder }))
|
||||
const evidence = { core, implementation: implementation.name, sha256: hash(implementation.code), manifestSha256: hash(manifestBytes), capabilities, outcome: 'incomplete', phases: [], checkpoints: [] }
|
||||
const evidence = { core, implementation: implementation.name, sha256: hash(implementation.code), manifestSha256: hash(manifestBytes), mediaDuration: manifest.duration, requestedPhaseSeconds: phaseSeconds, capabilities, outcome: 'incomplete', phases: [], checkpoints: [] }
|
||||
const progressFile = testInfo.outputPath('progress.jsonl')
|
||||
fs.mkdirSync(path.dirname(progressFile), { recursive: true })
|
||||
fs.writeFileSync(progressFile, `${JSON.stringify({ kind: 'started', project: testInfo.project.name, ...evidence })}\n`)
|
||||
if (Object.values(capabilities).some(value => value !== 'function')) {
|
||||
evidence.outcome = 'missing-native-capability-control'
|
||||
await testInfo.attach('mediabunny-soak', { contentType: 'application/json', body: JSON.stringify(evidence) })
|
||||
@@ -117,7 +125,7 @@ for (const core of ['published', 'candidate']) {
|
||||
})
|
||||
await page.click('#play')
|
||||
await page.evaluate(() => window.mbPlaying)
|
||||
const snapshot = () => page.evaluate(() => ({ wall: performance.now(), time: window.mbCanvas.currentTime, rate: window.mbCanvas.playbackRate, active: window.mbSoak.active.size, queued: window.mbCanvas.engine.audio.queuedNodes.size, iterators: window.mbSoak.iterators.size, draws: window.mbSoak.counters.draws, visibility: document.visibilityState, paused: window.mbCanvas.paused, error: window.mbCanvas.error }))
|
||||
const snapshot = () => page.evaluate(() => ({ wall: performance.now(), time: window.mbCanvas.currentTime, rate: window.mbCanvas.playbackRate, active: window.mbSoak.active.size, queued: window.mbCanvas.engine.audio.queuedNodes.size, iterators: window.mbSoak.iterators.size, draws: window.mbSoak.counters.draws, visibility: document.visibilityState, paused: window.mbCanvas.paused, error: window.mbCanvas.error, maxAVError: window.mbSoak.stats[window.mbSoak.phase]?.maxError ?? null }))
|
||||
async function sustain(name, seconds, rate) {
|
||||
await page.evaluate((name) => {
|
||||
window.mbSoak.stats[name] = { frames: 0, maxError: 0, sumError: 0, histogram: Array.from({ length: 1001 }, () => 0) }
|
||||
@@ -129,6 +137,7 @@ for (const core of ['published', 'candidate']) {
|
||||
await page.waitForTimeout(5000)
|
||||
const current = await snapshot()
|
||||
evidence.checkpoints.push({ phase: name, ...current })
|
||||
fs.appendFileSync(progressFile, `${JSON.stringify({ kind: 'sample', phase: name, ...current })}\n`)
|
||||
expect(current.visibility).toBe('visible')
|
||||
expect(current.error).toBeNull()
|
||||
expect(current.paused).toBe(false)
|
||||
@@ -152,11 +161,11 @@ for (const core of ['published', 'candidate']) {
|
||||
expect(stats.frames).toBeGreaterThan(seconds * 12)
|
||||
expect(stats.maxError).toBeLessThan(0.25)
|
||||
}
|
||||
await sustain('continuous-1x', 90, 1)
|
||||
await sustain('continuous-1x', phaseSeconds, 1)
|
||||
await page.evaluate(() => {
|
||||
window.mbCanvas.playbackRate = 2
|
||||
})
|
||||
await sustain('continuous-2x', 90, 2)
|
||||
await sustain('continuous-2x', phaseSeconds, 2)
|
||||
await page.evaluate(async () => {
|
||||
window.mbCanvas.playbackRate = 1
|
||||
await window.mbCanvas.engine.seek(100)
|
||||
@@ -190,7 +199,7 @@ for (const core of ['published', 'candidate']) {
|
||||
expect(cleanup).toMatchObject({ audioIterator: false, videoIterator: false, queued: 0, destroyed: true })
|
||||
expect(cleanup.created).toBe(cleanup.disconnected)
|
||||
evidence.cleanup = cleanup
|
||||
evidence.scope = 'Actual 600s generated HLS input, at least 180s uninterrupted wall playback at 1x/2x then seek/quality/audio; decoded frame vs audio clock, not acoustic output, hour-scale drift, heap leak proof or physical devices. Only active nodes/iterators are retained by instrumentation.'
|
||||
evidence.scope = `Actual ${manifest.duration}s generated HLS input, requested ${phaseSeconds}s wall playback at each of 1x/2x then seek/quality/audio; achieved duration is recorded per phase. Decoded frame vs audio clock, not acoustic output, heap leak proof, background throttling or physical devices. Only active nodes/iterators are retained by instrumentation; fixture files are read on demand.`
|
||||
await testInfo.attach('mediabunny-soak', { contentType: 'application/json', body: JSON.stringify(evidence) })
|
||||
}
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user