diff --git a/lib/player/offline_video_source.js b/lib/player/offline_video_source.js new file mode 100644 index 000000000..42b4236c0 --- /dev/null +++ b/lib/player/offline_video_source.js @@ -0,0 +1,213 @@ +/** + * Copyright 2015 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. + * + * @fileoverview Implements an offline video source. + */ + +goog.provide('shaka.player.OfflineVideoSource'); + +goog.require('shaka.asserts'); +goog.require('shaka.dash.MpdProcessor'); +goog.require('shaka.dash.MpdRequest'); +goog.require('shaka.dash.mpd'); +goog.require('shaka.log'); +goog.require('shaka.media.EmeManager'); +goog.require('shaka.media.StreamInfo'); +goog.require('shaka.player.StreamVideoSource'); +goog.require('shaka.util.ContentDatabase'); +goog.require('shaka.util.TypedBind'); + + + +/** + * Creates an OfflineVideoSource. + * @param {?number} groupId The unique ID of the group of streams + * in this source. + * @struct + * @constructor + * @extends {shaka.player.StreamVideoSource} + * @export + */ +shaka.player.OfflineVideoSource = function(groupId) { + shaka.player.StreamVideoSource.call(this, null); + + /** @private {?number} */ + this.groupId_ = groupId; +}; +goog.inherits(shaka.player.OfflineVideoSource, shaka.player.StreamVideoSource); + + +/** + * Stores the content described by the MPD for offline playback. + * @param {string} mpdUrl The MPD URL. + * @param {string} preferredLanguage The user's preferred language tag. + * @param {?shaka.player.DashVideoSource.ContentProtectionCallback} + * interpretContentProtection A callback to interpret the ContentProtection + * elements in the MPD. + * @return {!Promise.} The group ID of the stored content. + */ +shaka.player.OfflineVideoSource.prototype.store = function( + mpdUrl, preferredLanguage, interpretContentProtection) { + var mpdRequest = new shaka.dash.MpdRequest(mpdUrl); + + return mpdRequest.send().then(shaka.util.TypedBind(this, + /** @param {!shaka.dash.mpd.Mpd} mpd */ + function(mpd) { + var mpdProcessor = + new shaka.dash.MpdProcessor(interpretContentProtection); + mpdProcessor.process(mpd); + + this.manifestInfo = mpdProcessor.manifestInfo; + var baseClassLoad = shaka.player.StreamVideoSource.prototype.load; + return baseClassLoad.call(this, preferredLanguage); + }) + ).then(shaka.util.TypedBind(this, + function() { + var fakeVideoElement = /** @type {!HTMLVideoElement} */ ( + document.createElement('video')); + var emeManager = + new shaka.media.EmeManager(this, fakeVideoElement, this); + return emeManager.initialize(); + }) + ).then(shaka.util.TypedBind(this, + function() { + var selectedStreams = []; + // TODO (natalieharris) : Add EME support. + // TODO(story 1890046): Support multiple periods. + var duration = this.manifestInfo.periodInfos[0].duration; + if (!duration) { + shaka.log.warning('The duration of the stream being stored is null.'); + } + // Choose the first stream set from each type. + var streamSetInfos = []; + var desiredTypes = ['audio', 'video']; + // TODO (natalieharris) : Add text support. + for (var i = 0; i < desiredTypes.length; ++i) { + var type = desiredTypes[i]; + if (this.streamSetsByType[type]) { + streamSetInfos.push(this.streamSetsByType[type][0]); + } + } + + for (var i = 0; i < streamSetInfos.length; ++i) { + var streamSetInfo = streamSetInfos[i]; + shaka.asserts.assert(streamSetInfo.streamInfos.length > 0); + var streamInfo = this.selectStreamInfo_(streamSetInfo); + selectedStreams.push(streamInfo); + } + return this.insertGroup_(selectedStreams, duration); + }) + ); +}; + + +/** + * Inserts a group of streams into the database. + * @param {!Array.} streamInfos The streams to insert. + * @param {number} duration The duration of the entire stream. + * @return {!Promise.} The unique id assigned to the group. + * @private + */ +shaka.player.OfflineVideoSource.prototype.insertGroup_ = + function(streamInfos, duration) { + var streamIds = []; + var contentDatabase = new shaka.util.ContentDatabase(null); + var p = contentDatabase.setUpDatabase(); + + // Insert each stream into the database. + for (var i = 0; i < streamInfos.length; ++i) { + var getSegmentIndex = + shaka.media.StreamInfo.prototype.getSegmentIndex.bind(streamInfos[i]); + var insertStream = (function(i) { + return contentDatabase.insertStream( + streamInfos[i].mimeType, + streamInfos[i].codecs, + duration, + streamInfos[i].segmentIndex, + streamInfos[i].segmentInitializationData); + }).bind(this, i); + + p = p.then(getSegmentIndex).then(insertStream).then( + /** @param {number} streamId */ + function(streamId) { + streamIds.push(streamId); + return Promise.resolve(); + }); + } + // Insert information about the group of streams into the database and close + // the connection. + p = p.then(function() { + return contentDatabase.insertGroup(streamIds); + }).then( + /** @param {number} groupId */ + function(groupId) { + contentDatabase.closeDatabaseConnection(); + return Promise.resolve(groupId); + } + ).catch( + /** @param {Error} e */ + function(e) { + contentDatabase.closeDatabaseConnection(); + return Promise.reject(e); + }); + return p; +}; + + +/** + * Selects which stream from a stream info set should be stored offline. + * @param {!shaka.media.StreamSetInfo} streamSetInfo The stream set to select a + * stream from. + * @return {!shaka.media.StreamInfo} + * @private + */ +shaka.player.OfflineVideoSource.prototype.selectStreamInfo_ = + function(streamSetInfo) { + shaka.asserts.assert(streamSetInfo.streamInfos.length > 0); + var selected = streamSetInfo.streamInfos[0]; + + if (streamSetInfo.contentType == 'video') { + streamSetInfo.streamInfos.sort( + function(a, b) { return a.height - b.height }); + selected = streamSetInfo.streamInfos[0]; + for (var i = 1; i < streamSetInfo.streamInfos.length; ++i) { + // Select stream with height closest to, but not exceeding 480. + if (streamSetInfo.streamInfos[i].height > 480) { + break; + } else { + selected = streamSetInfo.streamInfos[i]; + } + } + } else if (streamSetInfo.contentType == 'audio') { + // Choose the middle stream from the available ones. If we have high, + // medium, and low quality audio, this is medium. If we only have high + // and low, this is high. + var index = Math.floor(streamSetInfo.streamInfos.length / 2); + selected = streamSetInfo.streamInfos[index]; + } + return selected; +}; + + +/** @override */ +shaka.player.OfflineVideoSource.prototype.destroy = function() { + // TODO (natalieharris) +}; + + +/** @override */ +shaka.player.OfflineVideoSource.prototype.load = function(preferredLanguage) { + // TODO (natalieharris) +}; diff --git a/lib/player/stream_video_source.js b/lib/player/stream_video_source.js index 51a98f984..eca110e5c 100644 --- a/lib/player/stream_video_source.js +++ b/lib/player/stream_video_source.js @@ -81,9 +81,9 @@ shaka.player.StreamVideoSource = function(manifestInfo) { /** * All usable stream sets. Mutually compatible within each type. - * @private {!Object.>} + * @protected {!Object.>} */ - this.streamSetsByType_ = {}; + this.streamSetsByType = {}; // TODO(story 1890046): Support multiple periods. /** @private {!shaka.util.EventManager} */ @@ -212,7 +212,7 @@ shaka.player.StreamVideoSource.prototype.load = function(preferredLanguage) { /** @override */ shaka.player.StreamVideoSource.prototype.getVideoTracks = function() { - if (!this.streamSetsByType_['video']) { + if (!this.streamSetsByType['video']) { return []; } @@ -223,8 +223,8 @@ shaka.player.StreamVideoSource.prototype.getVideoTracks = function() { /** @type {!Array.} */ var tracks = []; - for (var i = 0; i < this.streamSetsByType_['video'].length; ++i) { - var streamSetInfo = this.streamSetsByType_['video'][i]; + for (var i = 0; i < this.streamSetsByType['video'].length; ++i) { + var streamSetInfo = this.streamSetsByType['video'][i]; for (var j = 0; j < streamSetInfo.streamInfos.length; ++j) { var streamInfo = streamSetInfo.streamInfos[j]; @@ -251,7 +251,7 @@ shaka.player.StreamVideoSource.prototype.getVideoTracks = function() { /** @override */ shaka.player.StreamVideoSource.prototype.getAudioTracks = function() { - if (!this.streamSetsByType_['audio']) { + if (!this.streamSetsByType['audio']) { return []; } @@ -262,8 +262,8 @@ shaka.player.StreamVideoSource.prototype.getAudioTracks = function() { /** @type {!Array.} */ var tracks = []; - for (var i = 0; i < this.streamSetsByType_['audio'].length; ++i) { - var streamSetInfo = this.streamSetsByType_['audio'][i]; + for (var i = 0; i < this.streamSetsByType['audio'].length; ++i) { + var streamSetInfo = this.streamSetsByType['audio'][i]; var lang = streamSetInfo.lang; for (var j = 0; j < streamSetInfo.streamInfos.length; ++j) { @@ -285,7 +285,7 @@ shaka.player.StreamVideoSource.prototype.getAudioTracks = function() { /** @override */ shaka.player.StreamVideoSource.prototype.getTextTracks = function() { - if (!this.streamSetsByType_['text']) { + if (!this.streamSetsByType['text']) { return []; } @@ -296,8 +296,8 @@ shaka.player.StreamVideoSource.prototype.getTextTracks = function() { /** @type {!Array.} */ var tracks = []; - for (var i = 0; i < this.streamSetsByType_['text'].length; ++i) { - var streamSetInfo = this.streamSetsByType_['text'][i]; + for (var i = 0; i < this.streamSetsByType['text'].length; ++i) { + var streamSetInfo = this.streamSetsByType['text'][i]; var lang = streamSetInfo.lang; for (var j = 0; j < streamSetInfo.streamInfos.length; ++j) { @@ -349,30 +349,30 @@ shaka.player.StreamVideoSource.prototype.selectConfigurations = } // Use the IDs to convert the map of configs into a map of stream sets. - this.streamSetsByType_ = {}; + this.streamSetsByType = {}; var types = configs.keys(); for (var i = 0; i < types.length; ++i) { var type = types[i]; var cfgList = configs.get(type); - this.streamSetsByType_[type] = []; + this.streamSetsByType[type] = []; if (type == 'video') { // We only choose one video stream set. var id = cfgList[0].id; - this.streamSetsByType_[type].push(streamSetsById[id]); + this.streamSetsByType[type].push(streamSetsById[id]); } else if (type == 'audio') { // We choose mutually compatible stream sets for audio. var basicMimeType = cfgList[0].getBasicMimeType(); for (var j = 0; j < cfgList.length; ++j) { var cfg = cfgList[j]; if (cfg.getBasicMimeType() != basicMimeType) continue; - this.streamSetsByType_[type].push(streamSetsById[cfg.id]); + this.streamSetsByType[type].push(streamSetsById[cfg.id]); } } else { // We choose all stream sets otherwise. for (var j = 0; j < cfgList.length; ++j) { var id = cfgList[j].id; - this.streamSetsByType_[type].push(streamSetsById[id]); + this.streamSetsByType[type].push(streamSetsById[id]); } } } @@ -380,7 +380,7 @@ shaka.player.StreamVideoSource.prototype.selectConfigurations = // Assume subs will be needed. this.subsNeeded_ = true; - var audioSets = this.streamSetsByType_['audio']; + var audioSets = this.streamSetsByType['audio']; if (audioSets) { this.sortByLanguage_(audioSets); @@ -398,7 +398,7 @@ shaka.player.StreamVideoSource.prototype.selectConfigurations = } } - var textSets = this.streamSetsByType_['text']; + var textSets = this.streamSetsByType['text']; if (textSets) { this.sortByLanguage_(textSets); } @@ -482,13 +482,13 @@ shaka.player.StreamVideoSource.prototype.setRestrictions = */ shaka.player.StreamVideoSource.prototype.selectTrack_ = function(type, id, immediate) { - if (!this.streamSetsByType_[type]) { + if (!this.streamSetsByType[type]) { return false; } shaka.asserts.assert(this.streamsByType_[type]); - for (var i = 0; i < this.streamSetsByType_[type].length; ++i) { - var streamSetInfo = this.streamSetsByType_[type][i]; + for (var i = 0; i < this.streamSetsByType[type].length; ++i) { + var streamSetInfo = this.streamSetsByType[type][i]; for (var j = 0; j < streamSetInfo.streamInfos.length; ++j) { var streamInfo = streamSetInfo.streamInfos[j]; if (streamInfo.uniqueId == id) { @@ -565,8 +565,8 @@ shaka.player.StreamVideoSource.prototype.onMediaSourceOpen_ = var desiredTypes = ['audio', 'video', 'text']; for (var i = 0; i < desiredTypes.length; ++i) { var type = desiredTypes[i]; - if (this.streamSetsByType_[type]) { - selectedStreamSetInfos.push(this.streamSetsByType_[type][0]); + if (this.streamSetsByType[type]) { + selectedStreamSetInfos.push(this.streamSetsByType[type][0]); } } diff --git a/lib/util/content_database.js b/lib/util/content_database.js index 6c6355ca6..dd22ac411 100644 --- a/lib/util/content_database.js +++ b/lib/util/content_database.js @@ -264,8 +264,8 @@ shaka.util.ContentDatabase.prototype.insertGroup = function(streamIds) { * Inserts a stream into the database. * @param {string} mimeType The stream's mime type. * @param {string} codecs The stream's codecs. - * @param {number} duration The stream's entire duration. - * @param {!shaka.media.SegmentIndex} segmentIndex The stream's segment index. + * @param {?number} duration The stream's entire duration. + * @param {shaka.media.SegmentIndex} segmentIndex The stream's segment index. * @param {ArrayBuffer} initSegment The stream's segment of initialization data. * @return {!Promise.} The unique id assigned to the stream. */ @@ -274,7 +274,7 @@ shaka.util.ContentDatabase.prototype.insertStream = function(mimeType, duration, segmentIndex, initSegment) { - + shaka.asserts.assert(segmentIndex); shaka.asserts.assert(segmentIndex.getNumReferences() > 0); var p = this.getNextId_(this.getIndexStore_()); @@ -315,7 +315,7 @@ shaka.util.ContentDatabase.prototype.getNextId_ = function(store) { * Inserts a stream index into the stream index store. * @param {string} mimeType The stream's mime type. * @param {string} codecs The stream's codecs. - * @param {number} duration The stream's entire duration. + * @param {?number} duration The stream's entire duration. * @param {ArrayBuffer} initSegment The stream's segment of initialization data. * @param {!shaka.util.ContentDatabase.InsertStreamState} state The stream's * state information.