Files
filedrop/web/src/webrtc/transport.ts
agent 4b34a85599 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
2026-04-09 10:32:06 +08:00

71 lines
1.8 KiB
TypeScript

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;
}
}