Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / remoting / webapp / base / js / client_plugin_impl.js
blob80df124f017cec56b507220ab9ccde077c2afa95
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>} capabilities 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, capabilities) {
36   // TODO(kelvinp): Hack to remove all plugin elements as our current code does
37   // not handle connection cancellation properly.
38   container.innerText = '';
39   this.plugin_ = remoting.ClientPluginImpl.createPluginElement_();
40   this.plugin_.id = 'session-client-plugin';
41   container.appendChild(this.plugin_);
43   /** @private {Array<string>} */
44   this.capabilities_ = capabilities;
46   /** @private {remoting.ClientPlugin.ConnectionEventHandler} */
47   this.connectionEventHandler_ = null;
49   /** @private {?function(string, number, number)} */
50   this.updateMouseCursorImage_ = base.doNothing;
51   /** @private {?function(string, string)} */
52   this.updateClipboardData_ = base.doNothing;
53   /** @private {?function(string)} */
54   this.onCastExtensionHandler_ = base.doNothing;
55   /** @private {?function({rects:Array<Array<number>>}):void} */
56   this.debugRegionHandler_ = null;
58   /** @private {number} */
59   this.pluginApiVersion_ = -1;
60   /** @private {Array<string>} */
61   this.pluginApiFeatures_ = [];
62   /** @private {number} */
63   this.pluginApiMinVersion_ = -1;
64   /**
65    * Capabilities that are negotiated between the client and the host.
66    * @private {Array<remoting.ClientSession.Capability>}
67    */
68   this.hostCapabilities_ = null;
69   /** @private {boolean} */
70   this.helloReceived_ = false;
71   /** @private {function(boolean)|null} */
72   this.onInitializedCallback_ = null;
73   /** @private {function(string, string):void} */
74   this.onPairingComplete_ = function(clientId, sharedSecret) {};
75   /** @private {remoting.ClientSession.PerfStats} */
76   this.perfStats_ = new remoting.ClientSession.PerfStats();
78   /** @type {remoting.ClientPluginImpl} */
79   var that = this;
80   this.plugin_.addEventListener('message',
81         /** @param {Event} event Message event from the plugin. */
82         function(event) {
83           that.handleMessage_(
84               /** @type {remoting.ClientPluginMessage} */ (event.data));
85         }, false);
87   /** @private */
88   this.hostDesktop_ = new remoting.ClientPlugin.HostDesktopImpl(
89       this, this.postMessage_.bind(this));
91   /** @private */
92   this.extensions_ = new remoting.ProtocolExtensionManager(
93       this.sendClientMessage_.bind(this));
95   /** @private {remoting.CredentialsProvider} */
96   this.credentials_ = null;
98   /** @private {!Object} */
99   this.keyRemappings_ = {};
103  * Creates plugin element without adding it to a container.
105  * @return {HTMLEmbedElement} Plugin element
106  */
107 remoting.ClientPluginImpl.createPluginElement_ = function() {
108   var plugin =
109       /** @type {HTMLEmbedElement} */ (document.createElement('embed'));
110   plugin.src = 'remoting_client_pnacl.nmf';
111   plugin.type = 'application/x-pnacl';
112   plugin.width = '0';
113   plugin.height = '0';
114   plugin.tabIndex = 0;  // Required, otherwise focus() doesn't work.
115   return plugin;
119  * @param {remoting.ClientPlugin.ConnectionEventHandler} handler
120  */
121 remoting.ClientPluginImpl.prototype.setConnectionEventHandler =
122     function(handler) {
123   this.connectionEventHandler_ = handler;
127  * @param {function(string, number, number):void} handler
128  */
129 remoting.ClientPluginImpl.prototype.setMouseCursorHandler = function(handler) {
130   this.updateMouseCursorImage_ = handler;
134  * @param {function(string, string):void} handler
135  */
136 remoting.ClientPluginImpl.prototype.setClipboardHandler = function(handler) {
137   this.updateClipboardData_ = handler;
141  * @param {?function({rects:Array<Array<number>>}):void} handler
142  */
143 remoting.ClientPluginImpl.prototype.setDebugDirtyRegionHandler =
144     function(handler) {
145   this.debugRegionHandler_ = handler;
146   this.plugin_.postMessage(JSON.stringify(
147       { method: 'enableDebugRegion', data: { enable: handler != null } }));
151  * @param {string|remoting.ClientPluginMessage}
152  *    rawMessage Message from the plugin.
153  * @private
154  */
155 remoting.ClientPluginImpl.prototype.handleMessage_ = function(rawMessage) {
156   var message =
157       /** @type {remoting.ClientPluginMessage} */
158       ((typeof(rawMessage) == 'string') ? base.jsonParseSafe(rawMessage)
159                                         : rawMessage);
160   if (!message || !('method' in message) || !('data' in message)) {
161     console.error('Received invalid message from the plugin:', rawMessage);
162     return;
163   }
165   try {
166     this.handleMessageMethod_(message);
167   } catch(/** @type {*} */ e) {
168     console.error(e);
169   }
173  * @param {remoting.ClientPluginMessage}
174  *    message Parsed message from the plugin.
175  * @private
176  */
177 remoting.ClientPluginImpl.prototype.handleMessageMethod_ = function(message) {
178   /**
179    * Splits a string into a list of words delimited by spaces.
180    * @param {string} str String that should be split.
181    * @return {!Array<string>} List of words.
182    */
183   var tokenize = function(str) {
184     /** @type {Array<string>} */
185     var tokens = str.match(/\S+/g);
186     return tokens ? tokens : [];
187   };
189   if (this.connectionEventHandler_) {
190     var handler = this.connectionEventHandler_;
192     if (message.method == 'sendOutgoingIq') {
193       handler.onOutgoingIq(base.getStringAttr(message.data, 'iq'));
195     } else if (message.method == 'onConnectionStatus') {
196       var stateString = base.getStringAttr(message.data, 'state');
197       var state = remoting.ClientSession.State.fromString(stateString);
198       var error = remoting.ClientSession.ConnectionError.fromString(
199           base.getStringAttr(message.data, 'error'));
201       // Delay firing the CONNECTED event until the capabilities are negotiated,
202       // TODO(kelvinp): Fix the client plugin to fire capabilities and the
203       // connected event in the same message.
204       if (state === remoting.ClientSession.State.CONNECTED) {
205         console.assert(this.hostCapabilities_ === null,
206             'Capabilities should only be set after the session is connected');
207         return;
208       }
209       handler.onConnectionStatusUpdate(state, error);
211     } else if (message.method == 'onRouteChanged') {
212       var channel = base.getStringAttr(message.data, 'channel');
213       var connectionType = base.getStringAttr(message.data, 'connectionType');
214       handler.onRouteChanged(channel, connectionType);
216     } else if (message.method == 'onConnectionReady') {
217       var ready = base.getBooleanAttr(message.data, 'ready');
218       handler.onConnectionReady(ready);
220     } else if (message.method == 'setCapabilities') {
221       var capabilityString = base.getStringAttr(message.data, 'capabilities');
222       console.log('plugin: setCapabilities: [' + capabilityString + ']');
224       console.assert(this.hostCapabilities_ === null,
225                      'setCapabilities() should only be called once.');
226       this.hostCapabilities_ = tokenize(capabilityString);
228       handler.onConnectionStatusUpdate(
229           remoting.ClientSession.State.CONNECTED,
230           remoting.ClientSession.ConnectionError.NONE);
231       this.extensions_.start();
233     } else if (message.method == 'onFirstFrameReceived') {
234       handler.onFirstFrameReceived();
236     }
237   }
239   if (message.method == 'hello') {
240     this.helloReceived_ = true;
241     if (this.onInitializedCallback_ != null) {
242       this.onInitializedCallback_(true);
243       this.onInitializedCallback_ = null;
244     }
246   } else if (message.method == 'onDesktopSize') {
247     this.hostDesktop_.onSizeUpdated(message);
248   } else if (message.method == 'onDesktopShape') {
249     this.hostDesktop_.onShapeUpdated(message);
250   } else if (message.method == 'onPerfStats') {
251     // Return value is ignored. These calls will throw an error if the value
252     // is not a number.
253     base.getNumberAttr(message.data, 'videoBandwidth');
254     base.getNumberAttr(message.data, 'videoFrameRate');
255     base.getNumberAttr(message.data, 'captureLatency');
256     base.getNumberAttr(message.data, 'encodeLatency');
257     base.getNumberAttr(message.data, 'decodeLatency');
258     base.getNumberAttr(message.data, 'renderLatency');
259     base.getNumberAttr(message.data, 'roundtripLatency');
260     this.perfStats_ =
261         /** @type {remoting.ClientSession.PerfStats} */ (message.data);
263   } else if (message.method == 'injectClipboardItem') {
264     var mimetype = base.getStringAttr(message.data, 'mimeType');
265     var item = base.getStringAttr(message.data, 'item');
266     this.updateClipboardData_(mimetype, item);
268   } else if (message.method == 'fetchPin') {
269     // The pairingSupported value in the dictionary indicates whether both
270     // client and host support pairing. If the client doesn't support pairing,
271     // then the value won't be there at all, so give it a default of false.
272     var pairingSupported = base.getBooleanAttr(message.data, 'pairingSupported',
273                                           false);
274     this.credentials_.getPIN(pairingSupported).then(
275         this.onPinFetched_.bind(this)
276     );
278   } else if (message.method == 'fetchThirdPartyToken') {
279     var tokenUrl = base.getStringAttr(message.data, 'tokenUrl');
280     var hostPublicKey = base.getStringAttr(message.data, 'hostPublicKey');
281     var scope = base.getStringAttr(message.data, 'scope');
282     this.credentials_.getThirdPartyToken(tokenUrl, hostPublicKey, scope).then(
283       this.onThirdPartyTokenFetched_.bind(this)
284     );
285   } else if (message.method == 'pairingResponse') {
286     var clientId = base.getStringAttr(message.data, 'clientId');
287     var sharedSecret = base.getStringAttr(message.data, 'sharedSecret');
288     this.onPairingComplete_(clientId, sharedSecret);
290   } else if (message.method == 'unsetCursorShape') {
291     this.updateMouseCursorImage_('', 0, 0);
293   } else if (message.method == 'setCursorShape') {
294     var width = base.getNumberAttr(message.data, 'width');
295     var height = base.getNumberAttr(message.data, 'height');
296     var hotspotX = base.getNumberAttr(message.data, 'hotspotX');
297     var hotspotY = base.getNumberAttr(message.data, 'hotspotY');
298     var srcArrayBuffer = base.getObjectAttr(message.data, 'data');
300     var canvas =
301         /** @type {HTMLCanvasElement} */ (document.createElement('canvas'));
302     canvas.width = width;
303     canvas.height = height;
305     var context =
306         /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
307     var imageData = context.getImageData(0, 0, width, height);
308     console.assert(srcArrayBuffer instanceof ArrayBuffer,
309                    '|srcArrayBuffer| is not an ArrayBuffer.');
310     var src = new Uint8Array(/** @type {ArrayBuffer} */(srcArrayBuffer));
311     var dest = imageData.data;
312     for (var i = 0; i < /** @type {number} */(dest.length); i += 4) {
313       dest[i] = src[i + 2];
314       dest[i + 1] = src[i + 1];
315       dest[i + 2] = src[i];
316       dest[i + 3] = src[i + 3];
317     }
319     context.putImageData(imageData, 0, 0);
320     this.updateMouseCursorImage_(canvas.toDataURL(), hotspotX, hotspotY);
322   } else if (message.method == 'onDebugRegion') {
323     if (this.debugRegionHandler_) {
324       this.debugRegionHandler_(
325           /** @type {{rects: Array<(Array<number>)>}} **/(message.data));
326     }
327   } else if (message.method == 'extensionMessage') {
328     var extMsgType = base.getStringAttr(message.data, 'type');
329     var extMsgData = base.getStringAttr(message.data, 'data');
330     this.extensions_.onProtocolExtensionMessage(extMsgType, extMsgData);
332   }
336  * Deletes the plugin.
337  */
338 remoting.ClientPluginImpl.prototype.dispose = function() {
339   if (this.plugin_) {
340     this.plugin_.parentNode.removeChild(this.plugin_);
341     this.plugin_ = null;
342   }
344   base.dispose(this.extensions_);
345   this.extensions_ = null;
349  * @return {HTMLEmbedElement} HTML element that corresponds to the plugin.
350  */
351 remoting.ClientPluginImpl.prototype.element = function() {
352   return this.plugin_;
356  * @param {function(boolean): void} onDone
357  */
358 remoting.ClientPluginImpl.prototype.initialize = function(onDone) {
359   if (this.helloReceived_) {
360     onDone(true);
361   } else {
362     this.onInitializedCallback_ = onDone;
363   }
367  * @param {remoting.ClientSession.Capability} capability The capability to test
368  *     for.
369  * @return {boolean} True if the capability has been negotiated between
370  *     the client and host.
371  */
372 remoting.ClientPluginImpl.prototype.hasCapability = function(capability) {
373   return this.hostCapabilities_ !== null &&
374          this.hostCapabilities_.indexOf(capability) > -1;
378  * @param {string} iq Incoming IQ stanza.
379  */
380 remoting.ClientPluginImpl.prototype.onIncomingIq = function(iq) {
381   if (this.plugin_ && this.plugin_.postMessage) {
382     this.plugin_.postMessage(JSON.stringify(
383         { method: 'incomingIq', data: { iq: iq } }));
384   } else {
385     // plugin.onIq may not be set after the plugin has been shut
386     // down. Particularly this happens when we receive response to
387     // session-terminate stanza.
388     console.warn('plugin.onIq is not set so dropping incoming message.');
389   }
393  * @param {remoting.Host} host The host to connect to.
394  * @param {string} localJid Local jid.
395  * @param {remoting.CredentialsProvider} credentialsProvider
396  */
397 remoting.ClientPluginImpl.prototype.connect = function(host, localJid,
398                                                        credentialsProvider) {
399   remoting.experiments.get().then(this.connectWithExperiments_.bind(
400       this, host, localJid, credentialsProvider));
404  * @param {remoting.Host} host The host to connect to.
405  * @param {string} localJid Local jid.
406  * @param {remoting.CredentialsProvider} credentialsProvider
407  * @param {Array.<string>} experiments List of enabled experiments.
408  * @private
409  */
410 remoting.ClientPluginImpl.prototype.connectWithExperiments_ = function(
411     host, localJid, credentialsProvider, experiments) {
412   var keyFilter = '';
413   if (remoting.platformIsMac()) {
414     keyFilter = 'mac';
415   } else if (remoting.platformIsChromeOS()) {
416     keyFilter = 'cros';
417   }
419   this.plugin_.postMessage(JSON.stringify(
420       { method: 'delegateLargeCursors', data: {} }));
421   this.credentials_ = credentialsProvider;
422   this.useAsyncPinDialog_();
423   this.plugin_.postMessage(JSON.stringify({
424     method: 'connect',
425     data: {
426       hostJid: host.jabberId,
427       hostPublicKey: host.publicKey,
428       localJid: localJid,
429       sharedSecret: '',
430       authenticationTag: host.hostId,
431       capabilities: this.capabilities_.join(" "),
432       clientPairingId: credentialsProvider.getPairingInfo().clientId,
433       clientPairedSecret: credentialsProvider.getPairingInfo().sharedSecret,
434       keyFilter: keyFilter,
435       experiments: experiments.join(" ")
436     }
437   }));
441  * Release all currently pressed keys.
442  */
443 remoting.ClientPluginImpl.prototype.releaseAllKeys = function() {
444   this.plugin_.postMessage(JSON.stringify(
445       { method: 'releaseAllKeys', data: {} }));
449  * Sets and stores the key remapping setting for the current host.
451  * @param {!Object} remappings
452  */
453 remoting.ClientPluginImpl.prototype.setRemapKeys =
454     function(remappings) {
455   // Cancel any existing remappings and apply the new ones.
456   this.applyRemapKeys_(this.keyRemappings_, false);
457   this.applyRemapKeys_(remappings, true);
458   this.keyRemappings_ = /** @type {!Object} */ (base.deepCopy(remappings));
462  * Applies the configured key remappings to the session, or resets them.
464  * @param {!Object} remappings
465  * @param {boolean} apply True to apply remappings, false to cancel them.
466  * @private
467  */
468 remoting.ClientPluginImpl.prototype.applyRemapKeys_ =
469     function(remappings, apply) {
470   for (var i in remappings) {
471     var from = parseInt(i, 10);
472     var to = parseInt(remappings[i], 10);
473     if (apply) {
474       console.log('remapKey 0x' + from.toString(16) + '>0x' + to.toString(16));
475       this.remapKey(from, to);
476     } else {
477       console.log('cancel remapKey 0x' + from.toString(16));
478       this.remapKey(from, from);
479     }
480   }
484  * Sends a key combination to the remoting host, by sending down events for
485  * the given keys, followed by up events in reverse order.
487  * @param {Array<number>} keys Key codes to be sent.
488  * @return {void} Nothing.
489  */
490 remoting.ClientPluginImpl.prototype.injectKeyCombination =
491     function(keys) {
492   for (var i = 0; i < keys.length; i++) {
493     this.injectKeyEvent(keys[i], true);
494   }
495   for (var i = 0; i < keys.length; i++) {
496     this.injectKeyEvent(keys[i], false);
497   }
501  * Send a key event to the host.
503  * @param {number} usbKeycode The USB-style code of the key to inject.
504  * @param {boolean} pressed True to inject a key press, False for a release.
505  */
506 remoting.ClientPluginImpl.prototype.injectKeyEvent =
507     function(usbKeycode, pressed) {
508   this.plugin_.postMessage(JSON.stringify(
509       { method: 'injectKeyEvent', data: {
510           'usbKeycode': usbKeycode,
511           'pressed': pressed}
512       }));
516  * Remap one USB keycode to another in all subsequent key events.
518  * @param {number} fromKeycode The USB-style code of the key to remap.
519  * @param {number} toKeycode The USB-style code to remap the key to.
520  */
521 remoting.ClientPluginImpl.prototype.remapKey =
522     function(fromKeycode, toKeycode) {
523   this.plugin_.postMessage(JSON.stringify(
524       { method: 'remapKey', data: {
525           'fromKeycode': fromKeycode,
526           'toKeycode': toKeycode}
527       }));
531  * Enable/disable redirection of the specified key to the web-app.
533  * @param {number} keycode The USB-style code of the key.
534  * @param {Boolean} trap True to enable trapping, False to disable.
535  */
536 remoting.ClientPluginImpl.prototype.trapKey = function(keycode, trap) {
537   this.plugin_.postMessage(JSON.stringify(
538       { method: 'trapKey', data: {
539           'keycode': keycode,
540           'trap': trap}
541       }));
545  * Returns an associative array with a set of stats for this connecton.
547  * @return {remoting.ClientSession.PerfStats} The connection statistics.
548  */
549 remoting.ClientPluginImpl.prototype.getPerfStats = function() {
550   return this.perfStats_;
554  * Sends a clipboard item to the host.
556  * @param {string} mimeType The MIME type of the clipboard item.
557  * @param {string} item The clipboard item.
558  */
559 remoting.ClientPluginImpl.prototype.sendClipboardItem =
560     function(mimeType, item) {
561   this.plugin_.postMessage(JSON.stringify(
562       { method: 'sendClipboardItem',
563         data: { mimeType: mimeType, item: item }}));
567  * Notifies the plugin whether to send touch events to the host.
569  * @param {boolean} enable True if touch events should be sent.
570  */
571 remoting.ClientPluginImpl.prototype.enableTouchEvents = function(enable) {
572   this.plugin_.postMessage(
573       JSON.stringify({method: 'enableTouchEvents', data: {'enable': enable}}));
577  * Notifies the host that the client has the specified size and pixel density.
579  * @param {number} width The available client width in DIPs.
580  * @param {number} height The available client height in DIPs.
581  * @param {number} device_scale The number of device pixels per DIP.
582  */
583 remoting.ClientPluginImpl.prototype.notifyClientResolution =
584     function(width, height, device_scale) {
585   this.hostDesktop_.resize(width, height, device_scale);
589  * Requests that the host pause or resume sending video updates.
591  * @param {boolean} pause True to suspend video updates, false otherwise.
592  */
593 remoting.ClientPluginImpl.prototype.pauseVideo =
594     function(pause) {
595   this.plugin_.postMessage(JSON.stringify(
596       { method: 'videoControl', data: { pause: pause }}));
600  * Requests that the host pause or resume sending audio updates.
602  * @param {boolean} pause True to suspend audio updates, false otherwise.
603  */
604 remoting.ClientPluginImpl.prototype.pauseAudio =
605     function(pause) {
606   this.plugin_.postMessage(JSON.stringify(
607       { method: 'pauseAudio', data: { pause: pause }}));
611  * Requests that the host configure the video codec for lossless encode.
613  * @param {boolean} wantLossless True to request lossless encoding.
614  */
615 remoting.ClientPluginImpl.prototype.setLosslessEncode =
616     function(wantLossless) {
617   this.plugin_.postMessage(JSON.stringify(
618       { method: 'videoControl', data: { losslessEncode: wantLossless }}));
622  * Requests that the host configure the video codec for lossless color.
624  * @param {boolean} wantLossless True to request lossless color.
625  */
626 remoting.ClientPluginImpl.prototype.setLosslessColor =
627     function(wantLossless) {
628   this.plugin_.postMessage(JSON.stringify(
629       { method: 'videoControl', data: { losslessColor: wantLossless }}));
633  * Called when a PIN is obtained from the user.
635  * @param {string} pin The PIN.
636  * @private
637  */
638 remoting.ClientPluginImpl.prototype.onPinFetched_ =
639     function(pin) {
640   this.plugin_.postMessage(JSON.stringify(
641       { method: 'onPinFetched', data: { pin: pin }}));
645  * Tells the plugin to ask for the PIN asynchronously.
646  * @private
647  */
648 remoting.ClientPluginImpl.prototype.useAsyncPinDialog_ =
649     function() {
650   this.plugin_.postMessage(JSON.stringify(
651       { method: 'useAsyncPinDialog', data: {} }));
655  * Allows automatic mouse-lock.
656  */
657 remoting.ClientPluginImpl.prototype.allowMouseLock = function() {
658   this.plugin_.postMessage(JSON.stringify(
659       { method: 'allowMouseLock', data: {} }));
663  * Sets the third party authentication token and shared secret.
665  * @param {remoting.ThirdPartyToken} token
666  * @private
667  */
668 remoting.ClientPluginImpl.prototype.onThirdPartyTokenFetched_ = function(
669     token) {
670   this.plugin_.postMessage(JSON.stringify(
671     { method: 'onThirdPartyTokenFetched',
672       data: { token: token.token, sharedSecret: token.secret}}));
676  * Request pairing with the host for PIN-less authentication.
678  * @param {string} clientName The human-readable name of the client.
679  * @param {function(string, string):void} onDone, Callback to receive the
680  *     client id and shared secret when they are available.
681  */
682 remoting.ClientPluginImpl.prototype.requestPairing =
683     function(clientName, onDone) {
684   this.onPairingComplete_ = onDone;
685   this.plugin_.postMessage(JSON.stringify(
686       { method: 'requestPairing', data: { clientName: clientName } }));
690  * Send an extension message to the host.
692  * @param {string} type The message type.
693  * @param {string} message The message payload.
694  * @private
695  */
696 remoting.ClientPluginImpl.prototype.sendClientMessage_ =
697     function(type, message) {
698   this.plugin_.postMessage(JSON.stringify(
699       { method: 'extensionMessage',
700         data: { type: type, data: message } }));
704 remoting.ClientPluginImpl.prototype.hostDesktop = function() {
705   return this.hostDesktop_;
708 remoting.ClientPluginImpl.prototype.extensions = function() {
709   return this.extensions_;
713  * Callback passed to submodules to post a message to the plugin.
715  * @param {Object} message
716  * @private
717  */
718 remoting.ClientPluginImpl.prototype.postMessage_ = function(message) {
719   if (this.plugin_ && this.plugin_.postMessage) {
720     this.plugin_.postMessage(JSON.stringify(message));
721   }
725  * @constructor
726  * @implements {remoting.ClientPluginFactory}
727  */
728 remoting.DefaultClientPluginFactory = function() {};
731  * @param {Element} container
732  * @param {Array<string>} capabilities
733  * @return {remoting.ClientPlugin}
734  */
735 remoting.DefaultClientPluginFactory.prototype.createPlugin =
736     function(container, capabilities) {
737   return new remoting.ClientPluginImpl(container,
738                                        capabilities);
741 remoting.DefaultClientPluginFactory.prototype.preloadPlugin = function() {
742   var plugin = remoting.ClientPluginImpl.createPluginElement_();
743   plugin.addEventListener(
744       'loadend', function() { document.body.removeChild(plugin); }, false);
745   document.body.appendChild(plugin);