initial
This commit is contained in:
182
examples/README.md
Normal file
182
examples/README.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# wl-webrtc Examples
|
||||
|
||||
This directory contains example client applications for connecting to wl-webrtc servers.
|
||||
|
||||
## client.html
|
||||
|
||||
A simple web-based client that demonstrates how to connect to a wl-webrtc server using WebRTC.
|
||||
|
||||
### Usage
|
||||
|
||||
1. **Start the wl-webrtc server:**
|
||||
```bash
|
||||
./target/release/wl-webrtc start
|
||||
```
|
||||
|
||||
2. **Open the client in a web browser:**
|
||||
- Directly open `examples/client.html` in a browser
|
||||
- Or serve it with a local web server:
|
||||
```bash
|
||||
# Python 3
|
||||
python -m http.server 8000
|
||||
# Then navigate to http://localhost:8000/examples/client.html
|
||||
```
|
||||
|
||||
3. **Connect to the server:**
|
||||
- Enter the server URL (default: `ws://localhost:8443`)
|
||||
- Click "Connect"
|
||||
- Grant screen capture permissions when prompted by PipeWire
|
||||
- The video stream will appear in the browser
|
||||
|
||||
### Features
|
||||
|
||||
- **WebRTC Connection**: Real-time video streaming via WebRTC
|
||||
- **Status Monitoring**: Display connection status, bitrate, resolution, FPS, and latency
|
||||
- **Fullscreen Mode**: Press `Alt+F` or click "Fullscreen" button
|
||||
- **Error Handling**: Displays connection errors with helpful messages
|
||||
- **Responsive Design**: Works on desktop and mobile browsers
|
||||
|
||||
### Configuration
|
||||
|
||||
The client connects to a wl-webrtc server via WebSocket. By default, it connects to:
|
||||
- **Server URL**: `ws://localhost:8443`
|
||||
|
||||
To connect to a remote server:
|
||||
- Change the server URL in the input field
|
||||
- Ensure the server is accessible (firewall, NAT, etc.)
|
||||
- Configure STUN/TURN servers in the wl-webrtc config if needed
|
||||
|
||||
### Limitations
|
||||
|
||||
This is a minimal example client. Production clients should include:
|
||||
- Authentication and authorization
|
||||
- Secure HTTPS/WSS connections
|
||||
- Input event forwarding (mouse, keyboard)
|
||||
- Clipboard sharing
|
||||
- Audio support
|
||||
- Multiple screen selection
|
||||
- Connection quality indicators
|
||||
- Automatic reconnection
|
||||
|
||||
### Browser Compatibility
|
||||
|
||||
Tested on modern browsers with WebRTC support:
|
||||
- Chrome 88+
|
||||
- Firefox 85+
|
||||
- Safari 15+
|
||||
- Edge 88+
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Connection fails:**
|
||||
- Verify wl-webrtc server is running
|
||||
- Check server URL is correct
|
||||
- Check firewall settings
|
||||
- Review browser console for errors
|
||||
|
||||
**No video appears:**
|
||||
- Grant screen capture permissions
|
||||
- Check if PipeWire is running
|
||||
- Verify encoder is configured correctly
|
||||
- Check server logs for errors
|
||||
|
||||
**Poor quality:**
|
||||
- Increase bitrate in server config
|
||||
- Use hardware encoder (h264_vaapi or h264_nvenc)
|
||||
- Check network bandwidth
|
||||
- Reduce resolution or frame rate
|
||||
|
||||
**High latency:**
|
||||
- Check network ping to server
|
||||
- Use wired connection instead of WiFi
|
||||
- Reduce frame rate in server config
|
||||
- Use faster preset (ultrafast)
|
||||
- Ensure hardware encoder is being used
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Client Implementation
|
||||
|
||||
To build your own client:
|
||||
|
||||
1. **Connect via WebSocket** to the signaling server
|
||||
2. **Create WebRTC PeerConnection** with ICE servers
|
||||
3. **Create Offer** and send to server via WebSocket
|
||||
4. **Receive Answer** and set as remote description
|
||||
5. **Exchange ICE candidates** with server
|
||||
6. **Receive video track** and display in HTML video element
|
||||
|
||||
Example signaling flow:
|
||||
```
|
||||
Client Server
|
||||
| |
|
||||
|-------- WebSocket ----->|
|
||||
| |
|
||||
|------- Offer --------->|
|
||||
|<------ Answer ---------|
|
||||
| |
|
||||
|--- ICE Candidate ----->|
|
||||
|<--- ICE Candidate -----|
|
||||
| |
|
||||
|<---- Video Stream -----|
|
||||
| |
|
||||
```
|
||||
|
||||
### Data Channel Usage
|
||||
|
||||
The server supports WebRTC data channels for bi-directional messaging:
|
||||
|
||||
```javascript
|
||||
// Client side
|
||||
const dataChannel = peerConnection.createDataChannel('control');
|
||||
|
||||
dataChannel.onopen = () => {
|
||||
console.log('Data channel opened');
|
||||
};
|
||||
|
||||
dataChannel.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
console.log('Received:', message);
|
||||
};
|
||||
|
||||
// Send control messages
|
||||
dataChannel.send(JSON.stringify({
|
||||
type: 'mouse_move',
|
||||
x: 100,
|
||||
y: 200
|
||||
}));
|
||||
```
|
||||
|
||||
### Server-Side Events
|
||||
|
||||
The server sends events over the data channel:
|
||||
|
||||
- `connection_established`: Connection is ready
|
||||
- `connection_failed`: Connection failed
|
||||
- `stats_update`: Performance statistics
|
||||
- `error`: Error occurred
|
||||
|
||||
## Security Considerations
|
||||
|
||||
For production use:
|
||||
|
||||
1. **Use HTTPS/WSS** instead of HTTP/WS
|
||||
2. **Implement authentication** (tokens, certificates)
|
||||
3. **Validate all input** from clients
|
||||
4. **Rate limit** connections
|
||||
5. **Monitor for abuse**
|
||||
6. **Keep dependencies updated**
|
||||
7. **Use firewall rules** to restrict access
|
||||
8. **Enable TLS** for encryption
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [wl-webrtc README](../README.md) - Main project documentation
|
||||
- [DESIGN_CN.md](../DESIGN_CN.md) - Technical design and architecture
|
||||
- [config.toml.template](../config.toml.template) - Server configuration reference
|
||||
- [WebRTC API](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API) - Browser WebRTC documentation
|
||||
- [webrtc-rs](https://webrtc.rs/) - WebRTC implementation in Rust
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions and improvements to the examples are welcome! Please follow the main project's [CONTRIBUTING.md](../CONTRIBUTING.md) guidelines.
|
||||
600
examples/client.html
Normal file
600
examples/client.html
Normal file
@@ -0,0 +1,600 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>wl-webrtc - Remote Desktop Client</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #2d2d2d;
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #404040;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.5rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge.disconnected {
|
||||
background: #dc2626;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.status-badge.connecting {
|
||||
background: #ea580c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.status-badge.connected {
|
||||
background: #16a34a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
background: #0a0a0a;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#remoteVideo {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc2626;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #b91c1c;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6b7280;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.connection-info {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.config-form {
|
||||
background: #2d2d2d;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.config-form h2 {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #404040;
|
||||
border-radius: 4px;
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #dc2626;
|
||||
color: #fff;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error-message.visible {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>wl-webrtc</h1>
|
||||
<div class="status">
|
||||
<span id="connectionStatus" class="status-badge disconnected">Disconnected</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div id="errorMessage" class="error-message"></div>
|
||||
|
||||
<div id="configForm" class="config-form">
|
||||
<h2>Connect to Server</h2>
|
||||
<div class="form-group">
|
||||
<label for="serverUrl">Server URL</label>
|
||||
<input type="text" id="serverUrl" value="ws://localhost:8443" placeholder="ws://localhost:8443">
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button id="connectBtn" class="btn-primary">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="videoContainer" class="video-container hidden">
|
||||
<video id="remoteVideo" autoplay playsinline></video>
|
||||
</div>
|
||||
|
||||
<div id="connectionInfo" class="connection-info hidden">
|
||||
<div class="info-item">
|
||||
<div class="label">Bitrate</div>
|
||||
<div id="bitrate" class="value">0 Kbps</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">Resolution</div>
|
||||
<div id="resolution" class="value">0x0</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">FPS</div>
|
||||
<div id="fps" class="value">0</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">Latency</div>
|
||||
<div id="latency" class="value">0 ms</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="controls" class="controls hidden">
|
||||
<button id="disconnectBtn" class="btn-danger">Disconnect</button>
|
||||
<button id="fullscreenBtn" class="btn-secondary">Fullscreen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// State
|
||||
let peerConnection = null;
|
||||
let dataChannel = null;
|
||||
let ws = null;
|
||||
let statsInterval = null;
|
||||
let frameCount = 0;
|
||||
let lastFrameTime = Date.now();
|
||||
let rttInterval = null;
|
||||
let rttSamples = [];
|
||||
|
||||
// DOM Elements
|
||||
const connectBtn = document.getElementById('connectBtn');
|
||||
const disconnectBtn = document.getElementById('disconnectBtn');
|
||||
const fullscreenBtn = document.getElementById('fullscreenBtn');
|
||||
const serverUrlInput = document.getElementById('serverUrl');
|
||||
const connectionStatus = document.getElementById('connectionStatus');
|
||||
const remoteVideo = document.getElementById('remoteVideo');
|
||||
const configForm = document.getElementById('configForm');
|
||||
const videoContainer = document.getElementById('videoContainer');
|
||||
const connectionInfo = document.getElementById('connectionInfo');
|
||||
const controls = document.getElementById('controls');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
|
||||
// Stats Elements
|
||||
const bitrateEl = document.getElementById('bitrate');
|
||||
const resolutionEl = document.getElementById('resolution');
|
||||
const fpsEl = document.getElementById('fps');
|
||||
const latencyEl = document.getElementById('latency');
|
||||
|
||||
// Configuration
|
||||
const config = {
|
||||
iceServers: [
|
||||
{ urls: 'stun:stun.l.google.com:19302' }
|
||||
]
|
||||
};
|
||||
|
||||
// Update connection status
|
||||
function updateStatus(status) {
|
||||
connectionStatus.className = `status-badge ${status}`;
|
||||
connectionStatus.textContent = status.charAt(0).toUpperCase() + status.slice(1);
|
||||
}
|
||||
|
||||
// Show error message
|
||||
function showError(message) {
|
||||
errorMessage.textContent = message;
|
||||
errorMessage.classList.add('visible');
|
||||
setTimeout(() => {
|
||||
errorMessage.classList.remove('visible');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Create WebRTC peer connection
|
||||
function createPeerConnection() {
|
||||
peerConnection = new RTCPeerConnection(config);
|
||||
|
||||
// Handle ICE candidates
|
||||
peerConnection.onicecandidate = (event) => {
|
||||
if (event.candidate) {
|
||||
sendJson({
|
||||
type: 'ice-candidate',
|
||||
candidate: event.candidate
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Handle connection state changes
|
||||
peerConnection.onconnectionstatechange = () => {
|
||||
console.log('Connection state:', peerConnection.connectionState);
|
||||
switch (peerConnection.connectionState) {
|
||||
case 'connected':
|
||||
updateStatus('connected');
|
||||
startStatsMonitoring();
|
||||
break;
|
||||
case 'disconnected':
|
||||
updateStatus('connecting');
|
||||
break;
|
||||
case 'failed':
|
||||
updateStatus('disconnected');
|
||||
showError('Connection failed');
|
||||
cleanup();
|
||||
break;
|
||||
case 'closed':
|
||||
updateStatus('disconnected');
|
||||
cleanup();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle incoming tracks
|
||||
peerConnection.ontrack = (event) => {
|
||||
console.log('Received track:', event.track.kind);
|
||||
if (event.track.kind === 'video') {
|
||||
remoteVideo.srcObject = event.streams[0];
|
||||
videoContainer.classList.remove('hidden');
|
||||
connectionInfo.classList.remove('hidden');
|
||||
controls.classList.remove('hidden');
|
||||
configForm.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
// Create data channel for control messages
|
||||
dataChannel = peerConnection.createDataChannel('control');
|
||||
dataChannel.onopen = () => {
|
||||
console.log('Data channel opened');
|
||||
};
|
||||
dataChannel.onmessage = (event) => {
|
||||
console.log('Data channel message:', event.data);
|
||||
};
|
||||
|
||||
return peerConnection;
|
||||
}
|
||||
|
||||
// Send JSON over WebSocket
|
||||
function sendJson(data) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to server
|
||||
async function connect() {
|
||||
const url = serverUrlInput.value;
|
||||
console.log('Connecting to:', url);
|
||||
|
||||
updateStatus('connecting');
|
||||
connectBtn.disabled = true;
|
||||
|
||||
try {
|
||||
// Create WebSocket connection
|
||||
ws = new WebSocket(url);
|
||||
|
||||
ws.onopen = async () => {
|
||||
console.log('WebSocket connected');
|
||||
|
||||
// Create peer connection
|
||||
createPeerConnection();
|
||||
|
||||
// Create offer
|
||||
const offer = await peerConnection.createOffer();
|
||||
await peerConnection.setLocalDescription(offer);
|
||||
|
||||
// Send offer to server
|
||||
sendJson({
|
||||
type: 'offer',
|
||||
sdp: offer
|
||||
});
|
||||
};
|
||||
|
||||
ws.onmessage = async (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
console.log('Received:', message);
|
||||
|
||||
switch (message.type) {
|
||||
case 'answer':
|
||||
await peerConnection.setRemoteDescription(new RTCSessionDescription(message.sdp));
|
||||
break;
|
||||
|
||||
case 'ice-candidate':
|
||||
await peerConnection.addIceCandidate(new RTCIceCandidate(message.candidate));
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
showError(message.error);
|
||||
updateStatus('disconnected');
|
||||
cleanup();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
showError('WebSocket error. Check server is running.');
|
||||
updateStatus('disconnected');
|
||||
connectBtn.disabled = false;
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket closed');
|
||||
if (connectionStatus.classList.contains('connecting')) {
|
||||
updateStatus('disconnected');
|
||||
showError('Server closed the connection');
|
||||
}
|
||||
connectBtn.disabled = false;
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Connection error:', error);
|
||||
showError('Failed to connect: ' + error.message);
|
||||
updateStatus('disconnected');
|
||||
connectBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect from server
|
||||
function disconnect() {
|
||||
console.log('Disconnecting...');
|
||||
cleanup();
|
||||
configForm.classList.remove('hidden');
|
||||
videoContainer.classList.add('hidden');
|
||||
connectionInfo.classList.add('hidden');
|
||||
controls.classList.add('hidden');
|
||||
remoteVideo.srcObject = null;
|
||||
connectBtn.disabled = false;
|
||||
}
|
||||
|
||||
// Cleanup resources
|
||||
function cleanup() {
|
||||
if (statsInterval) {
|
||||
clearInterval(statsInterval);
|
||||
statsInterval = null;
|
||||
}
|
||||
if (rttInterval) {
|
||||
clearInterval(rttInterval);
|
||||
rttInterval = null;
|
||||
}
|
||||
if (dataChannel) {
|
||||
dataChannel.close();
|
||||
dataChannel = null;
|
||||
}
|
||||
if (peerConnection) {
|
||||
peerConnection.close();
|
||||
peerConnection = null;
|
||||
}
|
||||
if (ws) {
|
||||
ws.close();
|
||||
ws = null;
|
||||
}
|
||||
rttSamples = [];
|
||||
}
|
||||
|
||||
// Start statistics monitoring
|
||||
function startStatsMonitoring() {
|
||||
statsInterval = setInterval(async () => {
|
||||
if (!peerConnection || peerConnection.connectionState !== 'connected') {
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = await peerConnection.getStats();
|
||||
let bitrate = 0;
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
|
||||
stats.forEach(report => {
|
||||
if (report.type === 'inbound-rtp' && report.mediaType === 'video') {
|
||||
// Calculate bitrate
|
||||
if (report.bytesReceived && report.bytesReceivedLast) {
|
||||
const bytesPerSec = report.bytesReceived - report.bytesReceivedLast;
|
||||
bitrate = (bytesPerSec * 8) / 1000; // kbps
|
||||
}
|
||||
report.bytesReceivedLast = report.bytesReceived;
|
||||
|
||||
// Resolution
|
||||
width = report.frameWidth || 0;
|
||||
height = report.frameHeight || 0;
|
||||
|
||||
// FPS
|
||||
if (report.framesReceived && report.framesReceivedLast) {
|
||||
const framesPerSec = report.framesReceived - report.framesReceivedLast;
|
||||
fpsEl.textContent = framesPerSec.toFixed(1);
|
||||
}
|
||||
report.framesReceivedLast = report.framesReceived;
|
||||
}
|
||||
|
||||
if (report.type === 'remote-candidate' && report.currentRoundTripTime) {
|
||||
rttSamples.push(report.currentRoundTripTime * 1000); // Convert to ms
|
||||
if (rttSamples.length > 10) rttSamples.shift();
|
||||
const avgRtt = rttSamples.reduce((a, b) => a + b, 0) / rttSamples.length;
|
||||
latencyEl.textContent = avgRtt.toFixed(0) + ' ms';
|
||||
}
|
||||
});
|
||||
|
||||
bitrateEl.textContent = bitrate.toFixed(0) + ' Kbps';
|
||||
resolutionEl.textContent = `${width}x${height}`;
|
||||
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Toggle fullscreen
|
||||
function toggleFullscreen() {
|
||||
if (!document.fullscreenElement) {
|
||||
remoteVideo.requestFullscreen().catch(err => {
|
||||
showError(`Fullscreen error: ${err.message}`);
|
||||
});
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
connectBtn.addEventListener('click', connect);
|
||||
disconnectBtn.addEventListener('click', disconnect);
|
||||
fullscreenBtn.addEventListener('click', toggleFullscreen);
|
||||
|
||||
// Handle window close
|
||||
window.addEventListener('beforeunload', () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'f' && event.altKey) {
|
||||
toggleFullscreen();
|
||||
}
|
||||
if (event.key === 'Escape' && document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
});
|
||||
|
||||
// Update video stats on frame
|
||||
remoteVideo.addEventListener('play', () => {
|
||||
frameCount = 0;
|
||||
lastFrameTime = Date.now();
|
||||
});
|
||||
|
||||
remoteVideo.addEventListener('timeupdate', () => {
|
||||
frameCount++;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user