BareRTC/pkg/websocket.go
Noah Petherbridge 4f93c27651 Progress on Chat Server
* Reworked full screen CSS layout for the chat.html, still using Bulma
  components with some custom CSS Grid.
* Duplicate username handling: server can push a new username to change
  the client's selection.
* Who List sync between clients.
* Local video casting works so far - plays back your camera in the local
  feed. Your video broadcasting boolean is synced to backend, which
  lights up the video button in the Who List.
2023-01-26 20:34:58 -08:00

229 lines
5.4 KiB
Go

package barertc
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"git.kirsle.net/apps/barertc/pkg/log"
"nhooyr.io/websocket"
)
// Subscriber represents a connected WebSocket session.
type Subscriber struct {
// User properties
ID int // ID assigned by server
Username string
VideoActive bool
conn *websocket.Conn
ctx context.Context
messages chan []byte
closeSlow func()
}
// ReadLoop spawns a goroutine that reads from the websocket connection.
func (sub *Subscriber) ReadLoop(s *Server) {
go func() {
for {
msgType, data, err := sub.conn.Read(sub.ctx)
if err != nil {
log.Error("ReadLoop error: %+v", err)
s.DeleteSubscriber(sub)
s.Broadcast(Message{
Action: ActionPresence,
Username: sub.Username,
Message: "has exited the room!",
})
s.SendWhoList()
return
}
if msgType != websocket.MessageText {
log.Error("Unexpected MessageType")
continue
}
// Read the user's posted message.
var msg Message
log.Debug("Read(%s): %s", sub.Username, data)
if err := json.Unmarshal(data, &msg); err != nil {
log.Error("Message error: %s", err)
continue
}
// What action are they performing?
switch msg.Action {
case ActionLogin:
s.OnLogin(sub, msg)
case ActionMessage:
s.OnMessage(sub, msg)
case ActionMe:
s.OnMe(sub, msg)
default:
sub.SendJSON(Message{
Action: ActionMessage,
Username: "ChatServer",
Message: "Unsupported message type.",
})
}
}
}()
}
// SendJSON sends a JSON message to the websocket client.
func (sub *Subscriber) SendJSON(v interface{}) error {
data, err := json.Marshal(v)
if err != nil {
return err
}
log.Debug("SendJSON(%s): %s", sub.Username, data)
return sub.conn.Write(sub.ctx, websocket.MessageText, data)
}
// SendMe sends the current user state to the client.
func (sub *Subscriber) SendMe() {
sub.SendJSON(Message{
Action: ActionMe,
Username: sub.Username,
VideoActive: sub.VideoActive,
})
}
// WebSocket handles the /ws websocket connection.
func (s *Server) WebSocket() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := websocket.Accept(w, r, nil)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Websocket error: %s", err)
return
}
defer c.Close(websocket.StatusInternalError, "the sky is falling")
log.Debug("WebSocket: %s has connected", r.RemoteAddr)
// CloseRead starts a goroutine that will read from the connection
// until it is closed.
// ctx := c.CloseRead(r.Context())
ctx, _ := context.WithCancel(r.Context())
sub := &Subscriber{
conn: c,
ctx: ctx,
messages: make(chan []byte, s.subscriberMessageBuffer),
closeSlow: func() {
c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages")
},
}
s.AddSubscriber(sub)
// defer s.DeleteSubscriber(sub)
go sub.ReadLoop(s)
for {
select {
case msg := <-sub.messages:
err = writeTimeout(ctx, time.Second*5, c, msg)
if err != nil {
return
}
case <-ctx.Done():
return
}
}
})
}
// Auto incrementing Subscriber ID, assigned in AddSubscriber.
var SubscriberID int
// AddSubscriber adds a WebSocket subscriber to the server.
func (s *Server) AddSubscriber(sub *Subscriber) {
// Assign a unique ID.
SubscriberID++
sub.ID = SubscriberID
log.Debug("AddSubscriber: %s", sub.ID)
s.subscribersMu.Lock()
s.subscribers[sub] = struct{}{}
s.subscribersMu.Unlock()
}
// DeleteSubscriber removes a subscriber from the server.
func (s *Server) DeleteSubscriber(sub *Subscriber) {
log.Error("DeleteSubscriber: %s", sub.Username)
s.subscribersMu.Lock()
delete(s.subscribers, sub)
s.subscribersMu.Unlock()
}
// IterSubscribers loops over the subscriber list with a read lock. If the
// caller already holds a lock, pass the optional `true` parameter for isLocked.
func (s *Server) IterSubscribers(isLocked ...bool) chan *Subscriber {
var out = make(chan *Subscriber)
go func() {
log.Debug("IterSubscribers START..")
var result = []*Subscriber{}
// Has the caller already taken the read lock or do we get it?
if locked := len(isLocked) > 0 && isLocked[0]; !locked {
log.Debug("Taking the lock")
s.subscribersMu.RLock()
defer s.subscribersMu.RUnlock()
}
for sub := range s.subscribers {
result = append(result, sub)
}
for _, r := range result {
out <- r
}
close(out)
log.Debug("IterSubscribers STOP!")
}()
return out
}
// Broadcast a message to the chat room.
func (s *Server) Broadcast(msg Message) {
log.Debug("Broadcast: %+v", msg)
s.subscribersMu.RLock()
defer s.subscribersMu.RUnlock()
for sub := range s.IterSubscribers(true) {
sub.SendJSON(Message{
Action: msg.Action,
Username: msg.Username,
Message: msg.Message,
})
}
}
// SendWhoList broadcasts the connected members to everybody in the room.
func (s *Server) SendWhoList() {
var users = []WhoList{}
for sub := range s.IterSubscribers() {
users = append(users, WhoList{
Username: sub.Username,
VideoActive: sub.VideoActive,
})
}
for sub := range s.IterSubscribers() {
sub.SendJSON(Message{
Action: ActionWhoList,
WhoList: users,
})
}
}
func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
return c.Write(ctx, websocket.MessageText, msg)
}