Fix click-to-play positioning.
[chromium-blink-merge.git] / remoting / webapp / client_plugin.js
blob12fd0c053fbd5884405e30ce1e82a7be4d5745f4
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.
5 /**
6  * @fileoverview
7  * Class that wraps low-level details of interacting with the client plugin.
8  *
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.
12  */
14 'use strict';
16 /** @suppress {duplicate} */
17 var remoting = remoting || {};
19 /**
20  * @param {remoting.ViewerPlugin} plugin The plugin embed element.
21  * @param {function(string, string):boolean} onExtensionMessage The handler for
22  *     protocol extension messages. Returns true if a message is recognized;
23  *     false otherwise.
24  * @constructor
25  */
26 remoting.ClientPlugin = function(plugin, onExtensionMessage) {
27   this.plugin = plugin;
28   this.onExtensionMessage_ = onExtensionMessage;
30   this.desktopWidth = 0;
31   this.desktopHeight = 0;
32   this.desktopXDpi = 96;
33   this.desktopYDpi = 96;
35   /** @param {string} iq The Iq stanza received from the host. */
36   this.onOutgoingIqHandler = function (iq) {};
37   /** @param {string} message Log message. */
38   this.onDebugMessageHandler = function (message) {};
39   /**
40    * @param {number} state The connection state.
41    * @param {number} error The error code, if any.
42    */
43   this.onConnectionStatusUpdateHandler = function(state, error) {};
44   /** @param {boolean} ready Connection ready state. */
45   this.onConnectionReadyHandler = function(ready) {};
47   /**
48    * @param {string} tokenUrl Token-request URL, received from the host.
49    * @param {string} hostPublicKey Public key for the host.
50    * @param {string} scope OAuth scope to request the token for.
51    */
52   this.fetchThirdPartyTokenHandler = function(
53     tokenUrl, hostPublicKey, scope) {};
54   this.onDesktopSizeUpdateHandler = function () {};
55   /** @param {!Array.<string>} capabilities The negotiated capabilities. */
56   this.onSetCapabilitiesHandler = function (capabilities) {};
57   this.fetchPinHandler = function (supportsPairing) {};
58   /** @param {string} data Remote gnubbyd data. */
59   this.onGnubbyAuthHandler = function(data) {};
61   /** @type {remoting.MediaSourceRenderer} */
62   this.mediaSourceRenderer_ = null;
64   /** @type {number} */
65   this.pluginApiVersion_ = -1;
66   /** @type {Array.<string>} */
67   this.pluginApiFeatures_ = [];
68   /** @type {number} */
69   this.pluginApiMinVersion_ = -1;
70   /** @type {!Array.<string>} */
71   this.capabilities_ = [];
72   /** @type {boolean} */
73   this.helloReceived_ = false;
74   /** @type {function(boolean)|null} */
75   this.onInitializedCallback_ = null;
76   /** @type {function(string, string):void} */
77   this.onPairingComplete_ = function(clientId, sharedSecret) {};
78   /** @type {remoting.ClientSession.PerfStats} */
79   this.perfStats_ = new remoting.ClientSession.PerfStats();
81   /** @type {remoting.ClientPlugin} */
82   var that = this;
83   /** @param {Event} event Message event from the plugin. */
84   this.plugin.addEventListener('message', function(event) {
85       that.handleMessage_(event.data);
86     }, false);
87   window.setTimeout(this.showPluginForClickToPlay_.bind(this), 500);
90 /**
91  * Set of features for which hasFeature() can be used to test.
92  *
93  * @enum {string}
94  */
95 remoting.ClientPlugin.Feature = {
96   INJECT_KEY_EVENT: 'injectKeyEvent',
97   NOTIFY_CLIENT_RESOLUTION: 'notifyClientResolution',
98   ASYNC_PIN: 'asyncPin',
99   PAUSE_VIDEO: 'pauseVideo',
100   PAUSE_AUDIO: 'pauseAudio',
101   REMAP_KEY: 'remapKey',
102   SEND_CLIPBOARD_ITEM: 'sendClipboardItem',
103   THIRD_PARTY_AUTH: 'thirdPartyAuth',
104   TRAP_KEY: 'trapKey',
105   PINLESS_AUTH: 'pinlessAuth',
106   EXTENSION_MESSAGE: 'extensionMessage',
107   MEDIA_SOURCE_RENDERING: 'mediaSourceRendering',
108   VIDEO_CONTROL: 'videoControl'
112  * Chromoting session API version (for this javascript).
113  * This is compared with the plugin API version to verify that they are
114  * compatible.
116  * @const
117  * @private
118  */
119 remoting.ClientPlugin.prototype.API_VERSION_ = 6;
122  * The oldest API version that we support.
123  * This will differ from the |API_VERSION_| if we maintain backward
124  * compatibility with older API versions.
126  * @const
127  * @private
128  */
129 remoting.ClientPlugin.prototype.API_MIN_VERSION_ = 5;
132  * @param {string|{method:string, data:Object.<string,*>}}
133  *    rawMessage Message from the plugin.
134  * @private
135  */
136 remoting.ClientPlugin.prototype.handleMessage_ = function(rawMessage) {
137   var message =
138       /** @type {{method:string, data:Object.<string,*>}} */
139       ((typeof(rawMessage) == 'string') ? jsonParseSafe(rawMessage)
140                                         : rawMessage);
142   if (!message || !('method' in message) || !('data' in message)) {
143     console.error('Received invalid message from the plugin:', rawMessage);
144     return;
145   }
147   try {
148     this.handleMessageMethod_(message);
149   } catch(e) {
150     console.error(/** @type {*} */ (e));
151   }
155  * @param {{method:string, data:Object.<string,*>}}
156  *    message Parsed message from the plugin.
157  * @private
158  */
159 remoting.ClientPlugin.prototype.handleMessageMethod_ = function(message) {
160   /**
161    * Splits a string into a list of words delimited by spaces.
162    * @param {string} str String that should be split.
163    * @return {!Array.<string>} List of words.
164    */
165   var tokenize = function(str) {
166     /** @type {Array.<string>} */
167     var tokens = str.match(/\S+/g);
168     return tokens ? tokens : [];
169   };
171   if (message.method == 'hello') {
172     // Resize in case we had to enlarge it to support click-to-play.
173     this.hidePluginForClickToPlay_();
174     this.pluginApiVersion_ = getNumberAttr(message.data, 'apiVersion');
175     this.pluginApiMinVersion_ = getNumberAttr(message.data, 'apiMinVersion');
177     if (this.pluginApiVersion_ >= 7) {
178       this.pluginApiFeatures_ =
179           tokenize(getStringAttr(message.data, 'apiFeatures'));
181       // Negotiate capabilities.
183       /** @type {!Array.<string>} */
184       var requestedCapabilities = [];
185       if ('requestedCapabilities' in message.data) {
186         requestedCapabilities =
187             tokenize(getStringAttr(message.data, 'requestedCapabilities'));
188       }
190       /** @type {!Array.<string>} */
191       var supportedCapabilities = [];
192       if ('supportedCapabilities' in message.data) {
193         supportedCapabilities =
194             tokenize(getStringAttr(message.data, 'supportedCapabilities'));
195       }
197       // At the moment the webapp does not recognize any of
198       // 'requestedCapabilities' capabilities (so they all should be disabled)
199       // and do not care about any of 'supportedCapabilities' capabilities (so
200       // they all can be enabled).
201       this.capabilities_ = supportedCapabilities;
203       // Let the host know that the webapp can be requested to always send
204       // the client's dimensions.
205       this.capabilities_.push(
206           remoting.ClientSession.Capability.SEND_INITIAL_RESOLUTION);
208       // Let the host know that we're interested in knowing whether or not
209       // it rate-limits desktop-resize requests.
210       this.capabilities_.push(
211           remoting.ClientSession.Capability.RATE_LIMIT_RESIZE_REQUESTS);
212     } else if (this.pluginApiVersion_ >= 6) {
213       this.pluginApiFeatures_ = ['highQualityScaling', 'injectKeyEvent'];
214     } else {
215       this.pluginApiFeatures_ = ['highQualityScaling'];
216     }
217     this.helloReceived_ = true;
218     if (this.onInitializedCallback_ != null) {
219       this.onInitializedCallback_(true);
220       this.onInitializedCallback_ = null;
221     }
223   } else if (message.method == 'sendOutgoingIq') {
224     this.onOutgoingIqHandler(getStringAttr(message.data, 'iq'));
226   } else if (message.method == 'logDebugMessage') {
227     this.onDebugMessageHandler(getStringAttr(message.data, 'message'));
229   } else if (message.method == 'onConnectionStatus') {
230     var state = remoting.ClientSession.State.fromString(
231         getStringAttr(message.data, 'state'))
232     var error = remoting.ClientSession.ConnectionError.fromString(
233         getStringAttr(message.data, 'error'));
234     this.onConnectionStatusUpdateHandler(state, error);
236   } else if (message.method == 'onDesktopSize') {
237     this.desktopWidth = getNumberAttr(message.data, 'width');
238     this.desktopHeight = getNumberAttr(message.data, 'height');
239     this.desktopXDpi = getNumberAttr(message.data, 'x_dpi', 96);
240     this.desktopYDpi = getNumberAttr(message.data, 'y_dpi', 96);
241     this.onDesktopSizeUpdateHandler();
243   } else if (message.method == 'onPerfStats') {
244     // Return value is ignored. These calls will throw an error if the value
245     // is not a number.
246     getNumberAttr(message.data, 'videoBandwidth');
247     getNumberAttr(message.data, 'videoFrameRate');
248     getNumberAttr(message.data, 'captureLatency');
249     getNumberAttr(message.data, 'encodeLatency');
250     getNumberAttr(message.data, 'decodeLatency');
251     getNumberAttr(message.data, 'renderLatency');
252     getNumberAttr(message.data, 'roundtripLatency');
253     this.perfStats_ =
254         /** @type {remoting.ClientSession.PerfStats} */ message.data;
256   } else if (message.method == 'injectClipboardItem') {
257     var mimetype = getStringAttr(message.data, 'mimeType');
258     var item = getStringAttr(message.data, 'item');
259     if (remoting.clipboard) {
260       remoting.clipboard.fromHost(mimetype, item);
261     }
263   } else if (message.method == 'onFirstFrameReceived') {
264     if (remoting.clientSession) {
265       remoting.clientSession.onFirstFrameReceived();
266     }
268   } else if (message.method == 'onConnectionReady') {
269     var ready = getBooleanAttr(message.data, 'ready');
270     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     var pairingSupported = getBooleanAttr(message.data, 'pairingSupported',
277                                           false)
278     this.fetchPinHandler(pairingSupported);
280   } else if (message.method == 'setCapabilities') {
281     /** @type {!Array.<string>} */
282     var capabilities = tokenize(getStringAttr(message.data, 'capabilities'));
283     this.onSetCapabilitiesHandler(capabilities);
285   } else if (message.method == 'fetchThirdPartyToken') {
286     var tokenUrl = getStringAttr(message.data, 'tokenUrl');
287     var hostPublicKey = getStringAttr(message.data, 'hostPublicKey');
288     var scope = getStringAttr(message.data, 'scope');
289     this.fetchThirdPartyTokenHandler(tokenUrl, hostPublicKey, scope);
291   } else if (message.method == 'pairingResponse') {
292     var clientId = getStringAttr(message.data, 'clientId');
293     var sharedSecret = getStringAttr(message.data, 'sharedSecret');
294     this.onPairingComplete_(clientId, sharedSecret);
296   } else if (message.method == 'extensionMessage') {
297     var extMsgType = getStringAttr(message.data, 'type');
298     var extMsgData = getStringAttr(message.data, 'data');
299     switch (extMsgType) {
300       case 'gnubby-auth':
301         this.onGnubbyAuthHandler(extMsgData);
302         break;
303       case 'test-echo-reply':
304         console.log('Got echo reply: ' + extMsgData);
305         break;
306       default:
307         if (!this.onExtensionMessage_(extMsgType, extMsgData)) {
308           console.log('Unexpected message received: ' +
309                       extMsgType + ': ' + extMsgData);
310         }
311     }
313   } else if (message.method == 'mediaSourceReset') {
314     if (!this.mediaSourceRenderer_) {
315       console.error('Unexpected mediaSourceReset.');
316       return;
317     }
318     this.mediaSourceRenderer_.reset(getStringAttr(message.data, 'format'))
320   } else if (message.method == 'mediaSourceData') {
321     if (!(message.data['buffer'] instanceof ArrayBuffer)) {
322       console.error('Invalid mediaSourceData message:', message.data);
323       return;
324     }
325     if (!this.mediaSourceRenderer_) {
326       console.error('Unexpected mediaSourceData.');
327       return;
328     }
329     // keyframe flag may be absent from the message.
330     var keyframe = !!message.data['keyframe'];
331     this.mediaSourceRenderer_.onIncomingData(
332         (/** @type {ArrayBuffer} */ message.data['buffer']), keyframe);
333   }
337  * Deletes the plugin.
338  */
339 remoting.ClientPlugin.prototype.cleanup = function() {
340   this.plugin.parentNode.removeChild(this.plugin);
344  * @return {HTMLEmbedElement} HTML element that correspods to the plugin.
345  */
346 remoting.ClientPlugin.prototype.element = function() {
347   return this.plugin;
351  * @param {function(boolean): void} onDone
352  */
353 remoting.ClientPlugin.prototype.initialize = function(onDone) {
354   if (this.helloReceived_) {
355     onDone(true);
356   } else {
357     this.onInitializedCallback_ = onDone;
358   }
362  * @return {boolean} True if the plugin and web-app versions are compatible.
363  */
364 remoting.ClientPlugin.prototype.isSupportedVersion = function() {
365   if (!this.helloReceived_) {
366     console.error(
367         "isSupportedVersion() is called before the plugin is initialized.");
368     return false;
369   }
370   return this.API_VERSION_ >= this.pluginApiMinVersion_ &&
371       this.pluginApiVersion_ >= this.API_MIN_VERSION_;
375  * @param {remoting.ClientPlugin.Feature} feature The feature to test for.
376  * @return {boolean} True if the plugin supports the named feature.
377  */
378 remoting.ClientPlugin.prototype.hasFeature = function(feature) {
379   if (!this.helloReceived_) {
380     console.error(
381         "hasFeature() is called before the plugin is initialized.");
382     return false;
383   }
384   return this.pluginApiFeatures_.indexOf(feature) > -1;
388  * @return {boolean} True if the plugin supports the injectKeyEvent API.
389  */
390 remoting.ClientPlugin.prototype.isInjectKeyEventSupported = function() {
391   return this.pluginApiVersion_ >= 6;
395  * @param {string} iq Incoming IQ stanza.
396  */
397 remoting.ClientPlugin.prototype.onIncomingIq = function(iq) {
398   if (this.plugin && this.plugin.postMessage) {
399     this.plugin.postMessage(JSON.stringify(
400         { method: 'incomingIq', data: { iq: iq } }));
401   } else {
402     // plugin.onIq may not be set after the plugin has been shut
403     // down. Particularly this happens when we receive response to
404     // session-terminate stanza.
405     console.warn('plugin.onIq is not set so dropping incoming message.');
406   }
410  * @param {string} hostJid The jid of the host to connect to.
411  * @param {string} hostPublicKey The base64 encoded version of the host's
412  *     public key.
413  * @param {string} localJid Local jid.
414  * @param {string} sharedSecret The access code for IT2Me or the PIN
415  *     for Me2Me.
416  * @param {string} authenticationMethods Comma-separated list of
417  *     authentication methods the client should attempt to use.
418  * @param {string} authenticationTag A host-specific tag to mix into
419  *     authentication hashes.
420  * @param {string} clientPairingId For paired Me2Me connections, the
421  *     pairing id for this client, as issued by the host.
422  * @param {string} clientPairedSecret For paired Me2Me connections, the
423  *     paired secret for this client, as issued by the host.
424  */
425 remoting.ClientPlugin.prototype.connect = function(
426     hostJid, hostPublicKey, localJid, sharedSecret,
427     authenticationMethods, authenticationTag,
428     clientPairingId, clientPairedSecret) {
429   var keyFilter = '';
430   if (navigator.platform.indexOf('Mac') == -1) {
431     keyFilter = 'mac';
432   } else if (navigator.userAgent.match(/\bCrOS\b/)) {
433     keyFilter = 'cros';
434   }
435   this.plugin.postMessage(JSON.stringify(
436     { method: 'connect', data: {
437         hostJid: hostJid,
438         hostPublicKey: hostPublicKey,
439         localJid: localJid,
440         sharedSecret: sharedSecret,
441         authenticationMethods: authenticationMethods,
442         authenticationTag: authenticationTag,
443         capabilities: this.capabilities_.join(" "),
444         clientPairingId: clientPairingId,
445         clientPairedSecret: clientPairedSecret,
446         keyFilter: keyFilter
447       }
448     }));
452  * Release all currently pressed keys.
453  */
454 remoting.ClientPlugin.prototype.releaseAllKeys = function() {
455   this.plugin.postMessage(JSON.stringify(
456       { method: 'releaseAllKeys', data: {} }));
460  * Send a key event to the host.
462  * @param {number} usbKeycode The USB-style code of the key to inject.
463  * @param {boolean} pressed True to inject a key press, False for a release.
464  */
465 remoting.ClientPlugin.prototype.injectKeyEvent =
466     function(usbKeycode, pressed) {
467   this.plugin.postMessage(JSON.stringify(
468       { method: 'injectKeyEvent', data: {
469           'usbKeycode': usbKeycode,
470           'pressed': pressed}
471       }));
475  * Remap one USB keycode to another in all subsequent key events.
477  * @param {number} fromKeycode The USB-style code of the key to remap.
478  * @param {number} toKeycode The USB-style code to remap the key to.
479  */
480 remoting.ClientPlugin.prototype.remapKey =
481     function(fromKeycode, toKeycode) {
482   this.plugin.postMessage(JSON.stringify(
483       { method: 'remapKey', data: {
484           'fromKeycode': fromKeycode,
485           'toKeycode': toKeycode}
486       }));
490  * Enable/disable redirection of the specified key to the web-app.
492  * @param {number} keycode The USB-style code of the key.
493  * @param {Boolean} trap True to enable trapping, False to disable.
494  */
495 remoting.ClientPlugin.prototype.trapKey = function(keycode, trap) {
496   this.plugin.postMessage(JSON.stringify(
497       { method: 'trapKey', data: {
498           'keycode': keycode,
499           'trap': trap}
500       }));
504  * Returns an associative array with a set of stats for this connecton.
506  * @return {remoting.ClientSession.PerfStats} The connection statistics.
507  */
508 remoting.ClientPlugin.prototype.getPerfStats = function() {
509   return this.perfStats_;
513  * Sends a clipboard item to the host.
515  * @param {string} mimeType The MIME type of the clipboard item.
516  * @param {string} item The clipboard item.
517  */
518 remoting.ClientPlugin.prototype.sendClipboardItem =
519     function(mimeType, item) {
520   if (!this.hasFeature(remoting.ClientPlugin.Feature.SEND_CLIPBOARD_ITEM))
521     return;
522   this.plugin.postMessage(JSON.stringify(
523       { method: 'sendClipboardItem',
524         data: { mimeType: mimeType, item: item }}));
528  * Notifies the host that the client has the specified size and pixel density.
530  * @param {number} width The available client width in DIPs.
531  * @param {number} height The available client height in DIPs.
532  * @param {number} device_scale The number of device pixels per DIP.
533  */
534 remoting.ClientPlugin.prototype.notifyClientResolution =
535     function(width, height, device_scale) {
536   if (this.hasFeature(remoting.ClientPlugin.Feature.NOTIFY_CLIENT_RESOLUTION)) {
537     var dpi = Math.floor(device_scale * 96);
538     this.plugin.postMessage(JSON.stringify(
539         { method: 'notifyClientResolution',
540           data: { width: Math.floor(width * device_scale),
541                   height: Math.floor(height * device_scale),
542                   x_dpi: dpi, y_dpi: dpi }}));
543   }
547  * Requests that the host pause or resume sending video updates.
549  * @param {boolean} pause True to suspend video updates, false otherwise.
550  */
551 remoting.ClientPlugin.prototype.pauseVideo =
552     function(pause) {
553   if (this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) {
554     this.plugin.postMessage(JSON.stringify(
555         { method: 'videoControl', data: { pause: pause }}));
556   } else if (this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_VIDEO)) {
557     this.plugin.postMessage(JSON.stringify(
558         { method: 'pauseVideo', data: { pause: pause }}));
559   }
563  * Requests that the host pause or resume sending audio updates.
565  * @param {boolean} pause True to suspend audio updates, false otherwise.
566  */
567 remoting.ClientPlugin.prototype.pauseAudio =
568     function(pause) {
569   if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_AUDIO)) {
570     return;
571   }
572   this.plugin.postMessage(JSON.stringify(
573       { method: 'pauseAudio', data: { pause: pause }}));
577  * Requests that the host configure the video codec for lossless encode.
579  * @param {boolean} wantLossless True to request lossless encoding.
580  */
581 remoting.ClientPlugin.prototype.setLosslessEncode =
582     function(wantLossless) {
583   if (!this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) {
584     return;
585   }
586   this.plugin.postMessage(JSON.stringify(
587       { method: 'videoControl', data: { losslessEncode: wantLossless }}));
591  * Requests that the host configure the video codec for lossless color.
593  * @param {boolean} wantLossless True to request lossless color.
594  */
595 remoting.ClientPlugin.prototype.setLosslessColor =
596     function(wantLossless) {
597   if (!this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) {
598     return;
599   }
600   this.plugin.postMessage(JSON.stringify(
601       { method: 'videoControl', data: { losslessColor: wantLossless }}));
605  * Called when a PIN is obtained from the user.
607  * @param {string} pin The PIN.
608  */
609 remoting.ClientPlugin.prototype.onPinFetched =
610     function(pin) {
611   if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) {
612     return;
613   }
614   this.plugin.postMessage(JSON.stringify(
615       { method: 'onPinFetched', data: { pin: pin }}));
619  * Tells the plugin to ask for the PIN asynchronously.
620  */
621 remoting.ClientPlugin.prototype.useAsyncPinDialog =
622     function() {
623   if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) {
624     return;
625   }
626   this.plugin.postMessage(JSON.stringify(
627       { method: 'useAsyncPinDialog', data: {} }));
631  * Sets the third party authentication token and shared secret.
633  * @param {string} token The token received from the token URL.
634  * @param {string} sharedSecret Shared secret received from the token URL.
635  */
636 remoting.ClientPlugin.prototype.onThirdPartyTokenFetched = function(
637     token, sharedSecret) {
638   this.plugin.postMessage(JSON.stringify(
639     { method: 'onThirdPartyTokenFetched',
640       data: { token: token, sharedSecret: sharedSecret}}));
644  * Request pairing with the host for PIN-less authentication.
646  * @param {string} clientName The human-readable name of the client.
647  * @param {function(string, string):void} onDone, Callback to receive the
648  *     client id and shared secret when they are available.
649  */
650 remoting.ClientPlugin.prototype.requestPairing =
651     function(clientName, onDone) {
652   if (!this.hasFeature(remoting.ClientPlugin.Feature.PINLESS_AUTH)) {
653     return;
654   }
655   this.onPairingComplete_ = onDone;
656   this.plugin.postMessage(JSON.stringify(
657       { method: 'requestPairing', data: { clientName: clientName } }));
661  * Send an extension message to the host.
663  * @param {string} type The message type.
664  * @param {string} message The message payload.
665  */
666 remoting.ClientPlugin.prototype.sendClientMessage =
667     function(type, message) {
668   if (!this.hasFeature(remoting.ClientPlugin.Feature.EXTENSION_MESSAGE)) {
669     return;
670   }
671   this.plugin.postMessage(JSON.stringify(
672       { method: 'extensionMessage',
673         data: { type: type, data: message } }));
678  * Request MediaStream-based rendering.
680  * @param {remoting.MediaSourceRenderer} mediaSourceRenderer
681  */
682 remoting.ClientPlugin.prototype.enableMediaSourceRendering =
683     function(mediaSourceRenderer) {
684   if (!this.hasFeature(remoting.ClientPlugin.Feature.MEDIA_SOURCE_RENDERING)) {
685     return;
686   }
687   this.mediaSourceRenderer_ = mediaSourceRenderer;
688   this.plugin.postMessage(JSON.stringify(
689       { method: 'enableMediaSourceRendering', data: {} }));
693  * If we haven't yet received a "hello" message from the plugin, change its
694  * size so that the user can confirm it if click-to-play is enabled, or can
695  * see the "this plugin is disabled" message if it is actually disabled.
696  * @private
697  */
698 remoting.ClientPlugin.prototype.showPluginForClickToPlay_ = function() {
699   if (!this.helloReceived_) {
700     var width = 200;
701     var height = 200;
702     this.plugin.style.width = width + 'px';
703     this.plugin.style.height = height + 'px';
704     // Center the plugin just underneath the "Connnecting..." dialog.
705     var dialog = document.getElementById('client-dialog');
706     var dialogRect = dialog.getBoundingClientRect();
707     this.plugin.style.top = (dialogRect.bottom + 16) + 'px';
708     this.plugin.style.left = (window.innerWidth - width) / 2 + 'px';
709     this.plugin.style.position = 'fixed';
710   }
714  * Undo the CSS rules needed to make the plugin clickable for click-to-play.
715  * @private
716  */
717 remoting.ClientPlugin.prototype.hidePluginForClickToPlay_ = function() {
718   this.plugin.style.width = '';
719   this.plugin.style.height = '';
720   this.plugin.style.top = '';
721   this.plugin.style.left = '';
722   this.plugin.style.position = '';