Install dependencies: pip install opencv-python paho-mqtt ultralytics pycryptodome websockets
Sesuaikan kredensial di baris 30-34, 43-44, dan 52
Jalankan service: python person_detection_service.py
Buka website (file HTML terpisah) dan connect ke ws://[IP_SERVER]:8765
Tekan 'q' pada window OpenCV untuk berhenti
Tentunya kode pada website akan berubah, dimana semua komunikasi harus terhubung hanya melalui proxy kode service-nya, tidak lagi direct ke server broker atau lainnya. Dan komunikasi yang terjalin antara website dan proxy kode service melalui websocket.
Buat file kode baru untuk websitenya dengan nama MonitoringAESFullFrontEnd.html
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Person Detection - CCTV Monitoring (Secure Proxy)</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
body {
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
min-height: 100vh;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.card {
border-radius: 20px;
overflow: hidden;
box-shadow: 0 15px 35px rgba(0,0,0,0.3);
backdrop-filter: blur(10px);
background: rgba(255,255,255,0.95);
}
.card-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-weight: bold;
padding: 15px 20px;
border: none;
}
.video-container {
position: relative;
background: #000;
border-radius: 15px;
overflow: hidden;
aspect-ratio: 16/9;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
#videoFrame {
width: 100%;
height: 100%;
object-fit: contain;
transition: all 0.3s ease;
}
.video-overlay {
position: absolute;
bottom: 15px;
left: 15px;
right: 15px;
display: flex;
justify-content: space-between;
align-items: center;
pointer-events: none;
}
.status-badge {
background: rgba(0,0,0,0.75);
backdrop-filter: blur(5px);
padding: 8px 16px;
border-radius: 50px;
color: white;
font-size: 13px;
font-weight: 500;
}
.person-badge {
background: linear-gradient(135deg, #dc3545, #c82333);
padding: 8px 20px;
border-radius: 50px;
color: white;
font-weight: bold;
font-size: 16px;
animation: pulse 2s infinite;
box-shadow: 0 0 15px rgba(220,53,69,0.5);
}
.proxy-badge {
background: rgba(0,0,0,0.75);
backdrop-filter: blur(5px);
padding: 8px 16px;
border-radius: 50px;
color: #ffa500;
font-size: 12px;
font-weight: 500;
font-family: monospace;
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.9; }
100% { transform: scale(1); opacity: 1; }
}
.stat-card {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border-radius: 15px;
padding: 20px;
text-align: center;
margin-bottom: 15px;
transition: transform 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-icon {
font-size: 2rem;
margin-bottom: 10px;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
margin: 10px 0;
}
.alert-list {
max-height: 400px;
overflow-y: auto;
}
.alert-item {
background: white;
border-left: 4px solid;
padding: 12px 15px;
margin-bottom: 10px;
border-radius: 10px;
transition: all 0.3s;
animation: slideIn 0.5s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.alert-item.success {
border-left-color: #28a745;
background: linear-gradient(90deg, #f0fff4, white);
}
.alert-item.danger {
border-left-color: #dc3545;
background: linear-gradient(90deg, #fff5f5, white);
}
.alert-item.warning {
border-left-color: #ffc107;
background: linear-gradient(90deg, #fffbf0, white);
}
.alert-time {
font-size: 11px;
color: #6c757d;
margin-top: 5px;
}
.connection-status {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
animation: blink 1.5s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.connected { background-color: #28a745; box-shadow: 0 0 5px #28a745; }
.disconnected { background-color: #dc3545; box-shadow: 0 0 5px #dc3545; }
.btn-custom {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 50px;
padding: 10px 20px;
font-weight: 600;
transition: all 0.3s;
}
.btn-custom:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102,126,234,0.4);
color: white;
}
.frame-border {
transition: all 0.3s;
}
.frame-border.person-detected {
box-shadow: 0 0 30px rgba(220,53,69,0.8);
}
.config-panel {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
}
.config-btn {
background: linear-gradient(135deg, #667eea, #764ba2);
border: none;
border-radius: 50px;
padding: 10px 20px;
color: white;
font-weight: bold;
}
.modal-content {
background: linear-gradient(135deg, #0f0c29, #302b63);
color: white;
}
</style>
</head>
<body>
<div class="container">
<div class="text-center mb-4">
<h1 class="text-white">
<i class="fas fa-video me-3"></i>
Person Detection Monitoring System
</h1>
<p class="text-white-50">Real-time detection with YOLOv8 | Secure Proxy Architecture | No Direct MQTT Access</p>
</div>
<div class="row g-4">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<i class="fas fa-camera me-2"></i> Live CCTV Stream (with Bounding Box)
</div>
<div class="card-body p-3">
<div class="video-container frame-border" id="videoContainer">
<img id="videoFrame" src="" alt="Waiting for stream...">
<div class="video-overlay">
<div class="status-badge">
<span class="connection-status disconnected" id="statusIndicator"></span>
<span id="statusText">Disconnected</span>
</div>
<div class="proxy-badge">
<i class="fas fa-shield-alt"></i> Secure Proxy
</div>
<div class="person-badge" id="personBadge">
<i class="fas fa-user"></i> <span id="personCount">0</span>
</div>
</div>
</div>
<div class="alert alert-info mt-3 mb-0 small">
<i class="fas fa-info-circle me-2"></i>
<strong>Info:</strong> Bounding box <span style="color: #28a745; font-weight: bold;">Hijau</span> = Person terdeteksi.
<i class="fas fa-shield-alt ms-2"></i> Semua data melalui secure proxy (kredensial aman di server)
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<i class="fas fa-chart-line me-2"></i> Statistics
</div>
<div class="card-body">
<div class="stat-card">
<div class="stat-icon"><i class="fas fa-chart-bar"></i></div>
<div class="stat-value" id="totalDetections">0</div>
<div>Total Person Detections</div>
</div>
<div class="stat-card" style="background: linear-gradient(135deg, #f093fb, #f5576c);">
<div class="stat-icon"><i class="fas fa-clock"></i></div>
<div class="stat-value" id="lastDetection">-</div>
<div>Last Detection</div>
</div>
<div class="stat-card" style="background: linear-gradient(135deg, #4facfe, #00f2fe);">
<div class="stat-icon"><i class="fas fa-shield-alt"></i></div>
<div class="stat-value" id="proxyStatus">Active</div>
<div>Secure Proxy Mode</div>
</div>
<button class="btn-custom w-100" onclick="clearAlerts()">
<i class="fas fa-trash-alt me-2"></i> Clear Alert History
</button>
</div>
</div>
</div>
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="fas fa-bell me-2"></i> Alert History</span>
<span class="badge bg-light text-dark" id="alertCount">0</span>
</div>
<div class="card-body p-0">
<div class="alert-list p-3" id="alertList">
<div class="alert-item warning">
<i class="fas fa-hourglass-half me-2"></i>
<strong>Waiting for connection...</strong>
<div>Menunggu koneksi ke proxy server...</div>
<div class="alert-time"><i class="far fa-clock me-1"></i> Just now</div>
</div>
</div>
</div>
</div>
</div>
</div>
<footer class="text-center text-white-50 mt-4">
<small>Powered by YOLOv8 | Secure WebSocket Proxy | No Credentials in Frontend | Real-time Detection</small>
</footer>
</div>
<!-- Configuration Modal -->
<div class="config-panel">
<button class="config-btn" onclick="showConfigModal()">
<i class="fas fa-plug me-2"></i> Configure Connection
</button>
</div>
<div class="modal fade" id="configModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-network-wired me-2"></i>Proxy Connection Settings</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">WebSocket Server Address</label>
<input type="text" class="form-control" id="wsUrl" placeholder="ws://localhost:8765" value="ws://localhost:8765">
<small class="text-muted">Alamat server proxy (default: ws://localhost:8765)</small>
</div>
<div class="alert alert-warning">
<i class="fas fa-shield-alt me-2"></i>
<strong>Secure Mode:</strong> Kredensial MQTT dan AES disimpan aman di server proxy.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="connectToProxy()">Connect</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Tidak ada credential MQTT atau AES di sini!
// Semua kredensial disimpan aman di server proxy
let ws = null;
let stats = {
totalDetections: 0,
lastDetection: null,
currentPersonCount: 0
};
let reconnectAttempts = 0;
let lastFrameTime = 0;
let reconnectInterval = null;
const videoFrame = document.getElementById('videoFrame');
const videoContainer = document.getElementById('videoContainer');
const statusIndicator = document.getElementById('statusIndicator');
const statusText = document.getElementById('statusText');
const personCountSpan = document.getElementById('personCount');
const personBadge = document.getElementById('personBadge');
const totalDetectionsSpan = document.getElementById('totalDetections');
const lastDetectionSpan = document.getElementById('lastDetection');
const alertList = document.getElementById('alertList');
const alertCountSpan = document.getElementById('alertCount');
function showConfigModal() {
const modal = new bootstrap.Modal(document.getElementById('configModal'));
modal.show();
}
function connectToProxy() {
const wsUrl = document.getElementById('wsUrl').value;
// Close existing connection if any
if (ws) {
ws.close();
if (reconnectInterval) clearInterval(reconnectInterval);
}
try {
console.log('Connecting to proxy server:', wsUrl);
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('✅ Connected to proxy server');
updateConnectionStatus(true);
addAlert('Connected to secure proxy server', 'success');
reconnectAttempts = 0;
// Send ping periodically to keep connection alive
if (reconnectInterval) clearInterval(reconnectInterval);
reconnectInterval = setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ command: 'ping' }));
}
}, 30000);
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
handleMessage(data);
} catch (e) {
console.error('Error parsing message:', e);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
updateConnectionStatus(false);
addAlert('Proxy connection error', 'danger');
};
ws.onclose = () => {
console.log('Disconnected from proxy server');
updateConnectionStatus(false);
addAlert('Disconnected from proxy server, attempting to reconnect...', 'warning');
// Auto reconnect after 5 seconds
setTimeout(() => {
if (ws && ws.readyState === WebSocket.CLOSED) {
connectToProxy();
}
}, 5000);
};
// Close modal
bootstrap.Modal.getInstance(document.getElementById('configModal')).hide();
} catch (error) {
console.error('Failed to connect:', error);
addAlert(`Connection failed: ${error.message}`, 'danger');
}
}
function handleMessage(data) {
if (data.event === 'person_detected') {
stats.totalDetections++;
stats.lastDetection = new Date(data.timestamp);
stats.currentPersonCount = data.num_person;
updateStats();
addAlert(`🔴 ${data.message} (${data.num_person} person detected)`, 'success');
showToastNotification(data.message, data.num_person);
playBeep();
}
else if (data.event === 'video_frame') {
if (data.frame) {
displayFrame(data.frame);
stats.currentPersonCount = data.num_person;
updateStats();
personCountSpan.textContent = data.num_person;
if (data.num_person > 0) {
videoContainer.classList.add('person-detected');
personBadge.style.animation = 'pulse 2s infinite';
} else {
videoContainer.classList.remove('person-detected');
personBadge.style.animation = 'none';
}
}
}
else if (data.event === 'connection') {
console.log('Connection status:', data.message);
addAlert(data.message, 'success');
}
else if (data.event === 'pong') {
// Keep-alive response
console.debug('Keep-alive pong received');
}
}
function displayFrame(base64Frame) {
const currentTime = Date.now();
if (currentTime - lastFrameTime > 33) {
videoFrame.src = `data:image/jpeg;base64,${base64Frame}`;
lastFrameTime = currentTime;
}
}
function showToastNotification(message, personCount) {
let toastContainer = document.querySelector('.toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
toastContainer.style.zIndex = '9999';
document.body.appendChild(toastContainer);
}
const toastId = `toast-${Date.now()}`;
const toastHtml = `
<div id="${toastId}" class="toast" role="alert" data-bs-autohide="true" data-bs-delay="4000">
<div class="toast-header bg-danger text-white">
<i class="fas fa-user-shield me-2"></i>
<strong class="me-auto">Person Detected!</strong>
<small>Now</small>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">
<i class="fas fa-exclamation-triangle text-warning me-2"></i>
${message} (${personCount} person)
<div class="decrypt-stats mt-1">
<i class="fas fa-shield-alt"></i> Via Secure Proxy
</div>
</div>
</div>
`;
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
const toastElement = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastElement);
toast.show();
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
}
function playBeep() {
try {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (AudioContext) {
const audioCtx = new AudioContext();
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.frequency.value = 880;
gainNode.gain.value = 0.2;
oscillator.start();
gainNode.gain.exponentialRampToValueAtTime(0.00001, audioCtx.currentTime + 0.3);
oscillator.stop(audioCtx.currentTime + 0.3);
}
} catch (e) {
console.log('Beep not supported');
}
}
function updateStats() {
totalDetectionsSpan.textContent = stats.totalDetections;
if (stats.lastDetection) {
const diff = Math.floor((new Date() - stats.lastDetection) / 1000);
if (diff < 60) {
lastDetectionSpan.textContent = `${diff} detik yang lalu`;
} else if (diff < 3600) {
lastDetectionSpan.textContent = `${Math.floor(diff / 60)} menit yang lalu`;
} else {
lastDetectionSpan.textContent = `${Math.floor(diff / 3600)} jam yang lalu`;
}
}
}
function addAlert(message, type = 'info') {
const alertDiv = document.createElement('div');
alertDiv.className = `alert-item ${type}`;
const now = new Date();
const timeStr = now.toLocaleTimeString('id-ID', { hour12: false });
const dateStr = now.toLocaleDateString('id-ID');
let icon = '';
if (type === 'success') icon = '<i class="fas fa-exclamation-triangle text-danger me-2"></i>';
else if (type === 'danger') icon = '<i class="fas fa-times-circle text-danger me-2"></i>';
else icon = '<i class="fas fa-info-circle text-info me-2"></i>';
alertDiv.innerHTML = `
${icon}
<strong>${type === 'success' ? '🚨 PERSON DETECTED' : type.toUpperCase()}</strong>
<div class="mt-1">${message}</div>
<div class="alert-time">
<i class="fas fa-shield-alt me-1"></i> Secure Proxy
<i class="far fa-calendar-alt ms-2 me-1"></i>${dateStr}
<i class="far fa-clock ms-2 me-1"></i>${timeStr}
</div>
`;
alertList.insertBefore(alertDiv, alertList.firstChild);
while (alertList.children.length > 100) {
alertList.removeChild(alertList.lastChild);
}
alertCountSpan.textContent = alertList.children.length;
const alertContainer = document.querySelector('.alert-list');
if (alertContainer) alertContainer.scrollTop = 0;
}
function clearAlerts() {
alertList.innerHTML = '';
addAlert('Alert history cleared', 'warning');
alertCountSpan.textContent = '1';
}
function updateConnectionStatus(connected) {
if (connected) {
statusIndicator.className = 'connection-status connected';
statusText.textContent = 'Connected (Proxy)';
statusText.style.color = '#28a745';
} else {
statusIndicator.className = 'connection-status disconnected';
statusText.textContent = 'Disconnected';
statusText.style.color = '#dc3545';
}
}
window.addEventListener('load', () => {
videoFrame.src = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360"%3E%3Crect width="640" height="360" fill="%23222"%3E%3C/rect%3E%3Ctext x="320" y="180" text-anchor="middle" fill="%23666" font-size="16"%3EClick "Configure Connection" to connect%3C/text%3E%3C/svg%3E';
// Auto-show config modal on first load
setTimeout(() => {
showConfigModal();
}, 500);
});
window.addEventListener('beforeunload', () => {
if (ws) {
ws.close();
}
if (reconnectInterval) clearInterval(reconnectInterval);
});
</script>
</body>
</html>
Penjelasan kode:
Kode website ini adalah antarmuka web yang dirancang dengan arsitektur keamanan tinggi. Berbeda dengan versi sebelumnya yang terhubung langsung ke MQTT broker, versi ini hanya terhubung ke proxy server (service Python yang telah dijelaskan sebelumnya). Semua kredensial sensitif (MQTT username/password, AES key/IV) tidak pernah terekspos ke browser - tersimpan aman di server proxy.
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Person Detection - CCTV Monitoring (Secure Proxy)</title>
Penjelasan:
Halaman menggunakan bahasa Indonesia (lang="id")
Responsif untuk semua device (viewport)
Judul menegaskan arsitektur secure proxy
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
Penjelasan:
Bootstrap 5.3: Framework CSS untuk layout responsif dan komponen UI
Font Awesome 6.4: Library icon (kamera, lock, shield, bell, dll)
.proxy-badge {
background: rgba(0,0,0,0.75);
backdrop-filter: blur(5px);
padding: 8px 16px;
border-radius: 50px;
color: #ffa500; /* Warna orange untuk menonjolkan proxy */
font-size: 12px;
font-weight: 500;
font-family: monospace;
}
Penjelasan: Badge berwarna orange yang menandakan koneksi melalui secure proxy, berbeda dengan badge hijau untuk enkripsi biasa.
.config-panel {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
}
.modal-content {
background: linear-gradient(135deg, #0f0c29, #302b63);
color: white;
}
Penjelasan:
Tombol konfigurasi melayang di pojok kanan bawah
Modal settings dengan tema gelap untuk memasukkan alamat proxy server
Perbedaan Utama dengan Versi Sebelumnya
<div class="text-center mb-4">
<h1 class="text-white">
<i class="fas fa-video me-3"></i>
Person Detection Monitoring System
</h1>
<p class="text-white-50">Real-time detection with YOLOv8 | Secure Proxy Architecture | No Direct MQTT Access</p>
</div>
Penjelasan: Menekankan arsitektur baru: "Secure Proxy Architecture" dan "No Direct MQTT Access".
<div class="video-container frame-border" id="videoContainer">
<img id="videoFrame" src="" alt="Waiting for stream...">
<div class="video-overlay">
<div class="status-badge">
<span class="connection-status disconnected" id="statusIndicator"></span>
<span id="statusText">Disconnected</span>
</div>
<div class="proxy-badge">
<i class="fas fa-shield-alt"></i> Secure Proxy <!-- Beda: shield icon -->
</div>
<div class="person-badge" id="personBadge">
<i class="fas fa-user"></i> <span id="personCount">0</span>
</div>
</div>
</div>
Perubahan: Badge enkripsi diganti dengan badge "Secure Proxy" dengan icon shield, menandakan data melewati proxy.
<div class="stat-card" style="background: linear-gradient(135deg, #4facfe, #00f2fe);">
<div class="stat-icon"><i class="fas fa-shield-alt"></i></div>
<div class="stat-value" id="proxyStatus">Active</div>
<div>Secure Proxy Mode</div>
</div>
Penjelasan: Statistik ketiga menampilkan status "Secure Proxy Mode" bukan "AES128 Encryption".
<div class="modal fade" id="configModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-network-wired me-2"></i>Proxy Connection Settings</h5>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">WebSocket Server Address</label>
<input type="text" class="form-control" id="wsUrl" placeholder="ws://localhost:8765" value="ws://localhost:8765">
<small class="text-muted">Alamat server proxy (default: ws://localhost:8765)</small>
</div>
<div class="alert alert-warning">
<i class="fas fa-shield-alt me-2"></i>
<strong>Secure Mode:</strong> Kredensial MQTT dan AES disimpan aman di server proxy.
</div>
</div>
</div>
</div>
</div>
Fungsi: Modal untuk memasukkan alamat WebSocket server proxy. User bisa mengisi:
ws://localhost:8765 (jika service di komputer yang sama)
ws://192.168.x.x:8765 (jika service di komputer lain dalam jaringan)
<!-- VERSI SEBELUMNYA (Direct MQTT) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mqtt/4.3.7/mqtt.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<!-- VERSI INI (Secure Proxy) -->
<!-- TIDAK ADA library MQTT dan CryptoJS! -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
Penjelasan:
MQTT.js dihapus - Karena frontend tidak perlu koneksi langsung ke broker
CryptoJS dihapus - Karena dekripsi dilakukan di server proxy
Bootstrap JS tetap - Untuk modal dan toast notifications
javascript
// VERSI SEBELUMNYA (TIDAK AMAN) - TIDAK ADA DI KODE INI!
// const MQTT_CONFIG = {
// username: 'pbl2026', ← TEREXPOS!
// password: 'Pbl123456789#' ← TEREXPOS!
// };
// const AES_CONFIG = {
// key: '16bytekey1234567', ← TEREXPOS!
// iv: '16byteiv12345678' ← TEREXPOS!
// };
// VERSI INI (AMAN) - TIDAK ADA KREDENSIAL!
// Tidak ada konfigurasi MQTT atau AES di frontend!
Poin Keamanan Kritis: Tidak ada satu pun kredensial yang tersimpan di frontend!
let ws = null; // WebSocket connection object
let stats = {
totalDetections: 0, // Total person terdeteksi
lastDetection: null, // Waktu deteksi terakhir
currentPersonCount: 0 // Jumlah person saat ini
};
let reconnectAttempts = 0; // Hitungan percobaan reconnect
let lastFrameTime = 0; // Untuk throttling frame rate (33ms)
let reconnectInterval = null; // Interval untuk keep-alive ping
Perubahan: Tidak ada variabel untuk decryptSuccess/decryptFailed karena dekripsi dilakukan di server.
const videoFrame = document.getElementById('videoFrame');
const videoContainer = document.getElementById('videoContainer');
const statusIndicator = document.getElementById('statusIndicator');
const statusText = document.getElementById('statusText');
const personCountSpan = document.getElementById('personCount');
const personBadge = document.getElementById('personBadge');
const totalDetectionsSpan = document.getElementById('totalDetections');
const lastDetectionSpan = document.getElementById('lastDetection');
const alertList = document.getElementById('alertList');
const alertCountSpan = document.getElementById('alertCount');
function showConfigModal() {
const modal = new bootstrap.Modal(document.getElementById('configModal'));
modal.show();
}
Fungsi: Membuka modal konfigurasi untuk memasukkan alamat WebSocket server.
function connectToProxy() {
const wsUrl = document.getElementById('wsUrl').value; // Ambil URL dari input
// Tutup koneksi lama jika ada
if (ws) {
ws.close();
if (reconnectInterval) clearInterval(reconnectInterval);
}
try {
console.log('Connecting to proxy server:', wsUrl);
ws = new WebSocket(wsUrl); // WebSocket native, tanpa library eksternal!
ws.onopen = () => {
console.log('✅ Connected to proxy server');
updateConnectionStatus(true);
addAlert('Connected to secure proxy server', 'success');
reconnectAttempts = 0;
// Kirim ping setiap 30 detik untuk keep-alive
reconnectInterval = setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ command: 'ping' }));
}
}, 30000);
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
handleMessage(data);
} catch (e) {
console.error('Error parsing message:', e);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
updateConnectionStatus(false);
addAlert('Proxy connection error', 'danger');
};
ws.onclose = () => {
console.log('Disconnected from proxy server');
updateConnectionStatus(false);
addAlert('Disconnected from proxy server, attempting to reconnect...', 'warning');
// Auto reconnect setelah 5 detik
setTimeout(() => {
if (ws && ws.readyState === WebSocket.CLOSED) {
connectToProxy();
}
}, 5000);
};
// Tutup modal
bootstrap.Modal.getInstance(document.getElementById('configModal')).hide();
} catch (error) {
console.error('Failed to connect:', error);
addAlert(`Connection failed: ${error.message}`, 'danger');
}
}
Penjelasan Detail Disamping
Penting: Menggunakan WebSocket native browser (new WebSocket()), bukan library MQTT.js!
reconnectInterval = setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ command: 'ping' }));
}
}, 30000);
Fungsi: Mengirim ping setiap 30 detik untuk mencegah timeout koneksi. Server akan membalas dengan {'event': 'pong'}.
function handleMessage(data) {
if (data.event === 'person_detected') {
// Person detection notification
stats.totalDetections++;
stats.lastDetection = new Date(data.timestamp);
stats.currentPersonCount = data.num_person;
updateStats();
addAlert(`🔴 ${data.message} (${data.num_person} person detected)`, 'success');
showToastNotification(data.message, data.num_person);
playBeep();
}
else if (data.event === 'video_frame') {
// Video frame
if (data.frame) {
displayFrame(data.frame);
stats.currentPersonCount = data.num_person;
updateStats();
personCountSpan.textContent = data.num_person;
if (data.num_person > 0) {
videoContainer.classList.add('person-detected');
personBadge.style.animation = 'pulse 2s infinite';
} else {
videoContainer.classList.remove('person-detected');
personBadge.style.animation = 'none';
}
}
}
else if (data.event === 'connection') {
// Welcome message dari server
console.log('Connection status:', data.message);
addAlert(data.message, 'success');
}
else if (data.event === 'pong') {
// Response dari ping
console.debug('Keep-alive pong received');
}
}
Perbedaan Signifikan dengan Versi Sebelumnya:
Mengapa lebih sederhana? Karena server proxy sudah mendekripsi data sebelum mengirim ke frontend!
function displayFrame(base64Frame) {
const currentTime = Date.now();
// Throttling: hanya update setiap 33ms (~30 fps)
if (currentTime - lastFrameTime > 33) {
videoFrame.src = `data:image/jpeg;base64,${base64Frame}`;
lastFrameTime = currentTime;
}
}
Penjelasan: Sama dengan versi sebelumnya - mencegah overload browser dengan membatasi update frame ke ~30 fps.
function showToastNotification(message, personCount) {
let toastContainer = document.querySelector('.toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
toastContainer.style.zIndex = '9999';
document.body.appendChild(toastContainer);
}
const toastId = `toast-${Date.now()}`;
const toastHtml = `
<div id="${toastId}" class="toast" role="alert" data-bs-autohide="true" data-bs-delay="4000">
<div class="toast-header bg-danger text-white">
<i class="fas fa-user-shield me-2"></i>
<strong class="me-auto">Person Detected!</strong>
<small>Now</small>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">
<i class="fas fa-exclamation-triangle text-warning me-2"></i>
${message} (${personCount} person)
<div class="decrypt-stats mt-1">
<i class="fas fa-shield-alt"></i> Via Secure Proxy <!-- Beda: proxy badge -->
</div>
</div>
</div>
`;
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
const toastElement = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastElement);
toast.show();
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
}
Perubahan: Teks "AES128 Decrypted" diganti dengan "Via Secure Proxy".
function playBeep() {
try {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (AudioContext) {
const audioCtx = new AudioContext();
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.frequency.value = 880; // Nada A (880 Hz)
gainNode.gain.value = 0.2; // Volume 20%
oscillator.start();
gainNode.gain.exponentialRampToValueAtTime(0.00001, audioCtx.currentTime + 0.3);
oscillator.stop(audioCtx.currentTime + 0.3);
}
} catch (e) {
console.log('Beep not supported');
}
}
Penjelasan: Web Audio API untuk menghasilkan suara beep singkat. Volume rendah (20%) agar tidak mengganggu. Sama dengan versi sebelumnya.
function updateStats() {
totalDetectionsSpan.textContent = stats.totalDetections;
if (stats.lastDetection) {
const diff = Math.floor((new Date() - stats.lastDetection) / 1000);
if (diff < 60) {
lastDetectionSpan.textContent = `${diff} detik yang lalu`;
} else if (diff < 3600) {
lastDetectionSpan.textContent = `${Math.floor(diff / 60)} menit yang lalu`;
} else {
lastDetectionSpan.textContent = `${Math.floor(diff / 3600)} jam yang lalu`;
}
}
}
function addAlert(message, type = 'info') {
const alertDiv = document.createElement('div');
alertDiv.className = `alert-item ${type}`;
const now = new Date();
const timeStr = now.toLocaleTimeString('id-ID', { hour12: false });
const dateStr = now.toLocaleDateString('id-ID');
let icon = '';
if (type === 'success') icon = '<i class="fas fa-exclamation-triangle text-danger me-2"></i>';
else if (type === 'danger') icon = '<i class="fas fa-times-circle text-danger me-2"></i>';
else icon = '<i class="fas fa-info-circle text-info me-2"></i>';
alertDiv.innerHTML = `
${icon}
<strong>${type === 'success' ? '🚨 PERSON DETECTED' : type.toUpperCase()}</strong>
<div class="mt-1">${message}</div>
<div class="alert-time">
<i class="fas fa-shield-alt me-1"></i> Secure Proxy <!-- Beda: proxy badge -->
<i class="far fa-calendar-alt ms-2 me-1"></i>${dateStr}
<i class="far fa-clock ms-2 me-1"></i>${timeStr}
</div>
`;
alertList.insertBefore(alertDiv, alertList.firstChild);
// Limit to 100 alerts
while (alertList.children.length > 100) {
alertList.removeChild(alertList.lastChild);
}
alertCountSpan.textContent = alertList.children.length;
const alertContainer = document.querySelector('.alert-list');
if (alertContainer) alertContainer.scrollTop = 0;
}
Perubahan: Teks "AES128 Decrypted" diganti dengan "Secure Proxy".
function clearAlerts() {
alertList.innerHTML = '';
addAlert('Alert history cleared', 'warning');
alertCountSpan.textContent = '1';
}
function updateConnectionStatus(connected) {
if (connected) {
statusIndicator.className = 'connection-status connected';
statusText.textContent = 'Connected (Proxy)'; // Beda: "(Proxy)"
statusText.style.color = '#28a745';
} else {
statusIndicator.className = 'connection-status disconnected';
statusText.textContent = 'Disconnected';
statusText.style.color = '#dc3545';
}
}
Perubahan: Status text menjadi "Connected (Proxy)" bukan "Connected (AES128)".
window.addEventListener('load', () => {
// Placeholder SVG while waiting for connection
videoFrame.src = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360"%3E%3Crect width="640" height="360" fill="%23222"%3E%3C/rect%3E%3Ctext x="320" y="180" text-anchor="middle" fill="%23666" font-size="16"%3EClick "Configure Connection" to connect%3C/text%3E%3C/svg%3E';
// Auto-show config modal on first load (user-friendly)
setTimeout(() => {
showConfigModal();
}, 500);
});
Fungsi:
Menampilkan placeholder video saat pertama kali load
Otomatis membuka modal konfigurasi setelah 0.5 detik (memudahkan user baru)
window.addEventListener('beforeunload', () => {
if (ws) {
ws.close(); // Tutup koneksi WebSocket dengan bersih
}
if (reconnectInterval) {
clearInterval(reconnectInterval); // Hentikan ping interval
}
});
Fungsi: Membersihkan koneksi saat user menutup atau me-refresh halaman.
1. User membuka halaman
↓
2. Halaman load → menampilkan placeholder video
↓
3. Modal konfigurasi otomatis muncul (setelah 0.5 detik)
↓
4. User memasukkan alamat WebSocket server proxy
(contoh: ws://localhost:8765 atau ws://192.168.1.100:8765)
↓
5. Klik "Connect"
↓
6. WebSocket connection ke proxy server
↓
7. ┌─────────────────────────────────────────────────────┐
│ KONEKSI AKTIF: │
│ - Kirim ping setiap 30 detik (keep-alive) │
│ - Terima data dari proxy dalam format JSON │
│ - Data sudah plain text (tidak perlu dekripsi) │
└─────────────────────────────────────────────────────┘
↓
8. Saat menerima 'person_detected':
- Update statistik
- Tambah alert
- Tampilkan toast notification
- Bunyikan beep
↓
9. Saat menerima 'video_frame':
- Tampilkan frame di elemen img
- Update jumlah person di badge
- Efek visual jika ada person (border merah + animasi pulse)
↓
10. User dapat:
- Klik "Clear Alert History" untuk menghapus alert
- Klik "Configure Connection" untuk ganti server proxy
- Tutup halaman (koneksi otomatis ditutup)
<!-- Versi Sebelumnya -->
<script src="mqtt.js"></script>
<script src="crypto-js.js"></script>
<!-- Versi Ini -->
<!-- Tidak ada tambahan library selain Bootstrap -->
// Versi Sebelumnya
client = mqtt.connect('wss://hivemq.cloud:8884/mqtt', {
username: 'pbl2026', // TEREXPOS!
password: 'xxx' // TEREXPOS!
});
// Versi Ini
ws = new WebSocket('ws://localhost:8765');
// Tidak ada credential!
// Versi Sebelumnya
client.on('message', (topic, message) => {
const decrypted = decryptAES128(message.toString()); // Dekripsi di browser
handleMessage(topic, decrypted);
});
// Versi Ini
ws.onmessage = (event) => {
const data = JSON.parse(event.data); // Langsung parse, sudah plain text!
handleMessage(data);
};
1. Jalankan service Python (person_detection_service.py) di server
2. Buka file HTML ini di browser
3. Modal akan muncul - masukkan alamat WebSocket server:
o Jika service di komputer yang sama: ws://localhost:8765
o Jika di komputer lain: ws://[IP_SERVER]:8765
4. Klik Connect
5. Lihat stream video real-time dari kamera
6. Dapatkan notifikasi saat person terdeteksi
Keamanan Maksimal - Tidak ada kredensial di frontend
Sederhana - Frontend hanya perlu WebSocket (native browser)
Fleksibel - Bisa ganti alamat proxy tanpa reload page
Multi-client - Banyak browser bisa connect ke proxy yang sama
Audit-friendly - Semua credential di satu tempat (server)
Mobile friendly - Desain responsif, bisa diakses dari HP
Dengan cara yang sama, pastikan:
· RTSP Camera telah aktif
· Kode Service Python ServiceAES128FullBackend.py telah aktif
· Buka dengan internet browser file berikut MonitoringAESFullFrontEnd.html
Di sini tercatat alamat IP RTSP Camera adalah rtsp://192.168.34.229:1945
Kemudian, Jalankan kode service python.
Status terdeteksi
Cek di MQTTBox untuk melihat payload yang terenkripsi
Cek payload ketika di dekripsi
Cek halaman website MonitoringAESFullFrontEnd.html
Di sini localhost dapat diganti dengan alamat IP dimana kode service berada, karena kebetulan antara website dan kode service python berada dalam satu laptop/mesin yang sama maka anda dapat menggunakan localhost.
Sekarang kita coba mengganti localhost dengan alamat IP kode service python sesungguhnya.
Buka command prompt dan tuliskan perintah IPConfig.
IP nya adalah 192.168.134.230
Hasilnya adalah
Tampak bahwa respon website terjadi secara realtime tanpa perlu melakukan refresh page terlebih dahulu.