wip: 0.2.7
实现了 rtc 握手和打开数据通道
This commit is contained in:
parent
5bc695bde3
commit
21287e0874
@ -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;
|
||||
|
@ -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<Error | null>(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
|
||||
};
|
||||
};
|
@ -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<RTCPeerConnection| null>(null);
|
||||
const [client, setClient] = useState<Client| null>(null);
|
||||
const {id, name, set} = useLocalStore()
|
||||
const [rtc, setRTC] = useState<RTCPeerConnection>();
|
||||
const rtcRef = useRef<RTCPeerConnection>();
|
||||
const [clients, setClients] = useState<Client[]>([]);
|
||||
const {connect, close} = useWebsocket({})
|
||||
|
||||
@ -175,7 +176,6 @@ export const LocalSharing: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[D] generated bubbles =', bubbles);
|
||||
return bubbles;
|
||||
};
|
||||
|
||||
@ -183,38 +183,96 @@ export const LocalSharing: React.FC = () => {
|
||||
setTimeout(async () => {
|
||||
const res = await fetch(`/api/ulocal/clients`)
|
||||
const jes = await res.json() as Resp<Client[]>
|
||||
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)
|
||||
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', {
|
||||
@ -222,32 +280,49 @@ export const LocalSharing: React.FC = () => {
|
||||
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')
|
||||
}
|
||||
}
|
||||
setRTC(rtc)
|
||||
|
||||
return () => {close();}
|
||||
const _rtc = new RTCPeerConnection();
|
||||
rtcRef.current = _rtc; // 同步设置 ref
|
||||
setRTC(_rtc); // 更新状态(如果需要触发渲染)
|
||||
|
||||
return () => {
|
||||
close();
|
||||
if (rtcRef) {
|
||||
rtcRef.current.close()
|
||||
}
|
||||
}
|
||||
};
|
||||
fn()
|
||||
}, [])
|
||||
|
||||
return <div className={classes.container}>
|
||||
<CloudBackground/>
|
||||
<h1 className={classes.title}>{name}</h1>
|
||||
<h1 className={classes.title}>{name}
|
||||
<span> - {id}</span>
|
||||
</h1>
|
||||
{clients && generateBubbles().map(bubble => {
|
||||
return client ? (
|
||||
return (
|
||||
<div
|
||||
key={bubble.id}
|
||||
className={classes.bubble}
|
||||
@ -263,7 +338,7 @@ export const LocalSharing: React.FC = () => {
|
||||
>
|
||||
{bubble.name}
|
||||
</div>
|
||||
) : null;
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
17
frontend/src/store/local.ts
Normal file
17
frontend/src/store/local.ts
Normal file
@ -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<LocalStore>()((_set, get) => ({
|
||||
id: '',
|
||||
name: '',
|
||||
set: (id: string, name: string) => {
|
||||
_set(state => {
|
||||
return {...state, id: id, name: name};
|
||||
})
|
||||
}
|
||||
}))
|
@ -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())
|
||||
}
|
||||
|
@ -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",
|
||||
"time": time.Now().UnixMilli(),
|
||||
"data": map[string]any{
|
||||
"id": id,
|
||||
"from": from,
|
||||
"offer": offer,
|
||||
"candidate": candidate,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -244,7 +247,31 @@ func (rc *roomController) Answer(id string, answer *RoomOffer) {
|
||||
|
||||
rc.clients[id].msgChan <- map[string]any{
|
||||
"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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,8 +62,8 @@ func LocalOffer() nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Id string `json:"id"`
|
||||
From string `json:"from"`
|
||||
Offer *controller.RoomOffer `json:"offer"`
|
||||
Candidate *controller.RoomCandidate `json:"candidate"`
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user