Spaces:
Runtime error
Runtime error
Replace phone call transport with hosted Jitsi room inside call window (v2.9.0)
Browse files- client.js +149 -22
- index.html +16 -2
- package.json +1 -1
client.js
CHANGED
|
@@ -34,6 +34,9 @@ let pendingIceRecoveryTimer = null;
|
|
| 34 |
let pendingRelayFallbackTimer = null;
|
| 35 |
let pendingRemoteIceCandidates = [];
|
| 36 |
let callEventLog = [];
|
|
|
|
|
|
|
|
|
|
| 37 |
let hasUserInteractedWithPage = false;
|
| 38 |
let activeRoomSubscription = null;
|
| 39 |
let roomMessages = JSON.parse(localStorage.getItem('roomMessages') || '{}');
|
|
@@ -383,6 +386,122 @@ function setCallStatus(statusText) {
|
|
| 383 |
}
|
| 384 |
}
|
| 385 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
let callControlBindingsDone = false;
|
| 387 |
function bindCallControlButtons() {
|
| 388 |
if (callControlBindingsDone) return;
|
|
@@ -1034,12 +1153,12 @@ function connectSocket() {
|
|
| 1034 |
const byName = payload?.byUsername || 'User';
|
| 1035 |
stopIncomingCallAlert();
|
| 1036 |
showToast(`${byName} accepted the call`, 'success');
|
| 1037 |
-
const
|
| 1038 |
-
if (
|
| 1039 |
closeCustomDialog();
|
| 1040 |
-
|
| 1041 |
-
console.error('Failed to start
|
| 1042 |
-
showToast('Unable to
|
| 1043 |
cleanupCallSession(false);
|
| 1044 |
});
|
| 1045 |
}
|
|
@@ -6132,20 +6251,6 @@ function openVideoCallModal(targetName, targetId, callType) {
|
|
| 6132 |
setCallStatus('Dialing...');
|
| 6133 |
appendCallLog(`Outgoing ${callType.toLowerCase()} to ${targetName}`);
|
| 6134 |
|
| 6135 |
-
ensureRtcConfigLoaded()
|
| 6136 |
-
.then(() => ensureLocalCallStream())
|
| 6137 |
-
.then(() => {
|
| 6138 |
-
const localVideo = document.getElementById('localCallVideo');
|
| 6139 |
-
if (localVideo && localCallStream) {
|
| 6140 |
-
localVideo.srcObject = localCallStream;
|
| 6141 |
-
ensureLocalPreviewPlayback(localVideo);
|
| 6142 |
-
appendCallLog('Local preview started before answer');
|
| 6143 |
-
}
|
| 6144 |
-
})
|
| 6145 |
-
.catch((error) => {
|
| 6146 |
-
appendCallLog(`Local preview unavailable: ${error?.message || error}`);
|
| 6147 |
-
});
|
| 6148 |
-
|
| 6149 |
if (socket && socket.connected && targetId) {
|
| 6150 |
socket.emit('start video call', {
|
| 6151 |
targetId: targetId,
|
|
@@ -6184,9 +6289,9 @@ function handleIncomingVideoCall(payload) {
|
|
| 6184 |
toId: activeCallState.peerId
|
| 6185 |
});
|
| 6186 |
closeCustomDialog();
|
| 6187 |
-
|
| 6188 |
-
console.error('Failed to initialize
|
| 6189 |
-
showToast('Unable to
|
| 6190 |
cleanupCallSession(false);
|
| 6191 |
});
|
| 6192 |
|
|
@@ -6613,6 +6718,9 @@ async function startCallSession(peerName, peerId, isCaller) {
|
|
| 6613 |
}
|
| 6614 |
|
| 6615 |
function endVideoCall() {
|
|
|
|
|
|
|
|
|
|
| 6616 |
|
| 6617 |
window.userIntentionallEndedCall = true;
|
| 6618 |
|
|
@@ -6628,6 +6736,10 @@ function endVideoCall() {
|
|
| 6628 |
}
|
| 6629 |
|
| 6630 |
function toggleLocalMute() {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6631 |
if (!localCallStream) {
|
| 6632 |
showToast('Microphone unavailable on this device/browser', 'warning');
|
| 6633 |
return;
|
|
@@ -6655,6 +6767,10 @@ function toggleLocalMute() {
|
|
| 6655 |
}
|
| 6656 |
|
| 6657 |
function toggleCallCamera() {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6658 |
if (!localCallStream) {
|
| 6659 |
showToast('Camera unavailable on this device/browser', 'warning');
|
| 6660 |
return;
|
|
@@ -6734,6 +6850,17 @@ function cleanupCallSession(keepDialogOpen = false) {
|
|
| 6734 |
stopIncomingCallAlert();
|
| 6735 |
stopCallTimer();
|
| 6736 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6737 |
if (peerConnection) {
|
| 6738 |
peerConnection.onicecandidate = null;
|
| 6739 |
peerConnection.ontrack = null;
|
|
|
|
| 34 |
let pendingRelayFallbackTimer = null;
|
| 35 |
let pendingRemoteIceCandidates = [];
|
| 36 |
let callEventLog = [];
|
| 37 |
+
let currentCallProvider = 'webrtc';
|
| 38 |
+
let currentJitsiApi = null;
|
| 39 |
+
let currentCallRoomName = null;
|
| 40 |
let hasUserInteractedWithPage = false;
|
| 41 |
let activeRoomSubscription = null;
|
| 42 |
let roomMessages = JSON.parse(localStorage.getItem('roomMessages') || '{}');
|
|
|
|
| 386 |
}
|
| 387 |
}
|
| 388 |
|
| 389 |
+
function ensureJitsiApiLoaded() {
|
| 390 |
+
if (typeof window.JitsiMeetExternalAPI === 'function') {
|
| 391 |
+
return Promise.resolve();
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
return new Promise((resolve, reject) => {
|
| 395 |
+
const existing = document.querySelector('script[data-jitsi-external-api="true"]');
|
| 396 |
+
if (existing) {
|
| 397 |
+
existing.addEventListener('load', () => resolve(), { once: true });
|
| 398 |
+
existing.addEventListener('error', () => reject(new Error('Failed to load hosted call API')), { once: true });
|
| 399 |
+
return;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
const script = document.createElement('script');
|
| 403 |
+
script.src = 'https://meet.jit.si/external_api.js';
|
| 404 |
+
script.async = true;
|
| 405 |
+
script.dataset.jitsiExternalApi = 'true';
|
| 406 |
+
script.onload = () => resolve();
|
| 407 |
+
script.onerror = () => reject(new Error('Failed to load hosted call API'));
|
| 408 |
+
document.head.appendChild(script);
|
| 409 |
+
});
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
function setHostedCallVisible(isVisible) {
|
| 413 |
+
const host = document.getElementById('hostedCallContainer');
|
| 414 |
+
const remoteVideo = document.getElementById('remoteCallVideo');
|
| 415 |
+
const localVideo = document.getElementById('localCallVideo');
|
| 416 |
+
if (host) host.style.display = isVisible ? 'block' : 'none';
|
| 417 |
+
if (remoteVideo) remoteVideo.style.display = isVisible ? 'none' : 'block';
|
| 418 |
+
if (localVideo) localVideo.style.display = isVisible ? 'none' : 'block';
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
async function startHostedCallRoom(peerName, roomName) {
|
| 422 |
+
if (!roomName) {
|
| 423 |
+
throw new Error('Missing hosted room name');
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
openCallDialog(peerName);
|
| 427 |
+
setCallStatus('Joining hosted call...');
|
| 428 |
+
appendCallLog(`Joining hosted room ${roomName}`);
|
| 429 |
+
|
| 430 |
+
await ensureJitsiApiLoaded();
|
| 431 |
+
|
| 432 |
+
const host = document.getElementById('hostedCallContainer');
|
| 433 |
+
if (!host) {
|
| 434 |
+
throw new Error('Hosted call container not found');
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
if (currentJitsiApi) {
|
| 438 |
+
currentJitsiApi.dispose();
|
| 439 |
+
currentJitsiApi = null;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
host.innerHTML = '';
|
| 443 |
+
setHostedCallVisible(true);
|
| 444 |
+
currentCallProvider = 'hosted';
|
| 445 |
+
currentCallRoomName = roomName;
|
| 446 |
+
|
| 447 |
+
currentJitsiApi = new window.JitsiMeetExternalAPI('meet.jit.si', {
|
| 448 |
+
roomName,
|
| 449 |
+
parentNode: host,
|
| 450 |
+
width: '100%',
|
| 451 |
+
height: '100%',
|
| 452 |
+
userInfo: {
|
| 453 |
+
displayName: currentUser?.username || 'User'
|
| 454 |
+
},
|
| 455 |
+
configOverwrite: {
|
| 456 |
+
prejoinPageEnabled: false,
|
| 457 |
+
startWithAudioMuted: false,
|
| 458 |
+
startWithVideoMuted: false,
|
| 459 |
+
disableDeepLinking: true
|
| 460 |
+
},
|
| 461 |
+
interfaceConfigOverwrite: {
|
| 462 |
+
MOBILE_APP_PROMO: false,
|
| 463 |
+
SHOW_JITSI_WATERMARK: false,
|
| 464 |
+
SHOW_WATERMARK_FOR_GUESTS: false
|
| 465 |
+
}
|
| 466 |
+
});
|
| 467 |
+
|
| 468 |
+
currentJitsiApi.addListener('videoConferenceJoined', () => {
|
| 469 |
+
setCallStatus('Connected');
|
| 470 |
+
appendCallLog('Hosted room joined');
|
| 471 |
+
if (!callTimerInterval) {
|
| 472 |
+
startCallTimer();
|
| 473 |
+
}
|
| 474 |
+
});
|
| 475 |
+
currentJitsiApi.addListener('participantJoined', (payload) => {
|
| 476 |
+
appendCallLog(`Participant joined: ${payload?.displayName || payload?.id || 'remote user'}`);
|
| 477 |
+
});
|
| 478 |
+
currentJitsiApi.addListener('participantLeft', (payload) => {
|
| 479 |
+
appendCallLog(`Participant left: ${payload?.id || 'remote user'}`);
|
| 480 |
+
});
|
| 481 |
+
currentJitsiApi.addListener('audioMuteStatusChanged', ({ muted }) => {
|
| 482 |
+
const btn = document.getElementById('muteBtn');
|
| 483 |
+
if (btn) {
|
| 484 |
+
btn.textContent = muted ? '🔇' : '🎤';
|
| 485 |
+
btn.classList.toggle('muted', !!muted);
|
| 486 |
+
}
|
| 487 |
+
});
|
| 488 |
+
currentJitsiApi.addListener('videoMuteStatusChanged', ({ muted }) => {
|
| 489 |
+
const btn = document.getElementById('cameraBtn');
|
| 490 |
+
if (btn) {
|
| 491 |
+
btn.textContent = muted ? '🚫' : '📷';
|
| 492 |
+
btn.classList.toggle('cam-off', !!muted);
|
| 493 |
+
}
|
| 494 |
+
});
|
| 495 |
+
currentJitsiApi.addListener('readyToClose', () => {
|
| 496 |
+
appendCallLog('Hosted room requested close');
|
| 497 |
+
cleanupCallSession(false);
|
| 498 |
+
});
|
| 499 |
+
currentJitsiApi.addListener('videoConferenceLeft', () => {
|
| 500 |
+
appendCallLog('Hosted room left');
|
| 501 |
+
cleanupCallSession(false);
|
| 502 |
+
});
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
let callControlBindingsDone = false;
|
| 506 |
function bindCallControlButtons() {
|
| 507 |
if (callControlBindingsDone) return;
|
|
|
|
| 1153 |
const byName = payload?.byUsername || 'User';
|
| 1154 |
stopIncomingCallAlert();
|
| 1155 |
showToast(`${byName} accepted the call`, 'success');
|
| 1156 |
+
const roomName = activeCallState?.callId || payload?.callId;
|
| 1157 |
+
if (roomName) {
|
| 1158 |
closeCustomDialog();
|
| 1159 |
+
startHostedCallRoom(byName, `firetech-${roomName}`).catch((error) => {
|
| 1160 |
+
console.error('Failed to start hosted call session:', error);
|
| 1161 |
+
showToast('Unable to join hosted call', 'error');
|
| 1162 |
cleanupCallSession(false);
|
| 1163 |
});
|
| 1164 |
}
|
|
|
|
| 6251 |
setCallStatus('Dialing...');
|
| 6252 |
appendCallLog(`Outgoing ${callType.toLowerCase()} to ${targetName}`);
|
| 6253 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6254 |
if (socket && socket.connected && targetId) {
|
| 6255 |
socket.emit('start video call', {
|
| 6256 |
targetId: targetId,
|
|
|
|
| 6289 |
toId: activeCallState.peerId
|
| 6290 |
});
|
| 6291 |
closeCustomDialog();
|
| 6292 |
+
startHostedCallRoom(callerName, `firetech-${activeCallState.callId}`).catch((error) => {
|
| 6293 |
+
console.error('Failed to initialize hosted call session:', error);
|
| 6294 |
+
showToast('Unable to join hosted call', 'error');
|
| 6295 |
cleanupCallSession(false);
|
| 6296 |
});
|
| 6297 |
|
|
|
|
| 6718 |
}
|
| 6719 |
|
| 6720 |
function endVideoCall() {
|
| 6721 |
+
if (currentCallProvider === 'hosted' && currentJitsiApi) {
|
| 6722 |
+
currentJitsiApi.executeCommand('hangup');
|
| 6723 |
+
}
|
| 6724 |
|
| 6725 |
window.userIntentionallEndedCall = true;
|
| 6726 |
|
|
|
|
| 6736 |
}
|
| 6737 |
|
| 6738 |
function toggleLocalMute() {
|
| 6739 |
+
if (currentCallProvider === 'hosted' && currentJitsiApi) {
|
| 6740 |
+
currentJitsiApi.executeCommand('toggleAudio');
|
| 6741 |
+
return;
|
| 6742 |
+
}
|
| 6743 |
if (!localCallStream) {
|
| 6744 |
showToast('Microphone unavailable on this device/browser', 'warning');
|
| 6745 |
return;
|
|
|
|
| 6767 |
}
|
| 6768 |
|
| 6769 |
function toggleCallCamera() {
|
| 6770 |
+
if (currentCallProvider === 'hosted' && currentJitsiApi) {
|
| 6771 |
+
currentJitsiApi.executeCommand('toggleVideo');
|
| 6772 |
+
return;
|
| 6773 |
+
}
|
| 6774 |
if (!localCallStream) {
|
| 6775 |
showToast('Camera unavailable on this device/browser', 'warning');
|
| 6776 |
return;
|
|
|
|
| 6850 |
stopIncomingCallAlert();
|
| 6851 |
stopCallTimer();
|
| 6852 |
|
| 6853 |
+
if (currentJitsiApi) {
|
| 6854 |
+
try {
|
| 6855 |
+
currentJitsiApi.dispose();
|
| 6856 |
+
} catch (_) {
|
| 6857 |
+
}
|
| 6858 |
+
currentJitsiApi = null;
|
| 6859 |
+
}
|
| 6860 |
+
currentCallProvider = 'webrtc';
|
| 6861 |
+
currentCallRoomName = null;
|
| 6862 |
+
setHostedCallVisible(false);
|
| 6863 |
+
|
| 6864 |
if (peerConnection) {
|
| 6865 |
peerConnection.onicecandidate = null;
|
| 6866 |
peerConnection.ontrack = null;
|
index.html
CHANGED
|
@@ -969,6 +969,18 @@
|
|
| 969 |
#remoteCallVideo, #localCallVideo, #localCallVideoOld {
|
| 970 |
display: block;
|
| 971 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 972 |
#remoteCallVideo {
|
| 973 |
pointer-events: none;
|
| 974 |
}
|
|
@@ -3111,7 +3123,7 @@
|
|
| 3111 |
<div class="chat-header-title" id="chatTitle">💬 Select a friend to chat</div>
|
| 3112 |
</div>
|
| 3113 |
<div class="chat-header-actions">
|
| 3114 |
-
<span class="build-badge mobile-essential" id="buildVersionBadge" title="Frontend build version">v2.
|
| 3115 |
<button class="header-btn mobile-essential" onclick="toggleArchiveChat()" title="Archive chat">
|
| 3116 |
<i class="fas fa-archive"></i>
|
| 3117 |
</button>
|
|
@@ -3693,6 +3705,7 @@
|
|
| 3693 |
<!-- ========== FULL-SCREEN CALL OVERLAY ========== -->
|
| 3694 |
<div id="callOverlay">
|
| 3695 |
<div class="call-overlay-stage">
|
|
|
|
| 3696 |
<video id="remoteCallVideo" autoplay playsinline style="position:absolute; inset:0; width:100%; height:100%; object-fit:cover; z-index:0;"></video>
|
| 3697 |
<video id="localCallVideo" autoplay muted playsinline style="position:absolute; bottom:110px; right:14px; width:110px; height:150px; border-radius:12px; border:2px solid rgba(255,255,255,0.6); object-fit:cover; z-index:2;"></video>
|
| 3698 |
|
|
@@ -3730,8 +3743,9 @@
|
|
| 3730 |
</div>
|
| 3731 |
|
| 3732 |
<script src="/socket.io/socket.io.js"></script>
|
|
|
|
| 3733 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
| 3734 |
-
<script src="client.js?v=2.
|
| 3735 |
</body>
|
| 3736 |
</html>
|
| 3737 |
|
|
|
|
| 969 |
#remoteCallVideo, #localCallVideo, #localCallVideoOld {
|
| 970 |
display: block;
|
| 971 |
}
|
| 972 |
+
#hostedCallContainer {
|
| 973 |
+
position: absolute;
|
| 974 |
+
inset: 0;
|
| 975 |
+
z-index: 1;
|
| 976 |
+
display: none;
|
| 977 |
+
background: #0f1116;
|
| 978 |
+
}
|
| 979 |
+
#hostedCallContainer iframe {
|
| 980 |
+
width: 100%;
|
| 981 |
+
height: 100%;
|
| 982 |
+
border: 0;
|
| 983 |
+
}
|
| 984 |
#remoteCallVideo {
|
| 985 |
pointer-events: none;
|
| 986 |
}
|
|
|
|
| 3123 |
<div class="chat-header-title" id="chatTitle">💬 Select a friend to chat</div>
|
| 3124 |
</div>
|
| 3125 |
<div class="chat-header-actions">
|
| 3126 |
+
<span class="build-badge mobile-essential" id="buildVersionBadge" title="Frontend build version">v2.9.0</span>
|
| 3127 |
<button class="header-btn mobile-essential" onclick="toggleArchiveChat()" title="Archive chat">
|
| 3128 |
<i class="fas fa-archive"></i>
|
| 3129 |
</button>
|
|
|
|
| 3705 |
<!-- ========== FULL-SCREEN CALL OVERLAY ========== -->
|
| 3706 |
<div id="callOverlay">
|
| 3707 |
<div class="call-overlay-stage">
|
| 3708 |
+
<div id="hostedCallContainer"></div>
|
| 3709 |
<video id="remoteCallVideo" autoplay playsinline style="position:absolute; inset:0; width:100%; height:100%; object-fit:cover; z-index:0;"></video>
|
| 3710 |
<video id="localCallVideo" autoplay muted playsinline style="position:absolute; bottom:110px; right:14px; width:110px; height:150px; border-radius:12px; border:2px solid rgba(255,255,255,0.6); object-fit:cover; z-index:2;"></video>
|
| 3711 |
|
|
|
|
| 3743 |
</div>
|
| 3744 |
|
| 3745 |
<script src="/socket.io/socket.io.js"></script>
|
| 3746 |
+
<script src="https://meet.jit.si/external_api.js"></script>
|
| 3747 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
| 3748 |
+
<script src="client.js?v=2.9.0"></script>
|
| 3749 |
</body>
|
| 3750 |
</html>
|
| 3751 |
|
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.9.0",
|
| 4 |
"main": "server.js",
|
| 5 |
"scripts": {
|
| 6 |
"dev": "nodemon server.js",
|