Files
archived-vdo.ninja/examples/datachannel-pubsub.html
2025-10-21 20:52:45 -04:00

489 lines
18 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>&lt;iframe src="https://vdo.ninja/?room=myroom&amp;push=cam1&amp;datachannel=true"
style="display:none"&gt;&lt;/iframe&gt;
// 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>