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 * @param {remoting.ViewerPlugin} plugin The plugin embed element.
22 * @implements {remoting.ClientPlugin}
24 remoting
.ClientPluginAsync = function(plugin
) {
27 this.desktopWidth
= 0;
28 this.desktopHeight
= 0;
29 this.desktopXDpi
= 96;
30 this.desktopYDpi
= 96;
32 /** @param {string} iq The Iq stanza received from the host. */
33 this.onOutgoingIqHandler = function (iq
) {};
34 /** @param {string} message Log message. */
35 this.onDebugMessageHandler = function (message
) {};
37 * @param {number} state The connection state.
38 * @param {number} error The error code, if any.
40 this.onConnectionStatusUpdateHandler = function(state
, error
) {};
41 /** @param {boolean} ready Connection ready state. */
42 this.onConnectionReadyHandler = function(ready
) {};
44 * @param {string} tokenUrl Token-request URL, received from the host.
45 * @param {string} hostPublicKey Public key for the host.
46 * @param {string} scope OAuth scope to request the token for.
48 this.fetchThirdPartyTokenHandler = function(
49 tokenUrl
, hostPublicKey
, scope
) {};
50 this.onDesktopSizeUpdateHandler = function () {};
51 /** @param {!Array.<string>} capabilities The negotiated capabilities. */
52 this.onSetCapabilitiesHandler = function (capabilities
) {};
53 this.fetchPinHandler = function (supportsPairing
) {};
56 this.pluginApiVersion_
= -1;
57 /** @type {Array.<string>} */
58 this.pluginApiFeatures_
= [];
60 this.pluginApiMinVersion_
= -1;
61 /** @type {!Array.<string>} */
62 this.capabilities_
= [];
63 /** @type {boolean} */
64 this.helloReceived_
= false;
65 /** @type {function(boolean)|null} */
66 this.onInitializedCallback_
= null;
67 /** @type {function(string, string):void} */
68 this.onPairingComplete_ = function(clientId
, sharedSecret
) {};
69 /** @type {remoting.ClientSession.PerfStats} */
70 this.perfStats_
= new remoting
.ClientSession
.PerfStats();
72 /** @type {remoting.ClientPluginAsync} */
74 /** @param {Event} event Message event from the plugin. */
75 this.plugin
.addEventListener('message', function(event
) {
76 that
.handleMessage_(event
.data
);
78 window
.setTimeout(this.showPluginForClickToPlay_
.bind(this), 500);
82 * Chromoting session API version (for this javascript).
83 * This is compared with the plugin API version to verify that they are
89 remoting
.ClientPluginAsync
.prototype.API_VERSION_
= 6;
92 * The oldest API version that we support.
93 * This will differ from the |API_VERSION_| if we maintain backward
94 * compatibility with older API versions.
99 remoting
.ClientPluginAsync
.prototype.API_MIN_VERSION_
= 5;
102 * @param {string} messageStr Message from the plugin.
104 remoting
.ClientPluginAsync
.prototype.handleMessage_ = function(messageStr
) {
105 var message
= /** @type {{method:string, data:Object.<string,string>}} */
106 jsonParseSafe(messageStr
);
108 if (!message
|| !('method' in message
) || !('data' in message
)) {
109 console
.error('Received invalid message from the plugin: ' + messageStr
);
114 * Splits a string into a list of words delimited by spaces.
115 * @param {string} str String that should be split.
116 * @return {!Array.<string>} List of words.
118 var tokenize = function(str
) {
119 /** @type {Array.<string>} */
120 var tokens
= str
.match(/\S+/g);
121 return tokens
? tokens
: [];
124 if (message
.method
== 'hello') {
125 // Reset the size in case we had to enlarge it to support click-to-play.
126 this.plugin
.width
= 0;
127 this.plugin
.height
= 0;
128 if (typeof message
.data
['apiVersion'] != 'number' ||
129 typeof message
.data
['apiMinVersion'] != 'number') {
130 console
.error('Received invalid hello message: ' + messageStr
);
133 this.pluginApiVersion_
= /** @type {number} */ message
.data
['apiVersion'];
135 if (this.pluginApiVersion_
>= 7) {
136 if (typeof message
.data
['apiFeatures'] != 'string') {
137 console
.error('Received invalid hello message: ' + messageStr
);
140 this.pluginApiFeatures_
=
141 /** @type {Array.<string>} */ tokenize(message
.data
['apiFeatures']);
143 // Negotiate capabilities.
145 /** @type {!Array.<string>} */
146 var requestedCapabilities
= [];
147 if ('requestedCapabilities' in message
.data
) {
148 if (typeof message
.data
['requestedCapabilities'] != 'string') {
149 console
.error('Received invalid hello message: ' + messageStr
);
152 requestedCapabilities
= tokenize(message
.data
['requestedCapabilities']);
155 /** @type {!Array.<string>} */
156 var supportedCapabilities
= [];
157 if ('supportedCapabilities' in message
.data
) {
158 if (typeof message
.data
['supportedCapabilities'] != 'string') {
159 console
.error('Received invalid hello message: ' + messageStr
);
162 supportedCapabilities
= tokenize(message
.data
['supportedCapabilities']);
165 // At the moment the webapp does not recognize any of
166 // 'requestedCapabilities' capabilities (so they all should be disabled)
167 // and do not care about any of 'supportedCapabilities' capabilities (so
168 // they all can be enabled).
169 this.capabilities_
= supportedCapabilities
;
171 // Let the host know that the webapp can be requested to always send
172 // the client's dimensions.
173 this.capabilities_
.push(
174 remoting
.ClientSession
.Capability
.SEND_INITIAL_RESOLUTION
);
176 // Let the host know that we're interested in knowing whether or not
177 // it rate-limits desktop-resize requests.
178 this.capabilities_
.push(
179 remoting
.ClientSession
.Capability
.RATE_LIMIT_RESIZE_REQUESTS
);
180 } else if (this.pluginApiVersion_
>= 6) {
181 this.pluginApiFeatures_
= ['highQualityScaling', 'injectKeyEvent'];
183 this.pluginApiFeatures_
= ['highQualityScaling'];
185 this.pluginApiMinVersion_
=
186 /** @type {number} */ message
.data
['apiMinVersion'];
187 this.helloReceived_
= true;
188 if (this.onInitializedCallback_
!= null) {
189 this.onInitializedCallback_(true);
190 this.onInitializedCallback_
= null;
192 } else if (message
.method
== 'sendOutgoingIq') {
193 if (typeof message
.data
['iq'] != 'string') {
194 console
.error('Received invalid sendOutgoingIq message: ' + messageStr
);
197 this.onOutgoingIqHandler(message
.data
['iq']);
198 } else if (message
.method
== 'logDebugMessage') {
199 if (typeof message
.data
['message'] != 'string') {
200 console
.error('Received invalid logDebugMessage message: ' + messageStr
);
203 this.onDebugMessageHandler(message
.data
['message']);
204 } else if (message
.method
== 'onConnectionStatus') {
205 if (typeof message
.data
['state'] != 'string' ||
206 !remoting
.ClientSession
.State
.hasOwnProperty(message
.data
['state']) ||
207 typeof message
.data
['error'] != 'string') {
208 console
.error('Received invalid onConnectionState message: ' +
213 /** @type {remoting.ClientSession.State} */
214 var state
= remoting
.ClientSession
.State
[message
.data
['state']];
216 if (remoting
.ClientSession
.ConnectionError
.hasOwnProperty(
217 message
.data
['error'])) {
218 error
= /** @type {remoting.ClientSession.ConnectionError} */
219 remoting
.ClientSession
.ConnectionError
[message
.data
['error']];
221 error
= remoting
.ClientSession
.ConnectionError
.UNKNOWN
;
224 this.onConnectionStatusUpdateHandler(state
, error
);
225 } else if (message
.method
== 'onDesktopSize') {
226 if (typeof message
.data
['width'] != 'number' ||
227 typeof message
.data
['height'] != 'number') {
228 console
.error('Received invalid onDesktopSize message: ' + messageStr
);
231 this.desktopWidth
= /** @type {number} */ message
.data
['width'];
232 this.desktopHeight
= /** @type {number} */ message
.data
['height'];
233 this.desktopXDpi
= (typeof message
.data
['x_dpi'] == 'number') ?
234 /** @type {number} */ (message
.data
['x_dpi']) : 96;
235 this.desktopYDpi
= (typeof message
.data
['y_dpi'] == 'number') ?
236 /** @type {number} */ (message
.data
['y_dpi']) : 96;
237 this.onDesktopSizeUpdateHandler();
238 } else if (message
.method
== 'onPerfStats') {
239 if (typeof message
.data
['videoBandwidth'] != 'number' ||
240 typeof message
.data
['videoFrameRate'] != 'number' ||
241 typeof message
.data
['captureLatency'] != 'number' ||
242 typeof message
.data
['encodeLatency'] != 'number' ||
243 typeof message
.data
['decodeLatency'] != 'number' ||
244 typeof message
.data
['renderLatency'] != 'number' ||
245 typeof message
.data
['roundtripLatency'] != 'number') {
246 console
.error('Received incorrect onPerfStats message: ' + messageStr
);
250 /** @type {remoting.ClientSession.PerfStats} */ message
.data
;
251 } else if (message
.method
== 'injectClipboardItem') {
252 if (typeof message
.data
['mimeType'] != 'string' ||
253 typeof message
.data
['item'] != 'string') {
254 console
.error('Received incorrect injectClipboardItem message.');
257 if (remoting
.clipboard
) {
258 remoting
.clipboard
.fromHost(message
.data
['mimeType'],
259 message
.data
['item']);
261 } else if (message
.method
== 'onFirstFrameReceived') {
262 if (remoting
.clientSession
) {
263 remoting
.clientSession
.onFirstFrameReceived();
265 } else if (message
.method
== 'onConnectionReady') {
266 if (typeof message
.data
['ready'] != 'boolean') {
267 console
.error('Received incorrect onConnectionReady message.');
270 var ready
= /** @type {boolean} */ message
.data
['ready'];
271 this.onConnectionReadyHandler(ready
);
272 } else if (message
.method
== 'fetchPin') {
273 // The pairingSupported value in the dictionary indicates whether both
274 // client and host support pairing. If the client doesn't support pairing,
275 // then the value won't be there at all, so give it a default of false.
276 /** @type {boolean} */
277 var pairingSupported
= false;
278 if ('pairingSupported' in message
.data
) {
280 /** @type {boolean} */ message
.data
['pairingSupported'];
281 if (typeof pairingSupported
!= 'boolean') {
282 console
.error('Received incorrect fetchPin message.');
286 this.fetchPinHandler(pairingSupported
);
287 } else if (message
.method
== 'setCapabilities') {
288 if (typeof message
.data
['capabilities'] != 'string') {
289 console
.error('Received incorrect setCapabilities message.');
293 /** @type {!Array.<string>} */
294 var capabilities
= tokenize(message
.data
['capabilities']);
295 this.onSetCapabilitiesHandler(capabilities
);
296 } else if (message
.method
== 'fetchThirdPartyToken') {
297 if (typeof message
.data
['tokenUrl'] != 'string' ||
298 typeof message
.data
['hostPublicKey'] != 'string' ||
299 typeof message
.data
['scope'] != 'string') {
300 console
.error('Received incorrect fetchThirdPartyToken message.');
303 var tokenUrl
= /** @type {string} */ message
.data
['tokenUrl'];
305 /** @type {string} */ message
.data
['hostPublicKey'];
306 var scope
= /** @type {string} */ message
.data
['scope'];
307 this.fetchThirdPartyTokenHandler(tokenUrl
, hostPublicKey
, scope
);
308 } else if (message
.method
== 'pairingResponse') {
309 var clientId
= /** @type {string} */ message
.data
['clientId'];
310 var sharedSecret
= /** @type {string} */ message
.data
['sharedSecret'];
311 if (typeof clientId
!= 'string' || typeof sharedSecret
!= 'string') {
312 console
.error('Received incorrect pairingResponse message.');
315 this.onPairingComplete_(clientId
, sharedSecret
);
316 } else if (message
.method
== 'extensionMessage') {
317 if (typeof(message
.data
['type']) != 'string' ||
318 typeof(message
.data
['data']) != 'string') {
319 console
.error('Invalid extension message:', message
.data
);
322 switch (message
.data
['type']) {
323 case 'test-echo-reply':
324 console
.log('Got echo reply: ' + message
.data
['data']);
327 console
.log('Unexpected message received: ' +
328 message
.data
['type'] + ': ' + message
.data
['data']);
334 * Deletes the plugin.
336 remoting
.ClientPluginAsync
.prototype.cleanup = function() {
337 this.plugin
.parentNode
.removeChild(this.plugin
);
341 * @return {HTMLEmbedElement} HTML element that correspods to the plugin.
343 remoting
.ClientPluginAsync
.prototype.element = function() {
348 * @param {function(boolean): void} onDone
350 remoting
.ClientPluginAsync
.prototype.initialize = function(onDone
) {
351 if (this.helloReceived_
) {
354 this.onInitializedCallback_
= onDone
;
359 * @return {boolean} True if the plugin and web-app versions are compatible.
361 remoting
.ClientPluginAsync
.prototype.isSupportedVersion = function() {
362 if (!this.helloReceived_
) {
364 "isSupportedVersion() is called before the plugin is initialized.");
367 return this.API_VERSION_
>= this.pluginApiMinVersion_
&&
368 this.pluginApiVersion_
>= this.API_MIN_VERSION_
;
372 * @param {remoting.ClientPlugin.Feature} feature The feature to test for.
373 * @return {boolean} True if the plugin supports the named feature.
375 remoting
.ClientPluginAsync
.prototype.hasFeature = function(feature
) {
376 if (!this.helloReceived_
) {
378 "hasFeature() is called before the plugin is initialized.");
381 return this.pluginApiFeatures_
.indexOf(feature
) > -1;
385 * @return {boolean} True if the plugin supports the injectKeyEvent API.
387 remoting
.ClientPluginAsync
.prototype.isInjectKeyEventSupported = function() {
388 return this.pluginApiVersion_
>= 6;
392 * @param {string} iq Incoming IQ stanza.
394 remoting
.ClientPluginAsync
.prototype.onIncomingIq = function(iq
) {
395 if (this.plugin
&& this.plugin
.postMessage
) {
396 this.plugin
.postMessage(JSON
.stringify(
397 { method
: 'incomingIq', data
: { iq
: iq
} }));
399 // plugin.onIq may not be set after the plugin has been shut
400 // down. Particularly this happens when we receive response to
401 // session-terminate stanza.
402 console
.warn('plugin.onIq is not set so dropping incoming message.');
407 * @param {string} hostJid The jid of the host to connect to.
408 * @param {string} hostPublicKey The base64 encoded version of the host's
410 * @param {string} localJid Local jid.
411 * @param {string} sharedSecret The access code for IT2Me or the PIN
413 * @param {string} authenticationMethods Comma-separated list of
414 * authentication methods the client should attempt to use.
415 * @param {string} authenticationTag A host-specific tag to mix into
416 * authentication hashes.
417 * @param {string} clientPairingId For paired Me2Me connections, the
418 * pairing id for this client, as issued by the host.
419 * @param {string} clientPairedSecret For paired Me2Me connections, the
420 * paired secret for this client, as issued by the host.
422 remoting
.ClientPluginAsync
.prototype.connect = function(
423 hostJid
, hostPublicKey
, localJid
, sharedSecret
,
424 authenticationMethods
, authenticationTag
,
425 clientPairingId
, clientPairedSecret
) {
426 this.plugin
.postMessage(JSON
.stringify(
427 { method
: 'connect', data
: {
429 hostPublicKey
: hostPublicKey
,
431 sharedSecret
: sharedSecret
,
432 authenticationMethods
: authenticationMethods
,
433 authenticationTag
: authenticationTag
,
434 capabilities
: this.capabilities_
.join(" "),
435 clientPairingId
: clientPairingId
,
436 clientPairedSecret
: clientPairedSecret
442 * Release all currently pressed keys.
444 remoting
.ClientPluginAsync
.prototype.releaseAllKeys = function() {
445 this.plugin
.postMessage(JSON
.stringify(
446 { method
: 'releaseAllKeys', data
: {} }));
450 * Send a key event to the host.
452 * @param {number} usbKeycode The USB-style code of the key to inject.
453 * @param {boolean} pressed True to inject a key press, False for a release.
455 remoting
.ClientPluginAsync
.prototype.injectKeyEvent
=
456 function(usbKeycode
, pressed
) {
457 this.plugin
.postMessage(JSON
.stringify(
458 { method
: 'injectKeyEvent', data
: {
459 'usbKeycode': usbKeycode
,
465 * Remap one USB keycode to another in all subsequent key events.
467 * @param {number} fromKeycode The USB-style code of the key to remap.
468 * @param {number} toKeycode The USB-style code to remap the key to.
470 remoting
.ClientPluginAsync
.prototype.remapKey
=
471 function(fromKeycode
, toKeycode
) {
472 this.plugin
.postMessage(JSON
.stringify(
473 { method
: 'remapKey', data
: {
474 'fromKeycode': fromKeycode
,
475 'toKeycode': toKeycode
}
480 * Enable/disable redirection of the specified key to the web-app.
482 * @param {number} keycode The USB-style code of the key.
483 * @param {Boolean} trap True to enable trapping, False to disable.
485 remoting
.ClientPluginAsync
.prototype.trapKey = function(keycode
, trap
) {
486 this.plugin
.postMessage(JSON
.stringify(
487 { method
: 'trapKey', data
: {
494 * Returns an associative array with a set of stats for this connecton.
496 * @return {remoting.ClientSession.PerfStats} The connection statistics.
498 remoting
.ClientPluginAsync
.prototype.getPerfStats = function() {
499 return this.perfStats_
;
503 * Sends a clipboard item to the host.
505 * @param {string} mimeType The MIME type of the clipboard item.
506 * @param {string} item The clipboard item.
508 remoting
.ClientPluginAsync
.prototype.sendClipboardItem
=
509 function(mimeType
, item
) {
510 if (!this.hasFeature(remoting
.ClientPlugin
.Feature
.SEND_CLIPBOARD_ITEM
))
512 this.plugin
.postMessage(JSON
.stringify(
513 { method
: 'sendClipboardItem',
514 data
: { mimeType
: mimeType
, item
: item
}}));
518 * Notifies the host that the client has the specified size and pixel density.
520 * @param {number} width The available client width in DIPs.
521 * @param {number} height The available client height in DIPs.
522 * @param {number} device_scale The number of device pixels per DIP.
524 remoting
.ClientPluginAsync
.prototype.notifyClientResolution
=
525 function(width
, height
, device_scale
) {
526 if (this.hasFeature(remoting
.ClientPlugin
.Feature
.NOTIFY_CLIENT_RESOLUTION
)) {
527 var dpi
= Math
.floor(device_scale
* 96);
528 this.plugin
.postMessage(JSON
.stringify(
529 { method
: 'notifyClientResolution',
530 data
: { width
: Math
.floor(width
* device_scale
),
531 height
: Math
.floor(height
* device_scale
),
532 x_dpi
: dpi
, y_dpi
: dpi
}}));
537 * Requests that the host pause or resume sending video updates.
539 * @param {boolean} pause True to suspend video updates, false otherwise.
541 remoting
.ClientPluginAsync
.prototype.pauseVideo
=
543 if (!this.hasFeature(remoting
.ClientPlugin
.Feature
.PAUSE_VIDEO
))
545 this.plugin
.postMessage(JSON
.stringify(
546 { method
: 'pauseVideo', data
: { pause
: pause
}}));
550 * Requests that the host pause or resume sending audio updates.
552 * @param {boolean} pause True to suspend audio updates, false otherwise.
554 remoting
.ClientPluginAsync
.prototype.pauseAudio
=
556 if (!this.hasFeature(remoting
.ClientPlugin
.Feature
.PAUSE_AUDIO
))
558 this.plugin
.postMessage(JSON
.stringify(
559 { method
: 'pauseAudio', data
: { pause
: pause
}}));
563 * Called when a PIN is obtained from the user.
565 * @param {string} pin The PIN.
567 remoting
.ClientPluginAsync
.prototype.onPinFetched
=
569 if (!this.hasFeature(remoting
.ClientPlugin
.Feature
.ASYNC_PIN
)) {
572 this.plugin
.postMessage(JSON
.stringify(
573 { method
: 'onPinFetched', data
: { pin
: pin
}}));
577 * Tells the plugin to ask for the PIN asynchronously.
579 remoting
.ClientPluginAsync
.prototype.useAsyncPinDialog
=
581 if (!this.hasFeature(remoting
.ClientPlugin
.Feature
.ASYNC_PIN
)) {
584 this.plugin
.postMessage(JSON
.stringify(
585 { method
: 'useAsyncPinDialog', data
: {} }));
589 * Sets the third party authentication token and shared secret.
591 * @param {string} token The token received from the token URL.
592 * @param {string} sharedSecret Shared secret received from the token URL.
594 remoting
.ClientPluginAsync
.prototype.onThirdPartyTokenFetched = function(
595 token
, sharedSecret
) {
596 this.plugin
.postMessage(JSON
.stringify(
597 { method
: 'onThirdPartyTokenFetched',
598 data
: { token
: token
, sharedSecret
: sharedSecret
}}));
602 * Request pairing with the host for PIN-less authentication.
604 * @param {string} clientName The human-readable name of the client.
605 * @param {function(string, string):void} onDone, Callback to receive the
606 * client id and shared secret when they are available.
608 remoting
.ClientPluginAsync
.prototype.requestPairing
=
609 function(clientName
, onDone
) {
610 if (!this.hasFeature(remoting
.ClientPlugin
.Feature
.PINLESS_AUTH
)) {
613 this.onPairingComplete_
= onDone
;
614 this.plugin
.postMessage(JSON
.stringify(
615 { method
: 'requestPairing', data
: { clientName
: clientName
} }));
619 * Send an extension message to the host.
621 * @param {string} type The message type.
622 * @param {Object} message The message payload.
624 remoting
.ClientPluginAsync
.prototype.sendClientMessage
=
625 function(type
, message
) {
626 if (!this.hasFeature(remoting
.ClientPlugin
.Feature
.EXTENSION_MESSAGE
)) {
629 this.plugin
.postMessage(JSON
.stringify(
630 { method
: 'extensionMessage',
631 data
: { type
: type
, data
: JSON
.stringify(message
) } }));
636 * If we haven't yet received a "hello" message from the plugin, change its
637 * size so that the user can confirm it if click-to-play is enabled, or can
638 * see the "this plugin is disabled" message if it is actually disabled.
641 remoting
.ClientPluginAsync
.prototype.showPluginForClickToPlay_ = function() {
642 if (!this.helloReceived_
) {
645 this.plugin
.width
= width
;
646 this.plugin
.height
= height
;
647 // Center the plugin just underneath the "Connnecting..." dialog.
648 var parentNode
= this.plugin
.parentNode
;
649 var dialog
= document
.getElementById('client-dialog');
650 var dialogRect
= dialog
.getBoundingClientRect();
651 parentNode
.style
.top
= (dialogRect
.bottom
+ 16) + 'px';
652 parentNode
.style
.left
= (window
.innerWidth
- width
) / 2 + 'px';