-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