From f3038c85f900842e3fa04ad70393b60afbec20db Mon Sep 17 00:00:00 2001 From: Miroslav Pejic Date: Fri, 22 Aug 2025 20:03:50 +0200 Subject: [PATCH] [call-me] - add support for screen sharing --- README.md | 3 +- package-lock.json | 4 +- package.json | 2 +- public/client.js | 253 ++++++++++++++++++++++++++++++++++++++++++++++ public/index.html | 10 ++ public/style.css | 39 ++++++- 6 files changed, 305 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4ad2f84..56f53d3 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,9 @@ This project allows you to: - `Switch` between cameras, microphones, or speakers seamlessly during a call. - `Chat` in real time with all participants. - `Hide` your video feed as needed. -- `Toggle` your video. - `Toggle` your audio. +- `Toggle` your video. +- `Toggle` your screen. - `Hang up` the call when finished. - `Use the REST API` to retrieve the list of connected users or initiate a call. diff --git a/package-lock.json b/package-lock.json index 5251ac4..13cfa42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "call-me", - "version": "1.2.52", + "version": "1.2.55", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "call-me", - "version": "1.2.52", + "version": "1.2.55", "license": "AGPLv3", "dependencies": { "@ngrok/ngrok": "1.5.2", diff --git a/package.json b/package.json index 8464527..d69180f 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "call-me", - "version": "1.2.52", + "version": "1.2.55", "description": "Your Go-To for Instant Video Calls", "author": "Miroslav Pejic - miroslav.pejic.85@gmail.com", "license": "AGPLv3", diff --git a/public/client.js b/public/client.js index 6559827..19bba33 100755 --- a/public/client.js +++ b/public/client.js @@ -45,6 +45,7 @@ const hideBtn = document.getElementById('hideBtn'); const swapCameraBtn = document.getElementById('swapCameraBtn'); const videoBtn = document.getElementById('videoBtn'); const audioBtn = document.getElementById('audioBtn'); +const screenShareBtn = document.getElementById('screenShareBtn'); const leaveBtn = document.getElementById('leaveBtn'); const localVideoContainer = document.getElementById('localVideoContainer'); const localVideo = document.getElementById('localVideo'); @@ -63,6 +64,8 @@ let connectedUser; let thisConnection; let camera = 'user'; let stream; +let isScreenSharing = false; +let originalStream = null; // Store original camera stream // User list state let userSignedIn = false; @@ -422,6 +425,14 @@ async function handleEnumerateDevices() { swapCameraBtn.addEventListener('click', swapCamera); elemDisplay(swapCameraBtn, true, 'inline'); } + + // Check if screen sharing is supported + if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) { + elemDisplay(screenShareBtn, true, 'inline'); + } else { + elemDisplay(screenShareBtn, false); + console.log('Screen sharing not supported in this browser'); + } } catch (error) { handleError('Error enumerating devices', error); } @@ -436,6 +447,7 @@ function handleListeners() { hideBtn.addEventListener('click', toggleLocalVideo); videoBtn.addEventListener('click', handleVideoClick); audioBtn.addEventListener('click', handleAudioClick); + screenShareBtn.addEventListener('click', handleScreenShareClick); leaveBtn.addEventListener('click', handleLeaveClick); exitSidebarBtn.addEventListener('click', handleExitSidebarClick); localVideoContainer.addEventListener('click', toggleFullScreen); @@ -594,6 +606,142 @@ function handleAudioClick() { }); } +// Handle screen sharing toggle +async function handleScreenShareClick() { + try { + if (isScreenSharing) { + // Stop screen sharing and return to camera + await stopScreenSharing(); + } else { + // Start screen sharing + await startScreenSharing(); + } + } catch (error) { + handleError('Screen sharing failed', error.message); + console.error('Screen sharing error:', error); + } +} + +// Start screen sharing +async function startScreenSharing() { + try { + // Store original camera stream + originalStream = stream; + + // Get screen share stream + const screenStream = await navigator.mediaDevices.getDisplayMedia({ + video: { + cursor: 'always', + displaySurface: 'monitor', + }, + audio: true, + }); + + // Keep original audio track if available + const audioTrack = originalStream.getAudioTracks()[0]; + if (audioTrack) { + screenStream.addTrack(audioTrack); + } + + // Update stream and local video + stream = screenStream; + localVideo.srcObject = stream; + localVideo.classList.remove('mirror'); // Remove mirror for screen share + localVideo.classList.add('screen-share'); // Apply screen share styling + localVideo.classList.remove('camera-feed'); + + console.log('Local video classes after screen share start:', localVideo.className); + + // Update peer connection if it exists + if (thisConnection) { + const videoSender = thisConnection + .getSenders() + .find((sender) => sender.track && sender.track.kind === 'video'); + if (videoSender) { + await videoSender.replaceTrack(screenStream.getVideoTracks()[0]); + } + } + + // Update UI + isScreenSharing = true; + screenShareBtn.classList.add('btn-danger'); + screenShareBtn.classList.remove('btn-success'); + screenShareBtn.title = 'Stop screen sharing'; + screenShareBtn.innerHTML = ''; + + // Listen for screen share end (user clicks browser's stop sharing) + screenStream.getVideoTracks()[0].onended = () => { + stopScreenSharing(); + }; + + toast('Screen sharing started', 'success', 'top-end', 2000); + console.log('Screen sharing started'); + } catch (error) { + if (error.name === 'NotAllowedError') { + handleError('Screen sharing permission denied'); + } else if (error.name === 'NotSupportedError') { + handleError('Screen sharing not supported in this browser'); + } else { + handleError('Failed to start screen sharing', error.message); + } + throw error; + } +} + +// Stop screen sharing +async function stopScreenSharing() { + try { + if (!originalStream) { + handleError('No original stream available'); + return; + } + + // Stop screen share tracks + if (stream) { + stream.getTracks().forEach((track) => { + if (track.kind === 'video' || (track.kind === 'audio' && track.label.includes('monitor'))) { + track.stop(); + } + }); + } + + // Restore original camera stream + stream = originalStream; + localVideo.srcObject = stream; + handleVideoMirror(localVideo, stream); // Restore mirror for camera + localVideo.classList.remove('screen-share'); // Remove screen share styling + localVideo.classList.add('camera-feed'); // Apply camera feed styling + + console.log('Local video classes after screen share stop:', localVideo.className); + + // Update peer connection if it exists + if (thisConnection) { + const videoSender = thisConnection + .getSenders() + .find((sender) => sender.track && sender.track.kind === 'video'); + if (videoSender && originalStream.getVideoTracks()[0]) { + await videoSender.replaceTrack(originalStream.getVideoTracks()[0]); + } + } + + // Update UI + isScreenSharing = false; + screenShareBtn.classList.remove('btn-danger'); + screenShareBtn.classList.add('btn-success'); + screenShareBtn.title = 'Start screen sharing'; + screenShareBtn.innerHTML = ''; + + // Reset original stream reference + originalStream = null; + + toast('Screen sharing stopped', 'success', 'top-end', 2000); + console.log('Screen sharing stopped'); + } catch (error) { + handleError('Failed to stop screen sharing', error.message); + console.error('Stop screen sharing error:', error); + } +} + // Detect if back or front camera function detectCameraFacingMode(stream) { if (!stream || !stream.getVideoTracks().length) { @@ -650,6 +798,10 @@ function refreshLocalVideoStream(newStream) { stream = updatedStream; localVideo.srcObject = stream; + // Ensure camera feed styling is maintained during swap + localVideo.classList.add('camera-feed'); + localVideo.classList.remove('screen-share'); + handleVideoMirror(localVideo, stream); } @@ -764,6 +916,7 @@ async function handleSignIn(data) { localVideo.muted = true; localVideo.volume = 0; localVideo.controls = false; + localVideo.classList.add('camera-feed'); // Set default styling for camera localUsername.innerText = userName; initializeConnection(); await handleEnumerateDevices(); @@ -852,9 +1005,78 @@ function initializeConnection() { remoteVideo.autoplay = true; remoteVideo.controls = false; + // Detect if remote stream is screen share based on video track settings + const videoTrack = remoteStream.getVideoTracks()[0]; + if (videoTrack) { + const settings = videoTrack.getSettings(); + console.log('Remote video track settings:', settings); + + // Enhanced screen share detection + const isScreenShare = + // Direct indicators + settings.displaySurface === 'monitor' || + settings.displaySurface === 'window' || + settings.displaySurface === 'application' || + // Resolution-based detection (common screen resolutions) + settings.width >= 1920 || + settings.height >= 1080 || + // Aspect ratio detection (wider than typical cameras) + (settings.width && settings.height && settings.width / settings.height > 1.7) || + // Frame rate detection (screens often use lower frame rates) + (settings.frameRate && settings.frameRate <= 15); + + console.log('Screen share detection result:', isScreenShare); + console.log('Detection factors:', { + displaySurface: settings.displaySurface, + width: settings.width, + height: settings.height, + aspectRatio: + settings.width && settings.height ? (settings.width / settings.height).toFixed(2) : 'unknown', + frameRate: settings.frameRate, + }); + + if (isScreenShare) { + remoteVideo.classList.add('screen-share'); + remoteVideo.classList.remove('camera-feed'); + console.log('Remote screen share detected, classes:', remoteVideo.className); + } else { + remoteVideo.classList.add('camera-feed'); + remoteVideo.classList.remove('screen-share'); + console.log('Remote camera feed detected, classes:', remoteVideo.className); + } + + // Force a style refresh + remoteVideo.style.display = 'none'; + remoteVideo.offsetHeight; // Trigger reflow + remoteVideo.style.display = 'block'; + } + startSessionTime(); renderUserList(); // Update UI to show hang-up button + // Retry screen share detection after video loads + setTimeout(() => { + const videoTrack = remoteStream.getVideoTracks()[0]; + if (videoTrack) { + const settings = videoTrack.getSettings(); + console.log('Delayed remote video track settings:', settings); + + const isScreenShare = + settings.displaySurface === 'monitor' || + settings.displaySurface === 'window' || + settings.displaySurface === 'application' || + settings.width >= 1920 || + settings.height >= 1080 || + (settings.width && settings.height && settings.width / settings.height > 1.7); + + if (isScreenShare && !remoteVideo.classList.contains('screen-share')) { + remoteVideo.classList.add('screen-share'); + remoteVideo.classList.remove('camera-feed'); + console.log('Delayed detection: Remote screen share detected, classes:', remoteVideo.className); + } + } + }, 1000); + console.log('Remote stream set to video element'); } else { handleError('No stream available in the ontrack event.'); @@ -1034,6 +1256,11 @@ function disconnectConnection() { // Handle leaving the room function handleLeave(disconnect = true) { if (disconnect) { + // Stop screen sharing if active + if (isScreenSharing) { + stopScreenSharing(); + } + // Stop local and remote video tracks stopMediaStream(localVideo); stopMediaStream(remoteVideo); @@ -1048,9 +1275,17 @@ function handleLeave(disconnect = true) { // Remote user left - clean up properly for new connections console.log('Remote user left - cleaning up connection'); + // Stop screen sharing if active + if (isScreenSharing) { + stopScreenSharing(); + } + // Stop remote video tracks only stopMediaStream(remoteVideo); + // Reset remote video styling + remoteVideo.classList.remove('screen-share', 'camera-feed'); + // Clean up the peer connection so new connections work properly disconnectConnection(); @@ -1642,6 +1877,24 @@ function debugStreamState() { } } +// Test function to manually toggle remote video object-fit +function testRemoteVideoObjectFit() { + if (remoteVideo.classList.contains('screen-share')) { + remoteVideo.classList.remove('screen-share'); + remoteVideo.classList.add('camera-feed'); + console.log('Switched to camera-feed mode'); + } else { + remoteVideo.classList.remove('camera-feed'); + remoteVideo.classList.add('screen-share'); + console.log('Switched to screen-share mode'); + } + console.log('Remote video classes:', remoteVideo.className); +} + +// Make test function globally available +window.testRemoteVideoObjectFit = testRemoteVideoObjectFit; +window.debugStreamState = debugStreamState; + // Initialize devices when settings tab is accessed (not on page load) // This prevents conflicts with initial stream setup diff --git a/public/index.html b/public/index.html index a62b79c..ebdab9f 100755 --- a/public/index.html +++ b/public/index.html @@ -152,6 +152,16 @@ > + +