Skip to content

Commit

Permalink
Send MediaInfo message with duration when casting
Browse files Browse the repository at this point in the history
When casting, the cast dialogue contains information about the current
media, including volume, time, and duration. These values are
synchronized by different messages. We were sending over the
MediaStatus messages that synchronize current time, but were not
including duration in our MediaInfo messages. This caused the cast
dialogue progress bar to not display correctly.
This change makes it so that our MediaInfo messages now include
duration, and we now send over MediaInfo messages in situations where
we did not previously.

Closes #1174

Change-Id: Ic585f3befec9e44ef4e9895d04ddfad6cc5473b3
  • Loading branch information
theodab authored and joeyparrish committed Jan 19, 2018
1 parent 39383e7 commit c758562
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 10 deletions.
71 changes: 61 additions & 10 deletions lib/cast/cast_receiver.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ shaka.cast.CastReceiver =
/** @private {boolean} */
this.startUpdatingUpdateNumber_ = false;

/** @private {boolean} */
this.initialStatusUpdatePending_ = true;

/** @private {cast.receiver.CastMessageBus} */
this.shakaBus_ = null;

Expand Down Expand Up @@ -239,6 +242,13 @@ shaka.cast.CastReceiver.prototype.init_ = function() {

/** @private */
shaka.cast.CastReceiver.prototype.onSendersChanged_ = function() {
// Reset update message frequency values, to make sure whomever joined
// will get a full update message.
this.updateNumber_ = 0;
// Don't reset startUpdatingUpdateNumber_, because this operation does not
// result in new data being loaded.
this.initialStatusUpdatePending_ = true;

var manager = cast.receiver.CastReceiverManager.getInstance();
this.isConnected_ = manager.getSenders().length != 0;
this.onCastStatusChanged_();
Expand All @@ -256,7 +266,9 @@ shaka.cast.CastReceiver.prototype.onCastStatusChanged_ = function() {
Promise.resolve().then(function() {
var event = new shaka.util.FakeEvent('caststatuschanged');
this.dispatchEvent(event);
this.sendMediaStatus_(0);
// Send a media status message, with a media info message if appropriate.
if (!this.maybeSendMediaInfoMessage_())
this.sendMediaStatus_(0);
}.bind(this));
};

Expand Down Expand Up @@ -401,6 +413,44 @@ shaka.cast.CastReceiver.prototype.pollAttributes_ = function() {
'type': 'update',
'update': update
}, this.shakaBus_);

this.maybeSendMediaInfoMessage_();
};


/**
* Composes and sends a mediaStatus message if appropriate.
* @return {boolean}
* @private
*/
shaka.cast.CastReceiver.prototype.maybeSendMediaInfoMessage_ = function() {
if (this.initialStatusUpdatePending_ &&
(this.video_.duration || this.player_.isLive())) {
// Send over a media status message to set the duration of the cast
// dialogue.
this.sendMediaInfoMessage_();
this.initialStatusUpdatePending_ = false;
return true;
}
return false;
};


/**
* Composes and sends a mediaStatus message with a mediaInfo component.
* @private
*/
shaka.cast.CastReceiver.prototype.sendMediaInfoMessage_ = function() {
var media = {
'contentId': this.player_.getManifestUri(),
'streamType': this.player_.isLive() ? 'LIVE' : 'BUFFERED',
'duration': this.video_.duration,
// TODO: Is there a use case when this would be required?
// Sending an empty string for now since it's a mandatory
// field.
'contentType': ''
};
this.sendMediaStatus_(0, media);
};


