397 lines
13 KiB
TypeScript
397 lines
13 KiB
TypeScript
import { useState, useEffect, useCallback, useMemo } from "react";
|
|
import WebRTC_Initiator from "@/lib/webrtc_Initiator";
|
|
import WebRTC_Recipient from "@/lib/webrtc_Recipient";
|
|
import FileSender from "@/lib/fileSender";
|
|
import FileReceiver from "@/lib/fileReceiver";
|
|
import {
|
|
config,
|
|
getIceServers,
|
|
getSocketOptions,
|
|
} from "@/app/config/environment";
|
|
import type { CustomFile, fileMetadata, FileMeta } from "@/types/webrtc";
|
|
import type { Messages } from "@/types/messages";
|
|
import { useFileTransferStore } from "@/stores/fileTransferStore";
|
|
|
|
const developmentEnv = process.env.NEXT_PUBLIC_development === "true";
|
|
|
|
// Types for progress states
|
|
export type PeerProgressDetails = { progress: number; speed: number };
|
|
export type FileProgressPeers = { [peerId: string]: PeerProgressDetails };
|
|
export type ProgressState = { [fileId: string]: FileProgressPeers };
|
|
|
|
interface UseWebRTCConnectionProps {
|
|
shareContent: string;
|
|
sendFiles: CustomFile[];
|
|
isContentPresent: boolean;
|
|
|
|
// For user feedback and messages from the hook, if any (mostly for console for now)
|
|
messages: Messages | null;
|
|
putMessageInMs: (
|
|
message: string,
|
|
isShareEnd?: boolean,
|
|
displayTimeMs?: number
|
|
) => void;
|
|
}
|
|
|
|
export function useWebRTCConnection({
|
|
shareContent,
|
|
sendFiles,
|
|
isContentPresent,
|
|
messages,
|
|
putMessageInMs,
|
|
}: UseWebRTCConnectionProps) {
|
|
const [sender, setSender] = useState<WebRTC_Initiator | null>(null);
|
|
const [receiver, setReceiver] = useState<WebRTC_Recipient | null>(null);
|
|
const [senderFileTransfer, setSenderFileTransfer] =
|
|
useState<FileSender | null>(null);
|
|
const [receiverFileTransfer, setReceiverFileTransfer] =
|
|
useState<FileReceiver | null>(null);
|
|
|
|
// 从 store 中获取状态
|
|
const {
|
|
sharePeerCount,
|
|
retrievePeerCount,
|
|
sendProgress,
|
|
receiveProgress,
|
|
senderDisconnected,
|
|
setSharePeerCount,
|
|
setRetrievePeerCount,
|
|
setSendProgress,
|
|
setReceiveProgress,
|
|
setSenderDisconnected,
|
|
setIsAnyFileTransferring,
|
|
} = useFileTransferStore();
|
|
|
|
// Calculate isAnyFileTransferring internally based on progress states
|
|
const isAnyFileTransferring = useMemo(() => {
|
|
const allProgress = [
|
|
...Object.values(sendProgress),
|
|
...Object.values(receiveProgress),
|
|
];
|
|
return allProgress.some((fileProgress: unknown) => {
|
|
const typedFileProgress = fileProgress as FileProgressPeers;
|
|
return Object.values(typedFileProgress).some((progress: unknown) => {
|
|
const typedProgress = progress as PeerProgressDetails;
|
|
return typedProgress.progress > 0 && typedProgress.progress < 1;
|
|
});
|
|
});
|
|
}, [sendProgress, receiveProgress]);
|
|
|
|
// 更新 store 中的 isAnyFileTransferring 状态
|
|
useEffect(() => {
|
|
setIsAnyFileTransferring(isAnyFileTransferring);
|
|
}, [isAnyFileTransferring, setIsAnyFileTransferring]);
|
|
|
|
// Initialize WebRTC objects and their cleanup
|
|
useEffect(() => {
|
|
const webRTCConfig = {
|
|
iceServers: getIceServers(),
|
|
socketOptions: getSocketOptions() || {},
|
|
signalingServer: config.API_URL,
|
|
};
|
|
const senderConn = new WebRTC_Initiator(webRTCConfig);
|
|
const receiverConn = new WebRTC_Recipient(webRTCConfig);
|
|
setSender(senderConn);
|
|
setReceiver(receiverConn);
|
|
|
|
const senderFT = new FileSender(senderConn);
|
|
const receiverFT = new FileReceiver(receiverConn);
|
|
setSenderFileTransfer(senderFT);
|
|
setReceiverFileTransfer(receiverFT);
|
|
if (developmentEnv)
|
|
console.log("WebRTC connection and file transfer instances created");
|
|
|
|
return () => {
|
|
if (developmentEnv) console.log("Cleaning up WebRTC instances");
|
|
senderConn.cleanUpBeforeExit();
|
|
receiverConn.cleanUpBeforeExit();
|
|
};
|
|
}, []);
|
|
|
|
// Internal function to send text and file metadata to a specific peer
|
|
const sendStringAndMetasToPeer = useCallback(
|
|
async (peerId: string, textContent: string, filesToSend: CustomFile[]) => {
|
|
if (!senderFileTransfer) {
|
|
console.error(
|
|
"SenderFileTransfer not initialized for sendStringAndMetasToPeer"
|
|
);
|
|
return;
|
|
}
|
|
if (textContent) {
|
|
await senderFileTransfer.sendString(textContent, peerId);
|
|
}
|
|
if (filesToSend.length > 0) {
|
|
senderFileTransfer.sendFileMeta(filesToSend, peerId);
|
|
}
|
|
},
|
|
[senderFileTransfer]
|
|
);
|
|
|
|
// Exposed function to broadcast data to all connected sender peers
|
|
const broadcastDataToAllPeers = useCallback(
|
|
async (textContent: string, filesToSend: CustomFile[]) => {
|
|
if (!sender || sender.peerConnections.size === 0) {
|
|
if (developmentEnv)
|
|
console.warn(
|
|
"No sender peers to broadcast to, or sender not initialized."
|
|
);
|
|
return false;
|
|
}
|
|
if (!senderFileTransfer) {
|
|
console.error("senderFileTransfer is not initialized for broadcast.");
|
|
return false;
|
|
}
|
|
const peerIds = Array.from(sender.peerConnections.keys());
|
|
if (developmentEnv)
|
|
console.log(`Broadcasting to peers: ${peerIds.join(", ")}`);
|
|
try {
|
|
await Promise.all(
|
|
peerIds.map((peerId) =>
|
|
sendStringAndMetasToPeer(peerId, textContent, filesToSend)
|
|
)
|
|
);
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Error broadcasting data to peers:", error);
|
|
return false;
|
|
}
|
|
},
|
|
[sender, senderFileTransfer, sendStringAndMetasToPeer]
|
|
);
|
|
|
|
// Setup sender and receiver event handlers
|
|
useEffect(() => {
|
|
if (sender && senderFileTransfer) {
|
|
sender.onConnectionStateChange = (state, peerId) => {
|
|
if (developmentEnv)
|
|
console.log(`Sender connection state with ${peerId}: ${state}`);
|
|
// 更新连接状态
|
|
useFileTransferStore.getState().setShareConnectionState(state as any);
|
|
setSharePeerCount(sender.peerConnections.size);
|
|
if (state === "connected") {
|
|
senderFileTransfer.setProgressCallback(
|
|
(fileId, progress: number, speed: number) => {
|
|
useFileTransferStore.getState().updateSendProgress(fileId, peerId, { progress, speed });
|
|
},
|
|
peerId
|
|
);
|
|
}
|
|
};
|
|
sender.onDataChannelOpen = () => {
|
|
// 当数据通道打开时,标记发送方已加入房间
|
|
useFileTransferStore.getState().setIsSenderInRoom(true);
|
|
broadcastDataToAllPeers(shareContent, sendFiles);
|
|
}
|
|
|
|
sender.onPeerDisconnected = (peerId) => {
|
|
setTimeout(() => {
|
|
setSharePeerCount(sender.peerConnections.size);
|
|
// 检查是否所有 peer 都已断开连接
|
|
if (sender.peerConnections.size === 0) {
|
|
useFileTransferStore.getState().setIsSenderInRoom(false);
|
|
}
|
|
}, 0);
|
|
};
|
|
|
|
sender.onError = (error) => {
|
|
console.error("Sender Error:", error.message, error.context);
|
|
putMessageInMs(`Connection error: ${error.message}`, true);
|
|
};
|
|
}
|
|
|
|
if (receiver && receiverFileTransfer) {
|
|
receiver.onConnectionStateChange = (state, peerId) => {
|
|
if (developmentEnv)
|
|
console.log(`Receiver connection state with ${peerId}: ${state}`);
|
|
// 更新连接状态
|
|
useFileTransferStore.getState().setRetrieveConnectionState(state as any);
|
|
setRetrievePeerCount(receiver.peerConnections.size);
|
|
if (state === "connected") {
|
|
receiverFileTransfer.setProgressCallback(
|
|
(fileId, progress: number, speed: number) => {
|
|
useFileTransferStore.getState().updateReceiveProgress(fileId, peerId, { progress, speed });
|
|
}
|
|
);
|
|
} else if (state === "failed" || state === "disconnected") {
|
|
if (isAnyFileTransferring) {
|
|
receiverFileTransfer.gracefulShutdown();
|
|
}
|
|
}
|
|
};
|
|
|
|
receiverFileTransfer.onStringReceived = (data) => {
|
|
const peerId = "testId";
|
|
if (developmentEnv) console.log(`String received from peer ${peerId}`);
|
|
useFileTransferStore.getState().setRetrievedContent(data);
|
|
};
|
|
|
|
receiverFileTransfer.onFileMetaReceived = (meta) => {
|
|
const peerId = "testId";
|
|
if (developmentEnv)
|
|
console.log(
|
|
`File meta received from peer ${peerId} for: ${meta.name}`
|
|
);
|
|
const { type, ...metaWithoutType } = meta;
|
|
const store = useFileTransferStore.getState();
|
|
// Filter out existing file with same ID and add the new one
|
|
const DPrev = store.retrievedFileMetas.filter(
|
|
(existingFile) => existingFile.fileId !== metaWithoutType.fileId
|
|
);
|
|
store.setRetrievedFileMetas([...DPrev, metaWithoutType]);
|
|
};
|
|
|
|
receiverFileTransfer.onFileReceived = async (file) => {
|
|
const peerId = "testId"; // This should be dynamic in a multi-peer scenario
|
|
if (developmentEnv)
|
|
console.log(`File received from peer ${peerId}: ${file.name}`);
|
|
// Directly call the store action
|
|
useFileTransferStore.getState().addRetrievedFile(file);
|
|
};
|
|
|
|
receiver.onPeerDisconnected = (peerId) => {
|
|
if (developmentEnv)
|
|
console.log(`Receiver peer ${peerId} disconnected.`);
|
|
setSenderDisconnected(true);
|
|
setRetrievePeerCount(0);
|
|
useFileTransferStore.getState().setIsReceiverInRoom(false);
|
|
};
|
|
|
|
receiver.onConnectionEstablished = (peerId) => {
|
|
if (developmentEnv)
|
|
console.log(`Receiver connection established with ${peerId}.`);
|
|
setSenderDisconnected(false);
|
|
useFileTransferStore.getState().setIsReceiverInRoom(true);
|
|
};
|
|
|
|
receiver.onError = (error) => {
|
|
console.error("Receiver Error:", error.message, error.context);
|
|
putMessageInMs(`Connection error: ${error.message}`, false);
|
|
};
|
|
}
|
|
}, [
|
|
sender,
|
|
senderFileTransfer,
|
|
receiver,
|
|
receiverFileTransfer,
|
|
putMessageInMs,
|
|
broadcastDataToAllPeers,
|
|
shareContent,
|
|
sendFiles,
|
|
isAnyFileTransferring,
|
|
setSharePeerCount,
|
|
setRetrievePeerCount,
|
|
setSenderDisconnected,
|
|
]);
|
|
|
|
// Effect to handle graceful shutdown on page unload
|
|
useEffect(() => {
|
|
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
|
if (isContentPresent || isAnyFileTransferring) {
|
|
if (isAnyFileTransferring) {
|
|
receiverFileTransfer?.gracefulShutdown();
|
|
}
|
|
e.preventDefault();
|
|
e.returnValue = "";
|
|
}
|
|
};
|
|
|
|
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
|
|
return () => {
|
|
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
};
|
|
}, [isContentPresent, isAnyFileTransferring, receiverFileTransfer]);
|
|
|
|
const requestFile = useCallback(
|
|
(fileId: string, peerId?: string) => {
|
|
if (!receiverFileTransfer) return;
|
|
if (developmentEnv)
|
|
console.log(
|
|
`Requesting file ${fileId} from peer ${peerId || "default"}`
|
|
);
|
|
receiverFileTransfer.requestFile(fileId);
|
|
},
|
|
[receiverFileTransfer]
|
|
);
|
|
|
|
const requestFolder = useCallback(
|
|
(folderName: string, peerId?: string) => {
|
|
if (!receiverFileTransfer) return;
|
|
if (developmentEnv)
|
|
console.log(
|
|
`Requesting folder ${folderName} from peer ${peerId || "default"}`
|
|
);
|
|
receiverFileTransfer.requestFolder(folderName);
|
|
},
|
|
[receiverFileTransfer]
|
|
);
|
|
|
|
const setReceiverDirectoryHandle = useCallback(
|
|
async (directoryHandle: FileSystemDirectoryHandle): Promise<void> => {
|
|
if (!receiverFileTransfer) return;
|
|
if (developmentEnv)
|
|
console.log("Setting receiver save directory handle.");
|
|
return receiverFileTransfer.setSaveDirectory(directoryHandle);
|
|
},
|
|
[receiverFileTransfer]
|
|
);
|
|
|
|
const getReceiverSaveType = useCallback(():
|
|
| { [fileId: string]: boolean }
|
|
| undefined => {
|
|
return receiverFileTransfer?.saveType;
|
|
}, [receiverFileTransfer]);
|
|
|
|
// Reset function for receiver connection (for leave room functionality)
|
|
const resetReceiverConnection = useCallback(async () => {
|
|
if (receiver) {
|
|
setSenderDisconnected(false);
|
|
setRetrievePeerCount(0);
|
|
useFileTransferStore.getState().setIsReceiverInRoom(false);
|
|
await receiver.leaveRoomAndCleanup();
|
|
}
|
|
}, [receiver, setSenderDisconnected, setRetrievePeerCount]);
|
|
|
|
// Reset function for sender connection (for leave room functionality)
|
|
const resetSenderConnection = useCallback(async () => {
|
|
if (sender) {
|
|
await sender.leaveRoomAndCleanup();
|
|
setSharePeerCount(0);
|
|
useFileTransferStore.getState().setIsSenderInRoom(false);
|
|
}
|
|
}, [sender, setSharePeerCount]);
|
|
|
|
// Manual safe save function
|
|
const manualSafeSave = useCallback(() => {
|
|
if (receiverFileTransfer) {
|
|
receiverFileTransfer.gracefulShutdown();
|
|
if (putMessageInMs && messages) {
|
|
putMessageInMs(
|
|
messages.text.FileListDisplay.safeSaveSuccessMsg,
|
|
false,
|
|
3000
|
|
);
|
|
}
|
|
}
|
|
}, [receiverFileTransfer, putMessageInMs, messages]);
|
|
|
|
return {
|
|
sender,
|
|
receiver,
|
|
sharePeerCount,
|
|
retrievePeerCount,
|
|
sendProgress,
|
|
receiveProgress,
|
|
isAnyFileTransferring,
|
|
broadcastDataToAllPeers,
|
|
requestFile,
|
|
requestFolder,
|
|
setReceiverDirectoryHandle,
|
|
getReceiverSaveType,
|
|
senderDisconnected,
|
|
resetReceiverConnection,
|
|
resetSenderConnection,
|
|
manualSafeSave,
|
|
};
|
|
}
|