Uncaught (in promise) DOMException: Unknown ufrag
< /code>
Несмотря на эти ошибки, иногда файл успешно переносится, если я повторяю достаточно раз. При STUN/DIRECT, когда это возможно. Предотвратить эти ошибки? < /p>
// File: src/lib/webrtcSender.ts
import { socket, sendOffer, sendCandidate, registerDevice } from "./socket";
interface Options {
senderId: string;
receiverId: string;
file: File;
onStatus?: (status: string) => void;
}
export function sendFileOverWebRTC({
senderId,
receiverId,
file,
onStatus = () => {},
}: Options): void {
const peerConnection = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});
registerDevice(senderId);
const dataChannel = peerConnection.createDataChannel("fileTransfer");
let remoteDescriptionSet = false;
const pendingCandidates: RTCIceCandidateInit[] = [];
dataChannel.onopen = () => {
onStatus("Sending file...");
sendFileChunks();
};
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
sendCandidate(receiverId, event.candidate);
}
};
socket.off("receive_answer");
socket.on("receive_answer", async ({ answer }) => {
if (!remoteDescriptionSet && peerConnection.signalingState === "have-local-offer") {
await peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
remoteDescriptionSet = true;
// Drain pending candidates
for (const cand of pendingCandidates) {
await peerConnection.addIceCandidate(new RTCIceCandidate(cand));
}
pendingCandidates.length = 0;
} else {
console.warn("Unexpected signaling state:", peerConnection.signalingState);
}
});
socket.off("ice_candidate");
socket.on("ice_candidate", ({ candidate }) => {
if (remoteDescriptionSet) {
peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
} else {
pendingCandidates.push(candidate);
}
});
peerConnection.createOffer()
.then((offer) => peerConnection.setLocalDescription(offer))
.then(() => {
if (peerConnection.localDescription) {
sendOffer(senderId, receiverId, peerConnection.localDescription);
onStatus("Offer sent. Waiting for answer...");
}
});
function sendFileChunks() {
const chunkSize = 16_384;
const reader = new FileReader();
let offset = 0;
dataChannel.send(JSON.stringify({
type: "metadata",
filename: file.name,
filetype: file.type,
size: file.size,
}));
reader.onload = (e) => {
if (e.target?.readyState !== FileReader.DONE) return;
const chunk = e.target.result as ArrayBuffer;
const sendChunk = () => {
if (dataChannel.bufferedAmount > 1_000_000) {
// Wait until buffer drains
setTimeout(sendChunk, 100);
} else {
dataChannel.send(chunk);
offset += chunk.byteLength;
if (offset < file.size) {
readSlice(offset);
} else {
onStatus("File sent successfully!");
}
}
};
sendChunk();
};
reader.onerror = () => onStatus("File read error");
const readSlice = (o: number) => reader.readAsArrayBuffer(file.slice(o, o + chunkSize));
readSlice(0);
}
}
< /code>
// File: src/lib/webrtcSender.ts
import { socket, registerDevice, sendAnswer, sendCandidate } from './socket';
export function initializeReceiver(
fingerprint: string,
onStatus: (status: string) => void,
onFileReceived: (file: Blob, metadata: { name: string; type: string }) => void
) {
registerDevice(fingerprint);
let peerConnection: RTCPeerConnection | null = null;
let remoteDescriptionSet = false;
const pendingCandidates: RTCIceCandidateInit[] = [];
let receivedChunks: Uint8Array[] = [];
let receivedSize = 0;
let metadata: { name: string; type: string; size: number } | null = null;
socket.off('receive_offer');
socket.on('receive_offer', async ({ sender, offer }) => {
if (peerConnection) {
peerConnection.close(); // Prevent reuse
}
onStatus('Offer received. Creating answer...');
peerConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
sendCandidate(sender, event.candidate);
}
};
peerConnection.ondatachannel = (event) => {
const channel = event.channel;
channel.onopen = () => onStatus('Data channel open. Receiving file...');
channel.onmessage = async (event) => {
if (typeof event.data === 'string') {
try {
const msg = JSON.parse(event.data);
if (msg.type === 'metadata') {
metadata = {
name: msg.filename,
type: msg.filetype,
size: msg.size,
};
receivedChunks = [];
receivedSize = 0;
onStatus(`Receiving ${msg.filename} (${msg.size} bytes)`);
}
} catch {
console.warn('Invalid metadata message');
}
} else {
const chunk = event.data instanceof Blob
? new Uint8Array(await event.data.arrayBuffer())
: new Uint8Array(event.data);
receivedChunks.push(chunk);
receivedSize += chunk.byteLength;
if (metadata && receivedSize >= metadata.size) {
const blob = new Blob(receivedChunks, { type: metadata.type });
onFileReceived(blob, metadata);
onStatus('File received and ready to download.');
}
}
};
};
await peerConnection.setRemoteDescription(offer);
remoteDescriptionSet = true;
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
sendAnswer(sender, answer);
onStatus('Answer sent.');
// Drain buffered ICE candidates
for (const cand of pendingCandidates) {
await peerConnection.addIceCandidate(new RTCIceCandidate(cand));
}
pendingCandidates.length = 0;
});
socket.off("ice_candidate");
socket.on("ice_candidate", ({ candidate }) => {
if (remoteDescriptionSet && peerConnection) {
peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
} else {
pendingCandidates.push(candidate);
}
});
}
< /code>
// File: src/dash/page.tsx
'use client';
import { useEffect, useState, useRef } from 'react';
import { useRouter } from 'next/navigation';
import { useAuthStore } from '../../store/useAuthStore';
import api from '../../lib/axios';
import FingerprintJS from '@fingerprintjs/fingerprintjs';
import { sendFileOverWebRTC } from '../../lib/webrtcSender';
import { initializeReceiver } from '../../lib/webrtcReceiver';
export default function DashPage() {
const { user, checkAuth, loading } = useAuthStore();
const router = useRouter();
const [devices, setDevices] = useState([]);
const [deviceName, setDeviceName] = useState('');
const [fingerprint, setFingerprint] = useState('');
const [status, setStatus] = useState('Idle');
const [selectedFile, setSelectedFile] = useState(null);
const [selectedDevice, setSelectedDevice] = useState('');
const fileInputRef = useRef(null);
// Initial auth check
useEffect(() => {
checkAuth();
}, [checkAuth]);
useEffect(() => {
if (!loading && !user) {
router.replace('/auth');
}
}, [loading, user, router]);
// Fetch user's devices
useEffect(() => {
if (!loading && user) {
api
.get('/devices/')
.then((res) => setDevices(res.data))
.catch((err) => console.error('Device fetch failed', err));
}
}, [loading, user]);
// Fingerprint only
useEffect(() => {
const loadFingerprint = async () => {
setStatus('Loading fingerprint...');
const fp = await FingerprintJS.load();
const result = await fp.get();
setFingerprint(result.visitorId);
setStatus('Ready to add device');
};
loadFingerprint();
}, []);
// Initialize receiver
useEffect(() => {
if (fingerprint) {
initializeReceiver(
fingerprint,
(newStatus) => setStatus(newStatus),
(fileBlob, metadata) => {
const url = URL.createObjectURL(fileBlob);
const a = document.createElement('a');
a.href = url;
a.download = metadata.name;
a.click();
URL.revokeObjectURL(url);
}
);
}
}, [fingerprint]);
const handleAddDevice = async () => {
if (!deviceName || !fingerprint) {
alert('Missing fingerprint or device name');
return;
}
try {
await api.post('/add-device/', {
fingerprint,
device_name: deviceName,
});
setDeviceName('');
setStatus('Device added successfully');
// Refresh device list
const res = await api.get('/devices/');
setDevices(res.data);
} catch (error) {
console.error('Error adding device:', error);
setStatus('Failed to add device');
}
};
const handleFileChange = (e: React.ChangeEvent) => {
if (e.target.files && e.target.files.length > 0) {
setSelectedFile(e.target.files[0]);
}
};
const handleSendFile = () => {
if (!selectedFile || !selectedDevice) {
alert('Please select a file and a target device.');
return;
}
sendFileOverWebRTC({
senderId: fingerprint,
receiverId: selectedDevice,
file: selectedFile,
onStatus: setStatus,
});
};
if (loading) return
Loading dashboard...
;
if (!user) return null;
return (
Welcome, {user.username}
Your email: {user.email}
Your Devices:
- {devices.length === 0 &&
No devices found.
}
{devices.map((device: any) => ( -
{device.device_name} ({device.fingerprint})
))}
Add This Device
Status: {status}
setDeviceName(e.target.value)}
/>
Add This Device
Send a File
setSelectedDevice(e.target.value)}
>
Select a device
{devices.map((device: any) => (
{device.device_name}
))}
Send File
);
}
Подробнее здесь: https://stackoverflow.com/questions/796 ... -unknown-u
Мобильная версия