@@ -46,7 +42,14 @@
远程画面
-
+
diff --git a/web_client.js b/web_client.js
index 221a7f5..4b551ae 100644
--- a/web_client.js
+++ b/web_client.js
@@ -6,6 +6,57 @@ const iceConnectionLog = document.getElementById('ice-connection-state'),
clientId = "000000";
const websocket = new WebSocket('wss://api.crossdesk.cn:9090');
+// ===== WebSocket 心跳机制 =====
+let heartbeatInterval = null;
+let lastPongTime = Date.now();
+
+function startHeartbeat() {
+ stopHeartbeat(); // 避免重复定时
+ lastPongTime = Date.now();
+
+ // 每30秒发一次心跳
+ heartbeatInterval = setInterval(() => {
+ if (websocket.readyState === WebSocket.OPEN) {
+ websocket.send(JSON.stringify({ type: "ping", ts: Date.now() }));
+ console.log("sent ping");
+ }
+
+ // 如果90秒内没收到任何消息,认为连接断开
+ if (Date.now() - lastPongTime > 10000) {
+ console.warn("WebSocket heartbeat timeout, reconnecting...");
+ stopHeartbeat();
+ reconnectWebSocket();
+ }
+ }, 5000);
+}
+
+function stopHeartbeat() {
+ if (heartbeatInterval) {
+ clearInterval(heartbeatInterval);
+ heartbeatInterval = null;
+ }
+}
+
+// 监听服务器返回消息时更新时间
+websocket.addEventListener("message", (evt) => {
+ lastPongTime = Date.now(); // 收到任何消息都视为活跃
+});
+
+// 自动重连逻辑
+function reconnectWebSocket() {
+ try {
+ websocket.close();
+ } catch (e) {
+ console.error("Error closing websocket:", e);
+ }
+
+ console.log("Reconnecting WebSocket...");
+ setTimeout(() => {
+ window.location.reload(); // 简单策略:刷新页面重连
+ // 或者重新 new WebSocket('wss://api.crossdesk.cn:9090'),并重新注册事件
+ }, 2000);
+}
+
websocket.onopen = () => {
document.getElementById('connect').disabled = false;
sendLogin();
@@ -22,6 +73,16 @@ websocket.onmessage = async (evt) => {
} else if (message.type == "offer") {
await handleOffer(message)
+ } else if (message.type == "new_candidate") {
+ if (pc) {
+ const candidate = new RTCIceCandidate({
+ sdpMid: message.mid,
+ candidate: message.candidate
+ });
+ pc.addIceCandidate(candidate).catch(e => {
+ console.error("Error adding received ice candidate", e);
+ });
+ }
}
}
@@ -30,14 +91,21 @@ let dc = null;
function createPeerConnection() {
const config = {
- bundlePolicy: "max-bundle",
};
- if (document.getElementById('use-stun').checked) {
- config.iceServers = [{ urls: ['stun:api.crossdesk.cn:3478'] }];
- }
+ config.iceServers = [
+ { urls: ['stun:api.crossdesk.cn:3478'] },
+ {
+ urls: ['turn:api.crossdesk.cn:3478'],
+ username: 'crossdesk',
+ credential: 'crossdeskpw'
+ }
+ ];
- let pc = new RTCPeerConnection(config);
+ config.iceTransportPolicy = "all";
+
+ pc = new RTCPeerConnection(config);
+ console.log("Created RTCPeerConnection");
// Register some listeners to help debugging
pc.addEventListener('iceconnectionstatechange', () =>
@@ -52,6 +120,22 @@ function createPeerConnection() {
signalingLog.textContent += ' -> ' + pc.signalingState);
signalingLog.textContent = pc.signalingState;
+ // onicecandidate
+ pc.onicecandidate = function (event) {
+ var ice_candidate = event.candidate;
+ if(ice_candidate) {
+ 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
+ }));
+ console.log("sent new candidate: " + ice_candidate.candidate);
+ }
+ };
+
// Receive audio/video track
// More robust handling of audio/video track
pc.ontrack = (evt) => {
@@ -61,21 +145,33 @@ function createPeerConnection() {
// Only handle video track
if (evt.track.kind !== 'video') return;
- // Don't reset srcObject if stream already exists
if (!video.srcObject) {
const stream = evt.streams && evt.streams[0] ? evt.streams[0] : new MediaStream([evt.track]);
- video.srcObject = stream;
+
+ // 设置视频属性
+ video.setAttribute('playsinline', true); // iOS 内联播放
+ video.setAttribute('webkit-playsinline', true); // 旧版 iOS webkit 内核
+ video.setAttribute('x5-video-player-type', 'h5'); // 微信浏览器
+ video.setAttribute('x5-video-player-fullscreen', 'true');
+ video.setAttribute('autoplay', true);
video.muted = true;
- video.playsInline = true;
- // Set delayed playback
- setTimeout(() => {
+
+ video.srcObject = stream;
+
+ // 确保在用户交互后播放
+ const playVideo = () => {
video.play().catch(err => {
console.warn('video.play() failed:', err);
+ // 重试播放
+ setTimeout(playVideo, 1000);
});
- }, 500);
+ };
+
+ // 延迟执行播放
+ setTimeout(playVideo, 100);
+
console.log('attached new video stream:', stream.id);
} else {
- // Add track directly to existing stream
video.srcObject.addTrack(evt.track);
console.log('added track to existing stream:', evt.track.id);
}
@@ -149,7 +245,7 @@ async function sendAnswer(pc) {
remote_user_id: getTransmissionId(),
sdp: answer.sdp,
});
- console.log("send answer: " + msg);
+ // console.log("send answer: " + msg);
websocket.send(msg);
}
@@ -192,6 +288,7 @@ function sendRequest() {
user_id: clientId,
transmission_id: getTransmissionId() + '@' + getTransmissionPwd(),
}));
+ console.log("sent join_transmission");
}
function connect() {
@@ -234,6 +331,19 @@ function disconnect() {
// close peer connection
pc.close();
pc = null;
+
+ // 清空 video
+ const video = document.getElementById('video');
+ if (video.srcObject) {
+ video.srcObject.getTracks().forEach(track => track.stop());
+ video.srcObject = null;
+ }
+
+ // 清空日志
+ iceConnectionLog.textContent = '';
+ iceGatheringLog.textContent = '';
+ signalingLog.textContent = '';
+ dataChannelLog.textContent += '- disconnected\n';
}
@@ -248,3 +358,4 @@ function currentTimestamp() {
}
}
+