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. Specifically it:
12 * - Delivers incoming/outgoing signaling messages,
13 * - Adjusts plugin size and position when destop resolution changes,
15 * This class should not access the plugin directly, instead it should
16 * do it through ClientPlugin class which abstracts plugin version
22 /** @suppress {duplicate} */
23 var remoting = remoting || {};
26 * Interval that determines how often the web-app should send a new access token
32 remoting.ACCESS_TOKEN_RESEND_INTERVAL_MS = 15 * 60 * 1000;
35 * @param {remoting.Host} host The host to connect to.
36 * @param {remoting.SignalStrategy} signalStrategy Signal strategy.
37 * @param {HTMLElement} container Container element for the client view.
38 * @param {string} accessCode The IT2Me access code. Blank for Me2Me.
39 * @param {function(boolean, function(string): void): void} fetchPin
40 * Called by Me2Me connections when a PIN needs to be obtained
42 * @param {function(string, string, string,
43 * function(string, string): void): void}
44 * fetchThirdPartyToken Called by Me2Me connections when a third party
45 * authentication token must be obtained.
46 * @param {string} authenticationMethods Comma-separated list of
47 * authentication methods the client should attempt to use.
48 * @param {remoting.DesktopConnectedView.Mode} mode The mode of this connection.
49 * @param {string} clientPairingId For paired Me2Me connections, the
50 * pairing id for this client, as issued by the host.
51 * @param {string} clientPairedSecret For paired Me2Me connections, the
52 * paired secret for this client, as issued by the host.
53 * @param {string} defaultRemapKeys The default set of remap keys, to use
54 * when the client doesn't define any.
56 * @extends {base.EventSourceImpl}
58 remoting.ClientSession = function(host, signalStrategy, container, accessCode,
59 fetchPin, fetchThirdPartyToken,
60 authenticationMethods, mode, clientPairingId,
61 clientPairedSecret, defaultRemapKeys) {
63 this.state_ = remoting.ClientSession.State.CREATED;
66 this.error_ = remoting.Error.NONE;
71 this.accessCode_ = accessCode;
73 this.fetchPin_ = fetchPin;
75 this.fetchThirdPartyToken_ = fetchThirdPartyToken;
77 this.authenticationMethods_ = authenticationMethods;
79 this.clientPairingId_ = clientPairingId;
81 this.clientPairedSecret_ = clientPairedSecret;
84 this.uiHandler_ = new remoting.DesktopConnectedView(
85 this, container, this.host_, mode, defaultRemapKeys,
86 this.onPluginInitialized_.bind(this));
87 remoting.desktopConnectedView = this.uiHandler_;
91 /** @type {remoting.ClientPlugin}
95 this.hasReceivedFrame_ = false;
96 this.logToServer = new remoting.LogToServer(signalStrategy, mode);
99 this.signalStrategy_ = signalStrategy;
100 base.debug.assert(this.signalStrategy_.getState() ==
101 remoting.SignalStrategy.State.CONNECTED);
102 this.signalStrategy_.setIncomingStanzaCallback(
103 this.onIncomingMessage_.bind(this));
104 remoting.formatIq.setJids(this.signalStrategy_.getJid(), host.jabberId);
107 * Allow host-offline error reporting to be suppressed in situations where it
108 * would not be useful, for example, when using a cached host JID.
110 * @type {boolean} @private
112 this.logHostOfflineErrors_ = true;
114 /** @type {remoting.GnubbyAuthHandler} @private */
115 this.gnubbyAuthHandler_ = null;
117 /** @type {remoting.CastExtensionHandler} @private */
118 this.castExtensionHandler_ = null;
120 this.defineEvents(Object.keys(remoting.ClientSession.Events));
123 base.extend(remoting.ClientSession, base.EventSourceImpl);
125 /** @enum {string} */
126 remoting.ClientSession.Events = {
127 stateChanged: 'stateChanged',
128 videoChannelStateChanged: 'videoChannelStateChanged',
131 // Note that the positive values in both of these enums are copied directly
132 // from chromoting_scriptable_object.h and must be kept in sync. The negative
133 // values represent state transitions that occur within the web-app that have
134 // no corresponding plugin state transition.
135 /** @enum {number} */
136 remoting.ClientSession.State = {
137 CONNECTION_CANCELED: -3, // Connection closed (gracefully) before connecting.
138 CONNECTION_DROPPED: -2, // Succeeded, but subsequently closed with an error.
149 * @param {string} state The state name.
150 * @return {remoting.ClientSession.State} The session state enum value.
152 remoting.ClientSession.State.fromString = function(state) {
153 if (!remoting.ClientSession.State.hasOwnProperty(state)) {
154 throw "Invalid ClientSession.State: " + state;
156 return remoting.ClientSession.State[state];
160 @param {remoting.ClientSession.State} current
161 @param {remoting.ClientSession.State} previous
164 remoting.ClientSession.StateEvent = function(current, previous) {
165 /** @type {remoting.ClientSession.State} */
166 this.previous = previous
168 /** @type {remoting.ClientSession.State} */
169 this.current = current;
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 that host/client supports Google Drive integration, and that the
242 // client should send to the host the OAuth tokens to be used by Google Drive
244 GOOGLE_DRIVE: "googleDrive",
246 // Indicates that the client supports the video frame-recording extension.
247 VIDEO_RECORDER: 'videoRecorder',
249 // Indicates that the client supports 'cast'ing the video stream to a
250 // cast-enabled device.
255 * The set of capabilities negotiated between the client and host.
256 * @type {Array<string>}
259 remoting.ClientSession.prototype.capabilities_ = null;
262 * @param {remoting.ClientSession.Capability} capability The capability to test
264 * @return {boolean} True if the capability has been negotiated between
265 * the client and host.
267 remoting.ClientSession.prototype.hasCapability = function(capability) {
268 if (this.capabilities_ == null)
271 return this.capabilities_.indexOf(capability) > -1;
275 * Adds <embed> element to the UI container and readies the session object.
277 * @param {function(string, string):boolean} onExtensionMessage The handler for
278 * protocol extension messages. Returns true if a message is recognized;
280 * @param {Array<string>} requiredCapabilities A list of capabilities
281 * required by this application.
283 remoting.ClientSession.prototype.createPluginAndConnect =
284 function(onExtensionMessage, requiredCapabilities) {
285 this.uiHandler_.createPluginAndConnect(onExtensionMessage,
286 requiredCapabilities);
290 * @param {remoting.Error} error
291 * @param {remoting.ClientPlugin} plugin
293 remoting.ClientSession.prototype.onPluginInitialized_ = function(
295 if (error != remoting.Error.NONE) {
296 this.resetWithError_(error);
299 this.plugin_ = plugin;
300 plugin.setOnOutgoingIqHandler(this.sendIq_.bind(this));
301 plugin.setOnDebugMessageHandler(this.onDebugMessage_.bind(this));
303 plugin.setConnectionStatusUpdateHandler(
304 this.onConnectionStatusUpdate_.bind(this));
305 plugin.setRouteChangedHandler(this.onRouteChanged_.bind(this));
306 plugin.setConnectionReadyHandler(this.onConnectionReady_.bind(this));
307 plugin.setCapabilitiesHandler(this.onSetCapabilities_.bind(this));
308 plugin.setGnubbyAuthHandler(
309 this.processGnubbyAuthMessage_.bind(this));
310 plugin.setCastExtensionHandler(
311 this.processCastExtensionMessage_.bind(this));
313 this.initiateConnection_();
317 * @param {remoting.Error} error
319 remoting.ClientSession.prototype.resetWithError_ = function(error) {
320 this.signalStrategy_.setIncomingStanzaCallback(null);
323 this.setState_(remoting.ClientSession.State.FAILED);
327 * Deletes the <embed> element from the container, without sending a
328 * session_terminate request. This is to be called when the session was
329 * disconnected by the Host.
331 * @return {void} Nothing.
333 remoting.ClientSession.prototype.removePlugin = function() {
334 this.uiHandler_.removePlugin();
339 * Disconnect the current session with a particular |error|. The session will
340 * raise a |stateChanged| event in response to it. The caller should then call
341 * |cleanup| to remove and destroy the <embed> element.
343 * @param {remoting.Error} error The reason for the disconnection. Use
344 * remoting.Error.NONE if there is no error.
345 * @return {void} Nothing.
347 remoting.ClientSession.prototype.disconnect = function(error) {
348 var state = (error == remoting.Error.NONE) ?
349 remoting.ClientSession.State.CLOSED :
350 remoting.ClientSession.State.FAILED;
352 // The plugin won't send a state change notification, so we explicitly log
353 // the fact that the connection has closed.
354 this.logToServer.logClientSessionStateChange(state, error);
356 this.setState_(state);
360 * Deletes the <embed> element from the container and disconnects.
362 * @return {void} Nothing.
364 remoting.ClientSession.prototype.cleanup = function() {
367 'to="' + this.host_.jabberId + '" ' +
369 'id="session-terminate" ' +
370 'xmlns:cli="jabber:client">' +
372 'xmlns="urn:xmpp:jingle:1" ' +
373 'action="session-terminate" ' +
374 'sid="' + this.sessionId_ + '">' +
375 '<reason><success/></reason>' +
382 * @return {remoting.ClientSession.State} The current state.
384 remoting.ClientSession.prototype.getState = function() {
389 * @return {remoting.Error} The current error code.
391 remoting.ClientSession.prototype.getError = function() {
396 * Called when the client receives its first frame.
398 * @return {void} Nothing.
400 remoting.ClientSession.prototype.onFirstFrameReceived = function() {
401 this.hasReceivedFrame_ = true;
405 * @return {boolean} Whether the client has received a video buffer.
407 remoting.ClientSession.prototype.hasReceivedFrame = function() {
408 return this.hasReceivedFrame_;
412 * Sends a signaling message.
414 * @param {string} message XML string of IQ stanza to send to server.
415 * @return {void} Nothing.
418 remoting.ClientSession.prototype.sendIq_ = function(message) {
419 // Extract the session id, so we can close the session later.
420 var parser = new DOMParser();
421 var iqNode = parser.parseFromString(message, 'text/xml').firstChild;
422 var jingleNode = iqNode.firstChild;
424 var action = jingleNode.getAttribute('action');
425 if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') {
426 this.sessionId_ = jingleNode.getAttribute('sid');
430 console.log(remoting.timestamp(), remoting.formatIq.prettifySendIq(message));
431 if (this.signalStrategy_.getState() !=
432 remoting.SignalStrategy.State.CONNECTED) {
433 console.log("Message above is dropped because signaling is not connected.");
437 this.signalStrategy_.sendMessage(message);
441 * @param {string} msg
444 remoting.ClientSession.prototype.onDebugMessage_ = function(msg) {
445 console.log('plugin: ' + msg.trimRight());
449 * @param {Element} message
452 remoting.ClientSession.prototype.onIncomingMessage_ = function(message) {
456 var formatted = new XMLSerializer().serializeToString(message);
457 console.log(remoting.timestamp(),
458 remoting.formatIq.prettifyReceiveIq(formatted));
459 this.plugin_.onIncomingIq(formatted);
465 remoting.ClientSession.prototype.initiateConnection_ = function() {
466 /** @type {remoting.ClientSession} */
469 /** @param {string} sharedSecret Shared secret. */
470 function onSharedSecretReceived(sharedSecret) {
471 that.plugin_.connect(that.host_.jabberId, that.host_.publicKey,
472 that.signalStrategy_.getJid(), sharedSecret,
473 that.authenticationMethods_, that.host_.hostId,
474 that.clientPairingId_, that.clientPairedSecret_);
477 this.getSharedSecret_(onSharedSecretReceived);
481 * Gets shared secret to be used for connection.
483 * @param {function(string)} callback Callback called with the shared secret.
484 * @return {void} Nothing.
487 remoting.ClientSession.prototype.getSharedSecret_ = function(callback) {
488 /** @type remoting.ClientSession */
490 if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.THIRD_PARTY_AUTH)) {
491 /** @type{function(string, string, string): void} */
492 var fetchThirdPartyToken = function(tokenUrl, hostPublicKey, scope) {
493 that.fetchThirdPartyToken_(
494 tokenUrl, hostPublicKey, scope,
495 that.plugin_.onThirdPartyTokenFetched.bind(that.plugin_));
497 this.plugin_.setFetchThirdPartyTokenHandler(fetchThirdPartyToken);
499 if (this.accessCode_) {
500 // Shared secret was already supplied before connecting (It2Me case).
501 callback(this.accessCode_);
502 } else if (this.plugin_.hasFeature(
503 remoting.ClientPlugin.Feature.ASYNC_PIN)) {
504 // Plugin supports asynchronously asking for the PIN.
505 this.plugin_.useAsyncPinDialog();
506 /** @param {boolean} pairingSupported */
507 var fetchPin = function(pairingSupported) {
508 that.fetchPin_(pairingSupported,
509 that.plugin_.onPinFetched.bind(that.plugin_));
511 this.plugin_.setFetchPinHandler(fetchPin);
514 // Clients that don't support asking for a PIN asynchronously also don't
515 // support pairing, so request the PIN now without offering to remember it.
516 this.fetchPin_(false, callback);
521 * Callback that the plugin invokes to indicate that the connection
522 * status has changed.
524 * @param {number} status The plugin's status.
525 * @param {number} error The plugin's error state, if any.
528 remoting.ClientSession.prototype.onConnectionStatusUpdate_ =
529 function(status, error) {
530 if (status == remoting.ClientSession.State.CONNECTED) {
531 this.uiHandler_.updateClientSessionUi_(this);
533 } else if (status == remoting.ClientSession.State.FAILED) {
535 case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE:
536 this.error_ = remoting.Error.HOST_IS_OFFLINE;
538 case remoting.ClientSession.ConnectionError.SESSION_REJECTED:
539 this.error_ = remoting.Error.INVALID_ACCESS_CODE;
541 case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL:
542 this.error_ = remoting.Error.INCOMPATIBLE_PROTOCOL;
544 case remoting.ClientSession.ConnectionError.NETWORK_FAILURE:
545 this.error_ = remoting.Error.P2P_FAILURE;
547 case remoting.ClientSession.ConnectionError.HOST_OVERLOAD:
548 this.error_ = remoting.Error.HOST_OVERLOAD;
551 this.error_ = remoting.Error.UNEXPECTED;
554 this.setState_(/** @type {remoting.ClientSession.State} */ (status));
558 * Callback that the plugin invokes to indicate that the connection type for
559 * a channel has changed.
561 * @param {string} channel The channel name.
562 * @param {string} connectionType The new connection type.
565 remoting.ClientSession.prototype.onRouteChanged_ =
566 function(channel, connectionType) {
567 console.log('plugin: Channel ' + channel + ' using ' +
568 connectionType + ' connection.');
569 this.logToServer.setConnectionType(connectionType);
573 * Callback that the plugin invokes to indicate when the connection is
576 * @param {boolean} ready True if the connection is ready.
579 remoting.ClientSession.prototype.onConnectionReady_ = function(ready) {
580 // TODO(jamiewalch): Currently, the logic for determining whether or not the
581 // connection is available is based solely on whether or not any video frames
582 // have been received recently. which leads to poor UX on slow connections.
583 // Re-enable this once crbug.com/435315 has been fixed.
584 var ignoreVideoChannelState = true;
585 if (ignoreVideoChannelState) {
586 console.log('Video channel ' + (ready ? '' : 'not ') + 'ready.');
590 this.uiHandler_.onConnectionReady(ready);
592 this.raiseEvent(remoting.ClientSession.Events.videoChannelStateChanged,
597 * Called when the client-host capabilities negotiation is complete.
598 * TODO(kelvinp): Move this function out of ClientSession.
600 * @param {!Array<string>} capabilities The set of capabilities negotiated
601 * between the client and host.
602 * @return {void} Nothing.
605 remoting.ClientSession.prototype.onSetCapabilities_ = function(capabilities) {
606 if (this.capabilities_ != null) {
607 console.error('onSetCapabilities_() is called more than once');
611 this.capabilities_ = capabilities;
612 if (this.hasCapability(remoting.ClientSession.Capability.GOOGLE_DRIVE)) {
613 this.sendGoogleDriveAccessToken_();
615 if (this.hasCapability(
616 remoting.ClientSession.Capability.VIDEO_RECORDER)) {
617 this.uiHandler_.initVideoFrameRecorder();
622 * @param {remoting.ClientSession.State} newState The new state for the session.
623 * @return {void} Nothing.
626 remoting.ClientSession.prototype.setState_ = function(newState) {
627 var oldState = this.state_;
628 this.state_ = newState;
629 var state = this.state_;
630 if (oldState == remoting.ClientSession.State.CONNECTING) {
631 if (this.state_ == remoting.ClientSession.State.CLOSED) {
632 state = remoting.ClientSession.State.CONNECTION_CANCELED;
633 } else if (this.state_ == remoting.ClientSession.State.FAILED &&
634 this.error_ == remoting.Error.HOST_IS_OFFLINE &&
635 !this.logHostOfflineErrors_) {
636 // The application requested host-offline errors to be suppressed, for
637 // example, because this connection attempt is using a cached host JID.
638 console.log('Suppressing host-offline error.');
639 state = remoting.ClientSession.State.CONNECTION_CANCELED;
641 } else if (oldState == remoting.ClientSession.State.CONNECTED &&
642 this.state_ == remoting.ClientSession.State.FAILED) {
643 state = remoting.ClientSession.State.CONNECTION_DROPPED;
645 this.logToServer.logClientSessionStateChange(state, this.error_);
646 if (this.state_ == remoting.ClientSession.State.CONNECTED) {
647 this.createGnubbyAuthHandler_();
648 this.createCastExtensionHandler_();
651 this.raiseEvent(remoting.ClientSession.Events.stateChanged,
652 new remoting.ClientSession.StateEvent(newState, oldState)
657 * Returns an associative array with a set of stats for this connection.
659 * @return {remoting.ClientSession.PerfStats} The connection statistics.
661 remoting.ClientSession.prototype.getPerfStats = function() {
662 return this.plugin_.getPerfStats();
668 * @param {remoting.ClientSession.PerfStats} stats
670 remoting.ClientSession.prototype.logStatistics = function(stats) {
671 this.logToServer.logStatistics(stats);
675 * Enable or disable logging of connection errors due to a host being offline.
676 * For example, if attempting a connection using a cached JID, host-offline
677 * errors should not be logged because the JID will be refreshed and the
678 * connection retried.
680 * @param {boolean} enable True to log host-offline errors; false to suppress.
682 remoting.ClientSession.prototype.logHostOfflineErrors = function(enable) {
683 this.logHostOfflineErrors_ = enable;
687 * Request pairing with the host for PIN-less authentication.
689 * @param {string} clientName The human-readable name of the client.
690 * @param {function(string, string):void} onDone Callback to receive the
691 * client id and shared secret when they are available.
693 remoting.ClientSession.prototype.requestPairing = function(clientName, onDone) {
695 this.plugin_.requestPairing(clientName, onDone);
700 * Sends a clipboard item to the host.
702 * @param {string} mimeType The MIME type of the clipboard item.
703 * @param {string} item The clipboard item.
705 remoting.ClientSession.prototype.sendClipboardItem = function(mimeType, item) {
708 this.plugin_.sendClipboardItem(mimeType, item);
712 * Sends an extension message to the host.
714 * @param {string} type The message type.
715 * @param {string} message The message payload.
717 remoting.ClientSession.prototype.sendClientMessage = function(type, message) {
720 this.plugin_.sendClientMessage(type, message);
724 * Send a gnubby-auth extension message to the host.
725 * @param {Object} data The gnubby-auth message data.
727 remoting.ClientSession.prototype.sendGnubbyAuthMessage = function(data) {
730 this.plugin_.sendClientMessage('gnubby-auth', JSON.stringify(data));
734 * Process a remote gnubby auth request.
735 * @param {string} data Remote gnubby request data.
738 remoting.ClientSession.prototype.processGnubbyAuthMessage_ = function(data) {
739 if (this.gnubbyAuthHandler_) {
741 this.gnubbyAuthHandler_.onMessage(data);
742 } catch (/** @type {*} */ err) {
743 console.error('Failed to process gnubby message: ', err);
746 console.error('Received unexpected gnubby message');
751 * Create a gnubby auth handler and inform the host that gnubby auth is
755 remoting.ClientSession.prototype.createGnubbyAuthHandler_ = function() {
756 if (this.uiHandler_.getMode() == remoting.DesktopConnectedView.Mode.ME2ME) {
757 this.gnubbyAuthHandler_ = new remoting.GnubbyAuthHandler(this);
758 // TODO(psj): Move to more generic capabilities mechanism.
759 this.sendGnubbyAuthMessage({'type': 'control', 'option': 'auth-v1'});
764 * Timer callback to send the access token to the host.
767 remoting.ClientSession.prototype.sendGoogleDriveAccessToken_ = function() {
768 if (this.state_ != remoting.ClientSession.State.CONNECTED) {
771 /** @type {remoting.ClientSession} */
774 /** @param {string} token */
775 var sendToken = function(token) {
776 remoting.clientSession.sendClientMessage('accessToken', token);
778 /** @param {remoting.Error} error */
779 var sendError = function(error) {
780 console.log('Failed to refresh access token: ' + error);
782 remoting.identity.getNewToken().
784 catch(remoting.Error.handler(sendError));
785 window.setTimeout(this.sendGoogleDriveAccessToken_.bind(this),
786 remoting.ACCESS_TOKEN_RESEND_INTERVAL_MS);
790 * Send a Cast extension message to the host.
791 * @param {Object} data The cast message data.
793 remoting.ClientSession.prototype.sendCastExtensionMessage = function(data) {
796 this.plugin_.sendClientMessage('cast_message', JSON.stringify(data));
800 * Process a remote Cast extension message from the host.
801 * @param {string} data Remote cast extension data message.
804 remoting.ClientSession.prototype.processCastExtensionMessage_ = function(data) {
805 if (this.castExtensionHandler_) {
807 this.castExtensionHandler_.onMessage(data);
808 } catch (/** @type {*} */ err) {
809 console.error('Failed to process cast message: ', err);
812 console.error('Received unexpected cast message');
817 * Create a CastExtensionHandler and inform the host that cast extension
821 remoting.ClientSession.prototype.createCastExtensionHandler_ = function() {
822 if (remoting.app.hasCapability(remoting.ClientSession.Capability.CAST) &&
823 this.uiHandler_.getMode() == remoting.DesktopConnectedView.Mode.ME2ME) {
824 this.castExtensionHandler_ = new remoting.CastExtensionHandler(this);
829 * Handles protocol extension messages.
830 * @param {string} type Type of extension message.
831 * @param {Object} message The parsed extension message data.
832 * @return {boolean} True if the message was recognized, false otherwise.
834 remoting.ClientSession.prototype.handleExtensionMessage =
835 function(type, message) {
836 if (this.uiHandler_.handleExtensionMessage(type, message)) {
843 * Enables or disables rendering of dirty regions for debugging.
844 * @param {boolean} enable True to enable rendering.
846 remoting.ClientSession.prototype.enableDebugRegion = function(enable) {
848 this.plugin_.setDebugDirtyRegionHandler(
849 this.uiHandler_.handleDebugRegion.bind(this.uiHandler_));
851 this.plugin_.setDebugDirtyRegionHandler(null);