mirror of
https://github.com/imputnet/cobalt.git
synced 2025-06-28 09:28:29 +00:00
文件传输3
This commit is contained in:
parent
a0f785ae01
commit
415243583d
@ -69,9 +69,40 @@ export const setupSignalingServer = (httpServer) => {
|
||||
|
||||
ws.on('error', (error) => {
|
||||
console.error('WebSocket错误:', error);
|
||||
});
|
||||
|
||||
function handleCreateSession(ws, message) {
|
||||
}); function handleCreateSession(ws, message) {
|
||||
// 检查是否提供了现有会话ID(用于重连)
|
||||
if (message.existingSessionId) {
|
||||
const existingSession = sessions.get(message.existingSessionId);
|
||||
if (existingSession && !existingSession.creator) {
|
||||
// 重连到现有会话
|
||||
sessionId = message.existingSessionId;
|
||||
userRole = 'creator';
|
||||
existingSession.creator = { ws, publicKey: message.publicKey };
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'session_reconnected',
|
||||
sessionId: sessionId
|
||||
}));
|
||||
|
||||
// 如果有在线的 joiner,通知双方重新建立连接
|
||||
if (existingSession.joiner && existingSession.joiner.ws.readyState === ws.OPEN) {
|
||||
existingSession.joiner.ws.send(JSON.stringify({
|
||||
type: 'creator_reconnected',
|
||||
publicKey: message.publicKey
|
||||
}));
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'peer_already_joined',
|
||||
publicKey: existingSession.joiner.publicKey
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(`创建者重连会话: ${sessionId}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新会话
|
||||
sessionId = Math.random().toString(36).substring(2, 10);
|
||||
userRole = 'creator';
|
||||
|
||||
@ -87,9 +118,7 @@ export const setupSignalingServer = (httpServer) => {
|
||||
}));
|
||||
|
||||
console.log(`会话创建: ${sessionId}`);
|
||||
}
|
||||
|
||||
function handleJoinSession(ws, message) {
|
||||
}function handleJoinSession(ws, message) {
|
||||
const session = sessions.get(message.sessionId);
|
||||
|
||||
if (!session) {
|
||||
@ -100,7 +129,8 @@ export const setupSignalingServer = (httpServer) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.joiner) {
|
||||
// 检查是否已有活跃的 joiner
|
||||
if (session.joiner && session.joiner.ws.readyState === ws.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'error',
|
||||
message: '会话已满'
|
||||
@ -111,23 +141,31 @@ export const setupSignalingServer = (httpServer) => {
|
||||
sessionId = message.sessionId;
|
||||
userRole = 'joiner';
|
||||
|
||||
// 设置或重新设置 joiner
|
||||
session.joiner = { ws, publicKey: message.publicKey };
|
||||
|
||||
// 通知创建者有人加入,并交换公钥
|
||||
// 通知创建者有人加入(如果创建者在线)
|
||||
if (session.creator && session.creator.ws.readyState === ws.OPEN) {
|
||||
session.creator.ws.send(JSON.stringify({
|
||||
type: 'peer_joined',
|
||||
publicKey: message.publicKey
|
||||
}));
|
||||
|
||||
// 回复加入者创建者的公钥
|
||||
ws.send(JSON.stringify({
|
||||
type: 'session_joined',
|
||||
publicKey: session.creator.publicKey
|
||||
}));
|
||||
|
||||
console.log(`用户重连到会话: ${sessionId}`);
|
||||
} else {
|
||||
// 创建者不在线,通知加入者等待
|
||||
ws.send(JSON.stringify({
|
||||
type: 'waiting_for_creator',
|
||||
message: '等待创建者重新连接'
|
||||
}));
|
||||
console.log(`加入者在线,等待创建者: ${sessionId}`);
|
||||
}
|
||||
|
||||
// 回复加入者创建者的公钥
|
||||
ws.send(JSON.stringify({
|
||||
type: 'session_joined',
|
||||
publicKey: session.creator.publicKey
|
||||
}));
|
||||
|
||||
console.log(`用户加入会话: ${sessionId}`);
|
||||
}
|
||||
|
||||
function handleSignaling(ws, message, sessionId, userRole) {
|
||||
@ -153,15 +191,13 @@ export const setupSignalingServer = (httpServer) => {
|
||||
if (peer && peer.ws.readyState === ws.OPEN) {
|
||||
peer.ws.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
function handleDisconnect(sessionId, userRole) {
|
||||
} function handleDisconnect(sessionId, userRole) {
|
||||
if (!sessionId) return;
|
||||
|
||||
const session = sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
// 通知对端连接断开
|
||||
// 通知对端连接断开(但不删除会话)
|
||||
const peer = userRole === 'creator' ? session.joiner : session.creator;
|
||||
if (peer && peer.ws.readyState === ws.OPEN) {
|
||||
peer.ws.send(JSON.stringify({
|
||||
@ -169,9 +205,22 @@ export const setupSignalingServer = (httpServer) => {
|
||||
}));
|
||||
}
|
||||
|
||||
// 清理会话
|
||||
sessions.delete(sessionId);
|
||||
console.log(`会话结束: ${sessionId}`);
|
||||
// 只清理断开的用户,保留会话结构
|
||||
if (userRole === 'creator') {
|
||||
console.log(`创建者断开连接: ${sessionId}`);
|
||||
session.creator = null;
|
||||
} else {
|
||||
console.log(`加入者断开连接: ${sessionId}`);
|
||||
session.joiner = null;
|
||||
}
|
||||
|
||||
// 如果双方都断开,才删除会话
|
||||
if (!session.creator && !session.joiner) {
|
||||
sessions.delete(sessionId);
|
||||
console.log(`会话结束(双方都断开): ${sessionId}`);
|
||||
} else {
|
||||
console.log(`会话保留,等待重连: ${sessionId}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
<script lang="ts"> import { onMount, onDestroy } from 'svelte';
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { t } from '$lib/i18n/translations';
|
||||
import SettingsCategory from '$components/settings/SettingsCategory.svelte';
|
||||
import ActionButton from '$components/buttons/ActionButton.svelte';
|
||||
@ -50,10 +51,110 @@
|
||||
let receivingFiles = false;
|
||||
let transferProgress = 0;
|
||||
let currentReceivingFile: ReceivingFile | null = null;
|
||||
|
||||
// Session persistence
|
||||
let storedSessionId = '';
|
||||
let storedIsCreator = false;
|
||||
|
||||
// Load stored session data
|
||||
function loadStoredSession(): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
const stored = localStorage.getItem('clipboard_session');
|
||||
if (stored) {
|
||||
try {
|
||||
const sessionData = JSON.parse(stored);
|
||||
const now = Date.now();
|
||||
|
||||
// Check if session is still valid (30 minute timeout)
|
||||
if (sessionData.timestamp && (now - sessionData.timestamp) < 30 * 60 * 1000) {
|
||||
storedSessionId = sessionData.sessionId || '';
|
||||
storedIsCreator = sessionData.isCreator || false;
|
||||
console.log('📁 Loaded stored session:', {
|
||||
sessionId: storedSessionId,
|
||||
isCreator: storedIsCreator,
|
||||
age: Math.round((now - sessionData.timestamp) / 1000) + 's'
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
console.log('🕐 Stored session expired, clearing...');
|
||||
localStorage.removeItem('clipboard_session');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error loading stored session:', error);
|
||||
localStorage.removeItem('clipboard_session');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
storedSessionId = '';
|
||||
storedIsCreator = false;
|
||||
}
|
||||
|
||||
// Save session data to localStorage
|
||||
function saveSession(sessionId: string, isCreator: boolean): void {
|
||||
if (typeof window !== 'undefined' && sessionId) {
|
||||
const sessionData = {
|
||||
sessionId,
|
||||
isCreator,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
localStorage.setItem('clipboard_session', JSON.stringify(sessionData));
|
||||
console.log('💾 Session saved to localStorage:', sessionData);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear stored session
|
||||
function clearStoredSession(): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('clipboard_session');
|
||||
console.log('🗑️ Stored session cleared');
|
||||
}
|
||||
storedSessionId = '';
|
||||
storedIsCreator = false;
|
||||
}
|
||||
|
||||
// Attempt to reconnect to stored session
|
||||
async function reconnectToStoredSession(): Promise<boolean> {
|
||||
if (!storedSessionId) return false;
|
||||
|
||||
try {
|
||||
console.log('🔄 Attempting to reconnect to stored session:', storedSessionId);
|
||||
|
||||
await generateKeyPair();
|
||||
await connectWebSocket();
|
||||
|
||||
const publicKeyBuffer = await exportPublicKey();
|
||||
const publicKeyArray = Array.from(new Uint8Array(publicKeyBuffer));
|
||||
|
||||
if (ws) {
|
||||
if (storedIsCreator) {
|
||||
// Reconnect as creator
|
||||
ws.send(JSON.stringify({
|
||||
type: 'create_session',
|
||||
existingSessionId: storedSessionId,
|
||||
publicKey: publicKeyArray
|
||||
}));
|
||||
} else {
|
||||
// Reconnect as joiner
|
||||
ws.send(JSON.stringify({
|
||||
type: 'join_session',
|
||||
sessionId: storedSessionId,
|
||||
publicKey: publicKeyArray
|
||||
}));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error reconnecting to stored session:', error);
|
||||
clearStoredSession();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Constants
|
||||
const CHUNK_SIZE = 64 * 1024; // 64KB chunks
|
||||
|
||||
// Lifecycle functions
|
||||
let statusInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
onMount(async () => {
|
||||
// Check for session parameter in URL
|
||||
if (typeof window !== 'undefined') {
|
||||
@ -62,13 +163,43 @@
|
||||
if (sessionParam) {
|
||||
joinCode = sessionParam;
|
||||
await joinSession();
|
||||
} else {
|
||||
// Try to reconnect to stored session if no session param
|
||||
loadStoredSession();
|
||||
if (storedSessionId) {
|
||||
console.log('🔄 Reconnecting to stored session on mount:', storedSessionId);
|
||||
await reconnectToStoredSession();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start periodic connection status check
|
||||
statusInterval = setInterval(() => {
|
||||
if (dataChannel) {
|
||||
const wasConnected = peerConnected;
|
||||
const isNowConnected = dataChannel.readyState === 'open';
|
||||
|
||||
if (wasConnected !== isNowConnected) {
|
||||
console.log('Data channel state changed:', {
|
||||
was: wasConnected,
|
||||
now: isNowConnected,
|
||||
readyState: dataChannel.readyState
|
||||
});
|
||||
peerConnected = isNowConnected;
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (statusInterval) {
|
||||
clearInterval(statusInterval);
|
||||
}
|
||||
cleanup();
|
||||
}); function getWebSocketURL(): string {
|
||||
});
|
||||
|
||||
// Helper functions
|
||||
function getWebSocketURL(): string {
|
||||
if (typeof window === 'undefined') return 'ws://localhost:9000/ws';
|
||||
|
||||
const apiUrl = currentApiURL();
|
||||
@ -98,13 +229,14 @@
|
||||
{ name: 'ECDH', namedCurve: 'P-256' },
|
||||
false,
|
||||
['deriveKey']
|
||||
);
|
||||
}
|
||||
); }
|
||||
|
||||
async function exportPublicKey(): Promise<ArrayBuffer> {
|
||||
if (!keyPair) throw new Error('Key pair not generated');
|
||||
return await window.crypto.subtle.exportKey('raw', keyPair.publicKey);
|
||||
} async function importRemotePublicKey(publicKeyArray: number[]): Promise<void> {
|
||||
}
|
||||
|
||||
async function importRemotePublicKey(publicKeyArray: number[]): Promise<void> {
|
||||
const publicKeyBuffer = new Uint8Array(publicKeyArray).buffer;
|
||||
remotePublicKey = await window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
@ -208,6 +340,8 @@
|
||||
}
|
||||
});
|
||||
} async function handleWebSocketMessage(message: any): Promise<void> {
|
||||
console.log('Handling WebSocket message:', message.type, message);
|
||||
|
||||
switch (message.type) {
|
||||
case 'session_created':
|
||||
sessionId = message.sessionId;
|
||||
@ -215,23 +349,68 @@
|
||||
isCreator = true;
|
||||
console.log('Session created:', sessionId);
|
||||
await generateQRCode();
|
||||
saveSession(sessionId, isCreator); // Save session on create
|
||||
break;
|
||||
|
||||
case 'session_reconnected':
|
||||
sessionId = message.sessionId;
|
||||
isCreating = false;
|
||||
isCreator = true;
|
||||
console.log('Session reconnected:', sessionId);
|
||||
await generateQRCode();
|
||||
saveSession(sessionId, isCreator); // Update saved session on reconnect
|
||||
break;
|
||||
|
||||
case 'session_joined':
|
||||
console.log('Session joined successfully, setting up WebRTC...');
|
||||
await importRemotePublicKey(message.publicKey);
|
||||
await deriveSharedKey();
|
||||
isJoining = false;
|
||||
console.log('About to setup WebRTC as joiner (offer=false)');
|
||||
await setupWebRTC(false);
|
||||
break;
|
||||
|
||||
case 'waiting_for_creator':
|
||||
console.log('Waiting for creator to reconnect...');
|
||||
isJoining = false;
|
||||
// 可以显示等待状态给用户
|
||||
break;
|
||||
|
||||
case 'creator_reconnected':
|
||||
console.log('Creator reconnected, setting up WebRTC...');
|
||||
await importRemotePublicKey(message.publicKey);
|
||||
await deriveSharedKey();
|
||||
console.log('About to setup WebRTC as joiner (offer=false)');
|
||||
await setupWebRTC(false);
|
||||
break;
|
||||
|
||||
case 'peer_already_joined':
|
||||
console.log('Peer already joined, setting up WebRTC...');
|
||||
await importRemotePublicKey(message.publicKey);
|
||||
await deriveSharedKey();
|
||||
console.log('Setting up WebRTC as creator (offer=true)');
|
||||
await setupWebRTC(true);
|
||||
break;
|
||||
|
||||
case 'peer_joined':
|
||||
console.log('Peer joined, setting up WebRTC...');
|
||||
await importRemotePublicKey(message.publicKey);
|
||||
await deriveSharedKey();
|
||||
if (isCreator) {
|
||||
console.log('Setting up WebRTC as creator (offer=true)');
|
||||
await setupWebRTC(true);
|
||||
} else {
|
||||
console.log('Setting up WebRTC as joiner (offer=false)');
|
||||
await setupWebRTC(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'peer_disconnected':
|
||||
console.log('Peer disconnected, waiting for reconnection...');
|
||||
peerConnected = false;
|
||||
// 可以显示等待重连状态
|
||||
break;
|
||||
|
||||
case 'offer':
|
||||
await handleOffer(message.offer);
|
||||
break;
|
||||
@ -307,76 +486,251 @@
|
||||
alert(`Failed to join session: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
isJoining = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function setupWebRTC(shouldCreateOffer: boolean): Promise<void> {
|
||||
} async function setupWebRTC(shouldCreateOffer: boolean): Promise<void> {
|
||||
console.log('Setting up WebRTC...', { shouldCreateOffer, isCreator });
|
||||
|
||||
// Clean up any existing connection
|
||||
if (peerConnection) {
|
||||
console.log('🧹 Cleaning up existing peer connection');
|
||||
peerConnection.close();
|
||||
}
|
||||
|
||||
peerConnection = new RTCPeerConnection({
|
||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
||||
iceServers: [
|
||||
{ urls: 'stun:stun.l.google.com:19302' },
|
||||
{ urls: 'stun:stun1.l.google.com:19302' },
|
||||
{ urls: 'stun:stun2.l.google.com:19302' }
|
||||
],
|
||||
iceCandidatePoolSize: 10
|
||||
});
|
||||
|
||||
peerConnection.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
|
||||
if (event.candidate && ws) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'ice_candidate',
|
||||
candidate: event.candidate
|
||||
}));
|
||||
// Enhanced connection state tracking
|
||||
peerConnection.onconnectionstatechange = () => {
|
||||
const state = peerConnection?.connectionState;
|
||||
console.log('🔄 Peer connection state changed:', state);
|
||||
|
||||
if (state === 'connected') {
|
||||
console.log('🎉 Peer connection established successfully!');
|
||||
} else if (state === 'failed') {
|
||||
console.error('❌ Peer connection failed');
|
||||
peerConnected = false;
|
||||
// Try to restart connection after a delay
|
||||
setTimeout(() => {
|
||||
if (isCreator && state === 'failed') {
|
||||
console.log('🔄 Attempting to restart failed connection...');
|
||||
setupWebRTC(true);
|
||||
}
|
||||
}, 2000);
|
||||
} else if (state === 'disconnected') {
|
||||
console.warn('⚠️ Peer connection disconnected');
|
||||
peerConnected = false;
|
||||
}
|
||||
};
|
||||
|
||||
peerConnection.oniceconnectionstatechange = () => {
|
||||
const iceState = peerConnection?.iceConnectionState;
|
||||
console.log('🧊 ICE connection state changed:', iceState);
|
||||
|
||||
if (iceState === 'connected' || iceState === 'completed') {
|
||||
console.log('🎉 ICE connection established!');
|
||||
} else if (iceState === 'failed') {
|
||||
console.error('❌ ICE connection failed - checking data channel...');
|
||||
if (dataChannel && dataChannel.readyState !== 'open') {
|
||||
console.log('🔄 ICE failed but data channel not open, will restart');
|
||||
setTimeout(() => {
|
||||
if (isCreator && iceState === 'failed') {
|
||||
console.log('🔄 Restarting WebRTC due to ICE failure...');
|
||||
setupWebRTC(true);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
} else if (iceState === 'disconnected') {
|
||||
console.warn('⚠️ ICE connection disconnected');
|
||||
}
|
||||
};
|
||||
|
||||
peerConnection.onicegatheringstatechange = () => {
|
||||
const gatheringState = peerConnection?.iceGatheringState;
|
||||
console.log('🧊 ICE gathering state changed:', gatheringState);
|
||||
|
||||
if (gatheringState === 'complete') {
|
||||
console.log('✅ ICE gathering completed');
|
||||
}
|
||||
};
|
||||
|
||||
peerConnection.onicecandidate = (event: RTCPeerConnectionIceEvent) => {
|
||||
if (event.candidate) {
|
||||
console.log('🧊 New ICE candidate generated:', {
|
||||
candidate: event.candidate.candidate,
|
||||
sdpMid: event.candidate.sdpMid,
|
||||
sdpMLineIndex: event.candidate.sdpMLineIndex,
|
||||
type: event.candidate.type,
|
||||
protocol: event.candidate.protocol
|
||||
});
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'ice_candidate',
|
||||
candidate: event.candidate
|
||||
}));
|
||||
console.log('📤 ICE candidate sent via WebSocket');
|
||||
} else {
|
||||
console.error('❌ WebSocket not ready when sending ICE candidate');
|
||||
}
|
||||
} else {
|
||||
console.log('🏁 ICE gathering completed (null candidate received)');
|
||||
}
|
||||
};
|
||||
|
||||
peerConnection.onsignalingstatechange = () => {
|
||||
const signalingState = peerConnection?.signalingState;
|
||||
console.log('📡 Signaling state changed:', signalingState);
|
||||
|
||||
if (signalingState === 'stable') {
|
||||
console.log('✅ Signaling negotiation completed successfully');
|
||||
} else if (signalingState === 'closed') {
|
||||
console.log('🔒 Peer connection signaling closed');
|
||||
}
|
||||
};
|
||||
|
||||
peerConnection.ondatachannel = (event: RTCDataChannelEvent) => {
|
||||
console.log('📥 Data channel received from remote peer:', {
|
||||
label: event.channel.label,
|
||||
readyState: event.channel.readyState,
|
||||
ordered: event.channel.ordered,
|
||||
protocol: event.channel.protocol
|
||||
});
|
||||
dataChannel = event.channel;
|
||||
setupDataChannel(dataChannel);
|
||||
};
|
||||
|
||||
if (shouldCreateOffer) {
|
||||
dataChannel = peerConnection.createDataChannel('fileTransfer', { ordered: true });
|
||||
console.log('👑 Creating data channel and offer (as creator)...');
|
||||
dataChannel = peerConnection.createDataChannel('fileTransfer', {
|
||||
ordered: true,
|
||||
maxRetransmits: 3
|
||||
});
|
||||
console.log('📡 Data channel created:', {
|
||||
label: dataChannel.label,
|
||||
readyState: dataChannel.readyState,
|
||||
ordered: dataChannel.ordered
|
||||
});
|
||||
setupDataChannel(dataChannel);
|
||||
|
||||
peerConnection.createOffer().then((offer: RTCSessionDescriptionInit) => {
|
||||
if (peerConnection) {
|
||||
peerConnection.setLocalDescription(offer);
|
||||
if (ws) {
|
||||
ws.send(JSON.stringify({ type: 'offer', offer: offer }));
|
||||
}
|
||||
try {
|
||||
console.log('⏳ Creating WebRTC offer...');
|
||||
const offer = await peerConnection.createOffer();
|
||||
console.log('📝 Offer created, setting as local description...');
|
||||
await peerConnection.setLocalDescription(offer);
|
||||
console.log('✅ Local description set successfully');
|
||||
console.log('Offer details:', {
|
||||
type: offer.type,
|
||||
sdpPreview: offer.sdp?.substring(0, 200) + '...'
|
||||
});
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'offer', offer: offer }));
|
||||
console.log('📤 Offer sent via WebSocket');
|
||||
} else {
|
||||
console.error('❌ WebSocket not ready when sending offer');
|
||||
}
|
||||
});
|
||||
|
||||
// Enhanced timeout for connection establishment
|
||||
setTimeout(() => {
|
||||
if (dataChannel && dataChannel.readyState !== 'open') {
|
||||
console.warn('⚠️ Data channel still not open after 30 seconds');
|
||||
console.log('Current connection states:', {
|
||||
dataChannelState: dataChannel.readyState,
|
||||
peerConnectionState: peerConnection?.connectionState,
|
||||
iceConnectionState: peerConnection?.iceConnectionState,
|
||||
iceGatheringState: peerConnection?.iceGatheringState,
|
||||
signalingState: peerConnection?.signalingState
|
||||
});
|
||||
|
||||
// Auto-restart if stuck
|
||||
console.log('🔄 Auto-restarting WebRTC connection due to timeout...');
|
||||
setupWebRTC(true);
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating offer:', error);
|
||||
}
|
||||
} else {
|
||||
peerConnection.ondatachannel = (event: RTCDataChannelEvent) => {
|
||||
dataChannel = event.channel;
|
||||
setupDataChannel(dataChannel);
|
||||
};
|
||||
console.log('👥 Waiting for data channel from remote peer (as joiner)...');
|
||||
}
|
||||
}
|
||||
}function setupDataChannel(channel: RTCDataChannel): void {
|
||||
console.log('Setting up data channel:', {
|
||||
readyState: channel.readyState,
|
||||
label: channel.label,
|
||||
ordered: channel.ordered,
|
||||
protocol: channel.protocol
|
||||
});
|
||||
|
||||
function setupDataChannel(channel: RTCDataChannel): void {
|
||||
channel.onopen = () => {
|
||||
console.log('Data channel opened');
|
||||
console.log('🎉 Data channel opened successfully!');
|
||||
console.log('Data channel final state:', {
|
||||
readyState: channel.readyState,
|
||||
bufferedAmount: channel.bufferedAmount,
|
||||
maxPacketLifeTime: channel.maxPacketLifeTime,
|
||||
maxRetransmits: channel.maxRetransmits
|
||||
});
|
||||
peerConnected = true;
|
||||
console.log('✅ peerConnected set to true, UI should update');
|
||||
|
||||
// Force reactive update
|
||||
peerConnected = peerConnected;
|
||||
};
|
||||
|
||||
channel.onclose = () => {
|
||||
console.log('Data channel closed');
|
||||
console.log('❌ Data channel closed');
|
||||
console.log('Data channel close state:', {
|
||||
readyState: channel.readyState,
|
||||
peerConnectionState: peerConnection?.connectionState
|
||||
});
|
||||
peerConnected = false;
|
||||
};
|
||||
|
||||
channel.onerror = (error) => {
|
||||
console.error('❌ Data channel error:', error);
|
||||
console.error('Data channel error details:', {
|
||||
readyState: channel.readyState,
|
||||
error: error,
|
||||
peerConnectionState: peerConnection?.connectionState,
|
||||
iceConnectionState: peerConnection?.iceConnectionState
|
||||
});
|
||||
};
|
||||
|
||||
channel.onmessage = async (event) => {
|
||||
try {
|
||||
console.log('📩 Received data channel message, size:', event.data.byteLength || event.data.length);
|
||||
const encryptedData = event.data;
|
||||
const decryptedMessage = await decryptData(encryptedData);
|
||||
const message = JSON.parse(decryptedMessage);
|
||||
|
||||
console.log('📩 Decrypted message type:', message.type);
|
||||
|
||||
if (message.type === 'text') {
|
||||
textContent = message.data;
|
||||
console.log('📝 Text content received');
|
||||
} else if (message.type === 'file_info') {
|
||||
currentReceivingFile = {
|
||||
name: message.name,
|
||||
size: message.size,
|
||||
type: message.type,
|
||||
type: message.mimeType,
|
||||
chunks: [],
|
||||
receivedSize: 0
|
||||
};
|
||||
receivingFiles = true;
|
||||
console.log('📁 File transfer started:', message.name);
|
||||
} else if (message.type === 'file_chunk' && currentReceivingFile) {
|
||||
const chunkData = new Uint8Array(message.data);
|
||||
currentReceivingFile.chunks.push(chunkData);
|
||||
currentReceivingFile.receivedSize += chunkData.length;
|
||||
transferProgress = (currentReceivingFile.receivedSize / currentReceivingFile.size) * 100;
|
||||
|
||||
console.log(`📦 File chunk received: ${Math.round(transferProgress)}%`);
|
||||
|
||||
if (currentReceivingFile.receivedSize >= currentReceivingFile.size) {
|
||||
const completeFile = new Blob(currentReceivingFile.chunks, { type: currentReceivingFile.type });
|
||||
receivedFiles = [...receivedFiles, {
|
||||
@ -386,37 +740,106 @@
|
||||
blob: completeFile
|
||||
}];
|
||||
|
||||
console.log('✅ File transfer completed:', currentReceivingFile.name);
|
||||
currentReceivingFile = null;
|
||||
receivingFiles = false;
|
||||
transferProgress = 0;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling data channel message:', error);
|
||||
console.error('❌ Error handling data channel message:', error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function handleOffer(offer: RTCSessionDescriptionInit): Promise<void> {
|
||||
if (!peerConnection) return;
|
||||
// If the channel is already open, set the state immediately
|
||||
if (channel.readyState === 'open') {
|
||||
console.log('✅ Data channel was already open, setting peerConnected to true');
|
||||
peerConnected = true;
|
||||
} else {
|
||||
console.log('⏳ Data channel not yet open, current state:', channel.readyState);
|
||||
}
|
||||
} async function handleOffer(offer: RTCSessionDescriptionInit): Promise<void> {
|
||||
if (!peerConnection) {
|
||||
console.error('❌ No peer connection when handling offer');
|
||||
return;
|
||||
}
|
||||
|
||||
await peerConnection.setRemoteDescription(offer);
|
||||
const answer = await peerConnection.createAnswer();
|
||||
await peerConnection.setLocalDescription(answer);
|
||||
|
||||
if (ws) {
|
||||
ws.send(JSON.stringify({ type: 'answer', answer: answer }));
|
||||
try {
|
||||
console.log('📥 Received offer, setting as remote description...');
|
||||
console.log('Offer SDP preview:', offer.sdp?.substring(0, 200) + '...');
|
||||
|
||||
await peerConnection.setRemoteDescription(offer);
|
||||
console.log('✅ Remote description set successfully');
|
||||
|
||||
console.log('⏳ Creating answer...');
|
||||
const answer = await peerConnection.createAnswer();
|
||||
await peerConnection.setLocalDescription(answer);
|
||||
console.log('✅ Answer created and set as local description');
|
||||
console.log('Answer SDP preview:', answer.sdp?.substring(0, 200) + '...');
|
||||
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'answer', answer: answer }));
|
||||
console.log('📤 Answer sent via WebSocket');
|
||||
} else {
|
||||
console.error('❌ WebSocket not ready when sending answer');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error handling offer:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAnswer(answer: RTCSessionDescriptionInit): Promise<void> {
|
||||
if (!peerConnection) return;
|
||||
await peerConnection.setRemoteDescription(answer);
|
||||
if (!peerConnection) {
|
||||
console.error('❌ No peer connection when handling answer');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('📥 Received answer, setting as remote description...');
|
||||
console.log('Answer SDP preview:', answer.sdp?.substring(0, 200) + '...');
|
||||
|
||||
await peerConnection.setRemoteDescription(answer);
|
||||
console.log('✅ Remote description set successfully from answer');
|
||||
} catch (error) {
|
||||
console.error('❌ Error handling answer:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {
|
||||
if (!peerConnection) return;
|
||||
await peerConnection.addIceCandidate(candidate);
|
||||
if (!peerConnection) {
|
||||
console.error('❌ No peer connection when handling ICE candidate');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('🧊 Received ICE candidate:', {
|
||||
candidate: candidate.candidate,
|
||||
sdpMid: candidate.sdpMid,
|
||||
sdpMLineIndex: candidate.sdpMLineIndex
|
||||
});
|
||||
|
||||
// Wait for remote description to be set before adding ICE candidates
|
||||
if (peerConnection.remoteDescription) {
|
||||
await peerConnection.addIceCandidate(candidate);
|
||||
console.log('✅ ICE candidate added successfully');
|
||||
} else {
|
||||
console.warn('⚠️ Remote description not set yet, queuing ICE candidate');
|
||||
// You might want to queue candidates here, but for simplicity we'll just wait
|
||||
setTimeout(async () => {
|
||||
if (peerConnection && peerConnection.remoteDescription) {
|
||||
try {
|
||||
await peerConnection.addIceCandidate(candidate);
|
||||
console.log('✅ Queued ICE candidate added successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error adding queued ICE candidate:', error);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error handling ICE candidate:', error);
|
||||
console.error('ICE candidate details:', candidate);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendText(): Promise<void> {
|
||||
@ -566,6 +989,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function restartWebRTC(): Promise<void> {
|
||||
console.log('🔄 Manually restarting WebRTC connection...');
|
||||
|
||||
// Close existing connections
|
||||
if (dataChannel) {
|
||||
console.log('🧹 Closing existing data channel...');
|
||||
dataChannel.close();
|
||||
dataChannel = null;
|
||||
}
|
||||
|
||||
if (peerConnection) {
|
||||
console.log('🧹 Closing existing peer connection...');
|
||||
peerConnection.close();
|
||||
peerConnection = null;
|
||||
}
|
||||
|
||||
// Reset state
|
||||
peerConnected = false;
|
||||
|
||||
// Wait a bit before restarting
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Restart WebRTC
|
||||
if (isCreator) {
|
||||
console.log('🔄 Restarting as creator (creating offer)...');
|
||||
await setupWebRTC(true);
|
||||
} else {
|
||||
console.log('⚠️ Only creator can initiate restart. Waiting for creator to restart...');
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup(): void {
|
||||
if (dataChannel) {
|
||||
dataChannel.close();
|
||||
@ -655,11 +1109,99 @@
|
||||
<img src={qrCodeUrl} alt="QR Code" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="connection-status">
|
||||
<div class="connection-status">
|
||||
<span class="status-indicator" class:connected={peerConnected}></span>
|
||||
{peerConnected ? $t("clipboard.peer_connected") : $t("clipboard.waiting_peer")}
|
||||
</div>
|
||||
<!-- Debug panel -->
|
||||
<div class="debug-panel">
|
||||
<details>
|
||||
<summary>🔧 Connection Debug</summary>
|
||||
<div class="debug-info">
|
||||
<p><strong>WebSocket:</strong> {isConnected ? '✅ Connected' : '❌ Disconnected'}</p>
|
||||
<p><strong>Session ID:</strong> {sessionId || 'Not set'}</p>
|
||||
<p><strong>Is Creator:</strong> {isCreator ? 'Yes' : 'No'}</p>
|
||||
<p><strong>Peer Connected:</strong> {peerConnected ? '✅ Yes' : '❌ No'}</p>
|
||||
<p><strong>Data Channel:</strong> {dataChannel ? (dataChannel.readyState || 'Unknown') : 'Not created'}</p> <p><strong>Peer Connection:</strong> {peerConnection ? (peerConnection.connectionState || 'Unknown') : 'Not created'}</p>
|
||||
<p><strong>Signaling State:</strong> {peerConnection ? (peerConnection.signalingState || 'Unknown') : 'Not created'}</p>
|
||||
<p><strong>ICE Connection:</strong> {peerConnection ? (peerConnection.iceConnectionState || 'Unknown') : 'Not created'}</p>
|
||||
<p><strong>ICE Gathering:</strong> {peerConnection ? (peerConnection.iceGatheringState || 'Unknown') : 'Not created'}</p>
|
||||
|
||||
<div class="debug-actions">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => console.log('🔍 Full Debug State:', {
|
||||
isConnected, sessionId, isCreator, peerConnected,
|
||||
dataChannelState: dataChannel?.readyState,
|
||||
peerConnectionState: peerConnection?.connectionState,
|
||||
iceConnectionState: peerConnection?.iceConnectionState,
|
||||
iceGatheringState: peerConnection?.iceGatheringState,
|
||||
hasSharedKey: !!sharedKey,
|
||||
hasRemotePublicKey: !!remotePublicKey,
|
||||
wsReadyState: ws?.readyState
|
||||
})}
|
||||
class="debug-btn"
|
||||
>
|
||||
📝 Log Full State
|
||||
</button>
|
||||
|
||||
{#if dataChannel && dataChannel.readyState === 'open' && !peerConnected}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
console.log('🔧 Forcing peerConnected to true');
|
||||
peerConnected = true;
|
||||
}}
|
||||
class="debug-btn"
|
||||
>
|
||||
🔧 Force Connect
|
||||
</button>
|
||||
{/if}
|
||||
{#if peerConnection && dataChannel && dataChannel.readyState === 'connecting'}
|
||||
<button
|
||||
type="button"
|
||||
on:click={restartWebRTC}
|
||||
class="debug-btn restart-btn"
|
||||
>
|
||||
🔄 Restart WebRTC
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
console.log('🧪 Testing data channel send...');
|
||||
if (dataChannel && dataChannel.readyState === 'open') {
|
||||
try {
|
||||
dataChannel.send('test-ping');
|
||||
console.log('✅ Test message sent successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Test send failed:', error);
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ Data channel not ready for testing');
|
||||
}
|
||||
}}
|
||||
class="debug-btn"
|
||||
>
|
||||
🧪 Test Send
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if dataChannel && dataChannel.readyState === 'connecting'}
|
||||
<div class="debug-warning">
|
||||
⚠️ Data channel stuck in "connecting" state. This usually indicates:
|
||||
<ul>
|
||||
<li>ICE connection issues</li>
|
||||
<li>Firewall blocking WebRTC</li>
|
||||
<li>NAT traversal problems</li>
|
||||
</ul>
|
||||
Try the "Restart WebRTC" button above.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCategory>
|
||||
@ -885,6 +1427,87 @@
|
||||
background-color: var(--green);
|
||||
}
|
||||
|
||||
.debug-panel {
|
||||
margin-top: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.debug-panel summary {
|
||||
padding: 0.5rem;
|
||||
background-color: var(--background-alt);
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.debug-panel summary:hover {
|
||||
background-color: var(--hover-background);
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
padding: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.debug-info p {
|
||||
margin: 0.25rem 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.debug-actions {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.debug-btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: var(--accent-background);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 0.25rem;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
transition: all 0.2s ease;
|
||||
} .debug-btn:hover {
|
||||
background-color: var(--accent);
|
||||
color: var(--background);
|
||||
}
|
||||
|
||||
.restart-btn {
|
||||
background-color: var(--orange-background);
|
||||
border-color: var(--orange);
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.restart-btn:hover {
|
||||
background-color: var(--orange);
|
||||
color: var(--background);
|
||||
}
|
||||
|
||||
.debug-warning {
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
background-color: var(--yellow-background);
|
||||
border: 1px solid var(--yellow);
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.debug-warning ul {
|
||||
margin: 0.25rem 0 0 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.debug-warning li {
|
||||
margin: 0.1rem 0;
|
||||
}
|
||||
|
||||
.text-transfer textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
|
Loading…
Reference in New Issue
Block a user