rx-player
Version:
Canal+ HTML5 Video Player
393 lines (367 loc) • 14.2 kB
text/typescript
/**
* 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 type { IManifest } from "../../../../manifest";
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 type {
IManifestStreamEvent,
IParsedAdaptation,
IParsedAdaptations,
IParsedPeriod,
} from "../../types";
import type {
IEventStreamIntermediateRepresentation,
IPeriodIntermediateRepresentation,
} from "../node_parser_types";
import type ContentProtectionParser from "./content_protection_parser";
import flattenOverlappingPeriods from "./flatten_overlapping_periods";
import getPeriodsTimeInformation from "./get_periods_time_infos";
import type { IAdaptationSetContext } from "./parse_adaptation_sets";
import parseAdaptationSets from "./parse_adaptation_sets";
import resolveBaseURLs from "./resolve_base_urls";
const generatePeriodID = idGenerator();
/** Information about each linked Xlink. */
export type IXLinkInfos = WeakMap<
IPeriodIntermediateRepresentation,
{
/** Real URL (post-redirection) used to download this xlink. */
url?: string | undefined;
/** Time at which the request was sent (since the time origin), in ms. */
sendingTime?: number | undefined;
/** Time at which the request was received (since the time origin), in ms. */
receivedTime?: number | undefined;
}
>;
/**
* Process intermediate periods to create final parsed periods.
* @param {Array.<Object>} periodsIR
* @param {Object} context
* @returns {Array.<Object>}
*/
export default function parsePeriods(
periodsIR: IPeriodIntermediateRepresentation[],
context: IPeriodContext,
): IParsedPeriod[] {
const parsedPeriods: IParsedPeriod[] = [];
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: string;
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 =
context.unsafelyBaseOnPreviousManifest?.getPeriod(periodID) ?? null;
const availabilityTimeComplete = periodIR.attributes.availabilityTimeComplete;
const availabilityTimeOffset = periodIR.attributes.availabilityTimeOffset;
const { manifestProfiles, contentProtectionParser } = context;
const { segmentTemplate } = periodIR.children;
contentProtectionParser.addReferences(periodIR.children.contentProtections ?? []);
const adapCtxt: IAdaptationSetContext = {
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 = (context.xmlNamespaces ?? []).concat(
periodIR.attributes.namespaces ?? [],
);
const streamEvents = generateStreamEvents(
periodIR.children.eventStreams,
periodStart,
namespaces,
);
const parsedPeriod: IParsedPeriod = {
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: IPeriodContext,
minimumTime: number,
): [number, number] | undefined {
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: IParsedAdaptations,
): number | null | undefined {
let maxEncounteredPosition: number | null = null;
let allIndexAreEmpty = true;
const adaptationsVal = objectValues(adaptationsPerType).filter(
(ada): ada is IParsedAdaptation[] => !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: IEventStreamIntermediateRepresentation[],
periodStart: number,
xmlNamespaces: Array<{ key: string; value: string }>,
): IManifestStreamEvent[] {
const res: IManifestStreamEvent[] = [];
for (const eventStreamIr of baseIr) {
const { schemeIdUri = "", timescale = 1 } = eventStreamIr.attributes;
const allNamespaces = xmlNamespaces.concat(eventStreamIr.attributes.namespaces ?? []);
for (const eventIr of eventStreamIr.children.events) {
if (eventIr.eventStreamData !== undefined) {
const start = (eventIr.presentationTime ?? 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 as ArrayBuffer)),
};
} 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;
}
/** Context needed when calling `parsePeriods`. */
export interface IPeriodContext extends IInheritedAdaptationContext {
availabilityStartTime: number;
/**
* Difference between the server's clock, in milliseconds, and the
* monotonically-raising timestamp used by the RxPlayer.
*/
clockOffset?: number | undefined;
/** Duration (mediaPresentationDuration) of the whole MPD, in seconds. */
duration?: number | undefined;
/**
* The parser should take this Manifest - which is a previously parsed
* Manifest for the same dynamic content - as a base to speed-up the parsing
* process.
* /!\ If unexpected differences exist between the two, there is a risk of
* de-synchronization with what is actually on the server,
* Use with moderation.
*/
unsafelyBaseOnPreviousManifest: IManifest | null;
xlinkInfos: IXLinkInfos;
/**
* XML namespaces linked to the `<MPD>` element.
* May be needed to convert EventStream's Event elements back into the
* Document form.
*/
xmlNamespaces?: Array<{ key: string; value: string }> | undefined;
/** Parses contentProtection elements. */
contentProtectionParser: ContentProtectionParser;
}
type IInheritedAdaptationContext = Omit<
IAdaptationSetContext,
| "availabilityTimeComplete"
| "availabilityTimeOffset"
| "duration"
| "isLastPeriod"
| "start"
| "unsafelyBaseOnPreviousPeriod"
>;