[refactor] consolidate WebRTC logic, add config, remove duplicates

This commit is contained in:
dijunkun
2025-11-07 17:21:34 +08:00
parent c5462b7a8e
commit 7a8314ee85
5 changed files with 1376 additions and 1431 deletions

1234
control.js

File diff suppressed because it is too large Load Diff

View File

@@ -14,37 +14,36 @@
<!-- Connection settings + status -->
<div class="top-card">
<div class="connection-card">
<h2>Connection Settings</h2>
<h2>连接设置</h2>
<div class="option">
<label for="transmission-id">Transmission ID:</label>
<label for="transmission-id">远程设备 ID:</label>
<input id="transmission-id" type="text" value="931631602" />
</div>
<div class="option">
<label for="transmission-pwd">Password:</label>
<label for="transmission-pwd">密码:</label>
<input id="transmission-pwd" type="password" value="111111" />
</div>
<div class="actions">
<button id="connect" onclick="connect()" disabled>Connect</button>
<button id="connect" disabled>连接</button>
<button
id="disconnect"
style="display: none"
onclick="disconnect()"
>
Disconnect
断开连接
</button>
</div>
</div>
<div class="status-card">
<h2>Status</h2>
<h2>状态</h2>
<div class="status-item">
ICE Connection State: <span id="ice-connection-state"></span>
ICE状态: <span id="ice-connection-state"></span>
</div>
<div class="status-item">
Signaling State: <span id="signaling-state"></span>
信令状态: <span id="signaling-state"></span>
</div>
<div class="status-item">
Data Channel: <span id="datachannel-state"></span>
数据通道: <span id="datachannel-state"></span>
</div>
</div>
</div>
@@ -54,18 +53,18 @@
<div class="option" style="margin-bottom: 12px">
<label for="display-id">显示器 ID: </label>
<select id="display-id" style="width: 160px" disabled>
<option value="" selected>Waiting for track ID...</option>
<option value="" selected>候选画面 ID...</option>
</select>
<button
id="set-display"
disabled
onclick="CrossDeskControl.setDisplayId()"
>
Set
<button id="set-display" disabled>
设置
</button>
<!-- Add fullscreen control buttons -->
<button id="fullscreen-btn" class="control-btn">Maximize</button>
<button id="real-fullscreen-btn" class="control-btn">Fullscreen</button>
<button id="fullscreen-btn" class="control-btn">
最大化
</button>
<button id="real-fullscreen-btn" class="control-btn">
全屏
</button>
</div>
<!-- Video container, including draggable virtual mouse -->
@@ -82,12 +81,23 @@
<!-- Draggable virtual mouse -->
<div id="virtual-mouse">
<button id="virtual-left-btn" class="virtual-mouse-btn">
Left
</button>
<button id="virtual-right-btn" class="virtual-mouse-btn">
Right
</button>
<div class="virtual-mouse-top">
<button id="virtual-left-btn" class="virtual-mouse-btn">
左键
</button>
<div id="virtual-wheel" class="virtual-mouse-wheel">滚轮</div>
<button id="virtual-right-btn" class="virtual-mouse-btn">
右键
</button>
</div>
<div
id="virtual-touchpad"
class="virtual-mouse-touchpad"
style="touch-action: none"
>
触摸板区域 (请在此区域滑动控制鼠标)
</div>
<div id="virtual-mouse-drag-handle" class="drag-handle"></div>
</div>
</div>
</div>
@@ -95,9 +105,9 @@
<!-- Data Channel UI -->
<div class="top-card">
<div class="connection-card">
<h2>Data Channel</h2>
<h2>数据通道</h2>
<div class="option">
<label for="audio-capture">Capture Audio:</label>
<label for="audio-capture">音频捕获:</label>
<input id="audio-capture" type="checkbox" disabled />
</div>
@@ -115,15 +125,11 @@
<input
id="dc-input"
type="text"
placeholder="Enter message to send"
placeholder="输入信息发送"
style="flex: 1; padding: 8px"
/>
<button
id="dc-send"
onclick="CrossDeskControl.sendDataChannelMessage()"
disabled
>
Send
<button id="dc-send" disabled>
发送
</button>
</div>
</div>
@@ -131,6 +137,6 @@
</div>
<script src="control.js"></script>
<script src="webrtc_client.js"></script>
<script src="web_client.js"></script>
</body>
</html>

View File

