Skip to content

feat(cmcdv2): implement rtp, lb, dl, pb, tpb, ttfb and ttlb#7895

Open
cotid-qualabs wants to merge 12 commits into
video-dev:masterfrom
qualabs:issue/cmcdv2-bitrate-keys
Open

feat(cmcdv2): implement rtp, lb, dl, pb, tpb, ttfb and ttlb#7895
cotid-qualabs wants to merge 12 commits into
video-dev:masterfrom
qualabs:issue/cmcdv2-bitrate-keys

Conversation

@cotid-qualabs

Copy link
Copy Markdown
Contributor

This PR will...

Complete the implementation of several CMCD v2 request and response keys in CMCDController that were previously missing.

  • rtp (Requested Throughput): segment bitrate × rtpSafetyFactor (default 5), rounded to nearest 100 kbps
  • lb (Lowest Bitrate): lowest bitrate within the current maxAutoLevel range
  • pb (Playhead Bitrate): bitrate of the level currently at the playhead, tracked via FRAG_CHANGED
  • tpb (Top Playhead Bitrate): bitrate of the highest level allowed by maxAutoLevel
  • dl (Deadline): buffer length divided by playback rate, rounded to nearest 100 ms
  • ttfb / ttlb: derived from fragment loader response timing by wrapping the onSuccess callback to call reporter.recordResponseReceived()

Adds a new config option rtpSafetyFactor (default: 5) to let operators tune the headroom multiplier used for rtp.

Why is this Pull Request needed?

These keys are part of the CMCD v2 spec and were previously missing. Without them, CDN servers cannot use CMCD v2 signals to make informed caching and bitrate decisions.

Are there any points in the code the reviewer needs to double check?

  • The playheadLevel tracking relies on FRAG_CHANGED — verify this is the right event to determine which level is currently at the playhead vs. being buffered ahead.
  • tpb uses hls.maxAutoLevel, which may return -1 when unconstrained; the code guards this but worth a second look.
  • The onSuccess wrapper in the fragment loader spreads callbacks — verify there are no edge cases where this wrapping could affect existing loader error handling.

Resolves issues:

#6090

Checklist

  • changes have been done against master branch, and PR does not conflict
  • new unit / functional tests have been added (whenever applicable)
  • API or design changes are documented in API.md

@cotid-qualabs cotid-qualabs marked this pull request as draft June 5, 2026 01:47
@cotid-qualabs cotid-qualabs marked this pull request as ready for review June 5, 2026 01:52
@robwalch robwalch requested a review from littlespex June 5, 2026 16:34
@robwalch robwalch added the CMCD label Jun 5, 2026
@robwalch robwalch linked an issue Jun 5, 2026 that may be closed by this pull request
littlespex
littlespex previously approved these changes Jun 5, 2026

@littlespex littlespex left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

All of these changes look good for request/response based reports, but event based reports would not get these new values, or any values set in applyFragmentData for that matter. Ideally these fields would be set via reporter.update so that the values would persist for all reports. However, this would be a much larger change because the persistent values would need to be properly flagged with object type tokens. For now, I think this work leaves the feature in a better place than it was, so we can merge it.

@cotid-qualabs

cotid-qualabs commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

All of these changes look good for request/response based reports, but event based reports would not get these new values, or any values set in applyFragmentData for that matter. Ideally these fields would be set via reporter.update so that the values would persist for all reports. However, this would be a much larger change because the persistent values would need to be properly flagged with object type tokens. For now, I think this work leaves the feature in a better place than it was, so we can merge it.

I've been playing around with how we could workaround this and enable these keys in event mode, and here's an implementation I've been drafting for this PR:

Persists those keys in the reporter's shared state via reporter.update(), so they are automatically included in all subsequent reports — including event reports.

Each key is updated from the hls.js event that provides the most accurate context for it:

  • pb and rtp — updated on FRAG_CHANGED, which provides the fragment and its level directly. The object type token (ot) is also persisted here since fragment context is available.
  • lb and tpb — updated on LEVELS_UPDATED (initial manifest load) and MAX_AUTO_LEVEL_UPDATED (ABR cap changes), using the existing getLowestBandwidth / getTopBandwidth helpers (made optional-fragment to support this call site). These updates intentionally omit ot — without a stable fragment reference, persisting a potentially stale object type would be worse than omitting it.
  • dl — updated on BUFFER_APPENDED / BUFFER_FLUSHED alongside the existing bl update, using the current buffer length and playback rate.

The behavior of request reports is unchanged — applyFragmentData still computes all values with full per-request context, which remains the most precise snapshot at request time.

Known limitations (pre-existing or inherent to the approach):

  • tpb reflects maxAutoLevel (a bandwidth cap) rather than a true device capability cap, consistent with the existing request-report implementation.
  • dl may be slightly stale if playbackRate changes between buffer events.
  • lb/tpb event reports do not carry ot; a fully scoped solution would require per-object-type persistent state in the reporter, which is a larger refactor.

