mirror of
https://github.com/SrIzan10/vdo.ninja.git
synced 2026-05-01 11:05:24 +00:00
489 lines
18 KiB
HTML
489 lines
18 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>VDO.Ninja DataChannel Pub/Sub Example</title>
|
||
<style>
|
||
body {
|
||
font-family: Arial, sans-serif;
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
.container {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 20px;
|
||
}
|
||
.section {
|
||
border: 1px solid #ddd;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
}
|
||
.messages {
|
||
height: 300px;
|
||
overflow-y: auto;
|
||
border: 1px solid #eee;
|
||
padding: 10px;
|
||
margin: 10px 0;
|
||
background: #f5f5f5;
|
||
}
|
||
.message {
|
||
margin: 5px 0;
|
||
padding: 5px;
|
||
background: white;
|
||
border-radius: 4px;
|
||
}
|
||
.controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin: 10px 0;
|
||
}
|
||
input, button, select {
|
||
padding: 8px;
|
||
font-size: 14px;
|
||
}
|
||
input[type="text"] {
|
||
flex: 1;
|
||
}
|
||
.status {
|
||
padding: 10px;
|
||
background: #e0e0e0;
|
||
border-radius: 4px;
|
||
margin: 10px 0;
|
||
}
|
||
.status.connected {
|
||
background: #c8e6c9;
|
||
}
|
||
.status.error {
|
||
background: #ffcdd2;
|
||
}
|
||
.label-badge {
|
||
display: inline-block;
|
||
padding: 2px 8px;
|
||
background: #2196F3;
|
||
color: white;
|
||
border-radius: 12px;
|
||
font-size: 12px;
|
||
margin: 0 4px;
|
||
}
|
||
h3 {
|
||
margin-top: 0;
|
||
}
|
||
code {
|
||
background: #f0f0f0;
|
||
padding: 2px 4px;
|
||
border-radius: 3px;
|
||
font-size: 13px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>VDO.Ninja DataChannel Pub/Sub Example</h1>
|
||
<p>This example demonstrates replacing IFRAME-based messaging with the DataChannel SDK.</p>
|
||
|
||
<div class="container">
|
||
<div class="section">
|
||
<h3>Publisher Node</h3>
|
||
<div class="status" id="pubStatus">Disconnected</div>
|
||
|
||
<div class="controls">
|
||
<input type="text" id="pubRoom" placeholder="Room name" value="demo-room">
|
||
<input type="text" id="pubLabel" placeholder="Label" value="sensor-1">
|
||
<button onclick="connectPublisher()">Connect</button>
|
||
</div>
|
||
|
||
<h4>Publish Message</h4>
|
||
<div class="controls">
|
||
<input type="text" id="pubMessage" placeholder="Message to send">
|
||
<select id="pubTarget">
|
||
<option value="all">Broadcast to All</option>
|
||
<option value="label">To Label</option>
|
||
<option value="streamid">To Stream ID</option>
|
||
</select>
|
||
<input type="text" id="pubTargetValue" placeholder="Target label/ID" style="display:none;">
|
||
<button onclick="publishMessage()">Send</button>
|
||
</div>
|
||
|
||
<h4>Stream Data</h4>
|
||
<div class="controls">
|
||
<input type="file" id="fileInput">
|
||
<button onclick="streamFile()">Stream File</button>
|
||
</div>
|
||
|
||
<div class="messages" id="pubMessages"></div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>Subscriber Node</h3>
|
||
<div class="status" id="subStatus">Disconnected</div>
|
||
|
||
<div class="controls">
|
||
<input type="text" id="subRoom" placeholder="Room name" value="demo-room">
|
||
<input type="text" id="subLabel" placeholder="Label" value="controller">
|
||
<button onclick="connectSubscriber()">Connect</button>
|
||
</div>
|
||
|
||
<h4>Subscribe to Labels</h4>
|
||
<div class="controls">
|
||
<input type="text" id="subToLabel" placeholder="Label to subscribe" value="sensor-1">
|
||
<button onclick="subscribeToLabel()">Subscribe</button>
|
||
</div>
|
||
|
||
<div id="subscriptions"></div>
|
||
|
||
<h4>Received Messages</h4>
|
||
<div class="messages" id="subMessages"></div>
|
||
|
||
<h4>Received Streams</h4>
|
||
<div id="streams"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section" style="margin-top: 20px;">
|
||
<h3>Mesh Network Status</h3>
|
||
<div id="meshStatus"></div>
|
||
<canvas id="meshCanvas" width="800" height="400" style="border: 1px solid #ddd;"></canvas>
|
||
</div>
|
||
|
||
<div class="section" style="margin-top: 20px;">
|
||
<h3>Code Example (Replacing IFRAME)</h3>
|
||
<h4>Before (with IFRAME):</h4>
|
||
<pre><code><iframe src="https://vdo.ninja/?room=myroom&push=cam1&datachannel=true"
|
||
style="display:none"></iframe>
|
||
|
||
// Communicate via postMessage
|
||
iframe.contentWindow.postMessage({data: 'hello'}, '*');</code></pre>
|
||
|
||
<h4>After (with SDK):</h4>
|
||
<pre><code>const node = new VDONinjaDataChannel();
|
||
await node.joinRoom({room: 'myroom', streamID: 'cam1'});
|
||
|
||
// Direct data channel communication
|
||
node.publish({data: 'hello'});
|
||
|
||
// Subscribe to specific labels
|
||
node.subscribe('sensors');
|
||
node.addEventListener('data', (e) => {
|
||
console.log('Received:', e.detail.data);
|
||
});</code></pre>
|
||
</div>
|
||
|
||
<script src="../vdoninja-datachannel-sdk.js"></script>
|
||
<script>
|
||
let publisher = null;
|
||
let subscriber = null;
|
||
const meshNodes = new Map();
|
||
|
||
// Target selection handler
|
||
document.getElementById('pubTarget').addEventListener('change', (e) => {
|
||
const targetInput = document.getElementById('pubTargetValue');
|
||
if (e.target.value === 'all') {
|
||
targetInput.style.display = 'none';
|
||
} else {
|
||
targetInput.style.display = 'block';
|
||
targetInput.placeholder = e.target.value === 'label' ? 'Target label' : 'Target stream ID';
|
||
}
|
||
});
|
||
|
||
async function connectPublisher() {
|
||
if (publisher) {
|
||
publisher.disconnect();
|
||
}
|
||
|
||
publisher = new VDONinjaDataChannel({
|
||
debug: true,
|
||
meshMode: 'partial'
|
||
});
|
||
|
||
// Setup event handlers
|
||
publisher.addEventListener('connected', () => {
|
||
updateStatus('pubStatus', 'Connected', true);
|
||
addMessage('pubMessages', 'Connected to server', 'system');
|
||
});
|
||
|
||
publisher.addEventListener('disconnected', () => {
|
||
updateStatus('pubStatus', 'Disconnected', false);
|
||
});
|
||
|
||
publisher.addEventListener('peer-connected', (e) => {
|
||
addMessage('pubMessages', `Peer connected: ${e.detail.uuid}`, 'system');
|
||
updateMeshVisualization();
|
||
});
|
||
|
||
publisher.addEventListener('data', (e) => {
|
||
addMessage('pubMessages',
|
||
`Received from ${e.detail.label || e.detail.streamID}: ${JSON.stringify(e.detail.data)}`,
|
||
'received'
|
||
);
|
||
});
|
||
|
||
try {
|
||
await publisher.connect();
|
||
await publisher.joinRoom({
|
||
room: document.getElementById('pubRoom').value,
|
||
streamID: `pub-${Date.now()}`,
|
||
label: document.getElementById('pubLabel').value
|
||
});
|
||
|
||
meshNodes.set(publisher.uuid, {
|
||
sdk: publisher,
|
||
type: 'publisher',
|
||
label: publisher.label
|
||
});
|
||
|
||
updateStatus('pubStatus', `Connected as ${publisher.label}`, true);
|
||
} catch (error) {
|
||
updateStatus('pubStatus', `Error: ${error.message}`, false);
|
||
}
|
||
}
|
||
|
||
async function connectSubscriber() {
|
||
if (subscriber) {
|
||
subscriber.disconnect();
|
||
}
|
||
|
||
subscriber = new VDONinjaDataChannel({
|
||
debug: true,
|
||
meshMode: 'partial'
|
||
});
|
||
|
||
// Setup event handlers
|
||
subscriber.addEventListener('connected', () => {
|
||
updateStatus('subStatus', 'Connected', true);
|
||
addMessage('subMessages', 'Connected to server', 'system');
|
||
});
|
||
|
||
subscriber.addEventListener('disconnected', () => {
|
||
updateStatus('subStatus', 'Disconnected', false);
|
||
});
|
||
|
||
subscriber.addEventListener('peer-connected', (e) => {
|
||
addMessage('subMessages', `Peer connected: ${e.detail.uuid}`, 'system');
|
||
updateMeshVisualization();
|
||
});
|
||
|
||
subscriber.addEventListener('data', (e) => {
|
||
addMessage('subMessages',
|
||
`From ${e.detail.label || e.detail.streamID}: ${JSON.stringify(e.detail.data)}`,
|
||
'received'
|
||
);
|
||
});
|
||
|
||
subscriber.addEventListener('stream', (e) => {
|
||
handleReceivedStream(e.detail);
|
||
});
|
||
|
||
try {
|
||
await subscriber.connect();
|
||
await subscriber.joinRoom({
|
||
room: document.getElementById('subRoom').value,
|
||
streamID: `sub-${Date.now()}`,
|
||
label: document.getElementById('subLabel').value
|
||
});
|
||
|
||
meshNodes.set(subscriber.uuid, {
|
||
sdk: subscriber,
|
||
type: 'subscriber',
|
||
label: subscriber.label
|
||
});
|
||
|
||
updateStatus('subStatus', `Connected as ${subscriber.label}`, true);
|
||
} catch (error) {
|
||
updateStatus('subStatus', `Error: ${error.message}`, false);
|
||
}
|
||
}
|
||
|
||
function publishMessage() {
|
||
if (!publisher || !publisher.connected) {
|
||
alert('Publisher not connected');
|
||
return;
|
||
}
|
||
|
||
const message = document.getElementById('pubMessage').value;
|
||
const target = document.getElementById('pubTarget').value;
|
||
const targetValue = document.getElementById('pubTargetValue').value;
|
||
|
||
if (!message) return;
|
||
|
||
const data = {
|
||
message,
|
||
timestamp: new Date().toISOString(),
|
||
from: publisher.label
|
||
};
|
||
|
||
let options = {};
|
||
if (target === 'label' && targetValue) {
|
||
options.toLabel = targetValue;
|
||
} else if (target === 'streamid' && targetValue) {
|
||
options.toStreamID = targetValue;
|
||
}
|
||
|
||
publisher.publish(data, options);
|
||
|
||
addMessage('pubMessages',
|
||
`Sent to ${target === 'all' ? 'all' : `${target}: ${targetValue}`}: ${message}`,
|
||
'sent'
|
||
);
|
||
|
||
document.getElementById('pubMessage').value = '';
|
||
}
|
||
|
||
async function subscribeToLabel() {
|
||
if (!subscriber || !subscriber.connected) {
|
||
alert('Subscriber not connected');
|
||
return;
|
||
}
|
||
|
||
const label = document.getElementById('subToLabel').value;
|
||
if (!label) return;
|
||
|
||
subscriber.subscribe(label);
|
||
|
||
// Update UI
|
||
const subDiv = document.getElementById('subscriptions');
|
||
const badge = document.createElement('span');
|
||
badge.className = 'label-badge';
|
||
badge.textContent = label;
|
||
subDiv.appendChild(badge);
|
||
|
||
addMessage('subMessages', `Subscribed to label: ${label}`, 'system');
|
||
|
||
// Auto-connect to peers with this label
|
||
subscriber.peerLabels.forEach((peerLabel, uuid) => {
|
||
if (peerLabel === label) {
|
||
const streamID = subscriber.peerStreamIDs.get(uuid);
|
||
if (streamID) {
|
||
subscriber.view(streamID);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
async function streamFile() {
|
||
if (!publisher || !publisher.connected) {
|
||
alert('Publisher not connected');
|
||
return;
|
||
}
|
||
|
||
const fileInput = document.getElementById('fileInput');
|
||
const file = fileInput.files[0];
|
||
if (!file) return;
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
const streamId = `file-${Date.now()}`;
|
||
publisher.streamData(streamId, e.target.result, {
|
||
fileName: file.name,
|
||
fileType: file.type,
|
||
fileSize: file.size
|
||
});
|
||
|
||
addMessage('pubMessages', `Streaming file: ${file.name} (${file.size} bytes)`, 'sent');
|
||
};
|
||
|
||
reader.readAsArrayBuffer(file);
|
||
}
|
||
|
||
function handleReceivedStream(stream) {
|
||
const streamsDiv = document.getElementById('streams');
|
||
const streamDiv = document.createElement('div');
|
||
streamDiv.className = 'message';
|
||
|
||
const blob = new Blob([stream.data], { type: stream.metadata.fileType || 'application/octet-stream' });
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
streamDiv.innerHTML = `
|
||
<strong>Received Stream:</strong> ${stream.metadata.fileName || 'Unknown'}
|
||
(${stream.metadata.fileSize || stream.data.byteLength} bytes)
|
||
<a href="${url}" download="${stream.metadata.fileName || 'download'}">Download</a>
|
||
`;
|
||
|
||
streamsDiv.appendChild(streamDiv);
|
||
}
|
||
|
||
function updateStatus(elementId, text, connected) {
|
||
const element = document.getElementById(elementId);
|
||
element.textContent = text;
|
||
element.className = 'status' + (connected ? ' connected' : '');
|
||
}
|
||
|
||
function addMessage(containerId, text, type = 'info') {
|
||
const container = document.getElementById(containerId);
|
||
const message = document.createElement('div');
|
||
message.className = 'message';
|
||
|
||
const time = new Date().toLocaleTimeString();
|
||
const typeEmoji = {
|
||
system: '🔧',
|
||
sent: '📤',
|
||
received: '📥',
|
||
info: 'ℹ️'
|
||
};
|
||
|
||
message.innerHTML = `${typeEmoji[type] || ''} [${time}] ${text}`;
|
||
container.appendChild(message);
|
||
container.scrollTop = container.scrollHeight;
|
||
}
|
||
|
||
function updateMeshVisualization() {
|
||
const canvas = document.getElementById('meshCanvas');
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
|
||
// Simple mesh visualization
|
||
const nodes = Array.from(meshNodes.values());
|
||
const angleStep = (2 * Math.PI) / nodes.length;
|
||
const radius = 150;
|
||
const centerX = canvas.width / 2;
|
||
const centerY = canvas.height / 2;
|
||
|
||
// Draw nodes
|
||
nodes.forEach((node, index) => {
|
||
const x = centerX + radius * Math.cos(index * angleStep);
|
||
const y = centerY + radius * Math.sin(index * angleStep);
|
||
|
||
ctx.beginPath();
|
||
ctx.arc(x, y, 20, 0, 2 * Math.PI);
|
||
ctx.fillStyle = node.type === 'publisher' ? '#4CAF50' : '#2196F3';
|
||
ctx.fill();
|
||
|
||
ctx.fillStyle = 'white';
|
||
ctx.font = '12px Arial';
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText(node.label || 'Node', x, y + 4);
|
||
|
||
// Draw connections
|
||
if (node.sdk && node.sdk.peers) {
|
||
node.sdk.peers.forEach((pc, peerUuid) => {
|
||
const peerNode = meshNodes.get(peerUuid);
|
||
if (peerNode) {
|
||
const peerIndex = nodes.indexOf(peerNode);
|
||
const peerX = centerX + radius * Math.cos(peerIndex * angleStep);
|
||
const peerY = centerY + radius * Math.sin(peerIndex * angleStep);
|
||
|
||
ctx.beginPath();
|
||
ctx.moveTo(x, y);
|
||
ctx.lineTo(peerX, peerY);
|
||
ctx.strokeStyle = '#666';
|
||
ctx.stroke();
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// Update status text
|
||
const statusDiv = document.getElementById('meshStatus');
|
||
statusDiv.innerHTML = `<strong>Nodes:</strong> ${nodes.length} |
|
||
<strong>Publishers:</strong> ${nodes.filter(n => n.type === 'publisher').length} |
|
||
<strong>Subscribers:</strong> ${nodes.filter(n => n.type === 'subscriber').length}`;
|
||
}
|
||
|
||
// Auto-connect on load for demo
|
||
window.addEventListener('load', () => {
|
||
// Optional: auto-connect for easier testing
|
||
// connectPublisher();
|
||
// connectSubscriber();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |