Skip to content

Commit 2dc63e7

Browse files
CMCDv2: implement rtp, lb, dl, pb, tpb, ttfb and ttlb, adds config.rtpSafetyFactor (#7895)
* implemented rtp, lb, dl, pb, ttfb and ttlb --------- Co-authored-by: Casey Occhialini <1508707+littlespex@users.noreply.github.com>
1 parent 5c03223 commit 2dc63e7

5 files changed

Lines changed: 357 additions & 20 deletions

File tree

api-extractor/report/hls.js.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,7 @@ export type CMCDControllerConfig = {
10271027
useHeaders?: boolean;
10281028
includeKeys?: CmcdKey[];
10291029
version?: CmcdVersion;
1030+
rtpSafetyFactor?: number;
10301031
eventTargets?: (Omit<CmcdEventReportConfig, 'enabledKeys'> & {
10311032
includeKeys?: CmcdKey[];
10321033
})[];

docs/API.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1903,6 +1903,7 @@ data will be passed on all media requests (manifests, playlists, a/v segments, t
19031903
- `interval`: For the time-interval event, the reporting cadence in seconds. Defaults to `30`.
19041904
- `batchSize`: The number of events to batch before sending a report. Defaults to `1` (send each event immediately).
19051905
- `includeKeys`: An optional array of CMCD keys that overrides the top-level `includeKeys` for this target.
1906+
- `rtpSafetyFactor`: A multiplier applied to the segment bitrate to compute the `rtp` (requested throughput) field. The spec defines `rtp` as the maximum throughput the client considers sufficient, which should include headroom above the encoded bitrate to absorb network jitter and avoid rebuffering. Defaults to `5` (5× the segment bitrate). Only used when `version: 2`.
19061907
- `loader`: An optional async function `(request) => Promise<{ status }>` used to deliver CMCD v2 event reports. When omitted, event reports are delivered via `fetch` (honoring the Hls `xhrSetup`/`fetchSetup` hooks). Only used when `eventTargets` is configured.
19071908
- `reporterCallback`: An optional `(reporter: CmcdCustomReporter) => void` callback. Called once per `MANIFEST_LOADING`, before the reporter starts. Use it to seed custom CMCD keys or store the reference for firing custom events at runtime. Always use the most recently received reference, since a new source load yields a new instance.
19081909

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export type CMCDControllerConfig = {
103103
useHeaders?: boolean;
104104
includeKeys?: CmcdKey[];
105105
version?: CmcdVersion;
106+
rtpSafetyFactor?: number;
106107
eventTargets?: (Omit<CmcdEventReportConfig, 'enabledKeys'> & {
107108
includeKeys?: CmcdKey[];
108109
})[];

src/controller/cmcd-controller.ts

Lines changed: 110 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import type {
3232
BufferAppendedData,
3333
BufferFlushedData,
3434
ErrorData,
35+
LevelSwitchedData,
3536
LevelSwitchingData,
3637
ManifestLoadingData,
3738
MediaAttachedData,
@@ -44,6 +45,8 @@ import type {
4445
LoaderCallbacks,
4546
LoaderConfiguration,
4647
LoaderContext,
48+
LoaderResponse,
49+
LoaderStats,
4750
PlaylistLoaderContext,
4851
} from '../types/loader';
4952
import type { Cmcd } from '@svta/cml-cmcd';
@@ -65,6 +68,7 @@ export default class CMCDController implements ComponentAPI {
6568
private buffering: boolean = true;
6669
private playerState?: CmcdPlayerState;
6770
private reporter?: CmcdReporter;
71+
private playheadLevel?: Level;
6872

6973
constructor(hls: Hls) {
7074
this.hls = hls;
@@ -142,6 +146,7 @@ export default class CMCDController implements ComponentAPI {
142146
hls.on(Events.MEDIA_ENDED, this.onMediaEnded, this);
143147
hls.on(Events.ERROR, this.onError, this);
144148
hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
149+
hls.on(Events.LEVEL_SWITCHED, this.onLevelSwitched, this);
145150
hls.on(Events.BUFFER_APPENDED, this.onBufferInfoChange, this);
146151
hls.on(Events.BUFFER_FLUSHED, this.onBufferInfoChange, this);
147152
}
@@ -154,6 +159,7 @@ export default class CMCDController implements ComponentAPI {
154159
hls.off(Events.MEDIA_ENDED, this.onMediaEnded, this);
155160
hls.off(Events.ERROR, this.onError, this);
156161
hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this);
162+
hls.off(Events.LEVEL_SWITCHED, this.onLevelSwitched, this);
157163
hls.off(Events.BUFFER_APPENDED, this.onBufferInfoChange, this);
158164
hls.off(Events.BUFFER_FLUSHED, this.onBufferInfoChange, this);
159165
}
@@ -314,6 +320,16 @@ export default class CMCDController implements ComponentAPI {
314320
this.reporter.recordEvent(CmcdEventType.BITRATE_CHANGE, eventData);
315321
}
316322

323+
private onLevelSwitched(
324+
_event: Events.LEVEL_SWITCHED,
325+
data: LevelSwitchedData,
326+
) {
327+
const level = this.hls.levels[data.level];
328+
if (level) {
329+
this.playheadLevel = level;
330+
}
331+
}
332+
317333
private setPlayerState(state: CmcdPlayerState) {
318334
this.playerState = state;
319335
if (this.reporter) {
@@ -373,8 +389,6 @@ export default class CMCDController implements ComponentAPI {
373389
data.su = this.buffering;
374390
}
375391

376-
// TODO: Implement rtp, dl
377-
378392
const report = this.reporter.createRequestReport(
379393
{ url: context.url, headers: context.headers },
380394
data,
@@ -414,14 +428,44 @@ export default class CMCDController implements ComponentAPI {
414428
ot === CmcdObjectType.MUXED ||
415429
(ot == null && (frag.type === 'main' || frag.type === 'audio'))
416430
) {
417-
data.br = [level.bitrate / 1000];
418-
const tb = this.getTopBandwidth(frag) / 1000;
419-
if (Number.isFinite(tb)) {
420-
data.tb = [tb];
431+
const bitrateKbps = level.bitrate / 1000;
432+
data.br = [bitrateKbps];
433+
const { cmcd } = this.config;
434+
const rtpSafetyFactor = cmcd?.rtpSafetyFactor ?? 5;
435+
data.rtp = Math.round((bitrateKbps * rtpSafetyFactor) / 100) * 100;
436+
437+
if (
438+
ot === CmcdObjectType.MUXED ||
439+
(ot == null && frag.type === 'main')
440+
) {
441+
const tb = this.getTopBandwidth() / 1000;
442+
if (Number.isFinite(tb)) {
443+
data.tb = [tb];
444+
}
445+
446+
const lb = this.getLowestBandwidth() / 1000;
447+
if (Number.isFinite(lb)) {
448+
data.lb = [lb];
449+
}
421450
}
451+
422452
const bl = this.getBufferLength(frag);
423453
if (Number.isFinite(bl)) {
424454
data.bl = [bl];
455+
const pr = this.media?.playbackRate || 1;
456+
data.dl = Math.round(bl / pr / 100) * 100;
457+
}
458+
459+
if (this.playheadLevel) {
460+
data.pb = [this.playheadLevel.bitrate / 1000];
461+
}
462+
463+
const maxIdx = this.hls.maxAutoLevel;
464+
if (maxIdx >= 0) {
465+
const topLevel = this.hls.levels[maxIdx];
466+
if (topLevel) {
467+
data.tpb = [topLevel.bitrate / 1000];
468+
}
425469
}
426470
}
427471

@@ -534,18 +578,12 @@ export default class CMCDController implements ComponentAPI {
534578
* Audio renditions live in hls.audioTracks; everything else (including
535579
* audio-only main playlists) draws from hls.levels.
536580
*/
537-
private getTopBandwidth(fragment: Fragment | MediaFragment) {
581+
private getTopBandwidth() {
538582
let bitrate: number = 0;
539-
let levels;
540583
const hls = this.hls;
541-
542-
if (fragment.type === 'audio') {
543-
levels = hls.audioTracks;
544-
} else {
545-
const max = hls.maxAutoLevel;
546-
const len = max > -1 ? max + 1 : hls.levels.length;
547-
levels = hls.levels.slice(0, len);
548-
}
584+
const max = hls.maxAutoLevel;
585+
const len = max > -1 ? max + 1 : hls.levels.length;
586+
const levels = hls.levels.slice(0, len);
549587

550588
levels.forEach((level) => {
551589
if (level.bitrate > bitrate) {
@@ -556,6 +594,22 @@ export default class CMCDController implements ComponentAPI {
556594
return bitrate > 0 ? bitrate : NaN;
557595
}
558596

597+
private getLowestBandwidth() {
598+
let bitrate: number = Infinity;
599+
const hls = this.hls;
600+
const max = hls.maxAutoLevel;
601+
const len = max > -1 ? max + 1 : hls.levels.length;
602+
const levels = hls.levels.slice(0, len);
603+
604+
levels.forEach((level) => {
605+
if (level.bitrate < bitrate) {
606+
bitrate = level.bitrate;
607+
}
608+
});
609+
610+
return Number.isFinite(bitrate) ? bitrate : NaN;
611+
}
612+
559613
/**
560614
* Get the buffer length in milliseconds for the source backing this fragment.
561615
*/
@@ -604,6 +658,37 @@ export default class CMCDController implements ComponentAPI {
604658
this.reporter.update({ bl: [bl] });
605659
}
606660

661+
private recordFragmentResponse = (
662+
url: string,
663+
response: LoaderResponse,
664+
stats: LoaderStats,
665+
) => {
666+
const { cmcd } = this.config;
667+
const hasResponseTarget = cmcd?.eventTargets?.some((t) =>
668+
t.events?.includes(CmcdEventType.RESPONSE_RECEIVED),
669+
);
670+
if (!this.reporter || !(stats.loading.first > 0) || !hasResponseTarget) {
671+
return;
672+
}
673+
try {
674+
this.reporter.recordResponseReceived({
675+
request: { url },
676+
status: response.code,
677+
resourceTiming: {
678+
startTime: stats.loading.start,
679+
responseStart: stats.loading.first,
680+
duration: stats.loading.end - stats.loading.start,
681+
encodedBodySize: stats.total,
682+
},
683+
});
684+
} catch (error) {
685+
this.hls.logger.warn(
686+
'Could not record fragment response CMCD data.',
687+
error,
688+
);
689+
}
690+
};
691+
607692
/**
608693
* Create a playlist loader
609694
*/
@@ -652,6 +737,7 @@ export default class CMCDController implements ComponentAPI {
652737
private createFragmentLoader(): FragmentLoaderConstructor | undefined {
653738
const { fLoader } = this.config;
654739
const apply = this.applyFragmentData;
740+
const recordResponse = this.recordFragmentResponse;
655741
const Ctor = fLoader || (this.config.loader as FragmentLoaderConstructor);
656742

657743
return class CmcdFragmentLoader {
@@ -683,7 +769,14 @@ export default class CMCDController implements ComponentAPI {
683769
callbacks: LoaderCallbacks<FragmentLoaderContext>,
684770
) {
685771
apply(context);
686-
this.loader.load(context, config, callbacks);
772+
const { onSuccess } = callbacks;
773+
this.loader.load(context, config, {
774+
...callbacks,
775+
onSuccess: (response, stats, ctx, networkDetails) => {
776+
onSuccess(response, stats, ctx, networkDetails);
777+
recordResponse(context.url, response, stats);
778+
},
779+
});
687780
}
688781
};
689782
}

0 commit comments

Comments
 (0)