Spaces:
Runtime error
Runtime error
Fix video calls: allow STUN/P2P, fix ontrack stream, fix timer reset, fix ICE errors (v2.8.0)
Browse files- client.js +23 -50
- index.html +11 -6
- package.json +1 -1
- server.js +1 -1
client.js
CHANGED
|
@@ -2828,7 +2828,12 @@ async function addOrQueueIceCandidate(candidateData) {
|
|
| 2828 |
const hasRemoteDescription = Boolean(peerConnection.remoteDescription && peerConnection.remoteDescription.type);
|
| 2829 |
|
| 2830 |
if (hasRemoteDescription) {
|
| 2831 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2832 |
} else {
|
| 2833 |
remoteIceCandidatesQueue.push(candidate);
|
| 2834 |
}
|
|
@@ -6140,16 +6145,11 @@ async function ensureLocalCallStream() {
|
|
| 6140 |
try {
|
| 6141 |
const selectedMic = audioSettings?.microphoneId;
|
| 6142 |
const constraints = {
|
| 6143 |
-
audio:
|
| 6144 |
-
deviceId: selectedMic && selectedMic !== 'default' ? { exact: selectedMic } : undefined,
|
| 6145 |
-
echoCancellation: true,
|
| 6146 |
-
noiseSuppression: true,
|
| 6147 |
-
autoGainControl: true
|
| 6148 |
-
},
|
| 6149 |
video: {
|
| 6150 |
facingMode: 'user',
|
| 6151 |
-
width: { ideal:
|
| 6152 |
-
height: { ideal: 480 }
|
| 6153 |
}
|
| 6154 |
};
|
| 6155 |
localCallStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
@@ -6249,51 +6249,22 @@ function createPeerConnection(peerId) {
|
|
| 6249 |
};
|
| 6250 |
|
| 6251 |
peerConnection.ontrack = (event) => {
|
| 6252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6253 |
const remoteVideo = document.getElementById('remoteCallVideo');
|
| 6254 |
if (remoteVideo) {
|
| 6255 |
-
if (
|
| 6256 |
-
|
| 6257 |
-
// Keep reference to the actual remote stream so reconnect logic does not swap it out.
|
| 6258 |
-
remoteCallStream = incomingStream;
|
| 6259 |
-
if (remoteVideo.srcObject !== incomingStream) {
|
| 6260 |
-
remoteVideo.srcObject = incomingStream;
|
| 6261 |
-
}
|
| 6262 |
-
} else if (remoteCallStream) {
|
| 6263 |
-
remoteCallStream.addTrack(event.track);
|
| 6264 |
-
if (remoteVideo.srcObject !== remoteCallStream) {
|
| 6265 |
-
remoteVideo.srcObject = remoteCallStream;
|
| 6266 |
-
}
|
| 6267 |
}
|
| 6268 |
-
|
| 6269 |
-
|
| 6270 |
remoteVideo.muted = false;
|
| 6271 |
remoteVideo.volume = 1.0;
|
| 6272 |
-
|
| 6273 |
-
|
| 6274 |
-
if (audioSettings.speakerId && audioSettings.speakerId !== 'default' && typeof remoteVideo.setSinkId === 'function') {
|
| 6275 |
-
remoteVideo.setSinkId(audioSettings.speakerId).catch((sinkError) => {
|
| 6276 |
-
console.warn('Could not apply selected speaker for call:', sinkError);
|
| 6277 |
-
});
|
| 6278 |
-
}
|
| 6279 |
-
|
| 6280 |
-
|
| 6281 |
ensureRemoteMediaPlayback(remoteVideo);
|
| 6282 |
-
|
| 6283 |
-
// Fallback: start timer when media is actually flowing, even if connection state stays "connecting".
|
| 6284 |
-
if (!callTimerInterval) {
|
| 6285 |
-
startCallTimer();
|
| 6286 |
-
}
|
| 6287 |
-
|
| 6288 |
-
event.track.onunmute = () => {
|
| 6289 |
-
ensureRemoteMediaPlayback(remoteVideo);
|
| 6290 |
-
if (!callTimerInterval) {
|
| 6291 |
-
startCallTimer();
|
| 6292 |
-
}
|
| 6293 |
-
};
|
| 6294 |
}
|
| 6295 |
};
|
| 6296 |
-
|
| 6297 |
function showCallWarningThrottled(message) {
|
| 6298 |
const now = Date.now();
|
| 6299 |
if (now - lastCallWarningAt < 5000) return;
|
|
@@ -6314,12 +6285,14 @@ function createPeerConnection(peerId) {
|
|
| 6314 |
} else if (peerConnection.connectionState === 'connected') {
|
| 6315 |
callIceFailureCount = 0;
|
| 6316 |
showToast('📞 Call connected!', 'success');
|
| 6317 |
-
|
| 6318 |
-
|
|
|
|
|
|
|
|
|
|
| 6319 |
const remoteVidEl = document.getElementById('remoteCallVideo');
|
| 6320 |
if (remoteVidEl) {
|
| 6321 |
-
|
| 6322 |
-
if (remoteCallStream && !remoteVidEl.srcObject) {
|
| 6323 |
remoteVidEl.srcObject = remoteCallStream;
|
| 6324 |
}
|
| 6325 |
remoteVidEl.muted = false;
|
|
|
|
| 2828 |
const hasRemoteDescription = Boolean(peerConnection.remoteDescription && peerConnection.remoteDescription.type);
|
| 2829 |
|
| 2830 |
if (hasRemoteDescription) {
|
| 2831 |
+
try {
|
| 2832 |
+
await peerConnection.addIceCandidate(candidate);
|
| 2833 |
+
} catch (iceErr) {
|
| 2834 |
+
// Stale or duplicate ICE candidates are common and harmless after ICE restart
|
| 2835 |
+
console.warn('ICE candidate ignored:', iceErr.message);
|
| 2836 |
+
}
|
| 2837 |
} else {
|
| 2838 |
remoteIceCandidatesQueue.push(candidate);
|
| 2839 |
}
|
|
|
|
| 6145 |
try {
|
| 6146 |
const selectedMic = audioSettings?.microphoneId;
|
| 6147 |
const constraints = {
|
| 6148 |
+
audio: true, // Keep it simple for better compatibility
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6149 |
video: {
|
| 6150 |
facingMode: 'user',
|
| 6151 |
+
width: { min: 320, ideal: 640, max: 1280 }, // Use ranges, not fixed ideals
|
| 6152 |
+
height: { min: 240, ideal: 480, max: 720 }
|
| 6153 |
}
|
| 6154 |
};
|
| 6155 |
localCallStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
|
|
| 6249 |
};
|
| 6250 |
|
| 6251 |
peerConnection.ontrack = (event) => {
|
| 6252 |
+
// Add the incoming track to our managed MediaStream so srcObject stays consistent.
|
| 6253 |
+
if (event.track && remoteCallStream) {
|
| 6254 |
+
if (!remoteCallStream.getTrackById(event.track.id)) {
|
| 6255 |
+
remoteCallStream.addTrack(event.track);
|
| 6256 |
+
}
|
| 6257 |
+
}
|
| 6258 |
const remoteVideo = document.getElementById('remoteCallVideo');
|
| 6259 |
if (remoteVideo) {
|
| 6260 |
+
if (remoteVideo.srcObject !== remoteCallStream) {
|
| 6261 |
+
remoteVideo.srcObject = remoteCallStream;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6262 |
}
|
|
|
|
|
|
|
| 6263 |
remoteVideo.muted = false;
|
| 6264 |
remoteVideo.volume = 1.0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6265 |
ensureRemoteMediaPlayback(remoteVideo);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6266 |
}
|
| 6267 |
};
|
|
|
|
| 6268 |
function showCallWarningThrottled(message) {
|
| 6269 |
const now = Date.now();
|
| 6270 |
if (now - lastCallWarningAt < 5000) return;
|
|
|
|
| 6285 |
} else if (peerConnection.connectionState === 'connected') {
|
| 6286 |
callIceFailureCount = 0;
|
| 6287 |
showToast('📞 Call connected!', 'success');
|
| 6288 |
+
// Start timer only if not already running (avoids resetting a running clock)
|
| 6289 |
+
if (!callTimerInterval) {
|
| 6290 |
+
startCallTimer();
|
| 6291 |
+
}
|
| 6292 |
+
// Ensure remote media plays after ICE recovery
|
| 6293 |
const remoteVidEl = document.getElementById('remoteCallVideo');
|
| 6294 |
if (remoteVidEl) {
|
| 6295 |
+
if (!remoteVidEl.srcObject && remoteCallStream) {
|
|
|
|
| 6296 |
remoteVidEl.srcObject = remoteCallStream;
|
| 6297 |
}
|
| 6298 |
remoteVidEl.muted = false;
|
index.html
CHANGED
|
@@ -934,13 +934,18 @@
|
|
| 934 |
/* ==================== CALL OVERLAY ==================== */
|
| 935 |
#callOverlay {
|
| 936 |
position: fixed;
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
|
|
|
|
|
|
| 940 |
display: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 941 |
flex-direction: column;
|
| 942 |
}
|
| 943 |
-
#callOverlay.active { display: flex; }
|
| 944 |
#remoteCallVideo, #localCallVideo, #localCallVideoOld {
|
| 945 |
display: block;
|
| 946 |
}
|
|
@@ -3061,7 +3066,7 @@
|
|
| 3061 |
<div class="chat-header-title" id="chatTitle">💬 Select a friend to chat</div>
|
| 3062 |
</div>
|
| 3063 |
<div class="chat-header-actions">
|
| 3064 |
-
<span class="build-badge mobile-essential" id="buildVersionBadge" title="Frontend build version">v2.
|
| 3065 |
<button class="header-btn mobile-essential" onclick="toggleArchiveChat()" title="Archive chat">
|
| 3066 |
<i class="fas fa-archive"></i>
|
| 3067 |
</button>
|
|
@@ -3678,7 +3683,7 @@
|
|
| 3678 |
|
| 3679 |
<script src="/socket.io/socket.io.js"></script>
|
| 3680 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
| 3681 |
-
<script src="client.js?v=2.
|
| 3682 |
</body>
|
| 3683 |
</html>
|
| 3684 |
|
|
|
|
| 934 |
/* ==================== CALL OVERLAY ==================== */
|
| 935 |
#callOverlay {
|
| 936 |
position: fixed;
|
| 937 |
+
top: 0;
|
| 938 |
+
left: 0;
|
| 939 |
+
width: 100%;
|
| 940 |
+
height: 100%;
|
| 941 |
+
z-index: 9999; /* Ensure it's on top of everything */
|
| 942 |
display: none;
|
| 943 |
+
touch-action: manipulation; /* Prevents double-tap zoom delay */
|
| 944 |
+
}
|
| 945 |
+
#callOverlay.active {
|
| 946 |
+
display: flex;
|
| 947 |
flex-direction: column;
|
| 948 |
}
|
|
|
|
| 949 |
#remoteCallVideo, #localCallVideo, #localCallVideoOld {
|
| 950 |
display: block;
|
| 951 |
}
|
|
|
|
| 3066 |
<div class="chat-header-title" id="chatTitle">💬 Select a friend to chat</div>
|
| 3067 |
</div>
|
| 3068 |
<div class="chat-header-actions">
|
| 3069 |
+
<span class="build-badge mobile-essential" id="buildVersionBadge" title="Frontend build version">v2.8.0</span>
|
| 3070 |
<button class="header-btn mobile-essential" onclick="toggleArchiveChat()" title="Archive chat">
|
| 3071 |
<i class="fas fa-archive"></i>
|
| 3072 |
</button>
|
|
|
|
| 3683 |
|
| 3684 |
<script src="/socket.io/socket.io.js"></script>
|
| 3685 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
| 3686 |
+
<script src="client.js?v=2.8.0"></script>
|
| 3687 |
</body>
|
| 3688 |
</html>
|
| 3689 |
|
package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"name": "discord-like-app",
|
| 3 |
-
"version": "2.
|
| 4 |
"main": "server.js",
|
| 5 |
"scripts": {
|
| 6 |
"dev": "nodemon server.js",
|
|
|
|
| 1 |
{
|
| 2 |
"name": "discord-like-app",
|
| 3 |
+
"version": "2.8.0",
|
| 4 |
"main": "server.js",
|
| 5 |
"scripts": {
|
| 6 |
"dev": "nodemon server.js",
|
server.js
CHANGED
|
@@ -98,7 +98,7 @@ app.get('/api/rtc-config', (req, res) => {
|
|
| 98 |
const turnUrlsRaw = process.env.TURN_URLS || '';
|
| 99 |
const turnUsername = process.env.TURN_USERNAME || '';
|
| 100 |
const turnCredential = process.env.TURN_CREDENTIAL || '';
|
| 101 |
-
const relayFlag = String(process.env.RTC_FORCE_RELAY || '
|
| 102 |
const forceRelay = relayFlag === 'true' || relayFlag === '1' || relayFlag === 'yes';
|
| 103 |
|
| 104 |
let iceServers = [...defaultIceServers];
|
|
|
|
| 98 |
const turnUrlsRaw = process.env.TURN_URLS || '';
|
| 99 |
const turnUsername = process.env.TURN_USERNAME || '';
|
| 100 |
const turnCredential = process.env.TURN_CREDENTIAL || '';
|
| 101 |
+
const relayFlag = String(process.env.RTC_FORCE_RELAY || 'false').toLowerCase();
|
| 102 |
const forceRelay = relayFlag === 'true' || relayFlag === '1' || relayFlag === 'yes';
|
| 103 |
|
| 104 |
let iceServers = [...defaultIceServers];
|