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 that wraps low-level details of interacting with the client plugin.
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.
16 /** @suppress {duplicate} */
17 var remoting
= remoting
|| {};
20 remoting
.ClientPluginMessage = function() {
24 /** @type {Object<*>} */
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.
33 * @implements {remoting.ClientPlugin}
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;
65 * Capabilities that are negotiated between the client and the host.
66 * @private {Array<remoting.ClientSession.Capability>}
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} */
80 this.plugin_
.addEventListener('message',
81 /** @param {Event} event Message event from the plugin. */
84 /** @type {remoting.ClientPluginMessage} */ (event
.data
));
88 this.hostDesktop_
= new remoting
.ClientPlugin
.HostDesktopImpl(
89 this, this.postMessage_
.bind(this));
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
107 remoting
.ClientPluginImpl
.createPluginElement_ = function() {
109 /** @type {HTMLEmbedElement} */ (document
.createElement('embed'));
110 plugin
.src
= 'remoting_client_pnacl.nmf';
111 plugin
.type
= 'application/x-pnacl';
114 plugin
.tabIndex
= 0; // Required, otherwise focus() doesn't work.
119 * @param {remoting.ClientPlugin.ConnectionEventHandler} handler
121 remoting
.ClientPluginImpl
.prototype.setConnectionEventHandler
=
123 this.connectionEventHandler_
= handler
;
127 * @param {function(string, number, number):void} handler
129 remoting
.ClientPluginImpl
.prototype.setMouseCursorHandler = function(handler
) {
130 this.updateMouseCursorImage_
= handler
;
134 * @param {function(string, string):void} handler
136 remoting
.ClientPluginImpl
.prototype.setClipboardHandler = function(handler
) {
137 this.updateClipboardData_
= handler
;
141 * @param {?function({rects:Array<Array<number>>}):void} handler
143 remoting
.ClientPluginImpl
.prototype.setDebugDirtyRegionHandler
=
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.
155 remoting
.ClientPluginImpl
.prototype.handleMessage_ = function(rawMessage
) {
157 /** @type {remoting.ClientPluginMessage} */
158 ((typeof(rawMessage
) == 'string') ? base
.jsonParseSafe(rawMessage
)
160 if (!message
|| !('method' in message
) || !('data' in message
)) {
161 console
.error('Received invalid message from the plugin:', rawMessage
);
166 this.handleMessageMethod_(message
);
167 } catch(/** @type {*} */ e
) {
173 * @param {remoting.ClientPluginMessage}
174 * message Parsed message from the plugin.
177 remoting
.ClientPluginImpl
.prototype.handleMessageMethod_ = function(message
) {
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.
183 var tokenize = function(str
) {
184 /** @type {Array<string>} */
185 var tokens
= str
.match(/\S+/g);
186 return tokens
? tokens
: [];
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');
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();
239 if (message
.method
== 'hello') {
240 this.helloReceived_
= true;
241 if (this.onInitializedCallback_
!= null) {
242 this.onInitializedCallback_(true);
243 this.onInitializedCallback_
= null;
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
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');
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',
274 this.credentials_
.getPIN(pairingSupported
).then(
275 this.onPinFetched_
.bind(this)
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)
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');
301 /** @type {HTMLCanvasElement} */ (document
.createElement('canvas'));
302 canvas
.width
= width
;
303 canvas
.height
= height
;
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];
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
));
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
);
336 * Deletes the plugin.
338 remoting
.ClientPluginImpl
.prototype.dispose = function() {
340 this.plugin_
.parentNode
.removeChild(this.plugin_
);
344 base
.dispose(this.extensions_
);
345 this.extensions_
= null;
349 * @return {HTMLEmbedElement} HTML element that corresponds to the plugin.
351 remoting
.ClientPluginImpl
.prototype.element = function() {
356 * @param {function(boolean): void} onDone
358 remoting
.ClientPluginImpl
.prototype.initialize = function(onDone
) {
359 if (this.helloReceived_
) {
362 this.onInitializedCallback_
= onDone
;
367 * @param {remoting.ClientSession.Capability} capability The capability to test
369 * @return {boolean} True if the capability has been negotiated between
370 * the client and host.
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.
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
} }));
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.');
393 * @param {remoting.Host} host The host to connect to.
394 * @param {string} localJid Local jid.
395 * @param {remoting.CredentialsProvider} credentialsProvider
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.
410 remoting
.ClientPluginImpl
.prototype.connectWithExperiments_ = function(
411 host
, localJid
, credentialsProvider
, experiments
) {
413 if (remoting
.platformIsMac()) {
415 } else if (remoting
.platformIsChromeOS()) {
419 this.plugin_
.postMessage(JSON
.stringify(
420 { method
: 'delegateLargeCursors', data
: {} }));
421 this.credentials_
= credentialsProvider
;
422 this.useAsyncPinDialog_();
423 this.plugin_
.postMessage(JSON
.stringify({
426 hostJid
: host
.jabberId
,
427 hostPublicKey
: host
.publicKey
,
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(" ")
441 * Release all currently pressed keys.
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
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.
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);
474 console
.log('remapKey 0x' + from.toString(16) + '>0x' + to
.toString(16));
475 this.remapKey(from, to
);
477 console
.log('cancel remapKey 0x' + from.toString(16));
478 this.remapKey(from, from);
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.
490 remoting
.ClientPluginImpl
.prototype.injectKeyCombination
=
492 for (var i
= 0; i
< keys
.length
; i
++) {
493 this.injectKeyEvent(keys
[i
], true);
495 for (var i
= 0; i
< keys
.length
; i
++) {
496 this.injectKeyEvent(keys
[i
], false);
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.
506 remoting
.ClientPluginImpl
.prototype.injectKeyEvent
=
507 function(usbKeycode
, pressed
) {
508 this.plugin_
.postMessage(JSON
.stringify(
509 { method
: 'injectKeyEvent', data
: {
510 'usbKeycode': usbKeycode
,
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.
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
}
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.
536 remoting
.ClientPluginImpl
.prototype.trapKey = function(keycode
, trap
) {
537 this.plugin_
.postMessage(JSON
.stringify(
538 { method
: 'trapKey', data
: {
545 * Returns an associative array with a set of stats for this connecton.
547 * @return {remoting.ClientSession.PerfStats} The connection statistics.
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.
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.
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.
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.
593 remoting
.ClientPluginImpl
.prototype.pauseVideo
=
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.
604 remoting
.ClientPluginImpl
.prototype.pauseAudio
=
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.
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.
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.
638 remoting
.ClientPluginImpl
.prototype.onPinFetched_
=
640 this.plugin_
.postMessage(JSON
.stringify(
641 { method
: 'onPinFetched', data
: { pin
: pin
}}));
645 * Tells the plugin to ask for the PIN asynchronously.
648 remoting
.ClientPluginImpl
.prototype.useAsyncPinDialog_
=
650 this.plugin_
.postMessage(JSON
.stringify(
651 { method
: 'useAsyncPinDialog', data
: {} }));
655 * Allows automatic mouse-lock.
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
668 remoting
.ClientPluginImpl
.prototype.onThirdPartyTokenFetched_ = function(
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.
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.
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
718 remoting
.ClientPluginImpl
.prototype.postMessage_ = function(message
) {
719 if (this.plugin_
&& this.plugin_
.postMessage
) {
720 this.plugin_
.postMessage(JSON
.stringify(message
));
726 * @implements {remoting.ClientPluginFactory}
728 remoting
.DefaultClientPluginFactory = function() {};
731 * @param {Element} container
732 * @param {Array<string>} capabilities
733 * @return {remoting.ClientPlugin}
735 remoting
.DefaultClientPluginFactory
.prototype.createPlugin
=
736 function(container
, capabilities
) {
737 return new remoting
.ClientPluginImpl(container
,
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
);