From 3def38190ea462bd2befe9ecaec51b01cf1c3113 Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Wed, 29 Jun 2016 14:54:40 -0700 Subject: [PATCH] Cleanup cast idle state This simplifies the logic for idle state, fixes some buggy idle state transitions, and moves the idle logic into CastReceiver (with a little support from Player). Issue #261 Change-Id: Ic2729a4235c746ad46353bdf5dc7b605ab31f3ef --- demo/receiver_app.js | 33 ++++------------------ lib/cast/cast_receiver.js | 43 ++++++++++++++++++++++++++-- lib/player.js | 23 +++++++++++++++ test/cast/cast_receiver_unit.js | 50 +++++++++++++++++++++++++++------ 4 files changed, 111 insertions(+), 38 deletions(-) diff --git a/demo/receiver_app.js b/demo/receiver_app.js index 17e5ae3015..b098391476 100644 --- a/demo/receiver_app.js +++ b/demo/receiver_app.js @@ -85,13 +85,6 @@ ShakaReceiver.prototype.init = function() { this.video_.addEventListener( 'emptied', this.onPlayStateChange_.bind(this)); - this.video_.addEventListener( - 'canplay', this.checkIdle_.bind(this)); - this.video_.addEventListener( - 'emptied', this.checkIdle_.bind(this)); - this.video_.addEventListener( - 'ended', this.checkIdle_.bind(this)); - this.receiver_ = new shaka.cast.CastReceiver( this.video_, this.player_, this.appDataCallback_.bind(this)); this.receiver_.addEventListener( @@ -121,29 +114,15 @@ ShakaReceiver.prototype.appDataCallback_ = function(appData) { /** @private */ ShakaReceiver.prototype.checkIdle_ = function() { - var connected = this.receiver_.isConnected(); - var loaded = this.video_.readyState > 0; - var ended = this.video_.ended; - - var idle = !loaded || (!connected && ended); - console.debug('status changed', - 'connected=', connected, - 'loaded=', loaded, - 'ended=', ended, - 'idle=', idle); - - // If something is loaded, but we've just gone idle, unload the content, show - // the idle card, and set a timer to close the app. - if (idle && loaded) { - this.player_.unload(); + 'idle=', this.receiver_.isIdle()); + + // If the app is idle, show the idle card and set a timer to close the app. + // Otherwise, hide the idle card and cancel the timer. + if (this.receiver_.isIdle()) { this.idle_.style.display = 'block'; this.startIdleTimer_(); - } - - // If we are no longer idle, hide the idle card, and make sure we cancel any - // timers that would close the app. - if (!idle) { + } else { this.idle_.style.display = 'none'; this.cancelIdleTimer_(); } diff --git a/lib/cast/cast_receiver.js b/lib/cast/cast_receiver.js index 94f21a6845..b4b85d548d 100644 --- a/lib/cast/cast_receiver.js +++ b/lib/cast/cast_receiver.js @@ -61,6 +61,9 @@ shaka.cast.CastReceiver = function(video, player, opt_appDataCallback) { /** @private {boolean} */ this.isConnected_ = false; + /** @private {boolean} */ + this.isIdle_ = true; + /** @private {cast.receiver.CastMessageBus} */ this.bus_ = null; @@ -81,6 +84,16 @@ shaka.cast.CastReceiver.prototype.isConnected = function() { }; +/** + * @return {boolean} True if the receiver is not currently doing loading or + * playing anything. + * @export + */ +shaka.cast.CastReceiver.prototype.isIdle = function() { + return this.isIdle_; +}; + + /** * Destroys the underlying Player, then terminates the cast receiver app. * @@ -99,6 +112,7 @@ shaka.cast.CastReceiver.prototype.destroy = function() { this.targets_ = null; this.appDataCallback_ = null; this.isConnected_ = false; + this.isIdle_ = true; this.bus_ = null; this.pollTimerId_ = null; @@ -146,6 +160,26 @@ shaka.cast.CastReceiver.prototype.init_ = function() { // higher res anyway, given that the device only outputs 1080p to begin with. this.player_.setMaxHardwareResolution(1920, 1080); + // Maintain idle state. + this.player_.addEventListener('loading', function() { + this.isIdle_ = false; + this.onCastStatusChanged_(); + }.bind(this)); + this.player_.addEventListener('unloading', function() { + this.isIdle_ = true; + this.onCastStatusChanged_(); + }.bind(this)); + this.video_.addEventListener('ended', function() { + // Go idle 5 seconds after 'ended', assuming we haven't started again or + // been destroyed. + window.setTimeout(function() { + if (this.video_ && this.video_.ended) { + this.isIdle_ = true; + this.onCastStatusChanged_(); + } + }.bind(this), 5000); + }.bind(this)); + // Do not start polling until after the sender's 'init' message is handled. }; @@ -163,8 +197,13 @@ shaka.cast.CastReceiver.prototype.onSendersChanged_ = function() { * @private */ shaka.cast.CastReceiver.prototype.onCastStatusChanged_ = function() { - var event = new shaka.util.FakeEvent('caststatuschanged'); - this.dispatchEvent(event); + // Do this asynchronously so that synchronous changes to idle state (such as + // Player calling unload() as part of load()) are coalesced before the event + // goes out. + Promise.resolve().then(function() { + var event = new shaka.util.FakeEvent('caststatuschanged'); + this.dispatchEvent(event); + }.bind(this)); }; diff --git a/lib/player.js b/lib/player.js index d0c3cc711c..084951b33d 100644 --- a/lib/player.js +++ b/lib/player.js @@ -229,6 +229,26 @@ shaka.Player.version = GIT_VERSION; */ +/** + * @event shaka.Player.LoadingEvent + * @description Fired when the player begins loading. + * Used by the Cast receiver to determine idle state. + * @property {string} type + * 'loading' + * @exportDoc + */ + + +/** + * @event shaka.Player.UnloadingEvent + * @description Fired when the player unloads or fails to load. + * Used by the Cast receiver to determine idle state. + * @property {string} type + * 'unloading' + * @exportDoc + */ + + /** * @event shaka.Player.TextTrackVisibilityEvent * @description Fired when text track visibility changes. @@ -345,6 +365,7 @@ shaka.Player.prototype.load = function(manifestUri, opt_startTime, var unloadPromise = this.unload(); var loadChain = new shaka.util.CancelableChain(); this.loadChain_ = loadChain; + this.dispatchEvent(new shaka.util.FakeEvent('loading')); return loadChain.then(function() { return unloadPromise; @@ -415,6 +436,7 @@ shaka.Player.prototype.load = function(manifestUri, opt_startTime, // If we haven't started another load, clear the loadChain_ member. if (this.loadChain_ == loadChain) { this.loadChain_ = null; + this.dispatchEvent(new shaka.util.FakeEvent('unloading')); } return Promise.reject(error); }.bind(this)); @@ -736,6 +758,7 @@ shaka.Player.prototype.isBuffering = function() { */ shaka.Player.prototype.unload = function() { if (this.destroyed_) return Promise.resolve(); + this.dispatchEvent(new shaka.util.FakeEvent('unloading')); if (this.loadChain_) { // A load is in progress. Cancel it, then reset the streaming system. diff --git a/test/cast/cast_receiver_unit.js b/test/cast/cast_receiver_unit.js index 4169b85b7f..82a309a036 100644 --- a/test/cast/cast_receiver_unit.js +++ b/test/cast/cast_receiver_unit.js @@ -133,21 +133,53 @@ describe('CastReceiver', function() { }); describe('"caststatuschanged" event', function() { - it('is triggered when senders connect or disconnect', function() { + it('is triggered when senders connect or disconnect', function(done) { checkChromeOrChromecast(); - fakeConnectedSenders(0); + var listener = jasmine.createSpy('listener'); + receiver.addEventListener('caststatuschanged', listener); + + shaka.test.Util.delay(0.2).then(function() { + expect(listener).not.toHaveBeenCalled(); + fakeConnectedSenders(1); + return shaka.test.Util.delay(0.2); + }).then(function() { + expect(listener).toHaveBeenCalled(); + listener.calls.reset(); + mockReceiverManager.onSenderDisconnected(); + return shaka.test.Util.delay(0.2); + }).then(function() { + expect(listener).toHaveBeenCalled(); + }).catch(fail).then(done); + }); + it('is triggered when idle state changes', function(done) { + checkChromeOrChromecast(); var listener = jasmine.createSpy('listener'); receiver.addEventListener('caststatuschanged', listener); - expect(listener).not.toHaveBeenCalled(); - mockReceiverManager.onSenderConnected(); - expect(listener).toHaveBeenCalled(); + var fakeLoadingEvent = {type: 'loading'}; + var fakeUnloadingEvent = {type: 'unloading'}; + var fakeEndedEvent = {type: 'ended'}; - listener.calls.reset(); - mockReceiverManager.onSenderDisconnected(); - expect(listener).toHaveBeenCalled(); - }); + shaka.test.Util.delay(0.2).then(function() { + expect(listener).not.toHaveBeenCalled(); + mockPlayer.listeners['loading'](fakeLoadingEvent); + return shaka.test.Util.delay(0.2); + }).then(function() { + expect(listener).toHaveBeenCalled(); + listener.calls.reset(); + mockPlayer.listeners['unloading'](fakeUnloadingEvent); + return shaka.test.Util.delay(0.2); + }).then(function() { + expect(listener).toHaveBeenCalled(); + listener.calls.reset(); + mockVideo.ended = true; + mockVideo.listeners['ended'](fakeEndedEvent); + return shaka.test.Util.delay(5.2); // There is a long delay for 'ended' + }).then(function() { + expect(listener).toHaveBeenCalled(); + }).catch(fail).then(done); + }, /* timeout ms */ 8000); }); describe('local events', function() {