fix:Adapting to Firefox browser, not yet completed

This commit is contained in:
david_bai
2025-09-05 00:19:33 +08:00
parent a82ca50ee9
commit ec6a18dfc0
21 changed files with 961 additions and 207 deletions
-24
View File
@@ -51,40 +51,16 @@ const ClipboardApp = () => {
// 简化的 WebRTC 连接初始化
const {
sharePeerCount,
retrievePeerCount,
broadcastDataToAllPeers,
requestFile,
requestFolder,
setReceiverDirectoryHandle,
getReceiverSaveType,
senderDisconnected,
resetReceiverConnection,
resetSenderConnection,
manualSafeSave,
} = useWebRTCConnection({
messages,
putMessageInMs,
});
const resetAppState = useCallback(async () => {
try {
// Reset file transfer state
useFileTransferStore.getState().resetReceiverState();
// Reset WebRTC connection state
await resetReceiverConnection();
// Reset room input
setRetrieveRoomIdInput("");
console.log("Application state reset successfully");
} catch (error) {
console.error("Error during state reset:", error);
window.location.reload();
}
}, [resetReceiverConnection, setRetrieveRoomIdInput]);
// 大大简化的房间管理 - 不再需要传递任何 WebRTC 依赖
const {
processRoomIdInput,
@@ -11,6 +11,8 @@ import { getDictionary } from "@/lib/dictionary";
import { useLocale } from "@/hooks/useLocale";
import type { Messages } from "@/types/messages";
import { useFileTransferStore } from "@/stores/fileTransferStore";
import { supportsAutoDownload } from "@/lib/browserUtils";
import { postLogToBackend } from "@/app/config/api";
function formatFolderDis(template: string, num: number, size: string) {
return template.replace("{num}", num.toString()).replace("{size}", size);
@@ -78,6 +80,11 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
// Add a ref to store the previous showFinished state
const prevShowFinishedRef = useRef<{ [fileId: string]: boolean }>({});
// 添加待保存状态 - 用于非Chrome浏览器的手动保存
const [pendingSave, setPendingSave] = useState<{
[fileId: string]: boolean;
}>({});
const [pickedLocation, setPickedLocation] = useState<boolean>(false); // Whether a save directory has been selected
const [needPickLocation, setNeedPickLocation] = useState<boolean>(false); // Whether a save directory needs to be selected -- for large files, folders, or user choice
@@ -92,6 +99,19 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
[fileId: string]: number;
}>({});
// 处理手动保存 - 用于非Chrome浏览器
const handleManualSave = (item: FileMeta) => {
if (onDownload) {
onDownload(item);
// 清除待保存状态,让UI显示为"已完成"
setPendingSave((prev) => {
const updated = { ...prev };
delete updated[item.fileId];
return updated;
});
}
};
useEffect(() => {
getDictionary(locale)
.then((dict) => setMessages(dict))
@@ -237,7 +257,29 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
// Detecting false -> true transitions
if (!prevShowFinished && currentShowFinished) {
if (!isSaveToDisk && onDownload) {
onDownload(item);
const isAutoDownloadSupported = supportsAutoDownload();
// 根据浏览器能力决定下载行为
if (isAutoDownloadSupported) {
// Chrome等支持自动下载的浏览器:直接下载
postLogToBackend(
`[Firefox Debug] Auto-downloading file: ${item.name}`
);
onDownload(item);
} else {
// 非Chrome浏览器:设置为待保存状态,等待用户手动点击
postLogToBackend(
`[Firefox Debug] Setting pendingSave for non-Chrome browser: ${item.name}`
);
setPendingSave((prev) => ({
...prev,
[item.fileId]: true,
}));
}
} else {
postLogToBackend(
`[Firefox Debug] Skipping download logic - isSaveToDisk: ${isSaveToDisk}, onDownload: ${!!onDownload}`
);
}
// Increase download count - 文件传输完成时增加下载次数 (只计算一次)
@@ -250,22 +292,17 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
// Update the last status
prevShowFinishedRef.current[item.fileId] = currentShowFinished;
});
}, [
showFinished,
singleFiles,
folders,
saveType,
onDownload,
downloadCounts,
]);
}, [showFinished, singleFiles, folders, saveType, onDownload]);
//Actions corresponding to each file - progress, download, delete
const renderItemActions = (item: FileMeta) => {
const fileProgress = fileProgresses[item.fileId];
const activePeerId = activeTransfers[item.fileId];
const progress = activePeerId ? fileProgress?.[activePeerId] : null;
const showCompletion = showFinished[item.fileId];
const showCompletion =
showFinished[item.fileId] && !pendingSave[item.fileId]; // 只有传输完成且不在待保存状态时才显示完成
const isSaveToDisk = saveType ? saveType[item.fileId] : false;
const isPendingSave = pendingSave[item.fileId] || false;
// Get download count
const downloadCount = downloadCounts[item.fileId] || 0;
@@ -297,6 +334,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
onDownload && ( //Request && Download
<FileTransferButton
onRequest={() => onRequest(item)}
onSave={() => handleManualSave(item)}
isCurrentFileTransferring={
progress
? progress.progress > 0 && progress.progress < 1
@@ -304,6 +342,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
}
isOtherFileTransferring={isAnyFileTransferring && !progress}
isSavedToDisk={saveType ? saveType[item.fileId] : false}
isPendingSave={isPendingSave}
/>
)}
{/* display download Num*/}
@@ -13,22 +13,28 @@ import type { Messages } from "@/types/messages";
interface FileTransferButtonProps {
onRequest: () => void;
onSave?: () => void; // 新增:处理手动保存
isCurrentFileTransferring: boolean;
isOtherFileTransferring: boolean;
isSavedToDisk: boolean;
isPendingSave?: boolean; // 新增:是否待保存状态
}
// Manage buttons for different download statuses
const FileTransferButton = ({
onRequest,
onSave,
isCurrentFileTransferring,
isOtherFileTransferring,
isSavedToDisk,
isPendingSave = false,
}: FileTransferButtonProps) => {
const locale = useLocale();
const [messages, setMessages] = useState<Messages | null>(null);
// Button status judgment
// Button status judgment - 待保存状态时按钮应该可点击
const isDisabled =
isCurrentFileTransferring || isSavedToDisk || isOtherFileTransferring;
isCurrentFileTransferring ||
isSavedToDisk ||
(isOtherFileTransferring && !isPendingSave);
useEffect(() => {
getDictionary(locale)
@@ -41,6 +47,8 @@ const FileTransferButton = ({
return messages!.text.FileTransferButton.SavedToDisk_tips;
if (isCurrentFileTransferring)
return messages!.text.FileTransferButton.CurrentFileTransferring_tips;
if (isPendingSave)
return messages!.text.FileTransferButton.PendingSave_tips;
if (isOtherFileTransferring)
return messages!.text.FileTransferButton.OtherFileTransferring_tips;
return messages!.text.FileTransferButton.download_tips;
@@ -60,6 +68,12 @@ const FileTransferButton = ({
className: "mr-2 cursor-not-allowed",
};
}
if (isPendingSave) {
return {
variant: "default" as const, // 使用更明显的样式
className: "mr-2 bg-green-600 hover:bg-green-700 text-white",
};
}
if (isOtherFileTransferring) {
return {
variant: "outline" as const,
@@ -83,7 +97,7 @@ const FileTransferButton = ({
<TooltipTrigger asChild>
<span className="inline-block">
<Button
onClick={onRequest}
onClick={isPendingSave && onSave ? onSave : onRequest}
variant={buttonStyles.variant}
size="sm"
className={buttonStyles.className}
@@ -91,11 +105,13 @@ const FileTransferButton = ({
>
<Download
className={`mr-2 h-4 w-4 ${
isOtherFileTransferring ? "opacity-50" : ""
isOtherFileTransferring && !isPendingSave ? "opacity-50" : ""
}`}
/>
{isSavedToDisk
? messages.text.FileTransferButton.Saved_dis
: isPendingSave
? messages.text.FileTransferButton.Save_dis
: isOtherFileTransferring
? messages.text.FileTransferButton.Waiting_dis
: messages.text.FileTransferButton.Download_dis}
+2
View File
@@ -235,9 +235,11 @@ export const de: Messages = {
OtherFileTransferring_tips:
"Bitte warten Sie, bis die aktuelle Übertragung abgeschlossen ist",
download_tips: "Klicken Sie, um die Datei herunterzuladen",
PendingSave_tips: "Klicken Sie, um die Datei lokal zu speichern", // 新增
Saved_dis: "Gespeichert",
Waiting_dis: "Warten",
Download_dis: "Herunterladen",
Save_dis: "Speichern", // 新增
},
FileListDisplay: {
sending_dis: "Senden",
+2
View File
@@ -232,9 +232,11 @@ export const en: Messages = {
OtherFileTransferring_tips:
"Please wait for current transfer to complete",
download_tips: "Click to download file",
PendingSave_tips: "Click to save file locally", // 新增
Saved_dis: "Saved",
Waiting_dis: "Waiting",
Download_dis: "Download",
Save_dis: "Save", // 新增
},
FileListDisplay: {
sending_dis: "Sending",
+2
View File
@@ -233,9 +233,11 @@ export const es: Messages = {
OtherFileTransferring_tips:
"Por favor espera a que se complete la transferencia actual",
download_tips: "Haz clic para descargar el archivo",
PendingSave_tips: "Haz clic para guardar el archivo localmente", // 新增
Saved_dis: "Guardado",
Waiting_dis: "Esperando",
Download_dis: "Descargar",
Save_dis: "Guardar", // 新增
},
FileListDisplay: {
sending_dis: "Enviando",
+2
View File
@@ -236,9 +236,11 @@ export const fr: Messages = {
OtherFileTransferring_tips:
"Veuillez attendre que le transfert actuel soit terminé",
download_tips: "Cliquez pour télécharger le fichier",
PendingSave_tips: "Cliquez pour enregistrer le fichier localement", // 新增
Saved_dis: "Enregistré",
Waiting_dis: "En attente",
Download_dis: "Télécharger",
Save_dis: "Enregistrer", // 新增
},
FileListDisplay: {
sending_dis: "Envoi",
+2
View File
@@ -229,9 +229,11 @@ export const ja: Messages = {
CurrentFileTransferring_tips: "ファイルが転送中です",
OtherFileTransferring_tips: "現在の転送が完了するまでお待ちください",
download_tips: "クリックしてファイルをダウンロード",
PendingSave_tips: "クリックしてファイルをローカルに保存", // 新增
Saved_dis: "保存済み",
Waiting_dis: "待機中",
Download_dis: "ダウンロード",
Save_dis: "保存", // 新增
},
FileListDisplay: {
sending_dis: "送信中",
+2
View File
@@ -227,9 +227,11 @@ export const ko: Messages = {
CurrentFileTransferring_tips: "파일 전송 중",
OtherFileTransferring_tips: "현재 전송이 완료될 때까지 기다려주세요",
download_tips: "파일을 다운로드하려면 클릭하세요",
PendingSave_tips: "로컬에 파일을 저장하려면 클릭하세요", // 新增
Saved_dis: "저장됨",
Waiting_dis: "대기 중",
Download_dis: "다운로드",
Save_dis: "저장", // 新增
},
FileListDisplay: {
sending_dis: "전송 중",
+2
View File
@@ -216,9 +216,11 @@ export const zh: Messages = {
CurrentFileTransferring_tips: "文件正在传输中",
OtherFileTransferring_tips: "请等待当前传输完成",
download_tips: "点击下载文件",
PendingSave_tips: "点击保存文件到本地", // 新增
Saved_dis: "已保存",
Waiting_dis: "等待中",
Download_dis: "下载",
Save_dis: "保存", // 新增
},
FileListDisplay: {
sending_dis: "发送中",
+29 -3
View File
@@ -1,9 +1,10 @@
import { useCallback } from "react";
import { useCallback, useEffect } from "react";
import { CustomFile, FileMeta, fileMetadata } from "@/types/webrtc";
import { Messages } from "@/types/messages";
import JSZip from "jszip";
import { downloadAs } from "@/lib/fileUtils";
import { useFileTransferStore } from "@/stores/fileTransferStore";
import { postLogToBackend } from "@/app/config/api";
interface UseFileTransferHandlerProps {
messages: Messages | null;
@@ -26,7 +27,6 @@ export function useFileTransferHandler({
retrievedFiles,
retrievedFileMetas,
setShareContent,
setSendFiles,
addSendFiles,
removeSendFile,
setRetrievedContent,
@@ -68,7 +68,9 @@ export function useFileTransferHandler({
const handleDownloadFile = useCallback(
async (meta: FileMeta) => {
if (!messages) return;
if (!messages) {
return;
}
if (meta.folderName && meta.folderName !== "") {
const { retrievedFiles: latestRetrievedFiles } =
@@ -112,8 +114,32 @@ export function useFileTransferHandler({
);
if (fileToDownload) {
// 检查文件是否为空
if (fileToDownload.size === 0) {
postLogToBackend(
`[Firefox Debug] ERROR: File has 0 size! This explains the 0-byte download.`
);
}
// 检查文件是否为有效的Blob
if (fileToDownload instanceof Blob) {
postLogToBackend(`[Firefox Debug] File is a valid Blob object`);
} else {
postLogToBackend(
`[Firefox Debug] WARNING: File is not a Blob object, type: ${typeof fileToDownload}`
);
}
downloadAs(fileToDownload, fileToDownload.name);
return true;
} else {
// 调试日志:记录未找到文件的情况
const availableFileNames = latestRetrievedFiles.map((f) => f.name);
postLogToBackend(
`[Firefox Debug] File NOT found! Looking for: "${
meta.name
}", Available files: [${availableFileNames.join(", ")}]`
);
}
return false;
+137
View File
@@ -0,0 +1,137 @@
/**
* 浏览器检测工具函数
* 扩展以支持Firefox WebRTC兼容性处理
*/
/**
* 检测是否为 Chrome 浏览器
* @returns {boolean} 如果是 Chrome 返回 true,否则返回 false
*/
export const isChrome = (): boolean => {
// 检测 Chrome 浏览器,排除基于 Chromium 的 Edge
const userAgent = navigator.userAgent;
return (
userAgent.includes("Chrome") && !userAgent.includes("Edg") // 排除 Edge
);
};
/**
* 检测是否为 Firefox 浏览器
* @returns {boolean} 如果是 Firefox 返回 true,否则返回 false
*/
export const isFirefox = (): boolean => {
return navigator.userAgent.includes("Firefox");
};
/**
* 检测浏览器详细信息
*/
export function detectBrowser(): {
name: string;
version: string;
isFirefox: boolean;
isChrome: boolean;
isSafari: boolean;
isEdge: boolean;
} {
const userAgent = navigator.userAgent;
let name = "Unknown";
let version = "Unknown";
// Firefox检测
if (userAgent.includes("Firefox/")) {
name = "Firefox";
const match = userAgent.match(/Firefox\/(\d+(?:\.\d+)*)/);
if (match) version = match[1];
}
// Chrome检测 (注意:需要在Edge之前检测,因为Edge也包含Chrome字符串)
else if (userAgent.includes("Chrome/") && !userAgent.includes("Edg/")) {
name = "Chrome";
const match = userAgent.match(/Chrome\/(\d+(?:\.\d+)*)/);
if (match) version = match[1];
}
// Edge检测
else if (userAgent.includes("Edg/")) {
name = "Edge";
const match = userAgent.match(/Edg\/(\d+(?:\.\d+)*)/);
if (match) version = match[1];
}
// Safari检测
else if (userAgent.includes("Safari/") && !userAgent.includes("Chrome/")) {
name = "Safari";
const match = userAgent.match(/Version\/(\d+(?:\.\d+)*)/);
if (match) version = match[1];
}
return {
name,
version,
isFirefox: name === "Firefox",
isChrome: name === "Chrome",
isSafari: name === "Safari",
isEdge: name === "Edge",
};
}
/**
* 检测是否支持程序化下载
* Chrome 支持长时间传输后的自动下载,其他浏览器可能有限制
* @returns {boolean} 如果支持自动下载返回 true
*/
export const supportsAutoDownload = (): boolean => {
return isChrome();
};
/**
* 获取Firefox特定的WebRTC配置
*/
export function getFirefoxWebRTCConfig() {
return {
// Firefox可能需要更大的缓冲区阈值
bufferThreshold: 65536 * 4, // 256KB instead of 64KB
// Firefox可能需要更长的延迟
requestDelay: 15, // 15ms instead of 10ms
// Firefox对ArrayBuffer处理的特殊配置
binaryType: "arraybuffer" as BinaryType,
};
}
/**
* 为Firefox优化DataChannel配置
*/
export function getDataChannelConfig(browserName?: string): RTCDataChannelInit {
const isFirefoxBrowser = browserName === "Firefox" || isFirefox();
if (isFirefoxBrowser) {
return {
ordered: true,
// Firefox特定优化:更大的maxPacketLifeTime可能有助于数据传输
maxRetransmits: 3,
};
}
return {
ordered: true,
};
}
/**
* 记录浏览器兼容性信息
*/
export function logBrowserCompatibility() {
const browser = detectBrowser();
const message = `[Browser Compatibility] Browser: ${browser.name} ${browser.version}, isFirefox: ${browser.isFirefox}, userAgent: ${navigator.userAgent}`;
console.log(message);
// 动态导入以避免循环依赖
import("@/app/config/api")
.then(({ postLogToBackend }) => {
postLogToBackend(message);
})
.catch(console.error);
return browser;
}
+324 -73
View File
@@ -1,6 +1,9 @@
// Flow for receiving file(s)/folder(s): First, receive file metadata in batch, [decide if the user needs to select a save directory],
// then click to request, receive the file content, and after receiving endMeta, send an ack to finish.
// Flow for receiving a folder (same as above): Receive a batch file request.
// 🚀 新流程 - 接收端主导的文件传输:
// 1. 接收文件元数据 (fileMetadata)
// 2. 用户点击下载,发送文件请求 (fileRequest)
// 3. 接收所有数据块,自动检测完整性
// 4. 完成Store同步后,主动发送完成确认 (fileReceiveComplete/folderReceiveComplete)
// 文件夹传输:重复单文件流程,最后发送文件夹完成确认
import { SpeedCalculator } from "@/lib/speedCalculator";
import WebRTC_Recipient from "./webrtc_Recipient";
import {
@@ -11,12 +14,13 @@ import {
CurrentString,
StringMetadata,
StringChunk,
FileEnd,
FileHandlers,
FileMeta,
FileRequest,
FolderComplete,
FileReceiveComplete,
FolderReceiveComplete,
} from "@/types/webrtc";
import { postLogToBackend } from "@/app/config/api";
/**
* Manages the state of an active file reception.
@@ -32,6 +36,11 @@ interface ActiveFileReception {
resolve: () => void;
reject: (reason?: any) => void;
};
// 新增:用于跟踪数据接收统计
receivedChunksCount: number; // 实际接收到的chunk数量
expectedChunksCount: number; // 预期的chunk数量
lastChunkIndex: number; // 最后接收的chunk索引
isFinalized?: boolean; // 防止重复finalize的标记
}
class FileReceiver {
@@ -71,7 +80,6 @@ class FileReceiver {
string: this.handleReceivedStringChunk.bind(this),
stringMetadata: this.handleStringMetadata.bind(this),
fileMeta: this.handleFileMetadata.bind(this),
fileEnd: this.handleFileEnd.bind(this),
};
this.setupDataHandler();
@@ -178,6 +186,8 @@ class FileReceiver {
}
const receptionPromise = new Promise<void>((resolve, reject) => {
const expectedChunksCount = Math.ceil((fileInfo.size - offset) / 65536); // 计算预期chunk数量
this.activeFileReception = {
meta: fileInfo,
chunks: [],
@@ -186,7 +196,15 @@ class FileReceiver {
fileHandle: null,
writeStream: null,
completionNotifier: { resolve, reject },
// 新增统计字段
receivedChunksCount: 0,
expectedChunksCount: expectedChunksCount,
lastChunkIndex: -1,
};
postLogToBackend(
`[DEBUG] 🚀 FILE_INIT - ${fileInfo.name}, size: ${fileInfo.size}, chunks: ${expectedChunksCount}`
);
});
if (shouldSaveToDisk) {
@@ -197,6 +215,13 @@ class FileReceiver {
if (this.peerId) {
this.webrtcConnection.sendData(JSON.stringify(request), this.peerId);
this.log("log", "Sent fileRequest", { request });
// 调试日志:记录发送完成
postLogToBackend(`[DEBUG] 📤 FILE_REQUEST sent`);
} else {
postLogToBackend(
`[Firefox Debug] ERROR: Cannot send fileRequest - no peerId available!`
);
}
return receptionPromise;
@@ -257,29 +282,64 @@ class FileReceiver {
}
this.currentFolderName = null;
// After the loop, the receiver has requested all necessary files.
// Send a completion message to the sender to sync the final state.
const folderComplete: FolderComplete = {
type: "FolderComplete",
folderName: folderName,
};
// 🚀 新流程:发送文件夹接收完成确认
// 收集所有成功完成的文件ID
const completedFileIds = folderProgress.fileIds.filter(fileId => {
// 这里可以添加更复杂的验证逻辑,现在简单假设都成功了
return true;
});
if (this.peerId) {
this.webrtcConnection.sendData(
JSON.stringify(folderComplete),
this.peerId
);
this.log(
"log",
`Sent folderComplete message for ${folderName} to peer ${this.peerId}`
);
}
postLogToBackend(
`[Firefox Debug] 📁 All files in folder completed - ${folderName}, files: ${completedFileIds.length}/${folderProgress.fileIds.length}`
);
// 发送文件夹完成消息
this.sendFolderReceiveComplete(folderName, completedFileIds, true);
}
// endregion
// region WebRTC Data Handlers
/**
* 将各种二进制数据格式转换为ArrayBuffer
* 支持Firefox的Blob、Uint8Array等格式
*/
private async convertToArrayBuffer(data: any): Promise<ArrayBuffer | null> {
const originalType = Object.prototype.toString.call(data);
if (data instanceof ArrayBuffer) {
return data;
} else if (data instanceof Blob) {
try {
const arrayBuffer = await data.arrayBuffer();
if (data.size !== arrayBuffer.byteLength) {
postLogToBackend(`[DEBUG] ⚠️ Blob size mismatch: ${data.size}${arrayBuffer.byteLength}`);
}
return arrayBuffer;
} catch (error) {
postLogToBackend(`[DEBUG] ❌ Blob conversion failed: ${error}`);
return null;
}
} else if (data instanceof Uint8Array || ArrayBuffer.isView(data)) {
try {
const uint8Array = data instanceof Uint8Array
? data
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
const newArrayBuffer = new ArrayBuffer(uint8Array.length);
new Uint8Array(newArrayBuffer).set(uint8Array);
return newArrayBuffer;
} catch (error) {
postLogToBackend(`[DEBUG] ❌ TypedArray conversion failed: ${error}`);
return null;
}
} else {
postLogToBackend(`[DEBUG] ❌ Unknown data type: ${Object.prototype.toString.call(data)}`);
return null;
}
}
private async handleReceivedData(
data: string | ArrayBuffer,
data: string | ArrayBuffer | any,
peerId: string
): Promise<void> {
this.peerId = peerId;
@@ -300,24 +360,46 @@ class FileReceiver {
} catch (error) {
this.fireError("Error parsing received JSON data", { error });
}
} else if (data instanceof ArrayBuffer) {
if (!this.activeFileReception) {
this.fireError(
"Received a file chunk without an active file reception.",
{ peerId }
} else {
// 处理各种格式的二进制数据 - Firefox兼容性修复
const arrayBuffer = await this.convertToArrayBuffer(data);
if (arrayBuffer) {
// 调试日志:记录接收到二进制数据
postLogToBackend(
`[Firefox Debug] Received binary data - originalType: ${Object.prototype.toString.call(data)}, convertedSize: ${arrayBuffer.byteLength}, peerId: ${peerId}`
);
return;
if (!this.activeFileReception) {
postLogToBackend(
`[Firefox Debug] ERROR: Received file chunk but no active file reception!`
);
this.fireError(
"Received a file chunk without an active file reception.",
{ peerId }
);
return;
}
postLogToBackend(
`[Firefox Debug] Processing chunk for file: ${this.activeFileReception.meta.name}`
);
this.updateProgress(arrayBuffer.byteLength);
await this.handleFileChunk(arrayBuffer);
} else {
postLogToBackend(
`[Firefox Debug] ERROR: Failed to convert binary data to ArrayBuffer`
);
this.fireError("Received unsupported binary data format", {
dataType: Object.prototype.toString.call(data),
peerId
});
}
this.updateProgress(data.byteLength);
await this.handleFileChunk(data);
}
}
private handleFileMetadata(metadata: fileMetadata): void {
if (this.pendingFilesMeta.has(metadata.fileId)) {
console.log(
`[DEBUG] 📥 FileReceiver File metadata already exists, ignoring: ${metadata.fileId}`
);
return; // Ignore if already received.
}
@@ -326,7 +408,9 @@ class FileReceiver {
if (this.onFileMetaReceived) {
this.onFileMetaReceived(metadata);
} else {
console.error(`[DEBUG] ❌ FileReceiver onFileMetaReceived callback does not exist!`);
console.error(
`[DEBUG] ❌ FileReceiver onFileMetaReceived callback does not exist!`
);
}
// Record the file size for folder progress calculation.
if (metadata.folderName) {
@@ -368,46 +452,106 @@ class FileReceiver {
}
}
private async handleFileEnd(metadata: FileEnd): Promise<void> {
this.log("log", "File transmission ended", { metadata });
const reception = this.activeFileReception;
if (!reception || reception.meta.fileId !== metadata.fileId) {
this.log("warn", "Received fileEnd for unexpected file", { metadata });
return;
}
// 🔧 Key fix: Complete file processing first to ensure the file is added to Store
await this.finalizeFileReceive();
// 🏗️ Architecture refactor: Ensure Store state is fully synchronized before triggering progress callback
if (!this.currentFolderName) {
// 🔧 Optimized async ensure mechanism - ensure Store state is fully synchronized
await Promise.resolve(); // Ensure current execution stack is completed
await new Promise<void>((resolve) => {
// Use longer delay to ensure Store state is fully updated
setTimeout(() => {
this.progressCallback?.(reception.meta.fileId, 1, 0);
resolve();
}, 10); // Increase to 10ms to ensure Store state is fully synchronized
});
}
this.sendFileAck(reception.meta.fileId);
this.log("log", "Sent file-finish ack", { fileId: reception.meta.fileId });
reception.completionNotifier.resolve();
this.activeFileReception = null;
}
// endregion
// region File and Folder Processing
private async handleFileChunk(chunk: ArrayBuffer): Promise<void> {
if (!this.activeFileReception) return;
// 🐛 DEBUG: 记录接收到的原始chunk信息
const currentChunkIndex = this.activeFileReception.receivedChunksCount;
postLogToBackend(
`[DEBUG] 📥 RECEIVE chunk#${currentChunkIndex} - size: ${chunk.byteLength} bytes`
);
// 更新统计信息
this.activeFileReception.receivedChunksCount++;
this.activeFileReception.lastChunkIndex = Math.max(this.activeFileReception.lastChunkIndex, currentChunkIndex);
// 更新进度统计
this.updateProgress(chunk.byteLength);
if (this.activeFileReception.writeStream) {
await this.writeLargeFileChunk(chunk);
} else {
// 存储chunk到内存
this.activeFileReception.chunks.push(chunk);
// 🐛 DEBUG: 验证存储结果
const storedChunk = this.activeFileReception.chunks[this.activeFileReception.chunks.length - 1];
const currentTotalSize = this.activeFileReception.chunks.reduce((sum, c) => sum + (c?.byteLength || 0), 0);
postLogToBackend(
`[DEBUG] 📦 STORED chunk#${currentChunkIndex} - original: ${chunk.byteLength}, stored: ${storedChunk?.byteLength || 'null'}, total: ${currentTotalSize}`
);
// 🐛 DEBUG: 特别关注最后几个chunks
if (currentChunkIndex >= 65) {
postLogToBackend(
`[DEBUG] 🔍 CRITICAL_CHUNK#${currentChunkIndex} - input: ${chunk.byteLength}, stored: ${storedChunk?.byteLength}, isLast: ${currentChunkIndex >= 67}`
);
}
}
await this.checkAndAutoFinalize();
}
/**
* 🚀 新流程:自动检查数据完整性并触发finalize
* 不再依赖发送端的fileEnd信号,接收端自主判断完成
*/
private async checkAndAutoFinalize(): Promise<void> {
if (!this.activeFileReception) return;
const reception = this.activeFileReception;
const receivedChunks = reception.receivedChunksCount;
const expectedChunks = reception.expectedChunksCount;
// 计算当前实际接收的总大小
const currentTotalSize = reception.chunks.reduce((sum, chunk) => {
return sum + (chunk instanceof ArrayBuffer ? chunk.byteLength : 0);
}, 0);
const expectedSize = reception.meta.size;
const chunksComplete = (receivedChunks >= expectedChunks);
const sizeComplete = (currentTotalSize >= expectedSize);
const isDataComplete = chunksComplete && sizeComplete;
// 🐛 DEBUG: 完成状态检查
if (receivedChunks % 10 === 0 || receivedChunks >= expectedChunks - 5) {
postLogToBackend(
`[DEBUG] 🔄 Progress check - chunks: ${receivedChunks}/${expectedChunks}, size: ${currentTotalSize}/${expectedSize}, complete: ${isDataComplete}`
);
}
// 防止重复finalize
if (reception.isFinalized) {
return;
}
if (isDataComplete) {
postLogToBackend(
`[DEBUG] 🎯 TRIGGERING finalize - chunks: ${receivedChunks}/${expectedChunks}, size: ${currentTotalSize}/${expectedSize}`
);
reception.isFinalized = true;
try {
await this.finalizeFileReceive();
if (reception.completionNotifier) {
reception.completionNotifier.resolve();
}
this.activeFileReception = null;
postLogToBackend(`[DEBUG] ✅ Auto-finalize SUCCESS`);
} catch (error) {
postLogToBackend(`[DEBUG] ❌ Auto-finalize ERROR: ${error}`);
if (reception.completionNotifier) {
reception.completionNotifier.reject(error);
}
this.activeFileReception = null;
}
}
}
@@ -541,33 +685,140 @@ class FileReceiver {
const reception = this.activeFileReception;
if (!reception) return;
const fileBlob = new Blob(reception.chunks as ArrayBuffer[], {
postLogToBackend(
`[DEBUG] 🔍 FINALIZE START - fileName: ${reception.meta.name}, expectedSize: ${reception.meta.size}, chunksArray: ${reception.chunks.length}`
);
// 🐛 DEBUG: 详细分析每个chunk
let totalChunkSize = 0;
let validChunks = 0;
const chunkDetails: string[] = [];
reception.chunks.forEach((chunk, index) => {
if (chunk instanceof ArrayBuffer) {
validChunks++;
totalChunkSize += chunk.byteLength;
// 🐛 DEBUG: 特别关注最后几个chunks
if (index >= reception.chunks.length - 5) {
chunkDetails.push(`chunk#${index}: ${chunk.byteLength}bytes`);
postLogToBackend(
`[DEBUG] 🔍 FINAL_CHUNK_ANALYSIS - index: ${index}, size: ${chunk.byteLength}, isLast: ${index === reception.chunks.length - 1}`
);
}
// 检测异常大小
if (chunk.byteLength !== 65536 && index < reception.chunks.length - 1) {
postLogToBackend(`[DEBUG] ⚠️ UNEXPECTED_SIZE - chunk#${index}: ${chunk.byteLength} (should be 65536)`);
}
} else {
postLogToBackend(`[DEBUG] ❌ INVALID_CHUNK - index: ${index}, type: ${Object.prototype.toString.call(chunk)}`);
}
});
// 🐛 DEBUG: 总体分析
postLogToBackend(
`[DEBUG] 📊 CHUNK_SUMMARY - valid: ${validChunks}/${reception.chunks.length}, totalSize: ${totalChunkSize}, expected: ${reception.meta.size}, diff: ${reception.meta.size - totalChunkSize}`
);
if (chunkDetails.length > 0) {
postLogToBackend(`[DEBUG] 🔍 FINAL_CHUNKS: ${chunkDetails.join(', ')}`);
}
// 最终验证
const sizeDifference = reception.meta.size - totalChunkSize;
if (sizeDifference !== 0) {
postLogToBackend(`[DEBUG] ❌ SIZE_MISMATCH - missing: ${sizeDifference} bytes`);
} else {
postLogToBackend(`[DEBUG] ✅ SIZE_VERIFIED - ${totalChunkSize} bytes`);
}
// 创建文件
const fileBlob = new Blob(reception.chunks.filter(chunk => chunk instanceof ArrayBuffer) as ArrayBuffer[], {
type: reception.meta.fileType,
});
const file = new File([fileBlob], reception.meta.name, {
type: reception.meta.fileType,
});
postLogToBackend(`[DEBUG] 📄 FILE_CREATED - size: ${file.size}, expected: ${reception.meta.size}, match: ${file.size === reception.meta.size}`);
const customFile = Object.assign(file, {
fullName: reception.meta.fullName,
folderName: this.currentFolderName,
}) as CustomFile;
let storeUpdated = false;
if (this.onFileReceived) {
// 🔧 Key fix: Ensure onFileReceived callback is fully synchronized
await this.onFileReceived(customFile);
// 🔧 Multiple confirmation mechanism: Ensure Store state is fully synchronized
await Promise.resolve(); // First layer confirmation
await new Promise<void>((resolve) => setTimeout(() => resolve(), 0)); // Second layer confirmation
await Promise.resolve();
await new Promise<void>((resolve) => setTimeout(() => resolve(), 0));
storeUpdated = true;
postLogToBackend(`[DEBUG] ✅ STORE_UPDATED - ${reception.meta.name}`);
}
// 发送完成确认
this.sendFileReceiveComplete(
reception.meta.fileId,
totalChunkSize,
validChunks,
storeUpdated
);
}
// endregion
// region Communication
private sendFileAck(fileId: string): void {
/**
* 发送文件接收完成确认 - 新的接收端主导流程
*/
private sendFileReceiveComplete(
fileId: string,
receivedSize: number,
receivedChunks: number,
storeUpdated: boolean
): void {
if (!this.peerId) return;
const confirmation = JSON.stringify({ type: "fileAck", fileId });
this.webrtcConnection.sendData(confirmation, this.peerId);
const completeMessage: FileReceiveComplete = {
type: "fileReceiveComplete",
fileId,
receivedSize,
receivedChunks,
storeUpdated,
};
const success = this.webrtcConnection.sendData(JSON.stringify(completeMessage), this.peerId);
postLogToBackend(
`[DEBUG] 📤 SENT fileReceiveComplete - size: ${receivedSize}, chunks: ${receivedChunks}, success: ${success}`
);
}
/**
* 发送文件夹接收完成确认
*/
private sendFolderReceiveComplete(
folderName: string,
completedFileIds: string[],
allStoreUpdated: boolean
): void {
if (!this.peerId) return;
const completeMessage: FolderReceiveComplete = {
type: "folderReceiveComplete",
folderName,
completedFileIds,
allStoreUpdated,
};
const success = this.webrtcConnection.sendData(JSON.stringify(completeMessage), this.peerId);
postLogToBackend(
`[Firefox Debug] 📤 Sent folderReceiveComplete - folderName: ${folderName}, completedFiles: ${completedFileIds.length}, allStoreUpdated: ${allStoreUpdated}, success: ${success}`
);
}
// endregion
+208 -60
View File
@@ -1,8 +1,9 @@
// Flow for sending file(s)/folder(s): First, send file metadata, wait for the receiver's request, then send the file content.
// After the file is sent, send an endMeta, wait for the receiver's ack, and finish.
// Flow for sending a folder (same as above): Receive a batch file request.
// Loop through and send the metadata for all files, then record the file size information for the folder part to calculate progress.
// The receiving display end distinguishes between single files and folders.
// 🚀 新流程 - 接收端主导的文件传输:
// 1. 发送文件元数据 (fileMetadata)
// 2. 接收文件请求 (fileRequest)
// 3. 发送所有数据块,完成后等待接收端确认
// 4. 收到接收端确认 (fileReceiveComplete/folderReceiveComplete) 后设置进度100%
// 发送端不再主动发送完成信号,完全由接收端控制完成时机
import { generateFileId } from "@/lib/fileUtils";
import { SpeedCalculator } from "@/lib/speedCalculator";
import WebRTC_Initiator from "./webrtc_Initiator";
@@ -12,10 +13,11 @@ import {
WebRTCMessage,
PeerState,
FolderMeta,
FileAck,
FileRequest,
FolderComplete,
FileReceiveComplete,
FolderReceiveComplete,
} from "@/types/webrtc";
import { postLogToBackend } from "@/app/config/api";
class FileSender {
private webrtcConnection: WebRTC_Initiator;
@@ -115,23 +117,11 @@ class FileSender {
case "fileRequest":
this.handleFileRequest(message as FileRequest, peerId);
break;
case "fileAck":
peerState.isSending = false;
this.log("log", `Received file-finish ack from peer ${peerId}`, {
fileId: (message as FileAck).fileId,
});
case "fileReceiveComplete":
this.handleFileReceiveComplete(message as FileReceiveComplete, peerId);
break;
case "folderComplete":
const folderName = (message as FolderComplete).folderName;
this.log(
"log",
`Received folderComplete message for ${folderName} from peer ${peerId}`
);
// The receiver has confirmed the folder is complete.
// Force the progress to 100% for the sender's UI.
if (this.pendingFolerMeta[folderName]) {
peerState.progressCallback?.(folderName, 1, 0);
}
case "folderReceiveComplete":
this.handleFolderReceiveComplete(message as FolderReceiveComplete, peerId);
break;
default:
this.log("warn", `Unknown signaling message type received`, {
@@ -147,6 +137,69 @@ class FileSender {
): void {
this.getPeerState(peerId).progressCallback = callback;
}
/**
* 处理接收端发送的文件接收完成确认 - 新流程
*/
private handleFileReceiveComplete(
message: FileReceiveComplete,
peerId: string
): void {
const peerState = this.getPeerState(peerId);
postLogToBackend(
`[Firefox Debug] 📥 Received fileReceiveComplete - fileId: ${message.fileId}, receivedSize: ${message.receivedSize}, receivedChunks: ${message.receivedChunks}, storeUpdated: ${message.storeUpdated}`
);
// 清理发送状态
peerState.isSending = false;
// 触发单文件100%进度(只有非文件夹情况)
if (!peerState.currentFolderName) {
postLogToBackend(
`[Firefox Debug] 🎯 Setting single file progress to 100% - ${message.fileId}`
);
peerState.progressCallback?.(message.fileId, 1, 0);
} else {
postLogToBackend(
`[Firefox Debug] 📁 File in folder completed, not setting progress yet - ${message.fileId} (folder: ${peerState.currentFolderName})`
);
}
this.log("log", `File reception confirmed by peer ${peerId}`, {
fileId: message.fileId,
receivedSize: message.receivedSize,
storeUpdated: message.storeUpdated,
});
}
/**
* 处理接收端发送的文件夹接收完成确认 - 新流程
*/
private handleFolderReceiveComplete(
message: FolderReceiveComplete,
peerId: string
): void {
const peerState = this.getPeerState(peerId);
postLogToBackend(
`[Firefox Debug] 📥 Received folderReceiveComplete - folderName: ${message.folderName}, completedFiles: ${message.completedFileIds.length}, allStoreUpdated: ${message.allStoreUpdated}`
);
// 触发文件夹100%进度
if (this.pendingFolerMeta[message.folderName]) {
postLogToBackend(
`[Firefox Debug] 🎯 Setting folder progress to 100% - ${message.folderName}`
);
peerState.progressCallback?.(message.folderName, 1, 0);
}
this.log("log", `Folder reception confirmed by peer ${peerId}`, {
folderName: message.folderName,
completedFiles: message.completedFileIds.length,
allStoreUpdated: message.allStoreUpdated,
});
}
// Respond to a file request by sending the file
private async handleFileRequest(
request: FileRequest,
@@ -158,7 +211,13 @@ class FileSender {
"log",
`Handling file request for ${request.fileId} from ${peerId} with offset ${offset}`
);
// 🔧 Firefox兼容性修复:添加稍长延迟确保接收端完全准备好
// 根据[[memory:7549586]],这个延迟解决了时序竞态条件
await new Promise(resolve => setTimeout(resolve, 10));
if (file) {
postLogToBackend(`[Firefox Debug] Starting file send - fileName: ${file.name}, fileSize: ${file.size}, offset: ${offset}`);
await this.sendSingleFile(file, peerId, offset);
} else {
this.fireError(`File not found for request`, {
@@ -258,9 +317,15 @@ class FileSender {
try {
await this.processSendQueue(file, peerId);
await this.finalizeSendFile(fileId, peerId);
// 🚀 新流程:不再主动发送fileEnd,等待接收端的fileReceiveComplete确认
postLogToBackend(
`[Firefox Debug] 📤 File sending completed, waiting for receiver confirmation - ${file.name}`
);
await this.waitForTransferComplete(peerId); // Wait for transfer completion -- receiver confirmation
// 新流程:让接收端主导完成流程,不再主动发送fileEnd
await this.waitForTransferComplete(peerId); // Wait for receiver's fileReceiveComplete confirmation
} catch (error: any) {
this.fireError(`Error sending file ${file.name}: ${error.message}`, {
fileId,
@@ -293,11 +358,20 @@ class FileSender {
byteLength: number,
fileId: string,
fileSize: number,
peerId: string
peerId: string,
wasActuallySent: boolean = true // 新增:确保只有真正发送成功的数据才被统计
): Promise<void> {
const peerState = this.getPeerState(peerId);
if (!peerState) return;
// 🔧 重要修复:只有成功发送的数据才更新统计
if (!wasActuallySent) {
postLogToBackend(
`[Firefox Debug] ⚠️ Data send failed, not updating progress - fileId: ${fileId}, size: ${byteLength}`
);
return;
}
// Always update the individual file's progress first.
if (!peerState.totalBytesSent[fileId]) {
// This case should be handled by sendSingleFile's initialization
@@ -346,12 +420,18 @@ class FileSender {
throw new Error("Data channel not found");
}
// For ArrayBuffer, if it exceeds 64KB, it needs to be sent in fragments (fixes sendData failed)
if (data instanceof ArrayBuffer) {
await this.sendLargeArrayBuffer(data, peerId);
} else {
// Send string directly
await this.sendSingleData(data, peerId);
try {
// For ArrayBuffer, if it exceeds 64KB, it needs to be sent in fragments (fixes sendData failed)
if (data instanceof ArrayBuffer) {
await this.sendLargeArrayBuffer(data, peerId);
} else {
await this.sendSingleData(data, peerId);
}
} catch (error) {
// 确保所有发送失败都能被正确抛出
const errorMessage = `sendWithBackpressure failed: ${error}`;
postLogToBackend(`[Firefox Debug] ${errorMessage}`);
throw new Error(errorMessage);
}
}
@@ -394,12 +474,25 @@ class FileSender {
if (!dataChannel) {
throw new Error("Data channel not found");
}
// Firefox兼容性调试:记录发送前的数据信息
const dataType = typeof data === "string" ? "string" : data instanceof ArrayBuffer ? "ArrayBuffer" : "unknown";
const dataSize = typeof data === "string" ? data.length : data instanceof ArrayBuffer ? data.byteLength : 0;
postLogToBackend(`[Firefox Debug] Sending data - type: ${dataType}, size: ${dataSize}, channelState: ${dataChannel.readyState}`);
// Intelligent send control - decide sending strategy based on buffer status
await this.smartBufferControl(dataChannel, peerId);
// Send data
if (!this.webrtcConnection.sendData(data, peerId)) {
throw new Error("sendData failed");
const sendResult = this.webrtcConnection.sendData(data, peerId);
postLogToBackend(`[Firefox Debug] Data send result: ${sendResult ? 'success' : 'failed'} - type: ${dataType}, size: ${dataSize}`);
if (!sendResult) {
const errorMessage = `sendData failed for ${dataType} data of size ${dataSize}`;
postLogToBackend(`[Firefox Debug] ❌ ${errorMessage}`);
throw new Error(errorMessage);
}
}
@@ -554,10 +647,10 @@ class FileSender {
const MAX_WAIT_TIME = 3000;
const startTime = Date.now();
const adaptiveThreshold = this.getAdaptiveThreshold(peerId);
const threshold_75 = adaptiveThreshold * 0.75;
const threshold_low = adaptiveThreshold * 0.3;
const initialBuffered = dataChannel.bufferedAmount;
let pollCount = 0;
while (dataChannel.bufferedAmount > threshold_75) {
while (dataChannel.bufferedAmount > threshold_low) {
pollCount++;
if (Date.now() - startTime > MAX_WAIT_TIME) {
@@ -652,13 +745,21 @@ class FileSender {
let offset = peerState.readOffset || 0;
const batchSize = FileSender.OPTIMIZED_CONFIG.BATCH_SIZE;
let totalChunksSent = 0;
let totalBytesSentInLoop = 0;
// Initialize network performance monitoring
this.initializeNetworkPerformance(peerId);
postLogToBackend(
`[Firefox Debug] 🚀 processSendQueue started - fileName: ${file.name}, fileSize: ${file.size}, startOffset: ${offset}`
);
try {
let loopCount = 0;
// Use batch reading + loop instead of traditional recursion to greatly improve performance
while (offset < file.size && peerState.isSending) {
loopCount++;
// Batch read multiple large chunks - fully utilize memory advantages
const chunks = await this.readMultipleChunks(
@@ -671,48 +772,95 @@ class FileSender {
if (chunks.length === 0) break;
postLogToBackend(
`[Firefox Debug] 📦 Loop ${loopCount} - read ${chunks.length} chunks, totalSize: ${chunks.reduce((sum, c) => sum + c.byteLength, 0)}`
);
for (const chunk of chunks) {
if (!peerState.isSending || offset >= file.size) break;
// Use standard intelligent control sending
await this.sendWithBackpressure(chunk, peerId);
// 🔧 修复:检查发送是否成功
let sendSuccessful = false;
try {
await this.sendWithBackpressure(chunk, peerId);
sendSuccessful = true;
totalChunksSent++;
totalBytesSentInLoop += chunk.byteLength;
} catch (error) {
postLogToBackend(
`[Firefox Debug] ❌ Failed to send chunk ${totalChunksSent + 1}: ${error}`
);
sendSuccessful = false;
// 不更新统计,但继续尝试发送下一个chunk
}
// Update progress
offset += chunk.byteLength;
peerState.readOffset = offset;
// Update progress only if send was successful
if (sendSuccessful) {
offset += chunk.byteLength;
peerState.readOffset = offset;
// Update file and folder progress
await this.updateProgress(
chunk.byteLength,
fileId,
file.size,
peerId
);
// Update file and folder progress with success flag
await this.updateProgress(
chunk.byteLength,
fileId,
file.size,
peerId,
true // 明确标记为发送成功
);
// 每50个chunk记录一次进度
if (totalChunksSent % 50 === 0) {
const progress = ((offset / file.size) * 100).toFixed(2);
postLogToBackend(
`[Firefox Debug] 📊 Progress: ${totalChunksSent} chunks sent, ${totalBytesSentInLoop} bytes, ${progress}% complete`
);
}
} else {
// 发送失败但不中止传输,记录失败信息
postLogToBackend(
`[Firefox Debug] 🔄 Chunk send failed but continuing... failed chunks will be missing from total`
);
}
}
}
// File sending completed
if (offset >= file.size && !peerState.currentFolderName) {
peerState.progressCallback?.(fileId, 1, 0);
// File sending completed - final statistics using actual sent bytes
const actualBytesSent = peerState.totalBytesSent[fileId] || 0;
postLogToBackend(
`[Firefox Debug] 🏁 Send completed - totalChunks: ${totalChunksSent}, loopBytes: ${totalBytesSentInLoop}, actualBytes: ${actualBytesSent}, finalOffset: ${offset}, expected: ${file.size}`
);
// 验证统计一致性
if (totalBytesSentInLoop !== actualBytesSent) {
postLogToBackend(
`[Firefox Debug] ⚠️ Statistics mismatch! Loop counted ${totalBytesSentInLoop} bytes but progress tracked ${actualBytesSent} bytes`
);
}
// 🚀 新流程:不再在这里设置进度100%,等待接收端确认
// if (offset >= file.size && !peerState.currentFolderName) {
// peerState.progressCallback?.(fileId, 1, 0);
// }
postLogToBackend(
`[Firefox Debug] 🏁 All data sent, waiting for receiver to confirm completion...`
);
} catch (error: any) {
const errorMessage = `Error in hybrid optimized transfer: ${error.message}`;
postLogToBackend(
`[Firefox Debug] ❌ Send error after ${totalChunksSent} chunks, ${totalBytesSentInLoop} bytes: ${errorMessage}`
);
this.fireError(errorMessage, {
fileId,
peerId,
offset,
totalChunksSent,
totalBytesSentInLoop,
});
throw error;
}
}
//send fileEnd signal
private async finalizeSendFile(fileId: string, peerId: string): Promise<void> {
// this.log("log", `Finalizing file send for ${fileId} to ${peerId}`);
const endMessage = JSON.stringify({ type: "fileEnd", fileId });
if (!this.webrtcConnection.sendData(endMessage, peerId)) {
this.log("warn", `Failed to send fileEnd message for ${fileId}`);
}
// The isSending flag will be set to false upon receiving fileAck
}
private abortFileSend(fileId: string, peerId: string): void {
this.log("warn", `Aborting file send for ${fileId} to ${peerId}`);
+34 -8
View File
@@ -1,4 +1,5 @@
import { CustomFile } from "@/types/webrtc";
import { postLogToBackend } from "@/app/config/api";
// Adaptively format the file size with units
export const formatFileSize = (sizeInBytes: number): string => {
@@ -26,14 +27,39 @@ export const downloadAs = async (
file: Blob | File,
saveName: string
): Promise<void> => {
const url = URL.createObjectURL(file);
const a = document.createElement("a");
a.href = url;
a.download = saveName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// 调试日志:记录downloadAs函数被调用
postLogToBackend(`[Firefox Debug] downloadAs called - fileName: ${saveName}, fileSize: ${file.size}, fileType: ${file.type}`);
// 检查文件是否为空
if (file.size === 0) {
postLogToBackend(`[Firefox Debug] CRITICAL ERROR: downloadAs received a file with 0 size! This is the root cause of the 0-byte download issue.`);
}
try {
// 调试日志:记录URL创建过程
postLogToBackend(`[Firefox Debug] Creating object URL for file...`);
const url = URL.createObjectURL(file);
postLogToBackend(`[Firefox Debug] Object URL created successfully: ${url}`);
const a = document.createElement("a");
a.href = url;
a.download = saveName;
// 调试日志:记录DOM操作
postLogToBackend(`[Firefox Debug] Adding anchor element to DOM and triggering click...`);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// 调试日志:记录清理过程
postLogToBackend(`[Firefox Debug] Download triggered, cleaning up object URL...`);
URL.revokeObjectURL(url);
postLogToBackend(`[Firefox Debug] downloadAs completed successfully`);
} catch (error) {
postLogToBackend(`[Firefox Debug] ERROR in downloadAs: ${error}`);
throw error;
}
};
export const traverseFileTree = async (
+14
View File
@@ -9,6 +9,7 @@ import {
} from "@/app/config/environment";
import { useFileTransferStore } from "@/stores/fileTransferStore";
import type { CustomFile } from "@/types/webrtc";
import { postLogToBackend } from "@/app/config/api";
class WebRTCService {
public sender: WebRTC_Initiator;
@@ -123,6 +124,9 @@ class WebRTCService {
};
this.fileReceiver.onFileReceived = async (file) => {
// 调试日志:记录文件接收回调被调用
postLogToBackend(`[Firefox Debug] onFileReceived callback called - fileName: ${file.name}, size: ${file.size}, type: ${file.type}`);
// 🔧 Enhanced fix: Ensure Store state updates are fully synchronized with multiple verifications
const store = useFileTransferStore.getState();
@@ -132,7 +136,10 @@ class WebRTCService {
);
if (!existingFile) {
postLogToBackend(`[Firefox Debug] Adding file to store - no existing file found`);
store.addRetrievedFile(file);
} else {
postLogToBackend(`[Firefox Debug] File already exists in store - skipping duplicate`);
}
// 🔧 Additional ensure: Immediately verify if state update was successful with retry mechanism
@@ -146,9 +153,16 @@ class WebRTCService {
(f) => f.name === file.name && f.size === file.size
);
postLogToBackend(`[Firefox Debug] Verification attempt ${verificationAttempts}: fileExists: ${fileExists}, retrievedFiles.length: ${updatedStore.retrievedFiles.length}`);
if (!fileExists && verificationAttempts < maxVerificationAttempts) {
postLogToBackend(`[Firefox Debug] File not found in store, attempting to add again...`);
updatedStore.addRetrievedFile(file);
setTimeout(verifyFileAdded, 10);
} else if (fileExists) {
postLogToBackend(`[Firefox Debug] File successfully added to store!`);
} else {
postLogToBackend(`[Firefox Debug] ERROR: Failed to add file to store after ${maxVerificationAttempts} attempts!`);
}
};
+29 -7
View File
@@ -1,11 +1,16 @@
// Initiator flow: Join room; receive 'ready' event (this event is triggered by the socket server after a new recipient enters) -> createPeerConnection + createDataChannel -> createAndSendOffer
import BaseWebRTC, { WebRTCConfig } from "./webrtc_base";
import { postLogToBackend } from "@/app/config/api";
import { detectBrowser, getDataChannelConfig, logBrowserCompatibility } from "./browserUtils";
const developmentEnv = process.env.NEXT_PUBLIC_development!; // Development environment
export default class WebRTC_Initiator extends BaseWebRTC {
private browserInfo = detectBrowser();
constructor(config: WebRTCConfig) {
super(config);
// 记录浏览器兼容性信息
logBrowserCompatibility();
this.setupInitiatorSocketListeners();
}
@@ -75,16 +80,33 @@ export default class WebRTC_Initiator extends BaseWebRTC {
return;
}
try {
const dataChannel = peerConnection.createDataChannel("dataChannel", {
ordered: true,
// reliable: true
});
// this.log('log', `Created data channel for peer ${peerId}`);
dataChannel.bufferedAmountLowThreshold = 262144; //256 KB -- Can be adjusted as needed
// 使用浏览器特定的DataChannel配置
const dataChannelConfig = getDataChannelConfig(this.browserInfo.name);
postLogToBackend(
`[Firefox Debug] Creating DataChannel with config - browser: ${this.browserInfo.name}, config: ${JSON.stringify(dataChannelConfig)}`
);
const dataChannel = peerConnection.createDataChannel("dataChannel", dataChannelConfig);
// Firefox特定的缓冲区阈值优化
if (this.browserInfo.isFirefox) {
dataChannel.bufferedAmountLowThreshold = 131072; // 128KB for Firefox
postLogToBackend(`[Firefox Debug] Set Firefox-specific bufferedAmountLowThreshold: 128KB`);
} else {
dataChannel.bufferedAmountLowThreshold = 262144; // 256KB for others
}
this.setupDataChannel(dataChannel, peerId);
this.dataChannels.set(peerId, dataChannel);
postLogToBackend(
`[Firefox Debug] DataChannel created successfully - peer: ${peerId}, label: dataChannel, browser: ${this.browserInfo.name}`
);
} catch (error) {
postLogToBackend(
`[Firefox Debug] Error creating DataChannel - peer: ${peerId}, error: ${error}`
);
this.fireError(`Error creating data channel for peer ${peerId}`, {
error,
peerId,
+23 -1
View File
@@ -1,6 +1,7 @@
// Recipient flow: Join room; receive 'offer' event -> createPeerConnection + createDataChannel -> send answer
import BaseWebRTC, { WebRTCConfig } from "./webrtc_base";
import { postLogToBackend } from "@/app/config/api";
import { detectBrowser, logBrowserCompatibility } from "./browserUtils";
const developmentEnv = process.env.NEXT_PUBLIC_development!; // Development environment
interface AnswerPayload {
@@ -8,8 +9,12 @@ interface AnswerPayload {
peerId: string;
}
export default class WebRTC_Recipient extends BaseWebRTC {
private browserInfo = detectBrowser();
constructor(config: WebRTCConfig) {
super(config);
// 记录浏览器兼容性信息
logBrowserCompatibility();
this.setupRecipientSocketListeners();
}
@@ -105,10 +110,27 @@ export default class WebRTC_Recipient extends BaseWebRTC {
return;
}
// 调试日志:记录DataChannel事件监听器设置
postLogToBackend(`[Firefox Debug] Setting ondatachannel listener for peer: ${peerId}, connectionState: ${peerConnection.connectionState}, browser: ${this.browserInfo.name}`);
peerConnection.ondatachannel = (event) => {
// this.log('log', `Received data channel from peer ${peerId}`);
postLogToBackend(`[Firefox Debug] Received ondatachannel event - peer: ${peerId}, channel label: ${event.channel.label}, readyState: ${event.channel.readyState}, browser: ${this.browserInfo.name}`);
// Firefox特定的DataChannel配置
if (this.browserInfo.isFirefox) {
// 确保binaryType设置为arraybuffer(虽然这通常是默认值)
try {
// 注意:DataChannel没有binaryType属性,但我们可以在这里做其他Firefox特定的配置
postLogToBackend(`[Firefox Debug] Applying Firefox-specific DataChannel configuration`);
} catch (error) {
postLogToBackend(`[Firefox Debug] Firefox DataChannel configuration error: ${error}`);
}
}
this.setupDataChannel(event.channel, peerId);
this.dataChannels.set(peerId, event.channel);
postLogToBackend(`[Firefox Debug] DataChannel setup completed for peer: ${peerId}, total channels: ${this.dataChannels.size}, browser: ${this.browserInfo.name}`);
};
}
}
+66 -4
View File
@@ -344,7 +344,15 @@ export default class BaseWebRTC {
dataChannel: RTCDataChannel,
peerId: string
): void {
// 调试日志:记录DataChannel设置
postLogToBackend(
`[Firefox Debug] Setting up DataChannel for peer: ${peerId}, label: ${dataChannel.label}, readyState: ${dataChannel.readyState}`
);
dataChannel.onopen = () => {
postLogToBackend(
`[Firefox Debug] DataChannel opened for peer: ${peerId}, readyState: ${dataChannel.readyState}`
);
// this.log('log',`Data channel opened for peer ${peerId}`);
setTimeout(() => {
this.onDataChannelOpen?.(peerId);
@@ -352,11 +360,50 @@ export default class BaseWebRTC {
};
dataChannel.onmessage = (event) => {
this.onDataReceived?.(event.data, peerId);
// 增强的数据类型检测 - 支持Firefox的多种二进制数据格式
let dataType = "Unknown";
let dataSize = 0;
if (typeof event.data === "string") {
dataType = "String";
dataSize = event.data.length;
} else if (event.data instanceof ArrayBuffer) {
dataType = "ArrayBuffer";
dataSize = event.data.byteLength;
} else if (event.data instanceof Blob) {
dataType = "Blob";
dataSize = event.data.size;
} else if (event.data instanceof Uint8Array) {
dataType = "Uint8Array";
dataSize = event.data.byteLength;
} else if (ArrayBuffer.isView(event.data)) {
dataType = "TypedArray";
dataSize = event.data.byteLength;
} else {
// 详细的未知类型调试信息
dataType = `Unknown(${Object.prototype.toString.call(event.data)})`;
dataSize = event.data?.length || event.data?.size || event.data?.byteLength || 0;
}
postLogToBackend(
`[Firefox Debug] DataChannel onmessage - peer: ${peerId}, dataType: ${dataType}, size: ${dataSize}`
);
if (this.onDataReceived) {
this.onDataReceived(event.data, peerId);
}
};
dataChannel.onclose = () =>
dataChannel.onerror = (error) => {
this.log("error", `Data channel error for peer ${peerId}`, { error });
};
dataChannel.onclose = () => {
postLogToBackend(
`[Firefox Debug] DataChannel closed for peer: ${peerId}`
);
this.log("log", `Data channel with ${peerId} closed.`);
};
}
// Join a room. sendInitiatorOnline indicates whether to send "initiator online" message after joining.
public async joinRoom(
@@ -434,10 +481,25 @@ export default class BaseWebRTC {
protected sendToPeer(data: any, peerId: string): boolean {
const dataChannel = this.dataChannels.get(peerId);
if (dataChannel?.readyState === "open") {
dataChannel.send(data);
return true;
try {
// Firefox兼容性调试:记录发送详细信息
const dataType = typeof data === "string" ? "string" : data instanceof ArrayBuffer ? "ArrayBuffer" : typeof data;
const dataSize = typeof data === "string" ? data.length : data instanceof ArrayBuffer ? data.byteLength : 0;
postLogToBackend(`[Firefox Debug] sendToPeer - type: ${dataType}, size: ${dataSize}, bufferedAmount: ${dataChannel.bufferedAmount}`);
dataChannel.send(data);
postLogToBackend(`[Firefox Debug] sendToPeer success - bufferedAmount after: ${dataChannel.bufferedAmount}`);
return true;
} catch (error) {
postLogToBackend(`[Firefox Debug] sendToPeer error: ${error}`);
this.log("error", `Error sending data to peer ${peerId}`, { error });
return false;
}
}
postLogToBackend(`[Firefox Debug] DataChannel not ready - peerId: ${peerId}, state: ${dataChannel?.readyState || 'undefined'}`);
this.log("warn", `Data channel not ready for peer ${peerId}. Retrying...`);
return this.retryDataSend(data, peerId);
}
+2
View File
@@ -174,9 +174,11 @@ export type FileTransferButton = {
CurrentFileTransferring_tips: string;
OtherFileTransferring_tips: string;
download_tips: string;
PendingSave_tips: string; // 新增:待保存状态提示
Saved_dis: string;
Waiting_dis: string;
Download_dis: string;
Save_dis: string; // 新增:保存按钮文字
};
export type FileListDisplay = {
+12 -13
View File
@@ -29,11 +29,6 @@ export interface FileRequest {
offset?: number; // Optional: byte offset to resume from
}
export interface FileAck {
type: "fileAck";
fileId: string;
}
export interface StringMetadata {
type: "stringMetadata";
length: number;
@@ -46,24 +41,29 @@ export interface StringChunk {
total: number;
}
export interface FileEnd {
type: "fileEnd";
// 接收端主导的完成确认消息
export interface FileReceiveComplete {
type: "fileReceiveComplete";
fileId: string;
receivedSize: number;
receivedChunks: number;
storeUpdated: boolean; // 确认Store已更新
}
export interface FolderComplete {
type: "FolderComplete";
export interface FolderReceiveComplete {
type: "folderReceiveComplete";
folderName: string;
completedFileIds: string[];
allStoreUpdated: boolean; // 确认所有文件都已加入Store
}
export type WebRTCMessage =
| fileMetadata
| FileRequest
| FileAck
| StringMetadata
| StringChunk
| FileEnd
| FolderComplete;
| FileReceiveComplete
| FolderReceiveComplete;
export interface FolderMeta {
totalSize: number;
@@ -96,5 +96,4 @@ export interface FileHandlers {
string: (data: any, peerId: string) => void;
stringMetadata: (data: any, peerId: string) => void;
fileMeta: (data: any, peerId: string) => void;
fileEnd: (data: any) => Promise<void>;
}