init: FileDrop phase1 architecture and scaffold
- Rust axum signaling server with WebSocket support - Lit + TypeScript frontend with Vite - Redis session storage with TTL - WebRTC transport and crypto client stubs - Phase1 architecture plan in plans/ - Deploy directory structure prepared
This commit is contained in:
70
web/src/webrtc/transport.ts
Normal file
70
web/src/webrtc/transport.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export class WebRTCTransport {
|
||||
private pc: RTCPeerConnection | null = null;
|
||||
private dc: RTCDataChannel | null = null;
|
||||
private iceServers: RTCIceServer[];
|
||||
|
||||
constructor(iceServers: RTCIceServer[]) {
|
||||
this.iceServers = iceServers;
|
||||
}
|
||||
|
||||
async createOffer(): Promise<{ sdp: string; candidates: RTCIceCandidate[] }> {
|
||||
this.pc = new RTCPeerConnection({ iceServers: this.iceServers });
|
||||
this.dc = this.pc.createDataChannel('filedrop', { ordered: true });
|
||||
|
||||
const offer = await this.pc.createOffer();
|
||||
await this.pc.setLocalDescription(offer);
|
||||
|
||||
const candidates = await this.waitForIceGathering();
|
||||
return {
|
||||
sdp: this.pc.localDescription?.sdp || '',
|
||||
candidates,
|
||||
};
|
||||
}
|
||||
|
||||
async handleAnswer(answer: { sdp: string }) {
|
||||
if (!this.pc) return;
|
||||
await this.pc.setRemoteDescription(
|
||||
new RTCSessionDescription({ type: 'answer', sdp: answer.sdp })
|
||||
);
|
||||
}
|
||||
|
||||
async handleCandidate(candidate: RTCIceCandidateInit) {
|
||||
await this.pc?.addIceCandidate(candidate);
|
||||
}
|
||||
|
||||
onDataChannel(callback: (dc: RTCDataChannel) => void) {
|
||||
if (this.pc) {
|
||||
this.pc.ondatachannel = (e) => callback(e.channel);
|
||||
}
|
||||
}
|
||||
|
||||
getDataChannel() {
|
||||
return this.dc;
|
||||
}
|
||||
|
||||
private waitForIceGathering(): Promise<RTCIceCandidate[]> {
|
||||
return new Promise((resolve) => {
|
||||
const candidates: RTCIceCandidate[] = [];
|
||||
if (!this.pc) return resolve(candidates);
|
||||
|
||||
this.pc.onicecandidate = (e) => {
|
||||
if (e.candidate) candidates.push(e.candidate);
|
||||
};
|
||||
|
||||
this.pc.onicegatheringstatechange = () => {
|
||||
if (this.pc?.iceGatheringState === 'complete') {
|
||||
resolve(candidates);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => resolve(candidates), 5000);
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.dc?.close();
|
||||
this.pc?.close();
|
||||
this.dc = null;
|
||||
this.pc = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user