将播放器更换为原生播放器

main
落雨楓 5 months ago
parent 5d7f192b45
commit d0a954abce

@ -16,6 +16,7 @@ type ServerMessage struct {
type RoomStateChangedMessage struct {
Type string `json:"type,omitempty"` // "roomInfo", "stateChanged", etc.
ServerTime int64 `json:"serverTime,omitempty"`
URL string `json:"url,omitempty"`
PlayTime int64 `json:"playTime,omitempty"`
Playing bool `json:"playing,omitempty"`

@ -123,6 +123,7 @@ func (r *RoomImpl) BroadcastRoomState() {
if r.broadcastRoomInfoLock.TryLock() {
go func() {
r.Broadcast("roomInfo", RoomStateChangedMessage{
ServerTime: time.Now().Unix(),
URL: r.GetUrl(),
UserStatus: r.GetAllUserStatus(),
Playing: r.GetPlaying(),
@ -133,6 +134,7 @@ func (r *RoomImpl) BroadcastRoomState() {
time.Sleep(time.Millisecond * 500)
r.Broadcast("roomInfo", RoomStateChangedMessage{
ServerTime: time.Now().Unix(),
URL: r.GetUrl(),
UserStatus: r.GetAllUserStatus(),
Playing: r.GetPlaying(),

@ -5,12 +5,23 @@ import (
socket "github.com/zishang520/socket.io/v2/socket"
)
type ClientData struct {
Room string `json:"room"`
Username string `json:"username"`
IsAdmin bool `json:"is_admin"`
}
var server *socket.Server
var router *gin.Engine
func init() {
serverOpts := socket.DefaultServerOptions()
serverOpts.SetConnectionStateRecovery(nil)
csrOpts := socket.ConnectionStateRecovery{}
csrOpts.SetMaxDisconnectionDuration(2 * 60 * 1000) // 2 minutes
csrOpts.SetSkipMiddlewares(true)
serverOpts.SetConnectionStateRecovery(&csrOpts)
server = socket.NewServer(nil, serverOpts)

@ -1,8 +1,8 @@
module movie-sync-server
go 1.21
go 1.24.1
toolchain go1.21.5
toolchain go1.24.5
require (
github.com/gin-contrib/static v0.0.1
@ -10,13 +10,13 @@ require (
github.com/joho/godotenv v1.4.0
github.com/samber/lo v1.39.0
github.com/sirupsen/logrus v1.9.0
github.com/zishang520/socket.io/v2 v2.0.5
github.com/zishang520/socket.io/v2 v2.5.0
)
require (
github.com/MercuryEngineering/CookieMonster v0.0.0-20180304172713-1584578b3403 // indirect
github.com/PuerkitoBio/goquery v1.8.0 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
@ -30,27 +30,30 @@ require (
github.com/itchyny/gojq v0.12.7 // indirect
github.com/itchyny/timefmt-go v0.1.3 // indirect
github.com/kkdai/youtube/v2 v2.7.18 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/onsi/ginkgo/v2 v2.12.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/quic-go/quic-go v0.40.0 // indirect
github.com/quic-go/quic-go v0.53.0 // indirect
github.com/quic-go/webtransport-go v0.6.0 // indirect
github.com/robertkrimen/otto v0.0.0-20211024170158-b87d35c0b86f // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/zishang520/engine.io-go-parser v1.2.3 // indirect
github.com/zishang520/engine.io/v2 v2.0.3 // indirect
github.com/zishang520/socket.io-go-parser/v2 v2.0.4 // indirect
go.uber.org/mock v0.3.0 // indirect
github.com/zishang520/engine.io-go-parser v1.3.2 // indirect
github.com/zishang520/engine.io/v2 v2.5.0 // indirect
github.com/zishang520/socket.io-go-parser/v2 v2.5.0 // indirect
github.com/zishang520/webtransport-go v0.9.1 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/tools v0.22.0 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
)
@ -62,7 +65,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/iawia002/lux v0.18.0
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
@ -75,10 +78,10 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

@ -4,6 +4,8 @@ github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0g
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
@ -80,6 +82,8 @@ github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/iawia002/lia v0.1.0 h1:IzgR5pnOEt3bABB3TtcK5UCXkzcuc7+3GYxT0cVpYfU=
github.com/iawia002/lia v0.1.0/go.mod h1:Jxu7iNh5z17HWuLedH3jwh09aa5SO3g2BI2Ct87aXpY=
@ -96,6 +100,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kkdai/youtube/v2 v2.7.18 h1:bVP60bULmCZg5H9GsLLeBx0ONHEsZwdSrzPrVCVWJ1k=
github.com/kkdai/youtube/v2 v2.7.18/go.mod h1:CeUYFc227iiNNpEaipwmJIYPNsuH6rb/8H3HpWEG63U=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@ -137,10 +143,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
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/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/quic-go/quic-go v0.40.0 h1:GYd1iznlKm7dpHD7pOVpUvItgMPo/jrMgDWZhMCecqw=
github.com/quic-go/quic-go v0.40.0/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=
github.com/quic-go/quic-go v0.53.0 h1:QHX46sISpG2S03dPeZBgVIZp8dGagIaiu2FiVYvpCZI=
github.com/quic-go/quic-go v0.53.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/quic-go/webtransport-go v0.6.0 h1:CvNsKqc4W2HljHJnoT+rMmbRJybShZ0YPFDD3NxaZLY=
github.com/quic-go/webtransport-go v0.6.0/go.mod h1:9KjU4AEBqEQidGHNDkZrb8CAa1abRaosM2yGOyiikEc=
github.com/robertkrimen/otto v0.0.0-20211024170158-b87d35c0b86f h1:a7clxaGmmqtdNTXyvrp/lVO/Gnkzlhc/+dLs5v965GM=
@ -179,33 +189,53 @@ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1z
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zishang520/engine.io-go-parser v1.2.3 h1:y++zdMKIFgyVvH60TEEHw8gdJkS/qy22wesdALoh+HA=
github.com/zishang520/engine.io-go-parser v1.2.3/go.mod h1:UrXBVZWQgyHDITYmhnxi2d+NpEWBN8dACboD4dXcx38=
github.com/zishang520/engine.io-go-parser v1.3.2 h1:aEVrhQVhfk99Ct6htNffgHydUBC4dGclO/OXPz5CSy0=
github.com/zishang520/engine.io-go-parser v1.3.2/go.mod h1:fg/R4V7aytYwUTu4lGcPdjenDSXFWLlkDAGewWVOo3o=
github.com/zishang520/engine.io/v2 v2.0.3 h1:YXT4ZF40k1hyjbh4IBbP+hw4mp7F3Mx9ZwoIJm39b4Y=
github.com/zishang520/engine.io/v2 v2.0.3/go.mod h1:HM5pZZMFI/dNNmuwePLsaiGM67VY6vxJ5Isp+QRylMU=
github.com/zishang520/engine.io/v2 v2.5.0 h1:0ayZCt51c8lntxG5AWoM2mX40ryZlvRodAULXB1XK/s=
github.com/zishang520/engine.io/v2 v2.5.0/go.mod h1:ohfMsnzOCA9NEklEGiQ5Y9j6cWvzLNeVFaB+Bkn1KcQ=
github.com/zishang520/socket.io-go-parser/v2 v2.0.4 h1:PxzWPrTxueiKXFZJQduJ9sIIzsBbHMi9XO5nTLbIjS4=
github.com/zishang520/socket.io-go-parser/v2 v2.0.4/go.mod h1:fei4NeEGrSJK+uQPnuI4WVm8h+WLF/kVpDHVjeI+0bA=
github.com/zishang520/socket.io-go-parser/v2 v2.5.0 h1:uGKTwcH2qrrs9uwzfCo3ems+qUJZo9beZLK8ZXOwKYo=
github.com/zishang520/socket.io-go-parser/v2 v2.5.0/go.mod h1:GK9GIIs/KQbBKfnxgZJMBYSlsFemB2rL7EFUn0kmTfM=
github.com/zishang520/socket.io/v2 v2.0.5 h1:CImu9z6YKFif2mMX2b3y2OUhxxH8nz01PqP6+W6dXy4=
github.com/zishang520/socket.io/v2 v2.0.5/go.mod h1:r+spG2g+Q0lxhgTHevGl7/h4DzkKrO00i8AEF9vj2PQ=
github.com/zishang520/socket.io/v2 v2.5.0 h1:+KdZLbl4wWVzyI84RG+h21t8OY4ifz/Oo6qwgt9zHl8=
github.com/zishang520/socket.io/v2 v2.5.0/go.mod h1:+GyoPyakXDS6KsW81RAQpDA9+mJBXbcYcQ+Itx2D+rU=
github.com/zishang520/webtransport-go v0.9.1 h1:Y3gqPM8cIDvQILsTyXJ5G9fp2PYqGqLI2z+QXpgboQc=
github.com/zishang520/webtransport-go v0.9.1/go.mod h1:IgNAD6qLe3oWu7MSSkjusRNftpvjYxWjI4LmoH4VEyY=
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -222,6 +252,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -232,16 +264,22 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
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/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
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.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E=
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
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.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
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=

@ -63,6 +63,11 @@ func EventHandler() {
logrus.Infof("client disconnected: %s", client.Id())
room.DisconnectEndpoint(client)
})
// if client.Recovered() {
// // 尝试恢复连接状态
// room.JoinRecovered(client)
// }
})
}

@ -3,6 +3,7 @@ package room
import (
"movie-sync-server/entities"
"movie-sync-server/utils"
"time"
"github.com/sirupsen/logrus"
"github.com/zishang520/socket.io/v2/socket"
@ -15,14 +16,20 @@ func GetInfoEndpoint(client *socket.Socket, cliMsg *entities.ClientMessage) []by
u := r.GetUser(userID)
if u != nil {
u.Send(entities.MessageTypeRoomInfo, entities.RoomStateChangedMessage{
ServerTime: time.Now().Unix(),
URL: r.GetUrl(),
UserStatus: r.GetAllUserStatus(),
Playing: r.GetPlaying(),
PlayTime: r.GetPlayTime(),
})
} else {
logrus.Warnf("user [%s] not in room [%s]", userID, room)
rawMsg, err := utils.StructToMapViaJSON(entities.RoomStateChangedMessage{
ServerTime: time.Now().Unix(),
URL: r.GetUrl(),
UserStatus: r.GetAllUserStatus(),
Playing: r.GetPlaying(),
PlayTime: r.GetPlayTime(),
})
if err != nil {
logrus.WithError(err).Error("json marshal error")

@ -3,6 +3,7 @@ package room
import (
"movie-sync-server/entities"
"movie-sync-server/utils"
"time"
"github.com/sirupsen/logrus"
"github.com/zishang520/socket.io/v2/socket"
@ -17,8 +18,11 @@ func InitEndpoint(client *socket.Socket, cliMsg *entities.ClientMessage) []byte
// 发送当前房间状态
if r, ok := entities.GetCinema().GetRoom(roomname); ok {
rawMsg, err := utils.StructToMapViaJSON(entities.RoomStateChangedMessage{
ServerTime: time.Now().Unix(),
URL: r.GetUrl(),
UserStatus: r.GetAllUserStatus(),
Playing: r.GetPlaying(),
PlayTime: r.GetPlayTime(),
})
if err != nil {
logrus.WithError(err).Error("marshal error")
@ -27,8 +31,11 @@ func InitEndpoint(client *socket.Socket, cliMsg *entities.ClientMessage) []byte
client.Emit(entities.MessageTypeRoomInfo, rawMsg)
} else {
rawMsg, err := utils.StructToMapViaJSON(entities.RoomStateChangedMessage{
ServerTime: time.Now().Unix(),
URL: "",
UserStatus: []entities.UserStatus{},
Playing: false,
PlayTime: 0,
})
if err != nil {
logrus.WithError(err).Error("marshal error")

@ -9,31 +9,40 @@ import (
)
func JoinEndpoint(client *socket.Socket, cliMsg *entities.ClientMessage) []byte {
roomname, showName := cliMsg.Room, cliMsg.UserName
username := string(client.Id())
if roomname == "" || username == "" || showName == "" {
logrus.Warnf("room or username is empty: room: [%s], username: [%s], showName: [%s]", roomname, username, showName)
roomname, username := cliMsg.Room, cliMsg.UserName
userID := string(client.Id())
clientData, ok := client.Data().(*entities.ClientData)
if !ok {
clientData = &entities.ClientData{
Room: roomname,
}
}
if roomname == "" || userID == "" || username == "" {
logrus.Warnf("room or username is empty: room: [%s], username: [%s], showName: [%s]", roomname, userID, username)
return nil
}
var newUser entities.User = &entities.UserImpl{}
//首先判断当前用户是否想要加入已有的房间,如果房间不存在则新建房间
joined := false
newUser.SetID(userID)
newUser.SetSocket(client)
newUser.SetUsername(username)
var joinedRoom entities.Room
if r, ok := entities.GetCinema().GetRoom(roomname); ok {
u := r.GetUser(username)
u := r.GetUser(userID)
if u != nil {
logrus.Warnf("user [%s] already in room [%s]", username, roomname)
logrus.Warnf("user [%s] already in room [%s]", userID, roomname)
return nil
}
joinedRoom = r
newUser.SetID(username)
newUser.SetSocket(client)
newUser.SetUsername(showName)
if cliMsg.Password != "" {
if cliMsg.Password != conf.ServerSetting.HostPassword {
logrus.Warnf("user [%s] join room [%s] failed: incorrect password", username, roomname)
logrus.Warnf("user [%s] join room [%s] failed: incorrect password", userID, roomname)
newUser.Send(entities.MessageTypeMessage, entities.ServerNotificationMessage{
Severity: "error",
Message: "密码错误",
@ -43,25 +52,18 @@ func JoinEndpoint(client *socket.Socket, cliMsg *entities.ClientMessage) []byte
}
// 如果密码正确,则设置为管理员
logrus.Infof("user [%s] join room [%s] with password, set as admin", username, roomname)
logrus.Infof("user [%s] join room [%s] with password, set as admin", userID, roomname)
newUser.SetAdmin(true)
}
if oldUser := r.GetUser(username); oldUser != nil {
r.RemoveUser(username)
if oldUser := r.GetUser(userID); oldUser != nil {
r.RemoveUser(userID)
}
r.AddUser(newUser)
joined = true
}
if !joined {
} else {
// 创建房间时,检测密码
newUser.SetID(username)
newUser.SetUsername(showName)
newUser.SetSocket(client)
if cliMsg.Password != conf.ServerSetting.HostPassword {
logrus.Warnf("user [%s] join room [%s] failed: incorrect password", username, roomname)
logrus.Warnf("user [%s] join room [%s] failed: incorrect password", userID, roomname)
newUser.Send(entities.MessageTypeMessage, entities.ServerNotificationMessage{
Severity: "error",
Message: "密码错误",
@ -80,11 +82,15 @@ func JoinEndpoint(client *socket.Socket, cliMsg *entities.ClientMessage) []byte
entities.GetCinema().SetRoom(roomname, newRoom)
}
clientData.Username = username
clientData.IsAdmin = newUser.IsAdmin()
client.SetData(clientData)
newUser.Send("joined", entities.UserJoinLeaveMessage{
Type: "joined",
UserInfo: entities.UserStatus{
UserID: username,
UserName: showName,
UserID: userID,
UserName: username,
IsAdmin: newUser.IsAdmin(),
},
})
@ -92,8 +98,8 @@ func JoinEndpoint(client *socket.Socket, cliMsg *entities.ClientMessage) []byte
joinedRoom.Broadcast(entities.MessageTypeUserJoin, entities.UserJoinLeaveMessage{
Type: entities.MessageTypeUserJoin,
UserInfo: entities.UserStatus{
UserID: username,
UserName: showName,
UserID: userID,
UserName: username,
IsAdmin: newUser.IsAdmin(),
},
})
@ -101,6 +107,80 @@ func JoinEndpoint(client *socket.Socket, cliMsg *entities.ClientMessage) []byte
joinedRoom.GetUsers()
joinedRoom.BroadcastRoomState()
logrus.Infof("user [%s] join room [%s] success", username, roomname)
logrus.Infof("user [%s] join room [%s] success", userID, roomname)
return nil
}
func JoinRecovered(client *socket.Socket) {
userID := string(client.Id())
clientData, ok := client.Data().(*entities.ClientData)
if !ok {
return
}
if clientData.Room == "" || clientData.Username == "" {
logrus.Warnf("client data is incomplete: room [%s], username [%s]", clientData.Room, clientData.Username)
return
}
roomname := clientData.Room
username := clientData.Username
isAdmin := clientData.IsAdmin
logrus.Infof("recovering user [%s] in room [%s]", username, roomname)
var newUser entities.User = &entities.UserImpl{}
//首先判断当前用户是否想要加入已有的房间,如果房间不存在则新建房间
newUser.SetID(userID)
newUser.SetSocket(client)
newUser.SetUsername(username)
newUser.SetAdmin(isAdmin)
var joinedRoom entities.Room
if r, ok := entities.GetCinema().GetRoom(roomname); ok {
u := r.GetUser(userID)
if u != nil {
logrus.Warnf("user [%s] already in room [%s]", userID, roomname)
return
}
joinedRoom = r
if oldUser := r.GetUser(userID); oldUser != nil {
r.RemoveUser(userID)
}
r.AddUser(newUser)
} else {
var newRoom entities.Room = &entities.RoomImpl{}
newRoom.SetName(roomname)
newRoom.InitUsers()
newRoom.AddUser(newUser)
joinedRoom = newRoom
entities.GetCinema().SetRoom(roomname, newRoom)
}
client.SetData(clientData)
newUser.Send("joined", entities.UserJoinLeaveMessage{
Type: "joined",
UserInfo: entities.UserStatus{
UserID: userID,
UserName: username,
IsAdmin: newUser.IsAdmin(),
},
})
joinedRoom.Broadcast(entities.MessageTypeUserJoin, entities.UserJoinLeaveMessage{
Type: entities.MessageTypeUserJoin,
UserInfo: entities.UserStatus{
UserID: userID,
UserName: username,
IsAdmin: newUser.IsAdmin(),
},
})
joinedRoom.GetUsers()
joinedRoom.BroadcastRoomState()
logrus.Infof("user [%s] re-join room [%s] success", userID, roomname)
}

@ -1,25 +1,15 @@
import '@vidstack/react/player/styles/base.css'
import {
MediaPlayer,
MediaProvider,
MediaPlayerInstance,
isHLSProvider,
MediaProviderAdapter,
MediaProviderChangeEvent,
MediaPlayerState,
} from '@vidstack/react'
import { VideoLayout } from './video-control'
import { useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useRef } from 'react'
import { socket } from './socket'
import { ClientMessage, ClientUserStatus, RoomState, RoomStateChangedMessage, SetTimeMessage } from '@/lib/types/message'
import { $playerState, $userInfo, $userStatus } from '@/store/player'
import { useStore as useNanoStore } from '@nanostores/react'
import HLS from 'hls.js'
import { getUserPlayTime, getUserPlayTimeSeconds } from '@/lib/utils'
import { getUserPlayTimeSeconds, unixTimestampWithOffset } from '@/lib/utils'
const TIME_SYNC_ALLOWANCE = 5 // 如果播放时间与服务器时间差小于此值,则认为是同步的
export const Player = ({ roomName }: { roomName: string }) => {
const player = useRef<HTMLVideoElement | null>(null)
const userInfo = useNanoStore($userInfo)
const prevUserState = useRef<ClientUserStatus>({
playTime: 0,
@ -27,75 +17,111 @@ export const Player = ({ roomName }: { roomName: string }) => {
playing: false,
})
const prevRoomState = useRef<RoomState>({})
let player = useRef<MediaPlayerInstance>(null)
const prevPlayerState = useRef<MediaPlayerState | null>(null)
const isUserStartPlay = useRef(false)
const playerState = useNanoStore($playerState)
useEffect(() => {
/** 与服务器时间之间的时差 */
const serverTimeAdjust = useRef<number>(0)
const handleUserStateUpdated = useCallback((force: boolean = false) => {
if (!player.current) {
return
}
// Subscribe to state updates.
return player.current.subscribe((state) => {
if (prevPlayerState.current?.seeking && !state.seeking && userInfo?.isAdmin) {
console.log('seeking, update time')
socket.emit("setTime", {
username: userInfo?.username,
playTime: Math.floor(state.currentTime),
room: roomName
} as ClientMessage)
}
if (!prevPlayerState.current?.playing && state.playing) {
console.log('跳转到最新播放时间')
// 开始播放时,跳转到最新的播放时间
if (player.current &&
($userInfo.value?.isAdmin && !isUserStartPlay.current) && // 如果是管理员且用户没有开始播放,则跳转到最新的播放时间
prevRoomState.current.playing &&
prevRoomState.current.playTime !== undefined &&
prevRoomState.current.playTimeUpdateTime !== undefined) {
const deltaTime = (Date.now() / 1000) - prevRoomState.current.playTimeUpdateTime
const computedTime = getUserPlayTimeSeconds(prevUserState.current, serverTimeAdjust.current)
const playerTime = Math.floor(player.current?.currentTime ?? 0)
const isPlaying = !player.current?.paused
player.current.currentTime = prevRoomState.current.playTime + deltaTime
}
// 标记用户开始播放
isUserStartPlay.current = true;
if (force || isPlaying !== prevUserState.current.playing ||
(isPlaying && Math.abs(playerTime - computedTime) > TIME_SYNC_ALLOWANCE)) {
prevUserState.current = {
playTime: playerTime,
updateTime: unixTimestampWithOffset(serverTimeAdjust.current),
playing: isPlaying
}
prevPlayerState.current = {...state}
})
}, [userInfo, roomName, playerState?.url])
socket.emit("updateUserState", {
username: $userInfo.value?.username,
room: roomName,
playTime: playerTime,
playing: isPlaying,
} as ClientMessage)
}
}, [roomName])
const handleSeeked = useCallback(() => {
if (!player.current) {
return
}
const currentTime = Math.floor(player.current.currentTime)
console.log('seeked to', currentTime)
if ($userInfo.value?.isAdmin) {
socket.emit("setTime", {
username: $userInfo.value.username,
playTime: currentTime,
room: roomName
} as ClientMessage)
}
}, [roomName])
const handlePlay = useCallback(() => {
if (!player.current) {
return
}
handleUserStateUpdated(true)
// 开始播放时,跳转到最新的播放时间
if (player.current &&
($userInfo.value?.isAdmin && !isUserStartPlay.current) && // 如果是管理员且用户没有开始播放,则跳转到最新的播放时间
prevRoomState.current.playing &&
prevRoomState.current.playTime !== undefined &&
prevRoomState.current.playTimeUpdateTime !== undefined) {
const deltaTime = unixTimestampWithOffset(serverTimeAdjust.current) - prevRoomState.current.playTimeUpdateTime
player.current.currentTime = prevRoomState.current.playTime + deltaTime
}
// 如果是管理员,则发送播放事件
if ($userInfo.value?.isAdmin) {
socket.emit("play", {
username: $userInfo.value.username,
room: roomName
} as ClientMessage)
socket.emit("setTime", {
playTime: Math.floor(player.current.currentTime),
room: roomName,
username: $userInfo.value.username,
} as ClientMessage)
}
// 标记用户开始播放
isUserStartPlay.current = true
}, [roomName, handleUserStateUpdated])
const handlePause = useCallback(() => {
if (!player.current) {
return
}
console.log('paused')
if ($userInfo.value?.isAdmin) {
socket.emit("pause", {
username: $userInfo.value.username,
room: roomName
} as ClientMessage)
}
handleUserStateUpdated(true)
}, [roomName, handleUserStateUpdated])
useEffect(() => {
const interval = setInterval(() => {
if (!player.current) {
return
}
const computedTime = getUserPlayTimeSeconds(prevUserState.current)
const playerTime = Math.floor(player.current?.currentTime ?? 0)
const isPlaying = !player.current?.paused
if (isPlaying !== prevUserState.current.playing ||
(isPlaying && Math.abs(playerTime - computedTime) > TIME_SYNC_ALLOWANCE)) {
prevUserState.current = {
playTime: playerTime,
updateTime: Math.floor(Date.now() / 1000),
playing: isPlaying
}
socket.emit("updateUserState", {
username: userInfo?.username,
room: roomName,
playTime: playerTime,
playing: isPlaying,
} as ClientMessage)
}
handleUserStateUpdated()
}, 1000)
return () => clearInterval(interval)
}, [userInfo?.username, roomName, prevUserState])
}, [handleUserStateUpdated])
useEffect(() => {
function onDisconnect(e: any, d: any) {
@ -109,10 +135,12 @@ export const Player = ({ roomName }: { roomName: string }) => {
return
}
serverTimeAdjust.current = msg.serverTime ? msg.serverTime - (Date.now() / 1000) : 0
prevRoomState.current = {
url: msg.url,
playTime: msg.playTime,
playTimeUpdateTime: Date.now() / 1000,
playTimeUpdateTime: unixTimestampWithOffset(serverTimeAdjust.current),
playing: msg.playing,
userStatus: msg.userStatus,
}
@ -129,34 +157,51 @@ export const Player = ({ roomName }: { roomName: string }) => {
if (msg.playTime !== undefined &&
!$userInfo.value?.isAdmin && // 如果是管理员,则不需要更新播放时间
player.current?.state.canPlay && !player.current?.paused) {
player.current &&
player.current.readyState >= 2 && !player.current.paused) {
console.log('update play time to', msg.playTime)
// 检测当前播放时间
const localTime = Math.floor(player.current.currentTime)
const serverTime = msg.playTime
if (Math.abs(localTime - serverTime) > TIME_SYNC_ALLOWANCE) {
// 如果本地时间与服务器时间差异过大,则重置播放时间
player.current.currentTime = serverTime
$playerState.set({
...$playerState.value,
playTime: serverTime,
})
try {
player.current.currentTime = serverTime
$playerState.set({
...$playerState.value,
playTime: serverTime,
})
handleUserStateUpdated(true)
} catch (error) {
console.warn('Failed to set video currentTime:', error)
}
}
}
}
function onPause() {
console.log('pause')
if (player.current) {
if (!$userInfo.value?.isAdmin && player.current) {
player.current.pause()
handleUserStateUpdated(true)
}
}
function onPlay() {
console.log('play')
if (isUserStartPlay.current && // 仅在用户已经开始播放的情况下触发
player.current && player.current?.state.canPlay) {
player.current.play()
console.log('play', { isAdmin: $userInfo.value?.isAdmin, isUserStartPlay: isUserStartPlay.current, playerReady: player.current?.readyState})
if (!$userInfo.value?.isAdmin &&
isUserStartPlay.current && // 仅在用户已经开始播放的情况下触发
player.current && player.current.readyState >= 2) {
console.log('play video')
try {
player.current.play().then(() => {
handleUserStateUpdated(true);
}).catch(error => {
console.warn('Failed to play video:', error)
})
} catch (error) {
console.warn('Failed to call play():', error)
}
}
}
@ -168,7 +213,11 @@ export const Player = ({ roomName }: { roomName: string }) => {
return
}
player.current.currentTime = msg.playTime
try {
player.current.currentTime = msg.playTime
} catch (error) {
console.warn('Failed to set video currentTime:', error)
}
}
socket.on('disconnect', onDisconnect)
@ -184,33 +233,25 @@ export const Player = ({ roomName }: { roomName: string }) => {
socket.off('play', onPlay)
socket.off('setTime', onSetTime)
}
}, [])
function onProviderChange(
provider: MediaProviderAdapter | null,
nativeEvent: MediaProviderChangeEvent,
) {
if (isHLSProvider(provider)) {
provider.library = HLS
}
}
}, [roomName, handleUserStateUpdated])
return (
<div>
{playerState?.url && <MediaPlayer
key={playerState.url}
<video
className="w-full aspect-video bg-slate-900 text-white font-sans overflow-hidden rounded-md ring-media-focus data-[focus]:ring-4"
title="test"
src={playerState.url}
crossorigin
playsinline
onProviderChange={onProviderChange}
title="影片"
src={playerState?.url}
crossOrigin="anonymous"
playsInline
controls
controlsList="nodownload,noseek"
onSeeked={handleSeeked}
onPlay={handlePlay}
onPause={handlePause}
onEnded={handlePause}
ref={player}
>
<MediaProvider>
</MediaProvider>
<VideoLayout roomName={roomName} />
</MediaPlayer>}
</video>
</div>
)
}

@ -1,6 +1,6 @@
import { $userInfo, $userStatus } from "@/store/player"
import { useStore } from '@nanostores/react'
import { useEffect, useState } from "react"
import { useEffect, useMemo, useState } from "react"
import {
Table,
TableBody,
@ -14,9 +14,9 @@ import { socket } from "./socket"
import { ClientMessage } from "@/lib/types/message"
import { getUserPlayTime } from "@/lib/utils"
export const UserList = ({ roomName }: { roomName: string }) => {
export const UserList = ({ roomName, serverTimeAdjust }: { roomName: string, serverTimeAdjust: number }) => {
const userStatus = useStore($userStatus)
const [tick, setTick] = useState(0)
const [_, setTick] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
@ -25,6 +25,13 @@ export const UserList = ({ roomName }: { roomName: string }) => {
return () => clearInterval(interval)
}, [])
const sortedUserStatus = useMemo(() => {
return [...userStatus].sort((a, b) => {
// 按照username排序
return (a.username || '').localeCompare(b.username || '')
})
}, [userStatus])
return (
<Table key={userStatus.length}>
<TableHeader>
@ -35,10 +42,10 @@ export const UserList = ({ roomName }: { roomName: string }) => {
</TableRow>
</TableHeader>
<TableBody>
{userStatus.map((user) => (
{sortedUserStatus.map((user) => (
<TableRow key={user.userID}>
<TableCell className="font-medium">{user.username}</TableCell>
<TableCell className="text-center">{getUserPlayTime(user)}</TableCell>
<TableCell className="text-center">{getUserPlayTime(user, serverTimeAdjust)}</TableCell>
<TableCell className="text-center">{user.playing == true ? '播放中' : '暂停中'}</TableCell>
</TableRow>
))}

@ -32,10 +32,17 @@ export interface ServerNotificationMessage extends ServerMessage {
}
export interface RoomState {
/** 播放地址 */
url?: string;
/** 服务器时间 */
serverTime?: number;
/** 播放时间更新时间 */
playTimeUpdateTime?: number;
/** 播放时间 */
playTime?: number;
/** 是否正在播放 */
playing?: boolean;
/** 当前房间状态的用户状态 */
userStatus?: UserStatus[];
}

@ -6,16 +6,21 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function getUserPlayTimeSeconds(userStatus: UserStatus | undefined): number {
if (!userStatus || !userStatus.playTime || !userStatus.updateTime) {
export function getUserPlayTimeSeconds(userStatus: UserStatus | undefined, offsetTime: number): number {
if (!userStatus || !userStatus.playTime) {
return 0
}
const deltaTime = (Date.now() / 1000) - userStatus.updateTime
if (!userStatus.updateTime) {
return userStatus.playTime
}
const deltaTime = unixTimestampWithOffset(offsetTime) - userStatus.updateTime
return userStatus.playTime + deltaTime
}
export function getUserPlayTime(userStatus: UserStatus | undefined): string {
const totalTime = getUserPlayTimeSeconds(userStatus)
export function getUserPlayTime(userStatus: UserStatus | undefined, offsetTime: number): string {
const totalTime = getUserPlayTimeSeconds(userStatus, offsetTime)
if (totalTime < 0) {
return "00:00:00"
}
@ -23,4 +28,8 @@ export function getUserPlayTime(userStatus: UserStatus | undefined): string {
const minutes = Math.floor((totalTime % 3600) / 60);
const seconds = Math.floor(totalTime % 60);
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}
export function unixTimestampWithOffset(offset: number): number {
return Math.floor(Date.now() / 1000) + offset;
}

@ -4,7 +4,9 @@ import { Theme } from '@radix-ui/themes';
export default function Document() {
return (
<Html lang="en">
<Head />
<Head>
<meta name="robots" content="noindex, nofollow" />
</Head>
<body>
<Theme>
<Main />

@ -8,7 +8,7 @@ import { ClientMessage, RoomStateChangedMessage, ServerMessage, UserJoinLeaveMes
import { $playerState, $userInfo, $userStatus } from '@/store/player'
import { useStore } from '@nanostores/react'
import { useRouter } from 'next/router'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import "@radix-ui/themes/components/tabs"
import { Label } from '@/components/ui/label'
import { useSignal } from '@/lib/signal'
@ -18,10 +18,13 @@ export default function Page() {
const router = useRouter()
const userInfo = useStore($userInfo)
const prevRoomState = useRef<RoomStateChangedMessage | undefined>()
const joinRole = useSignal<string>('user')
const urlInput = useSignal<string | undefined>()
const urlInput = useSignal<string>('')
const isRoomEmpty = useSignal(false)
const isJoined = useSignal(false)
const serverTimeAdjust = useSignal<number>(0)
const roomName = router.query.room as string
@ -31,10 +34,23 @@ export default function Page() {
socket.emit("init", {
room: roomName,
} as ClientMessage)
if (isJoined.value) {
// 恢复连接状态
socket.emit('join', {
username: $userInfo.value?.username,
room: roomName,
password: $userInfo.value?.password,
} as ClientMessage)
}
}
function onRoomInfo(d: any) {
const msg = d as RoomStateChangedMessage
serverTimeAdjust.value = msg.serverTime ? msg.serverTime - (Date.now() / 1000) : 0
console.log('serverTimeAdjust', serverTimeAdjust.value)
if (!msg.userStatus || msg.userStatus.length === 0) {
isRoomEmpty.value = true
joinRole.value = 'admin'
@ -47,6 +63,13 @@ export default function Page() {
$userStatus.set([
...msg.userStatus
])
if (prevRoomState.current?.url !== msg.url) {
// 更新视频链接
urlInput.value = msg.url || ''
}
prevRoomState.current = d;
}
function onJoined(d: any) {
@ -115,7 +138,7 @@ export default function Page() {
return (
<div className='flex flex-col lg:flex-row m-2 justify-center'>
<Head>
<title>{roomName} | </title>
{roomName ? <title>{roomName} | </title> : <title></title>}
</Head>
{isJoined.value && <div className='w-full lg:mr-2'>
<Player roomName={roomName} />
@ -187,7 +210,12 @@ export default function Page() {
/>
<Button onClick={handleSetUrlClick}></Button>
</div>}
{roomName && <UserList roomName={roomName} />}
{roomName && (
<UserList
roomName={roomName}
serverTimeAdjust={serverTimeAdjust.value}
/>
)}
</div>
</div>)
}

@ -1,3 +1,3 @@
- [ ] 手机端播放器问题
- [ ] 自动重连后没有join的问题
- [ ] 播放结束后没有停止计时
- [x] 手机端播放器问题
- [x] 自动重连后没有join的问题
- [x] 播放结束后没有停止计时

Loading…
Cancel
Save