Files
simple-instant-stream/index.html
T
2026-06-14 01:06:25 +01:00

120 lines
4.1 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WHEP Viewer</title>
<style>
body { margin: 0; background: #111; color: #eee; font-family: monospace; }
#video { display: block; width: 100%; max-width: 800px; }
#stats {
max-width: 800px;
padding: 8px 12px;
background: rgba(0,0,0,0.6);
font-size: 13px;
line-height: 1.8;
}
.label { color: #aaa; }
.value { color: #7ef; font-weight: bold; }
.warn { color: #fa0; }
.bad { color: #f44; }
</style>
</head>
<body>
<video id="video" autoplay muted playsinline></video>
<div id="stats">Waiting for stream…</div>
<script>
const fmt = ms => ms < 0 ? '—' : ms.toFixed(1) + ' ms';
const colorClass = ms => ms < 0 ? '' : ms < 80 ? 'value' : ms < 200 ? 'warn' : 'bad';
let prevStats = null;
async function pollStats(pc) {
const reports = await pc.getStats();
let inbound = null;
let networkRttMs = -1;
reports.forEach(r => {
if (r.type === 'inbound-rtp' && r.kind === 'video') inbound = r;
// The nominated ICE candidate pair carries the active STUN ping RTT.
if (r.type === 'candidate-pair' && r.nominated && r.currentRoundTripTime != null) {
networkRttMs = r.currentRoundTripTime * 1000;
}
});
if (!inbound) return;
// Network one-way: ICE STUN ping RTT / 2.
const networkOneWayMs = networkRttMs >= 0 ? networkRttMs / 2 : -1;
// Jitter buffer latency: average time a packet waits before being emitted to decoder.
const jitterMs = inbound.jitterBufferEmittedCount > 0
? (inbound.jitterBufferDelay / inbound.jitterBufferEmittedCount) * 1000
: -1;
// Decode latency: average time spent decoding each frame.
const decodeMs = inbound.framesDecoded > 0
? (inbound.totalDecodeTime / inbound.framesDecoded) * 1000
: -1;
// Server → client: network transit + jitter buffer + decode.
const serverToClientMs =
(networkOneWayMs >= 0 && jitterMs >= 0 && decodeMs >= 0)
? networkOneWayMs + jitterMs + decodeMs
: -1;
// Frames per second (received from network, before decode).
const fps = inbound.framesPerSecond ?? -1;
// Packets lost ratio.
const totalPkts = (inbound.packetsReceived || 0) + (inbound.packetsLost || 0);
const lossRatio = totalPkts > 0
? ((inbound.packetsLost || 0) / totalPkts * 100).toFixed(1) + '%'
: '—';
const el = document.getElementById('stats');
const row = (label, val, cls) =>
`<span class="label">${label}:</span> <span class="${cls}">${val}</span>`;
el.innerHTML = [
row('Server → Client', fmt(serverToClientMs), colorClass(serverToClientMs)),
row(' Network (1-way)', fmt(networkOneWayMs), colorClass(networkOneWayMs)),
row(' Jitter buffer', fmt(jitterMs), colorClass(jitterMs)),
row(' Decode', fmt(decodeMs), colorClass(decodeMs)),
row('FPS', fps >= 0 ? fps.toFixed(1) : '—', 'value'),
row('Packet loss', lossRatio, 'value'),
row('Jitter', fmt(inbound.jitter * 1000), colorClass(inbound.jitter * 1000)),
].join('<br>');
prevStats = inbound;
}
const start = async () => {
window.pc = new RTCPeerConnection();
const pc = window.pc;
pc.addTransceiver('video', { direction: 'recvonly' });
pc.ontrack = (e) => {
const video = document.getElementById('video');
video.srcObject = new MediaStream([e.track]);
video.play().catch(err => console.error('play() failed:', err));
// Start polling once we have a track.
setInterval(() => pollStats(pc), 500);
};
pc.oniceconnectionstatechange = () => console.log('ice state:', pc.iceConnectionState);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const res = await fetch('http://localhost:5000/whep/test', {
method: 'POST',
headers: { 'Content-Type': 'application/sdp' },
body: offer.sdp,
});
const answer = await res.text();
await pc.setRemoteDescription({ type: 'answer', sdp: answer });
};
start();
</script>
</body>
</html>