core v1
This commit is contained in:
286
server/internal/websocket/websocket.go
Normal file
286
server/internal/websocket/websocket.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/gofiber/websocket/v2"
|
||||
"gorm.io/gorm"
|
||||
internalDb "server/internal/db"
|
||||
)
|
||||
|
||||
type SocketMsg struct {
|
||||
Event string `json:"event"`
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
|
||||
type SocketClient struct {
|
||||
Conn *websocket.Conn
|
||||
StudentID int64
|
||||
ClassID int64
|
||||
Role string // "student" | "teacher"
|
||||
Addr string
|
||||
}
|
||||
|
||||
type WsHub struct {
|
||||
mu sync.RWMutex
|
||||
students map[int64]*SocketClient
|
||||
teachers map[string]*SocketClient
|
||||
subscribers map[int64][]string // studentId -> list of teacher connection addresses
|
||||
}
|
||||
|
||||
var Hub = &WsHub{
|
||||
students: make(map[int64]*SocketClient),
|
||||
teachers: make(map[string]*SocketClient),
|
||||
subscribers: make(map[int64][]string),
|
||||
}
|
||||
|
||||
func (h *WsHub) GetOnlineStudentIDs(classID int64) []int64 {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
var ids []int64
|
||||
for _, client := range h.students {
|
||||
if client.ClassID == classID {
|
||||
ids = append(ids, client.StudentID)
|
||||
}
|
||||
}
|
||||
if ids == nil {
|
||||
return []int64{}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (h *WsHub) Register(c *SocketClient) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
c.Addr = c.Conn.RemoteAddr().String()
|
||||
if c.Role == "student" {
|
||||
h.students[c.StudentID] = c
|
||||
log.Printf("[WS] Student %d registered (Address: %s, Class: %d)", c.StudentID, c.Addr, c.ClassID)
|
||||
} else if c.Role == "teacher" {
|
||||
h.teachers[c.Addr] = c
|
||||
log.Printf("[WS] Teacher registered (Address: %s)", c.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WsHub) Unregister(c *SocketClient) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if c.Role == "student" {
|
||||
delete(h.students, c.StudentID)
|
||||
log.Printf("[WS] Student %d disconnected", c.StudentID)
|
||||
|
||||
// Báo cho các giáo viên đang xem là stream của học sinh đã dừng
|
||||
if teachers, exists := h.subscribers[c.StudentID]; exists {
|
||||
for _, tAddr := range teachers {
|
||||
if t, found := h.teachers[tAddr]; found {
|
||||
_ = t.Conn.WriteJSON(SocketMsg{
|
||||
Event: "teacher:stream-stopped",
|
||||
Data: map[string]any{"studentId": c.StudentID},
|
||||
})
|
||||
}
|
||||
}
|
||||
delete(h.subscribers, c.StudentID)
|
||||
}
|
||||
} else if c.Role == "teacher" {
|
||||
delete(h.teachers, c.Addr)
|
||||
log.Printf("[WS] Teacher %s disconnected", c.Addr)
|
||||
|
||||
// Dọn dẹp subscriptions của giáo viên này
|
||||
for sID, teachersList := range h.subscribers {
|
||||
newList := []string{}
|
||||
for _, addr := range teachersList {
|
||||
if addr != c.Addr {
|
||||
newList = append(newList, addr)
|
||||
}
|
||||
}
|
||||
if len(newList) == 0 {
|
||||
delete(h.subscribers, sID)
|
||||
// Nếu không còn ai xem học sinh này, gửi lệnh tắt camera/screen cho client học sinh
|
||||
if student, exists := h.students[sID]; exists {
|
||||
_ = student.Conn.WriteJSON(SocketMsg{Event: "stop_screenshot_stream"})
|
||||
_ = student.Conn.WriteJSON(SocketMsg{Event: "stop_webcam_stream"})
|
||||
}
|
||||
} else {
|
||||
h.subscribers[sID] = newList
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Teacher bắt đầu xem stream của Student
|
||||
func (h *WsHub) Subscribe(teacherAddr string, studentID int64) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
// Thêm giáo viên vào list người xem của học sinh
|
||||
teachersList := h.subscribers[studentID]
|
||||
alreadySubscribed := false
|
||||
for _, addr := range teachersList {
|
||||
if addr == teacherAddr {
|
||||
alreadySubscribed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !alreadySubscribed {
|
||||
h.subscribers[studentID] = append(teachersList, teacherAddr)
|
||||
log.Printf("[WS] Teacher %s subscribed to student %d stream", teacherAddr, studentID)
|
||||
}
|
||||
|
||||
// Phát lệnh cho máy học sinh bật stream (nếu học sinh đang online)
|
||||
if student, exists := h.students[studentID]; exists {
|
||||
_ = student.Conn.WriteJSON(SocketMsg{Event: "start_screenshot_stream"})
|
||||
_ = student.Conn.WriteJSON(SocketMsg{Event: "start_webcam_stream"})
|
||||
}
|
||||
}
|
||||
|
||||
// Teacher dừng xem stream của Student
|
||||
func (h *WsHub) Unsubscribe(teacherAddr string, studentID int64) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
teachersList, exists := h.subscribers[studentID]
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
newList := []string{}
|
||||
for _, addr := range teachersList {
|
||||
if addr != teacherAddr {
|
||||
newList = append(newList, addr)
|
||||
}
|
||||
}
|
||||
|
||||
if len(newList) == 0 {
|
||||
delete(h.subscribers, studentID)
|
||||
log.Printf("[WS] Student %d has no more proctor subscribers. Stopping streams.", studentID)
|
||||
|
||||
// Báo học sinh tắt camera & screen stream để tiết kiệm mạng và CPU
|
||||
if student, exists := h.students[studentID]; exists {
|
||||
_ = student.Conn.WriteJSON(SocketMsg{Event: "stop_screenshot_stream"})
|
||||
_ = student.Conn.WriteJSON(SocketMsg{Event: "stop_webcam_stream"})
|
||||
}
|
||||
} else {
|
||||
h.subscribers[studentID] = newList
|
||||
}
|
||||
}
|
||||
|
||||
// Chuyển tiếp frame ảnh từ Student đến các Teacher đã subscribe
|
||||
func (h *WsHub) RelayFrame(studentID int64, event string, data map[string]any) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
teachersList, exists := h.subscribers[studentID]
|
||||
if !exists || len(teachersList) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
relayEvent := "teacher:screenshot-stream-frame"
|
||||
if event == "webcam_stream_frame" {
|
||||
relayEvent = "teacher:webcam-stream-frame"
|
||||
}
|
||||
|
||||
msg := SocketMsg{
|
||||
Event: relayEvent,
|
||||
Data: map[string]any{
|
||||
"studentId": studentID,
|
||||
"imageBuffer": data["imageBuffer"],
|
||||
},
|
||||
}
|
||||
|
||||
for _, addr := range teachersList {
|
||||
if t, found := h.teachers[addr]; found {
|
||||
_ = t.Conn.WriteJSON(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket handler cho Fiber route
|
||||
func WebSocketHandler(db *gorm.DB) func(*websocket.Conn) {
|
||||
return func(c *websocket.Conn) {
|
||||
role := c.Query("role", "student")
|
||||
studentIDStr := c.Query("studentId", "0")
|
||||
classIDStr := c.Query("classId", "0")
|
||||
|
||||
studentID, _ := strconv.ParseInt(studentIDStr, 10, 64)
|
||||
classID, _ := strconv.ParseInt(classIDStr, 10, 64)
|
||||
|
||||
if role == "student" && db != nil {
|
||||
resolvedClassID := internalDb.FindActiveClassForStudent(db, studentID)
|
||||
if resolvedClassID > 0 {
|
||||
classID = resolvedClassID
|
||||
}
|
||||
}
|
||||
|
||||
client := &SocketClient{
|
||||
Conn: c,
|
||||
StudentID: studentID,
|
||||
ClassID: classID,
|
||||
Role: role,
|
||||
}
|
||||
|
||||
Hub.Register(client)
|
||||
defer func() {
|
||||
Hub.Unregister(client)
|
||||
c.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
_, msgBytes, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var msg SocketMsg
|
||||
if err := json.Unmarshal(msgBytes, &msg); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Xử lý các sự kiện
|
||||
switch msg.Event {
|
||||
case "screenshot_stream_frame", "webcam_stream_frame":
|
||||
// Nhận frame từ học sinh, chuyển tiếp về các thầy cô
|
||||
Hub.RelayFrame(client.StudentID, msg.Event, msg.Data)
|
||||
|
||||
case "teacher:subscribe":
|
||||
// Giáo viên đăng ký xem học sinh cụ thể
|
||||
if client.Role == "teacher" {
|
||||
if sIDVal, ok := msg.Data["studentId"]; ok {
|
||||
var sID int64
|
||||
switch v := sIDVal.(type) {
|
||||
case float64:
|
||||
sID = int64(v)
|
||||
case string:
|
||||
sID, _ = strconv.ParseInt(v, 10, 64)
|
||||
}
|
||||
if sID > 0 {
|
||||
Hub.Subscribe(client.Addr, sID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "teacher:unsubscribe":
|
||||
// Giáo viên hủy đăng ký
|
||||
if client.Role == "teacher" {
|
||||
if sIDVal, ok := msg.Data["studentId"]; ok {
|
||||
var sID int64
|
||||
switch v := sIDVal.(type) {
|
||||
case float64:
|
||||
sID = int64(v)
|
||||
case string:
|
||||
sID, _ = strconv.ParseInt(v, 10, 64)
|
||||
}
|
||||
if sID > 0 {
|
||||
Hub.Unsubscribe(client.Addr, sID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user