RTCDataChannelState;
+ getBufferedAmount: () => number;
+ waitForBufferDrain: (threshold?: number) => Promise;
isConnectedToRoom: (roomCode: string, role: 'sender' | 'receiver') => boolean;
// 当前房间信息
@@ -134,6 +136,8 @@ export function useSharedWebRTCManager(): WebRTCConnection {
// 工具方法
getChannelState: dataChannelManager.getChannelState,
+ getBufferedAmount: dataChannelManager.getBufferedAmount,
+ waitForBufferDrain: dataChannelManager.waitForBufferDrain,
isConnectedToRoom: stateManager.isConnectedToRoom,
// 媒体轨道方法
diff --git a/chuan-next/src/hooks/connection/useWebRTCDataChannelManager.ts b/chuan-next/src/hooks/connection/useWebRTCDataChannelManager.ts
index 197214c..9514bf5 100644
--- a/chuan-next/src/hooks/connection/useWebRTCDataChannelManager.ts
+++ b/chuan-next/src/hooks/connection/useWebRTCDataChannelManager.ts
@@ -44,6 +44,12 @@ export interface WebRTCDataChannelManager {
// 获取数据通道状态(兼容 RTCDataChannelState)
getChannelState: () => RTCDataChannelState;
+ // 获取当前缓冲区大小
+ getBufferedAmount: () => number;
+
+ // 等待缓冲区排空到指定阈值以下
+ waitForBufferDrain: (threshold?: number) => Promise;
+
// 处理数据通道消息 (P2P)
handleDataChannelMessage: (event: MessageEvent) => void;
}
@@ -92,8 +98,7 @@ export function useWebRTCDataChannelManager(
// 数据通道处理
if (role === 'sender') {
const dataChannel = pc.createDataChannel('shared-channel', {
- ordered: true,
- maxRetransmits: 3
+ ordered: true
});
dcRef.current = dataChannel;
@@ -471,6 +476,62 @@ export function useWebRTCDataChannelManager(
return dcRef.current?.readyState || 'closed';
}, []);
+ // 获取当前缓冲区大小
+ const getBufferedAmount = useCallback((): number => {
+ if (dcRef.current && dcRef.current.readyState === 'open') {
+ return dcRef.current.bufferedAmount;
+ }
+ if (relayWsRef.current && relayWsRef.current.readyState === WebSocket.OPEN) {
+ return relayWsRef.current.bufferedAmount;
+ }
+ return 0;
+ }, []);
+
+ // 等待缓冲区排空到阈值以下
+ const waitForBufferDrain = useCallback((threshold: number = 1 * 1024 * 1024): Promise => {
+ // P2P DataChannel
+ if (dcRef.current && dcRef.current.readyState === 'open') {
+ if (dcRef.current.bufferedAmount <= threshold) {
+ return Promise.resolve();
+ }
+ return new Promise((resolve) => {
+ const dc = dcRef.current!;
+ dc.bufferedAmountLowThreshold = threshold;
+ const onLow = () => {
+ dc.removeEventListener('bufferedamountlow', onLow);
+ resolve();
+ };
+ dc.addEventListener('bufferedamountlow', onLow);
+ // 安全超时,防止死等
+ setTimeout(() => {
+ dc.removeEventListener('bufferedamountlow', onLow);
+ resolve();
+ }, 5000);
+ });
+ }
+ // Relay WebSocket
+ if (relayWsRef.current && relayWsRef.current.readyState === WebSocket.OPEN) {
+ if (relayWsRef.current.bufferedAmount <= threshold) {
+ return Promise.resolve();
+ }
+ return new Promise((resolve) => {
+ const checkInterval = setInterval(() => {
+ if (!relayWsRef.current || relayWsRef.current.readyState !== WebSocket.OPEN ||
+ relayWsRef.current.bufferedAmount <= threshold) {
+ clearInterval(checkInterval);
+ resolve();
+ }
+ }, 50);
+ // 安全超时
+ setTimeout(() => {
+ clearInterval(checkInterval);
+ resolve();
+ }, 5000);
+ });
+ }
+ return Promise.resolve();
+ }, []);
+
return {
createDataChannel,
switchToRelay,
@@ -481,6 +542,8 @@ export function useWebRTCDataChannelManager(
registerMessageHandler,
registerDataHandler,
getChannelState,
+ getBufferedAmount,
+ waitForBufferDrain,
handleDataChannelMessage,
};
}
\ No newline at end of file
diff --git a/chuan-next/src/hooks/file-transfer/useFileStateManager.ts b/chuan-next/src/hooks/file-transfer/useFileStateManager.ts
index be3dc2f..6bb9486 100644
--- a/chuan-next/src/hooks/file-transfer/useFileStateManager.ts
+++ b/chuan-next/src/hooks/file-transfer/useFileStateManager.ts
@@ -73,12 +73,19 @@ export const useFileStateManager = ({
}, []);
// 更新文件进度
- const updateFileProgress = useCallback((fileId: string, fileName: string, progress: number) => {
+ const updateFileProgress = useCallback((fileId: string, fileName: string, progress: number, speed?: number, eta?: number) => {
const newStatus = progress >= 100 ? 'completed' as const : 'downloading' as const;
setFileList(prev => prev.map(item => {
if (item.id === fileId || item.name === fileName) {
console.log(`更新文件 ${item.name} 进度: ${item.progress} -> ${progress}`);
- return { ...item, progress, status: newStatus };
+ return {
+ ...item,
+ progress,
+ status: newStatus,
+ // 仅在传输中且有新值时更新速度/ETA,完成时清除
+ speed: newStatus === 'completed' ? undefined : (speed !== undefined ? speed : item.speed),
+ eta: newStatus === 'completed' ? undefined : (eta !== undefined ? eta : item.eta)
+ };
}
return item;
}));
diff --git a/chuan-next/src/hooks/file-transfer/useFileTransferBusiness.ts b/chuan-next/src/hooks/file-transfer/useFileTransferBusiness.ts
index 03804a1..1dbf910 100644
--- a/chuan-next/src/hooks/file-transfer/useFileTransferBusiness.ts
+++ b/chuan-next/src/hooks/file-transfer/useFileTransferBusiness.ts
@@ -20,6 +20,15 @@ interface FileReceiveProgress {
fileName: string;
totalChunks: number;
progress: number;
+ fileSize: number; // 文件总大小 bytes
+ startTime: number; // 开始接收时间
+ lastChunkTime: number; // 上一个块接收时间
+ // 滑动窗口测速
+ speedWindowBytes: number; // 窗口内累计字节数
+ speedWindowStart: number; // 窗口开始时间
+ lastReportedSpeed: number; // 上次上报的速度 bytes/s
+ lastReportedEta: number; // 上次上报的 ETA 秒
+ lastSpeedReportTime: number; // 上次上报速度的时间
}
// 文件元数据
@@ -57,50 +66,29 @@ interface TransferStatus {
lastChunkTime: number;
retryCount: Map;
averageSpeed: number; // KB/s
+ // 滑动窗口测速
+ speedWindowBytes: number; // 窗口内累计字节数
+ speedWindowStart: number; // 窗口开始时间
+ lastReportedSpeed: number; // 上次上报的速度 bytes/s
+ lastReportedEta: number; // 上次上报的 ETA 秒
+ lastSpeedReportTime: number; // 上次上报速度的时间
}
// 回调类型
type FileReceivedCallback = (fileData: { id: string; file: File }) => void;
type FileRequestedCallback = (fileId: string, fileName: string) => void;
-type FileProgressCallback = (progressInfo: { fileId: string; fileName: string; progress: number }) => void;
+type FileProgressCallback = (progressInfo: { fileId: string; fileName: string; progress: number; speed?: number; eta?: number }) => void;
type FileListReceivedCallback = (fileList: FileInfo[]) => void;
const CHANNEL_NAME = 'file-transfer';
-const CHUNK_SIZE = 256 * 1024; // 256KB
-const MAX_RETRIES = 5; // 最大重试次数
+const CHUNK_SIZE = 256 * 1024; // 256KB — WebRTC DataChannel 单次发送上限
+const MAX_RETRIES = 5; // 最大重试次数(仅用于连接恢复)
const RETRY_DELAY = 1000; // 重试延迟(毫秒)
-const ACK_TIMEOUT = 5000; // 确认超时(毫秒)
-
-/**
- * 计算数据的CRC32校验和
- */
-function calculateChecksum(data: ArrayBuffer): string {
- const buffer = new Uint8Array(data);
- let crc = 0xFFFFFFFF;
-
- for (let i = 0; i < buffer.length; i++) {
- crc ^= buffer[i];
- for (let j = 0; j < 8; j++) {
- crc = crc & 1 ? (crc >>> 1) ^ 0xEDB88320 : crc >>> 1;
- }
- }
-
- return (crc ^ 0xFFFFFFFF).toString(16).padStart(8, '0');
-}
-
-/**
- * 生成简单的校验和(备用方案)
- */
-function simpleChecksum(data: ArrayBuffer): string {
- const buffer = new Uint8Array(data);
- let sum = 0;
-
- for (let i = 0; i < Math.min(buffer.length, 1000); i++) {
- sum += buffer[i];
- }
-
- return sum.toString(16);
-}
+const ACK_TIMEOUT = 5000; // 完成确认超时(毫秒)
+const SPEED_WINDOW_MS = 2000; // 速度计算滑动窗口 2 秒
+const SPEED_REPORT_INTERVAL_MS = 1000; // 速度上报最小间隔 1 秒
+const BUFFER_HIGH_WATER = 2 * 1024 * 1024; // 2MB — 发送背压阈值
+const PROGRESS_LOG_INTERVAL = 50; // 每 50 个块打印一次日志
/**
* 文件传输业务层
@@ -137,8 +125,6 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
// 传输状态管理
const transferStatus = useRef