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.Logger} logger
28 * @param {remoting.ClientSession.EventHandler} listener
31 * @extends {base.EventSourceImpl}
32 * @implements {base.Disposable}
33 * @implements {remoting.ClientPlugin.ConnectionEventHandler}
35 remoting
.ClientSession = function(
36 plugin
, signalStrategy
, logger
, listener
) {
37 base
.inherits(this, base
.EventSourceImpl
);
40 this.state_
= remoting
.ClientSession
.State
.INITIALIZING
;
42 /** @private {!remoting.Error} */
43 this.error_
= remoting
.Error
.none();
45 /** @private {remoting.Host} */
48 /** @private {remoting.CredentialsProvider} */
49 this.credentialsProvider_
= null;
55 this.listener_
= listener
;
58 this.hasReceivedFrame_
= false;
60 /** @private {remoting.Logger} */
61 this.logger_
= logger
;
64 this.signalStrategy_
= signalStrategy
;
66 var state
= this.signalStrategy_
.getState();
67 console
.assert(state
== remoting
.SignalStrategy
.State
.CONNECTED
,
68 'ClientSession ctor called in state ' + state
+ '.');
69 this.signalStrategy_
.setIncomingStanzaCallback(
70 this.onIncomingMessage_
.bind(this));
72 /** @private {remoting.FormatIq} */
73 this.iqFormatter_
= null;
75 /** @private {remoting.XmppErrorCache} */
76 this.xmppErrorCache_
= new remoting
.XmppErrorCache();
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 videoChannelStateChanged
: 'videoChannelStateChanged'
96 * [START]-------> [onConnected] ------> [onDisconnected]
98 * |-----> [OnConnectionFailed]
101 remoting
.ClientSession
.EventHandler = function() {};
104 * Called when the connection failed before it is connected.
106 * @param {!remoting.Error} error
108 remoting
.ClientSession
.EventHandler
.prototype.onConnectionFailed
=
112 * Called when a new session has been connected. The |connectionInfo| will be
113 * valid until onDisconnected() is called.
115 * @param {!remoting.ConnectionInfo} connectionInfo
117 remoting
.ClientSession
.EventHandler
.prototype.onConnected
=
118 function(connectionInfo
) {};
121 * Called when the current session has been disconnected.
123 * @param {!remoting.Error} reason Reason that the session is disconnected.
124 * Set to remoting.Error.none() if there is no error.
126 remoting
.ClientSession
.EventHandler
.prototype.onDisconnected
=
129 // Note that the positive values in both of these enums are copied directly
130 // from connection_to_host.h and must be kept in sync. Code in
131 // chromoting_instance.cc converts the C++ enums into strings that must match
132 // the names given here.
133 // The negative values represent state transitions that occur within the
134 // web-app that have no corresponding plugin state transition.
136 // TODO(kelvinp): Merge this enum with remoting.ChromotingEvent.SessionState
137 // once we have migrated away from XMPP-based logging (crbug.com/523423).
138 // NOTE: The enums here correspond to the Chromoting.Connections enumerated
139 // histogram defined in src/tools/metrics/histograms/histograms.xml. UMA
140 // histograms don't work well with negative values, so only non-negative values
141 // have been used for Chromoting.Connections.
142 // The maximum values for the UMA enumerated histogram is included here for use
143 // when uploading values to UMA.
144 // The 2 lists should be kept in sync, and any new enums should be append-only.
145 /** @enum {number} */
146 remoting
.ClientSession
.State
= {
148 CONNECTION_CANCELED
: -3, // Connection closed (gracefully) before connecting.
149 CONNECTION_DROPPED
: -2, // Succeeded, but subsequently closed with an error.
162 * @param {string} state The state name.
163 * @return {remoting.ClientSession.State} The session state enum value.
165 remoting
.ClientSession
.State
.fromString = function(state
) {
166 if (!remoting
.ClientSession
.State
.hasOwnProperty(state
)) {
167 throw "Invalid ClientSession.State: " + state
;
169 return remoting
.ClientSession
.State
[state
];
172 /** @enum {number} */
173 remoting
.ClientSession
.ConnectionError
= {
178 INCOMPATIBLE_PROTOCOL
: 3,
184 * @param {string} error The connection error name.
185 * @return {remoting.ClientSession.ConnectionError} The connection error enum.
187 remoting
.ClientSession
.ConnectionError
.fromString = function(error
) {
188 if (!remoting
.ClientSession
.ConnectionError
.hasOwnProperty(error
)) {
189 console
.error('Unexpected ClientSession.ConnectionError string: ', error
);
190 return remoting
.ClientSession
.ConnectionError
.UNKNOWN
;
192 return remoting
.ClientSession
.ConnectionError
[error
];
196 * Type used for performance statistics collected by the plugin.
199 remoting
.ClientSession
.PerfStats = function() {};
200 /** @type {number} */
201 remoting
.ClientSession
.PerfStats
.prototype.videoBandwidth
;
202 /** @type {number} */
203 remoting
.ClientSession
.PerfStats
.prototype.videoFrameRate
;
204 /** @type {number} */
205 remoting
.ClientSession
.PerfStats
.prototype.captureLatency
;
206 /** @type {number} */
207 remoting
.ClientSession
.PerfStats
.prototype.encodeLatency
;
208 /** @type {number} */
209 remoting
.ClientSession
.PerfStats
.prototype.decodeLatency
;
210 /** @type {number} */
211 remoting
.ClientSession
.PerfStats
.prototype.renderLatency
;
212 /** @type {number} */
213 remoting
.ClientSession
.PerfStats
.prototype.roundtripLatency
;
215 // Keys for connection statistics.
216 remoting
.ClientSession
.STATS_KEY_VIDEO_BANDWIDTH
= 'videoBandwidth';
217 remoting
.ClientSession
.STATS_KEY_VIDEO_FRAME_RATE
= 'videoFrameRate';
218 remoting
.ClientSession
.STATS_KEY_CAPTURE_LATENCY
= 'captureLatency';
219 remoting
.ClientSession
.STATS_KEY_ENCODE_LATENCY
= 'encodeLatency';
220 remoting
.ClientSession
.STATS_KEY_DECODE_LATENCY
= 'decodeLatency';
221 remoting
.ClientSession
.STATS_KEY_RENDER_LATENCY
= 'renderLatency';
222 remoting
.ClientSession
.STATS_KEY_ROUNDTRIP_LATENCY
= 'roundtripLatency';
225 * Set of capabilities for which hasCapability() can be used to test.
229 remoting
.ClientSession
.Capability
= {
230 // When enabled this capability causes the client to send its screen
231 // resolution to the host once connection has been established. See
232 // this.plugin_.notifyClientResolution().
233 SEND_INITIAL_RESOLUTION
: 'sendInitialResolution',
235 // Let the host know that we're interested in knowing whether or not it
236 // rate limits desktop-resize requests.
237 // TODO(kelvinp): This has been supported since M-29. Currently we only have
238 // <1000 users on M-29 or below. Remove this and the capability on the host.
239 RATE_LIMIT_RESIZE_REQUESTS
: 'rateLimitResizeRequests',
241 // Indicates native touch input support. If the host does not support
242 // touch then the client will let Chrome synthesize mouse events from touch
243 // input, for compatibility with non-touch-aware systems.
244 TOUCH_EVENTS
: 'touchEvents',
246 // Indicates that host/client supports Google Drive integration, and that the
247 // client should send to the host the OAuth tokens to be used by Google Drive
249 GOOGLE_DRIVE
: 'googleDrive',
251 // Indicates that the client supports the video frame-recording extension.
252 VIDEO_RECORDER
: 'videoRecorder',
254 // Indicates that the client supports 'cast'ing the video stream to a
255 // cast-enabled device.
258 // Indicates desktop shape support.
259 DESKTOP_SHAPE
: 'desktopShape',
263 * Connects to |host| using |credentialsProvider| as the credentails.
265 * @param {remoting.Host} host
266 * @param {remoting.CredentialsProvider} credentialsProvider
268 remoting
.ClientSession
.prototype.connect = function(host
, credentialsProvider
) {
270 this.credentialsProvider_
= credentialsProvider
;
272 new remoting
.FormatIq(this.signalStrategy_
.getJid(), host
.jabberId
);
273 this.plugin_
.connect(this.host_
, this.signalStrategy_
.getJid(),
274 credentialsProvider
);
278 * Disconnect the current session with a particular |error|. The session will
279 * raise a |stateChanged| event in response to it. The caller should then call
280 * dispose() to remove and destroy the <embed> element.
282 * @param {!remoting.Error} error The reason for the disconnection. Use
283 * remoting.Error.none() if there is no error.
284 * @return {void} Nothing.
286 remoting
.ClientSession
.prototype.disconnect = function(error
) {
287 if (this.isFinished()) {
288 // Do not send the session-terminate Iq if disconnect() is already called or
289 // if it is initiated by the host.
295 'to="' + this.host_
.jabberId
+ '" ' +
297 'id="session-terminate" ' +
298 'xmlns:cli="jabber:client">' +
300 'xmlns="urn:xmpp:jingle:1" ' +
301 'action="session-terminate" ' +
302 'sid="' + this.sessionId_
+ '">' +
303 '<reason><success/></reason>' +
307 var state
= error
.isNone() ?
308 remoting
.ClientSession
.State
.CLOSED
:
309 remoting
.ClientSession
.State
.FAILED
;
311 this.setState_(state
);
315 * Deletes the <embed> element from the container and disconnects.
317 * @return {void} Nothing.
319 remoting
.ClientSession
.prototype.dispose = function() {
320 base
.dispose(this.connectedDisposables_
);
321 this.connectedDisposables_
= null;
322 base
.dispose(this.plugin_
);
327 * @return {remoting.ClientSession.State} The current state.
329 remoting
.ClientSession
.prototype.getState = function() {
334 * @return {remoting.Logger}.
336 remoting
.ClientSession
.prototype.getLogger = function() {
341 * @return {!remoting.Error} The current error code.
343 remoting
.ClientSession
.prototype.getError = function() {
348 * Drop the session when the computer is suspended for more than
349 * |suspendDurationInMS|.
351 * @param {number} suspendDurationInMS maximum duration of suspension allowed
352 * before the session will be dropped.
354 remoting
.ClientSession
.prototype.dropSessionOnSuspend = function(
355 suspendDurationInMS
) {
356 if (this.state_
!== remoting
.ClientSession
.State
.CONNECTED
) {
357 console
.error('The session is not connected.');
361 var suspendDetector
= new remoting
.SuspendDetector(suspendDurationInMS
);
362 this.connectedDisposables_
.add(
365 suspendDetector
, remoting
.SuspendDetector
.Events
.resume
,
366 this.disconnect
.bind(
367 this, new remoting
.Error(remoting
.Error
.Tag
.CLIENT_SUSPENDED
))));
371 * Called when the client receives its first frame.
373 * @return {void} Nothing.
375 remoting
.ClientSession
.prototype.onFirstFrameReceived = function() {
376 this.hasReceivedFrame_
= true;
380 * @return {boolean} Whether the client has received a video buffer.
382 remoting
.ClientSession
.prototype.hasReceivedFrame = function() {
383 return this.hasReceivedFrame_
;
387 * Sends a signaling message.
389 * @param {string} message XML string of IQ stanza to send to server.
390 * @return {void} Nothing.
393 remoting
.ClientSession
.prototype.sendIq_ = function(message
) {
394 // Extract the session id, so we can close the session later.
395 var parser
= new DOMParser();
396 var iqNode
= parser
.parseFromString(message
, 'text/xml').firstChild
;
397 var jingleNode
= iqNode
.firstChild
;
399 var action
= jingleNode
.getAttribute('action');
400 if (jingleNode
.nodeName
== 'jingle' && action
== 'session-initiate') {
401 this.sessionId_
= jingleNode
.getAttribute('sid');
405 console
.log(base
.timestamp() + this.iqFormatter_
.prettifySendIq(message
));
406 if (this.signalStrategy_
.getState() !=
407 remoting
.SignalStrategy
.State
.CONNECTED
) {
408 console
.log("Message above is dropped because signaling is not connected.");
412 this.signalStrategy_
.sendMessage(message
);
416 * @param {string} message XML string of IQ stanza to send to server.
418 remoting
.ClientSession
.prototype.onOutgoingIq = function(message
) {
419 this.sendIq_(message
);
423 * @param {string} msg
425 remoting
.ClientSession
.prototype.onDebugMessage = function(msg
) {
426 console
.log('plugin: ' + msg
.trimRight());
430 * @param {Element} message
433 remoting
.ClientSession
.prototype.onIncomingMessage_ = function(message
) {
437 var formatted
= new XMLSerializer().serializeToString(message
);
438 console
.log(base
.timestamp() +
439 this.iqFormatter_
.prettifyReceiveIq(formatted
));
440 this.xmppErrorCache_
.processStanza(message
);
441 this.plugin_
.onIncomingIq(formatted
);
445 * Callback that the plugin invokes to indicate that the connection
446 * status has changed.
448 * @param {remoting.ClientSession.State} status The plugin's status.
449 * @param {remoting.ClientSession.ConnectionError} error The plugin's error
452 remoting
.ClientSession
.prototype.onConnectionStatusUpdate
=
453 function(status
, error
) {
454 if (status
== remoting
.ClientSession
.State
.FAILED
) {
456 case remoting
.ClientSession
.ConnectionError
.HOST_IS_OFFLINE
:
457 this.error_
= new remoting
.Error(
458 remoting
.Error
.Tag
.HOST_IS_OFFLINE
);
460 case remoting
.ClientSession
.ConnectionError
.SESSION_REJECTED
:
461 this.error_
= new remoting
.Error(
462 remoting
.Error
.Tag
.INVALID_ACCESS_CODE
);
464 case remoting
.ClientSession
.ConnectionError
.INCOMPATIBLE_PROTOCOL
:
465 this.error_
= new remoting
.Error(
466 remoting
.Error
.Tag
.INCOMPATIBLE_PROTOCOL
);
468 case remoting
.ClientSession
.ConnectionError
.NETWORK_FAILURE
:
469 this.error_
= new remoting
.Error(
470 remoting
.Error
.Tag
.P2P_FAILURE
);
472 case remoting
.ClientSession
.ConnectionError
.HOST_OVERLOAD
:
473 this.error_
= new remoting
.Error(
474 remoting
.Error
.Tag
.HOST_OVERLOAD
);
477 this.error_
= remoting
.Error
.unexpected();
480 this.setState_(status
);
484 * Callback that the plugin invokes to indicate that the connection type for
485 * a channel has changed.
487 * @param {string} channel The channel name.
488 * @param {string} connectionType The new connection type.
491 remoting
.ClientSession
.prototype.onRouteChanged = function(channel
,
493 this.logger_
.setConnectionType(connectionType
);
497 * Callback that the plugin invokes to indicate when the connection is
500 * @param {boolean} ready True if the connection is ready.
502 remoting
.ClientSession
.prototype.onConnectionReady = function(ready
) {
503 // TODO(jamiewalch): Currently, the logic for determining whether or not the
504 // connection is available is based solely on whether or not any video frames
505 // have been received recently. which leads to poor UX on slow connections.
506 // Re-enable this once crbug.com/435315 has been fixed.
507 var ignoreVideoChannelState
= true;
508 if (ignoreVideoChannelState
) {
509 console
.log('Video channel ' + (ready
? '' : 'not ') + 'ready.');
513 this.raiseEvent(remoting
.ClientSession
.Events
.videoChannelStateChanged
,
517 /** @return {boolean} */
518 remoting
.ClientSession
.prototype.isFinished = function() {
519 var finishedStates
= [
520 remoting
.ClientSession
.State
.CLOSED
,
521 remoting
.ClientSession
.State
.FAILED
,
522 remoting
.ClientSession
.State
.CONNECTION_CANCELED
,
523 remoting
.ClientSession
.State
.CONNECTION_DROPPED
525 return finishedStates
.indexOf(this.getState()) !== -1;
528 * @param {remoting.ClientSession.State} newState The new state for the session.
529 * @return {void} Nothing.
532 remoting
.ClientSession
.prototype.setState_ = function(newState
) {
533 // If we are at a finished state, ignore further state changes.
534 if (this.isFinished()) {
538 var oldState
= this.state_
;
539 this.state_
= this.translateState_(oldState
, newState
);
541 if (newState
== remoting
.ClientSession
.State
.CONNECTED
) {
542 this.connectedDisposables_
.add(
543 new base
.RepeatingTimer(this.reportStatistics
.bind(this), 1000));
544 if (this.plugin_
.hasCapability(
545 remoting
.ClientSession
.Capability
.TOUCH_EVENTS
)) {
546 this.plugin_
.enableTouchEvents(true);
548 } else if (this.isFinished()) {
549 base
.dispose(this.connectedDisposables_
);
550 this.connectedDisposables_
= null;
553 this.notifyStateChanges_(oldState
, this.state_
);
554 // Record state count in an UMA enumerated histogram.
555 recordState(this.state_
);
556 this.logger_
.logClientSessionStateChange(
557 this.state_
, this.error_
, this.xmppErrorCache_
.getFirstError());
561 * Records a Chromoting Connection State, stored in an UMA enumerated histogram.
562 * @param {remoting.ClientSession.State} state State identifier.
564 function recordState(state
) {
565 // According to src/base/metrics/histogram.h, for a UMA enumerated histogram,
566 // the upper limit should be 1 above the max-enum.
567 var histogram_max
= remoting
.ClientSession
.State
.MAX_STATE_ENUM
-
568 remoting
.ClientSession
.State
.MIN_STATE_ENUM
+ 1;
570 var metricDescription
= {
571 metricName
: 'Chromoting.Connections',
572 type
: 'histogram-linear',
573 // According to histogram.h, minimum should be 1. Values less than minimum
574 // end up in the 0th bucket.
577 // The # of buckets should include 1 for underflow.
578 buckets
: histogram_max
+ 1
581 chrome
.metricsPrivate
.recordValue(metricDescription
, state
-
582 remoting
.ClientSession
.State
.MIN_STATE_ENUM
);
586 * @param {remoting.ClientSession.State} oldState The new state for the session.
587 * @param {remoting.ClientSession.State} newState The new state for the session.
590 remoting
.ClientSession
.prototype.notifyStateChanges_
=
591 function(oldState
, newState
) {
592 /** @type {remoting.Error} */
594 switch (this.state_
) {
595 case remoting
.ClientSession
.State
.CONNECTED
:
596 console
.log('Connection established.');
597 var connectionInfo
= new remoting
.ConnectionInfo(
598 this.host_
, this.credentialsProvider_
, this, this.plugin_
);
599 this.listener_
.onConnected(connectionInfo
);
602 case remoting
.ClientSession
.State
.CONNECTING
:
603 remoting
.identity
.getEmail().then(function(/** string */ email
) {
604 console
.log('Connecting as ' + email
);
608 case remoting
.ClientSession
.State
.AUTHENTICATED
:
609 console
.log('Connection authenticated.');
612 case remoting
.ClientSession
.State
.INITIALIZING
:
613 console
.log('Connection initializing .');
616 case remoting
.ClientSession
.State
.CLOSED
:
617 console
.log('Connection closed.');
618 this.listener_
.onDisconnected(remoting
.Error
.none());
621 case remoting
.ClientSession
.State
.CONNECTION_CANCELED
:
622 case remoting
.ClientSession
.State
.FAILED
:
623 error
= this.getError();
624 if (!error
.isNone()) {
625 console
.error('Connection failed: ' + error
.toString());
627 this.listener_
.onConnectionFailed(error
);
630 case remoting
.ClientSession
.State
.CONNECTION_DROPPED
:
631 error
= this.getError();
632 console
.error('Connection dropped: ' + error
.toString());
633 this.listener_
.onDisconnected(error
);
637 console
.error('Unexpected client plugin state: ' + newState
);
642 * @param {remoting.ClientSession.State} previous
643 * @param {remoting.ClientSession.State} current
644 * @return {remoting.ClientSession.State}
647 remoting
.ClientSession
.prototype.translateState_ = function(previous
, current
) {
648 var State
= remoting
.ClientSession
.State
;
649 if (previous
== State
.CONNECTING
|| previous
== State
.AUTHENTICATED
) {
650 if (current
== State
.CLOSED
) {
651 return remoting
.ClientSession
.State
.CONNECTION_CANCELED
;
653 } else if (previous
== State
.CONNECTED
&& current
== State
.FAILED
) {
654 return State
.CONNECTION_DROPPED
;
660 remoting
.ClientSession
.prototype.reportStatistics = function() {
661 this.logger_
.logStatistics(this.plugin_
.getPerfStats());