Merge branch 'main' of https://github.com/kunkundi/crossdesk-web-client
This commit is contained in:
63
control.js
63
control.js
@@ -205,9 +205,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendDisplayId(id) {
|
sendDisplayId(id) {
|
||||||
|
// 确保 id 是有效数字
|
||||||
|
const numericId = typeof id === "number" && Number.isFinite(id) ? id : parseInt(id, 10);
|
||||||
|
if (isNaN(numericId) || !Number.isFinite(numericId)) {
|
||||||
|
console.warn("sendDisplayId: Invalid display_id:", id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const action = {
|
const action = {
|
||||||
type: ControlType.display_id,
|
type: ControlType.display_id,
|
||||||
display_id: id | 0,
|
display_id: numericId | 0,
|
||||||
};
|
};
|
||||||
this.send(action);
|
this.send(action);
|
||||||
}
|
}
|
||||||
@@ -299,6 +305,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip if clicking inside panel area
|
||||||
|
if (this.isInsidePanel(event.clientX, event.clientY)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip if dragging panel
|
// Skip if dragging panel
|
||||||
if (this.state.draggingPanel) {
|
if (this.state.draggingPanel) {
|
||||||
return;
|
return;
|
||||||
@@ -353,6 +364,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip if moving inside panel area
|
||||||
|
if (this.isInsidePanel(event.clientX, event.clientY)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip if dragging panel
|
// Skip if dragging panel
|
||||||
if (this.state.draggingPanel) {
|
if (this.state.draggingPanel) {
|
||||||
return;
|
return;
|
||||||
@@ -451,6 +467,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onPointerUp(event) {
|
onPointerUp(event) {
|
||||||
|
// Skip if releasing inside panel area
|
||||||
|
if (this.isInsidePanel(event.clientX, event.clientY)) {
|
||||||
|
this.elements.video?.releasePointerCapture?.(event.pointerId ?? 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 移动端模式下,触摸结束不触发点击事件
|
// 移动端模式下,触摸结束不触发点击事件
|
||||||
if (this.state.isMobile && event.pointerType === "touch") {
|
if (this.state.isMobile && event.pointerType === "touch") {
|
||||||
this.elements.video?.releasePointerCapture?.(event.pointerId ?? 0);
|
this.elements.video?.releasePointerCapture?.(event.pointerId ?? 0);
|
||||||
@@ -485,6 +507,11 @@
|
|||||||
if (now - this.state.lastWheelAt < 50) return;
|
if (now - this.state.lastWheelAt < 50) return;
|
||||||
this.state.lastWheelAt = now;
|
this.state.lastWheelAt = now;
|
||||||
|
|
||||||
|
// Skip if wheeling inside panel area
|
||||||
|
if (this.isInsidePanel(event.clientX, event.clientY)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.ensureVideoRect();
|
this.ensureVideoRect();
|
||||||
if (!this.state.videoRect) return;
|
if (!this.state.videoRect) return;
|
||||||
|
|
||||||
@@ -517,12 +544,18 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const touch = event.touches[0];
|
||||||
|
|
||||||
|
// Skip if touching inside panel area
|
||||||
|
if (this.isInsidePanel(touch.clientX, touch.clientY)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip if pinch zoom is active, dragging panel, or if two touches (pinch gesture)
|
// Skip if pinch zoom is active, dragging panel, or if two touches (pinch gesture)
|
||||||
if (this.state.pinchZoomActive || this.state.draggingPanel || event.touches.length === 2) {
|
if (this.state.pinchZoomActive || this.state.draggingPanel || event.touches.length === 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const touch = event.touches[0];
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// 移动端模式下,触摸视频区域不触发点击事件
|
// 移动端模式下,触摸视频区域不触发点击事件
|
||||||
@@ -554,12 +587,18 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const touch = event.touches[0];
|
||||||
|
|
||||||
|
// Skip if moving inside panel area
|
||||||
|
if (this.isInsidePanel(touch.clientX, touch.clientY)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip if pinch zoom is active, dragging panel, or if two touches (pinch gesture)
|
// Skip if pinch zoom is active, dragging panel, or if two touches (pinch gesture)
|
||||||
if (this.state.pinchZoomActive || this.state.draggingPanel || event.touches.length === 2) {
|
if (this.state.pinchZoomActive || this.state.draggingPanel || event.touches.length === 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const touch = event.touches[0];
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
this.ensureVideoRect();
|
this.ensureVideoRect();
|
||||||
@@ -638,6 +677,18 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isInsidePanel(clientX, clientY) {
|
||||||
|
const panel = document.getElementById("connected-panel");
|
||||||
|
if (!panel) return false;
|
||||||
|
const rect = panel.getBoundingClientRect();
|
||||||
|
return (
|
||||||
|
clientX >= rect.left &&
|
||||||
|
clientX <= rect.right &&
|
||||||
|
clientY >= rect.top &&
|
||||||
|
clientY <= rect.bottom
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
updateNormalizedFromClient(clientX, clientY) {
|
updateNormalizedFromClient(clientX, clientY) {
|
||||||
if (!this.state.videoRect) return;
|
if (!this.state.videoRect) return;
|
||||||
this.state.normalizedPos = {
|
this.state.normalizedPos = {
|
||||||
@@ -1136,7 +1187,7 @@
|
|||||||
if (keyboardToggleMouse) {
|
if (keyboardToggleMouse) {
|
||||||
keyboardToggleMouse.addEventListener("touchstart", (e) => {
|
keyboardToggleMouse.addEventListener("touchstart", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
}, { passive: true });
|
||||||
keyboardToggleMouse.addEventListener("click", (e) => {
|
keyboardToggleMouse.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
@@ -1146,7 +1197,7 @@
|
|||||||
if (this.elements.virtualMouseMinimize) {
|
if (this.elements.virtualMouseMinimize) {
|
||||||
this.elements.virtualMouseMinimize.addEventListener("touchstart", (e) => {
|
this.elements.virtualMouseMinimize.addEventListener("touchstart", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
}, { passive: true });
|
||||||
this.elements.virtualMouseMinimize.addEventListener("click", (e) => {
|
this.elements.virtualMouseMinimize.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
@@ -1174,7 +1225,7 @@
|
|||||||
if (keyboardClose) {
|
if (keyboardClose) {
|
||||||
keyboardClose.addEventListener("touchstart", (e) => {
|
keyboardClose.addEventListener("touchstart", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
}, { passive: true });
|
||||||
keyboardClose.addEventListener("click", (e) => {
|
keyboardClose.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|||||||
28
index.html
28
index.html
@@ -10,6 +10,7 @@
|
|||||||
<title>CrossDesk Web Client</title>
|
<title>CrossDesk Web Client</title>
|
||||||
|
|
||||||
<!-- iOS Web App 配置 -->
|
<!-- iOS Web App 配置 -->
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta
|
<meta
|
||||||
name="apple-mobile-web-app-status-bar-style"
|
name="apple-mobile-web-app-status-bar-style"
|
||||||
@@ -51,18 +52,20 @@
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="option">
|
<form>
|
||||||
<label for="transmission-id">远程设备 ID:</label>
|
<div class="option">
|
||||||
<input id="transmission-id" type="text" value="" />
|
<label for="transmission-id">远程设备 ID:</label>
|
||||||
</div>
|
<input id="transmission-id" type="text" value="" autocomplete="off" />
|
||||||
<div class="option">
|
</div>
|
||||||
<label for="transmission-pwd">密码:</label>
|
<div class="option">
|
||||||
<input id="transmission-pwd" type="password" value="" />
|
<label for="transmission-pwd">密码:</label>
|
||||||
</div>
|
<input id="transmission-pwd" type="password" value="" autocomplete="current-password" />
|
||||||
<div class="actions">
|
</div>
|
||||||
<button id="connect" disabled>连接</button>
|
<div class="actions">
|
||||||
<button id="disconnect" style="display: none">断开连接</button>
|
<button id="connect" type="button" disabled>连接</button>
|
||||||
</div>
|
<button id="disconnect" type="button" style="display: none">断开连接</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -78,6 +81,7 @@
|
|||||||
x5-video-player-fullscreen="true"
|
x5-video-player-fullscreen="true"
|
||||||
muted
|
muted
|
||||||
></video>
|
></video>
|
||||||
|
<audio id="audio" autoplay></audio>
|
||||||
|
|
||||||
<!-- Connecting overlay -->
|
<!-- Connecting overlay -->
|
||||||
<div id="connecting-overlay" class="connecting-overlay" style="display: none">
|
<div id="connecting-overlay" class="connecting-overlay" style="display: none">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
"sizes": "any",
|
"sizes": "32x32",
|
||||||
"type": "image/x-icon"
|
"type": "image/x-icon"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
101
web_client.js
101
web_client.js
@@ -7,6 +7,7 @@ const elements = {
|
|||||||
disconnectBtn: document.getElementById("disconnect"),
|
disconnectBtn: document.getElementById("disconnect"),
|
||||||
media: document.getElementById("media"),
|
media: document.getElementById("media"),
|
||||||
video: document.getElementById("video"),
|
video: document.getElementById("video"),
|
||||||
|
audio: document.getElementById("audio"),
|
||||||
connectionOverlay: document.getElementById("connection-overlay"),
|
connectionOverlay: document.getElementById("connection-overlay"),
|
||||||
connectedOverlay: document.getElementById("connected-overlay"),
|
connectedOverlay: document.getElementById("connected-overlay"),
|
||||||
connectedPanel: document.getElementById("connected-panel"),
|
connectedPanel: document.getElementById("connected-panel"),
|
||||||
@@ -38,6 +39,8 @@ let pc = null;
|
|||||||
let clientId = "000000";
|
let clientId = "000000";
|
||||||
let heartbeatTimer = null;
|
let heartbeatTimer = null;
|
||||||
let lastPongAt = Date.now();
|
let lastPongAt = Date.now();
|
||||||
|
let trackIndex = 0; // Track index for display_id (0, 1, 2, ...)
|
||||||
|
const trackMap = new Map(); // Map<index, track> - stores tracks by their display_id index
|
||||||
|
|
||||||
const websocket = new WebSocket(CONFIG.signalingUrl);
|
const websocket = new WebSocket(CONFIG.signalingUrl);
|
||||||
|
|
||||||
@@ -218,8 +221,36 @@ function createPeerConnection() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
peer.ontrack = ({ track, streams }) => {
|
peer.ontrack = ({ track, streams }) => {
|
||||||
|
// Handle audio tracks
|
||||||
|
if (track.kind === "audio" && elements.audio) {
|
||||||
|
if (!elements.audio.srcObject) {
|
||||||
|
// First audio track: create new stream
|
||||||
|
const audioStream = streams && streams[0] ? streams[0] : new MediaStream([track]);
|
||||||
|
elements.audio.srcObject = audioStream;
|
||||||
|
elements.audio.autoplay = true;
|
||||||
|
// Try to play audio (may require user interaction)
|
||||||
|
elements.audio.play().catch(err => {
|
||||||
|
console.log("Audio autoplay prevented:", err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Additional audio track: add to existing stream
|
||||||
|
elements.audio.srcObject.addTrack(track);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle video tracks
|
||||||
if (track.kind !== "video" || !elements.video) return;
|
if (track.kind !== "video" || !elements.video) return;
|
||||||
|
|
||||||
|
// Use track index as display_id (0, 1, 2, ...)
|
||||||
|
const currentIndex = trackIndex;
|
||||||
|
trackIndex++;
|
||||||
|
|
||||||
|
// Store track in map
|
||||||
|
trackMap.set(currentIndex, track);
|
||||||
|
|
||||||
if (!elements.video.srcObject) {
|
if (!elements.video.srcObject) {
|
||||||
|
// First track: create new stream
|
||||||
const stream = streams && streams[0] ? streams[0] : new MediaStream([track]);
|
const stream = streams && streams[0] ? streams[0] : new MediaStream([track]);
|
||||||
elements.video.srcObject = stream;
|
elements.video.srcObject = stream;
|
||||||
elements.video.muted = true;
|
elements.video.muted = true;
|
||||||
@@ -232,23 +263,37 @@ function createPeerConnection() {
|
|||||||
// Wait for first frame to be decoded before hiding connecting overlay
|
// Wait for first frame to be decoded before hiding connecting overlay
|
||||||
hideConnectingOverlayOnFirstFrame();
|
hideConnectingOverlayOnFirstFrame();
|
||||||
} else {
|
} else {
|
||||||
|
// Additional track: add to existing stream
|
||||||
elements.video.srcObject.addTrack(track);
|
elements.video.srcObject.addTrack(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!elements.displaySelect) return;
|
if (!elements.displaySelect) return;
|
||||||
const trackId = track.id || "";
|
|
||||||
if (!trackId) return;
|
// Remove placeholder option "候选画面 ID..." when first track arrives
|
||||||
|
if (currentIndex === 0) {
|
||||||
|
const placeholderOption = Array.from(elements.displaySelect.options).find(
|
||||||
|
(opt) => opt.value === ""
|
||||||
|
);
|
||||||
|
if (placeholderOption) {
|
||||||
|
placeholderOption.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if option with this index already exists
|
||||||
const existingOption = Array.from(elements.displaySelect.options).find(
|
const existingOption = Array.from(elements.displaySelect.options).find(
|
||||||
(opt) => opt.value === trackId
|
(opt) => opt.value === String(currentIndex)
|
||||||
);
|
);
|
||||||
if (!existingOption) {
|
if (!existingOption) {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = trackId;
|
option.value = String(currentIndex);
|
||||||
option.textContent = trackId;
|
option.textContent = track.id || `Display ${currentIndex}`;
|
||||||
elements.displaySelect.appendChild(option);
|
elements.displaySelect.appendChild(option);
|
||||||
}
|
}
|
||||||
elements.displaySelect.value = trackId;
|
// Only set default value for the first track (index 0)
|
||||||
|
// Don't auto-switch when additional tracks arrive
|
||||||
|
if (currentIndex === 0 && !elements.displaySelect.value) {
|
||||||
|
elements.displaySelect.value = String(currentIndex);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
peer.ondatachannel = (event) => {
|
peer.ondatachannel = (event) => {
|
||||||
@@ -398,6 +443,12 @@ function disconnect() {
|
|||||||
updateStatus(elements.iceState, "");
|
updateStatus(elements.iceState, "");
|
||||||
updateStatus(elements.signalingState, "");
|
updateStatus(elements.signalingState, "");
|
||||||
updateStatus(elements.dataChannelState, "closed");
|
updateStatus(elements.dataChannelState, "closed");
|
||||||
|
// Reset track index and clear display select options
|
||||||
|
trackIndex = 0;
|
||||||
|
trackMap.clear();
|
||||||
|
if (elements.displaySelect) {
|
||||||
|
elements.displaySelect.innerHTML = '<option value="" selected>候选画面 ID...</option>';
|
||||||
|
}
|
||||||
// Reset status LEDs and hide indicator
|
// Reset status LEDs and hide indicator
|
||||||
updateStatusLed(elements.connectionStatusLed, false, true);
|
updateStatusLed(elements.connectionStatusLed, false, true);
|
||||||
updateStatusLed(elements.connectedStatusLed, false, false);
|
updateStatusLed(elements.connectedStatusLed, false, false);
|
||||||
@@ -455,6 +506,11 @@ function teardownPeerConnection() {
|
|||||||
elements.video.srcObject.getTracks().forEach((track) => track.stop());
|
elements.video.srcObject.getTracks().forEach((track) => track.stop());
|
||||||
elements.video.srcObject = null;
|
elements.video.srcObject = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (elements.audio?.srcObject) {
|
||||||
|
elements.audio.srcObject.getTracks().forEach((track) => track.stop());
|
||||||
|
elements.audio.srcObject = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStatus(element, value) {
|
function updateStatus(element, value) {
|
||||||
@@ -497,10 +553,33 @@ function enableDataChannelUi(enabled) {
|
|||||||
function setDisplayId() {
|
function setDisplayId() {
|
||||||
if (!elements.displaySelect) return;
|
if (!elements.displaySelect) return;
|
||||||
const raw = elements.displaySelect.value.trim();
|
const raw = elements.displaySelect.value.trim();
|
||||||
if (!raw) return;
|
if (!raw) {
|
||||||
|
// 如果值为空,不发送(保持原有行为)
|
||||||
|
return;
|
||||||
|
}
|
||||||
const parsed = parseInt(raw, 10);
|
const parsed = parseInt(raw, 10);
|
||||||
const numericValue = Number.isFinite(parsed) ? parsed : 0;
|
// 检查解析结果:如果解析失败(NaN)或者不是有效数字,不发送
|
||||||
control.sendDisplayId(numericValue);
|
if (isNaN(parsed) || !Number.isFinite(parsed)) {
|
||||||
|
console.warn("setDisplayId: Invalid display_id value:", raw);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch video track to the selected display_id
|
||||||
|
const selectedTrack = trackMap.get(parsed);
|
||||||
|
if (selectedTrack && elements.video) {
|
||||||
|
// Don't stop tracks - just replace the stream
|
||||||
|
// Stopping tracks makes them unusable
|
||||||
|
const newStream = new MediaStream([selectedTrack]);
|
||||||
|
elements.video.srcObject = newStream;
|
||||||
|
elements.video.muted = true;
|
||||||
|
elements.video.setAttribute("playsinline", "true");
|
||||||
|
elements.video.setAttribute("webkit-playsinline", "true");
|
||||||
|
elements.video.setAttribute("x5-video-player-type", "h5");
|
||||||
|
elements.video.setAttribute("x5-video-player-fullscreen", "true");
|
||||||
|
elements.video.autoplay = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
control.sendDisplayId(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1142,7 +1221,7 @@ if (elements.connectedOverlay) {
|
|||||||
|
|
||||||
document.addEventListener("touchmove", onTouchMove);
|
document.addEventListener("touchmove", onTouchMove);
|
||||||
document.addEventListener("touchend", onTouchEnd);
|
document.addEventListener("touchend", onTouchEnd);
|
||||||
});
|
}, { passive: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user