diff --git a/playwright.soak.config.js b/playwright.soak.config.js index 7eb90d4d9..982525152 100644 --- a/playwright.soak.config.js +++ b/playwright.soak.config.js @@ -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: [ diff --git a/refactor/baselines/mb-hour-soak-start.json b/refactor/baselines/mb-hour-soak-start.json new file mode 100644 index 000000000..d3a6d99f3 --- /dev/null +++ b/refactor/baselines/mb-hour-soak-start.json @@ -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." + ] +} diff --git a/refactor/changes/2026-09-16-PKG-MB-09-hour-soak.md b/refactor/changes/2026-09-16-PKG-MB-09-hour-soak.md new file mode 100644 index 000000000..a0f5dc2aa --- /dev/null +++ b/refactor/changes/2026-09-16-PKG-MB-09-hour-soak.md @@ -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 与相关风险继续保留未完成项。 diff --git a/refactor/mb-validation.md b/refactor/mb-validation.md index 6d1a6d837..d7039aefa 100644 --- a/refactor/mb-validation.md +++ b/refactor/mb-validation.md @@ -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)分开记录;不得把接口测试称为真实媒体验收。 diff --git a/refactor/plan.md b/refactor/plan.md index 180c262a1..49e06de02 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -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) diff --git a/refactor/progress.md b/refactor/progress.md index 052f7f615..6c29a15e5 100644 --- a/refactor/progress.md +++ b/refactor/progress.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 查询,区分缺失、 diff --git a/refactor/scripts/soak-options.test.mjs b/refactor/scripts/soak-options.test.mjs new file mode 100644 index 000000000..06081763f --- /dev/null +++ b/refactor/scripts/soak-options.test.mjs @@ -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/) +}) diff --git a/refactor/tasks.json b/refactor/tasks.json index a19b97e69..8eb91699d 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -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" ] }, { diff --git a/scripts/generate-hls-fixture.mjs b/scripts/generate-hls-fixture.mjs index 9cb3dc72a..762a59fe3 100644 --- a/scripts/generate-hls-fixture.mjs +++ b/scripts/generate-hls-fixture.mjs @@ -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] diff --git a/test/helpers/soak-options.js b/test/helpers/soak-options.js new file mode 100644 index 000000000..3854d65ef --- /dev/null +++ b/test/helpers/soak-options.js @@ -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 } +} diff --git a/test/soak/mediabunny.spec.js b/test/soak/mediabunny.spec.js index 309bd1e44..97645be0e 100644 --- a/test/soak/mediabunny.spec.js +++ b/test/soak/mediabunny.spec.js @@ -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) }) } })