package op import ( "errors" "hash/crc32" "net/url" "sync/atomic" "time" "github.com/go-resty/resty/v2" "github.com/google/uuid" "github.com/gorilla/websocket" log "github.com/sirupsen/logrus" "github.com/synctv-org/synctv/internal/conf" "github.com/synctv-org/synctv/internal/db" "github.com/synctv-org/synctv/internal/model" "github.com/synctv-org/synctv/utils" "github.com/zijiren233/gencontainer/rwmap" "github.com/zijiren233/livelib/av" "github.com/zijiren233/livelib/container/flv" rtmpProto "github.com/zijiren233/livelib/protocol/rtmp" "github.com/zijiren233/livelib/protocol/rtmp/core" rtmps "github.com/zijiren233/livelib/server" "github.com/zijiren233/stream" "golang.org/x/crypto/bcrypt" ) type Room struct { model.Room version uint32 current *current initOnce utils.Once hub *Hub channles rwmap.RWMap[string, *rtmps.Channel] } func (r *Room) LazyInit() (err error) { r.initOnce.Do(func() { r.hub = newHub(r.ID) var ms []*model.Movie ms, err = r.GetAllMoviesByRoomID() if err != nil { log.Errorf("failed to get movies: %s", err.Error()) return } for _, m := range ms { if err = r.initMovie(m); err != nil { log.Errorf("lazy init room %d movie %d failed: %s", r.ID, m.ID, err.Error()) DeleteMovieByID(r.ID, m.ID) } } }) return } func (r *Room) ClientNum() int64 { if r.hub == nil { return 0 } return r.hub.ClientNum() } func (r *Room) Broadcast(data Message, conf ...BroadcastConf) error { if r.hub == nil { return nil } return r.hub.Broadcast(data, conf...) } func (r *Room) GetChannel(channelName string) (*rtmps.Channel, error) { err := r.LazyInit() if err != nil { return nil, err } c, ok := r.channles.Load(channelName) if !ok { return nil, errors.New("channel not found") } return c, nil } func (r *Room) close() { if r.initOnce.Done() { r.hub.Close() r.channles.Range(func(_ string, c *rtmps.Channel) bool { c.Close() return true }) } } func (r *Room) Version() uint32 { return atomic.LoadUint32(&r.version) } func (r *Room) CheckVersion(version uint32) bool { return atomic.LoadUint32(&r.version) == version } func (r *Room) UpdateMovie(movieId uint, movie model.BaseMovieInfo) error { err := r.LazyInit() if err != nil { return err } m, err := GetMovieByID(r.ID, movieId) if err != nil { return err } err = r.terminateMovie(m) if err != nil { return err } m.MovieInfo.BaseMovieInfo = movie err = r.initMovie(m) if err != nil { return err } return SaveMovie(m) } func (r *Room) terminateMovie(movie *model.Movie) error { switch { case movie.Live && movie.RtmpSource, movie.Live && movie.Proxy: c, loaded := r.channles.LoadAndDelete(movie.PullKey) if loaded { return c.Close() } } return nil } func (r *Room) initMovie(movie *model.Movie) error { switch { case movie.RtmpSource && movie.Proxy: return errors.New("rtmp source and proxy can't be true at the same time") case movie.Live && movie.RtmpSource: if !conf.Conf.Rtmp.Enable { return errors.New("rtmp is not enabled") } if movie.PullKey == "" { movie.PullKey = uuid.NewString() } c, loaded := r.channles.LoadOrStore(movie.PullKey, rtmps.NewChannel()) if loaded { return errors.New("pull key already exists") } c.InitHlsPlayer() case movie.Live && movie.Proxy: if !conf.Conf.Proxy.LiveProxy { return errors.New("live proxy is not enabled") } u, err := url.Parse(movie.Url) if err != nil { return err } if utils.IsLocalIP(u.Host) { return errors.New("local ip is not allowed") } switch u.Scheme { case "rtmp": movie.PullKey = uuid.NewMD5(uuid.NameSpaceURL, []byte(movie.Url)).String() c, loaded := r.channles.LoadOrStore(movie.PullKey, rtmps.NewChannel()) if loaded { return errors.New("pull key already exists") } c.InitHlsPlayer() go func() { for { if c.Closed() { return } cli := core.NewConnClient() if err = cli.Start(movie.Url, av.PLAY); err != nil { cli.Close() time.Sleep(time.Second) continue } if err := c.PushStart(rtmpProto.NewReader(cli)); err != nil { cli.Close() time.Sleep(time.Second) } } }() case "http", "https": if movie.Type != "flv" { return errors.New("only flv is supported") } movie.PullKey = uuid.NewMD5(uuid.NameSpaceURL, []byte(movie.Url)).String() c, loaded := r.channles.LoadOrStore(movie.PullKey, rtmps.NewChannel()) if loaded { return errors.New("pull key already exists") } c.InitHlsPlayer() go func() { for { if c.Closed() { return } r := resty.New().R() for k, v := range movie.Headers { r.SetHeader(k, v) } // r.SetHeader("User-Agent", UserAgent) resp, err := r.Get(movie.Url) if err != nil { time.Sleep(time.Second) continue } if err := c.PushStart(flv.NewReader(resp.RawBody())); err != nil { time.Sleep(time.Second) } resp.RawBody().Close() } }() default: return errors.New("unsupported scheme") } case !movie.Live && movie.RtmpSource: return errors.New("rtmp source can't be true when movie is not live") case !movie.Live && movie.Proxy: if !conf.Conf.Proxy.MovieProxy { return errors.New("movie proxy is not enabled") } u, err := url.Parse(movie.Url) if err != nil { return err } if utils.IsLocalIP(u.Host) { return errors.New("local ip is not allowed") } if u.Scheme != "http" && u.Scheme != "https" { return errors.New("unsupported scheme") } movie.PullKey = uuid.NewMD5(uuid.NameSpaceURL, []byte(movie.Url)).String() case !movie.Live && !movie.Proxy, movie.Live && !movie.Proxy && !movie.RtmpSource: u, err := url.Parse(movie.Url) if err != nil { return err } if u.Scheme != "http" && u.Scheme != "https" { return errors.New("unsupported scheme") } movie.PullKey = "" default: return errors.New("unknown error") } return nil } func (r *Room) AddMovie(m model.MovieInfo) error { err := r.LazyInit() if err != nil { return err } movie := &model.Movie{ RoomID: r.ID, Position: uint(time.Now().UnixMilli()), MovieInfo: m, } err = r.initMovie(movie) if err != nil { return err } return CreateMovie(movie) } func (r *Room) HasPermission(user *model.User, permission model.Permission) bool { ur, err := db.GetRoomUserRelation(r.ID, user.ID) if err != nil { return false } return ur.HasPermission(permission) } func (r *Room) NeedPassword() bool { return len(r.HashedPassword) != 0 } func (r *Room) SetPassword(password string) error { if r.CheckPassword(password) && r.NeedPassword() { return errors.New("password is the same") } var hashedPassword []byte if password != "" { var err error hashedPassword, err = bcrypt.GenerateFromPassword(stream.StringToBytes(password), bcrypt.DefaultCost) if err != nil { return err } atomic.StoreUint32(&r.version, crc32.ChecksumIEEE(hashedPassword)) } r.HashedPassword = hashedPassword return db.SetRoomHashedPassword(r.ID, hashedPassword) } func (r *Room) SetUserRole(userID uint, role model.RoomRole) error { return db.SetUserRole(r.ID, userID, role) } func (r *Room) SetUserPermission(userID uint, permission model.Permission) error { return db.SetUserPermission(r.ID, userID, permission) } func (r *Room) AddUserPermission(userID uint, permission model.Permission) error { return db.AddUserPermission(r.ID, userID, permission) } func (r *Room) RemoveUserPermission(userID uint, permission model.Permission) error { return db.RemoveUserPermission(r.ID, userID, permission) } func (r *Room) DeleteUserPermission(userID uint) error { return db.DeleteUserPermission(r.ID, userID) } func (r *Room) GetMoviesCount() (int, error) { return GetMoviesCountByRoomID(r.ID) } func (r *Room) GetAllMoviesByRoomID() ([]*model.Movie, error) { ms, err := GetAllMoviesByRoomID(r.ID) if err != nil { return nil, err } var m []*model.Movie = make([]*model.Movie, 0, ms.Len()) for i := ms.Front(); i != nil; i = i.Next() { m = append(m, i.Value) } return m, nil } func (r *Room) GetMoviesByRoomIDWithPage(page, pageSize int) ([]*model.Movie, error) { return GetMoviesByRoomIDWithPage(r.ID, page, pageSize) } func (r *Room) GetMovieByID(id uint) (*model.Movie, error) { return GetMovieByID(r.ID, id) } func (r *Room) DeleteMovieByID(id uint) error { r.LazyInit() m, err := LoadAndDeleteMovieByID(r.ID, id) if err != nil { return err } return r.terminateMovie(m) } func (r *Room) ClearMovies() error { r.LazyInit() ms, err := db.LoadAndDeleteMoviesByRoomID(r.ID) if err != nil { return err } for _, m := range ms { r.terminateMovie(m) } return nil } func (r *Room) Current() *Current { c := r.current.Current() return &c } func (r *Room) ChangeCurrentMovie(id uint) error { r.LazyInit() m, err := GetMovieByID(r.ID, id) if err != nil { return err } r.current.SetMovie(*m) return nil } func (r *Room) SwapMoviePositions(id1, id2 uint) error { r.LazyInit() return SwapMoviePositions(r.ID, id1, id2) } func (r *Room) GetMovieWithPullKey(pullKey string) (*model.Movie, error) { return GetMovieWithPullKey(r.ID, pullKey) } func (r *Room) RegClient(user *User, conn *websocket.Conn) (*Client, error) { r.LazyInit() return r.hub.RegClient(newClient(user, r, conn)) } func (r *Room) UnregisterClient(user *User) error { r.LazyInit() return r.hub.UnRegClient(user) } func (r *Room) SetStatus(playing bool, seek float64, rate float64, timeDiff float64) Status { return r.current.SetStatus(playing, seek, rate, timeDiff) } func (r *Room) SetSeekRate(seek float64, rate float64, timeDiff float64) Status { return r.current.SetSeekRate(seek, rate, timeDiff) }