From 21287e087436c5dfab133f80f2bd34d8382f7e11 Mon Sep 17 00:00:00 2001 From: loveuer Date: Mon, 26 May 2025 17:40:06 +0800 Subject: [PATCH] =?UTF-8?q?wip:=200.2.7=20=20=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E4=BA=86=20rtc=20=E6=8F=A1=E6=89=8B=E5=92=8C=E6=89=93=E5=BC=80?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E9=80=9A=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deployment/nginx.conf | 1 + frontend/src/page/local/hook/register.tsx | 46 ------- frontend/src/page/local/local.tsx | 153 ++++++++++++++++------ frontend/src/store/local.ts | 17 +++ internal/api/api.go | 1 + internal/controller/room.go | 43 ++++-- internal/handler/local.go | 30 ++++- 7 files changed, 194 insertions(+), 97 deletions(-) delete mode 100644 frontend/src/page/local/hook/register.tsx create mode 100644 frontend/src/store/local.ts diff --git a/deployment/nginx.conf b/deployment/nginx.conf index f19f5dd..b417480 100644 --- a/deployment/nginx.conf +++ b/deployment/nginx.conf @@ -15,6 +15,7 @@ server { location /ushare { proxy_pass http://localhost:9119; + const rtc = new RTCPeerConnection({iceServers: [{urls: "stun:stun.qq.com:3478"}]}) proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/frontend/src/page/local/hook/register.tsx b/frontend/src/page/local/hook/register.tsx deleted file mode 100644 index 0089be6..0000000 --- a/frontend/src/page/local/hook/register.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import {useState, useCallback,} from 'react'; - -export const useRegister = () => { - const [id, setId] = useState(''); - const [name, setName] = useState(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - // 封装为可手动触发的异步函数 - const register = useCallback(async () => { - setLoading(true); - setError(null); - - try { - const response = await fetch('/api/ulocal/register', { - method: 'POST' - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = (await response.json()).data; - setId(data.id); - setName(data.name); - - return {id: data.id, name: data.name}; - } catch (err) { - setError(err instanceof Error ? err : new Error('Request failed')); - } finally { - setLoading(false); - } - - }, []); - - // 如果需要自动执行(组件挂载时自动注册) - // useEffect(() => { register() }, [register]); - - return { - id, - name, - register, // 手动触发函数 - loading, - error - }; -}; \ No newline at end of file diff --git a/frontend/src/page/local/local.tsx b/frontend/src/page/local/local.tsx index 32b8dbc..b73da92 100644 --- a/frontend/src/page/local/local.tsx +++ b/frontend/src/page/local/local.tsx @@ -1,9 +1,10 @@ import {CloudBackground} from "../../component/fluid/cloud.tsx"; -import {useEffect, useState} from "react"; +import {useEffect, useRef, useState} from "react"; import {createUseStyles} from "react-jss"; import {useWebsocket} from "../../hook/websocket/u-ws.tsx"; import {Resp} from "../../interface/response.ts"; import {useRegister} from "./hook/register.tsx"; +import {useLocalStore} from "../../store/local.ts"; const useClass = createUseStyles({ '@global': { @@ -70,9 +71,9 @@ interface Bubble { } interface WSMessage { - body: Client; + data: any; time: number; - type: "register" | "enter" | "leave" | "offer" | "answer" + type: "register" | "enter" | "leave" | "offer" | "answer" | "candidate" } interface Client { @@ -100,9 +101,9 @@ function handleFileChunk(chunk: any) { export const LocalSharing: React.FC = () => { const classes = useClass(); - const {id, name, register } = useRegister() - const [rtc, setRTC] = useState(null); - const [client, setClient] = useState(null); + const {id, name, set} = useLocalStore() + const [rtc, setRTC] = useState(); + const rtcRef = useRef(); const [clients, setClients] = useState([]); const {connect, close} = useWebsocket({}) @@ -175,7 +176,6 @@ export const LocalSharing: React.FC = () => { } } - console.log('[D] generated bubbles =', bubbles); return bubbles; }; @@ -183,71 +183,146 @@ export const LocalSharing: React.FC = () => { setTimeout(async () => { const res = await fetch(`/api/ulocal/clients`) const jes = await res.json() as Resp - console.log('[D] update clients =', jes) + // console.log('[D] update clients =', jes) setClients(jes.data) }, 500) } const handleWSEvent = async (e: MessageEvent) => { + let current_id: string; + let current_rtc: RTCPeerConnection 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 } - setClient(reg_resp.body) - break case "enter": await updateClients() - break + return case "leave": await updateClients() - break + return case "offer": - break + const offer_data = msg.data as { id: string; from: string; offer: RTCSessionDescriptionInit } + current_id = useLocalStore.getState().id + if (offer_data.id !== current_id) { + console.warn(`[W] wrong offer id, want = ${current_id}, got = ${offer_data.id}, data =`, offer_data) + return + } + + current_rtc = rtcRef.current + if(!current_rtc) { + console.warn('[W] rtc undefined') + + return + } + + await current_rtc.setRemoteDescription(offer_data.offer) + const answer = await current_rtc.createAnswer() + if(!answer) { + console.log('[W] answer undefined') + return + } + await current_rtc.setLocalDescription(answer) + + await fetch('/api/ulocal/answer', { + method: 'POST', + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({id: offer_data.from, answer: answer}) + }) + + return case "answer": - break + const answer_data = msg.data as {answer: RTCSessionDescriptionInit; id: string} + current_id = useLocalStore.getState().id + if (answer_data.id !== current_id) { + console.warn(`[W] wrong answer id, want = ${current_id}, got = ${answer_data.id}, data =`, answer_data) + } + + current_rtc = rtcRef.current + if(!current_rtc) { + console.warn('[W] rtc undefined') + return + } + + await current_rtc.setRemoteDescription(answer_data.answer) + + return + case "candidate": + const candidate_data = msg.data as { candidate: RTCIceCandidateInit } + current_rtc = rtcRef.current + if(!current_rtc) { + console.warn('[W] rtc undefined') + return + } + if(!candidate_data.candidate) { + console.log('[W] candidate data null') + return + } + await current_rtc.addIceCandidate(candidate_data.candidate) + return } } // 气泡点击处理 const handleBubbleClick = async (bubble: Bubble) => { - const offer = await rtc?.createOffer() - await rtc?.setLocalDescription(offer) - const data = { - id: bubble.id, - offer: offer, + console.log(`[D] click id = ${bubble.id}`) + const current_rtc = rtcRef.current + current_rtc.onnegotiationneeded = async() => { + const offer = await current_rtc.createOffer() + console.log('[D] offer created', offer) + await current_rtc.setLocalDescription(offer) + const data = { + id: bubble.id, + from: useLocalStore.getState().id, + offer: offer, + } + await fetch('/api/ulocal/offer', { + method: 'POST', + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(data) + }) } - await fetch('/api/ulocal/offer', { - method: 'POST', - headers: {"Content-Type": "application/json"}, - body: JSON.stringify(data) - }) + current_rtc.onicecandidate = async (e) => { + console.log('[D] on candidate ',e ) + await fetch('/api/ulocal/candidate', { + method: 'POST', + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({candidate: e.candidate, id: useLocalStore.getState().id}) + }) + } + const ch = current_rtc.createDataChannel('local', {ordered: true}) + setupDataChannel(ch) }; useEffect(() => { const fn = async () => { - const reg_data = await register(); - console.log(`[D] register id = ${reg_data?.id}`); - connect(`/api/ulocal/ws?id=${reg_data?.id}`, {fn: handleWSEvent}) + const response = await fetch('/api/ulocal/register', {method: 'POST'}); + const data = ((await response.json()) as Resp<{id: string; name: string}>).data; + set(data.id, data.name) + console.log(`[D] register id = ${data.id}`); + connect(`/api/ulocal/ws?id=${data.id}`, {fn: handleWSEvent}) await updateClients() - const rtc = new RTCPeerConnection({iceServers: [{urls: "stun:stun.qq.com:3478"}]}) - rtc.onicecandidate = async (e) => { - if(e.candidate) { - await fetch('/api/ulocal/candidate') + + const _rtc = new RTCPeerConnection(); + rtcRef.current = _rtc; // 同步设置 ref + setRTC(_rtc); // 更新状态(如果需要触发渲染) + + return () => { + close(); + if (rtcRef) { + rtcRef.current.close() } } - setRTC(rtc) - - return () => {close();} }; fn() }, []) return
-

{name}

+

{name} + - {id} +

{clients && generateBubbles().map(bubble => { - return client ? ( + return (
{ > {bubble.name}
- ) : null; + ); })}
} \ No newline at end of file diff --git a/frontend/src/store/local.ts b/frontend/src/store/local.ts new file mode 100644 index 0000000..27edaae --- /dev/null +++ b/frontend/src/store/local.ts @@ -0,0 +1,17 @@ +import {create} from 'zustand' + +export interface LocalStore { + id: string; + name: string; + set: (id: string, name: string) => void; +} + +export const useLocalStore = create()((_set, get) => ({ + id: '', + name: '', + set: (id: string, name: string) => { + _set(state => { + return {...state, id: id, name: name}; + }) + } +})) \ No newline at end of file diff --git a/internal/api/api.go b/internal/api/api.go index cb7bf27..a12e562 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -28,6 +28,7 @@ func Start(ctx context.Context) <-chan struct{} { api.Post("/register", handler.LocalRegister()) api.Post("/offer", handler.LocalOffer()) api.Post("/answer", handler.LocalAnswer()) + api.Post("/candidate", handler.LocalCandidate()) api.Get("/clients", handler.LocalClients()) api.Get("/ws", handler.LocalWS()) } diff --git a/internal/controller/room.go b/internal/controller/room.go index 4ff6ac3..a1c9aa5 100644 --- a/internal/controller/room.go +++ b/internal/controller/room.go @@ -224,16 +224,19 @@ func (rc *roomController) Unregister(client *roomClient) { rc.Broadcast(map[string]any{"type": RoomMessageTypeLeave, "time": time.Now().UnixMilli(), "body": client}) } -func (rc *roomController) Offer(id string, offer *RoomOffer, candidate *RoomCandidate) { +func (rc *roomController) Offer(id, from string, offer *RoomOffer) { if _, ok := rc.clients[id]; !ok { return } rc.clients[id].msgChan <- map[string]any{ - "type": "offer", - "id": id, - "offer": offer, - "candidate": candidate, + "type": "offer", + "time": time.Now().UnixMilli(), + "data": map[string]any{ + "id": id, + "from": from, + "offer": offer, + }, } } @@ -243,8 +246,32 @@ func (rc *roomController) Answer(id string, answer *RoomOffer) { } rc.clients[id].msgChan <- map[string]any{ - "type": "answer", - "id": id, - "answer": answer, + "type": "answer", + "time": time.Now().UnixMilli(), + "data": map[string]any{ + "id": id, + "answer": answer, + }, + } +} + +func (rc *roomController) Candidate(id string, candidate *RoomCandidate) { + if _, ok := rc.clients[id]; !ok { + return + } + + for _, client := range rc.clients { + if client.Id == id { + continue + } + + client.msgChan <- map[string]any{ + "type": "candidate", + "time": time.Now().UnixMilli(), + "data": map[string]any{ + "id": client.Id, + "candidate": candidate, + }, + } } } diff --git a/internal/handler/local.go b/internal/handler/local.go index b1d33ca..9eb23d7 100644 --- a/internal/handler/local.go +++ b/internal/handler/local.go @@ -61,9 +61,9 @@ func LocalWS() nf.HandlerFunc { func LocalOffer() nf.HandlerFunc { return func(c *nf.Ctx) error { type Req struct { - Id string `json:"id"` - Offer *controller.RoomOffer `json:"offer"` - Candidate *controller.RoomCandidate `json:"candidate"` + Id string `json:"id"` + From string `json:"from"` + Offer *controller.RoomOffer `json:"offer"` } var ( @@ -75,7 +75,7 @@ func LocalOffer() nf.HandlerFunc { return c.Status(http.StatusBadRequest).JSON(map[string]string{"err": err.Error()}) } - controller.RoomController.Offer(req.Id, req.Offer, req.Candidate) + controller.RoomController.Offer(req.Id, req.From, req.Offer) return resp.Resp200(c, req.Offer) } @@ -102,3 +102,25 @@ func LocalAnswer() nf.HandlerFunc { return resp.Resp200(c, req) } } + +func LocalCandidate() nf.HandlerFunc { + return func(c *nf.Ctx) error { + type Req struct { + Id string `json:"id"` + Candidate *controller.RoomCandidate `json:"candidate"` + } + + var ( + err error + req = new(Req) + ) + + if err = c.BodyParser(req); err != nil { + return c.Status(http.StatusBadRequest).JSON(map[string]string{"err": err.Error()}) + } + + controller.RoomController.Candidate(req.Id, req.Candidate) + + return resp.Resp200(c, req) + } +}