diff --git a/src/pages/file/index.vue b/src/pages/file/index.vue index 9a117e5..7ba0e38 100644 --- a/src/pages/file/index.vue +++ b/src/pages/file/index.vue @@ -21,11 +21,21 @@
-
-
+
+
桌面
-
+
通话
@@ -103,6 +113,8 @@ const receiveLoading = ref(false); const myId = ref(""); const targetId = ref(""); const isConnected = ref(false); +const isDesktopActive = ref(false); +const isCallActive = ref(false); // 文件系统相关 const selectedLocalFiles = ref([]); const selectedRemoteFiles = ref([]); @@ -112,9 +124,19 @@ onMounted(() => { }); const requestDesktop = () => { + if (!isConnected.value) return; + if (isDesktopActive.value) { + peer.endMedia(sign2peerid(targetId.value), "desktop"); + return; + } peer.requestDesktop(targetId.value); }; const requestCall = () => { + if (!isConnected.value) return; + if (isCallActive.value) { + peer.endMedia(sign2peerid(targetId.value), "call"); + return; + } peer.requestCall(targetId.value); }; const shareUrl = async () => { @@ -233,6 +255,8 @@ onMounted(() => { peer.on("peer-disconnected", ((event: CustomEvent) => { isConnected.value = peer.remoteConnection?.open; + isDesktopActive.value = false; + isCallActive.value = false; notification.error({ message: event.detail.peer + "连接已断开", }); @@ -244,6 +268,36 @@ onMounted(() => { }); console.error("连接错误:", event.detail); }) 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); }); @@ -284,11 +338,31 @@ onMounted(() => { .connect-item:hover { 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 { width: 100%; height: 100%; } +.connect-actions, .connect-section { display: flex; gap: 10px; diff --git a/src/pages/file/item/desptopView.vue b/src/pages/file/item/desptopView.vue index a271288..c402737 100644 --- a/src/pages/file/item/desptopView.vue +++ b/src/pages/file/item/desptopView.vue @@ -1,10 +1,14 @@ @@ -152,6 +221,10 @@ onUnmounted(() => { height: 300px; } +.desktop-view-wrapper.has-call-only { + height: 104px; +} + .desktop-view-wrapper.is-collapsed { height: 40px !important; } @@ -199,20 +272,14 @@ video { z-index: 10; opacity: 0; transition: opacity 0.3s; + display: flex; + gap: 10px; } .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; @@ -222,6 +289,24 @@ video { 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) { color: #fff; } diff --git a/src/pages/file/utils/peer.ts b/src/pages/file/utils/peer.ts index 726e32e..446780d 100644 --- a/src/pages/file/utils/peer.ts +++ b/src/pages/file/utils/peer.ts @@ -15,6 +15,7 @@ import { confirmWin, } from "./common"; +type MediaType = "desktop" | "call"; // 发送超时时间(毫秒) const SEND_TIMEOUT = 30000; @@ -33,8 +34,9 @@ class Peer extends EventTarget { string, { connection: MediaConnection; - stream: MediaStream; - type: "desktop" | "call"; + remoteStream: MediaStream | null; + localStream: MediaStream | null; + type: MediaType; } > = new Map(); constructor(sign: string) { @@ -102,7 +104,7 @@ class Peer extends EventTarget { } call.answer(stream); - self.setupMediaConnection(call, type); + self.setupMediaConnection(call, type, stream); self.dispatchEvent( new CustomEvent( @@ -208,7 +210,7 @@ class Peer extends EventTarget { metadata: { type: "desktop" }, }); - this.setupMediaConnection(call, "desktop"); + this.setupMediaConnection(call, "desktop", emptyStream); this.dispatchEvent( new CustomEvent("desktop-started", { @@ -243,7 +245,7 @@ class Peer extends EventTarget { metadata: { type: "call" }, }); - this.setupMediaConnection(call, "call"); + this.setupMediaConnection(call, "call", stream); this.dispatchEvent( new CustomEvent("call-started", { @@ -261,26 +263,73 @@ class Peer extends EventTarget { } // 结束媒体连接 - endMedia(peerId?: string) { + endMedia(peerId?: string, type?: MediaType) { if (peerId) { - this.endMediaConnection(peerId); - if (this.remoteConnection) { - this.send( - { - type: MessageType.end_call, - data: { - peerId, + const mediaTypes: MediaType[] = type ? [type] : ["desktop", "call"]; + mediaTypes.forEach((mediaType) => { + this.endMediaConnection(peerId, mediaType); + if (this.remoteConnection) { + this.send( + { + type: + mediaType === "desktop" + ? MessageType.end_desktop + : MessageType.end_call, + data: { + peerId, + }, }, - }, - this.remoteConnection, - ); - } + this.remoteConnection, + ).catch((error) => { + console.warn("send end media message error:", error); + }); + } + }); } else { // 如果没有指定 peerId,结束所有连接 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 void> = new Map(); async send( data: Message, @@ -384,9 +433,14 @@ class Peer extends EventTarget { private setupMediaConnection( call: MediaConnection, - type: "desktop" | "call", + type: MediaType, + localStream: MediaStream | null = null, ) { const peerId = call.peer; + this.updateMediaConnection(peerId, type, { + connection: call, + localStream, + }); call.on("stream", (remoteStream: MediaStream) => { console.log("stream", remoteStream); @@ -395,10 +449,9 @@ class Peer extends EventTarget { const hasAudio = remoteStream.getAudioTracks().length > 0; if ((type === "desktop" && hasVideo) || (type === "call" && hasAudio)) { - this.mediaConnections.set(peerId, { + this.updateMediaConnection(peerId, type, { connection: call, - stream: remoteStream, - type, + remoteStream, }); this.dispatchEvent( @@ -414,16 +467,18 @@ class Peer extends EventTarget { }); call.on("close", () => { - this.endMediaConnection(peerId); + this.endMediaConnection(peerId, type); }); } - private endMediaConnection(peerId: string) { - const mediaConn = this.mediaConnections.get(peerId); + private endMediaConnection(peerId: string, type: MediaType) { + const key = this.getMediaKey(peerId, type); + const mediaConn = this.mediaConnections.get(key); if (mediaConn) { + this.mediaConnections.delete(key); mediaConn.connection.close(); - mediaConn.stream.getTracks().forEach((track) => track.stop()); - this.mediaConnections.delete(peerId); + mediaConn.remoteStream?.getTracks().forEach((track) => track.stop()); + mediaConn.localStream?.getTracks().forEach((track) => track.stop()); this.dispatchEvent( new CustomEvent("media-ended", { @@ -438,8 +493,8 @@ class Peer extends EventTarget { // 结束所有媒体连接 endAllMediaConnections() { - for (const peerId of this.mediaConnections.keys()) { - this.endMediaConnection(peerId); + for (const mediaConn of Array.from(this.mediaConnections.values())) { + this.endMediaConnection(mediaConn.connection.peer, mediaConn.type); } } @@ -629,9 +684,10 @@ class Peer extends EventTarget { break; case MessageType.end_call: case MessageType.end_desktop: - if (remoteD?.peerId) { - this.endMediaConnection(remoteD.peerId); - } + this.endMediaConnection( + conn.peer, + data.type === MessageType.end_desktop ? "desktop" : "call", + ); break; default: resData.type = MessageType.error;