360 lines
8.9 KiB
JavaScript
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;
|
|
}
|
|
}
|