From 37befdf70ad89748f8962711176e1b509155989f Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Wed, 1 Apr 2015 14:37:56 -0700 Subject: [PATCH] Fix offline license storage quirks. 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 --- lib/media/eme_manager.js | 49 ++++++++++++- lib/player/offline_video_source.js | 106 +++++++++++------------------ 2 files changed, 88 insertions(+), 67 deletions(-) diff --git a/lib/media/eme_manager.js b/lib/media/eme_manager.js index 280528181e..f23c8d1b10 100644 --- a/lib/media/eme_manager.js +++ b/lib/media/eme_manager.js @@ -79,6 +79,19 @@ shaka.media.EmeManager = function(player, video, videoSource) { /** @private {!Array.} */ 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); @@ -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(); } @@ -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. @@ -208,7 +240,7 @@ shaka.media.EmeManager.prototype.buildKeySystemQueries_ = videoCapabilities: undefined, initDataTypes: undefined, distinctiveIdentifier: 'optional', - persistentState: 'optional' + persistentState: this.videoSource_.isOffline() ? 'required' : 'optional' }; } @@ -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; @@ -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); @@ -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 */ diff --git a/lib/player/offline_video_source.js b/lib/player/offline_video_source.js index ec306015a3..9c6bf93cb1 100644 --- a/lib/player/offline_video_source.js +++ b/lib/player/offline_video_source.js @@ -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); @@ -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']; @@ -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); }) ); }; @@ -136,7 +155,7 @@ shaka.player.OfflineVideoSource.prototype.store = function( * This should trigger encrypted events for any encrypted streams. * @param {!Array.} streamInfos The streams to * initialize. - * @return {boolean} Success of creating sourceBuffers and appending data. + * @return {!Promise} * @private */ shaka.player.OfflineVideoSource.prototype.initializeStreams_ = @@ -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(); }; @@ -165,7 +189,7 @@ 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); }; @@ -173,7 +197,7 @@ shaka.player.OfflineVideoSource.prototype.addSession_ = function(event) { /** * Inserts a group of streams into the database. - * @param {!Array.} streamSetInfos The streams to + * @param {!Array.} selectedStreams The streams to * insert. * @param {shaka.player.DrmSchemeInfo} drmScheme The DRM scheme. * @param {?number} duration The duration of the entire stream. @@ -181,9 +205,8 @@ shaka.player.OfflineVideoSource.prototype.addSession_ = function(event) { * @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(); @@ -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) { @@ -250,27 +247,6 @@ shaka.player.OfflineVideoSource.prototype.insertGroup_ = }; -/** - * Counts the number of streamSets with only encrypted streams. - * @param {!Array.} 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