fix
All checks were successful
Deploy on Master Change / deploy (push) Successful in 1m45s

This commit is contained in:
2026-07-20 06:06:54 +07:00
parent aed692cffe
commit 6ee9773f7f
15 changed files with 561 additions and 447 deletions

View File

@@ -1,15 +1,21 @@
package websocket
import (
"bufio"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"strconv"
"strings"
"sync"
"time"
internalDb "server/internal/db"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/websocket/v2"
"gorm.io/gorm"
internalDb "server/internal/db"
)
const (
@@ -53,26 +59,30 @@ type offlineGrace struct {
}
type WsHub struct {
mu sync.RWMutex
students map[int64]*SocketClient
teachers map[string]*SocketClient
teachersByStaff map[uint][]string // staffId -> teacher connection addresses
subscribers map[int64][]string // studentId -> list of teacher connection addresses
grace map[int64]offlineGrace
graceTimers map[int64]*time.Timer
subscriberModes map[string]string // "teacherAddr_studentId" -> "grid"|"focus"
lastRelayed map[string]time.Time // "teacherAddr_studentId_event" -> time
mu sync.RWMutex
students map[int64]*SocketClient
teachers map[string]*SocketClient
teachersByStaff map[uint][]string // staffId -> teacher connection addresses
subscribers map[int64][]string // studentId -> list of teacher connection addresses
grace map[int64]offlineGrace
graceTimers map[int64]*time.Timer
subscriberModes map[string]string // "teacherAddr_studentId" -> "grid"|"focus"
lastRelayed map[string]time.Time // "teacherAddr_studentId_event" -> time
httpScreenSubscribers map[int64][]chan []byte // studentId -> list of channels for screen MJPEG
httpWebcamSubscribers map[int64][]chan []byte // studentId -> list of channels for webcam MJPEG
}
var Hub = &WsHub{
students: make(map[int64]*SocketClient),
teachers: make(map[string]*SocketClient),
teachersByStaff: make(map[uint][]string),
subscribers: make(map[int64][]string),
grace: make(map[int64]offlineGrace),
graceTimers: make(map[int64]*time.Timer),
subscriberModes: make(map[string]string),
lastRelayed: make(map[string]time.Time),
students: make(map[int64]*SocketClient),
teachers: make(map[string]*SocketClient),
teachersByStaff: make(map[uint][]string),
subscribers: make(map[int64][]string),
grace: make(map[int64]offlineGrace),
graceTimers: make(map[int64]*time.Timer),
subscriberModes: make(map[string]string),
lastRelayed: make(map[string]time.Time),
httpScreenSubscribers: make(map[int64][]chan []byte),
httpWebcamSubscribers: make(map[int64][]chan []byte),
}
func (h *WsHub) IsStudentOnline(studentRkID int64) bool {
@@ -403,6 +413,45 @@ func (h *WsHub) RelayFrameRaw(studentID int64, event string, rawImageBuffer json
h.mu.Lock()
defer h.mu.Unlock()
// Broadcast to HTTP subscribers if any
if event == "screenshot_stream_frame" {
subs, exists := h.httpScreenSubscribers[studentID]
if exists && len(subs) > 0 {
var b64Str string
if err := json.Unmarshal(rawImageBuffer, &b64Str); err == nil && len(b64Str) > 0 {
if idx := strings.Index(b64Str, ","); idx != -1 {
b64Str = b64Str[idx+1:]
}
if rawBytes, err := base64.StdEncoding.DecodeString(b64Str); err == nil {
for _, ch := range subs {
select {
case ch <- rawBytes:
default:
}
}
}
}
}
} else if event == "webcam_stream_frame" {
subs, exists := h.httpWebcamSubscribers[studentID]
if exists && len(subs) > 0 {
var b64Str string
if err := json.Unmarshal(rawImageBuffer, &b64Str); err == nil && len(b64Str) > 0 {
if idx := strings.Index(b64Str, ","); idx != -1 {
b64Str = b64Str[idx+1:]
}
if rawBytes, err := base64.StdEncoding.DecodeString(b64Str); err == nil {
for _, ch := range subs {
select {
case ch <- rawBytes:
default:
}
}
}
}
}
}
teachersList, exists := h.subscribers[studentID]
if !exists || len(teachersList) == 0 {
return
@@ -568,7 +617,7 @@ func WebSocketHandler(db *gorm.DB) func(*websocket.Conn) {
case string:
sID, _ = strconv.ParseInt(v, 10, 64)
}
// Nhận chế độ subscription (mặc định là "focus")
mode := "focus"
if mVal, ok := msg.Data["mode"].(string); ok && mVal != "" {
@@ -600,3 +649,134 @@ func WebSocketHandler(db *gorm.DB) func(*websocket.Conn) {
}
}
}
func (h *WsHub) RegisterHttpSubscriber(studentID int64, kind string) chan []byte {
h.mu.Lock()
defer h.mu.Unlock()
ch := make(chan []byte, 16)
if kind == "screen" {
h.httpScreenSubscribers[studentID] = append(h.httpScreenSubscribers[studentID], ch)
} else if kind == "webcam" {
h.httpWebcamSubscribers[studentID] = append(h.httpWebcamSubscribers[studentID], ch)
}
// Always trigger streams start if there's any HTTP subscriber
if student, exists := h.students[studentID]; exists {
_ = student.WriteJSON(SocketMsg{Event: "start_screenshot_stream"})
_ = student.WriteJSON(SocketMsg{Event: "start_webcam_stream"})
}
return ch
}
func (h *WsHub) UnregisterHttpSubscriber(studentID int64, kind string, ch chan []byte) {
h.mu.Lock()
defer h.mu.Unlock()
if kind == "screen" {
subs := h.httpScreenSubscribers[studentID]
var next []chan []byte
for _, c := range subs {
if c != ch {
next = append(next, c)
}
}
if len(next) == 0 {
delete(h.httpScreenSubscribers, studentID)
} else {
h.httpScreenSubscribers[studentID] = next
}
} else if kind == "webcam" {
subs := h.httpWebcamSubscribers[studentID]
var next []chan []byte
for _, c := range subs {
if c != ch {
next = append(next, c)
}
}
if len(next) == 0 {
delete(h.httpWebcamSubscribers, studentID)
} else {
h.httpWebcamSubscribers[studentID] = next
}
}
// If no subscribers left (WS or HTTP), stop student's stream
wsSubs := h.subscribers[studentID]
httpScSubs := h.httpScreenSubscribers[studentID]
httpCamSubs := h.httpWebcamSubscribers[studentID]
if len(wsSubs) == 0 && len(httpScSubs) == 0 && len(httpCamSubs) == 0 {
if student, exists := h.students[studentID]; exists {
_ = student.WriteJSON(SocketMsg{Event: "stop_screenshot_stream"})
_ = student.WriteJSON(SocketMsg{Event: "stop_webcam_stream"})
}
}
close(ch)
}
func GetStudentScreenStreamHandler(c *fiber.Ctx) error {
studentIDVal := c.Params("studentId")
studentID, err := strconv.ParseInt(studentIDVal, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).SendString("Invalid student ID")
}
ch := Hub.RegisterHttpSubscriber(studentID, "screen")
c.Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
c.Set("Cache-Control", "no-cache")
c.Set("Connection", "keep-alive")
c.Set("Pragma", "no-cache")
c.Status(fiber.StatusOK)
c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {
defer Hub.UnregisterHttpSubscriber(studentID, "screen", ch)
for frame := range ch {
_, _ = fmt.Fprintf(w, "--frame\r\n")
_, _ = fmt.Fprintf(w, "Content-Type: image/jpeg\r\n")
_, _ = fmt.Fprintf(w, "Content-Length: %d\r\n\r\n", len(frame))
_, _ = w.Write(frame)
_, _ = fmt.Fprintf(w, "\r\n")
if err := w.Flush(); err != nil {
return
}
}
})
return nil
}
func GetStudentWebcamStreamHandler(c *fiber.Ctx) error {
studentIDVal := c.Params("studentId")
studentID, err := strconv.ParseInt(studentIDVal, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).SendString("Invalid student ID")
}
ch := Hub.RegisterHttpSubscriber(studentID, "webcam")
c.Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
c.Set("Cache-Control", "no-cache")
c.Set("Connection", "keep-alive")
c.Set("Pragma", "no-cache")
c.Status(fiber.StatusOK)
c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {
defer Hub.UnregisterHttpSubscriber(studentID, "webcam", ch)
for frame := range ch {
_, _ = fmt.Fprintf(w, "--frame\r\n")
_, _ = fmt.Fprintf(w, "Content-Type: image/jpeg\r\n")
_, _ = fmt.Fprintf(w, "Content-Length: %d\r\n\r\n", len(frame))
_, _ = w.Write(frame)
_, _ = fmt.Fprintf(w, "\r\n")
if err := w.Flush(); err != nil {
return
}
}
})
return nil
}