blob: e27983ccaaf58e490155d48bb0bc794ba5050c5c [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
* Class that wraps low-level details of interacting with the client plugin.
*
* This abstracts a <embed> element and controls the plugin which does
* the actual remoting work. It also handles differences between
* client plugins versions when it is necessary.
*/
'use strict';
/** @suppress {duplicate} */
var remoting = remoting || {};
/**
* @param {remoting.ViewerPlugin} plugin The plugin embed element.
* @constructor
* @implements {remoting.ClientPlugin}
*/
remoting.ClientPluginAsync = function(plugin) {
this.plugin = plugin;
this.desktopWidth = 0;
this.desktopHeight = 0;
this.desktopXDpi = 96;
this.desktopYDpi = 96;
/** @param {string} iq The Iq stanza received from the host. */
this.onOutgoingIqHandler = function (iq) {};
/** @param {string} message Log message. */
this.onDebugMessageHandler = function (message) {};
/**
* @param {number} state The connection state.
* @param {number} error The error code, if any.
*/
this.onConnectionStatusUpdateHandler = function(state, error) {};
/** @param {boolean} ready Connection ready state. */
this.onConnectionReadyHandler = function(ready) {};
this.onDesktopSizeUpdateHandler = function () {};
/** @type {number} */
this.pluginApiVersion_ = -1;
/** @type {Array.<string>} */
this.pluginApiFeatures_ = [];
/** @type {number} */
this.pluginApiMinVersion_ = -1;
/** @type {boolean} */
this.helloReceived_ = false;
/** @type {function(boolean)|null} */
this.onInitializedCallback_ = null;
/** @type {remoting.ClientSession.PerfStats} */
this.perfStats_ = new remoting.ClientSession.PerfStats();
/** @type {remoting.ClientPluginAsync} */
var that = this;
/** @param {Event} event Message event from the plugin. */
this.plugin.addEventListener('message', function(event) {
that.handleMessage_(event.data);
}, false);
window.setTimeout(this.showPluginForClickToPlay_.bind(this), 500);
};
/**
* Chromoting session API version (for this javascript).
* This is compared with the plugin API version to verify that they are
* compatible.
*
* @const
* @private
*/
remoting.ClientPluginAsync.prototype.API_VERSION_ = 6;
/**
* The oldest API version that we support.
* This will differ from the |API_VERSION_| if we maintain backward
* compatibility with older API versions.
*
* @const
* @private
*/
remoting.ClientPluginAsync.prototype.API_MIN_VERSION_ = 5;
/**
* @param {string} messageStr Message from the plugin.
*/
remoting.ClientPluginAsync.prototype.handleMessage_ = function(messageStr) {
var message = /** @type {{method:string, data:Object.<string,string>}} */
jsonParseSafe(messageStr);
if (!message || !('method' in message) || !('data' in message)) {
console.error('Received invalid message from the plugin: ' + messageStr);
return;
}
if (message.method == 'hello') {
// Reset the size in case we had to enlarge it to support click-to-play.
this.plugin.width = 0;
this.plugin.height = 0;
if (typeof message.data['apiVersion'] != 'number' ||
typeof message.data['apiMinVersion'] != 'number') {
console.error('Received invalid hello message: ' + messageStr);
return;
}
this.pluginApiVersion_ = /** @type {number} */ message.data['apiVersion'];
if (this.pluginApiVersion_ >= 7) {
if (typeof message.data['apiFeatures'] != 'string') {
console.error('Received invalid hello message: ' + messageStr);
return;
}
this.pluginApiFeatures_ =
/** @type {Array.<string>} */ message.data['apiFeatures'].split(' ');
} else if (this.pluginApiVersion_ >= 6) {
this.pluginApiFeatures_ = ['highQualityScaling', 'injectKeyEvent'];
} else {
this.pluginApiFeatures_ = ['highQualityScaling'];
}
this.pluginApiMinVersion_ =
/** @type {number} */ message.data['apiMinVersion'];
this.helloReceived_ = true;
if (this.onInitializedCallback_ != null) {
this.onInitializedCallback_(true);
this.onInitializedCallback_ = null;
}
} else if (message.method == 'sendOutgoingIq') {
if (typeof message.data['iq'] != 'string') {
console.error('Received invalid sendOutgoingIq message: ' + messageStr);
return;
}
this.onOutgoingIqHandler(message.data['iq']);
} else if (message.method == 'logDebugMessage') {
if (typeof message.data['message'] != 'string') {
console.error('Received invalid logDebugMessage message: ' + messageStr);
return;
}
this.onDebugMessageHandler(message.data['message']);
} else if (message.method == 'onConnectionStatus') {
if (typeof message.data['state'] != 'string' ||
!(message.data['state'] in remoting.ClientSession.State) ||
typeof message.data['error'] != 'string') {
console.error('Received invalid onConnectionState message: ' +
messageStr);
return;
}
/** @type {remoting.ClientSession.State} */
var state = remoting.ClientSession.State[message.data['state']];
var error;
if (message.data['error'] in remoting.ClientSession.ConnectionError) {
error = /** @type {remoting.ClientSession.ConnectionError} */
remoting.ClientSession.ConnectionError[message.data['error']];
} else {
error = remoting.ClientSession.ConnectionError.UNKNOWN;
}
this.onConnectionStatusUpdateHandler(state, error);
} else if (message.method == 'onDesktopSize') {
if (typeof message.data['width'] != 'number' ||
typeof message.data['height'] != 'number') {
console.error('Received invalid onDesktopSize message: ' + messageStr);
return;
}
this.desktopWidth = /** @type {number} */ message.data['width'];
this.desktopHeight = /** @type {number} */ message.data['height'];
this.desktopXDpi = (typeof message.data['x_dpi'] == 'number') ?
/** @type {number} */ (message.data['x_dpi']) : 96;
this.desktopYDpi = (typeof message.data['y_dpi'] == 'number') ?
/** @type {number} */ (message.data['y_dpi']) : 96;
this.onDesktopSizeUpdateHandler();
} else if (message.method == 'onPerfStats') {
if (typeof message.data['videoBandwidth'] != 'number' ||
typeof message.data['videoFrameRate'] != 'number' ||
typeof message.data['captureLatency'] != 'number' ||
typeof message.data['encodeLatency'] != 'number' ||
typeof message.data['decodeLatency'] != 'number' ||
typeof message.data['renderLatency'] != 'number' ||
typeof message.data['roundtripLatency'] != 'number') {
console.error('Received incorrect onPerfStats message: ' + messageStr);
return;
}
this.perfStats_ =
/** @type {remoting.ClientSession.PerfStats} */ message.data;
} else if (message.method == 'injectClipboardItem') {
if (typeof message.data['mimeType'] != 'string' ||
typeof message.data['item'] != 'string') {
console.error('Received incorrect injectClipboardItem message.');
return;
}
if (remoting.clipboard) {
remoting.clipboard.fromHost(message.data['mimeType'],
message.data['item']);
}
} else if (message.method == 'onFirstFrameReceived') {
if (remoting.clientSession) {
remoting.clientSession.onFirstFrameReceived();
}
} else if (message.method == 'onConnectionReady') {
if (typeof message.data['ready'] != 'boolean') {
console.error('Received incorrect onConnectionReady message.');
return;
}
var ready = /** @type {boolean} */ message.data['ready'];
this.onConnectionReadyHandler(ready);
}
}
/**
* Deletes the plugin.
*/
remoting.ClientPluginAsync.prototype.cleanup = function() {
this.plugin.parentNode.removeChild(this.plugin);
};
/**
* @return {HTMLEmbedElement} HTML element that correspods to the plugin.
*/
remoting.ClientPluginAsync.prototype.element = function() {
return this.plugin;
};
/**
* @param {function(boolean): void} onDone
*/
remoting.ClientPluginAsync.prototype.initialize = function(onDone) {
if (this.helloReceived_) {
onDone(true);
} else {
this.onInitializedCallback_ = onDone;
}
};
/**
* @return {boolean} True if the plugin and web-app versions are compatible.
*/
remoting.ClientPluginAsync.prototype.isSupportedVersion = function() {
if (!this.helloReceived_) {
console.error(
"isSupportedVersion() is called before the plugin is initialized.");
return false;
}
return this.API_VERSION_ >= this.pluginApiMinVersion_ &&
this.pluginApiVersion_ >= this.API_MIN_VERSION_;
};
/**
* @param {remoting.ClientPlugin.Feature} feature The feature to test for.
* @return {boolean} True if the plugin supports the named feature.
*/
remoting.ClientPluginAsync.prototype.hasFeature = function(feature) {
if (!this.helloReceived_) {
console.error(
"hasFeature() is called before the plugin is initialized.");
return false;
}
return this.pluginApiFeatures_.indexOf(feature) > -1;
};
/**
* @return {boolean} True if the plugin supports the injectKeyEvent API.
*/
remoting.ClientPluginAsync.prototype.isInjectKeyEventSupported = function() {
return this.pluginApiVersion_ >= 6;
};
/**
* @param {string} iq Incoming IQ stanza.
*/
remoting.ClientPluginAsync.prototype.onIncomingIq = function(iq) {
if (this.plugin && this.plugin.postMessage) {
this.plugin.postMessage(JSON.stringify(
{ method: 'incomingIq', data: { iq: iq } }));
} else {
// plugin.onIq may not be set after the plugin has been shut
// down. Particularly this happens when we receive response to
// session-terminate stanza.
console.warn('plugin.onIq is not set so dropping incoming message.');
}
};
/**
* @param {string} hostJid The jid of the host to connect to.
* @param {string} hostPublicKey The base64 encoded version of the host's
* public key.
* @param {string} localJid Local jid.
* @param {string} sharedSecret The access code for IT2Me or the PIN
* for Me2Me.
* @param {string} authenticationMethods Comma-separated list of
* authentication methods the client should attempt to use.
* @param {string} authenticationTag A host-specific tag to mix into
* authentication hashes.
*/
remoting.ClientPluginAsync.prototype.connect = function(
hostJid, hostPublicKey, localJid, sharedSecret,
authenticationMethods, authenticationTag) {
this.plugin.postMessage(JSON.stringify(
{ method: 'connect', data: {
hostJid: hostJid,
hostPublicKey: hostPublicKey,
localJid: localJid,
sharedSecret: sharedSecret,
authenticationMethods: authenticationMethods,
authenticationTag: authenticationTag
}
}));
};
/**
* Release all currently pressed keys.
*/
remoting.ClientPluginAsync.prototype.releaseAllKeys = function() {
this.plugin.postMessage(JSON.stringify(
{ method: 'releaseAllKeys', data: {} }));
};
/**
* Send a key event to the host.
*
* @param {number} usbKeycode The USB-style code of the key to inject.
* @param {boolean} pressed True to inject a key press, False for a release.
*/
remoting.ClientPluginAsync.prototype.injectKeyEvent =
function(usbKeycode, pressed) {
this.plugin.postMessage(JSON.stringify(
{ method: 'injectKeyEvent', data: {
'usbKeycode': usbKeycode,
'pressed': pressed}
}));
};
/**
* Remap one USB keycode to another in all subsequent key events.
*
* @param {number} fromKeycode The USB-style code of the key to remap.
* @param {number} toKeycode The USB-style code to remap the key to.
*/
remoting.ClientPluginAsync.prototype.remapKey =
function(fromKeycode, toKeycode) {
this.plugin.postMessage(JSON.stringify(
{ method: 'remapKey', data: {
'fromKeycode': fromKeycode,
'toKeycode': toKeycode}
}));
};
/**
* Returns an associative array with a set of stats for this connecton.
*
* @return {remoting.ClientSession.PerfStats} The connection statistics.
*/
remoting.ClientPluginAsync.prototype.getPerfStats = function() {
return this.perfStats_;
};
/**
* Sends a clipboard item to the host.
*
* @param {string} mimeType The MIME type of the clipboard item.
* @param {string} item The clipboard item.
*/
remoting.ClientPluginAsync.prototype.sendClipboardItem =
function(mimeType, item) {
if (!this.hasFeature(remoting.ClientPlugin.Feature.SEND_CLIPBOARD_ITEM))
return;
this.plugin.postMessage(JSON.stringify(
{ method: 'sendClipboardItem',
data: { mimeType: mimeType, item: item }}));
};
/**
* Notifies the host that the client has the specified dimensions.
*
* @param {number} width The available client width.
* @param {number} height The available client height.
*/
remoting.ClientPluginAsync.prototype.notifyClientDimensions =
function(width, height) {
if (!this.hasFeature(remoting.ClientPlugin.Feature.NOTIFY_CLIENT_DIMENSIONS))
return;
this.plugin.postMessage(JSON.stringify(
{ method: 'notifyClientDimensions',
data: { width: width, height: height }}));
};
/**
* Requests that the host pause or resume sending video updates.
*
* @param {boolean} pause True to suspend video updates, false otherwise.
*/
remoting.ClientPluginAsync.prototype.pauseVideo =
function(pause) {
if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_VIDEO))
return;
this.plugin.postMessage(JSON.stringify(
{ method: 'pauseVideo', data: { pause: pause }}));
};
/**
* Requests that the host pause or resume sending audio updates.
*
* @param {boolean} pause True to suspend audio updates, false otherwise.
*/
remoting.ClientPluginAsync.prototype.pauseAudio =
function(pause) {
if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_AUDIO))
return;
this.plugin.postMessage(JSON.stringify(
{ method: 'pauseAudio', data: { pause: pause }}));
};
/**
* If we haven't yet received a "hello" message from the plugin, change its
* size so that the user can confirm it if click-to-play is enabled, or can
* see the "this plugin is disabled" message if it is actually disabled.
* @private
*/
remoting.ClientPluginAsync.prototype.showPluginForClickToPlay_ = function() {
if (!this.helloReceived_) {
var width = 200;
var height = 200;
this.plugin.width = width;
this.plugin.height = height;
// Center the plugin just underneath the "Connnecting..." dialog.
var parentNode = this.plugin.parentNode;
var dialog = document.getElementById('client-dialog');
var dialogRect = dialog.getBoundingClientRect();
parentNode.style.top = (dialogRect.bottom + 16) + 'px';
parentNode.style.left = (window.innerWidth - width) / 2 + 'px';
}
};