-comunicate-demo
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 6m34s
Build Docker Image / Restart Docker Compose (push) Successful in 1s

This commit is contained in:
x2Skyz
2025-11-27 21:58:19 +07:00
parent 8e47a11ace
commit 012d590f40
3 changed files with 596 additions and 0 deletions

553
comunicate-demo/index.html Normal file
View 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
View 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)');
});

View 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"
}
}