/**
* @param {remoting.ViewerPlugin} plugin The plugin embed element.
+ * @param {function(string, string):boolean} onExtensionMessage The handler for
+ * protocol extension messages. Returns true if a message is recognized;
+ * false otherwise.
* @constructor
*/
-remoting.ClientPlugin = function(plugin) {
+remoting.ClientPlugin = function(plugin, onExtensionMessage) {
this.plugin = plugin;
+ this.onExtensionMessage_ = onExtensionMessage;
this.desktopWidth = 0;
this.desktopHeight = 0;
this.onConnectionStatusUpdateHandler = function(state, error) {};
/** @param {boolean} ready Connection ready state. */
this.onConnectionReadyHandler = function(ready) {};
+
/**
* @param {string} tokenUrl Token-request URL, received from the host.
* @param {string} hostPublicKey Public key for the host.
this.onSetCapabilitiesHandler = function (capabilities) {};
this.fetchPinHandler = function (supportsPairing) {};
+ /** @type {remoting.MediaSourceRenderer} */
+ this.mediaSourceRenderer_ = null;
+
/** @type {number} */
this.pluginApiVersion_ = -1;
/** @type {Array.<string>} */
THIRD_PARTY_AUTH: 'thirdPartyAuth',
TRAP_KEY: 'trapKey',
PINLESS_AUTH: 'pinlessAuth',
- EXTENSION_MESSAGE: 'extensionMessage'
+ EXTENSION_MESSAGE: 'extensionMessage',
+ MEDIA_SOURCE_RENDERING: 'mediaSourceRendering'
};
/**
remoting.ClientPlugin.prototype.API_MIN_VERSION_ = 5;
/**
- * @param {string} messageStr Message from the plugin.
+ * @param {string|{method:string, data:Object.<string,*>}}
+ * rawMessage Message from the plugin.
+ * @private
*/
-remoting.ClientPlugin.prototype.handleMessage_ = function(messageStr) {
- var message = /** @type {{method:string, data:Object.<string,string>}} */
- jsonParseSafe(messageStr);
+remoting.ClientPlugin.prototype.handleMessage_ = function(rawMessage) {
+ var message =
+ /** @type {{method:string, data:Object.<string,*>}} */
+ ((typeof(rawMessage) == 'string') ? jsonParseSafe(rawMessage)
+ : rawMessage);
if (!message || !('method' in message) || !('data' in message)) {
- console.error('Received invalid message from the plugin: ' + messageStr);
+ console.error('Received invalid message from the plugin:', rawMessage);
return;
}
+ try {
+ this.handleMessageMethod_(message);
+ } catch(e) {
+ console.error(/** @type {*} */ (e));
+ }
+}
+
+/**
+ * @param {{method:string, data:Object.<string,*>}}
+ * message Parsed message from the plugin.
+ * @private
+ */
+remoting.ClientPlugin.prototype.handleMessageMethod_ = function(message) {
/**
* Splits a string into a list of words delimited by spaces.
* @param {string} str String that should be split.
// 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'];
+ this.pluginApiVersion_ = getNumberAttr(message.data, 'apiVersion');
+ this.pluginApiMinVersion_ = getNumberAttr(message.data, 'apiMinVersion');
if (this.pluginApiVersion_ >= 7) {
- if (typeof message.data['apiFeatures'] != 'string') {
- console.error('Received invalid hello message: ' + messageStr);
- return;
- }
this.pluginApiFeatures_ =
- /** @type {Array.<string>} */ tokenize(message.data['apiFeatures']);
+ tokenize(getStringAttr(message.data, 'apiFeatures'));
// Negotiate capabilities.
/** @type {!Array.<string>} */
var requestedCapabilities = [];
if ('requestedCapabilities' in message.data) {
- if (typeof message.data['requestedCapabilities'] != 'string') {
- console.error('Received invalid hello message: ' + messageStr);
- return;
- }
- requestedCapabilities = tokenize(message.data['requestedCapabilities']);
+ requestedCapabilities =
+ tokenize(getStringAttr(message.data, 'requestedCapabilities'));
}
/** @type {!Array.<string>} */
var supportedCapabilities = [];
if ('supportedCapabilities' in message.data) {
- if (typeof message.data['supportedCapabilities'] != 'string') {
- console.error('Received invalid hello message: ' + messageStr);
- return;
- }
- supportedCapabilities = tokenize(message.data['supportedCapabilities']);
+ supportedCapabilities =
+ tokenize(getStringAttr(message.data, 'supportedCapabilities'));
}
// At the moment the webapp does not recognize any of
} 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' ||
- !remoting.ClientSession.State.hasOwnProperty(message.data['state']) ||
- typeof message.data['error'] != 'string') {
- console.error('Received invalid onConnectionState message: ' +
- messageStr);
- return;
- }
+ this.onOutgoingIqHandler(getStringAttr(message.data, 'iq'));
- /** @type {remoting.ClientSession.State} */
- var state = remoting.ClientSession.State[message.data['state']];
- var error;
- if (remoting.ClientSession.ConnectionError.hasOwnProperty(
- message.data['error'])) {
- error = /** @type {remoting.ClientSession.ConnectionError} */
- remoting.ClientSession.ConnectionError[message.data['error']];
- } else {
- error = remoting.ClientSession.ConnectionError.UNKNOWN;
- }
+ } else if (message.method == 'logDebugMessage') {
+ this.onDebugMessageHandler(getStringAttr(message.data, 'message'));
+ } else if (message.method == 'onConnectionStatus') {
+ var state = remoting.ClientSession.State.fromString(
+ getStringAttr(message.data, 'state'))
+ var error = remoting.ClientSession.ConnectionError.fromString(
+ getStringAttr(message.data, 'error'));
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.desktopWidth = getNumberAttr(message.data, 'width');
+ this.desktopHeight = getNumberAttr(message.data, 'height');
+ this.desktopXDpi = getNumberAttr(message.data, 'x_dpi', 96);
+ this.desktopYDpi = getNumberAttr(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;
- }
+ // Return value is ignored. These calls will throw an error if the value
+ // is not a number.
+ getNumberAttr(message.data, 'videoBandwidth');
+ getNumberAttr(message.data, 'videoFrameRate');
+ getNumberAttr(message.data, 'captureLatency');
+ getNumberAttr(message.data, 'encodeLatency');
+ getNumberAttr(message.data, 'decodeLatency');
+ getNumberAttr(message.data, 'renderLatency');
+ getNumberAttr(message.data, 'roundtripLatency');
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;
- }
+ var mimetype = getStringAttr(message.data, 'mimeType');
+ var item = getStringAttr(message.data, 'item');
if (remoting.clipboard) {
- remoting.clipboard.fromHost(message.data['mimeType'],
- message.data['item']);
+ remoting.clipboard.fromHost(mimetype, 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'];
+ var ready = getBooleanAttr(message.data, 'ready');
this.onConnectionReadyHandler(ready);
+
} 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.
- /** @type {boolean} */
- var pairingSupported = false;
- if ('pairingSupported' in message.data) {
- pairingSupported =
- /** @type {boolean} */ message.data['pairingSupported'];
- if (typeof pairingSupported != 'boolean') {
- console.error('Received incorrect fetchPin message.');
- return;
- }
- }
+ var pairingSupported = getBooleanAttr(message.data, 'pairingSupported',
+ false)
this.fetchPinHandler(pairingSupported);
- } else if (message.method == 'setCapabilities') {
- if (typeof message.data['capabilities'] != 'string') {
- console.error('Received incorrect setCapabilities message.');
- return;
- }
+ } else if (message.method == 'setCapabilities') {
/** @type {!Array.<string>} */
- var capabilities = tokenize(message.data['capabilities']);
+ var capabilities = tokenize(getStringAttr(message.data, 'capabilities'));
this.onSetCapabilitiesHandler(capabilities);
+
} else if (message.method == 'fetchThirdPartyToken') {
- if (typeof message.data['tokenUrl'] != 'string' ||
- typeof message.data['hostPublicKey'] != 'string' ||
- typeof message.data['scope'] != 'string') {
- console.error('Received incorrect fetchThirdPartyToken message.');
- return;
- }
- var tokenUrl = /** @type {string} */ message.data['tokenUrl'];
- var hostPublicKey =
- /** @type {string} */ message.data['hostPublicKey'];
- var scope = /** @type {string} */ message.data['scope'];
+ var tokenUrl = getStringAttr(message.data, 'tokenUrl');
+ var hostPublicKey = getStringAttr(message.data, 'hostPublicKey');
+ var scope = getStringAttr(message.data, 'scope');
this.fetchThirdPartyTokenHandler(tokenUrl, hostPublicKey, scope);
+
} else if (message.method == 'pairingResponse') {
- var clientId = /** @type {string} */ message.data['clientId'];
- var sharedSecret = /** @type {string} */ message.data['sharedSecret'];
- if (typeof clientId != 'string' || typeof sharedSecret != 'string') {
- console.error('Received incorrect pairingResponse message.');
- return;
- }
+ var clientId = getStringAttr(message.data, 'clientId');
+ var sharedSecret = getStringAttr(message.data, 'sharedSecret');
this.onPairingComplete_(clientId, sharedSecret);
+
} else if (message.method == 'extensionMessage') {
- if (typeof(message.data['type']) != 'string' ||
- typeof(message.data['data']) != 'string') {
- console.error('Invalid extension message:', message.data);
- return;
- }
- switch (message.data['type']) {
+ var extMsgType = getStringAttr(message, 'type');
+ var extMsgData = getStringAttr(message, 'data');
+ switch (extMsgType) {
case 'test-echo-reply':
- console.log('Got echo reply: ' + message.data['data']);
+ console.log('Got echo reply: ' + extMsgData);
break;
default:
- console.log('Unexpected message received: ' +
- message.data['type'] + ': ' + message.data['data']);
+ if (!this.onExtensionMessage_(extMsgType, extMsgData)) {
+ console.log('Unexpected message received: ' +
+ extMsgType + ': ' + extMsgData);
+ }
}
+
+ } else if (message.method == 'mediaSourceReset') {
+ if (!this.mediaSourceRenderer_) {
+ console.error('Unexpected mediaSourceReset.');
+ return;
+ }
+ this.mediaSourceRenderer_.reset(getStringAttr(message.data, 'format'))
+
+ } else if (message.method == 'mediaSourceData') {
+ if (!(message.data['buffer'] instanceof ArrayBuffer)) {
+ console.error('Invalid mediaSourceData message:', message.data);
+ return;
+ }
+ if (!this.mediaSourceRenderer_) {
+ console.error('Unexpected mediaSourceData.');
+ return;
+ }
+ this.mediaSourceRenderer_.onIncomingData(
+ (/** @type {ArrayBuffer} */ message.data['buffer']));
}
};
*/
remoting.ClientPlugin.prototype.pauseVideo =
function(pause) {
- if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_VIDEO))
+ if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_VIDEO)) {
return;
+ }
this.plugin.postMessage(JSON.stringify(
{ method: 'pauseVideo', data: { pause: pause }}));
};
*/
remoting.ClientPlugin.prototype.pauseAudio =
function(pause) {
- if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_AUDIO))
+ if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_AUDIO)) {
return;
+ }
this.plugin.postMessage(JSON.stringify(
{ method: 'pauseAudio', data: { pause: pause }}));
};
};
/**
+ * Request MediaStream-based rendering.
+ *
+ * @param {remoting.MediaSourceRenderer} mediaSourceRenderer
+ */
+remoting.ClientPlugin.prototype.enableMediaSourceRendering =
+ function(mediaSourceRenderer) {
+ if (!this.hasFeature(remoting.ClientPlugin.Feature.MEDIA_SOURCE_RENDERING)) {
+ return;
+ }
+ this.mediaSourceRenderer_ = mediaSourceRenderer;
+ this.plugin.postMessage(JSON.stringify(
+ { method: 'enableMediaSourceRendering', data: {} }));
+};
+
+/**
* 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.