Files
shaka-player/lib/offline/offline_utils.js
T
Joey Parrish 4334b6ea90 Add mediaSegmentReferences to shakaExtern.Stream.
This will allow us, in many cases, to detect time sync issues in
content by comparing the media segment references to the presentation
timeline.  If the references are outside the timeline, it may indicate
a common form of bad content, and we may be able to attempt to correct
it.

In some cases, the references are not available upfront.  We can't
detect time sync issues in DASH with SegmentTemplate+duration, because
the references are not explicitly described by the manifest.  We can't
detect time sync issues in DASH with SegmentBase, because the segment
index requires additional fetching and parsing.  (SegmentBase is not
used with live content, so this should be a non-issue.)

This only introduces the new data, but does not use it for detection
or correction yet.

Issue #933

Change-Id: If70b1bdbd3b08a7c8b7ae296da209737492dfe17
2017-07-25 20:13:42 +00:00

287 lines
8.7 KiB
JavaScript

/**
* @license
* Copyright 2016 Google Inc.
*
* 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.
*/
goog.provide('shaka.offline.OfflineUtils');
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.DBEngine');
goog.require('shaka.offline.IStorageEngine');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.StreamUtils');
/** @const {!Object.<string, string>} */
shaka.offline.OfflineUtils.DB_SCHEME = {'manifest': 'key', 'segment': 'key'};
/**
* Converts the given database manifest to a storedContent structure.
*
* @param {shakaExtern.ManifestDB} manifest
* @return {shakaExtern.StoredContent}
*/
shaka.offline.OfflineUtils.getStoredContent = function(manifest) {
goog.asserts.assert(manifest.periods.length > 0,
'Must be at least one Period.');
// Reconstruct the first period to get the variants
var timeline = new shaka.media.PresentationTimeline(null, 0);
var period = shaka.offline.OfflineUtils.reconstructPeriod(
manifest.periods[0], [], timeline);
var tracks = shaka.util.StreamUtils.getVariantTracks(period, null, null);
var textTracks = shaka.util.StreamUtils.getTextTracks(period, null);
tracks.push.apply(tracks, textTracks);
return {
offlineUri: 'offline:' + manifest.key,
originalManifestUri: manifest.originalManifestUri,
duration: manifest.duration,
size: manifest.size,
expiration: manifest.expiration == undefined ? Infinity :
manifest.expiration,
tracks: tracks,
appMetadata: manifest.appMetadata
};
};
/**
* Reconstructs a period object from the given database period.
*
* @param {shakaExtern.PeriodDB} period
* @param {!Array.<shakaExtern.DrmInfo>} drmInfos
* @param {shaka.media.PresentationTimeline} timeline
* @return {shakaExtern.Period}
*/
shaka.offline.OfflineUtils.reconstructPeriod = function(
period, drmInfos, timeline) {
var OfflineUtils = shaka.offline.OfflineUtils;
var ContentType = shaka.util.ManifestParserUtils.ContentType;
var textStreamsDb = period.streams.filter(function(streamDb) {
return streamDb.contentType == ContentType.TEXT;
});
var audioStreamsDb = period.streams.filter(function(streamDb) {
return streamDb.contentType == ContentType.AUDIO;
});
var videoStreamsDb = period.streams.filter(function(streamDb) {
return streamDb.contentType == ContentType.VIDEO;
});
var variants =
OfflineUtils.createVariants_(audioStreamsDb, videoStreamsDb, drmInfos);
var textStreams = textStreamsDb.map(OfflineUtils.createStream_);
period.streams.forEach(function(streamDb) {
var refs = OfflineUtils.getSegmentReferences_(streamDb);
timeline.notifySegments(period.startTime, refs);
});
return {
startTime: period.startTime,
variants: variants,
textStreams: textStreams
};
};
/**
* @param {!shakaExtern.StreamDB} streamDb
* @return {!Array.<!shaka.media.SegmentReference>}
* @private
*/
shaka.offline.OfflineUtils.getSegmentReferences_ = function(streamDb) {
return streamDb.segments.map(function(segment, i) {
var getUris = function() { return [segment.uri]; };
return new shaka.media.SegmentReference(
i, segment.startTime, segment.endTime, getUris, 0, null);
});
};
/**
* Creates Variants from audio and video StreamDB collections.
*
* @param {!Array.<!shakaExtern.StreamDB>} audios
* @param {!Array.<!shakaExtern.StreamDB>} videos
* @param {!Array.<!shakaExtern.DrmInfo>} drmInfos
* @return {!Array.<!shakaExtern.Variant>}
* @private
*/
shaka.offline.OfflineUtils.createVariants_ = function(
audios, videos, drmInfos) {
var variants = [];
if (!audios.length && !videos.length) return variants;
// Create a single null element so the double loop will work for audio-only or
// video-only variants.
if (!audios.length) {
audios = [null];
} else if (!videos.length) {
videos = [null];
}
var OfflineUtils = shaka.offline.OfflineUtils;
var id = 0;
for (var i = 0; i < audios.length; i++) {
for (var j = 0; j < videos.length; j++) {
if (OfflineUtils.areCompatible_(audios[i], videos[j])) {
var variant =
OfflineUtils.createVariant_(audios[i], videos[j], drmInfos, id++);
variants.push(variant);
}
}
}
return variants;
};
/**
* Checks if two streams can be combined into a variant.
*
* @param {?shakaExtern.StreamDB} stream1
* @param {?shakaExtern.StreamDB} stream2
* @return {boolean}
* @private
*/
shaka.offline.OfflineUtils.areCompatible_ = function(stream1, stream2) {
// Treat content that doesn't have variantIds as compatible
// with anything for compatibility with content stored before
// the variants were introduced.
if (!stream1 || !stream2 || !stream1.variantIds || !stream2.variantIds)
return true;
for (var i = 0; i < stream1.variantIds.length; i++) {
var containsId = stream2.variantIds.some(function(id) {
return id == stream1.variantIds[i];
});
if (containsId) {
return true;
}
}
return false;
};
/**
* Creates a Variant from an audio and a video StreamDBs.
* If one of the streams is null, it creates a Variant from the other.
*
* @param {?shakaExtern.StreamDB} audio
* @param {?shakaExtern.StreamDB} video
* @param {!Array.<!shakaExtern.DrmInfo>} drmInfos
* @param {number} id
* @return {!shakaExtern.Variant}
* @private
*/
shaka.offline.OfflineUtils.createVariant_ = function(
audio, video, drmInfos, id) {
return {
id: id,
language: audio ? audio.language : '',
// Use !! to get the compiler to use a boolean type. Otherwise it will
// deduce the type as {boolean|shakaExtern.StreamDB} even though |audio|
// will only be returned if it is falsy, so the type would be {boolean|null}
primary: (!!audio && audio.primary) || (!!video && video.primary),
audio: shaka.offline.OfflineUtils.createStream_(audio),
video: shaka.offline.OfflineUtils.createStream_(video),
bandwidth: 0,
drmInfos: drmInfos,
allowedByApplication: true,
allowedByKeySystem: true
};
};
/**
* Creates a shakaExtern.Stream from a StreamDB.
*
* @param {?shakaExtern.StreamDB} streamDb
* @return {?shakaExtern.Stream}
* @private
*/
shaka.offline.OfflineUtils.createStream_ = function(streamDb) {
if (!streamDb) return null;
var refs =
shaka.offline.OfflineUtils.getSegmentReferences_(streamDb);
var segmentIndex = new shaka.media.SegmentIndex(refs);
var initRef = streamDb.initSegmentUri ?
new shaka.media.InitSegmentReference(
function() { return [streamDb.initSegmentUri]; }, 0, null) :
null;
return {
id: streamDb.id,
createSegmentIndex: Promise.resolve.bind(Promise),
findSegmentPosition: segmentIndex.find.bind(segmentIndex),
getSegmentReference: segmentIndex.get.bind(segmentIndex),
initSegmentReference: initRef,
mediaSegmentReferences: refs,
presentationTimeOffset: streamDb.presentationTimeOffset,
mimeType: streamDb.mimeType,
codecs: streamDb.codecs,
width: streamDb.width || undefined,
height: streamDb.height || undefined,
frameRate: streamDb.frameRate || undefined,
kind: streamDb.kind,
encrypted: streamDb.encrypted,
keyId: streamDb.keyId,
language: streamDb.language,
label: streamDb.label || null,
type: streamDb.contentType,
primary: streamDb.primary,
trickModeVideo: null,
// TODO(modmaker): Store offline?
containsEmsgBoxes: false,
roles: [],
channelsCount: null
};
};
/**
* Determines if this platform supports any form of storage engine.
* @return {boolean}
*/
shaka.offline.OfflineUtils.isStorageEngineSupported = function() {
return shaka.offline.DBEngine.isSupported();
};
/**
* Create a new instance of the supported storage engine. The created instance
* will be uninitialized. If this platform does not support any storage
* engines, this function will return null.
* @return {shaka.offline.IStorageEngine}
*/
shaka.offline.OfflineUtils.createStorageEngine = function() {
return shaka.offline.DBEngine.isSupported() ?
new shaka.offline.DBEngine() :
null;
};