Compare commits

..

No commits in common. "19b9621dee34bbce9d0b84b9c5577bc35497e72e" and "b90c6952070ad38e87f9d4f77edf4fa73bf81c9d" have entirely different histories.

4 changed files with 95 additions and 338 deletions

View File

@ -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;

View File

@ -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 v-if="!stream" class="no-signal">
<a-empty description="等待远程桌面共享..." />
</div> </div>
</div> </div>
<div v-if="callStream" class="call-view">
<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>
</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;
isCollapsed.value = false; //
await nextTick();
if (videoRef.value) {
videoRef.value.srcObject = remoteStream; videoRef.value.srcObject = remoteStream;
} isCollapsed.value = false; //
} 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;
} }

View File

@ -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;

View File

@ -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) => {
this.endMediaConnection(peerId, mediaType);
if (this.remoteConnection) { if (this.remoteConnection) {
this.send( this.send(
{ {
type: type: MessageType.end_call,
mediaType === "desktop"
? MessageType.end_desktop
: MessageType.end_call,
data: { data: {
peerId, peerId,
}, },
}, },
this.remoteConnection, this.remoteConnection,
).catch((error) => { );
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;