tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

webrtc-preview.mjs (6403B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { html, classMap } from "chrome://global/content/vendor/lit.all.mjs";
      6 import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
      7 
      8 const { XPCOMUtils } = ChromeUtils.importESModule(
      9  "resource://gre/modules/XPCOMUtils.sys.mjs"
     10 );
     11 
     12 const lazy = {};
     13 XPCOMUtils.defineLazyPreferenceGetter(
     14  lazy,
     15  "testGumDelayMs",
     16  "privacy.webrtc.preview.testGumDelayMs",
     17  0
     18 );
     19 
     20 window.MozXULElement?.insertFTLIfNeeded("browser/webrtc-preview.ftl");
     21 
     22 /**
     23 * A class to handle a preview of a WebRTC stream.
     24 */
     25 export class WebRTCPreview extends MozLitElement {
     26  static properties = {
     27    // The ID of the device to preview.
     28    deviceId: String,
     29    // The media source type to preview.
     30    mediaSource: String,
     31    // Whether to show the preview control buttons.
     32    showPreviewControlButtons: Boolean,
     33 
     34    // Whether the preview is currently active.
     35    _previewActive: { type: Boolean, state: true },
     36    _loading: { type: Boolean, state: true },
     37  };
     38 
     39  static queries = {
     40    videoEl: "video",
     41  };
     42 
     43  // The stream object for the preview. Only set when the preview is active.
     44  #stream = null;
     45  // AbortController to cancel pending gUM requests when stopping preview.
     46  #abortController = null;
     47 
     48  constructor() {
     49    super();
     50 
     51    // By default hide the start preview button.
     52    this.showPreviewControlButtons = false;
     53 
     54    this._previewActive = false;
     55    this._loading = false;
     56  }
     57 
     58  disconnectedCallback() {
     59    super.disconnectedCallback();
     60 
     61    this.stopPreview();
     62  }
     63 
     64  /**
     65   * Start the preview.
     66   *
     67   * @param {object} options - The options for the preview.
     68   * @param {string} [options.deviceId = null] - The device ID of the camera to
     69   * use. If null the last used device will be used.
     70   * @param {string} [options.mediaSource = null] - The media source to use. If
     71   * null the last used media source will be used.
     72   * @param {boolean} [options.showPreviewControlButtons = null] - Whether to
     73   * show the preview control buttons. If null the last used value will be used.
     74   */
     75  async startPreview({
     76    deviceId = null,
     77    mediaSource = null,
     78    showPreviewControlButtons = null,
     79  } = {}) {
     80    // We can only start preview once the element is connected to the DOM and
     81    // the video element is available.
     82    // If you run into this error you're calling the preview method too early,
     83    // or you forgot to add it to the DOM.
     84    if (!this.isConnected || !this.videoEl) {
     85      throw new Error("Can not start preview: Not connected yet.");
     86    }
     87 
     88    if (deviceId != null) {
     89      this.deviceId = deviceId;
     90    }
     91    if (mediaSource != null) {
     92      this.mediaSource = mediaSource;
     93    }
     94    if (showPreviewControlButtons != null) {
     95      this.showPreviewControlButtons = showPreviewControlButtons;
     96    }
     97 
     98    if (this.deviceId == null) {
     99      throw new Error("Missing deviceId");
    100    }
    101 
    102    // Stop any existing preview.
    103    this.stopPreview();
    104 
    105    this.#abortController = new AbortController();
    106    let { signal } = this.#abortController;
    107 
    108    this._loading = true;
    109    this._previewActive = true;
    110 
    111    // Use the same constraints for both camera and screen share preview.
    112    let constraints = {
    113      video: {
    114        mediaSource: this.mediaSource,
    115        deviceId: { exact: this.deviceId },
    116        frameRate: 30,
    117        width: 854,
    118        height: 480,
    119      },
    120    };
    121 
    122    let stream;
    123 
    124    try {
    125      stream = await navigator.mediaDevices.getUserMedia(constraints);
    126      if (lazy.testGumDelayMs > 0) {
    127        await new Promise(resolve => setTimeout(resolve, lazy.testGumDelayMs));
    128      }
    129    } catch (error) {
    130      if (signal.aborted) {
    131        this.#dispatchTestEvent("aborted");
    132        return;
    133      }
    134      this._loading = false;
    135      if (
    136        error.name == "OverconstrainedError" &&
    137        error.constraint == "deviceId"
    138      ) {
    139        // Source has disappeared since enumeration, which can happen.
    140        // No preview.
    141        this.stopPreview();
    142        this.#dispatchTestEvent("error");
    143        return;
    144      }
    145      console.error(`error in preview: ${error.message} ${error.constraint}`);
    146      this.#dispatchTestEvent("error");
    147      return;
    148    }
    149 
    150    if (signal.aborted) {
    151      stream.getTracks().forEach(t => t.stop());
    152      this.#dispatchTestEvent("aborted");
    153      return;
    154    }
    155 
    156    this.videoEl.srcObject = stream;
    157    this.#stream = stream;
    158    this.#dispatchTestEvent("success");
    159  }
    160 
    161  #dispatchTestEvent(result) {
    162    if (lazy.testGumDelayMs > 0) {
    163      this.dispatchEvent(
    164        new CustomEvent("test-preview-complete", { detail: { result } })
    165      );
    166    }
    167  }
    168 
    169  /**
    170   * Stop the preview.
    171   */
    172  stopPreview() {
    173    // Abort any pending gUM request.
    174    this.#abortController?.abort();
    175    this.#abortController = null;
    176 
    177    this._loading = false;
    178 
    179    // Stop any existing playback.
    180    this.#stream?.getTracks().forEach(t => t.stop());
    181    this.#stream = null;
    182    if (this.videoEl) {
    183      this.videoEl.srcObject = null;
    184    }
    185 
    186    this._previewActive = false;
    187  }
    188 
    189  render() {
    190    return html`
    191      <link
    192        rel="stylesheet"
    193        href="chrome://browser/content/webrtc/webrtc-preview.css"
    194      />
    195      <div id="preview-container">
    196        <video
    197          autoplay
    198          tabindex="-1"
    199          @play=${() => (this._loading = false)}
    200          class=${classMap({ active: this._previewActive })}
    201        ></video>
    202        <moz-button
    203          id="show-preview-button"
    204          class="centered"
    205          data-l10n-id="webrtc-share-preview-button-show"
    206          @click=${() => this.startPreview()}
    207          ?hidden=${this.deviceId == null ||
    208          !this.showPreviewControlButtons ||
    209          this._previewActive}
    210        ></moz-button>
    211        <img
    212          id="loading-indicator"
    213          class="centered"
    214          src="chrome://global/skin/icons/loading.svg"
    215          alt="Loading"
    216          ?hidden=${!this._loading}
    217        />
    218      </div>
    219      <moz-button
    220        id="stop-preview-button"
    221        data-l10n-id="webrtc-share-preview-button-hide"
    222        @click=${() => this.stopPreview()}
    223        ?hidden=${!this.showPreviewControlButtons || !this._previewActive}
    224      ></moz-button>
    225    `;
    226  }
    227 }
    228 
    229 customElements.define("webrtc-preview", WebRTCPreview);