Files
crossdesk-web-client/web_client.js
2025-11-05 00:05:08 +08:00

362 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const iceConnectionLog = document.getElementById('ice-connection-state'),
iceGatheringLog = document.getElementById('ice-gathering-state'),
signalingLog = document.getElementById('signaling-state'),
dataChannelLog = document.getElementById('data-channel');
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();
}
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") {
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);
});
}
}
}
let pc = null;
let dc = null;
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);
console.log("Created RTCPeerConnection");
// Register some listeners to help debugging
pc.addEventListener('iceconnectionstatechange', () =>
iceConnectionLog.textContent += ' -> ' + pc.iceConnectionState);
iceConnectionLog.textContent = pc.iceConnectionState;
pc.addEventListener('icegatheringstatechange', () =>
iceGatheringLog.textContent += ' -> ' + pc.iceGatheringState);
iceGatheringLog.textContent = pc.iceGatheringState;
pc.addEventListener('signalingstatechange', () =>
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) => {
console.log('ontrack event:', evt);
const video = document.getElementById('video');
// Only handle video track
if (evt.track.kind !== 'video') return;
if (!video.srcObject) {
const stream = evt.streams && evt.streams[0] ? evt.streams[0] : new MediaStream([evt.track]);
// 设置视频属性
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.srcObject = stream;
// 确保在用户交互后播放
const playVideo = () => {
video.play().catch(err => {
console.warn('video.play() failed:', err);
// 重试播放
setTimeout(playVideo, 1000);
});
};
// 延迟执行播放
setTimeout(playVideo, 100);
console.log('attached new video stream:', stream.id);
} else {
video.srcObject.addTrack(evt.track);
console.log('added track to existing stream:', evt.track.id);
}
};
// Receive data channel
pc.ondatachannel = (evt) => {
dc = evt.channel;
dc.onopen = () => {
dataChannelLog.textContent += '- open\n';
dataChannelLog.scrollTop = dataChannelLog.scrollHeight;
};
let dcTimeout = null;
dc.onmessage = (evt) => {
if (typeof evt.data !== 'string') {
return;
}
dataChannelLog.textContent += '< ' + evt.data + '\n';
dataChannelLog.scrollTop = dataChannelLog.scrollHeight;
dcTimeout = setTimeout(() => {
if (!dc) {
return;
}
const message = `Pong ${currentTimestamp()}`;
dataChannelLog.textContent += '> ' + message + '\n';
dataChannelLog.scrollTop = dataChannelLog.scrollHeight;
dc.send(message);
}, 1000);
}
dc.onclose = () => {
clearTimeout(dcTimeout);
dcTimeout = null;
dataChannelLog.textContent += '- close\n';
dataChannelLog.scrollTop = dataChannelLog.scrollHeight;
};
}
return pc;
}
async 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;
msg = JSON.stringify({
type: "answer",
transmission_id: getTransmissionId(),
user_id: clientId,
remote_user_id: getTransmissionId(),
sdp: answer.sdp,
});
// console.log("send answer: " + msg);
websocket.send(msg);
}
async function handleOffer(offer) {
pc = createPeerConnection();
await pc.setRemoteDescription(offer);
await sendAnswer(pc);
}
function sendLogin() {
websocket.send(JSON.stringify({
type: "login",
user_id: "",
}));
console.log("send login");
}
function leaveTransmission() {
websocket.send(JSON.stringify({
type: "leave_transmission",
user_id: clientId,
transmission_id: getTransmissionId(),
}));
}
function getTransmissionId() {
return document.getElementById('transmission-id').value;
}
// Add function to get password
function getTransmissionPwd() {
return document.getElementById('transmission-pwd').value;
}
// Modify sendRequest function to use dynamic password
function sendRequest() {
websocket.send(JSON.stringify({
type: "join_transmission",
user_id: clientId,
transmission_id: getTransmissionId() + '@' + getTransmissionPwd(),
}));
console.log("sent join_transmission");
}
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();
// close data channel
if (dc) {
dc.close();
dc = null;
}
// close transceivers
if (pc.getTransceivers) {
pc.getTransceivers().forEach((transceiver) => {
if (transceiver.stop) {
transceiver.stop();
}
});
}
// close local audio/video
pc.getSenders().forEach((sender) => {
const track = sender.track;
if (track !== null) {
sender.track.stop();
}
});
// 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';
}
// Helper function to generate a timestamp
let startTime = null;
function currentTimestamp() {
if (startTime === null) {
startTime = Date.now();
return 0;
} else {
return Date.now() - startTime;
}
}