Skip to content

Commit

Permalink
CMCD: Add nrr and nor handling
Browse files Browse the repository at this point in the history
Previous iteration of CMCD [#1461] did not handle "nrr" nor... "nor"
attributes.

The former (nrr) gives a hint to the CDN of what the next byte range
request going to be (if it's one) and the later (nor) indicates the
relative URL based on the current one.

Note that if it's the same segment, the relative URL is just the file
name, as it seems logical for me - and as I didn't find an explicit
mention of this frequent case in the CMCD specification.
Even if the documentation for `nrr` indicates that an absent `nor`
but a present `nrr` is assumed to mean the same segment.
  • Loading branch information
peaBerberian committed Jun 24, 2024
1 parent 77cc51f commit da1ad63
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 13 deletions.
65 changes: 64 additions & 1 deletion src/core/cmcd/cmcd_data_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { ICmcdOptions, ICmcdPayload, ITrackType } from "../../public_types"
import createUuid from "../../utils/create_uuid";
import isNullOrUndefined from "../../utils/is_null_or_undefined";
import type { IRange } from "../../utils/ranges";
import { getRelativePathUrl } from "../../utils/resolve_url";
import TaskCanceller from "../../utils/task_canceller";

/**
Expand Down Expand Up @@ -171,12 +172,29 @@ export default class CmcdDataBuilder {
return this._producePayload(props);
}

public getCmcdDataForSegmentRequest(content: ICmcdSegmentInfo): ICmcdPayload {
public getCmcdDataForSegmentRequest(
content: ICmcdSegmentInfo,
nextSegment: ISegment | undefined | null,
): ICmcdPayload {
const lastObservation = this._playbackObserver?.getReference().getValue();

const props = this._getCommonCmcdData(this._lastThroughput[content.adaptation.type]);
props.br = Math.round(content.representation.bitrate / 1000);
props.d = Math.round(content.segment.duration * 1000);

if (
!isNullOrUndefined(nextSegment) &&
content.segment.url !== null &&
nextSegment.url !== null
) {
const relativePath = getRelativePathUrl(content.segment.url, nextSegment.url);
if (relativePath !== null) {
props.nor = relativePath;
if (nextSegment.range !== undefined && nextSegment.indexRange === undefined) {
props.nrr = `${nextSegment.range[0]}-${nextSegment.range[1] === Infinity ? "" : nextSegment.range[1]}`;
}
}
}
// TODO nor (next object request) and nrr (next range request)

switch (content.adaptation.type) {
Expand Down Expand Up @@ -329,6 +347,22 @@ export default class CmcdDataBuilder {
queryStringPayload += toAdd;
}
}
if (props.nor !== undefined) {
const toAdd = `nor=${formatStringPayload(props.nor)},`;
if (this._typePreference === TypePreference.Headers) {
cmcdRequestValue += toAdd;
} else {
queryStringPayload += toAdd;
}
}
if (props.nrr !== undefined) {
const toAdd = `nrr=${formatStringPayload(props.nrr)},`;
if (this._typePreference === TypePreference.Headers) {
cmcdRequestValue += toAdd;
} else {
queryStringPayload += toAdd;
}
}
if (props.ot !== undefined) {
const toAdd = `ot=${props.ot},`;
if (this._typePreference === TypePreference.Headers) {
Expand Down Expand Up @@ -595,6 +629,35 @@ interface ICmcdProperties {
* In kbps.
*/
tb?: number | undefined;
/**
* Next Object Request (nor)
* Relative path of the next object to be requested.
*
* This can be used to trigger pre-fetching by the CDN. This MUST be a path
* relative to the current request.
*
* This string MUST be URLEncoded. The client SHOULD NOT depend upon any
* pre-fetch action being taken - it is merely a request for such a pre-fetch
* to take place.
*/
nor?: string | undefined;
/**
* Next Range Request (nrr)
* If the next request will be a partial object request, then this string
* denotes the byte range to be requested. If the ‘nor’ field is not set, then
* the object is assumed to match the object currently being requested.
*
* The client SHOULD NOT depend upon any pre-fetch action being taken – it is
* merely a request for such a pre-fetch to take place. Formatting is similar
* to the HTTP Range header, except that the unit MUST be ‘byte’, the ‘Range:’
* prefix is NOT required and specifying multiple ranges is NOT allowed.
*
* Valid combinations are:
* "<range-start>-"
* "<range-start>-<range-end>"
* "-<suffix-length>"
*/
nrr?: string | undefined;
}

function formatStringPayload(str: string): string {
Expand Down
13 changes: 12 additions & 1 deletion src/core/fetchers/segment/prioritized_segment_fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import log from "../../../log";
import type { ISegment } from "../../../manifest";
import type { CancellationSignal } from "../../../utils/task_canceller";
import type {
ISegmentFetcher,
Expand Down Expand Up @@ -51,18 +52,27 @@ export default function applyPrioritizerToSegmentFetcher<TSegmentDataType>(
* @param {Object} content - content to request
* @param {Number} priority - priority at which the content should be requested.
* Lower number == higher priority.
* @param {Object|null|undefined} nextSegment - Information on the next
* segment that will be loaded after this one.
*
* Can be relied on in very specific scenarios to optimize caching on the
* CDN-side.
*
* Can be set to `undefined` if you do not care about this optimization, or to
* `null` if there's no segment after this one.
* @param {Object} callbacks
* @param {Object} cancelSignal
* @returns {Promise}
*/
createRequest(
content: ISegmentLoaderContent,
priority: number,
nextSegment: ISegment | undefined | null,
callbacks: IPrioritizedSegmentFetcherCallbacks<TSegmentDataType>,
cancelSignal: CancellationSignal,
): Promise<void> {
const givenTask = (innerCancelSignal: CancellationSignal) => {
return fetcher(content, callbacks, innerCancelSignal);
return fetcher(content, nextSegment, callbacks, innerCancelSignal);
};
const ret = prioritizer.create(givenTask, priority, callbacks, cancelSignal);
taskHandlers.set(ret, givenTask);
Expand Down Expand Up @@ -92,6 +102,7 @@ export interface IPrioritizedSegmentFetcher<TSegmentDataType> {
createRequest: (
content: ISegmentLoaderContent,
priority: number,
nextSegment: ISegment | undefined | null,
callbacks: IPrioritizedSegmentFetcherCallbacks<TSegmentDataType>,
cancellationSignal: CancellationSignal,
) => Promise<void>;
Expand Down
24 changes: 23 additions & 1 deletion src/core/fetchers/segment/segment_fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,21 @@ export default function createSegmentFetcher<TLoadedFormat, TSegmentDataType>(
/**
* Fetch a specific segment.
* @param {Object} content
* @param {Object|null|undefined} nextSegment - Information on the next
* segment that will be loaded after this one.
*
* Can be relied on in very specific scenarios to optimize caching on the
* CDN-side.
*
* Can be set to `undefined` if you do not care about this optimization, or to
* `null` if there's no segment after this one.
* @param {Object} fetcherCallbacks
* @param {Object} cancellationSignal
* @returns {Promise}
*/
return async function fetchSegment(
content: ISegmentLoaderContent,
nextSegment: ISegment | undefined | null,
fetcherCallbacks: ISegmentFetcherCallbacks<TSegmentDataType>,
cancellationSignal: CancellationSignal,
): Promise<void> {
Expand Down Expand Up @@ -293,7 +302,10 @@ export default function createSegmentFetcher<TLoadedFormat, TSegmentDataType>(
function callLoaderWithUrl(
cdnMetadata: ICdnMetadata | null,
): ReturnType<ISegmentLoader<TLoadedFormat>> {
requestOptions.cmcdPayload = cmcdDataBuilder?.getCmcdDataForSegmentRequest(content);
requestOptions.cmcdPayload = cmcdDataBuilder?.getCmcdDataForSegmentRequest(
content,
nextSegment,
);
return loadSegment(
cdnMetadata,
context,
Expand Down Expand Up @@ -398,6 +410,16 @@ export default function createSegmentFetcher<TLoadedFormat, TSegmentDataType>(
export type ISegmentFetcher<TSegmentDataType> = (
/** Information on the segment wanted. */
content: ISegmentLoaderContent,
/**
* Information on the next segment that will be loaded after this one.
*
* Can be relied on in very specific scenarios to optimize caching on the
* CDN-side.
*
* Can be set to `undefined` if you do not care about this optimization, or to
* `null` if there's no segment after this one.
*/
nextSegment: ISegment | undefined | null,
/** Callbacks the `ISegmentFetcher` will call as it loads the data. */
callbacks: ISegmentFetcherCallbacks<TSegmentDataType>,
/** CancellationSignal allowing to cancel the request. */
Expand Down
8 changes: 6 additions & 2 deletions src/core/fetchers/segment/segment_queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,10 @@ export default class SegmentQueue<T> extends EventEmitter<ISegmentQueueEvent<T>>
const { downloadQueue, content, initSegmentInfoRef, currentCanceller } = contentInfo;
const { segmentQueue } = downloadQueue.getValue();
const currentNeededSegment = segmentQueue[0];
const currentNextSegment = segmentQueue[1]?.segment ?? null;
const recursivelyRequestSegments = (
startingSegment: IQueuedSegment | undefined,
nextSegment: ISegment | undefined | null,
): void => {
if (currentCanceller !== null && currentCanceller.isUsed()) {
contentInfo.mediaSegmentRequest = null;
Expand Down Expand Up @@ -318,13 +320,14 @@ export default class SegmentQueue<T> extends EventEmitter<ISegmentQueueEvent<T>>
lastQueue.shift();
}
isComplete = true;
recursivelyRequestSegments(lastQueue[0]);
recursivelyRequestSegments(lastQueue[0], lastQueue[1]?.segment ?? null);
};

/** Scheduled actual segment request. */
const request = this._segmentFetcher.createRequest(
context,
priority,
nextSegment,
{
/**
* Callback called when the request has to be retried.
Expand Down Expand Up @@ -422,7 +425,7 @@ export default class SegmentQueue<T> extends EventEmitter<ISegmentQueueEvent<T>>

contentInfo.mediaSegmentRequest = { segment, priority, request, canceller };
};
recursivelyRequestSegments(currentNeededSegment);
recursivelyRequestSegments(currentNeededSegment, currentNextSegment);
}

/**
Expand Down Expand Up @@ -460,6 +463,7 @@ export default class SegmentQueue<T> extends EventEmitter<ISegmentQueueEvent<T>>
const request = this._segmentFetcher.createRequest(
context,
priority,
undefined,
{
onRetry: (err: IPlayerError): void => {
this.trigger("requestRetry", { segment, error: err });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default function loadAndPushSegment(
const pushOperations: Array<Promise<unknown>> = [];
return segmentFetcher(
segmentInfo,
undefined,
{
onChunk(parseChunk) {
const parsed = parseChunk(undefined);
Expand Down
10 changes: 2 additions & 8 deletions src/utils/__tests__/resolve_url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,10 +366,7 @@ describe("utils - getRelativePathUrl", () => {

it("should return `null` for different domains", () => {
expect(
getRelativePathUrl(
"https://www.example.fr/a.mp4",
"https://www.example.com/b.mp4",
),
getRelativePathUrl("https://www.example.fr/a.mp4", "https://www.example.com/b.mp4"),
).toEqual(null);
expect(
getRelativePathUrl(
Expand All @@ -387,10 +384,7 @@ describe("utils - getRelativePathUrl", () => {

it("should return `null` for different schemes", () => {
expect(
getRelativePathUrl(
"http://www.example.fr/a.mp4",
"https://www.example.com/b.mp4",
),
getRelativePathUrl("http://www.example.fr/a.mp4", "https://www.example.com/b.mp4"),
).toEqual(null);
expect(
getRelativePathUrl(
Expand Down

0 comments on commit da1ad63

Please sign in to comment.