Skip to content

fix(CMCD): Re-arm the reporter on every load() - #18

Open
littlespex wants to merge 5 commits into
mainfrom
claude/issue-10414-investigation-e197f5
Open

fix(CMCD): Re-arm the reporter on every load()#18
littlespex wants to merge 5 commits into
mainfrom
claude/issue-10414-investigation-e197f5

Conversation

@littlespex

Copy link
Copy Markdown
Member

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 CmcdManager entry point early-returns when the internal CmcdReporter 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 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()

  • New idempotent 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 explicit unload() then load().
  • Rejected alternative: re-arming inside 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

  • Pre-existing latent bug, also from the CML refactor: 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. sta dedup via lastPlayerState_ masked the doubling. Now mirrors reset() 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 for onLoad(), plus 2 duplicate-listener specs.
  • test/util/cmcd_integration.js: real-player repro of the issue. Two sequential load()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/86
  • build/test.py --quick: 3362/3362
  • build/check.py --force: clean
  • eslint on changed files: clean

Known 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

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().
Comment thread lib/util/cmcd_manager.js Outdated
Comment on lines +140 to +146
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();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not call reset() here instead?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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").

Comment thread lib/util/cmcd_manager.js Outdated
Comment on lines +193 to +196
this.maybeStartReporter_();
if (this.reporter_) {
this.setupEventListeners_();
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

littlespex and others added 3 commits August 7, 2026 14:37
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant