/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.offline.ManifestConverter');
goog.require('goog.asserts');
goog.require('shaka.media.InitSegmentReference');
goog.require('shaka.media.PresentationTimeline');
goog.require('shaka.media.SegmentIndex');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.offline.OfflineUri');
goog.require('shaka.util.ManifestParserUtils');
/**
* Utility class for converting database manifest objects back to normal
* player-ready objects. Used by the offline system to convert on-disk
* objects back to the in-memory objects.
*/
shaka.offline.ManifestConverter = class {
/**
* Create a new manifest converter. Need to know the mechanism and cell that
* the manifest is from so that all segments paths can be created.
*
* @param {string} mechanism
* @param {string} cell
*/
constructor(mechanism, cell) {
/** @private {string} */
this.mechanism_ = mechanism;
/** @private {string} */
this.cell_ = cell;
}
/**
* Convert a |shaka.extern.ManifestDB| object to a |shaka.extern.Manifest|
* object.
*
* @param {shaka.extern.ManifestDB} manifestDB
* @return {shaka.extern.Manifest}
*/
fromManifestDB(manifestDB) {
const timeline = new shaka.media.PresentationTimeline(null, 0);
timeline.setDuration(manifestDB.duration);
/** @type {!Array.<shaka.extern.StreamDB>} */
const audioStreams =
manifestDB.streams.filter((streamDB) => this.isAudio_(streamDB));
/** @type {!Array.<shaka.extern.StreamDB>} */
const videoStreams =
manifestDB.streams.filter((streamDB) => this.isVideo_(streamDB));
/** @type {!Map.<number, shaka.extern.Variant>} */
const variants = this.createVariants(audioStreams, videoStreams, timeline);
/** @type {!Array.<shaka.extern.Stream>} */
const textStreams =
manifestDB.streams.filter((streamDB) => this.isText_(streamDB))
.map((streamDB) => this.fromStreamDB_(streamDB, timeline));
/** @type {!Array.<shaka.extern.Stream>} */
const imageStreams =
manifestDB.streams.filter((streamDB) => this.isImage_(streamDB))
.map((streamDB) => this.fromStreamDB_(streamDB, timeline));
const drmInfos = manifestDB.drmInfo ? [manifestDB.drmInfo] : [];
if (manifestDB.drmInfo) {
for (const variant of variants.values()) {
if (variant.audio && variant.audio.encrypted) {
variant.audio.drmInfos = drmInfos;
}
if (variant.video && variant.video.encrypted) {
variant.video.drmInfos = drmInfos;
}
}
}
return {
presentationTimeline: timeline,
minBufferTime: 2,
offlineSessionIds: manifestDB.sessionIds,
variants: Array.from(variants.values()),
textStreams: textStreams,
imageStreams: imageStreams,
};
}
/**
* Recreates Variants from audio and video StreamDB collections.
*
* @param {!Array.<!shaka.extern.StreamDB>} audios
* @param {!Array.<!shaka.extern.StreamDB>} videos
* @param {shaka.media.PresentationTimeline} timeline
* @return {!Map.<number, !shaka.extern.Variant>}
*/
createVariants(audios, videos, timeline) {
// Get all the variant ids from all audio and video streams.
/** @type {!Set.<number>} */
const variantIds = new Set();
for (const streamDB of audios) {
for (const id of streamDB.variantIds) {
variantIds.add(id);
}
}
for (const streamDB of videos) {
for (const id of streamDB.variantIds) {
variantIds.add(id);
}
}
/** @type {!Map.<number, shaka.extern.Variant>} */
const variantMap = new Map();
for (const id of variantIds) {
variantMap.set(id, this.createEmptyVariant_(id));
}
// Assign each audio stream to its variants.
for (const audio of audios) {
/** @type {shaka.extern.Stream} */
const stream = this.fromStreamDB_(audio, timeline);
for (const variantId of audio.variantIds) {
const variant = variantMap.get(variantId);
goog.asserts.assert(
!variant.audio, 'A variant should only have one audio stream');
variant.language = stream.language;
variant.primary = variant.primary || stream.primary;
variant.audio = stream;
}
}
// Assign each video stream to its variants.
for (const video of videos) {
/** @type {shaka.extern.Stream} */
const stream = this.fromStreamDB_(video, timeline);
for (const variantId of video.variantIds) {
const variant = variantMap.get(variantId);
goog.asserts.assert(
!variant.video, 'A variant should only have one video stream');
variant.primary = variant.primary || stream.primary;
variant.video = stream;
}
}
return variantMap;
}
/**
* @param {shaka.extern.StreamDB} streamDB
* @param {shaka.media.PresentationTimeline} timeline
* @return {shaka.extern.Stream}
* @private
*/
fromStreamDB_(streamDB, timeline) {
/** @type {!Array.<!shaka.media.SegmentReference>} */
const segments = streamDB.segments.map(
(segment, index) => this.fromSegmentDB_(index, segment));
timeline.notifySegments(segments);
/** @type {!shaka.media.SegmentIndex} */
const segmentIndex = new shaka.media.SegmentIndex(segments);
/** @type {shaka.extern.Stream} */
const stream = {
id: streamDB.id,
originalId: streamDB.originalId,
createSegmentIndex: () => Promise.resolve(),
segmentIndex,
mimeType: streamDB.mimeType,
codecs: streamDB.codecs,
width: streamDB.width || undefined,
height: streamDB.height || undefined,
frameRate: streamDB.frameRate,
pixelAspectRatio: streamDB.pixelAspectRatio,
hdr: streamDB.hdr,
kind: streamDB.kind,
encrypted: streamDB.encrypted,
drmInfos: [],
keyIds: streamDB.keyIds,
language: streamDB.language,
label: streamDB.label,
type: streamDB.type,
primary: streamDB.primary,
trickModeVideo: null,
emsgSchemeIdUris: null,
roles: streamDB.roles,
forced: streamDB.forced,
channelsCount: streamDB.channelsCount,
audioSamplingRate: streamDB.audioSamplingRate,
spatialAudio: streamDB.spatialAudio,
closedCaptions: streamDB.closedCaptions,
tilesLayout: streamDB.tilesLayout,
};
return stream;
}
/**
* @param {number} index
* @param {shaka.extern.SegmentDB} segmentDB
* @return {!shaka.media.SegmentReference}
* @private
*/
fromSegmentDB_(index, segmentDB) {
/** @type {!shaka.offline.OfflineUri} */
const uri = shaka.offline.OfflineUri.segment(
this.mechanism_, this.cell_, segmentDB.dataKey);
const initSegmentReference = segmentDB.initSegmentKey != null ?
this.fromInitSegmentDB_(segmentDB.initSegmentKey) : null;
return new shaka.media.SegmentReference(
segmentDB.startTime,
segmentDB.endTime,
() => [uri.toString()],
/* startByte= */ 0,
/* endByte= */ null,
initSegmentReference,
segmentDB.timestampOffset,
segmentDB.appendWindowStart,
segmentDB.appendWindowEnd);
}
/**
* @param {number} key
* @return {!shaka.media.InitSegmentReference}
* @private
*/
fromInitSegmentDB_(key) {
/** @type {!shaka.offline.OfflineUri} */
const uri = shaka.offline.OfflineUri.segment(
this.mechanism_, this.cell_, key);
return new shaka.media.InitSegmentReference(
() => [uri.toString()],
/* startBytes= */ 0,
/* endBytes= */ null );
}
/**
* @param {shaka.extern.StreamDB} streamDB
* @return {boolean}
* @private
*/
isAudio_(streamDB) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
return streamDB.type == ContentType.AUDIO;
}
/**
* @param {shaka.extern.StreamDB} streamDB
* @return {boolean}
* @private
*/
isVideo_(streamDB) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
return streamDB.type == ContentType.VIDEO;
}
/**
* @param {shaka.extern.StreamDB} streamDB
* @return {boolean}
* @private
*/
isText_(streamDB) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
return streamDB.type == ContentType.TEXT;
}
/**
* @param {shaka.extern.StreamDB} streamDB
* @return {boolean}
* @private
*/
isImage_(streamDB) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
return streamDB.type == ContentType.IMAGE;
}
/**
* Creates an empty Variant.
*
* @param {number} id
* @return {!shaka.extern.Variant}
* @private
*/
createEmptyVariant_(id) {
return {
id: id,
language: '',
primary: false,
audio: null,
video: null,
bandwidth: 0,
allowedByApplication: true,
allowedByKeySystem: true,
decodingInfos: [],
};
}
};