From e98cd2844d4284260306fb658422a84e9b4b90c2 Mon Sep 17 00:00:00 2001 From: kura Date: Thu, 7 May 2026 22:02:25 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=9F=B3=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E7=9A=84=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/file/item/desptopView.vue | 781 ++++++++++++++++++++---- src/pages/file/item/fileTranserView.vue | 4 +- src/pages/file/utils/peer.ts | 33 +- 3 files changed, 702 insertions(+), 116 deletions(-) diff --git a/src/pages/file/item/desptopView.vue b/src/pages/file/item/desptopView.vue index 752c14f..c02ba82 100644 --- a/src/pages/file/item/desptopView.vue +++ b/src/pages/file/item/desptopView.vue @@ -1,8 +1,13 @@ @@ -318,21 +483,48 @@ interface MediaEndedEvent extends CustomEvent { interface MediaStartedEvent extends CustomEvent { detail: { peerId: string; + role?: "viewer" | "sharer" | "caller"; }; } +interface MediaRequestEvent extends CustomEvent { + detail: { + peerId: string; + type: "desktop" | "call"; + accept: () => Promise | void; + reject: () => void; + }; +} + +interface IncomingCallRequest { + peerId: string; + accept: () => Promise | void; + reject: () => void; +} + const videoRef = ref(null); const audioRef = ref(null); const videoContainer = ref(null); +const activeMediaPeerId = ref(""); +const incomingCallRequest = ref(null); const desktopStream = ref(null); const callStream = ref(null); const isDesktopActive = ref(false); +const isDesktopLocalSharing = ref(false); const isCallActive = ref(false); const isFullscreen = ref(false); const isCallMuted = ref(false); +const isDesktopCollapsed = ref(false); +const isDesktopShareCollapsed = ref(false); +const isCallCollapsed = ref(false); +const isIncomingCallHandling = ref(false); const callDuration = ref(0); let durationTimer: number | null = null; +const isRemoteDesktopActive = computed( + () => isDesktopActive.value && !isDesktopLocalSharing.value, +); + const callDurationStr = computed(() => { const mins = Math.floor(callDuration.value / 60); const secs = callDuration.value % 60; @@ -340,6 +532,7 @@ const callDurationStr = computed(() => { }); const startDurationTimer = () => { + if (durationTimer !== null) return; callDuration.value = 0; durationTimer = window.setInterval(() => { callDuration.value++; @@ -354,20 +547,95 @@ const stopDurationTimer = () => { callDuration.value = 0; }; +const isCurrentPeer = (peerId: string) => { + if (peerId === props.peerId || peerId === activeMediaPeerId.value) { + activeMediaPeerId.value = peerId; + return true; + } + if (!activeMediaPeerId.value) { + activeMediaPeerId.value = peerId; + return true; + } + return false; +}; + +const getMediaPeerId = () => activeMediaPeerId.value || props.peerId; + +const resetPeerWhenIdle = () => { + if ( + !isDesktopActive.value && + !isDesktopLocalSharing.value && + !isCallActive.value + ) { + activeMediaPeerId.value = ""; + } +}; + +const bindDesktopVideo = async () => { + await nextTick(); + if (!videoRef.value || !desktopStream.value) return; + if (videoRef.value.srcObject !== desktopStream.value) { + videoRef.value.srcObject = desktopStream.value; + } + videoRef.value.muted = true; + videoRef.value.play().catch(() => { + message.warning("浏览器阻止了自动播放,请点击桌面画面恢复播放"); + }); +}; + +const toggleDesktopCollapse = async () => { + isDesktopCollapsed.value = !isDesktopCollapsed.value; + if (!isDesktopCollapsed.value) { + await bindDesktopVideo(); + } +}; + +const toggleDesktopShareCollapse = () => { + isDesktopShareCollapsed.value = !isDesktopShareCollapsed.value; +}; + +const toggleCallCollapse = () => { + isCallCollapsed.value = !isCallCollapsed.value; +}; + +const handleMediaRequest = (event: MediaRequestEvent) => { + const { peerId, type, accept, reject } = event.detail; + if (type !== "call" || !isCurrentPeer(peerId)) return; + incomingCallRequest.value = { + peerId, + accept, + reject, + }; + isCallCollapsed.value = false; +}; + +const acceptIncomingCall = async () => { + if (!incomingCallRequest.value || isIncomingCallHandling.value) return; + isIncomingCallHandling.value = true; + try { + await incomingCallRequest.value.accept(); + incomingCallRequest.value = null; + } finally { + isIncomingCallHandling.value = false; + } +}; + +const rejectIncomingCall = () => { + if (!incomingCallRequest.value || isIncomingCallHandling.value) return; + incomingCallRequest.value.reject(); + incomingCallRequest.value = null; + resetPeerWhenIdle(); +}; + const handleStream = async (event: StreamEvent) => { const { stream: remoteStream, type } = event.detail; - if (props.peerId !== event.detail.peerId) return; + if (!isCurrentPeer(event.detail.peerId)) return; if (type === "desktop") { isDesktopActive.value = true; + isDesktopLocalSharing.value = false; desktopStream.value = remoteStream; - await nextTick(); - if (videoRef.value) { - videoRef.value.srcObject = remoteStream; - videoRef.value.muted = true; - videoRef.value.play().catch(() => { - message.warning("浏览器阻止了自动播放,请点击桌面画面恢复播放"); - }); - } + isDesktopCollapsed.value = false; + await bindDesktopVideo(); } else if (type === "call") { isCallActive.value = true; callStream.value = remoteStream; @@ -380,22 +648,35 @@ const handleStream = async (event: StreamEvent) => { }; const handleDesktopStarted = (event: MediaStartedEvent) => { - if (props.peerId !== event.detail.peerId) return; + if (!isCurrentPeer(event.detail.peerId)) return; + if (event.detail.role === "sharer") { + isDesktopActive.value = true; + isDesktopLocalSharing.value = true; + isDesktopShareCollapsed.value = false; + return; + } isDesktopActive.value = true; + isDesktopLocalSharing.value = false; + isDesktopCollapsed.value = false; }; const handleCallStarted = (event: MediaStartedEvent) => { - if (props.peerId !== event.detail.peerId) return; + if (!isCurrentPeer(event.detail.peerId)) return; isCallActive.value = true; + isCallCollapsed.value = false; + incomingCallRequest.value = null; startDurationTimer(); }; const handleMediaEnded = (event: MediaEndedEvent) => { const { type } = event.detail; - if (props.peerId !== event.detail.peerId) return; + if (!isCurrentPeer(event.detail.peerId)) return; if (type === "desktop") { isDesktopActive.value = false; + isDesktopLocalSharing.value = false; desktopStream.value = null; + isDesktopCollapsed.value = false; + isDesktopShareCollapsed.value = false; if (videoRef.value) { videoRef.value.srcObject = null; } @@ -406,20 +687,23 @@ const handleMediaEnded = (event: MediaEndedEvent) => { } else if (type === "call") { isCallActive.value = false; callStream.value = null; + isCallCollapsed.value = false; + incomingCallRequest.value = null; if (audioRef.value) { audioRef.value.srcObject = null; } stopDurationTimer(); message.info("语音通话已结束"); } + resetPeerWhenIdle(); }; const endDesktop = () => { - peer.endMedia(props.peerId, "desktop"); + peer.endMedia(getMediaPeerId(), "desktop"); }; const endCall = () => { - peer.endMedia(props.peerId, "call"); + peer.endMedia(getMediaPeerId(), "call"); stopDurationTimer(); }; @@ -463,8 +747,12 @@ const desktopStartedListener: EventListener = (event) => { const callStartedListener: EventListener = (event) => { handleCallStarted(event as MediaStartedEvent); }; +const mediaRequestListener: EventListener = (event) => { + handleMediaRequest(event as MediaRequestEvent); +}; onMounted(() => { + peer.on("media-request", mediaRequestListener); peer.on("stream", streamListener); peer.on("desktop-started", desktopStartedListener); peer.on("call-started", callStartedListener); @@ -473,6 +761,7 @@ onMounted(() => { }); onUnmounted(() => { + peer.off("media-request", mediaRequestListener); peer.off("stream", streamListener); peer.off("desktop-started", desktopStartedListener); peer.off("call-started", callStartedListener); @@ -485,7 +774,6 @@ onUnmounted(() => { diff --git a/src/pages/file/item/fileTranserView.vue b/src/pages/file/item/fileTranserView.vue index 67276ee..cd6c64a 100644 --- a/src/pages/file/item/fileTranserView.vue +++ b/src/pages/file/item/fileTranserView.vue @@ -83,7 +83,7 @@ const updateTasks = () => { tasks.value = Array.from( fileTransferMgrInstance .getAllFileTransfers() - .flatMap((transfer) => transfer.getTasks()) + .flatMap((transfer) => transfer.getTasks()), ); // newTasks.forEach((task) => { // const index = tasks.value.findIndex( @@ -109,7 +109,7 @@ updateTasks(); right: 0; background: white; box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); - z-index: 1000; + z-index: 10; } .transfer-header { diff --git a/src/pages/file/utils/peer.ts b/src/pages/file/utils/peer.ts index 93abfe0..887ca03 100644 --- a/src/pages/file/utils/peer.ts +++ b/src/pages/file/utils/peer.ts @@ -7,7 +7,7 @@ import { TransferStatus, TransferTask, } from "./fileTransfer"; -import { message, Modal, notification } from "ant-design-vue"; +import { message, notification } from "ant-design-vue"; import { randomChars, sign2peerid, @@ -86,7 +86,10 @@ class Peer extends EventTarget { // 保存 this 引用 const self = this; + let handled = false; const onOk = async () => { + if (handled) return; + handled = true; try { let stream: MediaStream; @@ -112,6 +115,7 @@ class Peer extends EventTarget { { detail: { peerId: call.peer, + role: type === "desktop" ? "sharer" : "caller", }, }, ), @@ -126,20 +130,34 @@ class Peer extends EventTarget { message.error("无法访问" + (type === "desktop" ? "屏幕" : "麦克风")); } }; + const onReject = () => { + if (handled) return; + handled = true; + call.close(); + message.info( + "已拒绝" + (type === "desktop" ? "屏幕共享" : "语音通话") + "请求", + ); + }; if (getPermissionSet()[permission]) { await onOk(); + } else if (type === "call") { + this.dispatchEvent( + new CustomEvent("media-request", { + detail: { + peerId: call.peer, + type, + accept: onOk, + reject: onReject, + }, + }), + ); } else { confirmWin(title, content, "接受", "拒绝") .then(async () => { await onOk(); }) .catch(() => { - call.close(); - message.info( - "已拒绝" + - (type === "desktop" ? "屏幕共享" : "语音通话") + - "请求", - ); + onReject(); }); } }); @@ -211,6 +229,7 @@ class Peer extends EventTarget { new CustomEvent("desktop-started", { detail: { peerId: call.peer, + role: "viewer", }, }), );