This commit is contained in:
dijunkun
2025-11-11 21:44:11 +08:00
4 changed files with 164 additions and 30 deletions

View File

@@ -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();
}); });

View File

@@ -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">

View File

@@ -10,7 +10,7 @@
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
"sizes": "any", "sizes": "32x32",
"type": "image/x-icon" "type": "image/x-icon"
}, },
{ {

View File

@@ -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 });
} }