rx-player
Version:
Canal+ HTML5 Video Player
272 lines (271 loc) • 12.5 kB
JavaScript
/**
* Copyright 2015 CANAL+ Group
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import log from "../../../../log";
import flatMap from "../../../../utils/flat_map";
import idGenerator from "../../../../utils/id_generator";
import isNullOrUndefined from "../../../../utils/is_null_or_undefined";
import isWorker from "../../../../utils/is_worker";
import getMonotonicTimeStamp from "../../../../utils/monotonic_timestamp";
import objectValues from "../../../../utils/object_values";
import { utf8ToStr } from "../../../../utils/string_parsing";
import flattenOverlappingPeriods from "./flatten_overlapping_periods";
import getPeriodsTimeInformation from "./get_periods_time_infos";
import parseAdaptationSets from "./parse_adaptation_sets";
import resolveBaseURLs from "./resolve_base_urls";
const generatePeriodID = idGenerator();
/**
* Process intermediate periods to create final parsed periods.
* @param {Array.<Object>} periodsIR
* @param {Object} context
* @returns {Array.<Object>}
*/
export default function parsePeriods(periodsIR, context) {
var _a, _b, _c, _d, _e;
const parsedPeriods = [];
const periodsTimeInformation = getPeriodsTimeInformation(periodsIR, context);
if (periodsTimeInformation.length !== periodsIR.length) {
throw new Error("MPD parsing error: the time information are incoherent.");
}
const { isDynamic, manifestBoundsCalculator } = context;
if (!isDynamic && !isNullOrUndefined(context.duration)) {
manifestBoundsCalculator.setLastPosition(context.duration);
}
// We parse it in reverse because we might need to deduce the buffer depth from
// the last Periods' indexes
for (let i = periodsIR.length - 1; i >= 0; i--) {
const isLastPeriod = i === periodsIR.length - 1;
const periodIR = periodsIR[i];
const xlinkInfos = context.xlinkInfos.get(periodIR);
const periodBaseURLs = resolveBaseURLs(context.baseURLs, periodIR.children.baseURLs);
const { periodStart, periodDuration, periodEnd } = periodsTimeInformation[i];
let periodID;
if (isNullOrUndefined(periodIR.attributes.id)) {
log.warn("DASH: No usable id found in the Period. Generating one.");
periodID = "gen-dash-period-" + generatePeriodID();
}
else {
periodID = periodIR.attributes.id;
}
// Avoid duplicate IDs
while (parsedPeriods.some((p) => p.id === periodID)) {
periodID += "-dup";
}
const receivedTime = xlinkInfos !== undefined ? xlinkInfos.receivedTime : context.receivedTime;
const unsafelyBaseOnPreviousPeriod = (_b = (_a = context.unsafelyBaseOnPreviousManifest) === null || _a === void 0 ? void 0 : _a.getPeriod(periodID)) !== null && _b !== void 0 ? _b : null;
const availabilityTimeComplete = periodIR.attributes.availabilityTimeComplete;
const availabilityTimeOffset = periodIR.attributes.availabilityTimeOffset;
const { manifestProfiles, contentProtectionParser } = context;
const { segmentTemplate } = periodIR.children;
contentProtectionParser.addReferences((_c = periodIR.children.contentProtections) !== null && _c !== void 0 ? _c : []);
const adapCtxt = {
availabilityTimeComplete,
availabilityTimeOffset,
baseURLs: periodBaseURLs,
contentProtectionParser,
manifestBoundsCalculator,
end: periodEnd,
isDynamic,
isLastPeriod,
manifestProfiles,
receivedTime,
segmentTemplate,
start: periodStart,
unsafelyBaseOnPreviousPeriod,
};
const { adaptations, thumbnailTracks } = parseAdaptationSets(periodIR.children.adaptations, adapCtxt);
const namespaces = ((_d = context.xmlNamespaces) !== null && _d !== void 0 ? _d : []).concat((_e = periodIR.attributes.namespaces) !== null && _e !== void 0 ? _e : []);
const streamEvents = generateStreamEvents(periodIR.children.eventStreams, periodStart, namespaces);
const parsedPeriod = {
id: periodID,
start: periodStart,
end: periodEnd,
duration: periodDuration,
thumbnailTracks,
adaptations,
streamEvents,
};
parsedPeriods.unshift(parsedPeriod);
if (!manifestBoundsCalculator.lastPositionIsKnown()) {
const lastPosition = getMaximumLastPosition(adaptations);
if (!isDynamic) {
if (typeof lastPosition === "number") {
manifestBoundsCalculator.setLastPosition(lastPosition);
}
}
else {
if (typeof lastPosition === "number") {
const positionTime = getMonotonicTimeStamp() / 1000;
manifestBoundsCalculator.setLastPosition(lastPosition, positionTime);
}
else {
const guessedLastPositionFromClock = guessLastPositionFromClock(context, periodStart);
if (guessedLastPositionFromClock !== undefined) {
const [guessedLastPosition, guessedPositionTime] = guessedLastPositionFromClock;
manifestBoundsCalculator.setLastPosition(guessedLastPosition, guessedPositionTime);
}
}
}
}
}
if (context.isDynamic && !manifestBoundsCalculator.lastPositionIsKnown()) {
// Guess a last time the last position
const guessedLastPositionFromClock = guessLastPositionFromClock(context, 0);
if (guessedLastPositionFromClock !== undefined) {
const [lastPosition, positionTime] = guessedLastPositionFromClock;
manifestBoundsCalculator.setLastPosition(lastPosition, positionTime);
}
}
return flattenOverlappingPeriods(parsedPeriods);
}
/**
* Try to guess the "last position", which is the last position
* available in the manifest in seconds, and the "position time", the
* monotonically-raising timestamp used by the RxPlayer, at which the
* last position was collected.
*
* These values allows to retrieve at any time in the future the new last
* position, by substracting the position time to the last position, and
* adding to it the new monotonically-raising timestamp.
*
* The last position and position time are returned by this function if and only if
* it would indicate a last position superior to the `minimumTime` given.
*
* This last part allows for example to detect which Period is likely to be the
* "current" one in multi-periods contents. By giving the Period's start as a
* `minimumTime`, you ensure that you will get a value only if the current time
* is in that period.
*
* This is useful as guessing the live time from the clock can be seen as a last
* resort. By detecting that the current time is before the currently considered
* Period, we can just parse and look at the previous Period. If we can guess
* the live time more directly from that previous one, we might be better off
* than just using the clock.
*
* @param {Object} context
* @param {number} minimumTime
* @returns {Array.<number|undefined>}
*/
function guessLastPositionFromClock(context, minimumTime) {
if (!isNullOrUndefined(context.clockOffset)) {
const lastPosition = context.clockOffset / 1000 - context.availabilityStartTime;
const positionTime = getMonotonicTimeStamp() / 1000;
const timeInSec = positionTime + lastPosition;
if (timeInSec >= minimumTime) {
return [timeInSec, positionTime];
}
}
else {
const now = Date.now() / 1000;
if (now >= minimumTime) {
log.warn("DASH Parser: no clock synchronization mechanism found." +
" Using the system clock instead.");
const lastPosition = now - context.availabilityStartTime;
const positionTime = getMonotonicTimeStamp() / 1000;
return [lastPosition, positionTime];
}
}
return undefined;
}
/**
* Try to extract the last position declared for any segments in a Period:
* - If at least a single index' last position is defined, take the maximum
* among them.
* - If segments are available but we cannot define the last position
* return undefined.
* - If no segment are available in that period, return null
* @param {Object} adaptationsPerType
* @returns {number|null|undefined}
*/
function getMaximumLastPosition(adaptationsPerType) {
let maxEncounteredPosition = null;
let allIndexAreEmpty = true;
const adaptationsVal = objectValues(adaptationsPerType).filter((ada) => !isNullOrUndefined(ada));
const allAdaptations = flatMap(adaptationsVal, (adaptationsForType) => adaptationsForType);
for (const adaptation of allAdaptations) {
const representations = adaptation.representations;
for (const representation of representations) {
const position = representation.index.getLastAvailablePosition();
if (position !== null) {
allIndexAreEmpty = false;
if (typeof position === "number") {
maxEncounteredPosition = isNullOrUndefined(maxEncounteredPosition)
? position
: Math.max(maxEncounteredPosition, position);
}
}
}
}
if (!isNullOrUndefined(maxEncounteredPosition)) {
return maxEncounteredPosition;
}
else if (allIndexAreEmpty) {
return null;
}
return undefined;
}
/**
* Generate parsed "eventStream" objects from a `StreamEvent` node's
* intermediate Representation.
* @param {Array.<Object>} baseIr - The array of every encountered StreamEvent's
* intermediate representations for a given Period.
* @param {number} periodStart - The time in seconds at which this corresponding
* Period starts.
* @returns {Array.<Object>} - The parsed objects.
*/
function generateStreamEvents(baseIr, periodStart, xmlNamespaces) {
var _a, _b;
const res = [];
for (const eventStreamIr of baseIr) {
const { schemeIdUri = "", timescale = 1 } = eventStreamIr.attributes;
const allNamespaces = xmlNamespaces.concat((_a = eventStreamIr.attributes.namespaces) !== null && _a !== void 0 ? _a : []);
for (const eventIr of eventStreamIr.children.events) {
if (eventIr.eventStreamData !== undefined) {
const start = ((_b = eventIr.presentationTime) !== null && _b !== void 0 ? _b : 0) / timescale + periodStart;
const end = eventIr.duration === undefined
? undefined
: start + eventIr.duration / timescale;
let element;
let xmlData;
if (!isWorker && eventIr.eventStreamData instanceof Element) {
element = eventIr.eventStreamData;
}
else {
try {
xmlData = {
namespaces: allNamespaces,
data: typeof eventIr.eventStreamData === "string"
? eventIr.eventStreamData
: utf8ToStr(new Uint8Array(eventIr.eventStreamData)),
};
}
catch (err) {
log.error("DASH: Error while parsing event-stream:", err instanceof Error ? err.message : "Unknown error");
}
}
res.push({
start,
end,
id: eventIr.id,
data: {
type: "dash-event-stream",
value: { schemeIdUri, timescale, element, xmlData },
},
});
}
}
}
return res;
}