Spaces:
Runtime error
Runtime error
Rewrite call UX reliability: receive-only fallback, playback retries, mobile controls (v2.7.1)
Browse files- client.js +61 -34
- index.html +17 -4
- package.json +1 -1
client.js
CHANGED
|
@@ -295,41 +295,50 @@ function formatMessageTimestamp(timestamp) {
|
|
| 295 |
function ensureRemoteMediaPlayback(videoEl) {
|
| 296 |
if (!videoEl) return;
|
| 297 |
|
| 298 |
-
const
|
| 299 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
remoteCallPlaybackStarted = true;
|
| 304 |
-
showToast('📞 Connected! Audio and video are live.', 'success');
|
| 305 |
-
}
|
| 306 |
-
}).catch((error) => {
|
| 307 |
-
if (error?.name === 'AbortError') {
|
| 308 |
-
setTimeout(() => {
|
| 309 |
-
if (videoEl.srcObject) {
|
| 310 |
-
videoEl.play().catch(() => {});
|
| 311 |
-
}
|
| 312 |
-
}, 120);
|
| 313 |
-
return;
|
| 314 |
-
}
|
| 315 |
|
| 316 |
-
|
| 317 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
console.warn('Retry playback failed:', retryErr);
|
| 324 |
-
});
|
| 325 |
-
document.removeEventListener('click', retryPlayback);
|
| 326 |
-
document.removeEventListener('touchstart', retryPlayback);
|
| 327 |
-
remoteCallPlaybackRetryBound = false;
|
| 328 |
-
};
|
| 329 |
-
document.addEventListener('click', retryPlayback, { once: true });
|
| 330 |
-
document.addEventListener('touchstart', retryPlayback, { once: true });
|
| 331 |
-
}
|
| 332 |
-
});
|
| 333 |
}
|
| 334 |
|
| 335 |
|
|
@@ -6435,6 +6444,10 @@ async function startCallSession(peerName, peerId, isCaller) {
|
|
| 6435 |
forceRelayOnly = rtcRuntimeConfig?.iceTransportPolicy === 'relay';
|
| 6436 |
|
| 6437 |
openCallDialog(peerName);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6438 |
await ensureRtcConfigLoaded();
|
| 6439 |
try {
|
| 6440 |
await ensureLocalCallStream();
|
|
@@ -6512,9 +6525,16 @@ function endVideoCall() {
|
|
| 6512 |
}
|
| 6513 |
|
| 6514 |
function toggleLocalMute() {
|
| 6515 |
-
if (!localCallStream)
|
|
|
|
|
|
|
|
|
|
| 6516 |
|
| 6517 |
const audioTracks = localCallStream.getAudioTracks();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6518 |
const currentlyEnabled = audioTracks.some(track => track.enabled);
|
| 6519 |
|
| 6520 |
audioTracks.forEach(track => {
|
|
@@ -6532,8 +6552,15 @@ function toggleLocalMute() {
|
|
| 6532 |
}
|
| 6533 |
|
| 6534 |
function toggleCallCamera() {
|
| 6535 |
-
if (!localCallStream)
|
|
|
|
|
|
|
|
|
|
| 6536 |
const videoTracks = localCallStream.getVideoTracks();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6537 |
isCallCameraOff = !isCallCameraOff;
|
| 6538 |
videoTracks.forEach(track => { track.enabled = !isCallCameraOff; });
|
| 6539 |
const btn = document.getElementById('cameraBtn');
|
|
|
|
| 295 |
function ensureRemoteMediaPlayback(videoEl) {
|
| 296 |
if (!videoEl) return;
|
| 297 |
|
| 298 |
+
const tryPlay = () => {
|
| 299 |
+
const playPromise = videoEl.play();
|
| 300 |
+
if (!playPromise || typeof playPromise.then !== 'function') return;
|
| 301 |
+
|
| 302 |
+
playPromise.then(() => {
|
| 303 |
+
if (!remoteCallPlaybackStarted) {
|
| 304 |
+
remoteCallPlaybackStarted = true;
|
| 305 |
+
showToast('📞 Connected! Audio and video are live.', 'success');
|
| 306 |
+
}
|
| 307 |
+
}).catch((error) => {
|
| 308 |
+
if (error?.name === 'AbortError') {
|
| 309 |
+
setTimeout(() => {
|
| 310 |
+
if (videoEl.srcObject) {
|
| 311 |
+
videoEl.play().catch(() => {});
|
| 312 |
+
}
|
| 313 |
+
}, 120);
|
| 314 |
+
return;
|
| 315 |
+
}
|
| 316 |
|
| 317 |
+
console.warn('Remote media autoplay blocked, waiting for user gesture:', error);
|
| 318 |
+
showToast('Tap once to enable call audio', 'warning');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
|
| 320 |
+
if (!remoteCallPlaybackRetryBound) {
|
| 321 |
+
remoteCallPlaybackRetryBound = true;
|
| 322 |
+
const retryPlayback = () => {
|
| 323 |
+
videoEl.play().catch((retryErr) => {
|
| 324 |
+
console.warn('Retry playback failed:', retryErr);
|
| 325 |
+
});
|
| 326 |
+
document.removeEventListener('click', retryPlayback);
|
| 327 |
+
document.removeEventListener('touchstart', retryPlayback);
|
| 328 |
+
document.removeEventListener('pointerdown', retryPlayback);
|
| 329 |
+
remoteCallPlaybackRetryBound = false;
|
| 330 |
+
};
|
| 331 |
+
document.addEventListener('click', retryPlayback, { once: true });
|
| 332 |
+
document.addEventListener('touchstart', retryPlayback, { once: true, passive: true });
|
| 333 |
+
document.addEventListener('pointerdown', retryPlayback, { once: true });
|
| 334 |
+
}
|
| 335 |
+
});
|
| 336 |
+
};
|
| 337 |
|
| 338 |
+
tryPlay();
|
| 339 |
+
// Extra retry loop for slow mobile media pipelines.
|
| 340 |
+
setTimeout(tryPlay, 250);
|
| 341 |
+
setTimeout(tryPlay, 900);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
}
|
| 343 |
|
| 344 |
|
|
|
|
| 6444 |
forceRelayOnly = rtcRuntimeConfig?.iceTransportPolicy === 'relay';
|
| 6445 |
|
| 6446 |
openCallDialog(peerName);
|
| 6447 |
+
// Start timer from session start to avoid "stuck at 00:00" perception on unstable networks.
|
| 6448 |
+
if (!callTimerInterval) {
|
| 6449 |
+
startCallTimer();
|
| 6450 |
+
}
|
| 6451 |
await ensureRtcConfigLoaded();
|
| 6452 |
try {
|
| 6453 |
await ensureLocalCallStream();
|
|
|
|
| 6525 |
}
|
| 6526 |
|
| 6527 |
function toggleLocalMute() {
|
| 6528 |
+
if (!localCallStream) {
|
| 6529 |
+
showToast('Microphone unavailable on this device/browser', 'warning');
|
| 6530 |
+
return;
|
| 6531 |
+
}
|
| 6532 |
|
| 6533 |
const audioTracks = localCallStream.getAudioTracks();
|
| 6534 |
+
if (!audioTracks.length) {
|
| 6535 |
+
showToast('No microphone track available', 'warning');
|
| 6536 |
+
return;
|
| 6537 |
+
}
|
| 6538 |
const currentlyEnabled = audioTracks.some(track => track.enabled);
|
| 6539 |
|
| 6540 |
audioTracks.forEach(track => {
|
|
|
|
| 6552 |
}
|
| 6553 |
|
| 6554 |
function toggleCallCamera() {
|
| 6555 |
+
if (!localCallStream) {
|
| 6556 |
+
showToast('Camera unavailable on this device/browser', 'warning');
|
| 6557 |
+
return;
|
| 6558 |
+
}
|
| 6559 |
const videoTracks = localCallStream.getVideoTracks();
|
| 6560 |
+
if (!videoTracks.length) {
|
| 6561 |
+
showToast('No camera track available', 'warning');
|
| 6562 |
+
return;
|
| 6563 |
+
}
|
| 6564 |
isCallCameraOff = !isCallCameraOff;
|
| 6565 |
videoTracks.forEach(track => { track.enabled = !isCallCameraOff; });
|
| 6566 |
const btn = document.getElementById('cameraBtn');
|
index.html
CHANGED
|
@@ -944,15 +944,19 @@
|
|
| 944 |
#remoteCallVideo, #localCallVideo, #localCallVideoOld {
|
| 945 |
display: block;
|
| 946 |
}
|
|
|
|
|
|
|
|
|
|
| 947 |
.call-overlay-header {
|
| 948 |
position: absolute;
|
| 949 |
top: 0; left: 0; right: 0;
|
| 950 |
padding: 16px 20px;
|
| 951 |
background: linear-gradient(180deg, rgba(0,0,0,0.75), transparent);
|
| 952 |
-
z-index:
|
| 953 |
display: flex;
|
| 954 |
align-items: center;
|
| 955 |
justify-content: space-between;
|
|
|
|
| 956 |
}
|
| 957 |
.call-overlay-controls {
|
| 958 |
position: absolute;
|
|
@@ -962,7 +966,9 @@
|
|
| 962 |
display: flex;
|
| 963 |
justify-content: center;
|
| 964 |
gap: 20px;
|
| 965 |
-
z-index:
|
|
|
|
|
|
|
| 966 |
}
|
| 967 |
.call-ctrl-btn {
|
| 968 |
width: 64px; height: 64px;
|
|
@@ -974,8 +980,12 @@
|
|
| 974 |
cursor: pointer;
|
| 975 |
display: flex; align-items: center; justify-content: center;
|
| 976 |
transition: all 0.2s;
|
|
|
|
|
|
|
|
|
|
| 977 |
}
|
| 978 |
.call-ctrl-btn:hover { background: rgba(255,255,255,0.35); transform: scale(1.08); }
|
|
|
|
| 979 |
.call-ctrl-btn.muted { background: rgba(240,71,71,0.5); border-color: #f04747; }
|
| 980 |
.call-ctrl-btn.cam-off { background: rgba(240,71,71,0.5); border-color: #f04747; }
|
| 981 |
.call-end-btn {
|
|
@@ -988,6 +998,9 @@
|
|
| 988 |
cursor: pointer;
|
| 989 |
display: flex; align-items: center; justify-content: center;
|
| 990 |
transition: all 0.2s;
|
|
|
|
|
|
|
|
|
|
| 991 |
}
|
| 992 |
.call-end-btn:hover { background: #c0392b; transform: scale(1.1); }
|
| 993 |
#callDebugPanel {
|
|
@@ -3048,7 +3061,7 @@
|
|
| 3048 |
<div class="chat-header-title" id="chatTitle">💬 Select a friend to chat</div>
|
| 3049 |
</div>
|
| 3050 |
<div class="chat-header-actions">
|
| 3051 |
-
<span class="build-badge mobile-essential" id="buildVersionBadge" title="Frontend build version">v2.7.
|
| 3052 |
<button class="header-btn mobile-essential" onclick="toggleArchiveChat()" title="Archive chat">
|
| 3053 |
<i class="fas fa-archive"></i>
|
| 3054 |
</button>
|
|
@@ -3665,7 +3678,7 @@
|
|
| 3665 |
|
| 3666 |
<script src="/socket.io/socket.io.js"></script>
|
| 3667 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
| 3668 |
-
<script src="client.js?v=2.7.
|
| 3669 |
</body>
|
| 3670 |
</html>
|
| 3671 |
|
|
|
|
| 944 |
#remoteCallVideo, #localCallVideo, #localCallVideoOld {
|
| 945 |
display: block;
|
| 946 |
}
|
| 947 |
+
#remoteCallVideo {
|
| 948 |
+
pointer-events: none;
|
| 949 |
+
}
|
| 950 |
.call-overlay-header {
|
| 951 |
position: absolute;
|
| 952 |
top: 0; left: 0; right: 0;
|
| 953 |
padding: 16px 20px;
|
| 954 |
background: linear-gradient(180deg, rgba(0,0,0,0.75), transparent);
|
| 955 |
+
z-index: 4;
|
| 956 |
display: flex;
|
| 957 |
align-items: center;
|
| 958 |
justify-content: space-between;
|
| 959 |
+
pointer-events: auto;
|
| 960 |
}
|
| 961 |
.call-overlay-controls {
|
| 962 |
position: absolute;
|
|
|
|
| 966 |
display: flex;
|
| 967 |
justify-content: center;
|
| 968 |
gap: 20px;
|
| 969 |
+
z-index: 5;
|
| 970 |
+
pointer-events: auto;
|
| 971 |
+
touch-action: manipulation;
|
| 972 |
}
|
| 973 |
.call-ctrl-btn {
|
| 974 |
width: 64px; height: 64px;
|
|
|
|
| 980 |
cursor: pointer;
|
| 981 |
display: flex; align-items: center; justify-content: center;
|
| 982 |
transition: all 0.2s;
|
| 983 |
+
pointer-events: auto;
|
| 984 |
+
touch-action: manipulation;
|
| 985 |
+
-webkit-tap-highlight-color: transparent;
|
| 986 |
}
|
| 987 |
.call-ctrl-btn:hover { background: rgba(255,255,255,0.35); transform: scale(1.08); }
|
| 988 |
+
.call-ctrl-btn:active { transform: scale(0.96); }
|
| 989 |
.call-ctrl-btn.muted { background: rgba(240,71,71,0.5); border-color: #f04747; }
|
| 990 |
.call-ctrl-btn.cam-off { background: rgba(240,71,71,0.5); border-color: #f04747; }
|
| 991 |
.call-end-btn {
|
|
|
|
| 998 |
cursor: pointer;
|
| 999 |
display: flex; align-items: center; justify-content: center;
|
| 1000 |
transition: all 0.2s;
|
| 1001 |
+
pointer-events: auto;
|
| 1002 |
+
touch-action: manipulation;
|
| 1003 |
+
-webkit-tap-highlight-color: transparent;
|
| 1004 |
}
|
| 1005 |
.call-end-btn:hover { background: #c0392b; transform: scale(1.1); }
|
| 1006 |
#callDebugPanel {
|
|
|
|
| 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 |
|
| 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 |
|
package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"name": "discord-like-app",
|
| 3 |
-
"version": "2.7.
|
| 4 |
"main": "server.js",
|
| 5 |
"scripts": {
|
| 6 |
"dev": "nodemon server.js",
|
|
|
|
| 1 |
{
|
| 2 |
"name": "discord-like-app",
|
| 3 |
+
"version": "2.7.1",
|
| 4 |
"main": "server.js",
|
| 5 |
"scripts": {
|
| 6 |
"dev": "nodemon server.js",
|