@@ -3,13 +3,22 @@
--danger-color: #f44336;
--background-color: #f5f5f5;
--border-radius: 8px;
--card-background: #ffffff;
--text-color: #333333;
--secondary-text-color: #666666;
--border-color: #dddddd;
--hover-color: #1976d2;
--danger-hover-color: #d32f2f;
--disabled-color: #cccccc;
--status-background: #f8f9fa;
}
body {
margin: 0;
font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, sans-serif;
background-color: var(--background-color);
color: #333;
color: var(--text-color);
line-height: 1.6;
}
.container {
@@ -19,12 +28,15 @@ body {
display: flex;
flex-direction: column;
gap: 20px;
min-height: 100vh;
}
h1 {
header h1 {
text-align: center;
color: var(--primary-color);
margin-bottom: 10px;
margin: 0 0 10px 0;
font-size: 2.5rem;
font-weight: 400;
}
/* Connection settings + status card */
@@ -36,48 +48,73 @@ h1 {
.connection-card,
.status-card {
background: white;
background: var(--card-background);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
flex: 1 1 300px;
transition: box-shadow 0.3s ease;
}
.connection-card:hover,
.status-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.connection-card h2,
.status-card h2 {
margin-top: 0;
color: #444;
font-weight: 500;
font-size: 1.5rem;
}
.option {
display: flex;
align-items: center;
margin-bottom: 15px;
gap: 10px;
}
.option label {
width: 120px;
color: #666;
color: var(--secondary-text-color);
font-weight: 500;
}
.option input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
transition: border-color 0.2s, box-shadow 0.2s;
min-width: 0;
}
.option input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2);
}
/* Unify dropdown and button height and style */
.option select {
padding: 10px 12px;
border: 1px solid #ddd;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
background-color: #fff;
background-color: var(--card-background);
height: 40px;
/* Consistent visual height with buttons */
box-sizing: border-box;
transition: border-color 0.2s, box-shadow 0.2s;
}
.option select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2);
}
.option select:disabled {
@@ -89,6 +126,8 @@ h1 {
.actions {
display: flex;
gap: 10px;
margin-top: 10px;
flex-wrap: wrap;
}
button {
@@ -99,34 +138,46 @@ button {
font-weight: 500;
color: white;
background-color: var(--primary-color);
transition: background-color 0.2s;
transition: background-color 0.2s, transform 0.1s;
font-size: 14px;
min-width: 80px;
display: inline-flex;
align-items: center;
justify-content: center;
}
button:hover {
background-color: #1976d2;
button:hover:not(:disabled) {
background-color: var(--hover-color);
transform: translateY(-1px);
}
button:active:not(:disabled) {
transform: translateY(0);
}
button:disabled {
background-color: #ccc;
background-color: var(--disabled-color);
cursor: not-allowed;
transform: none;
}
#disconnect {
background-color: var(--danger-color);
}
#disconnect:hover {
background-color: #d32f2f;
#disconnect:hover:not(:disabled) {
background-color: var(--danger-hover-color);
}
/* Status display */
.status-item {
padding: 10px;
background: #f8f9fa;
padding: 12px 15px;
background: var(--status-background);
border-radius: 4px;
border: 1px solid #ddd;
border: 1px solid var(--border-color);
margin-bottom: 10px;
transition: background-color 0.5s;
word-break: break-all;
}
/* Status change animation */
@@ -140,7 +191,7 @@ button:disabled {
}
50% {
background-color: #f8f9fa;
background-color: var(--status-background);
}
100% {
@@ -154,25 +205,32 @@ button:disabled {
border-radius: var(--border-radius);
padding: 20px;
position: relative;
transition: all 0.3s ease;
}
video {
width: 100%;
border-radius: 4px;
display: block;
}
#data-channel {
background: #f8f9fa;
border: 1px solid #ddd;
background: var(--status-background);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 15px;
font-family: monospace;
height: 200px;
overflow-y: auto;
white-space: pre-wrap;
resize: vertical;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.top-card {
flex-direction: column;
}
@@ -185,6 +243,18 @@ video {
width: 100%;
margin-bottom: 5px;
}
header h1 {
font-size: 2rem;
}
.actions {
flex-direction: column;
}
.actions button {
width: 100%;
}
}
/* Virtual mouse related styles */
@@ -195,21 +265,37 @@ video {
#virtual-mouse {
position: absolute;
top: 50%;
bottom: 20px;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
gap: 10px;
transform: translateX(-50%);
z-index: 10;
padding: 10px;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 30px;
border-radius: 15px;
touch-action: none;
min-width: 300px;
display: flex;
flex-direction: column;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
}
#virtual-mouse.minimized {
background-color: rgba(0, 0, 0, 0.7);
padding: 5px;
}
.virtual-mouse-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
gap: 10px;
}
.virtual-mouse-btn {
width: 60px;
height: 60px;
width: 70px;
height: 70px;
border-radius: 50%;
border: 2px solid white;
background-color: rgba(255, 255, 255, 0.9);
@@ -223,6 +309,8 @@ video {
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
user-select: none;
touch-action: manipulation;
flex: 1;
transition: all 0.1s ease;
}
.virtual-mouse-btn:active {
@@ -230,6 +318,83 @@ video {
transform: scale(0.95);
}
.virtual-mouse-wheel {
width: 50px;
height: 50px;
border-radius: 25px;
background-color: #ddd;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #333;
user-select: none;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
flex: 1;
transition: background-color 0.2s;
}
.virtual-mouse-wheel:active {
background-color: #ccc;
}
.virtual-mouse-touchpad {
height: 100px;
background-color: rgba(200, 200, 200, 0.5);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: #666;
font-size: 16px;
user-select: none;
margin-top: 5px;
border: 1px solid #aaa;
transition: background-color 0.2s;
}
.virtual-mouse-touchpad:active {
background-color: rgba(180, 180, 180, 0.5);
}
#virtual-mouse.minimized .virtual-mouse-top,
#virtual-mouse.minimized .virtual-mouse-touchpad {
display: none;
}
.drag-handle {
position: absolute;
top: -10px;
right: -10px;
width: 30px;
height: 30px;
border-radius: 50%;
background-color: #4CAF50;
cursor: move;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 18px;
transition: background-color 0.2s;
}
.drag-handle:hover {
background-color: #45a049;
}
.drag-handle::after {
content: "≡";
}
#virtual-mouse.minimized .drag-handle {
position: static;
width: 40px;
height: 40px;
}
/* Control button styles */
.control-btn {
margin-left: 10px;
@@ -242,10 +407,11 @@ video {
font-size: 14px;
height: 40px;
box-sizing: border-box;
transition: background-color 0.2s;
}
.control-btn:hover {
background-color: #1976d2;
background-color: var(--hover-color);
}
/* Fullscreen styles */
@@ -276,4 +442,30 @@ video {
#virtual-mouse {
display: none;
}
}
/* Focus styles for accessibility */
button:focus,
input:focus,
select:focus {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
/* Scrollbar styling */
#data-channel::-webkit-scrollbar {
width: 8px;
}
#data-channel::-webkit-scrollbar-track {
background: #f1f1f1;
}
#data-channel::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
#data-channel::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,186 +0,0 @@
// WebRTC + WebSocket 信令模块
(function () {
const iceConnectionLog = document.getElementById('ice-connection-state');
const signalingLog = document.getElementById('signaling-state');
let clientId = '000000';
const websocket = new WebSocket('wss://api.crossdesk.cn:9090');
let pc = null;
let heartbeatInterval = null;
let lastPongTime = Date.now();
function startHeartbeat() {
stopHeartbeat();
lastPongTime = Date.now();
heartbeatInterval = setInterval(() => {
if (websocket.readyState === WebSocket.OPEN) {
websocket.send(JSON.stringify({ type: 'ping', ts: Date.now() }));
}
if (Date.now() - lastPongTime > 10000) {
console.warn('WebSocket heartbeat timeout, reconnecting...');
stopHeartbeat();
reconnectWebSocket();
}
}, 3000);
}
function stopHeartbeat() { if (heartbeatInterval) { clearInterval(heartbeatInterval); heartbeatInterval = null; } }
websocket.addEventListener('message', (evt) => { lastPongTime = Date.now(); });
function reconnectWebSocket() {
try { websocket.close(); } catch (e) { }
console.log('Reconnecting WebSocket...');
setTimeout(() => { window.location.reload(); }, 2000);
}
websocket.onopen = () => {
document.getElementById('connect').disabled = false;
sendLogin();
startHeartbeat();
};
websocket.onmessage = async (evt) => {
if (typeof evt.data !== 'string') return;
const message = JSON.parse(evt.data);
if (message.type == 'login') {
clientId = message.user_id.split('@')[0];
console.log('Logged in as: ' + clientId);
} else if (message.type == 'offer') {
await handleOffer(message);
} else if (message.type == 'new_candidate_mid') {
if (!pc) { console.warn('PeerConnection not exist when adding candidate'); }
else {
const candidate = new RTCIceCandidate({ sdpMid: message.mid, candidate: message.candidate });
pc.addIceCandidate(candidate).catch(e => { console.error('Error adding received ice candidate', e); });
}
}
};
function createPeerConnection() {
const config = {};
config.iceServers = [{ urls: ['stun:api.crossdesk.cn:3478'] }, { urls: ['turn:api.crossdesk.cn:3478'], username: 'crossdesk', credential: 'crossdeskpw' }];
config.iceTransportPolicy = 'all';
pc = new RTCPeerConnection(config);
pc.addEventListener('iceconnectionstatechange', () => iceConnectionLog.textContent += ' -> ' + pc.iceConnectionState);
iceConnectionLog.textContent = pc.iceConnectionState;
pc.addEventListener('signalingstatechange', () => signalingLog.textContent += ' -> ' + pc.signalingState);
signalingLog.textContent = pc.signalingState;
pc.onicecandidate = function (event) {
var ice_candidate = event.candidate;
if (!ice_candidate) return;
websocket.send(JSON.stringify({ type: 'new_candidate_mid', transmission_id: getTransmissionId(), user_id: clientId, remote_user_id: getTransmissionId(), candidate: ice_candidate.candidate, mid: ice_candidate.sdpMid }));
};
pc.ontrack = (evt) => {
const video = document.getElementById('video');
if (evt.track.kind !== 'video') return;
// Record track ID and fill it back to the display ID input field
window.CROSSDESK_TRACK_ID = evt.track.id || '';
const displayIdInput = document.getElementById('display-id');
if (displayIdInput) {
if (displayIdInput.tagName === 'SELECT') {
const sel = displayIdInput;
const tid = window.CROSSDESK_TRACK_ID;
if (tid) {
let exists = false;
for (let i = 0; i < sel.options.length; i++) {
if (sel.options[i].value === tid) { exists = true; break; }
}
if (!exists) {
const opt = document.createElement('option');
opt.value = tid; opt.textContent = tid; sel.appendChild(opt);
}
sel.value = tid;
}
} else {
displayIdInput.value = window.CROSSDESK_TRACK_ID;
}
}
if (!video.srcObject) {
const stream = evt.streams && evt.streams[0] ? evt.streams[0] : new MediaStream([evt.track]);
video.setAttribute('playsinline', true);
video.setAttribute('webkit-playsinline', true);
video.setAttribute('x5-video-player-type', 'h5');
video.setAttribute('x5-video-player-fullscreen', 'true');
video.setAttribute('autoplay', true);
video.muted = true;
video.srcObject = stream;
} else {
video.srcObject.addTrack(evt.track);
}
};
pc.ondatachannel = (evt) => {
const dc = evt.channel;
dc.onopen = () => {
console.log('Data channel opened');
if (window.CrossDeskControl && window.CrossDeskControl.onDataChannelOpen) {
window.CrossDeskControl.onDataChannelOpen(dc);
}
};
dc.onmessage = (evt) => { if (typeof evt.data !== 'string') return; console.log('Received datachannel message: ' + evt.data); };
dc.onclose = () => {
if (window.CrossDeskControl && window.CrossDeskControl.onDataChannelClose) {
window.CrossDeskControl.onDataChannelClose();
}
};
};
return pc;
}
function waitGatheringComplete() { return new Promise((resolve) => { if (pc.iceGatheringState === 'complete') { resolve(); } else { pc.addEventListener('icegatheringstatechange', () => { if (pc.iceGatheringState === 'complete') { resolve(); } }); } }); }
async function sendAnswer(pc) {
await pc.setLocalDescription(await pc.createAnswer());
await waitGatheringComplete();
const answer = pc.localDescription;
websocket.send(JSON.stringify({ type: 'answer', transmission_id: getTransmissionId(), user_id: clientId, remote_user_id: getTransmissionId(), sdp: answer.sdp }));
}
async function handleOffer(offer) { pc = createPeerConnection(); await pc.setRemoteDescription(offer); await sendAnswer(pc); }
function sendLogin() { websocket.send(JSON.stringify({ type: 'login', user_id: 'web' })); }
function leaveTransmission() { websocket.send(JSON.stringify({ type: 'leave_transmission', user_id: clientId, transmission_id: getTransmissionId(), })); }
function getTransmissionId() { return document.getElementById('transmission-id').value; }
function getTransmissionPwd() { return document.getElementById('transmission-pwd').value; }
function sendRequest() { websocket.send(JSON.stringify({ type: 'join_transmission', user_id: clientId, transmission_id: getTransmissionId() + '@' + getTransmissionPwd(), })); }
function connect() {
document.getElementById('connect').style.display = 'none';
document.getElementById('disconnect').style.display = 'inline-block';
document.getElementById('media').style.display = 'block';
sendRequest();
}
function disconnect() {
document.getElementById('disconnect').style.display = 'none';
document.getElementById('media').style.display = 'none';
document.getElementById('connect').style.display = 'inline-block';
leaveTransmission();
if (pc) {
try {
// Close local sending tracks
pc.getSenders().forEach((sender) => { const track = sender.track; if (track !== null) { sender.track.stop(); } });
} catch (e) { }
pc.close();
pc = null;
}
const video = document.getElementById('video');
if (video && video.srcObject) { video.srcObject.getTracks().forEach(track => track.stop()); video.srcObject = null; }
iceConnectionLog.textContent = ''; signalingLog.textContent = '';
if (window.CrossDeskControl && window.CrossDeskControl.onDataChannelClose) { window.CrossDeskControl.onDataChannelClose(); }
}
// Expose connection control functions
window.connect = connect;
window.disconnect = disconnect;
})();