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);