import Drm from '../../utils/Drm.js';
import * as SRGQuality from '../../utils/SRGQuality.js';
import SRGStreamType from '../../utils/SRGStreamType.js';
import Image from '../../utils/Image.js';
import BlockingReason from '../../utils/BlockingReason.js';
import Utils from '../../utils/Utils.js';
/**
* @class MediaComposition
*/
class MediaComposition {
static cleanCache(mc) {
const cleanedMc = Object.assign({}, mc);
cleanedMc.cachedMainChapter = undefined;
cleanedMc.cachedSegments = undefined;
return cleanedMc;
}
/**
* Find a chapter by his URN
* @param {String} urn
* @returns {Object} chapter
*/
findChapterByUrn(urn) {
if (this.chapterList) {
const [chapter] = this.chapterList.filter(element => element.urn === urn);
return chapter;
}
return undefined;
}
/**
* Find a drm by resource
* @param {Resource} resource
* @returns {Array} of drm
*/
static findDrmListByResource(resource) {
return resource.drmList;
}
/**
* Filter the main resource by quality
* @param {String} quality is set to HD by default
* @return {Array} filtered resources by quality
*/
filterMainResourcesByQuality(quality = SRGQuality.types.HD) {
return this.getMainResources()
.filter(
resource => resource.quality === quality,
);
}
/**
* Find resource list by URN
* @param {String} urn
* @returns {Array|undefined} of resources
*/
findResourceListByUrn(urn) {
const chapterByUrn = this.findChapterByUrn(urn);
if (chapterByUrn) {
return chapterByUrn.resourceList || [];
}
return undefined;
}
/**
* Get the main chapter's image URL decorated with default width and format.
*
* @returns {String|undefined} image URL.
*/
getMainChapterImageUrl() {
const mainChapter = this.getMainChapter();
if (mainChapter && mainChapter.imageUrl) {
return Image.scale({
url: mainChapter.imageUrl,
});
}
return undefined;
}
/**
* Chapters as provided by the server
*
* @returns {Array} of chapters
*/
getChapters() {
return this.chapterList;
}
/**
* Block reason for main chapter. This also uses current date for STARTDATE.
* @returns {undefined|String} undefined if main chapter is not blocked
* @see BlockingReason
*/
getMainBlockReason() {
const mainChapter = this.getMainChapter();
if (!mainChapter) {
return undefined;
}
let { blockReason } = mainChapter;
if (!blockReason && new Date() < this.getMainValidFromDate()) {
blockReason = BlockingReason.STARTDATE;
}
return blockReason;
}
/**
* Compute a date from which this content is valid. Always return a date object.
* @returns {Date} date specified in media composition or EPOCH when no date present.
*/
getMainValidFromDate() {
const mainChapter = this.getMainChapter();
if (mainChapter) {
const { validFrom } = mainChapter;
if (validFrom) {
return new Date(validFrom);
}
}
return new Date(0);
}
/**
* Get the mediaComposition's main chapter
*/
getMainChapter() {
if (!this.cachedMainChapter) {
this.cachedMainChapter = this.findChapterByUrn(this.chapterUrn);
}
if (!this.cachedMainChapter && this.chapterList && this.chapterList.length > 0) {
// Fallback to fix missing chapterUrn in mediaComposition (should not happen)
[this.cachedMainChapter] = this.chapterList;
}
return this.cachedMainChapter;
}
/**
* Get videojs formatted main resources
* @param {String} preferredQuality set the preferred quality SD, HD, HQ
* @see SRGQuality all available qualities
* @param {String} preferredStreamType set the preferred stream type ON_DEMAND, LIVE, DVR
* @see SRGStreamType all available stream types
* @returns {Array} array of sources
*/
getMainResources(preferredQuality = undefined, preferredStreamType = undefined) {
const resourceList = this.getResourceList();
if (!resourceList || !resourceList.length) {
return undefined;
}
const resources = resourceList && resourceList.map((resource) => {
const { keySystems } = Drm.buildKeySystems(resource.drmList);
const [...keySystemOptions] = Drm.buildKeySystemOptions(resource.drmList);
const {
analyticsData,
analyticsMetadata,
dvr,
live,
mimeType: type,
presentation,
quality,
streaming,
streamOffset,
tokenType,
url: src,
} = resource;
const mergedAnalyticsData = this.getMergedAnalyticsData(analyticsData);
const mergedAnalyticsMetadata = this.getMergedAnalyticsMetadata(analyticsMetadata);
const mainSegment = this.findMainSegment();
const {
id, eventData, imageCopyright, mediaType, vendor: bu,
} = this.getMainChapter();
let pendingSeek;
if (this.pendingSeek) {
// eslint-disable-next-line prefer-destructuring
pendingSeek = this.pendingSeek;
} else if (mainSegment) {
pendingSeek = Utils.millisecondsToSeconds(mainSegment.markIn);
} else {
pendingSeek = undefined;
}
return {
analyticsData: mergedAnalyticsData,
analyticsMetadata: mergedAnalyticsMetadata,
bu,
eventData,
id,
imageCopyright,
keySystems,
keySystemOptions,
mediaType,
presentation,
quality,
src,
streaming,
streamOffset,
streamType: SRGStreamType.evaluate(dvr, live),
tokenType,
type,
pendingSegment: mainSegment,
pendingSeek,
playbackSettings: this.playbackSettings,
};
});
let orderedResources = MediaComposition.orderMainResourcesByQuality(
resources,
preferredQuality,
);
orderedResources = MediaComposition.orderMainResourcesByStreamType(
orderedResources,
preferredStreamType,
);
return orderedResources;
}
/**
* Get merged analytics data
* @returns {Object}
*/
getMergedAnalyticsData(analyticsData) {
return {
...this.analyticsData,
...this.getMainChapter().analyticsData,
...analyticsData,
};
}
/**
* Get merged analytics metadata
* @returns {Object}
*/
getMergedAnalyticsMetadata(analyticsMetadata) {
return {
...this.analyticsMetadata,
...this.getMainChapter().analyticsMetadata,
...analyticsMetadata,
};
}
/**
* Filter external text tracks that are already available internally.
*
* __Rules:__
* 1. TTML format is filtered
*
* 2. If both are empty that means only internal text tracks will be displayed
* to the user as they are automatically loaded by the player.
*
* 3. If subtitleInformationList is missing from the MediaComposition and subtitleList
* is available but the media contains internal text tracks that are also available internaly.
* It will result on a duplication client side.
*
* 4. If subtitleList and subtitleInformationList a merge between both will be operated,
* removing the external text tracks already available internaly.
*
* @param {Object} logger
*
* @returns {Array} external text tracks
*/
getFilteredExternalSubtitles(logger = console.log /* eslint-disable-line no-console */) {
const { subtitleList } = this.getMainChapter();
const [resourceWithSubtitles] = this.getResourceList()
.filter(resource => resource.subtitleInformationList);
const { subtitleInformationList } = { ...resourceWithSubtitles };
if ((!subtitleList && !subtitleInformationList) || (!subtitleList && subtitleInformationList)) {
return [];
}
if (subtitleList && !subtitleInformationList) {
logger('MediaComposition: No subtitleInformationList found');
// TTML format is not used anymore
return subtitleList.filter(subtitle => subtitle.format !== 'TTML');
}
const subtitles = subtitleList.filter((subtitle) => {
const addSubtitle = !subtitleInformationList
.find(subtitleInformation => (
subtitleInformation.locale === subtitle.locale
&& subtitle.type === subtitleInformation.type));
return subtitle.format !== 'TTML' && addSubtitle;
});
return subtitles;
}
/**
* Get the chapter's resource list
* @returns {Array} of resources
*/
getResourceList() {
const mainChapter = this.getMainChapter();
return (mainChapter && mainChapter.resourceList) || [];
}
/**
* Get segments of the main chapter ordered by markIn
* @returns {Array} of segments
*/
getMainSegments() {
const mainChapter = this.getMainChapter();
if (!this.cachedSegments && mainChapter && mainChapter.segmentList) {
this.cachedSegments = mainChapter.segmentList;
}
return this.cachedSegments || [];
}
/**
* Return segment from main chapter following segmentUrn in mediaComposition.
* @returns {undefined|*}
*/
findMainSegment() {
if (this.segmentUrn) {
const { segmentList } = this.getMainChapter();
if (segmentList) {
const [segment] = segmentList.filter(element => element.urn === this.segmentUrn);
return segment;
}
}
return undefined;
}
/**
* Get subdivisions to be displayed for this MediaComposition.
* They can be segments or chapters.
*
* @returns {Array} of subdivisions
*/
getSubdivisions() {
const chapters = this.getChapters();
const mainChapter = this.getMainChapter();
const subdivisions = [];
const displayableMainSegments = this.getMainSegments().filter(s => s.displayable);
if (chapters.length === 1 && displayableMainSegments.length === 0) {
return [];
}
chapters.forEach((chapter) => {
const isSegment = chapter.urn === mainChapter.urn && displayableMainSegments.length >= 1;
if (isSegment) {
subdivisions.push(...displayableMainSegments);
} else {
subdivisions.push(chapter);
}
});
return subdivisions;
}
/**
* Order the resources by a preferred quality
* FYI: The [...resourcesToOrder] avoid to mutate the parameter with the sort method
* @param {Array} resourcesToOrder array to order
* @param {String} quality, the default preferred quality is HD
* @see SRGQuality all available qualities
* @returns {Array} resources ordered by a preferred quality
*/
static orderMainResourcesByQuality(
resourcesToOrder,
quality = SRGQuality.types.HD,
) {
const preferredQuality = SRGQuality.list.filter(q => quality !== q);
preferredQuality.push(quality);
return [...resourcesToOrder].sort((a, b) => {
const aq = preferredQuality.indexOf(a.quality);
const bq = preferredQuality.indexOf(b.quality);
return bq - aq;
});
}
/**
* Order the stream types by a preferred type
* FYI: The [...resourcesToOrder] avoid to mutate the parameter with the sort method
* @param {Array} resourcesToOrder array to order
* @param {String} streamType, the default preferred stream type is DVR
* @see SRGStreamType all available stream types
* @returns {Array} resources ordered by a preferred stream type
*/
static orderMainResourcesByStreamType(
resourcesToOrder,
streamType = SRGStreamType.types.DVR,
) {
const preferredType = SRGStreamType.list.filter(s => streamType !== s);
preferredType.push(streamType);
return [...resourcesToOrder].sort((a, b) => {
const as = preferredType.indexOf(a.streamType);
const bs = preferredType.indexOf(b.streamType);
return bs - as;
});
}
}
export default MediaComposition;