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
) {};
43 this.onDesktopSizeUpdateHandler = function () {};
46 this.pluginApiVersion_
= -1;
47 /** @type {Array.<string>} */
48 this.pluginApiFeatures_
= [];
50 this.pluginApiMinVersion_
= -1;
51 /** @type {boolean} */
52 this.helloReceived_
= false;
53 /** @type {function(boolean)|null} */
54 this.onInitializedCallback_
= null;
56 /** @type {remoting.ClientSession.PerfStats} */
57 this.perfStats_
= new remoting
.ClientSession
.PerfStats();
59 /** @type {remoting.ClientPluginAsync} */
61 /** @param {Event} event Message event from the plugin. */
62 this.plugin
.addEventListener('message', function(event
) {
63 that
.handleMessage_(event
.data
);
65 window
.setTimeout(this.showPluginForClickToPlay_
.bind(this), 500);
69 * Chromoting session API version (for this javascript).
70 * This is compared with the plugin API version to verify that they are
76 remoting
.ClientPluginAsync
.prototype.API_VERSION_
= 6;
79 * The oldest API version that we support.
80 * This will differ from the |API_VERSION_| if we maintain backward
81 * compatibility with older API versions.
86 remoting
.ClientPluginAsync
.prototype.API_MIN_VERSION_
= 5;
89 * @param {string} messageStr Message from the plugin.
91 remoting
.ClientPluginAsync
.prototype.handleMessage_ = function(messageStr
) {
92 var message
= /** @type {{method:string, data:Object.<string,string>}} */
93 jsonParseSafe(messageStr
);
95 if (!message
|| !('method' in message
) || !('data' in message
)) {
96 console
.error('Received invalid message from the plugin: ' + messageStr
);
100 if (message
.method
== 'hello') {
101 // Reset the size in case we had to enlarge it to support click-to-play.
102 this.plugin
.width
= 0;
103 this.plugin
.height
= 0;
104 if (typeof message
.data
['apiVersion'] != 'number' ||
105 typeof message
.data
['apiMinVersion'] != 'number') {
106 console
.error('Received invalid hello message: ' + messageStr
);
109 this.pluginApiVersion_
= /** @type {number} */ message
.data
['apiVersion'];
110 if (this.pluginApiVersion_
>= 7) {
111 if (typeof message
.data
['apiFeatures'] != 'string') {
112 console
.error('Received invalid hello message: ' + messageStr
);
115 this.pluginApiFeatures_
=
116 /** @type {Array.<string>} */ message
.data
['apiFeatures'].split(' ');
117 } else if (this.pluginApiVersion_
>= 6) {
118 this.pluginApiFeatures_
= ['highQualityScaling', 'injectKeyEvent'];
120 this.pluginApiFeatures_
= ['highQualityScaling'];
122 this.pluginApiMinVersion_
=
123 /** @type {number} */ message
.data
['apiMinVersion'];
124 this.helloReceived_
= true;
125 if (this.onInitializedCallback_
!= null) {
126 this.onInitializedCallback_(true);
127 this.onInitializedCallback_
= null;
129 } else if (message
.method
== 'sendOutgoingIq') {
130 if (typeof message
.data
['iq'] != 'string') {
131 console
.error('Received invalid sendOutgoingIq message: ' + messageStr
);
134 this.onOutgoingIqHandler(message
.data
['iq']);
135 } else if (message
.method
== 'logDebugMessage') {
136 if (typeof message
.data
['message'] != 'string') {
137 console
.error('Received invalid logDebugMessage message: ' + messageStr
);
140 this.onDebugMessageHandler(message
.data
['message']);
141 } else if (message
.method
== 'onConnectionStatus') {
142 if (typeof message
.data
['state'] != 'string' ||
143 !(message
.data
['state'] in remoting
.ClientSession
.State
) ||
144 typeof message
.data
['error'] != 'string') {
145 console
.error('Received invalid onConnectionState message: ' +
150 /** @type {remoting.ClientSession.State} */
151 var state
= remoting
.ClientSession
.State
[message
.data
['state']];
153 if (message
.data
['error'] in remoting
.ClientSession
.ConnectionError
) {
154 error
= /** @type {remoting.ClientSession.ConnectionError} */
155 remoting
.ClientSession
.ConnectionError
[message
.data
['error']];
157 error
= remoting
.ClientSession
.ConnectionError
.UNKNOWN
;
160 this.onConnectionStatusUpdateHandler(state
, error
);
161 } else if (message
.method
== 'onDesktopSize') {
162 if (typeof message
.data
['width'] != 'number' ||
163 typeof message
.data
['height'] != 'number') {
164 console
.error('Received invalid onDesktopSize message: ' + messageStr
);
167 this.desktopWidth
= /** @type {number} */ message
.data
['width'];
168 this.desktopHeight
= /** @type {number} */ message
.data
['height'];
169 this.desktopXDpi
= (typeof message
.data
['x_dpi'] == 'number') ?
170 /** @type {number} */ (message
.data
['x_dpi']) : 96;
171 this.desktopYDpi
= (typeof message
.data
['y_dpi'] == 'number') ?
172 /** @type {number} */ (message
.data
['y_dpi']) : 96;
173 this.onDesktopSizeUpdateHandler();
174 } else if (message
.method
== 'onPerfStats') {
175 if (typeof message
.data
['videoBandwidth'] != 'number' ||
176 typeof message
.data
['videoFrameRate'] != 'number' ||
177 typeof message
.data
['captureLatency'] != 'number' ||
178 typeof message
.data
['encodeLatency'] != 'number' ||
179 typeof message
.data
['decodeLatency'] != 'number' ||
180 typeof message
.data
['renderLatency'] != 'number' ||
181 typeof message
.data
['roundtripLatency'] != 'number') {
182 console
.error('Received incorrect onPerfStats message: ' + messageStr
);
186 /** @type {remoting.ClientSession.PerfStats} */ message
.data
;
187 } else if (message
.method
== 'injectClipboardItem') {
188 if (typeof message
.data
['mimeType'] != 'string' ||
189 typeof message
.data
['item'] != 'string') {
190 console
.error('Received incorrect injectClipboardItem message.');
193 if (remoting
.clipboard
) {
194 remoting
.clipboard
.fromHost(message
.data
['mimeType'],
195 message
.data
['item']);
197 } else if (message
.method
== 'onFirstFrameReceived') {
198 if (remoting
.clientSession
) {
199 remoting
.clientSession
.onFirstFrameReceived();
201 } else if (message
.method
== 'onConnectionReady') {
202 if (typeof message
.data
['ready'] != 'boolean') {
203 console
.error('Received incorrect onConnectionReady message.');
206 var ready
= /** @type {boolean} */ message
.data
['ready'];
207 this.onConnectionReadyHandler(ready
);
212 * Deletes the plugin.
214 remoting
.ClientPluginAsync
.prototype.cleanup = function() {
215 this.plugin
.parentNode
.removeChild(this.plugin
);
219 * @return {HTMLEmbedElement} HTML element that correspods to the plugin.
221 remoting
.ClientPluginAsync
.prototype.element = function() {
226 * @param {function(boolean): void} onDone
228 remoting
.ClientPluginAsync
.prototype.initialize = function(onDone
) {
229 if (this.helloReceived_
) {
232 this.onInitializedCallback_
= onDone
;
237 * @return {boolean} True if the plugin and web-app versions are compatible.
239 remoting
.ClientPluginAsync
.prototype.isSupportedVersion = function() {
240 if (!this.helloReceived_
) {
242 "isSupportedVersion() is called before the plugin is initialized.");
245 return this.API_VERSION_
>= this.pluginApiMinVersion_
&&
246 this.pluginApiVersion_
>= this.API_MIN_VERSION_
;
250 * @param {remoting.ClientPlugin.Feature} feature The feature to test for.
251 * @return {boolean} True if the plugin supports the named feature.
253 remoting
.ClientPluginAsync
.prototype.hasFeature = function(feature
) {
254 if (!this.helloReceived_
) {
256 "hasFeature() is called before the plugin is initialized.");
259 return this.pluginApiFeatures_
.indexOf(feature
) > -1;
263 * @return {boolean} True if the plugin supports the injectKeyEvent API.
265 remoting
.ClientPluginAsync
.prototype.isInjectKeyEventSupported = function() {
266 return this.pluginApiVersion_
>= 6;
270 * @param {string} iq Incoming IQ stanza.
272 remoting
.ClientPluginAsync
.prototype.onIncomingIq = function(iq
) {
273 if (this.plugin
&& this.plugin
.postMessage
) {
274 this.plugin
.postMessage(JSON
.stringify(
275 { method
: 'incomingIq', data
: { iq
: iq
} }));
277 // plugin.onIq may not be set after the plugin has been shut
278 // down. Particularly this happens when we receive response to
279 // session-terminate stanza.
280 console
.warn('plugin.onIq is not set so dropping incoming message.');
285 * @param {string} hostJid The jid of the host to connect to.
286 * @param {string} hostPublicKey The base64 encoded version of the host's
288 * @param {string} localJid Local jid.
289 * @param {string} sharedSecret The access code for IT2Me or the PIN
291 * @param {string} authenticationMethods Comma-separated list of
292 * authentication methods the client should attempt to use.
293 * @param {string} authenticationTag A host-specific tag to mix into
294 * authentication hashes.
296 remoting
.ClientPluginAsync
.prototype.connect = function(
297 hostJid
, hostPublicKey
, localJid
, sharedSecret
,
298 authenticationMethods
, authenticationTag
) {
299 this.plugin
.postMessage(JSON
.stringify(
300 { method
: 'connect', data
: {
302 hostPublicKey
: hostPublicKey
,
304 sharedSecret
: sharedSecret
,
305 authenticationMethods
: authenticationMethods
,
306 authenticationTag
: authenticationTag
312 * Release all currently pressed keys.
314 remoting
.ClientPluginAsync
.prototype.releaseAllKeys = function() {
315 this.plugin
.postMessage(JSON
.stringify(
316 { method
: 'releaseAllKeys', data
: {} }));
320 * Send a key event to the host.
322 * @param {number} usbKeycode The USB-style code of the key to inject.
323 * @param {boolean} pressed True to inject a key press, False for a release.
325 remoting
.ClientPluginAsync
.prototype.injectKeyEvent
=
326 function(usbKeycode
, pressed
) {
327 this.plugin
.postMessage(JSON
.stringify(
328 { method
: 'injectKeyEvent', data
: {
329 'usbKeycode': usbKeycode
,
335 * Remap one USB keycode to another in all subsequent key events.
337 * @param {number} fromKeycode The USB-style code of the key to remap.
338 * @param {number} toKeycode The USB-style code to remap the key to.
340 remoting
.ClientPluginAsync
.prototype.remapKey
=
341 function(fromKeycode
, toKeycode
) {
342 this.plugin
.postMessage(JSON
.stringify(
343 { method
: 'remapKey', data
: {
344 'fromKeycode': fromKeycode
,
345 'toKeycode': toKeycode
}
350 * Returns an associative array with a set of stats for this connecton.
352 * @return {remoting.ClientSession.PerfStats} The connection statistics.
354 remoting
.ClientPluginAsync
.prototype.getPerfStats = function() {
355 return this.perfStats_
;
359 * Sends a clipboard item to the host.
361 * @param {string} mimeType The MIME type of the clipboard item.
362 * @param {string} item The clipboard item.
364 remoting
.ClientPluginAsync
.prototype.sendClipboardItem
=
365 function(mimeType
, item
) {
366 if (!this.hasFeature(remoting
.ClientPlugin
.Feature
.SEND_CLIPBOARD_ITEM
))
368 this.plugin
.postMessage(JSON
.stringify(
369 { method
: 'sendClipboardItem',
370 data
: { mimeType
: mimeType
, item
: item
}}));
374 * Notifies the host that the client has the specified dimensions.
376 * @param {number} width The available client width.
377 * @param {number} height The available client height.
379 remoting
.ClientPluginAsync
.prototype.notifyClientDimensions
=
380 function(width
, height
) {
381 if (!this.hasFeature(remoting
.ClientPlugin
.Feature
.NOTIFY_CLIENT_DIMENSIONS
))
383 this.plugin
.postMessage(JSON
.stringify(
384 { method
: 'notifyClientDimensions',
385 data
: { width
: width
, height
: height
}}));
389 * Requests that the host pause or resume sending video updates.
391 * @param {boolean} pause True to suspend video updates, false otherwise.
393 remoting
.ClientPluginAsync
.prototype.pauseVideo
=
395 if (!this.hasFeature(remoting
.ClientPlugin
.Feature
.PAUSE_VIDEO
))
397 this.plugin
.postMessage(JSON
.stringify(
398 { method
: 'pauseVideo', data
: { pause
: pause
}}));
402 * Requests that the host pause or resume sending audio updates.
404 * @param {boolean} pause True to suspend audio updates, false otherwise.
406 remoting
.ClientPluginAsync
.prototype.pauseAudio
=
408 if (!this.hasFeature(remoting
.ClientPlugin
.Feature
.PAUSE_AUDIO
))
410 this.plugin
.postMessage(JSON
.stringify(
411 { method
: 'pauseAudio', data
: { pause
: pause
}}));
415 * If we haven't yet received a "hello" message from the plugin, change its
416 * size so that the user can confirm it if click-to-play is enabled, or can
417 * see the "this plugin is disabled" message if it is actually disabled.
420 remoting
.ClientPluginAsync
.prototype.showPluginForClickToPlay_ = function() {
421 if (!this.helloReceived_
) {
424 this.plugin
.width
= width
;
425 this.plugin
.height
= height
;
426 // Center the plugin just underneath the "Connnecting..." dialog.
427 var parentNode
= this.plugin
.parentNode
;
428 var dialog
= document
.getElementById('client-dialog');
429 var dialogRect
= dialog
.getBoundingClientRect();
430 parentNode
.style
.top
= (dialogRect
.bottom
+ 16) + 'px';
431 parentNode
.style
.left
= (window
.innerWidth
- width
) / 2 + 'px';