dataProvider_model_MediaComposition.js
/**
* Represents the composition of media content.
*
* @class MediaComposition
* @property {string} chapterUrn URN (Uniform Resource Name) of the associated chapter.
* @property {string} segmentUrn URN of the associated segment.
* @property {Episode} episode Associated episode.
* @property {Show} show Associated show.
* @property {Channel} channel Associated channel.
* @property {Array.<Chapter>} chapterList List of associated chapters.
* @property {Array.<Topic>} topicList List of associated topics.
* @property {Object.<String, String>} analyticsData Analytics data associated with the media composition.
* @property {Object.<String, String>} analyticsMetadata Metadata associated with analytics for the media composition.
*/
class MediaComposition {
/**
* Find a chapter by its URN.
*
* @param {String} urn
*
* @returns {Chapter} chapter
*/
findChapterByUrn(urn) {
if (this.chapterList) {
const [chapter] = this.chapterList.filter(
(element) => element.urn === urn
);
return chapter;
}
return undefined;
}
/**
* Return a segment from main chapter following segmentUrn in mediaComposition.
*
* @returns {Segment|undefined} main segment
*/
findMainSegment() {
if (!this.segmentUrn) {
return undefined;
}
const segmentList = this.getMainSegments();
const [segment] = segmentList.filter(
(element) => element.urn === this.segmentUrn
);
return segment;
}
/**
* Find resource list by URN.
*
* @param {String} urn
* @returns {Array.<Resource>|undefined} of resources
*/
findResourceListByUrn(urn) {
const chapterByUrn = this.findChapterByUrn(urn);
if (chapterByUrn) {
return chapterByUrn.resourceList || [];
}
return undefined;
}
/**
* A list of chapters.
*
* @returns {Array.<Chapter>} of chapters
*/
getChapters() {
const AUDIO = 'AUDIO';
if (this.getMainChapter().mediaType === AUDIO) return [];
return this.chapterList.filter(({ mediaType }) => mediaType !== AUDIO);
}
/**
* 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 internally.
* 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 internally.
*
*
* @returns {Array.<Subtitle>} external text tracks
*/
getFilteredExternalSubtitles() {
const { subtitleList } = this.getMainChapter();
const [{ subtitleInformationList } = {}] = this.getResourceList().filter(
({ subtitleInformationList }) => subtitleInformationList
);
const onlyHasExternalSubtitles = subtitleList && !subtitleInformationList;
if (!subtitleList) {
return [];
}
// TTML format is not supported
const subtitles = subtitleList.filter(
(subtitle) => subtitle.format !== 'TTML'
);
if (onlyHasExternalSubtitles) {
return subtitles;
}
return subtitles.filter((subtitle) => {
const addSubtitle = !subtitleInformationList.find(
(subtitleInformation) =>
subtitleInformation.locale === subtitle.locale &&
subtitle.type === subtitleInformation.type
);
return addSubtitle;
});
}
/**
* Block reason for main chapter. This also uses current date for STARTDATE.
*
* @see BlockReason
*
* @returns {string | undefined} undefined if main chapter is not blocked
*/
getMainBlockReason() {
const mainChapter = this.getMainChapter();
if (!mainChapter) {
return undefined;
}
let { blockReason } = mainChapter;
if (!blockReason && new Date() < this.getMainValidFromDate()) {
blockReason = 'STARTDATE';
}
return blockReason;
}
/**
* Get blocked segments from the main chapter.
*
* @returns {Array.<Segment>} of blocked segments
*/
getMainBlockedSegments() {
return this.getMainSegments().filter(segment => segment.blockReason);
}
/**
* Get the mediaComposition's main chapter.
*
* @returns {Chapter}
*/
getMainChapter() {
if (!this.mainChapter) {
this.mainChapter = this.findChapterByUrn(this.chapterUrn);
}
if (!this.mainChapter && this.chapterList && this.chapterList.length > 0) {
[this.mainChapter] = this.chapterList;
}
return this.mainChapter;
}
/**
* 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 undefined;
}
return mainChapter.imageUrl;
}
/**
* Get main resources.
*
* @returns {Array.<MainResource>} array of sources.
*/
// eslint-disable-next-line max-lines-per-function
getMainResources() {
const resourceList = this.getResourceList();
if (!resourceList || !resourceList.length) {
return undefined;
}
return resourceList.map((resource) => ({
analyticsData: this.getMergedAnalyticsData(resource.analyticsData),
analyticsMetadata: this.getMergedAnalyticsMetadata(
resource.analyticsMetadata
),
blockReason: this.getMainChapter().blockReason,
blockedSegments: this.getMainBlockedSegments(),
imageUrl: this.getMainChapterImageUrl(),
chapters: this.getChapters(),
drmList: resource.drmList,
dvr: resource.dvr,
eventData: this.getMainChapter().eventData,
id: this.getMainChapter().id,
imageCopyright: this.getMainChapter().imageCopyright,
intervals: this.getMainTimeIntervals(),
live: resource.live,
mediaType: this.getMainChapter().mediaType,
mimeType: resource.mimeType,
presentation: resource.presentation,
quality: resource.quality,
streaming: resource.streaming,
streamOffset: resource.streamOffset,
subtitles: this.getFilteredExternalSubtitles(),
title: this.getMainChapter().title,
tokenType: resource.tokenType,
url: resource.url,
urn: this.chapterUrn,
vendor: this.getMainChapter().vendor,
}));
}
/**
* Get segments of the main chapter ordered by markIn.
*
* @returns {Array.<Segment>} of segments
*/
getMainSegments() {
const mainChapter = this.getMainChapter();
if (!this.mainSegments && mainChapter && mainChapter.segmentList) {
this.mainSegments = mainChapter.segmentList;
}
return this.mainSegments || [];
}
/**
* Retrieves an array of time intervals associated with the main chapter.
*
* @returns {Array.<TimeInterval>} An array of time intervals.
*/
getMainTimeIntervals() {
const {
timeIntervalList = []
} = this.getMainChapter() || {};
return timeIntervalList;
}
/**
* 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) {
return new Date(0);
}
const { validFrom } = mainChapter;
if (validFrom) {
return new Date(validFrom);
}
}
/**
* Get merged analytics data.
*
* @param {Object.<string, string>} analyticsData
* @returns {Object.<string, string>} Merged analytics data.
*/
getMergedAnalyticsData(analyticsData) {
return {
...this.analyticsData,
...this.getMainChapter().analyticsData,
...analyticsData,
};
}
/**
* Get merged analytics metadata.
*
* @param {Object.<string, string>} analyticsMetadata
* @returns {Object.<string, string>} Merged analytics metadata.
*/
getMergedAnalyticsMetadata(analyticsMetadata) {
return {
...this.analyticsMetadata,
...this.getMainChapter().analyticsMetadata,
...analyticsMetadata,
};
}
/**
* Get the chapter's resource list
* @returns {Array.<Resource>} of resources
*/
getResourceList() {
const { resourceList } = this.getMainChapter();
return resourceList || [];
}
}
export default MediaComposition;
/**
* @typedef {import('./typedef').Channel} Channel
* @typedef {import('./typedef').Chapter} Chapter
* @typedef {import('./typedef').Episode} Episode
* @typedef {import('./typedef').Resource} Resource
* @typedef {import('./typedef').Segment} Segment
* @typedef {import('./typedef').Show} Show
* @typedef {import('./typedef').Subtitle} Subtitle
* @typedef {import('./typedef').TimeInterval} TimeInterval
* @typedef {import('./typedef').Topic} Topic
* @typedef {import('./typedef').MainResource} MainResource
*/