Files
crossdesk-web-client/web_client.js

360 lines
8.9 KiB
JavaScript

const iceConnectionLog = document.getElementById('ice-connection-state'),
signalingLog = document.getElementById('signaling-state'),
dataChannelLog = document.getElementById('datachannel-state');
clientId = '000000';
const websocket = new WebSocket('wss://api.crossdesk.cn:9090');
let pc = null;
let dc = 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.error('Error closing websocket:', 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 == null) {
console.warn('PeerConnection not exist when adding candidate');
} else {
// console.log('received new candidate: ' + message.candidate);
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);
// Register some listeners to help debugging
pc.addEventListener(
'iceconnectionstatechange',
() => iceConnectionLog.textContent += ' -> ' + pc.iceConnectionState);
iceConnectionLog.textContent = pc.iceConnectionState;
pc.addEventListener(
'signalingstatechange',
() => signalingLog.textContent += ' -> ' + pc.signalingState);
signalingLog.textContent = pc.signalingState;
// onicecandidate
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
}));
// console.log('sent new candidate: ' + ice_candidate.candidate);
};
// Receive audio/video track
// More robust handling of audio/video track
pc.ontrack = (evt) => {
const video = document.getElementById('video');
const trackIdEl = document.getElementById('track-id');
// Only handle video track
if (evt.track.kind !== 'video') return;
// Update track id display
if (trackIdEl) {
trackIdEl.textContent = evt.track.id ? `(${evt.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;
const playVideo = () => {
video.play().catch(err => {
console.warn('video.play() failed:', err);
setTimeout(playVideo, 1000);
});
};
console.log('Attached new video stream:', evt.track.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 = '';
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: 'web',
}));
console.log('Send login');
}
function leaveTransmission() {
websocket.send(JSON.stringify({
type: 'leave_transmission',
user_id: clientId,
transmission_id: getTransmissionId(),
}));
console.log('Leave transmission: ' + 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('Join transmission: ' + getTransmissionId());
}
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;
const video = document.getElementById('video');
if (video.srcObject) {
video.srcObject.getTracks().forEach(track => track.stop());
video.srcObject = null;
}
const trackIdEl = document.getElementById('track-id');
if (trackIdEl) {
trackIdEl.textContent = '';
}
iceConnectionLog.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;
}
}