gecko-dev/browser/components/loop/content/shared/js/otSdkDriver.js

474 lines
15 KiB
JavaScript

/* 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/. */
/* global loop:true */
var loop = loop || {};
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
* actions, and instruct the SDK what to do as a result of actions.
*/
var OTSdkDriver = function(options) {
if (!options.dispatcher) {
throw new Error("Missing option dispatcher");
}
if (!options.sdk) {
throw new Error("Missing option sdk");
}
this.dispatcher = options.dispatcher;
this.sdk = options.sdk;
this.connections = {};
this.dispatcher.register(this, [
"setupStreamElements",
"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.
*
* @param {sharedActions.SetupStreamElements} actionData The data associated
* with the action. See action.js.
*/
setupStreamElements: function(actionData) {
this.getLocalElement = actionData.getLocalElementFunc;
this.getScreenShareElementFunc = actionData.getScreenShareElementFunc;
this.getRemoteElement = actionData.getRemoteElementFunc;
this.publisherConfig = actionData.publisherConfig;
// At this state we init the publisher, even though we might be waiting for
// the initial connect of the session. This saves time when setting up
// the media.
this.publisher = this.sdk.initPublisher(this.getLocalElement(),
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));
this.publisher.on("accessDialogOpened",
this._onAccessDialogOpened.bind(this));
},
/**
* Handles the setMute action. Informs the published stream to mute
* or unmute audio as appropriate.
*
* @param {sharedActions.SetMute} actionData The data associated with the
* action. See action.js.
*/
setMute: function(actionData) {
if (actionData.type === "audio") {
this.publisher.publishAudio(actionData.enabled);
} else {
this.publisher.publishVideo(actionData.enabled);
}
},
/**
* 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.
*
* sessionData items:
* - sessionId: The OT session ID
* - apiKey: The OT API key
* - sessionToken: The token for the OT session
*
* @param {Object} sessionData The session data for setting up the OT session.
*/
connectSession: function(sessionData) {
this.session = this.sdk.initSession(sessionData.sessionId);
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",
this._onSessionDisconnected.bind(this));
this.session.on("streamPropertyChanged", this._onStreamPropertyChanged.bind(this));
// This starts the actual session connection.
this.session.connect(sessionData.apiKey, sessionData.sessionToken,
this._onConnectionComplete.bind(this));
},
/**
* Disconnects the sdk session.
*/
disconnectSession: function() {
this.endScreenShare();
if (this.session) {
this.session.off("streamCreated streamDestroyed connectionDestroyed " +
"sessionDisconnected streamPropertyChanged");
this.session.disconnect();
delete this.session;
}
if (this.publisher) {
this.publisher.off("accessAllowed accessDenied accessDialogOpened streamCreated");
this.publisher.destroy();
delete this.publisher;
}
// Also, tidy these variables ready for next time.
delete this._sessionConnected;
delete this._publisherReady;
delete this._publishedLocalStream;
delete this._subscribedRemoteStream;
this.connections = {};
},
/**
* Oust all users from an ongoing session. This is typically done when a room
* owner deletes the room.
*
* @param {Function} callback Function to be invoked once all connections are
* ousted
*/
forceDisconnectAll: function(callback) {
if (!this._sessionConnected) {
callback();
return;
}
var connectionNames = Object.keys(this.connections);
if (connectionNames.length === 0) {
callback();
return;
}
var disconnectCount = 0;
connectionNames.forEach(function(id) {
var connection = this.connections[id];
this.session.forceDisconnect(connection, function() {
// When all connections have disconnected, call the callback, since
// we're done.
if (++disconnectCount === connectionNames.length) {
callback();
}
});
}, this);
},
/**
* Called once the session has finished connecting.
*
* @param {Error} error An OT error object, null if there was no error.
*/
_onConnectionComplete: function(error) {
if (error) {
console.error("Failed to complete connection", error);
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.COULD_NOT_CONNECT
}));
return;
}
this.dispatcher.dispatch(new sharedActions.ConnectedToSdkServers());
this._sessionConnected = true;
this._maybePublishLocalStream();
},
/**
* Handles the connection event for a peer's connection being dropped.
*
* @param {ConnectionEvent} event The event details
* https://tokbox.com/opentok/libraries/client/js/reference/ConnectionEvent.html
*/
_onConnectionDestroyed: function(event) {
var connection = event.connection;
if (connection && (connection.id in this.connections)) {
delete this.connections[connection.id];
}
this.dispatcher.dispatch(new sharedActions.RemotePeerDisconnected({
peerHungup: event.reason === "clientDisconnected"
}));
},
/**
* Handles the session event for the connection for this client being
* destroyed.
*
* @param {SessionDisconnectEvent} event The event details:
* https://tokbox.com/opentok/libraries/client/js/reference/SessionDisconnectEvent.html
*/
_onSessionDisconnected: function(event) {
var reason;
switch (event.reason) {
case "networkDisconnected":
reason = FAILURE_DETAILS.NETWORK_DISCONNECTED;
break;
case "forceDisconnected":
reason = FAILURE_DETAILS.EXPIRED_OR_INVALID;
break;
default:
// Other cases don't need to be handled.
return;
}
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: reason
}));
},
/**
* Handles the connection event for a newly connecting peer.
*
* @param {ConnectionEvent} event The event details
* https://tokbox.com/opentok/libraries/client/js/reference/ConnectionEvent.html
*/
_onConnectionCreated: function(event) {
var connection = event.connection;
if (this.session.connection.id === connection.id) {
return;
}
this.connections[connection.id] = connection;
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.
*
* @param {StreamEvent} event The event details:
* https://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
*/
_onRemoteStreamCreated: function(event) {
if (event.stream[STREAM_PROPERTIES.HAS_VIDEO]) {
this.dispatcher.dispatch(new sharedActions.VideoDimensionsChanged({
isLocal: false,
videoType: event.stream.videoType,
dimensions: event.stream[STREAM_PROPERTIES.VIDEO_DIMENSIONS]
}));
}
if (event.stream.videoType === "screen") {
this._handleRemoteScreenShareCreated(event.stream);
return;
}
var remoteElement = this.getRemoteElement();
this.session.subscribe(event.stream,
remoteElement, this._getCopyPublisherConfig());
this._subscribedRemoteStream = true;
if (this._checkAllStreamsConnected()) {
this.dispatcher.dispatch(new sharedActions.MediaConnected());
}
},
/**
* Handles the event when the local stream is created.
*
* @param {StreamEvent} event The event details:
* https://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
*/
_onLocalStreamCreated: function(event) {
if (event.stream[STREAM_PROPERTIES.HAS_VIDEO]) {
this.dispatcher.dispatch(new sharedActions.VideoDimensionsChanged({
isLocal: true,
videoType: event.stream.videoType,
dimensions: event.stream[STREAM_PROPERTIES.VIDEO_DIMENSIONS]
}));
}
},
/**
* 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"
* dialog from being shown.
*
* @param {OT.Event} event
*/
_onAccessDialogOpened: function(event) {
event.preventDefault();
},
/**
* Handles the publishing being complete.
*
* @param {OT.Event} event
*/
_onPublishComplete: function(event) {
event.preventDefault();
this._publisherReady = true;
this.dispatcher.dispatch(new sharedActions.GotMediaPermission());
this._maybePublishLocalStream();
},
/**
* Handles publishing of media being denied.
*
* @param {OT.Event} event
*/
_onPublishDenied: function(event) {
// This prevents the SDK's "access denied" dialog showing.
event.preventDefault();
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.MEDIA_DENIED
}));
},
/**
* Handles publishing of property changes to a stream.
*/
_onStreamPropertyChanged: function(event) {
if (event.changedProperty == STREAM_PROPERTIES.VIDEO_DIMENSIONS) {
this.dispatcher.dispatch(new sharedActions.VideoDimensionsChanged({
isLocal: event.stream.connection.id == this.session.connection.id,
videoType: event.stream.videoType,
dimensions: event.stream[STREAM_PROPERTIES.VIDEO_DIMENSIONS]
}));
}
},
/**
* Publishes the local stream if the session is connected
* and the publisher is ready.
*/
_maybePublishLocalStream: function() {
if (this._sessionConnected && this._publisherReady) {
// We are clear to publish the stream to the session.
this.session.publish(this.publisher);
// Now record the fact, and check if we've got all media yet.
this._publishedLocalStream = true;
if (this._checkAllStreamsConnected()) {
this.dispatcher.dispatch(new sharedActions.MediaConnected());
}
}
},
/**
* Used to check if both local and remote streams are available
* and send an action if they are.
*/
_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
}));
}
};
return OTSdkDriver;
})();