Files
rikkei_simple_care/client/app.go
2026-06-30 09:31:33 +07:00

922 lines
22 KiB
Go

package main
import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"client/internal/blocker"
"client/internal/screen"
"client/internal/winapi"
"github.com/gorilla/websocket"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
var blockedReportLast sync.Map // processName -> time.Time
var wifiReportLast sync.Map // ssidKey -> time.Time
var encryptionKey = []byte("simple-care-proctor-secretkey32!") // 32-byte key for AES-256
func encrypt(data []byte) ([]byte, error) {
block, err := aes.NewCipher(encryptionKey)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
ciphertext := gcm.Seal(nonce, nonce, data, nil)
return ciphertext, nil
}
func decrypt(data []byte) ([]byte, error) {
block, err := aes.NewCipher(encryptionKey)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return nil, errors.New("ciphertext too short")
}
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
return gcm.Open(nil, nonce, ciphertext, nil)
}
type StudentData struct {
StudentID int64 `json:"studentId"`
FullName string `json:"fullName"`
Email string `json:"email"`
Avatar string `json:"avatar"`
Dob string `json:"dob"`
StudentCode string `json:"studentCode"`
Phone string `json:"phone"`
SystemID int64 `json:"systemId"`
}
type App struct {
ctx context.Context
student *StudentData
mu sync.Mutex
sessionPath string
statsPath string
wsConn *websocket.Conn
wsConnected bool
wifiSSID string
wifiBSSID string
allowedApps string
onlineSecs int
offlineSecs int
unsyncedOn int
unsyncedOff int
lastSyncTime time.Time
isStreamingSc bool
streamScStop chan struct{}
statusMsg string
expectingLogin bool
backendOnline bool
dashboard StudentDashboardSnapshot
wifiEnforce bool
acceptedWifi map[string]bool
wifiRejected bool
}
type LocalStats struct {
OnlineSecs int `json:"onlineSecs"`
OfflineSecs int `json:"offlineSecs"`
UnsyncedOn int `json:"unsyncedOn"`
UnsyncedOff int `json:"unsyncedOff"`
}
type StudentShiftSnapshot struct {
Period int `json:"period"`
CourseName string `json:"courseName"`
StartTime string `json:"startTime"`
EndTime string `json:"endTime"`
IsActiveNow bool `json:"isActiveNow"`
OnlineSeconds int `json:"onlineSeconds"`
OfflineSeconds int `json:"offlineSeconds"`
AttendanceStatus int `json:"attendanceStatus"`
AttendanceLabel string `json:"attendanceLabel"`
}
type StudentDashboardSnapshot struct {
SessionDate string `json:"sessionDate"`
MonitorMode string `json:"monitorMode"`
MonitorLabel string `json:"monitorLabel"`
ClassRkID int64 `json:"classRkId"`
ClassName string `json:"className"`
ClassCode string `json:"classCode"`
CurrentPeriod int `json:"currentPeriod"`
CurrentCourseName string `json:"currentCourseName"`
CurrentShiftStart string `json:"currentShiftStart"`
CurrentShiftEnd string `json:"currentShiftEnd"`
InScheduleNow bool `json:"inScheduleNow"`
BlockerActive bool `json:"blockerActive"`
Shifts []StudentShiftSnapshot `json:"shifts"`
}
func NewApp() *App {
configDir, err := os.UserConfigDir()
if err != nil {
// Fallback to executable folder if UserConfigDir is unavailable
exePath, _ := os.Executable()
configDir = filepath.Dir(exePath)
}
appDir := filepath.Join(configDir, "SimpleCare")
_ = os.MkdirAll(appDir, 0755)
return &App{
sessionPath: filepath.Join(appDir, "student_session.json"),
statsPath: filepath.Join(appDir, "student_stats.json"),
lastSyncTime: time.Now(),
expectingLogin: false,
}
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
a.loadSession()
a.loadStats()
// Register blocker callbacks
blocker.Instance.OnBlocked = func(procName string, title string) {
go a.reportBlockedApp(procName, title)
}
blocker.Instance.OnKill = func(procName string, title string) {
_, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Type: runtime.WarningDialog,
Title: "Ứng dụng bị đóng",
Message: fmt.Sprintf("Hệ thống đã tự động đóng ứng dụng '%s' (%s) do không nằm trong danh sách các ứng dụng được phép sử dụng trong giờ học.", procName, title),
})
}
// Khởi chạy HTTP server nhận callback login thành công
a.startLocalServer()
// Khởi chạy vòng lặp giám sát định kỳ (mỗi 10 giây)
go a.monitorLoop()
// Khởi chạy vòng lặp đọc local storage của trang Rikkei Portal khi chưa đăng nhập
go a.authStorageScanner()
}
// startLocalServer khởi chạy server lắng nghe callback nhận thông tin sinh viên từ webview
func (a *App) startLocalServer() {
mux := http.NewServeMux()
mux.HandleFunc("/login-success", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
return
}
data := r.URL.Query().Get("data")
if data == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
a.mu.Lock()
expecting := a.expectingLogin
a.mu.Unlock()
if !expecting {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ignored"))
return
}
var st StudentData
if err := json.Unmarshal([]byte(data), &st); err == nil && st.StudentID > 0 {
a.saveSession(&st)
log.Printf("[LOCALSERVER] Student %s logged in successfully.", st.FullName)
a.mu.Lock()
a.expectingLogin = false
a.mu.Unlock()
blocker.Instance.Start()
// Kiểm tra điều kiện app được phép và giờ học trước khi tải giao diện
go a.fetchAllowedApps(st.SystemID)
go a.fetchStudentStatus(st.StudentID)
go a.fetchWifiPolicy()
// Chuyển hướng WebView về trang dashboard của app bằng cách reload app assets
runtime.WindowReloadApp(a.ctx)
w.Write([]byte("ok"))
} else {
w.WriteHeader(http.StatusBadRequest)
}
})
server := &http.Server{
Addr: "127.0.0.1:34115",
Handler: mux,
}
go func() {
_ = server.ListenAndServe()
}()
log.Println("[LOCALSERVER] Callback server started on http://127.0.0.1:34115")
}
// loadSession nạp session từ file cục bộ
func (a *App) loadSession() {
a.mu.Lock()
defer a.mu.Unlock()
data, err := os.ReadFile(a.sessionPath)
if err == nil {
decrypted, errDec := decrypt(data)
if errDec == nil {
data = decrypted
}
var s StudentData
if err := json.Unmarshal(data, &s); err == nil {
a.student = &s
log.Printf("[APP] Loaded local session: %s (%s)", s.FullName, s.StudentCode)
blocker.Instance.Start()
go a.fetchAllowedApps(s.SystemID)
go a.fetchStudentStatus(s.StudentID)
go a.fetchWifiPolicy()
go a.syncProfileToServer(&s)
}
}
}
// saveSession lưu session ra file
func (a *App) saveSession(s *StudentData) {
a.mu.Lock()
defer a.mu.Unlock()
a.student = s
data, _ := json.MarshalIndent(s, "", " ")
encrypted, err := encrypt(data)
if err == nil {
_ = os.WriteFile(a.sessionPath, encrypted, 0644)
} else {
_ = os.WriteFile(a.sessionPath, data, 0644)
}
go a.syncProfileToServer(s)
}
func (a *App) syncProfileToServer(s *StudentData) {
if s == nil || s.Avatar == "" {
return
}
payload := map[string]any{
"studentRkId": s.StudentID,
"classRkId": s.SystemID,
"sessionDate": time.Now().Format("2006-01-02"),
"avatar": s.Avatar,
}
bodyBytes, _ := json.Marshal(payload)
client := http.Client{Timeout: 5 * time.Second}
resp, err := client.Post("http://127.0.0.1:8080/api/student/sync-log", "application/json", bytes.NewBuffer(bodyBytes))
if err != nil {
return
}
_ = resp.Body.Close()
}
// loadStats nạp stats
func (a *App) loadStats() {
a.mu.Lock()
defer a.mu.Unlock()
data, err := os.ReadFile(a.statsPath)
if err == nil {
var ls LocalStats
if err := json.Unmarshal(data, &ls); err == nil {
a.onlineSecs = ls.OnlineSecs
a.offlineSecs = ls.OfflineSecs
a.unsyncedOn = ls.UnsyncedOn
a.unsyncedOff = ls.UnsyncedOff
}
}
}
// saveStats lưu stats ra file
func (a *App) saveStats() {
a.mu.Lock()
defer a.mu.Unlock()
ls := LocalStats{
OnlineSecs: a.onlineSecs,
OfflineSecs: a.offlineSecs,
UnsyncedOn: a.unsyncedOn,
UnsyncedOff: a.unsyncedOff,
}
data, _ := json.Marshal(ls)
_ = os.WriteFile(a.statsPath, data, 0644)
}
// CheckLoginStatus kiểm tra xem sinh viên đã đăng nhập chưa
func (a *App) CheckLoginStatus() bool {
a.mu.Lock()
defer a.mu.Unlock()
return a.student != nil
}
// GetStudentInfo trả về thông tin sinh viên
func (a *App) GetStudentInfo() map[string]any {
a.mu.Lock()
defer a.mu.Unlock()
if a.student == nil {
return nil
}
return map[string]any{
"studentId": a.student.StudentID,
"fullName": a.student.FullName,
"email": a.student.Email,
"avatar": a.student.Avatar,
"dob": a.student.Dob,
"studentCode": a.student.StudentCode,
"phone": a.student.Phone,
"systemId": a.student.SystemID,
}
}
// NavigateToLogin chuyển WebView đến trang đăng nhập Rikkei Portal
func (a *App) NavigateToLogin() {
a.mu.Lock()
a.expectingLogin = true
a.mu.Unlock()
runtime.WindowExecJS(a.ctx, "window.location.href = 'https://portal.rikkei.edu.vn/dangnhap'")
}
// Logout đăng xuất sinh viên
func (a *App) Logout() {
a.mu.Lock()
a.student = nil
a.expectingLogin = false
a.mu.Unlock()
_ = os.Remove(a.sessionPath)
_ = os.Remove(a.statsPath)
a.mu.Lock()
a.onlineSecs = 0
a.offlineSecs = 0
a.unsyncedOn = 0
a.unsyncedOff = 0
a.mu.Unlock()
blocker.Instance.Stop()
a.disconnectWS()
a.stopScreenshotStream()
runtime.WindowReloadApp(a.ctx)
}
// GetStats trả về Wifi, thời gian online/offline và trạng thái giám sát
func (a *App) GetStats() map[string]any {
a.mu.Lock()
defer a.mu.Unlock()
shifts := make([]StudentShiftSnapshot, len(a.dashboard.Shifts))
copy(shifts, a.dashboard.Shifts)
for i := range shifts {
if shifts[i].IsActiveNow {
shifts[i].OnlineSeconds += a.unsyncedOn
shifts[i].OfflineSeconds += a.unsyncedOff
}
}
return map[string]any{
"wifiSSID": a.wifiSSID,
"onlineSecs": a.onlineSecs,
"offlineSecs": a.offlineSecs,
"wsConnected": a.wsConnected,
"serverReachable": a.backendOnline,
"allowedApps": a.allowedApps,
"statusMsg": a.statusMsg,
"sessionDate": a.dashboard.SessionDate,
"monitorMode": a.dashboard.MonitorMode,
"monitorLabel": a.dashboard.MonitorLabel,
"className": a.dashboard.ClassName,
"classCode": a.dashboard.ClassCode,
"currentPeriod": a.dashboard.CurrentPeriod,
"currentCourseName": a.dashboard.CurrentCourseName,
"currentShiftStart": a.dashboard.CurrentShiftStart,
"currentShiftEnd": a.dashboard.CurrentShiftEnd,
"inScheduleNow": a.dashboard.InScheduleNow,
"blockerActive": a.dashboard.BlockerActive,
"shifts": shifts,
}
}
// SendWebcamFrame truyền webcam frame từ JS frontend lên máy chủ qua WS
func (a *App) SendWebcamFrame(frameBase64 string) {
a.mu.Lock()
conn := a.wsConn
a.mu.Unlock()
if conn != nil {
_ = conn.WriteJSON(map[string]any{
"event": "webcam_stream_frame",
"data": map[string]any{
"imageBuffer": frameBase64,
},
})
}
}
// authStorageScanner chỉ quét localStorage khi người dùng chủ động mở trang đăng nhập
func (a *App) authStorageScanner() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
<-ticker.C
a.mu.Lock()
expecting := a.expectingLogin
loggedIn := a.student != nil
a.mu.Unlock()
if loggedIn || !expecting {
continue
}
runtime.WindowExecJS(a.ctx, `
try {
const st = localStorage.getItem("student");
if (st) {
fetch("http://127.0.0.1:34115/login-success?data=" + encodeURIComponent(st));
}
} catch(e) {}
`)
}
}
// monitorLoop định kỳ 10s: check wifi, ping backend, cập nhật online/offline seconds và đồng bộ lên server
func (a *App) monitorLoop() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
<-ticker.C
if !a.CheckLoginStatus() {
continue
}
wifi := winapi.GetWifiConnection()
a.mu.Lock()
a.wifiSSID = wifi.SSID
a.wifiBSSID = wifi.BSSID
a.mu.Unlock()
backendOnline := a.pingBackend()
if backendOnline && wifi.SSID != "" && wifi.BSSID != "" {
go a.reportWifi(wifi.SSID, wifi.BSSID)
a.fetchWifiPolicy()
if !a.isWifiAllowed(wifi.SSID, wifi.BSSID) {
a.rejectUnauthorizedWifi(wifi.SSID, wifi.BSSID)
continue
}
}
a.mu.Lock()
a.backendOnline = backendOnline
if backendOnline {
a.onlineSecs += 10
a.unsyncedOn += 10
} else {
a.offlineSecs += 10
a.unsyncedOff += 10
}
a.mu.Unlock()
a.saveStats()
if backendOnline {
a.connectWS()
a.syncLogsToServer()
} else {
a.disconnectWS()
}
}
}
func (a *App) pingBackend() bool {
client := http.Client{Timeout: 3 * time.Second}
resp, err := client.Get("http://127.0.0.1:8080/api/stats")
if err != nil {
return false
}
_ = resp.Body.Close()
return resp.StatusCode == http.StatusOK
}
func (a *App) syncLogsToServer() {
a.mu.Lock()
student := a.student
unsyncedOn := a.unsyncedOn
unsyncedOff := a.unsyncedOff
ssid := a.wifiSSID
a.mu.Unlock()
if student == nil {
return
}
go a.fetchStudentStatus(student.StudentID)
go a.fetchAllowedApps(student.SystemID)
payload := map[string]any{
"studentRkId": student.StudentID,
"classRkId": student.SystemID,
"sessionDate": time.Now().Format("2006-01-02"),
"addOnlineSeconds": unsyncedOn,
"addOfflineSeconds": unsyncedOff,
"wifiSsid": ssid,
"avatar": student.Avatar,
}
bodyBytes, _ := json.Marshal(payload)
client := http.Client{Timeout: 5 * time.Second}
resp, err := client.Post("http://127.0.0.1:8080/api/student/sync-log", "application/json", bytes.NewBuffer(bodyBytes))
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
a.mu.Lock()
a.unsyncedOn -= unsyncedOn
a.unsyncedOff -= unsyncedOff
a.mu.Unlock()
a.saveStats()
log.Printf("[SYNC] Synced %ds online, %ds offline to server.", unsyncedOn, unsyncedOff)
}
}
func (a *App) reportBlockedApp(procName string, title string) {
a.mu.Lock()
student := a.student
a.mu.Unlock()
if student == nil || student.StudentID <= 0 {
return
}
procKey := strings.ToLower(strings.TrimSpace(procName))
if procKey == "" {
return
}
if v, ok := blockedReportLast.Load(procKey); ok {
if time.Since(v.(time.Time)) < 20*time.Second {
return
}
}
blockedReportLast.Store(procKey, time.Now())
payload := map[string]any{
"studentRkId": student.StudentID,
"processName": procName,
"windowTitle": title,
}
bodyBytes, _ := json.Marshal(payload)
client := http.Client{Timeout: 4 * time.Second}
resp, err := client.Post("http://127.0.0.1:8080/api/student/report-blocked-app", "application/json", bytes.NewBuffer(bodyBytes))
if err != nil {
log.Printf("[CLIENT] report-blocked-app failed: %v", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
log.Printf("[CLIENT] report-blocked-app status %d: %s", resp.StatusCode, string(body))
return
}
log.Printf("[CLIENT] report-blocked-app ok: %s", procName)
}
func (a *App) reportWifi(ssid, bssid string) {
a.mu.Lock()
student := a.student
a.mu.Unlock()
if student == nil || strings.TrimSpace(ssid) == "" || strings.TrimSpace(bssid) == "" {
return
}
key := winapi.BSSIDKey(bssid)
if len(key) != 12 {
return
}
if v, ok := wifiReportLast.Load(key); ok {
if time.Since(v.(time.Time)) < 30*time.Second {
return
}
}
wifiReportLast.Store(key, time.Now())
payload := map[string]any{
"studentRkId": student.StudentID,
"ssid": ssid,
"bssid": bssid,
}
bodyBytes, _ := json.Marshal(payload)
client := http.Client{Timeout: 4 * time.Second}
resp, err := client.Post("http://127.0.0.1:8080/api/student/report-wifi", "application/json", bytes.NewBuffer(bodyBytes))
if err != nil {
return
}
_ = resp.Body.Close()
}
func (a *App) fetchWifiPolicy() {
client := http.Client{Timeout: 4 * time.Second}
resp, err := client.Get("http://127.0.0.1:8080/api/student/wifi-policy")
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return
}
var res struct {
Enforce bool `json:"enforce"`
Accepted []struct {
SSID string `json:"ssid"`
BSSID string `json:"bssid"`
} `json:"accepted"`
}
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return
}
allowed := make(map[string]bool, len(res.Accepted))
for _, item := range res.Accepted {
k := winapi.BSSIDKey(item.BSSID)
if len(k) == 12 {
allowed[k] = true
}
}
a.mu.Lock()
a.wifiEnforce = res.Enforce
a.acceptedWifi = allowed
a.mu.Unlock()
}
func (a *App) isWifiAllowed(ssid, bssid string) bool {
a.mu.Lock()
enforce := a.wifiEnforce
allowed := a.acceptedWifi
a.mu.Unlock()
if !enforce || len(allowed) == 0 {
return true
}
bKey := winapi.BSSIDKey(bssid)
if bKey == "" || len(bKey) != 12 {
return false
}
return allowed[bKey]
}
func (a *App) rejectUnauthorizedWifi(ssid, bssid string) {
a.mu.Lock()
if a.wifiRejected {
a.mu.Unlock()
return
}
a.wifiRejected = true
a.mu.Unlock()
blocker.Instance.Stop()
a.disconnectWS()
_, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
Type: runtime.WarningDialog,
Title: "WiFi không được phép",
Message: fmt.Sprintf("Điểm phát WiFi \"%s\" (%s) không được phép.\n\nCó thể là mạng giả mạo (hotspot trùng tên). Hãy kết nối đúng WiFi của trường rồi mở lại ứng dụng.", ssid, bssid),
})
runtime.Quit(a.ctx)
}
func (a *App) fetchAllowedApps(classId int64) {
a.mu.Lock()
studentID := int64(0)
if a.student != nil {
studentID = a.student.StudentID
}
a.mu.Unlock()
client := http.Client{Timeout: 4 * time.Second}
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:8080/api/classes/%d/allowed-apps?studentId=%d", classId, studentID))
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
var res struct {
Keywords string `json:"keywords"`
Exit bool `json:"exit"`
Reason string `json:"reason"`
}
if err := json.NewDecoder(resp.Body).Decode(&res); err == nil {
if res.Exit {
reason := res.Reason
if reason == "" {
reason = "Không đáp ứng điều kiện học tập (chưa cấu hình ứng dụng được phép hoặc ngoài giờ học)."
}
log.Printf("[CLIENT] Conditions not met: %s. Blocker suspended.", reason)
a.mu.Lock()
a.allowedApps = ""
a.statusMsg = reason
a.mu.Unlock()
blocker.Instance.Stop()
return
}
a.mu.Lock()
a.allowedApps = res.Keywords
if a.dashboard.MonitorLabel != "" {
a.statusMsg = a.dashboard.MonitorLabel
} else {
a.statusMsg = "Đang trong giờ học"
}
a.mu.Unlock()
blocker.Instance.SetKeywords(res.Keywords)
blocker.Instance.Start()
}
}
}
func (a *App) fetchStudentStatus(studentID int64) {
if studentID <= 0 {
return
}
client := http.Client{Timeout: 4 * time.Second}
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:8080/api/student/status?studentRkId=%d", studentID))
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return
}
var res StudentDashboardSnapshot
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return
}
a.mu.Lock()
a.dashboard = res
if res.MonitorLabel != "" {
a.statusMsg = res.MonitorLabel
}
if res.ClassName != "" && res.MonitorMode == "learning" {
a.statusMsg = fmt.Sprintf("%s · %s", res.ClassName, res.MonitorLabel)
} else if res.ClassName != "" && res.MonitorMode == "outside_schedule" {
a.statusMsg = fmt.Sprintf("%s · Ngoài giờ học", res.ClassName)
} else if res.ClassName != "" && res.MonitorMode == "not_configured" {
a.statusMsg = fmt.Sprintf("%s · %s", res.ClassName, res.MonitorLabel)
}
a.mu.Unlock()
}
func (a *App) connectWS() {
a.mu.Lock()
if a.wsConnected || a.student == nil {
a.mu.Unlock()
return
}
student := a.student
a.mu.Unlock()
wsUrl := fmt.Sprintf("ws://127.0.0.1:8080/ws?role=student&studentId=%d&classId=%d", student.StudentID, student.SystemID)
dialer := websocket.Dialer{HandshakeTimeout: 4 * time.Second}
conn, _, err := dialer.Dial(wsUrl, nil)
if err != nil {
return
}
a.mu.Lock()
a.wsConn = conn
a.wsConnected = true
a.mu.Unlock()
log.Println("[WS] Connected to proctor websocket hub.")
go func() {
defer a.disconnectWS()
for {
var msg struct {
Event string `json:"event"`
Data map[string]any `json:"data"`
}
err := conn.ReadJSON(&msg)
if err != nil {
break
}
switch msg.Event {
case "start_screenshot_stream":
a.startScreenshotStream()
case "stop_screenshot_stream":
a.stopScreenshotStream()
case "start_webcam_stream":
runtime.EventsEmit(a.ctx, "start_webcam_stream")
case "stop_webcam_stream":
runtime.EventsEmit(a.ctx, "stop_webcam_stream")
}
}
}()
}
func (a *App) disconnectWS() {
a.mu.Lock()
defer a.mu.Unlock()
if a.wsConn != nil {
_ = a.wsConn.Close()
a.wsConn = nil
}
a.wsConnected = false
}
func (a *App) startScreenshotStream() {
a.mu.Lock()
if a.isStreamingSc {
a.mu.Unlock()
return
}
a.isStreamingSc = true
a.streamScStop = make(chan struct{})
a.mu.Unlock()
go func() {
ticker := time.NewTicker(250 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
frame, err := screen.CaptureScreen()
if err != nil {
continue
}
a.mu.Lock()
conn := a.wsConn
a.mu.Unlock()
if conn != nil {
_ = conn.WriteJSON(map[string]any{
"event": "screenshot_stream_frame",
"data": map[string]any{
"imageBuffer": frame,
},
})
}
case <-a.streamScStop:
return
}
}
}()
log.Println("[WS] Screenshot screen-streaming started.")
}
func (a *App) stopScreenshotStream() {
a.mu.Lock()
defer a.mu.Unlock()
if !a.isStreamingSc {
return
}
a.isStreamingSc = false
close(a.streamScStop)
log.Println("[WS] Screenshot screen-streaming stopped.")
}