mirror of
https://github.com/xtr-dev/rondevu-client.git
synced 2025-12-10 10:53:24 +00:00
Add ServiceHost, ServiceClient, and RondevuService for high-level service management
- Add RondevuService: High-level API for username claiming and service publishing with Ed25519 signatures - Add ServiceHost: Manages offer pool for hosting services with auto-replacement - Add ServiceClient: Connects to hosted services with automatic reconnection - Add NoOpSignaler: Placeholder signaler for connection setup - Integrate Ed25519 signature functionality from @noble/ed25519 - Add ESLint and Prettier configuration with 4-space indentation - Add demo with local signaling test - Version bump to 0.10.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
141
demo/README.md
Normal file
141
demo/README.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Rondevu WebRTC Local Test
|
||||
|
||||
Simple side-by-side demo for testing `WebRTCRondevuConnection` with **local signaling** (no server required).
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Opens browser at `http://localhost:3000`
|
||||
|
||||
## How It Works
|
||||
|
||||
This demo uses **local in-memory signaling** to test WebRTC connections between two peers on the same page. The `LocalSignaler` class simulates a signaling server by directly exchanging ICE candidates and SDP between peers.
|
||||
|
||||
### Architecture
|
||||
|
||||
- **LocalSignaler**: Implements the `Signaler` interface with local peer-to-peer communication
|
||||
- **Host (Peer A)**: Creates the offer (offerer role)
|
||||
- **Client (Peer B)**: Receives the offer and creates answer (answerer role)
|
||||
- **ICE Exchange**: Candidates are automatically exchanged between peers through the linked signalers
|
||||
|
||||
## Usage Steps
|
||||
|
||||
1. **Create Host** (Peer A)
|
||||
- Click "1️⃣ Create Host Connection" on the left side
|
||||
- The host will create an offer and display it
|
||||
- Status changes to "Connecting"
|
||||
|
||||
2. **Create Client** (Peer B)
|
||||
- Click "2️⃣ Create Client Connection" on the right side
|
||||
- The client receives the host's offer automatically
|
||||
- Creates an answer and sends it back to the host
|
||||
- Both peers exchange ICE candidates
|
||||
|
||||
3. **Connection Established**
|
||||
- Watch the status indicators turn green ("Connected")
|
||||
- Activity logs show the connection progress
|
||||
|
||||
4. **Send Messages**
|
||||
- Type a message in either peer's input field
|
||||
- Click "📤 Send" or press Enter
|
||||
- Messages appear in the other peer's activity log
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **No signaling server required** - Everything runs locally
|
||||
- ✅ **Automatic ICE candidate exchange** - Signalers handle candidate exchange
|
||||
- ✅ **Real-time activity logs** - See exactly what's happening
|
||||
- ✅ **Connection state indicators** - Visual feedback for connection status
|
||||
- ✅ **Bidirectional messaging** - Send messages in both directions
|
||||
|
||||
## Code Structure
|
||||
|
||||
### demo.js
|
||||
|
||||
```javascript
|
||||
// LocalSignaler - Implements local signaling
|
||||
class LocalSignaler {
|
||||
addIceCandidate(candidate) // Called when local peer has a candidate
|
||||
addListener(callback) // Listen for remote candidates
|
||||
linkTo(remoteSignaler) // Connect two signalers together
|
||||
}
|
||||
|
||||
// Create and link signalers
|
||||
const hostSignaler = new LocalSignaler('HOST', 'CLIENT')
|
||||
const clientSignaler = new LocalSignaler('CLIENT', 'HOST')
|
||||
hostSignaler.linkTo(clientSignaler)
|
||||
clientSignaler.linkTo(hostSignaler)
|
||||
|
||||
// Create connections
|
||||
const hostConnection = new WebRTCRondevuConnection({
|
||||
id: 'test-connection',
|
||||
host: 'client-peer',
|
||||
service: 'test.demo@1.0.0',
|
||||
offer: null, // No offer = offerer role
|
||||
context: new WebRTCContext(hostSignaler)
|
||||
})
|
||||
|
||||
const clientConnection = new WebRTCRondevuConnection({
|
||||
id: 'test-connection',
|
||||
host: 'host-peer',
|
||||
service: 'test.demo@1.0.0',
|
||||
offer: hostConnection.connection.localDescription, // With offer = answerer role
|
||||
context: new WebRTCContext(clientSignaler)
|
||||
})
|
||||
```
|
||||
|
||||
### index.html
|
||||
|
||||
- Side-by-side layout for Host and Client
|
||||
- Status indicators (disconnected/connecting/connected)
|
||||
- SDP display areas (offer/answer)
|
||||
- Message input and send buttons
|
||||
- Activity logs for each peer
|
||||
|
||||
## Debugging
|
||||
|
||||
Open the browser console to see detailed logs:
|
||||
|
||||
- `[HOST]` - Logs from the host connection
|
||||
- `[CLIENT]` - Logs from the client connection
|
||||
- ICE candidate exchange
|
||||
- Connection state changes
|
||||
- Message send/receive events
|
||||
|
||||
## Comparison: Local vs Remote Signaling
|
||||
|
||||
### Local Signaling (This Demo)
|
||||
```javascript
|
||||
const signaler = new LocalSignaler('HOST', 'CLIENT')
|
||||
signaler.linkTo(remoteSignaler) // Direct link
|
||||
```
|
||||
|
||||
**Pros**: No server, instant testing, no network latency
|
||||
**Cons**: Only works for same-page testing
|
||||
|
||||
### Remote Signaling (Production)
|
||||
```javascript
|
||||
const api = new RondevuAPI('https://api.ronde.vu', credentials)
|
||||
const signaler = new RondevuSignaler(api, offerId)
|
||||
```
|
||||
|
||||
**Pros**: Real peer discovery, works across networks
|
||||
**Cons**: Requires signaling server, network latency
|
||||
|
||||
## Next Steps
|
||||
|
||||
After testing locally, you can:
|
||||
|
||||
1. Switch to `RondevuSignaler` for real signaling server testing
|
||||
2. Test across different browsers/devices
|
||||
3. Test with STUN/TURN servers for NAT traversal
|
||||
4. Implement production signaling with Rondevu API
|
||||
|
||||
## Files
|
||||
|
||||
- `index.html` - UI layout and styling
|
||||
- `demo.js` - Local signaling implementation and WebRTC logic
|
||||
- `README.md` - This file
|
||||
304
demo/demo.js
Normal file
304
demo/demo.js
Normal file
@@ -0,0 +1,304 @@
|
||||
import { WebRTCRondevuConnection } from '../src/index.js'
|
||||
import { WebRTCContext } from '../src/webrtc-context.js'
|
||||
|
||||
// Local signaling implementation for testing
|
||||
class LocalSignaler {
|
||||
constructor(name, remoteName) {
|
||||
this.name = name
|
||||
this.remoteName = remoteName
|
||||
this.iceCandidates = []
|
||||
this.iceListeners = []
|
||||
this.remote = null
|
||||
this.remoteIceCandidates = []
|
||||
this.offerCallbacks = []
|
||||
this.answerCallbacks = []
|
||||
}
|
||||
|
||||
// Link two signalers together
|
||||
linkTo(remoteSignaler) {
|
||||
this.remote = remoteSignaler
|
||||
this.remoteIceCandidates = remoteSignaler.iceCandidates
|
||||
}
|
||||
|
||||
// Set local offer (called when offer is created)
|
||||
setOffer(offer) {
|
||||
console.log(`[${this.name}] Setting offer`)
|
||||
// Notify remote peer about the offer
|
||||
if (this.remote) {
|
||||
this.remote.offerCallbacks.forEach(callback => callback(offer))
|
||||
}
|
||||
}
|
||||
|
||||
// Set local answer (called when answer is created)
|
||||
setAnswer(answer) {
|
||||
console.log(`[${this.name}] Setting answer`)
|
||||
// Notify remote peer about the answer
|
||||
if (this.remote) {
|
||||
this.remote.answerCallbacks.forEach(callback => callback(answer))
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for offers from remote peer
|
||||
addOfferListener(callback) {
|
||||
this.offerCallbacks.push(callback)
|
||||
return () => {
|
||||
const index = this.offerCallbacks.indexOf(callback)
|
||||
if (index > -1) {
|
||||
this.offerCallbacks.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for answers from remote peer
|
||||
addAnswerListener(callback) {
|
||||
this.answerCallbacks.push(callback)
|
||||
return () => {
|
||||
const index = this.answerCallbacks.indexOf(callback)
|
||||
if (index > -1) {
|
||||
this.answerCallbacks.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add local ICE candidate (called by local connection)
|
||||
addIceCandidate(candidate) {
|
||||
console.log(`[${this.name}] Adding ICE candidate:`, candidate.candidate)
|
||||
this.iceCandidates.push(candidate)
|
||||
|
||||
// Immediately send to remote peer if linked
|
||||
if (this.remote) {
|
||||
setTimeout(() => {
|
||||
this.remote.iceListeners.forEach(listener => {
|
||||
console.log(`[${this.name}] Sending ICE to ${this.remoteName}`)
|
||||
listener(candidate)
|
||||
})
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for remote ICE candidates
|
||||
addListener(callback) {
|
||||
console.log(`[${this.name}] Adding ICE listener`)
|
||||
this.iceListeners.push(callback)
|
||||
|
||||
// Send any existing remote candidates
|
||||
this.remoteIceCandidates.forEach(candidate => {
|
||||
setTimeout(() => callback(candidate), 10)
|
||||
})
|
||||
|
||||
return () => {
|
||||
const index = this.iceListeners.indexOf(callback)
|
||||
if (index > -1) {
|
||||
this.iceListeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create signalers for host and client
|
||||
const hostSignaler = new LocalSignaler('HOST', 'CLIENT')
|
||||
const clientSignaler = new LocalSignaler('CLIENT', 'HOST')
|
||||
|
||||
// Link them together for bidirectional communication
|
||||
hostSignaler.linkTo(clientSignaler)
|
||||
clientSignaler.linkTo(hostSignaler)
|
||||
|
||||
// Store connections
|
||||
let hostConnection = null
|
||||
let clientConnection = null
|
||||
|
||||
// UI Update functions
|
||||
function updateStatus(peer, state) {
|
||||
const statusEl = document.getElementById(`status-${peer}`)
|
||||
if (statusEl) {
|
||||
statusEl.className = `status ${state}`
|
||||
statusEl.textContent = state.charAt(0).toUpperCase() + state.slice(1)
|
||||
}
|
||||
}
|
||||
|
||||
function addLog(peer, message) {
|
||||
const logEl = document.getElementById(`log-${peer}`)
|
||||
if (logEl) {
|
||||
const time = new Date().toLocaleTimeString()
|
||||
logEl.innerHTML += `<div class="log-entry">[${time}] ${message}</div>`
|
||||
logEl.scrollTop = logEl.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
// Create Host (Offerer)
|
||||
async function createHost() {
|
||||
try {
|
||||
addLog('a', 'Creating host connection (offerer)...')
|
||||
|
||||
const hostContext = new WebRTCContext(hostSignaler)
|
||||
|
||||
hostConnection = new WebRTCRondevuConnection({
|
||||
id: 'test-connection',
|
||||
host: 'client-peer',
|
||||
service: 'test.demo@1.0.0',
|
||||
offer: null,
|
||||
context: hostContext,
|
||||
})
|
||||
|
||||
// Listen for state changes
|
||||
hostConnection.events.on('state-change', state => {
|
||||
console.log('[HOST] State changed:', state)
|
||||
updateStatus('a', state)
|
||||
addLog('a', `State changed to: ${state}`)
|
||||
})
|
||||
|
||||
// Listen for messages
|
||||
hostConnection.events.on('message', message => {
|
||||
console.log('[HOST] Received message:', message)
|
||||
addLog('a', `📨 Received: ${message}`)
|
||||
})
|
||||
|
||||
addLog('a', '✅ Host connection created')
|
||||
updateStatus('a', 'connecting')
|
||||
|
||||
// Wait for host to be ready (offer created and set)
|
||||
await hostConnection.ready
|
||||
addLog('a', '✅ Host offer created')
|
||||
|
||||
// Get the offer
|
||||
const offer = hostConnection.connection.localDescription
|
||||
document.getElementById('offer-a').value = JSON.stringify(offer, null, 2)
|
||||
|
||||
addLog('a', 'Offer ready to send to client')
|
||||
} catch (error) {
|
||||
console.error('[HOST] Error:', error)
|
||||
addLog('a', `❌ Error: ${error.message}`)
|
||||
updateStatus('a', 'disconnected')
|
||||
}
|
||||
}
|
||||
|
||||
// Create Client (Answerer)
|
||||
async function createClient() {
|
||||
try {
|
||||
addLog('b', 'Creating client connection (answerer)...')
|
||||
|
||||
// Get offer from host
|
||||
if (!hostConnection) {
|
||||
alert('Please create host first!')
|
||||
return
|
||||
}
|
||||
|
||||
const offer = hostConnection.connection.localDescription
|
||||
if (!offer) {
|
||||
alert('Host offer not ready yet!')
|
||||
return
|
||||
}
|
||||
|
||||
addLog('b', 'Got offer from host')
|
||||
|
||||
const clientContext = new WebRTCContext(clientSignaler)
|
||||
|
||||
clientConnection = new WebRTCRondevuConnection({
|
||||
id: 'test-connection',
|
||||
host: 'host-peer',
|
||||
service: 'test.demo@1.0.0',
|
||||
offer: offer,
|
||||
context: clientContext,
|
||||
})
|
||||
|
||||
// Listen for state changes
|
||||
clientConnection.events.on('state-change', state => {
|
||||
console.log('[CLIENT] State changed:', state)
|
||||
updateStatus('b', state)
|
||||
addLog('b', `State changed to: ${state}`)
|
||||
})
|
||||
|
||||
// Listen for messages
|
||||
clientConnection.events.on('message', message => {
|
||||
console.log('[CLIENT] Received message:', message)
|
||||
addLog('b', `📨 Received: ${message}`)
|
||||
})
|
||||
|
||||
addLog('b', '✅ Client connection created')
|
||||
updateStatus('b', 'connecting')
|
||||
|
||||
// Wait for client to be ready
|
||||
await clientConnection.ready
|
||||
addLog('b', '✅ Client answer created')
|
||||
|
||||
// Get the answer
|
||||
const answer = clientConnection.connection.localDescription
|
||||
document.getElementById('answer-b').value = JSON.stringify(answer, null, 2)
|
||||
|
||||
// Set answer on host
|
||||
addLog('b', 'Setting answer on host...')
|
||||
await hostConnection.connection.setRemoteDescription(answer)
|
||||
addLog('b', '✅ Answer set on host')
|
||||
addLog('a', '✅ Answer received from client')
|
||||
} catch (error) {
|
||||
console.error('[CLIENT] Error:', error)
|
||||
addLog('b', `❌ Error: ${error.message}`)
|
||||
updateStatus('b', 'disconnected')
|
||||
}
|
||||
}
|
||||
|
||||
// Send test message from host to client
|
||||
function sendFromHost() {
|
||||
if (!hostConnection) {
|
||||
alert('Please create host first!')
|
||||
return
|
||||
}
|
||||
|
||||
const message = document.getElementById('message-a').value || 'Hello from Host!'
|
||||
addLog('a', `📤 Sending: ${message}`)
|
||||
hostConnection
|
||||
.sendMessage(message)
|
||||
.then(success => {
|
||||
if (success) {
|
||||
addLog('a', '✅ Message sent successfully')
|
||||
} else {
|
||||
addLog('a', '⚠️ Message queued (not connected)')
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
addLog('a', `❌ Error sending: ${error.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
// Send test message from client to host
|
||||
function sendFromClient() {
|
||||
if (!clientConnection) {
|
||||
alert('Please create client first!')
|
||||
return
|
||||
}
|
||||
|
||||
const message = document.getElementById('message-b').value || 'Hello from Client!'
|
||||
addLog('b', `📤 Sending: ${message}`)
|
||||
clientConnection
|
||||
.sendMessage(message)
|
||||
.then(success => {
|
||||
if (success) {
|
||||
addLog('b', '✅ Message sent successfully')
|
||||
} else {
|
||||
addLog('b', '⚠️ Message queued (not connected)')
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
addLog('b', `❌ Error sending: ${error.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
// Attach event listeners when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Clear all textareas on load
|
||||
document.getElementById('offer-a').value = ''
|
||||
document.getElementById('answer-b').value = ''
|
||||
|
||||
// Make functions globally available (for console testing)
|
||||
window.createHost = createHost
|
||||
window.createClient = createClient
|
||||
window.sendFromHost = sendFromHost
|
||||
window.sendFromClient = sendFromClient
|
||||
|
||||
console.log('🚀 Local signaling test loaded')
|
||||
console.log('Steps:')
|
||||
console.log('1. Click "Create Host" (Peer A)')
|
||||
console.log('2. Click "Create Client" (Peer B)')
|
||||
console.log('3. Wait for connection to establish')
|
||||
console.log('4. Send messages between peers')
|
||||
})
|
||||
280
demo/index.html
Normal file
280
demo/index.html
Normal file
@@ -0,0 +1,280 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Rondevu WebRTC Local Test</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5rem;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.peers {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.peer {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.peer h2 {
|
||||
color: #667eea;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5rem;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status.disconnected {
|
||||
background: #fee;
|
||||
color: #c33;
|
||||
}
|
||||
|
||||
.status.connecting {
|
||||
background: #ffeaa7;
|
||||
color: #d63031;
|
||||
}
|
||||
|
||||
.status.connected {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section h3 {
|
||||
color: #555;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
input[type='text']:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #5568d3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.log {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 4px;
|
||||
padding: 4px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.message-box {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.message-box input {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.message-box button {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.peers {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔗 Rondevu WebRTC Local Test</h1>
|
||||
|
||||
<div class="peers">
|
||||
<!-- Peer A (Host) -->
|
||||
<div class="peer">
|
||||
<h2>Peer A (Host/Offerer)</h2>
|
||||
|
||||
<div class="status disconnected" id="status-a">Disconnected</div>
|
||||
|
||||
<div class="section">
|
||||
<button onclick="createHost()">1️⃣ Create Host Connection</button>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Local Offer (SDP)</h3>
|
||||
<textarea
|
||||
id="offer-a"
|
||||
readonly
|
||||
placeholder="Offer will appear here..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Send Message</h3>
|
||||
<div class="message-box">
|
||||
<input
|
||||
type="text"
|
||||
id="message-a"
|
||||
placeholder="Type a message..."
|
||||
onkeypress="if (event.key === 'Enter') sendFromHost()"
|
||||
/>
|
||||
<button onclick="sendFromHost()">📤 Send</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Activity Log</h3>
|
||||
<div class="log" id="log-a"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Peer B (Client) -->
|
||||
<div class="peer">
|
||||
<h2>Peer B (Client/Answerer)</h2>
|
||||
|
||||
<div class="status disconnected" id="status-b">Disconnected</div>
|
||||
|
||||
<div class="section">
|
||||
<button onclick="createClient()">2️⃣ Create Client Connection</button>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Local Answer (SDP)</h3>
|
||||
<textarea
|
||||
id="answer-b"
|
||||
readonly
|
||||
placeholder="Answer will appear here..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Send Message</h3>
|
||||
<div class="message-box">
|
||||
<input
|
||||
type="text"
|
||||
id="message-b"
|
||||
placeholder="Type a message..."
|
||||
onkeypress="if (event.key === 'Enter') sendFromClient()"
|
||||
/>
|
||||
<button onclick="sendFromClient()">📤 Send</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Activity Log</h3>
|
||||
<div class="log" id="log-b"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/demo.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user