Dhurgh commited on
Commit
43c7d49
·
1 Parent(s): 10dde96

Fix video calls: allow STUN/P2P, fix ontrack stream, fix timer reset, fix ICE errors (v2.8.0)

Browse files
Files changed (4) hide show
  1. client.js +23 -50
  2. index.html +11 -6
  3. package.json +1 -1
  4. 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
- await peerConnection.addIceCandidate(candidate);
 
 
 
 
 
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: 720 },
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
- console.log('📞 Remote track received:', event.track.kind, event.streams.length);
 
 
 
 
 
6253
  const remoteVideo = document.getElementById('remoteCallVideo');
6254
  if (remoteVideo) {
6255
- if (event.streams && event.streams[0]) {
6256
- const incomingStream = event.streams[0];
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
- startCallTimer();
6318
- // Re-attach remote stream after ICE recovery so video/audio plays
 
 
 
6319
  const remoteVidEl = document.getElementById('remoteCallVideo');
6320
  if (remoteVidEl) {
6321
- // Only set srcObject when video currently has no stream to avoid replacing a valid incoming stream.
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
- inset: 0;
938
- background: #0a0a0a;
939
- z-index: 10000;
 
 
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.7.1</span>
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.7.1"></script>
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.7.1",
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 || 'true').toLowerCase();
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];