fix(CMCD): Re-arm the reporter on every load() - #18
Conversation
Since the CMCD rewrite in shaka-project#10060 (5.2.0), every CmcdManager entry point early-returns when the internal reporter is null. The reporter is torn down by reset() on every unload, including the internal unload that precedes every load() after the first, but it was only re-created by setMediaElement() (attach to a new element) or configure(). Nothing in the plain load() path re-created it, so CMCD went silent for every load() after the first unless the app happened to call configure() again. Add an idempotent CmcdManager.onLoad() that restarts the reporter and re-attaches its event listeners when they were torn down, and call it from Player.load() before the new asset's first request goes out (the mime-type HEAD probe, the manifest fetch via the internal PreloadManager, or src= URL decoration). Fixes shaka-project#10414
configure() tears the reporter down on an enabled toggle or a material config change, but left the previously registered video, player, and document listeners in place. The rebuild then registered a second set, so recordEvent-based events (mute, unmute, player expand and collapse) were reported once per registration. State-change updates (sta) are deduped by lastPlayerState_, which masked the doubling. Clear the listeners in the teardown branch, mirroring reset().
| this.reporter_.stop(true); | ||
| this.reporter_ = null; | ||
| this.lastPlayerState_ = null; | ||
| // Listeners pair with the reporter they were registered for; | ||
| // clear them so the rebuild below (or a later re-enable) does | ||
| // not register a second set and double-report events. | ||
| this.eventManager_.removeAll(); |
There was a problem hiding this comment.
Why not call reset() here instead?
There was a problem hiding this comment.
Done in 67d52e3. reset() is a superset of what this branch did, and the extra session-scoped clearing turned out to matter: the manifest path only pushes sf into the reporter when it differs from the cached sf_, so a reporter rebuilt after a material config change was never re-taught sf under the inline version. Added a unit test pinning that ("rebuilt reporter re-learns sf after a material config change").
| this.maybeStartReporter_(); | ||
| if (this.reporter_) { | ||
| this.setupEventListeners_(); | ||
| } |
There was a problem hiding this comment.
All calls to maybeStartReporter_ are immediately followed by the this.setupEventListeners_ block. Should this be moved into maybeStartReporter_? Or perhaps the entire block be turned into a ensureReporter_ function:
ensureReporter_() {
this.maybeStartReporter_();
if (this.reporter_) {
this.setupEventListeners_();
}
}
NOTE: ensureReporter_ may not be the best name. See if the code base has any existing naming conventions for this type of function.
There was a problem hiding this comment.
Folded setupEventListeners_() into maybeStartReporter_() in 67d52e3. It runs only when a reporter was actually created, and all three call sites (setMediaElement, configure, onLoad) collapse to bare maybeStartReporter_() calls with their redundant reporter/video guards removed.
On naming: the codebase convention for this shape is maybe*_ (maybeRetainResolvedAssetList_, maybeResetAssetListsOnSeek_, maybeSendMediaInfoMessage_); ensure* only appears as ensureSpace_/ensureNotDestroyed. Since the existing maybeStartReporter_ name still describes the folded behavior, no new function was needed.
Review feedback on the staging PR: - configure()'s teardown branch now delegates to reset() instead of duplicating its body. Besides removing duplication, this clears session-scoped state that the inline version missed: the manifest path only pushes sf into the reporter when it differs from the cached sf_, so a reporter rebuilt after a material config change was never re-taught sf. reset() clears the cache and the next manifest request re-teaches it (new unit test pins this). - Every maybeStartReporter_() call site repeated the same wire-up- listeners block; the listener wiring now lives inside maybeStartReporter_(), which only runs when a reporter was actually created. setMediaElement(), configure(), and onLoad() collapse to bare calls, and the redundant reporter/video guards at the call sites are gone.
Staging PR for review on the fork before submitting upstream.
Fixes the 5.2.0 regression reported in shaka-project#10414: CMCD stops being sent for every
load()after the first.Root cause
Since the CML refactor (shaka-project#10060), every
CmcdManagerentry point early-returns when the internalCmcdReporteris null. The reporter is torn down byreset()on every unload, including the internal unload that precedes everyload()after the first, but it was only re-created bysetMediaElement()(attach to a new element) orconfigure(). Nothing in the plainload()path re-created it, so CMCD went silent for the rest of the player's lifetime unless the app happened to reconfigure. The demo app resets and reapplies config before every asset load, which is why the bug never showed there.Changes
Commit 1: re-arm the reporter on every load()
CmcdManager.onLoad(): restarts the reporter and re-attaches its event listeners when they were torn down; no-ops when the reporter is already running, CMCD is disabled, or no media element is attached.Player.load()calls it right after the internal-unload block, before the new asset's first request goes out (mime-type HEAD probe, manifest fetch via the internal PreloadManager, or src= URL decoration). Covers MSE loads, src= loads, and explicitunload()thenload().reset()would leave an idle, unloaded player firing event-mode interval reports (CmcdReporter.start()arms interval timers and fires an immediate TIME_INTERVAL event).Commit 2: remove stale listeners on reconfigure
configure()'s teardown branch (enabled toggle or material config change) stopped the reporter but left listeners registered; the rebuild then registered a second set, so recordEvent-based events (mute, unmute, player expand and collapse) reported once per registration.stadedup vialastPlayerState_masked the doubling. Now mirrorsreset()by clearing listeners in the teardown branch.Tests
TDD: all 8 new specs were written first and observed failing for the expected reasons.
test/util/cmcd_manager_unit.js: 5 lifecycle specs foronLoad(), plus 2 duplicate-listener specs.test/util/cmcd_integration.js: real-player repro of the issue. Two sequentialload()s on one player; asserts the second load's manifest request carries valid CMCD with the same sid/cid. Deterministic (no timing waits):load()resolves after the manifest fetch and the recorder captures at request time.Verification
build/test.py --filter CmcdManager: 86/86build/test.py --quick: 3362/3362build/check.py --force: cleanKnown edge left open
preload()started while the player sits unloaded-idle still sends no CMCD on preload-time requests (5.1.x, being config-gated, would have applied it). Re-arming there would fire event-mode session-start reports for an asset that may never be loaded, so it is intentionally out of scope; the preloaded asset's post-load()requests do carry CMCD.🤖 Generated with Claude Code