rx-player
Version:
Canal+ HTML5 Video Player
435 lines (434 loc) • 18.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 { SUPPORTED_ADAPTATIONS_TYPE } from "../../../../manifest";
import arrayFind from "../../../../utils/array_find";
import arrayFindIndex from "../../../../utils/array_find_index";
import arrayIncludes from "../../../../utils/array_includes";
import isNonEmptyString from "../../../../utils/is_non_empty_string";
import isNullOrUndefined from "../../../../utils/is_null_or_undefined";
import attachTrickModeTrack from "./attach_trickmode_track";
import inferAdaptationType, { getThumbnailAdaptationSetInfo, } from "./infer_adaptation_type";
import parseRepresentations from "./parse_representations";
import resolveBaseURLs from "./resolve_base_urls";
/**
* Detect if the accessibility given defines an adaptation for the visually
* impaired.
* Based on DVB Document A168 (DVB-DASH) and DASH-IF 4.3.
* @param {Object} accessibility
* @returns {Boolean}
*/
function isVisuallyImpaired(accessibility) {
if (accessibility === undefined) {
return false;
}
const isVisuallyImpairedAudioDvbDash = accessibility.schemeIdUri === "urn:tva:metadata:cs:AudioPurposeCS:2007" &&
accessibility.value === "1";
const isVisuallyImpairedDashIf = accessibility.schemeIdUri === "urn:mpeg:dash:role:2011" &&
accessibility.value === "description";
return isVisuallyImpairedAudioDvbDash || isVisuallyImpairedDashIf;
}
/**
* Detect if the accessibility given defines an adaptation for the hard of
* hearing.
* Based on DVB Document A168 (DVB-DASH) and DASH specification.
* @param {Array.<Object>} accessibilities
* @param {Array.<Object>} roles
* @returns {Boolean}
*/
function isCaptionning(accessibilities, roles) {
if (accessibilities !== undefined) {
const hasDvbClosedCaptionSignaling = accessibilities.some((accessibility) => accessibility.schemeIdUri === "urn:tva:metadata:cs:AudioPurposeCS:2007" &&
accessibility.value === "2");
if (hasDvbClosedCaptionSignaling) {
return true;
}
}
if (roles !== undefined) {
const hasDashCaptionSinaling = roles.some((role) => role.schemeIdUri === "urn:mpeg:dash:role:2011" && role.value === "caption");
if (hasDashCaptionSinaling) {
return true;
}
}
return false;
}
/**
* Detect if the accessibility given defines an AdaptationSet containing a sign
* language interpretation.
* Based on DASH-IF 4.3.
* @param {Object} accessibility
* @returns {Boolean}
*/
function hasSignLanguageInterpretation(accessibility) {
if (accessibility === undefined) {
return false;
}
return (accessibility.schemeIdUri === "urn:mpeg:dash:role:2011" &&
accessibility.value === "sign");
}
/**
* Contruct Adaptation ID from the information we have.
* @param {Object} adaptation
* @param {Object} infos
* @returns {string}
*/
function getAdaptationID(adaptation, infos) {
if (isNonEmptyString(adaptation.attributes.id)) {
return adaptation.attributes.id;
}
const { isClosedCaption, isForcedSubtitle, isAudioDescription, isSignInterpreted, isTrickModeTrack, type, } = infos;
let idString = type;
if (isNonEmptyString(adaptation.attributes.language)) {
idString += `-${adaptation.attributes.language}`;
}
if (isClosedCaption === true) {
idString += "-cc";
}
if (isForcedSubtitle === true) {
idString += "-cc";
}
if (isAudioDescription === true) {
idString += "-ad";
}
if (isSignInterpreted === true) {
idString += "-si";
}
if (isTrickModeTrack) {
idString += "-trickMode";
}
if (isNonEmptyString(adaptation.attributes.contentType)) {
idString += `-${adaptation.attributes.contentType}`;
}
if (isNonEmptyString(adaptation.attributes.codecs)) {
idString += `-${adaptation.attributes.codecs}`;
}
if (isNonEmptyString(adaptation.attributes.mimeType)) {
idString += `-${adaptation.attributes.mimeType}`;
}
if (adaptation.attributes.frameRate !== undefined) {
idString += `-${String(adaptation.attributes.frameRate)}`;
}
return idString;
}
/**
* Returns a list of ID this adaptation can be seamlessly switched to
* @param {Object} adaptation
* @returns {Array.<string>}
*/
function getAdaptationSetSwitchingIDs(adaptation) {
if (!isNullOrUndefined(adaptation.children.supplementalProperties)) {
const { supplementalProperties } = adaptation.children;
for (const supplementalProperty of supplementalProperties) {
if (supplementalProperty.schemeIdUri ===
"urn:mpeg:dash:adaptation-set-switching:2016" &&
!isNullOrUndefined(supplementalProperty.value)) {
return supplementalProperty.value
.split(",")
.map((id) => id.trim())
.filter((id) => isNonEmptyString(id));
}
}
}
return [];
}
/**
* Process AdaptationSets intermediate representations to return under its final
* form.
* Note that the AdaptationSets returned are sorted by priority (from the most
* priority to the least one).
* @param {Array.<Object>} adaptationsIR
* @param {Object} context
* @returns {Array.<Object>}
*/
export default function parseAdaptationSets(adaptationsIR, context) {
var _a, _b, _c, _d, _e, _f, _g;
const parsedAdaptations = { video: [], audio: [], text: [] };
const parsedThumbnailTracks = [];
const trickModeAdaptations = [];
const adaptationSwitchingInfos = {};
const parsedAdaptationsIDs = [];
for (let adaptationIdx = 0; adaptationIdx < adaptationsIR.length; adaptationIdx++) {
const adaptation = adaptationsIR[adaptationIdx];
const adaptationChildren = adaptation.children;
const { essentialProperties, roles, label } = adaptationChildren;
const isMainAdaptation = Array.isArray(roles) &&
roles.some((role) => role.value === "main") &&
roles.some((role) => role.schemeIdUri === "urn:mpeg:dash:role:2011");
const representationsIR = adaptation.children.representations;
const availabilityTimeComplete = (_a = adaptation.attributes.availabilityTimeComplete) !== null && _a !== void 0 ? _a : context.availabilityTimeComplete;
let availabilityTimeOffset;
if (adaptation.attributes.availabilityTimeOffset !== undefined ||
context.availabilityTimeOffset !== undefined) {
availabilityTimeOffset =
((_b = adaptation.attributes.availabilityTimeOffset) !== null && _b !== void 0 ? _b : 0) +
((_c = context.availabilityTimeOffset) !== null && _c !== void 0 ? _c : 0);
}
const type = inferAdaptationType(adaptation, representationsIR);
if (type === undefined) {
continue;
}
const priority = (_d = adaptation.attributes.selectionPriority) !== null && _d !== void 0 ? _d : 1;
const originalID = adaptation.attributes.id;
const adaptationSetSwitchingIDs = getAdaptationSetSwitchingIDs(adaptation);
const parentSegmentTemplates = [];
if (context.segmentTemplate !== undefined) {
parentSegmentTemplates.push(context.segmentTemplate);
}
if (adaptation.children.segmentTemplate !== undefined) {
parentSegmentTemplates.push(adaptation.children.segmentTemplate);
}
const reprCtxt = {
availabilityTimeComplete,
availabilityTimeOffset,
baseURLs: resolveBaseURLs(context.baseURLs, adaptationChildren.baseURLs),
contentProtectionParser: context.contentProtectionParser,
manifestBoundsCalculator: context.manifestBoundsCalculator,
end: context.end,
isDynamic: context.isDynamic,
isLastPeriod: context.isLastPeriod,
manifestProfiles: context.manifestProfiles,
parentSegmentTemplates,
receivedTime: context.receivedTime,
start: context.start,
unsafelyBaseOnPreviousAdaptation: null,
};
const trickModeProperty = Array.isArray(essentialProperties)
? arrayFind(essentialProperties, (scheme) => {
return scheme.schemeIdUri === "http://dashif.org/guidelines/trickmode";
})
: undefined;
const trickModeAttachedAdaptationIds = (_e = trickModeProperty === null || trickModeProperty === void 0 ? void 0 : trickModeProperty.value) === null || _e === void 0 ? void 0 : _e.split(" ");
const isTrickModeTrack = trickModeAttachedAdaptationIds !== undefined;
const { accessibilities } = adaptationChildren;
let isDub;
if (roles !== undefined && roles.some((role) => role.value === "dub")) {
isDub = true;
}
let isClosedCaption;
if (type !== "text") {
isClosedCaption = false;
}
else {
isClosedCaption = isCaptionning(accessibilities, roles);
}
let isForcedSubtitle;
if (type === "text" &&
roles !== undefined &&
roles.some((role) => role.value === "forced-subtitle" || role.value === "forced_subtitle")) {
isForcedSubtitle = true;
}
let isAudioDescription;
if (type !== "audio") {
isAudioDescription = false;
}
else if (accessibilities !== undefined) {
isAudioDescription = accessibilities.some(isVisuallyImpaired);
}
let isSignInterpreted;
if (type !== "video") {
isSignInterpreted = false;
}
else if (accessibilities !== undefined) {
isSignInterpreted = accessibilities.some(hasSignLanguageInterpretation);
}
let adaptationID = getAdaptationID(adaptation, {
isAudioDescription,
isForcedSubtitle,
isClosedCaption,
isSignInterpreted,
isTrickModeTrack,
type,
});
// Avoid duplicate IDs
while (arrayIncludes(parsedAdaptationsIDs, adaptationID)) {
adaptationID += "-dup";
}
const newID = adaptationID;
parsedAdaptationsIDs.push(adaptationID);
reprCtxt.unsafelyBaseOnPreviousAdaptation =
(_g = (_f = context.unsafelyBaseOnPreviousPeriod) === null || _f === void 0 ? void 0 : _f.getAdaptation(adaptationID)) !== null && _g !== void 0 ? _g : null;
const representations = parseRepresentations(representationsIR, adaptation, reprCtxt);
if (type === "thumbnails") {
const track = createThumbnailTracks(adaptation, representations);
if (track !== null) {
parsedThumbnailTracks.push(...track);
}
continue;
}
const parsedAdaptationSet = {
id: adaptationID,
representations,
type,
isTrickModeTrack,
};
if (!isNullOrUndefined(adaptation.attributes.language)) {
parsedAdaptationSet.language = adaptation.attributes.language;
}
if (!isNullOrUndefined(isClosedCaption)) {
parsedAdaptationSet.closedCaption = isClosedCaption;
}
if (!isNullOrUndefined(isAudioDescription)) {
parsedAdaptationSet.audioDescription = isAudioDescription;
}
if (isDub === true) {
parsedAdaptationSet.isDub = true;
}
if (isForcedSubtitle !== undefined) {
parsedAdaptationSet.forcedSubtitles = isForcedSubtitle;
}
if (isSignInterpreted === true) {
parsedAdaptationSet.isSignInterpreted = true;
}
if (label !== undefined) {
parsedAdaptationSet.label = label;
}
if (trickModeAttachedAdaptationIds !== undefined) {
trickModeAdaptations.push({
adaptation: parsedAdaptationSet,
trickModeAttachedAdaptationIds,
});
}
else {
// look if we have to merge this into another Adaptation
let mergedIntoIdx = -1;
for (const id of adaptationSetSwitchingIDs) {
const switchingInfos = adaptationSwitchingInfos[id];
if (switchingInfos !== undefined &&
switchingInfos.newID !== newID &&
arrayIncludes(switchingInfos.adaptationSetSwitchingIDs, originalID)) {
mergedIntoIdx = arrayFindIndex(parsedAdaptations[type], (a) => a[0].id === id);
const mergedInto = parsedAdaptations[type][mergedIntoIdx];
if (mergedInto !== undefined &&
mergedInto[0].audioDescription === parsedAdaptationSet.audioDescription &&
mergedInto[0].closedCaption === parsedAdaptationSet.closedCaption &&
mergedInto[0].language === parsedAdaptationSet.language) {
log.info('DASH Parser: merging "switchable" AdaptationSets', originalID, id);
mergedInto[0].representations.push(...parsedAdaptationSet.representations);
mergedInto[1] = {
priority: Math.max(priority, mergedInto[1].priority),
isMainAdaptation: isMainAdaptation || mergedInto[1].isMainAdaptation,
indexInMpd: Math.min(adaptationIdx, mergedInto[1].indexInMpd),
};
break;
}
}
}
if (mergedIntoIdx < 0) {
parsedAdaptations[type].push([
parsedAdaptationSet,
{ priority, isMainAdaptation, indexInMpd: adaptationIdx },
]);
}
}
if (!isNullOrUndefined(originalID) &&
isNullOrUndefined(adaptationSwitchingInfos[originalID])) {
adaptationSwitchingInfos[originalID] = {
newID,
adaptationSetSwitchingIDs,
};
}
}
const adaptationsPerType = SUPPORTED_ADAPTATIONS_TYPE.reduce((acc, adaptationType) => {
const adaptationsParsedForType = parsedAdaptations[adaptationType];
if (adaptationsParsedForType.length > 0) {
adaptationsParsedForType.sort(compareAdaptations);
acc[adaptationType] = adaptationsParsedForType.map(([parsedAdaptation]) => parsedAdaptation);
}
return acc;
}, {});
parsedAdaptations.video.sort(compareAdaptations);
attachTrickModeTrack(adaptationsPerType, trickModeAdaptations);
return {
adaptations: adaptationsPerType,
thumbnailTracks: parsedThumbnailTracks,
};
}
/**
* From the given attributes, returns a parsed thumbnail track, or null if it
* fails to do so.
* @param {Object} adaptation
* @param {Array.<Object>} representations
* @returns {Object|null}
*/
function createThumbnailTracks(adaptation, representations) {
var _a, _b;
const tracks = [];
for (let i = 0; i < representations.length; i++) {
const representation = representations[i];
if (representation !== undefined) {
if (representation.mimeType === undefined) {
log.warn("DASH: Invalid thumbnails Representation, no mime-type");
continue;
}
const tileInfo = getThumbnailAdaptationSetInfo(adaptation, adaptation.children.representations[i]);
if (tileInfo === null) {
continue;
}
if (representation.height === undefined) {
log.warn("DASH: Invalid thumbnails Representation, no height information");
continue;
}
if (representation.width === undefined) {
log.warn("DASH: Invalid thumbnails Representation, no width information");
continue;
}
const start = (_a = representation.index.getFirstAvailablePosition()) !== null && _a !== void 0 ? _a : undefined;
const end = (_b = representation.index.getEnd()) !== null && _b !== void 0 ? _b : undefined;
let segmentDuration;
const targetDuration = representation.index.getTargetSegmentDuration();
if (targetDuration !== undefined && targetDuration.isPrecize) {
segmentDuration = targetDuration.duration;
}
else {
log.warn("DASH: Cannot produce duration estimate for thumbnail track");
}
tracks.push({
id: representation.id,
cdnMetadata: representation.cdnMetadata,
index: representation.index,
mimeType: representation.mimeType,
height: representation.height,
width: representation.width,
horizontalTiles: tileInfo.horizontalTiles,
verticalTiles: tileInfo.verticalTiles,
start,
end,
tileDuration: segmentDuration === undefined
? undefined
: segmentDuration / (tileInfo.horizontalTiles * tileInfo.verticalTiles),
});
}
}
return tracks;
}
/**
* Compare groups of parsed AdaptationSet, alongside some ordering metadata,
* allowing to easily sort them through JavaScript's `Array.prototype.sort`
* method.
* @param {Array.<Object>} a
* @param {Array.<Object>} b
* @returns {number}
*/
function compareAdaptations(a, b) {
const priorityDiff = b[1].priority - a[1].priority;
if (priorityDiff !== 0) {
return priorityDiff;
}
if (a[1].isMainAdaptation !== b[1].isMainAdaptation) {
return a[1].isMainAdaptation ? -1 : 1;
}
return a[1].indexInMpd - b[1].indexInMpd;
}