Compare commits
No commits in common. "19b9621dee34bbce9d0b84b9c5577bc35497e72e" and "b90c6952070ad38e87f9d4f77edf4fa73bf81c9d" have entirely different histories.
19b9621dee
...
b90c695207
@ -21,21 +21,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="connect-section">
|
<div class="connect-section">
|
||||||
<div class="connect-actions">
|
<div v-if="isConnected" class="connect-section">
|
||||||
<div
|
<div class="connect-item" @click="requestDesktop">
|
||||||
class="connect-item"
|
|
||||||
:class="{ disabled: !isConnected, active: isDesktopActive }"
|
|
||||||
title="桌面预览"
|
|
||||||
@click="requestDesktop"
|
|
||||||
>
|
|
||||||
<img src="/static/desktop.png" alt="桌面" />
|
<img src="/static/desktop.png" alt="桌面" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="connect-item" @click="requestCall">
|
||||||
class="connect-item"
|
|
||||||
:class="{ disabled: !isConnected, active: isCallActive }"
|
|
||||||
title="语音通话"
|
|
||||||
@click="requestCall"
|
|
||||||
>
|
|
||||||
<img src="/static/phone.png" alt="通话" />
|
<img src="/static/phone.png" alt="通话" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -113,8 +103,6 @@ const receiveLoading = ref(false);
|
|||||||
const myId = ref("");
|
const myId = ref("");
|
||||||
const targetId = ref("");
|
const targetId = ref("");
|
||||||
const isConnected = ref(false);
|
const isConnected = ref(false);
|
||||||
const isDesktopActive = ref(false);
|
|
||||||
const isCallActive = ref(false);
|
|
||||||
// 文件系统相关
|
// 文件系统相关
|
||||||
const selectedLocalFiles = ref<string[]>([]);
|
const selectedLocalFiles = ref<string[]>([]);
|
||||||
const selectedRemoteFiles = ref<string[]>([]);
|
const selectedRemoteFiles = ref<string[]>([]);
|
||||||
@ -124,19 +112,9 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const requestDesktop = () => {
|
const requestDesktop = () => {
|
||||||
if (!isConnected.value) return;
|
|
||||||
if (isDesktopActive.value) {
|
|
||||||
peer.endMedia(sign2peerid(targetId.value), "desktop");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
peer.requestDesktop(targetId.value);
|
peer.requestDesktop(targetId.value);
|
||||||
};
|
};
|
||||||
const requestCall = () => {
|
const requestCall = () => {
|
||||||
if (!isConnected.value) return;
|
|
||||||
if (isCallActive.value) {
|
|
||||||
peer.endMedia(sign2peerid(targetId.value), "call");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
peer.requestCall(targetId.value);
|
peer.requestCall(targetId.value);
|
||||||
};
|
};
|
||||||
const shareUrl = async () => {
|
const shareUrl = async () => {
|
||||||
@ -255,8 +233,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
peer.on("peer-disconnected", ((event: CustomEvent) => {
|
peer.on("peer-disconnected", ((event: CustomEvent) => {
|
||||||
isConnected.value = peer.remoteConnection?.open;
|
isConnected.value = peer.remoteConnection?.open;
|
||||||
isDesktopActive.value = false;
|
|
||||||
isCallActive.value = false;
|
|
||||||
notification.error({
|
notification.error({
|
||||||
message: event.detail.peer + "连接已断开",
|
message: event.detail.peer + "连接已断开",
|
||||||
});
|
});
|
||||||
@ -268,36 +244,6 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
console.error("连接错误:", event.detail);
|
console.error("连接错误:", event.detail);
|
||||||
}) as EventListener);
|
}) as EventListener);
|
||||||
|
|
||||||
peer.on("stream", ((event: CustomEvent) => {
|
|
||||||
if (event.detail.peerId !== sign2peerid(targetId.value)) return;
|
|
||||||
if (event.detail.type === "desktop") {
|
|
||||||
isDesktopActive.value = true;
|
|
||||||
} else if (event.detail.type === "call") {
|
|
||||||
isCallActive.value = true;
|
|
||||||
}
|
|
||||||
}) as EventListener);
|
|
||||||
|
|
||||||
peer.on("desktop-started", ((event: CustomEvent) => {
|
|
||||||
if (event.detail.peerId === sign2peerid(targetId.value)) {
|
|
||||||
isDesktopActive.value = true;
|
|
||||||
}
|
|
||||||
}) as EventListener);
|
|
||||||
|
|
||||||
peer.on("call-started", ((event: CustomEvent) => {
|
|
||||||
if (event.detail.peerId === sign2peerid(targetId.value)) {
|
|
||||||
isCallActive.value = true;
|
|
||||||
}
|
|
||||||
}) as EventListener);
|
|
||||||
|
|
||||||
peer.on("media-ended", ((event: CustomEvent) => {
|
|
||||||
if (event.detail.peerId !== sign2peerid(targetId.value)) return;
|
|
||||||
if (event.detail.type === "desktop") {
|
|
||||||
isDesktopActive.value = false;
|
|
||||||
} else if (event.detail.type === "call") {
|
|
||||||
isCallActive.value = false;
|
|
||||||
}
|
|
||||||
}) as EventListener);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -338,31 +284,11 @@ onMounted(() => {
|
|||||||
.connect-item:hover {
|
.connect-item:hover {
|
||||||
background: #cae5f9;
|
background: #cae5f9;
|
||||||
}
|
}
|
||||||
.connect-item.active {
|
|
||||||
background: #b7eb8f;
|
|
||||||
border-color: #52c41a;
|
|
||||||
}
|
|
||||||
.connect-item.active:hover {
|
|
||||||
background: #95de64;
|
|
||||||
}
|
|
||||||
.connect-item.disabled {
|
|
||||||
background: #f0f0f0;
|
|
||||||
border-color: #d9d9d9;
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.55;
|
|
||||||
}
|
|
||||||
.connect-item.disabled:hover {
|
|
||||||
background: #f0f0f0;
|
|
||||||
}
|
|
||||||
.connect-item.disabled img {
|
|
||||||
filter: grayscale(1);
|
|
||||||
}
|
|
||||||
.connect-item img {
|
.connect-item img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connect-actions,
|
|
||||||
.connect-section {
|
.connect-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|||||||
@ -1,14 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="desktop-view-wrapper"
|
class="desktop-view-wrapper"
|
||||||
:class="{
|
:class="{ 'is-collapsed': isCollapsed, 'has-stream': !!stream }"
|
||||||
'is-collapsed': isCollapsed,
|
|
||||||
'has-stream': hasMedia,
|
|
||||||
'has-call-only': !!callStream && !desktopStream,
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<div class="collapse-header" v-if="hasMedia" @click="toggleCollapse">
|
<div class="collapse-header" v-if="stream" @click="toggleCollapse">
|
||||||
<span>{{ headerTitle }}{{ isCollapsed ? "(已收起)" : "" }}</span>
|
<span>远程桌面{{ isCollapsed ? "(已收起)" : "" }}</span>
|
||||||
<a-button type="link">
|
<a-button type="link">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<UpOutlined v-if="!isCollapsed" />
|
<UpOutlined v-if="!isCollapsed" />
|
||||||
@ -16,46 +12,32 @@
|
|||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="desktop-view" :class="{ 'is-fullscreen': isFullscreen }">
|
||||||
v-if="desktopStream"
|
|
||||||
class="desktop-view"
|
|
||||||
:class="{ 'is-fullscreen': isFullscreen }"
|
|
||||||
>
|
|
||||||
<div class="video-container" ref="videoContainer">
|
<div class="video-container" ref="videoContainer">
|
||||||
<video ref="videoRef" autoplay playsinline></video>
|
<video ref="videoRef" autoplay playsinline></video>
|
||||||
<div class="controls">
|
<div class="controls" v-if="stream">
|
||||||
<a-button
|
<a-button
|
||||||
type="primary"
|
type="primary"
|
||||||
ghost
|
ghost
|
||||||
|
:icon="isFullscreen ? 'fullscreen-exit' : 'fullscreen'"
|
||||||
@click="toggleFullscreen"
|
@click="toggleFullscreen"
|
||||||
>
|
>
|
||||||
{{ isFullscreen ? "退出全屏" : "全屏" }}
|
{{ isFullscreen ? "退出全屏" : "全屏" }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button danger ghost @click="endDesktop">结束桌面</a-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-if="!stream" class="no-signal">
|
||||||
</div>
|
<a-empty description="等待远程桌面共享..." />
|
||||||
<div v-if="callStream" class="call-view">
|
</div>
|
||||||
<audio ref="audioRef" autoplay playsinline></audio>
|
|
||||||
<div class="call-info">
|
|
||||||
<PhoneOutlined />
|
|
||||||
<span>语音通话中</span>
|
|
||||||
</div>
|
|
||||||
<div class="call-controls">
|
|
||||||
<a-button ghost @click="toggleCallMuted">
|
|
||||||
{{ isCallMuted ? "取消静音" : "静音" }}
|
|
||||||
</a-button>
|
|
||||||
<a-button danger ghost @click="endCall">结束通话</a-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
import { peer } from "../utils/peer";
|
import { peer } from "../utils/peer";
|
||||||
import { message } from "ant-design-vue";
|
import { message } from "ant-design-vue";
|
||||||
import { UpOutlined, DownOutlined, PhoneOutlined } from "@ant-design/icons-vue";
|
import { UpOutlined, DownOutlined } from "@ant-design/icons-vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
peerId: string;
|
peerId: string;
|
||||||
@ -77,20 +59,10 @@ interface MediaEndedEvent extends CustomEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const videoRef = ref<HTMLVideoElement | null>(null);
|
const videoRef = ref<HTMLVideoElement | null>(null);
|
||||||
const audioRef = ref<HTMLAudioElement | null>(null);
|
|
||||||
const videoContainer = ref<HTMLElement | null>(null);
|
const videoContainer = ref<HTMLElement | null>(null);
|
||||||
const desktopStream = ref<MediaStream | null>(null);
|
const stream = ref<MediaStream | null>(null);
|
||||||
const callStream = ref<MediaStream | null>(null);
|
|
||||||
const isFullscreen = ref(false);
|
const isFullscreen = ref(false);
|
||||||
const isCollapsed = ref(false);
|
const isCollapsed = ref(false);
|
||||||
const isCallMuted = ref(false);
|
|
||||||
|
|
||||||
const hasMedia = computed(() => !!desktopStream.value || !!callStream.value);
|
|
||||||
const headerTitle = computed(() => {
|
|
||||||
if (desktopStream.value && callStream.value) return "远程桌面与通话";
|
|
||||||
if (desktopStream.value) return "远程桌面";
|
|
||||||
return "语音通话";
|
|
||||||
});
|
|
||||||
|
|
||||||
// 切换收起/展开状态
|
// 切换收起/展开状态
|
||||||
const toggleCollapse = () => {
|
const toggleCollapse = () => {
|
||||||
@ -98,27 +70,13 @@ const toggleCollapse = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 处理远程流
|
// 处理远程流
|
||||||
const handleStream = async (event: StreamEvent) => {
|
const handleStream = (event: StreamEvent) => {
|
||||||
const { stream: remoteStream, type } = event.detail;
|
const { stream: remoteStream, type } = event.detail;
|
||||||
if (props.peerId !== event.detail.peerId) return;
|
if (props.peerId !== event.detail.peerId) return;
|
||||||
if (type === "desktop") {
|
if (type === "desktop" && videoRef.value) {
|
||||||
desktopStream.value = remoteStream;
|
stream.value = remoteStream;
|
||||||
|
videoRef.value.srcObject = remoteStream;
|
||||||
isCollapsed.value = false; // 收到流时自动展开
|
isCollapsed.value = false; // 收到流时自动展开
|
||||||
await nextTick();
|
|
||||||
if (videoRef.value) {
|
|
||||||
videoRef.value.srcObject = remoteStream;
|
|
||||||
}
|
|
||||||
} else if (type === "call") {
|
|
||||||
callStream.value = remoteStream;
|
|
||||||
isCollapsed.value = false;
|
|
||||||
await nextTick();
|
|
||||||
if (audioRef.value) {
|
|
||||||
audioRef.value.srcObject = remoteStream;
|
|
||||||
audioRef.value.muted = isCallMuted.value;
|
|
||||||
audioRef.value.play().catch(() => {
|
|
||||||
message.warning("浏览器阻止了自动播放,请点击通话区域恢复声音");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -127,32 +85,11 @@ const handleMediaEnded = (event: MediaEndedEvent) => {
|
|||||||
const { type } = event.detail;
|
const { type } = event.detail;
|
||||||
if (props.peerId !== event.detail.peerId) return;
|
if (props.peerId !== event.detail.peerId) return;
|
||||||
if (type === "desktop") {
|
if (type === "desktop") {
|
||||||
desktopStream.value = null;
|
stream.value = null;
|
||||||
if (videoRef.value) {
|
if (videoRef.value) {
|
||||||
videoRef.value.srcObject = null;
|
videoRef.value.srcObject = null;
|
||||||
}
|
}
|
||||||
message.info("远程桌面共享已结束");
|
message.info("远程桌面共享已结束");
|
||||||
} else if (type === "call") {
|
|
||||||
callStream.value = null;
|
|
||||||
if (audioRef.value) {
|
|
||||||
audioRef.value.srcObject = null;
|
|
||||||
}
|
|
||||||
message.info("语音通话已结束");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const endDesktop = () => {
|
|
||||||
peer.endMedia(props.peerId, "desktop");
|
|
||||||
};
|
|
||||||
|
|
||||||
const endCall = () => {
|
|
||||||
peer.endMedia(props.peerId, "call");
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleCallMuted = () => {
|
|
||||||
isCallMuted.value = !isCallMuted.value;
|
|
||||||
if (audioRef.value) {
|
|
||||||
audioRef.value.muted = isCallMuted.value;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -180,27 +117,21 @@ const handleFullscreenChange = () => {
|
|||||||
isFullscreen.value = !!document.fullscreenElement;
|
isFullscreen.value = !!document.fullscreenElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const streamListener: EventListener = (event) => {
|
|
||||||
handleStream(event as StreamEvent);
|
|
||||||
};
|
|
||||||
const mediaEndedListener: EventListener = (event) => {
|
|
||||||
handleMediaEnded(event as MediaEndedEvent);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
peer.on("stream", streamListener);
|
peer.on("stream", handleStream as EventListener);
|
||||||
peer.on("media-ended", mediaEndedListener);
|
peer.on("media-ended", handleMediaEnded as EventListener);
|
||||||
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
peer.off("stream", streamListener);
|
peer.off("stream", handleStream as EventListener);
|
||||||
peer.off("media-ended", mediaEndedListener);
|
peer.off("media-ended", handleMediaEnded as EventListener);
|
||||||
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
||||||
|
|
||||||
// 清理视频流
|
// 清理视频流
|
||||||
desktopStream.value?.getTracks().forEach((track) => track.stop());
|
if (stream.value) {
|
||||||
callStream.value?.getTracks().forEach((track) => track.stop());
|
stream.value.getTracks().forEach((track) => track.stop());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -221,10 +152,6 @@ onUnmounted(() => {
|
|||||||
height: 300px;
|
height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.desktop-view-wrapper.has-call-only {
|
|
||||||
height: 104px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desktop-view-wrapper.is-collapsed {
|
.desktop-view-wrapper.is-collapsed {
|
||||||
height: 40px !important;
|
height: 40px !important;
|
||||||
}
|
}
|
||||||
@ -272,14 +199,20 @@ video {
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-container:hover .controls {
|
.video-container:hover .controls {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-signal {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.is-fullscreen {
|
.is-fullscreen {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -289,24 +222,6 @@ video {
|
|||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-view {
|
|
||||||
height: 64px;
|
|
||||||
padding: 0 16px;
|
|
||||||
background: #111;
|
|
||||||
color: #fff;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-top: 1px solid #2a2a2a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.call-info,
|
|
||||||
.call-controls {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.ant-empty-description) {
|
:deep(.ant-empty-description) {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -80,9 +80,6 @@ export class FileTransfer {
|
|||||||
this.pausePromise = undefined;
|
this.pausePromise = undefined;
|
||||||
this.pauseResolve = undefined;
|
this.pauseResolve = undefined;
|
||||||
this.aborted = false;
|
this.aborted = false;
|
||||||
this.closeReceiveWritable().catch((error) => {
|
|
||||||
console.error("close receive writable error", error);
|
|
||||||
});
|
|
||||||
this.receiveQueue = Promise.resolve();
|
this.receiveQueue = Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,8 +119,6 @@ export class FileTransfer {
|
|||||||
public totalSize: number = 0;
|
public totalSize: number = 0;
|
||||||
private fileBuffer: ArrayBuffer | null = null;
|
private fileBuffer: ArrayBuffer | null = null;
|
||||||
private receiveQueue: Promise<void> = Promise.resolve();
|
private receiveQueue: Promise<void> = Promise.resolve();
|
||||||
private receiveWritable: FileSystemWritableFileStream | null = null;
|
|
||||||
private receiveWritablePath: string = "";
|
|
||||||
private async waitForWritable(targetBufferedAmount = MAX_BUFFERED_AMOUNT) {
|
private async waitForWritable(targetBufferedAmount = MAX_BUFFERED_AMOUNT) {
|
||||||
const dc = this.conn.dataChannel;
|
const dc = this.conn.dataChannel;
|
||||||
if (!dc) {
|
if (!dc) {
|
||||||
@ -144,36 +139,6 @@ export class FileTransfer {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private async getReceiveWritable(path: string) {
|
|
||||||
if (this.receiveWritable && this.receiveWritablePath === path) {
|
|
||||||
return this.receiveWritable;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.receiveWritable) {
|
|
||||||
await this.receiveWritable.close();
|
|
||||||
this.receiveWritable = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dir = this.file.fileDirHandler as FileSystemDirectoryHandle;
|
|
||||||
let name = path;
|
|
||||||
if (name.split("/").length > 1) {
|
|
||||||
const dirPath = name.split("/").slice(0, -1).join("/");
|
|
||||||
name = name.split("/").slice(-1).join("");
|
|
||||||
dir = await this.file.createPath(dirPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileHandle = await dir.getFileHandle(name, { create: true });
|
|
||||||
this.receiveWritable = await fileHandle.createWritable();
|
|
||||||
this.receiveWritablePath = path;
|
|
||||||
return this.receiveWritable;
|
|
||||||
}
|
|
||||||
private async closeReceiveWritable() {
|
|
||||||
if (!this.receiveWritable) return;
|
|
||||||
const writable = this.receiveWritable;
|
|
||||||
this.receiveWritable = null;
|
|
||||||
this.receiveWritablePath = "";
|
|
||||||
await writable.close();
|
|
||||||
}
|
|
||||||
// 发送文件 (流水线模式 - 受限深度,防止撑爆SCTP缓冲区)
|
// 发送文件 (流水线模式 - 受限深度,防止撑爆SCTP缓冲区)
|
||||||
public async sendFile(savePath: string = ""): Promise<boolean> {
|
public async sendFile(savePath: string = ""): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
@ -217,6 +182,7 @@ export class FileTransfer {
|
|||||||
data: this.fileData,
|
data: this.fileData,
|
||||||
},
|
},
|
||||||
this.conn,
|
this.conn,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.offset = end;
|
this.offset = end;
|
||||||
@ -295,15 +261,11 @@ export class FileTransfer {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const path = fData.savePath + "/" + fData.name;
|
const path = fData.savePath + "/" + fData.name;
|
||||||
const writable = await this.getReceiveWritable(path);
|
await this.file.createFile(
|
||||||
await writable.write({
|
path,
|
||||||
type: "write",
|
fData.chunkData.buffer,
|
||||||
position: fData.chunkData.offset,
|
fData.chunkData.offset,
|
||||||
data: fData.chunkData.buffer,
|
);
|
||||||
});
|
|
||||||
if (this.status === TransferStatus.COMPLETED) {
|
|
||||||
await this.closeReceiveWritable();
|
|
||||||
}
|
|
||||||
// if (this.status == TransferStatus.COMPLETED) {
|
// if (this.status == TransferStatus.COMPLETED) {
|
||||||
// await this.file.renameFile(path, fData.savePath + '/' + fData.name);
|
// await this.file.renameFile(path, fData.savePath + '/' + fData.name);
|
||||||
// }
|
// }
|
||||||
@ -342,9 +304,6 @@ export class FileTransfer {
|
|||||||
// 取消传输
|
// 取消传输
|
||||||
public abort() {
|
public abort() {
|
||||||
this.aborted = true;
|
this.aborted = true;
|
||||||
this.closeReceiveWritable().catch((error) => {
|
|
||||||
console.error("close receive writable error", error);
|
|
||||||
});
|
|
||||||
this.resume(); // 恢复暂停的传输以便能够正确退出
|
this.resume(); // 恢复暂停的传输以便能够正确退出
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -441,16 +400,22 @@ export class TransferTask {
|
|||||||
this.updateProgress();
|
this.updateProgress();
|
||||||
}
|
}
|
||||||
public updateProgress(): TransferProgress {
|
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 totalSize = this.fileData.chunkData?.totalSize || this.fileData.size;
|
||||||
const transferredSize =
|
const transferredSize =
|
||||||
this.fileData.chunkData
|
this.fileData.chunkData?.offset || this.fileData.size;
|
||||||
? this.fileData.chunkData.offset + this.fileData.chunkData.buffer.byteLength
|
|
||||||
: this.fileData.size;
|
|
||||||
const speed = this.fileData.chunkData
|
const speed = this.fileData.chunkData
|
||||||
? (transferredSize / (Date.now() - this.startTime)) * 1000
|
? ((transferredSize + this.fileData.chunkData.buffer.byteLength) /
|
||||||
|
(Date.now() - this.startTime)) *
|
||||||
|
1000
|
||||||
: (this.fileData.size / (Date.now() - this.startTime)) * 1000;
|
: (this.fileData.size / (Date.now() - this.startTime)) * 1000;
|
||||||
const percent = this.fileData.chunkData
|
const percent = this.fileData.chunkData
|
||||||
? (transferredSize / totalSize) * 100
|
? ((transferredSize + this.fileData.chunkData.buffer.byteLength) /
|
||||||
|
totalSize) *
|
||||||
|
100
|
||||||
: (transferredSize / totalSize) * 100;
|
: (transferredSize / totalSize) * 100;
|
||||||
this.status =
|
this.status =
|
||||||
transferredSize >= totalSize ? TransferStatus.COMPLETED : this.status;
|
transferredSize >= totalSize ? TransferStatus.COMPLETED : this.status;
|
||||||
|
|||||||
@ -15,9 +15,8 @@ import {
|
|||||||
confirmWin,
|
confirmWin,
|
||||||
} from "./common";
|
} from "./common";
|
||||||
|
|
||||||
type MediaType = "desktop" | "call";
|
|
||||||
// 发送超时时间(毫秒)
|
// 发送超时时间(毫秒)
|
||||||
const SEND_TIMEOUT = 30000;
|
const SEND_TIMEOUT = 10000;
|
||||||
|
|
||||||
class Peer extends EventTarget {
|
class Peer extends EventTarget {
|
||||||
peer: PeerJs;
|
peer: PeerJs;
|
||||||
@ -34,9 +33,8 @@ class Peer extends EventTarget {
|
|||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
connection: MediaConnection;
|
connection: MediaConnection;
|
||||||
remoteStream: MediaStream | null;
|
stream: MediaStream;
|
||||||
localStream: MediaStream | null;
|
type: "desktop" | "call";
|
||||||
type: MediaType;
|
|
||||||
}
|
}
|
||||||
> = new Map();
|
> = new Map();
|
||||||
constructor(sign: string) {
|
constructor(sign: string) {
|
||||||
@ -104,7 +102,7 @@ class Peer extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
call.answer(stream);
|
call.answer(stream);
|
||||||
self.setupMediaConnection(call, type, stream);
|
self.setupMediaConnection(call, type);
|
||||||
|
|
||||||
self.dispatchEvent(
|
self.dispatchEvent(
|
||||||
new CustomEvent(
|
new CustomEvent(
|
||||||
@ -210,7 +208,7 @@ class Peer extends EventTarget {
|
|||||||
metadata: { type: "desktop" },
|
metadata: { type: "desktop" },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setupMediaConnection(call, "desktop", emptyStream);
|
this.setupMediaConnection(call, "desktop");
|
||||||
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("desktop-started", {
|
new CustomEvent("desktop-started", {
|
||||||
@ -245,7 +243,7 @@ class Peer extends EventTarget {
|
|||||||
metadata: { type: "call" },
|
metadata: { type: "call" },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setupMediaConnection(call, "call", stream);
|
this.setupMediaConnection(call, "call");
|
||||||
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("call-started", {
|
new CustomEvent("call-started", {
|
||||||
@ -263,73 +261,26 @@ class Peer extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 结束媒体连接
|
// 结束媒体连接
|
||||||
endMedia(peerId?: string, type?: MediaType) {
|
endMedia(peerId?: string) {
|
||||||
if (peerId) {
|
if (peerId) {
|
||||||
const mediaTypes: MediaType[] = type ? [type] : ["desktop", "call"];
|
this.endMediaConnection(peerId);
|
||||||
mediaTypes.forEach((mediaType) => {
|
if (this.remoteConnection) {
|
||||||
this.endMediaConnection(peerId, mediaType);
|
this.send(
|
||||||
if (this.remoteConnection) {
|
{
|
||||||
this.send(
|
type: MessageType.end_call,
|
||||||
{
|
data: {
|
||||||
type:
|
peerId,
|
||||||
mediaType === "desktop"
|
|
||||||
? MessageType.end_desktop
|
|
||||||
: MessageType.end_call,
|
|
||||||
data: {
|
|
||||||
peerId,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
this.remoteConnection,
|
},
|
||||||
).catch((error) => {
|
this.remoteConnection,
|
||||||
console.warn("send end media message error:", error);
|
);
|
||||||
});
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// 如果没有指定 peerId,结束所有连接
|
// 如果没有指定 peerId,结束所有连接
|
||||||
this.endAllMediaConnections();
|
this.endAllMediaConnections();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMediaKey(peerId: string, type: MediaType) {
|
|
||||||
return `${peerId}:${type}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getMediaConnection(peerId: string, type: MediaType) {
|
|
||||||
return this.mediaConnections.get(this.getMediaKey(peerId, type));
|
|
||||||
}
|
|
||||||
|
|
||||||
hasMedia(peerId: string, type: MediaType) {
|
|
||||||
return !!this.getMediaConnection(peerId, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateMediaConnection(
|
|
||||||
peerId: string,
|
|
||||||
type: MediaType,
|
|
||||||
data: Partial<{
|
|
||||||
connection: MediaConnection;
|
|
||||||
remoteStream: MediaStream | null;
|
|
||||||
localStream: MediaStream | null;
|
|
||||||
}>,
|
|
||||||
) {
|
|
||||||
const key = this.getMediaKey(peerId, type);
|
|
||||||
const current = this.mediaConnections.get(key);
|
|
||||||
const connection = data.connection || current?.connection;
|
|
||||||
if (!connection) return;
|
|
||||||
this.mediaConnections.set(key, {
|
|
||||||
connection,
|
|
||||||
remoteStream:
|
|
||||||
data.remoteStream !== undefined
|
|
||||||
? data.remoteStream
|
|
||||||
: current?.remoteStream || null,
|
|
||||||
localStream:
|
|
||||||
data.localStream !== undefined
|
|
||||||
? data.localStream
|
|
||||||
: current?.localStream || null,
|
|
||||||
type,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private callbackMap: Map<string, (data: any) => void> = new Map();
|
private callbackMap: Map<string, (data: any) => void> = new Map();
|
||||||
async send(
|
async send(
|
||||||
data: Message,
|
data: Message,
|
||||||
@ -433,14 +384,9 @@ class Peer extends EventTarget {
|
|||||||
|
|
||||||
private setupMediaConnection(
|
private setupMediaConnection(
|
||||||
call: MediaConnection,
|
call: MediaConnection,
|
||||||
type: MediaType,
|
type: "desktop" | "call",
|
||||||
localStream: MediaStream | null = null,
|
|
||||||
) {
|
) {
|
||||||
const peerId = call.peer;
|
const peerId = call.peer;
|
||||||
this.updateMediaConnection(peerId, type, {
|
|
||||||
connection: call,
|
|
||||||
localStream,
|
|
||||||
});
|
|
||||||
|
|
||||||
call.on("stream", (remoteStream: MediaStream) => {
|
call.on("stream", (remoteStream: MediaStream) => {
|
||||||
console.log("stream", remoteStream);
|
console.log("stream", remoteStream);
|
||||||
@ -449,9 +395,10 @@ class Peer extends EventTarget {
|
|||||||
const hasAudio = remoteStream.getAudioTracks().length > 0;
|
const hasAudio = remoteStream.getAudioTracks().length > 0;
|
||||||
|
|
||||||
if ((type === "desktop" && hasVideo) || (type === "call" && hasAudio)) {
|
if ((type === "desktop" && hasVideo) || (type === "call" && hasAudio)) {
|
||||||
this.updateMediaConnection(peerId, type, {
|
this.mediaConnections.set(peerId, {
|
||||||
connection: call,
|
connection: call,
|
||||||
remoteStream,
|
stream: remoteStream,
|
||||||
|
type,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
@ -467,18 +414,16 @@ class Peer extends EventTarget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
call.on("close", () => {
|
call.on("close", () => {
|
||||||
this.endMediaConnection(peerId, type);
|
this.endMediaConnection(peerId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private endMediaConnection(peerId: string, type: MediaType) {
|
private endMediaConnection(peerId: string) {
|
||||||
const key = this.getMediaKey(peerId, type);
|
const mediaConn = this.mediaConnections.get(peerId);
|
||||||
const mediaConn = this.mediaConnections.get(key);
|
|
||||||
if (mediaConn) {
|
if (mediaConn) {
|
||||||
this.mediaConnections.delete(key);
|
|
||||||
mediaConn.connection.close();
|
mediaConn.connection.close();
|
||||||
mediaConn.remoteStream?.getTracks().forEach((track) => track.stop());
|
mediaConn.stream.getTracks().forEach((track) => track.stop());
|
||||||
mediaConn.localStream?.getTracks().forEach((track) => track.stop());
|
this.mediaConnections.delete(peerId);
|
||||||
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("media-ended", {
|
new CustomEvent("media-ended", {
|
||||||
@ -493,8 +438,8 @@ class Peer extends EventTarget {
|
|||||||
|
|
||||||
// 结束所有媒体连接
|
// 结束所有媒体连接
|
||||||
endAllMediaConnections() {
|
endAllMediaConnections() {
|
||||||
for (const mediaConn of Array.from(this.mediaConnections.values())) {
|
for (const peerId of this.mediaConnections.keys()) {
|
||||||
this.endMediaConnection(mediaConn.connection.peer, mediaConn.type);
|
this.endMediaConnection(peerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -655,16 +600,23 @@ class Peer extends EventTarget {
|
|||||||
case MessageType.push_file_chunk:
|
case MessageType.push_file_chunk:
|
||||||
resData.type = MessageType.response_push_file_chunk;
|
resData.type = MessageType.response_push_file_chunk;
|
||||||
resData.data = "ok";
|
resData.data = "ok";
|
||||||
|
// 先回复确认,再异步处理文件I/O,避免阻塞消息循环
|
||||||
let fData = remoteD as FileData;
|
let fData = remoteD as FileData;
|
||||||
if (fData.preView) {
|
if (fData.preView) {
|
||||||
await fileMgrInstance.remoteRootFile
|
fileMgrInstance.remoteRootFile
|
||||||
.getFileInfo(fData.path)
|
.getFileInfo(fData.path)
|
||||||
.getTransfer(conn)
|
.getTransfer(conn)
|
||||||
.receiveFile(fData, true);
|
.receiveFile(fData, true)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("receiveFile preview error", err);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
await (await fileMgrInstance.getRootFile())
|
(await fileMgrInstance.getRootFile())
|
||||||
.getTransfer(conn)
|
.getTransfer(conn)
|
||||||
.receiveFile(fData);
|
.receiveFile(fData)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("receiveFile error", err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case MessageType.push_file_complete:
|
case MessageType.push_file_complete:
|
||||||
@ -684,10 +636,9 @@ class Peer extends EventTarget {
|
|||||||
break;
|
break;
|
||||||
case MessageType.end_call:
|
case MessageType.end_call:
|
||||||
case MessageType.end_desktop:
|
case MessageType.end_desktop:
|
||||||
this.endMediaConnection(
|
if (remoteD?.peerId) {
|
||||||
conn.peer,
|
this.endMediaConnection(remoteD.peerId);
|
||||||
data.type === MessageType.end_desktop ? "desktop" : "call",
|
}
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
resData.type = MessageType.error;
|
resData.type = MessageType.error;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user