Gunakan VSCode atau Notepad++ untuk membuat kode websitenya.
Simpan file kode websitenya dengan “MonitoringAES.html”.
Kode lengkapnya adalah sebagai berikut,:
<!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 (AES128 Encrypted)</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);
}
.encryption-badge {
background: rgba(0,0,0,0.75);
backdrop-filter: blur(5px);
padding: 8px 16px;
border-radius: 50px;
color: #00ff88;
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);
}
.decrypt-stats {
font-size: 11px;
color: #00ff88;
margin-top: 5px;
}
</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 | AES128 Encrypted | HiveMQ MQTT over WebSocket</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="encryption-badge">
<i class="fas fa-lock"></i> AES128-CBC
</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-lock ms-2"></i> Semua data terenkripsi AES128-CBC
</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-lock"></i></div>
<div class="stat-value" id="decryptStatus">Active</div>
<div>AES128 Encryption</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 MQTT...</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 | AES128-CBC Encrypted | HiveMQ MQTT over WebSocket | Real-time Detection</small>
</footer>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<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>
<script>
// ========== KONFIGURASI HIVEMQ CLOUD (WEBSOCKET) ==========
const MQTT_CONFIG = {
hostname: 'wss://c0099a6e70884169bfc6b2f482c29e2b.s1.eu.hivemq.cloud:8884/mqtt',
username: 'pbl2026',
password: 'Pbl123456789#',
topics: {
person: 'Person',
frame: 'VideoFrame'
}
};
// ========== KONFIGURASI AES128 (SAMA DENGAN SERVICE) ==========
const AES_CONFIG = {
key: CryptoJS.enc.Utf8.parse('16bytekey1234567'), // 16 bytes key
iv: CryptoJS.enc.Utf8.parse('16byteiv12345678') // 16 bytes IV
};
let stats = {
totalDetections: 0,
lastDetection: null,
currentPersonCount: 0,
decryptSuccess: 0,
decryptFailed: 0
};
let client = null;
let reconnectAttempts = 0;
let lastFrameTime = 0;
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');
/**
* Dekripsi AES128 CBC
* @param {string} encryptedBase64 - Data terenkripsi dalam format Base64
* @returns {object} - Hasil dekripsi dalam bentuk object JSON
*/
function decryptAES128(encryptedBase64) {
try {
// Decode Base64 ke Ciphertext
const ciphertext = CryptoJS.enc.Base64.parse(encryptedBase64);
// Konfigurasi decrypt
const decrypted = CryptoJS.AES.decrypt(
{ ciphertext: ciphertext },
AES_CONFIG.key,
{
iv: AES_CONFIG.iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
);
// Konversi ke string UTF-8
const decryptedText = decrypted.toString(CryptoJS.enc.Utf8);
if (!decryptedText) {
throw new Error('Decryption resulted in empty string');
}
// Parse JSON
const jsonData = JSON.parse(decryptedText);
stats.decryptSuccess++;
return jsonData;
} catch (error) {
stats.decryptFailed++;
console.error('Decryption failed:', error);
console.error('Encrypted data (first 100 chars):', encryptedBase64.substring(0, 100));
return null;
}
}
function connectMQTT() {
try {
const options = {
clientId: `web_viewer_${Math.random().toString(36).substr(2, 8)}`,
username: MQTT_CONFIG.username,
password: MQTT_CONFIG.password,
clean: true,
reconnectPeriod: 5000,
connectTimeout: 30000,
keepalive: 60
};
console.log('Connecting to HiveMQ via WebSocket...');
console.log('URL:', MQTT_CONFIG.hostname);
console.log('Username:', MQTT_CONFIG.username);
console.log('AES128 Encryption: ENABLED');
client = mqtt.connect(MQTT_CONFIG.hostname, options);
client.on('connect', () => {
console.log('✅ Connected to HiveMQ broker');
updateConnectionStatus(true);
client.subscribe(MQTT_CONFIG.topics.person, { qos: 1 });
client.subscribe(MQTT_CONFIG.topics.frame, { qos: 0 });
addAlert('Connected to HiveMQ broker successfully (AES128 Decryption Active)', 'success');
reconnectAttempts = 0;
});
client.on('error', (err) => {
console.error('MQTT error:', err);
updateConnectionStatus(false);
addAlert(`MQTT Error: ${err.message}`, 'danger');
});
client.on('offline', () => {
console.log('MQTT client offline');
updateConnectionStatus(false);
addAlert('MQTT client offline, attempting to reconnect...', 'warning');
});
client.on('reconnect', () => {
reconnectAttempts++;
console.log(`Reconnecting... attempt ${reconnectAttempts}`);
});
client.on('message', (topic, message) => {
try {
// Terima encrypted message sebagai string
const encryptedData = message.toString();
// Decrypt terlebih dahulu
const decryptedPayload = decryptAES128(encryptedData);
if (decryptedPayload) {
handleMessage(topic, decryptedPayload);
} else {
console.error('Failed to decrypt message from topic:', topic);
addAlert(`Failed to decrypt message from ${topic}`, 'danger');
}
} catch (e) {
console.error('Error processing message:', e);
addAlert(`Error: ${e.message}`, 'danger');
}
});
} catch (error) {
console.error('Failed to connect:', error);
addAlert(`Connection failed: ${error.message}`, 'danger');
setTimeout(connectMQTT, 5000);
}
}
function handleMessage(topic, payload) {
if (topic === MQTT_CONFIG.topics.person) {
if (payload.event === 'person_detected') {
stats.totalDetections++;
stats.lastDetection = new Date(payload.timestamp);
stats.currentPersonCount = payload.num_person;
updateStats();
addAlert(`🔴 ${payload.message} (${payload.num_person} person detected)`, 'success');
showToastNotification(payload.message, payload.num_person);
playBeep();
}
}
else if (topic === MQTT_CONFIG.topics.frame) {
if (payload.frame) {
displayFrame(payload.frame);
stats.currentPersonCount = payload.num_person;
updateStats();
personCountSpan.textContent = payload.num_person;
if (payload.num_person > 0) {
videoContainer.classList.add('person-detected');
personBadge.style.animation = 'pulse 2s infinite';
} else {
videoContainer.classList.remove('person-detected');
personBadge.style.animation = 'none';
}
}
}
}
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-lock"></i> AES128 Decrypted
</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`;
}
}
// Update decrypt status di console untuk debugging
if (stats.decryptFailed > 0) {
console.warn(`Decrypt stats - Success: ${stats.decryptSuccess}, Failed: ${stats.decryptFailed}`);
}
}
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-lock me-1"></i> AES128 Decrypted
<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 (AES128)';
statusText.style.color = '#28a745';
} else {
statusIndicator.className = 'connection-status disconnected';
statusText.textContent = 'Disconnected';
statusText.style.color = '#dc3545';
}
}
window.addEventListener('load', () => {
connectMQTT();
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"%3EWaiting for video stream...%3C/text%3E%3C/svg%3E';
});
window.addEventListener('beforeunload', () => {
if (client) {
client.end();
}
});
</script>
</body>
</html>
Kode website ini adalah antarmuka web berbasis real-time untuk memonitor deteksi person dari kamera CCTV. Website ini terhubung ke broker MQTT HiveMQ melalui WebSocket, menerima data terenkripsi AES128, lalu mendekripsinya untuk ditampilkan kepada pengguna. Ini adalah komponen frontend yang bekerja sama dengan service Python yang telah dijelaskan sebelumnya.
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
Penjelasan: Mendeklarasikan dokumen sebagai HTML5, menggunakan bahasa Indonesia, dan memastikan tampilan responsif di berbagai perangkat (desktop, tablet, mobile).
<title>Person Detection - CCTV Monitoring (AES128 Encrypted)</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">
Penjelasan:
Title: Menentukan judul tab browser
Bootstrap CSS: Framework CSS untuk tampilan modern dan responsif
Font Awesome: Library icon untuk elemen visual (ikon kamera, lock, bell, dll)
body {
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
min-height: 100vh;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
Penjelasan: Memberikan background gradasi ungu gelap yang modern dan elegan, mencakup seluruh tinggi viewport.
.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);
}
Penjelasan:
border-radius 20px: Membuat sudut card melengkung
box-shadow: Memberikan efek bayangan 3D
backdrop-filter: blur(10px): Efek kaca (glassmorphism) pada background
background semi-transparan: Membuat card terlihat modern
.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);
}
Penjelasan:
aspect-ratio: 16/9: Memastikan rasio video selalu 16:9 (standar widescreen)
position: relative: Untuk positioning overlay di dalamnya
.video-overlay {
position: absolute;
bottom: 15px;
left: 15px;
right: 15px;
display: flex;
justify-content: space-between;
align-items: center;
pointer-events: none;
}
Penjelasan: Overlay transparan di atas video yang menampilkan status koneksi, jumlah person, dan badge enkripsi. pointer-events: none membuat overlay tidak mengganggu interaksi dengan video.
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.9; }
100% { transform: scale(1); opacity: 1; }
}
Penjelasan: Animasi berdenyut (pulse) yang membuat badge person lebih menonjol saat ada deteksi.
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
Penjelasan: Alert muncul dengan animasi slide dari kanan, memberikan efek notifikasi yang halus.
.connection-status {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
animation: blink 1.5s infinite;
}
.connected { background-color: #28a745; }
.disconnected { background-color: #dc3545; }
Penjelasan: Indikator bulat kecil yang berkedip untuk menunjukkan status koneksi MQTT (hijau = connected, merah = disconnected).
<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 | AES128 Encrypted | HiveMQ MQTT over WebSocket</p>
</div>
Penjelasan: Header halaman dengan judul dan deskripsi singkat tentang teknologi yang digunakan.
<div class="row g-4">
<div class="col-lg-8"> <!-- Kolom kiri: 8 bagian dari 12 -->
<!-- Video stream card -->
</div>
<div class="col-lg-4"> <!-- Kolom kanan: 4 bagian dari 12 -->
<!-- Statistics card -->
</div>
</div>
Penjelasan: Menggunakan sistem grid Bootstrap 12 kolom. Kolom kiri (8/12) untuk video, kolom kanan (4/12) untuk statistik.
<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="encryption-badge">
<i class="fas fa-lock"></i> AES128-CBC
</div>
<div class="person-badge" id="personBadge">
<i class="fas fa-user"></i> <span id="personCount">0</span>
</div>
</div>
</div>
Penjelasan:
#videoFrame: Elemen img yang akan menampilkan stream video (base64)
#videoContainer: Container dengan border yang berubah merah saat ada person
Overlay: Menampilkan status koneksi, badge enkripsi, dan jumlah person
<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>
Penjelasan: Menampilkan total deteksi person dan waktu deteksi terakhir.
<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 MQTT...</div>
</div>
</div>
Penjelasan: Area scrollable untuk menampilkan riwayat alert deteksi person.
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<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>
Penjelasan:
Bootstrap JS: Untuk komponen interaktif (toast, modal)
MQTT.js: Library MQTT over WebSocket untuk koneksi ke broker
CryptoJS: Library kriptografi untuk dekripsi AES128
const MQTT_CONFIG = {
hostname: 'wss://c0099a6e70884169bfc6b2f482c29e2b.s1.eu.hivemq.cloud:8884/mqtt',
username: 'pbl2026',
password: 'Pbl123456789#',
topics: {
person: 'Person',
frame: 'VideoFrame'
}
};
Penjelasan: Konfigurasi koneksi ke HiveMQ via WebSocket (port 8884). Ganti dengan kredensial Anda sendiri.
const AES_CONFIG = {
key: CryptoJS.enc.Utf8.parse('16bytekey1234567'),
iv: CryptoJS.enc.Utf8.parse('16byteiv12345678')
};
Penjelasan: Key dan IV untuk dekripsi AES128. Harus sama persis dengan yang digunakan di service Python.
let stats = {
totalDetections: 0, // Total person terdeteksi
lastDetection: null, // Waktu deteksi terakhir
currentPersonCount: 0, // Jumlah person saat ini
decryptSuccess: 0, // Counter keberhasilan dekripsi
decryptFailed: 0 // Counter kegagalan dekripsi
};
let client = null; // MQTT client instance
let reconnectAttempts = 0; // Hitungan percobaan reconnect
let lastFrameTime = 0; // Throttling untuk frame rate
function decryptAES128(encryptedBase64) {
try {
// Step 1: Decode Base64 ke format yang bisa diproses CryptoJS
const ciphertext = CryptoJS.enc.Base64.parse(encryptedBase64);
// Step 2: Lakukan dekripsi dengan AES-CBC, PKCS7 padding
const decrypted = CryptoJS.AES.decrypt(
{ ciphertext: ciphertext },
AES_CONFIG.key,
{
iv: AES_CONFIG.iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
);
// Step 3: Konversi hasil dekripsi ke string UTF-8
const decryptedText = decrypted.toString(CryptoJS.enc.Utf8);
// Step 4: Parse JSON
const jsonData = JSON.parse(decryptedText);
stats.decryptSuccess++;
return jsonData;
} catch (error) {
stats.decryptFailed++;
console.error('Decryption failed:', error);
return null;
}
}
Penjelasan: Fungsi ini menerima string Base64 terenkripsi, mendekripsinya dengan AES128-CBC, dan mengembalikan object JSON asli. Jika gagal, mengembalikan null.
function connectMQTT() {
const options = {
clientId: `web_viewer_${Math.random().toString(36).substr(2, 8)}`,
username: MQTT_CONFIG.username,
password: MQTT_CONFIG.password,
clean: true,
reconnectPeriod: 5000, // Auto reconnect setiap 5 detik
connectTimeout: 30000, // Timeout 30 detik
keepalive: 60 // Keep-alive setiap 60 detik
};
client = mqtt.connect(MQTT_CONFIG.hostname, options);
// Event handler untuk berbagai situasi
client.on('connect', () => { ... });
client.on('error', (err) => { ... });
client.on('offline', () => { ... });
client.on('reconnect', () => { ... });
client.on('message', (topic, message) => { ... });
}
client.on('connect', () => {
updateConnectionStatus(true);
// Subscribe ke kedua topic
client.subscribe(MQTT_CONFIG.topics.person, { qos: 1 });
client.subscribe(MQTT_CONFIG.topics.frame, { qos: 0 });
addAlert('Connected to HiveMQ broker successfully', 'success');
});
Penjelasan: Saat koneksi berhasil, update status, subscribe ke topic 'Person' (QoS 1) dan 'VideoFrame' (QoS 0), lalu tampilkan alert sukses.
client.on('message', (topic, message) => {
try {
const encryptedData = message.toString();
const decryptedPayload = decryptAES128(encryptedData);
if (decryptedPayload) {
handleMessage(topic, decryptedPayload);
}
} catch (e) {
console.error('Error processing message:', e);
}
});
Penjelasan: Setiap pesan yang diterima langsung didekripsi, lalu diproses sesuai topiknya.
function handleMessage(topic, payload) {
if (topic === MQTT_CONFIG.topics.person) {
// Person detection notification
if (payload.event === 'person_detected') {
stats.totalDetections++;
stats.lastDetection = new Date(payload.timestamp);
updateStats();
addAlert(`🔴 ${payload.message} (${payload.num_person} person detected)`, 'success');
showToastNotification(payload.message, payload.num_person);
playBeep(); // Bunyi beep
}
}
else if (topic === MQTT_CONFIG.topics.frame) {
// Video frame
if (payload.frame) {
displayFrame(payload.frame);
personCountSpan.textContent = payload.num_person;
// Efek visual saat ada person
if (payload.num_person > 0) {
videoContainer.classList.add('person-detected');
personBadge.style.animation = 'pulse 2s infinite';
} else {
videoContainer.classList.remove('person-detected');
personBadge.style.animation = 'none';
}
}
}
}
function displayFrame(base64Frame) {
const currentTime = Date.now();
// Throttling: hanya update frame setiap 33ms (~30 fps)
if (currentTime - lastFrameTime > 33) {
videoFrame.src = `data:image/jpeg;base64,${base64Frame}`;
lastFrameTime = currentTime;
}
}
Penjelasan: Menampilkan frame video dengan format data:image/jpeg;base64, prefix. Pembatasan kecepatan (throttling) mencegah lag.
function showToastNotification(message, personCount) {
// Buat container toast jika belum ada
let toastContainer = document.querySelector('.toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
document.body.appendChild(toastContainer);
}
// Buat elemen toast baru
const toastHtml = `...`; // HTML toast dengan style merah
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
// Tampilkan toast
const toast = new bootstrap.Toast(toastElement);
toast.show();
}
Penjelasan: Menampilkan notifikasi pop-up (toast) di pojok kanan atas saat person terdeteksi. Toast akan otomatis hilang setelah 4 detik.
function playBeep() {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (AudioContext) {
const audioCtx = new AudioContext();
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.frequency.value = 880; // Frekuensi 880 Hz (nada A)
gainNode.gain.value = 0.2; // Volume 20%
oscillator.start();
gainNode.gain.exponentialRampToValueAtTime(0.00001, audioCtx.currentTime + 0.3);
oscillator.stop(audioCtx.currentTime + 0.3);
}
}
Penjelasan: Membuat suara beep singkat menggunakan Web Audio API. Suara hanya bertahan 0.3 detik.
addAlert(message, type)
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');
const dateStr = now.toLocaleDateString('id-ID');
alertDiv.innerHTML = `
<strong>${type === 'success' ? '🚨 PERSON DETECTED' : type.toUpperCase()}</strong>
<div class="mt-1">${message}</div>
<div class="alert-time">
<i class="fas fa-lock me-1"></i> AES128 Decrypted
${dateStr} ${timeStr}
</div>
`;
alertList.insertBefore(alertDiv, alertList.firstChild);
// Batasi maksimal 100 alert
while (alertList.children.length > 100) {
alertList.removeChild(alertList.lastChild);
}
}
Penjelasan: Menambahkan alert baru di bagian atas daftar. Alert akan memiliki warna berbeda berdasarkan tipe (success, danger, warning). Alert lama otomatis dihapus jika lebih dari 100.
updateStats()
javascript
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`;
}
}
}
// Saat halaman selesai loading
window.addEventListener('load', () => {
connectMQTT();
// Placeholder image while waiting for stream
videoFrame.src = 'data:image/svg+xml,...';
});
// Saat user menutup/refresh halaman
window.addEventListener('beforeunload', () => {
if (client) {
client.end(); // Tutup koneksi MQTT dengan bersih
}
});
1. User membuka halaman website
↓
2. Halaman loading, menampilkan placeholder
↓
3. JavaScript dijalankan, connectMQTT() dipanggil
↓
4. Koneksi WebSocket ke HiveMQ broker
↓
5. Subscribe ke topic 'Person' dan 'VideoFrame'
↓
6. Menunggu pesan masuk
↓
7. Saat pesan diterima:
- Decrypt AES128
- Parse JSON
- Tampilkan frame video (jika topic VideoFrame)
- Update statistik (jika topic Person)
- Tampilkan alert dan suara beep
↓
8. User dapat menghapus history alert dengan tombol
↓
9. User close halaman → connection ditutup
Peringatan Keamanan:
Kredensial MQTT dan AES terlihat di source code JavaScript. Untuk production, gunakan backend proxy seperti yang telah dijelaskan sebelumnya untuk menyembunyikan credential.