mirror of https://github.com/synctv-org/synctv.git
Feat: synctv init
This commit is contained in:
parent
586f37bbab
commit
acf7718c0a
|
@ -0,0 +1,4 @@
|
|||
/log
|
||||
/public/dist/*
|
||||
!*.gitkeep
|
||||
.DS_Store
|
|
@ -0,0 +1,19 @@
|
|||
[English](./README.md) | 中文
|
||||
|
||||
# 特点
|
||||
- [x] 同步观看
|
||||
- [x] 视频同步
|
||||
- [x] 直播同步
|
||||
- [x] 影院模式
|
||||
- [x] 聊天
|
||||
- [x] 弹幕
|
||||
- [x] 代理
|
||||
- [ ] 视频代理
|
||||
- [ ] 直播代理
|
||||
|
||||
# 免责声明
|
||||
- 这个程序是一个免费且开源的项目。它旨在播放网络上的视频文件,方便多人共同观看视频和学习golang。
|
||||
- 在使用时,请遵守相关法律法规,不要滥用。
|
||||
- 该程序仅进行客户端播放视频文件/流量转发,不会拦截、存储或篡改任何用户数据。
|
||||
- 在使用该程序之前,您应该了解并承担相应的风险,包括但不限于版权纠纷、法律限制等,这与该程序无关。
|
||||
- 如果有任何侵权行为,请通过[电子邮件](mailto:pyh1670605849@gmail.com)与我联系,将及时处理。
|
24
README.md
24
README.md
|
@ -1,2 +1,22 @@
|
|||
# synctv
|
||||
Synchronized viewing, theater, live streaming, video, long-distance relationship
|
||||
English | [中文](./README-CN.md)
|
||||
|
||||
# Features
|
||||
- [x] Synchronized viewing
|
||||
- [x] Videos Sync
|
||||
- [x] Live streaming
|
||||
- [x] Theater
|
||||
- [x] Chat
|
||||
- [x] Bullet chat
|
||||
- [x] Proxy
|
||||
- [ ] Videos proxy
|
||||
- [ ] Live proxy
|
||||
|
||||
# License
|
||||
The `SyncTV` is open-source software licensed under the AGPL-3.0 license.
|
||||
|
||||
# Disclaimer
|
||||
- This program is a free and open-source project. It aims to play video files on the internet, making it convenient for multiple people to watch videos and learn golang together.
|
||||
- Please comply with relevant laws and regulations when using it, and do not abuse it.
|
||||
- The program only plays video files/forwards traffic on the client-side and will not intercept, store, or tamper with any user data.
|
||||
- Before using the program, you should understand and assume the corresponding risks, including but not limited to copyright disputes, legal restrictions, etc., which are not related to the program.
|
||||
- If there is any infringement, please contact me via [email](mailto:pyh1670605849@gmail.com), and it will be dealt with promptly.
|
|
@ -0,0 +1,17 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/synctv-org/synctv/cmd/flags"
|
||||
)
|
||||
|
||||
func InitGinMode() error {
|
||||
if flags.Dev {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
gin.ForceConsoleColor()
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package cmd
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func ConfCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "conf",
|
||||
Short: "conf",
|
||||
Long: `config file`,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package flags
|
||||
|
||||
// Global
|
||||
var (
|
||||
Dev bool
|
||||
|
||||
LogStd bool
|
||||
|
||||
ConfigFile string
|
||||
|
||||
SkipEnv bool
|
||||
|
||||
SkipConfig bool
|
||||
|
||||
EnvNoPrefix bool
|
||||
)
|
|
@ -0,0 +1,24 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/synctv-org/synctv/internal/bootstrap"
|
||||
)
|
||||
|
||||
func Init(cmd *cobra.Command, args []string) error {
|
||||
bootstrap.InitConfig()
|
||||
bootstrap.InitLog()
|
||||
bootstrap.InitSysNotify()
|
||||
return nil
|
||||
}
|
||||
|
||||
var InitCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "init and check config",
|
||||
Long: `auto create config file or check config, and auto add new key and delete old key`,
|
||||
RunE: Init,
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(InitCmd)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/synctv-org/synctv/cmd/flags"
|
||||
)
|
||||
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "synctv-server",
|
||||
Short: "synctv-server",
|
||||
Long: `synctv-server https://github.com/synctv-org/synctv`,
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := RootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.LogStd, "log-std", true, "log to std")
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.EnvNoPrefix, "env-no-prefix", false, "env no SYNCTV_ prefix")
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.SkipConfig, "skip-config", false, "skip config")
|
||||
RootCmd.PersistentFlags().BoolVar(&flags.SkipEnv, "skip-env", false, "skip env")
|
||||
RootCmd.PersistentFlags().StringVarP(&flags.ConfigFile, "config", "f", "", "config file path")
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/soheilhy/cmux"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/synctv-org/synctv/internal/bootstrap"
|
||||
"github.com/synctv-org/synctv/internal/conf"
|
||||
"github.com/synctv-org/synctv/server"
|
||||
)
|
||||
|
||||
var ServerCmd = &cobra.Command{
|
||||
Use: "server",
|
||||
Short: "Start synctv-server",
|
||||
Long: `Start synctv-server`,
|
||||
PersistentPreRunE: Init,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error { return InitGinMode() },
|
||||
Run: Server,
|
||||
}
|
||||
|
||||
func Server(cmd *cobra.Command, args []string) {
|
||||
tcpServerAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", conf.Conf.Server.Listen, conf.Conf.Server.Port))
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
udpServerAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", conf.Conf.Server.Listen, conf.Conf.Server.Port))
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
serverListener, err := net.ListenTCP("tcp", tcpServerAddr)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
var useMux bool
|
||||
if conf.Conf.Rtmp.Port == 0 || conf.Conf.Rtmp.Port == conf.Conf.Server.Port {
|
||||
useMux = true
|
||||
conf.Conf.Rtmp.Port = conf.Conf.Server.Port
|
||||
}
|
||||
tcpRtmpAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", conf.Conf.Server.Listen, conf.Conf.Rtmp.Port))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if conf.Conf.Rtmp.Enable {
|
||||
if useMux {
|
||||
muxer := cmux.New(serverListener)
|
||||
e, s := server.NewAndInit()
|
||||
switch {
|
||||
case conf.Conf.Server.CertPath != "" && conf.Conf.Server.KeyPath != "":
|
||||
httpl := muxer.Match(cmux.HTTP2(), cmux.TLS())
|
||||
go http.ServeTLS(httpl, e.Handler(), conf.Conf.Server.CertPath, conf.Conf.Server.KeyPath)
|
||||
if conf.Conf.Server.Quic {
|
||||
go http3.ListenAndServeQUIC(udpServerAddr.String(), conf.Conf.Server.CertPath, conf.Conf.Server.KeyPath, e.Handler())
|
||||
}
|
||||
case conf.Conf.Server.CertPath == "" && conf.Conf.Server.KeyPath == "":
|
||||
httpl := muxer.Match(cmux.HTTP1Fast())
|
||||
go e.RunListener(httpl)
|
||||
default:
|
||||
log.Panic("cert and key must be both set")
|
||||
}
|
||||
tcp := muxer.Match(cmux.Any())
|
||||
go s.Serve(tcp)
|
||||
go muxer.Serve()
|
||||
} else {
|
||||
e, s := server.NewAndInit()
|
||||
switch {
|
||||
case conf.Conf.Server.CertPath != "" && conf.Conf.Server.KeyPath != "":
|
||||
go http.ServeTLS(serverListener, e.Handler(), conf.Conf.Server.CertPath, conf.Conf.Server.KeyPath)
|
||||
if conf.Conf.Server.Quic {
|
||||
go http3.ListenAndServeQUIC(udpServerAddr.String(), conf.Conf.Server.CertPath, conf.Conf.Server.KeyPath, e.Handler())
|
||||
}
|
||||
case conf.Conf.Server.CertPath == "" && conf.Conf.Server.KeyPath == "":
|
||||
go e.RunListener(serverListener)
|
||||
default:
|
||||
log.Panic("cert and key must be both set")
|
||||
}
|
||||
rtmpListener, err := net.ListenTCP("tcp", tcpRtmpAddr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
go s.Serve(rtmpListener)
|
||||
}
|
||||
} else {
|
||||
e, _ := server.NewAndInit()
|
||||
switch {
|
||||
case conf.Conf.Server.CertPath != "" && conf.Conf.Server.KeyPath != "":
|
||||
go http.ServeTLS(serverListener, e.Handler(), conf.Conf.Server.CertPath, conf.Conf.Server.KeyPath)
|
||||
if conf.Conf.Server.Quic {
|
||||
go http3.ListenAndServeQUIC(udpServerAddr.String(), conf.Conf.Server.CertPath, conf.Conf.Server.KeyPath, e.Handler())
|
||||
}
|
||||
case conf.Conf.Server.CertPath == "" && conf.Conf.Server.KeyPath == "":
|
||||
go e.RunListener(serverListener)
|
||||
default:
|
||||
log.Panic("cert and key must be both set")
|
||||
}
|
||||
}
|
||||
if conf.Conf.Rtmp.Enable {
|
||||
log.Infof("rtmp run on tcp://%s:%d", tcpServerAddr.IP, tcpRtmpAddr.Port)
|
||||
}
|
||||
if conf.Conf.Server.Quic && conf.Conf.Server.CertPath != "" && conf.Conf.Server.KeyPath != "" {
|
||||
log.Infof("quic run on udp://%s:%d", udpServerAddr.IP, udpServerAddr.Port)
|
||||
}
|
||||
log.Infof("website run on http://%s:%d", tcpServerAddr.IP, tcpServerAddr.Port)
|
||||
bootstrap.WaitCbk()
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(ServerCmd)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var VersionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version number of Sync TV Server",
|
||||
Long: `All software has versions. This is Sync TV Server's`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("synctv-server v0.1 -- HEAD")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(VersionCmd)
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
module github.com/synctv-org/synctv
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/caarlos0/env/v9 v9.0.0
|
||||
github.com/gin-contrib/cors v1.4.0
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-resty/resty/v2 v2.8.0
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/maruel/natural v1.1.0
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible
|
||||
github.com/quic-go/quic-go v0.39.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/soheilhy/cmux v0.1.5
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/zijiren233/gencontainer v0.0.0-20230930061950-82324a07eacb
|
||||
github.com/zijiren233/go-colorable v0.0.0-20230930131441-997304c961cb
|
||||
github.com/zijiren233/ksync v0.2.0
|
||||
github.com/zijiren233/livelib v0.0.0-20230930062256-1d07bbddcefb
|
||||
github.com/zijiren233/stream v0.5.1
|
||||
github.com/zijiren233/yaml-comment v0.2.0
|
||||
golang.org/x/crypto v0.13.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/bytedance/sonic v1.10.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.15.4 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.12.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.3.4 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
go.uber.org/mock v0.3.0 // indirect
|
||||
golang.org/x/arch v0.5.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.15.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
)
|
|
@ -0,0 +1,235 @@
|
|||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc=
|
||||
github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc=
|
||||
github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
|
||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
|
||||
github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs=
|
||||
github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-resty/resty/v2 v2.8.0 h1:J29d0JFWwSWrDCysnOK/YjsPMLQTx0TvgJEHVGvf2L8=
|
||||
github.com/go-resty/resty/v2 v2.8.0/go.mod h1:UCui0cMHekLrSntoMyofdSTaPpinlRHFtPpizuyDW2w=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 h1:pUa4ghanp6q4IJHwE9RwLgmVFfReJN+KbQ8ExNEUUoQ=
|
||||
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/maruel/natural v1.1.0 h1:2z1NgP/Vae+gYrtC0VuvrTJ6U35OuyUqDdfluLqMWuQ=
|
||||
github.com/maruel/natural v1.1.0/go.mod h1:eFVhYCcUOfZFxXoDZam8Ktya72wa79fNC3lc/leA0DQ=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
|
||||
github.com/onsi/ginkgo/v2 v2.12.1 h1:uHNEO1RP2SpuZApSkel9nEh1/Mu+hmQe7Q+Pepg5OYA=
|
||||
github.com/onsi/ginkgo/v2 v2.12.1/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
|
||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/quic-go/quic-go v0.39.0 h1:AgP40iThFMY0bj8jGxROhw3S0FMGa8ryqsmi9tBH3So=
|
||||
github.com/quic-go/quic-go v0.39.0/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
|
||||
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zijiren233/gencontainer v0.0.0-20230930061950-82324a07eacb h1:sOVY4BfQo/uzoACkFTvfCJd0mHhhPQjcwgKTICOW+Js=
|
||||
github.com/zijiren233/gencontainer v0.0.0-20230930061950-82324a07eacb/go.mod h1:V5oL7PrZxgisuLCblFWd89Jg99O8vM1n58llcxZ2hDY=
|
||||
github.com/zijiren233/go-colorable v0.0.0-20230930131441-997304c961cb h1:0DyOxf/TbbGodHhOVHNoPk+7v/YBJACs22gKpKlatWw=
|
||||
github.com/zijiren233/go-colorable v0.0.0-20230930131441-997304c961cb/go.mod h1:6TCzjDiQ8+5gWZiwsC3pnA5M0vUy2jV2Y7ciHJh729g=
|
||||
github.com/zijiren233/ksync v0.2.0 h1:OyXVXbVQYFEVfWM13NApt4LMHbLQ3HTs4oYcLmqL6NE=
|
||||
github.com/zijiren233/ksync v0.2.0/go.mod h1:YNvvoErcbtF86Xn18J8QJ14jKOXinxFVOzyp4hn8FKw=
|
||||
github.com/zijiren233/livelib v0.0.0-20230930062256-1d07bbddcefb h1:t6zLooW4uVz2gKWmbZN4ljXKJpbiLnQF9j1kTMfV8OM=
|
||||
github.com/zijiren233/livelib v0.0.0-20230930062256-1d07bbddcefb/go.mod h1:C4OwAodQXpMLYiioqH4ANjZJD4+sFRK0nPbXNoYZU5k=
|
||||
github.com/zijiren233/stream v0.5.1 h1:9SUwM/fpET6frtBRT5WZBHnan0Hyzkezk/P8N78cgZQ=
|
||||
github.com/zijiren233/stream v0.5.1/go.mod h1:iIrOm3qgIepQFmptD/HDY+YzamSSzQOtPjpVcK7FCOw=
|
||||
github.com/zijiren233/yaml-comment v0.2.0 h1:xGcmpFsjK+IIK1InHtl+rKxYVKQ9rne/aRP1gkczZt4=
|
||||
github.com/zijiren233/yaml-comment v0.2.0/go.mod h1:jc/3jBkvi9BHafiFUoLKGL9U/X5hHnIpiNIT9QV/994=
|
||||
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
|
||||
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
|
||||
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
|
@ -0,0 +1,93 @@
|
|||
package bootstrap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/caarlos0/env/v9"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/synctv-org/synctv/cmd/flags"
|
||||
"github.com/synctv-org/synctv/internal/conf"
|
||||
"github.com/synctv-org/synctv/utils"
|
||||
)
|
||||
|
||||
func InitConfig() {
|
||||
if flags.SkipConfig && flags.SkipEnv {
|
||||
log.Fatal("skip config and skip env at the same time")
|
||||
return
|
||||
}
|
||||
conf.Conf = conf.DefaultConfig()
|
||||
if !flags.SkipConfig {
|
||||
if flags.ConfigFile == "" {
|
||||
homeDir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
log.Fatalf("find home dir error: %v", err)
|
||||
}
|
||||
flags.ConfigFile = filepath.Join(homeDir, ".config", "synctv", "config.yaml")
|
||||
} else {
|
||||
fileAbs, err := filepath.Abs(flags.ConfigFile)
|
||||
if err != nil {
|
||||
log.Fatalf("get config file abs path error: %v", err)
|
||||
}
|
||||
flags.ConfigFile = fileAbs
|
||||
}
|
||||
err := confFromConfig(flags.ConfigFile, conf.Conf)
|
||||
if err != nil {
|
||||
log.Fatalf("load config from file error: %v", err)
|
||||
}
|
||||
log.Infof("load config success from file: %s", flags.ConfigFile)
|
||||
if err = restoreConfig(flags.ConfigFile, conf.Conf); err != nil {
|
||||
log.Warnf("restore config error: %v", err)
|
||||
} else {
|
||||
log.Info("restore config success")
|
||||
}
|
||||
}
|
||||
if !flags.SkipEnv {
|
||||
prefix := "SYNCTV_"
|
||||
if flags.EnvNoPrefix {
|
||||
prefix = ""
|
||||
log.Info("load config from env without prefix")
|
||||
} else {
|
||||
log.Infof("load config from env with prefix: %s", prefix)
|
||||
}
|
||||
err := confFromEnv(prefix, conf.Conf)
|
||||
if err != nil {
|
||||
log.Fatalf("load config from env error: %v", err)
|
||||
}
|
||||
log.Info("load config success from env")
|
||||
}
|
||||
}
|
||||
|
||||
func confFromConfig(filePath string, conf *conf.Config) error {
|
||||
if filePath == "" {
|
||||
return errors.New("config file path is empty")
|
||||
}
|
||||
if !utils.Exists(filePath) {
|
||||
log.Infof("config file not exists, create new config file: %s", filePath)
|
||||
err := conf.Save(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err := utils.ReadYaml(filePath, conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func restoreConfig(filePath string, conf *conf.Config) error {
|
||||
if filePath == "" {
|
||||
return errors.New("config file path is empty")
|
||||
}
|
||||
return conf.Save(filePath)
|
||||
}
|
||||
|
||||
func confFromEnv(prefix string, conf *conf.Config) error {
|
||||
return env.ParseWithOptions(conf, env.Options{
|
||||
Prefix: prefix,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package bootstrap
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/natefinch/lumberjack"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/synctv-org/synctv/cmd/flags"
|
||||
"github.com/synctv-org/synctv/internal/conf"
|
||||
"github.com/zijiren233/go-colorable"
|
||||
)
|
||||
|
||||
func setLog(l *logrus.Logger) {
|
||||
if flags.Dev {
|
||||
l.SetLevel(logrus.DebugLevel)
|
||||
l.SetReportCaller(true)
|
||||
} else {
|
||||
l.SetLevel(logrus.InfoLevel)
|
||||
l.SetReportCaller(false)
|
||||
}
|
||||
}
|
||||
|
||||
func InitLog() {
|
||||
setLog(logrus.StandardLogger())
|
||||
logConfig := conf.Conf.Log
|
||||
if logConfig.Enable {
|
||||
var l = &lumberjack.Logger{
|
||||
Filename: logConfig.FilePath,
|
||||
MaxSize: logConfig.MaxSize,
|
||||
MaxBackups: logConfig.MaxBackups,
|
||||
MaxAge: logConfig.MaxAge,
|
||||
Compress: logConfig.Compress,
|
||||
}
|
||||
if err := l.Rotate(); err != nil {
|
||||
logrus.Fatalf("log: rotate log file error: %v", err)
|
||||
}
|
||||
var w io.Writer = colorable.NewNonColorableWriter(l)
|
||||
if flags.Dev || flags.LogStd {
|
||||
w = io.MultiWriter(os.Stdout, w)
|
||||
} else {
|
||||
logrus.Infof("log: disable log to stdout, only log to file: %s", logConfig.FilePath)
|
||||
}
|
||||
logrus.SetOutput(w)
|
||||
}
|
||||
switch conf.Conf.Log.LogFormat {
|
||||
case "json":
|
||||
logrus.SetFormatter(&logrus.JSONFormatter{})
|
||||
default:
|
||||
if conf.Conf.Log.LogFormat != "text" {
|
||||
logrus.Warnf("unknown log format: %s, use default: text", conf.Conf.Log.LogFormat)
|
||||
}
|
||||
if colorable.IsTerminal(os.Stdout.Fd()) {
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
ForceColors: true,
|
||||
})
|
||||
} else {
|
||||
logrus.SetFormatter(&logrus.TextFormatter{})
|
||||
}
|
||||
}
|
||||
log.SetOutput(logrus.StandardLogger().Out)
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package bootstrap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/zijiren233/gencontainer/pqueue"
|
||||
)
|
||||
|
||||
var (
|
||||
c chan os.Signal
|
||||
notifyTaskLock sync.Mutex
|
||||
notifyTaskQueue = pqueue.NewMaxPriorityQueue[*SysNotifyTask]()
|
||||
WaitCbk func()
|
||||
)
|
||||
|
||||
type SysNotifyTask struct {
|
||||
Task func() error
|
||||
Name string
|
||||
}
|
||||
|
||||
func NewSysNotifyTask(name string, task func() error) *SysNotifyTask {
|
||||
return &SysNotifyTask{
|
||||
Name: name,
|
||||
Task: task,
|
||||
}
|
||||
}
|
||||
|
||||
func InitSysNotify() {
|
||||
c = make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGHUP /*1*/, syscall.SIGINT /*2*/, syscall.SIGQUIT /*3*/, syscall.SIGTERM /*15*/)
|
||||
WaitCbk = sync.OnceFunc(waitCbk)
|
||||
}
|
||||
|
||||
func waitCbk() {
|
||||
log.Info("wait sys notify")
|
||||
log.Infof("receive sys notify: %v", <-c)
|
||||
notifyTaskLock.Lock()
|
||||
defer notifyTaskLock.Unlock()
|
||||
log.Infof("task: running...")
|
||||
for notifyTaskQueue.Len() > 0 {
|
||||
_, task := notifyTaskQueue.Pop()
|
||||
func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Errorf("task: %s panic has returned: %v", task.Name, err)
|
||||
}
|
||||
}()
|
||||
log.Infof("task: %s running", task.Name)
|
||||
if err := task.Task(); err != nil {
|
||||
log.Errorf("task: %s an error occurred: %v", task.Name, err)
|
||||
}
|
||||
log.Infof("task: %s done", task.Name)
|
||||
}()
|
||||
}
|
||||
log.Info("task: all done")
|
||||
}
|
||||
|
||||
func RegisterSysNotifyTask(priority int, task *SysNotifyTask) error {
|
||||
if task == nil || task.Task == nil {
|
||||
return errors.New("task is nil")
|
||||
}
|
||||
notifyTaskLock.Lock()
|
||||
defer notifyTaskLock.Unlock()
|
||||
notifyTaskQueue.Push(priority, task)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"github.com/synctv-org/synctv/utils"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// Global
|
||||
Global GlobalConfig `yaml:"global"`
|
||||
|
||||
// Log
|
||||
Log LogConfig `yaml:"log"`
|
||||
|
||||
// Server
|
||||
Server ServerConfig `yaml:"server"`
|
||||
|
||||
// Jwt
|
||||
Jwt JwtConfig `yaml:"jwt"`
|
||||
|
||||
// Rtmp
|
||||
Rtmp RtmpConfig `yaml:"rtmp" hc:"you can use rtmp to publish live"`
|
||||
|
||||
// Proxy
|
||||
Proxy ProxyConfig `yaml:"proxy" hc:"you can use proxy to proxy movie and live when custom headers or network is slow to connect to origin server"`
|
||||
}
|
||||
|
||||
func (c *Config) Save(file string) error {
|
||||
return utils.WriteYaml(file, c)
|
||||
}
|
||||
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
// Global
|
||||
Global: DefaultGlobalConfig(),
|
||||
|
||||
// Log
|
||||
Log: DefaultLogConfig(),
|
||||
|
||||
// Server
|
||||
Server: DefaultServerConfig(),
|
||||
|
||||
// Jwt
|
||||
Jwt: DefaultJwtConfig(),
|
||||
|
||||
// Rtmp
|
||||
Rtmp: DefaultRtmpConfig(),
|
||||
|
||||
// Proxy
|
||||
Proxy: DefaultProxyConfig(),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package conf
|
||||
|
||||
type GlobalConfig struct {
|
||||
}
|
||||
|
||||
func DefaultGlobalConfig() GlobalConfig {
|
||||
return GlobalConfig{}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"github.com/synctv-org/synctv/utils"
|
||||
)
|
||||
|
||||
type JwtConfig struct {
|
||||
Secret string `yaml:"secret" lc:"jwt secret (default rand string)" env:"JWT_SECRET"`
|
||||
Expire int `yaml:"expire" lc:"expire time (default: 12 hour)" env:"JWT_EXPIRE"`
|
||||
}
|
||||
|
||||
func DefaultJwtConfig() JwtConfig {
|
||||
return JwtConfig{
|
||||
Secret: utils.RandString(32),
|
||||
Expire: 12,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package conf
|
||||
|
||||
type LogConfig struct {
|
||||
Enable bool `yaml:"enable" lc:"enable log to file (default: true)" env:"LOG_ENABLE"`
|
||||
LogFormat string `yaml:"log_format" lc:"log format, can be set: text | json (default: text)" env:"LOG_FORMAT"`
|
||||
FilePath string `yaml:"file_path" lc:"log file path (default: log/log.log)" env:"LOG_FILE_PATH"`
|
||||
MaxSize int `yaml:"max_size" lc:"max size per log file (default: 10 megabytes)" env:"LOG_MAX_SIZE"`
|
||||
MaxBackups int `yaml:"max_backups" lc:"max backups (default: 10)" env:"LOG_MAX_BACKUPS"`
|
||||
MaxAge int `yaml:"max_age" lc:"max age (default: 28 days)" env:"LOG_MAX_AGE"`
|
||||
Compress bool `yaml:"compress" lc:"compress (default: false)" env:"LOG_COMPRESS"`
|
||||
}
|
||||
|
||||
func DefaultLogConfig() LogConfig {
|
||||
return LogConfig{
|
||||
Enable: true,
|
||||
LogFormat: "text",
|
||||
FilePath: "log/log.log",
|
||||
MaxSize: 10,
|
||||
MaxBackups: 10,
|
||||
MaxAge: 28,
|
||||
Compress: false,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package conf
|
||||
|
||||
type ProxyConfig struct {
|
||||
MovieProxy bool `yaml:"movie_proxy" lc:"enable movie proxy (default: true)" env:"PROXY_MOVIE_PROXY"`
|
||||
LiveProxy bool `yaml:"live_proxy" lc:"enable live proxy (default: true)" env:"PROXY_LIVE_PROXY"`
|
||||
}
|
||||
|
||||
func DefaultProxyConfig() ProxyConfig {
|
||||
return ProxyConfig{
|
||||
MovieProxy: true,
|
||||
LiveProxy: true,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package conf
|
||||
|
||||
type RtmpConfig struct {
|
||||
Enable bool `yaml:"enable" lc:"enable rtmp server (default: true)" env:"RTMP_ENABLE"`
|
||||
Port uint16 `yaml:"port" lc:"rtmp server port (default use server port)" env:"RTMP_PORT"`
|
||||
|
||||
CustomPublishHost string `yaml:"custom_publish_host" lc:"publish host (default use http header host)" env:"RTMP_CUSTOM_PUBLISH_HOST"`
|
||||
RtmpPlayer bool `yaml:"rtmp_player" lc:"enable rtmp player (default: false)" env:"RTMP_PLAYER"`
|
||||
HlsPlayer bool `yaml:"hls_player" lc:"enable hls player (default: false)" env:"HLS_PLAYER"`
|
||||
}
|
||||
|
||||
func DefaultRtmpConfig() RtmpConfig {
|
||||
return RtmpConfig{
|
||||
Enable: true,
|
||||
Port: 0,
|
||||
CustomPublishHost: "",
|
||||
RtmpPlayer: false,
|
||||
HlsPlayer: false,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package conf
|
||||
|
||||
type ServerConfig struct {
|
||||
Listen string `yaml:"listen" lc:"server listen addr" env:"SERVER_LISTEN"`
|
||||
Port uint16 `yaml:"port" lc:"server listen port" env:"SERVER_PORT"`
|
||||
Quic bool `yaml:"quic" lc:"enable http3/quic, need enable ssl, set cert and key file (default: true)" env:"SERVER_QUIC"`
|
||||
|
||||
CertPath string `yaml:"cert_path" lc:"cert path" env:"SERVER_CERT_PATH"`
|
||||
KeyPath string `yaml:"key_path" lc:"key path" env:"SERVER_KEY_PATH"`
|
||||
}
|
||||
|
||||
func DefaultServerConfig() ServerConfig {
|
||||
return ServerConfig{
|
||||
Listen: "127.0.0.1",
|
||||
Port: 8080,
|
||||
Quic: true,
|
||||
CertPath: "",
|
||||
KeyPath: "",
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package conf
|
||||
|
||||
var (
|
||||
Conf *Config
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "github.com/synctv-org/synctv/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package public
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:embed dist/*
|
||||
var dist embed.FS
|
||||
|
||||
var Public, _ = fs.Sub(dist, "dist")
|
|
@ -0,0 +1,90 @@
|
|||
package room
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
user *User
|
||||
c chan Message
|
||||
wg sync.WaitGroup
|
||||
conn *websocket.Conn
|
||||
timeOut time.Duration
|
||||
closed uint64
|
||||
}
|
||||
|
||||
func NewClient(user *User, conn *websocket.Conn) (*Client, error) {
|
||||
if user == nil {
|
||||
return nil, errors.New("user is nil")
|
||||
}
|
||||
if conn == nil {
|
||||
return nil, errors.New("conn is nil")
|
||||
}
|
||||
return &Client{
|
||||
user: user,
|
||||
c: make(chan Message, 128),
|
||||
conn: conn,
|
||||
timeOut: 10 * time.Second,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) User() *User {
|
||||
return c.user
|
||||
}
|
||||
|
||||
func (c *Client) Username() string {
|
||||
return c.user.name
|
||||
}
|
||||
|
||||
func (c *Client) Room() *Room {
|
||||
return c.user.room
|
||||
}
|
||||
|
||||
func (c *Client) Broadcast(msg Message, conf ...BroadcastConf) {
|
||||
c.user.Broadcast(msg, conf...)
|
||||
}
|
||||
|
||||
func (c *Client) Send(msg Message) error {
|
||||
c.wg.Add(1)
|
||||
defer c.wg.Done()
|
||||
if c.Closed() {
|
||||
return ErrAlreadyClosed
|
||||
}
|
||||
c.c <- msg
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Unregister() error {
|
||||
return c.user.room.UnRegClient(c.user)
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
if !atomic.CompareAndSwapUint64(&c.closed, 0, 1) {
|
||||
return ErrAlreadyClosed
|
||||
}
|
||||
c.wg.Wait()
|
||||
close(c.c)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Closed() bool {
|
||||
return atomic.LoadUint64(&c.closed) == 1
|
||||
}
|
||||
|
||||
func (c *Client) GetReadChan() <-chan Message {
|
||||
return c.c
|
||||
}
|
||||
|
||||
func (c *Client) NextWriter(messageType int) (io.WriteCloser, error) {
|
||||
return c.conn.NextWriter(messageType)
|
||||
}
|
||||
|
||||
func (c *Client) NextReader() (int, io.Reader, error) {
|
||||
return c.conn.NextReader()
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package room
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
json "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
type Current struct {
|
||||
movie MovieInfo
|
||||
status Status
|
||||
lock *sync.RWMutex
|
||||
}
|
||||
|
||||
func newCurrent() *Current {
|
||||
return &Current{
|
||||
movie: MovieInfo{},
|
||||
status: newStatus(),
|
||||
lock: new(sync.RWMutex),
|
||||
}
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
Seek float64 `json:"seek"`
|
||||
Rate float64 `json:"rate"`
|
||||
Playing bool `json:"playing"`
|
||||
lastUpdateTime time.Time
|
||||
}
|
||||
|
||||
func newStatus() Status {
|
||||
return Status{
|
||||
Seek: 0,
|
||||
Rate: 1.0,
|
||||
lastUpdateTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Current) MarshalJSON() ([]byte, error) {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
c.updateSeek()
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"movie": c.movie,
|
||||
"status": c.status,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Current) Movie() MovieInfo {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
return c.movie
|
||||
}
|
||||
|
||||
func (c *Current) SetMovie(movie MovieInfo) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.movie = movie
|
||||
c.status.Seek = 0
|
||||
}
|
||||
|
||||
func (c *Current) Status() Status {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
c.updateSeek()
|
||||
return c.status
|
||||
}
|
||||
|
||||
func (c *Current) updateSeek() {
|
||||
if c.status.Playing {
|
||||
c.status.Seek += time.Since(c.status.lastUpdateTime).Seconds() * c.status.Rate
|
||||
}
|
||||
c.status.lastUpdateTime = time.Now()
|
||||
}
|
||||
|
||||
func (c *Current) SetStatus(playing bool, seek, rate, timeDiff float64) Status {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.status.Playing = playing
|
||||
c.status.Rate = rate
|
||||
if playing {
|
||||
c.status.Seek = seek + (timeDiff * rate)
|
||||
} else {
|
||||
c.status.Seek = seek
|
||||
}
|
||||
c.status.lastUpdateTime = time.Now()
|
||||
return c.status
|
||||
}
|
||||
|
||||
func (c *Current) SetSeekRate(seek, rate, timeDiff float64) Status {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if c.status.Playing {
|
||||
c.status.Seek = seek + (timeDiff * rate)
|
||||
} else {
|
||||
c.status.Seek = seek
|
||||
}
|
||||
c.status.Rate = rate
|
||||
c.status.lastUpdateTime = time.Now()
|
||||
return c.status
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
package room
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/synctv-org/synctv/cmd/flags"
|
||||
"github.com/synctv-org/synctv/utils"
|
||||
"github.com/zijiren233/gencontainer/rwmap"
|
||||
)
|
||||
|
||||
type hub struct {
|
||||
id string
|
||||
clientNum int64
|
||||
clients *rwmap.RWMap[string, *Client]
|
||||
broadcast chan *broadcastMessage
|
||||
exit chan struct{}
|
||||
closed uint32
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
type broadcastMessage struct {
|
||||
data Message
|
||||
sender string
|
||||
sendToSelf bool
|
||||
ignoreID []string
|
||||
}
|
||||
|
||||
type BroadcastConf func(*broadcastMessage)
|
||||
|
||||
func WithSender(sender string) BroadcastConf {
|
||||
return func(bm *broadcastMessage) {
|
||||
bm.sender = sender
|
||||
}
|
||||
}
|
||||
|
||||
func WithSendToSelf() BroadcastConf {
|
||||
return func(bm *broadcastMessage) {
|
||||
bm.sendToSelf = true
|
||||
}
|
||||
}
|
||||
|
||||
func WithIgnoreID(id ...string) BroadcastConf {
|
||||
return func(bm *broadcastMessage) {
|
||||
bm.ignoreID = append(bm.ignoreID, id...)
|
||||
}
|
||||
}
|
||||
|
||||
func newHub(id string) *hub {
|
||||
return &hub{
|
||||
id: id,
|
||||
broadcast: make(chan *broadcastMessage, 128),
|
||||
clients: &rwmap.RWMap[string, *Client]{},
|
||||
exit: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hub) Closed() bool {
|
||||
return atomic.LoadUint32(&h.closed) == 1
|
||||
}
|
||||
|
||||
var (
|
||||
ErrAlreadyClosed = fmt.Errorf("already closed")
|
||||
)
|
||||
|
||||
func (h *hub) Start() {
|
||||
go h.Serve()
|
||||
}
|
||||
|
||||
func (h *hub) Serve() error {
|
||||
if h.Closed() {
|
||||
return ErrAlreadyClosed
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case message := <-h.broadcast:
|
||||
h.devMessage(message.data)
|
||||
h.clients.Range(func(_ string, cli *Client) bool {
|
||||
if !message.sendToSelf {
|
||||
if cli.user.name == message.sender {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if utils.In(message.ignoreID, cli.user.name) {
|
||||
return true
|
||||
}
|
||||
if err := cli.Send(message.data); err != nil {
|
||||
if flags.Dev {
|
||||
log.Errorf("hub: %s, write to client err: %s\nmessage: %+v", h.id, err, message)
|
||||
}
|
||||
cli.Close()
|
||||
}
|
||||
return true
|
||||
})
|
||||
case <-h.exit:
|
||||
log.Infof("hub: %s, closed", h.id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hub) devMessage(msg Message) {
|
||||
if flags.Dev {
|
||||
switch msg.MessageType() {
|
||||
case websocket.TextMessage:
|
||||
log.Infof("hub: %s, broadcast:\nmessage: %+v", h.id, msg.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hub) Close() error {
|
||||
if !atomic.CompareAndSwapUint32(&h.closed, 0, 1) {
|
||||
return ErrAlreadyClosed
|
||||
}
|
||||
close(h.exit)
|
||||
h.clients.Range(func(_ string, client *Client) bool {
|
||||
client.Close()
|
||||
return true
|
||||
})
|
||||
h.wg.Wait()
|
||||
close(h.broadcast)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *hub) Broadcast(data Message, conf ...BroadcastConf) error {
|
||||
h.wg.Add(1)
|
||||
defer h.wg.Done()
|
||||
if h.Closed() {
|
||||
return ErrAlreadyClosed
|
||||
}
|
||||
msg := &broadcastMessage{data: data}
|
||||
for _, c := range conf {
|
||||
c(msg)
|
||||
}
|
||||
select {
|
||||
case h.broadcast <- msg:
|
||||
return nil
|
||||
case <-h.exit:
|
||||
return ErrAlreadyClosed
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hub) RegClient(user *User, conn *websocket.Conn) (*Client, error) {
|
||||
if h.Closed() {
|
||||
return nil, ErrAlreadyClosed
|
||||
}
|
||||
cli, err := NewClient(user, conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, loaded := h.clients.LoadOrStore(user.name, cli)
|
||||
if loaded {
|
||||
return nil, errors.New("client already registered")
|
||||
}
|
||||
atomic.AddInt64(&h.clientNum, 1)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (h *hub) UnRegClient(user *User) error {
|
||||
if h.Closed() {
|
||||
return ErrAlreadyClosed
|
||||
}
|
||||
if user == nil {
|
||||
return errors.New("user is nil")
|
||||
}
|
||||
_, loaded := h.clients.LoadAndDelete(user.name)
|
||||
if !loaded {
|
||||
return errors.New("client not found")
|
||||
}
|
||||
atomic.AddInt64(&h.clientNum, -1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *hub) ClientNum() int64 {
|
||||
return atomic.LoadInt64(&h.clientNum)
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package room
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Message interface {
|
||||
MessageType() int
|
||||
String() string
|
||||
Encode(wc io.Writer) error
|
||||
}
|
||||
|
||||
type ElementMessageType int
|
||||
|
||||
const (
|
||||
Error ElementMessageType = iota + 1
|
||||
ChatMessage
|
||||
Play
|
||||
Pause
|
||||
CheckSeek
|
||||
TooFast
|
||||
TooSlow
|
||||
ChangeRate
|
||||
ChangeSeek
|
||||
ChangeCurrent
|
||||
ChangeMovieList
|
||||
ChangePeopleNum
|
||||
)
|
||||
|
||||
type ElementMessage struct {
|
||||
Type ElementMessageType `json:"type" yaml:"type"`
|
||||
Sender string `json:"sender,omitempty" yaml:"sender,omitempty"`
|
||||
Message string `json:"message,omitempty" yaml:"message,omitempty"`
|
||||
Seek float64 `json:"seek,omitempty" yaml:"seek,omitempty"`
|
||||
Rate float64 `json:"rate,omitempty" yaml:"rate,omitempty"`
|
||||
Current *Current `json:"current,omitempty" yaml:"current,omitempty"`
|
||||
PeopleNum int64 `json:"peopleNum,omitempty" yaml:"peopleNum,omitempty"`
|
||||
Time int64 `json:"time,omitempty" yaml:"time,omitempty"`
|
||||
}
|
||||
|
||||
func (em *ElementMessage) MessageType() int {
|
||||
return websocket.TextMessage
|
||||
}
|
||||
|
||||
func (em *ElementMessage) String() string {
|
||||
out, _ := yaml.Marshal(em)
|
||||
switch em.Type {
|
||||
case Error:
|
||||
return fmt.Sprintf("Element Error: %s", out)
|
||||
case ChatMessage:
|
||||
return fmt.Sprintf("Element ChatMessage: %s", out)
|
||||
case Play:
|
||||
return fmt.Sprintf("Element Play: %s", out)
|
||||
case Pause:
|
||||
return fmt.Sprintf("Element Pause: %s", out)
|
||||
case CheckSeek:
|
||||
return fmt.Sprintf("Element CheckSeek: %s", out)
|
||||
case TooFast:
|
||||
return fmt.Sprintf("Element TooFast: %s", out)
|
||||
case TooSlow:
|
||||
return fmt.Sprintf("Element TooSlow: %s", out)
|
||||
case ChangeRate:
|
||||
return fmt.Sprintf("Element ChangeRate: %s", out)
|
||||
case ChangeSeek:
|
||||
return fmt.Sprintf("Element ChangeSeek: %s", out)
|
||||
case ChangeCurrent:
|
||||
return fmt.Sprintf("Element ChangeCurrent: %s", out)
|
||||
case ChangeMovieList:
|
||||
return fmt.Sprintf("Element ChangeMovieList: %s", out)
|
||||
case ChangePeopleNum:
|
||||
return fmt.Sprintf("Element ChangePeopleNum: %s", out)
|
||||
default:
|
||||
return fmt.Sprintf("Element Unknown: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func (em *ElementMessage) Encode(wc io.Writer) error {
|
||||
return json.NewEncoder(wc).Encode(em)
|
||||
}
|
||||
|
||||
type PingMessage struct{}
|
||||
|
||||
func (pm *PingMessage) MessageType() int {
|
||||
return websocket.PingMessage
|
||||
}
|
||||
|
||||
func (pm *PingMessage) String() string {
|
||||
return "Ping"
|
||||
}
|
||||
|
||||
func (pm *PingMessage) Encode(wc io.Writer) error {
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,347 @@
|
|||
package room
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/zijiren233/gencontainer/dllist"
|
||||
rtmps "github.com/zijiren233/livelib/server"
|
||||
)
|
||||
|
||||
type FormatErrMovieNotFound uint64
|
||||
|
||||
func (e FormatErrMovieNotFound) Error() string {
|
||||
return fmt.Sprintf("movie id: %v not found", uint64(e))
|
||||
}
|
||||
|
||||
type FormatErrMovieAlreadyExist uint64
|
||||
|
||||
func (e FormatErrMovieAlreadyExist) Error() string {
|
||||
return fmt.Sprintf("movie id: %v already exist", uint64(e))
|
||||
}
|
||||
|
||||
type movies struct {
|
||||
l *dllist.Dllist[*Movie]
|
||||
lock *sync.RWMutex
|
||||
}
|
||||
|
||||
// Url will be `PullKey` when Live and Proxy are true
|
||||
type Movie struct {
|
||||
BaseMovie
|
||||
PullKey string `json:"pullKey"`
|
||||
CreateAt int64 `json:"createAt"`
|
||||
LastEditAt int64 `json:"lastEditAt"`
|
||||
|
||||
id uint64
|
||||
channel *rtmps.Channel
|
||||
creator *User
|
||||
}
|
||||
|
||||
type BaseMovie struct {
|
||||
Url string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
Live bool `json:"live"`
|
||||
Proxy bool `json:"proxy"`
|
||||
RtmpSource bool `json:"rtmpSource"`
|
||||
Type string `json:"type"`
|
||||
Headers map[string][]string `json:"headers"`
|
||||
}
|
||||
|
||||
type MovieConf func(m *Movie)
|
||||
|
||||
func WithPullKey(PullKey string) MovieConf {
|
||||
return func(m *Movie) {
|
||||
m.PullKey = PullKey
|
||||
}
|
||||
}
|
||||
|
||||
func WithChannel(channel *rtmps.Channel) MovieConf {
|
||||
return func(m *Movie) {
|
||||
m.channel = channel
|
||||
}
|
||||
}
|
||||
|
||||
func WithCreator(creator *User) MovieConf {
|
||||
return func(m *Movie) {
|
||||
m.creator = creator
|
||||
}
|
||||
}
|
||||
|
||||
func NewMovie(id uint64, url, name, type_ string, live, proxy, rtmpSource bool, headers map[string][]string, conf ...MovieConf) (*Movie, error) {
|
||||
return NewMovieWithBaseMovie(id, BaseMovie{
|
||||
Url: url,
|
||||
Name: name,
|
||||
Live: live,
|
||||
Proxy: proxy,
|
||||
RtmpSource: rtmpSource,
|
||||
Type: type_,
|
||||
Headers: headers,
|
||||
})
|
||||
}
|
||||
|
||||
func NewMovieWithBaseMovie(id uint64, baseMovie BaseMovie, conf ...MovieConf) (*Movie, error) {
|
||||
now := time.Now().UnixMicro()
|
||||
m := &Movie{
|
||||
id: id,
|
||||
BaseMovie: baseMovie,
|
||||
CreateAt: now,
|
||||
LastEditAt: now,
|
||||
}
|
||||
m.Init(conf...)
|
||||
return m, m.Check()
|
||||
}
|
||||
|
||||
func (m *Movie) Check() error {
|
||||
_, err := url.Parse(m.Url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Movie) Id() uint64 {
|
||||
return m.id
|
||||
}
|
||||
|
||||
func (m *Movie) Init(conf ...MovieConf) {
|
||||
for _, c := range conf {
|
||||
c(m)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Movie) Creator() *User {
|
||||
return m.creator
|
||||
}
|
||||
|
||||
func (m *Movie) SetCreator(creator *User) {
|
||||
m.creator = creator
|
||||
}
|
||||
|
||||
func (m *Movie) Channel() *rtmps.Channel {
|
||||
return m.channel
|
||||
}
|
||||
|
||||
func (m *Movie) SetChannel(channel *rtmps.Channel) {
|
||||
m.channel = channel
|
||||
}
|
||||
|
||||
func newMovies() *movies {
|
||||
return &movies{l: dllist.New[*Movie](), lock: &sync.RWMutex{}}
|
||||
}
|
||||
|
||||
func (m *movies) Range(f func(e *dllist.Element[*Movie]) bool) (interrupt bool) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
interrupt = false
|
||||
for e := m.l.Front(); !interrupt && e != nil; e = e.Next() {
|
||||
interrupt = !f(e)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *movies) range_(f func(e *dllist.Element[*Movie]) bool) (interrupt bool) {
|
||||
interrupt = false
|
||||
for e := m.l.Front(); !interrupt && e != nil; e = e.Next() {
|
||||
interrupt = !f(e)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type MovieInfo struct {
|
||||
Id uint64 `json:"id"`
|
||||
Url string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
Live bool `json:"live"`
|
||||
Proxy bool `json:"proxy"`
|
||||
RtmpSource bool `json:"rtmpSource"`
|
||||
Type string `json:"type"`
|
||||
Headers map[string][]string `json:"headers"`
|
||||
PullKey string `json:"pullKey"`
|
||||
CreateAt int64 `json:"createAt"`
|
||||
LastEditAt int64 `json:"lastEditAt"`
|
||||
Creator string `json:"creator"`
|
||||
}
|
||||
|
||||
func (m *movies) MovieList() (movies []MovieInfo) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
movies = make([]MovieInfo, 0, m.l.Len())
|
||||
m.range_(func(e *dllist.Element[*Movie]) bool {
|
||||
movies = append(movies, MovieInfo{
|
||||
Id: e.Value.id,
|
||||
Url: e.Value.Url,
|
||||
Name: e.Value.Name,
|
||||
Live: e.Value.Live,
|
||||
Proxy: e.Value.Proxy,
|
||||
RtmpSource: e.Value.RtmpSource,
|
||||
Type: e.Value.Type,
|
||||
Headers: e.Value.Headers,
|
||||
CreateAt: e.Value.CreateAt,
|
||||
LastEditAt: e.Value.LastEditAt,
|
||||
Creator: e.Value.Creator().Name(),
|
||||
})
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (m *movies) Clear() error {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
m.l.Clear()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *movies) GetAndClear() (movies []*Movie) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
movies = make([]*Movie, 0, m.l.Len())
|
||||
m.range_(func(e *dllist.Element[*Movie]) bool {
|
||||
movies = append(movies, e.Value)
|
||||
return true
|
||||
})
|
||||
m.l.Clear()
|
||||
return
|
||||
}
|
||||
|
||||
func (m *movies) HasMovie(id uint64) bool {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
return m.hasMovie(id)
|
||||
}
|
||||
|
||||
func (m *movies) hasMovie(id uint64) bool {
|
||||
return m.range_(func(e *dllist.Element[*Movie]) bool {
|
||||
return e.Value.id != id
|
||||
})
|
||||
}
|
||||
|
||||
func (m *movies) GetMovie(id uint64) (movie *Movie, err error) {
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
e, err := m.getMovie(id)
|
||||
if err != nil {
|
||||
return nil, FormatErrMovieNotFound(id)
|
||||
}
|
||||
return e.Value, nil
|
||||
}
|
||||
|
||||
func (m *movies) GetMovieWithPullKey(pullKey string) (movie *Movie, err error) {
|
||||
if pullKey == "" {
|
||||
return nil, fmt.Errorf("pullKey is empty")
|
||||
}
|
||||
|
||||
m.lock.RLock()
|
||||
defer m.lock.RUnlock()
|
||||
|
||||
m.range_(func(e *dllist.Element[*Movie]) bool {
|
||||
if e.Value.PullKey == pullKey {
|
||||
movie = e.Value
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if movie == nil {
|
||||
return nil, fmt.Errorf("pullKey: %v not found", pullKey)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *movies) GetAndDelMovie(id ...uint64) (movies []*Movie, err error) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
es := make([]*dllist.Element[*Movie], 0, len(id))
|
||||
|
||||
for _, v := range id {
|
||||
e, err := m.getMovie(v)
|
||||
if err != nil {
|
||||
return nil, FormatErrMovieNotFound(v)
|
||||
}
|
||||
es = append(es, e)
|
||||
}
|
||||
|
||||
movies = make([]*Movie, 0, len(es))
|
||||
for _, e := range es {
|
||||
movies = append(movies, e.Value)
|
||||
e.Remove()
|
||||
}
|
||||
return movies, nil
|
||||
}
|
||||
|
||||
func (m *movies) getMovie(id uint64) (element *dllist.Element[*Movie], err error) {
|
||||
err = FormatErrMovieNotFound(id)
|
||||
m.range_(func(e *dllist.Element[*Movie]) bool {
|
||||
if e.Value.id == id {
|
||||
element = e
|
||||
err = nil
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (m *movies) PushBackMovie(movie *Movie) error {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
if m.hasMovie(movie.id) {
|
||||
return FormatErrMovieAlreadyExist(movie.id)
|
||||
}
|
||||
|
||||
m.l.PushBack(movie)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *movies) PushFrontMovie(movie *Movie) error {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
if m.hasMovie(movie.id) {
|
||||
return FormatErrMovieAlreadyExist(movie.id)
|
||||
}
|
||||
|
||||
m.l.PushFront(movie)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *movies) DelMovie(id uint64) error {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
e, err := m.getMovie(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.Remove()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *movies) SwapMovie(id1, id2 uint64) error {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
e1, err := m.getMovie(id1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e2, err := m.getMovie(id2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.l.Swap(e1, e2)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,412 @@
|
|||
package room
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/zijiren233/gencontainer/rwmap"
|
||||
rtmps "github.com/zijiren233/livelib/server"
|
||||
"github.com/zijiren233/stream"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRoomIDEmpty = errors.New("roomid is empty")
|
||||
ErrAdminPassWordEmpty = errors.New("admin password is empty")
|
||||
)
|
||||
|
||||
type Room struct {
|
||||
id string
|
||||
lock *sync.RWMutex
|
||||
password []byte
|
||||
needPassword bool
|
||||
version uint64
|
||||
current *Current
|
||||
maxInactivityTime time.Duration
|
||||
lastActive time.Time
|
||||
rtmps *rtmps.Server
|
||||
rtmpa *rtmps.App
|
||||
hidden bool
|
||||
timer *time.Timer
|
||||
inited bool
|
||||
users *rwmap.RWMap[string, *User]
|
||||
rootUser *User
|
||||
createAt time.Time
|
||||
mid uint64
|
||||
*movies
|
||||
*hub
|
||||
}
|
||||
|
||||
type RoomConf func(r *Room)
|
||||
|
||||
func WithVersion(version uint64) RoomConf {
|
||||
return func(r *Room) {
|
||||
r.version = version
|
||||
}
|
||||
}
|
||||
|
||||
func WithMaxInactivityTime(maxInactivityTime time.Duration) RoomConf {
|
||||
return func(r *Room) {
|
||||
r.maxInactivityTime = maxInactivityTime
|
||||
}
|
||||
}
|
||||
|
||||
func WithHidden(hidden bool) RoomConf {
|
||||
return func(r *Room) {
|
||||
r.hidden = hidden
|
||||
}
|
||||
}
|
||||
|
||||
func WithRootUser(u *User) RoomConf {
|
||||
return func(r *Room) {
|
||||
u.admin = true
|
||||
u.room = r
|
||||
r.rootUser = u
|
||||
r.AddUser(u)
|
||||
}
|
||||
}
|
||||
|
||||
// Version cant is 0
|
||||
func NewRoom(RoomID string, Password string, rtmps *rtmps.Server, conf ...RoomConf) (*Room, error) {
|
||||
if RoomID == "" {
|
||||
return nil, ErrRoomIDEmpty
|
||||
}
|
||||
|
||||
r := &Room{
|
||||
id: RoomID,
|
||||
lock: new(sync.RWMutex),
|
||||
movies: newMovies(),
|
||||
current: newCurrent(),
|
||||
maxInactivityTime: 12 * time.Hour,
|
||||
lastActive: time.Now(),
|
||||
hub: newHub(RoomID),
|
||||
rtmps: rtmps,
|
||||
users: &rwmap.RWMap[string, *User]{},
|
||||
createAt: time.Now(),
|
||||
}
|
||||
|
||||
for _, c := range conf {
|
||||
c(r)
|
||||
}
|
||||
|
||||
if r.version == 0 {
|
||||
r.version = rand.New(rand.NewSource(time.Now().UnixNano())).Uint64()
|
||||
}
|
||||
|
||||
return r, r.SetPassword(Password)
|
||||
}
|
||||
|
||||
func (r *Room) CreateAt() time.Time {
|
||||
return r.createAt
|
||||
}
|
||||
|
||||
func (r *Room) RootUser() *User {
|
||||
return r.rootUser
|
||||
}
|
||||
|
||||
func (r *Room) SetRootUser(u *User) {
|
||||
r.rootUser = u
|
||||
}
|
||||
|
||||
func (r *Room) NewUser(id string, password string, conf ...UserConf) (*User, error) {
|
||||
u, err := NewUser(id, password, r, conf...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, loaded := r.users.LoadOrStore(u.name, u)
|
||||
if loaded {
|
||||
return nil, errors.New("user already exist")
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (r *Room) AddUser(u *User) error {
|
||||
_, loaded := r.users.LoadOrStore(u.name, u)
|
||||
if loaded {
|
||||
return errors.New("user already exist")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Room) GetUser(id string) (*User, error) {
|
||||
u, ok := r.users.Load(id)
|
||||
if !ok {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (r *Room) DelUser(id string) error {
|
||||
_, ok := r.users.LoadAndDelete(id)
|
||||
if !ok {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Room) GetAndDelUser(id string) (u *User, ok bool) {
|
||||
return r.users.LoadAndDelete(id)
|
||||
}
|
||||
|
||||
func (r *Room) GetOrNewUser(id string, password string, conf ...UserConf) (*User, error) {
|
||||
u, err := NewUser(id, password, r, conf...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, _ := r.users.LoadOrStore(u.name, u)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r *Room) UserList() (users []User) {
|
||||
users = make([]User, 0, r.users.Len())
|
||||
r.users.Range(func(name string, u *User) bool {
|
||||
users = append(users, *u)
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Room) NewLiveChannel(channel string) (*rtmps.Channel, error) {
|
||||
c, err := r.rtmpa.NewChannel(channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (r *Room) Start() {
|
||||
go r.Serve()
|
||||
}
|
||||
|
||||
func (r *Room) Serve() {
|
||||
r.init()
|
||||
r.hub.Serve()
|
||||
}
|
||||
|
||||
func (r *Room) init() {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
if r.inited {
|
||||
return
|
||||
}
|
||||
r.inited = true
|
||||
if r.maxInactivityTime != 0 {
|
||||
r.timer = time.AfterFunc(time.Duration(r.maxInactivityTime), func() {
|
||||
r.Close()
|
||||
})
|
||||
}
|
||||
r.rtmpa = r.rtmps.GetOrNewApp(r.id)
|
||||
}
|
||||
|
||||
func (r *Room) Close() error {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
if err := r.hub.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
err := r.rtmps.DelApp(r.id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.timer != nil {
|
||||
r.timer.Stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Room) SetHidden(hidden bool) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
r.hidden = hidden
|
||||
}
|
||||
|
||||
func (r *Room) Hidden() bool {
|
||||
r.lock.RLock()
|
||||
defer r.lock.RUnlock()
|
||||
return r.hidden
|
||||
}
|
||||
|
||||
func (r *Room) ID() string {
|
||||
return r.id
|
||||
}
|
||||
|
||||
func (r *Room) UpdateActiveTime() {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
r.updateActiveTime()
|
||||
}
|
||||
|
||||
func (r *Room) updateActiveTime() {
|
||||
if r.maxInactivityTime != 0 {
|
||||
r.timer.Reset(r.maxInactivityTime)
|
||||
}
|
||||
r.lastActive = time.Now()
|
||||
}
|
||||
|
||||
func (r *Room) ResetMaxInactivityTime(maxInactivityTime time.Duration) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
r.maxInactivityTime = maxInactivityTime
|
||||
r.updateActiveTime()
|
||||
}
|
||||
|
||||
func (r *Room) LateActiveTime() time.Time {
|
||||
r.lock.RLock()
|
||||
defer r.lock.RUnlock()
|
||||
return r.lastActive
|
||||
}
|
||||
|
||||
func (r *Room) SetPassword(password string) error {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
if password != "" {
|
||||
b, err := bcrypt.GenerateFromPassword(stream.StringToBytes(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.password = b
|
||||
r.needPassword = true
|
||||
} else {
|
||||
r.needPassword = false
|
||||
}
|
||||
r.updateVersion()
|
||||
r.hub.clients.Range(func(_ string, value *Client) bool {
|
||||
value.Close()
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Room) CheckPassword(password string) (ok bool) {
|
||||
r.lock.RLock()
|
||||
defer r.lock.RUnlock()
|
||||
if !r.needPassword {
|
||||
return true
|
||||
}
|
||||
return bcrypt.CompareHashAndPassword(r.password, stream.StringToBytes(password)) == nil
|
||||
}
|
||||
|
||||
func (r *Room) NeedPassword() bool {
|
||||
r.lock.RLock()
|
||||
defer r.lock.RUnlock()
|
||||
return r.needPassword
|
||||
}
|
||||
|
||||
func (r *Room) Version() uint64 {
|
||||
return atomic.LoadUint64(&r.version)
|
||||
}
|
||||
|
||||
func (r *Room) CheckVersion(version uint64) bool {
|
||||
return r.Version() == version
|
||||
}
|
||||
|
||||
func (r *Room) SetVersion(version uint64) {
|
||||
atomic.StoreUint64(&r.version, version)
|
||||
}
|
||||
|
||||
func (r *Room) updateVersion() uint64 {
|
||||
return atomic.AddUint64(&r.version, 1)
|
||||
}
|
||||
|
||||
func (r *Room) Current() *Current {
|
||||
return r.current
|
||||
}
|
||||
|
||||
// Seek will be set to 0
|
||||
func (r *Room) ChangeCurrentMovie(id uint64) error {
|
||||
e, err := r.movies.getMovie(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.current.SetMovie(MovieInfo{
|
||||
Id: e.Value.id,
|
||||
Url: e.Value.Url,
|
||||
Name: e.Value.Name,
|
||||
Live: e.Value.Live,
|
||||
Proxy: e.Value.Proxy,
|
||||
RtmpSource: e.Value.RtmpSource,
|
||||
Type: e.Value.Type,
|
||||
Headers: e.Value.Headers,
|
||||
PullKey: e.Value.PullKey,
|
||||
CreateAt: e.Value.CreateAt,
|
||||
LastEditAt: e.Value.LastEditAt,
|
||||
Creator: e.Value.Creator().Name(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Room) SetStatus(playing bool, seek, rate, timeDiff float64) Status {
|
||||
r.UpdateActiveTime()
|
||||
return r.current.SetStatus(playing, seek, rate, timeDiff)
|
||||
}
|
||||
|
||||
func (r *Room) SetSeekRate(seek, rate, timeDiff float64) Status {
|
||||
r.UpdateActiveTime()
|
||||
return r.current.SetSeekRate(seek, rate, timeDiff)
|
||||
}
|
||||
|
||||
func (r *Room) PushBackMovie(movie *Movie) error {
|
||||
if r.hub.Closed() {
|
||||
return ErrAlreadyClosed
|
||||
}
|
||||
|
||||
return r.movies.PushBackMovie(movie)
|
||||
}
|
||||
|
||||
func (r *Room) PushFrontMovie(movie *Movie) error {
|
||||
if r.hub.Closed() {
|
||||
return ErrAlreadyClosed
|
||||
}
|
||||
|
||||
return r.movies.PushFrontMovie(movie)
|
||||
}
|
||||
|
||||
func (r *Room) DelMovie(id ...uint64) error {
|
||||
if r.hub.Closed() {
|
||||
return ErrAlreadyClosed
|
||||
}
|
||||
m, err := r.movies.GetAndDelMovie(id...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.closeLive(m)
|
||||
}
|
||||
|
||||
func (r *Room) ClearMovies() (err error) {
|
||||
if r.hub.Closed() {
|
||||
return ErrAlreadyClosed
|
||||
}
|
||||
return r.closeLive(r.movies.GetAndClear())
|
||||
}
|
||||
|
||||
func (r *Room) closeLive(m []*Movie) error {
|
||||
for _, m := range m {
|
||||
if m.Live {
|
||||
if err := r.rtmpa.DelChannel(m.PullKey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Room) SwapMovie(id1, id2 uint64) error {
|
||||
if r.hub.Closed() {
|
||||
return ErrAlreadyClosed
|
||||
}
|
||||
return r.movies.SwapMovie(id1, id2)
|
||||
}
|
||||
|
||||
func (r *Room) Broadcast(msg Message, conf ...BroadcastConf) error {
|
||||
r.UpdateActiveTime()
|
||||
return r.hub.Broadcast(msg, conf...)
|
||||
}
|
||||
|
||||
func (r *Room) RegClient(user *User, conn *websocket.Conn) (*Client, error) {
|
||||
r.updateActiveTime()
|
||||
return r.hub.RegClient(user, conn)
|
||||
}
|
|
@ -0,0 +1,219 @@
|
|||
package room
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/zijiren233/gencontainer/dllist"
|
||||
"github.com/zijiren233/stream"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
room *Room
|
||||
name string
|
||||
password []byte
|
||||
version uint64
|
||||
admin bool
|
||||
lastAct int64
|
||||
}
|
||||
|
||||
var (
|
||||
ErrUsernameEmpty = errors.New("user name is empty")
|
||||
ErrUserPasswordEmpty = errors.New("user password is empty")
|
||||
)
|
||||
|
||||
type UserConf func(*User)
|
||||
|
||||
func WithUserVersion(version uint64) UserConf {
|
||||
return func(u *User) {
|
||||
u.version = version
|
||||
}
|
||||
}
|
||||
|
||||
func WithUserAdmin(admin bool) UserConf {
|
||||
return func(u *User) {
|
||||
u.admin = admin
|
||||
}
|
||||
}
|
||||
|
||||
func NewUser(id string, password string, room *Room, conf ...UserConf) (*User, error) {
|
||||
if id == "" {
|
||||
return nil, ErrUsernameEmpty
|
||||
}
|
||||
if password == "" {
|
||||
return nil, ErrUserPasswordEmpty
|
||||
}
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword(stream.StringToBytes(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u := &User{
|
||||
room: room,
|
||||
name: id,
|
||||
password: hashedPassword,
|
||||
lastAct: time.Now().UnixMicro(),
|
||||
}
|
||||
for _, c := range conf {
|
||||
c(u)
|
||||
}
|
||||
if u.version == 0 {
|
||||
u.version = rand.New(rand.NewSource(time.Now().UnixNano())).Uint64()
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (u *User) LastAct() int64 {
|
||||
return atomic.LoadInt64(&u.lastAct)
|
||||
}
|
||||
|
||||
func (u *User) LastActTime() time.Time {
|
||||
return time.UnixMicro(u.LastAct())
|
||||
}
|
||||
|
||||
func (u *User) UpdateLastAct() int64 {
|
||||
return atomic.SwapInt64(&u.lastAct, time.Now().UnixMicro())
|
||||
}
|
||||
|
||||
func (u *User) Version() uint64 {
|
||||
return atomic.LoadUint64(&u.version)
|
||||
}
|
||||
|
||||
func (u *User) CheckVersion(version uint64) bool {
|
||||
return u.Version() == version
|
||||
}
|
||||
|
||||
func (u *User) SetVersion(version uint64) {
|
||||
atomic.StoreUint64(&u.version, version)
|
||||
}
|
||||
|
||||
func (u *User) updateVersion() uint64 {
|
||||
return atomic.AddUint64(&u.version, 1)
|
||||
}
|
||||
|
||||
func (u *User) CheckPassword(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword(u.password, stream.StringToBytes(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (u *User) SetPassword(password string) error {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword(stream.StringToBytes(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.password = hashedPassword
|
||||
u.updateVersion()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) CloseHub() {
|
||||
c, loaded := u.room.hub.clients.LoadAndDelete(u.name)
|
||||
if loaded {
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) IsRoot() bool {
|
||||
return u.room.rootUser == u
|
||||
}
|
||||
|
||||
func (u *User) Name() string {
|
||||
return u.name
|
||||
}
|
||||
|
||||
func (u *User) Room() *Room {
|
||||
return u.room
|
||||
}
|
||||
|
||||
func (u *User) IsAdmin() bool {
|
||||
return u.admin
|
||||
}
|
||||
|
||||
func (u *User) SetAdmin(admin bool) {
|
||||
u.admin = admin
|
||||
}
|
||||
|
||||
func (u *User) NewMovie(url string, name string, type_ string, live bool, proxy bool, rtmpSource bool, headers map[string][]string, conf ...MovieConf) (*Movie, error) {
|
||||
return u.NewMovieWithBaseMovie(BaseMovie{
|
||||
Url: url,
|
||||
Name: name,
|
||||
Live: live,
|
||||
Proxy: proxy,
|
||||
RtmpSource: rtmpSource,
|
||||
Type: type_,
|
||||
Headers: headers,
|
||||
}, conf...)
|
||||
}
|
||||
|
||||
func (u *User) NewMovieWithBaseMovie(baseMovie BaseMovie, conf ...MovieConf) (*Movie, error) {
|
||||
conf = append(conf, WithCreator(u))
|
||||
return NewMovieWithBaseMovie(atomic.AddUint64(&u.room.mid, 1), baseMovie, conf...)
|
||||
}
|
||||
|
||||
func (u *User) Movie(id uint64) (*MovieInfo, error) {
|
||||
u.room.movies.lock.RLock()
|
||||
defer u.room.movies.lock.RUnlock()
|
||||
|
||||
m, err := u.room.movies.GetMovie(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
movie := &MovieInfo{
|
||||
Id: m.Id(),
|
||||
Url: m.Url,
|
||||
Name: m.Name,
|
||||
Live: m.Live,
|
||||
Proxy: m.Proxy,
|
||||
RtmpSource: m.RtmpSource,
|
||||
Type: m.Type,
|
||||
Headers: m.Headers,
|
||||
PullKey: m.PullKey,
|
||||
CreateAt: m.CreateAt,
|
||||
LastEditAt: m.LastEditAt,
|
||||
Creator: m.Creator().Name(),
|
||||
}
|
||||
if movie.Proxy && u.name != movie.Creator {
|
||||
m.Headers = nil
|
||||
}
|
||||
return movie, nil
|
||||
}
|
||||
|
||||
func (u *User) MovieList() []*MovieInfo {
|
||||
u.room.movies.lock.RLock()
|
||||
defer u.room.movies.lock.RUnlock()
|
||||
|
||||
movies := make([]*MovieInfo, 0, u.room.movies.l.Len())
|
||||
u.room.movies.range_(func(e *dllist.Element[*Movie]) bool {
|
||||
m := &MovieInfo{
|
||||
Id: e.Value.Id(),
|
||||
Url: e.Value.Url,
|
||||
Name: e.Value.Name,
|
||||
Live: e.Value.Live,
|
||||
Proxy: e.Value.Proxy,
|
||||
RtmpSource: e.Value.RtmpSource,
|
||||
Type: e.Value.Type,
|
||||
Headers: e.Value.Headers,
|
||||
PullKey: e.Value.PullKey,
|
||||
CreateAt: e.Value.CreateAt,
|
||||
LastEditAt: e.Value.LastEditAt,
|
||||
Creator: e.Value.Creator().Name(),
|
||||
}
|
||||
if e.Value.Proxy && u.name != m.Creator {
|
||||
m.Headers = nil
|
||||
}
|
||||
movies = append(movies, m)
|
||||
return true
|
||||
})
|
||||
return movies
|
||||
}
|
||||
|
||||
func (u *User) RegClient(conn *websocket.Conn) (*Client, error) {
|
||||
return u.room.RegClient(u, conn)
|
||||
}
|
||||
|
||||
func (u *User) Broadcast(msg Message, conf ...BroadcastConf) error {
|
||||
return u.room.Broadcast(msg, append(conf, WithSender(u.name))...)
|
||||
}
|
|
@ -0,0 +1,600 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
json "github.com/json-iterator/go"
|
||||
"github.com/synctv-org/synctv/internal/conf"
|
||||
"github.com/synctv-org/synctv/room"
|
||||
"github.com/synctv-org/synctv/utils"
|
||||
"github.com/zijiren233/livelib/av"
|
||||
"github.com/zijiren233/livelib/protocol/hls"
|
||||
"github.com/zijiren233/livelib/protocol/httpflv"
|
||||
"github.com/zijiren233/livelib/protocol/rtmp"
|
||||
"github.com/zijiren233/livelib/protocol/rtmp/core"
|
||||
rtmps "github.com/zijiren233/livelib/server"
|
||||
"github.com/zijiren233/stream"
|
||||
)
|
||||
|
||||
func GetPageItems[T any](ctx *gin.Context, items []T) ([]T, error) {
|
||||
max, err := strconv.ParseUint(ctx.DefaultQuery("max", "10"), 10, 64)
|
||||
if err != nil {
|
||||
return items, errors.New("max must be a number")
|
||||
}
|
||||
|
||||
page, err := strconv.ParseUint(ctx.DefaultQuery("page", "1"), 10, 64)
|
||||
if err != nil {
|
||||
return items, errors.New("page must be a number")
|
||||
}
|
||||
|
||||
return utils.GetPageItems(items, int(max), int(page)), nil
|
||||
}
|
||||
|
||||
func MovieList(ctx *gin.Context) {
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ml := user.MovieList()
|
||||
|
||||
movies, err := GetPageItems(ctx, ml)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewApiDataResp(gin.H{
|
||||
"current": user.Room().Current(),
|
||||
"total": len(ml),
|
||||
"movies": movies,
|
||||
}))
|
||||
}
|
||||
|
||||
func CurrentMovie(ctx *gin.Context) {
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewApiDataResp(gin.H{
|
||||
"current": user.Room().Current(),
|
||||
}))
|
||||
}
|
||||
|
||||
func Movies(ctx *gin.Context) {
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ml := user.MovieList()
|
||||
|
||||
movies, err := GetPageItems(ctx, ml)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewApiDataResp(gin.H{
|
||||
"total": len(ml),
|
||||
"movies": movies,
|
||||
}))
|
||||
}
|
||||
|
||||
type PushMovieReq = room.BaseMovie
|
||||
|
||||
func PushMovie(ctx *gin.Context) {
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
req := new(PushMovieReq)
|
||||
if err := json.NewDecoder(ctx.Request.Body).Decode(req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
movie, err := user.NewMovieWithBaseMovie(*req)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case movie.RtmpSource && movie.Proxy:
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorStringResp("rtmp source and proxy can not be true at the same time"))
|
||||
return
|
||||
case movie.Live && movie.RtmpSource:
|
||||
if !conf.Conf.Rtmp.Enable {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorStringResp("rtmp source is not enabled"))
|
||||
return
|
||||
}
|
||||
movie.PullKey = uuid.New().String()
|
||||
c, err := user.Room().NewLiveChannel(movie.PullKey)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
movie.SetChannel(c)
|
||||
case movie.Live && movie.Proxy:
|
||||
if !conf.Conf.Proxy.LiveProxy {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorStringResp("live proxy is not enabled"))
|
||||
return
|
||||
}
|
||||
u, err := url.Parse(movie.Url)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
switch u.Scheme {
|
||||
case "rtmp":
|
||||
PullKey := uuid.New().String()
|
||||
c, err := user.Room().NewLiveChannel(PullKey)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
movie.PullKey = PullKey
|
||||
go func() {
|
||||
for {
|
||||
cli := core.NewConnClient()
|
||||
err = cli.Start(movie.PullKey, av.PLAY)
|
||||
if err != nil {
|
||||
cli.Close()
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
if err := c.PushStart(rtmp.NewReader(cli)); err != nil && err == rtmps.ErrClosed {
|
||||
cli.Close()
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
}()
|
||||
case "http", "https":
|
||||
// TODO: http https flv proxy
|
||||
fallthrough
|
||||
default:
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorStringResp("only support rtmp temporarily"))
|
||||
return
|
||||
}
|
||||
case !movie.Live && movie.RtmpSource:
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorStringResp("rtmp source must be live"))
|
||||
return
|
||||
case !movie.Live && movie.Proxy:
|
||||
if !conf.Conf.Proxy.MovieProxy {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorStringResp("movie proxy is not enabled"))
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case !movie.Live && !movie.Proxy, movie.Live && !movie.Proxy && !movie.RtmpSource:
|
||||
u, err := url.Parse(movie.Url)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorStringResp("only support http or https"))
|
||||
return
|
||||
}
|
||||
default:
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorStringResp("unknown error"))
|
||||
return
|
||||
}
|
||||
|
||||
s := ctx.DefaultQuery("pos", "back")
|
||||
switch s {
|
||||
case "back":
|
||||
err = user.Room().PushBackMovie(movie)
|
||||
case "front":
|
||||
err = user.Room().PushFrontMovie(movie)
|
||||
default:
|
||||
err = FormatErrNotSupportPosition(s)
|
||||
}
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.Broadcast(&room.ElementMessage{
|
||||
Type: room.ChangeMovieList,
|
||||
Sender: user.Name(),
|
||||
}, room.WithSendToSelf()); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, NewApiDataResp(gin.H{
|
||||
"id": movie.Id(),
|
||||
}))
|
||||
}
|
||||
|
||||
func NewPublishKey(ctx *gin.Context) {
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
req := new(IdReq)
|
||||
if err := json.NewDecoder(ctx.Request.Body).Decode(req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
movie, err := user.Room().GetMovie(req.Id)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if movie.Creator().Name() != user.Name() {
|
||||
ctx.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !movie.RtmpSource {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorStringResp("only live movie can get publish key"))
|
||||
return
|
||||
}
|
||||
|
||||
if movie.PullKey == "" {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, NewApiErrorStringResp("pull key is empty"))
|
||||
return
|
||||
}
|
||||
|
||||
token, err := NewRtmpAuthorization(movie.PullKey)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
host := conf.Conf.Rtmp.CustomPublishHost
|
||||
if host == "" {
|
||||
host = ctx.Request.Host
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewApiDataResp(gin.H{
|
||||
"host": host,
|
||||
"app": user.Room().ID(),
|
||||
"token": token,
|
||||
}))
|
||||
}
|
||||
|
||||
type EditMovieReq struct {
|
||||
Id uint64 `json:"id"`
|
||||
Url string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Headers map[string][]string `json:"headers"`
|
||||
}
|
||||
|
||||
func EditMovie(ctx *gin.Context) {
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
req := new(EditMovieReq)
|
||||
if err := json.NewDecoder(ctx.Request.Body).Decode(req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
m, err := user.Room().GetMovie(req.Id)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
// Dont edit live and proxy
|
||||
|
||||
m.Url = req.Url
|
||||
m.Name = req.Name
|
||||
m.Type = req.Type
|
||||
m.Headers = req.Headers
|
||||
|
||||
if err := user.Broadcast(&room.ElementMessage{
|
||||
Type: room.ChangeMovieList,
|
||||
Sender: user.Name(),
|
||||
}, room.WithSendToSelf()); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type IdsReq struct {
|
||||
Ids []uint64 `json:"ids"`
|
||||
}
|
||||
|
||||
func DelMovie(ctx *gin.Context) {
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
req := new(IdsReq)
|
||||
if err := json.NewDecoder(ctx.Request.Body).Decode(req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.Room().DelMovie(req.Ids...); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.Broadcast(&room.ElementMessage{
|
||||
Type: room.ChangeMovieList,
|
||||
Sender: user.Name(),
|
||||
}, room.WithSendToSelf()); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func ClearMovies(ctx *gin.Context) {
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.Room().ClearMovies(); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.Broadcast(&room.ElementMessage{
|
||||
Type: room.ChangeMovieList,
|
||||
Sender: user.Name(),
|
||||
}, room.WithSendToSelf()); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type SwapMovieReq struct {
|
||||
Id1 uint64 `json:"id1"`
|
||||
Id2 uint64 `json:"id2"`
|
||||
}
|
||||
|
||||
func SwapMovie(ctx *gin.Context) {
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
req := new(SwapMovieReq)
|
||||
if err := json.NewDecoder(ctx.Request.Body).Decode(req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.Room().SwapMovie(req.Id1, req.Id2); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.Broadcast(&room.ElementMessage{
|
||||
Type: room.ChangeMovieList,
|
||||
Sender: user.Name(),
|
||||
}, room.WithSendToSelf()); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type IdReq struct {
|
||||
Id uint64 `json:"id"`
|
||||
}
|
||||
|
||||
func ChangeCurrentMovie(ctx *gin.Context) {
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
req := new(IdReq)
|
||||
if err := json.NewDecoder(ctx.Request.Body).Decode(req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.Room().ChangeCurrentMovie(req.Id); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.Broadcast(&room.ElementMessage{
|
||||
Type: room.ChangeCurrent,
|
||||
Sender: user.Name(),
|
||||
Current: user.Room().Current(),
|
||||
}, room.WithSendToSelf()); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type RtmpClaims struct {
|
||||
PullKey string `json:"p"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func AuthRtmpPublish(Authorization string) (channelName string, err error) {
|
||||
t, err := jwt.ParseWithClaims(strings.TrimPrefix(Authorization, `Bearer `), &RtmpClaims{}, func(token *jwt.Token) (any, error) {
|
||||
return stream.StringToBytes(conf.Conf.Jwt.Secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", ErrAuthFailed
|
||||
}
|
||||
claims, ok := t.Claims.(*RtmpClaims)
|
||||
if !ok {
|
||||
return "", ErrAuthFailed
|
||||
}
|
||||
return claims.PullKey, nil
|
||||
}
|
||||
|
||||
var allowedProxyMovieType = map[string]struct{}{
|
||||
"video/avi": {},
|
||||
"video/mp4": {},
|
||||
"video/webm": {},
|
||||
}
|
||||
|
||||
func NewRtmpAuthorization(channelName string) (string, error) {
|
||||
claims := &RtmpClaims{
|
||||
PullKey: channelName,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(stream.StringToBytes(conf.Conf.Jwt.Secret))
|
||||
}
|
||||
|
||||
const UserAgent = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.40`
|
||||
|
||||
func ProxyMovie(ctx *gin.Context) {
|
||||
roomid := ctx.Query("roomid")
|
||||
if roomid == "" {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorStringResp("roomid is empty"))
|
||||
return
|
||||
}
|
||||
room, err := Rooms.GetRoom(roomid)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
}
|
||||
cm := room.Current().Movie()
|
||||
if !cm.Proxy || cm.Live {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorStringResp("not support proxy"))
|
||||
return
|
||||
}
|
||||
|
||||
u, err := url.Parse(cm.Url)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
req := resty.New().R().
|
||||
SetHeader("Range", ctx.GetHeader("Range")).
|
||||
SetHeader("User-Agent", UserAgent).
|
||||
SetHeader("Referer", fmt.Sprintf("%s://%s/", u.Scheme, u.Host)).
|
||||
SetHeader("Origin", fmt.Sprintf("%s://%s", u.Scheme, u.Host)).
|
||||
SetHeader("Accept", ctx.GetHeader("Accept")).
|
||||
SetHeader("Accept-Encoding", ctx.GetHeader("Accept-Encoding")).
|
||||
SetHeader("Accept-Language", ctx.GetHeader("Accept-Language"))
|
||||
|
||||
if cm.Headers != nil {
|
||||
for k, v := range cm.Headers {
|
||||
req.SetHeader(k, v[0])
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := req.Get(cm.Url)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.RawBody().Close()
|
||||
if _, ok := allowedProxyMovieType[resp.Header().Get("Content-Type")]; !ok {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(fmt.Errorf("this movie type support proxy: %s", resp.Header().Get("Content-Type"))))
|
||||
return
|
||||
}
|
||||
for k, v := range resp.Header() {
|
||||
ctx.Header(k, v[0])
|
||||
}
|
||||
ctx.Status(resp.StatusCode())
|
||||
io.Copy(ctx.Writer, resp.RawBody())
|
||||
}
|
||||
|
||||
type FormatErrNotSupportFileType string
|
||||
|
||||
func (e FormatErrNotSupportFileType) Error() string {
|
||||
return fmt.Sprintf("not support file type %s", string(e))
|
||||
}
|
||||
|
||||
func JoinLive(ctx *gin.Context) {
|
||||
if !conf.Conf.Proxy.LiveProxy && !conf.Conf.Rtmp.Enable {
|
||||
ctx.AbortWithStatusJSON(http.StatusForbidden, NewApiErrorStringResp("live proxy and rtmp source is not enabled"))
|
||||
return
|
||||
}
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
pullKey := strings.Trim(ctx.Param("pullKey"), "/")
|
||||
pullKeySplitd := strings.Split(pullKey, "/")
|
||||
fileName := pullKeySplitd[0]
|
||||
fileExt := path.Ext(pullKey)
|
||||
channelName := strings.TrimSuffix(fileName, fileExt)
|
||||
m, err := user.Room().GetMovieWithPullKey(channelName)
|
||||
// channel, err := s.GetChannelWithApp(r.ID(), channelName)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusNotFound, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
channel := m.Channel()
|
||||
switch fileExt {
|
||||
case ".flv":
|
||||
ctx.Header("Cache-Control", "no-store")
|
||||
w := httpflv.NewHttpFLVWriter(ctx.Writer)
|
||||
defer w.Close()
|
||||
channel.AddPlayer(w)
|
||||
w.SendPacket()
|
||||
case ".m3u8":
|
||||
ctx.Header("Cache-Control", "no-store")
|
||||
b, err := channel.GenM3U8PlayList(fmt.Sprintf("/api/movie/live/%s", channelName))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusNotFound, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
ctx.Data(http.StatusOK, hls.M3U8ContentType, b.Bytes())
|
||||
case ".ts":
|
||||
b, err := channel.GetTsFile(pullKeySplitd[1])
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusNotFound, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
ctx.Header("Cache-Control", "public, max-age=90")
|
||||
ctx.Data(http.StatusOK, hls.TSContentType, b)
|
||||
default:
|
||||
ctx.Header("Cache-Control", "no-store")
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(FormatErrNotSupportFileType(fileExt)))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/synctv-org/synctv/internal/conf"
|
||||
)
|
||||
|
||||
func Settings(ctx *gin.Context) {
|
||||
ctx.JSON(200, NewApiDataResp(gin.H{
|
||||
"rtmp": gin.H{
|
||||
"enable": conf.Conf.Rtmp.Enable,
|
||||
"rtmpPlayer": conf.Conf.Rtmp.RtmpPlayer,
|
||||
"hlsPlayer": conf.Conf.Rtmp.HlsPlayer,
|
||||
},
|
||||
"proxy": gin.H{
|
||||
"movieProxy": conf.Conf.Proxy.MovieProxy,
|
||||
"liveProxy": conf.Conf.Proxy.LiveProxy,
|
||||
},
|
||||
}))
|
||||
}
|
|
@ -0,0 +1,491 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
json "github.com/json-iterator/go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/maruel/natural"
|
||||
"github.com/synctv-org/synctv/internal/conf"
|
||||
"github.com/synctv-org/synctv/room"
|
||||
"github.com/zijiren233/gencontainer/vec"
|
||||
rtmps "github.com/zijiren233/livelib/server"
|
||||
"github.com/zijiren233/stream"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAuthFailed = errors.New("auth failed")
|
||||
ErrAuthExpired = errors.New("auth expired")
|
||||
ErrRoomAlready = errors.New("room already exists")
|
||||
)
|
||||
|
||||
type FormatErrNotSupportPosition string
|
||||
|
||||
func (e FormatErrNotSupportPosition) Error() string {
|
||||
return fmt.Sprintf("not support position %s", string(e))
|
||||
}
|
||||
|
||||
type AuthClaims struct {
|
||||
RoomID string `json:"id"`
|
||||
Version uint64 `json:"v"`
|
||||
Username string `json:"un"`
|
||||
UserVersion uint64 `json:"uv"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func AuthRoom(Authorization string) (*room.User, error) {
|
||||
t, err := jwt.ParseWithClaims(strings.TrimPrefix(Authorization, `Bearer `), &AuthClaims{}, func(token *jwt.Token) (any, error) {
|
||||
return stream.StringToBytes(conf.Conf.Jwt.Secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ErrAuthFailed
|
||||
}
|
||||
claims := t.Claims.(*AuthClaims)
|
||||
|
||||
r, err := Rooms.GetRoom(claims.RoomID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !r.CheckVersion(claims.Version) {
|
||||
return nil, ErrAuthExpired
|
||||
}
|
||||
|
||||
user, err := r.GetUser(claims.Username)
|
||||
if err != nil {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
if !user.CheckVersion(claims.UserVersion) {
|
||||
return nil, ErrAuthExpired
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func authWithPassword(roomid, password, username, userPassword string) (*room.User, error) {
|
||||
r, err := Rooms.GetRoom(roomid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !r.CheckPassword(password) {
|
||||
return nil, ErrAuthFailed
|
||||
}
|
||||
user, err := r.GetUser(username)
|
||||
if err != nil {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
if !user.CheckPassword(userPassword) {
|
||||
return nil, ErrAuthFailed
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func authOrNewWithPassword(roomid, password, username, userPassword string, conf ...room.UserConf) (*room.User, error) {
|
||||
r, err := Rooms.GetRoom(roomid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !r.CheckPassword(password) {
|
||||
return nil, ErrAuthFailed
|
||||
}
|
||||
user, err := r.GetOrNewUser(username, userPassword, conf...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !user.CheckPassword(userPassword) {
|
||||
return nil, ErrAuthFailed
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func newAuthorization(user *room.User) (string, error) {
|
||||
claims := &AuthClaims{
|
||||
RoomID: user.Room().ID(),
|
||||
Version: user.Room().Version(),
|
||||
Username: user.Name(),
|
||||
UserVersion: user.Version(),
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(conf.Conf.Jwt.Expire))),
|
||||
},
|
||||
}
|
||||
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(stream.StringToBytes(conf.Conf.Jwt.Secret))
|
||||
}
|
||||
|
||||
type CreateRoomReq struct {
|
||||
RoomID string `json:"roomId"`
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
UserPassword string `json:"userPassword"`
|
||||
Hidden bool `json:"hidden"`
|
||||
}
|
||||
|
||||
func NewCreateRoomHandler(s *rtmps.Server) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
req := new(CreateRoomReq)
|
||||
if err := json.NewDecoder(ctx.Request.Body).Decode(req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := room.NewUser(req.Username, req.UserPassword, nil, room.WithUserAdmin(true))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
r, err := Rooms.CreateRoom(req.RoomID, req.Password, s,
|
||||
room.WithHidden(req.Hidden),
|
||||
room.WithMaxInactivityTime(roomMaxInactivityTime),
|
||||
room.WithRootUser(user),
|
||||
)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
token, err := newAuthorization(user)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second * 5)
|
||||
defer ticker.Stop()
|
||||
var pre int64 = 0
|
||||
for range ticker.C {
|
||||
if r.Closed() {
|
||||
log.Infof("ws: room %s closed, stop broadcast people num", r.ID())
|
||||
return
|
||||
}
|
||||
current := r.ClientNum()
|
||||
if current != pre {
|
||||
if err := r.Broadcast(&room.ElementMessage{
|
||||
Type: room.ChangePeopleNum,
|
||||
PeopleNum: current,
|
||||
}); err != nil {
|
||||
log.Errorf("ws: room %s broadcast people num error: %v", r.ID(), err)
|
||||
continue
|
||||
}
|
||||
pre = current
|
||||
} else {
|
||||
if err := r.Broadcast(&room.PingMessage{}); err != nil {
|
||||
log.Errorf("ws: room %s broadcast ping error: %v", r.ID(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
r.Start()
|
||||
|
||||
r.SetRootUser(user)
|
||||
|
||||
ctx.JSON(http.StatusCreated, NewApiDataResp(gin.H{
|
||||
"token": token,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
type RoomListResp struct {
|
||||
RoomID string `json:"roomId"`
|
||||
PeopleNum int64 `json:"peopleNum"`
|
||||
NeedPassword bool `json:"needPassword"`
|
||||
Creator string `json:"creator"`
|
||||
CreateAt int64 `json:"createAt"`
|
||||
}
|
||||
|
||||
func RoomList(ctx *gin.Context) {
|
||||
r := Rooms.ListNonHidden()
|
||||
resp := vec.New[*RoomListResp](vec.WithCmpLess[*RoomListResp](func(v1, v2 *RoomListResp) bool {
|
||||
return v1.PeopleNum < v2.PeopleNum
|
||||
}), vec.WithCmpEqual[*RoomListResp](func(v1, v2 *RoomListResp) bool {
|
||||
return v1.PeopleNum == v2.PeopleNum
|
||||
}))
|
||||
for _, v := range r {
|
||||
resp.Push(&RoomListResp{
|
||||
RoomID: v.ID(),
|
||||
PeopleNum: v.ClientNum(),
|
||||
NeedPassword: v.NeedPassword(),
|
||||
Creator: v.RootUser().Name(),
|
||||
CreateAt: v.CreateAt().UnixMilli(),
|
||||
})
|
||||
}
|
||||
|
||||
switch ctx.DefaultQuery("sort", "peopleNum") {
|
||||
case "peopleNum":
|
||||
resp.SortStable()
|
||||
case "creator":
|
||||
resp.SortStableFunc(func(v1, v2 *RoomListResp) bool {
|
||||
return natural.Less(v1.Creator, v2.Creator)
|
||||
}, func(t1, t2 *RoomListResp) bool {
|
||||
return t1.Creator == t2.Creator
|
||||
})
|
||||
case "createAt":
|
||||
resp.SortStableFunc(func(v1, v2 *RoomListResp) bool {
|
||||
return v1.CreateAt < v2.CreateAt
|
||||
}, func(t1, t2 *RoomListResp) bool {
|
||||
return t1.CreateAt == t2.CreateAt
|
||||
})
|
||||
case "roomId":
|
||||
resp.SortStableFunc(func(v1, v2 *RoomListResp) bool {
|
||||
return natural.Less(v1.RoomID, v2.RoomID)
|
||||
}, func(t1, t2 *RoomListResp) bool {
|
||||
return t1.RoomID == t2.RoomID
|
||||
})
|
||||
case "needPassword":
|
||||
resp.SortStableFunc(func(v1, v2 *RoomListResp) bool {
|
||||
return v1.NeedPassword && !v2.NeedPassword
|
||||
}, func(t1, t2 *RoomListResp) bool {
|
||||
return t1.NeedPassword == t2.NeedPassword
|
||||
})
|
||||
default:
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorStringResp("sort must be peoplenum or roomid"))
|
||||
return
|
||||
}
|
||||
|
||||
switch ctx.DefaultQuery("order", "desc") {
|
||||
case "asc":
|
||||
// do nothing
|
||||
case "desc":
|
||||
resp.Reverse()
|
||||
default:
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorStringResp("order must be asc or desc"))
|
||||
return
|
||||
}
|
||||
|
||||
list, err := GetPageItems(ctx, resp.Slice())
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewApiDataResp(gin.H{
|
||||
"total": resp.Len(),
|
||||
"list": list,
|
||||
}))
|
||||
}
|
||||
|
||||
func CheckRoom(ctx *gin.Context) {
|
||||
r, err := Rooms.GetRoom(ctx.Query("roomId"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusNotFound, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewApiDataResp(gin.H{
|
||||
"peopleNum": r.ClientNum(),
|
||||
"needPassword": r.NeedPassword(),
|
||||
}))
|
||||
}
|
||||
|
||||
func CheckUser(ctx *gin.Context) {
|
||||
r, err := Rooms.GetRoom(ctx.Query("roomId"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusNotFound, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
u, err := r.GetUser(ctx.Query("username"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusNotFound, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewApiDataResp(gin.H{
|
||||
"idRoot": u.IsRoot(),
|
||||
"idAdmin": u.IsAdmin(),
|
||||
"lastAct": u.LastAct(),
|
||||
}))
|
||||
}
|
||||
|
||||
type LoginRoomReq struct {
|
||||
RoomID string `json:"roomId"`
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
UserPassword string `json:"userPassword"`
|
||||
}
|
||||
|
||||
func LoginRoom(ctx *gin.Context) {
|
||||
req := new(LoginRoomReq)
|
||||
if err := json.NewDecoder(ctx.Request.Body).Decode(req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
autoNew, err := strconv.ParseBool(ctx.DefaultQuery("autoNew", "false"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorStringResp("autoNew must be bool"))
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
user *room.User
|
||||
)
|
||||
if autoNew {
|
||||
user, err = authOrNewWithPassword(req.RoomID, req.Password, req.Username, req.UserPassword)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
user, err = authWithPassword(req.RoomID, req.Password, req.Username, req.UserPassword)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
token, err := newAuthorization(user)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewApiDataResp(gin.H{
|
||||
"token": token,
|
||||
}))
|
||||
}
|
||||
|
||||
func DeleteRoom(ctx *gin.Context) {
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := Rooms.DelRoom(user.Room().ID()); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type SetPasswordReq struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func SetPassword(ctx *gin.Context) {
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if !user.IsRoot() || !user.IsAdmin() {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorStringResp("only root or admin can set password"))
|
||||
return
|
||||
}
|
||||
|
||||
req := new(SetPasswordReq)
|
||||
if err := json.NewDecoder(ctx.Request.Body).Decode(req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
user.Room().SetPassword(req.Password)
|
||||
|
||||
token, err := newAuthorization(user)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewApiDataResp(gin.H{
|
||||
"token": token,
|
||||
}))
|
||||
}
|
||||
|
||||
type UsernameReq struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
func AddAdmin(ctx *gin.Context) {
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if !user.IsRoot() && !user.IsAdmin() {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorStringResp("only root or admin can add admin"))
|
||||
return
|
||||
}
|
||||
|
||||
req := new(UsernameReq)
|
||||
if err := json.NewDecoder(ctx.Request.Body).Decode(req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
u, err := user.Room().GetUser(req.Username)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusNotFound, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
u.SetAdmin(true)
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func DelAdmin(ctx *gin.Context) {
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if !user.IsRoot() {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorStringResp("only root can del admin"))
|
||||
return
|
||||
}
|
||||
|
||||
req := new(UsernameReq)
|
||||
if err := json.NewDecoder(ctx.Request.Body).Decode(req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
u, err := user.Room().GetUser(req.Username)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusNotFound, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
u.SetAdmin(false)
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func CloseRoom(ctx *gin.Context) {
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if !user.IsRoot() {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorStringResp("only root can close room"))
|
||||
return
|
||||
}
|
||||
|
||||
err = Rooms.DelRoom(user.Room().ID())
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
json "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
func Me(ctx *gin.Context) {
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewApiDataResp(gin.H{
|
||||
"isRoot": user.IsRoot(),
|
||||
"isAdmin": user.IsAdmin(),
|
||||
"username": user.Name(),
|
||||
"lastAct": user.LastAct(),
|
||||
}))
|
||||
}
|
||||
|
||||
func SetUserPassword(ctx *gin.Context) {
|
||||
user, err := AuthRoom(ctx.GetHeader("Authorization"))
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
req := new(SetPasswordReq)
|
||||
if err := json.NewDecoder(ctx.Request.Body).Decode(req); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.SetPassword(req.Password); err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusBadRequest, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
user.CloseHub()
|
||||
|
||||
token, err := newAuthorization(user)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusInternalServerError, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewApiDataResp(gin.H{
|
||||
"token": token,
|
||||
}))
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ApiResp struct {
|
||||
Time int64 `json:"time"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Data any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func (ar *ApiResp) SetError(err error) {
|
||||
ar.Error = err.Error()
|
||||
}
|
||||
|
||||
func (ar *ApiResp) SetDate(data any) {
|
||||
ar.Data = data
|
||||
}
|
||||
|
||||
func NewApiErrorResp(err error) *ApiResp {
|
||||
return &ApiResp{
|
||||
Time: time.Now().UnixMicro(),
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
func NewApiErrorStringResp(err string) *ApiResp {
|
||||
return &ApiResp{
|
||||
Time: time.Now().UnixMicro(),
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
func NewApiDataResp(data any) *ApiResp {
|
||||
return &ApiResp{
|
||||
Time: time.Now().UnixMicro(),
|
||||
Data: data,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/synctv-org/synctv/internal/conf"
|
||||
"github.com/synctv-org/synctv/public"
|
||||
"github.com/synctv-org/synctv/utils"
|
||||
rtmps "github.com/zijiren233/livelib/server"
|
||||
)
|
||||
|
||||
func Init(e *gin.Engine, s *rtmps.Server) {
|
||||
initOnce()
|
||||
|
||||
{
|
||||
s.SetParseChannelFunc(func(ReqAppName, ReqChannelName string, IsPublisher bool) (TrueAppName string, TrueChannel string, err error) {
|
||||
if IsPublisher {
|
||||
channelName, err := AuthRtmpPublish(ReqChannelName)
|
||||
if err != nil {
|
||||
log.Errorf("rtmp: publish auth to %s error: %v", ReqAppName, err)
|
||||
return "", "", err
|
||||
}
|
||||
if !Rooms.HasRoom(ReqAppName) {
|
||||
log.Infof("rtmp: publish to %s/%s error: %s", ReqAppName, channelName, fmt.Sprintf("room %s not exist", ReqAppName))
|
||||
return "", "", fmt.Errorf("room %s not exist", ReqAppName)
|
||||
}
|
||||
log.Infof("rtmp: publish to success: %s/%s", ReqAppName, channelName)
|
||||
return ReqAppName, channelName, nil
|
||||
} else if !conf.Conf.Rtmp.RtmpPlayer {
|
||||
log.Infof("rtmp: dial to %s/%s error: %s", ReqAppName, ReqChannelName, "rtmp player is not enabled")
|
||||
return "", "", fmt.Errorf("rtmp: dial to %s/%s error: %s", ReqAppName, ReqChannelName, "rtmp player is not enabled")
|
||||
}
|
||||
return ReqAppName, ReqChannelName, nil
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
web := e.Group("/web")
|
||||
|
||||
web.StaticFS("", http.FS(public.Public))
|
||||
}
|
||||
|
||||
{
|
||||
api := e.Group("/api")
|
||||
|
||||
{
|
||||
public := api.Group("/public")
|
||||
|
||||
public.GET("/settings", Settings)
|
||||
}
|
||||
|
||||
{
|
||||
room := api.Group("/room")
|
||||
|
||||
room.GET("/ws", NewWebSocketHandler(utils.NewWebSocketServer()))
|
||||
|
||||
room.GET("/check", CheckRoom)
|
||||
|
||||
room.GET("/user", CheckUser)
|
||||
|
||||
room.GET("/list", RoomList)
|
||||
|
||||
room.POST("/create", NewCreateRoomHandler(s))
|
||||
|
||||
room.POST("/login", LoginRoom)
|
||||
|
||||
room.POST("/delete", DeleteRoom)
|
||||
|
||||
room.POST("/pwd", SetPassword)
|
||||
|
||||
room.PUT("/admin", AddAdmin)
|
||||
|
||||
room.DELETE("/admin", DelAdmin)
|
||||
|
||||
room.POST("/close", CloseRoom)
|
||||
}
|
||||
|
||||
{
|
||||
movie := api.Group("/movie")
|
||||
|
||||
movie.GET("/list", MovieList)
|
||||
|
||||
movie.GET("/movies", Movies)
|
||||
|
||||
movie.GET("/current", CurrentMovie)
|
||||
|
||||
movie.POST("/current", ChangeCurrentMovie)
|
||||
|
||||
movie.POST("/push", PushMovie)
|
||||
|
||||
movie.POST("/edit", EditMovie)
|
||||
|
||||
movie.POST("/swap", SwapMovie)
|
||||
|
||||
movie.POST("/delete", DelMovie)
|
||||
|
||||
movie.POST("/clear", ClearMovies)
|
||||
|
||||
movie.GET("/proxy/:roomid", ProxyMovie)
|
||||
|
||||
{
|
||||
live := movie.Group("/live")
|
||||
|
||||
live.POST("/publishKey", NewPublishKey)
|
||||
|
||||
live.GET("/*pullKey", JoinLive)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
user := api.Group("/user")
|
||||
|
||||
user.GET("/me", Me)
|
||||
|
||||
user.POST("/pwd", SetUserPassword)
|
||||
}
|
||||
}
|
||||
|
||||
e.NoRoute(func(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, "/web/")
|
||||
})
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/synctv-org/synctv/room"
|
||||
"github.com/zijiren233/gencontainer/rwmap"
|
||||
"github.com/zijiren233/ksync"
|
||||
rtmps "github.com/zijiren233/livelib/server"
|
||||
)
|
||||
|
||||
const (
|
||||
roomMaxInactivityTime = time.Hour * 12
|
||||
)
|
||||
|
||||
var (
|
||||
Rooms *rooms
|
||||
initOnce = sync.OnceFunc(func() {
|
||||
Rooms = newRooms()
|
||||
})
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRoomIDEmpty = errors.New("roomid is empty")
|
||||
ErrRoomNotFound = errors.New("room not found")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrRoomAlreadyExist = errors.New("room already exist")
|
||||
)
|
||||
|
||||
type rooms struct {
|
||||
rooms *rwmap.RWMap[string, *room.Room]
|
||||
lock *ksync.Krwmutex
|
||||
}
|
||||
|
||||
func newRooms() *rooms {
|
||||
return &rooms{
|
||||
rooms: &rwmap.RWMap[string, *room.Room]{},
|
||||
lock: ksync.NewKrwmutex(),
|
||||
}
|
||||
}
|
||||
|
||||
func (rs *rooms) List() (rooms []*room.Room) {
|
||||
rooms = make([]*room.Room, 0, rs.rooms.Len())
|
||||
rs.rooms.Range(func(id string, r *room.Room) bool {
|
||||
rs.lock.RLock(id)
|
||||
defer rs.lock.RUnlock(id)
|
||||
if r.Closed() {
|
||||
rs.rooms.Delete(id)
|
||||
return true
|
||||
}
|
||||
rooms = append(rooms, r)
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (rs *rooms) ListNonHidden() (rooms []*room.Room) {
|
||||
rooms = make([]*room.Room, 0, rs.rooms.Len())
|
||||
rs.rooms.Range(func(id string, r *room.Room) bool {
|
||||
rs.lock.RLock(id)
|
||||
defer rs.lock.RUnlock(id)
|
||||
if r.Closed() {
|
||||
rs.rooms.Delete(id)
|
||||
return true
|
||||
}
|
||||
if !r.Hidden() {
|
||||
rooms = append(rooms, r)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (rs *rooms) ListHidden() (rooms []*room.Room) {
|
||||
rooms = make([]*room.Room, 0, rs.rooms.Len())
|
||||
rs.rooms.Range(func(id string, r *room.Room) bool {
|
||||
rs.lock.RLock(id)
|
||||
defer rs.lock.RUnlock(id)
|
||||
if r.Closed() {
|
||||
rs.rooms.Delete(id)
|
||||
return true
|
||||
}
|
||||
if r.Hidden() {
|
||||
rooms = append(rooms, r)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (rs *rooms) HasRoom(id string) bool {
|
||||
rs.lock.Lock(id)
|
||||
defer rs.lock.Unlock(id)
|
||||
r, ok := rs.rooms.Load(id)
|
||||
if !ok || r.Closed() {
|
||||
return false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func (rs *rooms) GetRoom(id string) (*room.Room, error) {
|
||||
if id == "" {
|
||||
return nil, ErrRoomIDEmpty
|
||||
}
|
||||
rs.lock.RLock(id)
|
||||
defer rs.lock.RUnlock(id)
|
||||
r, ok := rs.rooms.Load(id)
|
||||
if !ok || r.Closed() {
|
||||
return nil, ErrRoomNotFound
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (rs *rooms) CreateRoom(id string, password string, s *rtmps.Server, conf ...room.RoomConf) (*room.Room, error) {
|
||||
if id == "" {
|
||||
return nil, ErrRoomIDEmpty
|
||||
}
|
||||
rs.lock.Lock(id)
|
||||
defer rs.lock.Unlock(id)
|
||||
|
||||
if oldR, ok := rs.rooms.Load(id); ok && !oldR.Closed() {
|
||||
return nil, ErrRoomAlreadyExist
|
||||
}
|
||||
r, err := room.NewRoom(id, password, s, conf...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rs.rooms.Store(id, r)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (rs *rooms) DelRoom(id string) error {
|
||||
if id == "" {
|
||||
return ErrRoomIDEmpty
|
||||
}
|
||||
rs.lock.Lock(id)
|
||||
defer rs.lock.Unlock(id)
|
||||
r, ok := rs.rooms.LoadAndDelete(id)
|
||||
if !ok {
|
||||
return ErrRoomNotFound
|
||||
}
|
||||
return r.Close()
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/synctv-org/synctv/public"
|
||||
)
|
||||
|
||||
func WebServer(e gin.IRoutes) {
|
||||
e.StaticFS("", http.FS(public.Public))
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
json "github.com/json-iterator/go"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/synctv-org/synctv/cmd/flags"
|
||||
"github.com/synctv-org/synctv/room"
|
||||
"github.com/synctv-org/synctv/utils"
|
||||
)
|
||||
|
||||
const maxInterval = 10
|
||||
|
||||
func NewWebSocketHandler(wss *utils.WebSocket) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
token := ctx.GetHeader("Sec-WebSocket-Protocol")
|
||||
user, err := AuthRoom(token)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(http.StatusUnauthorized, NewApiErrorResp(err))
|
||||
return
|
||||
}
|
||||
wss.Server(ctx.Writer, ctx.Request, []string{token}, NewWSMessageHandler(user))
|
||||
}
|
||||
}
|
||||
|
||||
func NewWSMessageHandler(u *room.User) func(c *websocket.Conn) error {
|
||||
return func(c *websocket.Conn) error {
|
||||
client, err := u.RegClient(c)
|
||||
if err != nil {
|
||||
log.Errorf("ws: register client error: %v", err)
|
||||
b, err := json.Marshal(room.ElementMessage{
|
||||
Type: room.Error,
|
||||
Message: err.Error(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.WriteMessage(websocket.TextMessage, b)
|
||||
}
|
||||
log.Infof("ws: room %s user %s connected", u.Room().ID(), u.Name())
|
||||
defer func() {
|
||||
client.Unregister()
|
||||
client.Close()
|
||||
log.Infof("ws: room %s user %s disconnected", u.Room().ID(), u.Name())
|
||||
}()
|
||||
go handleReaderMessage(client)
|
||||
return handleWriterMessage(client)
|
||||
}
|
||||
}
|
||||
|
||||
func handleWriterMessage(c *room.Client) error {
|
||||
for v := range c.GetReadChan() {
|
||||
wc, err := c.NextWriter(v.MessageType())
|
||||
if err != nil {
|
||||
if flags.Dev {
|
||||
log.Errorf("ws: room %s user %s get next writer error: %v", c.Room().ID(), c.Username(), err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.Encode(wc); err != nil {
|
||||
if flags.Dev {
|
||||
log.Errorf("ws: room %s user %s encode message error: %v", c.Room().ID(), c.Username(), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := wc.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleReaderMessage(c *room.Client) error {
|
||||
defer c.Close()
|
||||
var timeDiff float64
|
||||
for {
|
||||
t, rd, err := c.NextReader()
|
||||
if err != nil {
|
||||
if flags.Dev {
|
||||
log.Errorf("ws: room %s user %s get next reader error: %v", c.Room().ID(), c.Username(), err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
log.Infof("ws: room %s user %s receive message type: %d", c.Room().ID(), c.Username(), t)
|
||||
switch t {
|
||||
case websocket.CloseMessage:
|
||||
if flags.Dev {
|
||||
log.Infof("ws: room %s user %s receive close message", c.Room().ID(), c.Username())
|
||||
}
|
||||
return nil
|
||||
case websocket.TextMessage:
|
||||
msg := room.ElementMessage{}
|
||||
if err := json.NewDecoder(rd).Decode(&msg); err != nil {
|
||||
log.Errorf("ws: room %s user %s decode message error: %v", c.Room().ID(), c.Username(), err)
|
||||
if err := c.Send(&room.ElementMessage{
|
||||
Type: room.Error,
|
||||
Message: err.Error(),
|
||||
}); err != nil {
|
||||
log.Errorf("ws: room %s user %s send error message error: %v", c.Room().ID(), c.Username(), err)
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if flags.Dev {
|
||||
log.Infof("ws: receive room %s user %s message: %+v", c.Room().ID(), c.Username(), msg)
|
||||
}
|
||||
if msg.Time != 0 {
|
||||
timeDiff = time.Since(time.UnixMilli(msg.Time)).Seconds()
|
||||
} else {
|
||||
timeDiff = 0.0
|
||||
}
|
||||
if timeDiff < 0 {
|
||||
timeDiff = 0
|
||||
} else if timeDiff > 1.5 {
|
||||
timeDiff = 1.5
|
||||
}
|
||||
switch msg.Type {
|
||||
case room.ChatMessage:
|
||||
c.Broadcast(&room.ElementMessage{
|
||||
Type: room.ChatMessage,
|
||||
Sender: c.Username(),
|
||||
Message: msg.Message,
|
||||
}, room.WithSendToSelf())
|
||||
case room.Play:
|
||||
status := c.Room().SetStatus(true, msg.Seek, msg.Rate, timeDiff)
|
||||
c.Broadcast(&room.ElementMessage{
|
||||
Type: room.Play,
|
||||
Sender: c.Username(),
|
||||
Seek: status.Seek,
|
||||
Rate: status.Rate,
|
||||
})
|
||||
case room.Pause:
|
||||
status := c.Room().SetStatus(false, msg.Seek, msg.Rate, timeDiff)
|
||||
c.Broadcast(&room.ElementMessage{
|
||||
Type: room.Pause,
|
||||
Sender: c.Username(),
|
||||
Seek: status.Seek,
|
||||
Rate: status.Rate,
|
||||
})
|
||||
case room.ChangeRate:
|
||||
status := c.Room().SetSeekRate(msg.Seek, msg.Rate, timeDiff)
|
||||
c.Broadcast(&room.ElementMessage{
|
||||
Type: room.ChangeRate,
|
||||
Sender: c.Username(),
|
||||
Seek: status.Seek,
|
||||
Rate: status.Rate,
|
||||
})
|
||||
case room.ChangeSeek:
|
||||
status := c.Room().SetSeekRate(msg.Seek, msg.Rate, timeDiff)
|
||||
c.Broadcast(&room.ElementMessage{
|
||||
Type: room.ChangeSeek,
|
||||
Sender: c.Username(),
|
||||
Seek: status.Seek,
|
||||
Rate: status.Rate,
|
||||
})
|
||||
case room.CheckSeek:
|
||||
status := c.Room().Current().Status()
|
||||
if status.Seek+maxInterval < msg.Seek+timeDiff {
|
||||
c.Send(&room.ElementMessage{
|
||||
Type: room.TooFast,
|
||||
Seek: status.Seek,
|
||||
Rate: status.Rate,
|
||||
})
|
||||
} else if status.Seek-maxInterval > msg.Seek+timeDiff {
|
||||
c.Send(&room.ElementMessage{
|
||||
Type: room.TooSlow,
|
||||
Seek: status.Seek,
|
||||
Rate: status.Rate,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package middlewares
|
||||
|
||||
import (
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func NewCors() gin.HandlerFunc {
|
||||
config := cors.DefaultConfig()
|
||||
config.AllowAllOrigins = true
|
||||
config.AllowHeaders = []string{"*"}
|
||||
config.AllowMethods = []string{"*"}
|
||||
return cors.New(config)
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package middlewares
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/synctv-org/synctv/internal/conf"
|
||||
)
|
||||
|
||||
func Init(e *gin.Engine) {
|
||||
e.
|
||||
Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out)).
|
||||
Use(NewCors())
|
||||
if conf.Conf.Server.Quic {
|
||||
e.Use(NewQuic())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package middlewares
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func NewQuic() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Alt-Svc", fmt.Sprintf("h3=\":%s\"; ma=86400", c.Request.URL.Port()))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/synctv-org/synctv/internal/conf"
|
||||
"github.com/synctv-org/synctv/server/handlers"
|
||||
"github.com/synctv-org/synctv/server/middlewares"
|
||||
rtmps "github.com/zijiren233/livelib/server"
|
||||
)
|
||||
|
||||
func Init(e *gin.Engine, s *rtmps.Server) {
|
||||
middlewares.Init(e)
|
||||
handlers.Init(e, s)
|
||||
}
|
||||
|
||||
func NewAndInit() (e *gin.Engine, s *rtmps.Server) {
|
||||
e = gin.New()
|
||||
s = rtmps.NewRtmpServer(rtmps.WithInitHlsPlayer(conf.Conf.Rtmp.HlsPlayer))
|
||||
Init(e, s)
|
||||
return
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
yamlcomment "github.com/zijiren233/yaml-comment"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
func RandString(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func RandBytes(n int) []byte {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = byte(rand.Intn(256))
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func GetPageItems[T any](items []T, max, page int) []T {
|
||||
start := (page - 1) * max
|
||||
if start < 0 {
|
||||
start = 0
|
||||
} else if start > len(items) {
|
||||
start = len(items)
|
||||
}
|
||||
end := int(page * max)
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
return items[start:end]
|
||||
}
|
||||
|
||||
func Index[T comparable](items []T, item T) int {
|
||||
for i, v := range items {
|
||||
if v == item {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func In[T comparable](items []T, item T) bool {
|
||||
return Index(items, item) != -1
|
||||
}
|
||||
|
||||
func Exists(name string) bool {
|
||||
if _, err := os.Stat(name); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func WriteYaml(file string, module any) error {
|
||||
err := os.MkdirAll(filepath.Dir(file), os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return yamlcomment.NewEncoder(yaml.NewEncoder(f)).Encode(module)
|
||||
}
|
||||
|
||||
func ReadYaml(file string, module any) error {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return yaml.NewDecoder(f).Decode(module)
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type WebSocket struct {
|
||||
Heartbeat time.Duration
|
||||
}
|
||||
|
||||
func DefaultWebSocket() *WebSocket {
|
||||
return &WebSocket{Heartbeat: time.Second * 5}
|
||||
}
|
||||
|
||||
type WebSocketConfig func(*WebSocket)
|
||||
|
||||
func WithHeartbeatInterval(d time.Duration) WebSocketConfig {
|
||||
return func(ws *WebSocket) {
|
||||
ws.Heartbeat = d
|
||||
}
|
||||
}
|
||||
|
||||
func NewWebSocketServer(conf ...WebSocketConfig) *WebSocket {
|
||||
ws := DefaultWebSocket()
|
||||
for _, wsc := range conf {
|
||||
wsc(ws)
|
||||
}
|
||||
return ws
|
||||
}
|
||||
|
||||
func (ws *WebSocket) Server(w http.ResponseWriter, r *http.Request, Subprotocols []string, handler func(c *websocket.Conn) error) error {
|
||||
wsc, err := ws.NewWebSocketClient(w, r, nil, WithSubprotocols(Subprotocols))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wsc.Close()
|
||||
return handler(wsc)
|
||||
}
|
||||
|
||||
type UpgraderConf func(*websocket.Upgrader)
|
||||
|
||||
func WithSubprotocols(Subprotocols []string) UpgraderConf {
|
||||
return func(ug *websocket.Upgrader) {
|
||||
ug.Subprotocols = Subprotocols
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WebSocket) newUpgrader(conf ...UpgraderConf) *websocket.Upgrader {
|
||||
ug := &websocket.Upgrader{
|
||||
HandshakeTimeout: time.Second * 30,
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
for _, uc := range conf {
|
||||
uc(ug)
|
||||
}
|
||||
return ug
|
||||
}
|
||||
|
||||
func (ws *WebSocket) NewWebSocketClient(w http.ResponseWriter, r *http.Request, responseHeader http.Header, conf ...UpgraderConf) (*websocket.Conn, error) {
|
||||
conn, err := ws.newUpgrader(conf...).Upgrade(w, r, responseHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
Loading…
Reference in New Issue