test(mediabunny): [PKG-MB-09] verify native picture-in-picture checkpoint

This commit is contained in:
Harvey Zhao committed 2026-09-12 22:11:27 +08:00
1 parent fb9fb23f53
commit 190fdb2776
9 files changed
+1466 -6

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,44 @@
# PKG-MB-09 检查点:真实 Document PiP 组合
本任务保持doing;本检查点只完成原生画中画的首组组合验证,不代表长播放、设备与全部组合验收。
执行环境为本机Windows,Playwright Chromium 153.0.8010.12、Firefox 155.0、WebKit 26.6。
原生行为按实际API探测与运行结果记录,不由浏览器名称猜测,也不将WebKit称为真实Safari设备。
新增mediabunny-dpip.spec.js使用固定main产物、实际发布核心/候选核心和Document PiP产物。
从真实button click调用requestWindow,未覆盖API,未使用iframe代替窗口。确认独立Window与
Canvas/播放器节点迁移,迁移后实际drawImage及媒体时钟继续前进。
矩阵为2个核心×video-only/HLS音视频×插件close/原生window.close/destroy×3个引擎,共36项。
Chromium/Firefox共24项完成真实窗口/Canvas播放;HLS另验证实际AudioBufferSourceNode调度,
插件close场景在PiP内切换到90P与French音轨后还原。关闭还原验证原parent/document、同一Canvas、
占位符清零和原生窗口关闭;销毁验证引擎关闭且不再向销毁后的ArtPlayer发document-pip=false。
WebKit共12项记录为Document PiP API不可用,未创建播放器或计作媒体播放成功。
此前dpip-helpers.js仍是受控requestWindow与真实iframe DOM测试;本文件有明确独立作用,
不能用本轮少数组合替换那些异常时序/部分setup失败回归,也不能把旧iframe证据重新标为原生。
首轮12项(8原生/4不可用)及扩展36项均保留report.json和整个results目录。
验证命令:设置ARTPLAYER_MB_ARTIFACT和ARTPLAYER_DPIP_ARTIFACT指向各包已构建main后,
运行`yarn test:browser test/browser/mediabunny-dpip.spec.js`。源模式也能运行,但报告必须记录
实际code hash和实施来源;产物验证不能默默降级为源构建。新用例已由默认浏览器文件发现机制纳入。
结果、产物/测试哈希、核心来源和浏览器版本见[原生窗口证据](../baselines/mb-native-pip-checkpoint.json)。
当前没有生产源码或依赖变更。最终完整CI1418项和44重复契约通过,324生产TS;台账失败与修复均保留。
## 接续范围
- 生成可重复长AV媒体,独立运行真实持续播放与1×/2×、seek/quality/audio切换,记录逐帧与音频时钟偏差。
- 跟踪在用音频节点、迭代器/RAF与destroy后清理,避免测试自身持有所有历史节点造成伪内存增长。
- 验证PiP opener不可见、重复开关、恢复播放与更多键盘/焦点组合;本轮opener始终visible,不作后台节流保证。
- 原生视频、Canvas proxy与完整Document PiP插件验收仍归PKG-DPIP-05,不能由本轮只测MediaBunny宣告完成。
- 真实Safari/设备支持矩阵、长播放和完整分发/许可仍未关闭;MB-CAP-01、MB-LIFE-01、MB-LICENSE-01保持open。
FFmpeg已在D:\ffmpeg\bin\ffmpeg.exe可用,长媒体应由明确命令生成并保存版本/参数/哈希,不能把短视频
若干次seek冒充长时间持续播放。本检查点不发布、不推送,不计作任务完成。
## CI台账修复
首轮完整CI在风险检查失败:MB-08验收后更新状态时,遗漏了MB-TYPE-01和MB-READY-01
的resolutionEvidence/resolutionRationale。原关闭事实及证据不变,补齐台账要求字段,
保留失败日志mb09-native-ci.log,不放宽验证器。今后任务/风险状态与最终文档更新后,
还须执行plan --check、risk-register --check及风险测试,避免只检查状态更新前的快照。
+9
View File
@@ -101,3 +101,12 @@ video-only输入及替换无sink仍readyState=4;任务保持doing,下一步
[变更](changes/2026-09-12-PKG-MB-08-capability.md)和[证据](baselines/mb-capability-validation.json)
核对全部源码/产物/报告/安装文件哈希。MB-TYPE-01/MB-READY-01关闭;任务done并立即专用提交。
39个WebKit能力对照不是播放或物理Safari支持,长播放/设备/组合及分发许可仍由09/10接续。
## PKG-MB-09 原生画中画检查点(doing)
36个新旧核心/媒体/关闭方式/浏览器场景通过,24项实际原生窗口中Canvas解码播放,
12项WebKit缺失API对照未计作播放。HLS在画中画中切换质量/音轨,关闭还原与销毁清理通过。
本轮opener始终visible,未覆盖后台节流。完整CI1418与44重复契约通过;补齐MB-08风险
关闭字段遗漏,失败日志保留。见[变更](changes/2026-09-12-PKG-MB-09-native-pip-checkpoint.md)
和[证据](baselines/mb-native-pip-checkpoint.json)。222项:105 done、8 doing、109 todo。
MB-09仍doing;接续持续播放/AV同步/更多组合与设备,DPIP-05仍独立验收。
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。
状态:todo 110 / doing 7 / blocked 0 / done 105 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 109 / doing 8 / blocked 0 / done 105 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -337,7 +337,7 @@
| PKG-MB-06 | artplayer-proxy-mediabunny<br>整理音频解码、时钟与同步 | PKG-MB-04 | AudioEngine 时钟、缓冲和 AudioContext 归属 | AV sync、倍速、无音轨、静音/音量和暂停恢复通过 | H | done |
| PKG-MB-07 | artplayer-proxy-mediabunny<br>整理 HLS 配对轨道与 selector | PKG-MB-05, PKG-MB-06, CORE-14 | m3u8 配对、质量/音频选择及拓扑清理 | 实际选择高亮、切到无轨道来源清理、无重复 readiness | H | done |
| PKG-MB-08 | artplayer-proxy-mediabunny<br>完成 TS 与媒体能力声明 | PKG-MB-07, ENG-04, ENG-06 | 8 个自有 JS 模块迁移与 Result/shim 类型 | 解码器/Stream/DOM 类型清楚,旧 Option 和 art.mediabunny 使用保持 | H | done |
| PKG-MB-09 | artplayer-proxy-mediabunny<br>完成新旧核心和真实媒体组合 | PKG-MB-08, CORE-22 | 跨浏览器能力、长播放、DPiP、HLS 音轨/质量报告 | 资源释放、事件顺序、AV sync 与支持范围满足基线 | H | todo |
| PKG-MB-09 | artplayer-proxy-mediabunny<br>完成新旧核心和真实媒体组合 | PKG-MB-08, CORE-22 | 跨浏览器能力、长播放、DPiP、HLS 音轨/质量报告 | 资源释放、事件顺序、AV sync 与支持范围满足基线 | H | doing |
| PKG-MB-10 | artplayer-proxy-mediabunny<br>完成分发和文档 | PKG-MB-09, ENG-07 | mediabunny.js、README、依赖版本、三产物和许可 | tarball 可消费、无意外依赖升级、旧调用与回退可用 | H | todo |
## 5 包迁移:artplayer-tool-iframe
@@ -528,4 +528,5 @@
- 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)
- PKG-FACTORY-01: [记录](baselines/factory-assignment-gaps.json) [记录](baselines/factory-compatibility-proposals.json) [记录](factory-compatibility-decision.md) [记录](changes/2026-09-12-PKG-FACTORY-01-decision.md)
+9
View File
@@ -1,5 +1,14 @@
# 进度与证据
## PKG-MB-09 原生画中画检查点(doing)
36个新旧核心/媒体/关闭方式/浏览器场景通过,24项实际原生窗口中Canvas解码播放,
12项WebKit缺失API对照未计作播放。HLS在画中画中切换质量/音轨,关闭还原与销毁清理通过。
本轮opener始终visible,未覆盖后台节流。完整CI1418与44重复契约通过;补齐MB-08风险
关闭字段遗漏,失败日志保留。见[变更](changes/2026-09-12-PKG-MB-09-native-pip-checkpoint.md)
和[证据](baselines/mb-native-pip-checkpoint.json)。222项:105 done、8 doing、109 todo。
MB-09仍doing;接续持续播放/AV同步/更多组合与设备,DPIP-05仍独立验收。
## PKG-MB-08完成:严格TS、兼容声明与解码能力边界
全包34生产TS已迁移,默认可选工厂/精确Canvas兼容;显式媒体类型不泄漏SDK现代声明。
+4
View File
@@ -54,3 +54,7 @@ yarn ci:check
```
校验已包含在 test:baseline 中:保护既有基线发现、任务引用、证据路径、生成索引、包覆盖、依赖解析和内嵌文件指纹。负例覆盖漏项、错误任务、无证据关闭、越界路径、错误哈希和锁文件版本偏移。它不能自动发现所有新的第三方文件或判定许可证合规,新增资源仍须进入人工/AI 审查与包任务验收。
任务或风险验收后若再次更新状态/关闭字段,提交前须针对最终快照运行plan --check、
risk-register --check与风险测试。resolved/accepted-with-scope必须填写resolutionEvidence
和resolutionRationale;不能仅在compatibleResolution或普通evidence中写关闭事实。
+12 -2
View File
@@ -3690,7 +3690,12 @@
"refactor/baselines/mb-entry-checkpoint.json",
"refactor/changes/2026-09-12-PKG-MB-08-capability.md",
"refactor/baselines/mb-capability-validation.json"
]
],
"resolutionEvidence": [
"refactor/changes/2026-09-12-PKG-MB-08-capability.md",
"refactor/baselines/mb-capability-validation.json"
],
"resolutionRationale": "Preserved exact optional whole-factory and HTMLCanvasElement result; real VideoShim implements opt-in media/Player/HLS/synthetic-frame types without modern SDK declarations leaking to older compilers. Conditional ESM declarations and classic legacy resolution work. Runtime supports both historical default namespace and callable use without a required default property narrowing factory replacement. Final isolated installs of two frozen releases and candidate pass 17 compiler/runtime cases; seven candidate modes each reject fifteen invalid uses and check bidirectional assignments against actual frozen declaration bytes. Complete distribution/license/device gates remain separate."
},
{
"id": "MB-CAP-01",
@@ -3729,7 +3734,12 @@
"refactor/baselines/mb-entry-checkpoint.json",
"refactor/changes/2026-09-12-PKG-MB-08-capability.md",
"refactor/baselines/mb-capability-validation.json"
]
],
"resolutionEvidence": [
"refactor/changes/2026-09-12-PKG-MB-08-capability.md",
"refactor/baselines/mb-capability-validation.json"
],
"resolutionRationale": "Coordinated metadata publishes once, trackless input rejects before decoder setup, and stale callbacks cannot publish. MB-08 additionally rejects existing but wholly undecodable tracks before metadata/data/canplay and before replacement success, preserving one usable audio/video track, SDK query errors and late-operation cancellation. Sixteen controlled Node cases and thirty added browser cases distinguish twelve controlled rejection cases, eight native partial-decoder playback cases and ten missing-API controls; the final 150-case media suite passes. Physical Safari/support-range work remains MB-CAP-01 rather than being claimed from these controls."
}
]
}
+5 -2
View File
@@ -3570,11 +3570,14 @@
"PKG-MB-08",
"CORE-22"
],
"status": "todo",
"status": "doing",
"risk": "H",
"deliverable": "跨浏览器能力、长播放、DPiP、HLS 音轨/质量报告",
"acceptance": "资源释放、事件顺序、AV sync 与支持范围满足基线",
"evidence": []
"evidence": [
"changes/2026-09-12-PKG-MB-09-native-pip-checkpoint.md",
"baselines/mb-native-pip-checkpoint.json"
]
},
{
"id": "PKG-MB-10",
+113
View File
@@ -0,0 +1,113 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import { hash } from '../../refactor/scripts/releases.mjs'
import { dpipCandidate } from '../helpers/dpip.js'
import { mbCandidate } from '../helpers/mediabunny.js'
import { expect, test } from './fixtures.js'
const implementation = await mbCandidate()
const dpip = await dpipCandidate()
const manifest = JSON.parse(fs.readFileSync(new URL('./media/hls/manifest.json', import.meta.url)))
const hls = new Map(Object.entries(manifest.files).map(([name, expected]) => {
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url))
assert.equal(hash(bytes), expected.sha256)
return [name, bytes]
}))
for (const core of ['published', 'candidate']) {
for (const media of ['video', 'hls']) {
for (const ending of ['close', 'native-close', 'destroy']) {
test(`MediaBunny ${implementation.name}: ${core} core native Document PiP ${media} ${ending}`, async ({ page, browserName }, testInfo) => {
await page.route('**/mb-dpip-hls/**', async (route) => {
const name = new URL(route.request().url()).pathname.split('/').at(-1)
const body = hls.get(name)
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 supported = await page.evaluate(() => typeof window.documentPictureInPicture?.requestWindow === 'function')
if (!supported) {
await testInfo.attach('mediabunny-dpip', { contentType: 'application/json', body: JSON.stringify({ core, media, ending, implementation: implementation.name, sha256: hash(implementation.code), dpipSha256: hash(dpip.code), outcome: 'native-document-pip-unavailable', browserName, supported }) })
return
}
for (const [code, global] of [[implementation.code, 'mbFactory'], [dpip.code, 'dpipFactory']]) {
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${code}; window.${global} = module.exports.default || module.exports; })();` })
}
await page.evaluate((media) => {
window.mbDraws = 0
window.mbPipEvents = []
window.art = new window.Artplayer({ container: '.player', url: media === 'video' ? '/test/pattern.mp4' : '/mb-dpip-hls/master.m3u8', muted: true, proxy: window.mbFactory(), plugins: [window.dpipFactory({ fallbackToVideoPiP: false })] })
window.mbCanvas = window.art.video
window.mbPlayer = window.art.template.$player
window.mbParent = window.mbPlayer.parentNode
window.mbPip = window.art.plugins.artplayerPluginDocumentPip
window.art.on('document-pip', value => window.mbPipEvents.push(value))
const ctx = window.mbCanvas.getContext('2d')
const draw = ctx.drawImage.bind(ctx)
ctx.drawImage = (...args) => {
window.mbDraws++
return draw(...args)
}
document.querySelector('#play').onclick = () => window.art.play()
const button = document.createElement('button')
button.id = 'open-native-pip'
button.textContent = 'Open native Document PiP'
button.onclick = () => {
window.mbOpening = window.mbPip.open()
}
document.body.append(button)
}, media)
let result
try {
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.mbCanvas.currentTime)).toBeGreaterThan(0.1)
await page.click('#open-native-pip')
await page.evaluate(() => window.mbOpening)
expect(await page.evaluate(() => window.mbPip.isActive)).toBe(true)
const before = await page.evaluate(() => ({ time: window.mbCanvas.currentTime, draws: window.mbDraws, adopted: window.mbPlayer.ownerDocument === window.documentPictureInPicture.window.document, sameCanvas: window.mbCanvas === window.art.video, distinctWindow: window.documentPictureInPicture.window !== window, openerVisibility: document.visibilityState }))
expect(before.adopted).toBe(true)
expect(before.sameCanvas).toBe(true)
expect(before.distinctWindow).toBe(true)
await expect.poll(() => page.evaluate(() => window.mbDraws)).toBeGreaterThan(before.draws + 2)
await expect.poll(() => page.evaluate(() => window.mbCanvas.currentTime)).toBeGreaterThan(before.time + 0.15)
if (media === 'hls') {
await expect.poll(() => page.evaluate(() => window.mbCanvas.engine.audio.queuedNodes.size)).toBeGreaterThan(0)
if (ending === 'close') {
await page.evaluate(async () => {
const state = await window.mbCanvas.getM3u8State()
await window.mbCanvas.switchM3u8Quality(state.levels.find(level => level.height === 90).id)
const next = await window.mbCanvas.getM3u8State()
await window.mbCanvas.switchM3u8Audio(next.audios.find(audio => audio.lang === 'fr').id)
})
expect(await page.evaluate(async () => {
const state = await window.mbCanvas.getM3u8State()
return { height: state.currentLevel.height, language: state.currentAudio.lang, adopted: window.mbCanvas.ownerDocument === window.documentPictureInPicture.window.document }
})).toEqual({ height: 90, language: 'fr', adopted: true })
}
}
if (ending !== 'destroy') {
if (ending === 'native-close')
await page.evaluate(() => window.documentPictureInPicture.window.close())
else await page.evaluate(() => window.mbPip.close())
await expect.poll(() => page.evaluate(() => window.mbPip.isActive)).toBe(false)
expect(await page.evaluate(() => window.mbPlayer.parentNode === window.mbParent && window.mbPlayer.ownerDocument === document && window.mbCanvas === window.art.video)).toBe(true)
}
else {
await page.evaluate(() => window.art.destroy(false))
await expect.poll(() => page.evaluate(() => window.mbPip.isActive)).toBe(false)
expect(await page.evaluate(() => window.mbCanvas.engine.destroyed)).toBe(true)
}
result = await page.evaluate(() => ({ events: window.mbPipEvents, placeholders: document.querySelectorAll('.artplayer-document-pip-placeholder').length, nativeWindowClosed: !window.documentPictureInPicture.window || window.documentPictureInPicture.window.closed }))
expect(result).toEqual({ events: ending === 'destroy' ? [true] : [true, false], placeholders: 0, nativeWindowClosed: true })
await testInfo.attach('mediabunny-dpip', { contentType: 'application/json', body: JSON.stringify({ core, media, ending, implementation: implementation.name, sha256: hash(implementation.code), dpipSha256: hash(dpip.code), outcome: 'native-document-pip-canvas-playback', before, result, scope: 'Actual requestWindow from a click, no iframe or API replacement; native Canvas decoding and window lifecycle. HLS includes native audio, and close cases change quality/audio inside PiP.' }) })
}
finally {
await page.evaluate(() => {
if (!window.art.isDestroy)
window.art.destroy(false)
window.documentPictureInPicture.window?.close()
})
}
})
}
}
}