This commit is contained in:
Yvan
2026-09-23 17:42:55 +08:00
parent 435b601273
commit d24eb2efd5
3 changed files with 58 additions and 3 deletions
+54 -3
View File
@@ -137,14 +137,23 @@ func New(hub platformws.Hub, streams *bizlive.StreamUsecase, rooms *bizlive.Room
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if logger == nil {
logger = slog.Default()
}
logger = logger.With("mod", "live")
m := &Manager{hub: hub, streams: streams, roomCatalog: rooms, chat: chat, logger: logger, api: api, rooms: make(map[string]*room), peers: make(map[*melody.Session]*peer), observers: make(map[*melody.Session]struct{}), stop: make(chan struct{})} m := &Manager{hub: hub, streams: streams, roomCatalog: rooms, chat: chat, logger: logger, api: api, rooms: make(map[string]*room), peers: make(map[*melody.Session]*peer), observers: make(map[*melody.Session]struct{}), stop: make(chan struct{})}
hub.OnMessage(m.onMessage) hub.OnMessage(m.onMessage)
hub.OnDisconnect(m.onDisconnect) hub.OnDisconnect(m.onDisconnect)
go m.heartbeatLoop() go m.heartbeatLoop()
logger.Info("live signaling manager initialized",
"stun_configured", len(liveICEServers()) > 0,
"udp_min_port", envPort("KRA_LIVE_UDP_MIN_PORT"),
"udp_max_port", envPort("KRA_LIVE_UDP_MAX_PORT"))
return m, m.Close, nil return m, m.Close, nil
} }
func (m *Manager) Close() { func (m *Manager) Close() {
m.liveLogger().Info("live signaling manager stopping")
m.stopOnce.Do(func() { close(m.stop) }) m.stopOnce.Do(func() { close(m.stop) })
m.mu.Lock() m.mu.Lock()
peers := make([]*peer, 0, len(m.peers)) peers := make([]*peer, 0, len(m.peers))
@@ -160,6 +169,7 @@ func (m *Manager) Close() {
func (m *Manager) onMessage(session *melody.Session, raw []byte) { func (m *Manager) onMessage(session *melody.Session, raw []byte) {
var msg message var msg message
if err := json.Unmarshal(raw, &msg); err != nil { if err := json.Unmarshal(raw, &msg); err != nil {
m.liveLogger().Warn("live signaling message rejected", "reason", "invalid_json", "message_bytes", len(raw), "remote_addr", sessionRemoteAddr(session))
m.send(session, "Error", map[string]any{"code": "INVALID_JSON", "message": "invalid json"}) m.send(session, "Error", map[string]any{"code": "INVALID_JSON", "message": "invalid json"})
return return
} }
@@ -167,6 +177,7 @@ func (m *Manager) onMessage(session *melody.Session, raw []byte) {
case "Join": case "Join":
var payload joinPayload var payload joinPayload
if err := json.Unmarshal(msg.Payload, &payload); err != nil { if err := json.Unmarshal(msg.Payload, &payload); err != nil {
m.liveLogger().Warn("live join rejected", "code", "INVALID_JOIN", "reason", "invalid_payload", "remote_addr", sessionRemoteAddr(session))
m.send(session, "JoinRejected", map[string]any{"code": "INVALID_JOIN", "message": "invalid join payload"}) m.send(session, "JoinRejected", map[string]any{"code": "INVALID_JOIN", "message": "invalid join payload"})
return return
} }
@@ -174,6 +185,7 @@ func (m *Manager) onMessage(session *melody.Session, raw []byte) {
case "Offer": case "Offer":
var payload sessionDescriptionPayload var payload sessionDescriptionPayload
if err := json.Unmarshal(msg.Payload, &payload); err != nil { if err := json.Unmarshal(msg.Payload, &payload); err != nil {
m.liveLogger().Warn("live offer rejected", "code", "INVALID_OFFER", "reason", "invalid_payload", "remote_addr", sessionRemoteAddr(session))
m.send(session, "Error", map[string]any{"code": "INVALID_OFFER", "message": "invalid offer payload"}) m.send(session, "Error", map[string]any{"code": "INVALID_OFFER", "message": "invalid offer payload"})
return return
} }
@@ -203,6 +215,7 @@ func (m *Manager) join(session *melody.Session, payload joinPayload) {
role := strings.ToLower(strings.TrimSpace(payload.Role)) role := strings.ToLower(strings.TrimSpace(payload.Role))
streamID := strings.TrimSpace(payload.StreamID) streamID := strings.TrimSpace(payload.StreamID)
if role == "observer" { if role == "observer" {
m.liveLogger().Info("live observer joined", "remote_addr", sessionRemoteAddr(session))
m.mu.Lock() m.mu.Lock()
m.observers[session] = struct{}{} m.observers[session] = struct{}{}
m.mu.Unlock() m.mu.Unlock()
@@ -211,11 +224,14 @@ func (m *Manager) join(session *melody.Session, payload joinPayload) {
return return
} }
if streamID == "" || (role != "publisher" && role != "subscriber") { if streamID == "" || (role != "publisher" && role != "subscriber") {
m.liveLogger().Warn("live join rejected", "code", "INVALID_JOIN", "role", role, "stream_id", streamID, "remote_addr", sessionRemoteAddr(session))
m.send(session, "JoinRejected", map[string]any{"code": "INVALID_JOIN", "message": "role and streamId are required"}) m.send(session, "JoinRejected", map[string]any{"code": "INVALID_JOIN", "message": "role and streamId are required"})
return return
} }
userID, authenticated := sessionUserID(session) userID, authenticated := sessionUserID(session)
m.liveLogger().Info("live join requested", "role", role, "stream_id", streamID, "user_id", userID, "authenticated", authenticated, "remote_addr", sessionRemoteAddr(session))
if role == "publisher" && !authenticated { if role == "publisher" && !authenticated {
m.liveLogger().Warn("live join rejected", "code", "AUTH_REQUIRED", "role", role, "stream_id", streamID, "remote_addr", sessionRemoteAddr(session))
m.send(session, "JoinRejected", map[string]any{"code": "AUTH_REQUIRED", "message": "authenticated user is required"}) m.send(session, "JoinRejected", map[string]any{"code": "AUTH_REQUIRED", "message": "authenticated user is required"})
return return
} }
@@ -227,10 +243,12 @@ func (m *Manager) join(session *melody.Session, payload joinPayload) {
} }
if role == "publisher" { if role == "publisher" {
if err := m.authorizeRoomPublisher(streamID, userID); err != nil { if err := m.authorizeRoomPublisher(streamID, userID); err != nil {
m.liveLogger().Warn("live join rejected", "code", "PUBLISH_FORBIDDEN", "role", role, "stream_id", streamID, "user_id", userID, "error", err)
m.send(session, "JoinRejected", map[string]any{"code": "PUBLISH_FORBIDDEN", "message": err.Error()}) m.send(session, "JoinRejected", map[string]any{"code": "PUBLISH_FORBIDDEN", "message": err.Error()})
return return
} }
} else if err := m.authorizeRoomSubscriber(streamID); err != nil { } else if err := m.authorizeRoomSubscriber(streamID); err != nil {
m.liveLogger().Warn("live join rejected", "code", "ROOM_UNAVAILABLE", "role", role, "stream_id", streamID, "user_id", userID, "error", err)
m.send(session, "JoinRejected", map[string]any{"code": "ROOM_UNAVAILABLE", "message": err.Error()}) m.send(session, "JoinRejected", map[string]any{"code": "ROOM_UNAVAILABLE", "message": err.Error()})
return return
} }
@@ -240,10 +258,12 @@ func (m *Manager) join(session *melody.Session, payload joinPayload) {
occupied := r.publisher != nil occupied := r.publisher != nil
r.mu.RUnlock() r.mu.RUnlock()
if occupied { if occupied {
m.liveLogger().Warn("live join rejected", "code", "DUPLICATE_PUBLISHER", "role", role, "stream_id", streamID, "user_id", userID)
m.send(session, "JoinRejected", map[string]any{"code": "DUPLICATE_PUBLISHER", "message": "stream already has a publisher"}) m.send(session, "JoinRejected", map[string]any{"code": "DUPLICATE_PUBLISHER", "message": "stream already has a publisher"})
return return
} }
if _, err := m.streams.RegisterPublisher(context.Background(), streamID, userID); err != nil { if _, err := m.streams.RegisterPublisher(context.Background(), streamID, userID); err != nil {
m.liveLogger().Warn("live join rejected", "code", errorCode(err), "role", role, "stream_id", streamID, "user_id", userID, "error", err)
m.send(session, "JoinRejected", map[string]any{"code": errorCode(err), "message": err.Error()}) m.send(session, "JoinRejected", map[string]any{"code": errorCode(err), "message": err.Error()})
return return
} }
@@ -259,6 +279,7 @@ func (m *Manager) join(session *melody.Session, payload joinPayload) {
m.peers[session] = p m.peers[session] = p
m.mu.Unlock() m.mu.Unlock()
m.send(session, "Joined", map[string]any{"role": role, "streamId": streamID, "mode": "pion-sfu"}) m.send(session, "Joined", map[string]any{"role": role, "streamId": streamID, "mode": "pion-sfu"})
m.liveLogger().Info("live publisher joined", "stream_id", streamID, "user_id", userID, "remote_addr", sessionRemoteAddr(session))
m.sendPublisherStatus(streamID, false, "publisher_waiting_for_offer") m.sendPublisherStatus(streamID, false, "publisher_waiting_for_offer")
m.broadcastStreamList() m.broadcastStreamList()
return return
@@ -280,6 +301,7 @@ func (m *Manager) join(session *melody.Session, payload joinPayload) {
m.closePeer(old, "logged_in_elsewhere") m.closePeer(old, "logged_in_elsewhere")
} }
if _, err := m.streams.RegisterSubscriber(context.Background(), streamID, userID); err != nil { if _, err := m.streams.RegisterSubscriber(context.Background(), streamID, userID); err != nil {
m.liveLogger().Warn("live join rejected", "code", errorCode(err), "role", role, "stream_id", streamID, "user_id", userID, "error", err)
m.send(session, "JoinRejected", map[string]any{"code": errorCode(err), "message": err.Error()}) m.send(session, "JoinRejected", map[string]any{"code": errorCode(err), "message": err.Error()})
return return
} }
@@ -297,6 +319,7 @@ func (m *Manager) join(session *melody.Session, payload joinPayload) {
publicUserID = 0 publicUserID = 0
} }
m.send(session, "Joined", map[string]any{"role": role, "streamId": streamID, "subscriberId": p.subscriberID, "userId": publicUserID, "mode": "pion-sfu"}) m.send(session, "Joined", map[string]any{"role": role, "streamId": streamID, "subscriberId": p.subscriberID, "userId": publicUserID, "mode": "pion-sfu"})
m.liveLogger().Info("live subscriber joined", "stream_id", streamID, "user_id", publicUserID, "subscriber_id", p.subscriberID, "authenticated", authenticated, "remote_addr", sessionRemoteAddr(session))
m.sendPublisherStatusTo(session, streamID, ready, "publisher_offline") m.sendPublisherStatusTo(session, streamID, ready, "publisher_offline")
if ready { if ready {
m.send(session, "PublisherStatus", map[string]any{"streamId": streamID, "ready": true}) m.send(session, "PublisherStatus", map[string]any{"streamId": streamID, "ready": true})
@@ -414,15 +437,19 @@ func (m *Manager) authorizeRoomSubscriber(streamID string) error {
func (m *Manager) handleOffer(session *melody.Session, payload sessionDescriptionPayload) { func (m *Manager) handleOffer(session *melody.Session, payload sessionDescriptionPayload) {
p := m.peer(session) p := m.peer(session)
if p == nil || (p.role != "publisher" && p.role != "subscriber") { if p == nil || (p.role != "publisher" && p.role != "subscriber") {
m.liveLogger().Warn("live offer rejected", "code", "NOT_JOINED", "remote_addr", sessionRemoteAddr(session))
m.send(session, "Error", map[string]any{"code": "NOT_JOINED", "message": "join a stream first"}) m.send(session, "Error", map[string]any{"code": "NOT_JOINED", "message": "join a stream first"})
return return
} }
if strings.TrimSpace(payload.SDP) == "" { if strings.TrimSpace(payload.SDP) == "" {
m.liveLogger().Warn("live offer rejected", "code", "INVALID_SDP", "role", p.role, "stream_id", p.streamID, "user_id", p.userID)
m.send(session, "Error", map[string]any{"code": "INVALID_SDP", "message": "sdp is required"}) m.send(session, "Error", map[string]any{"code": "INVALID_SDP", "message": "sdp is required"})
return return
} }
m.liveLogger().Info("live offer received", "role", p.role, "stream_id", p.streamID, "user_id", p.userID, "sdp_bytes", len(payload.SDP))
pc, err := m.api.NewPeerConnection(webrtc.Configuration{ICEServers: liveICEServers()}) pc, err := m.api.NewPeerConnection(webrtc.Configuration{ICEServers: liveICEServers()})
if err != nil { if err != nil {
m.liveLogger().Error("live peer connection creation failed", "role", p.role, "stream_id", p.streamID, "user_id", p.userID, "error", err)
m.send(session, "Error", map[string]any{"code": "PEER_CONNECTION", "message": err.Error()}) m.send(session, "Error", map[string]any{"code": "PEER_CONNECTION", "message": err.Error()})
return return
} }
@@ -449,31 +476,37 @@ func (m *Manager) handleOffer(session *melody.Session, payload sessionDescriptio
} }
} }
if err = pc.SetRemoteDescription(webrtc.SessionDescription{Type: webrtc.SDPTypeOffer, SDP: payload.SDP}); err != nil { if err = pc.SetRemoteDescription(webrtc.SessionDescription{Type: webrtc.SDPTypeOffer, SDP: payload.SDP}); err != nil {
m.liveLogger().Error("live remote description failed", "role", p.role, "stream_id", p.streamID, "user_id", p.userID, "error", err)
m.send(session, "Error", map[string]any{"code": "REMOTE_DESCRIPTION", "message": err.Error()}) m.send(session, "Error", map[string]any{"code": "REMOTE_DESCRIPTION", "message": err.Error()})
return return
} }
answer, err := pc.CreateAnswer(nil) answer, err := pc.CreateAnswer(nil)
if err != nil { if err != nil {
m.liveLogger().Error("live answer creation failed", "role", p.role, "stream_id", p.streamID, "user_id", p.userID, "error", err)
m.send(session, "Error", map[string]any{"code": "CREATE_ANSWER", "message": err.Error()}) m.send(session, "Error", map[string]any{"code": "CREATE_ANSWER", "message": err.Error()})
return return
} }
if err = pc.SetLocalDescription(answer); err != nil { if err = pc.SetLocalDescription(answer); err != nil {
m.liveLogger().Error("live local description failed", "role", p.role, "stream_id", p.streamID, "user_id", p.userID, "error", err)
m.send(session, "Error", map[string]any{"code": "LOCAL_DESCRIPTION", "message": err.Error()}) m.send(session, "Error", map[string]any{"code": "LOCAL_DESCRIPTION", "message": err.Error()})
return return
} }
<-webrtc.GatheringCompletePromise(pc) <-webrtc.GatheringCompletePromise(pc)
local := pc.LocalDescription() local := pc.LocalDescription()
if local == nil { if local == nil {
m.liveLogger().Error("live local description unavailable", "role", p.role, "stream_id", p.streamID, "user_id", p.userID)
m.send(session, "Error", map[string]any{"code": "NO_LOCAL_DESCRIPTION", "message": "local description unavailable"}) m.send(session, "Error", map[string]any{"code": "NO_LOCAL_DESCRIPTION", "message": "local description unavailable"})
return return
} }
m.send(session, "Answer", map[string]any{"type": local.Type.String(), "sdp": local.SDP}) m.send(session, "Answer", map[string]any{"type": local.Type.String(), "sdp": local.SDP})
m.liveLogger().Info("live answer sent", "role", p.role, "stream_id", p.streamID, "user_id", p.userID, "sdp_bytes", len(local.SDP))
if p.role == "publisher" { if p.role == "publisher" {
m.sendPublisherStatus(p.streamID, false, "waiting_for_tracks") m.sendPublisherStatus(p.streamID, false, "waiting_for_tracks")
} }
} }
func (m *Manager) handleICEConnectionStateChange(p *peer, pc *webrtc.PeerConnection, state webrtc.ICEConnectionState) { func (m *Manager) handleICEConnectionStateChange(p *peer, pc *webrtc.PeerConnection, state webrtc.ICEConnectionState) {
m.liveLogger().Info("live ICE state changed", "role", p.role, "stream_id", p.streamID, "user_id", p.userID, "state", state.String())
if state != webrtc.ICEConnectionStateFailed && state != webrtc.ICEConnectionStateClosed { if state != webrtc.ICEConnectionStateFailed && state != webrtc.ICEConnectionStateClosed {
return return
} }
@@ -488,6 +521,7 @@ func (m *Manager) onPublisherTrack(p *peer, remote *webrtc.TrackRemote) {
return return
} }
entry := &publishedTrack{id: remote.ID(), kind: remote.Kind(), local: local} entry := &publishedTrack{id: remote.ID(), kind: remote.Kind(), local: local}
m.liveLogger().Info("live publisher track received", "stream_id", p.streamID, "user_id", p.userID, "track_id", remote.ID(), "kind", strings.ToLower(remote.Kind().String()), "codec", codec.MimeType)
p.room.mu.Lock() p.room.mu.Lock()
p.room.tracks[remote.Kind()] = entry p.room.tracks[remote.Kind()] = entry
viewers := make([]*peer, 0, len(p.room.viewers)) viewers := make([]*peer, 0, len(p.room.viewers))
@@ -513,6 +547,7 @@ func (m *Manager) onPublisherTrack(p *peer, remote *webrtc.TrackRemote) {
func (m *Manager) onDisconnect(session *melody.Session) { func (m *Manager) onDisconnect(session *melody.Session) {
if p := m.peer(session); p != nil { if p := m.peer(session); p != nil {
m.liveLogger().Info("live signaling disconnected", "role", p.role, "stream_id", p.streamID, "user_id", p.userID, "remote_addr", sessionRemoteAddr(session))
m.closePeer(p, "disconnected") m.closePeer(p, "disconnected")
return return
} }
@@ -558,6 +593,7 @@ func (m *Manager) closePeerConnection(p *peer, candidate *webrtc.PeerConnection,
return return
} }
p.cleanup.Do(func() { p.cleanup.Do(func() {
m.liveLogger().Info("live peer closing", "role", p.role, "stream_id", p.streamID, "user_id", p.userID, "reason", reason)
if pc != nil { if pc != nil {
_ = pc.Close() _ = pc.Close()
} }
@@ -721,15 +757,30 @@ func (m *Manager) sendPublisherStatusTo(session *melody.Session, streamID string
func (m *Manager) send(session *melody.Session, msgType string, payload any) { func (m *Manager) send(session *melody.Session, msgType string, payload any) {
encoded, err := json.Marshal(map[string]any{"type": msgType, "payload": payload}) encoded, err := json.Marshal(map[string]any{"type": msgType, "payload": payload})
if err != nil { if err != nil {
m.liveLogger().Error("live signaling response encoding failed", "message_type", msgType, "error", err)
return return
} }
_ = m.hub.Send(session, encoded) if err = m.hub.Send(session, encoded); err != nil {
m.liveLogger().Warn("live signaling response send failed", "message_type", msgType, "remote_addr", sessionRemoteAddr(session), "error", err)
}
} }
func (m *Manager) log(message string, args ...any) { func (m *Manager) log(message string, args ...any) {
if m.logger != nil { m.liveLogger().Warn(message, args...)
m.logger.Warn(message, args...) }
func (m *Manager) liveLogger() *slog.Logger {
if m != nil && m.logger != nil {
return m.logger
} }
return slog.Default().With("mod", "live")
}
func sessionRemoteAddr(session *melody.Session) string {
if session == nil || session.Request == nil {
return ""
}
return session.Request.RemoteAddr
} }
func sessionUserID(session *melody.Session) (uint, bool) { func sessionUserID(session *melody.Session) (uint, bool) {
+2
View File
@@ -435,6 +435,8 @@ func routedLogPaths(module string, level zapcore.Level) []string {
paths = append(paths, filepath.Join("timedTask", "task.log")) paths = append(paths, filepath.Join("timedTask", "task.log"))
case "websocket": case "websocket":
paths = append(paths, filepath.Join("websocket", "websocket.log")) paths = append(paths, filepath.Join("websocket", "websocket.log"))
case "live":
paths = append(paths, filepath.Join("live", "live.log"))
case "error": case "error":
paths = append(paths, filepath.Join("error", "error.log")) paths = append(paths, filepath.Join("error", "error.log"))
default: default:
+2
View File
@@ -42,6 +42,7 @@ func TestZapHandlerRoutesHTTPAndErrorLogs(t *testing.T) {
logger := slog.New(handler) logger := slog.New(handler)
logger.Info("request", "mod", "http", "request_id", "req-1") logger.Info("request", "mod", "http", "request_id", "req-1")
logger.Info("handshake", "mod", "websocket") logger.Info("handshake", "mod", "websocket")
logger.Info("publisher joined", "mod", "live")
logger.Error("failed", "mod", "users") logger.Error("failed", "mod", "users")
cleanup() cleanup()
@@ -54,6 +55,7 @@ func TestZapHandlerRoutesHTTPAndErrorLogs(t *testing.T) {
filepath.Join(root, date, "application.log"), filepath.Join(root, date, "application.log"),
filepath.Join(root, date, "http", "access.log"), filepath.Join(root, date, "http", "access.log"),
filepath.Join(root, date, "websocket", "websocket.log"), filepath.Join(root, date, "websocket", "websocket.log"),
filepath.Join(root, date, "live", "live.log"),
filepath.Join(root, date, "users", "application.log"), filepath.Join(root, date, "users", "application.log"),
filepath.Join(root, date, "error", "error.log"), filepath.Join(root, date, "error", "error.log"),
} }