import './style.css';
import './app.css';
import logoUrl from './assets/logo.jpeg';
import {
decodeExamFilePayload,
detectExamViewKind,
renderSecureImage,
renderSecurePdf,
renderSecureText,
} from './examViewer.js';
import * as GoApp from '../wailsjs/go/main/App.js';
// Map Wails bindings to window.go.main.App for backward compatibility with obfuscated builds
window.go = window.go || {};
window.go.main = window.go.main || {};
if (typeof window.go.main.App === 'undefined') {
window.go.main.App = GoApp;
window.go.main._custom = true;
}
const brandLogoHtml = ` `;
const APP_VERSION = '1.3';
// Trạng thái cục bộ
let loggedIn = false;
let studentInfo = null;
let stats = {
wifiSSID: '',
onlineSecs: 0,
offlineSecs: 0,
wsConnected: false,
serverReachable: false,
allowedApps: '',
monitorMode: '',
monitorLabel: '',
className: '',
classCode: '',
currentPeriod: 0,
currentCourseName: '',
shifts: [],
exam: null
};
let chatOpen = false;
let chatMessages = [];
let chatConversations = [];
let activeStaffId = 0;
let lastRenderedMsgCount = 0;
let bannerStaffId = 0;
// Quản lý webcam
let webcamStream = null;
let webcamInterval = null;
const webcamVideo = document.createElement('video');
webcamVideo.autoplay = true;
webcamVideo.playsInline = true;
webcamVideo.style.display = 'none';
document.body.appendChild(webcamVideo);
const webcamCanvas = document.createElement('canvas');
webcamCanvas.width = 320;
webcamCanvas.height = 240;
webcamCanvas.style.display = 'none';
document.body.appendChild(webcamCanvas);
function init() {
const ready = (typeof window.ObfuscatedCall === 'function') ||
(typeof window.go !== 'undefined' && typeof window.go.main !== 'undefined' && !window.go.main._custom);
if (!ready || typeof window.runtime === 'undefined') {
setTimeout(init, 200);
return;
}
window.runtime.EventsOn('start_webcam_stream', startWebcam);
window.runtime.EventsOn('stop_webcam_stream', stopWebcam);
window.runtime.EventsOn('chat:message', (data) => {
updateChatBadge();
loadChatConversations().catch(console.error);
if (chatOpen && activeStaffId) {
loadChatMessages(activeStaffId, true).catch(console.error);
}
});
window.runtime.EventsOn('chat:notify', (data) => {
updateChatBadge();
if (data?.staffId) bannerStaffId = Number(data.staffId);
pulseChatFab();
});
window.runtime.EventsOn('exam:paper-sent', () => {
if (loggedIn) {
examPaperFiles = null;
examPaperFilesRoomId = 0;
lastExamPanelKey = '';
window.go.main.App.GetStats().then((s) => {
stats = { ...stats, ...s };
updateExamPanel(true);
}).catch(console.error);
}
});
checkLogin();
}
async function checkLogin() {
try {
const isLogged = await window.go.main.App.CheckLoginStatus();
if (isLogged) {
loggedIn = true;
studentInfo = await window.go.main.App.GetStudentInfo();
renderDashboard();
startStatsTicker();
ensureChatWidget();
updateChatBadge();
if (stats.monitorMode === 'exam') updateExamPanel();
} else {
loggedIn = false;
renderLoginPrompt();
}
} catch (err) {
console.error('Error checking login status:', err);
}
}
function renderLoginPrompt() {
document.querySelector('#app').innerHTML = `
${brandLogoHtml}
Simple Care v${APP_VERSION}
Rikkei Education
Đăng nhập sinh viên
Ứng dụng sẽ chuyển hướng bạn đến trang Rikkei Portal. Sau khi đăng nhập thành công, hệ thống sẽ tự động đồng bộ tài khoản học tập của bạn.
Đi đến trang đăng nhập
`;
document.getElementById('btn-goto-login').addEventListener('click', () => {
window.go.main.App.NavigateToLogin();
});
}
function monitorModeMeta(mode) {
switch (mode) {
case 'learning':
return { icon: '🛡️', label: 'Đang giám sát', cls: 'mode-learning' };
case 'exam':
return { icon: '📝', label: 'Phòng thi', cls: 'mode-exam' };
case 'outside_schedule':
return { icon: '😴', label: 'Ngoài giờ học', cls: 'mode-outside' };
case 'not_configured':
return { icon: '⚙️', label: 'Chưa sẵn sàng', cls: 'mode-config' };
default:
return { icon: '⏳', label: 'Đang kết nối', cls: 'mode-pending' };
}
}
function attendanceClass(status) {
if (status === 4) return 'att-ok';
if (status === 3) return 'att-late';
if (status === 1 || status === 2) return 'att-leave';
if (status === 0) return 'att-absent';
return 'att-pending';
}
function renderShiftsTable(shifts) {
if (!shifts || shifts.length === 0) {
return 'Hôm nay không có ca học theo lịch lớp.
';
}
return `
Ca
Môn học
Giờ
Trực tuyến
Ngoại tuyến
Điểm danh
${shifts.map(s => `
Ca ${s.period} ${s.isActiveNow ? ' đang học ' : ''}
${s.courseName || '—'}
${s.startTime}–${s.endTime}
${formatDuration(s.onlineSeconds || 0)}
${formatDuration(s.offlineSeconds || 0)}
${s.attendanceLabel || 'Chưa tính'}
`).join('')}
`;
}
let examPaperFiles = null;
let examPaperFilesRoomId = 0;
let examPaperFilesLoading = false;
let lastExamPanelKey = '';
let examViewerObjectUrl = null;
function wireExamViewerGuards(overlay) {
overlay.addEventListener('contextmenu', (e) => e.preventDefault());
overlay.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
if ((e.ctrlKey || e.metaKey) && (key === 'p' || key === 's')) {
e.preventDefault();
}
});
}
function ensureExamViewer() {
let overlay = document.getElementById('exam-viewer-overlay');
if (overlay) return overlay;
overlay = document.createElement('div');
overlay.id = 'exam-viewer-overlay';
overlay.className = 'exam-viewer-overlay';
overlay.innerHTML = `
`;
document.body.appendChild(overlay);
overlay.querySelector('#exam-viewer-close').addEventListener('click', closeExamViewer);
wireExamViewerGuards(overlay);
return overlay;
}
function setExamViewerLoading(loading) {
const loadingEl = document.getElementById('exam-viewer-loading');
const contentEl = document.getElementById('exam-viewer-content');
const errorEl = document.getElementById('exam-viewer-error');
if (loadingEl) loadingEl.classList.toggle('is-active', loading);
if (contentEl) contentEl.classList.toggle('is-ready', !loading);
if (errorEl) errorEl.classList.remove('is-active');
}
function setExamViewerError(message) {
const loadingEl = document.getElementById('exam-viewer-loading');
const contentEl = document.getElementById('exam-viewer-content');
const errorEl = document.getElementById('exam-viewer-error');
if (loadingEl) loadingEl.classList.remove('is-active');
if (contentEl) contentEl.classList.remove('is-ready');
if (errorEl) {
errorEl.classList.add('is-active');
errorEl.textContent = message;
}
}
function resetExamViewerPanels() {
const loadingEl = document.getElementById('exam-viewer-loading');
const contentEl = document.getElementById('exam-viewer-content');
const errorEl = document.getElementById('exam-viewer-error');
if (loadingEl) loadingEl.classList.remove('is-active');
if (contentEl) contentEl.classList.remove('is-ready');
if (errorEl) errorEl.classList.remove('is-active');
}
function clearExamViewerContent() {
if (examViewerObjectUrl) {
URL.revokeObjectURL(examViewerObjectUrl);
examViewerObjectUrl = null;
}
const contentEl = document.getElementById('exam-viewer-content');
if (contentEl) contentEl.innerHTML = '';
}
async function showExamViewer(url, title, fileName = '') {
const overlay = ensureExamViewer();
document.getElementById('exam-viewer-title').textContent = title || 'Xem trong app';
overlay.classList.add('is-open');
overlay.focus();
clearExamViewerContent();
resetExamViewerPanels();
setExamViewerLoading(true);
const contentEl = document.getElementById('exam-viewer-content');
if (!contentEl) return;
try {
const payload = await window.go.main.App.LoadExamViewFile(url);
const bytes = decodeExamFilePayload(payload);
const mime = payload?.mime || '';
const kind = detectExamViewKind(fileName || title, mime, bytes);
if (kind === 'pdf') {
await new Promise((resolve) => requestAnimationFrame(resolve));
await renderSecurePdf(contentEl, bytes);
} else if (kind === 'image') {
renderSecureImage(contentEl, bytes, mime);
const img = contentEl.querySelector('img');
if (img?.src?.startsWith('blob:')) examViewerObjectUrl = img.src;
if (img) {
await new Promise((resolve, reject) => {
if (img.complete) resolve();
else {
img.onload = () => resolve();
img.onerror = () => reject(new Error('Không hiển thị được ảnh'));
}
});
}
} else if (kind === 'text') {
renderSecureText(contentEl, bytes);
} else {
setExamViewerError('Chỉ hỗ trợ xem PDF, ảnh hoặc file text trong app.');
return;
}
setExamViewerLoading(false);
} catch (e) {
setExamViewerError(e?.message || e || 'Không mở được file');
}
}
function closeExamViewer() {
const overlay = document.getElementById('exam-viewer-overlay');
if (!overlay) return;
overlay.classList.remove('is-open');
clearExamViewerContent();
resetExamViewerPanels();
}
function renderExamResources() {
const ex = stats.exam;
if (!ex?.paperSent) return '';
if (examPaperFilesLoading && !examPaperFiles) {
return `
📎 Tài nguyên kèm đề
Đang tải danh sách tài nguyên...
`;
}
const resources = Array.isArray(examPaperFiles?.resources) ? examPaperFiles.resources : [];
if (!resources.length) {
return `
📎 Tài nguyên kèm đề
Gói đề này không có tài nguyên đính kèm.
`;
}
const rows = resources.map((r) => `
${escapeHtml(r.fileName || 'Tài nguyên')}
Tải về
`).join('');
return `
📎 Tài nguyên kèm đề (${resources.length})
${rows}
`;
}
async function loadExamPaperFiles(force = false) {
const ex = stats.exam;
if (!ex || stats.monitorMode !== 'exam' || !ex.paperSent) {
examPaperFiles = null;
examPaperFilesRoomId = 0;
examPaperFilesLoading = false;
return;
}
if (!force && examPaperFiles && examPaperFilesRoomId === ex.examRoomId) {
return;
}
examPaperFilesLoading = true;
try {
examPaperFiles = await window.go.main.App.GetExamPaperFiles();
examPaperFilesRoomId = ex.examRoomId;
} catch (err) {
console.error('GetExamPaperFiles failed:', err);
examPaperFiles = { resources: [] };
examPaperFilesRoomId = ex.examRoomId;
} finally {
examPaperFilesLoading = false;
}
}
function renderExamPanel() {
const ex = stats.exam;
if (!ex || stats.monitorMode !== 'exam') return '';
const paperBtn = ex.paperSent
? `📄 Xem đề${ex.paperTitle ? ` (${ex.paperTitle})` : ''} `
: `Chờ giảng viên gửi đề... `;
const quizBtn = ex.quizUrl
? `📝 Làm trắc nghiệm `
: '';
const submitBtn = ex.submitted
? `✓ Đã nộp bài tự luận `
: `📦 Nộp bài (chọn folder) `;
return `
📝 ${ex.examName || 'Phòng thi'}
Đề PDF và tài nguyên xem trong app. Trắc nghiệm mở trang quiz trực tiếp (giữ đăng nhập). F5 / Xóa cache / Về trang chính: menu Simple Care (F5, Ctrl+Delete, Ctrl+H).
${paperBtn}
${quizBtn}
${submitBtn}
${renderExamResources()}
`;
}
function wireExamPanel() {
const ex = stats.exam;
const paper = document.getElementById('btn-exam-paper');
if (paper) paper.addEventListener('click', async () => {
try {
const url = await window.go.main.App.GetExamPaperViewURL();
showExamViewer(url, ex?.paperTitle || 'Đề thi', ex?.paperTitle || 'de.pdf');
} catch (e) {
alert(e?.message || e || 'Không mở được đề');
}
});
const quiz = document.getElementById('btn-exam-quiz');
if (quiz) quiz.addEventListener('click', () => {
window.go.main.App.OpenExamQuiz().catch((e) => alert(e?.message || e));
});
const submit = document.getElementById('btn-exam-submit');
if (submit) submit.addEventListener('click', async () => {
try {
const name = await window.go.main.App.SubmitExamWork();
alert(`Đã nộp bài: ${name}`);
const fresh = await window.go.main.App.GetStats();
stats = { ...stats, ...fresh };
await loadExamPaperFiles();
updateExamPanel();
} catch (e) {
alert(e?.message || e || 'Nộp bài thất bại');
}
});
document.querySelectorAll('.btn-exam-res-dl').forEach((btn) => {
btn.addEventListener('click', async () => {
const id = Number(btn.getAttribute('data-id'));
try {
await window.go.main.App.DownloadExamResource(id);
} catch (e) {
alert(e?.message || e || 'Tải thất bại');
}
});
});
}
async function updateExamPanel(forceReloadFiles = false) {
const host = document.getElementById('exam-panel-host');
if (!host) return;
if (stats.monitorMode !== 'exam' || !stats.exam) {
host.innerHTML = '';
lastExamPanelKey = '';
return;
}
await loadExamPaperFiles(forceReloadFiles);
host.innerHTML = renderExamPanel();
wireExamPanel();
}
function examPanelKey() {
const ex = stats.exam;
if (!ex) return '';
const resCount = Array.isArray(examPaperFiles?.resources) ? examPaperFiles.resources.length : -1;
return `${ex.examRoomId}:${ex.paperSent}:${ex.submitted}:${resCount}:${examPaperFilesLoading}`;
}
function renderDashboard() {
if (!studentInfo) return;
const mode = monitorModeMeta(stats.monitorMode);
const classLine = stats.className
? `${stats.className}${stats.classCode ? ` (${stats.classCode})` : ''}`
: 'Đang xác định lớp...';
document.querySelector('#app').innerHTML = `
${studentInfo.fullName}
${studentInfo.studentCode}
${studentInfo.email}
Lớp
${classLine}
Điện thoại
${studentInfo.phone || 'Chưa cung cấp'}
${mode.icon}
${mode.label}
${classLine}
${stats.monitorLabel || stats.statusMsg || 'Đang kết nối...'}
${stats.currentPeriod > 0 && stats.currentCourseName
? `Ca ${stats.currentPeriod} · ${stats.currentCourseName}`
: 'Theo dõi theo lịch học của lớp'}
${renderExamPanel()}
📡
WiFi
${stats.wifiSSID || 'Đang quét...'}
🖥️
Máy chủ
${stats.serverReachable ? 'Đã kết nối' : 'Mất kết nối'}
Hôm nay — theo ca
00:00:00
·
00:00:00
${stats.sessionDate || ''}
${renderShiftsTable(stats.shifts)}
`;
document.getElementById('btn-logout').addEventListener('click', async () => {
if (confirm('Bạn có chắc chắn muốn đăng xuất khỏi ứng dụng giám sát?')) {
loggedIn = false;
studentInfo = null;
stopWebcam();
const w = document.getElementById('student-chat-widget');
if (w) w.remove();
await window.go.main.App.Logout();
}
});
document.getElementById('btn-open-github')?.addEventListener('click', () => {
window.go.main.App.OpenGitHub();
});
document.getElementById('btn-open-translate')?.addEventListener('click', () => {
window.go.main.App.OpenGoogleTranslate();
});
document.getElementById('btn-open-local-browser')?.addEventListener('click', () => {
window.go.main.App.OpenLocalBrowser();
});
wireExamPanel();
}
function ensureChatWidget() {
if (document.getElementById('student-chat-widget')) return;
const wrap = document.createElement('div');
wrap.id = 'student-chat-widget';
wrap.className = 'student-chat-dock';
wrap.innerHTML = `
💬0
Chọn hội thoại
Chọn giảng viên bên trái để xem tin nhắn
Gửi
`;
document.body.appendChild(wrap);
document.getElementById('student-chat-fab').addEventListener('click', () => setChatOpen(true));
document.getElementById('student-chat-close').addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
setChatOpen(false);
});
document.getElementById('student-chat-send').addEventListener('click', sendStudentChat);
document.getElementById('student-chat-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); sendStudentChat(); }
});
}
function pulseChatFab() {
const fab = document.getElementById('student-chat-fab');
if (!fab) return;
fab.classList.remove('student-chat-fab--pulse');
void fab.offsetWidth;
fab.classList.add('student-chat-fab--pulse');
}
function setChatOpen(open) {
chatOpen = open;
const modal = document.getElementById('student-chat-modal');
const fab = document.getElementById('student-chat-fab');
if (modal) modal.classList.toggle('is-open', open);
if (fab) fab.classList.toggle('is-hidden', open);
if (open) {
loadChatConversations().then(() => {
const sid = bannerStaffId || activeStaffId || Number(chatConversations[0]?.staffId || 0);
if (sid) selectStaffChat(sid);
}).catch(console.error);
}
}
function selectStaffChat(staffId) {
if (!staffId) return;
activeStaffId = staffId;
const conv = chatConversations.find((c) => Number(c.staffId) === staffId);
const head = document.getElementById('student-chat-thread-head');
if (head) head.textContent = conv?.staffName || conv?.staffEmail || 'Giảng viên';
renderChatConversations();
loadChatMessages(staffId).catch(console.error);
}
async function loadChatConversations() {
const rows = await window.go.main.App.GetChatConversations();
chatConversations = Array.isArray(rows) ? rows : [];
if (chatOpen) renderChatConversations();
updateChatBadge();
return chatConversations;
}
function renderChatConversations() {
const list = document.getElementById('student-chat-sidebar');
if (!list) return;
if (!chatConversations.length) {
list.innerHTML = 'Chưa có tin nhắn
';
return;
}
list.innerHTML = chatConversations.map((c) => {
const sid = Number(c.staffId || 0);
const unread = Number(c.unread || 0);
const active = sid === activeStaffId ? ' active' : '';
const name = escapeHtml(c.staffName || c.staffEmail || 'Giảng viên');
const preview = escapeHtml(c.lastMessage || '—');
return `
${name}${unread > 0 ? `${unread} ` : ''}
${preview}
`;
}).join('');
list.querySelectorAll('.student-chat-conv-row').forEach((btn) => {
btn.addEventListener('click', () => selectStaffChat(Number(btn.getAttribute('data-staff-id'))));
});
}
async function loadChatMessages(staffId, silent = false) {
const msgs = await window.go.main.App.GetChatMessages(staffId);
chatMessages = Array.isArray(msgs) ? msgs : [];
renderChatMessages(silent);
updateChatBadge();
if (!silent) loadChatConversations().catch(console.error);
}
function renderChatMessages(silent = false) {
const box = document.getElementById('student-chat-messages');
if (!box) return;
const wasAtBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 48;
if (!chatMessages.length) {
box.innerHTML = 'Chưa có tin nhắn
';
lastRenderedMsgCount = 0;
return;
}
if (chatMessages.length === lastRenderedMsgCount && silent) return;
lastRenderedMsgCount = chatMessages.length;
box.innerHTML = chatMessages.map((m) => {
const role = m.senderRole === 'student' ? 'student' : 'staff';
const time = m.createdAt ? new Date(m.createdAt).toLocaleTimeString('vi-VN', { hour: '2-digit', minute: '2-digit' }) : '';
return `
${escapeHtml(m.body || '')}
${time}
`;
}).join('');
if (!silent || wasAtBottom) {
requestAnimationFrame(() => { box.scrollTop = box.scrollHeight; });
}
}
function escapeHtml(s) {
return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}
async function sendStudentChat() {
const input = document.getElementById('student-chat-input');
if (!input || !input.value.trim() || !activeStaffId) return;
try {
await window.go.main.App.SendChatMessage(input.value.trim());
input.value = '';
await loadChatMessages(activeStaffId);
} catch (err) {
alert(err?.message || err || 'Gửi tin thất bại');
}
}
async function updateChatBadge() {
const badge = document.getElementById('student-chat-badge');
if (!badge || !window.go?.main?.App?.GetChatUnread) return;
try {
const n = await window.go.main.App.GetChatUnread();
if (n > 0) {
badge.hidden = false;
badge.textContent = n > 9 ? '9+' : String(n);
} else {
badge.hidden = true;
}
} catch {
/* ignore */
}
}
function formatDuration(totalSeconds) {
const hrs = Math.floor(totalSeconds / 3600);
const mins = Math.floor((totalSeconds % 3600) / 60);
const secs = totalSeconds % 60;
const pad = (num) => String(num).padStart(2, '0');
return `${pad(hrs)}:${pad(mins)}:${pad(secs)}`;
}
function startStatsTicker() {
const pullStats = async () => {
if (!loggedIn) return;
try {
const freshStats = await window.go.main.App.GetStats();
stats = { ...stats, ...freshStats };
if (!Array.isArray(stats.shifts)) stats.shifts = [];
const wifiEl = document.getElementById('wifi-ssid');
if (wifiEl) wifiEl.innerText = stats.wifiSSID || 'Không có WiFi';
const serverEl = document.getElementById('server-status');
if (serverEl) {
serverEl.innerText = stats.serverReachable ? 'Đã kết nối' : 'Mất kết nối';
serverEl.style.color = stats.serverReachable ? 'var(--online-color)' : 'var(--offline-color)';
}
const statusMsgEl = document.getElementById('status-message');
if (statusMsgEl) {
statusMsgEl.innerText = stats.monitorLabel || stats.statusMsg || 'Đang kết nối...';
}
const mode = monitorModeMeta(stats.monitorMode);
const statusIconEl = document.getElementById('status-icon');
if (statusIconEl) statusIconEl.innerText = mode.icon;
const modeTagEl = document.getElementById('monitor-mode-tag');
if (modeTagEl) modeTagEl.innerText = mode.label;
const banner = document.querySelector('.status-banner');
if (banner) banner.className = `card status-banner ${mode.cls}`;
const classLine = stats.className
? `${stats.className}${stats.classCode ? ` (${stats.classCode})` : ''}`
: 'Đang xác định lớp...';
const classLineEl = document.getElementById('status-class-line');
if (classLineEl) classLineEl.innerText = classLine;
const profileClassEl = document.getElementById('profile-class');
if (profileClassEl) profileClassEl.innerText = classLine;
const statusSubEl = document.getElementById('status-sub');
if (statusSubEl) {
if (stats.monitorMode === 'exam' && stats.exam?.examName) {
statusSubEl.innerText = stats.exam.examName;
} else {
statusSubEl.innerText = stats.currentPeriod > 0 && stats.currentCourseName
? `Ca ${stats.currentPeriod} · ${stats.currentCourseName}`
: 'Theo dõi theo lịch học của lớp';
}
}
if (stats.monitorMode === 'exam' && stats.exam) {
const panelKey = examPanelKey();
if (panelKey !== lastExamPanelKey) {
lastExamPanelKey = panelKey;
updateExamPanel();
}
} else if (lastExamPanelKey !== '') {
lastExamPanelKey = '';
const host = document.getElementById('exam-panel-host');
if (host) host.innerHTML = '';
}
const onlineEl = document.getElementById('clock-online');
if (onlineEl) onlineEl.innerText = formatDuration(stats.onlineSecs || 0);
const offlineEl = document.getElementById('clock-offline');
if (offlineEl) offlineEl.innerText = formatDuration(stats.offlineSecs || 0);
const shiftsWrap = document.getElementById('shifts-wrap');
if (shiftsWrap) shiftsWrap.innerHTML = renderShiftsTable(stats.shifts);
const sessionDateEl = document.getElementById('session-date');
if (sessionDateEl && stats.sessionDate) sessionDateEl.innerText = stats.sessionDate;
const indicator = document.querySelector('.status-indicator');
if (indicator) {
indicator.className = `status-indicator ${stats.serverReachable ? 'online' : 'offline'}`;
}
updateChatBadge();
} catch (err) {
console.error('Error fetching stats:', err);
}
};
pullStats();
setInterval(pullStats, 1000);
}
async function startWebcam() {
if (webcamStream) return;
try {
webcamStream = await navigator.mediaDevices.getUserMedia({
video: { width: 320, height: 240, frameRate: { max: 10 } }
});
webcamVideo.srcObject = webcamStream;
webcamInterval = setInterval(() => {
const ctx = webcamCanvas.getContext('2d');
if (ctx) {
ctx.drawImage(webcamVideo, 0, 0, webcamCanvas.width, webcamCanvas.height);
const dataUrl = webcamCanvas.toDataURL('image/jpeg', 0.4);
window.go.main.App.SendWebcamFrame(dataUrl);
}
}, 250);
} catch (err) {
console.error('Failed to open webcam:', err);
}
}
function stopWebcam() {
if (webcamInterval) {
clearInterval(webcamInterval);
webcamInterval = null;
}
if (webcamStream) {
webcamStream.getTracks().forEach(track => track.stop());
webcamStream = null;
}
webcamVideo.srcObject = null;
}
init();