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 * True if Cast capability is supported.
30 remoting.enableCast = false;
33 * @param {remoting.SignalStrategy} signalStrategy Signal strategy.
34 * @param {HTMLElement} container Container element for the client view.
35 * @param {string} hostDisplayName A human-readable name for the host.
36 * @param {string} accessCode The IT2Me access code. Blank for Me2Me.
37 * @param {function(boolean, function(string): void): void} fetchPin
38 * Called by Me2Me connections when a PIN needs to be obtained
40 * @param {function(string, string, string,
41 * function(string, string): void): void}
42 * fetchThirdPartyToken Called by Me2Me connections when a third party
43 * authentication token must be obtained.
44 * @param {string} authenticationMethods Comma-separated list of
45 * authentication methods the client should attempt to use.
46 * @param {string} hostId The host identifier for Me2Me, or empty for IT2Me.
47 * Mixed into authentication hashes for some authentication methods.
48 * @param {string} hostJid The jid of the host to connect to.
49 * @param {string} hostPublicKey The base64 encoded version of the host's
51 * @param {remoting.ClientSession.Mode} mode The mode of this connection.
52 * @param {string} clientPairingId For paired Me2Me connections, the
53 * pairing id for this client, as issued by the host.
54 * @param {string} clientPairedSecret For paired Me2Me connections, the
55 * paired secret for this client, as issued by the host.
57 * @extends {base.EventSource}
59 remoting.ClientSession = function(signalStrategy, container, hostDisplayName,
60 accessCode, fetchPin, fetchThirdPartyToken,
61 authenticationMethods, hostId, hostJid,
62 hostPublicKey, mode, clientPairingId,
65 this.state_ = remoting.ClientSession.State.CREATED;
68 this.error_ = remoting.Error.NONE;
70 /** @type {HTMLElement}
72 this.container_ = container;
75 this.hostDisplayName_ = hostDisplayName;
77 this.hostJid_ = hostJid;
79 this.hostPublicKey_ = hostPublicKey;
81 this.accessCode_ = accessCode;
83 this.fetchPin_ = fetchPin;
85 this.fetchThirdPartyToken_ = fetchThirdPartyToken;
87 this.authenticationMethods_ = authenticationMethods;
89 this.hostId_ = hostId;
93 this.clientPairingId_ = clientPairingId;
95 this.clientPairedSecret_ = clientPairedSecret;
98 /** @type {remoting.ClientPlugin}
102 this.shrinkToFit_ = true;
104 this.resizeToClient_ = true;
106 this.remapKeys_ = '';
108 this.hasReceivedFrame_ = false;
109 this.logToServer = new remoting.LogToServer();
112 this.signalStrategy_ = signalStrategy;
113 base.debug.assert(this.signalStrategy_.getState() ==
114 remoting.SignalStrategy.State.CONNECTED);
115 this.signalStrategy_.setIncomingStanzaCallback(
116 this.onIncomingMessage_.bind(this));
117 remoting.formatIq.setJids(this.signalStrategy_.getJid(), hostJid);
119 /** @type {number?} @private */
120 this.notifyClientResolutionTimer_ = null;
121 /** @type {number?} @private */
122 this.bumpScrollTimer_ = null;
124 // Bump-scroll test variables. Override to use a fake value for the width
125 // and height of the client plugin so that bump-scrolling can be tested
126 // without relying on the actual size of the host desktop.
127 /** @type {number} @private */
128 this.pluginWidthForBumpScrollTesting = 0;
129 /** @type {number} @private */
130 this.pluginHeightForBumpScrollTesting = 0;
133 * Allow host-offline error reporting to be suppressed in situations where it
134 * would not be useful, for example, when using a cached host JID.
136 * @type {boolean} @private
138 this.logHostOfflineErrors_ = true;
141 this.callPluginLostFocus_ = this.pluginLostFocus_.bind(this);
143 this.callPluginGotFocus_ = this.pluginGotFocus_.bind(this);
145 this.callOnFullScreenChanged_ = this.onFullScreenChanged_.bind(this)
147 /** @type {HTMLMediaElement} @private */
150 /** @type {Element} @private */
151 this.mouseCursorOverlay_ =
152 this.container_.querySelector('.mouse-cursor-overlay');
154 /** @type {Element} */
155 var img = this.mouseCursorOverlay_;
156 /** @param {Event} event @private */
157 this.updateMouseCursorPosition_ = function(event) {
158 img.style.top = event.y + 'px';
159 img.style.left = event.x + 'px';
162 /** @type {remoting.GnubbyAuthHandler} @private */
163 this.gnubbyAuthHandler_ = null;
165 /** @type {remoting.CastExtensionHandler} @private */
166 this.castExtensionHandler_ = null;
168 /** @type {remoting.VideoFrameRecorder} @private */
169 this.videoFrameRecorder_ = null;
171 this.defineEvents(Object.keys(remoting.ClientSession.Events));
174 base.extend(remoting.ClientSession, base.EventSource);
176 /** @enum {string} */
177 remoting.ClientSession.Events = {
178 stateChanged: 'stateChanged',
179 videoChannelStateChanged: 'videoChannelStateChanged',
180 bumpScrollStarted: 'bumpScrollStarted',
181 bumpScrollStopped: 'bumpScrollStopped'
185 * Get host display name.
189 remoting.ClientSession.prototype.getHostDisplayName = function() {
190 return this.hostDisplayName_;
194 * Called when the window or desktop size or the scaling settings change,
195 * to set the scroll-bar visibility.
197 * TODO(jamiewalch): crbug.com/252796: Remove this once crbug.com/240772 is
200 remoting.ClientSession.prototype.updateScrollbarVisibility = function() {
201 var needsVerticalScroll = false;
202 var needsHorizontalScroll = false;
203 if (!this.shrinkToFit_) {
204 // Determine whether or not horizontal or vertical scrollbars are
205 // required, taking into account their width.
206 var clientArea = this.getClientArea_();
207 needsVerticalScroll = clientArea.height < this.plugin_.getDesktopHeight();
208 needsHorizontalScroll = clientArea.width < this.plugin_.getDesktopWidth();
209 var kScrollBarWidth = 16;
210 if (needsHorizontalScroll && !needsVerticalScroll) {
211 needsVerticalScroll =
212 clientArea.height - kScrollBarWidth < this.plugin_.getDesktopHeight();
213 } else if (!needsHorizontalScroll && needsVerticalScroll) {
214 needsHorizontalScroll =
215 clientArea.width - kScrollBarWidth < this.plugin_.getDesktopWidth();
219 var scroller = document.getElementById('scroller');
220 if (needsHorizontalScroll) {
221 scroller.classList.remove('no-horizontal-scroll');
223 scroller.classList.add('no-horizontal-scroll');
225 if (needsVerticalScroll) {
226 scroller.classList.remove('no-vertical-scroll');
228 scroller.classList.add('no-vertical-scroll');
233 * @return {boolean} True if shrink-to-fit is enabled; false otherwise.
235 remoting.ClientSession.prototype.getShrinkToFit = function() {
236 return this.shrinkToFit_;
240 * @return {boolean} True if resize-to-client is enabled; false otherwise.
242 remoting.ClientSession.prototype.getResizeToClient = function() {
243 return this.resizeToClient_;
246 // Note that the positive values in both of these enums are copied directly
247 // from chromoting_scriptable_object.h and must be kept in sync. The negative
248 // values represent state transitions that occur within the web-app that have
249 // no corresponding plugin state transition.
250 /** @enum {number} */
251 remoting.ClientSession.State = {
252 CONNECTION_CANCELED: -3, // Connection closed (gracefully) before connecting.
253 CONNECTION_DROPPED: -2, // Succeeded, but subsequently closed with an error.
264 * @param {string} state The state name.
265 * @return {remoting.ClientSession.State} The session state enum value.
267 remoting.ClientSession.State.fromString = function(state) {
268 if (!remoting.ClientSession.State.hasOwnProperty(state)) {
269 throw "Invalid ClientSession.State: " + state;
271 return remoting.ClientSession.State[state];
276 @param {remoting.ClientSession.State} current
277 @param {remoting.ClientSession.State} previous
279 remoting.ClientSession.StateEvent = function(current, previous) {
280 /** @type {remoting.ClientSession.State} */
281 this.previous = previous
283 /** @type {remoting.ClientSession.State} */
284 this.current = current;
287 /** @enum {number} */
288 remoting.ClientSession.ConnectionError = {
293 INCOMPATIBLE_PROTOCOL: 3,
299 * @param {string} error The connection error name.
300 * @return {remoting.ClientSession.ConnectionError} The connection error enum.
302 remoting.ClientSession.ConnectionError.fromString = function(error) {
303 if (!remoting.ClientSession.ConnectionError.hasOwnProperty(error)) {
304 console.error('Unexpected ClientSession.ConnectionError string: ', error);
305 return remoting.ClientSession.ConnectionError.UNKNOWN;
307 return remoting.ClientSession.ConnectionError[error];
310 // The mode of this session.
311 /** @enum {number} */
312 remoting.ClientSession.Mode = {
318 * Type used for performance statistics collected by the plugin.
321 remoting.ClientSession.PerfStats = function() {};
322 /** @type {number} */
323 remoting.ClientSession.PerfStats.prototype.videoBandwidth;
324 /** @type {number} */
325 remoting.ClientSession.PerfStats.prototype.videoFrameRate;
326 /** @type {number} */
327 remoting.ClientSession.PerfStats.prototype.captureLatency;
328 /** @type {number} */
329 remoting.ClientSession.PerfStats.prototype.encodeLatency;
330 /** @type {number} */
331 remoting.ClientSession.PerfStats.prototype.decodeLatency;
332 /** @type {number} */
333 remoting.ClientSession.PerfStats.prototype.renderLatency;
334 /** @type {number} */
335 remoting.ClientSession.PerfStats.prototype.roundtripLatency;
337 // Keys for connection statistics.
338 remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'videoBandwidth';
339 remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'videoFrameRate';
340 remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'captureLatency';
341 remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encodeLatency';
342 remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decodeLatency';
343 remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'renderLatency';
344 remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtripLatency';
346 // Keys for per-host settings.
347 remoting.ClientSession.KEY_REMAP_KEYS = 'remapKeys';
348 remoting.ClientSession.KEY_RESIZE_TO_CLIENT = 'resizeToClient';
349 remoting.ClientSession.KEY_SHRINK_TO_FIT = 'shrinkToFit';
352 * Set of capabilities for which hasCapability_() can be used to test.
356 remoting.ClientSession.Capability = {
357 // When enabled this capability causes the client to send its screen
358 // resolution to the host once connection has been established. See
359 // this.plugin_.notifyClientResolution().
360 SEND_INITIAL_RESOLUTION: 'sendInitialResolution',
361 RATE_LIMIT_RESIZE_REQUESTS: 'rateLimitResizeRequests',
362 VIDEO_RECORDER: 'videoRecorder',
367 * The set of capabilities negotiated between the client and host.
368 * @type {Array.<string>}
371 remoting.ClientSession.prototype.capabilities_ = null;
374 * @param {remoting.ClientSession.Capability} capability The capability to test
376 * @return {boolean} True if the capability has been negotiated between
377 * the client and host.
380 remoting.ClientSession.prototype.hasCapability_ = function(capability) {
381 if (this.capabilities_ == null)
384 return this.capabilities_.indexOf(capability) > -1;
388 * Callback function called when the plugin element gets focus.
390 remoting.ClientSession.prototype.pluginGotFocus_ = function() {
391 remoting.clipboard.initiateToHost();
395 * Callback function called when the plugin element loses focus.
397 remoting.ClientSession.prototype.pluginLostFocus_ = function() {
399 // Release all keys to prevent them becoming 'stuck down' on the host.
400 this.plugin_.releaseAllKeys();
401 if (this.plugin_.element()) {
402 // Focus should stay on the element, not (for example) the toolbar.
403 // Due to crbug.com/246335, we can't restore the focus immediately,
404 // otherwise the plugin gets confused about whether or not it has focus.
406 this.plugin_.element().focus.bind(this.plugin_.element()), 0);
412 * Adds <embed> element to |container| and readies the sesion object.
414 * @param {function(string, string):boolean} onExtensionMessage The handler for
415 * protocol extension messages. Returns true if a message is recognized;
418 remoting.ClientSession.prototype.createPluginAndConnect =
419 function(onExtensionMessage) {
420 this.plugin_ = remoting.ClientPlugin.factory.createPlugin(
421 this.container_.querySelector('.client-plugin-container'),
423 remoting.HostSettings.load(this.hostId_,
424 this.onHostSettingsLoaded_.bind(this));
428 * @param {Object.<string>} options The current options for the host, or {}
429 * if this client has no saved settings for the host.
432 remoting.ClientSession.prototype.onHostSettingsLoaded_ = function(options) {
433 if (remoting.ClientSession.KEY_REMAP_KEYS in options &&
434 typeof(options[remoting.ClientSession.KEY_REMAP_KEYS]) ==
436 this.remapKeys_ = /** @type {string} */
437 options[remoting.ClientSession.KEY_REMAP_KEYS];
439 if (remoting.ClientSession.KEY_RESIZE_TO_CLIENT in options &&
440 typeof(options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT]) ==
442 this.resizeToClient_ = /** @type {boolean} */
443 options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT];
445 if (remoting.ClientSession.KEY_SHRINK_TO_FIT in options &&
446 typeof(options[remoting.ClientSession.KEY_SHRINK_TO_FIT]) ==
448 this.shrinkToFit_ = /** @type {boolean} */
449 options[remoting.ClientSession.KEY_SHRINK_TO_FIT];
452 /** @param {boolean} result */
453 this.plugin_.initialize(this.onPluginInitialized_.bind(this));
457 * Constrains the focus to the plugin element.
460 remoting.ClientSession.prototype.setFocusHandlers_ = function() {
461 this.plugin_.element().addEventListener(
462 'focus', this.callPluginGotFocus_, false);
463 this.plugin_.element().addEventListener(
464 'blur', this.callPluginLostFocus_, false);
465 this.plugin_.element().focus();
469 * @param {remoting.Error} error
471 remoting.ClientSession.prototype.resetWithError_ = function(error) {
472 this.signalStrategy_.setIncomingStanzaCallback(null);
473 this.plugin_.dispose();
476 this.setState_(remoting.ClientSession.State.FAILED);
480 * @param {boolean} initialized
482 remoting.ClientSession.prototype.onPluginInitialized_ = function(initialized) {
484 console.error('ERROR: remoting plugin not loaded');
485 this.resetWithError_(remoting.Error.MISSING_PLUGIN);
489 if (!this.plugin_.isSupportedVersion()) {
490 this.resetWithError_(remoting.Error.BAD_PLUGIN_VERSION);
494 // Show the Send Keys menu only if the plugin has the injectKeyEvent feature,
495 // and the Ctrl-Alt-Del button only in Me2Me mode.
496 if (!this.plugin_.hasFeature(
497 remoting.ClientPlugin.Feature.INJECT_KEY_EVENT)) {
498 var sendKeysElement = document.getElementById('send-keys-menu');
499 sendKeysElement.hidden = true;
500 } else if (this.mode_ != remoting.ClientSession.Mode.ME2ME) {
501 var sendCadElement = document.getElementById('send-ctrl-alt-del');
502 sendCadElement.hidden = true;
505 // Apply customized key remappings if the plugin supports remapKeys.
506 if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.REMAP_KEY)) {
507 this.applyRemapKeys_(true);
510 // Enable MediaSource-based rendering on Chrome 37 and above.
511 var chromeVersionMajor =
512 parseInt((remoting.getChromeVersion() || '0').split('.')[0], 10);
513 if (chromeVersionMajor >= 37 &&
514 this.plugin_.hasFeature(
515 remoting.ClientPlugin.Feature.MEDIA_SOURCE_RENDERING)) {
516 this.video_ = /** @type {HTMLMediaElement} */(
517 this.container_.querySelector('video'));
518 // Make sure that the <video> element is hidden until we get the first
520 this.video_.style.width = '0px';
521 this.video_.style.height = '0px';
523 var renderer = new remoting.MediaSourceRenderer(this.video_);
524 this.plugin_.enableMediaSourceRendering(renderer);
525 this.container_.classList.add('mediasource-rendering');
527 this.container_.classList.remove('mediasource-rendering');
530 this.plugin_.setOnOutgoingIqHandler(this.sendIq_.bind(this));
531 this.plugin_.setOnDebugMessageHandler(
532 /** @param {string} msg */
534 console.log('plugin: ' + msg.trimRight());
537 this.plugin_.setConnectionStatusUpdateHandler(
538 this.onConnectionStatusUpdate_.bind(this));
539 this.plugin_.setConnectionReadyHandler(this.onConnectionReady_.bind(this));
540 this.plugin_.setDesktopSizeUpdateHandler(
541 this.onDesktopSizeChanged_.bind(this));
542 this.plugin_.setCapabilitiesHandler(this.onSetCapabilities_.bind(this));
543 this.plugin_.setGnubbyAuthHandler(
544 this.processGnubbyAuthMessage_.bind(this));
545 this.plugin_.setMouseCursorHandler(this.updateMouseCursorImage_.bind(this));
546 this.plugin_.setCastExtensionHandler(
547 this.processCastExtensionMessage_.bind(this));
548 this.initiateConnection_();
552 * Deletes the <embed> element from the container, without sending a
553 * session_terminate request. This is to be called when the session was
554 * disconnected by the Host.
556 * @return {void} Nothing.
558 remoting.ClientSession.prototype.removePlugin = function() {
560 this.plugin_.element().removeEventListener(
561 'focus', this.callPluginGotFocus_, false);
562 this.plugin_.element().removeEventListener(
563 'blur', this.callPluginLostFocus_, false);
564 this.plugin_.dispose();
568 // Leave full-screen mode, and stop listening for related events.
569 var listener = this.callOnFullScreenChanged_;
570 remoting.fullscreen.activate(
573 remoting.fullscreen.removeListener(listener);
575 if (remoting.windowFrame) {
576 remoting.windowFrame.setClientSession(null);
578 remoting.toolbar.setClientSession(null);
580 remoting.optionsMenu.setClientSession(null);
581 document.body.classList.remove('connected');
583 // Remove mediasource-rendering class from the container - this will also
584 // hide the <video> element.
585 this.container_.classList.remove('mediasource-rendering');
587 this.container_.removeEventListener('mousemove',
588 this.updateMouseCursorPosition_,
593 * Disconnect the current session with a particular |error|. The session will
594 * raise a |stateChanged| event in response to it. The caller should then call
595 * |cleanup| to remove and destroy the <embed> element.
597 * @param {remoting.Error} error The reason for the disconnection. Use
598 * remoting.Error.NONE if there is no error.
599 * @return {void} Nothing.
601 remoting.ClientSession.prototype.disconnect = function(error) {
602 var state = (error == remoting.Error.NONE) ?
603 remoting.ClientSession.State.CLOSED :
604 remoting.ClientSession.State.FAILED;
606 // The plugin won't send a state change notification, so we explicitly log
607 // the fact that the connection has closed.
608 this.logToServer.logClientSessionStateChange(state, error, this.mode_);
610 this.setState_(state);
614 * Deletes the <embed> element from the container and disconnects.
616 * @return {void} Nothing.
618 remoting.ClientSession.prototype.cleanup = function() {
621 'to="' + this.hostJid_ + '" ' +
623 'id="session-terminate" ' +
624 'xmlns:cli="jabber:client">' +
626 'xmlns="urn:xmpp:jingle:1" ' +
627 'action="session-terminate" ' +
628 'sid="' + this.sessionId_ + '">' +
629 '<reason><success/></reason>' +
636 * @return {remoting.ClientSession.Mode} The current state.
638 remoting.ClientSession.prototype.getMode = function() {
643 * @return {remoting.ClientSession.State} The current state.
645 remoting.ClientSession.prototype.getState = function() {
650 * @return {remoting.Error} The current error code.
652 remoting.ClientSession.prototype.getError = function() {
657 * Sends a key combination to the remoting client, by sending down events for
658 * the given keys, followed by up events in reverse order.
661 * @param {[number]} keys Key codes to be sent.
662 * @return {void} Nothing.
664 remoting.ClientSession.prototype.sendKeyCombination_ = function(keys) {
665 for (var i = 0; i < keys.length; i++) {
666 this.plugin_.injectKeyEvent(keys[i], true);
668 for (var i = 0; i < keys.length; i++) {
669 this.plugin_.injectKeyEvent(keys[i], false);
674 * Sends a Ctrl-Alt-Del sequence to the remoting client.
676 * @return {void} Nothing.
678 remoting.ClientSession.prototype.sendCtrlAltDel = function() {
679 console.log('Sending Ctrl-Alt-Del.');
680 this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]);
684 * Sends a Print Screen keypress to the remoting client.
686 * @return {void} Nothing.
688 remoting.ClientSession.prototype.sendPrintScreen = function() {
689 console.log('Sending Print Screen.');
690 this.sendKeyCombination_([0x070046]);
694 * Sets and stores the key remapping setting for the current host.
696 * @param {string} remappings Comma separated list of key remappings.
698 remoting.ClientSession.prototype.setRemapKeys = function(remappings) {
699 // Cancel any existing remappings and apply the new ones.
700 this.applyRemapKeys_(false);
701 this.remapKeys_ = remappings;
702 this.applyRemapKeys_(true);
704 // Save the new remapping setting.
706 options[remoting.ClientSession.KEY_REMAP_KEYS] = this.remapKeys_;
707 remoting.HostSettings.save(this.hostId_, options);
711 * Applies the configured key remappings to the session, or resets them.
713 * @param {boolean} apply True to apply remappings, false to cancel them.
715 remoting.ClientSession.prototype.applyRemapKeys_ = function(apply) {
716 // By default, under ChromeOS, remap the right Control key to the right
718 var remapKeys = this.remapKeys_;
719 if (remapKeys == '' && remoting.runningOnChromeOS()) {
720 remapKeys = '0x0700e4>0x0700e7';
723 if (remapKeys == '') {
727 var remappings = remapKeys.split(',');
728 for (var i = 0; i < remappings.length; ++i) {
729 var keyCodes = remappings[i].split('>');
730 if (keyCodes.length != 2) {
731 console.log('bad remapKey: ' + remappings[i]);
734 var fromKey = parseInt(keyCodes[0], 0);
735 var toKey = parseInt(keyCodes[1], 0);
736 if (!fromKey || !toKey) {
737 console.log('bad remapKey code: ' + remappings[i]);
741 console.log('remapKey 0x' + fromKey.toString(16) +
742 '>0x' + toKey.toString(16));
743 this.plugin_.remapKey(fromKey, toKey);
745 console.log('cancel remapKey 0x' + fromKey.toString(16));
746 this.plugin_.remapKey(fromKey, fromKey);
752 * Set the shrink-to-fit and resize-to-client flags and save them if this is
753 * a Me2Me connection.
755 * @param {boolean} shrinkToFit True if the remote desktop should be scaled
756 * down if it is larger than the client window; false if scroll-bars
757 * should be added in this case.
758 * @param {boolean} resizeToClient True if window resizes should cause the
759 * host to attempt to resize its desktop to match the client window size;
760 * false to disable this behaviour for subsequent window resizes--the
761 * current host desktop size is not restored in this case.
762 * @return {void} Nothing.
764 remoting.ClientSession.prototype.setScreenMode =
765 function(shrinkToFit, resizeToClient) {
766 if (resizeToClient && !this.resizeToClient_) {
767 var clientArea = this.getClientArea_();
768 this.plugin_.notifyClientResolution(clientArea.width,
770 window.devicePixelRatio);
773 // If enabling shrink, reset bump-scroll offsets.
774 var needsScrollReset = shrinkToFit && !this.shrinkToFit_;
776 this.shrinkToFit_ = shrinkToFit;
777 this.resizeToClient_ = resizeToClient;
778 this.updateScrollbarVisibility();
780 if (this.hostId_ != '') {
782 options[remoting.ClientSession.KEY_SHRINK_TO_FIT] = this.shrinkToFit_;
783 options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT] = this.resizeToClient_;
784 remoting.HostSettings.save(this.hostId_, options);
787 this.updateDimensions();
788 if (needsScrollReset) {
795 * Called when the client receives its first frame.
797 * @return {void} Nothing.
799 remoting.ClientSession.prototype.onFirstFrameReceived = function() {
800 this.hasReceivedFrame_ = true;
804 * @return {boolean} Whether the client has received a video buffer.
806 remoting.ClientSession.prototype.hasReceivedFrame = function() {
807 return this.hasReceivedFrame_;
811 * Sends a signaling message.
814 * @param {string} message XML string of IQ stanza to send to server.
815 * @return {void} Nothing.
817 remoting.ClientSession.prototype.sendIq_ = function(message) {
818 // Extract the session id, so we can close the session later.
819 var parser = new DOMParser();
820 var iqNode = parser.parseFromString(message, 'text/xml').firstChild;
821 var jingleNode = iqNode.firstChild;
823 var action = jingleNode.getAttribute('action');
824 if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') {
825 this.sessionId_ = jingleNode.getAttribute('sid');
829 console.log(remoting.timestamp(), remoting.formatIq.prettifySendIq(message));
830 if (this.signalStrategy_.getState() !=
831 remoting.SignalStrategy.State.CONNECTED) {
832 console.log("Message above is dropped because signaling is not connected.");
836 this.signalStrategy_.sendMessage(message);
841 * @param {Element} message
843 remoting.ClientSession.prototype.onIncomingMessage_ = function(message) {
847 var formatted = new XMLSerializer().serializeToString(message);
848 console.log(remoting.timestamp(),
849 remoting.formatIq.prettifyReceiveIq(formatted));
850 this.plugin_.onIncomingIq(formatted);
856 remoting.ClientSession.prototype.initiateConnection_ = function() {
857 /** @type {remoting.ClientSession} */
860 /** @param {string} sharedSecret Shared secret. */
861 function onSharedSecretReceived(sharedSecret) {
862 that.plugin_.connect(
863 that.hostJid_, that.hostPublicKey_, that.signalStrategy_.getJid(),
864 sharedSecret, that.authenticationMethods_, that.hostId_,
865 that.clientPairingId_, that.clientPairedSecret_);
868 this.getSharedSecret_(onSharedSecretReceived);
872 * Gets shared secret to be used for connection.
874 * @param {function(string)} callback Callback called with the shared secret.
875 * @return {void} Nothing.
878 remoting.ClientSession.prototype.getSharedSecret_ = function(callback) {
879 /** @type remoting.ClientSession */
881 if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.THIRD_PARTY_AUTH)) {
882 /** @type{function(string, string, string): void} */
883 var fetchThirdPartyToken = function(tokenUrl, hostPublicKey, scope) {
884 that.fetchThirdPartyToken_(
885 tokenUrl, hostPublicKey, scope,
886 that.plugin_.onThirdPartyTokenFetched.bind(that.plugin_));
888 this.plugin_.setFetchThirdPartyTokenHandler(fetchThirdPartyToken);
890 if (this.accessCode_) {
891 // Shared secret was already supplied before connecting (It2Me case).
892 callback(this.accessCode_);
893 } else if (this.plugin_.hasFeature(
894 remoting.ClientPlugin.Feature.ASYNC_PIN)) {
895 // Plugin supports asynchronously asking for the PIN.
896 this.plugin_.useAsyncPinDialog();
897 /** @param {boolean} pairingSupported */
898 var fetchPin = function(pairingSupported) {
899 that.fetchPin_(pairingSupported,
900 that.plugin_.onPinFetched.bind(that.plugin_));
902 this.plugin_.setFetchPinHandler(fetchPin);
905 // Clients that don't support asking for a PIN asynchronously also don't
906 // support pairing, so request the PIN now without offering to remember it.
907 this.fetchPin_(false, callback);
912 * Callback that the plugin invokes to indicate that the connection
913 * status has changed.
916 * @param {number} status The plugin's status.
917 * @param {number} error The plugin's error state, if any.
919 remoting.ClientSession.prototype.onConnectionStatusUpdate_ =
920 function(status, error) {
921 if (status == remoting.ClientSession.State.CONNECTED) {
922 this.setFocusHandlers_();
923 this.onDesktopSizeChanged_();
924 if (this.resizeToClient_) {
925 var clientArea = this.getClientArea_();
926 this.plugin_.notifyClientResolution(clientArea.width,
928 window.devicePixelRatio);
930 // Activate full-screen related UX.
931 remoting.fullscreen.addListener(this.callOnFullScreenChanged_);
932 if (remoting.windowFrame) {
933 remoting.windowFrame.setClientSession(this);
935 remoting.toolbar.setClientSession(this);
937 remoting.optionsMenu.setClientSession(this);
938 document.body.classList.add('connected');
940 this.container_.addEventListener('mousemove',
941 this.updateMouseCursorPosition_,
944 } else if (status == remoting.ClientSession.State.FAILED) {
946 case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE:
947 this.error_ = remoting.Error.HOST_IS_OFFLINE;
949 case remoting.ClientSession.ConnectionError.SESSION_REJECTED:
950 this.error_ = remoting.Error.INVALID_ACCESS_CODE;
952 case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL:
953 this.error_ = remoting.Error.INCOMPATIBLE_PROTOCOL;
955 case remoting.ClientSession.ConnectionError.NETWORK_FAILURE:
956 this.error_ = remoting.Error.P2P_FAILURE;
958 case remoting.ClientSession.ConnectionError.HOST_OVERLOAD:
959 this.error_ = remoting.Error.HOST_OVERLOAD;
962 this.error_ = remoting.Error.UNEXPECTED;
965 this.setState_(/** @type {remoting.ClientSession.State} */ (status));
969 * Callback that the plugin invokes to indicate when the connection is
973 * @param {boolean} ready True if the connection is ready.
975 remoting.ClientSession.prototype.onConnectionReady_ = function(ready) {
977 this.container_.classList.add('session-client-inactive');
979 this.container_.classList.remove('session-client-inactive');
982 this.raiseEvent(remoting.ClientSession.Events.videoChannelStateChanged,
987 * Called when the client-host capabilities negotiation is complete.
989 * @param {!Array.<string>} capabilities The set of capabilities negotiated
990 * between the client and host.
991 * @return {void} Nothing.
994 remoting.ClientSession.prototype.onSetCapabilities_ = function(capabilities) {
995 if (this.capabilities_ != null) {
996 console.error('onSetCapabilities_() is called more than once');
1000 this.capabilities_ = capabilities;
1001 if (this.hasCapability_(
1002 remoting.ClientSession.Capability.SEND_INITIAL_RESOLUTION)) {
1003 var clientArea = this.getClientArea_();
1004 this.plugin_.notifyClientResolution(clientArea.width,
1006 window.devicePixelRatio);
1008 if (this.hasCapability_(
1009 remoting.ClientSession.Capability.VIDEO_RECORDER)) {
1010 this.videoFrameRecorder_ = new remoting.VideoFrameRecorder(this.plugin_);
1016 * @param {remoting.ClientSession.State} newState The new state for the session.
1017 * @return {void} Nothing.
1019 remoting.ClientSession.prototype.setState_ = function(newState) {
1020 var oldState = this.state_;
1021 this.state_ = newState;
1022 var state = this.state_;
1023 if (oldState == remoting.ClientSession.State.CONNECTING) {
1024 if (this.state_ == remoting.ClientSession.State.CLOSED) {
1025 state = remoting.ClientSession.State.CONNECTION_CANCELED;
1026 } else if (this.state_ == remoting.ClientSession.State.FAILED &&
1027 this.error_ == remoting.Error.HOST_IS_OFFLINE &&
1028 !this.logHostOfflineErrors_) {
1029 // The application requested host-offline errors to be suppressed, for
1030 // example, because this connection attempt is using a cached host JID.
1031 console.log('Suppressing host-offline error.');
1032 state = remoting.ClientSession.State.CONNECTION_CANCELED;
1034 } else if (oldState == remoting.ClientSession.State.CONNECTED &&
1035 this.state_ == remoting.ClientSession.State.FAILED) {
1036 state = remoting.ClientSession.State.CONNECTION_DROPPED;
1038 this.logToServer.logClientSessionStateChange(state, this.error_, this.mode_);
1039 if (this.state_ == remoting.ClientSession.State.CONNECTED) {
1040 this.createGnubbyAuthHandler_();
1041 this.createCastExtensionHandler_();
1044 this.raiseEvent(remoting.ClientSession.Events.stateChanged,
1045 new remoting.ClientSession.StateEvent(newState, oldState)
1050 * This is a callback that gets called when the window is resized.
1052 * @return {void} Nothing.
1054 remoting.ClientSession.prototype.onResize = function() {
1055 this.updateDimensions();
1057 if (this.notifyClientResolutionTimer_) {
1058 window.clearTimeout(this.notifyClientResolutionTimer_);
1059 this.notifyClientResolutionTimer_ = null;
1062 // Defer notifying the host of the change until the window stops resizing, to
1063 // avoid overloading the control channel with notifications.
1064 if (this.resizeToClient_) {
1065 var kResizeRateLimitMs = 1000;
1066 if (this.hasCapability_(
1067 remoting.ClientSession.Capability.RATE_LIMIT_RESIZE_REQUESTS)) {
1068 kResizeRateLimitMs = 250;
1070 var clientArea = this.getClientArea_();
1071 this.notifyClientResolutionTimer_ = window.setTimeout(
1072 this.plugin_.notifyClientResolution.bind(this.plugin_,
1075 window.devicePixelRatio),
1076 kResizeRateLimitMs);
1079 // If bump-scrolling is enabled, adjust the plugin margins to fully utilize
1080 // the new window area.
1081 this.resetScroll_();
1083 this.updateScrollbarVisibility();
1087 * Requests that the host pause or resume video updates.
1089 * @param {boolean} pause True to pause video, false to resume.
1090 * @return {void} Nothing.
1092 remoting.ClientSession.prototype.pauseVideo = function(pause) {
1094 this.plugin_.pauseVideo(pause);
1099 * Requests that the host pause or resume audio.
1101 * @param {boolean} pause True to pause audio, false to resume.
1102 * @return {void} Nothing.
1104 remoting.ClientSession.prototype.pauseAudio = function(pause) {
1106 this.plugin_.pauseAudio(pause)
1111 * This is a callback that gets called when the plugin notifies us of a change
1112 * in the size of the remote desktop.
1115 * @return {void} Nothing.
1117 remoting.ClientSession.prototype.onDesktopSizeChanged_ = function() {
1118 console.log('desktop size changed: ' +
1119 this.plugin_.getDesktopWidth() + 'x' +
1120 this.plugin_.getDesktopHeight() +' @ ' +
1121 this.plugin_.getDesktopXDpi() + 'x' +
1122 this.plugin_.getDesktopYDpi() + ' DPI');
1123 this.updateDimensions();
1124 this.updateScrollbarVisibility();
1128 * Refreshes the plugin's dimensions, taking into account the sizes of the
1129 * remote desktop and client window, and the current scale-to-fit setting.
1131 * @return {void} Nothing.
1133 remoting.ClientSession.prototype.updateDimensions = function() {
1134 if (this.plugin_.getDesktopWidth() == 0 ||
1135 this.plugin_.getDesktopHeight() == 0) {
1139 var clientArea = this.getClientArea_();
1140 var desktopWidth = this.plugin_.getDesktopWidth();
1141 var desktopHeight = this.plugin_.getDesktopHeight();
1143 // When configured to display a host at its original size, we aim to display
1144 // it as close to its physical size as possible, without losing data:
1145 // - If client and host have matching DPI, render the host pixel-for-pixel.
1146 // - If the host has higher DPI then still render pixel-for-pixel.
1147 // - If the host has lower DPI then let Chrome up-scale it to natural size.
1149 // We specify the plugin dimensions in Density-Independent Pixels, so to
1150 // render pixel-for-pixel we need to down-scale the host dimensions by the
1151 // devicePixelRatio of the client. To match the host pixel density, we choose
1152 // an initial scale factor based on the client devicePixelRatio and host DPI.
1154 // Determine the effective device pixel ratio of the host, based on DPI.
1155 var hostPixelRatioX = Math.ceil(this.plugin_.getDesktopXDpi() / 96);
1156 var hostPixelRatioY = Math.ceil(this.plugin_.getDesktopYDpi() / 96);
1157 var hostPixelRatio = Math.min(hostPixelRatioX, hostPixelRatioY);
1159 // Down-scale by the smaller of the client and host ratios.
1160 var scale = 1.0 / Math.min(window.devicePixelRatio, hostPixelRatio);
1162 if (this.shrinkToFit_) {
1163 // Reduce the scale, if necessary, to fit the whole desktop in the window.
1164 var scaleFitWidth = Math.min(scale, 1.0 * clientArea.width / desktopWidth);
1165 var scaleFitHeight =
1166 Math.min(scale, 1.0 * clientArea.height / desktopHeight);
1167 scale = Math.min(scaleFitHeight, scaleFitWidth);
1169 // If we're running full-screen then try to handle common side-by-side
1170 // multi-monitor combinations more intelligently.
1171 if (remoting.fullscreen.isActive()) {
1172 // If the host has two monitors each the same size as the client then
1173 // scale-to-fit will have the desktop occupy only 50% of the client area,
1174 // in which case it would be preferable to down-scale less and let the
1175 // user bump-scroll around ("scale-and-pan").
1176 // Triggering scale-and-pan if less than 65% of the client area would be
1177 // used adds enough fuzz to cope with e.g. 1280x800 client connecting to
1178 // a (2x1280)x1024 host nicely.
1179 // Note that we don't need to account for scrollbars while fullscreen.
1180 if (scale <= scaleFitHeight * 0.65) {
1181 scale = scaleFitHeight;
1183 if (scale <= scaleFitWidth * 0.65) {
1184 scale = scaleFitWidth;
1189 var pluginWidth = Math.round(desktopWidth * scale);
1190 var pluginHeight = Math.round(desktopHeight * scale);
1193 this.video_.style.width = pluginWidth + 'px';
1194 this.video_.style.height = pluginHeight + 'px';
1197 // Resize the plugin if necessary.
1198 // TODO(wez): Handle high-DPI to high-DPI properly (crbug.com/135089).
1199 this.plugin_.element().style.width = pluginWidth + 'px';
1200 this.plugin_.element().style.height = pluginHeight + 'px';
1202 // Position the container.
1203 // Note that clientWidth/Height take into account scrollbars.
1204 var clientWidth = document.documentElement.clientWidth;
1205 var clientHeight = document.documentElement.clientHeight;
1206 var parentNode = this.plugin_.element().parentNode;
1208 console.log('plugin dimensions: ' +
1209 parentNode.style.left + ',' +
1210 parentNode.style.top + '-' +
1211 pluginWidth + 'x' + pluginHeight + '.');
1215 * Returns an associative array with a set of stats for this connection.
1217 * @return {remoting.ClientSession.PerfStats} The connection statistics.
1219 remoting.ClientSession.prototype.getPerfStats = function() {
1220 return this.plugin_.getPerfStats();
1226 * @param {remoting.ClientSession.PerfStats} stats
1228 remoting.ClientSession.prototype.logStatistics = function(stats) {
1229 this.logToServer.logStatistics(stats, this.mode_);
1233 * Enable or disable logging of connection errors due to a host being offline.
1234 * For example, if attempting a connection using a cached JID, host-offline
1235 * errors should not be logged because the JID will be refreshed and the
1236 * connection retried.
1238 * @param {boolean} enable True to log host-offline errors; false to suppress.
1240 remoting.ClientSession.prototype.logHostOfflineErrors = function(enable) {
1241 this.logHostOfflineErrors_ = enable;
1245 * Request pairing with the host for PIN-less authentication.
1247 * @param {string} clientName The human-readable name of the client.
1248 * @param {function(string, string):void} onDone Callback to receive the
1249 * client id and shared secret when they are available.
1251 remoting.ClientSession.prototype.requestPairing = function(clientName, onDone) {
1253 this.plugin_.requestPairing(clientName, onDone);
1258 * Called when the full-screen status has changed, either via the
1259 * remoting.Fullscreen class, or via a system event such as the Escape key
1261 * @param {boolean} fullscreen True if the app is entering full-screen mode;
1262 * false if it is leaving it.
1265 remoting.ClientSession.prototype.onFullScreenChanged_ = function (fullscreen) {
1266 var htmlNode = /** @type {HTMLElement} */ (document.documentElement);
1267 this.enableBumpScroll_(fullscreen);
1269 htmlNode.classList.add('full-screen');
1271 htmlNode.classList.remove('full-screen');
1276 * Scroll the client plugin by the specified amount, keeping it visible.
1277 * Note that this is only used in content full-screen mode (not windowed or
1278 * browser full-screen modes), where window.scrollBy and the scrollTop and
1279 * scrollLeft properties don't work.
1280 * @param {number} dx The amount by which to scroll horizontally. Positive to
1281 * scroll right; negative to scroll left.
1282 * @param {number} dy The amount by which to scroll vertically. Positive to
1283 * scroll down; negative to scroll up.
1284 * @return {boolean} True if the requested scroll had no effect because both
1285 * vertical and horizontal edges of the screen have been reached.
1288 remoting.ClientSession.prototype.scroll_ = function(dx, dy) {
1290 * Helper function for x- and y-scrolling
1291 * @param {number|string} curr The current margin, eg. "10px".
1292 * @param {number} delta The requested scroll amount.
1293 * @param {number} windowBound The size of the window, in pixels.
1294 * @param {number} pluginBound The size of the plugin, in pixels.
1295 * @param {{stop: boolean}} stop Reference parameter used to indicate when
1296 * the scroll has reached one of the edges and can be stopped in that
1298 * @return {string} The new margin value.
1300 var adjustMargin = function(curr, delta, windowBound, pluginBound, stop) {
1301 var minMargin = Math.min(0, windowBound - pluginBound);
1302 var result = (curr ? parseFloat(curr) : 0) - delta;
1303 result = Math.min(0, Math.max(minMargin, result));
1304 stop.stop = (result == 0 || result == minMargin);
1305 return result + 'px';
1308 var plugin = this.plugin_.element();
1309 var style = this.container_.style;
1311 var stopX = { stop: false };
1312 var clientArea = this.getClientArea_();
1313 style.marginLeft = adjustMargin(style.marginLeft, dx, clientArea.width,
1314 this.pluginWidthForBumpScrollTesting || plugin.clientWidth, stopX);
1316 var stopY = { stop: false };
1317 style.marginTop = adjustMargin(
1318 style.marginTop, dy, clientArea.height,
1319 this.pluginHeightForBumpScrollTesting || plugin.clientHeight, stopY);
1320 return stopX.stop && stopY.stop;
1323 remoting.ClientSession.prototype.resetScroll_ = function() {
1324 this.container_.style.marginTop = '0px';
1325 this.container_.style.marginLeft = '0px';
1329 * Enable or disable bump-scrolling. When disabling bump scrolling, also reset
1330 * the scroll offsets to (0, 0).
1332 * @param {boolean} enable True to enable bump-scrolling, false to disable it.
1334 remoting.ClientSession.prototype.enableBumpScroll_ = function(enable) {
1335 var element = /*@type{HTMLElement} */ document.documentElement;
1337 /** @type {null|function(Event):void} */
1338 this.onMouseMoveRef_ = this.onMouseMove_.bind(this);
1339 element.addEventListener('mousemove', this.onMouseMoveRef_, false);
1341 element.removeEventListener('mousemove', this.onMouseMoveRef_, false);
1342 this.onMouseMoveRef_ = null;
1343 this.resetScroll_();
1348 * @param {Event} event The mouse event.
1351 remoting.ClientSession.prototype.onMouseMove_ = function(event) {
1352 if (this.bumpScrollTimer_) {
1353 window.clearTimeout(this.bumpScrollTimer_);
1354 this.bumpScrollTimer_ = null;
1358 * Compute the scroll speed based on how close the mouse is to the edge.
1359 * @param {number} mousePos The mouse x- or y-coordinate
1360 * @param {number} size The width or height of the content area.
1361 * @return {number} The scroll delta, in pixels.
1363 var computeDelta = function(mousePos, size) {
1365 if (mousePos >= size - threshold) {
1366 return 1 + 5 * (mousePos - (size - threshold)) / threshold;
1367 } else if (mousePos <= threshold) {
1368 return -1 - 5 * (threshold - mousePos) / threshold;
1373 var clientArea = this.getClientArea_();
1374 var dx = computeDelta(event.x, clientArea.width);
1375 var dy = computeDelta(event.y, clientArea.height);
1377 if (dx != 0 || dy != 0) {
1378 this.raiseEvent(remoting.ClientSession.Events.bumpScrollStarted);
1379 /** @type {remoting.ClientSession} */
1382 * Scroll the view, and schedule a timer to do so again unless we've hit
1383 * the edges of the screen. This timer is cancelled when the mouse moves.
1384 * @param {number} expected The time at which we expect to be called.
1386 var repeatScroll = function(expected) {
1387 /** @type {number} */
1388 var now = new Date().getTime();
1389 /** @type {number} */
1391 var lateAdjustment = 1 + (now - expected) / timeout;
1392 if (that.scroll_(lateAdjustment * dx, lateAdjustment * dy)) {
1393 that.raiseEvent(remoting.ClientSession.Events.bumpScrollStopped);
1395 that.bumpScrollTimer_ = window.setTimeout(
1396 function() { repeatScroll(now + timeout); },
1400 repeatScroll(new Date().getTime());
1405 * Sends a clipboard item to the host.
1407 * @param {string} mimeType The MIME type of the clipboard item.
1408 * @param {string} item The clipboard item.
1410 remoting.ClientSession.prototype.sendClipboardItem = function(mimeType, item) {
1413 this.plugin_.sendClipboardItem(mimeType, item);
1417 * Send a gnubby-auth extension message to the host.
1418 * @param {Object} data The gnubby-auth message data.
1420 remoting.ClientSession.prototype.sendGnubbyAuthMessage = function(data) {
1423 this.plugin_.sendClientMessage('gnubby-auth', JSON.stringify(data));
1427 * Process a remote gnubby auth request.
1428 * @param {string} data Remote gnubby request data.
1431 remoting.ClientSession.prototype.processGnubbyAuthMessage_ = function(data) {
1432 if (this.gnubbyAuthHandler_) {
1434 this.gnubbyAuthHandler_.onMessage(data);
1436 console.error('Failed to process gnubby message: ',
1437 /** @type {*} */ (err));
1440 console.error('Received unexpected gnubby message');
1445 * Create a gnubby auth handler and inform the host that gnubby auth is
1449 remoting.ClientSession.prototype.createGnubbyAuthHandler_ = function() {
1450 if (this.mode_ == remoting.ClientSession.Mode.ME2ME) {
1451 this.gnubbyAuthHandler_ = new remoting.GnubbyAuthHandler(this);
1452 // TODO(psj): Move to more generic capabilities mechanism.
1453 this.sendGnubbyAuthMessage({'type': 'control', 'option': 'auth-v1'});
1458 * @return {{width: number, height: number}} The height of the window's client
1459 * area. This differs between apps v1 and apps v2 due to the custom window
1460 * borders used by the latter.
1463 remoting.ClientSession.prototype.getClientArea_ = function() {
1464 return remoting.windowFrame ?
1465 remoting.windowFrame.getClientArea() :
1466 { 'width': window.innerWidth, 'height': window.innerHeight };
1470 * @param {string} url
1471 * @param {number} hotspotX
1472 * @param {number} hotspotY
1474 remoting.ClientSession.prototype.updateMouseCursorImage_ =
1475 function(url, hotspotX, hotspotY) {
1476 this.mouseCursorOverlay_.hidden = !url;
1478 this.mouseCursorOverlay_.style.marginLeft = '-' + hotspotX + 'px';
1479 this.mouseCursorOverlay_.style.marginTop = '-' + hotspotY + 'px';
1480 this.mouseCursorOverlay_.src = url;
1485 * @return {{top: number, left:number}} The top-left corner of the plugin.
1487 remoting.ClientSession.prototype.getPluginPositionForTesting = function() {
1488 var style = this.container_.style;
1490 top: parseFloat(style.marginTop),
1491 left: parseFloat(style.marginLeft)
1496 * Send a Cast extension message to the host.
1497 * @param {Object} data The cast message data.
1499 remoting.ClientSession.prototype.sendCastExtensionMessage = function(data) {
1502 this.plugin_.sendClientMessage('cast_message', JSON.stringify(data));
1506 * Process a remote Cast extension message from the host.
1507 * @param {string} data Remote cast extension data message.
1510 remoting.ClientSession.prototype.processCastExtensionMessage_ = function(data) {
1511 if (this.castExtensionHandler_) {
1513 this.castExtensionHandler_.onMessage(data);
1515 console.error('Failed to process cast message: ',
1516 /** @type {*} */ (err));
1519 console.error('Received unexpected cast message');
1524 * Create a CastExtensionHandler and inform the host that cast extension
1528 remoting.ClientSession.prototype.createCastExtensionHandler_ = function() {
1529 if (remoting.enableCast && this.mode_ == remoting.ClientSession.Mode.ME2ME) {
1530 this.castExtensionHandler_ = new remoting.CastExtensionHandler(this);
1535 * Returns true if the ClientSession can record video frames to a file.
1538 remoting.ClientSession.prototype.canRecordVideo = function() {
1539 return !!this.videoFrameRecorder_;
1543 * Returns true if the ClientSession is currently recording video frames.
1546 remoting.ClientSession.prototype.isRecordingVideo = function() {
1547 if (!this.videoFrameRecorder_) {
1550 return this.videoFrameRecorder_.isRecording();
1554 * Starts or stops recording of video frames.
1556 remoting.ClientSession.prototype.startStopRecording = function() {
1557 if (this.videoFrameRecorder_) {
1558 this.videoFrameRecorder_.startStopRecording();
1563 * Handles protocol extension messages.
1564 * @param {string} type Type of extension message.
1565 * @param {string} data Contents of the extension message.
1566 * @return {boolean} True if the message was recognized, false otherwise.
1568 remoting.ClientSession.prototype.handleExtensionMessage =
1569 function(type, data) {
1570 if (this.videoFrameRecorder_) {
1571 return this.videoFrameRecorder_.handleMessage(type, data);