Bug 1122032 Part 1 - Setup minimal screen sharing for Loop from desktop (disabled by default). r=mikedeboer

This commit is contained in:
Mark Banner 2015-02-02 21:53:19 +00:00
parent 7cfa8db02a
commit bd23ef68d0
28 changed files with 566 additions and 46 deletions

View File

@ -1653,6 +1653,7 @@ pref("shumway.disabled", true);
pref("image.mem.max_decoded_image_kb", 256000);
pref("loop.enabled", true);
pref("loop.screenshare.enabled", false);
pref("loop.server", "https://loop.services.mozilla.com/v0");
pref("loop.seenToS", "unseen");
pref("loop.showPartnerLogo", true);

View File

@ -28,7 +28,6 @@
<script type="text/javascript" src="loop/shared/js/utils.js"></script>
<script type="text/javascript" src="loop/shared/js/models.js"></script>
<script type="text/javascript" src="loop/shared/js/mixins.js"></script>
<script type="text/javascript" src="loop/shared/js/views.js"></script>
<script type="text/javascript" src="loop/shared/js/feedbackApiClient.js"></script>
<script type="text/javascript" src="loop/shared/js/actions.js"></script>
<script type="text/javascript" src="loop/shared/js/validate.js"></script>
@ -41,6 +40,7 @@
<script type="text/javascript" src="loop/shared/js/fxOSActiveRoomStore.js"></script>
<script type="text/javascript" src="loop/shared/js/activeRoomStore.js"></script>
<script type="text/javascript" src="loop/shared/js/feedbackStore.js"></script>
<script type="text/javascript" src="loop/shared/js/views.js"></script>
<script type="text/javascript" src="loop/shared/js/feedbackViews.js"></script>
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
<script type="text/javascript" src="loop/shared/js/websocket.js"></script>

View File

