/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ var loop = loop || {}; loop.store = loop.store || {}; loop.store.ROOM_STATES = { // The initial state of the room INIT: "room-init", // The store is gathering the room data GATHER: "room-gather", // The store has got the room data READY: "room-ready", // Obtaining media from the user MEDIA_WAIT: "room-media-wait", // Joining the room is taking place JOINING: "room-joining", // The room is known to be joined on the loop-server JOINED: "room-joined", // The room is connected to the sdk server. SESSION_CONNECTED: "room-session-connected", // There are participants in the room. HAS_PARTICIPANTS: "room-has-participants", // There was an issue with the room FAILED: "room-failed", // The room is full FULL: "room-full", // The room conversation has ended, displays the feedback view. ENDED: "room-ended", // The window is closing CLOSING: "room-closing" }; loop.store.ActiveRoomStore = (function() { "use strict"; var sharedActions = loop.shared.actions; var crypto = loop.crypto; 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; var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES; var OPTIONAL_ROOMINFO_FIELDS = { participants: "participants", roomContextUrls: "roomContextUrls", roomDescription: "roomDescription", roomInfoFailure: "roomInfoFailure", roomName: "roomName", roomState: "roomState", socialShareProviders: "socialShareProviders" }; /** * Active room store. * * @param {loop.Dispatcher} dispatcher The dispatcher for dispatching actions * and registering to consume actions. * @param {Object} options Options object: * - {mozLoop} mozLoop The MozLoop API object. * - {OTSdkDriver} sdkDriver The SDK driver instance. */ var ActiveRoomStore = loop.store.createStore({ /** * The time factor to adjust the expires time to ensure that we send a refresh * before the expiry. Currently set as 90%. */ expiresTimeFactor: 0.9, // XXX Further actions are registered in setupWindowData and // fetchServerData when we know what window type this is. At some stage, // we might want to consider store mixins or some alternative which // means the stores would only be created when we want them. actions: [ "setupWindowData", "fetchServerData" ], initialize: function(options) { if (!options.mozLoop) { throw new Error("Missing option mozLoop"); } this._mozLoop = options.mozLoop; if (!options.sdkDriver) { throw new Error("Missing option sdkDriver"); } this._sdkDriver = options.sdkDriver; this._isDesktop = options.isDesktop || false; }, /** * This is a list of states that need resetting when the room is left, * due to user choice, failure or other reason. It is a subset of * getInitialStoreState as some items (e.g. roomState, failureReason, * context information) can persist across room exit & re-entry. * * @type {Array} */ _statesToResetOnLeave: [ "audioMuted", "localSrcMediaElement", "localVideoDimensions", "mediaConnected", "receivingScreenShare", "remoteSrcMediaElement", "remoteVideoDimensions", "remoteVideoEnabled", "screenSharingState", "screenShareMediaElement", "videoMuted" ], /** * Returns initial state data for this active room. * * When adding states, consider if _statesToResetOnLeave needs updating * as well. */ getInitialStoreState: function() { return { roomState: ROOM_STATES.INIT, audioMuted: false, videoMuted: false, remoteVideoEnabled: false, failureReason: undefined, // Whether or not Firefox can handle this room in the conversation // window, rather than us handling it in the standalone. userAgentHandlesRoom: undefined, // Tracks if the room has been used during this // session. 'Used' means at least one call has been placed // with it. Entering and leaving the room without seeing // anyone is not considered as 'used' used: false, localVideoDimensions: {}, remoteVideoDimensions: {}, screenSharingState: SCREEN_SHARE_STATES.INACTIVE, receivingScreenShare: false, // Any urls (aka context) associated with the room. roomContextUrls: null, // The description for a room as stored in the context data. roomDescription: null, // Room information failed to be obtained for a reason. See ROOM_INFO_FAILURES. roomInfoFailure: null, // The name of the room. roomName: null, // Social API state. socialShareProviders: null, // True if media has been connected both-ways. mediaConnected: false }; }, /** * Handles a room failure. * * @param {sharedActions.RoomFailure} actionData */ roomFailure: function(actionData) { function getReason(serverCode) { switch (serverCode) { case REST_ERRNOS.INVALID_TOKEN: case REST_ERRNOS.EXPIRED: return FAILURE_DETAILS.EXPIRED_OR_INVALID; default: return FAILURE_DETAILS.UNKNOWN; } } console.error("Error in state `" + this._storeState.roomState + "`:", actionData.error); var exitState = this._storeState.roomState !== ROOM_STATES.FAILED ? this._storeState.roomState : this._storeState.failureExitState; this.setStoreState({ error: actionData.error, failureReason: getReason(actionData.error.errno), failureExitState: exitState }); this._leaveRoom(actionData.error.errno === REST_ERRNOS.ROOM_FULL ? ROOM_STATES.FULL : ROOM_STATES.FAILED, actionData.failedJoinRequest); }, /** * Attempts to retry getting the room data if there has been a room failure. */ retryAfterRoomFailure: function() { if (this._storeState.failureReason === FAILURE_DETAILS.EXPIRED_OR_INVALID) { console.error("Invalid retry attempt for expired or invalid url"); return; } switch (this._storeState.failureExitState) { case ROOM_STATES.GATHER: this.dispatchAction(new sharedActions.FetchServerData({ cryptoKey: this._storeState.roomCryptoKey, token: this._storeState.roomToken, windowType: "room" })); return; case ROOM_STATES.INIT: case ROOM_STATES.ENDED: case ROOM_STATES.CLOSING: console.error("Unexpected retry for exit state", this._storeState.failureExitState); return; default: // For all other states, we simply join the room. We avoid dispatching // another action here so that metrics doesn't get two notifications // in a row (one for retry, one for the join). this.joinRoom(); return; } }, /** * Registers the actions with the dispatcher that this store is interested * in after the initial setup has been performed. */ _registerPostSetupActions: function() { // Protect against calling this twice, as we don't want to register // before we know what type we are, but in some cases we need to re-do // an action (e.g. FetchServerData). if (this._registeredActions) { return; } this._registeredActions = true; this.dispatcher.register(this, [ "roomFailure", "retryAfterRoomFailure", "updateRoomInfo", "userAgentHandlesRoom", "gotMediaPermission", "joinRoom", "joinedRoom", "connectedToSdkServers", "connectionFailure", "setMute", "screenSharingState", "receivingScreenShare", "remotePeerDisconnected", "remotePeerConnected", "windowUnload", "leaveRoom", "feedbackComplete", "mediaStreamCreated", "mediaStreamDestroyed", "remoteVideoStatus", "videoDimensionsChanged", "startScreenShare", "endScreenShare", "updateSocialShareInfo", "connectionStatus", "mediaConnected" ]); this._onUpdateListener = this._handleRoomUpdate.bind(this); this._onDeleteListener = this._handleRoomDelete.bind(this); this._onSocialShareUpdate = this._handleSocialShareUpdate.bind(this); this._mozLoop.rooms.on("update:" + this._storeState.roomToken, this._onUpdateListener); this._mozLoop.rooms.on("delete:" + this._storeState.roomToken, this._onDeleteListener); window.addEventListener("LoopShareWidgetChanged", this._onSocialShareUpdate); window.addEventListener("LoopSocialProvidersChanged", this._onSocialShareUpdate); }, /** * Execute setupWindowData event action from the dispatcher. This gets * the room data from the mozLoop api, and dispatches an UpdateRoomInfo event. * It also dispatches JoinRoom as this action is only applicable to the desktop * client, and needs to auto-join. * * @param {sharedActions.SetupWindowData} actionData */ setupWindowData: function(actionData) { if (actionData.type !== "room") { // Nothing for us to do here, leave it to other stores. return; } this.setStoreState({ roomState: ROOM_STATES.GATHER, roomToken: actionData.roomToken, windowId: actionData.windowId }); this._registerPostSetupActions(); // Get the window data from the mozLoop api. this._mozLoop.rooms.get(actionData.roomToken, function(error, roomData) { if (error) { this.dispatchAction(new sharedActions.RoomFailure({ error: error, failedJoinRequest: false })); return; } this.dispatchAction(new sharedActions.UpdateRoomInfo({ participants: roomData.participants, roomContextUrls: roomData.decryptedContext.urls, roomDescription: roomData.decryptedContext.description, roomName: roomData.decryptedContext.roomName, roomState: ROOM_STATES.READY, roomUrl: roomData.roomUrl, socialShareProviders: this._mozLoop.getSocialShareProviders() })); // For the conversation window, we need to automatically // join the room. this.dispatchAction(new sharedActions.JoinRoom()); }.bind(this)); }, /** * Execute fetchServerData event action from the dispatcher. For rooms * we need to get the room context information from the server. We don't * need other data until the user decides to join the room. * This action is only used for the standalone UI. * * @param {sharedActions.FetchServerData} actionData * @return {Promise} For testing purposes, returns a promise that is resolved * once data is received from the server, and it is determined * if Firefox handles the room or not. */ fetchServerData: function(actionData) { if (actionData.windowType !== "room") { // Nothing for us to do here, leave it to other stores. return; } this.setStoreState({ roomState: ROOM_STATES.GATHER, roomToken: actionData.token, standalone: true }); this._registerPostSetupActions(); var dataPromise = this._getRoomDataForStandalone(actionData.cryptoKey); var userAgentHandlesPromise = this._promiseDetectUserAgentHandles(); return Promise.all([dataPromise, userAgentHandlesPromise]).then(function(results) { results.forEach(function(result) { this.dispatcher.dispatch(result); }.bind(this)); }.bind(this)); }, /** * Gets the room data for the standalone, decrypting it as necessary. * * @param {String} roomCryptoKey The crypto key associated to the room. * @return {Promise} A promise that is resolved once the get * and decryption is complete. */ _getRoomDataForStandalone: function(roomCryptoKey) { return new Promise(function(resolve, reject) { this._mozLoop.rooms.get(this._storeState.roomToken, function(err, result) { if (err) { resolve(new sharedActions.RoomFailure({ error: err, failedJoinRequest: false })); return; } var roomInfoData = new sharedActions.UpdateRoomInfo({ // If we've got this far, then we want to go to the ready state // regardless of success of failure. This is because failures of // crypto don't stop the user using the room, they just stop // us putting up the information. roomState: ROOM_STATES.READY, roomUrl: result.roomUrl }); if (!result.context && !result.roomName) { roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.NO_DATA; resolve(roomInfoData); return; } // This handles 'legacy', non-encrypted room names. if (result.roomName && !result.context) { roomInfoData.roomName = result.roomName; resolve(roomInfoData); return; } if (!crypto.isSupported()) { roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED; resolve(roomInfoData); return; } if (!roomCryptoKey) { roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.NO_CRYPTO_KEY; resolve(roomInfoData); return; } crypto.decryptBytes(roomCryptoKey, result.context.value) .then(function(decryptedResult) { var realResult = JSON.parse(decryptedResult); roomInfoData.roomDescription = realResult.description; roomInfoData.roomContextUrls = realResult.urls; roomInfoData.roomName = realResult.roomName; resolve(roomInfoData); }, function(error) { roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.DECRYPT_FAILED; resolve(roomInfoData); }); }.bind(this)); }.bind(this)); }, /** * If the user agent is Firefox, it sends a message to Firefox to see if * the room can be handled within Firefox rather than the standalone UI. * * @return {Promise} A promise that is resolved once it has been determined * if Firefox can handle the room. */ _promiseDetectUserAgentHandles: function() { return new Promise(function(resolve, reject) { function resolveWithNotHandlingResponse() { resolve(new sharedActions.UserAgentHandlesRoom({ handlesRoom: false })); } // If we're not Firefox, don't even try to see if it can be handled // in the browser. if (!loop.shared.utils.isFirefox(navigator.userAgent)) { resolveWithNotHandlingResponse(); return; } // Set up a timer in case older versions of Firefox don't give us a response. var timer = setTimeout(resolveWithNotHandlingResponse, 250); var webChannelListenerFunc; // Listen for the result. function webChannelListener(e) { if (e.detail.id !== "loop-link-clicker") { return; } // Stop the default response. clearTimeout(timer); // Remove the listener. window.removeEventListener("WebChannelMessageToContent", webChannelListenerFunc); // Resolve with the details of if we're able to handle or not. resolve(new sharedActions.UserAgentHandlesRoom({ handlesRoom: !!e.detail.message && e.detail.message.response })); } webChannelListenerFunc = webChannelListener.bind(this); window.addEventListener("WebChannelMessageToContent", webChannelListenerFunc); // Now send a message to the chrome to see if it can handle this room. window.dispatchEvent(new window.CustomEvent("WebChannelMessageToChrome", { detail: { id: "loop-link-clicker", message: { command: "checkWillOpenRoom", roomToken: this._storeState.roomToken } } })); }.bind(this)); }, /** * Handles the updateRoomInfo action. Updates the room data. * * @param {sharedActions.UpdateRoomInfo} actionData */ updateRoomInfo: function(actionData) { var newState = { roomUrl: actionData.roomUrl }; // Iterate over the optional fields that _may_ be present on the actionData // object. Object.keys(OPTIONAL_ROOMINFO_FIELDS).forEach(function(field) { if (actionData[field] !== undefined) { newState[OPTIONAL_ROOMINFO_FIELDS[field]] = actionData[field]; } }); this.setStoreState(newState); }, /** * Handles the userAgentHandlesRoom action. Updates the store's data with * the new state. * * @param {sharedActions.userAgentHandlesRoom} actionData */ userAgentHandlesRoom: function(actionData) { this.setStoreState({ userAgentHandlesRoom: actionData.handlesRoom }); }, /** * Handles the updateSocialShareInfo action. Updates the room data with new * Social API info. * * @param {sharedActions.UpdateSocialShareInfo} actionData */ updateSocialShareInfo: function(actionData) { this.setStoreState({ socialShareProviders: actionData.socialShareProviders }); }, /** * Handles room updates notified by the mozLoop rooms API. * * @param {String} eventName The name of the event * @param {Object} roomData The new roomData. */ _handleRoomUpdate: function(eventName, roomData) { this.dispatchAction(new sharedActions.UpdateRoomInfo({ urls: roomData.decryptedContext.urls, description: roomData.decryptedContext.description, participants: roomData.participants, roomName: roomData.decryptedContext.roomName, roomUrl: roomData.roomUrl })); }, /** * Handles the deletion of a room, notified by the mozLoop rooms API. * * @param {String} eventName The name of the event * @param {Object} roomData The roomData of the deleted room */ _handleRoomDelete: function(eventName, roomData) { this._sdkDriver.forceDisconnectAll(function() { window.close(); }); }, /** * Handles an update of the position of the Share widget and changes to list * of Social API providers, notified by the mozLoop API. */ _handleSocialShareUpdate: function() { this.dispatchAction(new sharedActions.UpdateSocialShareInfo({ socialShareProviders: this._mozLoop.getSocialShareProviders() })); }, /** * Checks that there are audio and video devices available, and joins the * room if there are. If there aren't then it will dispatch a ConnectionFailure * action with NO_MEDIA. */ _checkDevicesAndJoinRoom: function() { // XXX Ideally we'd do this check before joining a room, but we're waiting // for the UX for that. See bug 1166824. In the meantime this gives us // additional information for analysis. loop.shared.utils.hasAudioOrVideoDevices(function(hasDevices) { if (hasDevices) { // MEDIA_WAIT causes the views to dispatch sharedActions.SetupStreamElements, // which in turn starts the sdk obtaining the device permission. this.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT}); } else { this.dispatchAction(new sharedActions.ConnectionFailure({ reason: FAILURE_DETAILS.NO_MEDIA })); } }.bind(this)); }, /** * Hands off the room join to Firefox. */ _handoffRoomJoin: function() { var channelListener; function handleRoomJoinResponse(e) { if (e.detail.id !== "loop-link-clicker") { return; } window.removeEventListener("WebChannelMessageToContent", channelListener); if (!e.detail.message || !e.detail.message.response) { // XXX Firefox didn't handle this, even though it said it could // previously. We should add better user feedback here. console.error("Firefox didn't handle room it said it could."); } else { this.dispatcher.dispatch(new sharedActions.JoinedRoom({ apiKey: "", sessionToken: "", sessionId: "", expires: 0 })); } } channelListener = handleRoomJoinResponse.bind(this); window.addEventListener("WebChannelMessageToContent", channelListener); // Now we're set up, dispatch an event. window.dispatchEvent(new window.CustomEvent("WebChannelMessageToChrome", { detail: { id: "loop-link-clicker", message: { command: "openRoom", roomToken: this._storeState.roomToken } } })); }, /** * Handles the action to join to a room. */ joinRoom: function() { // Reset the failure reason if necessary. if (this.getStoreState().failureReason) { this.setStoreState({ failureReason: undefined }); } // If we're standalone and we know Firefox can handle the room, then hand // it off. if (this._storeState.standalone && this._storeState.userAgentHandlesRoom) { this.dispatcher.dispatch(new sharedActions.MetricsLogJoinRoom({ userAgentHandledRoom: true, ownRoom: true })); this._handoffRoomJoin(); return; } this.dispatcher.dispatch(new sharedActions.MetricsLogJoinRoom({ userAgentHandledRoom: false })); // Otherwise, we handle the room ourselves. this._checkDevicesAndJoinRoom(); }, /** * Handles the action that signifies when media permission has been * granted and starts joining the room. */ gotMediaPermission: function() { this.setStoreState({roomState: ROOM_STATES.JOINING}); this._mozLoop.rooms.join(this._storeState.roomToken, function(error, responseData) { if (error) { this.dispatchAction(new sharedActions.RoomFailure({ error: error, // This is an explicit flag to avoid the leave happening if join // fails. We can't track it on ROOM_STATES.JOINING as the user // might choose to leave the room whilst the XHR is in progress // which would then mean we'd run the race condition of not // notifying the server of a leave. failedJoinRequest: true })); return; } this.dispatchAction(new sharedActions.JoinedRoom({ apiKey: responseData.apiKey, sessionToken: responseData.sessionToken, sessionId: responseData.sessionId, expires: responseData.expires })); }.bind(this)); }, /** * Handles the data received from joining a room. It stores the relevant * data, and sets up the refresh timeout for ensuring membership of the room * is refreshed regularly. * * @param {sharedActions.JoinedRoom} actionData */ joinedRoom: function(actionData) { // If we're standalone and firefox is handling, then just store the new // state. No need to do anything else. if (this._storeState.standalone && this._storeState.userAgentHandlesRoom) { this.setStoreState({ roomState: ROOM_STATES.JOINED }); return; } this.setStoreState({ apiKey: actionData.apiKey, sessionToken: actionData.sessionToken, sessionId: actionData.sessionId, roomState: ROOM_STATES.JOINED }); this._setRefreshTimeout(actionData.expires); // Only send media telemetry on one side of the call: the desktop side. actionData.sendTwoWayMediaTelemetry = this._isDesktop; this._sdkDriver.connectSession(actionData); this._mozLoop.addConversationContext(this._storeState.windowId, actionData.sessionId, ""); }, /** * Handles recording when the sdk has connected to the servers. */ connectedToSdkServers: function() { this.setStoreState({ roomState: ROOM_STATES.SESSION_CONNECTED }); }, /** * Handles disconnection of this local client from the sdk servers. * * @param {sharedActions.ConnectionFailure} actionData */ connectionFailure: function(actionData) { var exitState = this._storeState.roomState === ROOM_STATES.FAILED ? this._storeState.failureExitState : this._storeState.roomState; // Treat all reasons as something failed. In theory, clientDisconnected // could be a success case, but there's no way we should be intentionally // sending that and still have the window open. this.setStoreState({ failureReason: actionData.reason, failureExitState: exitState }); this._leaveRoom(ROOM_STATES.FAILED); }, /** * Records the mute state for the stream. * * @param {sharedActions.setMute} actionData The mute state for the stream type. */ setMute: function(actionData) { var muteState = {}; muteState[actionData.type + "Muted"] = !actionData.enabled; this.setStoreState(muteState); }, /** * Handles a media stream being created. This may be a local or a remote stream. * * @param {sharedActions.MediaStreamCreated} actionData */ mediaStreamCreated: function(actionData) { if (actionData.isLocal) { this.setStoreState({ localVideoEnabled: actionData.hasVideo, localSrcMediaElement: actionData.srcMediaElement }); return; } this.setStoreState({ remoteVideoEnabled: actionData.hasVideo, remoteSrcMediaElement: actionData.srcMediaElement }); }, /** * Handles a media stream being destroyed. This may be a local or a remote stream. * * @param {sharedActions.MediaStreamDestroyed} actionData */ mediaStreamDestroyed: function(actionData) { if (actionData.isLocal) { this.setStoreState({ localSrcMediaElement: null }); return; } this.setStoreState({ remoteSrcMediaElement: null }); }, /** * Handles a remote stream having video enabled or disabled. * * @param {sharedActions.RemoteVideoStatus} actionData */ remoteVideoStatus: function(actionData) { this.setStoreState({ remoteVideoEnabled: actionData.videoEnabled }); }, /** * Records when the remote media has been connected. */ mediaConnected: function() { this.setStoreState({mediaConnected: true}); }, /** * Used to note the current screensharing state. */ screenSharingState: function(actionData) { this.setStoreState({screenSharingState: actionData.state}); this._mozLoop.setScreenShareState( this.getStoreState().windowId, actionData.state === SCREEN_SHARE_STATES.ACTIVE); }, /** * Used to note the current state of receiving screenshare data. * * XXX this should be split into multiple actions to make the code clearer. */ receivingScreenShare: function(actionData) { if (!actionData.receiving && this.getStoreState().remoteVideoDimensions.screen) { // Remove the remote video dimensions for type screen as we're not // getting the share anymore. var newDimensions = _.extend(this.getStoreState().remoteVideoDimensions); delete newDimensions.screen; this.setStoreState({ receivingScreenShare: actionData.receiving, remoteVideoDimensions: newDimensions, screenShareMediaElement: null }); } else { this.setStoreState({ receivingScreenShare: actionData.receiving, screenShareMediaElement: actionData.srcMediaElement ? actionData.srcMediaElement : null }); } }, /** * Handles switching browser (aka tab) sharing to a new window. Should * only be used for browser sharing. * * @param {Number} windowId The new windowId to start sharing. */ _handleSwitchBrowserShare: function(err, windowId) { if (err) { console.error("Error getting the windowId: " + err); this.dispatchAction(new sharedActions.ScreenSharingState({ state: SCREEN_SHARE_STATES.INACTIVE })); return; } var screenSharingState = this.getStoreState().screenSharingState; if (screenSharingState === SCREEN_SHARE_STATES.INACTIVE) { // Screen sharing is still pending, so assume that we need to kick it off. var options = { videoSource: "browser", constraints: { browserWindow: windowId, scrollWithPage: true } }; this._sdkDriver.startScreenShare(options); } else if (screenSharingState === SCREEN_SHARE_STATES.ACTIVE) { // Just update the current share. this._sdkDriver.switchAcquiredWindow(windowId); } else { console.error("Unexpectedly received windowId for browser sharing when pending"); } }, /** * Initiates a screen sharing publisher. * * @param {sharedActions.StartScreenShare} actionData */ startScreenShare: function(actionData) { this.dispatchAction(new sharedActions.ScreenSharingState({ state: SCREEN_SHARE_STATES.PENDING })); var options = { videoSource: actionData.type }; if (options.videoSource === "browser") { this._browserSharingListener = this._handleSwitchBrowserShare.bind(this); // Set up a listener for watching screen shares. This will get notified // with the first windowId when it is added, so we start off the sharing // from within the listener. this._mozLoop.addBrowserSharingListener(this._browserSharingListener); } else { this._sdkDriver.startScreenShare(options); } }, /** * Ends an active screenshare session. */ endScreenShare: function() { if (this._browserSharingListener) { // Remove the browser sharing listener as we don't need it now. this._mozLoop.removeBrowserSharingListener(this._browserSharingListener); this._browserSharingListener = null; } if (this._sdkDriver.endScreenShare()) { this.dispatchAction(new sharedActions.ScreenSharingState({ state: SCREEN_SHARE_STATES.INACTIVE })); } }, /** * Handles recording when a remote peer has connected to the servers. */ remotePeerConnected: function() { this.setStoreState({ roomState: ROOM_STATES.HAS_PARTICIPANTS, used: true }); }, /** * Handles a remote peer disconnecting from the session. As we currently only * support 2 participants, we declare the room as SESSION_CONNECTED as soon as * one participantleaves. */ remotePeerDisconnected: function() { // Update the participants to just the owner. var participants = this.getStoreState("participants"); if (participants) { participants = participants.filter(function(participant) { return participant.owner; }); } this.setStoreState({ mediaConnected: false, participants: participants, roomState: ROOM_STATES.SESSION_CONNECTED, remoteSrcMediaElement: null }); }, /** * Handles an SDK status update, forwarding it to the server. * * @param {sharedActions.ConnectionStatus} actionData */ connectionStatus: function(actionData) { this._mozLoop.rooms.sendConnectionStatus(this.getStoreState("roomToken"), this.getStoreState("sessionToken"), actionData); }, /** * Handles the window being unloaded. Ensures the room is left. */ windowUnload: function() { this._leaveRoom(ROOM_STATES.CLOSING); if (!this._onUpdateListener) { return; } // If we're closing the window, we can stop listening to updates. var roomToken = this.getStoreState().roomToken; this._mozLoop.rooms.off("update:" + roomToken, this._onUpdateListener); this._mozLoop.rooms.off("delete:" + roomToken, this._onDeleteListener); window.removeEventListener("LoopShareWidgetChanged", this._onShareWidgetUpdate); window.removeEventListener("LoopSocialProvidersChanged", this._onSocialProvidersUpdate); delete this._onUpdateListener; delete this._onDeleteListener; delete this._onShareWidgetUpdate; delete this._onSocialProvidersUpdate; }, /** * Handles a room being left. */ leaveRoom: function() { this._leaveRoom(ROOM_STATES.ENDED); }, /** * Handles setting of the refresh timeout callback. * * @param {Integer} expireTime The time until expiry (in seconds). */ _setRefreshTimeout: function(expireTime) { this._timeout = setTimeout(this._refreshMembership.bind(this), expireTime * this.expiresTimeFactor * 1000); }, /** * Refreshes the membership of the room with the server, and then * sets up the refresh for the next cycle. */ _refreshMembership: function() { this._mozLoop.rooms.refreshMembership(this._storeState.roomToken, this._storeState.sessionToken, function(error, responseData) { if (error) { this.dispatchAction(new sharedActions.RoomFailure({ error: error, failedJoinRequest: false })); return; } this._setRefreshTimeout(responseData.expires); }.bind(this)); }, /** * Handles leaving a room. Clears any membership timeouts, then * signals to the server the leave of the room. * * @param {ROOM_STATES} nextState The next state to switch to. * @param {Boolean} failedJoinRequest Optional. Set to true if the join * request to loop-server failed. It * will skip the leave message. */ _leaveRoom: function(nextState, failedJoinRequest) { if (loop.standaloneMedia) { loop.standaloneMedia.multiplexGum.reset(); } this._mozLoop.setScreenShareState( this.getStoreState().windowId, false); if (this._browserSharingListener) { // Remove the browser sharing listener as we don't need it now. this._mozLoop.removeBrowserSharingListener(this._browserSharingListener); this._browserSharingListener = null; } // We probably don't need to end screen share separately, but lets be safe. this._sdkDriver.disconnectSession(); // Reset various states. var originalStoreState = this.getInitialStoreState(); var newStoreState = {}; this._statesToResetOnLeave.forEach(function(state) { newStoreState[state] = originalStoreState[state]; }); this.setStoreState(newStoreState); if (this._timeout) { clearTimeout(this._timeout); delete this._timeout; } if (!failedJoinRequest && (this._storeState.roomState === ROOM_STATES.JOINING || this._storeState.roomState === ROOM_STATES.JOINED || this._storeState.roomState === ROOM_STATES.SESSION_CONNECTED || this._storeState.roomState === ROOM_STATES.HAS_PARTICIPANTS)) { this._mozLoop.rooms.leave(this._storeState.roomToken, this._storeState.sessionToken); } this.setStoreState({roomState: nextState}); }, /** * When feedback is complete, we go back to the ready state, rather than * init or gather, as we don't need to get the data from the server again. */ feedbackComplete: function() { this.setStoreState({ roomState: ROOM_STATES.READY, // Reset the used state here as the user has now given feedback and the // next time they enter the room, the other person might not be there. used: false }); }, /** * Handles a change in dimensions of a video stream and updates the store data * with the new dimensions of a local or remote stream. * * @param {sharedActions.VideoDimensionsChanged} actionData */ videoDimensionsChanged: function(actionData) { // NOTE: in the future, when multiple remote video streams are supported, // we'll need to make this support multiple remotes as well. Good // starting point for video tiling. var storeProp = (actionData.isLocal ? "local" : "remote") + "VideoDimensions"; var nextState = {}; nextState[storeProp] = this.getStoreState()[storeProp]; nextState[storeProp][actionData.videoType] = actionData.dimensions; this.setStoreState(nextState); } }); return ActiveRoomStore; })();