Skip to content

Commit

Permalink
Fix offline license storage quirks.
Browse files Browse the repository at this point in the history
This introduces a new heuristic to determine when licenses have
been stored.

Also moves license acquisition before storage of content, so that
failure is quicker on platforms which don't support offline licenses.

Issue #23.

Change-Id: I683690077e134e40285727e6586b0f265f36e7fb
  • Loading branch information
joeyparrish authored and Gerrit Code Review committed Apr 2, 2015
1 parent 7807b73 commit 37befdf
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 67 deletions.
49 changes: 47 additions & 2 deletions lib/media/eme_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ shaka.media.EmeManager = function(player, video, videoSource) {

/** @private {!Array.<!MediaKeySession>} */
this.sessions_ = [];

/** @private {number} */
this.numUpdates_ = 0;

/**
* Resolved when all sessions are probably ready. This is a heuristic, and
* is intended to support persisting licenses for offline playback.
* @private {!shaka.util.PublicPromise}
*/
this.allSessionsPresumedReady_ = new shaka.util.PublicPromise();

/** @private {?number} */
this.allSessionsReadyTimer_ = null;
};
goog.inherits(shaka.media.EmeManager, shaka.util.FakeEventTarget);

Expand Down Expand Up @@ -130,6 +143,7 @@ shaka.media.EmeManager.prototype.initialize = function() {
if (Object.keys(mediaKeySystemConfigs).length == 0) {
// All streams are unencrypted.
this.videoSource_.selectConfigurations(chosenStreams);
this.allSessionsPresumedReady_.resolve();
return Promise.resolve();
}

Expand All @@ -150,6 +164,24 @@ shaka.media.EmeManager.prototype.initialize = function() {
};


/**
* @param {number} timeoutMs A timeout in ms after which the promise should be
* rejected.
* @return {!Promise} resolved when all sessions are presumed ready.
*/
shaka.media.EmeManager.prototype.allSessionsReady = function(timeoutMs) {
if (this.allSessionsReadyTimer_ == null) {
this.allSessionsReadyTimer_ = window.setTimeout(
function() {
var error = new Error('Timeout waiting for sessions.');
error.type = 'storage';
this.allSessionsPresumedReady_.reject(error);
}.bind(this), timeoutMs);
}
return this.allSessionsPresumedReady_;
};


/**
* Choose unencrypted streams for each type if possible. Store chosen streams
* into chosenStreams.
Expand Down Expand Up @@ -208,7 +240,7 @@ shaka.media.EmeManager.prototype.buildKeySystemQueries_ =
videoCapabilities: undefined,
initDataTypes: undefined,
distinctiveIdentifier: 'optional',
persistentState: 'optional'
persistentState: this.videoSource_.isOffline() ? 'required' : 'optional'
};
}

Expand Down Expand Up @@ -400,7 +432,15 @@ shaka.media.EmeManager.prototype.onEncrypted_ = function(event) {
}

shaka.log.info('onEncrypted_', initData, event);
var session = this.createSession_();
try {
var session = this.createSession_();
} catch (exception) {
var event2 = shaka.util.FakeEvent.createErrorEvent(exception);
this.dispatchEvent(event2);
this.allSessionsPresumedReady_.reject(exception);
return;
}

var p = session.generateRequest(event.initDataType, event.initData);
this.requestGenerated_[initDataKey] = true;

Expand All @@ -410,6 +450,7 @@ shaka.media.EmeManager.prototype.onEncrypted_ = function(event) {
this.requestGenerated_[initDataKey] = false;
var event = shaka.util.FakeEvent.createErrorEvent(error);
this.dispatchEvent(event);
this.allSessionsPresumedReady_.reject(error);
})
);
this.sessions_.push(session);
Expand Down Expand Up @@ -533,6 +574,10 @@ shaka.media.EmeManager.prototype.requestLicense_ =
var event = shaka.util.FakeEvent.create(
{type: 'sessionReady', detail: session});
this.dispatchEvent(event);
this.numUpdates_++;
if (this.numUpdates_ >= this.sessions_.length) {
this.allSessionsPresumedReady_.resolve();
}
})
).catch(shaka.util.TypedBind(this,
/** @param {!Error} error */
Expand Down
106 changes: 41 additions & 65 deletions lib/player/offline_video_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ goog.inherits(shaka.player.OfflineVideoSource, shaka.player.StreamVideoSource);
shaka.player.OfflineVideoSource.prototype.store = function(
mpdUrl, preferredLanguage, interpretContentProtection) {
var emeManager;
var selectedStreams;
var mpdRequest = new shaka.dash.MpdRequest(mpdUrl);
var lang = shaka.util.LanguageUtils.normalize(preferredLanguage);

Expand All @@ -101,20 +102,15 @@ shaka.player.OfflineVideoSource.prototype.store = function(
var fakeVideoElement = /** @type {!HTMLVideoElement} */ (
document.createElement('video'));
fakeVideoElement.src = window.URL.createObjectURL(this.mediaSource);

emeManager =
new shaka.media.EmeManager(null, fakeVideoElement, this);
this.eventManager.listen(
emeManager, 'sessionReady', this.addSession_.bind(this));
emeManager, 'sessionReady', this.onSessionReady_.bind(this));
return emeManager.initialize();
})
).then(shaka.util.TypedBind(this,
function() {
// TODO(story 1890046): Support multiple periods.
var drmScheme = emeManager.getDrmScheme();
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'];
Expand All @@ -125,7 +121,30 @@ shaka.player.OfflineVideoSource.prototype.store = function(
streamSetInfos.push(this.streamSetsByType.get(type)[0]);
}
}
return this.insertGroup_(streamSetInfos, drmScheme, duration);
selectedStreams = streamSetInfos.map(this.selectStreamInfo_);
var async = [];
for (var i = 0; i < selectedStreams.length; ++i) {
async.push(selectedStreams[i].getSegmentInitializationData());
}
return Promise.all(async);
})
).then(shaka.util.TypedBind(this,
function() {
return this.initializeStreams_(selectedStreams);
})
).then(shaka.util.TypedBind(this,
function() {
return emeManager.allSessionsReady(this.timeoutMs);
})
).then(shaka.util.TypedBind(this,
function() {
var drmScheme = emeManager.getDrmScheme();
// 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.');
}
return this.insertGroup_(selectedStreams, drmScheme, duration);
})
);
};
Expand All @@ -136,7 +155,7 @@ shaka.player.OfflineVideoSource.prototype.store = function(
* This should trigger encrypted events for any encrypted streams.
* @param {!Array.<shaka.media.StreamInfo>} streamInfos The streams to
* initialize.
* @return {boolean} Success of creating sourceBuffers and appending data.
* @return {!Promise}
* @private
*/
shaka.player.OfflineVideoSource.prototype.initializeStreams_ =
Expand All @@ -147,16 +166,21 @@ shaka.player.OfflineVideoSource.prototype.initializeStreams_ =
var fullMimeType = streamInfos[i].getFullMimeType();
sourceBuffers[i] = this.mediaSource.addSourceBuffer(fullMimeType);
} catch (exception) {
shaka.log.debug('addSourceBuffer() failed', exception);
shaka.log.error('addSourceBuffer() failed', exception);
}
}

