-comunicate-demo
This commit is contained in:
553
comunicate-demo/index.html
Normal file
553
comunicate-demo/index.html
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>TTC Communication Client</title>
|
||||||
|
|
||||||
|
<!-- Tailwind CSS -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
|
||||||
|
<!-- Socket.io Client -->
|
||||||
|
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Icons -->
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: '#4F46E5', // Indigo 600
|
||||||
|
secondary: '#10B981', // Emerald 500
|
||||||
|
danger: '#EF4444', // Red 500
|
||||||
|
dark: '#1F2937' // Gray 800
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
/* Custom Scrollbar */
|
||||||
|
::-webkit-scrollbar { width: 8px; }
|
||||||
|
::-webkit-scrollbar-track { background: #f1f1f1; }
|
||||||
|
::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: #555; }
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
transform: scaleX(-1); /* Mirror effect for self view */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100 min-h-screen text-gray-800 font-sans">
|
||||||
|
|
||||||
|
<!-- Navbar -->
|
||||||
|
<nav class="bg-white shadow-sm border-b border-gray-200 px-6 py-4 flex justify-between items-center">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="bg-primary text-white p-2 rounded-lg">
|
||||||
|
<i class="fa-solid fa-network-wired"></i>
|
||||||
|
</div>
|
||||||
|
<h1 class="font-bold text-xl text-gray-700">TTC Microservice <span class="text-xs font-normal bg-gray-200 px-2 py-0.5 rounded text-gray-500">Demo Client</span></h1>
|
||||||
|
</div>
|
||||||
|
<div id="connectionStatus" class="flex items-center gap-2 text-sm text-red-500 font-medium">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-red-500 animate-pulse"></div>
|
||||||
|
Disconnected
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container mx-auto p-6 max-w-6xl grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||||
|
|
||||||
|
<!-- LEFT PANEL: Config & Notify -->
|
||||||
|
<div class="lg:col-span-4 space-y-6">
|
||||||
|
|
||||||
|
<!-- 1. Authentication -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
|
||||||
|
<h2 class="text-lg font-bold mb-4 flex items-center gap-2 text-gray-700">
|
||||||
|
<i class="fa-solid fa-key text-primary"></i> Authentication
|
||||||
|
</h2>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="text-xs font-semibold text-gray-500 uppercase">Server URL</label>
|
||||||
|
<!-- [FIX] เปลี่ยนค่าเริ่มต้นให้ว่างไว้ เดี๋ยว Script จะเติมให้เองตาม Context -->
|
||||||
|
<input type="text" id="serverUrl"
|
||||||
|
class="w-full mt-1 p-2 bg-gray-50 border border-gray-300 rounded focus:ring-2 focus:ring-primary focus:outline-none transition-all"
|
||||||
|
placeholder="Auto-detected..." value=" https://entrepreneur-faced-browsing-gateway.trycloudflare.com">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs font-semibold text-gray-500 uppercase">JWT Token</label>
|
||||||
|
<textarea id="jwtToken" rows="2" class="w-full mt-1 p-2 bg-gray-50 border border-gray-300 rounded focus:ring-2 focus:ring-primary focus:outline-none text-xs font-mono" placeholder="Paste your Bearer token here..."></textarea>
|
||||||
|
</div>
|
||||||
|
<button id="btnConnect" class="w-full bg-primary hover:bg-indigo-700 text-white font-medium py-2 rounded transition-colors shadow-sm flex justify-center items-center gap-2">
|
||||||
|
<i class="fa-solid fa-link"></i> Connect Socket
|
||||||
|
</button>
|
||||||
|
<div id="userInfoDisplay" class="hidden mt-2 p-2 bg-indigo-50 text-indigo-700 text-xs rounded border border-indigo-100">
|
||||||
|
<!-- User info will show here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. Notification System -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5 flex flex-col h-[500px]">
|
||||||
|
<h2 class="text-lg font-bold mb-4 flex items-center gap-2 text-gray-700">
|
||||||
|
<i class="fa-solid fa-bell text-secondary"></i> Notification
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Notify Form -->
|
||||||
|
<div class="space-y-3 mb-4 p-3 bg-gray-50 rounded-lg border border-gray-200">
|
||||||
|
<input type="text" id="notifyTargetId" class="w-full p-2 text-sm border border-gray-300 rounded" placeholder="Target User ID (seq)">
|
||||||
|
<input type="text" id="notifyTitle" class="w-full p-2 text-sm border border-gray-300 rounded" placeholder="Topic / Title">
|
||||||
|
<input type="text" id="notifyMessage" class="w-full p-2 text-sm border border-gray-300 rounded" placeholder="Message body...">
|
||||||
|
<button id="btnSendNotify" class="w-full bg-secondary hover:bg-emerald-600 text-white text-sm font-medium py-2 rounded transition-colors">
|
||||||
|
Send Realtime Notify
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notify Logs -->
|
||||||
|
<div class="flex-1 overflow-y-auto space-y-2 pr-1" id="notifyLogs">
|
||||||
|
<div class="text-center text-gray-400 text-xs mt-10">No notifications yet</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RIGHT PANEL: VoIP -->
|
||||||
|
<div class="lg:col-span-8">
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5 h-full flex flex-col">
|
||||||
|
<h2 class="text-lg font-bold mb-4 flex items-center gap-2 text-gray-700 border-b pb-3">
|
||||||
|
<i class="fa-solid fa-video text-pink-500"></i> Video Conference / VoIP
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Control Bar -->
|
||||||
|
<div class="flex flex-wrap gap-3 mb-4 items-end bg-gray-50 p-3 rounded-lg">
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="text-xs font-semibold text-gray-500 uppercase">Call To (User ID)</label>
|
||||||
|
<div class="flex gap-2 mt-1">
|
||||||
|
<input type="text" id="callTargetId" class="flex-1 p-2 border border-gray-300 rounded focus:ring-2 focus:ring-pink-500 focus:outline-none" placeholder="User ID...">
|
||||||
|
<button id="btnCall" class="bg-pink-500 hover:bg-pink-600 text-white px-4 py-2 rounded shadow-sm flex items-center gap-2 transition-colors">
|
||||||
|
<i class="fa-solid fa-phone"></i> Call
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="btnHangup" class="hidden bg-red-500 hover:bg-red-600 text-white px-6 py-2 rounded shadow-sm flex items-center gap-2 transition-colors">
|
||||||
|
<i class="fa-solid fa-phone-slash"></i> Hangup
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Incoming Call Alert (Modal-ish) -->
|
||||||
|
<div id="incomingCallAlert" class="hidden mb-4 bg-yellow-50 border border-yellow-200 rounded-lg p-4 flex justify-between items-center shadow-sm animate-bounce">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="bg-yellow-100 p-2 rounded-full text-yellow-600">
|
||||||
|
<i class="fa-solid fa-phone-volume fa-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-gray-800">Incoming Call...</h3>
|
||||||
|
<p class="text-sm text-gray-500">From User: <span id="callerNameDisplay" class="font-mono text-black">Unknown</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button id="btnAccept" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded shadow text-sm font-bold">
|
||||||
|
Accept
|
||||||
|
</button>
|
||||||
|
<button id="btnReject" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded shadow text-sm font-bold">
|
||||||
|
Reject
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Video Area -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 flex-1 min-h-[400px]">
|
||||||
|
<!-- Local -->
|
||||||
|
<div class="relative bg-gray-900 rounded-xl overflow-hidden shadow-inner group">
|
||||||
|
<video id="localVideo" autoplay playsinline muted class="w-full h-full object-cover video-container opacity-50"></video>
|
||||||
|
<div class="absolute bottom-4 left-4 text-white text-xs bg-black/50 px-2 py-1 rounded backdrop-blur-sm">
|
||||||
|
You (Local)
|
||||||
|
</div>
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center text-gray-500 group-hover:hidden" id="localPlaceholder">
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="fa-solid fa-camera-slash fa-2x mb-2"></i>
|
||||||
|
<p class="text-xs">Camera Off</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Remote -->
|
||||||
|
<div class="relative bg-gray-900 rounded-xl overflow-hidden shadow-inner">
|
||||||
|
<video id="remoteVideo" autoplay playsinline class="w-full h-full object-cover"></video>
|
||||||
|
<div class="absolute bottom-4 left-4 text-white text-xs bg-black/50 px-2 py-1 rounded backdrop-blur-sm">
|
||||||
|
Remote User
|
||||||
|
</div>
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center text-gray-600" id="remotePlaceholder">
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="fa-solid fa-user-slash fa-2x mb-2"></i>
|
||||||
|
<p class="text-xs">Waiting for connection...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MAIN SCRIPT -->
|
||||||
|
<script type="module">
|
||||||
|
/** * TTC Client Logic
|
||||||
|
* Implements Socket.io & WebRTC Standard
|
||||||
|
*/
|
||||||
|
|
||||||
|
// --- State ---
|
||||||
|
let socket = null;
|
||||||
|
let myStream = null;
|
||||||
|
let peerConnection = null;
|
||||||
|
let incomingSignal = null; // Store offer temporarily
|
||||||
|
let activeCallUser = null;
|
||||||
|
let myUserId = null;
|
||||||
|
|
||||||
|
// --- Config ---
|
||||||
|
const rtcConfig = {
|
||||||
|
iceServers: [
|
||||||
|
{ urls: 'stun:stun.l.google.com:19302' },
|
||||||
|
// { urls: 'stun:global.stun.twilio.com:3478' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- DOM Elements ---
|
||||||
|
const els = {
|
||||||
|
serverUrl: document.getElementById('serverUrl'),
|
||||||
|
jwtToken: document.getElementById('jwtToken'),
|
||||||
|
btnConnect: document.getElementById('btnConnect'),
|
||||||
|
status: document.getElementById('connectionStatus'),
|
||||||
|
userInfo: document.getElementById('userInfoDisplay'),
|
||||||
|
|
||||||
|
// Notify
|
||||||
|
notifyTarget: document.getElementById('notifyTargetId'),
|
||||||
|
notifyTitle: document.getElementById('notifyTitle'),
|
||||||
|
notifyMsg: document.getElementById('notifyMessage'),
|
||||||
|
btnSendNotify: document.getElementById('btnSendNotify'),
|
||||||
|
notifyLogs: document.getElementById('notifyLogs'),
|
||||||
|
|
||||||
|
// VoIP
|
||||||
|
callTarget: document.getElementById('callTargetId'),
|
||||||
|
btnCall: document.getElementById('btnCall'),
|
||||||
|
btnHangup: document.getElementById('btnHangup'),
|
||||||
|
incomingAlert: document.getElementById('incomingCallAlert'),
|
||||||
|
callerName: document.getElementById('callerNameDisplay'),
|
||||||
|
btnAccept: document.getElementById('btnAccept'),
|
||||||
|
btnReject: document.getElementById('btnReject'),
|
||||||
|
|
||||||
|
localVideo: document.getElementById('localVideo'),
|
||||||
|
remoteVideo: document.getElementById('remoteVideo'),
|
||||||
|
localPlace: document.getElementById('localPlaceholder'),
|
||||||
|
remotePlace: document.getElementById('remotePlaceholder'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// [FIX] Auto-populate Server URL to avoid Mixed Content Error
|
||||||
|
// ถ้าเข้าผ่าน https (ngrok) มันจะใช้ https ตามอัตโนมัติ
|
||||||
|
// window.onload = () => {
|
||||||
|
// els.serverUrl.value = window.location.origin;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// --- 1. Connection Logic ---
|
||||||
|
els.btnConnect.addEventListener('click', () => {
|
||||||
|
const url = els.serverUrl.value;
|
||||||
|
const token = els.jwtToken.value.trim();
|
||||||
|
|
||||||
|
//if (!token) return alert('Please enter a JWT Token');
|
||||||
|
|
||||||
|
// [FIX] Initialize Socket with Secure Options
|
||||||
|
// window.location.origin จะแก้ปัญหา Mixed Content เพราะมันจะใช้ protocol เดียวกับหน้าเว็บ
|
||||||
|
socket = io(url, {
|
||||||
|
auth: { token: token },
|
||||||
|
transports: ['websocket', 'polling'], // เพิ่ม polling เพื่อความชัวร์ในบาง network
|
||||||
|
secure: true, // Force secure connection
|
||||||
|
rejectUnauthorized: false // ยอมรับ Self-signed cert กรณีเทส local
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle Connection Events
|
||||||
|
socket.on('connect', () => {
|
||||||
|
updateStatus(true);
|
||||||
|
console.log('✅ Connected:', socket.id);
|
||||||
|
// Decode token roughly to show ID (Optional)
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||||
|
myUserId = payload.id;
|
||||||
|
els.userInfo.innerHTML = `Signed in as: <b>${payload.usrnam}</b> (ID: ${payload.id})`;
|
||||||
|
els.userInfo.classList.remove('hidden');
|
||||||
|
} catch(e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
updateStatus(false);
|
||||||
|
console.log('❌ Disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('connect_error', (err) => {
|
||||||
|
updateStatus(false);
|
||||||
|
console.error('Socket Error:', err);
|
||||||
|
alert('Connection Failed: ' + err.message + '\n(Check Mixed Content/CORS in Console)');
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Register Listeners ---
|
||||||
|
setupNotificationListeners();
|
||||||
|
setupVoIPListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateStatus(isConnected) {
|
||||||
|
if(isConnected) {
|
||||||
|
els.status.innerHTML = `<div class="w-2 h-2 rounded-full bg-green-500"></div> Connected`;
|
||||||
|
els.status.className = "flex items-center gap-2 text-sm text-green-600 font-bold";
|
||||||
|
els.btnConnect.disabled = true;
|
||||||
|
els.btnConnect.innerText = "Connected";
|
||||||
|
els.btnConnect.classList.add('bg-gray-400', 'cursor-not-allowed');
|
||||||
|
} else {
|
||||||
|
els.status.innerHTML = `<div class="w-2 h-2 rounded-full bg-red-500 animate-pulse"></div> Disconnected`;
|
||||||
|
els.status.className = "flex items-center gap-2 text-sm text-red-500 font-medium";
|
||||||
|
els.btnConnect.disabled = false;
|
||||||
|
els.btnConnect.innerText = "Connect Socket";
|
||||||
|
els.btnConnect.classList.remove('bg-gray-400', 'cursor-not-allowed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. Notification Logic ---
|
||||||
|
function setupNotificationListeners() {
|
||||||
|
// Sending
|
||||||
|
els.btnSendNotify.addEventListener('click', () => {
|
||||||
|
if(!socket) return alert("Not connected");
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
targetUserId: els.notifyTarget.value,
|
||||||
|
title: els.notifyTitle.value,
|
||||||
|
message: els.notifyMsg.value,
|
||||||
|
type: 'info'
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.emit('send_notification', data);
|
||||||
|
addNotifyLog('outgoing', `To ${data.targetUserId}: ${data.title}`);
|
||||||
|
els.notifyMsg.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Receiving
|
||||||
|
socket.on('receive_notification', (data) => {
|
||||||
|
addNotifyLog('incoming', `From ${data.from}: ${data.title} - ${data.message}`);
|
||||||
|
// Play sound if needed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNotifyLog(type, text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
const time = new Date().toLocaleTimeString('th-TH', { hour: '2-digit', minute:'2-digit'});
|
||||||
|
|
||||||
|
if(type === 'incoming') {
|
||||||
|
div.className = "bg-white border-l-4 border-secondary p-3 rounded shadow-sm text-sm";
|
||||||
|
div.innerHTML = `<div class="flex justify-between text-xs text-gray-400 mb-1"><span>Incoming</span> <span>${time}</span></div>
|
||||||
|
<div class="text-gray-700">${text}</div>`;
|
||||||
|
} else {
|
||||||
|
div.className = "bg-gray-50 border-l-4 border-gray-300 p-3 rounded text-sm";
|
||||||
|
div.innerHTML = `<div class="flex justify-between text-xs text-gray-400 mb-1"><span>Sent</span> <span>${time}</span></div>
|
||||||
|
<div class="text-gray-600">${text}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
els.notifyLogs.prepend(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3. VoIP / WebRTC Logic ---
|
||||||
|
async function getLocalStream() {
|
||||||
|
try {
|
||||||
|
// [FIX] ใช้ constraints แบบง่าย เพื่อลดโอกาสเกิด OverconstrainedError
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: true,
|
||||||
|
audio: { echoCancellation: true, noiseSuppression: true }
|
||||||
|
});
|
||||||
|
myStream = stream;
|
||||||
|
els.localVideo.srcObject = stream;
|
||||||
|
els.localVideo.classList.remove('opacity-50');
|
||||||
|
els.localPlace.classList.add('hidden');
|
||||||
|
return stream;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Media Error:", err);
|
||||||
|
|
||||||
|
let msg = "Cannot access Camera/Microphone.";
|
||||||
|
if(location.protocol !== 'https:' && location.hostname !== 'localhost') {
|
||||||
|
msg += " (Browser requires HTTPS or Localhost)";
|
||||||
|
} else {
|
||||||
|
msg += " " + err.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(msg);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPeerConnection(targetId) {
|
||||||
|
const pc = new RTCPeerConnection(rtcConfig);
|
||||||
|
|
||||||
|
// Add local tracks
|
||||||
|
if(myStream) {
|
||||||
|
myStream.getTracks().forEach(track => pc.addTrack(track, myStream));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ICE Candidates
|
||||||
|
pc.onicecandidate = (event) => {
|
||||||
|
if (event.candidate) {
|
||||||
|
socket.emit('send_ice_candidate', {
|
||||||
|
targetUserId: targetId,
|
||||||
|
candidate: event.candidate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle Remote Stream
|
||||||
|
pc.ontrack = (event) => {
|
||||||
|
const [remoteStream] = event.streams;
|
||||||
|
els.remoteVideo.srcObject = remoteStream;
|
||||||
|
els.remotePlace.classList.add('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Connection State Changes
|
||||||
|
pc.onconnectionstatechange = () => {
|
||||||
|
console.log("Peer State:", pc.connectionState);
|
||||||
|
if(pc.connectionState === 'disconnected' || pc.connectionState === 'failed') {
|
||||||
|
hangupUI();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return pc;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupVoIPListeners() {
|
||||||
|
|
||||||
|
// --- A. Caller Logic ---
|
||||||
|
els.btnCall.addEventListener('click', async () => {
|
||||||
|
const target = els.callTarget.value;
|
||||||
|
if(!target || !socket) return;
|
||||||
|
|
||||||
|
activeCallUser = target;
|
||||||
|
|
||||||
|
// 1. Get Media
|
||||||
|
const stream = await getLocalStream();
|
||||||
|
if (!stream) return; // Exit if media failed
|
||||||
|
|
||||||
|
// 2. Create PC
|
||||||
|
peerConnection = createPeerConnection(target);
|
||||||
|
|
||||||
|
// 3. Create Offer
|
||||||
|
const offer = await peerConnection.createOffer();
|
||||||
|
await peerConnection.setLocalDescription(offer);
|
||||||
|
|
||||||
|
// 4. Send Signal
|
||||||
|
socket.emit('call_user', {
|
||||||
|
userToCall: target,
|
||||||
|
signalData: offer
|
||||||
|
});
|
||||||
|
|
||||||
|
uiCallingState();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- B. Callee Logic (Incoming) ---
|
||||||
|
socket.on('call_incoming', (data) => {
|
||||||
|
incomingSignal = data.signal;
|
||||||
|
activeCallUser = data.from; // Store who is calling
|
||||||
|
els.callerName.innerText = `ID: ${data.from}`;
|
||||||
|
|
||||||
|
els.incomingAlert.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
els.btnAccept.addEventListener('click', async () => {
|
||||||
|
els.incomingAlert.classList.add('hidden');
|
||||||
|
|
||||||
|
// 1. Get Media
|
||||||
|
const stream = await getLocalStream();
|
||||||
|
if (!stream) return;
|
||||||
|
|
||||||
|
// 2. Create PC
|
||||||
|
peerConnection = createPeerConnection(activeCallUser);
|
||||||
|
|
||||||
|
// 3. Set Remote Desc (Offer)
|
||||||
|
await peerConnection.setRemoteDescription(incomingSignal);
|
||||||
|
|
||||||
|
// 4. Create Answer
|
||||||
|
const answer = await peerConnection.createAnswer();
|
||||||
|
await peerConnection.setLocalDescription(answer);
|
||||||
|
|
||||||
|
// 5. Send Answer
|
||||||
|
socket.emit('answer_call', {
|
||||||
|
to: activeCallUser,
|
||||||
|
signal: answer
|
||||||
|
});
|
||||||
|
|
||||||
|
uiConnectedState();
|
||||||
|
});
|
||||||
|
|
||||||
|
els.btnReject.addEventListener('click', () => {
|
||||||
|
els.incomingAlert.classList.add('hidden');
|
||||||
|
// Optional: Send reject signal
|
||||||
|
hangupUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- C. Connection Establishment ---
|
||||||
|
socket.on('call_accepted', async (data) => {
|
||||||
|
// Caller receives Answer
|
||||||
|
await peerConnection.setRemoteDescription(data.signal);
|
||||||
|
uiConnectedState();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('receive_ice_candidate', async (data) => {
|
||||||
|
if(peerConnection) {
|
||||||
|
try {
|
||||||
|
await peerConnection.addIceCandidate(data.candidate);
|
||||||
|
} catch(e) {
|
||||||
|
console.warn("ICE Error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- D. Hangup ---
|
||||||
|
els.btnHangup.addEventListener('click', () => {
|
||||||
|
socket.emit('end_call', { targetUserId: activeCallUser });
|
||||||
|
hangupUI();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('call_ended', () => {
|
||||||
|
alert("Call Ended by remote user");
|
||||||
|
hangupUI();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- UI Helpers ---
|
||||||
|
function uiCallingState() {
|
||||||
|
els.btnCall.disabled = true;
|
||||||
|
els.btnCall.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> Calling...`;
|
||||||
|
els.btnHangup.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function uiConnectedState() {
|
||||||
|
els.btnCall.classList.add('hidden');
|
||||||
|
els.btnHangup.classList.remove('hidden');
|
||||||
|
els.incomingAlert.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hangupUI() {
|
||||||
|
if(peerConnection) peerConnection.close();
|
||||||
|
if(myStream) myStream.getTracks().forEach(t => t.stop());
|
||||||
|
|
||||||
|
peerConnection = null;
|
||||||
|
myStream = null;
|
||||||
|
activeCallUser = null;
|
||||||
|
incomingSignal = null;
|
||||||
|
|
||||||
|
els.localVideo.srcObject = null;
|
||||||
|
els.remoteVideo.srcObject = null;
|
||||||
|
els.localPlace.classList.remove('hidden');
|
||||||
|
els.remotePlace.classList.remove('hidden');
|
||||||
|
|
||||||
|
els.btnCall.disabled = false;
|
||||||
|
els.btnCall.classList.remove('hidden');
|
||||||
|
els.btnCall.innerHTML = `<i class="fa-solid fa-phone"></i> Call`;
|
||||||
|
els.btnHangup.classList.add('hidden');
|
||||||
|
els.incomingAlert.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
comunicate-demo/index.js
Normal file
26
comunicate-demo/index.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// 1. สร้างไฟล์นี้ในโฟลเดอร์เดียวกับ index.html
|
||||||
|
// 2. รันคำสั่ง: npm install express (ถ้ายังไม่มี)
|
||||||
|
// 3. สตาร์ทเซิร์ฟเวอร์: node server_frontend.js
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
|
const app = express();
|
||||||
|
const PORT = 80; // พอร์ตสำหรับหน้าเว็บ (แยกกับ API 1011)
|
||||||
|
|
||||||
|
// ให้บริการไฟล์ Static ในโฟลเดอร์ปัจจุบัน
|
||||||
|
app.use(express.static(path.join(__dirname)));
|
||||||
|
|
||||||
|
// Route หลักส่ง index.html
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// สั่งให้ Listen ทุก IP ในเครื่อง (0.0.0.0)
|
||||||
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
|
console.log('---------------------------------------------------');
|
||||||
|
console.log(`🚀 Frontend Server running!`);
|
||||||
|
console.log(`🏠 Local: http://localhost:${PORT}`);
|
||||||
|
console.log(`📡 Network: http://<YOUR_IP_ADDRESS>:${PORT}`);
|
||||||
|
console.log('---------------------------------------------------');
|
||||||
|
console.log('To find your IP: Run "ipconfig" (Windows) or "ifconfig" (Mac/Linux)');
|
||||||
|
});
|
||||||
17
comunicate-demo/package.json
Normal file
17
comunicate-demo/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "comunicate-demo",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"path": "^0.12.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user