新增desktop 新增修复速率
This commit is contained in:
parent
e4702b877f
commit
f1c9f018a4
@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<DesktopView :peerId="sign2peerid(targetId)" />
|
||||
<div class="container">
|
||||
<!-- 连接状态 -->
|
||||
<div class="status-bar">
|
||||
@ -20,6 +21,14 @@
|
||||
</div>
|
||||
|
||||
<div class="connect-section">
|
||||
<div v-if="isConnected" class="connect-section">
|
||||
<div class="connect-item" @click="requestDesktop">
|
||||
<img src="/static/desktop.png" alt="桌面" />
|
||||
</div>
|
||||
<div class="connect-item" @click="requestCall">
|
||||
<img src="/static/phone.png" alt="通话" />
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
v-model="targetId"
|
||||
:placeholder="!myId ? '请稍后...' : '输入对方ID'"
|
||||
@ -79,11 +88,13 @@ import FileView from "./item/fileView.vue";
|
||||
import { notification } from "ant-design-vue";
|
||||
import { fileMgrInstance, type FileData } from "./utils/fileMgr";
|
||||
import { Button } from "ant-design-vue";
|
||||
import DesktopView from "./item/desptopView.vue";
|
||||
import {
|
||||
formatSize,
|
||||
getUrlParam,
|
||||
localCurrentFile,
|
||||
remoteCurrentFile,
|
||||
sign2peerid,
|
||||
} from "./utils/common";
|
||||
import FileTransferView from "./item/fileTranserView.vue";
|
||||
|
||||
@ -99,6 +110,13 @@ const selectedRemoteFiles = ref<string[]>([]);
|
||||
onMounted(() => {
|
||||
isPhone.value = document.body.clientWidth < 768;
|
||||
});
|
||||
|
||||
const requestDesktop = () => {
|
||||
peer.requestDesktop(targetId.value);
|
||||
};
|
||||
const requestCall = () => {
|
||||
peer.requestCall(targetId.value);
|
||||
};
|
||||
const shareUrl = async () => {
|
||||
if (myId.value) {
|
||||
const url =
|
||||
@ -252,6 +270,24 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.connect-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: #dff1ff;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #82c7fb;
|
||||
}
|
||||
.connect-item:hover {
|
||||
background: #cae5f9;
|
||||
}
|
||||
.connect-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.connect-section {
|
||||
display: flex;
|
||||
|
||||
232
src/pages/file/item/desptopView.vue
Normal file
232
src/pages/file/item/desptopView.vue
Normal file
@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div
|
||||
class="desktop-view-wrapper"
|
||||
:class="{ 'is-collapsed': isCollapsed, 'has-stream': !!stream }"
|
||||
>
|
||||
<div class="collapse-header" v-if="stream" @click="toggleCollapse">
|
||||
<span>远程桌面{{ isCollapsed ? "(已收起)" : "" }}</span>
|
||||
<a-button type="link">
|
||||
<template #icon>
|
||||
<UpOutlined v-if="!isCollapsed" />
|
||||
<DownOutlined v-else />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="desktop-view" :class="{ 'is-fullscreen': isFullscreen }">
|
||||
<div class="video-container" ref="videoContainer">
|
||||
<video ref="videoRef" autoplay playsinline></video>
|
||||
<div class="controls" v-if="stream">
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
:icon="isFullscreen ? 'fullscreen-exit' : 'fullscreen'"
|
||||
@click="toggleFullscreen"
|
||||
>
|
||||
{{ isFullscreen ? "退出全屏" : "全屏" }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div v-if="!stream" class="no-signal">
|
||||
<a-empty description="等待远程桌面共享..." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { peer } from "../utils/peer";
|
||||
import { message } from "ant-design-vue";
|
||||
import { UpOutlined, DownOutlined } from "@ant-design/icons-vue";
|
||||
|
||||
const props = defineProps<{
|
||||
peerId: string;
|
||||
}>();
|
||||
|
||||
interface StreamEvent extends CustomEvent {
|
||||
detail: {
|
||||
stream: MediaStream;
|
||||
type: "desktop" | "call";
|
||||
peerId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MediaEndedEvent extends CustomEvent {
|
||||
detail: {
|
||||
type: "desktop" | "call";
|
||||
peerId: string;
|
||||
};
|
||||
}
|
||||
|
||||
const videoRef = ref<HTMLVideoElement | null>(null);
|
||||
const videoContainer = ref<HTMLElement | null>(null);
|
||||
const stream = ref<MediaStream | null>(null);
|
||||
const isFullscreen = ref(false);
|
||||
const isCollapsed = ref(false);
|
||||
|
||||
// 切换收起/展开状态
|
||||
const toggleCollapse = () => {
|
||||
isCollapsed.value = !isCollapsed.value;
|
||||
};
|
||||
|
||||
// 处理远程流
|
||||
const handleStream = (event: StreamEvent) => {
|
||||
const { stream: remoteStream, type } = event.detail;
|
||||
if (props.peerId !== event.detail.peerId) return;
|
||||
if (type === "desktop" && videoRef.value) {
|
||||
stream.value = remoteStream;
|
||||
videoRef.value.srcObject = remoteStream;
|
||||
isCollapsed.value = false; // 收到流时自动展开
|
||||
}
|
||||
};
|
||||
|
||||
// 处理媒体结束
|
||||
const handleMediaEnded = (event: MediaEndedEvent) => {
|
||||
const { type } = event.detail;
|
||||
if (props.peerId !== event.detail.peerId) return;
|
||||
if (type === "desktop") {
|
||||
stream.value = null;
|
||||
if (videoRef.value) {
|
||||
videoRef.value.srcObject = null;
|
||||
}
|
||||
message.info("远程桌面共享已结束");
|
||||
}
|
||||
};
|
||||
|
||||
// 切换全屏
|
||||
const toggleFullscreen = async () => {
|
||||
if (!videoContainer.value) return;
|
||||
|
||||
try {
|
||||
if (!isFullscreen.value) {
|
||||
if (videoContainer.value.requestFullscreen) {
|
||||
await videoContainer.value.requestFullscreen();
|
||||
}
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
await document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
message.error("切换全屏失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 监听全屏变化
|
||||
const handleFullscreenChange = () => {
|
||||
isFullscreen.value = !!document.fullscreenElement;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
peer.on("stream", handleStream as EventListener);
|
||||
peer.on("media-ended", handleMediaEnded as EventListener);
|
||||
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
peer.off("stream", handleStream as EventListener);
|
||||
peer.off("media-ended", handleMediaEnded as EventListener);
|
||||
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
||||
|
||||
// 清理视频流
|
||||
if (stream.value) {
|
||||
stream.value.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.desktop-view-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: #000;
|
||||
transition: all 0.3s ease;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.desktop-view-wrapper.has-stream {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.desktop-view-wrapper.is-collapsed {
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
.collapse-header {
|
||||
height: 40px;
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.collapse-header:hover {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.desktop-view {
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
background: #000;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.video-container:hover .controls {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.no-signal {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.is-fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
:deep(.ant-empty-description) {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:deep(.anticon) {
|
||||
vertical-align: 0;
|
||||
}
|
||||
</style>
|
||||
@ -1,6 +1,7 @@
|
||||
import { type Ref, ref } from "vue";
|
||||
import { FileInfo } from "./fileMgr";
|
||||
import { Permission } from "./peer";
|
||||
import { Modal } from "ant-design-vue";
|
||||
|
||||
export const formatSize = (size: number): string => {
|
||||
if (size < 1024) return `${size} B`;
|
||||
@ -18,6 +19,22 @@ export const base64ToString = (base64: string) => {
|
||||
//base64转汉字
|
||||
return decodeURIComponent(atob(base64))
|
||||
}
|
||||
export const confirmWin = (title: string, content: string, okText: string, cancelText: string): Promise<boolean> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
Modal.confirm({
|
||||
title,
|
||||
content,
|
||||
okText,
|
||||
cancelText,
|
||||
async onOk() {
|
||||
resolve(true)
|
||||
},
|
||||
onCancel() {
|
||||
reject(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
const commonCharsCN = '的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多点行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当西从广业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决皮被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该冯价严龙飞';
|
||||
const commonCharsEN = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
export const randomChars = (num: number = 3, chars: string = commonCharsEN) => {
|
||||
@ -51,6 +68,8 @@ export function getPermissionSet(): { [key in Permission]: boolean } {
|
||||
edit: false,
|
||||
view: true,
|
||||
download: true,
|
||||
desktop: true,
|
||||
call: false
|
||||
};
|
||||
}
|
||||
return permissionSet;
|
||||
|
||||
@ -3,346 +3,401 @@ import { MessageType, peer } from "./peer";
|
||||
import { type DataConnection } from "peerjs";
|
||||
// 传输状态枚举
|
||||
export enum TransferStatus {
|
||||
WAITING = "waiting", // 等待传输
|
||||
SENDING = "sending", // 发送中
|
||||
RECEIVING = "receiving", // 接收中
|
||||
PAUSED = "paused", // 已暂停
|
||||
COMPLETED = "completed", // 已完成
|
||||
ERROR = "error", // 错误
|
||||
WAITING = "waiting", // 等待传输
|
||||
SENDING = "sending", // 发送中
|
||||
RECEIVING = "receiving", // 接收中
|
||||
PAUSED = "paused", // 已暂停
|
||||
COMPLETED = "completed", // 已完成
|
||||
ERROR = "error", // 错误
|
||||
}
|
||||
|
||||
// 传输进度接口
|
||||
export interface TransferProgress {
|
||||
transferredSize: number; // 已传输大小
|
||||
totalSize: number; // 总大小
|
||||
speed: number; // 传输速度 (bytes/s)
|
||||
status: TransferStatus; // 传输状态
|
||||
percent: number; // 进度百分比
|
||||
costTime: number; // 传输时间
|
||||
updateTime: number; // 更新时间
|
||||
transferredSize: number; // 已传输大小
|
||||
totalSize: number; // 总大小
|
||||
speed: number; // 传输速度 (bytes/s)
|
||||
status: TransferStatus; // 传输状态
|
||||
percent: number; // 进度百分比
|
||||
costTime: number; // 传输时间
|
||||
updateTime: number; // 更新时间
|
||||
}
|
||||
|
||||
// 分片配置
|
||||
const CHUNK_SIZE = 64 * 1024; // 64KB
|
||||
const MAX_CHUNK_SIZE = 200 * 1024; // 200KB
|
||||
const CHUNK_SIZE = 256 * 1024; // 256KB (从64KB提升,减少消息数量)
|
||||
const MAX_CHUNK_SIZE = 256 * 1024; // 256KB (WebRTC SCTP 最大安全消息大小)
|
||||
//多大的文件需要分片
|
||||
export const NEED_CHUNK_FILE_SIZE = 200 * 1024; // 200KB
|
||||
export const NEED_CHUNK_FILE_SIZE_PREVIEW = 50 * 1024 * 1024; // 50MB
|
||||
|
||||
// 流水线流控配置
|
||||
const MAX_BUFFERED_AMOUNT = 1024 * 1024; // 1MB - 触发流控的缓冲上限
|
||||
const DRAIN_THRESHOLD = 256 * 1024; // 256KB - 恢复发送的缓冲下限
|
||||
|
||||
export class FileTransfer {
|
||||
public conn: DataConnection;
|
||||
private file: FileInfo;
|
||||
private chunkSize: number;
|
||||
private transferredSize: number = 0;
|
||||
private lastTransferredSize: number = 0;
|
||||
private startTime: number = 0;
|
||||
private status: TransferStatus = TransferStatus.WAITING;
|
||||
private pausePromise?: Promise<void>;
|
||||
private pauseResolve?: () => void;
|
||||
private aborted: boolean = false;
|
||||
private preView: boolean = false;
|
||||
// 进度回调
|
||||
private onProgressCallback?: (transfer: FileTransfer) => void;
|
||||
public getFile(): FileInfo {
|
||||
return this.file;
|
||||
}
|
||||
constructor(conn: DataConnection, file: FileInfo, chunkSize: number = CHUNK_SIZE) {
|
||||
this.conn = conn;
|
||||
this.file = file;
|
||||
this.chunkSize = Math.min(chunkSize, MAX_CHUNK_SIZE);
|
||||
file.addTransfer(this)
|
||||
fileTransferMgrInstance.addFileTransfer(this)
|
||||
}
|
||||
public init(preView: boolean = false) {
|
||||
this.clear();
|
||||
this.status = TransferStatus.WAITING;
|
||||
this.preView = preView;
|
||||
public conn: DataConnection;
|
||||
private file: FileInfo;
|
||||
private chunkSize: number;
|
||||
private transferredSize: number = 0;
|
||||
private lastTransferredSize: number = 0;
|
||||
private startTime: number = 0;
|
||||
private status: TransferStatus = TransferStatus.WAITING;
|
||||
private pausePromise?: Promise<void>;
|
||||
private pauseResolve?: () => void;
|
||||
private aborted: boolean = false;
|
||||
private preView: boolean = false;
|
||||
// 进度回调
|
||||
private onProgressCallback?: (transfer: FileTransfer) => void;
|
||||
public getFile(): FileInfo {
|
||||
return this.file;
|
||||
}
|
||||
constructor(
|
||||
conn: DataConnection,
|
||||
file: FileInfo,
|
||||
chunkSize: number = CHUNK_SIZE,
|
||||
) {
|
||||
this.conn = conn;
|
||||
this.file = file;
|
||||
this.chunkSize = Math.min(chunkSize, MAX_CHUNK_SIZE);
|
||||
file.addTransfer(this);
|
||||
fileTransferMgrInstance.addFileTransfer(this);
|
||||
}
|
||||
public init(preView: boolean = false) {
|
||||
this.clear();
|
||||
this.status = TransferStatus.WAITING;
|
||||
this.preView = preView;
|
||||
|
||||
return this;
|
||||
}
|
||||
public clear() {
|
||||
this.offset = 0;
|
||||
this.totalSize = 0;
|
||||
this.fileBuffer = null;
|
||||
this.fileData = null;
|
||||
this.transferredSize = 0;
|
||||
this.lastTransferredSize = 0;
|
||||
this.startTime = 0;
|
||||
this.pausePromise = undefined;
|
||||
this.pauseResolve = undefined;
|
||||
this.aborted = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public clear() {
|
||||
this.offset = 0;
|
||||
this.totalSize = 0;
|
||||
this.fileBuffer = null;
|
||||
this.fileData = null;
|
||||
this.transferredSize = 0;
|
||||
this.lastTransferredSize = 0;
|
||||
this.startTime = 0;
|
||||
this.pausePromise = undefined;
|
||||
this.pauseResolve = undefined;
|
||||
this.aborted = false;
|
||||
}
|
||||
|
||||
// 设置进度回调
|
||||
public onProgress(callback: (transfer: FileTransfer) => void) {
|
||||
this.onProgressCallback = callback;
|
||||
}
|
||||
public currentProgress(): TransferProgress {
|
||||
return this.getProgress();
|
||||
}
|
||||
// 获取当前进度
|
||||
private getProgress(): TransferProgress {
|
||||
const now = Date.now();
|
||||
const timeElapsed = (now - this.startTime) / 1000; // 转换为秒
|
||||
const speed = timeElapsed > 0 ? (this.transferredSize - this.lastTransferredSize) / timeElapsed : 0;
|
||||
let totalSize = this.fileData?.chunkData?.totalSize || this.file.size;
|
||||
const progress: TransferProgress = {
|
||||
transferredSize: this.transferredSize,
|
||||
totalSize: totalSize,
|
||||
speed,
|
||||
status: this.status,
|
||||
percent: (this.transferredSize / totalSize) * 100,
|
||||
costTime: timeElapsed,
|
||||
updateTime: now
|
||||
// 设置进度回调
|
||||
public onProgress(callback: (transfer: FileTransfer) => void) {
|
||||
this.onProgressCallback = callback;
|
||||
}
|
||||
public currentProgress(): TransferProgress {
|
||||
return this.getProgress();
|
||||
}
|
||||
// 获取当前进度
|
||||
private getProgress(): TransferProgress {
|
||||
const now = Date.now();
|
||||
const timeElapsed = (now - this.startTime) / 1000; // 转换为秒
|
||||
const speed =
|
||||
timeElapsed > 0
|
||||
? (this.transferredSize - this.lastTransferredSize) / timeElapsed
|
||||
: 0;
|
||||
let totalSize = this.fileData?.chunkData?.totalSize || this.file.size;
|
||||
const progress: TransferProgress = {
|
||||
transferredSize: this.transferredSize,
|
||||
totalSize: totalSize,
|
||||
speed,
|
||||
status: this.status,
|
||||
percent: (this.transferredSize / totalSize) * 100,
|
||||
costTime: timeElapsed,
|
||||
updateTime: now,
|
||||
};
|
||||
|
||||
// 更新上次传输大小和开始时间
|
||||
this.lastTransferredSize = this.transferredSize;
|
||||
this.startTime = now;
|
||||
return progress;
|
||||
}
|
||||
public fileData: FileData;
|
||||
public offset: number = 0;
|
||||
public totalSize: number = 0;
|
||||
private fileBuffer: ArrayBuffer | null = null;
|
||||
// 发送文件 (流水线模式 - 不再逐片等待确认)
|
||||
public async sendFile(savePath: string = ""): Promise<boolean> {
|
||||
try {
|
||||
if (this.status == TransferStatus.WAITING) {
|
||||
this.startTime = Date.now();
|
||||
this.fileData = (await this.file.getFile()) as FileData;
|
||||
this.totalSize = this.fileData.size;
|
||||
this.fileBuffer = this.fileData.buffer;
|
||||
this.fileData.preView = this.preView;
|
||||
this.fileData.savePath = savePath;
|
||||
this.fileData.buffer = null; // 移除完整buffer,只传分片
|
||||
this.addTask(new TransferTask(this.fileData, this.file));
|
||||
this.status = TransferStatus.SENDING;
|
||||
this.updateProgress();
|
||||
}
|
||||
|
||||
const dc = this.conn.dataChannel;
|
||||
|
||||
// 流水线发送: 不再逐片等待确认,通过 bufferedAmount 控制背压
|
||||
while (this.offset < this.totalSize && !this.aborted) {
|
||||
if (this.pausePromise) {
|
||||
this.status = TransferStatus.PAUSED;
|
||||
this.updateProgress();
|
||||
await this.pausePromise;
|
||||
this.status = TransferStatus.SENDING;
|
||||
}
|
||||
|
||||
const end = Math.min(this.offset + this.chunkSize, this.totalSize);
|
||||
const chunk = this.fileBuffer.slice(this.offset, end);
|
||||
this.fileData.chunkData = {
|
||||
offset: this.offset,
|
||||
totalSize: this.totalSize,
|
||||
buffer: chunk,
|
||||
};
|
||||
|
||||
// 更新上次传输大小和开始时间
|
||||
this.lastTransferredSize = this.transferredSize;
|
||||
this.startTime = now;
|
||||
return progress;
|
||||
}
|
||||
public fileData: FileData
|
||||
public offset: number = 0;
|
||||
public totalSize: number = 0;
|
||||
private fileBuffer: ArrayBuffer | null = null;
|
||||
// 发送文件
|
||||
public async sendFile(savePath: string = ''): Promise<boolean> {
|
||||
try {
|
||||
if (this.status == TransferStatus.WAITING) {
|
||||
this.startTime = Date.now();
|
||||
this.fileData = await this.file.getFile() as FileData;
|
||||
this.totalSize = this.fileData.size;
|
||||
this.fileBuffer = this.fileData.buffer;
|
||||
this.fileData.preView = this.preView;
|
||||
this.fileData.savePath = savePath;
|
||||
this.addTask(new TransferTask(this.fileData, this.file));
|
||||
this.status = TransferStatus.SENDING;
|
||||
this.updateProgress();
|
||||
}
|
||||
|
||||
if (this.offset < this.totalSize && !this.aborted) {
|
||||
// 检查是否暂停
|
||||
if (this.pausePromise) {
|
||||
this.status = TransferStatus.PAUSED;
|
||||
this.updateProgress();
|
||||
await this.pausePromise;
|
||||
this.status = TransferStatus.SENDING;
|
||||
}
|
||||
|
||||
// 发送分片
|
||||
const chunk = this.fileBuffer.slice(this.offset, this.offset + this.chunkSize);
|
||||
this.fileData.buffer = null;
|
||||
this.fileData.chunkData = {
|
||||
offset: this.offset,
|
||||
totalSize: this.totalSize,
|
||||
buffer: chunk
|
||||
}
|
||||
return this.sendFileChunk(this.fileData).then((): Promise<boolean> => {
|
||||
this.offset += chunk.byteLength;
|
||||
this.transferredSize = this.offset;
|
||||
if (this.offset >= this.totalSize) {
|
||||
return this.sendFileComplete();
|
||||
} else {
|
||||
return this.sendFile();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.aborted) {
|
||||
this.status = TransferStatus.COMPLETED;
|
||||
this.updateProgress();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.status = TransferStatus.ERROR;
|
||||
this.updateProgress();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
private sendFileChunk(chunk: FileData): Promise<void> {
|
||||
this.updateProgress()
|
||||
return peer.send({
|
||||
// 发送分片 (fire-and-forget,不等确认)
|
||||
peer.send(
|
||||
{
|
||||
type: MessageType.push_file_chunk,
|
||||
data: chunk,
|
||||
}, this.conn);
|
||||
}
|
||||
private async sendFileComplete(): Promise<boolean> {
|
||||
await peer.send({
|
||||
data: this.fileData,
|
||||
},
|
||||
this.conn,
|
||||
true,
|
||||
);
|
||||
|
||||
this.offset = end;
|
||||
this.transferredSize = this.offset;
|
||||
this.updateProgress();
|
||||
|
||||
// 基于 bufferedAmount 的流控: 缓冲过大时等待排空
|
||||
if (dc && dc.bufferedAmount > MAX_BUFFERED_AMOUNT) {
|
||||
await new Promise<void>((resolve) => {
|
||||
const checkBuffer = () => {
|
||||
if (dc.bufferedAmount < DRAIN_THRESHOLD || this.aborted) {
|
||||
resolve();
|
||||
} else {
|
||||
requestAnimationFrame(checkBuffer);
|
||||
}
|
||||
};
|
||||
checkBuffer();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.aborted) {
|
||||
// 仅等待最终完成确认
|
||||
await peer.send(
|
||||
{
|
||||
type: MessageType.push_file_complete,
|
||||
data: this.fileData,
|
||||
}, this.conn);
|
||||
},
|
||||
this.conn,
|
||||
);
|
||||
this.status = TransferStatus.COMPLETED;
|
||||
this.updateProgress();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
this.status = TransferStatus.ERROR;
|
||||
this.updateProgress();
|
||||
throw error;
|
||||
}
|
||||
// 接收文件
|
||||
public async receiveFile(fData: FileData, preView: boolean = false): Promise<void> {
|
||||
try {
|
||||
let task = this.getTask(fData)
|
||||
if (!task) {
|
||||
this.addTask(new TransferTask(fData, this.file));
|
||||
} else {
|
||||
task.updateFileData(fData, TransferStatus.RECEIVING);
|
||||
}
|
||||
this.status = TransferStatus.RECEIVING;
|
||||
this.transferredSize = fData.chunkData.offset;
|
||||
this.startTime = Date.now();
|
||||
this.totalSize = fData.chunkData.totalSize;
|
||||
if (fData.chunkData.totalSize <= fData.chunkData.buffer.byteLength + fData.chunkData.offset) {
|
||||
this.status = TransferStatus.COMPLETED;
|
||||
}
|
||||
if (preView) {
|
||||
await this.file.addPreviewCacheBuffer(fData.chunkData.buffer, fData.chunkData.offset, fData.chunkData.totalSize);
|
||||
} else {
|
||||
const path = fData.savePath + '/' + fData.name;
|
||||
await this.file.createFile(path, fData.chunkData.buffer, fData.chunkData.offset);
|
||||
// if (this.status == TransferStatus.COMPLETED) {
|
||||
// await this.file.renameFile(path, fData.savePath + '/' + fData.name);
|
||||
// }
|
||||
}
|
||||
}
|
||||
// 接收文件
|
||||
public async receiveFile(
|
||||
fData: FileData,
|
||||
preView: boolean = false,
|
||||
): Promise<void> {
|
||||
try {
|
||||
let task = this.getTask(fData);
|
||||
if (!task) {
|
||||
this.addTask(new TransferTask(fData, this.file));
|
||||
} else {
|
||||
task.updateFileData(fData, TransferStatus.RECEIVING);
|
||||
}
|
||||
this.status = TransferStatus.RECEIVING;
|
||||
this.transferredSize = fData.chunkData.offset;
|
||||
this.startTime = Date.now();
|
||||
this.totalSize = fData.chunkData.totalSize;
|
||||
if (
|
||||
fData.chunkData.totalSize <=
|
||||
fData.chunkData.buffer.byteLength + fData.chunkData.offset
|
||||
) {
|
||||
this.status = TransferStatus.COMPLETED;
|
||||
}
|
||||
if (preView) {
|
||||
await this.file.addPreviewCacheBuffer(
|
||||
fData.chunkData.buffer,
|
||||
fData.chunkData.offset,
|
||||
fData.chunkData.totalSize,
|
||||
);
|
||||
} else {
|
||||
const path = fData.savePath + "/" + fData.name;
|
||||
await this.file.createFile(
|
||||
path,
|
||||
fData.chunkData.buffer,
|
||||
fData.chunkData.offset,
|
||||
);
|
||||
// if (this.status == TransferStatus.COMPLETED) {
|
||||
// await this.file.renameFile(path, fData.savePath + '/' + fData.name);
|
||||
// }
|
||||
}
|
||||
|
||||
this.updateProgress(fData);
|
||||
return
|
||||
} catch (error) {
|
||||
this.status = TransferStatus.ERROR;
|
||||
this.updateProgress(fData);
|
||||
throw error;
|
||||
}
|
||||
this.updateProgress(fData);
|
||||
return;
|
||||
} catch (error) {
|
||||
this.status = TransferStatus.ERROR;
|
||||
this.updateProgress(fData);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停传输
|
||||
public pause() {
|
||||
if (this.status === TransferStatus.SENDING || this.status === TransferStatus.RECEIVING) {
|
||||
this.pausePromise = new Promise(resolve => {
|
||||
this.pauseResolve = resolve;
|
||||
});
|
||||
}
|
||||
// 暂停传输
|
||||
public pause() {
|
||||
if (
|
||||
this.status === TransferStatus.SENDING ||
|
||||
this.status === TransferStatus.RECEIVING
|
||||
) {
|
||||
this.pausePromise = new Promise((resolve) => {
|
||||
this.pauseResolve = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复传输
|
||||
public resume() {
|
||||
if (this.pauseResolve) {
|
||||
this.pauseResolve();
|
||||
this.pausePromise = undefined;
|
||||
this.pauseResolve = undefined;
|
||||
}
|
||||
// 恢复传输
|
||||
public resume() {
|
||||
if (this.pauseResolve) {
|
||||
this.pauseResolve();
|
||||
this.pausePromise = undefined;
|
||||
this.pauseResolve = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 取消传输
|
||||
public abort() {
|
||||
this.aborted = true;
|
||||
this.resume(); // 恢复暂停的传输以便能够正确退出
|
||||
}
|
||||
// 取消传输
|
||||
public abort() {
|
||||
this.aborted = true;
|
||||
this.resume(); // 恢复暂停的传输以便能够正确退出
|
||||
}
|
||||
|
||||
// 更新进度
|
||||
private updateProgress(fData: FileData = this.fileData) {
|
||||
this.updateTask(fData);
|
||||
if (this.onProgressCallback) {
|
||||
this.onProgressCallback(this);
|
||||
}
|
||||
}
|
||||
private tasks: TransferTask[] = [];
|
||||
//记录传输任务
|
||||
public addTask(task: TransferTask) {
|
||||
this.tasks.push(task);
|
||||
this.updateProgress(task.fileData);
|
||||
return task;
|
||||
}
|
||||
public getTasks() {
|
||||
this.tasks.forEach(task => task.updateProgress());
|
||||
return this.tasks;
|
||||
}
|
||||
public updateTask(fData: FileData) {
|
||||
const task = this.tasks.find(task => task.fileData.path === fData.path);
|
||||
task.updateFileData(fData, this.status);
|
||||
}
|
||||
public getTask(fData: FileData) {
|
||||
return this.tasks.find(task => task.fileData.path === fData.path);
|
||||
}
|
||||
//清除已完成任务
|
||||
public clearCompletedTasks() {
|
||||
this.tasks = this.tasks.filter(t => t.status !== TransferStatus.COMPLETED);
|
||||
// 更新进度
|
||||
private updateProgress(fData: FileData = this.fileData) {
|
||||
this.updateTask(fData);
|
||||
if (this.onProgressCallback) {
|
||||
this.onProgressCallback(this);
|
||||
}
|
||||
}
|
||||
private tasks: TransferTask[] = [];
|
||||
//记录传输任务
|
||||
public addTask(task: TransferTask) {
|
||||
this.tasks.push(task);
|
||||
this.updateProgress(task.fileData);
|
||||
return task;
|
||||
}
|
||||
public getTasks() {
|
||||
this.tasks.forEach((task) => task.updateProgress());
|
||||
return this.tasks;
|
||||
}
|
||||
public updateTask(fData: FileData) {
|
||||
const task = this.tasks.find((task) => task.fileData.path === fData.path);
|
||||
task.updateFileData(fData, this.status);
|
||||
}
|
||||
public getTask(fData: FileData) {
|
||||
return this.tasks.find((task) => task.fileData.path === fData.path);
|
||||
}
|
||||
//清除已完成任务
|
||||
public clearCompletedTasks() {
|
||||
this.tasks = this.tasks.filter(
|
||||
(t) => t.status !== TransferStatus.COMPLETED,
|
||||
);
|
||||
}
|
||||
}
|
||||
class FileTransferMgr {
|
||||
private fileTransfers: Map<string, FileTransfer> = new Map();
|
||||
public addFileTransfer(transfer: FileTransfer) {
|
||||
transfer.onProgress(this.notifyTransferChanged.bind(this));
|
||||
this.fileTransfers.set(transfer.getFile().path, transfer);
|
||||
|
||||
}
|
||||
public getFileTransfer(path: string) {
|
||||
return this.fileTransfers.get(path);
|
||||
}
|
||||
public getAllFileTransfers() {
|
||||
return Array.from(this.fileTransfers.values());
|
||||
}
|
||||
// 传输进度变化回调
|
||||
private onTransferChangedHandler: ((transfer: FileTransfer) => void)[] = []
|
||||
public onTransferChanged(handler: (transfer: FileTransfer) => void) {
|
||||
this.onTransferChangedHandler.push(handler);
|
||||
}
|
||||
public removeTransferChangedHandler(handler: (transfer: FileTransfer) => void) {
|
||||
this.onTransferChangedHandler = this.onTransferChangedHandler.filter(h => h !== handler);
|
||||
}
|
||||
public notifyTransferChanged(transfer: FileTransfer) {
|
||||
this.onTransferChangedHandler.forEach(handler => handler(transfer));
|
||||
}
|
||||
|
||||
|
||||
private fileTransfers: Map<string, FileTransfer> = new Map();
|
||||
public addFileTransfer(transfer: FileTransfer) {
|
||||
transfer.onProgress(this.notifyTransferChanged.bind(this));
|
||||
this.fileTransfers.set(transfer.getFile().path, transfer);
|
||||
}
|
||||
public getFileTransfer(path: string) {
|
||||
return this.fileTransfers.get(path);
|
||||
}
|
||||
public getAllFileTransfers() {
|
||||
return Array.from(this.fileTransfers.values());
|
||||
}
|
||||
// 传输进度变化回调
|
||||
private onTransferChangedHandler: ((transfer: FileTransfer) => void)[] = [];
|
||||
public onTransferChanged(handler: (transfer: FileTransfer) => void) {
|
||||
this.onTransferChangedHandler.push(handler);
|
||||
}
|
||||
public removeTransferChangedHandler(
|
||||
handler: (transfer: FileTransfer) => void,
|
||||
) {
|
||||
this.onTransferChangedHandler = this.onTransferChangedHandler.filter(
|
||||
(h) => h !== handler,
|
||||
);
|
||||
}
|
||||
public notifyTransferChanged(transfer: FileTransfer) {
|
||||
this.onTransferChangedHandler.forEach((handler) => handler(transfer));
|
||||
}
|
||||
}
|
||||
export class TransferTask {
|
||||
//传输文件数据
|
||||
fileData: FileData;
|
||||
//接收载体目录
|
||||
file: FileInfo;
|
||||
//开始时间
|
||||
startTime: number = 0;
|
||||
//传输状态
|
||||
status: TransferStatus = TransferStatus.WAITING;
|
||||
//传输进度
|
||||
progress: TransferProgress = {
|
||||
transferredSize: 0,
|
||||
totalSize: 0,
|
||||
speed: 0,
|
||||
status: TransferStatus.WAITING,
|
||||
percent: 0,
|
||||
costTime: 0,
|
||||
updateTime: 0
|
||||
//传输文件数据
|
||||
fileData: FileData;
|
||||
//接收载体目录
|
||||
file: FileInfo;
|
||||
//开始时间
|
||||
startTime: number = 0;
|
||||
//传输状态
|
||||
status: TransferStatus = TransferStatus.WAITING;
|
||||
//传输进度
|
||||
progress: TransferProgress = {
|
||||
transferredSize: 0,
|
||||
totalSize: 0,
|
||||
speed: 0,
|
||||
status: TransferStatus.WAITING,
|
||||
percent: 0,
|
||||
costTime: 0,
|
||||
updateTime: 0,
|
||||
};
|
||||
constructor(fileData: FileData, file: FileInfo) {
|
||||
this.fileData = fileData;
|
||||
this.file = file;
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
public updateFileData(
|
||||
fileData: FileData,
|
||||
status: TransferStatus = TransferStatus.SENDING,
|
||||
) {
|
||||
this.fileData = fileData;
|
||||
this.status = status;
|
||||
this.updateProgress();
|
||||
}
|
||||
public updateProgress(): TransferProgress {
|
||||
if (this.status == TransferStatus.COMPLETED) {
|
||||
this.progress.updateTime = Date.now();
|
||||
return this.progress;
|
||||
}
|
||||
const totalSize = this.fileData.chunkData?.totalSize || this.fileData.size;
|
||||
const transferredSize =
|
||||
this.fileData.chunkData?.offset || this.fileData.size;
|
||||
const speed = this.fileData.chunkData
|
||||
? ((transferredSize + this.fileData.chunkData.buffer.byteLength) /
|
||||
(Date.now() - this.startTime)) *
|
||||
1000
|
||||
: (this.fileData.size / (Date.now() - this.startTime)) * 1000;
|
||||
const percent = this.fileData.chunkData
|
||||
? ((transferredSize + this.fileData.chunkData.buffer.byteLength) /
|
||||
totalSize) *
|
||||
100
|
||||
: (transferredSize / totalSize) * 100;
|
||||
this.status =
|
||||
transferredSize >= totalSize ? TransferStatus.COMPLETED : this.status;
|
||||
this.progress = {
|
||||
transferredSize: transferredSize,
|
||||
totalSize: totalSize,
|
||||
speed: speed == Infinity ? totalSize : speed,
|
||||
status: this.status,
|
||||
percent: percent,
|
||||
costTime: Date.now() - this.startTime,
|
||||
updateTime: Date.now(),
|
||||
};
|
||||
constructor(fileData: FileData, file: FileInfo) {
|
||||
this.fileData = fileData;
|
||||
this.file = file;
|
||||
this.startTime = Date.now();
|
||||
}
|
||||
public updateFileData(fileData: FileData, status: TransferStatus = TransferStatus.SENDING) {
|
||||
this.fileData = fileData;
|
||||
this.status = status;
|
||||
this.updateProgress();
|
||||
}
|
||||
public updateProgress(): TransferProgress {
|
||||
if (this.status == TransferStatus.COMPLETED) {
|
||||
this.progress.updateTime = Date.now();
|
||||
return this.progress;
|
||||
}
|
||||
const totalSize = this.fileData.chunkData?.totalSize || this.fileData.size;
|
||||
const transferredSize = this.fileData.chunkData?.offset || this.fileData.size;
|
||||
const speed = this.fileData.chunkData ? (transferredSize + this.fileData.chunkData.buffer.byteLength) / (Date.now() - this.startTime) * 1000 : this.fileData.size / (Date.now() - this.startTime) * 1000;
|
||||
const percent = this.fileData.chunkData ? (transferredSize + this.fileData.chunkData.buffer.byteLength) / totalSize * 100 : transferredSize / totalSize * 100;
|
||||
this.status = transferredSize >= totalSize ? TransferStatus.COMPLETED : this.status;
|
||||
this.progress = {
|
||||
transferredSize: transferredSize,
|
||||
totalSize: totalSize,
|
||||
speed: speed == Infinity ? totalSize : speed,
|
||||
status: this.status,
|
||||
percent: percent,
|
||||
costTime: Date.now() - this.startTime,
|
||||
updateTime: Date.now()
|
||||
}
|
||||
return this.progress;
|
||||
}
|
||||
return this.progress;
|
||||
}
|
||||
}
|
||||
export const fileTransferMgrInstance = new FileTransferMgr()
|
||||
export const fileTransferMgrInstance = new FileTransferMgr();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
BIN
static/desktop.png
Normal file
BIN
static/desktop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 517 B |
BIN
static/phone.png
Normal file
BIN
static/phone.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 876 B |
@ -4,6 +4,7 @@ import vue from '@vitejs/plugin-vue'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
build: {
|
||||
// 开发阶段启用源码映射:https://uniapp.dcloud.net.cn/tutorial/migration-to-vue3.html#需主动开启-sourcemap
|
||||
sourcemap: process.env.NODE_ENV === 'development',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user