if (streamInfos.length != sourceBuffers.length) return false;
if (streamInfos.length != sourceBuffers.length) {
var error = new Error('Error initializing streams.');
error.type = 'storage';
return Promise.reject(error);
}

for (var i = 0; i < streamInfos.length; ++i) {
sourceBuffers[i].appendBuffer(streamInfos[i].segmentInitializationData);
}
return true;

return Promise.resolve();
};


Expand All @@ -165,25 +189,24 @@ shaka.player.OfflineVideoSource.prototype.initializeStreams_ =
* @param {Event} event A sessionReady event.
* @private
*/
shaka.player.OfflineVideoSource.prototype.addSession_ = function(event) {
shaka.player.OfflineVideoSource.prototype.onSessionReady_ = function(event) {
var session = /** @type {MediaKeySession} */ (event.detail);
this.sessionIds_.push(session.sessionId);
};


/**
* Inserts a group of streams into the database.
* @param {!Array.<!shaka.media.StreamSetInfo>} streamSetInfos The streams to
* @param {!Array.<!shaka.media.StreamInfo>} selectedStreams The streams to
* insert.
* @param {shaka.player.DrmSchemeInfo} drmScheme The DRM scheme.
* @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(streamSetInfos, drmScheme, duration) {
function(selectedStreams, drmScheme, duration) {
var streamIds = [];
var selectedStreams = streamSetInfos.map(this.selectStreamInfo_);
var contentDatabase = new shaka.util.ContentDatabase(null);
var p = contentDatabase.setUpDatabase();

Expand All @@ -203,37 +226,11 @@ shaka.player.OfflineVideoSource.prototype.insertGroup_ =
return Promise.resolve();
});
}

// Insert information about the group of streams into the database and close
// the connection.
p = p.then(shaka.util.TypedBind(this, function() {
if (this.initializeStreams_(selectedStreams)) {
var sessionPromise = new shaka.util.PublicPromise();
var numEncryptedStreams = this.countEncryptedStreams_(streamSetInfos);
var startWaiting = Date.now();

var waitForSessions = (function() {
if (this.sessionIds_.length == numEncryptedStreams) {
contentDatabase.insertGroup(streamIds, this.sessionIds_).then(
function(id) { sessionPromise.resolve(id) });
} else {
var timeElapsed = Date.now() - startWaiting;
if (timeElapsed < this.timeoutMs) {
setTimeout(waitForSessions, 500);
} else {
var error = new Error('Timeout while initializing streams.');
error.type = 'storage';
return sessionPromise.reject(error);
}
}
}).bind(this);

waitForSessions();
return sessionPromise;
} else {
var error = new Error('Error initializing streams.');
error.type = 'storage';
return Promise.reject(error);
}
return contentDatabase.insertGroup(streamIds, this.sessionIds_);
})).then(
/** @param {number} groupId */
function(groupId) {
Expand All @@ -250,27 +247,6 @@ shaka.player.OfflineVideoSource.prototype.insertGroup_ =
};


/**
* Counts the number of streamSets with only encrypted streams.
* @param {!Array.<shaka.media.StreamSetInfo>} streamSetInfos
* @return {number}
* @private
*/
shaka.player.OfflineVideoSource.prototype.countEncryptedStreams_ =
function(streamSetInfos) {
var numEncryptedStreams = streamSetInfos.length;
for (var i = 0; i < streamSetInfos.length; ++i) {
for (var j = 0; j < streamSetInfos[i].drmSchemes.length; ++j) {
if (streamSetInfos[i].drmSchemes[j].keySystem == '') {
numEncryptedStreams--;
break;
}
}
}
return numEncryptedStreams;
};


/**
* Selects which stream from a stream info set should be stored offline.
* @param {!shaka.media.StreamSetInfo} streamSetInfo The stream set to select a
Expand Down

0 comments on commit 37befdf

Please sign in to comment.