diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index afbbaf41be..d099ae9128 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -23,6 +23,7 @@ goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.util.ArrayUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.EventManager'); +goog.require('shaka.util.FakeEvent'); goog.require('shaka.util.Functional'); goog.require('shaka.util.IDestroyable'); goog.require('shaka.util.ManifestParserUtils'); @@ -36,17 +37,16 @@ goog.require('shaka.util.Uint8ArrayUtils'); /** + * @param {shaka.media.DrmEngine.PlayerInterface} playerInterface + * * @constructor - * @param {!shaka.net.NetworkingEngine} networkingEngine - * @param {function(!shaka.util.Error)} onError Called when an error occurs. - * @param {function(!Object.)} onKeyStatus Called when key - * status changes. Argument is a map of hex key IDs to statuses. - * @param {function(string, number)} onExpirationUpdated * @struct * @implements {shaka.util.IDestroyable} */ -shaka.media.DrmEngine = function( - networkingEngine, onError, onKeyStatus, onExpirationUpdated) { +shaka.media.DrmEngine = function(playerInterface) { + /** @private {?shaka.media.DrmEngine.PlayerInterface} */ + this.playerInterface_ = playerInterface; + /** @private {Array.} */ this.supportedTypes_ = null; @@ -74,27 +74,18 @@ shaka.media.DrmEngine = function( /** @private {!shaka.util.PublicPromise} */ this.allSessionsLoaded_ = new shaka.util.PublicPromise(); - /** @private {shaka.net.NetworkingEngine} */ - this.networkingEngine_ = networkingEngine; - /** @private {?shakaExtern.DrmConfiguration} */ this.config_ = null; /** @private {?function(!shaka.util.Error)} */ this.onError_ = (function(err) { this.allSessionsLoaded_.reject(err); - onError(err); + playerInterface.onError(err); }.bind(this)); /** @private {!Object.} */ this.keyStatusByKeyId_ = {}; - /** @private {?function(!Object.)} */ - this.onKeyStatus_ = onKeyStatus; - - /** @private {?function(string, number)} */ - this.onExpirationUpdated_ = onExpirationUpdated; - /** @private {shaka.util.Timer} */ this.keyStatusTimer_ = new shaka.util.Timer( this.processKeyStatusChanges_.bind(this)); @@ -147,6 +138,32 @@ shaka.media.DrmEngine = function( shaka.media.DrmEngine.ActiveSession; +/** + * @typedef {{ + * netEngine: shaka.net.NetworkingEngine, + * onError: function(!shaka.util.Error), + * onKeyStatus: function(!Object.), + * onExpirationUpdated: function(string,number), + * onEvent: function(!Event) + * }} + * + * @property {shaka.net.NetworkingEngine} netEngine + * The NetworkingEngine instance to use. The caller retains ownership. + * @property {function(!shaka.util.Error)} onError + * Called when an error occurs. If the error is recoverable (see + * @link{shaka.util.Error}) then the caller may invoke either + * StreamingEngine.switch*() or StreamingEngine.seeked() to attempt recovery. + * @property {function(!Object.)} onKeyStatus + * Called when key status changes. Argument is a map of hex key IDs to + * statuses. + * @property {function(string,number)} onExpirationUpdated + * Called when the session expiration value changes. + * @property {function(!Event)} onEvent + * Called when an event occurs that should be sent to the app. + */ +shaka.media.DrmEngine.PlayerInterface; + + /** @override */ shaka.media.DrmEngine.prototype.destroy = function() { var Functional = shaka.util.Functional; @@ -187,10 +204,9 @@ shaka.media.DrmEngine.prototype.destroy = function() { this.eventManager_ = null; this.activeSessions_ = []; this.offlineSessionIds_ = []; - this.networkingEngine_ = null; // We don't own it, don't destroy() it. this.config_ = null; this.onError_ = null; - this.onExpirationUpdated_ = null; + this.playerInterface_ = null; return Promise.all(async); }; @@ -1073,12 +1089,15 @@ shaka.media.DrmEngine.prototype.sendLicenseRequest_ = function(event) { this.unpackPlayReadyRequest_(request); } - this.networkingEngine_.request(requestType, request) + this.playerInterface_.netEngine.request(requestType, request) .then(function(response) { if (this.destroyed_) return Promise.reject(); // Request succeeded, now pass the response to the CDM. return session.update(response.data).then(function() { + var event = new shaka.util.FakeEvent('drmsessionupdate'); + this.playerInterface_.onEvent(event); + if (activeSession) { if (activeSession.updatePromise) activeSession.updatePromise.resolve(); @@ -1307,7 +1326,7 @@ shaka.media.DrmEngine.prototype.processKeyStatusChanges_ = function() { shaka.util.Error.Code.EXPIRED)); } - this.onKeyStatus_(this.keyStatusByKeyId_); + this.playerInterface_.onKeyStatus(this.keyStatusByKeyId_); }; @@ -1515,7 +1534,8 @@ shaka.media.DrmEngine.prototype.pollExpiration_ = function() { new_ = Infinity; if (new_ != old) { - this.onExpirationUpdated_(session.session.sessionId, new_); + this.playerInterface_.onExpirationUpdated( + session.session.sessionId, new_); session.oldExpiration = new_; } }.bind(this)); diff --git a/lib/offline/storage.js b/lib/offline/storage.js index 9f5067cf57..8553f93d2d 100644 --- a/lib/offline/storage.js +++ b/lib/offline/storage.js @@ -298,8 +298,13 @@ shaka.offline.Storage.prototype.remove = function(content) { shaka.offline.OfflineManifestParser.reconstructManifest(manifestDb); var netEngine = this.player_.getNetworkingEngine(); goog.asserts.assert(netEngine, 'Player must not be destroyed'); - drmEngine = new shaka.media.DrmEngine( - netEngine, onError, function() {}, function() {}); + drmEngine = new shaka.media.DrmEngine({ + netEngine: netEngine, + onError: onError, + onKeyStatus: function() {}, + onExpirationUpdated: function() {}, + onEvent: function() {} + }); drmEngine.configure(this.player_.getConfiguration().drm); var isOffline = this.config_.usePersistentLicense || false; return drmEngine.init(manifest, isOffline); @@ -424,8 +429,13 @@ shaka.offline.Storage.prototype.loadInternal = function( .then(function(data) { this.checkDestroyed_(); manifest = data; - drmEngine = new shaka.media.DrmEngine( - netEngine, onError, onKeyStatusChange, function() {}); + drmEngine = new shaka.media.DrmEngine({ + netEngine: netEngine, + onError: onError, + onKeyStatus: onKeyStatusChange, + onExpirationUpdated: function() {}, + onEvent: function() {} + }); drmEngine.configure(config.drm); var isOffline = this.config_.usePersistentLicense || false; return drmEngine.init(manifest, isOffline); diff --git a/lib/player.js b/lib/player.js index dff801a263..f36cf93167 100644 --- a/lib/player.js +++ b/lib/player.js @@ -273,6 +273,15 @@ shaka.Player.version = GIT_VERSION; */ +/** + * @event shaka.Player.DrmSessionUpdateEvent + * @description Fired when the CDM has accepted the license response. + * @property {string} type + * 'drmsessionupdate' + * @exportDoc + */ + + /** * @event shaka.Player.TimelineRegionAdded * @description Fired when a media timeline region is added. @@ -772,11 +781,15 @@ shaka.Player.prototype.chooseCodecsAndFilterManifest_ = function() { */ shaka.Player.prototype.createDrmEngine = function() { goog.asserts.assert(this.networkingEngine_, 'Must not be destroyed'); - return new shaka.media.DrmEngine( - this.networkingEngine_, - this.onError_.bind(this), - this.onKeyStatus_.bind(this), - this.onExpirationUpdated_.bind(this)); + + var playerInterface = { + netEngine: this.networkingEngine_, + onError: this.onError_.bind(this), + onKeyStatus: this.onKeyStatus_.bind(this), + onExpirationUpdated: this.onExpirationUpdated_.bind(this), + onEvent: this.onEvent_.bind(this) + }; + return new shaka.media.DrmEngine(playerInterface); }; diff --git a/test/media/drm_engine_integration.js b/test/media/drm_engine_integration.js index 9b17f0325f..5a5262b754 100644 --- a/test/media/drm_engine_integration.js +++ b/test/media/drm_engine_integration.js @@ -46,6 +46,8 @@ describe('DrmEngine', function() { var onKeyStatusSpy; /** @type {!jasmine.Spy} */ var onExpirationSpy; + /** @type {!jasmine.Spy} */ + var onEventSpy; /** @type {!shaka.media.DrmEngine} */ var drmEngine; @@ -94,6 +96,7 @@ describe('DrmEngine', function() { onErrorSpy = jasmine.createSpy('onError'); onKeyStatusSpy = jasmine.createSpy('onKeyStatus'); onExpirationSpy = jasmine.createSpy('onExpirationUpdated'); + onEventSpy = jasmine.createSpy('onEvent'); mediaSource = new MediaSource(); video.src = window.URL.createObjectURL(mediaSource); @@ -111,10 +114,15 @@ describe('DrmEngine', function() { ].join(''); }); - drmEngine = new shaka.media.DrmEngine( - networkingEngine, shaka.test.Util.spyFunc(onErrorSpy), - shaka.test.Util.spyFunc(onKeyStatusSpy), - shaka.test.Util.spyFunc(onExpirationSpy)); + var playerInterface = { + netEngine: networkingEngine, + onError: shaka.test.Util.spyFunc(onErrorSpy), + onKeyStatus: shaka.test.Util.spyFunc(onKeyStatusSpy), + onExpirationUpdated: shaka.test.Util.spyFunc(onExpirationSpy), + onEvent: shaka.test.Util.spyFunc(onEventSpy) + }; + + drmEngine = new shaka.media.DrmEngine(playerInterface); var config = { retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(), clearKeys: {}, diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index e26372c8be..0bf577cca8 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -32,6 +32,8 @@ describe('DrmEngine', function() { var onKeyStatusSpy; /** @type {!jasmine.Spy} */ var onExpirationSpy; + /** @type {!jasmine.Spy} */ + var onEventSpy; /** @type {!shaka.test.FakeNetworkingEngine} */ var fakeNetEngine; @@ -65,6 +67,7 @@ describe('DrmEngine', function() { onErrorSpy = jasmine.createSpy('onError'); onKeyStatusSpy = jasmine.createSpy('onKeyStatus'); onExpirationSpy = jasmine.createSpy('onExpirationUpdated'); + onEventSpy = jasmine.createSpy('onEvent'); }); beforeEach(function() { @@ -83,6 +86,7 @@ describe('DrmEngine', function() { logErrorSpy.calls.reset(); onKeyStatusSpy.calls.reset(); onExpirationSpy.calls.reset(); + onEventSpy.calls.reset(); // By default, error logs and callbacks result in failure. onErrorSpy.and.callFake(fail); @@ -113,10 +117,15 @@ describe('DrmEngine', function() { license = (new Uint8Array(0)).buffer; fakeNetEngine.setResponseMap({ 'http://abc.drm/license': license }); - drmEngine = new shaka.media.DrmEngine( - fakeNetEngine, shaka.test.Util.spyFunc(onErrorSpy), - shaka.test.Util.spyFunc(onKeyStatusSpy), - shaka.test.Util.spyFunc(onExpirationSpy)); + var playerInterface = { + netEngine: fakeNetEngine, + onError: shaka.test.Util.spyFunc(onErrorSpy), + onKeyStatus: shaka.test.Util.spyFunc(onKeyStatusSpy), + onExpirationUpdated: shaka.test.Util.spyFunc(onExpirationSpy), + onEvent: shaka.test.Util.spyFunc(onEventSpy) + }; + + drmEngine = new shaka.media.DrmEngine(playerInterface); config = { retryParameters: retryParameters, delayLicenseRequestUntilPlayed: false, @@ -1067,6 +1076,23 @@ describe('DrmEngine', function() { }).catch(fail).then(done); }); + it('publishes an event if update succeeds', function(done) { + initAndAttach().then(function() { + var initData = new Uint8Array(1); + mockVideo.on['encrypted']( + { initDataType: 'webm', initData: initData, keyId: null }); + var message = new Uint8Array(0); + session1.on['message']({ target: session1, message: message}); + session1.update.and.returnValue(Promise.resolve()); + + return shaka.test.Util.delay(0.5); + }).then(function() { + expect(onEventSpy).toHaveBeenCalledWith( + jasmine.objectContaining({ type: 'drmsessionupdate' })); + done(); + }).catch(fail); + }); + it('dispatches an error if update fails', function(done) { onErrorSpy.and.stub(); diff --git a/test/offline/offline_integration.js b/test/offline/offline_integration.js index 059709d49e..4d18f1eebd 100644 --- a/test/offline/offline_integration.js +++ b/test/offline/offline_integration.js @@ -134,8 +134,13 @@ describe('Offline', /** @suppress {accessControls} */ function() { var manifest = OfflineManifestParser.reconstructManifest(manifestDb); var netEngine = player.getNetworkingEngine(); goog.asserts.assert(netEngine, 'Must have a NetworkingEngine'); - drmEngine = new shaka.media.DrmEngine( - netEngine, onError, function() {}, function() {}); + drmEngine = new shaka.media.DrmEngine({ + netEngine: netEngine, + onError: onError, + onKeyStatus: function() {}, + onExpirationUpdated: function() {}, + onEvent: function() {} + }); drmEngine.configure(player.getConfiguration().drm); return drmEngine.init(manifest, true /* isOffline */); })