@ -42,7 +42,8 @@ loop.conversation = (function(mozL10n) {
conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore)
.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore),
mozLoop: React.PropTypes.object.isRequired,
},
getInitialState: function() {
@ -78,6 +79,7 @@ loop.conversation = (function(mozL10n) {
case "room": {
return (React.createElement(DesktopRoomConversationView, {
dispatcher: this.props.dispatcher,
mozLoop: this.props.mozLoop,
roomStore: this.props.roomStore}
));
}
@ -189,7 +191,8 @@ loop.conversation = (function(mozL10n) {
client: client,
conversation: conversation,
dispatcher: dispatcher,
sdk: window.OT}
sdk: window.OT,
mozLoop: navigator.mozLoop}
), document.querySelector('#main'));
dispatcher.dispatch(new sharedActions.GetWindowData({

View File

@ -42,7 +42,8 @@ loop.conversation = (function(mozL10n) {
conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore)
.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore),
mozLoop: React.PropTypes.object.isRequired,
},
getInitialState: function() {
@ -78,6 +79,7 @@ loop.conversation = (function(mozL10n) {
case "room": {
return (<DesktopRoomConversationView
dispatcher={this.props.dispatcher}
mozLoop={this.props.mozLoop}
roomStore={this.props.roomStore}
/>);
}
@ -190,6 +192,7 @@ loop.conversation = (function(mozL10n) {
conversation={conversation}
dispatcher={dispatcher}
sdk={window.OT}
mozLoop={navigator.mozLoop}
/>, document.querySelector('#main'));
dispatcher.dispatch(new sharedActions.GetWindowData({

View File

@ -14,6 +14,7 @@ loop.roomViews = (function(mozL10n) {
var sharedActions = loop.shared.actions;
var sharedMixins = loop.shared.mixins;
var ROOM_STATES = loop.store.ROOM_STATES;
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
var sharedViews = loop.shared.views;
/**
@ -169,7 +170,8 @@ loop.roomViews = (function(mozL10n) {
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
mozLoop: React.PropTypes.object.isRequired,
},
_renderInvitationOverlay: function() {
@ -193,6 +195,7 @@ loop.roomViews = (function(mozL10n) {
publishVideo: !this.state.videoMuted
}),
getLocalElementFunc: this._getElement.bind(this, ".local"),
getScreenShareElementFunc: this._getElement.bind(this, ".screen"),
getRemoteElementFunc: this._getElement.bind(this, ".remote")
}));
}
@ -238,6 +241,11 @@ loop.roomViews = (function(mozL10n) {
"room-preview": this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS
});
var screenShareData = {
state: this.state.screenSharingState,
visible: this.props.mozLoop.getLoopPref("screenshare.enabled")
};
switch(this.state.roomState) {
case ROOM_STATES.FAILED:
case ROOM_STATES.FULL: {
@ -268,13 +276,16 @@ loop.roomViews = (function(mozL10n) {
React.createElement("div", {className: "video_wrapper remote_wrapper"},
React.createElement("div", {className: "video_inner remote"})
),
React.createElement("div", {className: localStreamClasses})
React.createElement("div", {className: localStreamClasses}),
React.createElement("div", {className: "screen hide"})
),
React.createElement(sharedViews.ConversationToolbar, {
dispatcher: this.props.dispatcher,
video: {enabled: !this.state.videoMuted, visible: true},
audio: {enabled: !this.state.audioMuted, visible: true},
publishStream: this.publishStream,
hangup: this.leaveRoom})
hangup: this.leaveRoom,
screenShare: screenShareData})
)
)
)

View File

@ -14,6 +14,7 @@ loop.roomViews = (function(mozL10n) {
var sharedActions = loop.shared.actions;
var sharedMixins = loop.shared.mixins;
var ROOM_STATES = loop.store.ROOM_STATES;
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
var sharedViews = loop.shared.views;
/**
@ -169,7 +170,8 @@ loop.roomViews = (function(mozL10n) {
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
mozLoop: React.PropTypes.object.isRequired,
},
_renderInvitationOverlay: function() {
@ -193,6 +195,7 @@ loop.roomViews = (function(mozL10n) {
publishVideo: !this.state.videoMuted
}),
getLocalElementFunc: this._getElement.bind(this, ".local"),
getScreenShareElementFunc: this._getElement.bind(this, ".screen"),
getRemoteElementFunc: this._getElement.bind(this, ".remote")
}));
}
@ -238,6 +241,11 @@ loop.roomViews = (function(mozL10n) {
"room-preview": this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS
});
var screenShareData = {
state: this.state.screenSharingState,
visible: this.props.mozLoop.getLoopPref("screenshare.enabled")
};
switch(this.state.roomState) {
case ROOM_STATES.FAILED:
case ROOM_STATES.FULL: {
@ -269,12 +277,15 @@ loop.roomViews = (function(mozL10n) {
<div className="video_inner remote"></div>
</div>
<div className={localStreamClasses}></div>
<div className="screen hide"></div>
</div>
<sharedViews.ConversationToolbar
dispatcher={this.props.dispatcher}
video={{enabled: !this.state.videoMuted, visible: true}}
audio={{enabled: !this.state.audioMuted, visible: true}}
publishStream={this.publishStream}
hangup={this.leaveRoom} />
hangup={this.leaveRoom}
screenShare={screenShareData} />
</div>
</div>
</div>

View File

@ -43,6 +43,11 @@
margin-right: 16px;
}
.btn-screen-share-entry {
float: right !important;
border-left: 1px solid #5a5a5a;
}
.conversation-toolbar-btn-box {
border-right: 1px solid #5a5a5a;
}
@ -147,11 +152,11 @@
}
/* Common media control buttons behavior */
.conversation-toolbar .media-control {
.conversation-toolbar .transparent-button {
background-color: transparent;
opacity: 1;
}
.conversation-toolbar .media-control:hover {
.conversation-toolbar .transparent-button:hover {
background-color: rgba(255,255,255,.35);
opacity: 1;
}
@ -192,6 +197,21 @@
}
}
/* Screen share button */
.btn-screen-share {
/* XXX Replace this with the real button: bug 1126286 */
background-image: url(../img/video-inverse-14x14.png);
}
.btn-screen-share.active {
background-color: #6CB23E;
opacity: 1;
}
.btn-screen-share.disabled {
/* XXX Add css here for disabled state: bug 1126286 */
}
.fx-embedded .remote_wrapper {
position: absolute;
top: 0px;

View File

@ -160,6 +160,9 @@ loop.shared.actions = (function() {
publisherConfig: Object,
// The local stream element
getLocalElementFunc: Function,
// The screen share element; optional until all conversation
// types support it.
// getScreenShareElementFunc: Function,
// The remote stream element
getRemoteElementFunc: Function
}),
@ -195,6 +198,26 @@ loop.shared.actions = (function() {
enabled: Boolean
}),
/**
* Used to start a screen share.
*/
StartScreenShare: Action.define("startScreenShare", {
}),
/**
* Used to end a screen share.
*/
EndScreenShare: Action.define("endScreenShare", {
}),
/**
* Used to notifiy that screen sharing is active or not.
*/
ScreenSharingState: Action.define("screenSharingState", {
// One of loop.shared.utils.SCREEN_SHARE_STATES.
state: String
}),
/**
* Creates a new room.
* XXX: should move to some roomActions module - refs bug 1079284

View File

@ -12,12 +12,14 @@ loop.store.ActiveRoomStore = (function() {
var sharedActions = loop.shared.actions;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
// Error numbers taken from
// https://github.com/mozilla-services/loop-server/blob/master/loop/errno.json
var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
var ROOM_STATES = loop.store.ROOM_STATES;
/**
* Active room store.
*
@ -70,7 +72,8 @@ loop.store.ActiveRoomStore = (function() {
// anyone is not considered as 'used'
used: false,
localVideoDimensions: {},
remoteVideoDimensions: {}
remoteVideoDimensions: {},
screenSharingState: SCREEN_SHARE_STATES.INACTIVE
};
},
@ -117,6 +120,7 @@ loop.store.ActiveRoomStore = (function() {
"connectedToSdkServers",
"connectionFailure",
"setMute",
"screenSharingState",
"remotePeerDisconnected",
"remotePeerConnected",
"windowUnload",
@ -369,6 +373,13 @@ loop.store.ActiveRoomStore = (function() {
this.setStoreState(muteState);
},
/**
* Used to note the current screensharing state.
*/
screenSharingState: function(actionData) {
this.setStoreState({screenSharingState: actionData.state});
},
/**
* Handles recording when a remote peer has connected to the servers.
*/

View File

@ -10,6 +10,7 @@ loop.OTSdkDriver = (function() {
var sharedActions = loop.shared.actions;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var STREAM_PROPERTIES = loop.shared.utils.STREAM_PROPERTIES;
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
/**
* This is a wrapper for the OT sdk. It is used to translate the SDK events into
@ -30,11 +31,21 @@ loop.OTSdkDriver = (function() {
this.dispatcher.register(this, [
"setupStreamElements",
"setMute"
"setMute",
"startScreenShare",
"endScreenShare"
]);
};
OTSdkDriver.prototype = {
/**
* Clones the publisher config into a new object, as the sdk modifies the
* properties object.
*/
_getCopyPublisherConfig: function() {
return _.extend({}, this.publisherConfig);
},
/**
* Handles the setupStreamElements action. Saves the required data and
* kicks off the initialising of the publisher.
@ -44,6 +55,7 @@ loop.OTSdkDriver = (function() {
*/
setupStreamElements: function(actionData) {
this.getLocalElement = actionData.getLocalElementFunc;
this.getScreenShareElementFunc = actionData.getScreenShareElementFunc;
this.getRemoteElement = actionData.getRemoteElementFunc;
this.publisherConfig = actionData.publisherConfig;
@ -51,7 +63,7 @@ loop.OTSdkDriver = (function() {
// the initial connect of the session. This saves time when setting up
// the media.
this.publisher = this.sdk.initPublisher(this.getLocalElement(),
this.publisherConfig);
this._getCopyPublisherConfig());
this.publisher.on("streamCreated", this._onLocalStreamCreated.bind(this));
this.publisher.on("accessAllowed", this._onPublishComplete.bind(this));
this.publisher.on("accessDenied", this._onPublishDenied.bind(this));
@ -74,6 +86,41 @@ loop.OTSdkDriver = (function() {
}
},
/**
* Initiates a screen sharing publisher.
*/
startScreenShare: function() {
this.dispatcher.dispatch(new sharedActions.ScreenSharingState({
state: SCREEN_SHARE_STATES.PENDING
}));
var config = this._getCopyPublisherConfig();
// This is temporary until we get a sharing type selector
config.videoSource = "window";
this.screenshare = this.sdk.initPublisher(this.getScreenShareElementFunc(),
config);
this.screenshare.on("accessAllowed", this._onScreenShareGranted.bind(this));
this.screenshare.on("accessDenied", this._onScreenShareDenied.bind(this));
},
/**
* Ends an active screenshare session.
*/
endScreenShare: function() {
if (!this.screenshare) {
return;
}
this.session.unpublish(this.screenshare);
this.screenshare.off("accessAllowed accessDenied");
this.screenshare.destroy();
delete this.screenshare;
this.dispatcher.dispatch(new sharedActions.ScreenSharingState({
state: SCREEN_SHARE_STATES.INACTIVE
}));
},
/**
* Connects a session for the SDK, listening to the required events.
*
@ -104,6 +151,8 @@ loop.OTSdkDriver = (function() {
* Disconnects the sdk session.
*/
disconnectSession: function() {
this.endScreenShare();
if (this.session) {
this.session.off("streamCreated streamDestroyed connectionDestroyed " +
"sessionDisconnected streamPropertyChanged");
@ -246,8 +295,16 @@ loop.OTSdkDriver = (function() {
}));
}
var remoteElement;
if (event.stream.videoType === "screen") {
// XXX Implement in part 2.
remoteElement = "null";
} else {
remoteElement = this.getRemoteElement();
}
this.session.subscribe(event.stream,
this.getRemoteElement(), this.publisherConfig);
remoteElement, this._getCopyPublisherConfig());
this._subscribedRemoteStream = true;
if (this._checkAllStreamsConnected()) {
@ -347,6 +404,25 @@ loop.OTSdkDriver = (function() {
_checkAllStreamsConnected: function() {
return this._publishedLocalStream &&
this._subscribedRemoteStream;
},
/**
* Called when a screenshare is complete, publishes it to the session.
*/
_onScreenShareGranted: function() {
this.session.publish(this.screenshare);
this.dispatcher.dispatch(new sharedActions.ScreenSharingState({
state: SCREEN_SHARE_STATES.ACTIVE
}));
},
/**
* Called when a screenshare is denied. Notifies the other stores.
*/
_onScreenShareDenied: function() {
this.dispatcher.dispatch(new sharedActions.ScreenSharingState({
state: SCREEN_SHARE_STATES.INACTIVE
}));
}
};

View File

@ -48,6 +48,13 @@ loop.shared.utils = (function(mozL10n) {
HAS_VIDEO: "hasVideo"
};
var SCREEN_SHARE_STATES = {
INACTIVE: "ss-inactive",
// Pending is when the user is being prompted, aka gUM in progress.
PENDING: "ss-pending",
ACTIVE: "ss-active"
};
/**
* Format a given date into an l10n-friendly string.
*
@ -145,6 +152,7 @@ loop.shared.utils = (function(mozL10n) {
REST_ERRNOS: REST_ERRNOS,
WEBSOCKET_REASONS: WEBSOCKET_REASONS,
STREAM_PROPERTIES: STREAM_PROPERTIES,
SCREEN_SHARE_STATES: SCREEN_SHARE_STATES,
Helper: Helper,
composeCallUrlEmail: composeCallUrlEmail,
formatDate: formatDate,

View File

@ -8,11 +8,13 @@
/* global loop:true, React */
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.views = (function(_, OT, l10n) {
loop.shared.views = (function(_, l10n) {
"use strict";
var sharedActions = loop.shared.actions;
var sharedModels = loop.shared.models;
var sharedMixins = loop.shared.mixins;
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
/**
* Media control button.
@ -46,6 +48,7 @@ loop.shared.views = (function(_, OT, l10n) {
var classesObj = {
"btn": true,
"media-control": true,
"transparent-button": true,
"local-media": this.props.scope === "local",
"muted": !this.props.enabled,
"hide": !this.props.visible
@ -72,6 +75,60 @@ loop.shared.views = (function(_, OT, l10n) {
}
});
/**
* Screen sharing control button.
*
* Required props:
* - {loop.Dispatcher} dispatcher The dispatcher instance
* - {Boolean} visible Set to true to display the button
* - {String} state One of the screen sharing states, see
* loop.shared.utils.SCREEN_SHARE_STATES
*/
var ScreenShareControlButton = React.createClass({displayName: "ScreenShareControlButton",
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
visible: React.PropTypes.bool.isRequired,
state: React.PropTypes.string.isRequired,
},
handleClick: function() {
if (this.props.state === SCREEN_SHARE_STATES.ACTIVE) {
this.props.dispatcher.dispatch(
new sharedActions.EndScreenShare({}));
} else {
this.props.dispatcher.dispatch(
new sharedActions.StartScreenShare({}));
}
},
_getTitle: function() {
var prefix = this.props.state === SCREEN_SHARE_STATES.ACTIVE ?
"active" : "inactive";
return l10n.get(prefix + "_screenshare_button_title");
},
render: function() {
if (!this.props.visible) {
return null;
}
var screenShareClasses = React.addons.classSet({
"btn": true,
"btn-screen-share": true,
"transparent-button": true,
"active": this.props.state === SCREEN_SHARE_STATES.ACTIVE,
"disabled": this.props.state === SCREEN_SHARE_STATES.PENDING
});
return (
React.createElement("button", {className: screenShareClasses,
onClick: this.handleClick,
title: this._getTitle()})
);
}
});
/**
* Conversation controls.
*/
@ -80,13 +137,16 @@ loop.shared.views = (function(_, OT, l10n) {
return {
video: {enabled: true, visible: true},
audio: {enabled: true, visible: true},
screenShare: {state: SCREEN_SHARE_STATES.INACTIVE, visible: false},
enableHangup: true
};
},
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
video: React.PropTypes.object.isRequired,
audio: React.PropTypes.object.isRequired,
screenShare: React.PropTypes.object,
hangup: React.PropTypes.func.isRequired,
publishStream: React.PropTypes.func.isRequired,
hangupButtonLabel: React.PropTypes.string,
@ -110,7 +170,6 @@ loop.shared.views = (function(_, OT, l10n) {
},
render: function() {
var cx = React.addons.classSet;
return (
React.createElement("ul", {className: "conversation-toolbar"},
React.createElement("li", {className: "conversation-toolbar-btn-box btn-hangup-entry"},
@ -131,6 +190,11 @@ loop.shared.views = (function(_, OT, l10n) {
enabled: this.props.audio.enabled,
visible: this.props.audio.visible,
scope: "local", type: "audio"})
),
React.createElement("li", {className: "conversation-toolbar-btn-box btn-screen-share-entry"},
React.createElement(ScreenShareControlButton, {dispatcher: this.props.dispatcher,
visible: this.props.screenShare.visible,
state: this.props.screenShare.state})
)
)
);
@ -461,6 +525,7 @@ loop.shared.views = (function(_, OT, l10n) {
ConversationView: ConversationView,
ConversationToolbar: ConversationToolbar,
MediaControlButton: MediaControlButton,
ScreenShareControlButton: ScreenShareControlButton,
NotificationListView: NotificationListView
};
})(_, window.OT, navigator.mozL10n || document.mozL10n);
})(_, navigator.mozL10n || document.mozL10n);

View File

@ -8,11 +8,13 @@
/* global loop:true, React */
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.views = (function(_, OT, l10n) {
loop.shared.views = (function(_, l10n) {
"use strict";
var sharedActions = loop.shared.actions;
var sharedModels = loop.shared.models;
var sharedMixins = loop.shared.mixins;
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
/**
* Media control button.
@ -46,6 +48,7 @@ loop.shared.views = (function(_, OT, l10n) {
var classesObj = {
"btn": true,
"media-control": true,
"transparent-button": true,
"local-media": this.props.scope === "local",
"muted": !this.props.enabled,
"hide": !this.props.visible
@ -72,6 +75,60 @@ loop.shared.views = (function(_, OT, l10n) {
}
});
/**
* Screen sharing control button.
*
* Required props:
* - {loop.Dispatcher} dispatcher The dispatcher instance
* - {Boolean} visible Set to true to display the button
* - {String} state One of the screen sharing states, see
* loop.shared.utils.SCREEN_SHARE_STATES
*/
var ScreenShareControlButton = React.createClass({
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
visible: React.PropTypes.bool.isRequired,
state: React.PropTypes.string.isRequired,
},
handleClick: function() {
if (this.props.state === SCREEN_SHARE_STATES.ACTIVE) {
this.props.dispatcher.dispatch(
new sharedActions.EndScreenShare({}));
} else {
this.props.dispatcher.dispatch(
new sharedActions.StartScreenShare({}));
}
},
_getTitle: function() {
var prefix = this.props.state === SCREEN_SHARE_STATES.ACTIVE ?
"active" : "inactive";
return l10n.get(prefix + "_screenshare_button_title");
},
render: function() {
if (!this.props.visible) {
return null;
}
var screenShareClasses = React.addons.classSet({
"btn": true,
"btn-screen-share": true,
"transparent-button": true,
"active": this.props.state === SCREEN_SHARE_STATES.ACTIVE,
"disabled": this.props.state === SCREEN_SHARE_STATES.PENDING
});
return (
<button className={screenShareClasses}
onClick={this.handleClick}
title={this._getTitle()}></button>
);
}
});
/**
* Conversation controls.
*/
@ -80,13 +137,16 @@ loop.shared.views = (function(_, OT, l10n) {
return {
video: {enabled: true, visible: true},
audio: {enabled: true, visible: true},
screenShare: {state: SCREEN_SHARE_STATES.INACTIVE, visible: false},
enableHangup: true
};
},
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
video: React.PropTypes.object.isRequired,
audio: React.PropTypes.object.isRequired,
screenShare: React.PropTypes.object,
hangup: React.PropTypes.func.isRequired,
publishStream: React.PropTypes.func.isRequired,
hangupButtonLabel: React.PropTypes.string,
@ -110,7 +170,6 @@ loop.shared.views = (function(_, OT, l10n) {
},
render: function() {
var cx = React.addons.classSet;
return (
<ul className="conversation-toolbar">
<li className="conversation-toolbar-btn-box btn-hangup-entry">
@ -132,6 +191,11 @@ loop.shared.views = (function(_, OT, l10n) {
visible={this.props.audio.visible}
scope="local" type="audio" />
</li>
<li className="conversation-toolbar-btn-box btn-screen-share-entry">
<ScreenShareControlButton dispatcher={this.props.dispatcher}
visible={this.props.screenShare.visible}
state={this.props.screenShare.state} />
</li>
</ul>
);
}
@ -461,6 +525,7 @@ loop.shared.views = (function(_, OT, l10n) {
ConversationView: ConversationView,
ConversationToolbar: ConversationToolbar,
MediaControlButton: MediaControlButton,
ScreenShareControlButton: ScreenShareControlButton,
NotificationListView: NotificationListView
};
})(_, window.OT, navigator.mozL10n || document.mozL10n);
})(_, navigator.mozL10n || document.mozL10n);

View File

@ -101,7 +101,6 @@
<script type="text/javascript" src="shared/js/utils.js"></script>
<script type="text/javascript" src="shared/js/models.js"></script>
<script type="text/javascript" src="shared/js/mixins.js"></script>
<script type="text/javascript" src="shared/js/views.js"></script>
<script type="text/javascript" src="shared/js/feedbackApiClient.js"></script>
<script type="text/javascript" src="shared/js/actions.js"></script>
<script type="text/javascript" src="shared/js/validate.js"></script>
@ -113,6 +112,7 @@
<script type="text/javascript" src="shared/js/fxOSActiveRoomStore.js"></script>
<script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
<script type="text/javascript" src="shared/js/feedbackStore.js"></script>
<script type="text/javascript" src="shared/js/views.js"></script>
<script type="text/javascript" src="shared/js/feedbackViews.js"></script>
<script type="text/javascript" src="js/standaloneAppStore.js"></script>
<script type="text/javascript" src="js/standaloneClient.js"></script>

View File

@ -17,6 +17,9 @@ mute_local_audio_button_title=Mute your audio
unmute_local_audio_button_title=Unmute your audio
mute_local_video_button_title=Mute your video
unmute_local_video_button_title=Unmute your video
active_screenshare_button_title=Stop sharing
inactive_screenshare_button_title=Share your screen
outgoing_call_title=Start conversation?
call_with_contact_title=Conversation with {{incomingCallIdentity}}
welcome=Welcome to the {{clientShortname}} web client.

View File

@ -141,7 +141,8 @@ describe("loop.conversation", function() {
sdk: {},
conversationStore: conversationStore,
conversationAppStore: conversationAppStore,
dispatcher: dispatcher
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
}));
}

View File

@ -43,7 +43,6 @@
<script src="../../content/shared/js/feedbackApiClient.js"></script>
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/validate.js"></script>
@ -56,6 +55,7 @@
<script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
<script src="../../content/shared/js/activeRoomStore.js"></script>
<script src="../../content/shared/js/feedbackStore.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/feedbackViews.js"></script>
<script src="../../content/js/client.js"></script>
<script src="../../content/js/conversationAppStore.js"></script>

View File

@ -6,20 +6,25 @@ describe("loop.roomViews", function () {
"use strict";
var ROOM_STATES = loop.store.ROOM_STATES;
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
var sandbox, dispatcher, roomStore, activeRoomStore, fakeWindow;
var fakeMozLoop;
beforeEach(function() {
sandbox = sinon.sandbox.create();
dispatcher = new loop.Dispatcher();
fakeMozLoop = {
getAudioBlob: sinon.stub(),
getLoopPref: sinon.stub()
};
fakeWindow = {
document: {},
navigator: {
mozLoop: {
getAudioBlob: sinon.stub()
}
mozLoop: fakeMozLoop
},
addEventListener: function() {},
removeEventListener: function() {}
@ -62,16 +67,10 @@ describe("loop.roomViews", function () {
roomStore: roomStore
}));
expect(testView.state).eql({
roomState: ROOM_STATES.INIT,
audioMuted: false,
videoMuted: false,
failureReason: undefined,
used: false,
foo: "bar",
localVideoDimensions: {},
remoteVideoDimensions: {}
});
var expectedState = _.extend({foo: "bar"},
activeRoomStore.getInitialStoreState());
expect(testView.state).eql(expectedState);
});
it("should listen to store changes", function() {
@ -216,7 +215,8 @@ describe("loop.roomViews", function () {
return TestUtils.renderIntoDocument(
React.createElement(loop.roomViews.DesktopRoomConversationView, {
dispatcher: dispatcher,
roomStore: roomStore
roomStore: roomStore,
mozLoop: fakeMozLoop
}));
}
@ -276,6 +276,20 @@ describe("loop.roomViews", function () {
expect(muteBtn.classList.contains("muted")).eql(true);
});
it("should dispatch a `StartScreenShare` action when sharing is not active " +
"and the screen share button is pressed", function() {
view = mountTestComponent();
view.setState({screenSharingState: SCREEN_SHARE_STATES.INACTIVE});
var muteBtn = view.getDOMNode().querySelector('.btn-mute-video');
React.addons.TestUtils.Simulate.click(muteBtn);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "setMute"));
});
describe("#componentWillUpdate", function() {
function expectActionDispatched(view) {
sinon.assert.calledOnce(dispatcher.dispatch);

View File

@ -9,6 +9,7 @@ describe("loop.store.ActiveRoomStore", function () {
var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
var ROOM_STATES = loop.store.ROOM_STATES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
var sandbox, dispatcher, store, fakeMozLoop, fakeSdkDriver;
var fakeMultiplexGum;
@ -646,6 +647,16 @@ describe("loop.store.ActiveRoomStore", function () {
});
});
describe("#screenSharingState", function() {
it("should save the state", function() {
store.screenSharingState(new sharedActions.ScreenSharingState({
state: SCREEN_SHARE_STATES.ACTIVE
}));
expect(store.getStoreState().screenSharingState).eql(SCREEN_SHARE_STATES.ACTIVE);
});
});
describe("#remotePeerConnected", function() {
it("should set the state to `HAS_PARTICIPANTS`", function() {
store.remotePeerConnected();

View File

@ -42,7 +42,6 @@
<script src="../../content/shared/js/utils.js"></script>
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/feedbackApiClient.js"></script>
<script src="../../content/shared/js/validate.js"></script>
@ -56,6 +55,7 @@
<script src="../../content/shared/js/roomStore.js"></script>
<script src="../../content/shared/js/conversationStore.js"></script>
<script src="../../content/shared/js/feedbackStore.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/feedbackViews.js"></script>
<!-- Test scripts -->

View File

@ -9,6 +9,7 @@ describe("loop.OTSdkDriver", function () {
var sharedActions = loop.shared.actions;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var STREAM_PROPERTIES = loop.shared.utils.STREAM_PROPERTIES;
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
var sandbox;
var dispatcher, driver, publisher, sdk, session, sessionData;
var fakeLocalElement, fakeRemoteElement, publisherConfig, fakeEvent;
@ -35,6 +36,7 @@ describe("loop.OTSdkDriver", function () {
connect: sinon.stub(),
disconnect: sinon.stub(),
publish: sinon.stub(),
unpublish: sinon.stub(),
subscribe: sinon.stub(),
forceDisconnect: sinon.stub()
}, Backbone.Events);
@ -119,6 +121,74 @@ describe("loop.OTSdkDriver", function () {
});
});
describe("#startScreenShare", function() {
var fakeElement;
beforeEach(function() {
sandbox.stub(dispatcher, "dispatch");
fakeElement = {
className: "fakeVideo"
};
driver.getScreenShareElementFunc = function() {
return fakeElement;
};
});
it("should dispatch a `ScreenSharingState` action", function() {
driver.startScreenShare(new sharedActions.StartScreenShare());
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ScreenSharingState({
state: SCREEN_SHARE_STATES.PENDING
}));
});
it("should initialize a publisher", function() {
driver.startScreenShare(new sharedActions.StartScreenShare());
sinon.assert.calledOnce(sdk.initPublisher);
sinon.assert.calledWithMatch(sdk.initPublisher,
fakeElement, {videoSource: "window"});
});
});
describe("#endScreenShare", function() {
beforeEach(function() {
driver.getScreenShareElementFunc = function() {};
driver.startScreenShare(new sharedActions.StartScreenShare());
sandbox.stub(dispatcher, "dispatch");
driver.session = session;
});
it("should unpublish the share", function() {
driver.endScreenShare(new sharedActions.EndScreenShare());
sinon.assert.calledOnce(session.unpublish);
});
it("should destroy the share", function() {
driver.endScreenShare(new sharedActions.EndScreenShare());
sinon.assert.calledOnce(publisher.destroy);
});
it("should dispatch a `ScreenSharingState` action", function() {
driver.endScreenShare(new sharedActions.EndScreenShare());
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ScreenSharingState({
state: SCREEN_SHARE_STATES.INACTIVE
}));
});
});
describe("#connectSession", function() {
it("should initialise a new session", function() {
driver.connectSession(sessionData);
@ -214,7 +284,7 @@ describe("loop.OTSdkDriver", function () {
});
});
describe("Events", function() {
describe("Events (general media)", function() {
beforeEach(function() {
driver.connectSession(sessionData);
@ -427,4 +497,46 @@ describe("loop.OTSdkDriver", function () {
});
});
});
describe("Events (screenshare)", function() {
beforeEach(function() {
driver.connectSession(sessionData);
driver.getScreenShareElementFunc = function() {};
driver.startScreenShare(new sharedActions.StartScreenShare());
sandbox.stub(dispatcher, "dispatch");
});
describe("accessAllowed", function() {
it("should publish the stream", function() {
publisher.trigger("accessAllowed", fakeEvent);
sinon.assert.calledOnce(session.publish);
});
it("should dispatch a `ScreenSharingState` action", function() {
publisher.trigger("accessAllowed", fakeEvent);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ScreenSharingState({
state: SCREEN_SHARE_STATES.ACTIVE
}));
});
});
describe("accessDenied", function() {
it("should dispatch a `ScreenShareState` action", function() {
publisher.trigger("accessDenied", fakeEvent);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ScreenSharingState({
state: SCREEN_SHARE_STATES.INACTIVE
}));
});
});
});
});

View File

@ -12,10 +12,11 @@ var TestUtils = React.addons.TestUtils;
describe("loop.shared.views", function() {
"use strict";
var sharedModels = loop.shared.models,
sharedViews = loop.shared.views,
getReactElementByClass = TestUtils.findRenderedDOMComponentWithClass,
sandbox, fakeAudioXHR;
var sharedModels = loop.shared.models;
var sharedViews = loop.shared.views;
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
var getReactElementByClass = TestUtils.findRenderedDOMComponentWithClass;
var sandbox, fakeAudioXHR, dispatcher;
beforeEach(function() {
sandbox = sinon.sandbox.create();
@ -23,6 +24,10 @@ describe("loop.shared.views", function() {
sandbox.stub(l10n, "get", function(x) {
return "translated:" + x;
});
dispatcher = new loop.Dispatcher();
sandbox.stub(dispatcher, "dispatch");
fakeAudioXHR = {
open: sinon.spy(),
send: function() {},
@ -92,6 +97,76 @@ describe("loop.shared.views", function() {
});
});
describe("ScreenShareControlButton", function() {
it("should render a visible share button", function() {
var comp = TestUtils.renderIntoDocument(
React.createElement(sharedViews.ScreenShareControlButton, {
dispatcher: dispatcher,
visible: true,
state: SCREEN_SHARE_STATES.INACTIVE
}));
expect(comp.getDOMNode().classList.contains("active")).eql(false);
expect(comp.getDOMNode().classList.contains("disabled")).eql(false);
});
it("should render a disabled share button when share is pending", function() {
var comp = TestUtils.renderIntoDocument(
React.createElement(sharedViews.ScreenShareControlButton, {
dispatcher: dispatcher,
visible: true,
state: SCREEN_SHARE_STATES.PENDING
}));
expect(comp.getDOMNode().classList.contains("active")).eql(false);
expect(comp.getDOMNode().classList.contains("disabled")).eql(true);
});
it("should render an active share button", function() {
var comp = TestUtils.renderIntoDocument(
React.createElement(sharedViews.ScreenShareControlButton, {
dispatcher: dispatcher,
visible: true,
state: SCREEN_SHARE_STATES.ACTIVE
}));
expect(comp.getDOMNode().classList.contains("active")).eql(true);
expect(comp.getDOMNode().classList.contains("disabled")).eql(false);
});
it("should dispatch a StartScreenShare action on click when the state is not active",
function() {
var comp = TestUtils.renderIntoDocument(
React.createElement(sharedViews.ScreenShareControlButton, {
dispatcher: dispatcher,
visible: true,
state: SCREEN_SHARE_STATES.INACTIVE
}));
TestUtils.Simulate.click(comp.getDOMNode());
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.StartScreenShare({}));
});
it("should dispatch a EndScreenShare action on click when the state is active",
function() {
var comp = TestUtils.renderIntoDocument(
React.createElement(sharedViews.ScreenShareControlButton, {
dispatcher: dispatcher,
visible: true,
state: SCREEN_SHARE_STATES.ACTIVE
}));
TestUtils.Simulate.click(comp.getDOMNode());
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.EndScreenShare({}));
});
});
describe("ConversationToolbar", function() {
var hangup, publishStream;

View File

@ -41,7 +41,6 @@
<script src="../../content/shared/js/utils.js"></script>
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/feedbackApiClient.js"></script>
<script src="../../content/shared/js/actions.js"></script>
@ -52,6 +51,7 @@
<script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
<script src="../../content/shared/js/activeRoomStore.js"></script>
<script src="../../content/shared/js/feedbackStore.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/feedbackViews.js"></script>
<script src="../../content/shared/js/otSdkDriver.js"></script>
<script src="../../standalone/content/js/multiplexGum.js"></script>

View File

@ -54,6 +54,7 @@ navigator.mozLoop = {
switch(pref) {
// Ensure we skip FTE completely.
case "gettingStarted.seen":
case "screenshare.enabled":
return true;
}
},

View File

@ -37,7 +37,6 @@
<script src="../content/shared/js/utils.js"></script>
<script src="../content/shared/js/models.js"></script>
<script src="../content/shared/js/mixins.js"></script>
<script src="../content/shared/js/views.js"></script>
<script src="../content/shared/js/websocket.js"></script>
<script src="../content/shared/js/validate.js"></script>
<script src="../content/shared/js/dispatcher.js"></script>
@ -48,6 +47,7 @@
<script src="../content/shared/js/fxOSActiveRoomStore.js"></script>
<script src="../content/shared/js/activeRoomStore.js"></script>
<script src="../content/shared/js/feedbackStore.js"></script>
<script src="../content/shared/js/views.js"></script>
<script src="../content/shared/js/feedbackViews.js"></script>
<script src="../content/js/roomViews.js"></script>
<script src="../content/js/conversationViews.js"></script>

View File

@ -567,6 +567,7 @@
React.createElement(DesktopRoomConversationView, {
roomStore: roomStore,
dispatcher: dispatcher,
mozLoop: navigator.mozLoop,
roomState: ROOM_STATES.INIT})
)
),
@ -577,6 +578,7 @@
React.createElement(DesktopRoomConversationView, {
roomStore: roomStore,
dispatcher: dispatcher,
mozLoop: navigator.mozLoop,
roomState: ROOM_STATES.HAS_PARTICIPANTS})
)
)

View File

@ -567,6 +567,7 @@
<DesktopRoomConversationView
roomStore={roomStore}
dispatcher={dispatcher}
mozLoop={navigator.mozLoop}
roomState={ROOM_STATES.INIT} />
</div>
</Example>
@ -577,6 +578,7 @@
<DesktopRoomConversationView
roomStore={roomStore}
dispatcher={dispatcher}
mozLoop={navigator.mozLoop}
roomState={ROOM_STATES.HAS_PARTICIPANTS} />
</div>
</Example>

View File

@ -183,6 +183,8 @@ mute_local_audio_button_title=Mute your audio
unmute_local_audio_button_title=Unmute your audio
mute_local_video_button_title=Mute your video
unmute_local_video_button_title=Unmute your video
active_screenshare_button_title=Stop sharing
inactive_screenshare_button_title=Share your screen
## LOCALIZATION NOTE (call_with_contact_title): The title displayed
## when calling a contact. Don't translate the part between {{..}} because