mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 10:56:15 -08:00
test(hls): [PKG-HLS-SDK-01] preserve group stall evidence and verify ongoing playback
This commit is contained in:
1 parent
f37658d657
commit
ca967ea1a2
9 files changed
+38631
-19
No files matched your search
@@ -135,6 +135,16 @@ HTTP without ArtPlayer/plugin or a Worker observer still reproduced crashes with
|
||||
both reset-first and SDK-first teardown. A passing no-worker control does not
|
||||
replace worker acceptance. See the 2026-09-14 HTTP teardown checkpoint in refactor.
|
||||
|
||||
For the separate grouped-playback stall, `--switch-boundary switched|selected|immediate`
|
||||
compares completed SDK audio switching, selected UI/getter state, and same-call audio/level
|
||||
selection. The default remains switched. `--prefill` adds a controlled fully buffered
|
||||
fixture condition; it is not a player workaround. Both diagnostic and integration paths
|
||||
now require the clock and frame count to advance at the target height after high/low
|
||||
switches. Keep the original 7000ms height assertion and the failed trace even when a
|
||||
later control passes. Diagnostic private SDK fields are read-only, version-specific;
|
||||
never read load-level getters after SDK destruction. See the 2026-09-15 switch-boundary
|
||||
checkpoint in refactor for the reproduced empty-buffer failure and remaining attribution.
|
||||
|
||||
## Package file boundary
|
||||
|
||||
The package .npmignore excludes src and tsconfig.json. The implementation config
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,75 @@
|
||||
# PKG-HLS-SDK-01 分组切换边界与持续播放
|
||||
|
||||
来源 HEAD:`f37658d657dc89f4a1597ebd73b311529fc8e012`。任务仍为 doing,
|
||||
HLS-PLAYBACK-01 和独立的 HLS-CRASH-01 均未关闭。本轮没有生产源码、类型、
|
||||
依赖、媒体夹具或分发产物修改,没有改变 SDK 所有权或消费者配置。
|
||||
|
||||
## 本轮取得的失败证据
|
||||
|
||||
原分组集成场景再次复现 Firefox 155.0、Hls.js 1.7.2、发布核心、候选插件组合失败。
|
||||
SDK 音轨已为 high 组 index 0,但视频高度仍为 90,播放位置停在 0.999146,
|
||||
paused=false、ended=false、readyState=2、buffered=[]。原 7000ms 高度断言失败,
|
||||
发生在本次新增持续播放断言之前。该轮四项测试为三通过、一失败,不删失败或重试。
|
||||
|
||||
该失败的 SDK error 数组为空。独立 trace 的 42 条媒体夹具请求均返回 200,
|
||||
最终切到 high 组后继续请求 French 音频,但没有再次请求 high 视频片段。
|
||||
早期自动选择阶段曾请求 high-01 至 high-05,不能写成整个测试从未请求 high 视频。
|
||||
这些事实将下一步收窄到加载/缓冲状态转换,但尚不足以认定 SDK 或核心的根因。
|
||||
|
||||
失败报告和 trace 保留在独立的 hls-boundary-suite 路径,后续测试使用各自的输出
|
||||
目录。机器证据包含原始报告指纹、失败附件及从 trace 提取的完整夹具请求序列。
|
||||
较早 hls-sdk-validation.json 的历史失败保持原样。
|
||||
|
||||
## 验证补强
|
||||
|
||||
`test/browser/hls-sdk.spec.js` 原先返回 low 后只验证音轨数量与菜单。现在 high 和
|
||||
返回 low 均要求未暂停、目标高度、媒体时钟推进超过 0.3 秒、额外帧数超过两帧。
|
||||
没有额外 play/seek/reload,也没有扩大已有断言超时。检查新增观察和播放时间,
|
||||
因此不能把本轮销毁成功用于关闭此前独立的 Firefox 销毁崩溃。
|
||||
|
||||
新增失败现场的 SDK 控制器状态、nextLoadPosition、独立缓冲区及加载/flush 事件。
|
||||
这些私有字段仅供固定 SDK 的只读诊断,不作为生产依赖或跨版本公共契约。
|
||||
SDK 销毁后不读取 currentLevel/loadLevel/nextLoadLevel:初版采集在成功销毁后读取
|
||||
nextLoadLevel 时出现失效 getter,导致诊断附件只有 diagnosticError。该两轮原始
|
||||
报告保留,最终轮明确检查附件没有 diagnosticError;不把它们当完整状态证据。
|
||||
|
||||
## 可重跑对照
|
||||
|
||||
`refactor/scripts/hls-sdk-diagnostic.mjs` 增加三个音轨/分组边界:
|
||||
|
||||
- `--switch-boundary switched`:默认,保持等待 AUDIO_TRACK_SWITCHED。
|
||||
- `--switch-boundary selected`:插件宿主等待 UI 显示 French,直接宿主检查 audioTrack=1。
|
||||
- `--switch-boundary immediate`:同一次页面调用内选择音轨后立即选择 high。
|
||||
|
||||
`--prefill` 等待单个原生 buffered 区间到达 duration-0.1;固定夹具视频 12 秒、
|
||||
音频 12.032 秒,因此允许编码尾部差异。它在初次选择 low 前等待,非 immediate
|
||||
模式还在选择 high 前等待。该受控对照不修改默认播放器,不是修复或延时兜底。
|
||||
|
||||
```sh
|
||||
node refactor/scripts/hls-sdk-diagnostic.mjs --version 1.7.2 --iterations 3 --switch-boundary immediate --capture-before-destroy
|
||||
node refactor/scripts/hls-sdk-diagnostic.mjs --version 1.7.2 --iterations 2 --host published --plugin --switch-boundary selected --transport route --observe-workers --capture-before-destroy
|
||||
node refactor/scripts/hls-sdk-diagnostic.mjs --version 1.7.2 --iterations 2 --switch-boundary selected --prefill --capture-before-destroy
|
||||
yarn test:browser hls-sdk.spec.js --grep "group changes" --workers=1
|
||||
```
|
||||
|
||||
19 个可读取的诊断迭代通过:早期三种宿主/边界九次;加入双向持续播放后的直接
|
||||
三次、发布核心两次、候选核心两次、prefill 两次、默认 switched 一次,共十次。
|
||||
全部使用 Windows Firefox 155.0 和固定 Hls.js 1.7.2。直接模式不加载 ArtPlayer 或
|
||||
插件。诊断宿主使用报告中哈希的既有 dist,集成用例使用源码选择的候选构建;
|
||||
本轮不声称重新构建或隔离安装了这些 dist。
|
||||
|
||||
最初两轮共八次只打印 passed 后便在 JSON 序列化阶段失败,不能计入上述十九次。
|
||||
原因是新增 LEVEL_SWITCHING 的 details 为带循环引用的对象;已限定采集标量字段。
|
||||
不是播放器失败,也没有把未写出的报告认定为成功。
|
||||
|
||||
## 最终验证与剩余
|
||||
|
||||
最终三引擎分组用例八通过、四 Windows WebKit MSE 能力跳过,无重试;SDK 1.5.17
|
||||
与 1.7.2、新旧核心均包括在有 MSE 的八项中。原失败之后的中间轮另行保存,不能
|
||||
用最终通过覆盖或关闭原故障。定向 lint、固定 SDK 来源单测、严格工具链通过。
|
||||
Node 24.21.0、Yarn Classic 1.22.22;无新依赖,无重打包或远端执行。
|
||||
|
||||
见 [机器证据](../baselines/hls-switch-boundary-validation.json)。完整目标仍需要解释
|
||||
空缓冲且不再请求视频的路径,结合新增控制器/缓冲事件建立更小的可重复案例,
|
||||
再决定归属与兼容修复;单次或多次通过不能关闭风险。Safari/真机/全部分发与
|
||||
三轮发布复盘仍按原门槛执行。回退本提交仅移除诊断和验收增强,不涉及运行时。
|
||||
+1
-1
@@ -569,7 +569,7 @@
|
||||
- PKG-HLS-02: [记录](hls-validation.md) [记录](baselines/hls-sdk.json) [记录](baselines/hls-validation.json) [记录](changes/2026-09-11-PKG-HLS-02-tests.md)
|
||||
- PKG-HLS-03: [记录](changes/2026-09-12-PKG-HLS-03-modules.md) [记录](baselines/hls-modules-validation.json) [记录](hls-validation.md)
|
||||
- PKG-HLS-04: [记录](changes/2026-09-12-PKG-HLS-04-types.md) [记录](baselines/hls-types-validation.json) [记录](hls-validation.md)
|
||||
- PKG-HLS-SDK-01: [记录](changes/2026-09-12-PKG-HLS-SDK-01-integration.md) [记录](baselines/hls-sdk-validation.json) [记录](baselines/hls-sdk-diagnostics.json) [记录](changes/2026-09-14-PKG-HLS-SDK-01-http-teardown.md) [记录](baselines/hls-http-teardown-validation.json)
|
||||
- PKG-HLS-SDK-01: [记录](changes/2026-09-12-PKG-HLS-SDK-01-integration.md) [记录](baselines/hls-sdk-validation.json) [记录](baselines/hls-sdk-diagnostics.json) [记录](changes/2026-09-14-PKG-HLS-SDK-01-http-teardown.md) [记录](baselines/hls-http-teardown-validation.json) [记录](changes/2026-09-15-PKG-HLS-SDK-01-switch-boundary.md) [记录](baselines/hls-switch-boundary-validation.json)
|
||||
- PKG-DASH-01: [记录](changes/2026-09-12-PKG-DASH-01-contract.md) [记录](baselines/dash-control-contract.md) [记录](baselines/dash-control-release.json)
|
||||
- PKG-DASH-02: [记录](changes/2026-09-12-PKG-DASH-02-tests.md) [记录](dash-validation.md) [记录](baselines/dash-validation.json)
|
||||
- PKG-DASH-03: [记录](changes/2026-09-12-PKG-DASH-03-runtime.md) [记录](baselines/dash-runtime.json) [记录](dash-validation.md)
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-HLS-SDK-01 分组切换与持续播放
|
||||
|
||||
再次复现 Firefox/Hls.js 1.7.2/发布核心分组切换失败:音轨已更新,视频仍90高、
|
||||
约1秒、未暂停且缓冲区为空。独立trace中最终切组后没有新的视频片段请求,
|
||||
42条媒体请求均200;不是仅有菜单或网络错误的推断。保留三通过一失败的原轮。
|
||||
|
||||
增加音轨选择/完成/同调用切组及整段缓冲对照,双向档位切换要求真实时钟和帧
|
||||
增长;19次有效诊断通过不能解释失败。修正两处诊断采集问题,失败报告独立保留。
|
||||
最终分组矩阵8通过、4 Windows WebKit MSE跳过;lint、来源单测和工具链通过。
|
||||
见[记录](changes/2026-09-15-PKG-HLS-SDK-01-switch-boundary.md)及
|
||||
[证据](baselines/hls-switch-boundary-validation.json)。任务和两项风险继续开放,
|
||||
总数203 done、21 doing、43 todo不变。后续围绕控制器/分轨缓冲状态定位,
|
||||
不更改消费者SDK参数或以反复通过宣布修复;未推送、部署或发布。
|
||||
|
||||
## PKG-ASR-05 首次媒体失败与恢复
|
||||
|
||||
首次metadata之前的原生媒体失败、play拒绝、零音频资源、stop/destroy以及首个
|
||||
|
||||
+3
-1
@@ -2832,7 +2832,9 @@
|
||||
"evidence": [
|
||||
"test/browser/hls-sdk.spec.js",
|
||||
"refactor/changes/2026-09-12-PKG-HLS-SDK-01-integration.md",
|
||||
"refactor/baselines/hls-sdk-validation.json"
|
||||
"refactor/baselines/hls-sdk-validation.json",
|
||||
"refactor/changes/2026-09-15-PKG-HLS-SDK-01-switch-boundary.md",
|
||||
"refactor/baselines/hls-switch-boundary-validation.json"
|
||||
],
|
||||
"compatibleResolution": "区分 SDK、测试媒体/时序和宿主问题,保留真实档位/解码断言及 trace;不扩大等待或只验证菜单。",
|
||||
"closureCriteria": "直接 Hls 与新旧核心对照定位切换停滞,验证处置和正常/连续组切换,不能以重复通过关闭。"
|
||||
|
||||
@@ -10,13 +10,14 @@ import { observeWorkers } from '../../test/helpers/worker-observer.js'
|
||||
import { ensureArchive, hash, readMember, refactorDir } from './releases.mjs'
|
||||
|
||||
// Direct SDK control: no ArtPlayer or plugin is loaded, and no worker output is substituted.
|
||||
export async function diagnose({ version = '1.7.2', iterations = 5, transport = 'http', teardown = 'reset-first', worker = true, host = 'direct', plugin = false, sdkLogs = false, workerObserver = false, captureBeforeDestroy = false } = {}) {
|
||||
export async function diagnose({ version = '1.7.2', iterations = 5, transport = 'http', teardown = 'reset-first', worker = true, host = 'direct', plugin = false, sdkLogs = false, workerObserver = false, captureBeforeDestroy = false, switchBoundary = 'switched', prefill = false } = {}) {
|
||||
const runnerSHA256 = hash(fs.readFileSync(new URL(import.meta.url)))
|
||||
const observerSHA256 = hash(fs.readFileSync(new URL('../../test/helpers/worker-observer.js', import.meta.url)))
|
||||
assert(Number.isInteger(iterations) && iterations > 0 && iterations <= 50)
|
||||
assert(['http', 'route'].includes(transport))
|
||||
assert(['reset-first', 'sdk-first'].includes(teardown))
|
||||
assert(['direct', 'published', 'candidate'].includes(host))
|
||||
assert(['switched', 'selected', 'immediate'].includes(switchBoundary))
|
||||
assert(!plugin || host !== 'direct', 'Plugin comparison requires an ArtPlayer host')
|
||||
const matrix = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/hls-sdk-matrix.json')))
|
||||
const release = matrix.releases.find(item => item.version === version)
|
||||
@@ -98,7 +99,7 @@ export async function diagnose({ version = '1.7.2', iterations = 5, transport =
|
||||
let art
|
||||
const records = []
|
||||
const buffer = ranges => Array.from({ length: ranges.length }, (_, index) => [ranges.start(index), ranges.end(index)])
|
||||
const state = () => ({ time: video.currentTime, readyState: video.readyState, height: video.videoHeight, paused: video.paused, buffered: buffer(video.buffered), seekable: buffer(video.seekable) })
|
||||
const state = () => ({ time: video.currentTime, readyState: video.readyState, height: video.videoHeight, paused: video.paused, frames: video.getVideoPlaybackQuality().totalVideoFrames, buffered: buffer(video.buffered), seekable: buffer(video.seekable) })
|
||||
const record = (event, data) => {
|
||||
records.push({ at: performance.now(), event, data, media: state() })
|
||||
if (records.length > 400)
|
||||
@@ -108,8 +109,8 @@ export async function diagnose({ version = '1.7.2', iterations = 5, transport =
|
||||
function attach(element) {
|
||||
video = element
|
||||
hls = new window.Hls({ enableWorker: worker, startLevel: 0, debug: sdkLogs ? logger : false })
|
||||
for (const key of ['ERROR', 'AUDIO_TRACKS_UPDATED', 'AUDIO_TRACK_SWITCHED', 'LEVEL_SWITCHED', 'FRAG_BUFFERED', 'BUFFER_FLUSHED']) {
|
||||
hls.on(window.Hls.Events[key], (_, data) => record(key, { id: data.id, level: data.level, fatal: data.fatal, details: data.details, frag: data.frag && { type: data.frag.type, sn: data.frag.sn, level: data.frag.level }, tracks: data.audioTracks?.map(track => ({ id: track.id, name: track.name, group: track.groupId })) }))
|
||||
for (const key of ['ERROR', 'AUDIO_TRACKS_UPDATED', 'AUDIO_TRACK_SWITCHING', 'AUDIO_TRACK_SWITCHED', 'LEVEL_SWITCHING', 'LEVEL_SWITCHED', 'FRAG_BUFFERED', 'BUFFER_FLUSHED']) {
|
||||
hls.on(window.Hls.Events[key], (_, data) => record(key, { id: data.id, level: typeof data.level === 'number' ? data.level : undefined, fatal: data.fatal, details: typeof data.details === 'string' ? data.details : undefined, frag: data.frag && { type: data.frag.type, sn: data.frag.sn, level: data.frag.level }, tracks: data.audioTracks?.map(track => ({ id: track.id, name: track.name, group: track.groupId })) }))
|
||||
}
|
||||
for (const name of ['waiting', 'stalled', 'seeking', 'seeked', 'playing', 'pause', 'ended', 'loadedmetadata', 'canplay', 'error'])
|
||||
video.addEventListener(name, () => record(name))
|
||||
@@ -143,6 +144,7 @@ export async function diagnose({ version = '1.7.2', iterations = 5, transport =
|
||||
get hls() { return hls },
|
||||
get video() { return video },
|
||||
records,
|
||||
record,
|
||||
state,
|
||||
destroy() {
|
||||
record('destroy:start', teardown)
|
||||
@@ -170,21 +172,60 @@ export async function diagnose({ version = '1.7.2', iterations = 5, transport =
|
||||
await wait(() => window.direct.video?.readyState >= 3)
|
||||
await page.evaluate(() => window.direct.video.play())
|
||||
await wait(() => window.direct.video.currentTime > 0.3)
|
||||
if (prefill) {
|
||||
entry.phase = 'initial-prefill'
|
||||
await wait(() => {
|
||||
const video = window.direct.video
|
||||
return video.buffered.length === 1 && video.buffered.end(0) >= video.duration - 0.1
|
||||
})
|
||||
}
|
||||
entry.phase = 'low-group'
|
||||
await page.evaluate(() => {
|
||||
window.direct.hls.currentLevel = 0
|
||||
})
|
||||
await wait(() => window.direct.hls.audioTracks.length === 2 && window.direct.hls.audioTracks.every(track => track.groupId === 'low'))
|
||||
await page.evaluate(() => {
|
||||
await page.evaluate((switchBoundary) => {
|
||||
window.direct.mark = performance.now()
|
||||
window.direct.hls.audioTrack = 1
|
||||
})
|
||||
await wait(() => window.direct.records.some(item => item.at >= window.direct.mark && item.event === 'AUDIO_TRACK_SWITCHED' && item.data.id === 1))
|
||||
if (switchBoundary === 'immediate') {
|
||||
window.direct.record('high-group:requested', { boundary: switchBoundary, audioTrack: window.direct.hls.audioTrack })
|
||||
window.direct.hls.currentLevel = 1
|
||||
}
|
||||
}, switchBoundary)
|
||||
if (switchBoundary === 'switched') {
|
||||
await wait(() => window.direct.records.some(item => item.at >= window.direct.mark && item.event === 'AUDIO_TRACK_SWITCHED' && item.data.id === 1))
|
||||
}
|
||||
else if (switchBoundary === 'selected') {
|
||||
if (plugin)
|
||||
await expect(page.locator('.art-control-hls-audio .art-selector-value')).toHaveText('French', { timeout: 7000 })
|
||||
else
|
||||
await wait(() => window.direct.hls.audioTrack === 1)
|
||||
}
|
||||
entry.phase = 'high-group'
|
||||
await page.evaluate(() => {
|
||||
window.direct.hls.currentLevel = 1
|
||||
})
|
||||
if (switchBoundary !== 'immediate') {
|
||||
if (prefill) {
|
||||
entry.phase = 'low-prefill'
|
||||
await wait(() => {
|
||||
const video = window.direct.video
|
||||
return video.buffered.length === 1 && video.buffered.end(0) >= video.duration - 0.1
|
||||
})
|
||||
entry.phase = 'high-group'
|
||||
}
|
||||
await page.evaluate((switchBoundary) => {
|
||||
window.direct.record('high-group:requested', { boundary: switchBoundary, audioTrack: window.direct.hls.audioTrack })
|
||||
window.direct.hls.currentLevel = 1
|
||||
}, switchBoundary)
|
||||
}
|
||||
await wait(() => window.direct.video.videoHeight === 180 && window.direct.hls.audioTrack === 0)
|
||||
entry.phase = 'high-group-playback'
|
||||
await page.evaluate(() => {
|
||||
window.direct.playbackStart = window.direct.state()
|
||||
})
|
||||
await wait(() => {
|
||||
const current = window.direct.state()
|
||||
return !current.paused && current.height === 180 && current.time > window.direct.playbackStart.time + 0.3 && current.frames > window.direct.playbackStart.frames + 2
|
||||
})
|
||||
entry.highPlayback = await page.evaluate(() => ({ start: window.direct.playbackStart, end: window.direct.state() }))
|
||||
entry.phase = 'commentary-track'
|
||||
await page.evaluate(() => {
|
||||
window.direct.hls.audioTrack = 2
|
||||
@@ -195,6 +236,16 @@ export async function diagnose({ version = '1.7.2', iterations = 5, transport =
|
||||
window.direct.hls.currentLevel = 0
|
||||
})
|
||||
await wait(() => window.direct.hls.audioTracks.length === 2)
|
||||
entry.phase = 'return-low-playback'
|
||||
await wait(() => window.direct.video.videoHeight === 90)
|
||||
await page.evaluate(() => {
|
||||
window.direct.playbackStart = window.direct.state()
|
||||
})
|
||||
await wait(() => {
|
||||
const current = window.direct.state()
|
||||
return !current.paused && current.height === 90 && current.time > window.direct.playbackStart.time + 0.3 && current.frames > window.direct.playbackStart.frames + 2
|
||||
})
|
||||
entry.lowPlayback = await page.evaluate(() => ({ start: window.direct.playbackStart, end: window.direct.state() }))
|
||||
if (captureBeforeDestroy) {
|
||||
entry.phase = 'capture-before-destroy'
|
||||
entry.beforeDestroy = await page.evaluate(() => ({ coreLoaded: typeof window.Artplayer, pluginLoaded: typeof window.artplayerPluginHlsControl, media: window.direct.state(), sdk: { version: window.Hls.version, workerEnabled: window.direct.hls.config.enableWorker, level: window.direct.hls.currentLevel, audioTrack: window.direct.hls.audioTrack }, events: window.direct.records }))
|
||||
@@ -214,7 +265,7 @@ export async function diagnose({ version = '1.7.2', iterations = 5, transport =
|
||||
finally {
|
||||
entry.failurePhase = entry.status === 'failed' ? entry.phase : undefined
|
||||
entry.phase = 'final-state'
|
||||
entry.state = await page.evaluate(() => ({ coreLoaded: typeof window.Artplayer, pluginLoaded: typeof window.artplayerPluginHlsControl, workers: window.workerEvidence, media: window.direct?.state(), events: window.direct?.records, sdk: { version: window.Hls?.version, level: window.direct?.hls.currentLevel, audioTrack: window.direct?.hls.audioTrack } })).catch(error => ({ error: error.message }))
|
||||
entry.state = await page.evaluate(() => ({ coreLoaded: typeof window.Artplayer, pluginLoaded: typeof window.artplayerPluginHlsControl, workers: window.workerEvidence, media: window.direct?.state(), events: window.direct?.records, sdk: { version: window.Hls?.version, level: window.direct?.hls.currentLevel, audioTrack: window.direct?.hls.audioTrack, mainState: window.direct?.hls.streamController?.state, audioState: window.direct?.hls.audioStreamController?.state } })).catch(error => ({ error: error.message }))
|
||||
entry.phase = 'trace-stop'
|
||||
await context.tracing.stop({ path: path.join(directory, `${iteration}.zip`) }).catch((error) => {
|
||||
entry.traceError = error.message
|
||||
@@ -236,14 +287,14 @@ export async function diagnose({ version = '1.7.2', iterations = 5, transport =
|
||||
await browser?.close()
|
||||
server.closeAllConnections()
|
||||
await new Promise(resolve => server.close(resolve))
|
||||
fs.writeFileSync(path.join(directory, 'report.json'), `${JSON.stringify({ version, iterations, transport, teardown, worker, host, plugin, sdkLogs, workerObserver, captureBeforeDestroy, runnerSHA256, observerSHA256, browserVersion, release, inputs: Object.fromEntries([...files].map(([file, bytes]) => [file, hash(bytes)])), results }, null, 2)}\n`)
|
||||
fs.writeFileSync(path.join(directory, 'report.json'), `${JSON.stringify({ version, iterations, transport, teardown, worker, host, plugin, sdkLogs, workerObserver, captureBeforeDestroy, switchBoundary, prefill, runnerSHA256, observerSHA256, browserVersion, release, inputs: Object.fromEntries([...files].map(([file, bytes]) => [file, hash(bytes)])), results }, null, 2)}\n`)
|
||||
console.log(`Diagnostic report: ${directory}`)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
const { values } = parseArgs({ options: { 'version': { type: 'string', default: '1.7.2' }, 'iterations': { type: 'string', default: '5' }, 'transport': { type: 'string', default: 'http' }, 'teardown': { type: 'string', default: 'reset-first' }, 'no-worker': { type: 'boolean', default: false }, 'host': { type: 'string', default: 'direct' }, 'plugin': { type: 'boolean', default: false }, 'sdk-logs': { type: 'boolean', default: false }, 'observe-workers': { type: 'boolean', default: false }, 'capture-before-destroy': { type: 'boolean', default: false } } })
|
||||
diagnose({ version: values.version, iterations: Number(values.iterations), transport: values.transport, teardown: values.teardown, worker: !values['no-worker'], host: values.host, plugin: values.plugin, sdkLogs: values['sdk-logs'], workerObserver: values['observe-workers'], captureBeforeDestroy: values['capture-before-destroy'] }).then((results) => {
|
||||
const { values } = parseArgs({ options: { 'version': { type: 'string', default: '1.7.2' }, 'iterations': { type: 'string', default: '5' }, 'transport': { type: 'string', default: 'http' }, 'teardown': { type: 'string', default: 'reset-first' }, 'host': { type: 'string', default: 'direct' }, 'no-worker': { type: 'boolean', default: false }, 'plugin': { type: 'boolean', default: false }, 'sdk-logs': { type: 'boolean', default: false }, 'observe-workers': { type: 'boolean', default: false }, 'capture-before-destroy': { type: 'boolean', default: false }, 'switch-boundary': { type: 'string', default: 'switched' }, 'prefill': { type: 'boolean', default: false } } })
|
||||
diagnose({ version: values.version, iterations: Number(values.iterations), transport: values.transport, teardown: values.teardown, worker: !values['no-worker'], host: values.host, plugin: values.plugin, sdkLogs: values['sdk-logs'], workerObserver: values['observe-workers'], captureBeforeDestroy: values['capture-before-destroy'], switchBoundary: values['switch-boundary'], prefill: values.prefill }).then((results) => {
|
||||
process.exitCode = results.some(result => result.status !== 'passed') ? 1 : 0
|
||||
}).catch((error) => {
|
||||
console.error(error)
|
||||
|
||||
+3
-1
@@ -2149,7 +2149,9 @@
|
||||
"baselines/hls-sdk-validation.json",
|
||||
"baselines/hls-sdk-diagnostics.json",
|
||||
"changes/2026-09-14-PKG-HLS-SDK-01-http-teardown.md",
|
||||
"baselines/hls-http-teardown-validation.json"
|
||||
"baselines/hls-http-teardown-validation.json",
|
||||
"changes/2026-09-15-PKG-HLS-SDK-01-switch-boundary.md",
|
||||
"baselines/hls-switch-boundary-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -41,12 +41,18 @@ test.beforeAll(async () => {
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
const state = await page.evaluate(() => {
|
||||
const video = window.art?.video
|
||||
const hls = window.art?.hls
|
||||
const ranges = buffered => buffered && Array.from({ length: buffered.length }, (_, index) => [buffered.start(index), buffered.end(index)])
|
||||
const controller = source => source && ({ state: source.state, level: source.level, nextLoadPosition: source.nextLoadPosition, altAudio: source.altAudio, buffered: ranges(source.mediaBuffer?.buffered), frag: source.fragCurrent && { sn: source.fragCurrent.sn, level: source.fragCurrent.level, type: source.fragCurrent.type } })
|
||||
return {
|
||||
workers: window.workerEvidence,
|
||||
errors: window.sdkErrors,
|
||||
events: window.sdkEvents,
|
||||
supported: window.Hls?.isSupported(),
|
||||
destroyed: window.destroyedEngines,
|
||||
controllers: { main: controller(hls?.streamController), audio: controller(hls?.audioStreamController) },
|
||||
sdk: !window.destroyedEngines && hls ? { currentLevel: hls.currentLevel, loadLevel: hls.loadLevel, nextLoadLevel: hls.nextLoadLevel, mediaSourceState: hls.bufferController?.mediaSource?.readyState } : { destroyed: window.destroyedEngines },
|
||||
buffers: hls?.bufferController?.sourceBuffers?.map(([type, source]) => ({ type, updating: source?.updating, buffered: ranges(source?.buffered) })),
|
||||
tracks: window.art?.hls?.audioTracks?.map(track => ({ id: track.id, name: track.name, groupId: track.groupId })),
|
||||
media: video && { time: video.currentTime, paused: video.paused, ended: video.ended, readyState: video.readyState, height: video.videoHeight, buffered: Array.from({ length: video.buffered.length }, (_, index) => [video.buffered.start(index), video.buffered.end(index)]) },
|
||||
}
|
||||
@@ -81,8 +87,8 @@ async function open(page, testInfo, release, core, plugin = 'candidate', manifes
|
||||
art.hls = hls
|
||||
const mediaState = () => ({ time: video.currentTime, paused: video.paused, readyState: video.readyState, height: video.videoHeight, buffered: Array.from({ length: video.buffered.length }, (_, index) => [video.buffered.start(index), video.buffered.end(index)]) })
|
||||
hls.on(window.Hls.Events.ERROR, (_, data) => window.sdkErrors.push({ type: data.type, details: data.details, fatal: data.fatal, media: mediaState() }))
|
||||
for (const key of ['AUDIO_TRACKS_UPDATED', 'AUDIO_TRACK_SWITCHED', 'LEVEL_SWITCHED', 'MEDIA_ATTACHED', 'MEDIA_DETACHED'])
|
||||
hls.on(window.Hls.Events[key], (_, data) => window.sdkEvents.push({ key, id: data.id, level: data.level, tracks: data.audioTracks?.map(track => ({ id: track.id, name: track.name, groupId: track.groupId })), media: mediaState() }))
|
||||
for (const key of ['AUDIO_TRACKS_UPDATED', 'AUDIO_TRACK_SWITCHED', 'LEVEL_SWITCHED', 'MEDIA_ATTACHED', 'MEDIA_DETACHED', 'BUFFER_FLUSHING', 'BUFFER_FLUSHED', 'FRAG_LOADING', 'FRAG_BUFFERED'])
|
||||
hls.on(window.Hls.Events[key], (_, data) => window.sdkEvents.push({ key, id: data.id, level: data.level, type: data.type, frag: data.frag && { sn: data.frag.sn, level: data.frag.level, type: data.frag.type }, tracks: data.audioTracks?.map(track => ({ id: track.id, name: track.name, groupId: track.groupId })), media: mediaState() }))
|
||||
hls.on(window.Hls.Events.DESTROYING, () => window.destroyedEngines++)
|
||||
hls.loadSource(url)
|
||||
hls.attachMedia(video)
|
||||
@@ -112,6 +118,14 @@ async function destroyed(page) {
|
||||
expect(await page.evaluate(() => ({ media: window.art.hls.media, children: document.querySelector('.player').childElementCount, errors: window.sdkErrors.filter(error => error.fatal) }))).toEqual({ media: null, children: 0, errors: [] })
|
||||
}
|
||||
|
||||
async function playbackAdvances(page, height) {
|
||||
const start = await page.evaluate(() => ({ time: window.art.video.currentTime, frames: window.art.video.getVideoPlaybackQuality().totalVideoFrames }))
|
||||
await expect.poll(() => page.evaluate(({ start, height }) => {
|
||||
const video = window.art.video
|
||||
return !video.paused && video.videoHeight === height && video.currentTime > start.time + 0.3 && video.getVideoPlaybackQuality().totalVideoFrames > start.frames + 2
|
||||
}, { start, height })).toBe(true)
|
||||
}
|
||||
|
||||
for (const release of matrix.releases) {
|
||||
test(`SDK ${release.version}: MSE capability and unsupported cleanup`, async ({ page, browserName }, testInfo) => {
|
||||
await open(page, testInfo, release, 'candidate')
|
||||
@@ -165,6 +179,7 @@ for (const release of matrix.releases) {
|
||||
await expect.poll(() => page.evaluate(() => window.art.hls.audioTracks.map(track => ({ id: track.id, name: track.name, group: track.groupId })))).toEqual([{ id: 0, name: 'French', group: 'high' }, { id: 1, name: 'English', group: 'high' }, { id: 2, name: 'Commentary', group: 'high' }])
|
||||
await expect.poll(() => page.evaluate(() => ({ index: window.art.hls.audioTrack, height: window.art.video.videoHeight }))).toEqual({ index: 0, height: 180 })
|
||||
await expect(page.locator('.art-control-hls-audio .art-selector-value')).toHaveText('French')
|
||||
await playbackAdvances(page, 180)
|
||||
await page.evaluate(() => {
|
||||
window.art.controls.show = true
|
||||
})
|
||||
@@ -179,6 +194,7 @@ for (const release of matrix.releases) {
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.hls.audioTracks.length)).toBe(2)
|
||||
await expect(control.locator('.art-selector-item')).toHaveCount(2)
|
||||
await playbackAdvances(page, 90)
|
||||
await destroyed(page)
|
||||
})
|
||||
test(`${core} core: actual SDK detaches and reattaches to the same media`, async ({ page }, testInfo) => {
|
||||
|
||||
Reference in new issue
Block a user