diff --git a/resources/views/interview/candidate_room.blade.php b/resources/views/interview/candidate_room.blade.php
index 4b75d5f..060828d 100644
--- a/resources/views/interview/candidate_room.blade.php
+++ b/resources/views/interview/candidate_room.blade.php
@@ -13,6 +13,7 @@
+
@@ -428,7 +429,7 @@
javascript: "// JavaScript (Node.js) Solution\nconsole.log(\"Hello from SingleLogin Assessment!\");\nconst nums = [10, 20, 30];\nconsole.log(\"Total:\", nums.reduce((a, b) => a + b, 0));"
};
-
+
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' } });
require(['vs/editor/editor.main'], function () {
const initialLang = '{{ strtolower($interview->language) }}';
@@ -592,7 +593,7 @@ function captureAndUploadTabScreenshot(reason = 'tab_switch') {
let drewScreenStream = false;
const isCallStart = (reason === 'call_start');
- const bannerLabel = isCallStart
+ const bannerLabel = isCallStart
? '📸 CALL STARTED • CANDIDATE SYSTEM SCREEN SNAPSHOT • ' + new Date().toLocaleString()
: '🚨 CANDIDATE SYSTEM COMPUTER DESKTOP SNAPSHOT • ' + new Date().toLocaleString();
@@ -697,7 +698,7 @@ function fallbackCompositeSnapshot(reason = 'tab_switch') {
tempCtx.fillRect(0, tempCanvas.height - 42, tempCanvas.width, 42);
tempCtx.fillStyle = isCallStart ? '#10b981' : '#ef4444';
tempCtx.font = 'bold 14px sans-serif';
- const text = isCallStart
+ const text = isCallStart
? '📸 CALL STARTED • CANDIDATE SYSTEM SNAPSHOT • ' + new Date().toLocaleString()
: '🚨 CANDIDATE SYSTEM SNAPSHOT • ' + new Date().toLocaleString();
tempCtx.fillText(text, 16, tempCanvas.height - 15);
@@ -1016,7 +1017,7 @@ class CandRingtoneEngine {
this.init();
if (this.isPlaying) return;
this.isPlaying = true;
-
+
this.playTone();
if (this.interval) clearInterval(this.interval);
this.interval = setInterval(() => {
@@ -1211,7 +1212,7 @@ function showCandidateJoinOption() {
if (joinBtn) joinBtn.style.display = 'none';
- if (!localMediaStream) {
+ if (!localMediaStream || localMediaStream.getVideoTracks().length === 0) {
await startCandidateCameraStream();
}
@@ -1224,7 +1225,6 @@ function showCandidateJoinOption() {
badge.style.color = '#34d399';
}
- // Dual 2-Way Connection: Answer incoming call AND establish connection to interviewer peer
const stream = localMediaStream || new MediaStream();
if (activeCallInstance) {
@@ -1249,8 +1249,7 @@ function showCandidateJoinOption() {
const remoteVideo = document.getElementById('interviewer-video');
const placeholder = document.getElementById('interviewer-video-placeholder');
if (remoteVideo) {
- remoteVideo.srcObject = remoteStream;
- remoteVideo.play().catch(e => console.log('Remote video play error:', e));
+ safePlayMediaStream(remoteVideo, remoteStream);
}
if (placeholder) placeholder.style.display = 'none';
});
@@ -1267,7 +1266,60 @@ function showCandidateJoinOption() {
}
}
- // 4. WebRTC Real-Time 2-Way Interview Call Setup (Dual Signaling: BroadcastChannel + PeerJS)
+ function safePlayMediaStream(videoElem, stream, isSelf = false) {
+ if (!videoElem || !stream) return;
+ if (isSelf) {
+ videoElem.muted = true;
+ }
+ if (videoElem.srcObject !== stream) {
+ videoElem.srcObject = stream;
+ }
+ const playPromise = videoElem.play();
+ if (playPromise !== undefined) {
+ playPromise.catch(err => {
+ if (err.name === 'AbortError') {
+ videoElem.onloadedmetadata = () => {
+ videoElem.play().catch(() => {});
+ };
+ } else if (err.name === 'NotAllowedError') {
+ videoElem.play().catch(() => {});
+ } else {
+ console.log('Media play warning:', err);
+ }
+ });
+ }
+ }
+
+ // Metered TURN Server Integration (Pre-fetched for seamless WebRTC P2P + TURN relay)
+ const METERED_CREDENTIALS_URL = "https://single-login-system.metered.live/api/v1/turn/credentials?apiKey=ffc9146ac5c2f75d83d7c679d8553a21c8fa";
+ let globalIceServers = [
+ { urls: 'stun:stun.l.google.com:19302' }
+ ];
+ let meteredIceFetchPromise = null;
+
+ function initMeteredIceServers() {
+ if (meteredIceFetchPromise) return meteredIceFetchPromise;
+ meteredIceFetchPromise = fetch(METERED_CREDENTIALS_URL)
+ .then(r => r.json())
+ .then(meteredIce => {
+ if (Array.isArray(meteredIce) && meteredIce.length > 0) {
+ globalIceServers = [
+ { urls: 'stun:stun.l.google.com:19302' },
+ ...meteredIce.slice(0, 3)
+ ];
+ }
+ return globalIceServers;
+ })
+ .catch(e => {
+ console.warn('Metered TURN fetch warning:', e);
+ return globalIceServers;
+ });
+ return meteredIceFetchPromise;
+ }
+
+ // Initiate TURN credentials pre-fetch immediately
+ initMeteredIceServers();
+
let localMediaStream = null;
let peerInstance = null;
let candidatePeerConnection = null;
@@ -1319,7 +1371,22 @@ function showCandidateJoinOption() {
if (msg.type === 'interviewer_joined' || msg.type === 'offer') {
latestCallStatus = 'active';
- triggerCandidateRing(msg.offer || null);
+ if (isCandidateCallConnected && localMediaStream && peerInstance) {
+ const interviewerPeerId = 'interviewer_' + interviewId;
+ try {
+ const outCall = peerInstance.call(interviewerPeerId, localMediaStream);
+ if (outCall) {
+ outCall.on('stream', remoteStream => {
+ const remoteVideo = document.getElementById('interviewer-video');
+ const placeholder = document.getElementById('interviewer-video-placeholder');
+ if (remoteVideo) safePlayMediaStream(remoteVideo, remoteStream, false);
+ if (placeholder) placeholder.style.display = 'none';
+ });
+ }
+ } catch(e) {}
+ } else {
+ triggerCandidateRing(msg.offer || null);
+ }
} else if (msg.type === 'answer' && msg.sender !== 'candidate' && candidatePeerConnection) {
await candidatePeerConnection.setRemoteDescription(new RTCSessionDescription(msg.answer));
} else if (msg.type === 'ice-candidate' && msg.sender !== 'candidate' && candidatePeerConnection && msg.candidate) {
@@ -1341,13 +1408,10 @@ function showCandidateJoinOption() {
async function createCandidatePeerConnection() {
if (candidatePeerConnection) return candidatePeerConnection;
+ await initMeteredIceServers();
candidatePeerConnection = new RTCPeerConnection({
- iceServers: [
- { urls: 'stun:stun.l.google.com:19302' },
- { urls: 'stun:stun1.l.google.com:19302' },
- { urls: 'stun:stun2.l.google.com:19302' }
- ]
+ iceServers: globalIceServers
});
if (localMediaStream) {
@@ -1360,8 +1424,7 @@ function showCandidateJoinOption() {
const remoteVideo = document.getElementById('interviewer-video');
const placeholder = document.getElementById('interviewer-video-placeholder');
if (remoteVideo && event.streams[0]) {
- remoteVideo.srcObject = event.streams[0];
- remoteVideo.play().catch(e => console.log(e));
+ safePlayMediaStream(remoteVideo, event.streams[0]);
}
if (placeholder) placeholder.style.display = 'none';
@@ -1373,7 +1436,7 @@ function showCandidateJoinOption() {
}
};
- candidatePeerConnection.onconnectionstatechange = () => {
+ candidatePeerConnection.onconnectionstatechange = async () => {
const state = candidatePeerConnection.connectionState;
if (state === 'connected') {
const badge = document.getElementById('call-status-badge');
@@ -1382,7 +1445,9 @@ function showCandidateJoinOption() {
badge.style.background = 'rgba(16,185,129,0.3)';
badge.style.color = '#34d399';
}
- } else if (state === 'disconnected' || state === 'failed' || state === 'closed') {
+ } else if (state === 'failed' || state === 'disconnected') {
+ console.warn('Candidate P2P Connection State:', state);
+ } else if (state === 'closed') {
candidateLeaveCall();
}
};
@@ -1411,8 +1476,9 @@ function showCandidateJoinOption() {
callSigChannel.postMessage({ type: 'answer', answer: answer, sender: 'candidate' });
}
- function initCandidatePeer() {
+ async function initCandidatePeer() {
if (typeof Peer === 'undefined') return;
+ await initMeteredIceServers();
const peerId = 'cand_' + interviewId;
if (peerInstance) {
@@ -1421,11 +1487,7 @@ function initCandidatePeer() {
peerInstance = new Peer(peerId, {
config: {
- iceServers: [
- { urls: 'stun:stun.l.google.com:19302' },
- { urls: 'stun:stun1.l.google.com:19302' },
- { urls: 'stun:stun2.l.google.com:19302' }
- ]
+ iceServers: globalIceServers
}
});
@@ -1441,15 +1503,11 @@ function initCandidatePeer() {
triggerCandidateRing();
}
- const sendStream = localMediaStream || new MediaStream();
- call.answer(sendStream);
-
call.on('stream', remoteStream => {
const remoteVideo = document.getElementById('interviewer-video');
const placeholder = document.getElementById('interviewer-video-placeholder');
if (remoteVideo) {
- remoteVideo.srcObject = remoteStream;
- remoteVideo.play().catch(e => console.log('Candidate remote video play error:', e));
+ safePlayMediaStream(remoteVideo, remoteStream);
}
if (placeholder) placeholder.style.display = 'none';
@@ -1462,33 +1520,57 @@ function initCandidatePeer() {
}
}
});
+ if (localMediaStream && localMediaStream.getVideoTracks().length > 0) {
+ try { call.answer(localMediaStream); } catch(e) {}
+ }
call.on('close', () => {
candidateLeaveCall();
});
});
+
+ peerInstance.on('error', async err => {
+ console.warn('Candidate PeerJS error:', err);
+ if (err.type === 'peer-unavailable' || err.type === 'network' || err.type === 'webrtc') {
+ const meteredServers = await fetchMeteredTurnIceServers();
+ console.log('Re-initializing Candidate PeerJS with Metered TURN Relay Servers...');
+ if (peerInstance) {
+ try { peerInstance.destroy(); } catch(e) {}
+ }
+ peerInstance = new Peer(peerId, {
+ config: { iceServers: meteredServers }
+ });
+ }
+ });
}
function startCandidateCameraStream() {
const avatarPlaceholder = document.getElementById('webcam-avatar-placeholder');
+ const audioConstraints = {
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true
+ };
+
return navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user'
},
- audio: true
+ audio: audioConstraints
})
- .catch(() => navigator.mediaDevices.getUserMedia({ video: true, audio: false }))
+ .catch(() => navigator.mediaDevices.getUserMedia({ video: true, audio: audioConstraints }))
+ .catch(() => navigator.mediaDevices.getUserMedia({ video: true, audio: true }))
.catch(() => navigator.mediaDevices.getUserMedia({ video: false, audio: true }))
+ .catch(() => navigator.mediaDevices.getUserMedia({ video: true, audio: false }))
.then(stream => {
localMediaStream = stream;
const vid = document.getElementById('webcam');
if (vid) {
vid.muted = true;
- vid.srcObject = stream;
- vid.play().catch(e => console.log('Candidate webcam play error:', e));
+ safePlayMediaStream(vid, stream, true);
}
if (avatarPlaceholder) avatarPlaceholder.style.display = 'none';
diff --git a/resources/views/interview/index.blade.php b/resources/views/interview/index.blade.php
index 934792f..a82dbe4 100644
--- a/resources/views/interview/index.blade.php
+++ b/resources/views/interview/index.blade.php
@@ -10,6 +10,7 @@
+