/**
* @class Skyway
*/
(function() {
/**
* Please check on the {{#crossLink "Skyway/init:method"}}init(){{/crossLink}}
* function on how you can initialize Skyway. Note that:
* - You will have to subscribe all Skyway events first before calling
* {{#crossLink "Skyway/init:method"}}init(){{/crossLink}}.
* - If you need an api key, please [register an api key](http://
* developer.temasys.com.sg) at our developer console.
* @class Skyway
* @constructor
* @example
* // Getting started on how to use Skyway
* var SkywayDemo = new Skyway();
* SkywayDemo.init('apiKey');
* @since 0.1.0
*/
function Skyway() {
if (!(this instanceof Skyway)) {
return new Skyway();
}
/**
* Version of Skyway
* @attribute VERSION
* @type String
* @readOnly
* @since 0.1.0
*/
this.VERSION = '@@version';
/**
* The list of available regional servers.
* - This is for developers to set the nearest region server
* for Skyway to connect to for faster connectivity.
* - The available regional servers are:
* @attribute REGIONAL_SERVER
* @type JSON
* @param {String} US1 USA server 1.
* @param {String} US2 USA server 2.
* @param {String} SG Singapore server.
* @param {String} EU Europe server.
* @readOnly
* @since 0.5.0
*/
this.REGIONAL_SERVER = {
US1: 'us1',
US2: 'us2',
SG: 'sg',
EU: 'eu'
};
/**
* The list of ICE connection states.
* - Check out the [w3 specification documentation](http://dev.w3.org/2011/
* webrtc/editor/webrtc.html#rtciceconnectionstate-enum).
* - This is the RTCIceConnection state of the peer.
* - The states that would occur are:
* @attribute ICE_CONNECTION_STATE
* @type JSON
* @param {String} STARTING The ICE agent is gathering addresses
* and/or waiting for remote candidates to be supplied.
* @param {String} CHECKING The ICE agent has received remote candidates
* on at least one component, and is checking candidate pairs but has
* not yet found a connection. In addition to checking, it may also
* still be gathering.
* @param {String} CONNECTED The ICE agent has found a usable connection
* for all components but is still checking other candidate pairs to see
* if there is a better connection. It may also still be gathering.
* @param {String} COMPLETED The ICE agent has finished gathering and
* checking and found a connection for all components.
* @param {String} FAILED The ICE agent is finished checking all
* candidate pairs and failed to find a connection for at least one
* component.
* @param {String} DISCONNECTED Liveness checks have failed for one or
* more components. This is more aggressive than "failed", and may
* trigger intermittently (and resolve itself without action) on
* a flaky network.
* @param {String} CLOSED The ICE agent has shut down and is no
* longer responding to STUN requests.
* @readOnly
* @since 0.1.0
*/
this.ICE_CONNECTION_STATE = {
STARTING: 'starting',
CHECKING: 'checking',
CONNECTED: 'connected',
COMPLETED: 'completed',
CLOSED: 'closed',
FAILED: 'failed',
DISCONNECTED: 'disconnected'
};
/**
* The list of peer connection states.
* - Check out the [w3 specification documentation](http://dev.w3.org/2011/
* webrtc/editor/webrtc.html#rtcpeerstate-enum).
* - This is the RTCSignalingState of the peer.
* - The states that would occur are:
* @attribute PEER_CONNECTION_STATE
* @type JSON
* @param {String} STABLE There is no offer/answer exchange in progress.
* This is also the initial state in which case the local and remote
* descriptions are empty.
* @param {String} HAVE_LOCAL_OFFER A local description, of type "offer",
* has been successfully applied.
* @param {String} HAVE_REMOTE_OFFER A remote description, of type "offer",
* has been successfully applied.
* @param {String} HAVE_LOCAL_PRANSWER A remote description of type "offer"
* has been successfully applied and a local description of type "pranswer"
* has been successfully applied.
* @param {String} HAVE_REMOTE_PRANSWER "Answer" remote description is applied.
* @param {String} ESTABLISHED A local description of type "offer" has
* been successfully applied and a remote description of type "pranswer"
* has been successfully applied.
* @param {String} CLOSED The connection is closed.
* @readOnly
* @since 0.1.0
*/
this.PEER_CONNECTION_STATE = {
STABLE: 'stable',
HAVE_LOCAL_OFFER: 'have-local-offer',
HAVE_REMOTE_OFFER: 'have-remote-offer',
HAVE_LOCAL_PRANSWER: 'have-local-pranswer',
HAVE_REMOTE_PRANSWER: 'have-remote-pranswer',
ESTABLISHED: 'established',
CLOSED: 'closed'
};
/**
* The list of ICE candidate generation states.
* - Check out the [w3 specification documentation](http://dev.w3.org/2011/
* webrtc/editor/webrtc.html#rtcicegatheringstate-enum).
* - This is RTCIceGatheringState of the peer.
* - The states that would occur are:
* @attribute CANDIDATE_GENERATION_STATE
* @type JSON
* @param {String} NEW The object was just created, and no networking
* has occurred yet.
* @param {String} GATHERING The ICE engine is in the process of gathering
* candidates for this RTCPeerConnection.
* @param {String} COMPLETED The ICE engine has completed gathering. Events
* such as adding a new interface or a new TURN server will cause the
* state to go back to gathering.
* @readOnly
* @since 0.4.1
*/
this.CANDIDATE_GENERATION_STATE = {
NEW: 'new',
GATHERING: 'gathering',
COMPLETED: 'completed'
};
/**
* The list of handshake progress steps.
* - This are the list of steps for the Skyway peer connection.
* - The steps that would occur are:
* @type JSON
* @attribute HANDSHAKE_PROGRESS
* @param {String} ENTER Step 1. Received "enter" from peer.
* @param {String} WELCOME Step 2. Received "welcome" from peer.
* @param {String} OFFER Step 3. Received "offer" from peer.
* @param {String} ANSWER Step 4. Received "answer" from peer.
* @param {String} ERROR Error state.
* @readOnly
* @since 0.1.0
*/
this.HANDSHAKE_PROGRESS = {
ENTER: 'enter',
WELCOME: 'welcome',
OFFER: 'offer',
ANSWER: 'answer',
ERROR: 'error'
};
/**
* The list of datachannel states.
* - Check out the [w3 specification documentation](http://dev.w3.org/2011/
* webrtc/editor/webrtc.html#idl-def-RTCDataChannelState).
* - This is the RTCDataChannelState of the peer.
* - <u>ERROR</u> is an additional implemented state by Skyway
* for further error tracking.
* - The states that would occur are:
* @attribute DATA_CHANNEL_STATE
* @type JSON
* @param {String} CONNECTING The user agent is attempting to establish
* the underlying data transport. This is the initial state of a
* RTCDataChannel object created with createDataChannel().
* @param {String} OPEN The underlying data transport is established
* and communication is possible. This is the initial state of a
* RTCDataChannel object dispatched as a part of a RTCDataChannelEvent.
* @param {String} CLOSING The procedure to close down the underlying
* data transport has started.
* @param {String} CLOSED The underlying data transport has been closed
* or could not be established.
* @param {String} ERROR Datachannel has occurred an error.
* @readOnly
* @since 0.1.0
*/
this.DATA_CHANNEL_STATE = {
CONNECTING: 'connecting',
OPEN: 'open',
CLOSING: 'closing',
CLOSED: 'closed',
ERROR: 'error'
};
/**
* The list of signaling actions received.
* - These are usually received from the signaling server to warn the user.
* - The system action outcomes are:
* @attribute SYSTEM_ACTION
* @type JSON
* @param {String} WARNING Server is warning user that the room is closing.
* @param {String} REJECT Server has rejected user from room.
* @param {String} CLOSED Server has closed the room.
* @readOnly
* @since 0.1.0
*/
this.SYSTEM_ACTION = {
WARNING: 'warning',
REJECT: 'reject',
CLOSED: 'close'
};
/**
* The list of api server data retrieval state.
* - These are the states to inform the state of retrieving the
* information from the api server required to start the peer
* connection or if the browser is eligible to start the peer connection.
* - This is the first event that would fired, because Skyway would retrieve
* information from the api server that is required to start the connection.
* - Once the state is <u>COMPLETED</u>, Skyway is ready to start the call.
* - The states that would occur are:
* @attribute READY_STATE_CHANGE
* @type JSON
* @param {Integer} INIT Skyway has just started. No information are
* retrieved yet.
* @param {Integer} LOADING Skyway is starting the retrieval of the
* connection information.
* @param {Integer} COMPLETED Skyway has completed retrieving the
* connection.
* @param {Integer} ERROR Skyway has occurred an error when
* retrieving the connection information.
* @readOnly
* @since 0.1.0
*/
this.READY_STATE_CHANGE = {
INIT: 0,
LOADING: 1,
COMPLETED: 2,
ERROR: -1
};
/**
* The list of ready state change errors.
* - These are the error states from the error object error code.
* - The states that would occur are:
* @attribute READY_STATE_CHANGE_ERROR
* @type JSON
* @param {Integer} API_INVALID Api Key provided does not exist.
* @param {Integer} API_DOMAIN_NOT_MATCH Api Key used in domain does
* not match.
* @param {Integer} API_CORS_DOMAIN_NOT_MATCH Api Key used in CORS
* domain does not match.
* @param {Integer} API_CREDENTIALS_INVALID Api Key credentials does
* not exist.
* @param {Integer} API_CREDENTIALS_NOT_MATCH Api Key credentials does not
* match what is expected.
* @param {Integer} API_INVALID_PARENT_KEY Api Key does not have a parent
* key nor is a root key.
* @param {Integer} API_NOT_ENOUGH_CREDIT Api Key does not have enough
* credits to use.
* @param {Integer} API_NOT_ENOUGH_PREPAID_CREDIT Api Key does not have
* enough prepaid credits to use.
* @param {Integer} API_FAILED_FINDING_PREPAID_CREDIT Api Key preapid
* payments does not exist.
* @param {Integer} API_NO_MEETING_RECORD_FOUND Api Key does not have a
* meeting record at this timing. This occurs when Api Key is a
* static one.
* @param {Integer} ROOM_LOCKED Room is locked.
* @param {Integer} NO_SOCKET_IO No socket.io dependency is loaded to use.
* @param {Integer} NO_XMLHTTPREQUEST_SUPPORT Browser does not support
* XMLHttpRequest to use.
* @param {Integer} NO_WEBRTC_SUPPORT Browser does not have WebRTC support.
* @param {Integer} NO_PATH No path is loaded yet.
* @param {Integer} INVALID_XMLHTTPREQUEST_STATUS Invalid XMLHttpRequest
* when retrieving information.
* @readOnly
* @since 0.4.0
*/
this.READY_STATE_CHANGE_ERROR = {
API_INVALID: 4001,
API_DOMAIN_NOT_MATCH: 4002,
API_CORS_DOMAIN_NOT_MATCH: 4003,
API_CREDENTIALS_INVALID: 4004,
API_CREDENTIALS_NOT_MATCH: 4005,
API_INVALID_PARENT_KEY: 4006,
API_NOT_ENOUGH_CREDIT: 4007,
API_NOT_ENOUGH_PREPAID_CREDIT: 4008,
API_FAILED_FINDING_PREPAID_CREDIT: 4009,
API_NO_MEETING_RECORD_FOUND: 4010,
ROOM_LOCKED: 5001,
NO_SOCKET_IO: 1,
NO_XMLHTTPREQUEST_SUPPORT: 2,
NO_WEBRTC_SUPPORT: 3,
NO_PATH: 4,
INVALID_XMLHTTPREQUEST_STATUS: 5,
SCRIPT_ERROR: 6
};
/**
* The list of datachannel transfer types.
* - This is used to identify if the stream is an upload stream or
* a download stream.
* - The available types are:
* @attribute DATA_TRANSFER_TYPE
* @type JSON
* @param {String} UPLOAD The datachannel transfer is an upload stream.
* @param {String} DOWNLOAD The datachannel transfer is a download stream.
* @readOnly
* @since 0.1.0
*/
this.DATA_TRANSFER_TYPE = {
UPLOAD: 'upload',
DOWNLOAD: 'download'
};
/**
* The list of datachannel transfer state.
* - These are the states to inform the state of the data transfer.
* - The list of states would occur are:
* @attribute DATA_TRANSFER_STATE
* @type JSON
* @param {String} UPLOAD_REQUEST Peer has a data transfer request.
* @param {String} UPLOAD_STARTED Data transfer of upload has just started.
* @param {String} DOWNLOAD_STARTED Data transfer of download has
* just started.
* @param {String} UPLOADING Data upload transfer is occurring.
* @param {String} DOWNLOADING Data download transfer is occurring.
* @param {String} UPLOAD_COMPLETED Data upload transfer has been completed.
* @param {String} DOWNLOAD_COMPLETED Data download transfer has been
* completed.
* @param {String} REJECTED Peer rejected user's data transfer request.
* @param {String} ERROR Data transfer had an error occurred
* when uploading or downloading file.
* @readOnly
* @since 0.4.0
*/
this.DATA_TRANSFER_STATE = {
UPLOAD_REQUEST: 'request',
UPLOAD_STARTED: 'uploadStarted',
DOWNLOAD_STARTED: 'downloadStarted',
REJECTED: 'rejected',
ERROR: 'error',
UPLOADING: 'uploading',
DOWNLOADING: 'downloading',
UPLOAD_COMPLETED: 'uploadCompleted',
DOWNLOAD_COMPLETED: 'downloadCompleted'
};
/**
* The list of data transfer data types.
* - <b><i>TODO</i></b>: ArrayBuffer and Blob data transfer in
* datachannel.
* - The available data transfer data types are:
* @attribute DATA_TRANSFER_DATA_TYPE
* @type JSON
* @param {String} BINARY_STRING BinaryString data type.
* @param {String} ARRAY_BUFFER Still-implementing. ArrayBuffer data type.
* @param {String} BLOB Still-implementing. Blob data type.
* @readOnly
* @since 0.1.0
*/
this.DATA_TRANSFER_DATA_TYPE = {
BINARY_STRING: 'binaryString',
ARRAY_BUFFER: 'arrayBuffer',
BLOB: 'blob'
};
/**
* The list of signaling message types.
* - These are the list of available signaling message types expected to
* be received.
* - These message types are fixed.
* - The available message types are:
* @attribute SIG_TYPE
* @type JSON
* @readOnly
* @param {String} JOIN_ROOM
* - Send: User request to join the room.
* @param {String} IN_ROOM
* - Received: Response from server that user has joined the room.
* @param {String} ENTER
* - Send: Broadcast message to inform other connected peers in the room
* that the user is the new peer joining the room.
* - Received: A peer has just joined the room.
* To send a welcome message.
* @param {String} WELCOME
* - Send: Respond to user to request peer to create the offer.
* - Received: Response from peer that peer acknowledges the user has
* joined the room. To send and create an offer message.
* @param {String} OFFER
* - Send: Respond to peer's request to create an offer.
* - Received: Response from peer's offer message. User to create and
* send an answer message.
* @param {String} ANSWER
* - Send: Response to peer's offer message.
* - Received: Response from peer's answer message.
* Connection is established.
* @param {String} CANDIDATE
* - Send: User to send the ICE candidate after onicecandidate is called.
* - Received: User to add peer's ice candidate in addIceCandidate.
* @param {String} BYE
* - Received: Peer has left the room.
* @param {String} CHAT
* - Send: Deprecated. User sends a chat message.
* - Received: Deprecated. Peer sends a chat message to user.
* @param {String} REDIRECT
* - Received: Server warning to user.
* @param {String} ERROR
* - Received: Deprecated. Server error occurred.
* @param {String} UPDATE_USER
* - Send: User's custom data is updated and to inform other peers
* of updated custom data.
* - Received: Peer's user custom data has changed.
* @param {String} ROOM_LOCK
* - Send: Room lock action has changed and to inform other peers
* of updated room lock status.
* - Received: Room lock status has changed.
* @param {String} MUTE_VIDEO
* - Send: User has muted video and to inform other peers
* of updated muted video stream status.
* - Received: Peer muted video status has changed.
* @param {String} MUTE_AUDIO
* - Send: User has muted audio and to inform other peers
* of updated muted audio stream status.
* - Received: Peer muted audio status has changed.
* @param {String} PUBLIC_MESSAGE
* - Send: User sends a broadcast message to all peers.
* - Received: User receives a peer's broadcast message.
* @param {String} PRIVATE_MESSAGE
* - Send: User sends a private message to a peer.
* - Received: User receives a private message from a peer.
* @private
* @since 0.3.0
*/
this.SIG_TYPE = {
JOIN_ROOM: 'joinRoom',
IN_ROOM: 'inRoom',
ENTER: this.HANDSHAKE_PROGRESS.ENTER,
WELCOME: this.HANDSHAKE_PROGRESS.WELCOME,
OFFER: this.HANDSHAKE_PROGRESS.OFFER,
ANSWER: this.HANDSHAKE_PROGRESS.ANSWER,
CANDIDATE: 'candidate',
BYE: 'bye',
CHAT: 'chat',
REDIRECT: 'redirect',
ERROR: 'error',
UPDATE_USER: 'updateUserEvent',
ROOM_LOCK: 'roomLockEvent',
MUTE_VIDEO: 'muteVideoEvent',
MUTE_AUDIO: 'muteAudioEvent',
PUBLIC_MESSAGE: 'public',
PRIVATE_MESSAGE: 'private',
GROUP: 'group'
};
/**
* The list of actions for room lock application.
* - This are the list of actions available for locking a room.
* - The available actions are:
* @attribute LOCK_ACTION
* @type JSON
* @param {String} LOCK Lock the room
* @param {String} UNLOCK Unlock the room
* @param {String} STATUS Get the status to check the room is locked
* or not.
* @readOnly
* @since 0.2.0
*/
this.LOCK_ACTION = {
LOCK: 'lock',
UNLOCK: 'unlock',
STATUS: 'check'
};
/**
* The list of recommended video resolutions.
* - Note that the higher the resolution, the connectivity speed might
* be affected.
* - The available video resolutions type are:
* @param {JSON} QVGA QVGA video resolution.
* @param {Integer} QVGA.width 320
* @param {Integer} QVGA.height 180
* @param {JSON} VGA VGA video resolution.
* @param {Integer} VGA.width 640
* @param {Integer} VGA.height 360
* @param {JSON} HD HD video quality
* @param {Integer} HD.width 1280
* @param {Integer} HD.height 720
* @param {JSON} FHD Might not be supported. Full HD video resolution.
* @param {Integer} FHD.width 1920
* @param {Integer} FHD.height 1080
* @attribute VIDEO_RESOLUTION
* @type JSON
* @readOnly
* @since 0.2.0
*/
this.VIDEO_RESOLUTION = {
QVGA: {
width: 320,
height: 180
},
VGA: {
width: 640,
height: 360
},
HD: {
width: 1280,
height: 720
},
FHD: {
width: 1920,
height: 1080
} // Please check support
};
/**
* The path that user is currently connect to.
* - NOTE ALEX: check if last char is '/'
* @attribute _path
* @type String
* @default _serverPath
* @final
* @required
* @private
* @since 0.1.0
*/
this._path = null;
/**
* The path that Skyway makes rest api calls to.
* @attribute _serverPath
* @type String
* @final
* @required
* @private
* @since 0.2.0
*/
this._serverPath = '//api.temasys.com.sg';
/**
* The regional server that Skyway connects to.
* @attribute _serverRegion
* @type String
* @private
* @since 0.5.0
*/
this._serverRegion = null;
/**
* The server that user connects to to make
* api calls to.
* - The reason why users can input this value is to give
* users the chance to connect to any of our beta servers
* if available instead of the stable version.
* @attribute _roomServer
* @type String
* @private
* @since 0.3.0
*/
this._roomServer = null;
/**
* The API Key ID.
* @attribute _apiKey
* @type String
* @private
* @since 0.3.0
*/
this._apiKey = null;
/**
* The default room that the user connects to if no room is provided in
* {{#crossLink "Skyway/joinRoom:method"}}joinRoom(){{/crossLink}}.
* @attribute _defaultRoom
* @type String
* @private
* @since 0.3.0
*/
this._defaultRoom = null;
/**
* The room that the user is currently connected to.
* @attribute _selectedRoom
* @type String
* @default _defaultRoom
* @private
* @since 0.3.0
*/
this._selectedRoom = null;
/**
* The static room's meeting starting date and time.
* - The value is in ISO formatted string.
* @attribute _roomStart
* @type String
* @private
* @optional
* @since 0.3.0
*/
this._roomStart = null;
/**
* The static room's meeting duration.
* @attribute _roomDuration
* @type Integer
* @private
* @optional
* @since 0.3.0
*/
this._roomDuration = null;
/**
* The credentials required to set the start date and time
* and the duration.
* @attribute _roomCredentials
* @type String
* @private
* @optional
* @since 0.3.0
*/
this._roomCredentials = null;
/**
* The received server key.
* @attribute _key
* @type String
* @private
* @since 0.1.0
*/
this._key = null;
/**
* The actual socket object that handles the connection.
* @attribute _socket
* @type Object
* @required
* @private
* @since 0.1.0
*/
this._socket = null;
/**
* The version of the
* {{#crossLink "Skyway/_socket:attribute"}}_socket{{/crossLink}}
* object.
* @attribute _socketVersion
* @type Float
* @private
* @since 0.1.0
*/
this._socketVersion = null;
/**
* User information, credential and the local stream(s).
* @attribute _user
* @type JSON
* @param {String} id User's session id.
* @param {String} sid User's secret id. This is the id used as the peerId.
* @param {String} apiOwner Owner of the room.
* @param {Array} streams The array of user's MediaStream(s).
* @param {String} timestamp User's timestamp.
* @param {String} token User access token.
* @param {JSON} info Optional. User information object.
* @param {JSON} info.settings User stream settings.
* @param {Boolean|JSON} info.settings.audio User audio settings.
* @param {Boolean} info.settings.audio.stereo User has enabled stereo
* or not.
* @param {Boolean|JSON} info.settings.video User video settings.
* @param {Bolean|JSON} info.settings.video.resolution User video
* resolution set. [Rel: Skyway.VIDEO_RESOLUTION]
* @param {Integer} info.settings.video.resolution.width User video
* resolution width.
* @param {Integer} info.settings.video.resolution.height User video
* resolution height.
* @param {Integer} info.settings.video.frameRate User video minimum
* frame rate.
* @param {JSON} info.mediaStatus User MediaStream(s) status.
* @param {Boolean} info.mediaStatus.audioMuted Is user's audio muted.
* @param {Boolean} info.mediaStatus.videoMuted Is user's vide muted.
* @param {String|JSON} info.userData User's custom data set.
* @required
* @private
* @since 0.3.0
*/
this._user = null;
/**
* The room connection information.
* @attribute _room
* @type JSON
* @param {JSON} room Room information and credentials.
* @param {String} room.id RoomId of the room user is connected to.
* @param {String} room.token Token of the room user is connected to.
* @param {String} room.tokenTimestamp Token timestamp of the room
* user is connected to.
* @param {JSON} room.signalingServer The signaling server settings
* the room has to connect to.
* @param {String} room.signalingServer.protocol The protocol the room
* has to use.
* @param {String} room.signalingServer.ip The ip address of the
* signaling server the room has to connect to.
* @param {String} room.signalingServer.port The port that the room
& has to connec to.
* @param {JSON} room.pcHelper Holder for all the constraints objects used
* in a peerconnection lifetime. Some are initialized by default, some are initialized by
* internal methods, all can be overriden through updateUser. Future APIs will help user
* modifying specific parts (audio only, video only, ...) separately without knowing the
* intricacies of constraints.
* @param {JSON} room.pcHelper.pcConstraints The peer connection constraints object.
* @param {JSON} room.pcHelper.pcConfig Will be provided upon connection to a room
* @param {JSON} room.pcHelper.pcConfig.mandatory Mandantory options.
* @param {Array} room.pcHelper.pcConfig.optional Optional options.
* - Ex: [{DtlsSrtpKeyAgreement: true}]
* @param {JSON} room.pcHelper.offerConstraints The offer constraints object.
* @param {JSON} room.pcHelper.offerConstraints.mandatory Offer mandantory object.
* - Ex: {MozDontOfferDataChannel:true}
* @param {Array} room.pcHelper.offerConstraints.optional Offer optional object.
* @param {JSON} room.pcHelper.sdpConstraints Sdp constraints object
* @param {JSON} room.pcHelper.sdpConstraints.mandatory Sdp mandantory object.
* - Ex: { 'OfferToReceiveAudio':true, 'OfferToReceiveVideo':true }
* @param {Array} room.pcHelper.sdpConstraints.optional Sdp optional object.
* @required
* @private
* @since 0.3.0
*/
this._room = null;
/**
* Internal array of peer connections.
* @attribute _peerConnections
* @type Object
* @required
* @private
* @since 0.1.0
*/
this._peerConnections = [];
/**
* Internal array of peer informations.
* @attribute _peerInformations
* @type Object
* @private
* @required
* @since 0.3.0
*/
this._peerInformations = [];
/**
* Internal array of datachannels.
* @attribute _dataChannels
* @type Object
* @private
* @required
* @since 0.2.0
*/
this._dataChannels = [];
/**
* Internal array of data upload transfers.
* @attribute _uploadDataTransfers
* @type Array
* @private
* @required
* @since 0.4.1
*/
this._uploadDataTransfers = [];
/**
* Internal array of data upload sessions.
* @attribute _uploadDataSessions
* @type Array
* @private
* @required
* @since 0.4.1
*/
this._uploadDataSessions = [];
/**
* Internal array of data download transfers.
* @attribute _downloadDataTransfers
* @type Array
* @private
* @required
* @since 0.4.1
*/
this._downloadDataTransfers = [];
/**
* Internal array of data download sessions.
* @attribute _downloadDataSessions
* @type Array
* @private
* @required
* @since 0.4.1
*/
this._downloadDataSessions = [];
/**
* Internal array of data transfers timeout.
* @attribute _dataTransfersTimeout
* @type Array
* @private
* @required
* @since 0.4.1
*/
this._dataTransfersTimeout = [];
/**
* The current Skyway ready state change.
* [Rel: Skyway.READY_STATE_CHANGE]
* @attribute _readyState
* @type Integer
* @private
* @required
* @since 0.1.0
*/
this._readyState = 0;
/**
* The current socket opened state.
* @attribute _channel_open
* @type Boolean
* @private
* @required
* @since 0.1.0
*/
this._channel_open = false;
/**
* The current state if room is locked.
* @attribute _room_lock
* @type Boolean
* @private
* @required
* @since 0.4.0
*/
this._room_lock = false;
/**
* The current state if user is in the room.
* @attribute _in_room
* @type Boolean
* @private
* @required
* @since 0.1.0
*/
this._in_room = false;
/**
* The fixed size for each data chunk.
* @attribute _chunkFileSize
* @type Integer
* @private
* @final
* @required
* @since 0.1.0
*/
this._chunkFileSize = 49152;
/**
* The fixed for each data chunk for firefox implementation.
* - Firefox the sender chunks 49152 but receives as 16384.
* @attribute _mozChunkFileSize
* @type Integer
* @private
* @final
* @required
* @since 0.2.0
*/
this._mozChunkFileSize = 16384;
/**
* The current state if ICE trickle is enabled.
* @attribute _enableIceTrickle
* @type Boolean
* @default true
* @private
* @required
* @since 0.3.0
*/
this._enableIceTrickle = true;
/**
* The current state if datachannel is enabled.
* @attribute _enableDataChannel
* @type Boolean
* @default true
* @private
* @required
* @since 0.3.0
*/
this._enableDataChannel = true;
/**
* The user stream settings.
* - By default, all is false.
* @attribute _streamSettings
* @type JSON
* @default {
* 'audio' : false,
* 'video' : false
* }
* @private
* @since 0.2.0
*/
this._streamSettings = {
audio: false,
video: false
};
/**
* Gets information from api server.
* @method _requestServerInfo
* @param {String} method The http method.
* @param {String} url The url to do a rest call.
* @param {Function} callback The callback fired after Skyway
* receives a response from the api server.
* @param {JSON} params HTTP Params
* @private
* @since 0.2.0
*/
this._requestServerInfo = function(method, url, callback, params) {
var xhr = new window.XMLHttpRequest();
console.info('XHR - Fetching infos from webserver');
xhr.onreadystatechange = function() {
if (this.readyState === this.DONE) {
console.info('XHR - Got infos from webserver.');
if (this.status !== 200) {
console.info('XHR - ERROR ' + this.status, false);
}
console.info(JSON.parse(this.response) || '{}');
callback(this.status, JSON.parse(this.response || '{}'));
}
};
xhr.open(method, url, true);
if (params) {
console.info(params);
xhr.setRequestHeader('Content-type', 'application/json;charset=UTF-8');
xhr.send(JSON.stringify(params));
} else {
xhr.send();
}
};
/**
* Parse the information received from the api server.
* @method _parseInfo
* @param {JSON} info The parsed information from the server.
* @param {Skyway} self Skyway object.
* @trigger readyStateChange
* @private
* @required
* @since 0.1.0
*/
this._parseInfo = function(info, self) {
console.log(info);
if (!info.pc_constraints && !info.offer_constraints) {
self._trigger('readyStateChange', self.READY_STATE_CHANGE.ERROR, {
status: 200,
content: info.info,
errorCode: info.error
});
return;
}
console.log(JSON.parse(info.pc_constraints));
console.log(JSON.parse(info.offer_constraints));
self._key = info.cid;
self._user = {
id: info.username,
token: info.userCred,
timeStamp: info.timeStamp,
apiOwner: info.apiOwner,
streams: [],
info: {}
};
self._room = {
id: info.room_key,
token: info.roomCred,
start: info.start,
len: info.len,
signalingServer: {
ip: info.ipSigserver,
port: info.portSigserver,
protocol: info.protocol
},
pcHelper: {
pcConstraints: JSON.parse(info.pc_constraints),
pcConfig: null,
offerConstraints: JSON.parse(info.offer_constraints),
sdpConstraints: {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
}
}
};
self._readyState = 2;
self._trigger('readyStateChange', self.READY_STATE_CHANGE.COMPLETED);
console.info('API - Parsed infos from webserver. Ready.');
};
/**
* Start the loading of information from the api server.
* @method _loadInfo
* @param {Skyway} self Skyway object.
* @trigger readyStateChange
* @private
* @required
* @since 0.1.0
*/
this._loadInfo = function(self) {
if (!window.io) {
console.error('API - Socket.io not loaded.');
self._trigger('readyStateChange', self.READY_STATE_CHANGE.ERROR, {
status: null,
content: 'Socket.io not found',
errorCode: self.READY_STATE_CHANGE_ERROR.NO_SOCKET_IO
});
return;
}
if (!window.XMLHttpRequest) {
console.error('XHR - XMLHttpRequest not supported');
self._trigger('readyStateChange', self.READY_STATE_CHANGE.ERROR, {
status: null,
content: 'XMLHttpRequest not available',
errorCode: self.READY_STATE_CHANGE_ERROR.NO_XMLHTTPREQUEST_SUPPORT
});
return;
}
if (!window.RTCPeerConnection) {
console.error('RTC - WebRTC not supported.');
self._trigger('readyStateChange', self.READY_STATE_CHANGE.ERROR, {
status: null,
content: 'WebRTC not available',
errorCode: self.READY_STATE_CHANGE_ERROR.NO_WEBRTC_SUPPORT
});
return;
}
if (!self._path) {
console.error('API - No connection info. Call init() first.');
self._trigger('readyStateChange', self.READY_STATE_CHANGE.ERROR, {
status: null,
content: 'No API Path is found',
errorCode: self.READY_STATE_CHANGE_ERROR.NO_PATH
});
return;
}
self._readyState = 1;
self._trigger('readyStateChange', self.READY_STATE_CHANGE.LOADING);
self._requestServerInfo('GET', self._path, function(status, response) {
if (status !== 200) {
// 403 - Room is locked
// 401 - API Not authorized
// 402 - run out of credits
var errorMessage = 'XMLHttpRequest status not OK\nStatus was: ' + status;
self._readyState = 0;
self._trigger('readyStateChange', self.READY_STATE_CHANGE.ERROR, {
status: status,
content: (response) ? (response.info || errorMessage) : errorMessage,
errorCode: response.error ||
self.READY_STATE_CHANGE_ERROR.INVALID_XMLHTTPREQUEST_STATUS
});
console.error(errorMessage);
return;
}
console.info(response);
self._parseInfo(response, self);
});
console.log('API - Waiting for webserver to provide infos.');
};
}
this.Skyway = Skyway;
/**
* To register a callback function to an event.
* @method on
* @param {String} eventName The Skyway event.
* @param {Function} callback The callback fired after the event is triggered.
* @example
* SkywayDemo.on('peerJoined', function (peerId, peerInfo) {
* console.info(peerId + ' has joined the room');
* console.log('Peer information are:');
* console.info(peerInfo);
* });
* @since 0.1.0
*/
Skyway.prototype.on = function(eventName, callback) {
if ('function' === typeof callback) {
this._events[eventName] = this._events[eventName] || [];
this._events[eventName].push(callback);
}
};
/**
* To unregister a callback function from an event.
* @method off
* @param {String} eventName The Skyway event.
* @param {Function} callback The callback fired after the event is triggered.
* @example
* SkywayDemo.off('peerJoined', callback);
* @since 0.1.0
*/
Skyway.prototype.off = function(eventName, callback) {
if (callback === undefined) {
this._events[eventName] = [];
return;
}
var arr = this._events[eventName],
l = arr.length;
for (var i = 0; i < l; i++) {
if (arr[i] === callback) {
arr.splice(i, 1);
break;
}
}
};
/**
* Trigger all the callbacks associated with an event.
* - Note that extra arguments can be passed to the callback which
* extra argument can be expected by callback is documented by each event.
* @method _trigger
* @param {String} eventName The Skyway event.
* @for Skyway
* @private
* @since 0.1.0
*/
Skyway.prototype._trigger = function(eventName) {
var args = Array.prototype.slice.call(arguments),
arr = this._events[eventName];
args.shift();
if (arr) {
for (var e in arr) {
if (arr.hasOwnProperty(e)) {
try {
if (arr[e].apply(this, args) === false) {
break;
}
} catch(error) {
console.warn(error);
}
}
}
}
};
/**
* Intiailize Skyway to retrieve connection information.
* - <b><i>IMPORTANT</i></b>: Please call this method to load all server
* information before joining the room or doing anything else.
* - If you would like to set the start time and duration of the room,
* you have to generate the credentials. In example 3, we use the
* [CryptoJS](https://code.google.com/p/crypto-js/) library.
* - Step 1: Generate the hash. It is created by using the roomname,
* duration and the timestamp (in ISO String format).
* - Step 2: Generate the Credentials. It is is generated by converting
* the hash to a Base64 string and then encoding it to a URI string.
* - Step 3: Initialize Skyway
* @method init
* @param {String|JSON} options Connection options or API Key ID
* @param {String} options.apiKey API Key ID to identify with the Temasys
* backend server
* @param {String} options.defaultRoom Optional. The default room to connect
* to if there is no room provided in
* {{#crossLink "Skyway/joinRoom:method"}}joinRoom(){{/crossLink}}.
* @param {String} options.roomServer Optional. Path to the Temasys
* backend server. If there's no room provided, default room would be used.
* @param {String} options.region Optional. The regional server that user
* chooses to use. [Rel: Skyway.REGIONAL_SERVER]
* @param {Boolean} options.iceTrickle Optional. The option to enable
* ICE trickle or not.
* - Default is true.
* @param {Boolean} options.dataChannel Optional. The option to enable
* datachannel or not.
* - Default is true.
* @param {JSON} options.credentials Optional. Credentials options for
* setting a static meeting.
* @param {String} options.credentials.startDateTime The start timing of the
* meeting in Date ISO String
* @param {Integer} options.credentials.duration The duration of the meeting
* @param {String} options.credentials.credentials The credentials required
* to set the timing and duration of a meeting.
* @example
* // Note: Default room is apiKey when no room
* // Example 1: To initalize without setting any default room.
* SkywayDemo.init('apiKey');
*
* // Example 2: To initialize with apikey, roomServer and defaultRoom
* SkywayDemo.init({
* 'apiKey' : 'apiKey',
* 'roomServer' : 'http://xxxx.com',
* 'defaultRoom' : 'mainHangout'
* });
*
* // Example 3: To initialize with credentials to set startDateTime and
* // duration of the room
* var hash = CryptoJS.HmacSHA1(roomname + '_' + duration + '_' +
* (new Date()).toISOString(), token);
* var credentials = encodeURIComponent(hash.toString(CryptoJS.enc.Base64));
* SkywayDemo.init({
* 'apiKey' : 'apiKey',
* 'roomServer' : 'http://xxxx.com',
* 'defaultRoom' : 'mainHangout'
* 'credentials' : {
* 'startDateTime' : (new Date()).toISOString(),
* 'duration' : 500,
* 'credentials' : credentials
* }
* });
* @trigger readyStateChange
* @for Skyway
* @required
* @since 0.3.0
*/
Skyway.prototype.init = function(options) {
if (!options) {
console.error('API - No apiKey is inputted');
return;
}
var apiKey, room, defaultRoom;
var startDateTime, duration, credentials;
var roomserver = this._serverPath;
var region;
var iceTrickle = true;
var dataChannel = true;
if (typeof options === 'string') {
apiKey = options;
defaultRoom = apiKey;
room = apiKey;
} else {
apiKey = options.apiKey;
roomserver = options.roomServer || roomserver;
roomserver = (roomserver.lastIndexOf('/') ===
(roomserver.length - 1)) ? roomserver.substring(0,
roomserver.length - 1) : roomserver;
region = options.region || region;
defaultRoom = options.defaultRoom || apiKey;
room = defaultRoom;
iceTrickle = (typeof options.iceTrickle === 'boolean') ?
options.iceTrickle : iceTrickle;
dataChannel = (typeof options.dataChannel === 'boolean') ?
options.dataChannel : dataChannel;
// Custom default meeting timing and duration
// Fallback to default if no duration or startDateTime provided
if (options.credentials) {
startDateTime = options.credentials.startDateTime ||
(new Date()).toISOString();
duration = options.credentials.duration || 200;
credentials = options.credentials.credentials;
}
}
this._readyState = 0;
this._trigger('readyStateChange', this.READY_STATE_CHANGE.INIT);
this._apiKey = apiKey;
this._roomServer = roomserver;
this._defaultRoom = defaultRoom;
this._selectedRoom = room;
this._serverRegion = region;
this._enableIceTrickle = iceTrickle;
this._enableDataChannel = dataChannel;
this._path = roomserver + '/api/' + apiKey + '/' + room;
if (credentials) {
this._roomStart = startDateTime;
this._roomDuration = duration;
this._roomCredentials = credentials;
this._path += (credentials) ? ('/' + startDateTime + '/' +
duration + '?&cred=' + credentials) : '';
}
if (region) {
this._path += ((this._path.indexOf('?&') > -1) ?
'&' : '?&') + 'rg=' + region;
}
console.log('API - Path: ' + this._path);
console.info('API - ICE Trickle: ' + ((typeof options.iceTrickle ===
'boolean') ? options.iceTrickle : '[Default: true]'));
this._loadInfo(this);
};
/**
* Initialize Skyway to retrieve new connection information bbasd on options.
* @method _reinit
* @param {String|JSON} options Connection options or API Key ID
* @param {String} options.apiKey API Key ID to identify with the Temasys
* backend server
* @param {String} options.defaultRoom Optional. The default room to connect to
* if there is no room provided in
* {{#crossLink "Skyway/joinRoom:method"}}joinRoom(){{/crossLink}}.
* @param {String} options.roomServer Optional. Path to the Temasys
* backend server. If there's no room provided, default room would be used.
* @param {String} options.region Optional. The regional server that user
* chooses to use. [Rel: Skyway.REGIONAL_SERVER]
* @param {Boolean} options.iceTrickle Optional. The option to enable
* ICE trickle or not.
* - Default is true.
* @param {Boolean} options.dataChannel Optional. The option to enable
* datachannel or not.
* - Default is true.
* @param {JSON} options.credentials Optional. Credentials options for
* setting a static meeting.
* @param {String} options.credentials.startDateTime The start timing of the
* meeting in date ISO String
* @param {Integer} options.credentials.duration The duration of the meeting
* @param {String} options.credentials.credentials The credentials required
* to set the timing and duration of a meeting.
* @param {Function} callback The callback fired once Skyway is re-initialized.
* @trigger readyStateChange
* @private
* @since 0.4.0
*/
Skyway.prototype._reinit = function(options, callback) {
var self = this;
var startDateTime, duration, credentials;
var apiKey = options.apiKey || self._apiKey;
var roomserver = options.roomServer || self._roomServer;
roomserver = (roomserver.lastIndexOf('/') ===
(roomserver.length - 1)) ? roomserver.substring(0,
roomserver.length - 1) : roomserver;
var region = options.region || self._serverRegion;
var defaultRoom = options.defaultRoom || self._defaultRoom;
var room = options.room || defaultRoom;
var iceTrickle = (typeof options.iceTrickle === 'boolean') ?
options.iceTrickle : self._enableIceTrickle;
var dataChannel = (typeof options.dataChannel === 'boolean') ?
options.dataChannel : self._enableDataChannel;
if (options.credentials) {
startDateTime = options.credentials.startDateTime ||
(new Date()).toISOString();
duration = options.credentials.duration || 500;
credentials = options.credentials.credentials ||
self._roomCredentials;
} else if (self._roomCredentials) {
startDateTime = self._roomStart;
duration = self._roomDuration;
credentials = self._roomCredentials;
}
self._apiKey = apiKey;
self._roomServer = roomserver;
self._defaultRoom = defaultRoom;
self._selectedRoom = room;
self._serverRegion = region;
self._enableIceTrickle = iceTrickle;
self._enableDataChannel = dataChannel;
self._path = roomserver + '/api/' + apiKey + '/' + room;
if (credentials) {
self._roomStart = startDateTime;
self._roomDuration = duration;
self._roomCredentials = credentials;
self._path += (credentials) ? ('/' + startDateTime + '/' +
duration + '?&cred=' + credentials) : '';
}
if (region) {
self._path += ((self._path.indexOf('?&') > -1) ?
'&' : '?&') + 'rg=' + region;
}
console.log('API - Path: ' + this._path);
console.info('API - ICE Trickle: ' + ((typeof options.iceTrickle ===
'boolean') ? options.iceTrickle : '[Default: true]'));
self._requestServerInfo('GET', self._path, function(status, response) {
if (status !== 200) {
var errorMessage = 'XMLHttpRequest status not OK.\nStatus was: ' + status;
self._readyState = 0;
self._trigger('readyStateChange', self.READY_STATE_CHANGE.ERROR, {
status: status,
content: (response) ? (response.info || errorMessage) : errorMessage,
errorCode: response.error ||
self.READY_STATE_CHANGE_ERROR.INVALID_XMLHTTPREQUEST_STATUS
});
console.error(errorMessage);
return;
}
console.info(response);
var info = response;
try {
self._key = info.cid;
self._user = {
id: info.username,
token: info.userCred,
timeStamp: info.timeStamp,
apiOwner: info.apiOwner,
streams: []
};
self._room = {
id: info.room_key,
token: info.roomCred,
start: info.start,
len: info.len,
signalingServer: {
ip: info.ipSigserver,
port: info.portSigserver,
protocol: info.protocol
},
pcHelper: {
pcConstraints: JSON.parse(info.pc_constraints),
pcConfig: null,
offerConstraints: JSON.parse(info.offer_constraints),
sdpConstraints: {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
}
}
};
callback();
} catch (error) {
self._readyState = 0;
self._trigger('readyStateChange', self.READY_STATE_CHANGE.ERROR, {
status: null,
content: error,
errorCode: self.READY_STATE_CHANGE_ERROR.SCRIPT_ERROR
});
console.error('API - Error occurred rejoining room');
console.error(error);
return;
}
});
};
/**
* Updates the user custom data.
* - Please note that the custom data would be overrided so please call
* {{#crossLink "Skyway/getUserData:method"}}getUserData(){{/crossLink}}
* and then modify the information you want individually.
* - {{#crossLink "Skyway/peerUpdated:event"}}peerUpdated{{/crossLink}}
* only fires after <b>setUserData()</b> is fired.
* after the user joins the room.
* @method setUserData
* @param {JSON|String} userData User custom data.
* @example
* // Example 1: Intial way of setting data before user joins the room
* SkywayDemo.setUserData({
* displayName: 'Bobby Rays',
* fbUserId: 'blah'
* });
*
* // Example 2: Way of setting data after user joins the room
* var userData = SkywayDemo.getUserData();
* userData.userData.displayName = 'New Name';
* userData.userData.fbUserId = 'another Id';
* SkywayDemo.setUserData(userData);
* @trigger peerUpdated
* @since 0.4.1
*/
Skyway.prototype.setUserData = function(userData) {
var self = this;
// NOTE ALEX: be smarter and copy fields and only if different
if (self._readyState === self.READY_STATE_CHANGE.COMPLETED) {
self._user.info = self._user.info || {};
self._user.info.userData = userData ||
self._user.info.userData || {};
if (self._in_room) {
self._sendMessage({
type: self.SIG_TYPE.UPDATE_USER,
mid: self._user.sid,
rid: self._room.id,
userData: self._user.info.userData
});
self._trigger('peerUpdated', self._user.sid, self._user.info, true);
} else {
console.warn('API - User is not in the room. Broadcast of' +
' updated information will be dropped.');
}
} else {
var checkInRoom = setInterval(function () {
if (self._readyState === self.READY_STATE_CHANGE.COMPLETED) {
clearInterval(checkInRoom);
self.setUserData(userData);
}
}, 50);
}
};
/**
* Gets the user custom data.
* @method getUserData
* @return {JSON|String} User custom data.
* @example
* var userInfo = SkywayDemo.getUserData();
* @since 0.4.0
*/
Skyway.prototype.getUserData = function() {
return (this._user) ?
((this._user.info) ? (this._user.info.userData || '')
: '') : '';
};
/**
* Gets the peer information.
* - If input peerId is user's id or empty, <b>getPeerInfo()</b>
* would return user's peer information.
* @method getPeerInfo
* @param {String} peerId PeerId of the peer information to retrieve.
* @return {JSON} Peer information.
* @example
* // Example 1: To get other peer's information
* var peerInfo = SkywayDemo.getPeerInfo(peerId);
*
* // Example 2: To get own information
* var userInfo = SkywayDemo.getPeerInfo();
* @since 0.4.0
*/
Skyway.prototype.getPeerInfo = function(peerId) {
return (peerId && peerId !== this._user.sid) ?
this._peerInformations[peerId] :
((this._user) ? this._user.info : null);
};
/* Syntactically private variables and utility functions */
Skyway.prototype._events = {
/**
* Event fired when the socket connection to the signaling
* server is open.
* @event channelOpen
* @since 0.1.0
*/
'channelOpen': [],
/**
* Event fired when the socket connection to the signaling
* server has closed.
* @event channelClose
* @since 0.1.0
*/
'channelClose': [],
/**
* Event fired when the socket connection received a message
* from the signaling server.
* @event channelMessage
* @param {JSON} message
* @since 0.1.0
*/
'channelMessage': [],
/**
* Event fired when the socket connection has occurred an error.
* @event channelError
* @param {Object|String} error Error message or object thrown.
* @since 0.1.0
*/
'channelError': [],
/**
* Event fired whether the room is ready for use.
* @event readyStateChange
* @param {String} readyState [Rel: Skyway.READY_STATE_CHANGE]
* @param {JSON} error Error object thrown.
* @param {Integer} error.status Http status when retrieving information.
* May be empty for other errors.
* @param {String} error.content Error message.
* @param {Integer} error.errorCode Error code.
* [Rel: Skyway.READY_STATE_CHANGE_ERROR]
* @since 0.4.0
*/
'readyStateChange': [],
/**
* Event fired when a peer's handshake progress has changed.
* @event handshakeProgress
* @param {String} step The handshake progress step.
* [Rel: Skyway.HANDSHAKE_PROGRESS]
* @param {String} peerId PeerId of the peer's handshake progress.
* @param {Object|String} error Error message or object thrown.
* @since 0.3.0
*/
'handshakeProgress': [],
/**
* Event fired when an ICE gathering state has changed.
* @event candidateGenerationState
* @param {String} state The ice candidate generation state.
* [Rel: Skyway.CANDIDATE_GENERATION_STATE]
* @param {String} peerId PeerId of the peer that had an ice candidate
* generation state change.
* @since 0.1.0
*/
'candidateGenerationState': [],
/**
* Event fired when a peer Connection state has changed.
* @event peerConnectionState
* @param {String} state The peer connection state.
* [Rel: Skyway.PEER_CONNECTION_STATE]
* @param {String} peerId PeerId of the peer that had a peer connection state
* change.
* @since 0.1.0
*/
'peerConnectionState': [],
/**
* Event fired when an ICE connection state has changed.
* @iceConnectionState
* @param {String} state The ice connection state.
* [Rel: Skyway.ICE_CONNECTION_STATE]
* @param {String} peerId PeerId of the peer that had an ice connection state change.
* @since 0.1.0
*/
'iceConnectionState': [],
/**
* Event fired when webcam or microphone media access fails.
* @event mediaAccessError
* @param {Object|String} error Error object thrown.
* @since 0.1.0
*/
'mediaAccessError': [],
/**
* Event fired when webcam or microphone media acces passes.
* @event mediaAccessSuccess
* @param {Object} stream MediaStream object.
* @since 0.1.0
*/
'mediaAccessSuccess': [],
/**
* Event fired when a peer joins the room.
* @event peerJoined
* @param {String} peerId PeerId of the peer that joined the room.
* @param {JSON} peerInfo Peer's information.
* @param {JSON} peerInfo.settings Peer's stream settings.
* @param {Boolean|JSON} peerInfo.settings.audio Peer's audio stream
* settings.
* @param {Boolean} peerInfo.settings.audio.stereo If peer has stereo
* enabled or not.
* @param {Boolean|JSON} peerInfo.settings.video Peer's video stream
* settings.
* @param {JSON} peerInfo.settings.video.resolution
* Peer's video stream resolution [Rel: Skyway.VIDEO_RESOLUTION]
* @param {Integer} peerInfo.settings.video.resolution.width
* Peer's video stream resolution width.
* @param {Integer} peerInfo.settings.video.resolution.height
* Peer's video stream resolution height.
* @param {Integer} peerInfo.settings.video.frameRate
* Peer's video stream resolution minimum frame rate.
* @param {JSON} peerInfo.mediaStatus Peer stream status.
* @param {Boolean} peerInfo.mediaStatus.audioMuted If peer's audio
* stream is muted.
* @param {Boolean} peerInfo.mediaStatus.videoMuted If peer's video
* stream is muted.
* @param {JSON|String} peerInfo.userData Peer's custom user data.
* @param {Boolean} isSelf Is the peer self.
* @since 0.3.0
*/
'peerJoined': [],
/**
* Event fired when a peer information is updated.
* @event peerUpdated
* @param {String} peerId PeerId of the peer that had information updaed.
* @param {JSON} peerInfo Peer's information.
* @param {JSON} peerInfo.settings Peer's stream settings.
* @param {Boolean|JSON} peerInfo.settings.audio Peer's audio stream
* settings.
* @param {Boolean} peerInfo.settings.audio.stereo If peer has stereo
* enabled or not.
* @param {Boolean|JSON} peerInfo.settings.video Peer's video stream
* settings.
* @param {JSON} peerInfo.settings.video.resolution
* Peer's video stream resolution [Rel: Skyway.VIDEO_RESOLUTION]
* @param {Integer} peerInfo.settings.video.resolution.width
* Peer's video stream resolution width.
* @param {Integer} peerInfo.settings.video.resolution.height
* Peer's video stream resolution height.
* @param {Integer} peerInfo.settings.video.frameRate
* Peer's video stream resolution minimum frame rate.
* @param {JSON} peerInfo.mediaStatus Peer stream status.
* @param {Boolean} peerInfo.mediaStatus.audioMuted If peer's audio
* stream is muted.
* @param {Boolean} peerInfo.mediaStatus.videoMuted If peer's video
* stream is muted.
* @param {JSON|String} peerInfo.userData Peer's custom user data.
* @param {Boolean} isSelf Is the peer self.
* @since 0.3.0
*/
'peerUpdated': [],
/**
* Event fired when a peer leaves the room
* @event peerLeft
* @param {String} peerId PeerId of the peer that left.
* @param {JSON} peerInfo Peer's information.
* @param {JSON} peerInfo.settings Peer's stream settings.
* @param {Boolean|JSON} peerInfo.settings.audio Peer's audio stream
* settings.
* @param {Boolean} peerInfo.settings.audio.stereo If peer has stereo
* enabled or not.
* @param {Boolean|JSON} peerInfo.settings.video Peer's video stream
* settings.
* @param {JSON} peerInfo.settings.video.resolution
* Peer's video stream resolution [Rel: Skyway.VIDEO_RESOLUTION]
* @param {Integer} peerInfo.settings.video.resolution.width
* Peer's video stream resolution width.
* @param {Integer} peerInfo.settings.video.resolution.height
* Peer's video stream resolution height.
* @param {Integer} peerInfo.settings.video.frameRate
* Peer's video stream resolution minimum frame rate.
* @param {JSON} peerInfo.mediaStatus Peer stream status.
* @param {Boolean} peerInfo.mediaStatus.audioMuted If peer's audio
* stream is muted.
* @param {Boolean} peerInfo.mediaStatus.videoMuted If peer's video
* stream is muted.
* @param {JSON|String} peerInfo.userData Peer's custom user data.
* @param {Boolean} isSelf Is the peer self.
* @since 0.3.0
*/
'peerLeft': [],
/**
* TODO Event fired when a peer joins the room
* @event presenceChanged
* @param {JSON} users The list of users
* @private
* @deprecated
* @since 0.1.0
*/
'presenceChanged': [],
//-- per peer, peer connection events
/**
* Event fired when a remote stream has become available.
* - This occurs after the user joins the room.
* - This is changed from <b>addPeerStream</b> event.
* - Note that <b>addPeerStream</b> is removed from the specs.
* @event incomingStream
* @param {Object} stream MediaStream object.
* @param {String} peerId PeerId of the peer that is sending the stream.
* @param {Boolean} isSelf Is the peer self.
* @since 0.4.0
*/
'incomingStream': [],
/**
* Event fired when a message being broadcasted is received.
* - This is changed from <b>chatMessageReceived</b>,
* <b>privateMessage</b> and <b>publicMessage</b> event.
* - Note that <b>chatMessageReceived</b>, <b>privateMessage</b>
* and <b>publicMessage</b> is removed from the specs.
* @event incomingMessage
* @param {JSON} message Message object that is received.
* @param {JSON|String} message.content Data that is broadcasted.
* @param {String} message.senderPeerId PeerId of the sender peer.
* @param {String} message.targetPeerId PeerId that is specifically
* targeted to receive the message.
* @param {Boolean} message.isPrivate Is data received a private message.
* @param {Boolean} message.isDataChannel Is data received from a
* data channel.
* @param {String} peerId PeerId of the sender peer.
* @param {JSON} peerInfo Peer's information.
* @param {JSON} peerInfo.settings Peer's stream settings.
* @param {Boolean|JSON} peerInfo.settings.audio Peer's audio stream
* settings.
* @param {Boolean} peerInfo.settings.audio.stereo If peer has stereo
* enabled or not.
* @param {Boolean|JSON} peerInfo.settings.video Peer's video stream
* settings.
* @param {JSON} peerInfo.settings.video.resolution
* Peer's video stream resolution [Rel: Skyway.VIDEO_RESOLUTION]
* @param {Integer} peerInfo.settings.video.resolution.width
* Peer's video stream resolution width.
* @param {Integer} peerInfo.settings.video.resolution.height
* Peer's video stream resolution height.
* @param {Integer} peerInfo.settings.video.frameRate
* Peer's video stream resolution minimum frame rate.
* @param {JSON} peerInfo.mediaStatus Peer stream status.
* @param {Boolean} peerInfo.mediaStatus.audioMuted If peer's audio
* stream is muted.
* @param {Boolean} peerInfo.mediaStatus.videoMuted If peer's video
* stream is muted.
* @param {JSON|String} peerInfo.userData Peer's custom user data.
* @param {Boolean} isSelf Is the peer self.
* @since 0.4.1
*/
'incomingMessage': [],
/**
* Event fired when a room lock status has changed.
* @event roomLock
* @param {Boolean} isLocked Is the room locked.
* @param {String} peerId PeerId of the peer that is locking/unlocking
* the room.
* @param {JSON} peerInfo Peer's information.
* @param {JSON} peerInfo.settings Peer's stream settings.
* @param {Boolean|JSON} peerInfo.settings.audio Peer's audio stream
* settings.
* @param {Boolean} peerInfo.settings.audio.stereo If peer has stereo
* enabled or not.
* @param {Boolean|JSON} peerInfo.settings.video Peer's video stream
* settings.
* @param {JSON} peerInfo.settings.video.resolution
* Peer's video stream resolution [Rel: Skyway.VIDEO_RESOLUTION]
* @param {Integer} peerInfo.settings.video.resolution.width
* Peer's video stream resolution width.
* @param {Integer} peerInfo.settings.video.resolution.height
* Peer's video stream resolution height.
* @param {Integer} peerInfo.settings.video.frameRate
* Peer's video stream resolution minimum frame rate.
* @param {JSON} peerInfo.mediaStatus Peer stream status.
* @param {Boolean} peerInfo.mediaStatus.audioMuted If peer's audio
* stream is muted.
* @param {Boolean} peerInfo.mediaStatus.videoMuted If peer's video
* stream is muted.
* @param {JSON|String} peerInfo.userData Peer's custom user data.
* @param {Boolean} isSelf Is the peer self.
* @since 0.4.0
*/
'roomLock': [],
//-- data state events
/**
* Event fired when a peer's datachannel state has changed.
* @event dataChannelState
* @param {String} state The datachannel state.
* [Rel: Skyway.DATA_CHANNEL_STATE]
* @param {String} peerId PeerId of peer that has a datachannel
* state change.
* @since 0.1.0
*/
'dataChannelState': [],
/**
* Event fired when a data transfer state has changed.
* - Note that <u>transferInfo.data</u> sends the blob data, and
* no longer a blob url.
* @event dataTransferState
* @param {String} state The data transfer state.
* [Rel: Skyway.DATA_TRANSFER_STATE]
* @param {String} transferId TransferId of the data.
* @param {String} peerId PeerId of the peer that has a data
* transfer state change.
* @param {JSON} transferInfo Data transfer information.
* @param {JSON} transferInfo.percentage The percetange of data being
* uploaded / downloaded.
* @param {JSON} transferInfo.senderPeerId PeerId of the sender.
* @param {JSON} transferInfo.data The blob data. See the
* [createObjectURL](https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL)
* method on how you can convert the blob to a download link.
* @param {JSON} transferInfo.name Data name.
* @param {JSON} transferInfo.size Data size.
* @param {JSON} error The error object.
* @param {String} error.message Error message thrown.
* @param {String} error.transferType Is error from uploading or downloading.
* [Rel: Skyway.DATA_TRANSFER_TYPE]
* @since 0.4.1
*/
'dataTransferState': [],
/**
* Event fired when the signaling server warns the user.
* @event systemAction
* @param {String} action The action that is required for
* the user to follow. [Rel: Skyway.SYSTEM_ACTION]
* @param {String} message The reason for the action.
* @since 0.1.0
*/
'systemAction': []
};
Skyway.prototype._dataChannelEvents = {
/**
* Fired when a datachannel has a blob data send request.
* @event Datachannel: WRQ
* @param {String} userAgent The user's browser agent.
* @param {String} name The blob data name.
* @param {Integer} size The blob data size.
* @param {Integer} chunkSize The expected chunk size.
* @param {Integer} timeout The timeout in seconds.
* @private
* @since 0.4.0
*/
'WRQ': [],
/**
* Fired when a datachannel has a blob data send request acknowledgement.
* - 0: User accepts the request.
* - -1: User rejects the request.
* - Above 0: User acknowledges the blob data packet.
* @event Datachannel: ACK
* @param {Integer} ackN The acknowledge number.
* @param {Integer} userAgent The user's browser agent.
* @private
* @since 0.4.0
*/
'ACK': [],
/**
* Fired when a datachannel transfer has an error occurred.
* @event Datachannel: ERROR
* @param {String} message The error message.
* @param {Boolean} isSender If user's the uploader.
* @private
* @since 0.4.0
*/
'ERROR': [],
/**
* Fired when a datachannel chat has been received.
* @event Datachannel: CHAT
* @param {String} type If the message is a private or group message.
* - PRIVATE: This message is a private message targeted to a peer.
* - GROUP: This message is to be sent to all peers.
* @param {String} peerId PeerId of the sender.
* @param {JSON|String} message The message data or object.
* @private
* @since 0.4.0
*/
'CHAT': []
};
/**
* Broadcast a message to all peers.
* - <b><i>WARNING</i></b>: Map arrays data would be lost when stringified
* in JSON, so refrain from using map arrays.
* @method sendMessage
* @param {String|JSON} message The message data to send.
* @param {String} targetPeerId PeerId of the peer to send a private
* message data to.
* @example
* // Example 1: Send to all peers
* SkywayDemo.sendMessage('Hi there!');
*
* // Example 2: Send to a targeted peer
* SkywayDemo.sendMessage('Hi there peer!', targetPeerId);
* @trigger incomingMessage
* @since 0.4.0
*/
Skyway.prototype.sendMessage = function(message, targetPeerId) {
var params = {
cid: this._key,
data: message,
mid: this._user.sid,
rid: this._room.id,
type: this.SIG_TYPE.PUBLIC_MESSAGE
};
if (targetPeerId) {
params.target = targetPeerId;
params.type = this.SIG_TYPE.PRIVATE_MESSAGE;
}
this._sendMessage(params);
this._trigger('incomingMessage', {
content: message,
isPrivate: (targetPeerId) ? true: false,
targetPeerId: targetPeerId || null,
isDataChannel: false,
senderPeerId: this._user.sid
}, this._user.sid, this._user.info, true);
};
/**
* Broadcasts to all P2P datachannel messages and sends to a
* peer only when targetPeerId is provided.
* - This is ideal for sending strings or json objects lesser than 16KB
* [as noted in here](http://www.webrtc.org/chrome).
* - For huge data, please check out function
* {{#crossLink "Skyway/sendBlobData:method"}}sendBlobData(){{/crossLink}}.
* - <b><i>WARNING</i></b>: Map arrays data would be lost when stringified
* in JSON, so refrain from using map arrays.
* @method sendP2PMessage
* @param {String|JSON} message The message data to send.
* @param {String} targetPeerId Optional. Provide if you want to send to
* only one peer
* @example
* // Example 1: Send to all peers
* SkywayDemo.sendP2PMessage('Hi there! This is from a DataChannel!');
*
* // Example 2: Send to specific peer
* SkywayDemo.sendP2PMessage('Hi there peer! This is from a DataChannel!', targetPeerId);
* @trigger incomingMessage
* @since 0.4.0
*/
Skyway.prototype.sendP2PMessage = function(message, targetPeerId) {
// Handle typeof object sent over
for (var peerId in this._dataChannels) {
if (this._dataChannels.hasOwnProperty(peerId)) {
if ((targetPeerId && targetPeerId === peerId) || !targetPeerId) {
this._sendDataChannel(peerId, ['CHAT', ((targetPeerId) ?
'PRIVATE' : 'GROUP'), this._user.sid,
((typeof message === 'object') ? JSON.stringify(message) :
message)]);
}
}
}
this._trigger('incomingMessage', {
content: message,
isPrivate: (targetPeerId) ? true : false,
targetPeerId: targetPeerId || null, // is not null if there's user
isDataChannel: true,
senderPeerId: this._user.sid
}, this._user.sid, this._user.info, true);
};
/**
* Gets the default webcam and microphone.
* - Please do not be confused with the [MediaStreamConstraints](http://dev.w3.
* org/2011/webrtc/editor/archives/20140817/getusermedia.html#dictionary
* -mediastreamconstraints-members) specified in the original w3c specs.
* - This is an implemented function for Skyway.
* @method getUserMedia
* @param {JSON} options Optional. MediaStream constraints.
* @param {JSON|Boolean} options.audio Option to allow audio stream.
* @param {Boolean} options.audio.stereo Option to enable stereo
* during call.
* @param {JSON|Boolean} options.video Option to allow video stream.
* @param {JSON} options.video.resolution The resolution of video stream.
* - Check out <a href="#attr_VIDEO_RESOLUTION">VIDEO_RESOLUTION</a>.
* @param {Integer} options.video.resolution.width
* The video stream resolution width.
* @param {Integer} options.video.resolution.height
* The video stream resolution height.
* @param {Integer} options.video.frameRate
* The video stream mininum frameRate.
* @example
* // Default is to get both audio and video
* // Example 1: Get both audio and video by default.
* SkywayDemo.getUserMedia();
*
* // Example 2: Get the audio stream only
* SkywayDemo.getUserMedia({
* 'video' : false,
* 'audio' : true
* });
*
* // Example 3: Set the stream settings for the audio and video
* SkywayDemo.getUserMedia({
* 'video' : {
* 'resolution': SkywayDemo.VIDEO_RESOLUTION.HD,
* 'frameRate': 50
* },
* 'audio' : {
* 'stereo': true
* }
* });
* @trigger mediaAccessSuccess, mediaAccessError
* @since 0.4.1
*/
Skyway.prototype.getUserMedia = function(options) {
var self = this;
var getStream = false;
options = options || {
audio: true,
video: true
};
// prevent undefined error
self._user = self._user || {};
self._user.info = self._user.info || {};
self._user.info.settings = self._user.info.settings || {};
self._user.streams = self._user.streams || [];
// called during joinRoom
if (self._user.info.settings) {
// So it would invoke to getMediaStream defaults
if (!options.video && !options.audio) {
console.warn('API - No streams requested. Request an audio/video or both.');
} else if (self._user.info.settings.audio !== options.audio ||
self._user.info.settings.video !== options.video) {
if (Object.keys(self._user.streams).length > 0) {
// NOTE: User's stream may hang.. so find a better way?
// NOTE: Also make a use case for multiple streams?
getStream = self._setStreams(options);
if (getStream) {
// NOTE: When multiple streams, streams should not be cleared.
self._user.streams = [];
}
} else {
getStream = true;
}
}
} else { // called before joinRoom
getStream = true;
}
self._parseStreamSettings(options);
if (getStream) {
try {
window.getUserMedia({
audio: self._streamSettings.audio,
video: self._streamSettings.video
}, function(stream) {
self._onUserMediaSuccess(stream, self);
}, function(error) {
self._onUserMediaError(error, self);
});
console.log('API [MediaStream] - Requested ' +
((self._streamSettings.audio) ? 'A' : '') +
((self._streamSettings.audio &&
self._streamSettings.video) ? '/' : '') +
((self._streamSettings.video) ? 'V' : ''));
} catch (error) {
this._onUserMediaError(error, self);
}
} else if (Object.keys(self._user.streams).length > 0) {
console.warn('API - User already has stream. Reactiving stream only.');
} else {
console.warn('API - Not retrieving stream.');
}
};
/**
* Access to user's MediaStream is successful.
* @method _onUserMediaSuccess
* @param {MediaStream} stream MediaStream object.
* @param {Skyway} self Skyway object.
* @trigger mediaAccessSuccess
* @private
* @since 0.3.0
*/
Skyway.prototype._onUserMediaSuccess = function(stream, self) {
console.log('API - User has granted access to local media.');
self._trigger('mediaAccessSuccess', stream);
var checkReadyState = setInterval(function () {
if (self._readyState === self.READY_STATE_CHANGE.COMPLETED) {
clearInterval(checkReadyState);
self._user.streams[stream.id] = stream;
self._user.streams[stream.id].active = true;
var checkIfUserInRoom = setInterval(function () {
if (self._in_room) {
clearInterval(checkIfUserInRoom);
self._trigger('incomingStream', self._user.sid, stream, true);
}
}, 500);
}
}, 500);
};
/**
* Access to user's MediaStream failed.
* @method _onUserMediaError
* @param {Object} error Error object that was thrown.
* @param {Skyway} self Skyway object.
* @trigger mediaAccessFailure
* @private
* @since 0.1.0
*/
Skyway.prototype._onUserMediaError = function(error, self) {
console.log('API - getUserMedia failed with exception type: ' +
(error.name || error));
if (error.message) {
console.log('API - getUserMedia failed with exception: ' + error.message);
}
if (error.constraintName) {
console.log('API - getUserMedia failed because of the following constraint: ' +
error.constraintName);
}
self._trigger('mediaAccessError', error);
};
/**
* Handles everu incoming signaling message received.
* - If it's a SIG_TYPE.GROUP message, break them down to single messages
* and let {{#crossLink "Skyway/_processSingleMessage:method"}}
* _processSingleMessage(){{/crossLink}} to handle them.
* @method _processSigMessage
* @param {String} messageString The message object stringified received.
* @private
* @since 0.1.0
*/
Skyway.prototype._processSigMessage = function(messageString) {
var message = JSON.parse(messageString);
if (message.type === this.SIG_TYPE.GROUP) {
console.log('API - Bundle of ' + message.lists.length + ' messages.');
for (var i = 0; i < message.lists.length; i++) {
this._processSingleMessage(message.lists[i]);
}
} else {
this._processSingleMessage(message);
}
};
/**
* Handles the single signaling message received.
* @method _processingSingleMessage
* @param {JSON} message The message object received.
* @private
* @since 0.1.0
*/
Skyway.prototype._processSingleMessage = function(message) {
this._trigger('channelMessage', message);
var origin = message.mid;
if (!origin || origin === this._user.sid) {
origin = 'Server';
}
console.log('API - [' + origin + '] Incoming message: ' + message.type);
if (message.mid === this._user.sid &&
message.type !== this.SIG_TYPE.REDIRECT &&
message.type !== this.SIG_TYPE.IN_ROOM) {
console.log('API - Ignoring message: ' + message.type + '.');
return;
}
switch (message.type) {
//--- BASIC API Messages ----
case this.SIG_TYPE.PUBLIC_MESSAGE:
this._publicMessageHandler(message);
break;
case this.SIG_TYPE.PRIVATE_MESSAGE:
this._privateMessageHandler(message);
break;
case this.SIG_TYPE.IN_ROOM:
this._inRoomHandler(message);
break;
case this.SIG_TYPE.ENTER:
this._enterHandler(message);
break;
case this.SIG_TYPE.WELCOME:
this._welcomeHandler(message);
break;
case this.SIG_TYPE.OFFER:
this._offerHandler(message);
break;
case this.SIG_TYPE.ANSWER:
this._answerHandler(message);
break;
case this.SIG_TYPE.CANDIDATE:
this._candidateHandler(message);
break;
case this.SIG_TYPE.BYE:
this._byeHandler(message);
break;
case this.SIG_TYPE.REDIRECT:
this._redirectHandler(message);
break;
case this.SIG_TYPE.ERROR:
this._errorHandler(message);
break;
//--- ADVANCED API Messages ----
case this.SIG_TYPE.UPDATE_USER:
this._updateUserEventHandler(message);
break;
case this.SIG_TYPE.MUTE_VIDEO:
this._muteVideoEventHandler(message);
break;
case this.SIG_TYPE.MUTE_AUDIO:
this._muteAudioEventHandler(message);
break;
case this.SIG_TYPE.ROOM_LOCK:
this._roomLockEventHandler(message);
break;
default:
console.log('API - [' + message.mid + '] Unsupported message type received: ' + message.type);
break;
}
};
/**
* Signaling server sends an error message.
* - SIG_TYPE: ERROR
* - This occurs when an error was thrown by the signaling server.
* @method _errorHandler
* @param {JSON} message The message object received.
* @param {String} message.rid RoomId of the connected room.
* @param {String} message.mid PeerId of the peer that is sending the error message.
* @param {String} message.kind The type of error.
* @param {String} message.type The type of message received.
* @private
* @deprecated
* @since 0.1.0
*/
Skyway.prototype._errorHandler = function(message) {
console.log('API - [Server] Error occurred: ' + message.kind);
// location.href = '/?error=' + message.kind;
};
/**
* Signaling server sends a redirect message.
* - SIG_TYPE: REDIRECT
* - This occurs when the signaling server is warning us or wanting
* to move us out when the peer sends too much messages at the
* same tme.
* @method _redirectHandler
* @param {JSON} message The message object received.
* @param {String} message.rid RoomId of the connected room.
* @param {String} message.url Deprecated. Url to redirect user to.
* @param {String} message.info The reason for this action.
* @param {String} message.action The action to work on.
* [Rel: Skyway.SYSTEM_ACTION]
* @param {String} message.type The type of message received.
* @trigger systemAction
* @private
* @since 0.1.0
*/
Skyway.prototype._redirectHandler = function(message) {
console.log('API - [Server]: ' + message.info);
this._trigger('systemAction', message.action, message.info);
};
/**
* Signaling server sends a updateUserEvent message.
* - SIG_TYPE: UPDATE_USER
* - This occurs when a peer's custom user data is updated.
* @method _updateUserEventHandler
* @param {JSON} message The message object received.
* @param {String} message.rid RoomId of the connected room.
* @param {String} message.mid PeerId of the peer that is sending the
* updated event.
* @param {String} message.userData The peer's user data.
* @param {String} message.type The type of message received.
* @trigger peerUpdated
* @private
* @since 0.2.0
*/
Skyway.prototype._updateUserEventHandler = function(message) {
var targetMid = message.mid;
console.log('API - [' + targetMid + '] received \'updateUserEvent\'.');
if (this._peerInformations[targetMid]) {
this._peerInformations[targetMid].userData = message.userData || {};
this._trigger('peerUpdated', targetMid,
this._peerInformations[targetMid], false);
}
};
/**
* Signaling server sends a roomLockEvent message.
* - SIG_TYPE: ROOM_LOCK
* - This occurs when a room lock status has changed.
* @method _roomLockEventHandler
* @param {JSON} message The message object received.
* @param {String} message.rid RoomId of the connected room.
* @param {String} message.mid PeerId of the peer that is sending the
* updated room lock status.
* @param {String} message.lock If room is locked or not.
* @param {String} message.type The type of message received.
* @trigger roomLock
* @private
* @since 0.2.0
*/
Skyway.prototype._roomLockEventHandler = function(message) {
var targetMid = message.mid;
console.log('API - [' + targetMid + '] received \'roomLockEvent\'.');
this._trigger('roomLock', message.lock, targetMid,
this._peerInformations[targetMid], false);
};
/**
* Signaling server sends a muteAudioEvent message.
* - SIG_TYPE: MUTE_AUDIO
* - This occurs when a peer's audio stream muted
* status has changed.
* @method _muteAudioEventHandler
* @param {JSON} message The message object received.
* @param {String} message.rid RoomId of the connected room.
* @param {String} message.mid PeerId of the peer that is sending
* their own updated audio stream status.
* @param {String} message.muted If audio stream is muted or not.
* @param {String} message.type The type of message received.
* @trigger peerUpdated
* @private
* @since 0.2.0
*/
Skyway.prototype._muteAudioEventHandler = function(message) {
var targetMid = message.mid;
console.log('API - [' + targetMid + '] received \'muteAudioEvent\'.');
if (this._peerInformations[targetMid]) {
this._peerInformations[targetMid].mediaStatus.audioMuted = message.muted;
this._trigger('peerUpdated', targetMid,
this._peerInformations[targetMid], false);
}
};
/**
* Signaling server sends a muteVideoEvent message.
* - SIG_TYPE: MUTE_VIDEO
* - This occurs when a peer's video stream muted
* status has changed.
* @method _muteVideoEventHandler
* @param {JSON} message The message object received.
* @param {String} message.rid RoomId of the connected room.
* @param {String} message.mid PeerId of the peer that is sending
* their own updated video streams status.
* @param {String} message.muted If video stream is muted or not.
* @param {String} message.type The type of message received.
* @trigger peerUpdated
* @private
* @since 0.2.0
*/
Skyway.prototype._muteVideoEventHandler = function(message) {
var targetMid = message.mid;
console.log('API - [' + targetMid + '] received \'muteVideoEvent\'.');
if (this._peerInformations[targetMid]) {
this._peerInformations[targetMid].mediaStatus.videoMuted = message.muted;
this._trigger('peerUpdated', targetMid,
this._peerInformations[targetMid], false);
}
};
/**
* Signaling server sends a bye message.
* - SIG_TYPE: BYE
* - This occurs when a peer left the room.
* @method _byeHandler
* @param {JSON} message The message object received.
* @param {String} message.rid RoomId of the connected room.
* @param {String} message.mid PeerId of the peer that has left the room.
* @param {String} message.type The type of message received.
* @trigger peerLeft
* @private
* @since 0.1.0
*/
Skyway.prototype._byeHandler = function(message) {
var targetMid = message.mid;
console.log('API - [' + targetMid + '] received \'bye\'.');
this._removePeer(targetMid);
};
/**
* Signaling server sends a privateMessage message.
* - SIG_TYPE: PRIVATE_MESSAGE
* - This occurs when a peer sends private message to user.
* @method _privateMessageHandler
* @param {JSON} message The message object received.
* @param {JSON|String} message.data The data received.
* @param {String} message.rid RoomId of the connected room.
* @param {String} message.cid CredentialId of the room.
* @param {String} message.mid PeerId of the peer that is sending a private
* broadcast message.
* @param {Boolean} message.isDataChannel Is the message sent from datachannel.
* @param {String} message.type The type of message received.
* @trigger privateMessage
* @private
* @since 0.4.0
*/
Skyway.prototype._privateMessageHandler = function(message) {
var targetMid = message.mid;
this._trigger('incomingMessage', {
content: message.data,
isPrivate: true,
targetPeerId: message.target, // is not null if there's user
isDataChannel: (message.isDataChannel) ? true : false,
senderPeerId: targetMid
}, targetMid, this._peerInformations[targetMid], false);
};
/**
* Signaling server sends a publicMessage message.
* - SIG_TYPE: PUBLIC_MESSAGE
* - This occurs when a peer broadcasts a public message to
* all connected peers.
* @method _publicMessageHandler
* @param {JSON} message The message object received.
* @param {JSON|String} message.data The data broadcasted
* @param {String} message.rid RoomId of the connected room.
* @param {String} message.cid CredentialId of the room.
* @param {String} message.mid PeerId of the peer that is sending a private
* broadcast message.
* @param {Boolean} message.isDataChannel Is the message sent from datachannel.
* @param {String} message.type The type of message received.
* @trigger publicMessage
* @private
* @since 0.4.0
*/
Skyway.prototype._publicMessageHandler = function(message) {
var targetMid = message.mid;
this._trigger('incomingMessage', {
content: message.data,
isPrivate: false,
targetPeerId: null, // is not null if there's user
isDataChannel: (message.isDataChannel) ? true : false,
senderPeerId: targetMid
}, targetMid, this._peerInformations[targetMid], false);
};
/**
* Signaling server sends an inRoom message.
* - SIG_TYPE: IN_ROOM
* - This occurs the user has joined the room.
* @method _inRoomHandler
* @param {JSON} message The message object received.
* @param {String} message.rid RoomId of the connected room.
* @param {String} message.sid PeerId of self.
* @param {String} message.mid PeerId of the peer that is
* sending the joinRoom message.
* @param {JSON} message.pc_config The peerconnection configuration.
* @param {String} message.type The type of message received.
* @trigger peerJoined
* @private
* @since 0.1.0
*/
Skyway.prototype._inRoomHandler = function(message) {
var self = this;
console.log('API - We\'re in the room! Chat functionalities are now available.');
console.log('API - We\'ve been given the following PC Constraint by the sig server: ');
console.dir(message.pc_config);
self._room.pcHelper.pcConfig = self._setFirefoxIceServers(message.pc_config);
self._in_room = true;
self._user.sid = message.sid;
self._trigger('peerJoined', self._user.sid, self._user.info, true);
// NOTE ALEX: should we wait for local streams?
// or just go with what we have (if no stream, then one way?)
// do we hardcode the logic here, or give the flexibility?
// It would be better to separate, do we could choose with whom
// we want to communicate, instead of connecting automatically to all.
var params = {
type: self.SIG_TYPE.ENTER,
mid: self._user.sid,
rid: self._room.id,
agent: window.webrtcDetectedBrowser.browser,
version: window.webrtcDetectedBrowser.version,
userInfo: self._user.info
};
console.log('API - Sending enter.');
self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ENTER, self._user.sid);
self._sendMessage(params);
};
/**
* Signaling server sends a enter message.
* - SIG_TYPE: ENTER
* - This occurs when a peer just entered the room.
* - If we don't have a connection with the peer, send a welcome.
* @method _enterHandler
* @param {JSON} message The message object received.
* @param {String} message.rid RoomId of the connected room.
* @param {String} message.mid PeerId of the peer that is sending the enter shake.
* @param {String} message.agent Peer's browser agent.
* @param {String} message.version Peer's browser version.
* @param {String} message.userInfo Peer's user information.
* @param {JSON} message.userInfo.settings Peer's stream settings
* @param {Boolean|JSON} message.userInfo.settings.audio
* @param {Boolean} message.userInfo.settings.audio.stereo
* @param {Boolean|JSON} message.userInfo.settings.video
* @param {JSON} message.userInfo.settings.video.resolution [Rel: Skyway.VIDEO_RESOLUTION]
* @param {Integer} message.userInfo.settings.video.resolution.width
* @param {Integer} message.userInfo.settings.video.resolution.height
* @param {Integer} message.userInfo.settings.video.frameRate
* @param {JSON} message.userInfo.mediaStatus Peer stream status.
* @param {Boolean} message.userInfo.mediaStatus.audioMuted If peer's audio stream is muted.
* @param {Boolean} message.userInfo.mediaStatus.videoMuted If peer's video stream is muted.
* @param {String|JSON} message.userInfo.userData Peer custom data
* @param {String} message.type The type of message received.
* @trigger handshakeProgress, peerJoined
* @private
* @since 0.1.0
*/
Skyway.prototype._enterHandler = function(message) {
var self = this;
var targetMid = message.mid;
// need to check entered user is new or not.
if (!self._peerConnections[targetMid]) {
message.agent = (!message.agent) ? 'Chrome' : message.agent;
var browserAgent = message.agent + ((message.version) ? ('|' + message.version) : '');
// should we resend the enter so we can be the offerer?
checkMediaDataChannelSettings(false, browserAgent, function(beOfferer) {
self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ENTER, targetMid);
var params = {
type: ((beOfferer) ? self.SIG_TYPE.ENTER : self.SIG_TYPE.WELCOME),
mid: self._user.sid,
rid: self._room.id,
agent: window.webrtcDetectedBrowser.browser,
userInfo: self._user.info
};
console.info(JSON.stringify(params));
if (!beOfferer) {
console.log('API - [' + targetMid + '] Sending welcome.');
self._peerInformations[targetMid] = message.userInfo;
self._trigger('peerJoined', targetMid, message.userInfo, false);
self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.WELCOME, targetMid);
params.target = targetMid;
}
self._sendMessage(params);
});
} else {
// NOTE ALEX: and if we already have a connection when the peer enter,
// what should we do? what are the possible use case?
console.log('API - Received "enter" when Peer "' + targetMid +
'" is already added.');
return;
}
};
/**
* Signaling server sends a welcome message.
* - SIG_TYPE: WELCOME
* - This occurs when we've just received a welcome.
* - If there is no existing connection with this peer,
* create one, then set the remotedescription and answer.
* @method _welcomeHandler
* @param {JSON} message The message object received.
* @param {String} message.rid RoomId of the connected room.
* @param {String} message.mid PeerId of the peer that is sending the welcome shake.
* @param {Boolean} message.receiveOnly Peer to receive only
* @param {Boolean} message.enableIceTrickle Option to enable Ice trickle or not
* @param {Boolean} message.enableDataChannel Option to enable DataChannel or not
* @param {JSON} message.userInfo Peer Skyway._user.info data.
* @param {JSON} message.userInfo.settings Peer stream settings
* @param {Boolean|JSON} message.userInfo.settings.audio
* @param {Boolean} message.userInfo.settings.audio.stereo
* @param {Boolean|JSON} message.userInfo.settings.video
* @param {JSON} message.userInfo.settings.video.resolution [Rel: Skyway.VIDEO_RESOLUTION]
* @param {Integer} message.userInfo.settings.video.resolution.width
* @param {Integer} message.userInfo.settings.video.resolution.height
* @param {Integer} message.userInfo.settings.video.frameRate
* @param {JSON} message.userInfo.mediaStatus Peer stream status.
* @param {Boolean} message.userInfo.mediaStatus.audioMuted If Peer's Audio stream is muted.
* @param {Boolean} message.userInfo.mediaStatus.videoMuted If Peer's Video stream is muted.
* @param {String|JSON} message.userInfo.userData Peer custom data
* @param {String} message.agent Browser agent
* @param {String} message.type The type of message received.
* @trigger handshakeProgress, peerJoined
* @private
* @since 0.1.0
*/
Skyway.prototype._welcomeHandler = function(message) {
var targetMid = message.mid;
// Prevent duplicates and receiving own peer
if (!this._peerConnections[targetMid]) {
message.agent = (!message.agent) ? 'Chrome' : message.agent;
this._trigger('handshakeProgress', this.HANDSHAKE_PROGRESS.WELCOME, targetMid);
this._peerInformations[targetMid] = message.userInfo;
this._trigger('peerJoined', targetMid, message.userInfo, false);
this._enableIceTrickle = (typeof message.enableIceTrickle === 'boolean') ?
message.enableIceTrickle : this._enableIceTrickle;
this._enableDataChannel = (typeof message.enableDataChannel === 'boolean') ?
message.enableDataChannel : this._enableDataChannel;
this._openPeer(targetMid, message.agent, true, message.receiveOnly);
} else {
console.log('API - Not creating offer because user is' +
' connected to peer already.');
}
};
/**
* Signaling server sends an offer message.
* - SIG_TYPE: OFFER
* - This occurs when we've just received an offer.
* - If there is no existing connection with this peer, create one,
* then set the remotedescription and answer.
* @method _offerHandler
* @param {JSON} message The message object received.
* @param {String} message.rid RoomId of the connected room.
* @param {String} message.mid PeerId of the peer that is sending the offer shake.
* @param {String} message.sdp Offer sessionDescription
* @param {String} message.type The type of message received.
* @trigger handshakeProgress
* @private
* @since 0.1.0
*/
Skyway.prototype._offerHandler = function(message) {
var self = this;
var targetMid = message.mid;
message.agent = (!message.agent) ? 'Chrome' : message.agent;
self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.OFFER, targetMid);
var offer = new window.RTCSessionDescription(message);
console.log('API - [' + targetMid + '] Received offer:');
console.dir(offer);
var pc = self._peerConnections[targetMid];
if (!pc) {
self._openPeer(targetMid, message.agent, false);
pc = self._peerConnections[targetMid];
}
pc.setRemoteDescription(new RTCSessionDescription(offer), function() {
self._doAnswer(targetMid);
}, function(error) {
self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ERROR, targetMid, error);
console.error('API - [' + targetMid + '] Failed setting remote description for offer.');
console.error(error);
});
};
/**
* Signaling server sends a candidate message.
* - SIG_TYPE: CANDIDATE
* - This occurs when a peer sends an ice candidate.
* @method _candidateHandler
* @param {JSON} message The message object received.
* @param {String} message.rid RoomId of the connected room.
* @param {String} message.mid PeerId of the peer that is sending the
* offer shake.
* @param {String} message.sdp Offer sessionDescription.
* @param {String} message.target PeerId that is specifically
* targeted to receive the message.
* @param {String} message.id Peer's ICE candidate id.
* @param {String} message.candidate Peer's ICE candidate object.
* @param {String} message.label Peer's ICE candidate label.
* @param {String} message.type The type of message received.
* @private
* @since 0.1.0
*/
Skyway.prototype._candidateHandler = function(message) {
var targetMid = message.mid;
var pc = this._peerConnections[targetMid];
if (pc) {
if (pc.iceConnectionState === this.ICE_CONNECTION_STATE.CONNECTED) {
console.log('API - [' + targetMid + '] Received but not adding Candidate ' +
'as we are already connected to this peer.');
return;
}
var messageCan = message.candidate.split(' ');
var canType = messageCan[7];
console.log('API - [' + targetMid + '] Received ' + canType + ' Candidate.');
// if (canType !== 'relay' && canType !== 'srflx') {
// trace('Skipping non relay and non srflx candidates.');
var index = message.label;
var candidate = new window.RTCIceCandidate({
sdpMLineIndex: index,
candidate: message.candidate
});
pc.addIceCandidate(candidate); //,
// NOTE ALEX: not implemented in chrome yet, need to wait
// function () { trace('ICE - addIceCandidate Succesfull. '); },
// function (error) { trace('ICE - AddIceCandidate Failed: ' + error); }
//);
console.log('API - [' + targetMid + '] Added Candidate.');
} else {
console.log('API - [' + targetMid + '] Received but not adding Candidate ' +
'as PeerConnection not present.');
// NOTE ALEX: if the offer was slow, this can happen
// we might keep a buffer of candidates to replay after receiving an offer.
}
};
/**
* Signaling server sends an answer message.
* - SIG_TYPE: ANSWER
* - This occurs when a peer sends an answer message is received.
* @method _answerHandler
* @param {String} message.type Message type
* @param {JSON} message The message object received.
* @param {String} message.rid RoomId of the connected room.
* @param {String} message.sdp Answer sessionDescription
* @param {String} message.mid PeerId of the peer that is sending the enter shake.
* @param {String} message.type The type of message received.
* @trigger handshakeProgress
* @private
* @since 0.1.0
*/
Skyway.prototype._answerHandler = function(message) {
var self = this;
var targetMid = message.mid;
self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ANSWER, targetMid);
var answer = new window.RTCSessionDescription(message);
console.log('API - [' + targetMid + '] Received answer:');
console.dir(answer);
var pc = self._peerConnections[targetMid];
pc.setRemoteDescription(new RTCSessionDescription(answer), function() {
pc.remotePeerReady = true;
}, function(error) {
self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ERROR, targetMid, error);
console.error('API - [' + targetMid + '] Failed setting remote description for answer.');
console.error(error);
});
};
/**
* Actually clean the peerconnection and trigger an event.
* Can be called by _byHandler and leaveRoom.
* @method _removePeer
* @param {String} peerId PeerId of the peer that has left.
* @trigger peerLeft
* @private
* @since 0.1.0
*/
Skyway.prototype._removePeer = function(peerId) {
this._trigger('peerLeft', peerId, this._peerInformations[peerId], false);
if (this._peerConnections[peerId]) {
this._peerConnections[peerId].close();
}
delete this._peerConnections[peerId];
delete this._peerInformations[peerId];
};
/**
* We have succesfully received an offer and set it locally. This function will take care
* of cerating and sendng the corresponding answer. Handshake step 4.
* @method _doAnswer
* @param {String} targetMid PeerId of the peer to send answer to.
* @private
* @since 0.1.0
*/
Skyway.prototype._doAnswer = function(targetMid) {
var self = this;
var pc = self._peerConnections[targetMid];
console.log('API - [' + targetMid + '] Creating answer.');
if (pc) {
pc.createAnswer(function(answer) {
console.log('API - [' + targetMid + '] Created answer.');
console.dir(answer);
self._setLocalAndSendMessage(targetMid, answer);
}, function(error) {
self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ERROR, targetMid, error);
console.error('API - [' + targetMid + '] Failed creating an answer.');
console.error(error);
}, self._room.pcHelper.sdpConstraints);
} else {
return;
/* Houston ..*/
}
};
/**
* We have a peer, this creates a peerconnection object to handle the call.
* if we are the initiator, we then starts the O/A handshake.
* @method _openPeer
* @param {String} targetMid PeerId of the peer we should connect to.
* @param {String} peerAgentBrowser Peer's browser
* @param {Boolean} toOffer Wether we should start the O/A or wait.
* @param {Boolean} receiveOnly Should they only receive?
* @private
* @since 0.1.0
*/
Skyway.prototype._openPeer = function(targetMid, peerAgentBrowser, toOffer, receiveOnly) {
var self = this;
if (self._peerConnections[targetMid]) {
console.log('API - [' + targetMid + '] PeerConnection has already been ' +
'created. Abort.');
return;
}
console.log('API - [' + targetMid + '] Creating PeerConnection.');
self._peerConnections[targetMid] = self._createPeerConnection(targetMid);
if (!receiveOnly) {
self._addLocalStream(targetMid);
}
// I'm the callee I need to make an offer
if (toOffer) {
if (self._enableDataChannel) {
self._createDataChannel(targetMid);
self._doCall(targetMid, peerAgentBrowser);
} else {
self._doCall(targetMid, peerAgentBrowser);
}
}
};
/**
* Sends our Local MediaStream to other Peers.
* By default, it sends all it's other stream
* @method _addLocalStream
* @param {String} peerId PeerId of the peer to send local stream to.
* @private
* @since 0.2.0
*/
Skyway.prototype._addLocalStream = function(peerId) {
// NOTE ALEX: here we could do something smarter
// a mediastream is mainly a container, most of the info
// are attached to the tracks. We should iterates over track and print
console.log('API - [' + peerId + '] Adding local stream.');
if (Object.keys(this._user.streams).length > 0) {
for (var stream in this._user.streams) {
if (this._user.streams.hasOwnProperty(stream)) {
if (this._user.streams[stream].active) {
this._peerConnections[peerId].addStream(this._user.streams[stream]);
}
}
}
} else {
console.log('API - WARNING - No stream to send. You will be only receiving.');
}
};
/**
* The remote peer advertised streams, that we are forwarding to the app. This is part
* of the peerConnection's addRemoteDescription() API's callback.
* @method _onRemoteStreamAdded
* @param {String} targetMid PeerId of the peer that has remote stream to send.
* @param {Event} event This is provided directly by the peerconnection API.
* @trigger incomingStream
* @private
* @since 0.1.0
*/
Skyway.prototype._onRemoteStreamAdded = function(targetMid, event) {
console.log('API - [' + targetMid + '] Remote Stream added.');
this._trigger('incomingStream', targetMid, event.stream, false);
};
/**
* It then sends it to the peer. Handshake step 3 (offer) or 4 (answer)
* @method _doCall
* @param {String} targetMid PeerId of the peer to send offer to.
* @private
* @since 0.1.0
*/
Skyway.prototype._doCall = function(targetMid, peerAgentBrowser) {
var self = this;
var pc = self._peerConnections[targetMid];
// NOTE ALEX: handle the pc = 0 case, just to be sure
var inputConstraints = self._room.pcHelper.offerConstraints;
var sc = self._room.pcHelper.sdpConstraints;
for (var name in sc.mandatory) {
if (sc.mandatory.hasOwnProperty(name)) {
inputConstraints.mandatory[name] = sc.mandatory[name];
}
}
inputConstraints.optional.concat(sc.optional);
console.log('API - [' + targetMid + '] Creating offer.');
checkMediaDataChannelSettings(true, peerAgentBrowser,
function(unifiedOfferConstraints) {
pc.createOffer(function(offer) {
self._setLocalAndSendMessage(targetMid, offer);
}, function(error) {
self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ERROR,
targetMid, error);
console.error('API - [' + targetMid + '] Failed creating an offer.');
console.error(error);
}, unifiedOfferConstraints);
}, inputConstraints);
};
/**
* Finds a line in the SDP and returns it.
* - To set the value to the line, add an additional parameter to the method.
* @method _findSDPLine
* @param {Array} sdpLines Sdp received.
* @param {Array} condition The conditions.
* @param {String} value Value to set Sdplines to
* @return {Array} [index, line] - Returns the sdpLines based on the condition
* @private
* @since 0.2.0
*/
Skyway.prototype._findSDPLine = function(sdpLines, condition, value) {
for (var index in sdpLines) {
if (sdpLines.hasOwnProperty(index)) {
for (var c in condition) {
if (condition.hasOwnProperty(c)) {
if (sdpLines[index].indexOf(c) === 0) {
sdpLines[index] = value;
return [index, sdpLines[index]];
}
}
}
}
}
return [];
};
/**
* Adds stereo feature to the SDP.
* - This requires OPUS to be enabled in the SDP or it will not work.
* @method _addStereo
* @param {Array} sdpLines Sdp received.
* @return {Array} Updated version with Stereo feature
* @private
* @since 0.2.0
*/
Skyway.prototype._addStereo = function(sdpLines) {
var opusLineFound = false,
opusPayload = 0;
// Check if opus exists
var rtpmapLine = this._findSDPLine(sdpLines, ['a=rtpmap:']);
if (rtpmapLine.length) {
if (rtpmapLine[1].split(' ')[1].indexOf('opus/48000/') === 0) {
opusLineFound = true;
opusPayload = (rtpmapLine[1].split(' ')[0]).split(':')[1];
}
}
// Find the A=FMTP line with the same payload
if (opusLineFound) {
var fmtpLine = this._findSDPLine(sdpLines, ['a=fmtp:' + opusPayload]);
if (fmtpLine.length) {
sdpLines[fmtpLine[0]] = fmtpLine[1] + '; stereo=1';
}
}
return sdpLines;
};
/**
* Set Audio, Video and Data Bitrate in SDP
* @method _setSDPBitrate
* @param {Array} sdpLines Sdp received.
* @return {Array} Updated version with custom Bandwidth settings
* @private
* @since 0.2.0
*/
Skyway.prototype._setSDPBitrate = function(sdpLines) {
// Find if user has audioStream
var bandwidth = this._streamSettings.bandwidth;
var maLineFound = this._findSDPLine(sdpLines, ['m=', 'a=']).length;
var cLineFound = this._findSDPLine(sdpLines, ['c=']).length;
// Find the RTPMAP with Audio Codec
if (maLineFound && cLineFound) {
if (bandwidth.audio) {
var audioLine = this._findSDPLine(sdpLines, ['a=mid:audio', 'm=mid:audio']);
sdpLines.splice(audioLine[0], 0, 'b=AS:' + bandwidth.audio);
}
if (bandwidth.video) {
var videoLine = this._findSDPLine(sdpLines, ['a=mid:video', 'm=mid:video']);
sdpLines.splice(videoLine[0], 0, 'b=AS:' + bandwidth.video);
}
if (bandwidth.data) {
var dataLine = this._findSDPLine(sdpLines, ['a=mid:data', 'm=mid:data']);
sdpLines.splice(dataLine[0], 0, 'b=AS:' + bandwidth.data);
}
}
return sdpLines;
};
/**
* This takes an offer or an aswer generated locally and set it in the peerconnection
* it then sends it to the peer. Handshake step 3 (offer) or 4 (answer)
* @method _setLocalAndSendMessage
* @param {String} targetMid PeerId of the peer to send offer/answer to.
* @param {JSON} sessionDescription This should be provided by the peerconnection API.
* User might 'tamper' with it, but then , the setLocal may fail.
* @trigger handshakeProgress
* @private
* @since 0.1.0
*/
Skyway.prototype._setLocalAndSendMessage = function(targetMid, sessionDescription) {
var self = this;
var pc = self._peerConnections[targetMid];
console.log('API - [' + targetMid + '] Created ' +
sessionDescription.type + '.');
console.log(sessionDescription);
// NOTE ALEX: handle the pc = 0 case, just to be sure
var sdpLines = sessionDescription.sdp.split('\r\n');
if (self._streamSettings.stereo) {
self._addStereo(sdpLines);
console.info('API - User has requested Stereo');
}
if (self._streamSettings.bandwidth) {
sdpLines = self._setSDPBitrate(sdpLines, self._streamSettings.bandwidth);
console.info('API - Custom Bandwidth settings');
console.info('API - Video: ' + self._streamSettings.bandwidth.video);
console.info('API - Audio: ' + self._streamSettings.bandwidth.audio);
console.info('API - Data: ' + self._streamSettings.bandwidth.data);
}
sessionDescription.sdp = sdpLines.join('\r\n');
// NOTE ALEX: opus should not be used for mobile
// Set Opus as the preferred codec in SDP if Opus is present.
//sessionDescription.sdp = preferOpus(sessionDescription.sdp);
// limit bandwidth
//sessionDescription.sdp = this._limitBandwidth(sessionDescription.sdp);
console.log('API - [' + targetMid + '] Setting local Description (' +
sessionDescription.type + ').');
pc.setLocalDescription(sessionDescription, function() {
console.log('API - [' + targetMid + '] Set ' + sessionDescription.type + '.');
self._trigger('handshakeProgress', sessionDescription.type, targetMid);
if (self._enableIceTrickle || (!self._enableIceTrickle &&
sessionDescription.type !== self.HANDSHAKE_PROGRESS.OFFER)) {
console.log('API - [' + targetMid + '] Sending ' + sessionDescription.type + '.');
self._sendMessage({
type: sessionDescription.type,
sdp: sessionDescription.sdp,
mid: self._user.sid,
agent: window.webrtcDetectedBrowser.browser,
target: targetMid,
rid: self._room.id
});
}
}, function(error) {
self._trigger('handshakeProgress', self.HANDSHAKE_PROGRESS.ERROR, targetMid, error);
console.error('API - [' + targetMid + '] There was a problem setting the Local Description.');
console.error(error);
});
};
/**
* Sets the STUN server specially for Firefox for ICE Connection.
* @method _setFirefoxIceServers
* @param {JSON} config Ice configuration servers url object.
* @return {JSON} Updated configuration
* @private
* @since 0.1.0
*/
Skyway.prototype._setFirefoxIceServers = function(config) {
if (window.webrtcDetectedBrowser.mozWebRTC) {
// NOTE ALEX: shoul dbe given by the server
var newIceServers = [{
'url': 'stun:stun.services.mozilla.com'
}];
for (var i = 0; i < config.iceServers.length; i++) {
var iceServer = config.iceServers[i];
var iceServerType = iceServer.url.split(':')[0];
if (iceServerType === 'stun') {
if (iceServer.url.indexOf('google')) {
continue;
}
iceServer.url = [iceServer.url];
newIceServers.push(iceServer);
} else {
var newIceServer = {};
newIceServer.credential = iceServer.credential;
newIceServer.url = iceServer.url.split(':')[0];
newIceServer.username = iceServer.url.split(':')[1].split('@')[0];
newIceServer.url += ':' + iceServer.url.split(':')[1].split('@')[1];
newIceServers.push(newIceServer);
}
}
config.iceServers = newIceServers;
}
return config;
};
/**
* Waits for MediaStream.
* - Once the stream is loaded, callback is called
* - If there's not a need for stream, callback is called
* @method _waitForMediaStream
* @param {Function} callback Callback after requested constraints are loaded.
* @param {JSON} options Optional. Media Constraints.
* @param {JSON} options.user Optional. User custom data.
* @param {Boolean|JSON} options.audio This call requires audio
* @param {Boolean} options.audio.stereo Enabled stereo or not
* @param {Boolean|JSON} options.video This call requires video
* @param {JSON} options.video.resolution [Rel: Skyway.VIDEO_RESOLUTION]
* @param {Integer} options.video.resolution.width Video width
* @param {Integer} options.video.resolution.height Video height
* @param {Integer} options.video.frameRate Mininum frameRate of Video
* @param {String} options.bandwidth Bandwidth settings
* @param {String} options.bandwidth.audio Audio Bandwidth
* @param {String} options.bandwidth.video Video Bandwidth
* @param {String} options.bandwidth.data Data Bandwidth
* @private
* @since 0.4.0
*/
Skyway.prototype._waitForMediaStream = function(callback, options) {
var self = this;
options = options || {};
self.getUserMedia(options);
console.log('API - requireVideo: ' + options.video);
console.log('API - requireAudio: ' + options.audio);
// If options video or audio false, do the opposite to throw a true.
var hasAudio = (options.audio) ? false : true;
var hasVideo = (options.video) ? false : true;
if (options.video || options.audio) {
// lets wait for a minute and then we pull the updates
var count = 0;
var checkForStream = setInterval(function() {
if (count < 5) {
for (var stream in self._user.streams) {
if (self._user.streams.hasOwnProperty(stream)) {
if (options.audio &&
self._user.streams[stream].getAudioTracks().length > 0) {
hasAudio = true;
}
if (options.video &&
self._user.streams[stream].getVideoTracks().length > 0) {
hasVideo = true;
}
if (hasAudio && hasVideo) {
clearInterval(checkForStream);
callback();
} else {
count++;
}
}
}
} else {
clearInterval(checkForStream);
var error = ((!hasAudio && options.audio) ? 'Expected audio but no ' +
'audio stream received' : '') + '\n' + ((!hasVideo && options.video) ?
'Expected video but no video stream received' : '');
self._trigger('mediaAccessError', error);
}
}, 2000);
} else {
callback();
}
};
/**
* Opens or closes existing MediaStreams.
* @method _setStreams
* @param {JSON} options
* @param {JSON} options.audio Enable audio or not
* @param {JSON} options.video Enable video or not
* @return {Boolean} Whether we should re-fetch mediaStreams or not
* @private
* @since 0.3.0
*/
Skyway.prototype._setStreams = function(options) {
var hasAudioTracks = false, hasVideoTracks = false;
if (!this._user) {
console.error('API - User has no streams to close');
return;
}
for (var stream in this._user.streams) {
if (this._user.streams.hasOwnProperty(stream)) {
var audios = this._user.streams[stream].getAudioTracks();
var videos = this._user.streams[stream].getVideoTracks();
for (var audio in audios) {
if (audios.hasOwnProperty(audio)) {
audios[audio].enabled = options.audio;
hasAudioTracks = true;
}
}
for (var video in videos) {
if (videos.hasOwnProperty(video)) {
videos[video].enabled = options.video;
hasVideoTracks = true;
}
}
if (!options.video && !options.audio) {
this._user.streams[stream].active = false;
} else {
this._user.streams[stream].active = true;
}
}
}
return ((!hasAudioTracks && options.audio) ||
(!hasVideoTracks && options.video));
};
/**
* Creates a peerconnection to communicate with the peer whose ID is 'targetMid'.
* All the peerconnection callbacks are set up here. This is a quite central piece.
* @method _createPeerConnection
* @param {String} targetMid
* @return {Object} The created peer connection object.
* @private
* @since 0.1.0
*/
Skyway.prototype._createPeerConnection = function(targetMid) {
var pc, self = this;
try {
pc = new window.RTCPeerConnection(
self._room.pcHelper.pcConfig,
self._room.pcHelper.pcConstraints);
console.log(
'API - [' + targetMid + '] Created PeerConnection.');
console.log(
'API - [' + targetMid + '] PC config: ');
console.dir(self._room.pcHelper.pcConfig);
console.log(
'API - [' + targetMid + '] PC constraints: ' +
JSON.stringify(self._room.pcHelper.pcConstraints));
} catch (error) {
console.log('API - [' + targetMid + '] Failed to create PeerConnection: ' + error.message);
return null;
}
// callbacks
// standard not implemented: onnegotiationneeded,
pc.ondatachannel = function(event) {
var dc = event.channel || event;
console.log('API - [' + targetMid + '] Received DataChannel -> ' +
dc.label);
if (self._enableDataChannel) {
self._createDataChannel(targetMid, dc);
} else {
console.info('API - [' + targetMid + '] Not adding DataChannel');
}
};
pc.onaddstream = function(event) {
self._onRemoteStreamAdded(targetMid, event);
};
pc.onicecandidate = function(event) {
console.dir(event);
self._onIceCandidate(targetMid, event);
};
pc.oniceconnectionstatechange = function() {
checkIceConnectionState(targetMid, pc.iceConnectionState, function(iceConnectionState) {
console.log('API - [' + targetMid + '] ICE connection state changed -> ' +
iceConnectionState);
self._trigger('iceConnectionState', iceConnectionState, targetMid);
});
};
// pc.onremovestream = function () {
// self._onRemoteStreamRemoved(targetMid);
// };
pc.onsignalingstatechange = function() {
console.log('API - [' + targetMid + '] PC connection state changed -> ' +
pc.signalingState);
var signalingState = pc.signalingState;
if (pc.signalingState !== self.PEER_CONNECTION_STATE.STABLE &&
pc.signalingState !== self.PEER_CONNECTION_STATE.CLOSED) {
pc.hasSetOffer = true;
} else if (pc.signalingState === self.PEER_CONNECTION_STATE.STABLE &&
pc.hasSetOffer) {
signalingState = self.PEER_CONNECTION_STATE.ESTABLISHED;
}
self._trigger('peerConnectionState', signalingState, targetMid);
};
pc.onicegatheringstatechange = function() {
console.log('API - [' + targetMid + '] ICE gathering state changed -> ' +
pc.iceGatheringState);
self._trigger('candidateGenerationState', pc.iceGatheringState, targetMid);
};
return pc;
};
/**
* A candidate has just been generated (ICE gathering) and will be sent to the peer.
* Part of connection establishment.
* @method _onIceCandidate
* @param {String} targetMid
* @param {Event} event This is provided directly by the peerconnection API.
* @trigger candidateGenerationState
* @private
* @since 0.1.0
*/
Skyway.prototype._onIceCandidate = function(targetMid, event) {
if (event.candidate) {
if (this._enableIceTrickle) {
var messageCan = event.candidate.candidate.split(' ');
var candidateType = messageCan[7];
console.log('API - [' + targetMid + '] Created and sending ' +
candidateType + ' candidate.');
this._sendMessage({
type: this.SIG_TYPE.CANDIDATE,
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate,
mid: this._user.sid,
target: targetMid,
rid: this._room.id
});
}
} else {
console.log('API - [' + targetMid + '] End of gathering.');
this._trigger('candidateGenerationState', this.CANDIDATE_GENERATION_STATE.COMPLETED,
targetMid);
// Disable Ice trickle option
if (!this._enableIceTrickle) {
var sessionDescription = this._peerConnections[targetMid].localDescription;
console.log('API - [' + targetMid + '] Sending offer.');
this._sendMessage({
type: sessionDescription.type,
sdp: sessionDescription.sdp,
mid: this._user.sid,
agent: window.webrtcDetectedBrowser.browser,
target: targetMid,
rid: this._room.id
});
}
}
};
/**
* Sends a message to the signaling server.
* - Not to be confused with method
* {{#crossLink "Skyway/sendMessage:method"}}sendMessage(){{/crossLink}}
* that broadcasts messages. This is for sending socket messages.
* @method _sendMessage
* @param {JSON} message
* @private
* @since 0.1.0
*/
Skyway.prototype._sendMessage = function(message) {
if (!this._channel_open) {
return;
}
var messageString = JSON.stringify(message);
console.log('API - [' + (message.target ? message.target : 'server') +
'] Outgoing message: ' + message.type);
this._socket.send(messageString);
};
/**
* Initiate a socket signaling connection.
* @method _openChannel
* @trigger channelMessage, channelOpen, channelError, channelClose
* @private
* @since 0.1.0
*/
Skyway.prototype._openChannel = function() {
var self = this;
if (self._channel_open ||
self._readyState !== self.READY_STATE_CHANGE.COMPLETED) {
return;
}
console.log('API - Opening channel.');
var ip_signaling = self._room.signalingServer.protocol + '://' +
self._room.signalingServer.ip + ':' + self._room.signalingServer.port;
console.log('API - Signaling server URL: ' + ip_signaling);
if (self._socketVersion >= 1) {
self._socket = io.connect(ip_signaling, {
forceNew: true
});
} else {
self._socket = window.io.connect(ip_signaling, {
'force new connection': true
});
}
self._socket = window.io.connect(ip_signaling, {
'force new connection': true
});
self._socket.on('connect', function() {
self._channel_open = true;
self._trigger('channelOpen');
});
self._socket.on('error', function(error) {
self._channel_open = false;
self._trigger('channelError', error);
console.error('API - Channel Error occurred.');
console.error(error);
});
self._socket.on('disconnect', function() {
self._trigger('channelClose');
});
self._socket.on('message', function(message) {
self._processSigMessage(message);
});
};
/**
* Closes the socket signaling connection.
* @method _closeChannel
* @private
* @since 0.1.0
*/
Skyway.prototype._closeChannel = function() {
if (!this._channel_open) {
return;
}
this._socket.disconnect();
this._socket = null;
this._channel_open = false;
};
/**
* Create a DataChannel. Only SCTPDataChannel support
* @method _createDataChannel
* @param {String} peerId PeerId of the peer which the datachannel is connected to
* @param {Object} dc The datachannel object received.
* @trigger dataChannelState
* @private
* @since 0.1.0
*/
Skyway.prototype._createDataChannel = function(peerId, dc) {
var self = this;
var pc = self._peerConnections[peerId];
var channelName = self._user.sid + '_' + peerId;
var dcOpened = function () {
console.log('API - DataChannel [' + peerId + ']: DataChannel is opened.');
self._dataChannels[peerId] = dc;
self._trigger('dataChannelState', dc.readyState, peerId);
};
if (!dc) {
if (!webrtcDetectedBrowser.isSCTPDCSupported && !webrtcDetectedBrowser.isPluginSupported) {
console.warn('API - DataChannel [' + peerId + ']: Does not support SCTP');
}
dc = pc.createDataChannel(channelName);
self._trigger('dataChannelState', dc.readyState, peerId);
var checkDcOpened = setInterval(function () {
if (dc.readyState === self.DATA_CHANNEL_STATE.OPENED) {
clearInterval(checkDcOpened);
dcOpened();
}
}, 50);
}
console.log('API - DataChannel [' + peerId + ']: Binary type support is "' +
dc.binaryType + '"');
dc.onerror = function(error) {
console.error('API - DataChannel [' + peerId + ']: Failed retrieveing DataChannel.');
console.exception(error);
self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.ERROR, peerId, error);
};
dc.onclose = function() {
console.log('API - DataChannel [' + peerId + ']: DataChannel closed.');
self._closeDataChannel(peerId, self);
self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.CLOSED, peerId);
};
dc.onopen = dcOpened;
dc.push = dc.send;
dc.send = function (data) {
console.log('API - DataChannel [' + peerId + ']: Sending data - length : ' +
data.length);
dc.push(data);
};
dc.onmessage = function(event) {
console.log('API - DataChannel [' + peerId + ']: DataChannel message received');
self._dataChannelHandler(event.data, peerId, self);
};
};
/**
* Sends data to the datachannel.
* @method _sendDataChannel
* @param {String} peerId PeerId of the peer's datachannel to send data.
* @param {JSON} data The data to send.
* @private
* @since 0.1.0
*/
Skyway.prototype._sendDataChannel = function(peerId, data) {
var dc = this._dataChannels[peerId];
if (!dc) {
console.error('API - DataChannel [' + peerId + ']: No available existing DataChannel');
return;
} else {
if (dc.readyState === this.DATA_CHANNEL_STATE.OPEN) {
console.log('API - DataChannel [' + peerId + ']: Sending Data from DataChannel');
try {
var dataString = '';
for (var i = 0; i < data.length; i++) {
dataString += data[i];
dataString += (i !== (data.length - 1)) ? '|' : '';
}
dc.send(dataString);
} catch (error) {
console.error('API - DataChannel [' + peerId + ']: Failed executing send on DataChannel');
console.error(error);
this._trigger('dataChannelState', this.DATA_CHANNEL_STATE.ERROR, peerId, error);
}
} else {
console.error('API - DataChannel [' + peerId +
']: DataChannel is not ready.\nState is: "' + dc.readyState + '"');
this._trigger('dataChannelState', this.DATA_CHANNEL_STATE.ERROR,
peerId, 'DataChannel is not ready.\nState is: ' + dc.readyState);
}
}
};
/**
* Closes the datachannel.
* @method _closeDataChannel
* @param {String} peerId PeerId of the peer's datachannel to close.
* @param {Skyway} self Skyway object.
* @private
* @since 0.1.0
*/
Skyway.prototype._closeDataChannel = function(peerId, self) {
var dc = self._dataChannels[peerId];
if (dc) {
if (dc.readyState !== self.DATA_CHANNEL_STATE.CLOSED) {
dc.close();
}
delete self._dataChannels[peerId];
}
};
/**
* Handles all datachannel protocol events.
* @method _dataChannelHandler
* @param {String|Object} data The data received from datachannel.
* @private
* @since 0.1.0
*/
Skyway.prototype._dataChannelHandler = function(dataString, peerId, self) {
// PROTOCOL ESTABLISHMENT
if (typeof dataString === 'string') {
if (dataString.indexOf('|') > -1 && dataString.indexOf('|') < 6) {
var data = dataString.split('|');
var state = data[0];
console.log('API - DataChannel [' + peerId + ']: Received ' + state);
switch (state) {
case 'WRQ':
self._dataChannelWRQHandler(peerId, data, self);
break;
case 'ACK':
self._dataChannelACKHandler(peerId, data, self);
break;
case 'ERROR':
self._dataChannelERRORHandler(peerId, data, self);
break;
case 'CHAT':
self._dataChannelCHATHandler(peerId, data, self);
break;
default:
console.error('API - DataChannel [' + peerId + ']: Invalid command');
}
} else {
console.log('API - DataChannel [' + peerId + ']: Received "DATA"');
self._dataChannelDATAHandler(peerId, dataString,
self.DATA_TRANSFER_DATA_TYPE.BINARY_STRING, self);
}
}
};
/**
* The user receives a blob request.
* From here, it's up to the user to accept or reject it
* @method _dataChannelWRQHandler
* @param {String} peerId PeerId of the peer that is sending the request.
* @param {Array} data The data object received from datachannel.
* @param {Skyway} self Skyway object.
* @trigger dataTransferState
* @private
* @since 0.4.0
*/
Skyway.prototype._dataChannelWRQHandler = function(peerId, data, self) {
var transferId = this._user.sid + this.DATA_TRANSFER_TYPE.DOWNLOAD +
(((new Date()).toISOString().replace(/-/g, '').replace(/:/g, ''))).replace('.', '');
var name = data[2];
var binarySize = parseInt(data[3], 10);
var expectedSize = parseInt(data[4], 10);
var timeout = parseInt(data[5], 10);
self._downloadDataSessions[peerId] = {
transferId: transferId,
name: name,
size: binarySize,
ackN: 0,
receivedSize: 0,
chunkSize: expectedSize,
timeout: timeout
};
self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.UPLOAD_REQUEST,
transferId, peerId, {
name: name,
size: binarySize,
senderPeerId: peerId
});
};
/**
* User's response to accept or reject data transfer request.
* @method respondBlobRequest
* @param {String} peerId PeerId of the peer that is expected to receive
* the request response.
* @param {Boolean} accept The response of the user to accept the data
* transfer or not.
* @trigger dataTransferState
* @since 0.4.0
*/
Skyway.prototype.respondBlobRequest = function (peerId, accept) {
if (accept) {
this._downloadDataTransfers[peerId] = [];
var data = this._downloadDataSessions[peerId];
this._sendDataChannel(peerId, ['ACK', 0,
window.webrtcDetectedBrowser.browser]);
this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.DOWNLOAD_STARTED,
data.transferId, peerId, {
name: data.name,
size: data.size,
senderPeerId: peerId
});
} else {
this._sendDataChannel(peerId, ['ACK', -1]);
delete this._downloadDataSessions[peerId];
}
};
/**
* The user receives an acknowledge of the blob request.
* @method _dataChannelACKHandler
* @param {String} peerId PeerId of the peer that is sending the acknowledgement.
* @param {Array} data The data object received from datachannel.
* @param {Skyway} self Skyway object.
* @trigger dataTransferState
* @private
* @since 0.1.0
*/
Skyway.prototype._dataChannelACKHandler = function(peerId, data, self) {
self._clearDataChannelTimeout(peerId, true, self);
var ackN = parseInt(data[1], 10);
var chunksLength = self._uploadDataTransfers[peerId].length;
var uploadedDetails = self._uploadDataSessions[peerId];
var transferId = uploadedDetails.transferId;
var timeout = uploadedDetails.timeout;
console.log('API - DataChannel Received "ACK": ' + ackN + ' / ' + chunksLength);
if (ackN > -1) {
// Still uploading
if (ackN < chunksLength) {
var fileReader = new FileReader();
fileReader.onload = function() {
// Load Blob as dataurl base64 string
var base64BinaryString = fileReader.result.split(',')[1];
self._sendDataChannel(peerId, [base64BinaryString]);
self._setDataChannelTimeout(peerId, timeout, true, self);
self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.UPLOADING,
transferId, peerId, {
percentage: (((ackN + 1) / chunksLength) * 100).toFixed()
});
};
fileReader.readAsDataURL(self._uploadDataTransfers[peerId][ackN]);
} else if (ackN === chunksLength) {
self._trigger('dataTransferState',
self.DATA_TRANSFER_STATE.UPLOAD_COMPLETED, transferId, peerId, {
name: uploadedDetails.name
});
delete self._uploadDataTransfers[peerId];
delete self._uploadDataSessions[peerId];
}
} else {
self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.REJECTED,
transferId, peerId);
delete self._uploadDataTransfers[peerId];
delete self._uploadDataSessions[peerId];
}
};
/**
* The user receives a datachannel broadcast message.
* @method _dataChannelCHATHandler
* @param {String} peerId PeerId of the peer that is sending a broadcast message.
* @param {Array} data The data object received from datachannel.
* @param {Skyway} self Skyway object.
* @trigger incomingMessage
* @private
* @since 0.4.0
*/
Skyway.prototype._dataChannelCHATHandler = function(peerId, data) {
var isPrivate = (data[1] === 'PRIVATE') ? true : false;
var senderPeerId = data[2];
var params = {
cid: this._key,
mid: senderPeerId,
rid: this._room.id,
isDataChannel: true
};
// Get remaining parts as the message contents.
// Get the index of the first char of chat content
//var start = 3 + data.slice(0, 3).join('').length;
params.data = '';
// Add all char from start to the end of dataStr.
// This method is to allow '|' to appear in the chat message.
for (var i = 3; i < data.length; i++) {
params.data += data[i];
}
// Handle different type of data
try {
var result = JSON.parse(params.data);
params.data = result;
console.log('API - Received data is a JSON.');
} catch (error) {
console.log('API - Received data is not a JSON.');
}
//console.info(this._user.sid);
//console.info(senderPeerId);
//console.info(peerId);
if (isPrivate) {
params.target = this._user.sid;
params.type = this.SIG_TYPE.PRIVATE_MESSAGE;
} else {
params.target = this._user.sid;
params.type = this.SIG_TYPE.PUBLIC_MESSAGE;
}
// Create a message using event.data, message mid.
this._processSingleMessage(params);
};
/**
* The user receives a timeout error.
* @method _dataChannelERRORHandler
* @param {String} peerId PeerId of the peer that is sending the error.
* @param {Array} data The data object received from datachannel.
* @param {Skyway} self Skyway object.
* @trigger dataTransferState
* @private
* @since 0.1.0
*/
Skyway.prototype._dataChannelERRORHandler = function(peerId, data, self) {
var isUploader = data[2];
var transferId = (isUploader) ? self._uploadDataSessions[peerId].transferId :
self._downloadDataSessions[peerId].transferId;
self._clearDataChannelTimeout(peerId, isUploader, self);
self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.ERROR,
transferId, peerId, null, {
message: data[1],
transferType: ((isUploader) ? self.DATA_TRANSFER_TYPE.UPLOAD :
self.DATA_TRANSFER_TYPE.DOWNLOAD)
});
};
/**
* This is when the data is sent from the sender to the receiving user.
* @method _dataChannelDATAHandler
* @param {String} peerId PeerId of the peer that is sending the data.
* @param {ArrayBuffer|Blob|String} dataString The data received.
* @param {String} dataType The data type received from datachannel.
* [Rel: Skyway.DATA_TRANSFER_DATA_TYPE]
* @param {Skyway} self Skyway object.
* @trigger dataTransferState
* @private
* @since 0.4.1
*/
Skyway.prototype._dataChannelDATAHandler = function(peerId, dataString, dataType, self) {
var chunk, error = '';
self._clearDataChannelTimeout(peerId, false, self);
var transferStatus = self._downloadDataSessions[peerId];
var transferId = transferStatus.transferId;
if (dataType === self.DATA_TRANSFER_DATA_TYPE.BINARY_STRING) {
chunk = self._base64ToBlob(dataString);
} else if (dataType === self.DATA_TRANSFER_DATA_TYPE.ARRAY_BUFFER) {
chunk = new Blob(dataString);
} else if (dataType === self.DATA_TRANSFER_DATA_TYPE.BLOB) {
chunk = dataString;
} else {
error = 'Unhandled data exception: ' + dataType;
console.error('API - ' + error);
self._trigger('dataTransferState',
self.DATA_TRANSFER_STATE.ERROR, transferId, peerId, null, {
message: error,
transferType: self.DATA_TRANSFER_TYPE.DOWNLOAD
});
return;
}
var receivedSize = (chunk.size * (4 / 3));
console.log('API - DataChannel [' + peerId + ']: Chunk size: ' + chunk.size);
if (transferStatus.chunkSize >= receivedSize) {
self._downloadDataTransfers[peerId].push(chunk);
transferStatus.ackN += 1;
transferStatus.receivedSize += receivedSize;
var totalReceivedSize = transferStatus.receivedSize;
var percentage = ((totalReceivedSize / transferStatus.size) * 100).toFixed();
self._sendDataChannel(peerId, ['ACK', transferStatus.ackN,
self._user.sid]);
if (transferStatus.chunkSize === receivedSize) {
self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.DOWNLOADING,
transferId, peerId, {
percentage: percentage
});
self._setDataChannelTimeout(peerId, transferStatus.timeout, false, self);
self._downloadDataTransfers[peerId].info = transferStatus;
} else {
var blob = new Blob(self._downloadDataTransfers[peerId]);
self._trigger('dataTransferState', self.DATA_TRANSFER_STATE.DOWNLOAD_COMPLETED,
transferId, peerId, {
data: blob
});
delete self._downloadDataTransfers[peerId];
delete self._downloadDataSessions[peerId];
}
} else {
error = 'Packet not match - [Received]' + receivedSize +
' / [Expected]' + transferStatus.chunkSize;
self._trigger('dataTransferState',
self.DATA_TRANSFER_STATE.ERROR, transferId, peerId, null, {
message: error,
transferType: self.DATA_TRANSFER_TYPE.DOWNLOAD
});
console.error('API - DataChannel [' + peerId + ']: ' + error);
}
};
/**
* Sets the datachannel timeout.
* - If timeout is met, it will send the 'ERROR' message
* @method _setDataChannelTimeout
* @param {String} peerId PeerId of the datachannel to set timeout.
* @param {Integer} timeout The timeout to set in seconds.
* @param {Boolean} isSender Is peer the sender or the receiver?
* @param {Skyway} self Skyway object.
* @private
* @since 0.1.0
*/
Skyway.prototype._setDataChannelTimeout = function(peerId, timeout, isSender, self) {
if (!self._dataTransfersTimeout[peerId]) {
self._dataTransfersTimeout[peerId] = [];
}
var type = (isSender) ? self.DATA_TRANSFER_TYPE.UPLOAD :
self.DATA_TRANSFER_TYPE.DOWNLOAD;
self._dataTransfersTimeout[peerId][type] = setTimeout(function() {
if (self._dataTransfersTimeout[peerId][type]) {
if (isSender) {
delete self._uploadDataTransfers[peerId];
delete self._uploadDataSessions[peerId];
} else {
delete self._downloadDataTransfers[peerId];
delete self._downloadDataSessions[peerId];
}
self._sendDataChannel(peerId, ['ERROR',
'Connection Timeout. Longer than ' + timeout + ' seconds. Connection is abolished.',
isSender
]);
console.error('API - Data Transfer ' + ((isSender) ? 'for': 'from') + ' ' +
peerId + ' failed. Connection timeout');
self._clearDataChannelTimeout(peerId, isSender, self);
}
}, 1000 * timeout);
};
/**
* Clears the datachannel timeout.
* @method _clearDataChannelTimeout
* @param {String} peerId PeerId of the datachannel to clear timeout.
* @param {Boolean} isSender Is peer the sender or the receiver?
* @param {Skyway} self Skyway object.
* @private
* @since 0.1.0
*/
Skyway.prototype._clearDataChannelTimeout = function(peerId, isSender, self) {
if (self._dataTransfersTimeout[peerId]) {
var type = (isSender) ? self.DATA_TRANSFER_TYPE.UPLOAD :
self.DATA_TRANSFER_TYPE.DOWNLOAD;
clearTimeout(self._dataTransfersTimeout[peerId][type]);
delete self._dataTransfersTimeout[peerId][type];
}
};
/**
* Converts base64 string to raw binary data.
* - Doesn't handle URLEncoded DataURIs
* - See StackOverflow answer #6850276 for code that does this
* This is to convert the base64 binary string to a blob
* @author Code from devnull69 @ stackoverflow.com
* @method _base64ToBlob
* @param {String} dataURL Blob base64 dataurl.
* @private
* @since 0.1.0
*/
Skyway.prototype._base64ToBlob = function(dataURL) {
var byteString = atob(dataURL.replace(/\s\r\n/g, ''));
// write the bytes of the string to an ArrayBuffer
var ab = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(ab);
for (var j = 0; j < byteString.length; j++) {
ia[j] = byteString.charCodeAt(j);
}
// write the ArrayBuffer to a blob, and you're done
return new Blob([ab]);
};
/**
* Chunks blob data into chunks.
* @method _chunkFile
* @param {Blob} blob The blob data to chunk.
* @param {Integer} blobByteSize The blob data size.
* @private
* @since 0.1.0
*/
Skyway.prototype._chunkFile = function(blob, blobByteSize) {
var chunksArray = [],
startCount = 0,
endCount = 0;
if (blobByteSize > this._chunkFileSize) {
// File Size greater than Chunk size
while ((blobByteSize - 1) > endCount) {
endCount = startCount + this._chunkFileSize;
chunksArray.push(blob.slice(startCount, endCount));
startCount += this._chunkFileSize;
}
if ((blobByteSize - (startCount + 1)) > 0) {
chunksArray.push(blob.slice(startCount, blobByteSize - 1));
}
} else {
// File Size below Chunk size
chunksArray.push(blob);
}
return chunksArray;
};
/**
* Start a data transfer with peer(s).
* - Note that peers have the option to download or reject receiving the blob data.
* - This method is ideal for sending files.
* - To send a private file to a peer, input the peerId after the
* data information.
* @method sendBlobData
* @param {Object} data The data to be sent over. Data has to be a blob.
* @param {JSON} dataInfo The data information.
* @param {String} dataInfo.transferId transferId of the data.
* @param {String} dataInfo.name Data name.
* @param {Integer} dataInfo.timeout The timeout to wait for packets.
* [Default is 60].
* @param {Integer} dataInfo.size The data size
* @param {String} targetPeerId PeerId targeted to receive data.
* Leave blank to send to all peers.
* @example
* // Send file to all peers connected
* SkywayDemo.sendBlobData(file, {
* 'name' : file.name,
* 'size' : file.size,
* 'timeout' : 67
* });
*
* // Send file to individual peer
* SkywayDemo.sendBlobData(blob, {
* 'name' : 'My Html',
* 'size' : blob.size,
* 'timeout' : 87
* }, targetPeerId);
* @trigger dataTransferState
* @since 0.4.1
*/
Skyway.prototype.sendBlobData = function(data, dataInfo, targetPeerId) {
if (!data && !dataInfo) {
return false;
}
var noOfPeersSent = 0;
dataInfo.timeout = dataInfo.timeout || 60;
dataInfo.transferId = this._user.sid + this.DATA_TRANSFER_TYPE.UPLOAD +
(((new Date()).toISOString().replace(/-/g, '').replace(/:/g, ''))).replace('.', '');
if (targetPeerId) {
if (this._dataChannels.hasOwnProperty(targetPeerId)) {
this._sendBlobDataToPeer(data, dataInfo, targetPeerId);
noOfPeersSent = 1;
} else {
console.log('API - DataChannel [' + targetPeerId + '] does not exists');
}
} else {
targetpeerId = this._user.sid;
for (var peerId in this._dataChannels) {
if (this._dataChannels.hasOwnProperty(peerId)) {
// Binary String filesize [Formula n = 4/3]
this._sendBlobDataToPeer(data, dataInfo, peerId);
noOfPeersSent++;
} else {
console.log('API - DataChannel [' + peerId + '] does not exists');
}
}
}
if (noOfPeersSent > 0) {
this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.UPLOAD_STARTED,
dataInfo.transferId, targetPeerId, {
transferId: dataInfo.transferId,
senderPeerId: this._user.sid,
name: dataInfo.name,
size: dataInfo.size,
timeout: dataInfo.timeout || 60,
data: data
});
} else {
var error = 'No available datachannels to send data.';
this._trigger('dataTransferState', this.DATA_TRANSFER_STATE.ERROR,
transferId, targetPeerId, null, {
message: error,
transferType: this.DATA_TRANSFER_TYPE.UPLOAD
});
console.log('API - ' + error);
this._uploadDataTransfers = [];
this._uploadDataSessions = [];
}
};
/**
* Sends blob data to individual peer.
* - This sends the {{#crossLink "Skyway/WRQ:event"}}WRQ{{/crossLink}}
* and to initiate the TFTP protocol.
* @method _sendBlobDataToPeer
* @param {Blob} data The blob data to be sent over.
* @param {JSON} dataInfo The data information.
* @param {String} dataInfo.transferId TransferId of the data.
* @param {String} dataInfo.name Data name.
* @param {Integer} dataInfo.timeout Data timeout to wait for packets.
* [Default is 60].
* @param {Integer} dataInfo.size Data size
* @param {String} targetPeerId PeerId targeted to receive data.
* Leave blank to send to all peers.
* @private
* @since 0.1.0
*/
Skyway.prototype._sendBlobDataToPeer = function(data, dataInfo, targetPeerId) {
var binarySize = (dataInfo.size * (4 / 3)).toFixed();
var chunkSize = (this._chunkFileSize * (4 / 3)).toFixed();
if (window.webrtcDetectedBrowser.browser === 'Firefox' &&
window.webrtcDetectedBrowser.version < 30) {
chunkSize = this._mozChunkFileSize;
}
this._uploadDataTransfers[targetPeerId] = this._chunkFile(data, dataInfo.size);
this._uploadDataSessions[targetPeerId] = {
name: dataInfo.name,
size: binarySize,
transferId: dataInfo.transferId,
timeout: dataInfo.timeout
};
this._sendDataChannel(targetPeerId, ['WRQ',
window.webrtcDetectedBrowser.browser,
dataInfo.name, binarySize, chunkSize, dataInfo.timeout
]);
this._setDataChannelTimeout(targetPeerId, dataInfo.timeout, true, this);
};
/**
* Handles all the room lock events.
* @method _handleLock
* @param {String} lockAction Lock action to send to server for response.
* [Rel: SkywayDemo.LOCK_ACTION]
* @param {Function} callback The callback to return the response after
* everything's loaded.
* @trigger roomLock
* @private
* @since 0.4.0
*/
Skyway.prototype._handleLock = function(lockAction, callback) {
var self = this;
var url = self._serverPath + '/rest/room/lock';
var params = {
api: self._apiKey,
rid: self._selectedRoom || self._defaultRoom,
start: self._room.start,
len: self._room.len,
cred: self._room.token,
action: lockAction,
end: (new Date((new Date(self._room.start))
.getTime() + (self._room.len * 60 * 60 * 1000))).toISOString()
};
self._requestServerInfo('POST', url, function(status, response) {
if (status !== 200) {
console.error('API - Failed ' + lockAction + 'ing room.\nReason was:');
console.error('XMLHttpRequest status not OK.\nStatus was: ' + status);
return;
}
console.info(response);
if (response.status) {
self._room_lock = response.content.lock;
self._trigger('roomLock', response.content.lock, self._user.sid,
self._user.info, true);
if (lockAction !== self.LOCK_ACTION.STATUS) {
self._sendMessage({
type: self.SIG_TYPE.ROOM_LOCK,
mid: self._user.sid,
rid: self._room.id,
lock: response.content.lock
});
}
} else {
console.error('API - Failed ' + lockAction + 'ing room.\nReason was:');
console.error(response.message);
}
}, params);
};
/**
* Handles all audio and video mute events.
* - If there is no available audio or video stream, it will call
* {{#crossLink "Skyway/leaveRoom:method"}}leaveRoom(){{/crossLink}}
* and call {{#crossLink "Skyway/joinRoom:method"}}joinRoom(){{/crossLink}}
* to join user in the room to send their audio and video stream.
* @method _handleAV
* @param {String} mediaType Media types expected to receive.
* [Rel: 'audio' or 'video']
* @param {Boolean} enableMedia Enable it or disable it
* @trigger peerUpdated
* @private
* @since 0.4.0
*/
Skyway.prototype._handleAV = function(mediaType, enableMedia) {
if (mediaType !== 'audio' && mediaType !== 'video') {
return;
} else if (!this._in_room) {
console.error('API - User is not in the room. Cannot ' +
((enableMedia) ? 'enable' : 'disable') + ' ' + mediaType);
return;
}
// Loop and enable tracks accordingly
var hasTracks = false, isStreamActive = false;
for (var stream in this._user.streams) {
if (this._user.streams.hasOwnProperty(stream)) {
var tracks = (mediaType === 'audio') ?
this._user.streams[stream].getAudioTracks() :
this._user.streams[stream].getVideoTracks();
for (var track in tracks) {
if (tracks.hasOwnProperty(track)) {
tracks[track].enabled = enableMedia;
hasTracks = true;
}
}
isStreamActive = this._user.streams[stream].active;
}
}
// Broadcast to other peers
if (!(hasTracks && isStreamActive) && enableMedia) {
this.leaveRoom();
var hasProperty = (this._user) ? ((this._user.info) ? (
(this._user.info.settings) ? true : false) : false) : false;
// set timeout? to 500?
this.joinRoom({
audio: (mediaType === 'audio') ? true : ((hasProperty) ?
this._user.info.settings.audio : false),
video: (mediaType === 'video') ? true : ((hasProperty) ?
this._user.info.settings.video : false)
});
} else {
this._sendMessage({
type: ((mediaType === 'audio') ? this.SIG_TYPE.MUTE_AUDIO :
this.SIG_TYPE.MUTE_VIDEO),
mid: this._user.sid,
rid: this._room.id,
muted: !enableMedia
});
}
this._user.info.mediaStatus[mediaType + 'Muted'] = !enableMedia;
this._trigger('peerUpdated', this._user.sid, this._user.info, true);
};
/**
* Lock the room to prevent peers from joining the room.
* @method lockRoom
* @example
* SkywayDemo.lockRoom();
* @trigger lockRoom
* @since 0.2.0
*/
Skyway.prototype.lockRoom = function() {
this._handleLock(this.LOCK_ACTION.LOCK);
};
/**
* Unlock the room to allow peers to join the room.
* @method unlockRoom
* @example
* SkywayDemo.unlockRoom();
* @trigger lockRoom
* @since 0.2.0
*/
Skyway.prototype.unlockRoom = function() {
this._handleLock(this.LOCK_ACTION.UNLOCK);
};
/**
* Get the lock status of the room.
* - <b><i>WARNING</i></b>: If there's too many peers toggling the
* room lock feature at the same time, the returned results may not
* be completely correct since while retrieving the room lock status,
* another peer may be toggling it.
* @method isRoomLocked
* @example
* if(SkywayDemo.isRoomLocked()) {
* SkywayDemo.unlockRoom();
* } else {
* SkywayDemo.lockRoom();
* }
* @beta
* @since 0.4.0
*/
Skyway.prototype.isRoomLocked = function() {
this._handleLock(this.LOCK_ACTION.STATUS, function (lockAction) {
return lockAction;
});
};
/**
* Enable microphone.
* - If microphone is not enabled from the beginning, user would have to reinitate the
* {{#crossLink "Skyway/joinRoom:method"}}joinRoom(){{/crossLink}}
* process and ask for microphone again.
* @method enableAudio
* @trigger peerUpdated
* @example
* SkywayDemo.enableAudio();
* @since 0.4.0
*/
Skyway.prototype.enableAudio = function() {
this._handleAV('audio', true);
};
/**
* Disable microphone.
* - If microphone is not enabled from the beginning, there is no effect.
* @method disableAudio
* @example
* SkywayDemo.disableAudio();
* @trigger peerUpdated
* @since 0.4.0
*/
Skyway.prototype.disableAudio = function() {
this._handleAV('audio', false);
};
/**
* Enable webcam video.
* - If webcam is not enabled from the beginning, user would have to reinitate the
* {{#crossLink "Skyway/joinRoom:method"}}joinRoom(){{/crossLink}}
* process and ask for webcam again.
* @method enableVideo
* @example
* SkywayDemo.enableVideo();
* @trigger peerUpdated
* @since 0.4.0
*/
Skyway.prototype.enableVideo = function() {
this._handleAV('video', true);
};
/**
* Disable webcam video.
* - If webcam is not enabled from the beginning, there is no effect.
* - Note that in a Chrome-to-chrome session, each party's peer audio
* may appear muted in when the audio is muted.
* - You may follow up the bug on [here](https://github.com/Temasys/SkywayJS/issues/14).
* @method disableVideo
* @example
* SkywayDemo.disableVideo();
* @trigger peerUpdated
* @since 0.4.0
*/
Skyway.prototype.disableVideo = function() {
this._handleAV('video', false);
};
/**
* Parse stream settings
* @method _parseStreamSettings
* @param {JSON} options Optional. Media Constraints.
* @param {JSON} options.user Optional. User custom data.
* @param {Boolean|JSON} options.audio This call requires audio
* @param {Boolean} options.audio.stereo Enabled stereo or not
* @param {Boolean|JSON} options.video This call requires video
* @param {JSON} options.video.resolution [Rel: Skyway.VIDEO_RESOLUTION]
* @param {Integer} options.video.resolution.width Video width
* @param {Integer} options.video.resolution.height Video height
* @param {Integer} options.video.frameRate Mininum frameRate of Video
* @param {String} options.bandwidth Bandwidth settings
* @param {String} options.bandwidth.audio Audio Bandwidth
* @param {String} options.bandwidth.video Video Bandwidth
* @param {String} options.bandwidth.data Data Bandwidth
* @private
* @since 0.4.0
*/
Skyway.prototype._parseStreamSettings = function(options) {
options = options || {};
this._user.info = this._user.info || {};
this._user.info.settings = this._user.info.settings || {};
this._user.info.mediaStatus = this._user.info.mediaStatus || {};
// Set User
this._user.info.userData = options.user || this._user.info.userData || '';
// Set Bandwidth
this._streamSettings.bandwidth = options.bandwidth ||
this._streamSettings.bandwidth || {};
this._user.info.settings.bandwidth = options.bandwidth ||
this._user.info.settings.bandwidth || {};
// Set audio settings
this._user.info.settings.audio = (typeof options.audio === 'boolean' ||
typeof options.audio === 'object') ? options.audio :
(this._streamSettings.audio || false);
this._user.info.mediaStatus.audioMuted = (options.audio) ?
((typeof this._user.info.mediaStatus.audioMuted === 'boolean') ?
this._user.info.mediaStatus.audioMuted : !options.audio) : true;
console.info(this._user.info.mediaStatus.audioMuted);
// Set video settings
this._user.info.settings.video = (typeof options.video === 'boolean' ||
typeof options.video === 'object') ? options.video :
(this._streamSettings.video || false);
// Set user media status options
this._user.info.mediaStatus.videoMuted = (options.video) ?
((typeof this._user.info.mediaStatus.videoMuted === 'boolean') ?
this._user.info.mediaStatus.videoMuted : !options.video) : true;
console.dir(this._user.info);
if (!options.video && !options.audio) {
return;
}
// If undefined, at least set to boolean
options.video = options.video || false;
options.audio = options.audio || false;
// Set Video
if (typeof options.video === 'object') {
if (typeof options.video.resolution === 'object') {
var width = options.video.resolution.width;
var height = options.video.resolution.height;
var frameRate = (typeof options.video.frameRate === 'number') ?
options.video.frameRate : 50;
if (!width || !height) {
options.video = true;
} else {
options.video = {
mandatory: {
minWidth: width,
minHeight: height
},
optional: [{ minFrameRate: frameRate }]
};
}
}
}
// Set Audio
if (typeof options.audio === 'object') {
options.stereo = (typeof options.audio.stereo === 'boolean') ?
options.audio.stereo : false;
options.audio = true;
}
// Set stream settings
this._streamSettings.video = options.video;
this._streamSettings.audio = options.audio;
this._streamSettings.stereo = options.stereo;
};
/**
* User to join the room.
* - You may call {{#crossLink "Skyway/getUserMedia:method"}}
* getUserMedia(){{/crossLink}} first if you want to get
* MediaStream and joining Room seperately.
* - If <b>joinRoom()</b> parameters is empty, it simply uses
* any previous media or user data settings.
* - If no room is specified, user would be joining the default room.
* @method joinRoom
* @param {String} room Optional. Room to join user in.
* @param {JSON} options Optional. Media Constraints.
* @param {JSON|String} options.user Optional. User custom data.
* @param {Boolean|JSON} options.audio This call requires audio stream.
* @param {Boolean} options.audio.stereo Option to enable stereo
* during call.
* @param {Boolean|JSON} options.video This call requires video stream.
* @param {JSON} options.video.resolution The resolution of video stream.
* [Rel: Skyway.VIDEO_RESOLUTION]
* @param {Integer} options.video.resolution.width
* The video stream resolution width.
* @param {Integer} options.video.resolution.height
* The video stream resolution height.
* @param {Integer} options.video.frameRate
* The video stream mininum frameRate.
* @param {JSON} options.bandwidth Stream bandwidth settings.
* @param {Integer} options.bandwidth.audio Audio stream bandwidth in kbps.
* - Recommended: 50 kbps.
* @param {Integer} options.bandwidth.video Video stream bandwidth in kbps.
* - Recommended: 256 kbps.
* @param {Integer} options.bandwidth.data Data stream bandwidth in kbps.
* - Recommended: 1638400 kbps.
* @example
* // To just join the default room without any video or audio
* // Note that calling joinRoom without any parameters
* // Still sends any available existing MediaStreams allowed.
* // See Examples 2, 3, 4 and 5 etc to prevent video or audio stream
* SkywayDemo.joinRoom();
*
* // To just join the default room with bandwidth settings
* SkywayDemo.joinRoom({
* 'bandwidth': {
* 'data': 14440
* }
* });
*
* // Example 1: To call getUserMedia and joinRoom seperately
* SkywayDemo.getUserMedia();
* SkywayDemo.on('mediaAccessSuccess', function (stream)) {
* attachMediaStream($('.localVideo')[0], stream);
* SkywayDemo.joinRoom();
* });
*
* // Example 2: Join a room without any video or audio
* SkywayDemo.joinRoom('room');
*
* // Example 3: Join a room with audio only
* SkywayDemo.joinRoom('room', {
* 'audio' : true,
* 'video' : false
* });
*
* // Example 4: Join a room with prefixed video width and height settings
* SkywayDemo.joinRoom('room', {
* 'audio' : true,
* 'video' : {
* 'resolution' : {
* 'width' : 640,
* 'height' : 320
* }
* }
* });
*
* // Example 5: Join a room with userData and settings with audio, video
* // and bandwidth
* SkwayDemo.joinRoom({
* 'user': {
* 'item1': 'My custom data',
* 'item2': 'Put whatever, string or JSON or array'
* },
* 'audio' : {
* 'stereo' : true
* },
* 'video' : {
* 'res' : SkywayDemo.VIDEO_RESOLUTION.VGA,
* 'frameRate' : 50
* },
* 'bandwidth' : {
* 'audio' : 48,
* 'video' : 256,
* 'data' : 14480
* }
* });
* @trigger peerJoined
* @since 0.2.0
*/
Skyway.prototype.joinRoom = function(room, mediaOptions) {
console.info(mediaOptions);
var self = this;
if (self._in_room) {
return;
}
var sendJoinRoomMessage = function() {
console.log('API - Joining room: ' + self._room.id);
self._sendMessage({
type: self.SIG_TYPE.JOIN_ROOM,
uid: self._user.id,
cid: self._key,
rid: self._room.id,
userCred: self._user.token,
timeStamp: self._user.timeStamp,
apiOwner: self._user.apiOwner,
roomCred: self._room.token,
start: self._room.start,
len: self._room.len
});
// self._user.peer = self._createPeerConnection(self._user.sid);
};
var doJoinRoom = function() {
var checkChannelOpen = setInterval(function () {
if (!self._channel_open) {
if (self._readyState === self.READY_STATE_CHANGE.COMPLETED) {
self._openChannel();
}
} else {
clearInterval(checkChannelOpen);
self._waitForMediaStream(function() {
sendJoinRoomMessage();
}, mediaOptions);
}
}, 500);
};
if (typeof room === 'string') {
self._reinit({
room: room
}, doJoinRoom);
} else {
mediaOptions = room;
doJoinRoom();
}
};
/**
* User to leave the room.
* @method leaveRoom
* @example
* SkywayDemo.leaveRoom();
* @trigger peerLeft, channelClose
* @since 0.1.0
*/
Skyway.prototype.leaveRoom = function() {
if (!this._in_room) {
return;
}
for (var pc_index in this._peerConnections) {
if (this._peerConnections.hasOwnProperty(pc_index)) {
this._removePeer(pc_index);
}
}
this._in_room = false;
this._closeChannel();
this._trigger('peerLeft', this._user.sid, this._user.info, true);
};
}).call(this);