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);