wip: 0.2.7
实现了 rtc 握手和打开数据通道
This commit is contained in:
parent
5bc695bde3
commit
21287e0874
@ -15,6 +15,7 @@ server {
|
|||||||
|
|
||||||
location /ushare {
|
location /ushare {
|
||||||
proxy_pass http://localhost:9119;
|
proxy_pass http://localhost:9119;
|
||||||
|
const rtc = new RTCPeerConnection({iceServers: [{urls: "stun:stun.qq.com:3478"}]})
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
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 {CloudBackground} from "../../component/fluid/cloud.tsx";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useRef, useState} from "react";
|
||||||
import {createUseStyles} from "react-jss";
|
import {createUseStyles} from "react-jss";
|
||||||
import {useWebsocket} from "../../hook/websocket/u-ws.tsx";
|
import {useWebsocket} from "../../hook/websocket/u-ws.tsx";
|
||||||
import {Resp} from "../../interface/response.ts";
|
import {Resp} from "../../interface/response.ts";
|
||||||
import {useRegister} from "./hook/register.tsx";
|
import {useRegister} from "./hook/register.tsx";
|
||||||
|
import {useLocalStore} from "../../store/local.ts";
|
||||||
|
|
||||||
const useClass = createUseStyles({
|
const useClass = createUseStyles({
|
||||||
'@global': {
|
'@global': {
|
||||||
@ -70,9 +71,9 @@ interface Bubble {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface WSMessage {
|
interface WSMessage {
|
||||||
body: Client;
|
data: any;
|
||||||
time: number;
|
time: number;
|
||||||
type: "register" | "enter" | "leave" | "offer" | "answer"
|
type: "register" | "enter" | "leave" | "offer" | "answer" | "candidate"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Client {
|
interface Client {
|
||||||
@ -100,9 +101,9 @@ function handleFileChunk(chunk: any) {
|
|||||||
|
|
||||||
export const LocalSharing: React.FC = () => {
|
export const LocalSharing: React.FC = () => {
|
||||||
const classes = useClass();
|
const classes = useClass();
|
||||||
const {id, name, register } = useRegister()
|
const {id, name, set} = useLocalStore()
|
||||||
const [rtc, setRTC] = useState<RTCPeerConnection| null>(null);
|
const [rtc, setRTC] = useState<RTCPeerConnection>();
|
||||||
const [client, setClient] = useState<Client| null>(null);
|
const rtcRef = useRef<RTCPeerConnection>();
|
||||||
const [clients, setClients] = useState<Client[]>([]);
|
const [clients, setClients] = useState<Client[]>([]);
|
||||||
const {connect, close} = useWebsocket({})
|
const {connect, close} = useWebsocket({})
|
||||||
|
|
||||||
@ -175,7 +176,6 @@ export const LocalSharing: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[D] generated bubbles =', bubbles);
|
|
||||||
return bubbles;
|
return bubbles;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -183,38 +183,96 @@ export const LocalSharing: React.FC = () => {
|
|||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const res = await fetch(`/api/ulocal/clients`)
|
const res = await fetch(`/api/ulocal/clients`)
|
||||||
const jes = await res.json() as Resp<Client[]>
|
const jes = await res.json() as Resp<Client[]>
|
||||||
console.log('[D] update clients =', jes)
|
// console.log('[D] update clients =', jes)
|
||||||
setClients(jes.data)
|
setClients(jes.data)
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleWSEvent = async (e: MessageEvent) => {
|
const handleWSEvent = async (e: MessageEvent) => {
|
||||||
|
let current_id: string;
|
||||||
|
let current_rtc: RTCPeerConnection
|
||||||
const msg = JSON.parse(e.data) as WSMessage
|
const msg = JSON.parse(e.data) as WSMessage
|
||||||
console.log('[D] ws event msg =', msg)
|
console.log('[D] ws event msg =', msg)
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case "register":
|
|
||||||
const reg_resp = JSON.parse(e.data) as { body: Client }
|
|
||||||
setClient(reg_resp.body)
|
|
||||||
break
|
|
||||||
case "enter":
|
case "enter":
|
||||||
await updateClients()
|
await updateClients()
|
||||||
break
|
return
|
||||||
case "leave":
|
case "leave":
|
||||||
await updateClients()
|
await updateClients()
|
||||||
break
|
return
|
||||||
case "offer":
|
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":
|
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 handleBubbleClick = async (bubble: Bubble) => {
|
||||||
const offer = await rtc?.createOffer()
|
console.log(`[D] click id = ${bubble.id}`)
|
||||||
await rtc?.setLocalDescription(offer)
|
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 = {
|
const data = {
|
||||||
id: bubble.id,
|
id: bubble.id,
|
||||||
|
from: useLocalStore.getState().id,
|
||||||
offer: offer,
|
offer: offer,
|
||||||
}
|
}
|
||||||
await fetch('/api/ulocal/offer', {
|
await fetch('/api/ulocal/offer', {
|
||||||
@ -222,32 +280,49 @@ export const LocalSharing: React.FC = () => {
|
|||||||
headers: {"Content-Type": "application/json"},
|
headers: {"Content-Type": "application/json"},
|
||||||
body: JSON.stringify(data)
|
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(() => {
|
useEffect(() => {
|
||||||
const fn = async () => {
|
const fn = async () => {
|
||||||
const reg_data = await register();
|
const response = await fetch('/api/ulocal/register', {method: 'POST'});
|
||||||
console.log(`[D] register id = ${reg_data?.id}`);
|
const data = ((await response.json()) as Resp<{id: string; name: string}>).data;
|
||||||
connect(`/api/ulocal/ws?id=${reg_data?.id}`, {fn: handleWSEvent})
|
set(data.id, data.name)
|
||||||
|
console.log(`[D] register id = ${data.id}`);
|
||||||
|
connect(`/api/ulocal/ws?id=${data.id}`, {fn: handleWSEvent})
|
||||||
await updateClients()
|
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()
|
fn()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <div className={classes.container}>
|
return <div className={classes.container}>
|
||||||
<CloudBackground/>
|
<CloudBackground/>
|
||||||
<h1 className={classes.title}>{name}</h1>
|
<h1 className={classes.title}>{name}
|
||||||
|
<span> - {id}</span>
|
||||||
|
</h1>
|
||||||
{clients && generateBubbles().map(bubble => {
|
{clients && generateBubbles().map(bubble => {
|
||||||
return client ? (
|
return (
|
||||||
<div
|
<div
|
||||||
key={bubble.id}
|
key={bubble.id}
|
||||||
className={classes.bubble}
|
className={classes.bubble}
|
||||||
@ -263,7 +338,7 @@ export const LocalSharing: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{bubble.name}
|
{bubble.name}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</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("/register", handler.LocalRegister())
|
||||||
api.Post("/offer", handler.LocalOffer())
|
api.Post("/offer", handler.LocalOffer())
|
||||||
api.Post("/answer", handler.LocalAnswer())
|
api.Post("/answer", handler.LocalAnswer())
|
||||||
|
api.Post("/candidate", handler.LocalCandidate())
|
||||||
api.Get("/clients", handler.LocalClients())
|
api.Get("/clients", handler.LocalClients())
|
||||||
api.Get("/ws", handler.LocalWS())
|
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})
|
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 {
|
if _, ok := rc.clients[id]; !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rc.clients[id].msgChan <- map[string]any{
|
rc.clients[id].msgChan <- map[string]any{
|
||||||
"type": "offer",
|
"type": "offer",
|
||||||
|
"time": time.Now().UnixMilli(),
|
||||||
|
"data": map[string]any{
|
||||||
"id": id,
|
"id": id,
|
||||||
|
"from": from,
|
||||||
"offer": offer,
|
"offer": offer,
|
||||||
"candidate": candidate,
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,7 +247,31 @@ func (rc *roomController) Answer(id string, answer *RoomOffer) {
|
|||||||
|
|
||||||
rc.clients[id].msgChan <- map[string]any{
|
rc.clients[id].msgChan <- map[string]any{
|
||||||
"type": "answer",
|
"type": "answer",
|
||||||
|
"time": time.Now().UnixMilli(),
|
||||||
|
"data": map[string]any{
|
||||||
"id": id,
|
"id": id,
|
||||||
"answer": answer,
|
"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 {
|
return func(c *nf.Ctx) error {
|
||||||
type Req struct {
|
type Req struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
|
From string `json:"from"`
|
||||||
Offer *controller.RoomOffer `json:"offer"`
|
Offer *controller.RoomOffer `json:"offer"`
|
||||||
Candidate *controller.RoomCandidate `json:"candidate"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -75,7 +75,7 @@ func LocalOffer() nf.HandlerFunc {
|
|||||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"err": err.Error()})
|
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)
|
return resp.Resp200(c, req.Offer)
|
||||||
}
|
}
|
||||||
@ -102,3 +102,25 @@ func LocalAnswer() nf.HandlerFunc {
|
|||||||
return resp.Resp200(c, req)
|
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