Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / remoting / webapp / base / js / client_plugin_impl.js
blobc89c0cf0703882f30e456374e4610aae6ca235ba
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6  * @fileoverview
7  * Class 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  */
14 'use strict';
16 /** @suppress {duplicate} */
17 var remoting = remoting || {};
19 /** @constructor */
20 remoting.ClientPluginMessage = function() {
21   /** @type {string} */
22   this.method = '';
24   /** @type {Object<*>} */
25   this.data = {};
28 /**
29  * @param {Element} container The container for the embed element.
30  * @param {Array<string>} requiredCapabilities The set of capabilties that the
31  *     session must support for this application.
32  * @constructor
33  * @implements {remoting.ClientPlugin}
34  */
35 remoting.ClientPluginImpl = function(container,
36                                      requiredCapabilities) {
37   // TODO(kelvinp): Hack to remove all plugin elements as our current code does
38   // not handle connection cancellation properly.
39   container.innerText = '';
40   this.plugin_ = remoting.ClientPluginImpl.createPluginElement_();
41   this.plugin_.id = 'session-client-plugin';
42   container.appendChild(this.plugin_);
44   /** @private {Array<string>} */
45   this.requiredCapabilities_ = requiredCapabilities;
47   /** @private {remoting.ClientPlugin.ConnectionEventHandler} */
48   this.connectionEventHandler_ = null;
50   /** @private {?function(string, number, number)} */
51   this.updateMouseCursorImage_ = base.doNothing;
52   /** @private {?function(string, string)} */
53   this.updateClipboardData_ = base.doNothing;
54   /** @private {?function(string)} */
55   this.onCastExtensionHandler_ = base.doNothing;
56   /** @private {?function({rects:Array<Array<number>>}):void} */
57   this.debugRegionHandler_ = null;
59   /** @private {number} */
60   this.pluginApiVersion_ = -1;
61   /** @private {Array<string>} */
62   this.pluginApiFeatures_ = [];
63   /** @private {number} */
64   this.pluginApiMinVersion_ = -1;
65   /**
66    * Capabilities to be used for the next connect request.
67    * @private {!Array<string>}
68    */
69   this.capabilities_ = [];
70   /**
71    * Capabilities that are negotiated between the client and the host.
72    * @private {Array<remoting.ClientSession.Capability>}
73    */
74   this.hostCapabilities_ = null;
75   /** @private {boolean} */
76   this.helloReceived_ = false;
77   /** @private {function(boolean)|null} */
78   this.onInitializedCallback_ = null;
79   /** @private {function(string, string):void} */
80   this.onPairingComplete_ = function(clientId, sharedSecret) {};
81   /** @private {remoting.ClientSession.PerfStats} */
82   this.perfStats_ = new remoting.ClientSession.PerfStats();
84   /** @type {remoting.ClientPluginImpl} */
85   var that = this;
86   this.plugin_.addEventListener('message',
87         /** @param {Event} event Message event from the plugin. */
88         function(event) {
89           that.handleMessage_(
90               /** @type {remoting.ClientPluginMessage} */ (event.data));
91         }, false);
93   if (remoting.settings.CLIENT_PLUGIN_TYPE == 'native') {
94     window.setTimeout(this.showPluginForClickToPlay_.bind(this), 500);
95   }
97   /** @private */
98   this.hostDesktop_ = new remoting.ClientPlugin.HostDesktopImpl(
99       this, this.postMessage_.bind(this));
101   /** @private */
102   this.extensions_ = new remoting.ProtocolExtensionManager(
103       this.sendClientMessage_.bind(this));
105   /** @private {remoting.CredentialsProvider} */
106   this.credentials_ = null;
108   /** @private {!Object} */
109   this.keyRemappings_ = {};
113  * Creates plugin element without adding it to a container.
115  * @return {HTMLEmbedElement} Plugin element
116  */
117 remoting.ClientPluginImpl.createPluginElement_ = function() {
118   var plugin =
119       /** @type {HTMLEmbedElement} */ (document.createElement('embed'));
120   if (remoting.settings.CLIENT_PLUGIN_TYPE == 'pnacl') {
121     plugin.src = 'remoting_client_pnacl.nmf';
122     plugin.type = 'application/x-pnacl';
123   } else if (remoting.settings.CLIENT_PLUGIN_TYPE == 'nacl') {
124     plugin.src = 'remoting_client_nacl.nmf';
125     plugin.type = 'application/x-nacl';
126   } else {
127     plugin.src = 'about://none';
128     plugin.type = 'application/vnd.chromium.remoting-viewer';
129   }
130   plugin.width = '0';
131   plugin.height = '0';
132   plugin.tabIndex = 0;  // Required, otherwise focus() doesn't work.
133   return plugin;
137  * Chromoting session API version (for this javascript).
138  * This is compared with the plugin API version to verify that they are
139  * compatible.
141  * @const
142  * @private
143  */
144 remoting.ClientPluginImpl.prototype.API_VERSION_ = 6;
147  * The oldest API version that we support.
148  * This will differ from the |API_VERSION_| if we maintain backward
149  * compatibility with older API versions.
151  * @const
152  * @private
153  */
154 remoting.ClientPluginImpl.prototype.API_MIN_VERSION_ = 5;
157  * @param {remoting.ClientPlugin.ConnectionEventHandler} handler
158  */
159 remoting.ClientPluginImpl.prototype.setConnectionEventHandler =
160     function(handler) {
161   this.connectionEventHandler_ = handler;
165  * @param {function(string, number, number):void} handler
166  */
167 remoting.ClientPluginImpl.prototype.setMouseCursorHandler = function(handler) {
168   this.updateMouseCursorImage_ = handler;
172  * @param {function(string, string):void} handler
173  */
174 remoting.ClientPluginImpl.prototype.setClipboardHandler = function(handler) {
175   this.updateClipboardData_ = handler;
179  * @param {?function({rects:Array<Array<number>>}):void} handler
180  */
181 remoting.ClientPluginImpl.prototype.setDebugDirtyRegionHandler =
182     function(handler) {
183   this.debugRegionHandler_ = handler;
184   this.plugin_.postMessage(JSON.stringify(
185       { method: 'enableDebugRegion', data: { enable: handler != null } }));
189  * @param {string|remoting.ClientPluginMessage}
190  *    rawMessage Message from the plugin.
191  * @private
192  */
193 remoting.ClientPluginImpl.prototype.handleMessage_ = function(rawMessage) {
194   var message =
195       /** @type {remoting.ClientPluginMessage} */
196       ((typeof(rawMessage) == 'string') ? base.jsonParseSafe(rawMessage)
197                                         : rawMessage);
198   if (!message || !('method' in message) || !('data' in message)) {
199     console.error('Received invalid message from the plugin:', rawMessage);
200     return;
201   }
203   try {
204     this.handleMessageMethod_(message);
205   } catch(/** @type {*} */ e) {
206     console.error(e);
207   }
211  * @param {remoting.ClientPluginMessage}
212  *    message Parsed message from the plugin.
213  * @private
214  */
215 remoting.ClientPluginImpl.prototype.handleMessageMethod_ = function(message) {
216   /**
217    * Splits a string into a list of words delimited by spaces.
218    * @param {string} str String that should be split.
219    * @return {!Array<string>} List of words.
220    */
221   var tokenize = function(str) {
222     /** @type {Array<string>} */
223     var tokens = str.match(/\S+/g);
224     return tokens ? tokens : [];
225   };
227   if (this.connectionEventHandler_) {
228     var handler = this.connectionEventHandler_;
230     if (message.method == 'sendOutgoingIq') {
231       handler.onOutgoingIq(base.getStringAttr(message.data, 'iq'));
233     } else if (message.method == 'logDebugMessage') {
234       handler.onDebugMessage(base.getStringAttr(message.data, 'message'));
236     } else if (message.method == 'onConnectionStatus') {
237       var stateString = base.getStringAttr(message.data, 'state');
238       var state = remoting.ClientSession.State.fromString(stateString);
239       var error = remoting.ClientSession.ConnectionError.fromString(
240           base.getStringAttr(message.data, 'error'));
242       // Delay firing the CONNECTED event until the capabilities are negotiated,
243       // TODO(kelvinp): Fix the client plugin to fire capabilities and the
244       // connected event in the same message.
245       if (state === remoting.ClientSession.State.CONNECTED) {
246         console.assert(this.hostCapabilities_ === null,
247             'Capabilities should only be set after the session is connected');
248         return;
249       }
250       handler.onConnectionStatusUpdate(state, error);
252     } else if (message.method == 'onRouteChanged') {
253       var channel = base.getStringAttr(message.data, 'channel');
254       var connectionType = base.getStringAttr(message.data, 'connectionType');
255       handler.onRouteChanged(channel, connectionType);
257     } else if (message.method == 'onConnectionReady') {
258       var ready = base.getBooleanAttr(message.data, 'ready');
259       handler.onConnectionReady(ready);
261     } else if (message.method == 'setCapabilities') {
262       var capabilityString = base.getStringAttr(message.data, 'capabilities');
263       console.log('plugin: setCapabilities: [' + capabilityString + ']');
265       console.assert(this.hostCapabilities_ === null,
266                      'setCapabilities() should only be called once.');
267       this.hostCapabilities_ = tokenize(capabilityString);
269       handler.onConnectionStatusUpdate(
270           remoting.ClientSession.State.CONNECTED,
271           remoting.ClientSession.ConnectionError.NONE);
272       this.extensions_.start();
274     } else if (message.method == 'onFirstFrameReceived') {
275       handler.onFirstFrameReceived();
277     }
278   }
280   if (message.method == 'hello') {
281     // Resize in case we had to enlarge it to support click-to-play.
282     this.hidePluginForClickToPlay_();
283     this.pluginApiVersion_ = base.getNumberAttr(message.data, 'apiVersion');
284     this.pluginApiMinVersion_ =
285         base.getNumberAttr(message.data, 'apiMinVersion');
287     if (this.pluginApiVersion_ >= 7) {
288       this.pluginApiFeatures_ =
289           tokenize(base.getStringAttr(message.data, 'apiFeatures'));
291       // Negotiate capabilities.
292       /** @type {!Array<string>} */
293       var supportedCapabilities = [];
294       if ('supportedCapabilities' in message.data) {
295         supportedCapabilities =
296             tokenize(base.getStringAttr(message.data, 'supportedCapabilities'));
297       }
298       // At the moment the webapp does not recognize any of
299       // 'requestedCapabilities' capabilities (so they all should be disabled)
300       // and do not care about any of 'supportedCapabilities' capabilities (so
301       // they all can be enabled).
302       // All the required capabilities (specified by the app) are added to this.
303       this.capabilities_ = supportedCapabilities.concat(
304           this.requiredCapabilities_);
305     } else if (this.pluginApiVersion_ >= 6) {
306       this.pluginApiFeatures_ = ['highQualityScaling', 'injectKeyEvent'];
307     } else {
308       this.pluginApiFeatures_ = ['highQualityScaling'];
309     }
310     this.helloReceived_ = true;
311     if (this.onInitializedCallback_ != null) {
312       this.onInitializedCallback_(true);
313       this.onInitializedCallback_ = null;
314     }
316   } else if (message.method == 'onDesktopSize') {
317     this.hostDesktop_.onSizeUpdated(message);
318   } else if (message.method == 'onDesktopShape') {
319     this.hostDesktop_.onShapeUpdated(message);
320   } else if (message.method == 'onPerfStats') {
321     // Return value is ignored. These calls will throw an error if the value
322     // is not a number.
323     base.getNumberAttr(message.data, 'videoBandwidth');
324     base.getNumberAttr(message.data, 'videoFrameRate');
325     base.getNumberAttr(message.data, 'captureLatency');
326     base.getNumberAttr(message.data, 'encodeLatency');
327     base.getNumberAttr(message.data, 'decodeLatency');
328     base.getNumberAttr(message.data, 'renderLatency');
329     base.getNumberAttr(message.data, 'roundtripLatency');
330     this.perfStats_ =
331         /** @type {remoting.ClientSession.PerfStats} */ (message.data);
333   } else if (message.method == 'injectClipboardItem') {
334     var mimetype = base.getStringAttr(message.data, 'mimeType');
335     var item = base.getStringAttr(message.data, 'item');
336     this.updateClipboardData_(mimetype, item);
338   } else if (message.method == 'fetchPin') {
339     // The pairingSupported value in the dictionary indicates whether both
340     // client and host support pairing. If the client doesn't support pairing,
341     // then the value won't be there at all, so give it a default of false.
342     var pairingSupported = base.getBooleanAttr(message.data, 'pairingSupported',
343                                           false);
344     this.credentials_.getPIN(pairingSupported).then(
345         this.onPinFetched_.bind(this)
346     );
348   } else if (message.method == 'fetchThirdPartyToken') {
349     var tokenUrl = base.getStringAttr(message.data, 'tokenUrl');
350     var hostPublicKey = base.getStringAttr(message.data, 'hostPublicKey');
351     var scope = base.getStringAttr(message.data, 'scope');
352     this.credentials_.getThirdPartyToken(tokenUrl, hostPublicKey, scope).then(
353       this.onThirdPartyTokenFetched_.bind(this)
354     );
355   } else if (message.method == 'pairingResponse') {
356     var clientId = base.getStringAttr(message.data, 'clientId');
357     var sharedSecret = base.getStringAttr(message.data, 'sharedSecret');
358     this.onPairingComplete_(clientId, sharedSecret);
360   } else if (message.method == 'unsetCursorShape') {
361     this.updateMouseCursorImage_('', 0, 0);
363   } else if (message.method == 'setCursorShape') {
364     var width = base.getNumberAttr(message.data, 'width');
365     var height = base.getNumberAttr(message.data, 'height');
366     var hotspotX = base.getNumberAttr(message.data, 'hotspotX');
367     var hotspotY = base.getNumberAttr(message.data, 'hotspotY');
368     var srcArrayBuffer = base.getObjectAttr(message.data, 'data');
370     var canvas =
371         /** @type {HTMLCanvasElement} */ (document.createElement('canvas'));
372     canvas.width = width;
373     canvas.height = height;
375     var context =
376         /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
377     var imageData = context.getImageData(0, 0, width, height);
378     console.assert(srcArrayBuffer instanceof ArrayBuffer,
379                    '|srcArrayBuffer| is not an ArrayBuffer.');
380     var src = new Uint8Array(/** @type {ArrayBuffer} */(srcArrayBuffer));
381     var dest = imageData.data;
382     for (var i = 0; i < /** @type {number} */(dest.length); i += 4) {
383       dest[i] = src[i + 2];
384       dest[i + 1] = src[i + 1];
385       dest[i + 2] = src[i];
386       dest[i + 3] = src[i + 3];
387     }
389     context.putImageData(imageData, 0, 0);
390     this.updateMouseCursorImage_(canvas.toDataURL(), hotspotX, hotspotY);
392   } else if (message.method == 'onDebugRegion') {
393     if (this.debugRegionHandler_) {
394       this.debugRegionHandler_(
395           /** @type {{rects: Array<(Array<number>)>}} **/(message.data));
396     }
397   } else if (message.method == 'extensionMessage') {
398     var extMsgType = base.getStringAttr(message.data, 'type');
399     var extMsgData = base.getStringAttr(message.data, 'data');
400     this.extensions_.onProtocolExtensionMessage(extMsgType, extMsgData);
402   }
406  * Deletes the plugin.
407  */
408 remoting.ClientPluginImpl.prototype.dispose = function() {
409   if (this.plugin_) {
410     this.plugin_.parentNode.removeChild(this.plugin_);
411     this.plugin_ = null;
412   }
414   base.dispose(this.extensions_);
415   this.extensions_ = null;
419  * @return {HTMLEmbedElement} HTML element that corresponds to the plugin.
420  */
421 remoting.ClientPluginImpl.prototype.element = function() {
422   return this.plugin_;
426  * @param {function(boolean): void} onDone
427  */
428 remoting.ClientPluginImpl.prototype.initialize = function(onDone) {
429   if (this.helloReceived_) {
430     onDone(true);
431   } else {
432     this.onInitializedCallback_ = onDone;
433   }
437  * @return {boolean} True if the plugin and web-app versions are compatible.
438  */
439 remoting.ClientPluginImpl.prototype.isSupportedVersion = function() {
440   if (!this.helloReceived_) {
441     console.error(
442         "isSupportedVersion() is called before the plugin is initialized.");
443     return false;
444   }
445   return this.API_VERSION_ >= this.pluginApiMinVersion_ &&
446       this.pluginApiVersion_ >= this.API_MIN_VERSION_;
450  * @param {remoting.ClientPlugin.Feature} feature The feature to test for.
451  * @return {boolean} True if the plugin supports the named feature.
452  */
453 remoting.ClientPluginImpl.prototype.hasFeature = function(feature) {
454   if (!this.helloReceived_) {
455     console.error(
456         "hasFeature() is called before the plugin is initialized.");
457     return false;
458   }
459   return this.pluginApiFeatures_.indexOf(feature) > -1;
464  * @param {remoting.ClientSession.Capability} capability The capability to test
465  *     for.
466  * @return {boolean} True if the capability has been negotiated between
467  *     the client and host.
468  */
469 remoting.ClientPluginImpl.prototype.hasCapability = function(capability) {
470   return this.hostCapabilities_ !== null &&
471          this.hostCapabilities_.indexOf(capability) > -1;
475  * @return {boolean} True if the plugin supports the injectKeyEvent API.
476  */
477 remoting.ClientPluginImpl.prototype.isInjectKeyEventSupported = function() {
478   return this.pluginApiVersion_ >= 6;
482  * @param {string} iq Incoming IQ stanza.
483  */
484 remoting.ClientPluginImpl.prototype.onIncomingIq = function(iq) {
485   if (this.plugin_ && this.plugin_.postMessage) {
486     this.plugin_.postMessage(JSON.stringify(
487         { method: 'incomingIq', data: { iq: iq } }));
488   } else {
489     // plugin.onIq may not be set after the plugin has been shut
490     // down. Particularly this happens when we receive response to
491     // session-terminate stanza.
492     console.warn('plugin.onIq is not set so dropping incoming message.');
493   }
497  * @param {remoting.Host} host The host to connect to.
498  * @param {string} localJid Local jid.
499  * @param {remoting.CredentialsProvider} credentialsProvider
500  */
501 remoting.ClientPluginImpl.prototype.connect =
502     function(host, localJid, credentialsProvider) {
503   var keyFilter = '';
504   if (remoting.platformIsMac()) {
505     keyFilter = 'mac';
506   } else if (remoting.platformIsChromeOS()) {
507     keyFilter = 'cros';
508   }
510   // Use PPB_VideoDecoder API only in Chrome 43 and above. It is broken in
511   // previous versions of Chrome, see crbug.com/459103 and crbug.com/463577 .
512   var enableVideoDecodeRenderer =
513       parseInt((remoting.getChromeVersion() || '0').split('.')[0], 10) >= 43;
514   this.plugin_.postMessage(JSON.stringify(
515       { method: 'delegateLargeCursors', data: {} }));
516   var methods = 'third_party,spake2_pair,spake2_hmac,spake2_plain';
517   this.credentials_ = credentialsProvider;
518   this.useAsyncPinDialog_();
519   this.plugin_.postMessage(JSON.stringify(
520     { method: 'connect', data: {
521         hostJid: host.jabberId,
522         hostPublicKey: host.publicKey,
523         localJid: localJid,
524         sharedSecret: '',
525         authenticationMethods: methods,
526         authenticationTag: host.hostId,
527         capabilities: this.capabilities_.join(" "),
528         clientPairingId: credentialsProvider.getPairingInfo().clientId,
529         clientPairedSecret: credentialsProvider.getPairingInfo().sharedSecret,
530         keyFilter: keyFilter,
531         enableVideoDecodeRenderer: enableVideoDecodeRenderer
532       }
533     }));
537  * Release all currently pressed keys.
538  */
539 remoting.ClientPluginImpl.prototype.releaseAllKeys = function() {
540   this.plugin_.postMessage(JSON.stringify(
541       { method: 'releaseAllKeys', data: {} }));
545  * Sets and stores the key remapping setting for the current host.
547  * @param {!Object} remappings
548  */
549 remoting.ClientPluginImpl.prototype.setRemapKeys =
550     function(remappings) {
551   // Cancel any existing remappings and apply the new ones.
552   this.applyRemapKeys_(this.keyRemappings_, false);
553   this.applyRemapKeys_(remappings, true);
554   this.keyRemappings_ = /** @type {!Object} */ (base.deepCopy(remappings));
558  * Applies the configured key remappings to the session, or resets them.
560  * @param {!Object} remappings
561  * @param {boolean} apply True to apply remappings, false to cancel them.
562  * @private
563  */
564 remoting.ClientPluginImpl.prototype.applyRemapKeys_ =
565     function(remappings, apply) {
566   for (var i in remappings) {
567     var from = parseInt(i, 10);
568     var to = parseInt(remappings[i], 10);
569     if (apply) {
570       console.log('remapKey 0x' + from.toString(16) + '>0x' + to.toString(16));
571       this.remapKey(from, to);
572     } else {
573       console.log('cancel remapKey 0x' + from.toString(16));
574       this.remapKey(from, from);
575     }
576   }
580  * Sends a key combination to the remoting host, by sending down events for
581  * the given keys, followed by up events in reverse order.
583  * @param {Array<number>} keys Key codes to be sent.
584  * @return {void} Nothing.
585  */
586 remoting.ClientPluginImpl.prototype.injectKeyCombination =
587     function(keys) {
588   for (var i = 0; i < keys.length; i++) {
589     this.injectKeyEvent(keys[i], true);
590   }
591   for (var i = 0; i < keys.length; i++) {
592     this.injectKeyEvent(keys[i], false);
593   }
597  * Send a key event to the host.
599  * @param {number} usbKeycode The USB-style code of the key to inject.
600  * @param {boolean} pressed True to inject a key press, False for a release.
601  */
602 remoting.ClientPluginImpl.prototype.injectKeyEvent =
603     function(usbKeycode, pressed) {
604   this.plugin_.postMessage(JSON.stringify(
605       { method: 'injectKeyEvent', data: {
606           'usbKeycode': usbKeycode,
607           'pressed': pressed}
608       }));
612  * Remap one USB keycode to another in all subsequent key events.
614  * @param {number} fromKeycode The USB-style code of the key to remap.
615  * @param {number} toKeycode The USB-style code to remap the key to.
616  */
617 remoting.ClientPluginImpl.prototype.remapKey =
618     function(fromKeycode, toKeycode) {
619   this.plugin_.postMessage(JSON.stringify(
620       { method: 'remapKey', data: {
621           'fromKeycode': fromKeycode,
622           'toKeycode': toKeycode}
623       }));
627  * Enable/disable redirection of the specified key to the web-app.
629  * @param {number} keycode The USB-style code of the key.
630  * @param {Boolean} trap True to enable trapping, False to disable.
631  */
632 remoting.ClientPluginImpl.prototype.trapKey = function(keycode, trap) {
633   this.plugin_.postMessage(JSON.stringify(
634       { method: 'trapKey', data: {
635           'keycode': keycode,
636           'trap': trap}
637       }));
641  * Returns an associative array with a set of stats for this connecton.
643  * @return {remoting.ClientSession.PerfStats} The connection statistics.
644  */
645 remoting.ClientPluginImpl.prototype.getPerfStats = function() {
646   return this.perfStats_;
650  * Sends a clipboard item to the host.
652  * @param {string} mimeType The MIME type of the clipboard item.
653  * @param {string} item The clipboard item.
654  */
655 remoting.ClientPluginImpl.prototype.sendClipboardItem =
656     function(mimeType, item) {
657   if (!this.hasFeature(remoting.ClientPlugin.Feature.SEND_CLIPBOARD_ITEM))
658     return;
659   this.plugin_.postMessage(JSON.stringify(
660       { method: 'sendClipboardItem',
661         data: { mimeType: mimeType, item: item }}));
665  * Notifies the plugin whether to send touch events to the host.
667  * @param {boolean} enable True if touch events should be sent.
668  */
669 remoting.ClientPluginImpl.prototype.enableTouchEvents = function(enable) {
670   this.plugin_.postMessage(
671       JSON.stringify({method: 'enableTouchEvents', data: {'enable': enable}}));
675  * Notifies the host that the client has the specified size and pixel density.
677  * @param {number} width The available client width in DIPs.
678  * @param {number} height The available client height in DIPs.
679  * @param {number} device_scale The number of device pixels per DIP.
680  */
681 remoting.ClientPluginImpl.prototype.notifyClientResolution =
682     function(width, height, device_scale) {
683   this.hostDesktop_.resize(width, height, device_scale);
687  * Requests that the host pause or resume sending video updates.
689  * @param {boolean} pause True to suspend video updates, false otherwise.
690  */
691 remoting.ClientPluginImpl.prototype.pauseVideo =
692     function(pause) {
693   if (this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) {
694     this.plugin_.postMessage(JSON.stringify(
695         { method: 'videoControl', data: { pause: pause }}));
696   } else if (this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_VIDEO)) {
697     this.plugin_.postMessage(JSON.stringify(
698         { method: 'pauseVideo', data: { pause: pause }}));
699   }
703  * Requests that the host pause or resume sending audio updates.
705  * @param {boolean} pause True to suspend audio updates, false otherwise.
706  */
707 remoting.ClientPluginImpl.prototype.pauseAudio =
708     function(pause) {
709   if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_AUDIO)) {
710     return;
711   }
712   this.plugin_.postMessage(JSON.stringify(
713       { method: 'pauseAudio', data: { pause: pause }}));
717  * Requests that the host configure the video codec for lossless encode.
719  * @param {boolean} wantLossless True to request lossless encoding.
720  */
721 remoting.ClientPluginImpl.prototype.setLosslessEncode =
722     function(wantLossless) {
723   if (!this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) {
724     return;
725   }
726   this.plugin_.postMessage(JSON.stringify(
727       { method: 'videoControl', data: { losslessEncode: wantLossless }}));
731  * Requests that the host configure the video codec for lossless color.
733  * @param {boolean} wantLossless True to request lossless color.
734  */
735 remoting.ClientPluginImpl.prototype.setLosslessColor =
736     function(wantLossless) {
737   if (!this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) {
738     return;
739   }
740   this.plugin_.postMessage(JSON.stringify(
741       { method: 'videoControl', data: { losslessColor: wantLossless }}));
745  * Called when a PIN is obtained from the user.
747  * @param {string} pin The PIN.
748  * @private
749  */
750 remoting.ClientPluginImpl.prototype.onPinFetched_ =
751     function(pin) {
752   if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) {
753     return;
754   }
755   this.plugin_.postMessage(JSON.stringify(
756       { method: 'onPinFetched', data: { pin: pin }}));
760  * Tells the plugin to ask for the PIN asynchronously.
761  * @private
762  */
763 remoting.ClientPluginImpl.prototype.useAsyncPinDialog_ =
764     function() {
765   if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) {
766     return;
767   }
768   this.plugin_.postMessage(JSON.stringify(
769       { method: 'useAsyncPinDialog', data: {} }));
773  * Allows automatic mouse-lock.
774  */
775 remoting.ClientPluginImpl.prototype.allowMouseLock = function() {
776   this.plugin_.postMessage(JSON.stringify(
777       { method: 'allowMouseLock', data: {} }));
781  * Sets the third party authentication token and shared secret.
783  * @param {remoting.ThirdPartyToken} token
784  * @private
785  */
786 remoting.ClientPluginImpl.prototype.onThirdPartyTokenFetched_ = function(
787     token) {
788   this.plugin_.postMessage(JSON.stringify(
789     { method: 'onThirdPartyTokenFetched',
790       data: { token: token.token, sharedSecret: token.secret}}));
794  * Request pairing with the host for PIN-less authentication.
796  * @param {string} clientName The human-readable name of the client.
797  * @param {function(string, string):void} onDone, Callback to receive the
798  *     client id and shared secret when they are available.
799  */
800 remoting.ClientPluginImpl.prototype.requestPairing =
801     function(clientName, onDone) {
802   if (!this.hasFeature(remoting.ClientPlugin.Feature.PINLESS_AUTH)) {
803     return;
804   }
805   this.onPairingComplete_ = onDone;
806   this.plugin_.postMessage(JSON.stringify(
807       { method: 'requestPairing', data: { clientName: clientName } }));
811  * Send an extension message to the host.
813  * @param {string} type The message type.
814  * @param {string} message The message payload.
815  * @private
816  */
817 remoting.ClientPluginImpl.prototype.sendClientMessage_ =
818     function(type, message) {
819   if (!this.hasFeature(remoting.ClientPlugin.Feature.EXTENSION_MESSAGE)) {
820     return;
821   }
822   this.plugin_.postMessage(JSON.stringify(
823       { method: 'extensionMessage',
824         data: { type: type, data: message } }));
828 remoting.ClientPluginImpl.prototype.hostDesktop = function() {
829   return this.hostDesktop_;
832 remoting.ClientPluginImpl.prototype.extensions = function() {
833   return this.extensions_;
837  * If we haven't yet received a "hello" message from the plugin, change its
838  * size so that the user can confirm it if click-to-play is enabled, or can
839  * see the "this plugin is disabled" message if it is actually disabled.
840  * @private
841  */
842 remoting.ClientPluginImpl.prototype.showPluginForClickToPlay_ = function() {
843   if (!this.helloReceived_) {
844     var width = 200;
845     var height = 200;
846     this.plugin_.style.width = width + 'px';
847     this.plugin_.style.height = height + 'px';
848     // Center the plugin just underneath the "Connnecting..." dialog.
849     var dialog = document.getElementById('client-dialog');
850     var dialogRect = dialog.getBoundingClientRect();
851     this.plugin_.style.top = (dialogRect.bottom + 16) + 'px';
852     this.plugin_.style.left = (window.innerWidth - width) / 2 + 'px';
853     this.plugin_.style.position = 'fixed';
854   }
858  * Undo the CSS rules needed to make the plugin clickable for click-to-play.
859  * @private
860  */
861 remoting.ClientPluginImpl.prototype.hidePluginForClickToPlay_ = function() {
862   this.plugin_.style.width = '';
863   this.plugin_.style.height = '';
864   this.plugin_.style.top = '';
865   this.plugin_.style.left = '';
866   this.plugin_.style.position = '';
870  * Callback passed to submodules to post a message to the plugin.
872  * @param {Object} message
873  * @private
874  */
875 remoting.ClientPluginImpl.prototype.postMessage_ = function(message) {
876   if (this.plugin_ && this.plugin_.postMessage) {
877     this.plugin_.postMessage(JSON.stringify(message));
878   }
882  * @constructor
883  * @implements {remoting.ClientPluginFactory}
884  */
885 remoting.DefaultClientPluginFactory = function() {};
888  * @param {Element} container
889  * @param {Array<string>} requiredCapabilities
890  * @return {remoting.ClientPlugin}
891  */
892 remoting.DefaultClientPluginFactory.prototype.createPlugin =
893     function(container, requiredCapabilities) {
894   return new remoting.ClientPluginImpl(container,
895                                        requiredCapabilities);
898 remoting.DefaultClientPluginFactory.prototype.preloadPlugin = function() {
899   if (remoting.settings.CLIENT_PLUGIN_TYPE != 'pnacl') {
900     return;
901   }
903   var plugin = remoting.ClientPluginImpl.createPluginElement_();
904   plugin.addEventListener(
905       'loadend', function() { document.body.removeChild(plugin); }, false);
906   document.body.appendChild(plugin);