11import { useCallback , useEffect , useRef , useState } from 'react' ;
22import { connectSignal , type SignalClient } from '@/tools/webrtc/signal-client' ;
33import { createPeer , type PeerHandles } from '@/tools/webrtc/peer' ;
4+ import { createManualConnection , type ManualConnection } from '@/tools/webrtc/manual' ;
45import {
56 CHUNK_SIZE ,
67 chunkCount ,
@@ -27,8 +28,10 @@ const HIGH_WATER = 8 * 1024 * 1024; // pause sending above this bufferedAmount
2728export 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