Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / remoting / webapp / client_plugin.js
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.
4
5 /**
6  * @fileoverview
7  * Class that wraps low-level details of interacting with the client plugin.
8  *
9  * This abstracts a <embed> element and controls the plugin which does
10  * the actual remoting work. It also handles differences between
11  * client plugins versions when it is necessary.
12  */
13
14 'use strict';
15
16 /** @suppress {duplicate} */
17 var remoting = remoting || {};
18
19 /**
20  * @param {remoting.ViewerPlugin} plugin The plugin embed element.
21  * @param {function(string, string):boolean} onExtensionMessage The handler for
22  *     protocol extension messages. Returns true if a message is recognized;
23  *     false otherwise.
24  * @constructor
25  */
26 remoting.ClientPlugin = function(plugin, onExtensionMessage) {
27   this.plugin = plugin;
28   this.onExtensionMessage_ = onExtensionMessage;
29
30   this.desktopWidth = 0;
31   this.desktopHeight = 0;
32   this.desktopXDpi = 96;
33   this.desktopYDpi = 96;
34
35   /** @param {string} iq The Iq stanza received from the host. */
36   this.onOutgoingIqHandler = function (iq) {};
37   /** @param {string} message Log message. */
38   this.onDebugMessageHandler = function (message) {};
39   /**
40    * @param {number} state The connection state.
41    * @param {number} error The error code, if any.
42    */
43   this.onConnectionStatusUpdateHandler = function(state, error) {};
44   /** @param {boolean} ready Connection ready state. */
45   this.onConnectionReadyHandler = function(ready) {};
46
47   /**
48    * @param {string} tokenUrl Token-request URL, received from the host.
49    * @param {string} hostPublicKey Public key for the host.
50    * @param {string} scope OAuth scope to request the token for.
51    */
52   this.fetchThirdPartyTokenHandler = function(
53     tokenUrl, hostPublicKey, scope) {};
54   this.onDesktopSizeUpdateHandler = function () {};
55   /** @param {!Array.<string>} capabilities The negotiated capabilities. */
56   this.onSetCapabilitiesHandler = function (capabilities) {};
57   this.fetchPinHandler = function (supportsPairing) {};
58
59   /** @type {remoting.MediaSourceRenderer} */
60   this.mediaSourceRenderer_ = null;
61
62   /** @type {number} */
63   this.pluginApiVersion_ = -1;
64   /** @type {Array.<string>} */
65   this.pluginApiFeatures_ = [];
66   /** @type {number} */
67   this.pluginApiMinVersion_ = -1;
68   /** @type {!Array.<string>} */
69   this.capabilities_ = [];
70   /** @type {boolean} */
71   this.helloReceived_ = false;
72   /** @type {function(boolean)|null} */
73   this.onInitializedCallback_ = null;
74   /** @type {function(string, string):void} */
75   this.onPairingComplete_ = function(clientId, sharedSecret) {};
76   /** @type {remoting.ClientSession.PerfStats} */
77   this.perfStats_ = new remoting.ClientSession.PerfStats();
78
79   /** @type {remoting.ClientPlugin} */
80   var that = this;
81   /** @param {Event} event Message event from the plugin. */
82   this.plugin.addEventListener('message', function(event) {
83       that.handleMessage_(event.data);
84     }, false);
85   window.setTimeout(this.showPluginForClickToPlay_.bind(this), 500);
86 };
87
88 /**
89  * Set of features for which hasFeature() can be used to test.
90  *
91  * @enum {string}
92  */
93 remoting.ClientPlugin.Feature = {
94   INJECT_KEY_EVENT: 'injectKeyEvent',
95   NOTIFY_CLIENT_RESOLUTION: 'notifyClientResolution',
96   ASYNC_PIN: 'asyncPin',
97   PAUSE_VIDEO: 'pauseVideo',
98   PAUSE_AUDIO: 'pauseAudio',
99   REMAP_KEY: 'remapKey',
100   SEND_CLIPBOARD_ITEM: 'sendClipboardItem',
101   THIRD_PARTY_AUTH: 'thirdPartyAuth',
102   TRAP_KEY: 'trapKey',
103   PINLESS_AUTH: 'pinlessAuth',
104   EXTENSION_MESSAGE: 'extensionMessage',
105   MEDIA_SOURCE_RENDERING: 'mediaSourceRendering'
106 };
107
108 /**
109  * Chromoting session API version (for this javascript).
110  * This is compared with the plugin API version to verify that they are
111  * compatible.
112  *
113  * @const
114  * @private
115  */
116 remoting.ClientPlugin.prototype.API_VERSION_ = 6;
117
118 /**
119  * The oldest API version that we support.
120  * This will differ from the |API_VERSION_| if we maintain backward
121  * compatibility with older API versions.
122  *
123  * @const
124  * @private
125  */
126 remoting.ClientPlugin.prototype.API_MIN_VERSION_ = 5;
127
128 /**
129  * @param {string|{method:string, data:Object.<string,*>}}
130  *    rawMessage Message from the plugin.
131  * @private
132  */
133 remoting.ClientPlugin.prototype.handleMessage_ = function(rawMessage) {
134   var message =
135       /** @type {{method:string, data:Object.<string,*>}} */
136       ((typeof(rawMessage) == 'string') ? jsonParseSafe(rawMessage)
137                                         : rawMessage);
138
139   if (!message || !('method' in message) || !('data' in message)) {
140     console.error('Received invalid message from the plugin:', rawMessage);
141     return;
142   }
143
144   try {
145     this.handleMessageMethod_(message);
146   } catch(e) {
147     console.error(/** @type {*} */ (e));
148   }
149 }
150
151 /**
152  * @param {{method:string, data:Object.<string,*>}}
153  *    message Parsed message from the plugin.
154  * @private
155  */
156 remoting.ClientPlugin.prototype.handleMessageMethod_ = function(message) {
157   /**
158    * Splits a string into a list of words delimited by spaces.
159    * @param {string} str String that should be split.
160    * @return {!Array.<string>} List of words.
161    */
162   var tokenize = function(str) {
163     /** @type {Array.<string>} */
164     var tokens = str.match(/\S+/g);
165     return tokens ? tokens : [];
166   };
167
168   if (message.method == 'hello') {
169     // Reset the size in case we had to enlarge it to support click-to-play.
170     this.plugin.width = 0;
171     this.plugin.height = 0;
172     this.pluginApiVersion_ = getNumberAttr(message.data, 'apiVersion');
173     this.pluginApiMinVersion_ = getNumberAttr(message.data, 'apiMinVersion');
174
175     if (this.pluginApiVersion_ >= 7) {
176       this.pluginApiFeatures_ =
177           tokenize(getStringAttr(message.data, 'apiFeatures'));
178
179       // Negotiate capabilities.
180
181       /** @type {!Array.<string>} */
182       var requestedCapabilities = [];
183       if ('requestedCapabilities' in message.data) {
184         requestedCapabilities =
185             tokenize(getStringAttr(message.data, 'requestedCapabilities'));
186       }
187
188       /** @type {!Array.<string>} */
189       var supportedCapabilities = [];
190       if ('supportedCapabilities' in message.data) {
191         supportedCapabilities =
192             tokenize(getStringAttr(message.data, 'supportedCapabilities'));
193       }
194
195       // At the moment the webapp does not recognize any of
196       // 'requestedCapabilities' capabilities (so they all should be disabled)
197       // and do not care about any of 'supportedCapabilities' capabilities (so
198       // they all can be enabled).
199       this.capabilities_ = supportedCapabilities;
200
201       // Let the host know that the webapp can be requested to always send
202       // the client's dimensions.
203       this.capabilities_.push(
204           remoting.ClientSession.Capability.SEND_INITIAL_RESOLUTION);
205
206       // Let the host know that we're interested in knowing whether or not
207       // it rate-limits desktop-resize requests.
208       this.capabilities_.push(
209           remoting.ClientSession.Capability.RATE_LIMIT_RESIZE_REQUESTS);
210     } else if (this.pluginApiVersion_ >= 6) {
211       this.pluginApiFeatures_ = ['highQualityScaling', 'injectKeyEvent'];
212     } else {
213       this.pluginApiFeatures_ = ['highQualityScaling'];
214     }
215     this.helloReceived_ = true;
216     if (this.onInitializedCallback_ != null) {
217       this.onInitializedCallback_(true);
218       this.onInitializedCallback_ = null;
219     }
220
221   } else if (message.method == 'sendOutgoingIq') {
222     this.onOutgoingIqHandler(getStringAttr(message.data, 'iq'));
223
224   } else if (message.method == 'logDebugMessage') {
225     this.onDebugMessageHandler(getStringAttr(message.data, 'message'));
226
227   } else if (message.method == 'onConnectionStatus') {
228     var state = remoting.ClientSession.State.fromString(
229         getStringAttr(message.data, 'state'))
230     var error = remoting.ClientSession.ConnectionError.fromString(
231         getStringAttr(message.data, 'error'));
232     this.onConnectionStatusUpdateHandler(state, error);
233
234   } else if (message.method == 'onDesktopSize') {
235     this.desktopWidth = getNumberAttr(message.data, 'width');
236     this.desktopHeight = getNumberAttr(message.data, 'height');
237     this.desktopXDpi = getNumberAttr(message.data, 'x_dpi', 96);
238     this.desktopYDpi = getNumberAttr(message.data, 'y_dpi', 96);
239     this.onDesktopSizeUpdateHandler();
240
241   } else if (message.method == 'onPerfStats') {
242     // Return value is ignored. These calls will throw an error if the value
243     // is not a number.
244     getNumberAttr(message.data, 'videoBandwidth');
245     getNumberAttr(message.data, 'videoFrameRate');
246     getNumberAttr(message.data, 'captureLatency');
247     getNumberAttr(message.data, 'encodeLatency');
248     getNumberAttr(message.data, 'decodeLatency');
249     getNumberAttr(message.data, 'renderLatency');
250     getNumberAttr(message.data, 'roundtripLatency');
251     this.perfStats_ =
252         /** @type {remoting.ClientSession.PerfStats} */ message.data;
253
254   } else if (message.method == 'injectClipboardItem') {
255     var mimetype = getStringAttr(message.data, 'mimeType');
256     var item = getStringAttr(message.data, 'item');
257     if (remoting.clipboard) {
258       remoting.clipboard.fromHost(mimetype, item);
259     }
260
261   } else if (message.method == 'onFirstFrameReceived') {
262     if (remoting.clientSession) {
263       remoting.clientSession.onFirstFrameReceived();
264     }
265
266   } else if (message.method == 'onConnectionReady') {
267     var ready = getBooleanAttr(message.data, 'ready');
268     this.onConnectionReadyHandler(ready);
269
270   } else if (message.method == 'fetchPin') {
271     // The pairingSupported value in the dictionary indicates whether both
272     // client and host support pairing. If the client doesn't support pairing,
273     // then the value won't be there at all, so give it a default of false.
274     var pairingSupported = getBooleanAttr(message.data, 'pairingSupported',
275                                           false)
276     this.fetchPinHandler(pairingSupported);
277
278   } else if (message.method == 'setCapabilities') {
279     /** @type {!Array.<string>} */
280     var capabilities = tokenize(getStringAttr(message.data, 'capabilities'));
281     this.onSetCapabilitiesHandler(capabilities);
282
283   } else if (message.method == 'fetchThirdPartyToken') {
284     var tokenUrl = getStringAttr(message.data, 'tokenUrl');
285     var hostPublicKey = getStringAttr(message.data, 'hostPublicKey');
286     var scope = getStringAttr(message.data, 'scope');
287     this.fetchThirdPartyTokenHandler(tokenUrl, hostPublicKey, scope);
288
289   } else if (message.method == 'pairingResponse') {
290     var clientId = getStringAttr(message.data, 'clientId');
291     var sharedSecret = getStringAttr(message.data, 'sharedSecret');
292     this.onPairingComplete_(clientId, sharedSecret);
293
294   } else if (message.method == 'extensionMessage') {
295     var extMsgType = getStringAttr(message, 'type');
296     var extMsgData = getStringAttr(message, 'data');
297     switch (extMsgType) {
298       case 'test-echo-reply':
299         console.log('Got echo reply: ' + extMsgData);
300         break;
301       default:
302         if (!this.onExtensionMessage_(extMsgType, extMsgData)) {
303           console.log('Unexpected message received: ' +
304                       extMsgType + ': ' + extMsgData);
305         }
306     }
307
308   } else if (message.method == 'mediaSourceReset') {
309     if (!this.mediaSourceRenderer_) {
310       console.error('Unexpected mediaSourceReset.');
311       return;
312     }
313     this.mediaSourceRenderer_.reset(getStringAttr(message.data, 'format'))
314
315   } else if (message.method == 'mediaSourceData') {
316     if (!(message.data['buffer'] instanceof ArrayBuffer)) {
317       console.error('Invalid mediaSourceData message:', message.data);
318       return;
319     }
320     if (!this.mediaSourceRenderer_) {
321       console.error('Unexpected mediaSourceData.');
322       return;
323     }
324     this.mediaSourceRenderer_.onIncomingData(
325         (/** @type {ArrayBuffer} */ message.data['buffer']));
326   }
327 };
328
329 /**
330  * Deletes the plugin.
331  */
332 remoting.ClientPlugin.prototype.cleanup = function() {
333   this.plugin.parentNode.removeChild(this.plugin);
334 };
335
336 /**
337  * @return {HTMLEmbedElement} HTML element that correspods to the plugin.
338  */
339 remoting.ClientPlugin.prototype.element = function() {
340   return this.plugin;
341 };
342
343 /**
344  * @param {function(boolean): void} onDone
345  */
346 remoting.ClientPlugin.prototype.initialize = function(onDone) {
347   if (this.helloReceived_) {
348     onDone(true);
349   } else {
350     this.onInitializedCallback_ = onDone;
351   }
352 };
353
354 /**
355  * @return {boolean} True if the plugin and web-app versions are compatible.
356  */
357 remoting.ClientPlugin.prototype.isSupportedVersion = function() {
358   if (!this.helloReceived_) {
359     console.error(
360         "isSupportedVersion() is called before the plugin is initialized.");
361     return false;
362   }
363   return this.API_VERSION_ >= this.pluginApiMinVersion_ &&
364       this.pluginApiVersion_ >= this.API_MIN_VERSION_;
365 };
366
367 /**
368  * @param {remoting.ClientPlugin.Feature} feature The feature to test for.
369  * @return {boolean} True if the plugin supports the named feature.
370  */
371 remoting.ClientPlugin.prototype.hasFeature = function(feature) {
372   if (!this.helloReceived_) {
373     console.error(
374         "hasFeature() is called before the plugin is initialized.");
375     return false;
376   }
377   return this.pluginApiFeatures_.indexOf(feature) > -1;
378 };
379
380 /**
381  * @return {boolean} True if the plugin supports the injectKeyEvent API.
382  */
383 remoting.ClientPlugin.prototype.isInjectKeyEventSupported = function() {
384   return this.pluginApiVersion_ >= 6;
385 };
386
387 /**
388  * @param {string} iq Incoming IQ stanza.
389  */
390 remoting.ClientPlugin.prototype.onIncomingIq = function(iq) {
391   if (this.plugin && this.plugin.postMessage) {
392     this.plugin.postMessage(JSON.stringify(
393         { method: 'incomingIq', data: { iq: iq } }));
394   } else {
395     // plugin.onIq may not be set after the plugin has been shut
396     // down. Particularly this happens when we receive response to
397     // session-terminate stanza.
398     console.warn('plugin.onIq is not set so dropping incoming message.');
399   }
400 };
401
402 /**
403  * @param {string} hostJid The jid of the host to connect to.
404  * @param {string} hostPublicKey The base64 encoded version of the host's
405  *     public key.
406  * @param {string} localJid Local jid.
407  * @param {string} sharedSecret The access code for IT2Me or the PIN
408  *     for Me2Me.
409  * @param {string} authenticationMethods Comma-separated list of
410  *     authentication methods the client should attempt to use.
411  * @param {string} authenticationTag A host-specific tag to mix into
412  *     authentication hashes.
413  * @param {string} clientPairingId For paired Me2Me connections, the
414  *     pairing id for this client, as issued by the host.
415  * @param {string} clientPairedSecret For paired Me2Me connections, the
416  *     paired secret for this client, as issued by the host.
417  */
418 remoting.ClientPlugin.prototype.connect = function(
419     hostJid, hostPublicKey, localJid, sharedSecret,
420     authenticationMethods, authenticationTag,
421     clientPairingId, clientPairedSecret) {
422   this.plugin.postMessage(JSON.stringify(
423     { method: 'connect', data: {
424         hostJid: hostJid,
425         hostPublicKey: hostPublicKey,
426         localJid: localJid,
427         sharedSecret: sharedSecret,
428         authenticationMethods: authenticationMethods,
429         authenticationTag: authenticationTag,
430         capabilities: this.capabilities_.join(" "),
431         clientPairingId: clientPairingId,
432         clientPairedSecret: clientPairedSecret
433       }
434     }));
435 };
436
437 /**
438  * Release all currently pressed keys.
439  */
440 remoting.ClientPlugin.prototype.releaseAllKeys = function() {
441   this.plugin.postMessage(JSON.stringify(
442       { method: 'releaseAllKeys', data: {} }));
443 };
444
445 /**
446  * Send a key event to the host.
447  *
448  * @param {number} usbKeycode The USB-style code of the key to inject.
449  * @param {boolean} pressed True to inject a key press, False for a release.
450  */
451 remoting.ClientPlugin.prototype.injectKeyEvent =
452     function(usbKeycode, pressed) {
453   this.plugin.postMessage(JSON.stringify(
454       { method: 'injectKeyEvent', data: {
455           'usbKeycode': usbKeycode,
456           'pressed': pressed}
457       }));
458 };
459
460 /**
461  * Remap one USB keycode to another in all subsequent key events.
462  *
463  * @param {number} fromKeycode The USB-style code of the key to remap.
464  * @param {number} toKeycode The USB-style code to remap the key to.
465  */
466 remoting.ClientPlugin.prototype.remapKey =
467     function(fromKeycode, toKeycode) {
468   this.plugin.postMessage(JSON.stringify(
469       { method: 'remapKey', data: {
470           'fromKeycode': fromKeycode,
471           'toKeycode': toKeycode}
472       }));
473 };
474
475 /**
476  * Enable/disable redirection of the specified key to the web-app.
477  *
478  * @param {number} keycode The USB-style code of the key.
479  * @param {Boolean} trap True to enable trapping, False to disable.
480  */
481 remoting.ClientPlugin.prototype.trapKey = function(keycode, trap) {
482   this.plugin.postMessage(JSON.stringify(
483       { method: 'trapKey', data: {
484           'keycode': keycode,
485           'trap': trap}
486       }));
487 };
488
489 /**
490  * Returns an associative array with a set of stats for this connecton.
491  *
492  * @return {remoting.ClientSession.PerfStats} The connection statistics.
493  */
494 remoting.ClientPlugin.prototype.getPerfStats = function() {
495   return this.perfStats_;
496 };
497
498 /**
499  * Sends a clipboard item to the host.
500  *
501  * @param {string} mimeType The MIME type of the clipboard item.
502  * @param {string} item The clipboard item.
503  */
504 remoting.ClientPlugin.prototype.sendClipboardItem =
505     function(mimeType, item) {
506   if (!this.hasFeature(remoting.ClientPlugin.Feature.SEND_CLIPBOARD_ITEM))
507     return;
508   this.plugin.postMessage(JSON.stringify(
509       { method: 'sendClipboardItem',
510         data: { mimeType: mimeType, item: item }}));
511 };
512
513 /**
514  * Notifies the host that the client has the specified size and pixel density.
515  *
516  * @param {number} width The available client width in DIPs.
517  * @param {number} height The available client height in DIPs.
518  * @param {number} device_scale The number of device pixels per DIP.
519  */
520 remoting.ClientPlugin.prototype.notifyClientResolution =
521     function(width, height, device_scale) {
522   if (this.hasFeature(remoting.ClientPlugin.Feature.NOTIFY_CLIENT_RESOLUTION)) {
523     var dpi = Math.floor(device_scale * 96);
524     this.plugin.postMessage(JSON.stringify(
525         { method: 'notifyClientResolution',
526           data: { width: Math.floor(width * device_scale),
527                   height: Math.floor(height * device_scale),
528                   x_dpi: dpi, y_dpi: dpi }}));
529   }
530 };
531
532 /**
533  * Requests that the host pause or resume sending video updates.
534  *
535  * @param {boolean} pause True to suspend video updates, false otherwise.
536  */
537 remoting.ClientPlugin.prototype.pauseVideo =
538     function(pause) {
539   if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_VIDEO)) {
540     return;
541   }
542   this.plugin.postMessage(JSON.stringify(
543       { method: 'pauseVideo', data: { pause: pause }}));
544 };
545
546 /**
547  * Requests that the host pause or resume sending audio updates.
548  *
549  * @param {boolean} pause True to suspend audio updates, false otherwise.
550  */
551 remoting.ClientPlugin.prototype.pauseAudio =
552     function(pause) {
553   if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_AUDIO)) {
554     return;
555   }
556   this.plugin.postMessage(JSON.stringify(
557       { method: 'pauseAudio', data: { pause: pause }}));
558 };
559
560 /**
561  * Called when a PIN is obtained from the user.
562  *
563  * @param {string} pin The PIN.
564  */
565 remoting.ClientPlugin.prototype.onPinFetched =
566     function(pin) {
567   if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) {
568     return;
569   }
570   this.plugin.postMessage(JSON.stringify(
571       { method: 'onPinFetched', data: { pin: pin }}));
572 };
573
574 /**
575  * Tells the plugin to ask for the PIN asynchronously.
576  */
577 remoting.ClientPlugin.prototype.useAsyncPinDialog =
578     function() {
579   if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) {
580     return;
581   }
582   this.plugin.postMessage(JSON.stringify(
583       { method: 'useAsyncPinDialog', data: {} }));
584 };
585
586 /**
587  * Sets the third party authentication token and shared secret.
588  *
589  * @param {string} token The token received from the token URL.
590  * @param {string} sharedSecret Shared secret received from the token URL.
591  */
592 remoting.ClientPlugin.prototype.onThirdPartyTokenFetched = function(
593     token, sharedSecret) {
594   this.plugin.postMessage(JSON.stringify(
595     { method: 'onThirdPartyTokenFetched',
596       data: { token: token, sharedSecret: sharedSecret}}));
597 };
598
599 /**
600  * Request pairing with the host for PIN-less authentication.
601  *
602  * @param {string} clientName The human-readable name of the client.
603  * @param {function(string, string):void} onDone, Callback to receive the
604  *     client id and shared secret when they are available.
605  */
606 remoting.ClientPlugin.prototype.requestPairing =
607     function(clientName, onDone) {
608   if (!this.hasFeature(remoting.ClientPlugin.Feature.PINLESS_AUTH)) {
609     return;
610   }
611   this.onPairingComplete_ = onDone;
612   this.plugin.postMessage(JSON.stringify(
613       { method: 'requestPairing', data: { clientName: clientName } }));
614 };
615
616 /**
617  * Send an extension message to the host.
618  *
619  * @param {string} type The message type.
620  * @param {Object} message The message payload.
621  */
622 remoting.ClientPlugin.prototype.sendClientMessage =
623     function(type, message) {
624   if (!this.hasFeature(remoting.ClientPlugin.Feature.EXTENSION_MESSAGE)) {
625     return;
626   }
627   this.plugin.postMessage(JSON.stringify(
628     { method: 'extensionMessage',
629       data: { type: type, data: JSON.stringify(message) } }));
630
631 };
632
633 /**
634  * Request MediaStream-based rendering.
635  *
636  * @param {remoting.MediaSourceRenderer} mediaSourceRenderer
637  */
638 remoting.ClientPlugin.prototype.enableMediaSourceRendering =
639     function(mediaSourceRenderer) {
640   if (!this.hasFeature(remoting.ClientPlugin.Feature.MEDIA_SOURCE_RENDERING)) {
641     return;
642   }
643   this.mediaSourceRenderer_ = mediaSourceRenderer;
644   this.plugin.postMessage(JSON.stringify(
645       { method: 'enableMediaSourceRendering', data: {} }));
646 };
647
648 /**
649  * If we haven't yet received a "hello" message from the plugin, change its
650  * size so that the user can confirm it if click-to-play is enabled, or can
651  * see the "this plugin is disabled" message if it is actually disabled.
652  * @private
653  */
654 remoting.ClientPlugin.prototype.showPluginForClickToPlay_ = function() {
655   if (!this.helloReceived_) {
656     var width = 200;
657     var height = 200;
658     this.plugin.width = width;
659     this.plugin.height = height;
660     // Center the plugin just underneath the "Connnecting..." dialog.
661     var parentNode = this.plugin.parentNode;
662     var dialog = document.getElementById('client-dialog');
663     var dialogRect = dialog.getBoundingClientRect();
664     parentNode.style.top = (dialogRect.bottom + 16) + 'px';
665     parentNode.style.left = (window.innerWidth - width) / 2 + 'px';
666   }
667 };