blob: 8e0348ce1ad0552c5615c10bb62fee1dbc55c90c [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 || {};
/**
* @type {string} The host configuration which will be sent to host to control
* its experiment behavior. We do not have a short term plan to support host
* experiment in WebApp, so this variable can only be controlled by
* developer console, and it's for debugging purpose only.
*/
remoting.hostConfiguration = '';
/** @constructor */
remoting.ClientPluginMessage = function() {
/** @type {string} */
this.method = '';
/** @type {Object<*>} */
this.data = {};
};
/**
* @param {Element} container The container for the embed element.
* @param {Array<string>} capabilities The set of capabilties that the
* session must support for this application.
* @constructor
* @implements {remoting.ClientPlugin}
*/
remoting.ClientPluginImpl = function(container, capabilities) {
// TODO(kelvinp): Hack to remove all plugin elements as our current code does
// not handle connection cancellation properly.
container.innerText = '';
this.plugin_ = remoting.ClientPluginImpl.createPluginElement_();
this.plugin_.id = 'session-client-plugin';
container.appendChild(this.plugin_);
/** @private {Array<string>} */
this.capabilities_ = capabilities;
/** @private {remoting.ClientSession} */
this.connectionEventHandler_ = null;
/** @private {?function(string, number, number)} */
this.updateMouseCursorImage_ = base.doNothing;
/** @private {?function(string, string)} */
this.updateClipboardData_ = base.doNothing;
/** @private {?function(string)} */
this.onCastExtensionHandler_ = base.doNothing;
/** @private {?function({rects:Array<Array<number>>}):void} */
this.debugRegionHandler_ = null;
/** @private {number} */
this.pluginApiVersion_ = -1;
/** @private {Array<string>} */
this.pluginApiFeatures_ = [];
/** @private {number} */
this.pluginApiMinVersion_ = -1;
/**
* Capabilities that are negotiated between the client and the host.
* @private {Array<remoting.ClientSession.Capability>}
*/
this.hostCapabilities_ = null;
/** @private {base.Deferred} */
this.onInitializedDeferred_ = new base.Deferred();
/** @private {function(string, string):void} */
this.onPairingComplete_ = function(clientId, sharedSecret) {};
/** @private {remoting.ClientSession.PerfStats} */
this.perfStats_ = new remoting.ClientSession.PerfStats();
/** @type {remoting.ClientPluginImpl} */
var that = this;
this.eventHooks_ = new base.Disposables(
new base.DomEventHook(
this.plugin_, 'message', this.handleMessage_.bind(this), false),
new base.DomEventHook(
this.plugin_, 'crash', this.onPluginCrashed_.bind(this), false),
new base.DomEventHook(
this.plugin_, 'error', this.onPluginLoadError_.bind(this), false));
/** @private */
this.hostDesktop_ = new remoting.ClientPlugin.HostDesktopImpl(
this, this.postMessage_.bind(this));
/** @private */
this.extensions_ = new remoting.ProtocolExtensionManager(
this.sendClientMessage_.bind(this));
/** @private {remoting.CredentialsProvider} */
this.credentials_ = null;
/** @private {!Object} */
this.keyRemappings_ = {};
};
/**
* Creates plugin element without adding it to a container.
*
* @return {HTMLEmbedElement} Plugin element
*/
remoting.ClientPluginImpl.createPluginElement_ = function() {
var plugin =
/** @type {HTMLEmbedElement} */ (document.createElement('embed'));
plugin.src = 'remoting_client_pnacl.nmf';
plugin.type = 'application/x-pnacl';
plugin.width = '0';
plugin.height = '0';
plugin.tabIndex = 0; // Required, otherwise focus() doesn't work.
return plugin;
};
/**
* @param {remoting.ClientPlugin.ConnectionEventHandler} handler
*/
remoting.ClientPluginImpl.prototype.setConnectionEventHandler =
function(handler) {
this.connectionEventHandler_ = handler;
};
/**
* @param {function(string, number, number):void} handler
*/
remoting.ClientPluginImpl.prototype.setMouseCursorHandler = function(handler) {
this.updateMouseCursorImage_ = handler;
};
/**
* @param {function(string, string):void} handler
*/
remoting.ClientPluginImpl.prototype.setClipboardHandler = function(handler) {
this.updateClipboardData_ = handler;
};
/**
* @param {?function({rects:Array<Array<number>>}):void} handler
*/
remoting.ClientPluginImpl.prototype.setDebugDirtyRegionHandler =
function(handler) {
this.debugRegionHandler_ = handler;
this.plugin_.postMessage(JSON.stringify(
{ method: 'enableDebugRegion', data: { enable: handler != null } }));
};
/**
* @param {Event} event Message from the plugin.
* @private
*/
remoting.ClientPluginImpl.prototype.handleMessage_ = function(event) {
var rawMessage =
/** @type {remoting.ClientPluginMessage|string} */ (event.data);
var message =
/** @type {remoting.ClientPluginMessage} */
((typeof(rawMessage) == 'string') ? base.jsonParseSafe(rawMessage)
: rawMessage);
if (!message || !('method' in message) || !('data' in message)) {
console.error('Received invalid message from the plugin:', rawMessage);
return;
}
try {
this.handleMessageMethod_(message);
} catch(/** @type {*} */ e) {
console.error(e);
}
};
/** @private */
remoting.ClientPluginImpl.prototype.onPluginCrashed_ = function(event) {
// If the plugin is initialized, there should be a connection event handler
// and we should report the crash through it. Otherwise, we should reject the
// initialization promise.
if (this.connectionEventHandler_) {
this.connectionEventHandler_.onConnectionStatusUpdate(
remoting.ClientSession.State.FAILED,
remoting.ClientSession.ConnectionError.NACL_PLUGIN_CRASHED);
} else {
this.onInitializedDeferred_.reject(
new remoting.Error(remoting.Error.Tag.NACL_PLUGIN_CRASHED));
}
console.error('NaCl Module crashed. ');
};
/** @private */
remoting.ClientPluginImpl.prototype.onPluginLoadError_ = function() {
console.error('Failed to load plugin : ' + this.plugin_.lastError);
this.onInitializedDeferred_.reject(
new remoting.Error(
remoting.Error.Tag.MISSING_PLUGIN, this.plugin_.lastError));
};
/**
* @param {remoting.ClientPluginMessage}
* message Parsed message from the plugin.
* @private
*/
remoting.ClientPluginImpl.prototype.handleMessageMethod_ = function(message) {
/**
* Splits a string into a list of words delimited by spaces.
* @param {string} str String that should be split.
* @return {!Array<string>} List of words.
*/
var tokenize = function(str) {
/** @type {Array<string>} */
var tokens = str.match(/\S+/g);
return tokens ? tokens : [];
};
if (this.connectionEventHandler_) {
var handler = this.connectionEventHandler_;
if (message.method == 'sendOutgoingIq') {
handler.onOutgoingIq(base.getStringAttr(message.data, 'iq'));
} else if (message.method == 'onConnectionStatus') {
var stateString = base.getStringAttr(message.data, 'state');
var state = remoting.ClientSession.State.fromString(stateString);
var error = remoting.ClientSession.ConnectionError.fromString(
base.getStringAttr(message.data, 'error'));
// Delay firing the CONNECTED event until the capabilities are negotiated,
// TODO(kelvinp): Fix the client plugin to fire capabilities and the
// connected event in the same message.
if (state === remoting.ClientSession.State.CONNECTED) {
console.assert(this.hostCapabilities_ === null,
'Capabilities should only be set after the session is connected');
return;
}
handler.onConnectionStatusUpdate(state, error);
} else if (message.method == 'onRouteChanged') {
var channel = base.getStringAttr(message.data, 'channel');
var connectionType = base.getStringAttr(message.data, 'connectionType');
handler.onRouteChanged(channel, connectionType);
} else if (message.method == 'onConnectionReady') {
var ready = base.getBooleanAttr(message.data, 'ready');
handler.onConnectionReady(ready);
} else if (message.method == 'setCapabilities') {
var capabilityString = base.getStringAttr(message.data, 'capabilities');
console.log('plugin: setCapabilities: [' + capabilityString + ']');
console.assert(this.hostCapabilities_ === null,
'setCapabilities() should only be called once.');
this.hostCapabilities_ = tokenize(capabilityString);
handler.onConnectionStatusUpdate(
remoting.ClientSession.State.CONNECTED,
remoting.ClientSession.ConnectionError.NONE);
this.extensions_.start();
} else if (message.method == 'onFirstFrameReceived') {
handler.onFirstFrameReceived();
} else if (message.method == 'networkInfo') {
handler.getLogger().setNetworkInterfaceCount(
base.getNumberAttr(message.data, 'interfaceCount'));
}
}
if (message.method == 'hello') {
this.onInitializedDeferred_.resolve();
} else if (message.method == 'onDesktopSize') {
this.hostDesktop_.onSizeUpdated(message);
} else if (message.method == 'onPerfStats') {
// Return value is ignored. These calls will throw an error if the value
// is not a number.
base.getNumberAttr(message.data, 'videoBandwidth');
base.getNumberAttr(message.data, 'videoFrameRate');
base.getNumberAttr(message.data, 'captureLatency');
base.getNumberAttr(message.data, 'maxCaptureLatency');
base.getNumberAttr(message.data, 'encodeLatency');
base.getNumberAttr(message.data, 'maxEncodeLatency');
base.getNumberAttr(message.data, 'decodeLatency');
base.getNumberAttr(message.data, 'maxDecodeLatency');
base.getNumberAttr(message.data, 'renderLatency');
base.getNumberAttr(message.data, 'maxRenderLatency');
base.getNumberAttr(message.data, 'roundtripLatency');
base.getNumberAttr(message.data, 'maxRoundtripLatency');
this.perfStats_ =
/** @type {remoting.ClientSession.PerfStats} */ (message.data);
} else if (message.method == 'injectClipboardItem') {
var mimetype = base.getStringAttr(message.data, 'mimeType');
var item = base.getStringAttr(message.data, 'item');
this.updateClipboardData_(mimetype, item);
} else if (message.method == 'fetchPin') {
// The pairingSupported value in the dictionary indicates whether both
// client and host support pairing. If the client doesn't support pairing,
// then the value won't be there at all, so give it a default of false.
var pairingSupported = base.getBooleanAttr(message.data, 'pairingSupported',
false);
this.credentials_.getPIN(pairingSupported).then(
this.onPinFetched_.bind(this)
);
} else if (message.method == 'fetchThirdPartyToken') {
var tokenUrl = base.getStringAttr(message.data, 'tokenUrl');
var hostPublicKey = base.getStringAttr(message.data, 'hostPublicKey');
var scope = base.getStringAttr(message.data, 'scope');
this.credentials_.getThirdPartyToken(tokenUrl, hostPublicKey, scope).then(
this.onThirdPartyTokenFetched_.bind(this)
);
} else if (message.method == 'pairingResponse') {
var clientId = base.getStringAttr(message.data, 'clientId');
var sharedSecret = base.getStringAttr(message.data, 'sharedSecret');
this.onPairingComplete_(clientId, sharedSecret);
} else if (message.method == 'unsetCursorShape') {
this.updateMouseCursorImage_('', 0, 0);
} else if (message.method == 'setCursorShape') {
var width = base.getNumberAttr(message.data, 'width');
var height = base.getNumberAttr(message.data, 'height');
var hotspotX = base.getNumberAttr(message.data, 'hotspotX');
var hotspotY = base.getNumberAttr(message.data, 'hotspotY');
var srcArrayBuffer = base.getObjectAttr(message.data, 'data');
var canvas =
/** @type {HTMLCanvasElement} */ (document.createElement('canvas'));
canvas.width = width;
canvas.height = height;
var context =
/** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
var imageData = context.getImageData(0, 0, width, height);
console.assert(srcArrayBuffer instanceof ArrayBuffer,
'|srcArrayBuffer| is not an ArrayBuffer.');
var src = new Uint8Array(/** @type {ArrayBuffer} */(srcArrayBuffer));
var dest = imageData.data;
for (var i = 0; i < /** @type {number} */(dest.length); i += 4) {
dest[i] = src[i + 2];
dest[i + 1] = src[i + 1];
dest[i + 2] = src[i];
dest[i + 3] = src[i + 3];
}
context.putImageData(imageData, 0, 0);
this.updateMouseCursorImage_(canvas.toDataURL(), hotspotX, hotspotY);
} else if (message.method == 'onDebugRegion') {
if (this.debugRegionHandler_) {
this.debugRegionHandler_(
/** @type {{rects: Array<(Array<number>)>}} **/(message.data));
}
} else if (message.method == 'extensionMessage') {
var extMsgType = base.getStringAttr(message.data, 'type');
var extMsgData = base.getStringAttr(message.data, 'data');
this.extensions_.onProtocolExtensionMessage(extMsgType, extMsgData);
}
};
/**
* Deletes the plugin.
*/
remoting.ClientPluginImpl.prototype.dispose = function() {
base.dispose(this.eventHooks_);
this.eventHooks_ = null;
if (this.plugin_) {
this.plugin_.parentNode.removeChild(this.plugin_);
this.plugin_ = null;
}
base.dispose(this.extensions_);
this.extensions_ = null;
this.connectionEventHandler_ = null;
};
/**
* @return {HTMLEmbedElement} HTML element that corresponds to the plugin.
*/
remoting.ClientPluginImpl.prototype.element = function() {
return this.plugin_;
};
/**
* @override {remoting.ClientPlugin}
*/
remoting.ClientPluginImpl.prototype.initialize = function() {
// If Nacl is disabled, we won't receive any error events, rejecting the
// promise immediately.
if (!base.isNaclEnabled()) {
return Promise.reject(new remoting.Error(remoting.Error.Tag.NACL_DISABLED));
}
return this.onInitializedDeferred_.promise();
};
/**
* @param {remoting.ClientSession.Capability} capability The capability to test
* for.
* @return {boolean} True if the capability has been negotiated between
* the client and host.
*/
remoting.ClientPluginImpl.prototype.hasCapability = function(capability) {
return this.hostCapabilities_ !== null &&
this.hostCapabilities_.indexOf(capability) > -1;
};
/**
* @param {string} iq Incoming IQ stanza.
*/
remoting.ClientPluginImpl.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 {remoting.Host} host The host to connect to.
* @param {string} localJid Local jid.
* @param {remoting.CredentialsProvider} credentialsProvider
*/
remoting.ClientPluginImpl.prototype.connect = function(host, localJid,
credentialsProvider) {
remoting.experiments.get().then(this.connectWithExperiments_.bind(
this, host, localJid, credentialsProvider));
};
/**
* @param {remoting.Host} host The host to connect to.
* @param {string} localJid Local jid.
* @param {remoting.CredentialsProvider} credentialsProvider
* @param {Array.<string>} experiments List of enabled experiments.
* @private
*/
remoting.ClientPluginImpl.prototype.connectWithExperiments_ = function(
host, localJid, credentialsProvider, experiments) {
var keyFilter = '';
if (remoting.platformIsMac()) {
keyFilter = 'mac';
} else if (remoting.platformIsChromeOS()) {
keyFilter = 'cros';
} else if (remoting.platformIsWindows()) {
keyFilter = 'windows';
}
this.plugin_.postMessage(JSON.stringify(
{ method: 'delegateLargeCursors', data: {} }));
this.credentials_ = credentialsProvider;
this.useAsyncPinDialog_();
this.plugin_.postMessage(JSON.stringify({
method: 'connect',
data: {
hostId: host.hostId,
hostJid: host.jabberId,
hostPublicKey: host.publicKey,
localJid: localJid,
sharedSecret: '',
capabilities: this.capabilities_.join(" "),
clientPairingId: credentialsProvider.getPairingInfo().clientId,
clientPairedSecret: credentialsProvider.getPairingInfo().sharedSecret,
keyFilter: keyFilter,
experiments: experiments.join(" "),
hostConfiguration: remoting.hostConfiguration
}
}));
};
/**
* Release all currently pressed keys.
*/
remoting.ClientPluginImpl.prototype.releaseAllKeys = function() {
this.plugin_.postMessage(JSON.stringify(
{ method: 'releaseAllKeys', data: {} }));
};
/**
* Sets and stores the key remapping setting for the current host.
*
* @param {!Object} remappings
*/
remoting.ClientPluginImpl.prototype.setRemapKeys =
function(remappings) {
// Cancel any existing remappings and apply the new ones.
this.applyRemapKeys_(this.keyRemappings_, false);
this.applyRemapKeys_(remappings, true);
this.keyRemappings_ = /** @type {!Object} */ (base.deepCopy(remappings));
};
/**
* Applies the configured key remappings to the session, or resets them.
*
* @param {!Object} remappings
* @param {boolean} apply True to apply remappings, false to cancel them.
* @private
*/
remoting.ClientPluginImpl.prototype.applyRemapKeys_ =
function(remappings, apply) {
for (var i in remappings) {
var from = parseInt(i, 10);
var to = parseInt(remappings[i], 10);
if (apply) {
console.log('remapKey 0x' + from.toString(16) + '>0x' + to.toString(16));
this.remapKey(from, to);
} else {
console.log('cancel remapKey 0x' + from.toString(16));
this.remapKey(from, from);
}
}
};
/**
* Sends a key combination to the remoting host, by sending down events for
* the given keys, followed by up events in reverse order.
*
* @param {Array<number>} keys Key codes to be sent.
* @return {void} Nothing.
*/
remoting.ClientPluginImpl.prototype.injectKeyCombination =
function(keys) {
for (var i = 0; i < keys.length; i++) {
this.injectKeyEvent(keys[i], true);
}
for (var i = 0; i < keys.length; i++) {
this.injectKeyEvent(keys[i], false);
}
};
/**
* 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.ClientPluginImpl.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.ClientPluginImpl.prototype.remapKey =
function(fromKeycode, toKeycode) {
this.plugin_.postMessage(JSON.stringify(
{ method: 'remapKey', data: {
'fromKeycode': fromKeycode,
'toKeycode': toKeycode}
}));
};
/**
* Enable/disable redirection of the specified key to the web-app.
*
* @param {number} keycode The USB-style code of the key.
* @param {Boolean} trap True to enable trapping, False to disable.
*/
remoting.ClientPluginImpl.prototype.trapKey = function(keycode, trap) {
this.plugin_.postMessage(JSON.stringify(
{ method: 'trapKey', data: {
'keycode': keycode,
'trap': trap}
}));
};
/**
* Returns an associative array with a set of stats for this connecton.
*
* @return {remoting.ClientSession.PerfStats} The connection statistics.
*/
remoting.ClientPluginImpl.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.ClientPluginImpl.prototype.sendClipboardItem =
function(mimeType, item) {
this.plugin_.postMessage(JSON.stringify(
{ method: 'sendClipboardItem',
data: { mimeType: mimeType, item: item }}));
};
/**
* Notifies the plugin whether to send touch events to the host.
*
* @param {boolean} enable True if touch events should be sent.
*/
remoting.ClientPluginImpl.prototype.enableTouchEvents = function(enable) {
this.plugin_.postMessage(
JSON.stringify({method: 'enableTouchEvents', data: {'enable': enable}}));
};
/**
* Notifies the host that the client has the specified size and pixel density.
*
* @param {number} width The available client width in DIPs.
* @param {number} height The available client height in DIPs.
* @param {number} device_scale The number of device pixels per DIP.
*/
remoting.ClientPluginImpl.prototype.notifyClientResolution =
function(width, height, device_scale) {
this.hostDesktop_.resize(width, height, device_scale);
};
/**
* Requests that the host pause or resume sending video updates.
*
* @param {boolean} pause True to suspend video updates, false otherwise.
*/
remoting.ClientPluginImpl.prototype.pauseVideo =
function(pause) {
this.plugin_.postMessage(JSON.stringify(
{ method: 'videoControl', data: { pause: pause }}));
};
/**
* Requests that the host pause or resume sending audio updates.
*
* @param {boolean} pause True to suspend audio updates, false otherwise.
*/
remoting.ClientPluginImpl.prototype.pauseAudio =
function(pause) {
this.plugin_.postMessage(JSON.stringify(
{ method: 'pauseAudio', data: { pause: pause }}));
};
/**
* Requests that the host configure the video codec for lossless encode.
*
* @param {boolean} wantLossless True to request lossless encoding.
*/
remoting.ClientPluginImpl.prototype.setLosslessEncode =
function(wantLossless) {
this.plugin_.postMessage(JSON.stringify(
{ method: 'videoControl', data: { losslessEncode: wantLossless }}));
};
/**
* Requests that the host configure the video codec for lossless color.
*
* @param {boolean} wantLossless True to request lossless color.
*/
remoting.ClientPluginImpl.prototype.setLosslessColor =
function(wantLossless) {
this.plugin_.postMessage(JSON.stringify(
{ method: 'videoControl', data: { losslessColor: wantLossless }}));
};
/**
* Called when a PIN is obtained from the user.
*
* @param {string} pin The PIN.
* @private
*/
remoting.ClientPluginImpl.prototype.onPinFetched_ =
function(pin) {
this.plugin_.postMessage(JSON.stringify(
{ method: 'onPinFetched', data: { pin: pin }}));
};
/**
* Tells the plugin to ask for the PIN asynchronously.
* @private
*/
remoting.ClientPluginImpl.prototype.useAsyncPinDialog_ =
function() {
this.plugin_.postMessage(JSON.stringify(
{ method: 'useAsyncPinDialog', data: {} }));
};
/**
* Allows automatic mouse-lock.
*/
remoting.ClientPluginImpl.prototype.allowMouseLock = function() {
this.plugin_.postMessage(JSON.stringify(
{ method: 'allowMouseLock', data: {} }));
};
/**
* Sets the third party authentication token and shared secret.
*
* @param {remoting.ThirdPartyToken} token
* @private
*/
remoting.ClientPluginImpl.prototype.onThirdPartyTokenFetched_ = function(
token) {
this.plugin_.postMessage(JSON.stringify(
{ method: 'onThirdPartyTokenFetched',
data: { token: token.token, sharedSecret: token.secret}}));
};
/**
* Request pairing with the host for PIN-less authentication.
*
* @param {string} clientName The human-readable name of the client.
* @param {function(string, string):void} onDone, Callback to receive the
* client id and shared secret when they are available.
*/
remoting.ClientPluginImpl.prototype.requestPairing =
function(clientName, onDone) {
this.onPairingComplete_ = onDone;
this.plugin_.postMessage(JSON.stringify(
{ method: 'requestPairing', data: { clientName: clientName } }));
};
/**
* Send an extension message to the host.
*
* @param {string} type The message type.
* @param {string} message The message payload.
* @private
*/
remoting.ClientPluginImpl.prototype.sendClientMessage_ =
function(type, message) {
this.plugin_.postMessage(JSON.stringify(
{ method: 'extensionMessage',
data: { type: type, data: message } }));
};
remoting.ClientPluginImpl.prototype.hostDesktop = function() {
return this.hostDesktop_;
};
remoting.ClientPluginImpl.prototype.extensions = function() {
return this.extensions_;
};
/**
* Callback passed to submodules to post a message to the plugin.
*
* @param {Object} message
* @private
*/
remoting.ClientPluginImpl.prototype.postMessage_ = function(message) {
if (this.plugin_ && this.plugin_.postMessage) {
this.plugin_.postMessage(JSON.stringify(message));
}
};
/**
* @constructor
* @implements {remoting.ClientPluginFactory}
*/
remoting.DefaultClientPluginFactory = function() {};
/**
* @param {Element} container
* @param {Array<string>} capabilities
* @return {remoting.ClientPlugin}
*/
remoting.DefaultClientPluginFactory.prototype.createPlugin =
function(container, capabilities) {
return new remoting.ClientPluginImpl(container,
capabilities);
};
remoting.DefaultClientPluginFactory.prototype.preloadPlugin = function() {
var plugin = remoting.ClientPluginImpl.createPluginElement_();
plugin.addEventListener(
'loadend', function() { document.body.removeChild(plugin); }, false);
document.body.appendChild(plugin);
};