Supervised user whitelists: Cleanup
[chromium-blink-merge.git] / remoting / webapp / crd / js / client_session.js
blob8a6bdb870f63bcd1b15258e48dc3c7dd16452a25
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.
5 /**
6 * @fileoverview
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
12 * messages.
14 * This class should not access the plugin directly, instead it should
15 * do it through ClientPlugin class which abstracts plugin version
16 * differences.
19 'use strict';
21 /** @suppress {duplicate} */
22 var remoting = remoting || {};
24 /**
25 * @param {remoting.ClientPlugin} plugin
26 * @param {remoting.SignalStrategy} signalStrategy Signal strategy.
27 * @param {remoting.ClientSession.EventHandler} listener
29 * @constructor
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);
37 /** @private */
38 this.state_ = remoting.ClientSession.State.INITIALIZING;
40 /** @private {!remoting.Error} */
41 this.error_ = remoting.Error.none();
43 /** @private {remoting.Host} */
44 this.host_ = null;
46 /** @private {remoting.CredentialsProvider} */
47 this.credentialsProvider_ = null;
49 /** @private */
50 this.sessionId_ = '';
52 /** @private */
53 this.listener_ = listener;
55 /** @private */
56 this.hasReceivedFrame_ = false;
58 /** @private */
59 this.logToServer_ = new remoting.LogToServer(signalStrategy);
61 /** @private */
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;
71 /**
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);
83 /** @private */
84 this.connectedDisposables_ = new base.Disposables();
86 this.defineEvents(Object.keys(remoting.ClientSession.Events));
89 /** @enum {string} */
90 remoting.ClientSession.Events = {
91 stateChanged: 'stateChanged', // deprecated.
92 videoChannelStateChanged: 'videoChannelStateChanged',
95 /**
96 * @interface
97 * [START]-------> [onConnected] ------> [onDisconnected]
98 * | |
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 =
111 function(error) {};
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.
143 CREATED: -1,
144 UNKNOWN: 0,
145 INITIALIZING: 1,
146 CONNECTING: 2,
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.
151 AUTHENTICATED: 3,
152 CONNECTED: 4,
153 CLOSED: 5,
154 FAILED: 6
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
171 @constructor
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 = {
183 UNKNOWN: -1,
184 NONE: 0,
185 HOST_IS_OFFLINE: 1,
186 SESSION_REJECTED: 2,
187 INCOMPATIBLE_PROTOCOL: 3,
188 NETWORK_FAILURE: 4,
189 HOST_OVERLOAD: 5
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.
206 * @constructor
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.
236 * @enum {string}
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
252 // on the host.
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.
260 CAST: 'casting',
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) {
270 this.host_ = host;
271 this.credentialsProvider_ = credentialsProvider;
272 this.iqFormatter_ =
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) {
288 this.sendIq_(
289 '<cli:iq ' +
290 'to="' + this.host_.jabberId + '" ' +
291 'type="set" ' +
292 'id="session-terminate" ' +
293 'xmlns:cli="jabber:client">' +
294 '<jingle ' +
295 'xmlns="urn:xmpp:jingle:1" ' +
296 'action="session-terminate" ' +
297 'sid="' + this.sessionId_ + '">' +
298 '<reason><success/></reason>' +
299 '</jingle>' +
300 '</cli:iq>');
302 var state = error.isNone() ?
303 remoting.ClientSession.State.CLOSED :
304 remoting.ClientSession.State.FAILED;
305 this.error_ = error;
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_);
318 this.plugin_ = null;
322 * @return {remoting.ClientSession.State} The current state.
324 remoting.ClientSession.prototype.getState = function() {
325 return this.state_;
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() {
339 return this.error_;
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.
363 * @private
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;
370 if (jingleNode) {
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.");
381 return;
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
403 * @private
405 remoting.ClientSession.prototype.onIncomingMessage_ = function(message) {
406 if (!this.plugin_) {
407 return;
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
421 * state, if any.
423 remoting.ClientSession.prototype.onConnectionStatusUpdate =
424 function(status, error) {
425 if (status == remoting.ClientSession.State.FAILED) {
426 switch (error) {
427 case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE:
428 this.error_ = new remoting.Error(
429 remoting.Error.Tag.HOST_IS_OFFLINE);
430 break;
431 case remoting.ClientSession.ConnectionError.SESSION_REJECTED:
432 this.error_ = new remoting.Error(
433 remoting.Error.Tag.INVALID_ACCESS_CODE);
434 break;
435 case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL:
436 this.error_ = new remoting.Error(
437 remoting.Error.Tag.INCOMPATIBLE_PROTOCOL);
438 break;
439 case remoting.ClientSession.ConnectionError.NETWORK_FAILURE:
440 this.error_ = new remoting.Error(
441 remoting.Error.Tag.P2P_FAILURE);
442 break;
443 case remoting.ClientSession.ConnectionError.HOST_OVERLOAD:
444 this.error_ = new remoting.Error(
445 remoting.Error.Tag.HOST_OVERLOAD);
446 break;
447 default:
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.
460 * @private
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
471 * ready.
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.');
483 return;
486 this.raiseEvent(remoting.ClientSession.Events.videoChannelStateChanged,
487 ready);
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.
503 * @private
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.
524 * @private
526 remoting.ClientSession.prototype.notifyStateChanges_ =
527 function(oldState, newState) {
528 /** @type {remoting.Error} */
529 var 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);
536 break;
538 case remoting.ClientSession.State.CONNECTING:
539 remoting.identity.getEmail().then(function(/** string */ email) {
540 console.log('Connecting as ' + email);
542 break;
544 case remoting.ClientSession.State.AUTHENTICATED:
545 console.log('Connection authenticated.');
546 break;
548 case remoting.ClientSession.State.INITIALIZING:
549 console.log('Connection initializing .');
550 break;
552 case remoting.ClientSession.State.CLOSED:
553 console.log('Connection closed.');
554 this.listener_.onDisconnected();
555 break;
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);
564 break;
566 case remoting.ClientSession.State.CONNECTION_DROPPED:
567 error = this.getError();
568 console.error('Connection dropped: ' + error.toString());
569 this.listener_.onError(error);
570 break;
572 default:
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}
589 * @private
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;
607 return current;
610 /** @private */
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;