1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
7 * Class handling creation and teardown of a remoting client session.
9 * The ClientSession class controls lifetime of the client plugin
10 * object and provides the plugin with the functionality it needs to
11 * establish connection, e.g. delivers incoming/outgoing signaling
14 * This class should not access the plugin directly, instead it should
15 * do it through ClientPlugin class which abstracts plugin version
21 /** @suppress {duplicate} */
22 var remoting
= remoting
|| {};
25 * @param {remoting.ClientPlugin} plugin
26 * @param {remoting.SignalStrategy} signalStrategy Signal strategy.
27 * @param {remoting.ClientSession.EventHandler} listener
30 * @extends {base.EventSourceImpl}
31 * @implements {base.Disposable}
32 * @implements {remoting.ClientPlugin.ConnectionEventHandler}
34 remoting
.ClientSession = function(plugin
, signalStrategy
, listener
) {
35 base
.inherits(this, base
.EventSourceImpl
);
38 this.state_
= remoting
.ClientSession
.State
.INITIALIZING
;
40 /** @private {!remoting.Error} */
41 this.error_
= remoting
.Error
.none();
43 /** @private {remoting.Host} */
46 /** @private {remoting.CredentialsProvider} */
47 this.credentialsProvider_
= null;
53 this.listener_
= listener
;
56 this.hasReceivedFrame_
= false;
59 this.logToServer_
= new remoting
.LogToServer(signalStrategy
);
62 this.signalStrategy_
= signalStrategy
;
63 base
.debug
.assert(this.signalStrategy_
.getState() ==
64 remoting
.SignalStrategy
.State
.CONNECTED
);
65 this.signalStrategy_
.setIncomingStanzaCallback(
66 this.onIncomingMessage_
.bind(this));
68 /** @private {remoting.FormatIq} */
69 this.iqFormatter_
= null;
72 * Allow host-offline error reporting to be suppressed in situations where it
73 * would not be useful, for example, when using a cached host JID.
75 * @type {boolean} @private
77 this.logHostOfflineErrors_
= true;
79 /** @private {remoting.ClientPlugin} */
80 this.plugin_
= plugin
;
81 plugin
.setConnectionEventHandler(this);
84 this.connectedDisposables_
= new base
.Disposables();
86 this.defineEvents(Object
.keys(remoting
.ClientSession
.Events
));
90 remoting
.ClientSession
.Events
= {
91 stateChanged
: 'stateChanged', // deprecated.
92 videoChannelStateChanged
: 'videoChannelStateChanged',
97 * [START]-------> [onConnected] ------> [onDisconnected]
99 * |-----> [OnConnectionFailed] |----> [onError]
101 * TODO(kelvinp): Route session state changes through this interface.
103 remoting
.ClientSession
.EventHandler = function() {};
106 * Called when the connection failed before it is connected.
108 * @param {!remoting.Error} error
110 remoting
.ClientSession
.EventHandler
.prototype.onConnectionFailed
=
114 * Called when a new session has been connected. The |connectionInfo| will be
115 * valid until onDisconnected() or onError() is called.
117 * @param {!remoting.ConnectionInfo} connectionInfo
119 remoting
.ClientSession
.EventHandler
.prototype.onConnected
=
120 function(connectionInfo
) {};
123 * Called when the current session has been disconnected.
125 remoting
.ClientSession
.EventHandler
.prototype.onDisconnected = function() {};
128 * Called when an error needs to be displayed to the user.
129 * @param {!remoting.Error} error
131 remoting
.ClientSession
.EventHandler
.prototype.onError = function(error
) {};
133 // Note that the positive values in both of these enums are copied directly
134 // from connection_to_host.h and must be kept in sync. Code in
135 // chromoting_instance.cc converts the C++ enums into strings that must match
136 // the names given here.
137 // The negative values represent state transitions that occur within the
138 // web-app that have no corresponding plugin state transition.
139 /** @enum {number} */
140 remoting
.ClientSession
.State
= {
141 CONNECTION_CANCELED
: -3, // Connection closed (gracefully) before connecting.
142 CONNECTION_DROPPED
: -2, // Succeeded, but subsequently closed with an error.
147 // We don't currently receive AUTHENTICATED from the host - it comes through
148 // as 'CONNECTING' instead.
149 // TODO(garykac) Update chromoting_instance.cc to send this once we've
150 // shipped a webapp release with support for AUTHENTICATED.
158 * @param {string} state The state name.
159 * @return {remoting.ClientSession.State} The session state enum value.
161 remoting
.ClientSession
.State
.fromString = function(state
) {
162 if (!remoting
.ClientSession
.State
.hasOwnProperty(state
)) {
163 throw "Invalid ClientSession.State: " + state
;
165 return remoting
.ClientSession
.State
[state
];
169 @param {remoting.ClientSession.State} current
170 @param {remoting.ClientSession.State} previous
173 remoting
.ClientSession
.StateEvent = function(current
, previous
) {
174 /** @type {remoting.ClientSession.State} */
175 this.previous
= previous
177 /** @type {remoting.ClientSession.State} */
178 this.current
= current
;
181 /** @enum {number} */
182 remoting
.ClientSession
.ConnectionError
= {
187 INCOMPATIBLE_PROTOCOL
: 3,
193 * @param {string} error The connection error name.
194 * @return {remoting.ClientSession.ConnectionError} The connection error enum.
196 remoting
.ClientSession
.ConnectionError
.fromString = function(error
) {
197 if (!remoting
.ClientSession
.ConnectionError
.hasOwnProperty(error
)) {
198 console
.error('Unexpected ClientSession.ConnectionError string: ', error
);
199 return remoting
.ClientSession
.ConnectionError
.UNKNOWN
;
201 return remoting
.ClientSession
.ConnectionError
[error
];
205 * Type used for performance statistics collected by the plugin.
208 remoting
.ClientSession
.PerfStats = function() {};
209 /** @type {number} */
210 remoting
.ClientSession
.PerfStats
.prototype.videoBandwidth
;
211 /** @type {number} */
212 remoting
.ClientSession
.PerfStats
.prototype.videoFrameRate
;
213 /** @type {number} */
214 remoting
.ClientSession
.PerfStats
.prototype.captureLatency
;
215 /** @type {number} */
216 remoting
.ClientSession
.PerfStats
.prototype.encodeLatency
;
217 /** @type {number} */
218 remoting
.ClientSession
.PerfStats
.prototype.decodeLatency
;
219 /** @type {number} */
220 remoting
.ClientSession
.PerfStats
.prototype.renderLatency
;
221 /** @type {number} */
222 remoting
.ClientSession
.PerfStats
.prototype.roundtripLatency
;
224 // Keys for connection statistics.
225 remoting
.ClientSession
.STATS_KEY_VIDEO_BANDWIDTH
= 'videoBandwidth';
226 remoting
.ClientSession
.STATS_KEY_VIDEO_FRAME_RATE
= 'videoFrameRate';
227 remoting
.ClientSession
.STATS_KEY_CAPTURE_LATENCY
= 'captureLatency';
228 remoting
.ClientSession
.STATS_KEY_ENCODE_LATENCY
= 'encodeLatency';
229 remoting
.ClientSession
.STATS_KEY_DECODE_LATENCY
= 'decodeLatency';
230 remoting
.ClientSession
.STATS_KEY_RENDER_LATENCY
= 'renderLatency';
231 remoting
.ClientSession
.STATS_KEY_ROUNDTRIP_LATENCY
= 'roundtripLatency';
234 * Set of capabilities for which hasCapability() can be used to test.
238 remoting
.ClientSession
.Capability
= {
239 // When enabled this capability causes the client to send its screen
240 // resolution to the host once connection has been established. See
241 // this.plugin_.notifyClientResolution().
242 SEND_INITIAL_RESOLUTION
: 'sendInitialResolution',
244 // Let the host know that we're interested in knowing whether or not it
245 // rate limits desktop-resize requests.
246 // TODO(kelvinp): This has been supported since M-29. Currently we only have
247 // <1000 users on M-29 or below. Remove this and the capability on the host.
248 RATE_LIMIT_RESIZE_REQUESTS
: 'rateLimitResizeRequests',
250 // Indicates that host/client supports Google Drive integration, and that the
251 // client should send to the host the OAuth tokens to be used by Google Drive
253 GOOGLE_DRIVE
: 'googleDrive',
255 // Indicates that the client supports the video frame-recording extension.
256 VIDEO_RECORDER
: 'videoRecorder',
258 // Indicates that the client supports 'cast'ing the video stream to a
259 // cast-enabled device.
264 * Connects to |host| using |credentialsProvider| as the credentails.
266 * @param {remoting.Host} host
267 * @param {remoting.CredentialsProvider} credentialsProvider
269 remoting
.ClientSession
.prototype.connect = function(host
, credentialsProvider
) {
271 this.credentialsProvider_
= credentialsProvider
;
273 new remoting
.FormatIq(this.signalStrategy_
.getJid(), host
.jabberId
);
274 this.plugin_
.connect(this.host_
, this.signalStrategy_
.getJid(),
275 credentialsProvider
);
279 * Disconnect the current session with a particular |error|. The session will
280 * raise a |stateChanged| event in response to it. The caller should then call
281 * dispose() to remove and destroy the <embed> element.
283 * @param {!remoting.Error} error The reason for the disconnection. Use
284 * remoting.Error.none() if there is no error.
285 * @return {void} Nothing.
287 remoting
.ClientSession
.prototype.disconnect = function(error
) {
290 'to="' + this.host_
.jabberId
+ '" ' +
292 'id="session-terminate" ' +
293 'xmlns:cli="jabber:client">' +
295 'xmlns="urn:xmpp:jingle:1" ' +
296 'action="session-terminate" ' +
297 'sid="' + this.sessionId_
+ '">' +
298 '<reason><success/></reason>' +
302 var state
= error
.isNone() ?
303 remoting
.ClientSession
.State
.CLOSED
:
304 remoting
.ClientSession
.State
.FAILED
;
306 this.setState_(state
);
310 * Deletes the <embed> element from the container and disconnects.
312 * @return {void} Nothing.
314 remoting
.ClientSession
.prototype.dispose = function() {
315 base
.dispose(this.connectedDisposables_
);
316 this.connectedDisposables_
= null;
317 base
.dispose(this.plugin_
);
322 * @return {remoting.ClientSession.State} The current state.
324 remoting
.ClientSession
.prototype.getState = function() {
329 * @return {remoting.LogToServer}.
331 remoting
.ClientSession
.prototype.getLogger = function() {
332 return this.logToServer_
;
336 * @return {!remoting.Error} The current error code.
338 remoting
.ClientSession
.prototype.getError = function() {
343 * Called when the client receives its first frame.
345 * @return {void} Nothing.
347 remoting
.ClientSession
.prototype.onFirstFrameReceived = function() {
348 this.hasReceivedFrame_
= true;
352 * @return {boolean} Whether the client has received a video buffer.
354 remoting
.ClientSession
.prototype.hasReceivedFrame = function() {
355 return this.hasReceivedFrame_
;
359 * Sends a signaling message.
361 * @param {string} message XML string of IQ stanza to send to server.
362 * @return {void} Nothing.
365 remoting
.ClientSession
.prototype.sendIq_ = function(message
) {
366 // Extract the session id, so we can close the session later.
367 var parser
= new DOMParser();
368 var iqNode
= parser
.parseFromString(message
, 'text/xml').firstChild
;
369 var jingleNode
= iqNode
.firstChild
;
371 var action
= jingleNode
.getAttribute('action');
372 if (jingleNode
.nodeName
== 'jingle' && action
== 'session-initiate') {
373 this.sessionId_
= jingleNode
.getAttribute('sid');
377 console
.log(base
.timestamp() + this.iqFormatter_
.prettifySendIq(message
));
378 if (this.signalStrategy_
.getState() !=
379 remoting
.SignalStrategy
.State
.CONNECTED
) {
380 console
.log("Message above is dropped because signaling is not connected.");
384 this.signalStrategy_
.sendMessage(message
);
388 * @param {string} message XML string of IQ stanza to send to server.
390 remoting
.ClientSession
.prototype.onOutgoingIq = function(message
) {
391 this.sendIq_(message
);
395 * @param {string} msg
397 remoting
.ClientSession
.prototype.onDebugMessage = function(msg
) {
398 console
.log('plugin: ' + msg
.trimRight());
402 * @param {Element} message
405 remoting
.ClientSession
.prototype.onIncomingMessage_ = function(message
) {
409 var formatted
= new XMLSerializer().serializeToString(message
);
410 console
.log(base
.timestamp() +
411 this.iqFormatter_
.prettifyReceiveIq(formatted
));
412 this.plugin_
.onIncomingIq(formatted
);
416 * Callback that the plugin invokes to indicate that the connection
417 * status has changed.
419 * @param {remoting.ClientSession.State} status The plugin's status.
420 * @param {remoting.ClientSession.ConnectionError} error The plugin's error
423 remoting
.ClientSession
.prototype.onConnectionStatusUpdate
=
424 function(status
, error
) {
425 if (status
== remoting
.ClientSession
.State
.FAILED
) {
427 case remoting
.ClientSession
.ConnectionError
.HOST_IS_OFFLINE
:
428 this.error_
= new remoting
.Error(
429 remoting
.Error
.Tag
.HOST_IS_OFFLINE
);
431 case remoting
.ClientSession
.ConnectionError
.SESSION_REJECTED
:
432 this.error_
= new remoting
.Error(
433 remoting
.Error
.Tag
.INVALID_ACCESS_CODE
);
435 case remoting
.ClientSession
.ConnectionError
.INCOMPATIBLE_PROTOCOL
:
436 this.error_
= new remoting
.Error(
437 remoting
.Error
.Tag
.INCOMPATIBLE_PROTOCOL
);
439 case remoting
.ClientSession
.ConnectionError
.NETWORK_FAILURE
:
440 this.error_
= new remoting
.Error(
441 remoting
.Error
.Tag
.P2P_FAILURE
);
443 case remoting
.ClientSession
.ConnectionError
.HOST_OVERLOAD
:
444 this.error_
= new remoting
.Error(
445 remoting
.Error
.Tag
.HOST_OVERLOAD
);
448 this.error_
= remoting
.Error
.unexpected();
451 this.setState_(status
);
455 * Callback that the plugin invokes to indicate that the connection type for
456 * a channel has changed.
458 * @param {string} channel The channel name.
459 * @param {string} connectionType The new connection type.
462 remoting
.ClientSession
.prototype.onRouteChanged
=
463 function(channel
, connectionType
) {
464 console
.log('plugin: Channel ' + channel
+ ' using ' +
465 connectionType
+ ' connection.');
466 this.logToServer_
.setConnectionType(connectionType
);
470 * Callback that the plugin invokes to indicate when the connection is
473 * @param {boolean} ready True if the connection is ready.
475 remoting
.ClientSession
.prototype.onConnectionReady = function(ready
) {
476 // TODO(jamiewalch): Currently, the logic for determining whether or not the
477 // connection is available is based solely on whether or not any video frames
478 // have been received recently. which leads to poor UX on slow connections.
479 // Re-enable this once crbug.com/435315 has been fixed.
480 var ignoreVideoChannelState
= true;
481 if (ignoreVideoChannelState
) {
482 console
.log('Video channel ' + (ready
? '' : 'not ') + 'ready.');
486 this.raiseEvent(remoting
.ClientSession
.Events
.videoChannelStateChanged
,
490 /** @return {boolean} */
491 remoting
.ClientSession
.prototype.isFinished = function() {
492 var finishedStates
= [
493 remoting
.ClientSession
.State
.CLOSED
,
494 remoting
.ClientSession
.State
.FAILED
,
495 remoting
.ClientSession
.State
.CONNECTION_CANCELED
,
496 remoting
.ClientSession
.State
.CONNECTION_DROPPED
498 return finishedStates
.indexOf(this.getState()) !== -1;
501 * @param {remoting.ClientSession.State} newState The new state for the session.
502 * @return {void} Nothing.
505 remoting
.ClientSession
.prototype.setState_ = function(newState
) {
506 var oldState
= this.state_
;
507 this.state_
= this.translateState_(oldState
, newState
);
509 if (newState
== remoting
.ClientSession
.State
.CONNECTED
) {
510 this.connectedDisposables_
.add(
511 new base
.RepeatingTimer(this.reportStatistics
.bind(this), 1000));
512 } else if (this.isFinished()) {
513 base
.dispose(this.connectedDisposables_
);
514 this.connectedDisposables_
= null;
517 this.notifyStateChanges_(oldState
, this.state_
);
518 this.logToServer_
.logClientSessionStateChange(this.state_
, this.error_
);
522 * @param {remoting.ClientSession.State} oldState The new state for the session.
523 * @param {remoting.ClientSession.State} newState The new state for the session.
526 remoting
.ClientSession
.prototype.notifyStateChanges_
=
527 function(oldState
, newState
) {
528 /** @type {remoting.Error} */
530 switch (this.state_
) {
531 case remoting
.ClientSession
.State
.CONNECTED
:
532 console
.log('Connection established.');
533 var connectionInfo
= new remoting
.ConnectionInfo(
534 this.host_
, this.credentialsProvider_
, this, this.plugin_
);
535 this.listener_
.onConnected(connectionInfo
);
538 case remoting
.ClientSession
.State
.CONNECTING
:
539 remoting
.identity
.getEmail().then(function(/** string */ email
) {
540 console
.log('Connecting as ' + email
);
544 case remoting
.ClientSession
.State
.AUTHENTICATED
:
545 console
.log('Connection authenticated.');
548 case remoting
.ClientSession
.State
.INITIALIZING
:
549 console
.log('Connection initializing .');
552 case remoting
.ClientSession
.State
.CLOSED
:
553 console
.log('Connection closed.');
554 this.listener_
.onDisconnected();
557 case remoting
.ClientSession
.State
.CONNECTION_CANCELED
:
558 case remoting
.ClientSession
.State
.FAILED
:
559 error
= this.getError();
560 if (!error
.isNone()) {
561 console
.error('Connection failed: ' + error
.toString());
563 this.listener_
.onConnectionFailed(error
);
566 case remoting
.ClientSession
.State
.CONNECTION_DROPPED
:
567 error
= this.getError();
568 console
.error('Connection dropped: ' + error
.toString());
569 this.listener_
.onError(error
);
573 console
.error('Unexpected client plugin state: ' + newState
);
574 // This should only happen if the web-app and client plugin get out of
575 // sync, and even then the version check should ensure compatibility.
576 this.listener_
.onError(
577 new remoting
.Error(remoting
.Error
.Tag
.MISSING_PLUGIN
));
580 this.raiseEvent(remoting
.ClientSession
.Events
.stateChanged
,
581 new remoting
.ClientSession
.StateEvent(newState
, oldState
)
586 * @param {remoting.ClientSession.State} previous
587 * @param {remoting.ClientSession.State} current
588 * @return {remoting.ClientSession.State}
591 remoting
.ClientSession
.prototype.translateState_ = function(previous
, current
) {
592 var State
= remoting
.ClientSession
.State
;
593 if (previous
== State
.CONNECTING
|| previous
== State
.AUTHENTICATED
) {
594 if (current
== State
.CLOSED
) {
595 return remoting
.ClientSession
.State
.CONNECTION_CANCELED
;
596 } else if (current
== State
.FAILED
&&
597 this.error_
.hasTag(remoting
.Error
.Tag
.HOST_IS_OFFLINE
) &&
598 !this.logHostOfflineErrors_
) {
599 // The application requested host-offline errors to be suppressed, for
600 // example, because this connection attempt is using a cached host JID.
601 console
.log('Suppressing host-offline error.');
602 return State
.CONNECTION_CANCELED
;
604 } else if (previous
== State
.CONNECTED
&& current
== State
.FAILED
) {
605 return State
.CONNECTION_DROPPED
;
611 remoting
.ClientSession
.prototype.reportStatistics = function() {
612 this.logToServer_
.logStatistics(this.plugin_
.getPerfStats());
616 * Enable or disable logging of connection errors due to a host being offline.
617 * For example, if attempting a connection using a cached JID, host-offline
618 * errors should not be logged because the JID will be refreshed and the
619 * connection retried.
621 * @param {boolean} enable True to log host-offline errors; false to suppress.
623 remoting
.ClientSession
.prototype.logHostOfflineErrors = function(enable
) {
624 this.logHostOfflineErrors_
= enable
;