rx-player
Version:
Canal+ HTML5 Video Player
339 lines (338 loc) • 14.3 kB
JavaScript
import log from "../../log";
import createUuid from "../../utils/create_uuid";
import isNullOrUndefined from "../../utils/is_null_or_undefined";
import TaskCanceller from "../../utils/task_canceller";
import { getRelativeUrl } from "../../utils/url-utils";
/**
* `rtp`, for "REQUESTED_MAXIMUM_THROUGHPUT", indicates the maximum throughput
* needed to load a given segment without experience degration.
* It acts as a hint to a CDN so it can scale its resources between multiple
* clients.
*
* We could indicate through `rtp` the exact minimum bandwidth needed, but this
* may lead to much higher risk of rebuffering, so we prefer to multiply that
* value by a safe-enough factor, this `RTP_FACTOR`.
*/
const RTP_FACTOR = 4;
/**
* Class allowing to easily obtain "Common Media Client Data" (CMCD) properties
* that may be relied on while performing HTTP(S) requests on a CDN.
*
* @class CmcdDataBuilder
*/
export default class CmcdDataBuilder {
/**
* Create a new `CmcdDataBuilder`, linked to the given options (see type
* definition).
* @param {Object} options
*/
constructor(options) {
var _a, _b;
this._sessionId = (_a = options.sessionId) !== null && _a !== void 0 ? _a : createUuid();
this._contentId = (_b = options.contentId) !== null && _b !== void 0 ? _b : createUuid();
this._typePreference =
options.communicationType === "headers"
? 0 /* TypePreference.Headers */
: 1 /* TypePreference.QueryString */;
this._bufferStarvationToggle = false;
this._playbackObserver = null;
this._lastThroughput = {};
this._canceller = null;
}
/**
* Start listening to the given `playbackObserver` so the `CmcdDataBuilder`
* can extract some playback-linked metadata that it needs.
*
* It will keep listening for media data until `stopMonitoringPlayback` is called.
*
* If `startMonitoringPlayback` is called again, the previous monitoring is
* also cancelled.
* @param {Object} playbackObserver
*/
startMonitoringPlayback(playbackObserver) {
var _a;
(_a = this._canceller) === null || _a === void 0 ? void 0 : _a.cancel();
this._canceller = new TaskCanceller();
this._playbackObserver = playbackObserver;
playbackObserver.listen((obs) => {
if (obs.rebuffering !== null) {
this._bufferStarvationToggle = true;
}
}, { includeLastObservation: true, clearSignal: this._canceller.signal });
}
/**
* Stop the monitoring of playback conditions started from the last
* `stopMonitoringPlayback` call.
*/
stopMonitoringPlayback() {
var _a;
(_a = this._canceller) === null || _a === void 0 ? void 0 : _a.cancel();
this._canceller = null;
this._playbackObserver = null;
}
/**
* Update the last measured throughput for a specific media type.
* Needed for some of CMCD's properties.
* @param {string} trackType
* @param {number|undefined} throughput - Last throughput measured for that
* media type. `undefined` if unknown.
*/
updateThroughput(trackType, throughput) {
this._lastThroughput[trackType] = throughput;
}
/**
* Returns the base of data that is common to all resources' requests.
* @param {number|undefined} lastThroughput - The last measured throughput to
* provide. `undefined` to provide no throughput.
* @returns {Object}
*/
_getCommonCmcdData(lastThroughput) {
var _a;
const props = {};
props.bs = this._bufferStarvationToggle;
this._bufferStarvationToggle = false;
props.cid = this._contentId;
props.mtp =
lastThroughput !== undefined
? Math.floor(Math.round(lastThroughput / 1000 / 100) * 100)
: undefined;
props.sid = this._sessionId;
const lastObservation = (_a = this._playbackObserver) === null || _a === void 0 ? void 0 : _a.getReference().getValue();
props.pr =
lastObservation === undefined || lastObservation.speed === 1
? undefined
: lastObservation.speed;
if (lastObservation !== undefined) {
props.su = lastObservation.rebuffering !== null;
}
return props;
}
/**
* For the given type of Manifest, returns the corresponding CMCD payload
* that should be provided alongside its request.
* @param {string} transportType
* @returns {Object}
*/
getCmcdDataForManifest(transportType) {
var _a;
const props = this._getCommonCmcdData((_a = this._lastThroughput.video) !== null && _a !== void 0 ? _a : this._lastThroughput.audio);
props.ot = "m";
switch (transportType) {
case "dash":
props.sf = "d";
break;
case "smooth":
props.sf = "s";
break;
default:
props.sf = "o";
break;
}
return this._producePayload(props);
}
/**
* For the given segment information, returns the corresponding CMCD payload
* that should be provided alongside its request.
* @param {Object} content
* @returns {Object}
*/
getCmcdDataForSegmentRequest(content) {
var _a, _b, _c, _d;
const lastObservation = (_a = this._playbackObserver) === null || _a === void 0 ? void 0 : _a.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);
switch (content.adaptation.type) {
case "video":
props.ot = "v";
break;
case "audio":
props.ot = "a";
break;
case "text":
props.ot = "c";
break;
}
if (content.segment.isInit) {
props.ot = "i";
}
if (!isNullOrUndefined(content.nextSegment) &&
content.segment.url !== null &&
content.nextSegment.url !== null) {
// We add a special case for some initialization segment which need
// multiple byte-ranges to fully request, as the `CmcdDataBuilder`
// is not supposed to keep track of how the requesting part of the
// RxPlayer actually perform its multi-byte-range requests
if (!content.nextSegment.isInit || content.nextSegment.indexRange === undefined) {
const currSegmentUrl = content.segment.url;
const nextSegmentUrl = content.nextSegment.url;
const relativeUrl = getRelativeUrl(currSegmentUrl, nextSegmentUrl);
if (relativeUrl !== null) {
if (relativeUrl !== ".") {
props.nor = encodeURIComponent(relativeUrl);
}
if (content.nextSegment.range !== undefined) {
props.nrr = String(content.nextSegment.range[0]) + "-";
if (isFinite(content.nextSegment.range[1])) {
props.nrr += String(content.nextSegment.range[1]);
}
}
}
}
}
let precizeBufferLengthMs;
if (lastObservation !== undefined &&
(props.ot === "v" || props.ot === "a" || props.ot === "av")) {
const bufferedForType = lastObservation.buffered[content.adaptation.type];
if (!isNullOrUndefined(bufferedForType)) {
// TODO more precize position estimate?
const position = (_d = (_c = (_b = this._playbackObserver) === null || _b === void 0 ? void 0 : _b.getCurrentTime()) !== null && _c !== void 0 ? _c : lastObservation.position.getWanted()) !== null && _d !== void 0 ? _d : lastObservation.position.getPolled();
for (const range of bufferedForType) {
if (position >= range.start && position < range.end) {
precizeBufferLengthMs = (range.end - position) * 1000;
props.bl = Math.floor(Math.round(precizeBufferLengthMs / 100) * 100);
break;
}
}
}
}
const precizeDeadlineMs = precizeBufferLengthMs === undefined || lastObservation === undefined
? undefined
: precizeBufferLengthMs / lastObservation.speed;
props.dl =
precizeDeadlineMs === undefined
? undefined
: Math.floor(Math.round(precizeDeadlineMs / 100) * 100);
if (precizeDeadlineMs !== undefined) {
// estimate the file size, in kilobits
const estimatedFileSizeKb = (content.representation.bitrate * content.segment.duration) / 1000;
const wantedCeilBandwidthKbps = estimatedFileSizeKb / (precizeDeadlineMs / 1000);
props.rtp = Math.floor(Math.round((wantedCeilBandwidthKbps * RTP_FACTOR) / 100) * 100);
}
switch (content.manifest.transport) {
case "dash":
props.sf = "d";
break;
case "smooth":
props.sf = "s";
break;
default:
props.sf = "o";
break;
}
props.st = content.manifest.isDynamic ? "l" : "v";
props.tb = content.adaptation.representations.reduce((acc, representation) => {
if (representation.isPlayable() !== true) {
return acc;
}
if (acc === undefined) {
return Math.round(representation.bitrate / 1000);
}
return Math.max(acc, Math.round(representation.bitrate / 1000));
}, undefined);
return this._producePayload(props);
}
/**
* From the given CMCD properties, produce the corresponding payload according
* to current settings.
* @param {Object} props
* @returns {Object}
*/
_producePayload(props) {
const headers = {
object: "",
request: "",
session: "",
status: "",
};
let queryStringPayload = "";
const addPayload = (payload, headerName) => {
if (this._typePreference === 0 /* TypePreference.Headers */) {
headers[headerName] += payload;
}
else {
queryStringPayload += payload;
}
};
const addNumberProperty = (prop, headerName) => {
const val = props[prop];
if (val !== undefined) {
const toAdd = `${prop}=${String(val)},`;
addPayload(toAdd, headerName);
}
};
const addBooleanProperty = (prop, headerName) => {
if (props[prop] === true) {
const toAdd = `${prop},`;
addPayload(toAdd, headerName);
}
};
const addStringProperty = (prop, headerName) => {
const val = props[prop];
if (val !== undefined) {
const formatted = `"${val.replace("\\", "\\\\").replace('"', '\\"')}"`;
const toAdd = `prop=${formatted},`;
addPayload(toAdd, headerName);
}
};
const addTokenProperty = (prop, headerName) => {
const val = props[prop];
if (val !== undefined) {
const toAdd = `prop=${val},`;
addPayload(toAdd, headerName);
}
};
addNumberProperty("bl", "request");
addNumberProperty("br", "object");
addBooleanProperty("bs", "status");
addStringProperty("cid", "session");
addNumberProperty("d", "object");
addNumberProperty("dl", "request");
addNumberProperty("mtp", "request");
addStringProperty("nor", "request");
addStringProperty("nrr", "request");
addTokenProperty("ot", "object");
addNumberProperty("pr", "session");
addNumberProperty("rtp", "status");
addTokenProperty("sf", "session");
addStringProperty("sid", "session");
addTokenProperty("st", "session");
addBooleanProperty("su", "request");
addNumberProperty("tb", "object");
if (this._typePreference === 0 /* TypePreference.Headers */) {
if (headers.object[headers.object.length - 1] === ",") {
headers.object = headers.object.substring(0, headers.object.length - 1);
}
if (headers.request[headers.request.length - 1] === ",") {
headers.request = headers.request.substring(0, headers.request.length - 1);
}
if (headers.session[headers.session.length - 1] === ",") {
headers.session = headers.session.substring(0, headers.session.length - 1);
}
if (headers.status[headers.status.length - 1] === ",") {
headers.status = headers.status.substring(0, headers.status.length - 1);
}
log.debug("CMCD: proposing headers payload");
return {
type: "headers",
value: {
/* eslint-disable @typescript-eslint/naming-convention */
"CMCD-Object": headers.object,
"CMCD-Request": headers.request,
"CMCD-Session": headers.session,
"CMCD-Status": headers.status,
/* eslint-enable @typescript-eslint/naming-convention */
},
};
}
if (queryStringPayload[queryStringPayload.length - 1] === ",") {
queryStringPayload = queryStringPayload.substring(0, queryStringPayload.length - 1);
}
queryStringPayload = encodeURIComponent(queryStringPayload);
log.debug("CMCD: proposing query string payload", queryStringPayload);
return {
type: "query",
value: [["CMCD", queryStringPayload]],
};
}
}