Bug 1210865 - Change how Loop's data channels are setup to cope with the newer SDK that doesn't allow setting them up until subscription is complete. r=dmose

This commit is contained in:
Mark Banner 2015-11-10 08:58:10 +00:00
parent b8b9d7e44d
commit 4b933355a4
3 changed files with 284 additions and 188 deletions

View File

@ -620,7 +620,7 @@ loop.OTSdkDriver = (function() {
this.dispatcher.dispatch(new sharedActions.MediaConnected());
}
this._setupDataChannelIfNeeded(sdkSubscriberObject.stream.connection);
this._setupDataChannelIfNeeded(sdkSubscriberObject);
},
/**
@ -655,61 +655,24 @@ loop.OTSdkDriver = (function() {
* channel set-up routines. A data channel cannot be requested before this
* time as the peer connection is not set up.
*
* @param {OT.Connection} connection The OT connection class object.paul
* sched
* @param {OT.Subscriber} sdkSubscriberObject The subscriber object for the stream.
*
*/
_setupDataChannelIfNeeded: function(connection) {
if (this._useDataChannels) {
this.session.signal({
type: "readyForDataChannel",
to: connection
}, function(signalError) {
if (signalError) {
console.error(signalError);
}
});
}
},
/**
* Handles receiving the signal that the other end of the connection
* has subscribed to the stream and we're ready to setup the data channel.
*
* We get data channels for both the publisher and subscriber on reception
* of the signal, as it means that a) the remote client is setup for data
* channels, and b) that subscribing of streams has definitely completed
* for both clients.
*
* @param {OT.SignalEvent} event Details of the signal received.
*/
_onReadyForDataChannel: function(event) {
// If we don't want data channels, just ignore the message. We haven't
// send the other side a message, so it won't display anything.
_setupDataChannelIfNeeded: function(sdkSubscriberObject) {
if (!this._useDataChannels) {
return;
}
// This won't work until a subscriber exists for this publisher
this.publisher._.getDataChannel("text", {}, function(err, channel) {
if (err) {
console.error(err);
return;
this.session.signal({
type: "readyForDataChannel",
to: sdkSubscriberObject.stream.connection
}, function(signalError) {
if (signalError) {
console.error(signalError);
}
});
this._publisherChannel = channel;
channel.on({
close: function(e) {
// XXX We probably want to dispatch and handle this somehow.
console.log("Published data channel closed!");
}
});
this._checkDataChannelsAvailable();
}.bind(this));
this.subscriber._.getDataChannel("text", {}, function(err, channel) {
sdkSubscriberObject._.getDataChannel("text", {}, function(err, channel) {
// Sends will queue until the channel is fully open.
if (err) {
console.error(err);
@ -741,6 +704,44 @@ loop.OTSdkDriver = (function() {
}.bind(this));
},
/**
* Handles receiving the signal that the other end of the connection
* has subscribed to the stream and we're ready to setup the data channel.
*
* We create the publisher data channel when we get the signal as it means
* that the remote client is setup for data
* channels. Getting the data channel for the subscriber is handled
* separately when the subscription completes.
*
* @param {OT.SignalEvent} event Details of the signal received.
*/
_onReadyForDataChannel: function(event) {
// If we don't want data channels, just ignore the message. We haven't
// send the other side a message, so it won't display anything.
if (!this._useDataChannels) {
return;
}
// This won't work until a subscriber exists for this publisher
this.publisher._.getDataChannel("text", {}, function(err, channel) {
if (err) {
console.error(err);
return;
}
this._publisherChannel = channel;
channel.on({
close: function(e) {
// XXX We probably want to dispatch and handle this somehow.
console.log("Published data channel closed!");
}
});
this._checkDataChannelsAvailable();
}.bind(this));
},
/**
* Checks to see if all channels have been obtained, and if so it dispatches
* a notification to the stores to inform them.

View File

@ -142,6 +142,50 @@ class Test1BrowserCall(MarionetteTestCase):
self.switch_to_chatbox()
self.check_video(".remote-video")
def send_chat_message(self, text):
"""
Sends a chat message using the current context.
:param text: The text to send.
"""
chatbox = self.wait_for_element_displayed(By.CSS_SELECTOR,
".text-chat-box > form > input")
chatbox.send_keys(text + "\n")
def check_received_message(self, expectedText):
"""
Checks a chat message has been received in the current context. The
test assumes only one chat message will be received during the tests.
:param expectedText: The expected text of the chat message.
"""
text_entry = self.wait_for_element_displayed(By.CSS_SELECTOR,
".text-chat-entry.received > p > span")
self.assertEqual(text_entry.text, expectedText,
"should have received the correct message")
def check_text_messaging(self):
"""
Checks text messaging between the generator and clicker in a bi-directional
fashion.
"""
# Send a message using the link generator.
self.switch_to_chatbox()
self.send_chat_message("test1")
# Now check the result on the link clicker.
self.switch_to_standalone()
self.check_received_message("test1")
# Then send a message using the standalone.
self.send_chat_message("test2")
# Finally check the link generator got it.
self.switch_to_chatbox()
self.check_received_message("test2")
def local_enable_screenshare(self):
self.switch_to_chatbox()
button = self.marionette.find_element(By.CLASS_NAME, "btn-screen-share")
@ -242,6 +286,9 @@ class Test1BrowserCall(MarionetteTestCase):
self.standalone_check_remote_video()
self.local_check_remote_video()
# Check text messaging
self.check_text_messaging()
# since bi-directional media is connected, make sure we've set
# the start time
self.local_check_media_start_time_initialized()

View File

@ -752,6 +752,9 @@ describe("loop.OTSdkDriver", function() {
};
fakeSubscriberObject = _.extend({
"_": {
getDataChannel: sinon.stub()
},
session: { connection: fakeConnection },
stream: fakeStream
}, Backbone.Events);
@ -996,131 +999,196 @@ describe("loop.OTSdkDriver", function() {
}));
});
it("should subscribe to a camera stream", function() {
session.trigger("streamCreated", { stream: fakeStream });
describe("Audio/Video streams", function() {
beforeEach(function() {
session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
videoElement).returns(this.fakeSubscriberObject);
});
sinon.assert.calledOnce(session.subscribe);
sinon.assert.calledWithExactly(session.subscribe,
fakeStream, sinon.match.instanceOf(HTMLDivElement), publisherConfig,
sinon.match.func);
});
it("should subscribe to a camera stream", function() {
session.trigger("streamCreated", { stream: fakeStream });
it("should dispatch MediaStreamCreated after subscribe is complete", function() {
session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
videoElement).returns(this.fakeSubscriberObject);
driver.session = session;
fakeStream.connection = fakeConnection;
fakeStream.hasVideo = true;
sinon.assert.calledOnce(session.subscribe);
sinon.assert.calledWithExactly(session.subscribe,
fakeStream, sinon.match.instanceOf(HTMLDivElement), publisherConfig,
sinon.match.func);
});
session.trigger("streamCreated", { stream: fakeStream });
it("should dispatch MediaStreamCreated after streamCreated is triggered on the session", function() {
driver.session = session;
fakeStream.connection = fakeConnection;
fakeStream.hasVideo = true;
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.MediaStreamCreated({
hasVideo: true,
isLocal: false,
srcMediaElement: videoElement
}));
});
session.trigger("streamCreated", { stream: fakeStream });
it("should dispatch MediaStreamCreated after subscribe with audio-only indication if hasVideo=false", function() {
session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
videoElement);
fakeStream.connection = fakeConnection;
fakeStream.hasVideo = false;
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.MediaStreamCreated({
hasVideo: true,
isLocal: false,
srcMediaElement: videoElement
}));
});
session.trigger("streamCreated", { stream: fakeStream });
it("should dispatch MediaStreamCreated after streamCreated with audio-only indication if hasVideo=false", function() {
fakeStream.connection = fakeConnection;
fakeStream.hasVideo = false;
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.MediaStreamCreated({
hasVideo: false,
isLocal: false,
srcMediaElement: videoElement
}));
});
session.trigger("streamCreated", { stream: fakeStream });
it("should trigger a readyForDataChannel signal after subscribe is complete", function() {
session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
document.createElement("video"));
driver._useDataChannels = true;
fakeStream.connection = fakeConnection;
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.MediaStreamCreated({
hasVideo: false,
isLocal: false,
srcMediaElement: videoElement
}));
});
session.trigger("streamCreated", { stream: fakeStream });
it("should dispatch a mediaConnected action if both streams are up", function() {
driver._publishedLocalStream = true;
sinon.assert.calledOnce(session.signal);
sinon.assert.calledWith(session.signal, {
type: "readyForDataChannel",
to: fakeConnection
session.trigger("streamCreated", { stream: fakeStream });
// Called twice due to the VideoDimensionsChanged above.
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
new sharedActions.MediaConnected({}));
});
it("should store the start time when both streams are up and" +
" driver._sendTwoWayMediaTelemetry is true", function() {
driver._sendTwoWayMediaTelemetry = true;
driver._publishedLocalStream = true;
var startTime = 1;
sandbox.stub(performance, "now").returns(startTime);
session.trigger("streamCreated", { stream: fakeStream });
expect(driver._getTwoWayMediaStartTime()).to.eql(startTime);
});
it("should not store the start time when both streams are up and" +
" driver._isDesktop is false", function() {
driver._isDesktop = false;
driver._publishedLocalStream = true;
var startTime = 73;
sandbox.stub(performance, "now").returns(startTime);
session.trigger("streamCreated", { stream: fakeStream });
expect(driver._getTwoWayMediaStartTime()).to.not.eql(startTime);
});
describe("Data channel setup", function() {
var fakeChannel;
beforeEach(function() {
fakeChannel = _.extend({}, Backbone.Events);
fakeStream.connection = fakeConnection;
driver._useDataChannels = true;
});
it("should trigger a readyForDataChannel signal after subscribe is complete", function() {
session.trigger("streamCreated", { stream: fakeStream });
sinon.assert.calledOnce(session.signal);
sinon.assert.calledWith(session.signal, {
type: "readyForDataChannel",
to: fakeConnection
});
});
it("should not trigger readyForDataChannel signal if data channels are not wanted", function() {
driver._useDataChannels = false;
session.trigger("streamCreated", { stream: fakeStream });
sinon.assert.notCalled(session.signal);
});
it("should get the data channel after subscribe is complete", function() {
session.trigger("streamCreated", { stream: fakeStream });
sinon.assert.calledOnce(fakeSubscriberObject._.getDataChannel);
sinon.assert.calledWith(fakeSubscriberObject._.getDataChannel, "text", {});
});
it("should not get the data channel if data channels are not wanted", function() {
driver._useDataChannels = false;
session.trigger("streamCreated", { stream: fakeStream });
sinon.assert.notCalled(fakeSubscriberObject._.getDataChannel);
});
it("should dispatch `DataChannelsAvailable` if the publisher channel is setup", function() {
// Fake a publisher channel.
driver._publisherChannel = {};
fakeSubscriberObject._.getDataChannel.callsArgWith(2, null, fakeChannel);
session.trigger("streamCreated", { stream: fakeStream });
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.DataChannelsAvailable({
available: true
}));
});
it("should not dispatch `DataChannelsAvailable` if the publisher channel isn't setup", function() {
fakeSubscriberObject._.getDataChannel.callsArgWith(2, null, fakeChannel);
session.trigger("streamCreated", { stream: fakeStream });
sinon.assert.neverCalledWith(dispatcher.dispatch,
new sharedActions.DataChannelsAvailable({
available: true
}));
});
it("should dispatch `ReceivedTextChatMessage` when a text message is received", function() {
var data = '{"contentType":"' + CHAT_CONTENT_TYPES.TEXT +
'","message":"Are you there?","receivedTimestamp": "2015-06-25T00:29:14.197Z"}';
var clock = sinon.useFakeTimers();
fakeSubscriberObject._.getDataChannel.callsArgWith(2, null, fakeChannel);
session.trigger("streamCreated", { stream: fakeStream });
// Now send the message.
fakeChannel.trigger("message", {
data: data
});
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ReceivedTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Are you there?",
receivedTimestamp: "1970-01-01T00:00:00.000Z"
}));
/* Restore the time. */
clock.restore();
});
});
});
it("should not trigger readyForDataChannel signal if data channels are not wanted", function() {
session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
document.createElement("video"));
driver._useDataChannels = false;
fakeStream.connection = fakeConnection;
describe("screen sharing streams", function() {
it("should subscribe to a screen sharing stream", function() {
fakeStream.videoType = "screen";
session.trigger("streamCreated", { stream: fakeStream });
session.trigger("streamCreated", { stream: fakeStream });
sinon.assert.notCalled(session.signal);
});
sinon.assert.calledOnce(session.subscribe);
sinon.assert.calledWithExactly(session.subscribe,
fakeStream, sinon.match.instanceOf(HTMLDivElement), publisherConfig,
sinon.match.func);
});
it("should subscribe to a screen sharing stream", function() {
fakeStream.videoType = "screen";
session.trigger("streamCreated", { stream: fakeStream });
sinon.assert.calledOnce(session.subscribe);
sinon.assert.calledWithExactly(session.subscribe,
fakeStream, sinon.match.instanceOf(HTMLDivElement), publisherConfig,
sinon.match.func);
});
it("should dispatch a mediaConnected action if both streams are up", function() {
session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
videoElement);
driver._publishedLocalStream = true;
session.trigger("streamCreated", { stream: fakeStream });
// Called twice due to the VideoDimensionsChanged above.
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
new sharedActions.MediaConnected({}));
});
it("should store the start time when both streams are up and" +
" driver._sendTwoWayMediaTelemetry is true", function() {
session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
videoElement);
driver._sendTwoWayMediaTelemetry = true;
driver._publishedLocalStream = true;
var startTime = 1;
sandbox.stub(performance, "now").returns(startTime);
session.trigger("streamCreated", { stream: fakeStream });
expect(driver._getTwoWayMediaStartTime()).to.eql(startTime);
});
it("should not store the start time when both streams are up and" +
" driver._isDesktop is false", function() {
session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
videoElement);
driver._isDesktop = false;
driver._publishedLocalStream = true;
var startTime = 73;
sandbox.stub(performance, "now").returns(startTime);
session.trigger("streamCreated", { stream: fakeStream });
expect(driver._getTwoWayMediaStartTime()).to.not.eql(startTime);
});
it("should not dispatch a mediaConnected action for screen sharing streams",
function() {
it("should not dispatch a mediaConnected action for screen sharing streams", function() {
driver._publishedLocalStream = true;
fakeStream.videoType = "screen";
@ -1130,16 +1198,14 @@ describe("loop.OTSdkDriver", function() {
sinon.match.hasOwn("name", "mediaConnected"));
});
it("should not dispatch a ReceivingScreenShare action for camera streams",
function() {
it("should not dispatch a ReceivingScreenShare action for camera streams", function() {
session.trigger("streamCreated", { stream: fakeStream });
sinon.assert.neverCalledWithMatch(dispatcher.dispatch,
new sharedActions.ReceivingScreenShare({ receiving: true }));
});
it("should dispatch a ReceivingScreenShare action for screen" +
" sharing streams", function() {
it("should dispatch a ReceivingScreenShare action for screen sharing streams", function() {
fakeStream.videoType = "screen";
session.trigger("streamCreated", { stream: fakeStream });
@ -1149,6 +1215,7 @@ describe("loop.OTSdkDriver", function() {
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ReceivingScreenShare({ receiving: true }));
});
});
});
describe("streamDestroyed: publisher/local", function() {
@ -1499,16 +1566,11 @@ describe("loop.OTSdkDriver", function() {
sinon.assert.calledOnce(publisher._.getDataChannel);
});
it("should get the data channel for the subscriber", function() {
session.trigger("signal:readyForDataChannel");
sinon.assert.calledOnce(subscriber._.getDataChannel);
});
it("should dispatch `DataChannelsAvailable` once both data channels have been obtained", function() {
it("should dispatch `DataChannelsAvailable` if the subscriber channel is setup", function() {
var fakeChannel = _.extend({}, Backbone.Events);
subscriber._.getDataChannel.callsArgWith(2, null, fakeChannel);
driver._subscriberChannel = fakeChannel;
publisher._.getDataChannel.callsArgWith(2, null, fakeChannel);
session.trigger("signal:readyForDataChannel");
@ -1520,31 +1582,17 @@ describe("loop.OTSdkDriver", function() {
}));
});
it("should dispatch `ReceivedTextChatMessage` when a text message is received", function() {
it("should not dispatch `DataChannelsAvailable` if the subscriber channel isn't setup", function() {
var fakeChannel = _.extend({}, Backbone.Events);
var data = '{"contentType":"' + CHAT_CONTENT_TYPES.TEXT +
'","message":"Are you there?","receivedTimestamp": "2015-06-25T00:29:14.197Z"}';
var clock = sinon.useFakeTimers();
subscriber._.getDataChannel.callsArgWith(2, null, fakeChannel);
publisher._.getDataChannel.callsArgWith(2, null, fakeChannel);
session.trigger("signal:readyForDataChannel");
// Now send the message.
fakeChannel.trigger("message", {
data: data
});
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ReceivedTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Are you there?",
receivedTimestamp: "1970-01-01T00:00:00.000Z"
sinon.assert.neverCalledWith(dispatcher.dispatch,
new sharedActions.DataChannelsAvailable({
available: true
}));
/* Restore the time. */
clock.restore();
});
});