diff --git a/externs/chromecast.js b/externs/chromecast.js index a1c7817054..cc1f25f60f 100644 --- a/externs/chromecast.js +++ b/externs/chromecast.js @@ -278,6 +278,14 @@ chrome.cast.Session.prototype.addMessageListener = function( chrome.cast.Session.prototype.addUpdateListener = function(listener) {}; +/** + * @param {Function} successCallback + * @param {Function} errorCallback + */ +chrome.cast.Session.prototype.leave = function( + successCallback, errorCallback) {}; + + /** * @param {string} namespace * @param {!Object|string} message diff --git a/lib/cast/cast_proxy.js b/lib/cast/cast_proxy.js index d7bc44f3af..dc5be2060f 100644 --- a/lib/cast/cast_proxy.js +++ b/lib/cast/cast_proxy.js @@ -82,10 +82,16 @@ goog.inherits(shaka.cast.CastProxy, shaka.util.FakeEventTarget); /** * Destroys the proxy and the underlying local Player. * + * @param {boolean=} opt_forceDisconnect If true, force the receiver app to shut + * down by disconnecting. Does nothing if not connected. * @override * @export */ -shaka.cast.CastProxy.prototype.destroy = function() { +shaka.cast.CastProxy.prototype.destroy = function(opt_forceDisconnect) { + if (opt_forceDisconnect && this.sender_) { + this.sender_.forceDisconnect(); + } + var async = [ this.eventManager_ ? this.eventManager_.destroy() : null, this.localPlayer_ ? this.localPlayer_.destroy() : null, @@ -207,6 +213,15 @@ shaka.cast.CastProxy.prototype.suggestDisconnect = function() { }; +/** + * Force the receiver app to shut down by disconnecting. + * @export + */ +shaka.cast.CastProxy.prototype.forceDisconnect = function() { + this.sender_.forceDisconnect(); +}; + + /** * Initialize the Proxies and the Cast sender. * @private diff --git a/lib/cast/cast_sender.js b/lib/cast/cast_sender.js index 3c69f6cc39..cbe6c3a4f8 100644 --- a/lib/cast/cast_sender.js +++ b/lib/cast/cast_sender.js @@ -98,7 +98,7 @@ shaka.cast.CastSender = shaka.cast.CastSender.prototype.destroy = function() { this.rejectAllPromises_(); if (this.session_) { - this.session_.stop(function() {}, function() {}); + this.session_.leave(function() {}, function() {}); this.session_ = null; } @@ -109,7 +109,6 @@ shaka.cast.CastSender.prototype.destroy = function() { this.hasReceivers_ = false; this.isCasting_ = false; this.appData_ = null; - this.session_ = null; this.cachedProperties_ = null; this.asyncCallPromises_ = null; this.castPromise_ = null; @@ -256,6 +255,23 @@ shaka.cast.CastSender.prototype.showDisconnectDialog = function() { }; +/** + * Forces the receiver app to shut down by disconnecting. Does nothing if not + * connected. + */ +shaka.cast.CastSender.prototype.forceDisconnect = function() { + if (!this.isCasting_) { + return; + } + + this.rejectAllPromises_(); + if (this.session_) { + this.session_.stop(function() {}, function() {}); + this.session_ = null; + } +}; + + /** * Getter for properties of remote objects. * @param {string} targetName diff --git a/test/cast/cast_proxy_unit.js b/test/cast/cast_proxy_unit.js index 7744b049fb..40fdca11f5 100644 --- a/test/cast/cast_proxy_unit.js +++ b/test/cast/cast_proxy_unit.js @@ -630,11 +630,23 @@ describe('CastProxy', function() { it('destroys the local player and the sender', function(done) { expect(mockPlayer.destroy).not.toHaveBeenCalled(); expect(mockSender.destroy).not.toHaveBeenCalled(); + expect(mockSender.forceDisconnect).not.toHaveBeenCalled(); proxy.destroy().catch(fail).then(done); expect(mockPlayer.destroy).toHaveBeenCalled(); expect(mockSender.destroy).toHaveBeenCalled(); + expect(mockSender.forceDisconnect).not.toHaveBeenCalled(); + }); + + it('optionally forces the sender to disconnect', function(done) { + expect(mockSender.destroy).not.toHaveBeenCalled(); + expect(mockSender.forceDisconnect).not.toHaveBeenCalled(); + + proxy.destroy(true).catch(fail).then(done); + + expect(mockSender.destroy).toHaveBeenCalled(); + expect(mockSender.forceDisconnect).toHaveBeenCalled(); }); }); @@ -658,6 +670,7 @@ describe('CastProxy', function() { receiverName: jasmine.createSpy('receiverName'), hasRemoteProperties: jasmine.createSpy('hasRemoteProperties'), setAppData: jasmine.createSpy('setAppData'), + forceDisconnect: jasmine.createSpy('forceDisconnect'), showDisconnectDialog: jasmine.createSpy('showDisconnectDialog'), cast: jasmine.createSpy('cast'), get: jasmine.createSpy('get'), diff --git a/test/cast/cast_sender_unit.js b/test/cast/cast_sender_unit.js index 979481093b..323a3b68ce 100644 --- a/test/cast/cast_sender_unit.js +++ b/test/cast/cast_sender_unit.js @@ -317,16 +317,24 @@ describe('CastSender', function() { }); }); - describe('disconnect', function() { - it('stops the session if we are casting', function(done) { + describe('showDisconnectDialog', function() { + it('opens the dialog if we are casting', function(done) { sender.init(); fakeReceiverAvailability(true); sender.cast(fakeInitState).then(function() { expect(sender.isCasting()).toBe(true); + expect(mockSession.leave).not.toHaveBeenCalled(); expect(mockSession.stop).not.toHaveBeenCalled(); + mockCastApi.requestSession.calls.reset(); sender.showDisconnectDialog(); + + // this call opens the dialog: expect(mockCastApi.requestSession).toHaveBeenCalled(); + // these were not used: + expect(mockSession.leave).not.toHaveBeenCalled(); + expect(mockSession.stop).not.toHaveBeenCalled(); + fakeRemoteDisconnect(); }).catch(fail).then(done); fakeSessionConnection(); @@ -562,12 +570,13 @@ describe('CastSender', function() { }); }); - describe('destroy', function() { + describe('forceDisconnect', function() { it('disconnects and cancels all async operations', function(done) { sender.init(); fakeReceiverAvailability(true); sender.cast(fakeInitState).then(function() { expect(sender.isCasting()).toBe(true); + expect(mockSession.leave).not.toHaveBeenCalled(); expect(mockSession.stop).not.toHaveBeenCalled(); var method = sender.get('player', 'load'); @@ -577,7 +586,8 @@ describe('CastSender', function() { // Wait a tick for the Promise status to be set. return shaka.test.Util.delay(0.1).then(function() { expect(p.status).toBe('pending'); - sender.destroy().catch(fail); + sender.forceDisconnect(); + expect(mockSession.leave).not.toHaveBeenCalled(); expect(mockSession.stop).toHaveBeenCalled(); // Wait a tick for the Promise status to change. @@ -595,6 +605,41 @@ describe('CastSender', function() { }); }); + describe('destroy', function() { + it('leaves the session and cancels all async operations', function(done) { + sender.init(); + fakeReceiverAvailability(true); + sender.cast(fakeInitState).then(function() { + expect(sender.isCasting()).toBe(true); + expect(mockSession.leave).not.toHaveBeenCalled(); + expect(mockSession.stop).not.toHaveBeenCalled(); + + var method = sender.get('player', 'load'); + var p = method(); + shaka.test.Util.capturePromiseStatus(p); + + // Wait a tick for the Promise status to be set. + return shaka.test.Util.delay(0.1).then(function() { + expect(p.status).toBe('pending'); + sender.destroy().catch(fail); + expect(mockSession.leave).toHaveBeenCalled(); + expect(mockSession.stop).not.toHaveBeenCalled(); + + // Wait a tick for the Promise status to change. + return shaka.test.Util.delay(0.1); + }).then(function() { + expect(p.status).toBe('rejected'); + return p.catch(function(error) { + shaka.test.Util.expectToEqualError(error, new shaka.util.Error( + shaka.util.Error.Category.PLAYER, + shaka.util.Error.Code.LOAD_INTERRUPTED)); + }); + }); + }).catch(fail).then(done); + fakeSessionConnection(); + }); + }); + function createMockCastApi() { return { isAvailable: true, @@ -612,6 +657,7 @@ describe('CastSender', function() { receiver: { friendlyName: 'SomeDevice' }, addUpdateListener: jasmine.createSpy('Session.addUpdateListener'), addMessageListener: jasmine.createSpy('Session.addMessageListener'), + leave: jasmine.createSpy('Session.leave'), sendMessage: jasmine.createSpy('Session.sendMessage'), stop: jasmine.createSpy('Session.stop') };