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) {
|
||||
// 确保 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 = {
|
||||
type: ControlType.display_id,
|
||||
display_id: id | 0,
|
||||
display_id: numericId | 0,
|
||||
};
|
||||
this.send(action);
|
||||
}
|
||||
@@ -299,6 +305,11 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if clicking inside panel area
|
||||
if (this.isInsidePanel(event.clientX, event.clientY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if dragging panel
|
||||
if (this.state.draggingPanel) {
|
||||
return;
|
||||
@@ -353,6 +364,11 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if moving inside panel area
|
||||
if (this.isInsidePanel(event.clientX, event.clientY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if dragging panel
|
||||
if (this.state.draggingPanel) {
|
||||
return;
|
||||
@@ -451,6 +467,12 @@
|
||||
}
|
||||
|
||||
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") {
|
||||
this.elements.video?.releasePointerCapture?.(event.pointerId ?? 0);
|
||||
@@ -485,6 +507,11 @@
|
||||
if (now - this.state.lastWheelAt < 50) return;
|
||||
this.state.lastWheelAt = now;
|
||||
|
||||
// Skip if wheeling inside panel area
|
||||
if (this.isInsidePanel(event.clientX, event.clientY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ensureVideoRect();
|
||||
if (!this.state.videoRect) return;
|
||||
|
||||
@@ -517,12 +544,18 @@
|
||||
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)
|
||||
if (this.state.pinchZoomActive || this.state.draggingPanel || event.touches.length === 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const touch = event.touches[0];
|
||||
event.preventDefault();
|
||||
|
||||
// 移动端模式下,触摸视频区域不触发点击事件
|
||||
@@ -554,12 +587,18 @@
|
||||
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)
|
||||
if (this.state.pinchZoomActive || this.state.draggingPanel || event.touches.length === 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const touch = event.touches[0];
|
||||
event.preventDefault();
|
||||
|
||||
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) {
|
||||
if (!this.state.videoRect) return;
|
||||
this.state.normalizedPos = {
|
||||
@@ -1136,7 +1187,7 @@
|
||||
if (keyboardToggleMouse) {
|
||||
keyboardToggleMouse.addEventListener("touchstart", (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}, { passive: true });
|
||||
keyboardToggleMouse.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
@@ -1146,7 +1197,7 @@
|
||||
if (this.elements.virtualMouseMinimize) {
|
||||
this.elements.virtualMouseMinimize.addEventListener("touchstart", (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}, { passive: true });
|
||||
this.elements.virtualMouseMinimize.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
@@ -1174,7 +1225,7 @@
|
||||
if (keyboardClose) {
|
||||
keyboardClose.addEventListener("touchstart", (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}, { passive: true });
|
||||
keyboardClose.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
12
index.html
12
index.html
@@ -10,6 +10,7 @@
|
||||
<title>CrossDesk Web Client</title>
|
||||
|
||||
<!-- 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-status-bar-style"
|
||||
@@ -51,18 +52,20 @@
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<form>
|
||||
<div class="option">
|
||||
<label for="transmission-id">远程设备 ID:</label>
|
||||
<input id="transmission-id" type="text" value="" />
|
||||
<input id="transmission-id" type="text" value="" autocomplete="off" />
|
||||
</div>
|
||||
<div class="option">
|
||||
<label for="transmission-pwd">密码:</label>
|
||||
<input id="transmission-pwd" type="password" value="" />
|
||||
<input id="transmission-pwd" type="password" value="" autocomplete="current-password" />
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="connect" disabled>连接</button>
|
||||
<button id="disconnect" style="display: none">断开连接</button>
|
||||
<button id="connect" type="button" disabled>连接</button>
|
||||
<button id="disconnect" type="button" style="display: none">断开连接</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,6 +81,7 @@
|
||||
x5-video-player-fullscreen="true"
|
||||
muted
|
||||
></video>
|
||||
<audio id="audio" autoplay></audio>
|
||||
|
||||
<!-- Connecting overlay -->
|
||||
<div id="connecting-overlay" class="connecting-overlay" style="display: none">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "any",
|
||||
"sizes": "32x32",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ const elements = {
|
||||
disconnectBtn: document.getElementById("disconnect"),
|
||||
media: document.getElementById("media"),
|
||||
video: document.getElementById("video"),
|
||||
audio: document.getElementById("audio"),
|
||||
connectionOverlay: document.getElementById("connection-overlay"),
|
||||
connectedOverlay: document.getElementById("connected-overlay"),
|
||||
connectedPanel: document.getElementById("connected-panel"),
|
||||
@@ -38,6 +39,8 @@ let pc = null;
|
||||
let clientId = "000000";
|
||||
let heartbeatTimer = null;
|
||||
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);
|
||||
|
||||
@@ -218,8 +221,36 @@ function createPeerConnection() {
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
// 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) {
|
||||
// First track: create new stream
|
||||
const stream = streams && streams[0] ? streams[0] : new MediaStream([track]);
|
||||
elements.video.srcObject = stream;
|
||||
elements.video.muted = true;
|
||||
@@ -232,23 +263,37 @@ function createPeerConnection() {
|
||||
// Wait for first frame to be decoded before hiding connecting overlay
|
||||
hideConnectingOverlayOnFirstFrame();
|
||||
} else {
|
||||
// Additional track: add to existing stream
|
||||
elements.video.srcObject.addTrack(track);
|
||||
}
|
||||
|
||||
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(
|
||||
(opt) => opt.value === trackId
|
||||
(opt) => opt.value === String(currentIndex)
|
||||
);
|
||||
if (!existingOption) {
|
||||
const option = document.createElement("option");
|
||||
option.value = trackId;
|
||||
option.textContent = trackId;
|
||||
option.value = String(currentIndex);
|
||||
option.textContent = track.id || `Display ${currentIndex}`;
|
||||
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) => {
|
||||
@@ -398,6 +443,12 @@ function disconnect() {
|
||||
updateStatus(elements.iceState, "");
|
||||
updateStatus(elements.signalingState, "");
|
||||
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
|
||||
updateStatusLed(elements.connectionStatusLed, false, true);
|
||||
updateStatusLed(elements.connectedStatusLed, false, false);
|
||||
@@ -455,6 +506,11 @@ function teardownPeerConnection() {
|
||||
elements.video.srcObject.getTracks().forEach((track) => track.stop());
|
||||
elements.video.srcObject = null;
|
||||
}
|
||||
|
||||
if (elements.audio?.srcObject) {
|
||||
elements.audio.srcObject.getTracks().forEach((track) => track.stop());
|
||||
elements.audio.srcObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(element, value) {
|
||||
@@ -497,10 +553,33 @@ function enableDataChannelUi(enabled) {
|
||||
function setDisplayId() {
|
||||
if (!elements.displaySelect) return;
|
||||
const raw = elements.displaySelect.value.trim();
|
||||
if (!raw) return;
|
||||
if (!raw) {
|
||||
// 如果值为空,不发送(保持原有行为)
|
||||
return;
|
||||
}
|
||||
const parsed = parseInt(raw, 10);
|
||||
const numericValue = Number.isFinite(parsed) ? parsed : 0;
|
||||
control.sendDisplayId(numericValue);
|
||||
// 检查解析结果:如果解析失败(NaN)或者不是有效数字,不发送
|
||||
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("touchend", onTouchEnd);
|
||||
});
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user