this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add preview before call

+67 -8
+55 -8
app.js
··· 20 20 pc = null; 21 21 jetstream = null; 22 22 status = signal(""); 23 + previewStream = signal(null); 23 24 signalUri = null; // AT URI of the record we published 24 25 expiryTimer = null; 25 26 ··· 32 33 this.did.value = session.info.sub; 33 34 this.xrpc = client(session); 34 35 this.#startJetstream(); 36 + this.#startPreview(); 35 37 deleteExpiredRecords(this.xrpc, this.did.value).catch((e) => 36 38 console.error("failed to clean up expired records", e), 37 39 ); ··· 46 48 componentWillUnmount() { 47 49 this.jetstream?.close(); 48 50 this.#hangup(); 51 + this.#stopPreview(); 49 52 } 50 53 51 54 #startJetstream() { ··· 155 158 #hangup() { 156 159 this.pc?.close(); 157 160 this.pc = null; 158 - this.localStream.value?.getTracks().forEach((t) => t.stop()); 161 + // Stop call stream tracks only if they differ from the preview 162 + if (this.localStream.value && this.localStream.value !== this.previewStream.value) { 163 + this.localStream.value.getTracks().forEach((t) => t.stop()); 164 + } 159 165 this.localStream.value = null; 160 166 this.remoteStream.value = null; 161 167 this.incomingOffer.value = null; ··· 164 170 this.#deleteSignalRecord(); 165 171 } 166 172 173 + async #startPreview() { 174 + try { 175 + const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); 176 + this.previewStream.value = stream; 177 + } catch (e) { 178 + console.error("preview camera failed", e); 179 + } 180 + } 181 + 182 + #stopPreview() { 183 + this.previewStream.value?.getTracks().forEach((t) => t.stop()); 184 + this.previewStream.value = null; 185 + } 186 + 167 187 #deleteSignalRecord() { 168 188 if (!this.signalUri || !this.xrpc) return; 169 189 const rkey = this.signalUri.split("/").pop(); ··· 190 210 logout = () => { 191 211 this.jetstream?.close(); 192 212 this.#hangup(); 213 + this.#stopPreview(); 193 214 localStorage.removeItem("webrtc:did"); 194 215 this.did.value = ""; 195 216 this.xrpc = null; ··· 208 229 <button onClick=${this.logout}>Sign out</button> 209 230 </header> 210 231 211 - ${state === "idle" && html`<${CallForm} onCall=${this.startCall} />`} 232 + ${state === "idle" && html` 233 + <${Preview} stream=${this.previewStream.value} /> 234 + <${CallForm} onCall=${this.startCall} /> 235 + `} 212 236 213 - ${state === "incoming" && 214 - html`<${IncomingCall} 215 - callerDid=${this.incomingOffer.value?.callerDid} 216 - onAccept=${this.acceptCall} 217 - onDecline=${this.declineCall} 218 - />`} 237 + ${state === "incoming" && html` 238 + <${Preview} stream=${this.previewStream.value} /> 239 + <${IncomingCall} 240 + callerDid=${this.incomingOffer.value?.callerDid} 241 + onAccept=${this.acceptCall} 242 + onDecline=${this.declineCall} 243 + /> 244 + `} 219 245 220 246 ${(state === "calling" || state === "connected") && 221 247 html`<${VideoCall} ··· 291 317 </div> 292 318 </div> 293 319 `; 320 + } 321 + } 322 + 323 + class Preview extends Component { 324 + ref = createRef(); 325 + 326 + componentDidMount() { 327 + this.#sync(); 328 + } 329 + 330 + componentDidUpdate() { 331 + this.#sync(); 332 + } 333 + 334 + #sync() { 335 + if (this.ref.current) this.ref.current.srcObject = this.props.stream || null; 336 + } 337 + 338 + render() { 339 + if (!this.props.stream) return null; 340 + return html`<video ref=${this.ref} autoplay playsinline muted class="preview-video" />`; 294 341 } 295 342 } 296 343
+12
style.css
··· 143 143 border-radius: 8px; 144 144 border: 2px solid #333; 145 145 object-fit: cover; 146 + transform: scaleX(-1); 147 + } 148 + .preview-video { 149 + position: fixed; 150 + bottom: 1rem; 151 + right: 1rem; 152 + width: 180px; 153 + border-radius: 8px; 154 + border: 2px solid #333; 155 + object-fit: cover; 156 + transform: scaleX(-1); 157 + z-index: 50; 146 158 } 147 159 .hangup { 148 160 background: #ef4444;