This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user