<!DOCTYPE html>
<html>
<head>
<title>RustRTC Echo</title>
<style>
body {
font-family: sans-serif;
padding: 20px;
}
#log {
background: #f0f0f0;
padding: 10px;
border: 1px solid #ccc;
height: 300px;
overflow-y: scroll;
}
</style>
</head>
<body>
<h1>RustRTC Echo Demo</h1>
<div>
<label for="modeSelect">Mode:</label>
<select id="modeSelect">
<option value="echo">Data Channel + Local Cam Echo</option>
<option value="datachannel">Data Channel Only</option>
<option value="video">Data Channel + Remote Video</option>
<option value="video-only">Remote Video Only</option>
</select>
<button onclick="start()" id="startBtn">Start Connection</button>
</div>
<div style="margin-top: 10px;">
<input type="text" id="msgInput" placeholder="Type a message..." disabled>
<button onclick="sendMsg()" id="sendBtn" disabled>Send</button>
</div>
<div style="margin-top: 10px; padding: 5px; background: #e0e0e0;">
<strong>ICE Connection State:</strong> <span id="iceState">new</span> |
<strong>ICE Gathering State:</strong> <span id="gatherState">new</span> |
<strong>Signaling State:</strong> <span id="signalingState">stable</span>
</div>
<div style="display: flex; margin-top: 10px;">
<div style="margin-right: 10px;">
<h3>Local Video</h3>
<video id="localVideo" autoplay playsinline muted
style="width: 320px; height: 240px; background: black;"></video>
</div>
<div>
<h3>Remote Video</h3>
<video id="remoteVideo" autoplay playsinline
style="width: 320px; height: 240px; background: black;"></video>
</div>
</div>
<div id="log" style="margin-top: 10px;"></div>
<script>
let dataChannel = null;
async function start() {
document.getElementById('startBtn').disabled = true;
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
const log = (msg) => {
const el = document.getElementById('log');
el.innerHTML += msg + '<br>';
el.scrollTop = el.scrollHeight;
console.log(msg);
};
const updateState = () => {
document.getElementById('iceState').textContent = pc.iceConnectionState;
document.getElementById('gatherState').textContent = pc.iceGatheringState;
document.getElementById('signalingState').textContent = pc.signalingState;
};
pc.oniceconnectionstatechange = () => {
log("ICE Connection State: " + pc.iceConnectionState);
updateState();
};
pc.onicegatheringstatechange = () => {
log("ICE Gathering State: " + pc.iceGatheringState);
updateState();
};
pc.onsignalingstatechange = () => {
log("Signaling State: " + pc.signalingState);
updateState();
};
pc.ontrack = (e) => {
log("Received remote track: " + e.track.kind);
if (e.track.kind === 'video') {
document.getElementById('remoteVideo').srcObject = e.streams[0];
}
};
const mode = document.getElementById('modeSelect').value;
if (mode === 'echo' || mode === 'video') {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
document.getElementById('localVideo').srcObject = stream;
stream.getTracks().forEach(track => pc.addTrack(track, stream));
log("Added local video track");
} catch (e) {
log("Could not get local video: " + e);
pc.addTransceiver('video', { direction: 'recvonly' });
log("Added video transceiver (recvonly) as fallback");
}
} else if (mode === 'video-only') {
pc.addTransceiver('video', { direction: 'recvonly' });
log("Added video transceiver (recvonly)");
}
if (mode !== 'video-only') {
const dc = pc.createDataChannel("echo", { negotiated: true, id: 0 });
dataChannel = dc;
dc.onopen = () => {
log("DataChannel open");
document.getElementById('msgInput').disabled = false;
document.getElementById('sendBtn').disabled = false;
document.getElementById('msgInput').focus();
};
dc.onmessage = (e) => {
log("Received: " + e.data);
};
}
pc.onicecandidate = (e) => {
if (e.candidate) {
log("New ICE candidate: " + e.candidate.candidate);
} else {
log("End of candidates.");
}
};
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
log("Gathering candidates...");
await new Promise(resolve => {
if (pc.iceGatheringState === 'complete') {
resolve();
} else {
const check = () => {
if (pc.iceGatheringState === 'complete') {
pc.removeEventListener('icegatheringstatechange', check);
resolve();
}
};
pc.addEventListener('icegatheringstatechange', check);
}
});
const offerSdp = pc.localDescription.sdp;
log("Sending Offer SDP: " + offerSdp);
log("Using mode: " + mode);
const response = await fetch('/offer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sdp: offerSdp,
type: pc.localDescription.type,
mode: mode
})
});
const answer = await response.json();
log("Received Answer SDP: " + answer.sdp);
await pc.setRemoteDescription(new RTCSessionDescription({
type: answer.type,
sdp: answer.sdp
}));
}
function sendMsg() {
const input = document.getElementById('msgInput');
const msg = input.value;
if (dataChannel && dataChannel.readyState === 'open' && msg) {
const log = (msg) => {
const el = document.getElementById('log');
el.innerHTML += msg + '<br>';
el.scrollTop = el.scrollHeight;
console.log(msg);
};
log("Sending: " + msg);
dataChannel.send(msg);
input.value = '';
}
}
document.getElementById('msgInput').addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
sendMsg();
}
});
</script>
</body>
</html>