diff --git a/frontend/src/page/local.tsx b/frontend/src/page/local.tsx index 3adc5b4..6d55e93 100644 --- a/frontend/src/page/local.tsx +++ b/frontend/src/page/local.tsx @@ -72,18 +72,18 @@ interface Bubble { interface WSMessage { body: Client; time: number; - type: "enter" | "leave" | "offer" | "answer" + type: "register" | "enter" | "leave" | "offer" | "answer" } interface Client { client_type: 'desktop' | 'mobile' | 'tablet'; app_type: 'web'; - room: string; ip: number; name: string; id: string; register_at: string; offer: RTCSessionDescription; + candidate: RTCIceCandidateInit; } interface Store { @@ -184,19 +184,26 @@ export const LocalSharing: React.FC = () => { return bubbles; }; - const updateClients = async (room?: string) => { - const res = await fetch(`/api/ulocal/clients?room=${room ? room : rtcStore.client?.room}`) - const jes = await res.json() as Resp - console.log('[D] update clients called, resp =', jes) - setRTCStore(val => { - return {...val, clients: jes.data} - }) + const updateClients = async () => { + setTimeout(async () => { + const res = await fetch(`/api/ulocal/clients`) + const jes = await res.json() as Resp + setRTCStore(val => { + return {...val, clients: jes.data} + }) + }, 500) } const handleWSEvent = async (e: MessageEvent) => { const msg = JSON.parse(e.data) as WSMessage console.log('[D] ws event msg =', msg) switch (msg.type) { + case "register": + const reg_resp = JSON.parse(e.data) as { body: Client } + setRTCStore(val => { + return {...val, client: reg_resp.body} + }) + break case "enter": await updateClients() break @@ -204,89 +211,119 @@ export const LocalSharing: React.FC = () => { await updateClients() break case "offer": - console.log('[D] rtc =', rtcStore.rtc) - const offer = JSON.parse(e.data) as { offer: RTCSessionDescriptionInit, id: number, room: string } - console.log('[D] offer =', offer) - await rtcStore.rtc?.setRemoteDescription(offer.offer) + const res_offer = JSON.parse(e.data) as { + offer: RTCSessionDescriptionInit, + id: number, + candidate: RTCIceCandidateInit + } + console.log('[D] offer res =', res_offer) + await rtcStore.rtc?.setRemoteDescription(res_offer.offer) const answer = await rtcStore.rtc?.createAnswer() await rtcStore.rtc?.setLocalDescription(answer) - console.log('[D] answer =', answer) - await fetch("/api/ulocal/answer", { - method: "POST", + await fetch('/api/ulocal/answer', { + method: 'POST', headers: {"Content-Type": "application/json"}, - body: JSON.stringify({id: offer.id, room: offer.room, answer: answer}) + body: JSON.stringify({id: rtcStore.client?.id, answer: answer}) }) break case "answer": - // const _answer = JSON.parse(e.data) as { answer: RTCSessionDescriptionInit, id: number, room: string } - // await rtcStore.rtc?.setRemoteDescription(_answer.answer) - // break + const res_answer = JSON.parse(e.data) as { answer: RTCSessionDescriptionInit, id: number } + await rtcStore.rtc?.setRemoteDescription(res_answer.answer) + break } } + // useEffect(() => { + // const fn = async () => { + // // const rtc = new RTCPeerConnection({iceServers: [{urls: "stun:stun.qq.com:3478"}]}) + // const rtc = new RTCPeerConnection({}) + // rtc.onconnectionstatechange = () => { + // console.log('rtc connection state =', rtc.connectionState) + // } + // const dataChannel = rtc.createDataChannel('fileTransfer', {ordered: true}); + // setupDataChannel(dataChannel); + // const waitCandidate = (): Promise => { + // return new Promise(resolve => { + // rtc.onicecandidate = (e) => { + // resolve(e.candidate) + // } + // }) + // } + // + // const waitNegotiation = (): Promise => { + // return new Promise(resolve => { + // rtc.onnegotiationneeded = async () => { + // const _offer = await rtc.createOffer() + // await rtc.setLocalDescription(_offer) + // resolve() + // } + // }) + // } + // + // await waitNegotiation() + // const candidate: RTCIceCandidate | null = await waitCandidate(); + // + // const res = await fetch("/api/ulocal/register", { + // method: "POST", + // headers: {"Content-Type": "application/json"}, + // body: JSON.stringify({candidate: candidate}) + // }) + // const jes = await res.json() as Resp; + // + // console.log('[D] register resp =', jes) + // + // if (!jes.data.id) { + // message.error("注册失败") + // throw new Error("register failed") + // } + // + // setRTCStore(val => { + // return { + // ...val, + // client: jes.data, + // candidate: candidate, + // offer: rtc.localDescription, + // rtc: rtc, + // } + // }) + // + // const api = `${window.location.protocol === 'https' ? 'wss' : 'ws'}://${window.location.host}/api/ulocal/ws?id=${jes.data.id}` + // console.log('[D] websocket url =', api) + // connect(api, {fn: handleWSEvent}) + // + // await updateClients() + // } + // + // fn() + // + // return () => close(); + // }, [] + // ); + +// 气泡点击处理 + const handleBubbleClick = async (bubble: Bubble) => { + const offer = await rtcStore.rtc?.createOffer() + await rtcStore.rtc?.setLocalDescription(offer) + // await fetch('/api/ulocal/offer', { + // method: 'POST', + // headers: {"Content-Type": "application/json"}, + // body: JSON.stringify({ + // id: bubble.id, + // offer: rtcStore.offer, + // candidate: rtcStore.candidate, + // }) + // }) + }; + useEffect(() => { const fn = async () => { + connect("/api/ulocal/ws", {fn: handleWSEvent}) + await updateClients() const rtc = new RTCPeerConnection({iceServers: [{urls: "stun:stun.qq.com:3478"}]}) - const dataChannel = rtc.createDataChannel('fileTransfer', {ordered: true}); - setupDataChannel(dataChannel); - const waitCandidate = new Promise(resolve => { - rtc.onicecandidate = (e) => { - resolve(e.candidate) - } - }) - const waitNegotiationneeded = new Promise(resolve => { - rtc.onnegotiationneeded = async () => { - const _offer = await rtc.createOffer() - await rtc.setLocalDescription(_offer) - resolve() - } - }) - - - await waitNegotiationneeded - const candidate: RTCIceCandidate | null = await waitCandidate; - if (!candidate) throw new Error("candidate is null") - - const res = await fetch("/api/ulocal/register", { - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify({candidate: candidate, offer: rtc.localDescription}) - }) - const jes = await res.json() as Resp; - - if (!jes.data.id) { - message.error("注册失败") - throw new Error("register failed") - } - - setRTCStore(val => { - return {...val, client: jes.data, candidate: candidate, offer: rtc.localDescription, rtc: rtc, ch: dataChannel} - }) - - const api = `${window.location.protocol === 'https' ? 'wss' : 'ws'}://${window.location.host}/api/ulocal/ws?id=${jes.data.id}` - //console.log('[D] websocket url =', api) - connect(api, {fn: handleWSEvent}) - - await updateClients(jes.data.room) - } - + setRTCStore(val => {return {...val, rtc: rtc}}) + }; fn() - - return () => close(); - }, []); - - // 气泡点击处理 - const handleBubbleClick = async (id: string) => { - await fetch('/api/ulocal/offer', { - method: 'POST', - headers: {"Content-Type": "application/json"}, - body: JSON.stringify({room: rtcStore.client?.room, id: id, offer: rtcStore.offer}) - }) - - setTimeout(() => { - rtcStore.ch?.send("hello, world") - }, 1000) - }; + }, []) return
@@ -305,7 +342,7 @@ export const LocalSharing: React.FC = () => { `${Math.random() * 0.5}s, ${0.5 + Math.random() * 2}s` }} - onClick={() => handleBubbleClick(bubble.id)} + onClick={() => handleBubbleClick(bubble)} > {bubble.name}
diff --git a/internal/api/api.go b/internal/api/api.go index cb7bf27..4e26b74 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -25,7 +25,6 @@ func Start(ctx context.Context) <-chan struct{} { { api := app.Group("/api/ulocal") - api.Post("/register", handler.LocalRegister()) api.Post("/offer", handler.LocalOffer()) api.Post("/answer", handler.LocalAnswer()) api.Get("/clients", handler.LocalClients()) diff --git a/internal/controller/room.go b/internal/controller/room.go index c76b1a0..4fa6338 100644 --- a/internal/controller/room.go +++ b/internal/controller/room.go @@ -55,12 +55,9 @@ type roomClient struct { ClientType RoomClientType `json:"client_type"` AppType RoomAppType `json:"app_type"` IP string `json:"ip"` - Room string `json:"room"` Name string `json:"name"` Id string `json:"id"` RegisterAt time.Time `json:"register_at"` - Offer *RoomOffer `json:"offer"` - Candidate *RoomCandidate `json:"candidate"` msgChan chan any } @@ -130,42 +127,25 @@ func (rc *roomClient) start(ctx context.Context) { type roomController struct { sync.Mutex - ctx context.Context - rooms map[string]map[string]*roomClient // map[room_id(remote-IP)][Id] - notReadies map[string]*roomClient + ctx context.Context + //rooms map[string]map[string]*roomClient // map[room_id(remote-IP)][Id] + clients map[string]*roomClient } var ( RoomController = &roomController{ - rooms: make(map[string]map[string]*roomClient), - notReadies: make(map[string]*roomClient), + clients: make(map[string]*roomClient), } ) func (rc *roomController) Start(ctx context.Context) { rc.ctx = ctx - go func() { - ticker := time.NewTicker(1 * time.Minute) - for { - select { - case <-rc.ctx.Done(): - return - case now := <-ticker.C: - for _, nrc := range rc.notReadies { - if now.Sub(nrc.RegisterAt).Minutes() > 1 { - rc.Lock() - delete(rc.notReadies, nrc.Id) - rc.Unlock() - } - } - } - } - }() } -func (rc *roomController) Register(ip, userAgent string, candidate *RoomCandidate, offer *RoomOffer) *roomClient { +func (rc *roomController) Register(conn *websocket.Conn, ip, userAgent string) *roomClient { nrc := &roomClient{ controller: rc, + conn: conn, ClientType: ClientTypeDesktop, AppType: RoomAppTypeWeb, IP: ip, @@ -173,8 +153,6 @@ func (rc *roomController) Register(ip, userAgent string, candidate *RoomCandidat Name: tool.RandomName(), msgChan: make(chan any, 1), RegisterAt: time.Now(), - Candidate: candidate, - Offer: offer, } ua := useragent.Parse(userAgent) @@ -185,121 +163,71 @@ func (rc *roomController) Register(ip, userAgent string, candidate *RoomCandidat nrc.ClientType = ClientTypeTablet } - key := "local" - if !tool.IsPrivateIP(ip) { - key = ip - } + log.Debug("controller.room: registry client, IP = %s, Id = %s, Name = %s", nrc.IP, nrc.Id, nrc.Name) - nrc.Room = key + nrc.start(rc.ctx) + + nrc.msgChan <- map[string]any{"type": "register", "time": time.Now().UnixMilli(), "body": nrc} + rc.Broadcast(map[string]any{"type": "enter", "time": time.Now().UnixMilli(), "body": nrc}) rc.Lock() - - log.Debug("controller.room: registry client, IP = %s(%s), Id = %s, Name = %s", key, nrc.IP, nrc.Id, nrc.Name) - rc.notReadies[nrc.Id] = nrc - if _, ok := rc.rooms[nrc.Room]; !ok { - rc.rooms[nrc.Room] = make(map[string]*roomClient) - } - + rc.clients[nrc.Id] = nrc rc.Unlock() return nrc } -func (rc *roomController) Enter(conn *websocket.Conn, id string) { - client, ok := rc.notReadies[id] - if !ok { - log.Warn("controller.room: entry room id not exist, id = %s", id) - return - } - - rc.Lock() - - if _, ok = rc.rooms[client.Room]; !ok { - log.Warn("controller.room: entry room not exist, room = %s, id = %s, name = %s", client.Room, id, client.Name) - return - } - - rc.rooms[client.Room][id] = client - client.conn = conn - - rc.Unlock() - - client.start(rc.ctx) - - rc.Broadcast(client.Room, map[string]any{"type": RoomMessageTypeEnter, "time": time.Now().UnixMilli(), "body": client}) -} - -func (rc *roomController) List(room string) []*roomClient { - log.Debug("controller.room: list room = %s", room) +func (rc *roomController) List() []*roomClient { clientList := make([]*roomClient, 0) - clients, ok := rc.rooms[room] - if !ok { - return clientList - } - - for _, client := range clients { + for _, client := range rc.clients { clientList = append(clientList, client) } return clientList } -func (rc *roomController) Broadcast(room string, msg any) { - for _, client := range rc.rooms[room] { +func (rc *roomController) Broadcast(msg any) { + for _, client := range rc.clients { select { case client.msgChan <- msg: case <-time.After(2 * time.Second): - log.Warn("RoomController: broadcast timeout, room = %s, client Id = %s, IP = %s", room, client.Id, client.IP) + log.Warn("RoomController: broadcast timeout, client Id = %s, IP = %s", client.Id, client.IP) } } } func (rc *roomController) Unregister(client *roomClient) { - key := "local" - if !tool.IsPrivateIP(client.IP) { - key = client.IP - } - - log.Debug("controller.room: unregister client, IP = %s(%s), Id = %s, Name = %s", client.IP, key, client.Id, client.Name) + log.Debug("controller.room: unregister client, IP = %s, Id = %s, Name = %s", client.IP, client.Id, client.Name) rc.Lock() - delete(rc.rooms[key], client.Id) + delete(rc.clients, client.Id) rc.Unlock() - rc.Broadcast(key, map[string]any{"type": RoomMessageTypeLeave, "time": time.Now().UnixMilli(), "body": client}) + rc.Broadcast(map[string]any{"type": RoomMessageTypeLeave, "time": time.Now().UnixMilli(), "body": client}) } -func (rc *roomController) Offer(room, id string, offer *RoomOffer) { - if _, ok := rc.rooms[room]; !ok { +func (rc *roomController) Offer(id string, offer *RoomOffer, candidate *RoomCandidate) { + if _, ok := rc.clients[id]; !ok { return } - if _, ok := rc.rooms[room][id]; !ok { - return - } - - rc.rooms[room][id].msgChan <- map[string]any{ - "type": "offer", - "id": id, - "room": room, - "offer": offer, + rc.clients[id].msgChan <- map[string]any{ + "type": "offer", + "id": id, + "offer": offer, + "candidate": candidate, } } -func (rc *roomController) Answer(room, id string, answer *RoomOffer) { - if _, ok := rc.rooms[room]; !ok { +func (rc *roomController) Answer(id string, answer *RoomOffer) { + if _, ok := rc.clients[id]; !ok { return } - if _, ok := rc.rooms[room][id]; !ok { - return - } - - rc.rooms[room][id].msgChan <- map[string]any{ + rc.clients[id].msgChan <- map[string]any{ "type": "answer", "id": id, - "room": room, "answer": answer, } } diff --git a/internal/handler/local.go b/internal/handler/local.go index bdcd55c..0ade484 100644 --- a/internal/handler/local.go +++ b/internal/handler/local.go @@ -9,38 +9,9 @@ import ( "net/http" ) -func LocalRegister() nf.HandlerFunc { - return func(c *nf.Ctx) error { - type Req struct { - Candidate *controller.RoomCandidate `json:"candidate"` - Offer *controller.RoomOffer `json:"offer"` - } - - var ( - err error - req = new(Req) - ip = c.IP(true) - ua = c.Get("User-Agent") - ) - - if err = c.BodyParser(req); err != nil { - return c.Status(http.StatusBadRequest).JSON(map[string]interface{}{"msg": err.Error()}) - } - - client := controller.RoomController.Register(ip, ua, req.Candidate, req.Offer) - - return resp.Resp200(c, client) - } -} - func LocalClients() nf.HandlerFunc { return func(c *nf.Ctx) error { - room := c.Query("room") - if room == "" { - return c.Status(http.StatusBadRequest).JSON(map[string]string{"err": "room can't be empty"}) - } - - list := controller.RoomController.List(room) + list := controller.RoomController.List() return resp.Resp200(c, list) } @@ -56,12 +27,10 @@ func LocalWS() nf.HandlerFunc { } return func(c *nf.Ctx) error { - - id := c.Query("id") - - if id == "" { - return c.Status(http.StatusBadRequest).JSON(map[string]string{"error": "id is empty"}) - } + var ( + ip = c.IP(true) + ua = c.Get("User-Agent") + ) conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { @@ -69,7 +38,7 @@ func LocalWS() nf.HandlerFunc { return err } - controller.RoomController.Enter(conn, id) + controller.RoomController.Register(conn, ip, ua) return nil } @@ -78,9 +47,9 @@ func LocalWS() nf.HandlerFunc { func LocalOffer() nf.HandlerFunc { return func(c *nf.Ctx) error { type Req struct { - Room string `json:"room"` - Id string `json:"id"` - Offer *controller.RoomOffer `json:"offer"` + Id string `json:"id"` + Offer *controller.RoomOffer `json:"offer"` + Candidate *controller.RoomCandidate `json:"candidate"` } var ( @@ -92,7 +61,7 @@ func LocalOffer() nf.HandlerFunc { return c.Status(http.StatusBadRequest).JSON(map[string]string{"err": err.Error()}) } - controller.RoomController.Offer(req.Room, req.Id, req.Offer) + controller.RoomController.Offer(req.Id, req.Offer, req.Candidate) return resp.Resp200(c, req.Offer) } @@ -101,7 +70,6 @@ func LocalOffer() nf.HandlerFunc { func LocalAnswer() nf.HandlerFunc { return func(c *nf.Ctx) error { type Req struct { - Room string `json:"room"` Id string `json:"id"` Answer *controller.RoomOffer `json:"answer"` } @@ -115,7 +83,7 @@ func LocalAnswer() nf.HandlerFunc { return c.Status(http.StatusBadRequest).JSON(map[string]string{"err": err.Error()}) } - controller.RoomController.Answer(req.Room, req.Id, req.Answer) + controller.RoomController.Answer(req.Id, req.Answer) return resp.Resp200(c, req) }