• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4
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 */
13
14'use strict';
15
16/** @suppress {duplicate} */
17var remoting = remoting || {};
18
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 */
26remoting.ClientPlugin = function(plugin, onExtensionMessage) {
27  this.plugin = plugin;
28  this.onExtensionMessage_ = onExtensionMessage;
29
30  this.desktopWidth = 0;
31  this.desktopHeight = 0;
32  this.desktopXDpi = 96;
33  this.desktopYDpi = 96;
34
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) {};
46
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) {};
60  /**
61   * @param {string} url
62   * @param {number} hotspotX
63   * @param {number} hotspotY
64   */
65  this.updateMouseCursorImage = function(url, hotspotX, hotspotY) {};
66
67  /** @type {remoting.MediaSourceRenderer} */
68  this.mediaSourceRenderer_ = null;
69
70  /** @type {number} */
71  this.pluginApiVersion_ = -1;
72  /** @type {Array.<string>} */
73  this.pluginApiFeatures_ = [];
74  /** @type {number} */
75  this.pluginApiMinVersion_ = -1;
76  /** @type {!Array.<string>} */
77  this.capabilities_ = [];
78  /** @type {boolean} */
79  this.helloReceived_ = false;
80  /** @type {function(boolean)|null} */
81  this.onInitializedCallback_ = null;
82  /** @type {function(string, string):void} */
83  this.onPairingComplete_ = function(clientId, sharedSecret) {};
84  /** @type {remoting.ClientSession.PerfStats} */
85  this.perfStats_ = new remoting.ClientSession.PerfStats();
86
87  /** @type {remoting.ClientPlugin} */
88  var that = this;
89  /** @param {Event} event Message event from the plugin. */
90  this.plugin.addEventListener('message', function(event) {
91      that.handleMessage_(event.data);
92    }, false);
93
94  if (remoting.settings.CLIENT_PLUGIN_TYPE == 'native') {
95    window.setTimeout(this.showPluginForClickToPlay_.bind(this), 500);
96  }
97};
98
99/**
100 * Set of features for which hasFeature() can be used to test.
101 *
102 * @enum {string}
103 */
104remoting.ClientPlugin.Feature = {
105  INJECT_KEY_EVENT: 'injectKeyEvent',
106  NOTIFY_CLIENT_RESOLUTION: 'notifyClientResolution',
107  ASYNC_PIN: 'asyncPin',
108  PAUSE_VIDEO: 'pauseVideo',
109  PAUSE_AUDIO: 'pauseAudio',
110  REMAP_KEY: 'remapKey',
111  SEND_CLIPBOARD_ITEM: 'sendClipboardItem',
112  THIRD_PARTY_AUTH: 'thirdPartyAuth',
113  TRAP_KEY: 'trapKey',
114  PINLESS_AUTH: 'pinlessAuth',
115  EXTENSION_MESSAGE: 'extensionMessage',
116  MEDIA_SOURCE_RENDERING: 'mediaSourceRendering',
117  VIDEO_CONTROL: 'videoControl'
118};
119
120/**
121 * Chromoting session API version (for this javascript).
122 * This is compared with the plugin API version to verify that they are
123 * compatible.
124 *
125 * @const
126 * @private
127 */
128remoting.ClientPlugin.prototype.API_VERSION_ = 6;
129
130/**
131 * The oldest API version that we support.
132 * This will differ from the |API_VERSION_| if we maintain backward
133 * compatibility with older API versions.
134 *
135 * @const
136 * @private
137 */
138remoting.ClientPlugin.prototype.API_MIN_VERSION_ = 5;
139
140/**
141 * @param {string|{method:string, data:Object.<string,*>}}
142 *    rawMessage Message from the plugin.
143 * @private
144 */
145remoting.ClientPlugin.prototype.handleMessage_ = function(rawMessage) {
146  var message =
147      /** @type {{method:string, data:Object.<string,*>}} */
148      ((typeof(rawMessage) == 'string') ? jsonParseSafe(rawMessage)
149                                        : rawMessage);
150  if (!message || !('method' in message) || !('data' in message)) {
151    console.error('Received invalid message from the plugin:', rawMessage);
152    return;
153  }
154
155  try {
156    this.handleMessageMethod_(message);
157  } catch(e) {
158    console.error(/** @type {*} */ (e));
159  }
160}
161
162/**
163 * @param {{method:string, data:Object.<string,*>}}
164 *    message Parsed message from the plugin.
165 * @private
166 */
167remoting.ClientPlugin.prototype.handleMessageMethod_ = function(message) {
168  /**
169   * Splits a string into a list of words delimited by spaces.
170   * @param {string} str String that should be split.
171   * @return {!Array.<string>} List of words.
172   */
173  var tokenize = function(str) {
174    /** @type {Array.<string>} */
175    var tokens = str.match(/\S+/g);
176    return tokens ? tokens : [];
177  };
178
179  if (message.method == 'hello') {
180    // Resize in case we had to enlarge it to support click-to-play.
181    this.hidePluginForClickToPlay_();
182    this.pluginApiVersion_ = getNumberAttr(message.data, 'apiVersion');
183    this.pluginApiMinVersion_ = getNumberAttr(message.data, 'apiMinVersion');
184
185    if (this.pluginApiVersion_ >= 7) {
186      this.pluginApiFeatures_ =
187          tokenize(getStringAttr(message.data, 'apiFeatures'));
188
189      // Negotiate capabilities.
190
191      /** @type {!Array.<string>} */
192      var requestedCapabilities = [];
193      if ('requestedCapabilities' in message.data) {
194        requestedCapabilities =
195            tokenize(getStringAttr(message.data, 'requestedCapabilities'));
196      }
197
198      /** @type {!Array.<string>} */
199      var supportedCapabilities = [];
200      if ('supportedCapabilities' in message.data) {
201        supportedCapabilities =
202            tokenize(getStringAttr(message.data, 'supportedCapabilities'));
203      }
204
205      // At the moment the webapp does not recognize any of
206      // 'requestedCapabilities' capabilities (so they all should be disabled)
207      // and do not care about any of 'supportedCapabilities' capabilities (so
208      // they all can be enabled).
209      this.capabilities_ = supportedCapabilities;
210
211      // Let the host know that the webapp can be requested to always send
212      // the client's dimensions.
213      this.capabilities_.push(
214          remoting.ClientSession.Capability.SEND_INITIAL_RESOLUTION);
215
216      // Let the host know that we're interested in knowing whether or not
217      // it rate-limits desktop-resize requests.
218      this.capabilities_.push(
219          remoting.ClientSession.Capability.RATE_LIMIT_RESIZE_REQUESTS);
220    } else if (this.pluginApiVersion_ >= 6) {
221      this.pluginApiFeatures_ = ['highQualityScaling', 'injectKeyEvent'];
222    } else {
223      this.pluginApiFeatures_ = ['highQualityScaling'];
224    }
225    this.helloReceived_ = true;
226    if (this.onInitializedCallback_ != null) {
227      this.onInitializedCallback_(true);
228      this.onInitializedCallback_ = null;
229    }
230
231  } else if (message.method == 'sendOutgoingIq') {
232    this.onOutgoingIqHandler(getStringAttr(message.data, 'iq'));
233
234  } else if (message.method == 'logDebugMessage') {
235    this.onDebugMessageHandler(getStringAttr(message.data, 'message'));
236
237  } else if (message.method == 'onConnectionStatus') {
238    var state = remoting.ClientSession.State.fromString(
239        getStringAttr(message.data, 'state'))
240    var error = remoting.ClientSession.ConnectionError.fromString(
241        getStringAttr(message.data, 'error'));
242    this.onConnectionStatusUpdateHandler(state, error);
243
244  } else if (message.method == 'onDesktopSize') {
245    this.desktopWidth = getNumberAttr(message.data, 'width');
246    this.desktopHeight = getNumberAttr(message.data, 'height');
247    this.desktopXDpi = getNumberAttr(message.data, 'x_dpi', 96);
248    this.desktopYDpi = getNumberAttr(message.data, 'y_dpi', 96);
249    this.onDesktopSizeUpdateHandler();
250
251  } else if (message.method == 'onPerfStats') {
252    // Return value is ignored. These calls will throw an error if the value
253    // is not a number.
254    getNumberAttr(message.data, 'videoBandwidth');
255    getNumberAttr(message.data, 'videoFrameRate');
256    getNumberAttr(message.data, 'captureLatency');
257    getNumberAttr(message.data, 'encodeLatency');
258    getNumberAttr(message.data, 'decodeLatency');
259    getNumberAttr(message.data, 'renderLatency');
260    getNumberAttr(message.data, 'roundtripLatency');
261    this.perfStats_ =
262        /** @type {remoting.ClientSession.PerfStats} */ message.data;
263
264  } else if (message.method == 'injectClipboardItem') {
265    var mimetype = getStringAttr(message.data, 'mimeType');
266    var item = getStringAttr(message.data, 'item');
267    if (remoting.clipboard) {
268      remoting.clipboard.fromHost(mimetype, item);
269    }
270
271  } else if (message.method == 'onFirstFrameReceived') {
272    if (remoting.clientSession) {
273      remoting.clientSession.onFirstFrameReceived();
274    }
275
276  } else if (message.method == 'onConnectionReady') {
277    var ready = getBooleanAttr(message.data, 'ready');
278    this.onConnectionReadyHandler(ready);
279
280  } else if (message.method == 'fetchPin') {
281    // The pairingSupported value in the dictionary indicates whether both
282    // client and host support pairing. If the client doesn't support pairing,
283    // then the value won't be there at all, so give it a default of false.
284    var pairingSupported = getBooleanAttr(message.data, 'pairingSupported',
285                                          false)
286    this.fetchPinHandler(pairingSupported);
287
288  } else if (message.method == 'setCapabilities') {
289    /** @type {!Array.<string>} */
290    var capabilities = tokenize(getStringAttr(message.data, 'capabilities'));
291    this.onSetCapabilitiesHandler(capabilities);
292
293  } else if (message.method == 'fetchThirdPartyToken') {
294    var tokenUrl = getStringAttr(message.data, 'tokenUrl');
295    var hostPublicKey = getStringAttr(message.data, 'hostPublicKey');
296    var scope = getStringAttr(message.data, 'scope');
297    this.fetchThirdPartyTokenHandler(tokenUrl, hostPublicKey, scope);
298
299  } else if (message.method == 'pairingResponse') {
300    var clientId = getStringAttr(message.data, 'clientId');
301    var sharedSecret = getStringAttr(message.data, 'sharedSecret');
302    this.onPairingComplete_(clientId, sharedSecret);
303
304  } else if (message.method == 'extensionMessage') {
305    var extMsgType = getStringAttr(message.data, 'type');
306    var extMsgData = getStringAttr(message.data, 'data');
307    switch (extMsgType) {
308      case 'gnubby-auth':
309        this.onGnubbyAuthHandler(extMsgData);
310        break;
311      case 'test-echo-reply':
312        console.log('Got echo reply: ' + extMsgData);
313        break;
314      default:
315        if (!this.onExtensionMessage_(extMsgType, extMsgData)) {
316          console.log('Unexpected message received: ' +
317                      extMsgType + ': ' + extMsgData);
318        }
319    }
320
321  } else if (message.method == 'mediaSourceReset') {
322    if (!this.mediaSourceRenderer_) {
323      console.error('Unexpected mediaSourceReset.');
324      return;
325    }
326    this.mediaSourceRenderer_.reset(getStringAttr(message.data, 'format'))
327
328  } else if (message.method == 'mediaSourceData') {
329    if (!(message.data['buffer'] instanceof ArrayBuffer)) {
330      console.error('Invalid mediaSourceData message:', message.data);
331      return;
332    }
333    if (!this.mediaSourceRenderer_) {
334      console.error('Unexpected mediaSourceData.');
335      return;
336    }
337    // keyframe flag may be absent from the message.
338    var keyframe = !!message.data['keyframe'];
339    this.mediaSourceRenderer_.onIncomingData(
340        (/** @type {ArrayBuffer} */ message.data['buffer']), keyframe);
341
342  } else if (message.method == 'unsetCursorShape') {
343    this.updateMouseCursorImage('', 0, 0);
344
345  } else if (message.method == 'setCursorShape') {
346    var width = getNumberAttr(message.data, 'width');
347    var height = getNumberAttr(message.data, 'height');
348    var hotspotX = getNumberAttr(message.data, 'hotspotX');
349    var hotspotY = getNumberAttr(message.data, 'hotspotY');
350    var srcArrayBuffer = getObjectAttr(message.data, 'data');
351
352    var canvas =
353        /** @type {HTMLCanvasElement} */ (document.createElement('canvas'));
354    canvas.width = width;
355    canvas.height = height;
356
357    var context =
358        /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d'));
359    var imageData = context.getImageData(0, 0, width, height);
360    base.debug.assert(srcArrayBuffer instanceof ArrayBuffer);
361    var src = new Uint8Array(/** @type {ArrayBuffer} */(srcArrayBuffer));
362    var dest = imageData.data;
363    for (var i = 0; i < /** @type {number} */(dest.length); i += 4) {
364      dest[i] = src[i + 2];
365      dest[i + 1] = src[i + 1];
366      dest[i + 2] = src[i];
367      dest[i + 3] = src[i + 3];
368    }
369
370    context.putImageData(imageData, 0, 0);
371    this.updateMouseCursorImage(canvas.toDataURL(), hotspotX, hotspotY);
372  }
373};
374
375/**
376 * Deletes the plugin.
377 */
378remoting.ClientPlugin.prototype.cleanup = function() {
379  this.plugin.parentNode.removeChild(this.plugin);
380};
381
382/**
383 * @return {HTMLEmbedElement} HTML element that correspods to the plugin.
384 */
385remoting.ClientPlugin.prototype.element = function() {
386  return this.plugin;
387};
388
389/**
390 * @param {function(boolean): void} onDone
391 */
392remoting.ClientPlugin.prototype.initialize = function(onDone) {
393  if (this.helloReceived_) {
394    onDone(true);
395  } else {
396    this.onInitializedCallback_ = onDone;
397  }
398};
399
400/**
401 * @return {boolean} True if the plugin and web-app versions are compatible.
402 */
403remoting.ClientPlugin.prototype.isSupportedVersion = function() {
404  if (!this.helloReceived_) {
405    console.error(
406        "isSupportedVersion() is called before the plugin is initialized.");
407    return false;
408  }
409  return this.API_VERSION_ >= this.pluginApiMinVersion_ &&
410      this.pluginApiVersion_ >= this.API_MIN_VERSION_;
411};
412
413/**
414 * @param {remoting.ClientPlugin.Feature} feature The feature to test for.
415 * @return {boolean} True if the plugin supports the named feature.
416 */
417remoting.ClientPlugin.prototype.hasFeature = function(feature) {
418  if (!this.helloReceived_) {
419    console.error(
420        "hasFeature() is called before the plugin is initialized.");
421    return false;
422  }
423  return this.pluginApiFeatures_.indexOf(feature) > -1;
424};
425
426/**
427 * @return {boolean} True if the plugin supports the injectKeyEvent API.
428 */
429remoting.ClientPlugin.prototype.isInjectKeyEventSupported = function() {
430  return this.pluginApiVersion_ >= 6;
431};
432
433/**
434 * @param {string} iq Incoming IQ stanza.
435 */
436remoting.ClientPlugin.prototype.onIncomingIq = function(iq) {
437  if (this.plugin && this.plugin.postMessage) {
438    this.plugin.postMessage(JSON.stringify(
439        { method: 'incomingIq', data: { iq: iq } }));
440  } else {
441    // plugin.onIq may not be set after the plugin has been shut
442    // down. Particularly this happens when we receive response to
443    // session-terminate stanza.
444    console.warn('plugin.onIq is not set so dropping incoming message.');
445  }
446};
447
448/**
449 * @param {string} hostJid The jid of the host to connect to.
450 * @param {string} hostPublicKey The base64 encoded version of the host's
451 *     public key.
452 * @param {string} localJid Local jid.
453 * @param {string} sharedSecret The access code for IT2Me or the PIN
454 *     for Me2Me.
455 * @param {string} authenticationMethods Comma-separated list of
456 *     authentication methods the client should attempt to use.
457 * @param {string} authenticationTag A host-specific tag to mix into
458 *     authentication hashes.
459 * @param {string} clientPairingId For paired Me2Me connections, the
460 *     pairing id for this client, as issued by the host.
461 * @param {string} clientPairedSecret For paired Me2Me connections, the
462 *     paired secret for this client, as issued by the host.
463 */
464remoting.ClientPlugin.prototype.connect = function(
465    hostJid, hostPublicKey, localJid, sharedSecret,
466    authenticationMethods, authenticationTag,
467    clientPairingId, clientPairedSecret) {
468  var keyFilter = '';
469  if (navigator.platform.indexOf('Mac') == -1) {
470    keyFilter = 'mac';
471  } else if (navigator.userAgent.match(/\bCrOS\b/)) {
472    keyFilter = 'cros';
473  }
474  this.plugin.postMessage(JSON.stringify(
475      { method: 'delegateLargeCursors', data: {} }));
476  this.plugin.postMessage(JSON.stringify(
477    { method: 'connect', data: {
478        hostJid: hostJid,
479        hostPublicKey: hostPublicKey,
480        localJid: localJid,
481        sharedSecret: sharedSecret,
482        authenticationMethods: authenticationMethods,
483        authenticationTag: authenticationTag,
484        capabilities: this.capabilities_.join(" "),
485        clientPairingId: clientPairingId,
486        clientPairedSecret: clientPairedSecret,
487        keyFilter: keyFilter
488      }
489    }));
490};
491
492/**
493 * Release all currently pressed keys.
494 */
495remoting.ClientPlugin.prototype.releaseAllKeys = function() {
496  this.plugin.postMessage(JSON.stringify(
497      { method: 'releaseAllKeys', data: {} }));
498};
499
500/**
501 * Send a key event to the host.
502 *
503 * @param {number} usbKeycode The USB-style code of the key to inject.
504 * @param {boolean} pressed True to inject a key press, False for a release.
505 */
506remoting.ClientPlugin.prototype.injectKeyEvent =
507    function(usbKeycode, pressed) {
508  this.plugin.postMessage(JSON.stringify(
509      { method: 'injectKeyEvent', data: {
510          'usbKeycode': usbKeycode,
511          'pressed': pressed}
512      }));
513};
514
515/**
516 * Remap one USB keycode to another in all subsequent key events.
517 *
518 * @param {number} fromKeycode The USB-style code of the key to remap.
519 * @param {number} toKeycode The USB-style code to remap the key to.
520 */
521remoting.ClientPlugin.prototype.remapKey =
522    function(fromKeycode, toKeycode) {
523  this.plugin.postMessage(JSON.stringify(
524      { method: 'remapKey', data: {
525          'fromKeycode': fromKeycode,
526          'toKeycode': toKeycode}
527      }));
528};
529
530/**
531 * Enable/disable redirection of the specified key to the web-app.
532 *
533 * @param {number} keycode The USB-style code of the key.
534 * @param {Boolean} trap True to enable trapping, False to disable.
535 */
536remoting.ClientPlugin.prototype.trapKey = function(keycode, trap) {
537  this.plugin.postMessage(JSON.stringify(
538      { method: 'trapKey', data: {
539          'keycode': keycode,
540          'trap': trap}
541      }));
542};
543
544/**
545 * Returns an associative array with a set of stats for this connecton.
546 *
547 * @return {remoting.ClientSession.PerfStats} The connection statistics.
548 */
549remoting.ClientPlugin.prototype.getPerfStats = function() {
550  return this.perfStats_;
551};
552
553/**
554 * Sends a clipboard item to the host.
555 *
556 * @param {string} mimeType The MIME type of the clipboard item.
557 * @param {string} item The clipboard item.
558 */
559remoting.ClientPlugin.prototype.sendClipboardItem =
560    function(mimeType, item) {
561  if (!this.hasFeature(remoting.ClientPlugin.Feature.SEND_CLIPBOARD_ITEM))
562    return;
563  this.plugin.postMessage(JSON.stringify(
564      { method: 'sendClipboardItem',
565        data: { mimeType: mimeType, item: item }}));
566};
567
568/**
569 * Notifies the host that the client has the specified size and pixel density.
570 *
571 * @param {number} width The available client width in DIPs.
572 * @param {number} height The available client height in DIPs.
573 * @param {number} device_scale The number of device pixels per DIP.
574 */
575remoting.ClientPlugin.prototype.notifyClientResolution =
576    function(width, height, device_scale) {
577  if (this.hasFeature(remoting.ClientPlugin.Feature.NOTIFY_CLIENT_RESOLUTION)) {
578    var dpi = Math.floor(device_scale * 96);
579    this.plugin.postMessage(JSON.stringify(
580        { method: 'notifyClientResolution',
581          data: { width: Math.floor(width * device_scale),
582                  height: Math.floor(height * device_scale),
583                  x_dpi: dpi, y_dpi: dpi }}));
584  }
585};
586
587/**
588 * Requests that the host pause or resume sending video updates.
589 *
590 * @param {boolean} pause True to suspend video updates, false otherwise.
591 */
592remoting.ClientPlugin.prototype.pauseVideo =
593    function(pause) {
594  if (this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) {
595    this.plugin.postMessage(JSON.stringify(
596        { method: 'videoControl', data: { pause: pause }}));
597  } else if (this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_VIDEO)) {
598    this.plugin.postMessage(JSON.stringify(
599        { method: 'pauseVideo', data: { pause: pause }}));
600  }
601};
602
603/**
604 * Requests that the host pause or resume sending audio updates.
605 *
606 * @param {boolean} pause True to suspend audio updates, false otherwise.
607 */
608remoting.ClientPlugin.prototype.pauseAudio =
609    function(pause) {
610  if (!this.hasFeature(remoting.ClientPlugin.Feature.PAUSE_AUDIO)) {
611    return;
612  }
613  this.plugin.postMessage(JSON.stringify(
614      { method: 'pauseAudio', data: { pause: pause }}));
615};
616
617/**
618 * Requests that the host configure the video codec for lossless encode.
619 *
620 * @param {boolean} wantLossless True to request lossless encoding.
621 */
622remoting.ClientPlugin.prototype.setLosslessEncode =
623    function(wantLossless) {
624  if (!this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) {
625    return;
626  }
627  this.plugin.postMessage(JSON.stringify(
628      { method: 'videoControl', data: { losslessEncode: wantLossless }}));
629};
630
631/**
632 * Requests that the host configure the video codec for lossless color.
633 *
634 * @param {boolean} wantLossless True to request lossless color.
635 */
636remoting.ClientPlugin.prototype.setLosslessColor =
637    function(wantLossless) {
638  if (!this.hasFeature(remoting.ClientPlugin.Feature.VIDEO_CONTROL)) {
639    return;
640  }
641  this.plugin.postMessage(JSON.stringify(
642      { method: 'videoControl', data: { losslessColor: wantLossless }}));
643};
644
645/**
646 * Called when a PIN is obtained from the user.
647 *
648 * @param {string} pin The PIN.
649 */
650remoting.ClientPlugin.prototype.onPinFetched =
651    function(pin) {
652  if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) {
653    return;
654  }
655  this.plugin.postMessage(JSON.stringify(
656      { method: 'onPinFetched', data: { pin: pin }}));
657};
658
659/**
660 * Tells the plugin to ask for the PIN asynchronously.
661 */
662remoting.ClientPlugin.prototype.useAsyncPinDialog =
663    function() {
664  if (!this.hasFeature(remoting.ClientPlugin.Feature.ASYNC_PIN)) {
665    return;
666  }
667  this.plugin.postMessage(JSON.stringify(
668      { method: 'useAsyncPinDialog', data: {} }));
669};
670
671/**
672 * Sets the third party authentication token and shared secret.
673 *
674 * @param {string} token The token received from the token URL.
675 * @param {string} sharedSecret Shared secret received from the token URL.
676 */
677remoting.ClientPlugin.prototype.onThirdPartyTokenFetched = function(
678    token, sharedSecret) {
679  this.plugin.postMessage(JSON.stringify(
680    { method: 'onThirdPartyTokenFetched',
681      data: { token: token, sharedSecret: sharedSecret}}));
682};
683
684/**
685 * Request pairing with the host for PIN-less authentication.
686 *
687 * @param {string} clientName The human-readable name of the client.
688 * @param {function(string, string):void} onDone, Callback to receive the
689 *     client id and shared secret when they are available.
690 */
691remoting.ClientPlugin.prototype.requestPairing =
692    function(clientName, onDone) {
693  if (!this.hasFeature(remoting.ClientPlugin.Feature.PINLESS_AUTH)) {
694    return;
695  }
696  this.onPairingComplete_ = onDone;
697  this.plugin.postMessage(JSON.stringify(
698      { method: 'requestPairing', data: { clientName: clientName } }));
699};
700
701/**
702 * Send an extension message to the host.
703 *
704 * @param {string} type The message type.
705 * @param {string} message The message payload.
706 */
707remoting.ClientPlugin.prototype.sendClientMessage =
708    function(type, message) {
709  if (!this.hasFeature(remoting.ClientPlugin.Feature.EXTENSION_MESSAGE)) {
710    return;
711  }
712  this.plugin.postMessage(JSON.stringify(
713      { method: 'extensionMessage',
714        data: { type: type, data: message } }));
715
716};
717
718/**
719 * Request MediaStream-based rendering.
720 *
721 * @param {remoting.MediaSourceRenderer} mediaSourceRenderer
722 */
723remoting.ClientPlugin.prototype.enableMediaSourceRendering =
724    function(mediaSourceRenderer) {
725  if (!this.hasFeature(remoting.ClientPlugin.Feature.MEDIA_SOURCE_RENDERING)) {
726    return;
727  }
728  this.mediaSourceRenderer_ = mediaSourceRenderer;
729  this.plugin.postMessage(JSON.stringify(
730      { method: 'enableMediaSourceRendering', data: {} }));
731};
732
733/**
734 * If we haven't yet received a "hello" message from the plugin, change its
735 * size so that the user can confirm it if click-to-play is enabled, or can
736 * see the "this plugin is disabled" message if it is actually disabled.
737 * @private
738 */
739remoting.ClientPlugin.prototype.showPluginForClickToPlay_ = function() {
740  if (!this.helloReceived_) {
741    var width = 200;
742    var height = 200;
743    this.plugin.style.width = width + 'px';
744    this.plugin.style.height = height + 'px';
745    // Center the plugin just underneath the "Connnecting..." dialog.
746    var dialog = document.getElementById('client-dialog');
747    var dialogRect = dialog.getBoundingClientRect();
748    this.plugin.style.top = (dialogRect.bottom + 16) + 'px';
749    this.plugin.style.left = (window.innerWidth - width) / 2 + 'px';
750    this.plugin.style.position = 'fixed';
751  }
752};
753
754/**
755 * Undo the CSS rules needed to make the plugin clickable for click-to-play.
756 * @private
757 */
758remoting.ClientPlugin.prototype.hidePluginForClickToPlay_ = function() {
759  this.plugin.style.width = '';
760  this.plugin.style.height = '';
761  this.plugin.style.top = '';
762  this.plugin.style.left = '';
763  this.plugin.style.position = '';
764};
765