Comment thread src/controller/cmcd-controller.ts Outdated
Comment on lines +323 to +329
private onFragChanged(event: Events.FRAG_CHANGED, data: FragChangedData) {
const level = this.hls.levels[data.frag.level];
if (level) {
this.playheadLevel = level;
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You should use LEVEL_SWITCHED if all you want is an update when the level index changes. FRAG_CHANGED is only triggered for the main variant playlist and you aren't using any part of it.

const fragPlaying = this.fragPlaying;
if (fragPlaying) {
const fragCurrentLevel = fragPlaying.level;
this.hls.trigger(Events.FRAG_CHANGED, {
frag: fragPlaying,
previousFrag,
});
if (previousFrag?.level !== fragCurrentLevel) {
this.hls.trigger(Events.LEVEL_SWITCHED, {
level: fragCurrentLevel,
});

If you need an enhancement like previous level/frag or Level (object), let me know so that we can make that happen prior to .0 release.

Comment thread src/controller/cmcd-controller.ts Outdated
data.br = [level.bitrate / 1000];
const bitrateKbps = level.bitrate / 1000;
data.br = [bitrateKbps];
const rtpSafetyFactor = this.hls.config.cmcd?.rtpSafetyFactor ?? 5;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Use const { cmcd } = this.config; as in other parts of this module. (avoid referencing this.hls when possible)

Comment thread src/controller/cmcd-controller.ts Outdated
Comment on lines +594 to +614
private getLowestBandwidth(fragment: Fragment | MediaFragment) {
let bitrate: number = Infinity;
let levels;
const hls = this.hls;

if (fragment.type === 'audio') {
levels = hls.audioTracks;
} else {
const max = hls.maxAutoLevel;
const len = max > -1 ? max + 1 : hls.levels.length;
levels = hls.levels.slice(0, len);
}

levels.forEach((level) => {
if (level.bitrate < bitrate) {
bitrate = level.bitrate;
}
});

return bitrate < Infinity ? bitrate : NaN;
}

@robwalch robwalch Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Audio segments do not have bitrate. HLS variant ("level") bitrates are the combined bitrate of audio and video.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same goes for getTopBandwidth but it looks like I let that slip.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If you wanted to use level.bitrate to calculate video bitrate, you'd need to subtract an estimate of the audio bitrate. audio bitrate can be guessed, but not measured until audio fragments are loaded.

Comment thread src/controller/cmcd-controller.ts Outdated
Comment on lines +433 to +441
const tb = this.getTopBandwidth(frag) / 1000;
if (Number.isFinite(tb)) {
data.tb = [tb];
}

const lb = this.getLowestBandwidth(frag) / 1000;
if (Number.isFinite(lb)) {
data.lb = [lb];
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

audio tracks don't have a bitrate and the main playlist (variant or "level") bitrates include audio and video combined.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is fine for muxed content (ot=MUXED). In other cases, tb and lb will be incorrect for video and not reported for audio.

Comment on lines +439 to +440
// Single level at 1000 bps = 1 kbps → lb=(1)
expectField(url, `lb%3D%281%29`);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This only covers variants without audio alternates.

Comment thread src/controller/cmcd-controller.ts Outdated
Comment on lines +771 to +778
const { onSuccess } = callbacks;
this.loader.load(context, config, {
...callbacks,
onSuccess: (response, stats, ctx, networkDetails) => {
onSuccess(response, stats, ctx, networkDetails);
recordResponse(context.url, response, stats);
},
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Doesn't the response mode need to be enabled? Can we not do this when the response event is not registered to be reported?

Co-authored-by: Rob Walch <robwalch@users.noreply.github.com>
@cotid-qualabs

Copy link
Copy Markdown
Contributor Author

All comments have been addressed 😄. Let me know if there's anything else we can improve. We can also discuss the feasibility of enabling these keys for event reports on this PR

@cotid-qualabs

Copy link
Copy Markdown
Contributor Author

Following up on the known limitation around event-mode reports, I've been thinking about a scoped approach that could resolve it without the larger per-object-type refactor.

The idea is to gate all reporter.update() calls for rtp, lb, pb, tpb, and dl behind a muxed-stream check. When the active level has no separate audio group, ot=av is stable and unambiguous for the lifetime of that level and there is no risk of persisting a stale object type. In that case, we can safely include ot in every update() call, which means event-mode reports would get the full picture.

For non-muxed streams (alternate audio renditions), we'd skip the update entirely rather than report without ot or risk mixing up audio and video buffers. This sidesteps the per-object-type persistent state problem at the cost of not supporting those keys in event mode for non-muxed content , which seems like a reasonable trade-off for now.

Wanted to float this before going further, does this direction seem good to you?

@littlespex

Copy link
Copy Markdown
Collaborator

We still wouldn't want to include ot on the update calls because it doesn't have the same meaning in event reports. Instead you would use the parameterized value for the mixed buffer length, and produce values like br=(1000;av). Looking at this further, all of the buffer related fields should also be parameterized. It might be worth a separate issue to address all the parameterized fields at once (bitrate and buffer).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Development

Successfully merging this pull request may close these issues.

Implement CMCD requested maximum throughput (rtp) property

3 participants