From d0a954abced91141d4eda2289221b14e4fa1793c Mon Sep 17 00:00:00 2001 From: Lex Lim Date: Sun, 20 Jul 2025 17:34:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=86=E6=92=AD=E6=94=BE=E5=99=A8=E6=9B=B4?= =?UTF-8?q?=E6=8D=A2=E4=B8=BA=E5=8E=9F=E7=94=9F=E6=92=AD=E6=94=BE=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- entities/message.go | 1 + entities/room.go | 2 + entities/server.go | 13 +- go.mod | 39 +++--- go.sum | 38 ++++++ services/handler.go | 5 + services/room/get_info.go | 7 + services/room/init.go | 7 + services/room/join.go | 136 +++++++++++++++---- src/components/player.tsx | 247 ++++++++++++++++++++--------------- src/components/user-list.tsx | 17 ++- src/lib/types/message.ts | 7 + src/lib/utils.ts | 19 ++- src/pages/_document.tsx | 4 +- src/pages/room/[room].tsx | 36 ++++- todo.md | 6 +- 16 files changed, 416 insertions(+), 168 deletions(-) diff --git a/entities/message.go b/entities/message.go index 11b1dbe..5dbd258 100644 --- a/entities/message.go +++ b/entities/message.go @@ -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"` diff --git a/entities/room.go b/entities/room.go index 218512e..0c65b91 100644 --- a/entities/room.go +++ b/entities/room.go @@ -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(), diff --git a/entities/server.go b/entities/server.go index 9f38e2c..e4ba7e3 100644 --- a/entities/server.go +++ b/entities/server.go @@ -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) diff --git a/go.mod b/go.mod index b2c6bf9..7ddef4f 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 5a9c4f3..3402c4c 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/services/handler.go b/services/handler.go index 97f653f..63dacb9 100644 --- a/services/handler.go +++ b/services/handler.go @@ -63,6 +63,11 @@ func EventHandler() { logrus.Infof("client disconnected: %s", client.Id()) room.DisconnectEndpoint(client) }) + + // if client.Recovered() { + // // 尝试恢复连接状态 + // room.JoinRecovered(client) + // } }) } diff --git a/services/room/get_info.go b/services/room/get_info.go index 1f1d047..3d4f8e8 100644 --- a/services/room/get_info.go +++ b/services/room/get_info.go @@ -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") diff --git a/services/room/init.go b/services/room/init.go index 518d61d..33bbac7 100644 --- a/services/room/init.go +++ b/services/room/init.go @@ -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") diff --git a/services/room/join.go b/services/room/join.go index 61b8fbd..2248a9c 100644 --- a/services/room/join.go +++ b/services/room/join.go @@ -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) +} diff --git a/src/components/player.tsx b/src/components/player.tsx index 9ab7fb4..9de0cba 100644 --- a/src/components/player.tsx +++ b/src/components/player.tsx @@ -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(null) + const userInfo = useNanoStore($userInfo) const prevUserState = useRef({ playTime: 0, @@ -27,75 +17,111 @@ export const Player = ({ roomName }: { roomName: string }) => { playing: false, }) const prevRoomState = useRef({}) - let player = useRef(null) - const prevPlayerState = useRef(null) const isUserStartPlay = useRef(false) const playerState = useNanoStore($playerState) - - useEffect(() => { + + /** 与服务器时间之间的时差 */ + const serverTimeAdjust = useRef(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 (
- {playerState?.url && - - - - } +
) } \ No newline at end of file diff --git a/src/components/user-list.tsx b/src/components/user-list.tsx index 516cec7..e725066 100644 --- a/src/components/user-list.tsx +++ b/src/components/user-list.tsx @@ -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 ( @@ -35,10 +42,10 @@ export const UserList = ({ roomName }: { roomName: string }) => { - {userStatus.map((user) => ( + {sortedUserStatus.map((user) => ( {user.username} - {getUserPlayTime(user)} + {getUserPlayTime(user, serverTimeAdjust)} {user.playing == true ? '播放中' : '暂停中'} ))} diff --git a/src/lib/types/message.ts b/src/lib/types/message.ts index c03aa20..ee7076b 100644 --- a/src/lib/types/message.ts +++ b/src/lib/types/message.ts @@ -32,10 +32,17 @@ export interface ServerNotificationMessage extends ServerMessage { } export interface RoomState { + /** 播放地址 */ url?: string; + /** 服务器时间 */ + serverTime?: number; + /** 播放时间更新时间 */ playTimeUpdateTime?: number; + /** 播放时间 */ playTime?: number; + /** 是否正在播放 */ playing?: boolean; + /** 当前房间状态的用户状态 */ userStatus?: UserStatus[]; } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index d4665fb..6807337 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -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; } \ No newline at end of file diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index 1ff489b..647e43b 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -4,7 +4,9 @@ import { Theme } from '@radix-ui/themes'; export default function Document() { return ( - + + +
diff --git a/src/pages/room/[room].tsx b/src/pages/room/[room].tsx index 51e53a3..93fe964 100644 --- a/src/pages/room/[room].tsx +++ b/src/pages/room/[room].tsx @@ -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() + const joinRole = useSignal('user') - const urlInput = useSignal() + const urlInput = useSignal('') const isRoomEmpty = useSignal(false) const isJoined = useSignal(false) + const serverTimeAdjust = useSignal(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 (
- 「{roomName}」的房间 | 一起看 + {roomName ? 「{roomName}」的房间 | 一起看 : 一起看} {isJoined.value &&
@@ -187,7 +210,12 @@ export default function Page() { />
} - {roomName && } + {roomName && ( + + )}
) } \ No newline at end of file diff --git a/todo.md b/todo.md index 44ec1cb..9051787 100644 --- a/todo.md +++ b/todo.md @@ -1,3 +1,3 @@ -- [ ] 手机端播放器问题 -- [ ] 自动重连后没有join的问题 -- [ ] 播放结束后没有停止计时 +- [x] 手机端播放器问题 +- [x] 自动重连后没有join的问题 +- [x] 播放结束后没有停止计时