[refactor] consolidate WebRTC logic, add config, remove duplicates
This commit is contained in:
1234
control.js
1234
control.js
File diff suppressed because it is too large
Load Diff
76
index.html
76
index.html
@@ -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>
|
||||
|
||||
250
styles.css
250
styles.css
@@ -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;
|
||||
}
|
||||
1061
web_client.js
1061
web_client.js
File diff suppressed because it is too large
Load Diff
186
webrtc_client.js
186
webrtc_client.js
@@ -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;
|
||||
})();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user