rx-player
Version:
Canal+ HTML5 Video Player
367 lines (366 loc) • 16.1 kB
JavaScript
"use strict";
var __values = (this && this.__values) || function(o) {
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function () {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
Object.defineProperty(exports, "__esModule", { value: true });
var log_1 = require("../../log");
var create_uuid_1 = require("../../utils/create_uuid");
var is_null_or_undefined_1 = require("../../utils/is_null_or_undefined");
var task_canceller_1 = require("../../utils/task_canceller");
var url_utils_1 = require("../../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`.
*/
var 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
*/
var CmcdDataBuilder = /** @class */ (function () {
/**
* Create a new `CmcdDataBuilder`, linked to the given options (see type
* definition).
* @param {Object} options
*/
function CmcdDataBuilder(options) {
var _a, _b;
this._sessionId = (_a = options.sessionId) !== null && _a !== void 0 ? _a : (0, create_uuid_1.default)();
this._contentId = (_b = options.contentId) !== null && _b !== void 0 ? _b : (0, create_uuid_1.default)();
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
*/
CmcdDataBuilder.prototype.startMonitoringPlayback = function (playbackObserver) {
var _this = this;
var _a;
(_a = this._canceller) === null || _a === void 0 ? void 0 : _a.cancel();
this._canceller = new task_canceller_1.default();
this._playbackObserver = playbackObserver;
playbackObserver.listen(function (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.
*/
CmcdDataBuilder.prototype.stopMonitoringPlayback = function () {
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.
*/
CmcdDataBuilder.prototype.updateThroughput = function (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}
*/
CmcdDataBuilder.prototype._getCommonCmcdData = function (lastThroughput) {
var _a;
var 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;
var 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}
*/
CmcdDataBuilder.prototype.getCmcdDataForManifest = function (transportType) {
var _a;
var 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}
*/
CmcdDataBuilder.prototype.getCmcdDataForSegmentRequest = function (content) {
var e_1, _a;
var _b, _c, _d, _e;
var lastObservation = (_b = this._playbackObserver) === null || _b === void 0 ? void 0 : _b.getReference().getValue();
var 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 (!(0, is_null_or_undefined_1.default)(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) {
var currSegmentUrl = content.segment.url;
var nextSegmentUrl = content.nextSegment.url;
var relativeUrl = (0, url_utils_1.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]);
}
}
}
}
}
var precizeBufferLengthMs;
if (lastObservation !== undefined &&
(props.ot === "v" || props.ot === "a" || props.ot === "av")) {
var bufferedForType = lastObservation.buffered[content.adaptation.type];
if (!(0, is_null_or_undefined_1.default)(bufferedForType)) {
// TODO more precize position estimate?
var position = (_e = (_d = (_c = this._playbackObserver) === null || _c === void 0 ? void 0 : _c.getCurrentTime()) !== null && _d !== void 0 ? _d : lastObservation.position.getWanted()) !== null && _e !== void 0 ? _e : lastObservation.position.getPolled();
try {
for (var bufferedForType_1 = __values(bufferedForType), bufferedForType_1_1 = bufferedForType_1.next(); !bufferedForType_1_1.done; bufferedForType_1_1 = bufferedForType_1.next()) {
var range = bufferedForType_1_1.value;
if (position >= range.start && position < range.end) {
precizeBufferLengthMs = (range.end - position) * 1000;
props.bl = Math.floor(Math.round(precizeBufferLengthMs / 100) * 100);
break;
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (bufferedForType_1_1 && !bufferedForType_1_1.done && (_a = bufferedForType_1.return)) _a.call(bufferedForType_1);
}
finally { if (e_1) throw e_1.error; }
}
}
}
var 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
var estimatedFileSizeKb = (content.representation.bitrate * content.segment.duration) / 1000;
var 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(function (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}
*/
CmcdDataBuilder.prototype._producePayload = function (props) {
var _this = this;
var headers = {
object: "",
request: "",
session: "",
status: "",
};
var queryStringPayload = "";
var addPayload = function (payload, headerName) {
if (_this._typePreference === 0 /* TypePreference.Headers */) {
headers[headerName] += payload;
}
else {
queryStringPayload += payload;
}
};
var addNumberProperty = function (prop, headerName) {
var val = props[prop];
if (val !== undefined) {
var toAdd = "".concat(prop, "=").concat(String(val), ",");
addPayload(toAdd, headerName);
}
};
var addBooleanProperty = function (prop, headerName) {
if (props[prop] === true) {
var toAdd = "".concat(prop, ",");
addPayload(toAdd, headerName);
}
};
var addStringProperty = function (prop, headerName) {
var val = props[prop];
if (val !== undefined) {
var formatted = "\"".concat(val.replace("\\", "\\\\").replace('"', '\\"'), "\"");
var toAdd = "prop=".concat(formatted, ",");
addPayload(toAdd, headerName);
}
};
var addTokenProperty = function (prop, headerName) {
var val = props[prop];
if (val !== undefined) {
var toAdd = "prop=".concat(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_1.default.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_1.default.debug("CMCD: proposing query string payload", queryStringPayload);
return {
type: "query",
value: [["CMCD", queryStringPayload]],
};
};
return CmcdDataBuilder;
}());
exports.default = CmcdDataBuilder;