wip: v0.2.6
This commit is contained in:
parent
b8645a68ed
commit
5bc695bde3
@ -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!)
|
||||
|
46
frontend/src/page/local/hook/register.tsx
Normal file
46
frontend/src/page/local/hook/register.tsx
Normal 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
|
||||
};
|
||||
};
|
@ -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}
|
@ -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: {
|
@ -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: {
|
@ -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())
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user