diff --git a/index.html b/index.html index 86c28b7..4c049e7 100644 --- a/index.html +++ b/index.html @@ -12,13 +12,9 @@

连接设置

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