Expand Down Expand Up @@ -452,6 +502,7 @@ shaka.cast.CastReceiver.prototype.onShakaMessage_ = function(event) {
// Reset update message frequency values after initialization.
this.updateNumber_ = 0;
this.startUpdatingUpdateNumber_ = false;
this.initialStatusUpdatePending_ = true;

this.initState_(message['initState'], message['appData']);
// The sender is supposed to reflect the cast system volume after
Expand Down Expand Up @@ -505,6 +556,13 @@ shaka.cast.CastReceiver.prototype.onShakaMessage_ = function(event) {
var senderId = event.senderId;
var target = this.targets_[targetName];
var p = target[methodName].apply(target, args);
if (targetName == 'player' && methodName == 'load') {
// Wait until the manifest has actually loaded to send another media
// info message, so on a new load it doesn't send the old info over.
p = p.then(function() {
this.initialStatusUpdatePending_ = true;
}.bind(this));
}
// Replies must go back to the specific sender who initiated, so that we
// don't have to deal with conflicting IDs between senders.
p.then(this.sendAsyncComplete_.bind(this, senderId, id, /* error */ null),
Expand Down Expand Up @@ -580,6 +638,7 @@ shaka.cast.CastReceiver.prototype.onGenericMessage_ = function(event) {
// Reset update message frequency values after a load.
this.updateNumber_ = 0;
this.startUpdatingUpdateNumber_ = false;
this.initialStatusUpdatePending_ = false; // This already sends an update.

var mediaInfo = message['media'];
var contentId = mediaInfo['contentId'];
Expand All @@ -590,15 +649,7 @@ shaka.cast.CastReceiver.prototype.onGenericMessage_ = function(event) {
this.video_.autoplay = true;
this.player_.load(manifestUri, currentTime).then(function() {
// Notify generic controllers that the media has changed.
var media = {
'contentId': manifestUri,
'streamType': this.player_.isLive() ? 'LIVE' : 'BUFFERED',
// TODO: Is there a use case when this would be required?
// Sending an empty string for now since it's a mandatory
// field.
'contentType': ''
};
this.sendMediaStatus_(0, media);
this.sendMediaInfoMessage_();
}.bind(this)).catch(function(error) {
// Load failed. Dispatch the error message to the sender.
var type = 'LOAD_FAILED';
Expand Down
124 changes: 124 additions & 0 deletions test/cast/cast_receiver_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,130 @@ describe('CastReceiver', function() {
});
});

describe('sends duration', function() {
beforeEach(function(done) {
checkChromeOrChromecast();

receiver = new CastReceiver(
mockVideo, mockPlayer, Util.spyFunc(mockAppDataCallback));
fakeConnectedSenders(1);
mockPlayer.load = function() {
mockVideo.duration = 1;
mockPlayer.getManifestUri = function() {
return 'URI A';
};
return Promise.resolve();
};
fakeIncomingMessage({
type: 'init',
initState: { manifest: 'URI A' },
appData: {}
}, mockShakaMessageBus);

// The messages will show up asychronously:
Util.delay(0.1).then(function() {
expectMediaInfo('URI A', 1);
mockGenericMessageBus.messages = [];
}).then(done);
});

it('only once, if nothing else changes', function(done) {
checkChromeOrChromecast();

Util.delay(0.5).then(function() {
expect(mockGenericMessageBus.messages.length).toBe(0);
}).then(done);
});

it('after new sender connects', function(done) {
checkChromeOrChromecast();

fakeConnectedSenders(1);
Util.delay(0.5).then(function() {
expectMediaInfo('URI A', 1);
expect(mockGenericMessageBus.messages.length).toBe(0);
}).then(done);
});

it('for correct manifest after loading new manifest', function(done) {
checkChromeOrChromecast();

// Change media information, but only after half a second.
mockPlayer.load = function() {
return Util.delay(0.5).then(function() {
mockVideo.duration = 2;
mockPlayer.getManifestUri = function() {
return 'URI B';
};
});
};
fakeIncomingMessage({
type: 'asyncCall',
id: '5',
targetName: 'player',
methodName: 'load',
args: ['URI B']
}, mockShakaMessageBus, 'senderId');

// Wait for the mockPlayer to finish 'loading' before checking again.
Util.delay(1.0).then(function() {
expectMediaInfo('URI B', 2); // pollAttributes_
expect(mockGenericMessageBus.messages.length).toBe(0);
}).then(done);
});

it('after LOAD system message', function(done) {
checkChromeOrChromecast();

mockPlayer.load = function() {
mockVideo.duration = 2;
mockPlayer.getManifestUri = function() {
return 'URI B';
};
return Promise.resolve();
};
var message = {
// Arbitrary number
'requestId': 0,
'type': 'LOAD',
'autoplay': false,
'currentTime': 10,
'media': {
'contentId': 'URI B',
'contentType': 'video/mp4',
'streamType': 'BUFFERED'
}
};
fakeIncomingMessage(message, mockGenericMessageBus);

Util.delay(0.5).then(function() {
expectMediaInfo('URI B', 2);
expect(mockGenericMessageBus.messages.length).toBe(0);
}).then(done);
});

function expectMediaInfo(expectedUri, expectedDuration) {
expect(mockGenericMessageBus.messages.length).toBeGreaterThan(0);
if (mockGenericMessageBus.messages.length == 0)
return;
expect(mockGenericMessageBus.messages[0]).toEqual(
{
requestId: 0,
type: 'MEDIA_STATUS',
status: [jasmine.objectContaining({
media: {
contentId: expectedUri,
streamType: 'BUFFERED',
duration: expectedDuration,
contentType: ''
}
})]
}
);
mockGenericMessageBus.messages.shift();
}
});

describe('respects generic control messages', function() {
beforeEach(function() {
receiver = new CastReceiver(
Expand Down

0 comments on commit c758562

Please sign in to comment.