Compare commits
No commits in common. "be3c548962af45b3910a0bf2623dc3d109cf10a1" and "7d39a6dbe9b7b9c372f2dc9da39b37029787418c" have entirely different histories.
be3c548962
...
7d39a6dbe9
@ -46,22 +46,6 @@
|
|||||||
>
|
>
|
||||||
<img src="/static/phone.png" alt="通话" />
|
<img src="/static/phone.png" alt="通话" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="connect-item"
|
|
||||||
:class="{ disabled: !isConnected, active: isCameraActive }"
|
|
||||||
title="摄像头"
|
|
||||||
@click="requestCamera"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path d="M23 7l-7 5 7 5V7z" />
|
|
||||||
<rect x="1" y="5" width="15" height="14" rx="2" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
v-model="targetId"
|
v-model="targetId"
|
||||||
@ -142,7 +126,6 @@ const isInboundConnected = ref(false);
|
|||||||
const isConnected = ref(false);
|
const isConnected = ref(false);
|
||||||
const isDesktopActive = ref(false);
|
const isDesktopActive = ref(false);
|
||||||
const isCallActive = ref(false);
|
const isCallActive = ref(false);
|
||||||
const isCameraActive = ref(false);
|
|
||||||
// 文件系统相关
|
// 文件系统相关
|
||||||
const selectedLocalFiles = ref<string[]>([]);
|
const selectedLocalFiles = ref<string[]>([]);
|
||||||
const selectedRemoteFiles = ref<string[]>([]);
|
const selectedRemoteFiles = ref<string[]>([]);
|
||||||
@ -167,15 +150,6 @@ const requestCall = () => {
|
|||||||
}
|
}
|
||||||
peer.requestCall(targetId.value);
|
peer.requestCall(targetId.value);
|
||||||
};
|
};
|
||||||
const requestCamera = () => {
|
|
||||||
if (!isConnected.value) return;
|
|
||||||
if (isCameraActive.value) {
|
|
||||||
peer.endMedia(sign2peerid(targetId.value), "camera");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
peer.requestCamera(targetId.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const shareUrl = async () => {
|
const shareUrl = async () => {
|
||||||
if (myId.value) {
|
if (myId.value) {
|
||||||
const url =
|
const url =
|
||||||
@ -315,7 +289,6 @@ onMounted(() => {
|
|||||||
clearConnectedPeer(event.detail.peer);
|
clearConnectedPeer(event.detail.peer);
|
||||||
isDesktopActive.value = false;
|
isDesktopActive.value = false;
|
||||||
isCallActive.value = false;
|
isCallActive.value = false;
|
||||||
isCameraActive.value = false;
|
|
||||||
notification.error({
|
notification.error({
|
||||||
message: event.detail.peer + "连接已断开",
|
message: event.detail.peer + "连接已断开",
|
||||||
});
|
});
|
||||||
@ -334,8 +307,6 @@ onMounted(() => {
|
|||||||
isDesktopActive.value = true;
|
isDesktopActive.value = true;
|
||||||
} else if (event.detail.type === "call") {
|
} else if (event.detail.type === "call") {
|
||||||
isCallActive.value = true;
|
isCallActive.value = true;
|
||||||
} else if (event.detail.type === "camera") {
|
|
||||||
isCameraActive.value = true;
|
|
||||||
}
|
}
|
||||||
}) as EventListener);
|
}) as EventListener);
|
||||||
|
|
||||||
@ -351,24 +322,15 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}) as EventListener);
|
}) as EventListener);
|
||||||
|
|
||||||
peer.on("camera-started", ((event: CustomEvent) => {
|
|
||||||
if (isActivePeer(event.detail.peerId)) {
|
|
||||||
isCameraActive.value = true;
|
|
||||||
}
|
|
||||||
}) as EventListener);
|
|
||||||
|
|
||||||
peer.on("media-ended", ((event: CustomEvent) => {
|
peer.on("media-ended", ((event: CustomEvent) => {
|
||||||
if (!isActivePeer(event.detail.peerId)) return;
|
if (!isActivePeer(event.detail.peerId)) return;
|
||||||
if (event.detail.type === "desktop") {
|
if (event.detail.type === "desktop") {
|
||||||
isDesktopActive.value = false;
|
isDesktopActive.value = false;
|
||||||
} else if (event.detail.type === "call") {
|
} else if (event.detail.type === "call") {
|
||||||
isCallActive.value = false;
|
isCallActive.value = false;
|
||||||
} else if (event.detail.type === "camera") {
|
|
||||||
isCameraActive.value = false;
|
|
||||||
}
|
}
|
||||||
}) as EventListener);
|
}) as EventListener);
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -456,13 +418,6 @@ onMounted(() => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connect-item svg {
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
margin: auto;
|
|
||||||
color: #1677ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connect-actions,
|
.connect-actions,
|
||||||
.connect-section {
|
.connect-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -130,7 +130,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="isRemoteDesktopActive && isDesktopCollapsed"
|
v-if="isRemoteDesktopActive && isDesktopCollapsed"
|
||||||
class="desktop-collapsed-tab"
|
class="desktop-collapsed-tab"
|
||||||
:class="desktopCollapsedClasses"
|
:class="{ 'is-stacked': isCallActive && !isCallCollapsed }"
|
||||||
@click="toggleDesktopCollapse"
|
@click="toggleDesktopCollapse"
|
||||||
title="展开桌面"
|
title="展开桌面"
|
||||||
aria-label="展开桌面"
|
aria-label="展开桌面"
|
||||||
@ -156,7 +156,6 @@
|
|||||||
<div
|
<div
|
||||||
v-if="isDesktopLocalSharing && !isDesktopShareCollapsed"
|
v-if="isDesktopLocalSharing && !isDesktopShareCollapsed"
|
||||||
class="desktop-sharing-panel"
|
class="desktop-sharing-panel"
|
||||||
:class="{ 'above-call-collapsed': isCallActive && isCallCollapsed }"
|
|
||||||
>
|
>
|
||||||
<div class="sharing-panel-header">
|
<div class="sharing-panel-header">
|
||||||
<div class="sharing-panel-title">
|
<div class="sharing-panel-title">
|
||||||
@ -217,7 +216,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="isDesktopLocalSharing && isDesktopShareCollapsed"
|
v-if="isDesktopLocalSharing && isDesktopShareCollapsed"
|
||||||
class="desktop-sharing-tab"
|
class="desktop-sharing-tab"
|
||||||
:class="desktopCollapsedClasses"
|
:class="{ 'is-stacked': isCallActive && !isCallCollapsed }"
|
||||||
@click="toggleDesktopShareCollapse"
|
@click="toggleDesktopShareCollapse"
|
||||||
title="展开桌面预览提示"
|
title="展开桌面预览提示"
|
||||||
aria-label="展开桌面预览提示"
|
aria-label="展开桌面预览提示"
|
||||||
@ -238,227 +237,17 @@
|
|||||||
</button>
|
</button>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<!-- Remote Camera Floating Panel -->
|
|
||||||
<Transition name="panel-slide">
|
|
||||||
<div
|
|
||||||
v-if="isRemoteCameraActive && !isCameraCollapsed"
|
|
||||||
class="camera-floating-panel"
|
|
||||||
:class="{
|
|
||||||
'above-call': isCallActive && !isCallCollapsed,
|
|
||||||
'above-sharing':
|
|
||||||
!isCallActive && isDesktopLocalSharing && !isDesktopShareCollapsed,
|
|
||||||
'above-call-collapsed': isCallActive && isCallCollapsed,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="call-panel-header">
|
|
||||||
<div class="panel-header-left">
|
|
||||||
<span class="panel-header-icon camera">
|
|
||||||
<svg
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path d="M23 7l-7 5 7 5V7z" />
|
|
||||||
<rect x="1" y="5" width="15" height="14" rx="2" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span>远程摄像头</span>
|
|
||||||
</div>
|
|
||||||
<div class="panel-header-actions">
|
|
||||||
<button
|
|
||||||
class="panel-icon-btn"
|
|
||||||
@click="toggleCameraCollapse"
|
|
||||||
title="收起摄像头"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<polyline points="18 15 12 9 6 15" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="panel-icon-btn"
|
|
||||||
@click="endCamera"
|
|
||||||
title="结束摄像头"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
|
||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="camera-panel-body">
|
|
||||||
<video
|
|
||||||
v-if="cameraStream"
|
|
||||||
ref="cameraVideoRef"
|
|
||||||
autoplay
|
|
||||||
playsinline
|
|
||||||
></video>
|
|
||||||
<div v-else class="loading-state">
|
|
||||||
<span>等待摄像头画面...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<Transition name="tab-slide">
|
|
||||||
<button
|
|
||||||
v-if="isRemoteCameraActive && isCameraCollapsed"
|
|
||||||
class="camera-collapsed-tab"
|
|
||||||
:class="cameraCollapsedClasses"
|
|
||||||
@click="toggleCameraCollapse"
|
|
||||||
title="展开摄像头"
|
|
||||||
aria-label="展开摄像头"
|
|
||||||
>
|
|
||||||
<span class="sharing-status-dot"></span>
|
|
||||||
<svg
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path d="M23 7l-7 5 7 5V7z" />
|
|
||||||
<rect x="1" y="5" width="15" height="14" rx="2" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<!-- Local Camera Sharing Indicator -->
|
|
||||||
<Transition name="panel-slide">
|
|
||||||
<div
|
|
||||||
v-if="isCameraLocalSharing && !isCameraShareCollapsed"
|
|
||||||
class="desktop-sharing-panel camera-sharing-panel"
|
|
||||||
>
|
|
||||||
<div class="sharing-panel-header">
|
|
||||||
<div class="sharing-panel-title">
|
|
||||||
<svg
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path d="M23 7l-7 5 7 5V7z" />
|
|
||||||
<rect x="1" y="5" width="15" height="14" rx="2" />
|
|
||||||
</svg>
|
|
||||||
<span>摄像头正在被预览</span>
|
|
||||||
</div>
|
|
||||||
<div class="panel-header-actions">
|
|
||||||
<button
|
|
||||||
class="panel-icon-btn"
|
|
||||||
@click="toggleCameraShareCollapse"
|
|
||||||
title="收起提示"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<polyline points="18 15 12 9 6 15" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="panel-icon-btn"
|
|
||||||
@click="endCamera"
|
|
||||||
title="结束摄像头"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="14"
|
|
||||||
height="14"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
|
||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="sharing-panel-body">
|
|
||||||
<div class="sharing-status-dot"></div>
|
|
||||||
<span>对方正在查看你的摄像头</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<Transition name="tab-slide">
|
|
||||||
<button
|
|
||||||
v-if="isCameraLocalSharing && isCameraShareCollapsed"
|
|
||||||
class="camera-sharing-tab"
|
|
||||||
:class="cameraCollapsedClasses"
|
|
||||||
@click="toggleCameraShareCollapse"
|
|
||||||
title="展开摄像头预览提示"
|
|
||||||
aria-label="展开摄像头预览提示"
|
|
||||||
>
|
|
||||||
<span class="sharing-status-dot"></span>
|
|
||||||
<svg
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path d="M23 7l-7 5 7 5V7z" />
|
|
||||||
<rect x="1" y="5" width="15" height="14" rx="2" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<!-- Incoming Call Request -->
|
<!-- Incoming Call Request -->
|
||||||
<Transition name="panel-slide">
|
<Transition name="panel-slide">
|
||||||
<div
|
<div
|
||||||
v-if="incomingCallRequest"
|
v-if="incomingCallRequest"
|
||||||
class="call-floating-panel incoming-call-panel"
|
class="call-floating-panel incoming-call-panel"
|
||||||
:class="{
|
:class="{ 'is-stacked': isDesktopLocalSharing }"
|
||||||
'is-stacked': isDesktopLocalSharing || isCameraLocalSharing,
|
|
||||||
'above-call-collapsed': isCallActive && isCallCollapsed,
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<div class="call-panel-header">
|
<div class="call-panel-header">
|
||||||
<div class="panel-header-left">
|
<div class="panel-header-left">
|
||||||
<span
|
<span class="panel-header-icon">
|
||||||
class="panel-header-icon"
|
|
||||||
:class="{ camera: incomingCallRequest.type === 'camera' }"
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
v-if="incomingCallRequest.type === 'camera'"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path d="M23 7l-7 5 7 5V7z" />
|
|
||||||
<rect x="1" y="5" width="15" height="14" rx="2" />
|
|
||||||
</svg>
|
|
||||||
<svg
|
|
||||||
v-else
|
|
||||||
width="18"
|
width="18"
|
||||||
height="18"
|
height="18"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -471,32 +260,12 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span>{{
|
<span>语音通话请求</span>
|
||||||
incomingCallRequest.type === "camera"
|
|
||||||
? "摄像头请求"
|
|
||||||
: "语音通话请求"
|
|
||||||
}}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="call-panel-body">
|
<div class="call-panel-body">
|
||||||
<div
|
<div class="call-status-icon">
|
||||||
class="call-status-icon"
|
|
||||||
:class="{ camera: incomingCallRequest.type === 'camera' }"
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
v-if="incomingCallRequest.type === 'camera'"
|
|
||||||
width="48"
|
|
||||||
height="48"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.5"
|
|
||||||
>
|
|
||||||
<path d="M23 7l-7 5 7 5V7z" />
|
|
||||||
<rect x="1" y="5" width="15" height="14" rx="2" />
|
|
||||||
</svg>
|
|
||||||
<svg
|
|
||||||
v-else
|
|
||||||
width="48"
|
width="48"
|
||||||
height="48"
|
height="48"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -509,13 +278,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="call-status-text">
|
<div class="call-status-text">对方请求语音通话</div>
|
||||||
{{
|
|
||||||
incomingCallRequest.type === "camera"
|
|
||||||
? "对方请求查看摄像头"
|
|
||||||
: "对方请求语音通话"
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="incoming-call-actions">
|
<div class="incoming-call-actions">
|
||||||
<button
|
<button
|
||||||
@ -541,7 +304,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="isCallActive && !isCallCollapsed && !incomingCallRequest"
|
v-if="isCallActive && !isCallCollapsed && !incomingCallRequest"
|
||||||
class="call-floating-panel"
|
class="call-floating-panel"
|
||||||
:class="{ 'is-stacked': isDesktopLocalSharing || isCameraLocalSharing }"
|
:class="{ 'is-stacked': isDesktopLocalSharing }"
|
||||||
>
|
>
|
||||||
<div class="call-panel-header">
|
<div class="call-panel-header">
|
||||||
<div class="panel-header-left">
|
<div class="panel-header-left">
|
||||||
@ -671,11 +434,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="isCallCollapsed && isCallActive"
|
v-if="isCallCollapsed && isCallActive"
|
||||||
class="call-collapsed-btn"
|
class="call-collapsed-btn"
|
||||||
:class="{
|
:class="{ 'is-stacked': isDesktopLocalSharing && !isDesktopShareCollapsed }"
|
||||||
'is-stacked':
|
|
||||||
(isDesktopLocalSharing && !isDesktopShareCollapsed) ||
|
|
||||||
(isCameraLocalSharing && !isCameraShareCollapsed),
|
|
||||||
}"
|
|
||||||
@click="toggleCallCollapse"
|
@click="toggleCallCollapse"
|
||||||
title="展开通话"
|
title="展开通话"
|
||||||
>
|
>
|
||||||
@ -709,14 +468,14 @@ const props = defineProps<{
|
|||||||
interface StreamEvent extends CustomEvent {
|
interface StreamEvent extends CustomEvent {
|
||||||
detail: {
|
detail: {
|
||||||
stream: MediaStream;
|
stream: MediaStream;
|
||||||
type: "desktop" | "call" | "camera";
|
type: "desktop" | "call";
|
||||||
peerId: string;
|
peerId: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MediaEndedEvent extends CustomEvent {
|
interface MediaEndedEvent extends CustomEvent {
|
||||||
detail: {
|
detail: {
|
||||||
type: "desktop" | "call" | "camera";
|
type: "desktop" | "call";
|
||||||
peerId: string;
|
peerId: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -731,7 +490,7 @@ interface MediaStartedEvent extends CustomEvent {
|
|||||||
interface MediaRequestEvent extends CustomEvent {
|
interface MediaRequestEvent extends CustomEvent {
|
||||||
detail: {
|
detail: {
|
||||||
peerId: string;
|
peerId: string;
|
||||||
type: "desktop" | "call" | "camera";
|
type: "desktop" | "call";
|
||||||
accept: () => Promise<void> | void;
|
accept: () => Promise<void> | void;
|
||||||
reject: () => void;
|
reject: () => void;
|
||||||
};
|
};
|
||||||
@ -739,31 +498,24 @@ interface MediaRequestEvent extends CustomEvent {
|
|||||||
|
|
||||||
interface IncomingCallRequest {
|
interface IncomingCallRequest {
|
||||||
peerId: string;
|
peerId: string;
|
||||||
type: "call" | "camera";
|
|
||||||
accept: () => Promise<void> | void;
|
accept: () => Promise<void> | void;
|
||||||
reject: () => void;
|
reject: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoRef = ref<HTMLVideoElement | null>(null);
|
const videoRef = ref<HTMLVideoElement | null>(null);
|
||||||
const cameraVideoRef = ref<HTMLVideoElement | null>(null);
|
|
||||||
const audioRef = ref<HTMLAudioElement | null>(null);
|
const audioRef = ref<HTMLAudioElement | null>(null);
|
||||||
const videoContainer = ref<HTMLElement | null>(null);
|
const videoContainer = ref<HTMLElement | null>(null);
|
||||||
const activeMediaPeerId = ref("");
|
const activeMediaPeerId = ref("");
|
||||||
const incomingCallRequest = ref<IncomingCallRequest | null>(null);
|
const incomingCallRequest = ref<IncomingCallRequest | null>(null);
|
||||||
const desktopStream = ref<MediaStream | null>(null);
|
const desktopStream = ref<MediaStream | null>(null);
|
||||||
const cameraStream = ref<MediaStream | null>(null);
|
|
||||||
const callStream = ref<MediaStream | null>(null);
|
const callStream = ref<MediaStream | null>(null);
|
||||||
const isDesktopActive = ref(false);
|
const isDesktopActive = ref(false);
|
||||||
const isDesktopLocalSharing = ref(false);
|
const isDesktopLocalSharing = ref(false);
|
||||||
const isCameraActive = ref(false);
|
|
||||||
const isCameraLocalSharing = ref(false);
|
|
||||||
const isCallActive = ref(false);
|
const isCallActive = ref(false);
|
||||||
const isFullscreen = ref(false);
|
const isFullscreen = ref(false);
|
||||||
const isCallMuted = ref(false);
|
const isCallMuted = ref(false);
|
||||||
const isDesktopCollapsed = ref(false);
|
const isDesktopCollapsed = ref(false);
|
||||||
const isDesktopShareCollapsed = ref(false);
|
const isDesktopShareCollapsed = ref(false);
|
||||||
const isCameraCollapsed = ref(false);
|
|
||||||
const isCameraShareCollapsed = ref(false);
|
|
||||||
const isCallCollapsed = ref(false);
|
const isCallCollapsed = ref(false);
|
||||||
const isIncomingCallHandling = ref(false);
|
const isIncomingCallHandling = ref(false);
|
||||||
const callDuration = ref(0);
|
const callDuration = ref(0);
|
||||||
@ -772,30 +524,6 @@ let durationTimer: number | null = null;
|
|||||||
const isRemoteDesktopActive = computed(
|
const isRemoteDesktopActive = computed(
|
||||||
() => isDesktopActive.value && !isDesktopLocalSharing.value,
|
() => isDesktopActive.value && !isDesktopLocalSharing.value,
|
||||||
);
|
);
|
||||||
const isRemoteCameraActive = computed(
|
|
||||||
() => isCameraActive.value && !isCameraLocalSharing.value,
|
|
||||||
);
|
|
||||||
const isCollapsedCallVisible = computed(
|
|
||||||
() => isCallActive.value && isCallCollapsed.value,
|
|
||||||
);
|
|
||||||
const isCollapsedCameraVisible = computed(
|
|
||||||
() =>
|
|
||||||
(isRemoteCameraActive.value && isCameraCollapsed.value) ||
|
|
||||||
(isCameraLocalSharing.value && isCameraShareCollapsed.value),
|
|
||||||
);
|
|
||||||
const isExpandedCallPanelVisible = computed(
|
|
||||||
() =>
|
|
||||||
isCallActive.value && !isCallCollapsed.value && !incomingCallRequest.value,
|
|
||||||
);
|
|
||||||
const cameraCollapsedClasses = computed(() => ({
|
|
||||||
"slot-2": isCollapsedCallVisible.value,
|
|
||||||
"above-call-panel": isExpandedCallPanelVisible.value,
|
|
||||||
}));
|
|
||||||
const desktopCollapsedClasses = computed(() => ({
|
|
||||||
"slot-3": isCollapsedCallVisible.value && isCollapsedCameraVisible.value,
|
|
||||||
"slot-2": isCollapsedCallVisible.value !== isCollapsedCameraVisible.value,
|
|
||||||
"above-call-panel": isExpandedCallPanelVisible.value,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const callDurationStr = computed(() => {
|
const callDurationStr = computed(() => {
|
||||||
const mins = Math.floor(callDuration.value / 60);
|
const mins = Math.floor(callDuration.value / 60);
|
||||||
@ -837,8 +565,6 @@ const resetPeerWhenIdle = () => {
|
|||||||
if (
|
if (
|
||||||
!isDesktopActive.value &&
|
!isDesktopActive.value &&
|
||||||
!isDesktopLocalSharing.value &&
|
!isDesktopLocalSharing.value &&
|
||||||
!isCameraActive.value &&
|
|
||||||
!isCameraLocalSharing.value &&
|
|
||||||
!isCallActive.value
|
!isCallActive.value
|
||||||
) {
|
) {
|
||||||
activeMediaPeerId.value = "";
|
activeMediaPeerId.value = "";
|
||||||
@ -857,18 +583,6 @@ const bindDesktopVideo = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const bindCameraVideo = async () => {
|
|
||||||
await nextTick();
|
|
||||||
if (!cameraVideoRef.value || !cameraStream.value) return;
|
|
||||||
if (cameraVideoRef.value.srcObject !== cameraStream.value) {
|
|
||||||
cameraVideoRef.value.srcObject = cameraStream.value;
|
|
||||||
}
|
|
||||||
cameraVideoRef.value.muted = true;
|
|
||||||
cameraVideoRef.value.play().catch(() => {
|
|
||||||
message.warning("浏览器阻止了自动播放,请点击摄像头画面恢复播放");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleDesktopCollapse = async () => {
|
const toggleDesktopCollapse = async () => {
|
||||||
isDesktopCollapsed.value = !isDesktopCollapsed.value;
|
isDesktopCollapsed.value = !isDesktopCollapsed.value;
|
||||||
if (!isDesktopCollapsed.value) {
|
if (!isDesktopCollapsed.value) {
|
||||||
@ -876,31 +590,19 @@ const toggleDesktopCollapse = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleCameraCollapse = async () => {
|
|
||||||
isCameraCollapsed.value = !isCameraCollapsed.value;
|
|
||||||
if (!isCameraCollapsed.value) {
|
|
||||||
await bindCameraVideo();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleDesktopShareCollapse = () => {
|
const toggleDesktopShareCollapse = () => {
|
||||||
isDesktopShareCollapsed.value = !isDesktopShareCollapsed.value;
|
isDesktopShareCollapsed.value = !isDesktopShareCollapsed.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleCameraShareCollapse = () => {
|
|
||||||
isCameraShareCollapsed.value = !isCameraShareCollapsed.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleCallCollapse = () => {
|
const toggleCallCollapse = () => {
|
||||||
isCallCollapsed.value = !isCallCollapsed.value;
|
isCallCollapsed.value = !isCallCollapsed.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMediaRequest = (event: MediaRequestEvent) => {
|
const handleMediaRequest = (event: MediaRequestEvent) => {
|
||||||
const { peerId, type, accept, reject } = event.detail;
|
const { peerId, type, accept, reject } = event.detail;
|
||||||
if ((type !== "call" && type !== "camera") || !isCurrentPeer(peerId)) return;
|
if (type !== "call" || !isCurrentPeer(peerId)) return;
|
||||||
incomingCallRequest.value = {
|
incomingCallRequest.value = {
|
||||||
peerId,
|
peerId,
|
||||||
type,
|
|
||||||
accept,
|
accept,
|
||||||
reject,
|
reject,
|
||||||
};
|
};
|
||||||
@ -934,12 +636,6 @@ const handleStream = async (event: StreamEvent) => {
|
|||||||
desktopStream.value = remoteStream;
|
desktopStream.value = remoteStream;
|
||||||
isDesktopCollapsed.value = false;
|
isDesktopCollapsed.value = false;
|
||||||
await bindDesktopVideo();
|
await bindDesktopVideo();
|
||||||
} else if (type === "camera") {
|
|
||||||
isCameraActive.value = true;
|
|
||||||
isCameraLocalSharing.value = false;
|
|
||||||
cameraStream.value = remoteStream;
|
|
||||||
isCameraCollapsed.value = false;
|
|
||||||
await bindCameraVideo();
|
|
||||||
} else if (type === "call") {
|
} else if (type === "call") {
|
||||||
isCallActive.value = true;
|
isCallActive.value = true;
|
||||||
callStream.value = remoteStream;
|
callStream.value = remoteStream;
|
||||||
@ -964,19 +660,6 @@ const handleDesktopStarted = (event: MediaStartedEvent) => {
|
|||||||
isDesktopCollapsed.value = false;
|
isDesktopCollapsed.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCameraStarted = (event: MediaStartedEvent) => {
|
|
||||||
if (!isCurrentPeer(event.detail.peerId)) return;
|
|
||||||
if (event.detail.role === "sharer") {
|
|
||||||
isCameraActive.value = true;
|
|
||||||
isCameraLocalSharing.value = true;
|
|
||||||
isCameraShareCollapsed.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isCameraActive.value = true;
|
|
||||||
isCameraLocalSharing.value = false;
|
|
||||||
isCameraCollapsed.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCallStarted = (event: MediaStartedEvent) => {
|
const handleCallStarted = (event: MediaStartedEvent) => {
|
||||||
if (!isCurrentPeer(event.detail.peerId)) return;
|
if (!isCurrentPeer(event.detail.peerId)) return;
|
||||||
isCallActive.value = true;
|
isCallActive.value = true;
|
||||||
@ -1001,16 +684,6 @@ const handleMediaEnded = (event: MediaEndedEvent) => {
|
|||||||
if (!isCallActive.value) {
|
if (!isCallActive.value) {
|
||||||
stopDurationTimer();
|
stopDurationTimer();
|
||||||
}
|
}
|
||||||
} else if (type === "camera") {
|
|
||||||
isCameraActive.value = false;
|
|
||||||
isCameraLocalSharing.value = false;
|
|
||||||
cameraStream.value = null;
|
|
||||||
isCameraCollapsed.value = false;
|
|
||||||
isCameraShareCollapsed.value = false;
|
|
||||||
if (cameraVideoRef.value) {
|
|
||||||
cameraVideoRef.value.srcObject = null;
|
|
||||||
}
|
|
||||||
message.info("摄像头预览已结束");
|
|
||||||
} else if (type === "call") {
|
} else if (type === "call") {
|
||||||
isCallActive.value = false;
|
isCallActive.value = false;
|
||||||
callStream.value = null;
|
callStream.value = null;
|
||||||
@ -1034,10 +707,6 @@ const endCall = () => {
|
|||||||
stopDurationTimer();
|
stopDurationTimer();
|
||||||
};
|
};
|
||||||
|
|
||||||
const endCamera = () => {
|
|
||||||
peer.endMedia(getMediaPeerId(), "camera");
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleCallMuted = () => {
|
const toggleCallMuted = () => {
|
||||||
isCallMuted.value = !isCallMuted.value;
|
isCallMuted.value = !isCallMuted.value;
|
||||||
if (audioRef.value) {
|
if (audioRef.value) {
|
||||||
@ -1078,9 +747,6 @@ const desktopStartedListener: EventListener = (event) => {
|
|||||||
const callStartedListener: EventListener = (event) => {
|
const callStartedListener: EventListener = (event) => {
|
||||||
handleCallStarted(event as MediaStartedEvent);
|
handleCallStarted(event as MediaStartedEvent);
|
||||||
};
|
};
|
||||||
const cameraStartedListener: EventListener = (event) => {
|
|
||||||
handleCameraStarted(event as MediaStartedEvent);
|
|
||||||
};
|
|
||||||
const mediaRequestListener: EventListener = (event) => {
|
const mediaRequestListener: EventListener = (event) => {
|
||||||
handleMediaRequest(event as MediaRequestEvent);
|
handleMediaRequest(event as MediaRequestEvent);
|
||||||
};
|
};
|
||||||
@ -1089,7 +755,6 @@ onMounted(() => {
|
|||||||
peer.on("media-request", mediaRequestListener);
|
peer.on("media-request", mediaRequestListener);
|
||||||
peer.on("stream", streamListener);
|
peer.on("stream", streamListener);
|
||||||
peer.on("desktop-started", desktopStartedListener);
|
peer.on("desktop-started", desktopStartedListener);
|
||||||
peer.on("camera-started", cameraStartedListener);
|
|
||||||
peer.on("call-started", callStartedListener);
|
peer.on("call-started", callStartedListener);
|
||||||
peer.on("media-ended", mediaEndedListener);
|
peer.on("media-ended", mediaEndedListener);
|
||||||
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
||||||
@ -1099,13 +764,11 @@ onUnmounted(() => {
|
|||||||
peer.off("media-request", mediaRequestListener);
|
peer.off("media-request", mediaRequestListener);
|
||||||
peer.off("stream", streamListener);
|
peer.off("stream", streamListener);
|
||||||
peer.off("desktop-started", desktopStartedListener);
|
peer.off("desktop-started", desktopStartedListener);
|
||||||
peer.off("camera-started", cameraStartedListener);
|
|
||||||
peer.off("call-started", callStartedListener);
|
peer.off("call-started", callStartedListener);
|
||||||
peer.off("media-ended", mediaEndedListener);
|
peer.off("media-ended", mediaEndedListener);
|
||||||
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
||||||
stopDurationTimer();
|
stopDurationTimer();
|
||||||
desktopStream.value?.getTracks().forEach((track) => track.stop());
|
desktopStream.value?.getTracks().forEach((track) => track.stop());
|
||||||
cameraStream.value?.getTracks().forEach((track) => track.stop());
|
|
||||||
callStream.value?.getTracks().forEach((track) => track.stop());
|
callStream.value?.getTracks().forEach((track) => track.stop());
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -1329,7 +992,7 @@ video {
|
|||||||
.desktop-collapsed-tab {
|
.desktop-collapsed-tab {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
bottom: 24px;
|
bottom: 84px;
|
||||||
z-index: 1002;
|
z-index: 1002;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -1351,6 +1014,10 @@ video {
|
|||||||
transform: scale(1.08);
|
transform: scale(1.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.desktop-collapsed-tab.is-stacked {
|
||||||
|
bottom: 372px;
|
||||||
|
}
|
||||||
|
|
||||||
.desktop-collapsed-tab .sharing-status-dot {
|
.desktop-collapsed-tab .sharing-status-dot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 7px;
|
right: 7px;
|
||||||
@ -1374,10 +1041,6 @@ video {
|
|||||||
0 0 0 1px rgba(255, 255, 255, 0.08);
|
0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.desktop-sharing-panel.above-call-collapsed {
|
|
||||||
bottom: 88px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sharing-panel-header {
|
.sharing-panel-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -1421,7 +1084,7 @@ video {
|
|||||||
.desktop-sharing-tab {
|
.desktop-sharing-tab {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
bottom: 24px;
|
bottom: 84px;
|
||||||
z-index: 1002;
|
z-index: 1002;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -1443,6 +1106,10 @@ video {
|
|||||||
transform: scale(1.08);
|
transform: scale(1.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.desktop-sharing-tab.is-stacked {
|
||||||
|
bottom: 372px;
|
||||||
|
}
|
||||||
|
|
||||||
.desktop-sharing-tab .sharing-status-dot {
|
.desktop-sharing-tab .sharing-status-dot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 7px;
|
right: 7px;
|
||||||
@ -1451,107 +1118,6 @@ video {
|
|||||||
height: 7px;
|
height: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.camera-sharing-panel {
|
|
||||||
bottom: 148px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.camera-sharing-tab,
|
|
||||||
.camera-collapsed-tab {
|
|
||||||
position: fixed;
|
|
||||||
right: 16px;
|
|
||||||
bottom: 24px;
|
|
||||||
z-index: 1002;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: linear-gradient(145deg, #1b2332, #121821);
|
|
||||||
color: #40a9ff;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 16px rgba(0, 0, 0, 0.4),
|
|
||||||
0 0 0 1px rgba(255, 255, 255, 0.08);
|
|
||||||
transition: all 0.25s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.camera-sharing-tab:hover,
|
|
||||||
.camera-collapsed-tab:hover {
|
|
||||||
transform: scale(1.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.camera-sharing-tab.slot-2,
|
|
||||||
.camera-collapsed-tab.slot-2,
|
|
||||||
.desktop-collapsed-tab.slot-2,
|
|
||||||
.desktop-sharing-tab.slot-2 {
|
|
||||||
bottom: 84px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desktop-collapsed-tab.slot-3,
|
|
||||||
.desktop-sharing-tab.slot-3 {
|
|
||||||
bottom: 144px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.camera-sharing-tab.above-call-panel,
|
|
||||||
.camera-collapsed-tab.above-call-panel {
|
|
||||||
bottom: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desktop-collapsed-tab.above-call-panel,
|
|
||||||
.desktop-sharing-tab.above-call-panel {
|
|
||||||
bottom: 372px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.camera-sharing-tab .sharing-status-dot,
|
|
||||||
.camera-collapsed-tab .sharing-status-dot {
|
|
||||||
position: absolute;
|
|
||||||
right: 7px;
|
|
||||||
top: 7px;
|
|
||||||
width: 7px;
|
|
||||||
height: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.camera-floating-panel {
|
|
||||||
position: fixed;
|
|
||||||
right: 24px;
|
|
||||||
bottom: 24px;
|
|
||||||
width: min(420px, calc(100vw - 48px));
|
|
||||||
z-index: 1002;
|
|
||||||
overflow: hidden;
|
|
||||||
color: #fff;
|
|
||||||
background: linear-gradient(145deg, #111827, #162033);
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow:
|
|
||||||
0 8px 32px rgba(0, 0, 0, 0.5),
|
|
||||||
0 0 0 1px rgba(255, 255, 255, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.camera-floating-panel.above-call {
|
|
||||||
bottom: 360px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.camera-floating-panel.above-sharing {
|
|
||||||
bottom: 148px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.camera-floating-panel.above-call-collapsed {
|
|
||||||
bottom: 88px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.camera-panel-body {
|
|
||||||
height: 260px;
|
|
||||||
background: #050505;
|
|
||||||
}
|
|
||||||
|
|
||||||
.camera-panel-body video {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Call-Only Floating Panel (expanded) */
|
/* Call-Only Floating Panel (expanded) */
|
||||||
.call-floating-panel {
|
.call-floating-panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -1572,10 +1138,6 @@ video {
|
|||||||
bottom: 148px;
|
bottom: 148px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-floating-panel.above-call-collapsed {
|
|
||||||
bottom: 88px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incoming-call-panel .call-panel-body {
|
.incoming-call-panel .call-panel-body {
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
@ -1643,10 +1205,6 @@ video {
|
|||||||
color: #52c41a;
|
color: #52c41a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header-icon.camera {
|
|
||||||
color: #40a9ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-header-actions {
|
.panel-header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -1695,11 +1253,6 @@ video {
|
|||||||
animation: pulse 2s ease-in-out infinite;
|
animation: pulse 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-status-icon.camera {
|
|
||||||
background: rgba(64, 169, 255, 0.1);
|
|
||||||
color: #40a9ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<div class="permission-container">
|
<div class="permission-container">
|
||||||
<div
|
<div
|
||||||
class="permission-item"
|
class="permission-item"
|
||||||
v-for="permission in permissionsToShow"
|
v-for="(allowed, permission) in localPermissionSet"
|
||||||
:key="permission"
|
:key="permission"
|
||||||
>
|
>
|
||||||
<Checkbox v-model:checked="localPermissionSet[permission]">
|
<Checkbox v-model:checked="localPermissionSet[permission]">
|
||||||
@ -23,18 +23,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Modal, Checkbox } from "ant-design-vue";
|
import { Modal, Checkbox } from "ant-design-vue";
|
||||||
import { computed, onMounted, ref, reactive } from "vue";
|
import { ref, reactive } from "vue";
|
||||||
import { Permission } from "../utils/peer";
|
import { Permission } from "../utils/peer";
|
||||||
import { getPermissionSet, setPermissionSet } from "../utils/common";
|
import { getPermissionSet, setPermissionSet } from "../utils/common";
|
||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const localPermissionSet = reactive({ ...getPermissionSet() });
|
const localPermissionSet = reactive({ ...getPermissionSet() });
|
||||||
const hasCamera = ref(false);
|
|
||||||
const permissionsToShow = computed(() =>
|
|
||||||
Object.values(Permission).filter(
|
|
||||||
(permission) => permission !== Permission.camera || hasCamera.value,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const permissionLabels: Record<Permission, string> = {
|
const permissionLabels: Record<Permission, string> = {
|
||||||
[Permission.edit]: "编辑权限",
|
[Permission.edit]: "编辑权限",
|
||||||
@ -42,7 +36,6 @@ const permissionLabels: Record<Permission, string> = {
|
|||||||
[Permission.download]: "下载权限",
|
[Permission.download]: "下载权限",
|
||||||
[Permission.desktop]: "桌面预览权限",
|
[Permission.desktop]: "桌面预览权限",
|
||||||
[Permission.call]: "语音通话权限",
|
[Permission.call]: "语音通话权限",
|
||||||
[Permission.camera]: "摄像头权限",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const permissionDescs: Record<Permission, string> = {
|
const permissionDescs: Record<Permission, string> = {
|
||||||
@ -51,16 +44,6 @@ const permissionDescs: Record<Permission, string> = {
|
|||||||
[Permission.download]: "允许传输文件到本地",
|
[Permission.download]: "允许传输文件到本地",
|
||||||
[Permission.desktop]: "允许对方请求预览当前桌面",
|
[Permission.desktop]: "允许对方请求预览当前桌面",
|
||||||
[Permission.call]: "允许对方请求语音通话",
|
[Permission.call]: "允许对方请求语音通话",
|
||||||
[Permission.camera]: "允许对方请求查看当前摄像头",
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkCamera = async () => {
|
|
||||||
try {
|
|
||||||
const devices = await navigator.mediaDevices?.enumerateDevices?.();
|
|
||||||
hasCamera.value = devices?.some((device) => device.kind === "videoinput") ?? false;
|
|
||||||
} catch {
|
|
||||||
hasCamera.value = false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOk = () => {
|
const handleOk = () => {
|
||||||
@ -74,14 +57,9 @@ const handleCancel = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showDialog = () => {
|
const showDialog = () => {
|
||||||
checkCamera();
|
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
checkCamera();
|
|
||||||
});
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
showDialog,
|
showDialog,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -64,14 +64,12 @@ export const getUrlParam = (key: string) => {
|
|||||||
let permissionSet: { [key in Permission]: boolean } = null;
|
let permissionSet: { [key in Permission]: boolean } = null;
|
||||||
export function getPermissionSet(): { [key in Permission]: boolean } {
|
export function getPermissionSet(): { [key in Permission]: boolean } {
|
||||||
if (!permissionSet) {
|
if (!permissionSet) {
|
||||||
permissionSet = {
|
permissionSet = getCache('permissionSet') || {
|
||||||
edit: false,
|
edit: false,
|
||||||
view: true,
|
view: true,
|
||||||
download: true,
|
download: true,
|
||||||
desktop: true,
|
desktop: true,
|
||||||
call: false,
|
call: false
|
||||||
camera: false,
|
|
||||||
...(getCache('permissionSet') || {})
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return permissionSet;
|
return permissionSet;
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import {
|
|||||||
confirmWin,
|
confirmWin,
|
||||||
} from "./common";
|
} from "./common";
|
||||||
|
|
||||||
type MediaType = "desktop" | "call" | "camera";
|
type MediaType = "desktop" | "call";
|
||||||
// 发送超时时间(毫秒)
|
// 发送超时时间(毫秒)
|
||||||
const SEND_TIMEOUT = 30000;
|
const SEND_TIMEOUT = 30000;
|
||||||
|
|
||||||
@ -78,28 +78,11 @@ class Peer extends EventTarget {
|
|||||||
// 处理媒体连接
|
// 处理媒体连接
|
||||||
this.peer.on("call", async (call) => {
|
this.peer.on("call", async (call) => {
|
||||||
//弹出确认框
|
//弹出确认框
|
||||||
const mediaType = call.metadata?.type;
|
const type = call.metadata?.type === "desktop" ? "desktop" : "call";
|
||||||
const type: MediaType =
|
const title = type === "desktop" ? "屏幕共享请求" : "语音通话请求";
|
||||||
mediaType === "desktop" || mediaType === "camera" ? mediaType : "call";
|
const content = `${call.peer} 请求与您进行${type === "desktop" ? "屏幕共享" : "语音通话"},是否接受?`;
|
||||||
const title =
|
|
||||||
type === "desktop"
|
|
||||||
? "屏幕共享请求"
|
|
||||||
: type === "camera"
|
|
||||||
? "摄像头请求"
|
|
||||||
: "语音通话请求";
|
|
||||||
const mediaName =
|
|
||||||
type === "desktop"
|
|
||||||
? "屏幕共享"
|
|
||||||
: type === "camera"
|
|
||||||
? "摄像头"
|
|
||||||
: "语音通话";
|
|
||||||
const content = `${call.peer} 请求与您进行${mediaName},是否接受?`;
|
|
||||||
const permission =
|
const permission =
|
||||||
type === "desktop"
|
type === "desktop" ? Permission.desktop : Permission.call;
|
||||||
? Permission.desktop
|
|
||||||
: type === "camera"
|
|
||||||
? Permission.camera
|
|
||||||
: Permission.call;
|
|
||||||
|
|
||||||
// 保存 this 引用
|
// 保存 this 引用
|
||||||
const self = this;
|
const self = this;
|
||||||
@ -116,11 +99,6 @@ class Peer extends EventTarget {
|
|||||||
video: true,
|
video: true,
|
||||||
audio: false,
|
audio: false,
|
||||||
});
|
});
|
||||||
} else if (type === "camera") {
|
|
||||||
stream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
video: true,
|
|
||||||
audio: false,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
stream = await navigator.mediaDevices.getUserMedia({
|
stream = await navigator.mediaDevices.getUserMedia({
|
||||||
audio: true,
|
audio: true,
|
||||||
@ -129,29 +107,15 @@ class Peer extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
call.answer(stream);
|
call.answer(stream);
|
||||||
self.setupMediaConnection(
|
self.setupMediaConnection(call, type, stream, type !== "desktop");
|
||||||
call,
|
|
||||||
type,
|
|
||||||
stream,
|
|
||||||
type !== "desktop" && type !== "camera",
|
|
||||||
);
|
|
||||||
|
|
||||||
self.dispatchEvent(
|
self.dispatchEvent(
|
||||||
new CustomEvent(
|
new CustomEvent(
|
||||||
type === "desktop"
|
type === "desktop" ? "desktop-started" : "call-started",
|
||||||
? "desktop-started"
|
|
||||||
: type === "camera"
|
|
||||||
? "camera-started"
|
|
||||||
: "call-started",
|
|
||||||
{
|
{
|
||||||
detail: {
|
detail: {
|
||||||
peerId: call.peer,
|
peerId: call.peer,
|
||||||
role:
|
role: type === "desktop" ? "sharer" : "caller",
|
||||||
type === "desktop"
|
|
||||||
? "sharer"
|
|
||||||
: type === "camera"
|
|
||||||
? "sharer"
|
|
||||||
: "caller",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -160,34 +124,23 @@ class Peer extends EventTarget {
|
|||||||
console.error("获取媒体设备失败:", error);
|
console.error("获取媒体设备失败:", error);
|
||||||
self.dispatchEvent(
|
self.dispatchEvent(
|
||||||
new CustomEvent("error", {
|
new CustomEvent("error", {
|
||||||
detail:
|
detail: "无法访问" + (type === "desktop" ? "屏幕" : "麦克风"),
|
||||||
"无法访问" +
|
|
||||||
(type === "desktop"
|
|
||||||
? "屏幕"
|
|
||||||
: type === "camera"
|
|
||||||
? "摄像头"
|
|
||||||
: "麦克风"),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
message.error(
|
message.error("无法访问" + (type === "desktop" ? "屏幕" : "麦克风"));
|
||||||
"无法访问" +
|
|
||||||
(type === "desktop"
|
|
||||||
? "屏幕"
|
|
||||||
: type === "camera"
|
|
||||||
? "摄像头"
|
|
||||||
: "麦克风"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onReject = () => {
|
const onReject = () => {
|
||||||
if (handled) return;
|
if (handled) return;
|
||||||
handled = true;
|
handled = true;
|
||||||
call.close();
|
call.close();
|
||||||
message.info("已拒绝" + mediaName + "请求");
|
message.info(
|
||||||
|
"已拒绝" + (type === "desktop" ? "屏幕共享" : "语音通话") + "请求",
|
||||||
|
);
|
||||||
};
|
};
|
||||||
if (getPermissionSet()[permission]) {
|
if (getPermissionSet()[permission]) {
|
||||||
await onOk();
|
await onOk();
|
||||||
} else if (type === "call" || type === "camera") {
|
} else if (type === "call") {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("media-request", {
|
new CustomEvent("media-request", {
|
||||||
detail: {
|
detail: {
|
||||||
@ -304,22 +257,6 @@ class Peer extends EventTarget {
|
|||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
private createCameraOfferStream() {
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.width = 1;
|
|
||||||
canvas.height = 1;
|
|
||||||
const context = canvas.getContext("2d");
|
|
||||||
context?.fillRect(0, 0, 1, 1);
|
|
||||||
|
|
||||||
const stream = canvas.captureStream(1);
|
|
||||||
const [track] = stream.getVideoTracks();
|
|
||||||
if (!track) {
|
|
||||||
throw new Error("无法创建摄像头协商视频轨");
|
|
||||||
}
|
|
||||||
track.enabled = false;
|
|
||||||
return stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
async requestCall(id: string) {
|
async requestCall(id: string) {
|
||||||
if (!this.remoteConnection) {
|
if (!this.remoteConnection) {
|
||||||
notification.error({
|
notification.error({
|
||||||
@ -355,45 +292,10 @@ class Peer extends EventTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestCamera(id: string) {
|
|
||||||
if (!this.remoteConnection) {
|
|
||||||
notification.error({
|
|
||||||
message: "请创建连接",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const offerStream = this.createCameraOfferStream();
|
|
||||||
|
|
||||||
const call = this.peer.call(sign2peerid(id), offerStream, {
|
|
||||||
metadata: { type: "camera" },
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setupMediaConnection(call, "camera", offerStream);
|
|
||||||
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent("camera-started", {
|
|
||||||
detail: {
|
|
||||||
peerId: call.peer,
|
|
||||||
role: "viewer",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("创建摄像头连接失败:", error);
|
|
||||||
notification.error({
|
|
||||||
message: "创建摄像头连接失败",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 结束媒体连接
|
// 结束媒体连接
|
||||||
endMedia(peerId?: string, type?: MediaType) {
|
endMedia(peerId?: string, type?: MediaType) {
|
||||||
if (peerId) {
|
if (peerId) {
|
||||||
const mediaTypes: MediaType[] = type
|
const mediaTypes: MediaType[] = type ? [type] : ["desktop", "call"];
|
||||||
? [type]
|
|
||||||
: ["desktop", "call", "camera"];
|
|
||||||
mediaTypes.forEach((mediaType) => {
|
mediaTypes.forEach((mediaType) => {
|
||||||
this.endMediaConnection(peerId, mediaType);
|
this.endMediaConnection(peerId, mediaType);
|
||||||
if (this.remoteConnection) {
|
if (this.remoteConnection) {
|
||||||
@ -402,9 +304,7 @@ class Peer extends EventTarget {
|
|||||||
type:
|
type:
|
||||||
mediaType === "desktop"
|
mediaType === "desktop"
|
||||||
? MessageType.end_desktop
|
? MessageType.end_desktop
|
||||||
: mediaType === "camera"
|
: MessageType.end_call,
|
||||||
? MessageType.end_camera
|
|
||||||
: MessageType.end_call,
|
|
||||||
data: {
|
data: {
|
||||||
peerId,
|
peerId,
|
||||||
},
|
},
|
||||||
@ -580,10 +480,7 @@ class Peer extends EventTarget {
|
|||||||
const hasVideo = remoteStream.getVideoTracks().length > 0;
|
const hasVideo = remoteStream.getVideoTracks().length > 0;
|
||||||
const hasAudio = remoteStream.getAudioTracks().length > 0;
|
const hasAudio = remoteStream.getAudioTracks().length > 0;
|
||||||
|
|
||||||
if (
|
if ((type === "desktop" && hasVideo) || (type === "call" && hasAudio)) {
|
||||||
((type === "desktop" || type === "camera") && hasVideo) ||
|
|
||||||
(type === "call" && hasAudio)
|
|
||||||
) {
|
|
||||||
this.updateMediaConnection(peerId, type, {
|
this.updateMediaConnection(peerId, type, {
|
||||||
connection: call,
|
connection: call,
|
||||||
remoteStream,
|
remoteStream,
|
||||||
@ -817,16 +714,11 @@ class Peer extends EventTarget {
|
|||||||
}
|
}
|
||||||
resData.data = "ok";
|
resData.data = "ok";
|
||||||
break;
|
break;
|
||||||
case MessageType.end_desktop:
|
|
||||||
case MessageType.end_camera:
|
|
||||||
case MessageType.end_call:
|
case MessageType.end_call:
|
||||||
|
case MessageType.end_desktop:
|
||||||
this.endMediaConnection(
|
this.endMediaConnection(
|
||||||
conn.peer,
|
conn.peer,
|
||||||
data.type === MessageType.end_desktop
|
data.type === MessageType.end_desktop ? "desktop" : "call",
|
||||||
? "desktop"
|
|
||||||
: data.type === MessageType.end_camera
|
|
||||||
? "camera"
|
|
||||||
: "call",
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -877,7 +769,6 @@ export enum MessageType {
|
|||||||
response_push_file_complete = "response_push_file_complete",
|
response_push_file_complete = "response_push_file_complete",
|
||||||
end_call = "end_call",
|
end_call = "end_call",
|
||||||
end_desktop = "end_desktop",
|
end_desktop = "end_desktop",
|
||||||
end_camera = "end_camera",
|
|
||||||
}
|
}
|
||||||
export enum Permission {
|
export enum Permission {
|
||||||
edit = "edit",
|
edit = "edit",
|
||||||
@ -885,7 +776,6 @@ export enum Permission {
|
|||||||
download = "download",
|
download = "download",
|
||||||
desktop = "desktop",
|
desktop = "desktop",
|
||||||
call = "call",
|
call = "call",
|
||||||
camera = "camera",
|
|
||||||
}
|
}
|
||||||
export const PermissionLimit = {
|
export const PermissionLimit = {
|
||||||
[Permission.edit]: [
|
[Permission.edit]: [
|
||||||
@ -905,7 +795,6 @@ export const PermissionLimit = {
|
|||||||
[Permission.download]: [MessageType.request_getFile],
|
[Permission.download]: [MessageType.request_getFile],
|
||||||
[Permission.desktop]: false,
|
[Permission.desktop]: false,
|
||||||
[Permission.call]: false,
|
[Permission.call]: false,
|
||||||
[Permission.camera]: false,
|
|
||||||
};
|
};
|
||||||
export function uuidv4(): string {
|
export function uuidv4(): string {
|
||||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user