OLD | NEW |
| (Empty) |
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 /** @constructor */ | |
20 remoting.ClientPluginMessage = function() { | |
21 /** @type {string} */ | |
22 this.method = ''; | |
23 | |
24 /** @type {Object<string,*>} */ | |
25 this.data = {}; | |
26 }; | |
27 | |
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_); | |
43 | |
44 /** @private {Array<string>} */ | |
45 this.requiredCapabilities_ = requiredCapabilities; | |
46 | |
47 /** @private {remoting.ClientPlugin.ConnectionEventHandler} */ | |
48 this.connectionEventHandler_ = null; | |
49 | |
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; | |
58 | |
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(); | |
83 | |
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); | |
92 | |
93 if (remoting.settings.CLIENT_PLUGIN_TYPE == 'native') { | |
94 window.setTimeout(this.showPluginForClickToPlay_.bind(this), 500); | |
95 } | |
96 | |
97 /** @private */ | |
98 this.hostDesktop_ = new remoting.ClientPlugin.HostDesktopImpl( | |
99 this, this.postMessage_.bind(this)); | |
100 | |
101 /** @private */ | |
102 this.extensions_ = new remoting.ProtocolExtensionManager( | |
103 this.sendClientMessage_.bind(this)); | |
104 | |
105 /** @private {remoting.CredentialsProvider} */ | |
106 this.credentials_ = null; | |
107 | |
108 /** @private {string} */ | |
109 this.keyRemappings_ = ''; | |
110 }; | |
111 | |
112 /** | |
113 * Creates plugin element without adding it to a container. | |
114 * | |
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; | |
134 } | |
135 | |
136 /** | |
137 * Chromoting session API version (for this javascript). | |
138 * This is compared with the plugin API version to verify that they are | |
139 * compatible. | |
140 * | |
141 * @const | |
142 * @private | |
143 */ | |
144 remoting.ClientPluginImpl.prototype.API_VERSION_ = 6; | |
145 | |
146 /** | |
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. | |
150 * | |
151 * @const | |
152 * @private | |
153 */ | |
154 remoting.ClientPluginImpl.prototype.API_MIN_VERSION_ = 5; | |
155 | |
156 /** | |
157 * @param {remoting.ClientPlugin.ConnectionEventHandler} handler | |
158 */ | |
159 remoting.ClientPluginImpl.prototype.setConnectionEventHandler = | |
160 function(handler) { | |
161 this.connectionEventHandler_ = handler; | |
162 }; | |
163 | |
164 /** | |
165 * @param {function(string, number, number):void} handler | |
166 */ | |
167 remoting.ClientPluginImpl.prototype.setMouseCursorHandler = function(handler) { | |
168 this.updateMouseCursorImage_ = handler; | |
169 }; | |
170 | |
171 /** | |
172 * @param {function(string, string):void} handler | |
173 */ | |
174 remoting.ClientPluginImpl.prototype.setClipboardHandler = function(handler) { | |
175 this.updateClipboardData_ = handler; | |
176 }; | |
177 | |
178 /** | |
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 } })); | |
186 }; | |
187 | |
188 /** | |
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 } | |
202 | |
203 try { | |
204 this.handleMessageMethod_(message); | |
205 } catch(/** @type {*} */ e) { | |
206 console.error(e); | |
207 } | |
208 } | |
209 | |
210 /** | |
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 }; | |
226 | |
227 if (this.connectionEventHandler_) { | |
228 var handler = this.connectionEventHandler_; | |
229 | |
230 if (message.method == 'sendOutgoingIq') { | |
231 handler.onOutgoingIq(base.getStringAttr(message.data, 'iq')); | |
232 | |
233 } else if (message.method == 'logDebugMessage') { | |
234 handler.onDebugMessage(base.getStringAttr(message.data, 'message')); | |
235 | |
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')); | |
241 | |
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 base.debug.assert(this.hostCapabilities_ === null, | |
247 'Capabilities should only be set after the session is connected'); | |
248 return; | |
249 } | |
250 handler.onConnectionStatusUpdate(state, error); | |
251 | |
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); | |
256 | |
257 } else if (message.method == 'onConnectionReady') { | |
258 var ready = base.getBooleanAttr(message.data, 'ready'); | |
259 handler.onConnectionReady(ready); | |
260 | |
261 } else if (message.method == 'setCapabilities') { | |
262 var capabilityString = base.getStringAttr(message.data, 'capabilities'); | |
263 console.log('plugin: setCapabilities: [' + capabilityString + ']'); | |
264 | |
265 base.debug.assert(this.hostCapabilities_ === null, | |
266 'setCapabilities() should only be called once'); | |
267 this.hostCapabilities_ = tokenize(capabilityString); | |
268 | |
269 handler.onConnectionStatusUpdate( | |
270 remoting.ClientSession.State.CONNECTED, | |
271 remoting.ClientSession.ConnectionError.NONE); | |
272 this.extensions_.start(); | |
273 | |
274 } else if (message.method == 'onFirstFrameReceived') { | |
275 handler.onFirstFrameReceived(); | |
276 | |
277 } | |
278 } | |
279 | |
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'); | |
286 | |
287 if (this.pluginApiVersion_ >= 7) { | |
288 this.pluginApiFeatures_ = | |
289 tokenize(base.getStringAttr(message.data, 'apiFeatures')); | |
290 | |
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 } | |
315 | |
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); | |
332 | |
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); | |
337 | |
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 ); | |
347 | |
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); | |
359 | |
360 } else if (message.method == 'unsetCursorShape') { | |
361 this.updateMouseCursorImage_('', 0, 0); | |
362 | |
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'); | |
369 | |
370 var canvas = | |
371 /** @type {HTMLCanvasElement} */ (document.createElement('canvas')); | |
372 canvas.width = width; | |
373 canvas.height = height; | |
374 | |
375 var context = | |
376 /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d')); | |
377 var imageData = context.getImageData(0, 0, width, height); | |
378 base.debug.assert(srcArrayBuffer instanceof ArrayBuffer); | |
379 var src = new Uint8Array(/** @type {ArrayBuffer} */(srcArrayBuffer)); | |
380 var dest = imageData.data; | |
381 for (var i = 0; i < /** @type {number} */(dest.length); i += 4) { | |
382 dest[i] = src[i + 2]; | |
383 dest[i + 1] = src[i + 1]; | |
384 dest[i + 2] = src[i]; | |
385 dest[i + 3] = src[i + 3]; | |
386 } | |
387 | |
388 context.putImageData(imageData, 0, 0); | |
389 this.updateMouseCursorImage_(canvas.toDataURL(), hotspotX, hotspotY); | |
390 | |
391 } else if (message.method == 'onDebugRegion') { | |
392 if (this.debugRegionHandler_) { | |
393 this.debugRegionHandler_( | |
394 /** @type {{rects: Array<(Array<number>)>}} **/(message.data)); | |
395 } | |
396 } else if (message.method == 'extensionMessage') { | |
397 var extMsgType = base.getStringAttr(message.data, 'type'); | |
398 var extMsgData = base.getStringAttr(message.data, 'data'); | |
399 this.extensions_.onProtocolExtensionMessage(extMsgType, extMsgData); | |
400 | |
401 } | |
402 }; | |
403 | |
404 /** | |
405 * Deletes the plugin. | |
406 */ | |
407 remoting.ClientPluginImpl.prototype.dispose = function() { | |
408 if (this.plugin_) { | |
409 this.plugin_.parentNode.removeChild(this.plugin_); | |
410 this.plugin_ = null; | |
411 } | |
412 | |
413 base.dispose(this.extensions_); | |
414 this.extensions_ = null; | |
415 }; | |
416 | |
417 /** | |
418 * @return {HTMLEmbedElement} HTML element that corresponds to the plugin. | |
419 */ | |
420 remoting.ClientPluginImpl.prototype.element = function() { | |
421 return this.plugin_; | |
422 }; | |
423 | |
424 /** | |
425 * @param {function(boolean): void} onDone | |
426 */ | |
427 remoting.ClientPluginImpl.prototype.initialize = function(onDone) { | |
428 if (this.helloReceived_) { | |
429 onDone(true); | |
430 } else { | |
431 this.onInitializedCallback_ = onDone; | |
432 } | |
433 }; | |
434 | |
435 /** | |
436 * @return {boolean} True if the plugin and web-app versions are compatible. | |
437 */ | |
438 remoting.ClientPluginImpl.prototype.isSupportedVersion = function() { | |
439 if (!this.helloReceived_) { | |
440 console.error( | |
441 "isSupportedVersion() is called before the plugin is initialized."); | |
442 return false; | |
443 } | |
444 return this.API_VERSION_ >= this.pluginApiMinVersion_ && | |
445 this.pluginApiVersion_ >= this.API_MIN_VERSION_; | |
446 }; | |
447 | |
448 /** | |
449 * @param {remoting.ClientPlugin.Feature} feature The feature to test for. | |
450 * @return {boolean} True if the plugin supports the named feature. | |
451 */ | |
452 remoting.ClientPluginImpl.prototype.hasFeature = function(feature) { | |
453 if (!this.helloReceived_) { | |
454 console.error( | |
455 "hasFeature() is called before the plugin is initialized."); | |
456 return false; | |
457 } | |
458 return this.pluginApiFeatures_.indexOf(feature) > -1; | |
459 }; | |
460 | |
461 | |
462 /** | |
463 * @param {remoting.ClientSession.Capability} capability The capability to test | |
464 * for. | |
465 * @return {boolean} True if the capability has been negotiated between | |
466 * the client and host. | |
467 */ | |
468 remoting.ClientPluginImpl.prototype.hasCapability = function(capability) { | |
469 return this.hostCapabilities_ !== null && | |
470 this.hostCapabilities_.indexOf(capability) > -1; | |
471 }; | |
472 | |
473 /** | |
474 * @return {boolean} True if the plugin supports the injectKeyEvent API. | |
475 */ | |
476 remoting.ClientPluginImpl.prototype.isInjectKeyEventSupported = function() { | |
477 return this.pluginApiVersion_ >= 6; | |
478 }; | |
479 | |
480 /** | |
481 * @param {string} iq Incoming IQ stanza. | |
482 */ | |
483 remoting.ClientPluginImpl.prototype.onIncomingIq = function(iq) { | |
484 if (this.plugin_ && this.plugin_.postMessage) { | |
485 this.plugin_.postMessage(JSON.stringify( | |
486 { method: 'incomingIq', data: { iq: iq } })); | |
487 } else { | |
488 // plugin.onIq may not be set after the plugin has been shut | |
489 // down. Particularly this happens when we receive response to | |
490 // session-terminate stanza. | |
491 console.warn('plugin.onIq is not set so dropping incoming message.'); | |
492 } | |
493 }; | |
494 | |
495 /** | |
496 * @param {remoting.Host} host The host to connect to. | |
497 * @param {string} localJid Local jid. | |
498 * @param {remoting.CredentialsProvider} credentialsProvider | |
499 */ | |
500 remoting.ClientPluginImpl.prototype.connect = | |
501 function(host, localJid, credentialsProvider) { | |
502 var keyFilter = ''; | |
503 if (remoting.platformIsMac()) { | |
504 keyFilter = 'mac'; | |
505 } else if (remoting.platformIsChromeOS()) { | |
506 keyFilter = 'cros'; | |
507 } | |
508 | |
509 // Use PPB_VideoDecoder API only in Chrome 43 and above. It is broken in | |
510 // previous versions of Chrome, see crbug.com/459103 and crbug.com/463577 . | |
511 var enableVideoDecodeRenderer = | |
512 parseInt((remoting.getChromeVersion() || '0').split('.')[0], 10) >= 43; | |
513 this.plugin_.postMessage(JSON.stringify( | |
514 { method: 'delegateLargeCursors', data: {} })); | |
515 var methods = 'third_party,spake2_pair,spake2_hmac,spake2_plain'; | |
516 this.credentials_ = credentialsProvider; | |
517 this.useAsyncPinDialog_(); | |
518 this.plugin_.postMessage(JSON.stringify( | |
519 { method: 'connect', data: { | |
520 hostJid: host.jabberId, | |
521 hostPublicKey: host.publicKey, | |
522 localJid: localJid, | |
523 sharedSecret: '', | |
524 authenticationMethods: methods, | |
525 authenticationTag: host.hostId, | |
526 capabilities: this.capabilities_.join(" "), | |
527 clientPairingId: credentialsProvider.getPairingInfo().clientId, | |
528 clientPairedSecret: credentialsProvider.getPairingInfo().sharedSecret, | |
529 keyFilter: keyFilter, | |
530 enableVideoDecodeRenderer: enableVideoDecodeRenderer | |
531 } | |
532 })); | |
533 }; | |
534 | |
535 /** | |
536 * Release all currently pressed keys. | |
537 */ | |
538 remoting.ClientPluginImpl.prototype.releaseAllKeys = function() { | |
539 this.plugin_.postMessage(JSON.stringify( | |
540 { method: 'releaseAllKeys', data: {} })); | |
541 }; | |
542 | |
543 /** | |
544 * Sets and stores the key remapping setting for the current host. | |
545 * | |
546 * @param {string} remappings Comma separated list of key remappings. | |
547 */ | |
548 remoting.ClientPluginImpl.prototype.setRemapKeys = | |
549 function(remappings) { | |
550 // Cancel any existing remappings and apply the new ones. | |
551 this.applyRemapKeys_(this.keyRemappings_, false); | |
552 this.applyRemapKeys_(remappings, true); | |
553 this.keyRemappings_ = remappings; | |
554 }; | |
555 | |
556 /** | |
557 * Applies the configured key remappings to the session, or resets them. | |
558 * | |
559 * @param {string} remapKeys | |
560 * @param {boolean} apply True to apply remappings, false to cancel them. | |
561 * @private | |
562 */ | |
563 remoting.ClientPluginImpl.prototype.applyRemapKeys_ = | |
564 function(remapKeys, apply) { | |
565 if (remapKeys == '') { | |
566 return; | |
567 } | |
568 | |
569 var remappings = remapKeys.split(','); | |
570 for (var i = 0; i < remappings.length; ++i) { | |
571 var keyCodes = remappings[i].split('>'); | |
572 if (keyCodes.length != 2) { | |
573 console.log('bad remapKey: ' + remappings[i]); | |
574 continue; | |
575 } | |
576 var fromKey = parseInt(keyCodes[0], 0); | |
577 var toKey = parseInt(keyCodes[1], 0); | |
578 if (!fromKey || !toKey) { | |
579 console.log('bad remapKey code: ' + remappings[i]); | |
580 continue; | |
581 } | |
582 if (apply) { | |
583 console.log('remapKey 0x' + fromKey.toString(16) + | |
584 '>0x' + toKey.toString(16)); | |
585 this.remapKey(fromKey, toKey); | |
586 } else { | |
587 console.log('cancel remapKey 0x' + fromKey.toString(16)); | |
588 this.remapKey(fromKey, fromKey); | |
589 } | |
590 } | |
591 }; | |
592 | |
593 /** | |
594 * Sends a key combination to the remoting host, by sending down events for | |
595 * the given keys, followed by up events in reverse order. | |
596 * | |
597 * @param {Array<number>} keys Key codes to be sent. | |
598 * @return {void} Nothing. | |
599 */ | |
600 remoting.ClientPluginImpl.prototype.injectKeyCombination = | |
601 function(keys) { | |
602 for (var i = 0; i < keys.length; i++) { | |
603 this.injectKeyEvent(keys[i], true); | |
604 } | |
605 for (var i = 0; i < keys.length; i++) { | |
606 this.injectKeyEvent(keys[i], false); | |
607 } | |
608 }; | |
609 | |
610 /** | |
611 * Send a key event to the host. | |
612 * | |
613 * @param {number} usbKeycode The USB-style code of the key to inject. | |
614 * @param {boolean} pressed True to inject a key press, False for a release. | |
615 */ | |
616 remoting.ClientPluginImpl.prototype.injectKeyEvent = | |
617 function(usbKeycode, pressed) { | |
618 this.plugin_.postMessage(JSON.stringify( | |
619 { method: 'injectKeyEvent', data: { | |
620 'usbKeycode': usbKeycode, | |
621 'pressed': pressed} | |
622 })); | |
623 }; | |
624 | |
625 /** | |
626 * Remap one USB keycode to another in all subsequent key events. | |
627 * | |
628 * @param {number} fromKeycode The USB-style code of the key to remap. | |
629 * @param {number} toKeycode The USB-style code to remap the key to. | |
630 */ | |
631 remoting.ClientPluginImpl.prototype.remapKey = | |
632 function(fromKeycode, toKeycode) { | |
633 this.plugin_.postMessage(JSON.stringify( | |
634 { method: 'remapKey', data: { | |
635 'fromKeycode': fromKeycode, | |
636 'toKeycode': toKeycode} | |
637 })); | |
638 }; | |
639 | |
640 /** | |
641 * Enable/disable redirection of the specified key to the web-app. | |
642 * | |
643 * @param {number} keycode The USB-style code of the key. | |
644 * @param {Boolean} trap True to enable trapping, False to disable. | |
645 */ | |
646 remoting.ClientPluginImpl.prototype.trapKey = function(keycode, trap) { | |
647 this.plugin_.postMessage(JSON.stringify( | |
648 { method: 'trapKey', data: { | |
649 'keycode': keycode, | |
650 'trap': trap} | |
651 })); | |
652 }; | |
653 | |
654 /** | |
655 * Returns an associative array with a set of stats for this connecton. | |
656 * | |
657 * @return {remoting.ClientSession.PerfStats} The connection statistics. | |
658 */ | |
659 remoting.ClientPluginImpl.prototype.getPerfStats = function() { | |
660 return this.perfStats_; | |
661 }; | |
662 | |
663 /** | |
664 * Sends a clipboard item to the host. | |
665 * | |
666 * @param {string} mimeType The MIME type of the clipboard item. | |
667 * @param {string} item The clipboard item. | |
668 */ | |
669 remoting.ClientPluginImpl.prototype.sendClipboardItem = | |
670 function(mimeType, item) { | |
671 if (!this.hasFeature(remoting.ClientPlugin.Feature.SEND_CLIPBOARD_ITEM)) | |
672 return; | |
673 this.plugin_.postMessage(JSON.stringify( | |
674 { method: 'sendClipboardItem', | |
675 data: { mimeType: mimeType, item: item }})); | |
676 }; | |
677 | |
678 /** | |
679 * Notifies the host that the client has the specified size and pixel density. | |
680 * | |
681 * @param {number} width The available client width in DIPs. | |
682 * @param {number} height The available client height in DIPs. | |
683 * @param {number} device_scale The number of device pixels per DIP. | |
684 */ | |
685 remoting.ClientPluginImpl.prototype.notifyClientResolution = | |
686 function(width, height, device_scale) { | |
687 this.hostDesktop_.resize(width, height, device_scale); | |
688 }; | |
689 | |
690 /** | |
691 * Requests that the host pause or resume sending video updates. | |
692 * | |
693 * @param {boolean} pause True to suspend video updates, false otherwise. | |
694 */ | |
695 remoting.ClientPluginImpl.prototype.pauseVideo = | |
696 function(pause) { | |
697 if (this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) { | |
698 this.plugin_.postMessage(JSON.stringify( | |
699 { method: 'videoControl', data: { pause: pause }})); | |
700 } else if (this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_VIDEO)) { | |
701 this.plugin_.postMessage(JSON.stringify( | |
702 { method: 'pauseVideo', data: { pause: pause }})); | |
703 } | |
704 }; | |
705 | |
706 /** | |
707 * Requests that the host pause or resume sending audio updates. | |
708 * | |
709 * @param {boolean} pause True to suspend audio updates, false otherwise. | |
710 */ | |
711 remoting.ClientPluginImpl.prototype.pauseAudio = | |
712 function(pause) { | |
713 if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_AUDIO)) { | |
714 return; | |
715 } | |
716 this.plugin_.postMessage(JSON.stringify( | |
717 { method: 'pauseAudio', data: { pause: pause }})); | |
718 }; | |
719 | |
720 /** | |
721 * Requests that the host configure the video codec for lossless encode. | |
722 * | |
723 * @param {boolean} wantLossless True to request lossless encoding. | |
724 */ | |
725 remoting.ClientPluginImpl.prototype.setLosslessEncode = | |
726 function(wantLossless) { | |
727 if (!this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) { | |
728 return; | |
729 } | |
730 this.plugin_.postMessage(JSON.stringify( | |
731 { method: 'videoControl', data: { losslessEncode: wantLossless }})); | |
732 }; | |
733 | |
734 /** | |
735 * Requests that the host configure the video codec for lossless color. | |
736 * | |
737 * @param {boolean} wantLossless True to request lossless color. | |
738 */ | |
739 remoting.ClientPluginImpl.prototype.setLosslessColor = | |
740 function(wantLossless) { | |
741 if (!this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) { | |
742 return; | |
743 } | |
744 this.plugin_.postMessage(JSON.stringify( | |
745 { method: 'videoControl', data: { losslessColor: wantLossless }})); | |
746 }; | |
747 | |
748 /** | |
749 * Called when a PIN is obtained from the user. | |
750 * | |
751 * @param {string} pin The PIN. | |
752 * @private | |
753 */ | |
754 remoting.ClientPluginImpl.prototype.onPinFetched_ = | |
755 function(pin) { | |
756 if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) { | |
757 return; | |
758 } | |
759 this.plugin_.postMessage(JSON.stringify( | |
760 { method: 'onPinFetched', data: { pin: pin }})); | |
761 }; | |
762 | |
763 /** | |
764 * Tells the plugin to ask for the PIN asynchronously. | |
765 * @private | |
766 */ | |
767 remoting.ClientPluginImpl.prototype.useAsyncPinDialog_ = | |
768 function() { | |
769 if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) { | |
770 return; | |
771 } | |
772 this.plugin_.postMessage(JSON.stringify( | |
773 { method: 'useAsyncPinDialog', data: {} })); | |
774 }; | |
775 | |
776 /** | |
777 * Allows automatic mouse-lock. | |
778 */ | |
779 remoting.ClientPluginImpl.prototype.allowMouseLock = function() { | |
780 this.plugin_.postMessage(JSON.stringify( | |
781 { method: 'allowMouseLock', data: {} })); | |
782 }; | |
783 | |
784 /** | |
785 * Sets the third party authentication token and shared secret. | |
786 * | |
787 * @param {remoting.ThirdPartyToken} token | |
788 * @private | |
789 */ | |
790 remoting.ClientPluginImpl.prototype.onThirdPartyTokenFetched_ = function( | |
791 token) { | |
792 this.plugin_.postMessage(JSON.stringify( | |
793 { method: 'onThirdPartyTokenFetched', | |
794 data: { token: token.token, sharedSecret: token.secret}})); | |
795 }; | |
796 | |
797 /** | |
798 * Request pairing with the host for PIN-less authentication. | |
799 * | |
800 * @param {string} clientName The human-readable name of the client. | |
801 * @param {function(string, string):void} onDone, Callback to receive the | |
802 * client id and shared secret when they are available. | |
803 */ | |
804 remoting.ClientPluginImpl.prototype.requestPairing = | |
805 function(clientName, onDone) { | |
806 if (!this.hasFeature(remoting.ClientPlugin.Feature.PINLESS_AUTH)) { | |
807 return; | |
808 } | |
809 this.onPairingComplete_ = onDone; | |
810 this.plugin_.postMessage(JSON.stringify( | |
811 { method: 'requestPairing', data: { clientName: clientName } })); | |
812 }; | |
813 | |
814 /** | |
815 * Send an extension message to the host. | |
816 * | |
817 * @param {string} type The message type. | |
818 * @param {string} message The message payload. | |
819 * @private | |
820 */ | |
821 remoting.ClientPluginImpl.prototype.sendClientMessage_ = | |
822 function(type, message) { | |
823 if (!this.hasFeature(remoting.ClientPlugin.Feature.EXTENSION_MESSAGE)) { | |
824 return; | |
825 } | |
826 this.plugin_.postMessage(JSON.stringify( | |
827 { method: 'extensionMessage', | |
828 data: { type: type, data: message } })); | |
829 | |
830 }; | |
831 | |
832 remoting.ClientPluginImpl.prototype.hostDesktop = function() { | |
833 return this.hostDesktop_; | |
834 }; | |
835 | |
836 remoting.ClientPluginImpl.prototype.extensions = function() { | |
837 return this.extensions_; | |
838 }; | |
839 | |
840 /** | |
841 * If we haven't yet received a "hello" message from the plugin, change its | |
842 * size so that the user can confirm it if click-to-play is enabled, or can | |
843 * see the "this plugin is disabled" message if it is actually disabled. | |
844 * @private | |
845 */ | |
846 remoting.ClientPluginImpl.prototype.showPluginForClickToPlay_ = function() { | |
847 if (!this.helloReceived_) { | |
848 var width = 200; | |
849 var height = 200; | |
850 this.plugin_.style.width = width + 'px'; | |
851 this.plugin_.style.height = height + 'px'; | |
852 // Center the plugin just underneath the "Connnecting..." dialog. | |
853 var dialog = document.getElementById('client-dialog'); | |
854 var dialogRect = dialog.getBoundingClientRect(); | |
855 this.plugin_.style.top = (dialogRect.bottom + 16) + 'px'; | |
856 this.plugin_.style.left = (window.innerWidth - width) / 2 + 'px'; | |
857 this.plugin_.style.position = 'fixed'; | |
858 } | |
859 }; | |
860 | |
861 /** | |
862 * Undo the CSS rules needed to make the plugin clickable for click-to-play. | |
863 * @private | |
864 */ | |
865 remoting.ClientPluginImpl.prototype.hidePluginForClickToPlay_ = function() { | |
866 this.plugin_.style.width = ''; | |
867 this.plugin_.style.height = ''; | |
868 this.plugin_.style.top = ''; | |
869 this.plugin_.style.left = ''; | |
870 this.plugin_.style.position = ''; | |
871 }; | |
872 | |
873 /** | |
874 * Callback passed to submodules to post a message to the plugin. | |
875 * | |
876 * @param {Object} message | |
877 * @private | |
878 */ | |
879 remoting.ClientPluginImpl.prototype.postMessage_ = function(message) { | |
880 if (this.plugin_ && this.plugin_.postMessage) { | |
881 this.plugin_.postMessage(JSON.stringify(message)); | |
882 } | |
883 }; | |
884 | |
885 /** | |
886 * @constructor | |
887 * @implements {remoting.ClientPluginFactory} | |
888 */ | |
889 remoting.DefaultClientPluginFactory = function() {}; | |
890 | |
891 /** | |
892 * @param {Element} container | |
893 * @param {Array<string>} requiredCapabilities | |
894 * @return {remoting.ClientPlugin} | |
895 */ | |
896 remoting.DefaultClientPluginFactory.prototype.createPlugin = | |
897 function(container, requiredCapabilities) { | |
898 return new remoting.ClientPluginImpl(container, | |
899 requiredCapabilities); | |
900 }; | |
901 | |
902 remoting.DefaultClientPluginFactory.prototype.preloadPlugin = function() { | |
903 if (remoting.settings.CLIENT_PLUGIN_TYPE != 'pnacl') { | |
904 return; | |
905 } | |
906 | |
907 var plugin = remoting.ClientPluginImpl.createPluginElement_(); | |
908 plugin.addEventListener( | |
909 'loadend', function() { document.body.removeChild(plugin); }, false); | |
910 document.body.appendChild(plugin); | |
911 }; | |
OLD | NEW |