Skip to content

Commit ac00a91

Browse files
authored
Merge pull request #64 from slaveofcode/feat/webrtc-connection-options
feat(webrtc): Network category + user-chosen ICE servers & serverless manual mode
2 parents 6fe1fa9 + 6c49b6b commit ac00a91

12 files changed

Lines changed: 559 additions & 43 deletions

File tree

src/components/ToolGrid.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { tools } from '@/registry/tools';
2-
import { categories, categoryColors } from '@/registry/categories';
2+
import { categories, categoryColors, categoryNotes } from '@/registry/categories';
33

44
/**
55
* Static tool grid grouped by category. Rendered without a client directive so
@@ -25,6 +25,9 @@ export function ToolGrid() {
2525
({categoryTools.filter(tool => !tool.desktopOnly).length})
2626
</span>
2727
</div>
28+
{categoryNotes[category] && (
29+
<p className="mb-4 max-w-3xl text-sm text-muted-foreground">{categoryNotes[category]}</p>
30+
)}
2831
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
2932
{categoryTools.map(tool => {
3033
const Icon = tool.icon;

src/hooks/useFileTransfer.ts

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useCallback, useEffect, useRef, useState } from 'react';
22
import { connectSignal, type SignalClient } from '@/tools/webrtc/signal-client';
33
import { createPeer, type PeerHandles } from '@/tools/webrtc/peer';
4+
import { createManualConnection, type ManualConnection } from '@/tools/webrtc/manual';
45
import {
56
CHUNK_SIZE,
67
chunkCount,
@@ -27,8 +28,10 @@ const HIGH_WATER = 8 * 1024 * 1024; // pause sending above this bufferedAmount
2728
export function useFileTransfer() {
2829
const signalRef = useRef<SignalClient | null>(null);
2930
const peerRef = useRef<PeerHandles | null>(null);
31+
const manualRef = useRef<ManualConnection | null>(null);
3032
const channelRef = useRef<RTCDataChannel | null>(null);
3133
const modeRef = useRef<TransferMode>('send');
34+
const iceRef = useRef<RTCIceServer[] | undefined>(undefined);
3235
const recvRef = useRef<{ meta: TransferMeta | null; chunks: ArrayBuffer[]; received: number }>({
3336
meta: null,
3437
chunks: [],
@@ -44,13 +47,22 @@ export function useFileTransfer() {
4447
const cleanup = useCallback(() => {
4548
channelRef.current?.close();
4649
peerRef.current?.close();
50+
manualRef.current?.close();
4751
signalRef.current?.close();
4852
channelRef.current = null;
4953
peerRef.current = null;
54+
manualRef.current = null;
5055
signalRef.current = null;
5156
recvRef.current = { meta: null, chunks: [], received: 0 };
5257
}, []);
5358

59+
const handleState = useCallback((state: RTCPeerConnectionState) => {
60+
if (state === 'failed' || state === 'disconnected') {
61+
setError('Could not connect — the network may be too restrictive (no relay server).');
62+
setStatus('error');
63+
}
64+
}, []);
65+
5466
const handleRecv = useCallback((data: string | ArrayBuffer) => {
5567
const r = recvRef.current;
5668
if (typeof data === 'string') {
@@ -92,20 +104,18 @@ export function useFileTransfer() {
92104
const signal = signalRef.current;
93105
peerRef.current = createPeer({
94106
initiator,
107+
iceServers: iceRef.current,
95108
sendSignal: msg => signal.send(msg),
96-
onState: state => {
97-
if (state === 'failed' || state === 'disconnected') {
98-
setError('Could not connect — the network may be too restrictive (no relay server).');
99-
setStatus('error');
100-
}
101-
},
109+
onState: handleState,
102110
onChannel: wireChannel,
103111
});
104-
}, [wireChannel]);
112+
}, [wireChannel, handleState]);
105113

106-
const connect = useCallback((mode: TransferMode, roomId: string) => {
114+
// --- Automatic signaling (via our server) ---
115+
const connect = useCallback((mode: TransferMode, roomId: string, iceServers?: RTCIceServer[]) => {
107116
cleanup();
108117
modeRef.current = mode;
118+
iceRef.current = iceServers;
109119
setError('');
110120
setProgress(0);
111121
setReceivedBlob(null);
@@ -120,7 +130,7 @@ export function useFileTransfer() {
120130
else setStatus('waiting');
121131
break;
122132
case 'peer-joined':
123-
setupPeer(true); // host starts the offer
133+
setupPeer(true);
124134
break;
125135
case 'peer-left':
126136
setError('The other device disconnected.');
@@ -141,6 +151,41 @@ export function useFileTransfer() {
141151
});
142152
}, [cleanup, setupPeer]);
143153

154+
// --- Manual signaling (serverless copy-paste) ---
155+
const manualCreateOffer = useCallback(async (iceServers?: RTCIceServer[]): Promise<string> => {
156+
cleanup();
157+
modeRef.current = 'send';
158+
setError('');
159+
setProgress(0);
160+
setReceivedBlob(null);
161+
setIncoming(null);
162+
setStatus('connecting');
163+
const conn = createManualConnection({ initiator: true, iceServers, onState: handleState, onChannel: wireChannel });
164+
manualRef.current = conn;
165+
const code = await conn.createOfferCode();
166+
setStatus('waiting');
167+
return code;
168+
}, [cleanup, wireChannel, handleState]);
169+
170+
const manualAcceptAnswer = useCallback(async (answerCode: string): Promise<void> => {
171+
await manualRef.current?.acceptAnswer(answerCode);
172+
}, []);
173+
174+
const manualAcceptOffer = useCallback(async (offerCode: string, iceServers?: RTCIceServer[]): Promise<string> => {
175+
cleanup();
176+
modeRef.current = 'receive';
177+
setError('');
178+
setProgress(0);
179+
setReceivedBlob(null);
180+
setIncoming(null);
181+
setStatus('connecting');
182+
const conn = createManualConnection({ initiator: false, iceServers, onState: handleState, onChannel: wireChannel });
183+
manualRef.current = conn;
184+
const answer = await conn.acceptOfferReturnAnswer(offerCode);
185+
setStatus('waiting');
186+
return answer;
187+
}, [cleanup, wireChannel, handleState]);
188+
144189
const sendFile = useCallback(async (file: File) => {
145190
const channel = channelRef.current;
146191
if (!channel || channel.readyState !== 'open') return;
@@ -177,5 +222,18 @@ export function useFileTransfer() {
177222

178223
useEffect(() => () => cleanup(), [cleanup]);
179224

180-
return { status, error, progress, incoming, receivedBlob, connect, sendFile, reset, CHUNK_SIZE };
225+
return {
226+
status,
227+
error,
228+
progress,
229+
incoming,
230+
receivedBlob,
231+
connect,
232+
manualCreateOffer,
233+
manualAcceptAnswer,
234+
manualAcceptOffer,
235+
sendFile,
236+
reset,
237+
CHUNK_SIZE,
238+
};
181239
}

0 commit comments

Comments
 (0)