diff --git a/js/revamp/src/network/SignalingManager.js b/js/revamp/src/network/SignalingManager.js index af590cd..3230e8b 100644 --- a/js/revamp/src/network/SignalingManager.js +++ b/js/revamp/src/network/SignalingManager.js @@ -3,6 +3,10 @@ class SignalingManager { this.webrtcConnection = webrtcConnection; this.metadataManager = metadataManager; this.role = null; // 'sender' or 'receiver' + this.fileMetadata = null; + this.localMetadata = null; + this.remoteMetadata = null; + this.connectionState = 'idle'; // 'idle', 'preparing', 'waiting', 'connecting', 'connected', 'failed' } /** @@ -10,32 +14,354 @@ class SignalingManager { * @param {Object} fileMetadata - File metadata * @returns {Promise} - Metadata to share */ - async initiateSender(fileMetadata) {} + async initiateSender(fileMetadata) { + try { + this.role = 'sender'; + this.fileMetadata = fileMetadata; + this.connectionState = 'preparing'; + + console.log('Initiating sender with file:', fileMetadata); + + // Initialize WebRTC connection as initiator + await this.webrtcConnection.initialize(true); + + // Create WebRTC offer + const offerData = await this.webrtcConnection.createOffer(); + console.log('Created WebRTC offer'); + + // Create metadata payload combining file info and WebRTC data + const metadataPayload = { + version: "1.0", + role: "sender", + file: { + fileName: fileMetadata.fileName, + fileSize: fileMetadata.fileSize, + totalChunks: fileMetadata.totalChunks, + chunkSize: fileMetadata.chunkSize, + fileType: fileMetadata.fileType, + checksum: fileMetadata.checksum + }, + webrtc: { + sdp: offerData.sdp, + type: offerData.type, + iceCandidates: offerData.iceCandidates + }, + timestamp: Date.now() + }; + + // Use MetadataManager to create shareable string + const shareableMetadata = this.metadataManager.createMetadataPayload( + metadataPayload.file, + offerData.iceCandidates, + { + sdp: offerData.sdp, + type: offerData.type + } + ); + + this.localMetadata = metadataPayload; + this.connectionState = 'waiting'; + + console.log('Sender metadata created successfully'); + return shareableMetadata; + + } catch (error) { + console.error('Error initiating sender:', error); + this.connectionState = 'failed'; + throw new Error(`Failed to initiate sender: ${error.message}`); + } + } /** * Initiates connection as receiver * @param {string} senderMetadata - Received metadata * @returns {Promise} - Response metadata */ - async initiateReceiver(senderMetadata) {} + async initiateReceiver(senderMetadata) { + try { + this.role = 'receiver'; + this.connectionState = 'preparing'; + + console.log('Initiating receiver with sender metadata'); + + // Parse sender's metadata + const parsedMetadata = this.metadataManager.parseMetadataPayload(senderMetadata); + + if (!parsedMetadata || parsedMetadata.role !== 'sender') { + throw new Error('Invalid sender metadata'); + } + + this.remoteMetadata = parsedMetadata; + this.fileMetadata = parsedMetadata.file; + + console.log('Parsed sender metadata:', { + fileName: this.fileMetadata.fileName, + fileSize: this.fileMetadata.fileSize, + totalChunks: this.fileMetadata.totalChunks + }); + + // Initialize WebRTC connection as non-initiator + await this.webrtcConnection.initialize(false); + + // Create answer to sender's offer + const answerData = await this.webrtcConnection.createAnswer(parsedMetadata.webrtc.sdp); + console.log('Created WebRTC answer'); + + // Add sender's ICE candidates + if (parsedMetadata.webrtc.iceCandidates && parsedMetadata.webrtc.iceCandidates.length > 0) { + for (const candidate of parsedMetadata.webrtc.iceCandidates) { + await this.webrtcConnection.addIceCandidate(candidate); + } + console.log(`Added ${parsedMetadata.webrtc.iceCandidates.length} ICE candidates`); + } + + // Create response metadata payload + const responsePayload = { + version: "1.0", + role: "receiver", + webrtc: { + sdp: answerData.sdp, + type: answerData.type, + iceCandidates: answerData.iceCandidates + }, + timestamp: Date.now() + }; + + // Use MetadataManager to create shareable response + const shareableResponse = this.metadataManager.createMetadataPayload( + null, // No file info for receiver + answerData.iceCandidates, + { + sdp: answerData.sdp, + type: answerData.type + } + ); + + this.localMetadata = responsePayload; + this.connectionState = 'waiting'; + + console.log('Receiver response metadata created successfully'); + return shareableResponse; + + } catch (error) { + console.error('Error initiating receiver:', error); + this.connectionState = 'failed'; + throw new Error(`Failed to initiate receiver: ${error.message}`); + } + } /** * Completes connection with remote metadata * @param {string} remoteMetadata - Remote peer metadata * @returns {Promise} */ - async completeConnection(remoteMetadata) {} + async completeConnection(remoteMetadata) { + try { + this.connectionState = 'connecting'; + + console.log('Completing connection with remote metadata'); + + // Parse remote metadata + const parsedRemote = this.metadataManager.parseMetadataPayload(remoteMetadata); + + if (!parsedRemote || !parsedRemote.webrtc) { + throw new Error('Invalid remote metadata'); + } + + // Validate role compatibility + if (this.role === 'sender' && parsedRemote.role !== 'receiver') { + throw new Error('Role mismatch: expected receiver metadata'); + } + if (this.role === 'receiver' && parsedRemote.role !== 'sender') { + throw new Error('Role mismatch: expected sender metadata'); + } + + this.remoteMetadata = parsedRemote; + + // Set remote description + await this.webrtcConnection.setRemoteDescription({ + type: parsedRemote.webrtc.type, + sdp: parsedRemote.webrtc.sdp + }); + + console.log('Set remote description'); + + // Add remote ICE candidates + if (parsedRemote.webrtc.iceCandidates && parsedRemote.webrtc.iceCandidates.length > 0) { + for (const candidate of parsedRemote.webrtc.iceCandidates) { + await this.webrtcConnection.addIceCandidate(candidate); + } + console.log(`Added ${parsedRemote.webrtc.iceCandidates.length} remote ICE candidates`); + } + + // Wait for connection to establish + await this._waitForConnection(); + + this.connectionState = 'connected'; + console.log('Connection completed successfully'); + + } catch (error) { + console.error('Error completing connection:', error); + this.connectionState = 'failed'; + throw new Error(`Failed to complete connection: ${error.message}`); + } + } + + /** + * Waits for WebRTC connection to establish + * @returns {Promise} + * @private + */ + _waitForConnection() { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Connection timeout')); + }, 10000); // 10 second timeout + + const checkConnection = () => { + const stats = this.webrtcConnection.getDataChannelState(); + const connectionState = this.webrtcConnection.peerConnection?.connectionState; + + console.log('Connection state:', connectionState, 'Data channel:', stats); + + if (stats === 'open') { + clearTimeout(timeout); + resolve(); + } else if (connectionState === 'failed' || connectionState === 'closed') { + clearTimeout(timeout); + reject(new Error('Connection failed')); + } else { + setTimeout(checkConnection, 100); // Check every 100ms + } + }; + + checkConnection(); + }); + } /** * Handles connection state changes * @param {string} state - Connection state * @returns {void} */ - handleConnectionState(state) {} + handleConnectionState(state) { + console.log('Connection state changed:', state); + + switch (state) { + case 'connected': + case 'completed': + if (this.connectionState === 'connecting') { + this.connectionState = 'connected'; + } + break; + + case 'disconnected': + this.connectionState = 'disconnected'; + break; + + case 'failed': + case 'closed': + this.connectionState = 'failed'; + break; + + case 'datachannel-open': + console.log('Data channel is now open and ready for file transfer'); + if (this.connectionState === 'connecting') { + this.connectionState = 'connected'; + } + break; + + case 'datachannel-closed': + console.log('Data channel closed'); + break; + + case 'datachannel-error': + console.error('Data channel error occurred'); + this.connectionState = 'failed'; + break; + + default: + console.log('Unhandled connection state:', state); + } + } + + /** + * Gets current connection status + * @returns {Object} - Connection status information + */ + getConnectionStatus() { + return { + role: this.role, + connectionState: this.connectionState, + hasFileMetadata: !!this.fileMetadata, + hasLocalMetadata: !!this.localMetadata, + hasRemoteMetadata: !!this.remoteMetadata, + webrtcState: this.webrtcConnection.peerConnection?.connectionState || 'not-initialized', + dataChannelState: this.webrtcConnection.getDataChannelState(), + fileInfo: this.fileMetadata ? { + fileName: this.fileMetadata.fileName, + fileSize: this.fileMetadata.fileSize, + totalChunks: this.fileMetadata.totalChunks + } : null + }; + } + + /** + * Checks if connection is ready for file transfer + * @returns {boolean} - True if ready for transfer + */ + isReadyForTransfer() { + return this.connectionState === 'connected' && + this.webrtcConnection.getDataChannelState() === 'open'; + } + + /** + * Gets file metadata (for receiver to display file info) + * @returns {Object|null} - File metadata or null + */ + getFileMetadata() { + return this.fileMetadata; + } /** * Resets connection * @returns {void} */ - reset() {} -} + reset() { + console.log('Resetting signaling manager'); + + // Close WebRTC connection + if (this.webrtcConnection) { + this.webrtcConnection.close(); + } + + // Reset all state + this.role = null; + this.fileMetadata = null; + this.localMetadata = null; + this.remoteMetadata = null; + this.connectionState = 'idle'; + + console.log('Signaling manager reset complete'); + } + + /** + * Gets detailed connection statistics + * @returns {Promise} - Detailed stats + */ + async getDetailedStats() { + const webrtcStats = await this.webrtcConnection.getStats(); + + return { + signaling: { + role: this.role, + connectionState: this.connectionState, + hasFileMetadata: !!this.fileMetadata, + fileSize: this.fileMetadata?.fileSize || 0, + totalChunks: this.fileMetadata?.totalChunks || 0 + }, + webrtc: webrtcStats, + timestamp: Date.now() + }; + } +} \ No newline at end of file diff --git a/js/revamp/src/network/WebRTCConnection.js b/js/revamp/src/network/WebRTCConnection.js index 4160feb..7ebdda5 100644 --- a/js/revamp/src/network/WebRTCConnection.js +++ b/js/revamp/src/network/WebRTCConnection.js @@ -5,6 +5,8 @@ class WebRTCConnection { this.onDataChannelMessage = onDataChannelMessage; this.onConnectionStateChange = onConnectionStateChange; this.iceCandidates = []; + this.isInitiator = false; + this.remoteDataChannel = null; } /** @@ -12,51 +14,317 @@ class WebRTCConnection { * @param {boolean} isInitiator - True if sender * @returns {Promise} */ - async initialize(isInitiator) {} + async initialize(isInitiator) { + this.isInitiator = isInitiator; + + // Configuration for ICE servers (STUN/TURN) + const configuration = { + iceServers: [ + { urls: 'stun:stun.l.google.com:19302' }, + { urls: 'stun:stun1.l.google.com:19302' } + ] + }; + + this.peerConnection = new RTCPeerConnection(configuration); + + // Set up event handlers + this.peerConnection.onicecandidate = (event) => { + if (event.candidate) { + this.iceCandidates.push(event.candidate); + } + }; + + this.peerConnection.onconnectionstatechange = () => { + const state = this.peerConnection.connectionState; + console.log('Connection state change:', state); + if (this.onConnectionStateChange) { + this.onConnectionStateChange(state); + } + }; + + this.peerConnection.oniceconnectionstatechange = () => { + console.log('ICE connection state:', this.peerConnection.iceConnectionState); + }; + + // If initiator (sender), create data channel + if (isInitiator) { + this.dataChannel = this.peerConnection.createDataChannel('fileTransfer', { + ordered: true, + maxRetransmits: 3 + }); + this._setupDataChannelHandlers(this.dataChannel); + } else { + // If receiver, wait for data channel from remote peer + this.peerConnection.ondatachannel = (event) => { + this.remoteDataChannel = event.channel; + this._setupDataChannelHandlers(this.remoteDataChannel); + }; + } + } + + /** + * Sets up data channel event handlers + * @param {RTCDataChannel} dataChannel - Data channel to set up + * @private + */ + _setupDataChannelHandlers(dataChannel) { + dataChannel.onopen = () => { + console.log('Data channel opened'); + if (this.onConnectionStateChange) { + this.onConnectionStateChange('datachannel-open'); + } + }; + + dataChannel.onclose = () => { + console.log('Data channel closed'); + if (this.onConnectionStateChange) { + this.onConnectionStateChange('datachannel-closed'); + } + }; + + dataChannel.onerror = (error) => { + console.error('Data channel error:', error); + if (this.onConnectionStateChange) { + this.onConnectionStateChange('datachannel-error'); + } + }; + + dataChannel.onmessage = (event) => { + if (this.onDataChannelMessage) { + try { + const data = JSON.parse(event.data); + this.onDataChannelMessage(data); + } catch (error) { + console.error('Error parsing data channel message:', error); + } + } + }; + } /** * Creates offer (for sender) * @returns {Promise} - SDP offer and ICE candidates */ - async createOffer() {} + async createOffer() { + if (!this.peerConnection) { + throw new Error('Peer connection not initialized'); + } + + // Clear previous ICE candidates + this.iceCandidates = []; + + const offer = await this.peerConnection.createOffer(); + await this.peerConnection.setLocalDescription(offer); + + // Wait for ICE gathering to complete + await this._waitForIceGathering(); + + return { + sdp: offer.sdp, + type: "offer", + iceCandidates: [...this.iceCandidates] + }; + } /** * Creates answer (for receiver) * @param {RTCSessionDescription} offer - Received offer * @returns {Promise} - SDP answer and ICE candidates */ - async createAnswer(offer) {} + async createAnswer(offer) { + if (!this.peerConnection) { + throw new Error('Peer connection not initialized'); + } + + // Clear previous ICE candidates + this.iceCandidates = []; + + // Set remote description (the offer) + await this.peerConnection.setRemoteDescription(new RTCSessionDescription({ + type: 'offer', + sdp: offer + })); + + // Create answer + const answer = await this.peerConnection.createAnswer(); + await this.peerConnection.setLocalDescription(answer); + + // Wait for ICE gathering to complete + await this._waitForIceGathering(); + + return { + sdp: answer.sdp, + type: "answer", + iceCandidates: [...this.iceCandidates] + }; + } /** * Sets remote description * @param {RTCSessionDescription} description - Remote SDP * @returns {Promise} */ - async setRemoteDescription(description) {} + async setRemoteDescription(description) { + if (!this.peerConnection) { + throw new Error('Peer connection not initialized'); + } + + await this.peerConnection.setRemoteDescription(new RTCSessionDescription(description)); + } /** * Adds ICE candidate * @param {RTCIceCandidate} candidate - ICE candidate * @returns {Promise} */ - async addIceCandidate(candidate) {} + async addIceCandidate(candidate) { + if (!this.peerConnection) { + throw new Error('Peer connection not initialized'); + } + + try { + await this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate)); + } catch (error) { + console.error('Error adding ICE candidate:', error); + } + } + + /** + * Waits for ICE gathering to complete + * @returns {Promise} + * @private + */ + _waitForIceGathering() { + return new Promise((resolve) => { + if (this.peerConnection.iceGatheringState === 'complete') { + resolve(); + return; + } + + const timeout = setTimeout(() => { + this.peerConnection.removeEventListener('icegatheringstatechange', onStateChange); + resolve(); // Resolve even if not complete to avoid hanging + }, 3000); // 3 second timeout + + const onStateChange = () => { + if (this.peerConnection.iceGatheringState === 'complete') { + clearTimeout(timeout); + this.peerConnection.removeEventListener('icegatheringstatechange', onStateChange); + resolve(); + } + }; + + this.peerConnection.addEventListener('icegatheringstatechange', onStateChange); + }); + } /** * Sends data through data channel * @param {Object} data - Data to send * @returns {void} */ - sendData(data) {} + sendData(data) { + const channel = this.dataChannel || this.remoteDataChannel; + + if (!channel) { + console.error('No data channel available'); + return; + } + + if (channel.readyState !== 'open') { + console.error('Data channel is not open. State:', channel.readyState); + return; + } + + try { + const jsonString = JSON.stringify(data); + channel.send(jsonString); + } catch (error) { + console.error('Error sending data:', error); + } + } + + /** + * Gets data channel ready state + * @returns {string} - Ready state of data channel + */ + getDataChannelState() { + const channel = this.dataChannel || this.remoteDataChannel; + return channel ? channel.readyState : 'unavailable'; + } /** * Closes connection * @returns {void} */ - close() {} + close() { + if (this.dataChannel) { + this.dataChannel.close(); + this.dataChannel = null; + } + + if (this.remoteDataChannel) { + this.remoteDataChannel.close(); + this.remoteDataChannel = null; + } + + if (this.peerConnection) { + this.peerConnection.close(); + this.peerConnection = null; + } + + this.iceCandidates = []; + } /** * Gets connection stats * @returns {Promise} - Connection statistics */ - async getStats() {} -} + async getStats() { + if (!this.peerConnection) { + return { + connectionState: 'closed', + iceConnectionState: 'closed', + dataChannelState: 'closed' + }; + } + + try { + const stats = await this.peerConnection.getStats(); + const statsReport = {}; + + stats.forEach((report) => { + if (report.type === 'data-channel') { + statsReport.dataChannel = { + messagesSent: report.messagesSent, + messagesReceived: report.messagesReceived, + bytesSent: report.bytesSent, + bytesReceived: report.bytesReceived + }; + } else if (report.type === 'candidate-pair' && report.nominated) { + statsReport.connection = { + bytesSent: report.bytesSent, + bytesReceived: report.bytesReceived, + packetsLost: report.packetsLost, + currentRoundTripTime: report.currentRoundTripTime + }; + } + }); + + return { + connectionState: this.peerConnection.connectionState, + iceConnectionState: this.peerConnection.iceConnectionState, + dataChannelState: this.getDataChannelState(), + ...statsReport + }; + } catch (error) { + console.error('Error getting stats:', error); + return { + connectionState: this.peerConnection.connectionState, + iceConnectionState: this.peerConnection.iceConnectionState, + dataChannelState: this.getDataChannelState(), + error: error.message + }; + } + } +} \ No newline at end of file