新增摄像头
This commit is contained in:
parent
7d39a6dbe9
commit
6b5e402f3a
@ -46,6 +46,22 @@
|
|||||||
>
|
>
|
||||||
<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"
|
||||||
@ -126,6 +142,7 @@ 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[]>([]);
|
||||||
@ -150,6 +167,15 @@ 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 =
|
||||||
@ -289,6 +315,7 @@ 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 + "连接已断开",
|
||||||
});
|
});
|
||||||
@ -307,6 +334,8 @@ 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);
|
||||||
|
|
||||||
@ -322,15 +351,24 @@ 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>
|
||||||
@ -418,6 +456,13 @@ 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="{ 'is-stacked': isCallActive && !isCallCollapsed }"
|
:class="desktopCollapsedClasses"
|
||||||
@click="toggleDesktopCollapse"
|
@click="toggleDesktopCollapse"
|
||||||
title="展开桌面"
|
title="展开桌面"
|
||||||
aria-label="展开桌面"
|
aria-label="展开桌面"
|
||||||
@ -216,7 +216,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="isDesktopLocalSharing && isDesktopShareCollapsed"
|
v-if="isDesktopLocalSharing && isDesktopShareCollapsed"
|
||||||
class="desktop-sharing-tab"
|
class="desktop-sharing-tab"
|
||||||
:class="{ 'is-stacked': isCallActive && !isCallCollapsed }"
|
:class="desktopCollapsedClasses"
|
||||||
@click="toggleDesktopShareCollapse"
|
@click="toggleDesktopShareCollapse"
|
||||||
title="展开桌面预览提示"
|
title="展开桌面预览提示"
|
||||||
aria-label="展开桌面预览提示"
|
aria-label="展开桌面预览提示"
|
||||||
@ -237,17 +237,215 @@
|
|||||||
</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,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<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="{ 'is-stacked': isDesktopLocalSharing }"
|
:class="{ 'is-stacked': isDesktopLocalSharing || isCameraLocalSharing }"
|
||||||
>
|
>
|
||||||
<div class="call-panel-header">
|
<div class="call-panel-header">
|
||||||
<div class="panel-header-left">
|
<div class="panel-header-left">
|
||||||
<span class="panel-header-icon">
|
<span
|
||||||
|
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"
|
||||||
@ -260,12 +458,32 @@
|
|||||||
/>
|
/>
|
||||||
</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 class="call-status-icon">
|
<div
|
||||||
|
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"
|
||||||
@ -278,7 +496,13 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="call-status-text">对方请求语音通话</div>
|
<div class="call-status-text">
|
||||||
|
{{
|
||||||
|
incomingCallRequest.type === "camera"
|
||||||
|
? "对方请求查看摄像头"
|
||||||
|
: "对方请求语音通话"
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="incoming-call-actions">
|
<div class="incoming-call-actions">
|
||||||
<button
|
<button
|
||||||
@ -304,7 +528,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 }"
|
:class="{ 'is-stacked': isDesktopLocalSharing || isCameraLocalSharing }"
|
||||||
>
|
>
|
||||||
<div class="call-panel-header">
|
<div class="call-panel-header">
|
||||||
<div class="panel-header-left">
|
<div class="panel-header-left">
|
||||||
@ -434,7 +658,11 @@
|
|||||||
<div
|
<div
|
||||||
v-if="isCallCollapsed && isCallActive"
|
v-if="isCallCollapsed && isCallActive"
|
||||||
class="call-collapsed-btn"
|
class="call-collapsed-btn"
|
||||||
:class="{ 'is-stacked': isDesktopLocalSharing && !isDesktopShareCollapsed }"
|
:class="{
|
||||||
|
'is-stacked':
|
||||||
|
(isDesktopLocalSharing && !isDesktopShareCollapsed) ||
|
||||||
|
(isCameraLocalSharing && !isCameraShareCollapsed),
|
||||||
|
}"
|
||||||
@click="toggleCallCollapse"
|
@click="toggleCallCollapse"
|
||||||
title="展开通话"
|
title="展开通话"
|
||||||
>
|
>
|
||||||
@ -468,14 +696,14 @@ const props = defineProps<{
|
|||||||
interface StreamEvent extends CustomEvent {
|
interface StreamEvent extends CustomEvent {
|
||||||
detail: {
|
detail: {
|
||||||
stream: MediaStream;
|
stream: MediaStream;
|
||||||
type: "desktop" | "call";
|
type: "desktop" | "call" | "camera";
|
||||||
peerId: string;
|
peerId: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MediaEndedEvent extends CustomEvent {
|
interface MediaEndedEvent extends CustomEvent {
|
||||||
detail: {
|
detail: {
|
||||||
type: "desktop" | "call";
|
type: "desktop" | "call" | "camera";
|
||||||
peerId: string;
|
peerId: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -490,7 +718,7 @@ interface MediaStartedEvent extends CustomEvent {
|
|||||||
interface MediaRequestEvent extends CustomEvent {
|
interface MediaRequestEvent extends CustomEvent {
|
||||||
detail: {
|
detail: {
|
||||||
peerId: string;
|
peerId: string;
|
||||||
type: "desktop" | "call";
|
type: "desktop" | "call" | "camera";
|
||||||
accept: () => Promise<void> | void;
|
accept: () => Promise<void> | void;
|
||||||
reject: () => void;
|
reject: () => void;
|
||||||
};
|
};
|
||||||
@ -498,24 +726,31 @@ 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);
|
||||||
@ -524,6 +759,30 @@ 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);
|
||||||
@ -565,6 +824,8 @@ const resetPeerWhenIdle = () => {
|
|||||||
if (
|
if (
|
||||||
!isDesktopActive.value &&
|
!isDesktopActive.value &&
|
||||||
!isDesktopLocalSharing.value &&
|
!isDesktopLocalSharing.value &&
|
||||||
|
!isCameraActive.value &&
|
||||||
|
!isCameraLocalSharing.value &&
|
||||||
!isCallActive.value
|
!isCallActive.value
|
||||||
) {
|
) {
|
||||||
activeMediaPeerId.value = "";
|
activeMediaPeerId.value = "";
|
||||||
@ -583,6 +844,18 @@ 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) {
|
||||||
@ -590,19 +863,31 @@ 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" || !isCurrentPeer(peerId)) return;
|
if ((type !== "call" && type !== "camera") || !isCurrentPeer(peerId)) return;
|
||||||
incomingCallRequest.value = {
|
incomingCallRequest.value = {
|
||||||
peerId,
|
peerId,
|
||||||
|
type,
|
||||||
accept,
|
accept,
|
||||||
reject,
|
reject,
|
||||||
};
|
};
|
||||||
@ -636,6 +921,12 @@ 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;
|
||||||
@ -660,6 +951,19 @@ 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;
|
||||||
@ -684,6 +988,16 @@ 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;
|
||||||
@ -707,6 +1021,10 @@ 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) {
|
||||||
@ -747,6 +1065,9 @@ 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);
|
||||||
};
|
};
|
||||||
@ -755,6 +1076,7 @@ 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);
|
||||||
@ -764,11 +1086,13 @@ 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>
|
||||||
@ -992,7 +1316,7 @@ video {
|
|||||||
.desktop-collapsed-tab {
|
.desktop-collapsed-tab {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
bottom: 84px;
|
bottom: 24px;
|
||||||
z-index: 1002;
|
z-index: 1002;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -1014,10 +1338,6 @@ 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;
|
||||||
@ -1084,7 +1404,7 @@ video {
|
|||||||
.desktop-sharing-tab {
|
.desktop-sharing-tab {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
bottom: 84px;
|
bottom: 24px;
|
||||||
z-index: 1002;
|
z-index: 1002;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -1106,10 +1426,6 @@ 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;
|
||||||
@ -1118,6 +1434,103 @@ 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-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;
|
||||||
@ -1205,6 +1618,10 @@ 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;
|
||||||
@ -1253,6 +1670,11 @@ 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="(allowed, permission) in localPermissionSet"
|
v-for="permission in permissionsToShow"
|
||||||
:key="permission"
|
:key="permission"
|
||||||
>
|
>
|
||||||
<Checkbox v-model:checked="localPermissionSet[permission]">
|
<Checkbox v-model:checked="localPermissionSet[permission]">
|
||||||
@ -23,12 +23,18 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Modal, Checkbox } from "ant-design-vue";
|
import { Modal, Checkbox } from "ant-design-vue";
|
||||||
import { ref, reactive } from "vue";
|
import { computed, onMounted, 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]: "编辑权限",
|
||||||
@ -36,6 +42,7 @@ 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> = {
|
||||||
@ -44,6 +51,16 @@ 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 = () => {
|
||||||
@ -57,9 +74,14 @@ const handleCancel = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showDialog = () => {
|
const showDialog = () => {
|
||||||
|
checkCamera();
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkCamera();
|
||||||
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
showDialog,
|
showDialog,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -64,12 +64,14 @@ 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 = getCache('permissionSet') || {
|
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";
|
type MediaType = "desktop" | "call" | "camera";
|
||||||
// 发送超时时间(毫秒)
|
// 发送超时时间(毫秒)
|
||||||
const SEND_TIMEOUT = 30000;
|
const SEND_TIMEOUT = 30000;
|
||||||
|
|
||||||
@ -78,11 +78,24 @@ class Peer extends EventTarget {
|
|||||||
// 处理媒体连接
|
// 处理媒体连接
|
||||||
this.peer.on("call", async (call) => {
|
this.peer.on("call", async (call) => {
|
||||||
//弹出确认框
|
//弹出确认框
|
||||||
const type = call.metadata?.type === "desktop" ? "desktop" : "call";
|
const mediaType = call.metadata?.type;
|
||||||
const title = type === "desktop" ? "屏幕共享请求" : "语音通话请求";
|
const type: MediaType =
|
||||||
const content = `${call.peer} 请求与您进行${type === "desktop" ? "屏幕共享" : "语音通话"},是否接受?`;
|
mediaType === "desktop" || mediaType === "camera" ? mediaType : "call";
|
||||||
|
const title =
|
||||||
|
type === "desktop"
|
||||||
|
? "屏幕共享请求"
|
||||||
|
: type === "camera"
|
||||||
|
? "摄像头请求"
|
||||||
|
: "语音通话请求";
|
||||||
|
const mediaName =
|
||||||
|
type === "desktop" ? "屏幕共享" : type === "camera" ? "摄像头" : "语音通话";
|
||||||
|
const content = `${call.peer} 请求与您进行${mediaName},是否接受?`;
|
||||||
const permission =
|
const permission =
|
||||||
type === "desktop" ? Permission.desktop : Permission.call;
|
type === "desktop"
|
||||||
|
? Permission.desktop
|
||||||
|
: type === "camera"
|
||||||
|
? Permission.camera
|
||||||
|
: Permission.call;
|
||||||
|
|
||||||
// 保存 this 引用
|
// 保存 this 引用
|
||||||
const self = this;
|
const self = this;
|
||||||
@ -99,6 +112,11 @@ 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,
|
||||||
@ -107,15 +125,29 @@ class Peer extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
call.answer(stream);
|
call.answer(stream);
|
||||||
self.setupMediaConnection(call, type, stream, type !== "desktop");
|
self.setupMediaConnection(
|
||||||
|
call,
|
||||||
|
type,
|
||||||
|
stream,
|
||||||
|
type !== "desktop" && type !== "camera",
|
||||||
|
);
|
||||||
|
|
||||||
self.dispatchEvent(
|
self.dispatchEvent(
|
||||||
new CustomEvent(
|
new CustomEvent(
|
||||||
type === "desktop" ? "desktop-started" : "call-started",
|
type === "desktop"
|
||||||
|
? "desktop-started"
|
||||||
|
: type === "camera"
|
||||||
|
? "camera-started"
|
||||||
|
: "call-started",
|
||||||
{
|
{
|
||||||
detail: {
|
detail: {
|
||||||
peerId: call.peer,
|
peerId: call.peer,
|
||||||
role: type === "desktop" ? "sharer" : "caller",
|
role:
|
||||||
|
type === "desktop"
|
||||||
|
? "sharer"
|
||||||
|
: type === "camera"
|
||||||
|
? "sharer"
|
||||||
|
: "caller",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -124,23 +156,34 @@ class Peer extends EventTarget {
|
|||||||
console.error("获取媒体设备失败:", error);
|
console.error("获取媒体设备失败:", error);
|
||||||
self.dispatchEvent(
|
self.dispatchEvent(
|
||||||
new CustomEvent("error", {
|
new CustomEvent("error", {
|
||||||
detail: "无法访问" + (type === "desktop" ? "屏幕" : "麦克风"),
|
detail:
|
||||||
|
"无法访问" +
|
||||||
|
(type === "desktop"
|
||||||
|
? "屏幕"
|
||||||
|
: type === "camera"
|
||||||
|
? "摄像头"
|
||||||
|
: "麦克风"),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
message.error("无法访问" + (type === "desktop" ? "屏幕" : "麦克风"));
|
message.error(
|
||||||
|
"无法访问" +
|
||||||
|
(type === "desktop"
|
||||||
|
? "屏幕"
|
||||||
|
: type === "camera"
|
||||||
|
? "摄像头"
|
||||||
|
: "麦克风"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onReject = () => {
|
const onReject = () => {
|
||||||
if (handled) return;
|
if (handled) return;
|
||||||
handled = true;
|
handled = true;
|
||||||
call.close();
|
call.close();
|
||||||
message.info(
|
message.info("已拒绝" + mediaName + "请求");
|
||||||
"已拒绝" + (type === "desktop" ? "屏幕共享" : "语音通话") + "请求",
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
if (getPermissionSet()[permission]) {
|
if (getPermissionSet()[permission]) {
|
||||||
await onOk();
|
await onOk();
|
||||||
} else if (type === "call") {
|
} else if (type === "call" || type === "camera") {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("media-request", {
|
new CustomEvent("media-request", {
|
||||||
detail: {
|
detail: {
|
||||||
@ -292,10 +335,43 @@ class Peer extends EventTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async requestCamera(id: string) {
|
||||||
|
if (!this.remoteConnection) {
|
||||||
|
notification.error({
|
||||||
|
message: "请创建连接",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const offerStream = this.createDesktopOfferStream();
|
||||||
|
|
||||||
|
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 ? [type] : ["desktop", "call"];
|
const mediaTypes: MediaType[] = type ? [type] : ["desktop", "call", "camera"];
|
||||||
mediaTypes.forEach((mediaType) => {
|
mediaTypes.forEach((mediaType) => {
|
||||||
this.endMediaConnection(peerId, mediaType);
|
this.endMediaConnection(peerId, mediaType);
|
||||||
if (this.remoteConnection) {
|
if (this.remoteConnection) {
|
||||||
@ -304,6 +380,8 @@ class Peer extends EventTarget {
|
|||||||
type:
|
type:
|
||||||
mediaType === "desktop"
|
mediaType === "desktop"
|
||||||
? MessageType.end_desktop
|
? MessageType.end_desktop
|
||||||
|
: mediaType === "camera"
|
||||||
|
? MessageType.end_camera
|
||||||
: MessageType.end_call,
|
: MessageType.end_call,
|
||||||
data: {
|
data: {
|
||||||
peerId,
|
peerId,
|
||||||
@ -480,7 +558,10 @@ 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 ((type === "desktop" && hasVideo) || (type === "call" && hasAudio)) {
|
if (
|
||||||
|
((type === "desktop" || type === "camera") && hasVideo) ||
|
||||||
|
(type === "call" && hasAudio)
|
||||||
|
) {
|
||||||
this.updateMediaConnection(peerId, type, {
|
this.updateMediaConnection(peerId, type, {
|
||||||
connection: call,
|
connection: call,
|
||||||
remoteStream,
|
remoteStream,
|
||||||
@ -714,11 +795,16 @@ class Peer extends EventTarget {
|
|||||||
}
|
}
|
||||||
resData.data = "ok";
|
resData.data = "ok";
|
||||||
break;
|
break;
|
||||||
case MessageType.end_call:
|
|
||||||
case MessageType.end_desktop:
|
case MessageType.end_desktop:
|
||||||
|
case MessageType.end_camera:
|
||||||
|
case MessageType.end_call:
|
||||||
this.endMediaConnection(
|
this.endMediaConnection(
|
||||||
conn.peer,
|
conn.peer,
|
||||||
data.type === MessageType.end_desktop ? "desktop" : "call",
|
data.type === MessageType.end_desktop
|
||||||
|
? "desktop"
|
||||||
|
: data.type === MessageType.end_camera
|
||||||
|
? "camera"
|
||||||
|
: "call",
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -769,6 +855,7 @@ 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",
|
||||||
@ -776,6 +863,7 @@ 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]: [
|
||||||
@ -795,6 +883,7 @@ 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