mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-12-24 13:48:04 +08:00
203 lines
5.0 KiB
Go
203 lines
5.0 KiB
Go
package plugin_room
|
||
|
||
import (
|
||
"context"
|
||
_ "embed"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net"
|
||
"net/http"
|
||
"strings"
|
||
"sync"
|
||
|
||
"github.com/gobwas/ws"
|
||
"github.com/gobwas/ws/wsutil"
|
||
"github.com/google/uuid"
|
||
. "m7s.live/v5"
|
||
"m7s.live/v5/pkg"
|
||
"m7s.live/v5/pkg/task"
|
||
"m7s.live/v5/pkg/util"
|
||
)
|
||
|
||
// User 结构体代表一个房间用户
|
||
type User struct {
|
||
task.Task
|
||
Subscriber *Subscriber
|
||
ID string
|
||
StreamPath string
|
||
Token string `json:"-" yaml:"-"`
|
||
Room *Room `json:"-" yaml:"-"`
|
||
net.Conn `json:"-" yaml:"-"`
|
||
writeLock sync.Mutex
|
||
}
|
||
|
||
func (u *User) GetKey() string {
|
||
return u.ID
|
||
}
|
||
|
||
func (u *User) Start() (err error) {
|
||
u.Subscriber, err = u.Room.Plugin.Subscribe(u, u.StreamPath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
u.Subscriber.DataChannel = make(chan pkg.IDataFrame, 10)
|
||
return nil
|
||
}
|
||
|
||
func (u *User) Go() error {
|
||
for data := range u.Subscriber.DataChannel {
|
||
u.writeLock.Lock()
|
||
wsutil.WriteServerText(u.Conn, data.([]byte))
|
||
u.writeLock.Unlock()
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (u *User) Dispose() {
|
||
close(u.Subscriber.DataChannel)
|
||
}
|
||
|
||
func (u *User) Send(event string, data any) {
|
||
if u.Conn != nil {
|
||
u.writeLock.Lock()
|
||
defer u.writeLock.Unlock()
|
||
j, err := json.Marshal(map[string]any{"event": event, "data": data})
|
||
if err == nil {
|
||
wsutil.WriteServerText(u.Conn, j)
|
||
}
|
||
}
|
||
}
|
||
|
||
type Room struct {
|
||
*Publisher
|
||
ID string
|
||
Users task.Manager[string, *User]
|
||
}
|
||
|
||
//go:embed default.yaml
|
||
var defaultYaml DefaultYaml
|
||
|
||
type RoomPlugin struct {
|
||
Plugin
|
||
AppName string `default:"room" desc:"用于订阅房间消息的应用名(streamPath第一段)"`
|
||
Size int `default:"20" desc:"房间大小"`
|
||
Private map[string]string `desc:"私密房间" key:"房间号" value:"密码"`
|
||
Verify struct {
|
||
URL string `desc:"验证用户身份的URL"`
|
||
Method string `desc:"验证用户身份的HTTP方法"`
|
||
Header map[string]string `desc:"验证用户身份的HTTP头" key:"名称" value:"值"`
|
||
} `desc:"验证用户身份"`
|
||
Ping string `default:"ping" desc:"用于客户端与服务器保持心跳时客户端发送的特殊字符串"`
|
||
Pong string `default:"pong" desc:"用于客户端与服务器保持心跳时服务器响应的特殊字符串"`
|
||
lock sync.RWMutex
|
||
rooms util.Collection[string, *Room]
|
||
}
|
||
|
||
var _ = InstallPlugin[RoomPlugin](defaultYaml)
|
||
|
||
func (rc *RoomPlugin) OnPublish(p *Publisher) {
|
||
args := p.Args
|
||
token := args.Get("token")
|
||
ss := strings.Split(token, ":")
|
||
if len(ss) != 3 {
|
||
return
|
||
}
|
||
roomId := ss[0]
|
||
userId := ss[1]
|
||
if roomId != "" && rc.rooms.Has(roomId) {
|
||
room, _ := rc.rooms.Get(roomId)
|
||
if user, ok := room.Users.Get(userId); ok {
|
||
if user.Token == token {
|
||
user.StreamPath = p.StreamPath
|
||
data, _ := json.Marshal(map[string]any{
|
||
"event": "publish",
|
||
"data": p.StreamPath,
|
||
"userId": user.ID,
|
||
})
|
||
room.WriteData(data)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func (rc *RoomPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||
ss := strings.Split(r.URL.Path, "/")[1:]
|
||
var roomId, userId, token string
|
||
if len(ss) == 2 {
|
||
roomId = ss[0]
|
||
userId = ss[1]
|
||
} else {
|
||
http.Error(w, "invalid url", http.StatusBadRequest)
|
||
return
|
||
}
|
||
if rc.Verify.URL != "" {
|
||
req, _ := http.NewRequest(rc.Verify.Method, rc.Verify.URL, nil)
|
||
req.Header = r.Header
|
||
res, _ := http.DefaultClient.Do(req)
|
||
if res.StatusCode != 200 {
|
||
http.Error(w, "verify failed", http.StatusForbidden)
|
||
}
|
||
}
|
||
if rc.Private != nil {
|
||
rc.lock.RLock()
|
||
pass, ok := rc.Private[roomId]
|
||
rc.lock.Unlock()
|
||
if ok {
|
||
if pass != r.URL.Query().Get("password") {
|
||
http.Error(w, "password wrong", http.StatusForbidden)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
var room *Room
|
||
var ok bool
|
||
if room, ok = rc.rooms.Get(roomId); !ok {
|
||
room = &Room{ID: roomId}
|
||
if _, err := rc.Publish(context.Background(), rc.AppName+"/"+roomId); err == nil {
|
||
rc.rooms.Add(room)
|
||
} else {
|
||
http.Error(w, "room already exist", http.StatusBadRequest)
|
||
return
|
||
}
|
||
}
|
||
if room.Users.Has(userId) {
|
||
http.Error(w, "user exist", http.StatusBadRequest)
|
||
return
|
||
}
|
||
conn, _, _, err := ws.UpgradeHTTP(r, w)
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
return
|
||
}
|
||
defer conn.Close()
|
||
token = fmt.Sprintf("%s:%s:%s", roomId, userId, uuid.NewString())
|
||
user := &User{Room: room, Conn: conn, Token: token, ID: userId}
|
||
data, _ := json.Marshal(map[string]any{"event": "userjoin", "data": user})
|
||
room.WriteData(data)
|
||
room.Users.Add(user)
|
||
user.Send("joined", map[string]any{"token": token, "userList": room.Users.Items})
|
||
defer func() {
|
||
user.Stop(err)
|
||
if room.Users.Length == 0 {
|
||
room.Stop(err)
|
||
rc.rooms.RemoveByKey(roomId)
|
||
}
|
||
}()
|
||
var msg []byte
|
||
var op ws.OpCode
|
||
for {
|
||
msg, op, err = wsutil.ReadClientData(conn)
|
||
if string(msg) == rc.Ping {
|
||
wsutil.WriteServerText(conn, []byte(rc.Pong))
|
||
} else {
|
||
data, _ := json.Marshal(map[string]any{"event": "msg", "data": string(msg), "userId": userId})
|
||
room.WriteData(data)
|
||
}
|
||
if op == ws.OpClose || err != nil {
|
||
data, _ := json.Marshal(map[string]any{"event": "userleave", "userId": userId, "data": user})
|
||
room.WriteData(data)
|
||
return
|
||
}
|
||
}
|
||
}
|