Bug 1122032 Part 2 - Show the Loop screenshare video in place of the remote video for now. r=mikedeboer

This commit is contained in:
Mark Banner 2015-02-02 21:53:19 +00:00
parent bd23ef68d0
commit b729195eb0
18 changed files with 297 additions and 40 deletions

View File

@ -898,7 +898,7 @@ loop.conversationViews = (function(mozL10n) {
React.createElement("div", {className: "conversation"},
React.createElement("div", {className: "media nested"},
React.createElement("div", {className: "video_wrapper remote_wrapper"},
React.createElement("div", {className: "video_inner remote"})
React.createElement("div", {className: "video_inner remote remote-stream"})
),
React.createElement("div", {className: localStreamClasses})
),

View File

@ -898,7 +898,7 @@ loop.conversationViews = (function(mozL10n) {
<div className="conversation">
<div className="media nested">
<div className="video_wrapper remote_wrapper">
<div className="video_inner remote"></div>
<div className="video_inner remote remote-stream"></div>
</div>
<div className={localStreamClasses}></div>
</div>

View File

@ -274,7 +274,7 @@ loop.roomViews = (function(mozL10n) {
React.createElement("div", {className: "conversation room-conversation"},
React.createElement("div", {className: "media nested"},
React.createElement("div", {className: "video_wrapper remote_wrapper"},
React.createElement("div", {className: "video_inner remote"})
React.createElement("div", {className: "video_inner remote remote-stream"})
),
React.createElement("div", {className: localStreamClasses}),
React.createElement("div", {className: "screen hide"})

View File

@ -274,7 +274,7 @@ loop.roomViews = (function(mozL10n) {
<div className="conversation room-conversation">
<div className="media nested">
<div className="video_wrapper remote_wrapper">
<div className="video_inner remote"></div>
<div className="video_inner remote remote-stream"></div>
</div>
<div className={localStreamClasses}></div>
<div className="screen hide"></div>

View File

@ -231,7 +231,7 @@
/* Side by side video elements */
.conversation .media.side-by-side .remote {
.conversation .media.side-by-side .remote-stream {
width: 50%;
float: left;
}
@ -509,7 +509,7 @@
max-height: none;
}
.conversation .media.nested .remote {
.conversation .media.nested .remote-stream {
display: inline-block;
position: absolute; /* workaround for lack of object-fit; see bug 1020445 */
width: 100%;

View File

@ -218,6 +218,13 @@ loop.shared.actions = (function() {
state: String
}),
/**
* Used to notify that a shared screen is being received (or not).
*/
ReceivingScreenShare: Action.define("receivingScreenShare", {
receiving: Boolean
}),
/**
* Creates a new room.
* XXX: should move to some roomActions module - refs bug 1079284

View File

@ -73,7 +73,8 @@ loop.store.ActiveRoomStore = (function() {
used: false,
localVideoDimensions: {},
remoteVideoDimensions: {},
screenSharingState: SCREEN_SHARE_STATES.INACTIVE
screenSharingState: SCREEN_SHARE_STATES.INACTIVE,
receivingScreenShare: false
};
},
@ -121,6 +122,7 @@ loop.store.ActiveRoomStore = (function() {
"connectionFailure",
"setMute",
"screenSharingState",
"receivingScreenShare",
"remotePeerDisconnected",
"remotePeerConnected",
"windowUnload",
@ -380,6 +382,13 @@ loop.store.ActiveRoomStore = (function() {
this.setStoreState({screenSharingState: actionData.state});
},
/**
* Used to note the current state of receiving screenshare data.
*/
receivingScreenShare: function(actionData) {
this.setStoreState({receivingScreenShare: actionData.receiving});
},
/**
* Handles recording when a remote peer has connected to the servers.
*/

View File

@ -259,7 +259,8 @@ loop.shared.mixins = (function() {
},
/**
* Retrieve the dimensions of the remote video stream.
* Retrieve the dimensions of the active remote video stream. This assumes
* that if screens are being shared, the remote camera stream is hidden.
* Example output:
* {
* width: 680,
@ -270,6 +271,8 @@ loop.shared.mixins = (function() {
* offsetY: 0
* }
*
* Note: This expects a class on the element that has the name "remote" or the
* same name as the possible video types (currently only "screen").
* Note: Once we support multiple remote video streams, this function will
* need to be updated.
* @return {Object} contains the remote stream dimension properties of its
@ -320,7 +323,7 @@ loop.shared.mixins = (function() {
// Calculate the size of each individual letter- or pillarbox for convenience.
remoteVideoDimensions.offsetX = remoteVideoDimensions.width -
remoteVideoDimensions.streamWidth
remoteVideoDimensions.streamWidth;
if (remoteVideoDimensions.offsetX > 0) {
remoteVideoDimensions.offsetX /= 2;
}
@ -351,18 +354,22 @@ loop.shared.mixins = (function() {
this._bufferedUpdateVideo = null;
var localStreamParent = this._getElement(".local .OT_publisher");
var remoteStreamParent = this._getElement(".remote .OT_subscriber");
var screenShareStreamParent = this._getElement('.screen .OT_subscriber');
if (localStreamParent) {
localStreamParent.style.width = "100%";
}
if (remoteStreamParent) {
remoteStreamParent.style.height = "100%";
}
if (screenShareStreamParent) {
screenShareStreamParent.style.height = "100%";
}
// Update the position and dimensions of the containers of local video
// streams, if necessary. The consumer of this mixin should implement the
// actual updating mechanism.
Object.keys(this._videoDimensionsCache.local).forEach(function(videoType) {
var ratio = this._videoDimensionsCache.local[videoType].aspectRatio
var ratio = this._videoDimensionsCache.local[videoType].aspectRatio;
if (videoType == "camera" && this.updateLocalCameraPosition) {
this.updateLocalCameraPosition(ratio);
}

View File

@ -136,6 +136,7 @@ loop.OTSdkDriver = (function() {
this.session.on("connectionCreated", this._onConnectionCreated.bind(this));
this.session.on("streamCreated", this._onRemoteStreamCreated.bind(this));
this.session.on("streamDestroyed", this._onRemoteStreamDestroyed.bind(this));
this.session.on("connectionDestroyed",
this._onConnectionDestroyed.bind(this));
this.session.on("sessionDisconnected",
@ -280,6 +281,30 @@ loop.OTSdkDriver = (function() {
this.dispatcher.dispatch(new sharedActions.RemotePeerConnected());
},
/**
* Handles when a remote screen share is created, subscribing to
* the stream, and notifying the stores that a share is being
* received.
*
* @param {Stream} stream The SDK Stream:
* https://tokbox.com/opentok/libraries/client/js/reference/Stream.html
*/
_handleRemoteScreenShareCreated: function(stream) {
if (!this.getScreenShareElementFunc) {
return;
}
// Let the stores know first so they can update the display.
this.dispatcher.dispatch(new sharedActions.ReceivingScreenShare({
receiving: true
}));
var remoteElement = this.getScreenShareElementFunc();
this.session.subscribe(stream,
remoteElement, this._getCopyPublisherConfig());
},
/**
* Handles the event when the remote stream is created.
*
@ -295,14 +320,13 @@ loop.OTSdkDriver = (function() {
}));
}
var remoteElement;
if (event.stream.videoType === "screen") {
// XXX Implement in part 2.
remoteElement = "null";
} else {
remoteElement = this.getRemoteElement();
this._handleRemoteScreenShareCreated(event.stream);
return;
}
var remoteElement = this.getRemoteElement();
this.session.subscribe(event.stream,
remoteElement, this._getCopyPublisherConfig());
@ -328,6 +352,25 @@ loop.OTSdkDriver = (function() {
}
},
/**
* Handles the event when the remote stream is destroyed.
*
* @param {StreamEvent} event The event details:
* https://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
*/
_onRemoteStreamDestroyed: function(event) {
if (event.stream.videoType !== "screen") {
return;
}
// All we need to do is notify the store we're no longer receiving,
// the sdk should do the rest.
this.dispatcher.dispatch(new sharedActions.ReceivingScreenShare({
receiving: false
}));
},
/**
* Called from the sdk when the media access dialog is opened.
* Prevents the default action, to prevent the SDK's "allow access"

View File

@ -358,7 +358,7 @@ loop.shared.views = (function(_, l10n) {
React.createElement("div", {className: "conversation"},
React.createElement("div", {className: "media nested"},
React.createElement("div", {className: "video_wrapper remote_wrapper"},
React.createElement("div", {className: "video_inner remote"})
React.createElement("div", {className: "video_inner remote remote-stream"})
),
React.createElement("div", {className: localStreamClasses})
),

View File

@ -358,7 +358,7 @@ loop.shared.views = (function(_, l10n) {
<div className="conversation">
<div className="media nested">
<div className="video_wrapper remote_wrapper">
<div className="video_inner remote"></div>
<div className="video_inner remote remote-stream"></div>
</div>
<div className={localStreamClasses}></div>
</div>

View File

@ -251,7 +251,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this.getDefaultPublisherConfig({publishVideo: true}),
getLocalElementFunc: this._getElement.bind(this, ".local"),
getRemoteElementFunc: this._getElement.bind(this, ".remote")
getRemoteElementFunc: this._getElement.bind(this, ".remote"),
getScreenShareElementFunc: this._getElement.bind(this, ".screen")
}));
}
@ -339,6 +340,19 @@ loop.standaloneRoomViews = (function(mozL10n) {
"local-stream-audio": this.state.videoMuted
});
var remoteStreamClasses = React.addons.classSet({
"video_inner": true,
"remote": true,
"remote-stream": true,
hide: this.state.receivingScreenShare
});
var screenShareStreamClasses = React.addons.classSet({
"screen": true,
"remote-stream": true,
hide: !this.state.receivingScreenShare
});
return (
React.createElement("div", {className: "room-conversation-wrapper"},
React.createElement("div", {className: "beta-logo"}),
@ -357,11 +371,13 @@ loop.standaloneRoomViews = (function(mozL10n) {
mozL10n.get("self_view_hidden_message")
),
React.createElement("div", {className: "video_wrapper remote_wrapper"},
React.createElement("div", {className: "video_inner remote"})
React.createElement("div", {className: remoteStreamClasses}),
React.createElement("div", {className: screenShareStreamClasses})
),
React.createElement("div", {className: localStreamClasses})
),
React.createElement(sharedViews.ConversationToolbar, {
dispatcher: this.props.dispatcher,
video: {enabled: !this.state.videoMuted,
visible: this._roomIsActive()},
audio: {enabled: !this.state.audioMuted,

View File

@ -251,7 +251,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this.getDefaultPublisherConfig({publishVideo: true}),
getLocalElementFunc: this._getElement.bind(this, ".local"),
getRemoteElementFunc: this._getElement.bind(this, ".remote")
getRemoteElementFunc: this._getElement.bind(this, ".remote"),
getScreenShareElementFunc: this._getElement.bind(this, ".screen")
}));
}
@ -339,6 +340,19 @@ loop.standaloneRoomViews = (function(mozL10n) {
"local-stream-audio": this.state.videoMuted
});
var remoteStreamClasses = React.addons.classSet({
"video_inner": true,
"remote": true,
"remote-stream": true,
hide: this.state.receivingScreenShare
});
var screenShareStreamClasses = React.addons.classSet({
"screen": true,
"remote-stream": true,
hide: !this.state.receivingScreenShare
});
return (
<div className="room-conversation-wrapper">
<div className="beta-logo" />
@ -357,11 +371,13 @@ loop.standaloneRoomViews = (function(mozL10n) {
{mozL10n.get("self_view_hidden_message")}
</span>
<div className="video_wrapper remote_wrapper">
<div className="video_inner remote"></div>
<div className={remoteStreamClasses}></div>
<div className={screenShareStreamClasses}></div>
</div>
<div className={localStreamClasses}></div>
</div>
<sharedViews.ConversationToolbar
dispatcher={this.props.dispatcher}
video={{enabled: !this.state.videoMuted,
visible: this._roomIsActive()}}
audio={{enabled: !this.state.audioMuted,

View File

@ -14,6 +14,7 @@ FIREFOX_PREFERENCES = {
"media.volume_scale": "0",
"loop.gettingStarted.seen": True,
"loop.seenToS": "seen",
"loop.screenshare.enabled": True,
# this dialog is fragile, and likely to introduce intermittent failures
"media.navigator.permission.disabled": True

View File

@ -4,7 +4,6 @@ import urlparse
from errors import NoSuchElementException, StaleElementException
# noinspection PyUnresolvedReferences
from wait import Wait
from time import sleep
import os
import sys
@ -126,23 +125,32 @@ class Test1BrowserCall(MarionetteTestCase):
join_button.click()
# Assumes the standlone or the conversation window is selected first.
def check_remote_video(self):
video_wrapper = self.wait_for_element_displayed(
By.CSS_SELECTOR,
".media .OT_subscriber .OT_widget-container", 20)
video = self.wait_for_subelement_displayed(
video_wrapper, By.TAG_NAME, "video")
def check_video(self, selector):
video_wrapper = self.wait_for_element_displayed(By.CSS_SELECTOR,
selector, 20)
video = self.wait_for_subelement_displayed(video_wrapper,
By.TAG_NAME, "video")
self.wait_for_element_attribute_to_be_false(video, "paused")
self.assertEqual(video.get_attribute("ended"), "false")
def standalone_check_remote_video(self):
self.switch_to_standalone()
self.check_remote_video()
self.check_video(".remote .OT_subscriber .OT_widget-container")
def local_check_remote_video(self):
self.switch_to_chatbox()
self.check_remote_video()
self.check_video(".remote .OT_subscriber .OT_widget-container")
def local_enable_screenshare(self):
self.switch_to_chatbox()
button = self.marionette.find_element(By.CLASS_NAME, "btn-screen-share")
button.click()
def standalone_check_remote_screenshare(self):
self.switch_to_standalone()
self.check_video(".media .screen .OT_subscriber .OT_widget-container")
def local_leave_room_and_verify_feedback(self):
button = self.marionette.find_element(By.CLASS_NAME, "btn-hangup")
@ -170,6 +178,11 @@ class Test1BrowserCall(MarionetteTestCase):
self.standalone_check_remote_video()
self.local_check_remote_video()
# XXX To enable this, we either need to navigate the permissions prompt
# or have a route where we don't need the permissions prompt.
# self.local_enable_screenshare()
# self.standalone_check_remote_screenshare()
# hangup the call
self.local_leave_room_and_verify_feedback()

View File

@ -657,6 +657,16 @@ describe("loop.store.ActiveRoomStore", function () {
});
});
describe("#receivingScreenShare", function() {
it("should save the state", function() {
store.receivingScreenShare(new sharedActions.ReceivingScreenShare({
receiving: true
}));
expect(store.getStoreState().receivingScreenShare).eql(true);
});
});
describe("#remotePeerConnected", function() {
it("should set the state to `HAS_PARTICIPANTS`", function() {
store.remotePeerConnected();

View File

@ -236,13 +236,15 @@ describe("loop.shared.mixins", function() {
});
describe("Events", function() {
var localElement, remoteElement;
var localElement, remoteElement, screenShareElement;
beforeEach(function() {
sandbox.stub(view, "getDOMNode").returns({
querySelector: function(classSelector) {
if (classSelector.contains("local")) {
return localElement;
} else if (classSelector.contains("screen")) {
return screenShareElement;
}
return remoteElement;
}
@ -275,6 +277,19 @@ describe("loop.shared.mixins", function() {
expect(remoteElement.style.height).eql("100%");
});
it("should update the height on the screen share stream element", function() {
screenShareElement = {
offsetWidth: 100,
offsetHeight: 100,
style: { height: "0%" }
};
rootObject.events.resize();
sandbox.clock.tick(10);
expect(screenShareElement.style.height).eql("100%");
});
});
describe("orientationchange", function() {
@ -303,6 +318,19 @@ describe("loop.shared.mixins", function() {
expect(remoteElement.style.height).eql("100%");
});
it("should update the height on the screen share stream element", function() {
screenShareElement = {
offsetWidth: 100,
offsetHeight: 100,
style: { height: "0%" }
};
rootObject.events.orientationchange();
sandbox.clock.tick(10);
expect(screenShareElement.style.height).eql("100%");
});
});

View File

@ -12,13 +12,15 @@ describe("loop.OTSdkDriver", function () {
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
var sandbox;
var dispatcher, driver, publisher, sdk, session, sessionData;
var fakeLocalElement, fakeRemoteElement, publisherConfig, fakeEvent;
var fakeLocalElement, fakeRemoteElement, fakeScreenElement;
var publisherConfig, fakeEvent;
beforeEach(function() {
sandbox = sinon.sandbox.create();
fakeLocalElement = {fake: 1};
fakeRemoteElement = {fake: 2};
fakeScreenElement = {fake: 3};
fakeEvent = {
preventDefault: sinon.stub()
};
@ -290,6 +292,7 @@ describe("loop.OTSdkDriver", function () {
dispatcher.dispatch(new sharedActions.SetupStreamElements({
getLocalElementFunc: function() {return fakeLocalElement;},
getScreenShareElementFunc: function() {return fakeScreenElement;},
getRemoteElementFunc: function() {return fakeRemoteElement;},
publisherConfig: publisherConfig
}));
@ -353,16 +356,50 @@ describe("loop.OTSdkDriver", function () {
});
});
describe("streamCreated", function() {
describe("streamCreated (publisher/local)", function() {
it("should dispatch a VideoDimensionsChanged action", function() {
var fakeStream = {
hasVideo: true,
videoType: "camera",
videoDimensions: {width: 1, height: 2}
};
publisher.trigger("streamCreated", {stream: fakeStream});
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.VideoDimensionsChanged({
isLocal: true,
videoType: "camera",
dimensions: {width: 1, height: 2}
}));
});
});
describe("streamCreated (session/remote)", function() {
var fakeStream;
beforeEach(function() {
fakeStream = {
fakeStream: 3
hasVideo: true,
videoType: "camera",
videoDimensions: {width: 1, height: 2}
};
});
it("should subscribe to the stream", function() {
it("should dispatch a VideoDimensionsChanged action", function() {
session.trigger("streamCreated", {stream: fakeStream});
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.VideoDimensionsChanged({
isLocal: false,
videoType: "camera",
dimensions: {width: 1, height: 2}
}));
});
it("should subscribe to a camera stream", function() {
session.trigger("streamCreated", {stream: fakeStream});
sinon.assert.calledOnce(session.subscribe);
@ -370,15 +407,85 @@ describe("loop.OTSdkDriver", function () {
fakeStream, fakeRemoteElement, publisherConfig);
});
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, fakeScreenElement, publisherConfig);
});
it("should dispach a mediaConnected action if both streams are up", function() {
driver._publishedLocalStream = true;
session.trigger("streamCreated", {stream: fakeStream});
sinon.assert.calledOnce(dispatcher.dispatch);
// Called twice due to the VideoDimensionsChanged above.
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "mediaConnected"));
});
it("should not dispatch a mediaConnected action for screen sharing streams",
function() {
driver._publishedLocalStream = true;
fakeStream.videoType = "screen";
session.trigger("streamCreated", {stream: fakeStream});
sinon.assert.neverCalledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "mediaConnected"));
});
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() {
fakeStream.videoType = "screen";
session.trigger("streamCreated", {stream: fakeStream});
// Called twice due to the VideoDimensionsChanged above.
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
new sharedActions.ReceivingScreenShare({receiving: true}));
});
});
describe("streamDestroyed", function() {
var fakeStream;
beforeEach(function() {
fakeStream = {
videoType: "screen"
};
});
it("should dispatch a ReceivingScreenShare action", function() {
session.trigger("streamDestroyed", {stream: fakeStream});
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ReceivingScreenShare({
receiving: false
}));
});
it("should not dispatch an action if the videoType is camera", function() {
fakeStream.videoType = "camera";
session.trigger("streamDestroyed", {stream: fakeStream});
sinon.assert.notCalled(dispatcher.dispatch);
});
});
describe("streamPropertyChanged", function() {
@ -415,8 +522,8 @@ describe("loop.OTSdkDriver", function () {
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "videoDimensionsChanged"))
})
sinon.match.hasOwn("name", "videoDimensionsChanged"));
});
});
describe("connectionCreated", function() {