wip: v0.2.6

This commit is contained in:
loveuer 2025-05-26 09:37:23 +08:00
parent b8645a68ed
commit 5bc695bde3
10 changed files with 141 additions and 146 deletions

View File

@ -3,8 +3,8 @@ import { createRoot } from 'react-dom/client'
import './index.css'
import {createBrowserRouter, RouterProvider} from "react-router-dom";
import {Login} from "./page/login.tsx";
import {FileSharing} from "./page/share.tsx";
import {LocalSharing} from "./page/local.tsx";
import {FileSharing} from "./page/share/share.tsx";
import {LocalSharing} from "./page/local/local.tsx";
const container = document.getElementById('root')
const root = createRoot(container!)

View File

@ -0,0 +1,46 @@
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
};
};

View File

@ -1,9 +1,9 @@
import {CloudBackground} from "../component/fluid/cloud.tsx";
import {CloudBackground} from "../../component/fluid/cloud.tsx";
import {useEffect, useState} from "react";
import {createUseStyles} from "react-jss";
import {useWebsocket} from "../hook/websocket/u-ws.tsx";
import {Resp} from "../interface/response.ts";
import {message} from "../hook/message/u-message.tsx";
import {useWebsocket} from "../../hook/websocket/u-ws.tsx";
import {Resp} from "../../interface/response.ts";
import {useRegister} from "./hook/register.tsx";
const useClass = createUseStyles({
'@global': {
@ -86,15 +86,6 @@ interface Client {
candidate: RTCIceCandidateInit;
}
interface Store {
client: Client | null
clients: Client[]
rtc: RTCPeerConnection | null
ch: RTCDataChannel | null
offer: RTCSessionDescription | null
candidate: RTCIceCandidate | null
}
function setupDataChannel(ch: RTCDataChannel) {
ch.onopen = () => console.log('通道已打开!');
ch.onmessage = (e) => handleFileChunk(e.data);
@ -109,7 +100,10 @@ function handleFileChunk(chunk: any) {
export const LocalSharing: React.FC = () => {
const classes = useClass();
const [rtcStore, setRTCStore] = useState<Store>({} as Store)
const {id, name, register } = useRegister()
const [rtc, setRTC] = useState<RTCPeerConnection| null>(null);
const [client, setClient] = useState<Client| null>(null);
const [clients, setClients] = useState<Client[]>([]);
const {connect, close} = useWebsocket({})
// 生成随机颜色
@ -121,8 +115,8 @@ export const LocalSharing: React.FC = () => {
};
// 防碰撞位置生成
const generateBubbles = (cs: Client[]) => {
if (!cs) return []
const generateBubbles = () => {
if (!clients) return []
const BUBBLE_SIZE = 100;
const centerX = window.innerWidth / 2;
@ -132,11 +126,11 @@ export const LocalSharing: React.FC = () => {
let currentRadius = 0;
let angleStep = (2 * Math.PI) / 6; // 初始6个位置
for (let index = 0; index < cs.length; index++) {
for (let index = 0; index < clients.length; index++) {
let attempt = 0;
let validPosition = false;
if (cs[index].id == rtcStore.client?.id) {
if (clients[index].id == id) {
continue
}
@ -164,8 +158,8 @@ export const LocalSharing: React.FC = () => {
if (inBounds && !collision) {
bubbles.push({
id: cs[index].id,
name: cs[index].name,
id: clients[index].id,
name: clients[index].name,
x: x,
y: y,
color: generateColor(),
@ -181,6 +175,7 @@ export const LocalSharing: React.FC = () => {
}
}
console.log('[D] generated bubbles =', bubbles);
return bubbles;
};
@ -188,9 +183,8 @@ export const LocalSharing: React.FC = () => {
setTimeout(async () => {
const res = await fetch(`/api/ulocal/clients`)
const jes = await res.json() as Resp<Client[]>
setRTCStore(val => {
return {...val, clients: jes.data}
})
console.log('[D] update clients =', jes)
setClients(jes.data)
}, 500)
}
@ -200,9 +194,7 @@ export const LocalSharing: React.FC = () => {
switch (msg.type) {
case "register":
const reg_resp = JSON.parse(e.data) as { body: Client }
setRTCStore(val => {
return {...val, client: reg_resp.body}
})
setClient(reg_resp.body)
break
case "enter":
await updateClients()
@ -211,126 +203,51 @@ export const LocalSharing: React.FC = () => {
await updateClients()
break
case "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)
await fetch('/api/ulocal/answer', {
method: 'POST',
headers: {"Content-Type": "application/json"},
body: JSON.stringify({id: rtcStore.client?.id, answer: answer})
})
break
case "answer":
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<RTCIceCandidate | null> => {
// return new Promise<RTCIceCandidate | null>(resolve => {
// rtc.onicecandidate = (e) => {
// resolve(e.candidate)
// }
// })
// }
//
// const waitNegotiation = (): Promise<void> => {
// return new Promise<void>(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<Client>;
//
// 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,
// })
// })
const offer = await rtc?.createOffer()
await rtc?.setLocalDescription(offer)
const data = {
id: bubble.id,
offer: offer,
}
await fetch('/api/ulocal/offer', {
method: 'POST',
headers: {"Content-Type": "application/json"},
body: JSON.stringify(data)
})
};
useEffect(() => {
const fn = async () => {
connect("/api/ulocal/ws", {fn: handleWSEvent})
const reg_data = await register();
console.log(`[D] register id = ${reg_data?.id}`);
connect(`/api/ulocal/ws?id=${reg_data?.id}`, {fn: handleWSEvent})
await updateClients()
const rtc = new RTCPeerConnection({iceServers: [{urls: "stun:stun.qq.com:3478"}]})
setRTCStore(val => {return {...val, rtc: rtc}})
rtc.onicecandidate = async (e) => {
if(e.candidate) {
await fetch('/api/ulocal/candidate')
}
}
setRTC(rtc)
return () => {close();}
};
fn()
}, [])
return <div className={classes.container}>
<CloudBackground/>
<h1 className={classes.title}>{rtcStore.client?.name}</h1>
{rtcStore.clients && generateBubbles(rtcStore.clients).map(bubble => {
// const client = clients.find(c => c.id === bubble.id);
return rtcStore.client ? (
<h1 className={classes.title}>{name}</h1>
{clients && generateBubbles().map(bubble => {
return client ? (
<div
key={bubble.id}
className={classes.bubble}

View File

@ -1,9 +1,9 @@
import {createUseStyles} from "react-jss";
import {UButton} from "../../component/button/u-button.tsx";
import {UButton} from "../../../component/button/u-button.tsx";
import React, {useState} from "react";
import {useStore} from "../../store/share.ts";
import {message} from "../../hook/message/u-message.tsx";
import {useFileUpload} from "../../api/upload.ts";
import {useStore} from "../../../store/share.ts";
import {message} from "../../../hook/message/u-message.tsx";
import {useFileUpload} from "../../../api/upload.ts";
const useUploadStyle = createUseStyles({
container: {

View File

@ -1,6 +1,6 @@
import {createUseStyles} from "react-jss";
import {UButton} from "../../component/button/u-button.tsx";
import {useStore} from "../../store/share.ts";
import {UButton} from "../../../component/button/u-button.tsx";
import {useStore} from "../../../store/share.ts";
const useStyle = createUseStyles({
container: {

View File

@ -25,6 +25,7 @@ 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())

View File

@ -129,11 +129,13 @@ type roomController struct {
sync.Mutex
ctx context.Context
//rooms map[string]map[string]*roomClient // map[room_id(remote-IP)][Id]
pre map[string]*roomClient
clients map[string]*roomClient
}
var (
RoomController = &roomController{
pre: make(map[string]*roomClient),
clients: make(map[string]*roomClient),
}
)
@ -142,10 +144,9 @@ func (rc *roomController) Start(ctx context.Context) {
rc.ctx = ctx
}
func (rc *roomController) Register(conn *websocket.Conn, ip, userAgent string) *roomClient {
func (rc *roomController) Register(ip, userAgent string) *roomClient {
nrc := &roomClient{
controller: rc,
conn: conn,
ClientType: ClientTypeDesktop,
AppType: RoomAppTypeWeb,
IP: ip,
@ -163,16 +164,32 @@ func (rc *roomController) Register(conn *websocket.Conn, ip, userAgent string) *
nrc.ClientType = ClientTypeTablet
}
log.Debug("controller.room: registry client, IP = %s, Id = %s, Name = %s", nrc.IP, nrc.Id, nrc.Name)
rc.Lock()
defer rc.Unlock()
nrc.start(rc.ctx)
rc.pre[nrc.Id] = nrc
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})
return nrc
}
func (rc *roomController) Enter(conn *websocket.Conn, id string) *roomClient {
log.Debug("controller.room: registry client, id = %s", id)
rc.Lock()
defer rc.Unlock()
nrc, ok := rc.pre[id]
if !ok {
return nil
}
nrc.conn = conn
nrc.start(rc.ctx)
rc.Broadcast(map[string]any{"type": "enter", "time": time.Now().UnixMilli(), "body": nrc})
delete(rc.pre, nrc.Id)
rc.clients[nrc.Id] = nrc
rc.Unlock()
return nrc
}

View File

@ -9,6 +9,19 @@ import (
"net/http"
)
func LocalRegister() nf.HandlerFunc {
return func(c *nf.Ctx) error {
var (
ip = c.IP(true)
ua = c.Get("User-Agent")
)
client := controller.RoomController.Register(ip, ua)
return resp.Resp200(c, client)
}
}
func LocalClients() nf.HandlerFunc {
return func(c *nf.Ctx) error {
list := controller.RoomController.List()
@ -27,10 +40,11 @@ func LocalWS() nf.HandlerFunc {
}
return func(c *nf.Ctx) error {
var (
ip = c.IP(true)
ua = c.Get("User-Agent")
)
id := c.Query("id")
if id == "" {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"error": "id is empty"})
}
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
@ -38,7 +52,7 @@ func LocalWS() nf.HandlerFunc {
return err
}
controller.RoomController.Register(conn, ip, ua)
controller.RoomController.Enter(conn, id)
return nil
}