Skip to content

Commit

Permalink
OfflineVideoSource store functionality. Issue #22
Browse files Browse the repository at this point in the history
Change-Id: Ib55ce40619929f7139a0651f0e0874a0bd4fc054
  • Loading branch information
natalieharris authored and Gerrit Code Review committed Mar 16, 2015
1 parent e2f31dd commit 18a843e
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 27 deletions.
213 changes: 213 additions & 0 deletions lib/player/offline_video_source.js
Original file line number Diff line number Diff line change
@@ -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.<number>} 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.<!shaka.media.StreamInfo>} streamInfos The streams to insert.
* @param {number} duration The duration of the entire stream.
* @return {!Promise.<number>} 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)
};
46 changes: 23 additions & 23 deletions lib/player/stream_video_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ shaka.player.StreamVideoSource = function(manifestInfo) {

/**
* All usable stream sets. Mutually compatible within each type.
* @private {!Object.<string, !Array.<!shaka.media.StreamSetInfo>>}
* @protected {!Object.<string, !Array.<!shaka.media.StreamSetInfo>>}
*/
this.streamSetsByType_ = {};
this.streamSetsByType = {};
// TODO(story 1890046): Support multiple periods.

/** @private {!shaka.util.EventManager} */
Expand Down Expand Up @@ -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 [];
}

Expand All @@ -223,8 +223,8 @@ shaka.player.StreamVideoSource.prototype.getVideoTracks = function() {
/** @type {!Array.<!shaka.player.VideoTrack>} */
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];

Expand All @@ -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 [];
}

Expand All @@ -262,8 +262,8 @@ shaka.player.StreamVideoSource.prototype.getAudioTracks = function() {
/** @type {!Array.<!shaka.player.AudioTrack>} */
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) {
Expand All @@ -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 [];
}

Expand All @@ -296,8 +296,8 @@ shaka.player.StreamVideoSource.prototype.getTextTracks = function() {
/** @type {!Array.<!shaka.player.TextTrack>} */
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) {
Expand Down Expand Up @@ -349,38 +349,38 @@ 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]);
}
}
}

// Assume subs will be needed.
this.subsNeeded_ = true;

var audioSets = this.streamSetsByType_['audio'];
var audioSets = this.streamSetsByType['audio'];
if (audioSets) {
this.sortByLanguage_(audioSets);

Expand All @@ -398,7 +398,7 @@ shaka.player.StreamVideoSource.prototype.selectConfigurations =
}
}

var textSets = this.streamSetsByType_['text'];
var textSets = this.streamSetsByType['text'];
if (textSets) {
this.sortByLanguage_(textSets);
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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]);
}
}

Expand Down
8 changes: 4 additions & 4 deletions lib/util/content_database.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.<number>} The unique id assigned to the stream.
*/
Expand All @@ -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_());
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 18a843e

Please sign in to comment.