test(dash): [PKG-DASH-05] isolate SDK paused-seek buffer metric deadlock

This commit is contained in:
Harvey Zhao committed 2026-09-12 06:51:25 +08:00
1 parent a9cf50bc4e
commit bafbf2c8b0
12 files changed
+3921 -21

No files matched your search

@@ -151,8 +151,12 @@ devices and installed package acceptance remain separate release gates.
`dash-sdk.spec.js` now separately loads hash-pinned npm dash.js 4.5.2/5.2.1 and local
multi-quality/multi-audio DASH segments. It covers decoding, explicit update after
external selection, source topology replacement, retained callbacks and SDK ownership.
It also includes a native SDK control without an ArtPlayer instance. Early SDK 4.5.2
paused-seek stalls remain open; later passes do not resolve them. Unsupported MSE is
It also includes a native SDK control without an ArtPlayer instance. SDK 4.5.2
paused-seek stalls reproduce independently: its empty buffer-clear path preserves
stale buffer metrics and can prevent further scheduling. A diagnostic backport of
the SDK 5 buffer-level refresh confirms the cause; no production workaround ships.
See the [diagnosis](../../refactor/changes/2026-09-12-PKG-DASH-05-seek-diagnosis.md).
The compatibility risk remains open. Unsupported MSE is
recorded as a playback capability gap, not acceptance. SDK event tests also cover
external selections without explicit update, same-SDK source replacement, asynchronous
formatter error/recovery, and actual setting clicks during SDK refresh.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,68 @@
# PKG-DASH-05:暂停 seek 停滞的 SDK 根因
起点 `a9cf50bc`。本批没有修改生产源码、公开声明、依赖或产物。DASH-SEEK-01 从
“偶发且来源未知”推进到可独立复现的 SDK 4.5.2 调度缺陷;**风险仍 open**,不能把诊断
补丁的通过当成实际发布 SDK 或本项目的修复。DASH-05 继续 doing。
## 证据链
1. 给现有真实 SDK 测试增加原生 SourceBuffer 观测。原生 addSourceBuffer 仍以相同 this、
参数执行一次,返回原对象;额外监听 updateend 并读取缓冲,不改写媒体时间或片段。
首次 10 项中 9 通过、1 失败,再次复现新核心/已发布插件的 6 秒停滞。
2. 失败时视频 SourceBuffer 与 video.buffered 均止于 5.999999 秒,排除 Chromium 上
“SourceBuffer 其实已覆盖 6 秒”的猜测。SDK 仍保留暂停前约 2.975 秒的视频缓冲指标。
3. 加入实际 SDK seekTime、timeupdate、bufferLevelUpdated 和处理器状态:10 项中
8 通过、2 失败,新旧核心搭配发布插件均失败。SDK 收到的目标为 6;视频 hasTarget=false、
pruning=false、complete=false,旧缓冲量约 2.96 秒;seek 后没有后续时间/缓冲更新。
4. 原生 SDK 对照不创建 ArtPlayer,只使用同一媒体、相同 SDK 设置和选择顺序。等待音轨
渲染完成,在约 3 秒暂停,并观察半秒内没有时钟/追加事件,再 seek 到 6 秒。
实际 npm 4.5.2 在 Chromium/Firefox 都失败,5.2.1 两边通过。该等待用于建立真实暂停
稳态、排除仍排队的进度更新,没有延长恢复超时或改变目标。
5. 仅在上述断言失败后,启用诊断开关注入一次原生元素 timeupdate:SDK 4 视频指标立即
从 2.81/2.748 变成 0,随后请求下一视频片段,buffered 延长到约 8 秒,播放越过 6.2。
原测试仍记为失败,合成事件没有进入生产实现或默认验证路径。
6. 读取校验过完整 npm 归档的 debug 成员:4.5.2 的 BufferController.clearBuffers 在
ranges 为空时直接 resolve;5.2.1 同一分支先调用 _updateBufferLevel。
4.x ScheduleController 以缓存指标是否小于目标决定请求;失败时视频指标约 2.8 而
目标为 2,即使当前位置实际缺帧也不会下载。无后续 progress/timeupdate 时形成停滞。
7. 保持 SDK 4 debug 字节不变的原生对照 1 通过/1 失败;仅在空裁剪分支增加
_updateBufferLevel 的诊断补丁,原生及新旧核心/插件组合 10 项全部通过。
这是因果对照,不是正式 SDK 的发布准入。
Firefox 的视频范围恰好止于 6,SDK 的包含端点判断返回 true,但解码下一帧仍需下一片段。
该路径的 pruneBuffer 也经过空 clearBuffers;相同缓冲量刷新解释了两引擎的恢复。
这批证据说明是 SDK 调度所需数据未更新,不是 ArtPlayer 新核心修改了 seek 目标;历史
Firefox/候选插件失败仍保留,不能据此声称所有其他 DASH 风险已经解决。
## 测试入口与隔离
默认 `yarn test:browser test/browser/dash-sdk.spec.js` 仍加载原始固定 npm min 产物。
新增 `test/browser/dash-buffer-observer.js` 保存浏览器缓冲、SDK 指标及可用处理器状态。
SDK 内部诊断 getter 不可用时只标记 unavailable;不通过修改 SDK 来模拟这些状态。
每页新建后安装观测器,页面关闭释放资源,不复用到产品 demo。
原生复现可用 `--grep 'native SDK' --project chromium --project firefox --trace on`。
诊断环境变量(用完应清空;每次运行前后归档共享报告和 traces):
| 变量 | 值与用途 |
| --- | --- |
| ARTPLAYER_DASH_DIAGNOSE_STALL | 1:已失败且停在 6 秒时,保存合成时间更新的前后状态,不改变失败结果 |
| ARTPLAYER_DASH_DIAGNOSTIC_SDK | none/未设置:原始 min;upstream4:原始 SDK 4 debug;bufferlevel4:单分支诊断补丁 |
debug 成员来自完整性校验后的 npm 归档;补丁仅在内存替换唯一匹配的完整分支,匹配数
不是一就报错。输入附件记录原始/有效代码 SHA256、member、模式和 acceptance=false。
不得在正式报告中省略诊断模式,不得把补丁归档冒充上游包,也不得将合成事件作为产品修复。
过程中一次括号笔误导致测试发现失败,日志保留;修正语法后继续取证。加入 seekTime 的
中间运行 10 项通过,但没有以它关闭风险。全部失败、通过和对照的报告/trace 分别冻结,
见 [机器证据](../baselines/dash-seek-diagnosis.json)。本地缓存不提交,测试和摘要可重跑。
## 后续与回退
旧 SDK 合法调用仍须兼容;不能要求用户升级 SDK 后才算重构完成。后续应评审隔离且不改变
调用方媒体事件/SDK 所有权的兼容处置,或正式记录第三方边界与发布处理。在此之前,风险和
设备/完整分发门槛保持开放。不得在核心伪造 timeupdate、偏移 seek 或全局放宽测试。
局部 lint、既有 SDK/媒体来源测试及计划/风险链接校验用于本批验证;没有改生产代码,
不重复宣称完成全项目 CI 或 114 项完整浏览器验收。回退本批仅涉及诊断测试与文档,
生产产物与 a9cf50bc 的指纹保持一致。
+8
View File
@@ -79,3 +79,11 @@ UMD 测试 VM 显式注入宿主 console,以检查原始错误身份;没有
纯配置修改但没有 SDK 事件时,保留同步 update 为即时刷新入口;播放 tick 仅检查 Auto
布尔值变化,没有新定时器。真实 SDK/媒体验证与剩余 seek/设备边界见
[事件改造记录](changes/2026-09-12-PKG-DASH-05-sdk-events.md)。历史失败证据保留,后续通过不抹去它们。
## 05 暂停 seek 诊断
实际 SDK 4.5.2 在不创建 ArtPlayer 的稳定暂停用例中也会停滞,SDK 5.2.1 的对应用例通过。
根因与空裁剪分支漏更新缓冲指标有关,DASH-SEEK-01 仍 open;没有发布兼容补丁。
`dash-buffer-observer.js` 记录原生 SourceBuffer/SDK 指标,两个显式诊断环境变量只用于
失败后的合成事件和内存 SDK 补丁对照,不能用于正式验收。命令、证据、限制和回退见
[seek 根因记录](changes/2026-09-12-PKG-DASH-05-seek-diagnosis.md)。
+1 -1
View File
@@ -483,4 +483,4 @@
- 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)
- PKG-DASH-04: [记录](changes/2026-09-12-PKG-DASH-04-types.md) [记录](baselines/dash-types-validation.json) [记录](dash-validation.md)
- PKG-DASH-05: [记录](changes/2026-09-12-PKG-DASH-05-sdk-checkpoint.md) [记录](baselines/dash-sdk.json) [记录](baselines/dash-sdk-checkpoint.json) [记录](dash-validation.md) [记录](changes/2026-09-12-PKG-DASH-05-sdk-types.md) [记录](baselines/dash-sdk-types-validation.json) [记录](changes/2026-09-12-PKG-DASH-05-sdk-events.md) [记录](baselines/dash-sdk-events-validation.json)
- PKG-DASH-05: [记录](changes/2026-09-12-PKG-DASH-05-sdk-checkpoint.md) [记录](baselines/dash-sdk.json) [记录](baselines/dash-sdk-checkpoint.json) [记录](dash-validation.md) [记录](changes/2026-09-12-PKG-DASH-05-sdk-types.md) [记录](baselines/dash-sdk-types-validation.json) [记录](changes/2026-09-12-PKG-DASH-05-sdk-events.md) [记录](baselines/dash-sdk-events-validation.json) [记录](changes/2026-09-12-PKG-DASH-05-seek-diagnosis.md) [记录](baselines/dash-seek-diagnosis.json)
+12
View File
@@ -1,5 +1,17 @@
# 进度与证据
## 当前实施:DASH-05 暂停 seek 根因已定位
真实 SourceBuffer、SDK seekTime/缓冲事件和处理器状态表明,SDK 4.5.2 在空裁剪分支未更新
暂停前缓冲量,调度器继续按旧值阻止下载。不创建 ArtPlayer 的稳定暂停用例在 Chromium/
Firefox 都复现;失败后合成一次 timeupdate 能恢复,测试仍保留失败。SDK 5.2.1 原生用例
两边通过;将其缓冲刷新移入 SDK 4 的单分支诊断补丁后,原生/新旧组合 10 项通过。
本批只增加诊断测试和证据,没有向生产注入事件、修补 SDK、增加依赖或改变产物。
DASH-SEEK-01 根因已知但兼容处置尚未完成,仍 open。DASH-05 仍 doing,217 项仍为
74 done、4 doing、139 todo。详见 [根因记录](changes/2026-09-12-PKG-DASH-05-seek-diagnosis.md)
和 [执行证据](baselines/dash-seek-diagnosis.json)。其余包的独立源码迁移可继续,发布门槛不降低。
## 当前实施:DASH-05 SDK 事件刷新源码改造
新增第六个严格 TS 模块 sdk-events:按 SDK 质量/音轨/流事件合并刷新菜单,保留同步
+1 -1
View File
@@ -140,4 +140,4 @@
| DASH-STATE-01 | resolved / 已复现 | DASH 当前项高亮使用真值 ID、音轨身份和标签去重,边界待核实 | PKG-DASH-02, PKG-DASH-03, PKG-DASH-05 |
| DASH-TYPE-01 | resolved / 源码/产物事实 | DASH 类型未表达默认工厂参数、SDK 回调对象和现代模块入口 | PKG-DASH-04 |
| DASH-DEMO-01 | open / 源码/产物事实 | DASH 示例仅新 SDK 且重复安装会累积销毁回调 | PKG-DASH-06 |
| DASH-SEEK-01 | open / 已复现 | 真实 dash.js 4.5.2 暂停 seek 到缓冲片段边界后可能持续 waiting | PKG-DASH-05, REVIEW-02 |
| DASH-SEEK-01 | open / 已复现 | dash.js 4.5.2 空裁剪漏更新缓冲量,稳定暂停 seek 后调度停滞 | PKG-DASH-05, REVIEW-02 |
+6 -3
View File
@@ -2964,7 +2964,7 @@
},
{
"id": "DASH-SEEK-01",
"title": "真实 dash.js 4.5.2 暂停 seek 到缓冲片段边界后可能持续 waiting",
"title": "dash.js 4.5.2 空裁剪漏更新缓冲量,稳定暂停 seek 后调度停滞",
"confirmation": "reproduced",
"status": "open",
"owners": [
@@ -2974,9 +2974,12 @@
"evidence": [
"test/browser/dash-sdk.spec.js",
"refactor/baselines/dash-sdk-checkpoint.json",
"refactor/changes/2026-09-12-PKG-DASH-05-sdk-checkpoint.md"
"refactor/changes/2026-09-12-PKG-DASH-05-sdk-checkpoint.md",
"refactor/changes/2026-09-12-PKG-DASH-05-seek-diagnosis.md",
"refactor/baselines/dash-seek-diagnosis.json",
"test/browser/dash-buffer-observer.js"
],
"compatibleResolution": "先比较实际 SDK、SourceBuffer/video 缓冲与暂停 seek 调度;不偏移公开 seek 目标,不吞异常或增加超时伪造通过。",
"compatibleResolution": "已用原生 SDK、SourceBuffer/调度指标和 SDK 5 空裁剪刷新的一行诊断回植确认根因。兼容处置尚未落地;不在核心伪造 timeupdate、不偏移 seek、不强制所有旧用户升级 SDK,也不将诊断补丁计为正式上游通过。",
"closureCriteria": "原失败的新旧核心/插件组合有确定根因与对应修复/可验证环境边界;原生对照及原顺序、原目标回归成立,后续偶然通过不能关闭。"
}
]
+3 -1
View File
@@ -2070,7 +2070,9 @@
"changes/2026-09-12-PKG-DASH-05-sdk-types.md",
"baselines/dash-sdk-types-validation.json",
"changes/2026-09-12-PKG-DASH-05-sdk-events.md",
"baselines/dash-sdk-events-validation.json"
"baselines/dash-sdk-events-validation.json",
"changes/2026-09-12-PKG-DASH-05-seek-diagnosis.md",
"baselines/dash-seek-diagnosis.json"
]
},
{
+1 -1
View File
@@ -1054,7 +1054,7 @@
"refactor/scripts/dash-sdk-types.test.mjs"
],
"contract": "Caller owns art.dash. Candidate adapts the released 4.x quality method surface and workspace 5.x stable representation ID surface. Controlled contracts and DOM validation pass; actual pinned 4.5.2/5.2.1 SDK media remains PKG-DASH-05.",
"validationStatus": "Pinned npm SDK 4.5.2/5.2.1 main/legacy media, automatic state refresh, same-SDK source replacement, settings selection and formatter recovery exercised. DASH-SEEK-01 and Windows WebKit MSE gap remain. Actual SDK declarations and exact peer closure tested across ten compiler/module configurations, with no added plugin errors and upstream-only Node10/old DOM limitations. Devices and full distribution remain incomplete; see refactor/baselines/dash-sdk-events-validation.json.",
"validationStatus": "Pinned npm SDK 4.5.2/5.2.1 main/legacy media, automatic state refresh, same-SDK source replacement, settings selection and formatter recovery exercised. DASH-SEEK-01 is isolated to SDK 4 stale buffer metrics in its empty-clear branch using native playback and a diagnostic SDK 5 backport; no production mitigation ships and the risk remains open (refactor/baselines/dash-seek-diagnosis.json). Windows WebKit MSE, devices and full distribution remain incomplete. Actual SDK declarations and peer closure retain the ten-configuration matrix and upstream-only Node10/old DOM limits.",
"demoPathsSource": "refactor/baselines/demo-inventory.json"
},
{
+50
View File
@@ -0,0 +1,50 @@
// Runs inside the page. Every SDK operation still uses the native SourceBuffer.
export function observeDashBuffers() {
const buffers = []
const events = []
const ranges = value => Array.from({ length: value.length }, (_, index) => [value.start(index), value.end(index)])
function snapshot() {
const video = window.art?.video || window.nativeVideo
const dash = window.art?.dash || window.nativeDash
const metrics = dash?.getDashMetrics()
let processors
try {
processors = dash?.getActiveStream()?.getProcessors().map((processor) => {
const buffer = processor.getBufferController()
return { type: processor.getType(), level: processor.getBufferLevel(), target: processor.getScheduleController().getBufferTarget(), complete: processor.isBufferingCompleted(), pruning: buffer.getIsPruningInProgress(), hasTarget: video ? buffer.hasBufferAtTime(video.currentTime) : null }
})
}
catch (error) {
// SDK initialization/teardown and generations expose different internals.
processors = { unavailable: String(error) }
}
return {
time: video?.currentTime,
paused: video?.paused,
seeking: video?.seeking,
readyState: video?.readyState,
video: video ? ranges(video.buffered) : [],
levels: metrics ? { video: metrics.getCurrentBufferLevel('video'), audio: metrics.getCurrentBufferLevel('audio') } : null,
processors,
buffers: buffers.map(({ source, buffer, mime }) => {
try {
return { mime, sourceState: source.readyState, updating: buffer.updating, ranges: ranges(buffer.buffered), timestampOffset: buffer.timestampOffset }
}
catch (error) {
return { mime, sourceState: source.readyState, error: error.name }
}
}),
}
}
const mark = name => events.push({ name, at: performance.now(), ...snapshot() })
for (const Source of new Set([window.MediaSource, window.ManagedMediaSource].filter(Boolean))) {
const addSourceBuffer = Source.prototype.addSourceBuffer
Source.prototype.addSourceBuffer = function (...args) {
const buffer = Reflect.apply(addSourceBuffer, this, args)
buffers.push({ source: this, buffer, mime: args[0] })
buffer.addEventListener('updateend', () => mark('sourcebuffer:updateend'))
return buffer
}
}
window.dashBuffers = { snapshot, mark, events }
}
+60 -12
View File
@@ -3,6 +3,7 @@ import fs from 'node:fs'
import process from 'node:process'
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { observeDashBuffers } from './dash-buffer-observer.js'
import { expect, test } from './fixtures.js'
const sdks = new Map()
@@ -11,6 +12,8 @@ let candidate
let published
let mediaManifest
let pluginRelease
const diagnosticSDK = process.env.ARTPLAYER_DASH_DIAGNOSTIC_SDK || 'none'
assert(['none', 'upstream4', 'bufferlevel4'].includes(diagnosticSDK), 'Unknown diagnostic SDK mode')
test.beforeAll(async () => {
const baseline = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/dash-sdk.json', import.meta.url)))
@@ -18,7 +21,20 @@ test.beforeAll(async () => {
const archive = await ensureArchive(sdk.release)
const bytes = readMember(archive, sdk.codeMember)
assert.equal(hash(bytes), sdk.release.files[sdk.codeMember])
sdks.set(sdk.release.version, { ...sdk, code: bytes.toString() })
let code = bytes.toString()
let diagnostic
if (sdk.release.version === '4.5.2' && diagnosticSDK !== 'none') {
const member = 'package/dist/dash.all.debug.js'
code = readMember(archive, member).toString()
const upstreamSHA256 = hash(code)
if (diagnosticSDK === 'bufferlevel4') {
const before = 'function clearBuffers(ranges) {\n return new Promise(function (resolve, reject) {\n if (!ranges || !sourceBufferSink || ranges.length === 0) {\n resolve();'
assert.equal(code.split(before).length, 2, 'Diagnostic patch must match exactly one upstream branch')
code = code.replace(before, before.replace(' resolve();', ' _updateBufferLevel();\n resolve();'))
}
diagnostic = { mode: diagnosticSDK, member, upstreamSHA256, effectiveSHA256: hash(code), acceptance: false }
}
sdks.set(sdk.release.version, { ...sdk, code, diagnostic })
}
pluginRelease = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/dash-control-release.json', import.meta.url))).release
const bytes = readMember(await ensureArchive(pluginRelease), 'package/dist/artplayer-plugin-dash-control.js')
@@ -50,9 +66,21 @@ test.afterEach(async ({ page }, testInfo) => {
}
const video = art?.video || window.nativeVideo
const buffered = video ? Array.from({ length: video.buffered.length }, (_, index) => [video.buffered.start(index), video.buffered.end(index)]) : []
return { errors: window.sdkErrors, events: window.sdkEvents, mediaEvents: window.mediaEvents, nativePlay: window.nativePlay, tracks, levels, buffered, paused: video?.paused, seeking: video?.seeking, time: video?.currentTime, ready: art?.isReady, width: video?.videoWidth, height: video?.videoHeight, error: video?.error?.code, readyState: video?.readyState, destroyed: window.sdkDestroyed }
return { errors: window.sdkErrors, events: window.sdkEvents, mediaEvents: window.mediaEvents, nativePlay: window.nativePlay, tracks, levels, buffered, paused: video?.paused, seeking: video?.seeking, time: video?.currentTime, ready: art?.isReady, width: video?.videoWidth, height: video?.videoHeight, error: video?.error?.code, readyState: video?.readyState, destroyed: window.sdkDestroyed, bufferState: window.dashBuffers?.snapshot(), bufferEvents: window.dashBuffers?.events }
}).catch(error => ({ error: error.message }))
await testInfo.attach('dash-sdk-state', { contentType: 'application/json', body: JSON.stringify(state) })
// Opt-in counterfactual after a failed assertion; it never turns a failure green.
if (process.env.ARTPLAYER_DASH_DIAGNOSE_STALL === '1' && testInfo.status !== testInfo.expectedStatus && state.seeking && state.time === 6) {
const recovery = await page.evaluate(() => {
const before = window.dashBuffers.snapshot()
const video = window.art?.video || window.nativeVideo
video.dispatchEvent(new Event('timeupdate'))
return { before, afterDispatch: window.dashBuffers.snapshot() }
})
recovery.advanced = await page.waitForFunction(() => (window.art?.video || window.nativeVideo).currentTime > 6.2, null, { timeout: 3000 }).then(() => true, () => false)
recovery.final = await page.evaluate(() => window.dashBuffers.snapshot())
await testInfo.attach('diagnostic-synthetic-timeupdate', { contentType: 'application/json', body: JSON.stringify(recovery) })
}
if (!page.isClosed()) {
await page.evaluate(() => {
if (window.art && !window.art.isDestroy)
@@ -65,9 +93,10 @@ test.afterEach(async ({ page }, testInfo) => {
async function loadSDK(page, version, core, testInfo) {
const sdk = sdks.get(version)
await page.goto(`/test/player.html?core=${core}`)
await page.evaluate(observeDashBuffers)
await page.addScriptTag({ content: sdk.code })
const capability = await page.evaluate(() => ({ sdk: window.dashjs.supportsMediaSource(), mse: typeof window.MediaSource, managed: typeof window.ManagedMediaSource, avc: Boolean(window.MediaSource?.isTypeSupported('video/mp4; codecs="avc1.42c01e"')), aac: Boolean(window.MediaSource?.isTypeSupported('audio/mp4; codecs="mp4a.40.2"')) }))
await testInfo.attach('dash-sdk-inputs', { contentType: 'application/json', body: JSON.stringify({ sdk: sdk.release, codeMember: sdk.codeMember, pluginRelease, candidateSHA256: hash(candidate), candidate: process.env.ARTPLAYER_DASH_ARTIFACT || 'workspace source build', media: mediaManifest, capability }) })
await testInfo.attach('dash-sdk-inputs', { contentType: 'application/json', body: JSON.stringify({ sdk: sdk.release, codeMember: sdk.diagnostic?.member || sdk.codeMember, diagnostic: sdk.diagnostic, pluginRelease, candidateSHA256: hash(candidate), candidate: process.env.ARTPLAYER_DASH_ARTIFACT || 'workspace source build', media: mediaManifest, capability }) })
return capability
}
@@ -98,16 +127,19 @@ async function openDash(page, version, core, plugin, testInfo) {
dash.updateSettings({ debug: { logLevel: 0 }, streaming: { abr: { initialBitrate: { video: 150 } }, buffer: { fastSwitchEnabled: true, bufferTimeDefault: 2, bufferTimeAtTopQuality: 2 } } })
const events = window.dashjs.MediaPlayer.events
dash.on(events.ERROR, event => window.sdkErrors.push({ code: event.error?.code, message: event.error?.message, event: event.type }))
for (const name of ['STREAM_INITIALIZED', 'QUALITY_CHANGE_REQUESTED', 'QUALITY_CHANGE_RENDERED', 'TRACK_CHANGE_RENDERED', 'STREAM_TEARDOWN_COMPLETE', 'BUFFER_LEVEL_STATE_CHANGED', 'FRAGMENT_LOADING_COMPLETED', 'PLAYBACK_SEEKING', 'PLAYBACK_SEEKED']) {
for (const name of ['STREAM_INITIALIZED', 'QUALITY_CHANGE_REQUESTED', 'QUALITY_CHANGE_RENDERED', 'TRACK_CHANGE_RENDERED', 'STREAM_TEARDOWN_COMPLETE', 'BUFFER_LEVEL_STATE_CHANGED', 'BUFFER_LEVEL_UPDATED', 'FRAGMENT_LOADING_COMPLETED', 'PLAYBACK_SEEKING', 'PLAYBACK_SEEKED']) {
if (events[name])
dash.on(events[name], event => window.sdkEvents.push({ name, at: performance.now(), mediaType: event.mediaType, oldQuality: event.oldQuality, newQuality: event.newQuality, state: event.state, url: event.request?.url, time: video.currentTime }))
dash.on(events[name], event => window.sdkEvents.push({ name, at: performance.now(), mediaType: event.mediaType, oldQuality: event.oldQuality, newQuality: event.newQuality, state: event.state, bufferLevel: event.bufferLevel, url: event.request?.url, time: video.currentTime, seekTime: event.seekTime }))
}
dash.initialize(video, url, false)
} },
})
const art = window.art
for (const name of ['play', 'playing', 'pause', 'seeking', 'seeked', 'waiting', 'emptied', 'loadedmetadata', 'loadeddata', 'canplay', 'error']) {
art.video.addEventListener(name, () => window.mediaEvents.push({ name, at: performance.now(), time: art.video.currentTime, paused: art.video.paused, readyState: art.video.readyState, buffered: Array.from({ length: art.video.buffered.length }, (_, index) => [art.video.buffered.start(index), art.video.buffered.end(index)]) }))
for (const name of ['play', 'playing', 'pause', 'seeking', 'seeked', 'waiting', 'timeupdate', 'emptied', 'loadedmetadata', 'loadeddata', 'canplay', 'error']) {
art.video.addEventListener(name, () => {
window.mediaEvents.push({ name, at: performance.now(), time: art.video.currentTime, paused: art.video.paused, readyState: art.video.readyState, buffered: Array.from({ length: art.video.buffered.length }, (_, index) => [art.video.buffered.start(index), art.video.buffered.end(index)]) })
window.dashBuffers.mark(`media:${name}`)
})
}
art.on('destroy', () => {
art.dash.destroy()
@@ -164,7 +196,9 @@ for (const core of ['published', 'candidate']) {
await page.locator('#pause').click()
expect(await page.evaluate(() => window.art.video.paused)).toBe(true)
await page.evaluate(() => {
window.dashBuffers.mark('before-seek')
window.art.seek = 6
window.dashBuffers.mark('assigned-seek')
})
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(6.2)
@@ -192,11 +226,14 @@ for (const version of ['4.5.2', '5.2.1']) {
dash.updateSettings({ debug: { logLevel: 0 }, streaming: { abr: { initialBitrate: { video: 150 } }, buffer: { fastSwitchEnabled: true, bufferTimeDefault: 2, bufferTimeAtTopQuality: 2 } } })
const events = window.dashjs.MediaPlayer.events
dash.on(events.ERROR, event => window.sdkErrors.push({ code: event.error?.code, message: event.error?.message }))
for (const name of ['FRAGMENT_LOADING_COMPLETED', 'TRACK_CHANGE_RENDERED', 'QUALITY_CHANGE_RENDERED', 'BUFFER_LEVEL_STATE_CHANGED', 'PLAYBACK_SEEKING', 'PLAYBACK_SEEKED']) {
dash.on(events[name], event => window.sdkEvents.push({ name, at: performance.now(), mediaType: event.mediaType, state: event.state, url: event.request?.url, time: video.currentTime }))
for (const name of ['FRAGMENT_LOADING_COMPLETED', 'TRACK_CHANGE_RENDERED', 'QUALITY_CHANGE_RENDERED', 'BUFFER_LEVEL_STATE_CHANGED', 'BUFFER_LEVEL_UPDATED', 'PLAYBACK_SEEKING', 'PLAYBACK_SEEKED']) {
dash.on(events[name], event => window.sdkEvents.push({ name, at: performance.now(), mediaType: event.mediaType, state: event.state, bufferLevel: event.bufferLevel, url: event.request?.url, time: video.currentTime, seekTime: event.seekTime }))
}
for (const name of ['play', 'playing', 'pause', 'seeking', 'seeked', 'waiting']) {
video.addEventListener(name, () => window.mediaEvents.push({ name, at: performance.now(), time: video.currentTime, paused: video.paused, readyState: video.readyState, buffered: Array.from({ length: video.buffered.length }, (_, index) => [video.buffered.start(index), video.buffered.end(index)]) }))
for (const name of ['play', 'playing', 'pause', 'seeking', 'seeked', 'waiting', 'timeupdate']) {
video.addEventListener(name, () => {
window.mediaEvents.push({ name, at: performance.now(), time: video.currentTime, paused: video.paused, readyState: video.readyState, buffered: Array.from({ length: video.buffered.length }, (_, index) => [video.buffered.start(index), video.buffered.end(index)]) })
window.dashBuffers.mark(`media:${name}`)
})
}
dash.initialize(video, '/dash-fixture/master.mpd', false)
document.querySelector('#play').onclick = () => {
@@ -222,16 +259,27 @@ for (const version of ['4.5.2', '5.2.1']) {
})
await expect.poll(() => page.evaluate(() => window.nativeVideo.videoHeight)).toBe(180)
// Match the failed integrated run: video data ends at the next seek target.
await expect.poll(() => page.evaluate(() => window.nativeVideo.buffered.end(window.nativeVideo.buffered.length - 1))).toBeCloseTo(6, 4)
await expect.poll(() => page.evaluate(() => {
const video = window.nativeVideo
return video.currentTime >= 2.8 && Math.abs(video.buffered.end(video.buffered.length - 1) - 6) < 0.0001
})).toBe(true)
await page.evaluate(() => {
const dash = window.nativeDash
dash.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: true } } } })
dash.setCurrentTrack(dash.getTracksFor('audio').find(track => track.lang === 'fr'))
})
await expect.poll(() => page.evaluate(() => window.nativeDash.getCurrentTrackFor('audio').lang)).toBe('fr')
// The recorded integrated stall happened after the audio switch was rendered.
await expect.poll(() => page.evaluate(() => window.sdkEvents.some(event => event.name === 'TRACK_CHANGE_RENDERED' && event.mediaType === 'audio'))).toBe(true)
await page.locator('#pause').click()
// Observe a genuinely held pause: no clock/append events for half a second.
// This separates the seek from progress events still queued by the track switch.
await page.waitForFunction(() => window.nativeVideo.paused && performance.now() - window.dashBuffers.events.at(-1).at >= 500)
expect(await page.evaluate(() => window.nativeVideo.paused)).toBe(true)
await page.evaluate(() => {
window.dashBuffers.mark('before-seek')
window.nativeVideo.currentTime = 6
window.dashBuffers.mark('assigned-seek')
})
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.nativeVideo.currentTime)).toBeGreaterThan(6.2)