diff --git a/config/livekit.yaml b/config/livekit.yaml new file mode 100644 index 000000000..1237e9e43 --- /dev/null +++ b/config/livekit.yaml @@ -0,0 +1,17 @@ +port: 7880 +rtc: + tcp_port: 7881 + port_range_start: 50000 + port_range_end: 60000 + use_external_ip: true + +redis: + address: redis:6379 + password: openIM123 + +# LiveKit 要求 API secret 至少 32 字符;生产环境请替换为强随机串并与 openim-rpc-rtc.yml 中 apiSecret 一致 +keys: + devkey: openim-livekit-default-secret-32chars-min + +logging: + level: info diff --git a/config/openim-rpc-rtc.yml b/config/openim-rpc-rtc.yml index b2f558273..5a68ac5b1 100644 --- a/config/openim-rpc-rtc.yml +++ b/config/openim-rpc-rtc.yml @@ -18,14 +18,14 @@ prometheus: liveKit: # LiveKit server address reachable from the RTC service (internal/backend address) - # Example: http://livekit:7880 - internalAddress: http://localhost:7880 + # When deployed via docker-compose, use the service name 'livekit' + internalAddress: http://livekit:7880 # LiveKit server address reachable from clients (external/public address) - # Example: wss://livekit.example.com - externalAddress: ws://localhost:7880 + # Production should use wss://livekit.example.com with TLS + externalAddress: ws://192.168.1.91:7880 # LiveKit API key (configured in your LiveKit server) apiKey: devkey - # LiveKit API secret (configured in your LiveKit server) - apiSecret: secret + # LiveKit API secret(须 ≥32 字符,须与 config/livekit.yaml keys 中对应值一致) + apiSecret: openim-livekit-default-secret-32chars-min # Token expiry in seconds (default: 3600 = 1 hour) tokenExpiry: 3600 diff --git a/docker-compose.yml b/docker-compose.yml index c7f30806c..6a38d0d48 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -163,6 +163,24 @@ services: networks: - openim + livekit: + image: "${LIVEKIT_IMAGE:-livekit/livekit-server:latest}" + container_name: livekit + restart: always + ports: + - "7880:7880" + - "7881:7881" + - "50000-50100:50000-50100/udp" + volumes: + - ./config/livekit.yaml:/etc/livekit.yaml + command: --config /etc/livekit.yaml --node-ip=0.0.0.0 + depends_on: + - redis + environment: + TZ: Asia/Shanghai + networks: + - openim + minio: image: "${MINIO_IMAGE}" ports: diff --git a/internal/api/prometheus_discovery.go b/internal/api/prometheus_discovery.go index 6ec563721..a914de313 100644 --- a/internal/api/prometheus_discovery.go +++ b/internal/api/prometheus_discovery.go @@ -108,3 +108,7 @@ func (p *PrometheusDiscoveryApi) MessageGateway(c *gin.Context) { func (p *PrometheusDiscoveryApi) MessageTransfer(c *gin.Context) { p.discovery(c, prommetrics.MessageTransferKeyName) } + +func (p *PrometheusDiscoveryApi) Rtc(c *gin.Context) { + p.discovery(c, p.config.Share.RpcRegisterName.Rtc) +} diff --git a/internal/api/router.go b/internal/api/router.go index 6f474a67f..3415706a1 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -12,6 +12,7 @@ import ( "github.com/openimsdk/protocol/group" "github.com/openimsdk/protocol/msg" "github.com/openimsdk/protocol/relation" + "github.com/openimsdk/protocol/rtc" "github.com/openimsdk/protocol/third" "github.com/openimsdk/protocol/user" @@ -103,6 +104,10 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co if err != nil { return nil, err } + rtcConn, err := client.GetConn(ctx, config.Share.RpcRegisterName.Rtc) + if err != nil { + return nil, err + } gin.SetMode(gin.ReleaseMode) r := gin.New() if v, ok := binding.Validator.Engine().(*validator.Validate); ok { @@ -301,6 +306,20 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co captchaGroup.POST("/verify", cp.VerifyCaptcha) } + { + rc := NewRtcApi(rtc.NewRtcServiceClient(rtcConn)) + rtcGroup := r.Group("/rtc") + rtcGroup.POST("/signal_message_assemble", rc.SignalMessageAssemble) + rtcGroup.POST("/signal_get_room_by_group_id", rc.SignalGetRoomByGroupID) + rtcGroup.POST("/signal_get_token_by_room_id", rc.SignalGetTokenByRoomID) + rtcGroup.POST("/signal_get_rooms", rc.SignalGetRooms) + rtcGroup.POST("/get_signal_invitation_info", rc.GetSignalInvitationInfo) + rtcGroup.POST("/get_signal_invitation_info_start_app", rc.GetSignalInvitationInfoStartApp) + rtcGroup.POST("/signal_send_custom_signal", rc.SignalSendCustomSignal) + rtcGroup.POST("/get_signal_invitation_records", rc.GetSignalInvitationRecords) + rtcGroup.POST("/delete_signal_records", rc.DeleteSignalRecords) + } + { statisticsGroup := r.Group("/statistics") statisticsGroup.POST("/user/register", u.UserRegisterCount) @@ -330,6 +349,7 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co proDiscoveryGroup.GET("/push", pd.Push) proDiscoveryGroup.GET("/msg_gateway", pd.MessageGateway) proDiscoveryGroup.GET("/msg_transfer", pd.MessageTransfer) + proDiscoveryGroup.GET("/rtc", pd.Rtc) } return r, nil } diff --git a/internal/api/rtc.go b/internal/api/rtc.go new file mode 100644 index 000000000..33436435c --- /dev/null +++ b/internal/api/rtc.go @@ -0,0 +1,65 @@ +// Copyright © 2024 OpenIM. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "github.com/gin-gonic/gin" + "github.com/openimsdk/protocol/rtc" + "github.com/openimsdk/tools/a2r" +) + +type RtcApi struct { + Client rtc.RtcServiceClient +} + +func NewRtcApi(client rtc.RtcServiceClient) RtcApi { + return RtcApi{Client: client} +} + +func (o *RtcApi) SignalMessageAssemble(c *gin.Context) { + a2r.Call(c, rtc.RtcServiceClient.SignalMessageAssemble, o.Client) +} + +func (o *RtcApi) SignalGetRoomByGroupID(c *gin.Context) { + a2r.Call(c, rtc.RtcServiceClient.SignalGetRoomByGroupID, o.Client) +} + +func (o *RtcApi) SignalGetTokenByRoomID(c *gin.Context) { + a2r.Call(c, rtc.RtcServiceClient.SignalGetTokenByRoomID, o.Client) +} + +func (o *RtcApi) SignalGetRooms(c *gin.Context) { + a2r.Call(c, rtc.RtcServiceClient.SignalGetRooms, o.Client) +} + +func (o *RtcApi) GetSignalInvitationInfo(c *gin.Context) { + a2r.Call(c, rtc.RtcServiceClient.GetSignalInvitationInfo, o.Client) +} + +func (o *RtcApi) GetSignalInvitationInfoStartApp(c *gin.Context) { + a2r.Call(c, rtc.RtcServiceClient.GetSignalInvitationInfoStartApp, o.Client) +} + +func (o *RtcApi) SignalSendCustomSignal(c *gin.Context) { + a2r.Call(c, rtc.RtcServiceClient.SignalSendCustomSignal, o.Client) +} + +func (o *RtcApi) GetSignalInvitationRecords(c *gin.Context) { + a2r.Call(c, rtc.RtcServiceClient.GetSignalInvitationRecords, o.Client) +} + +func (o *RtcApi) DeleteSignalRecords(c *gin.Context) { + a2r.Call(c, rtc.RtcServiceClient.DeleteSignalRecords, o.Client) +} diff --git a/internal/rpc/rtc/signal.go b/internal/rpc/rtc/signal.go index 869fd60c4..33d73bdb3 100644 --- a/internal/rpc/rtc/signal.go +++ b/internal/rpc/rtc/signal.go @@ -32,7 +32,7 @@ import ( "github.com/openimsdk/tools/log" "github.com/openimsdk/tools/mcontext" "github.com/openimsdk/tools/utils/datautil" - "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" ) // SignalMessageAssemble processes a signal request from the WebSocket gateway @@ -379,8 +379,8 @@ func (s *rtcServer) SignalSendCustomSignal(ctx context.Context, req *rtc.SignalS if uid == opUserID { continue } - if err := s.sendSignalingNotification(ctx, opUserID, uid, int32(constant.SingleChatType), nil, content); err != nil { - log.ZWarn(ctx, "sendSignalingNotification customSignal failed", err, "to", uid) + if err := s.sendCustomSignalNotification(ctx, opUserID, uid, int32(constant.SingleChatType), content); err != nil { + log.ZWarn(ctx, "sendCustomSignalNotification failed", err, "to", uid) } } return &rtc.SignalSendCustomSignalResp{}, nil @@ -462,10 +462,28 @@ func (s *rtcServer) sendSignalingNotification(ctx context.Context, sendID, recvI return err } -// marshalSignalReq serialises a SignalReq to JSON bytes using protojson, -// which correctly handles protobuf oneof fields for client-side parsing. +// sendCustomSignalNotification sends a CustomSignalNotification (1605) to a user. +func (s *rtcServer) sendCustomSignalNotification(ctx context.Context, sendID, recvID string, sessionType int32, content []byte) error { + now := time.Now().UnixMilli() + msgData := &sdkws.MsgData{ + SendID: sendID, + RecvID: recvID, + SessionType: sessionType, + ContentType: int32(constant.CustomSignalNotification), + MsgFrom: int32(constant.SysMsgType), + Content: content, + CreateTime: now, + SendTime: now, + ServerMsgID: uuid.New().String(), + ClientMsgID: uuid.New().String(), + Options: make(map[string]bool), + } + _, err := s.msgClient.MsgClient.SendMsg(ctx, &pbmsg.SendMsgReq{MsgData: msgData}) + return err +} + func marshalSignalReq(req *rtc.SignalReq) []byte { - b, _ := protojson.Marshal(req) + b, _ := proto.Marshal(req) return b } diff --git a/start-config.yml b/start-config.yml index 66051f962..d06738143 100644 --- a/start-config.yml +++ b/start-config.yml @@ -11,6 +11,7 @@ serviceBinaries: openim-rpc-group: 1 openim-rpc-friend: 1 openim-rpc-msg: 1 + openim-rpc-rtc: 1 openim-rpc-third: 1 toolBinaries: - check-free-memory