Add files via upload

This commit is contained in:
Steve Seguin
2025-10-21 20:52:45 -04:00
committed by GitHub
parent 20c815e607
commit 2fa7f629be
26 changed files with 7731 additions and 447 deletions

View File

@@ -1,6 +1,6 @@
<html>
<head>
<title>IFRAME Example</title>
<head>
<title>Add to Scene Controller - VDO.Ninja</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<style>
@@ -142,4 +142,4 @@
</div>
</body>
</html>
</html>

View File

@@ -1,5 +1,5 @@
<html>
<head><title>Twitch + Video</title>
<head><title>Big Mute Button Remote - VDO.Ninja</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<style>
@@ -118,4 +118,4 @@ function loadIframes(url=false){
</script>
</body>
</html>
</html>

View File

@@ -1,5 +1,6 @@
<html>
<head>
<head>
<title>Legacy Layout Console - VDO.Ninja</title>
<style>
body {
margin:0;

View File

@@ -1,6 +1,6 @@
<html>
<head>
<title>Video with sensor overlayed data</title>
<title>Custom Labels Overlay - VDO.Ninja</title>
<meta charset="UTF-8">
<style>
body{
@@ -109,4 +109,4 @@ if (window.obsstudio){
</script>
</body>
</html>
</html>

View File

@@ -1,6 +1,6 @@
<html>
<head>
<title>Video with sensor overlayed data</title>
<title>Dynamic Overlay Frame - VDO.Ninja</title>
<meta charset="UTF-8">
<style>
body{
@@ -146,4 +146,4 @@ if (window.obsstudio){
</script>
</body>
</html>
</html>

View File

@@ -1,4 +1,8 @@
<html>
<head>
<meta charset="utf-8" />
<title>Custom Video Switcher - VDO.Ninja</title>
</head>
<body>
<button onclick="togglePlayer('STREAM123a')" >toggle video 1</button>
<button onclick="togglePlayer('STREAM123b')" >toggle video 2</button>
@@ -51,4 +55,4 @@
</script>
</body>
</html>
</html>

View File

@@ -0,0 +1,489 @@
<!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>

531
examples/dataiframes.md Normal file
View File

@@ -0,0 +1,531 @@
# VDO.Ninja IFRAME API: Generic P2P Data Transmission Guide
This guide focuses specifically on how to send and receive generic data between clients using VDO.Ninja's peer-to-peer (P2P) data channels.
## Understanding the P2P Data Channels
VDO.Ninja provides a powerful API that allows websites to send arbitrary data between connected clients through its peer-to-peer infrastructure. This enables you to:
- Create custom communication channels between clients
- Implement application-specific data exchange
- Build interactive multi-user experiences
- Exchange any type of serializable data
## Why VDO.Ninja's P2P Data Channels Are Powerful
VDO.Ninja's data channels offer several compelling advantages that make them ideal for modern web applications:
- **Production-Proven Reliability**: Used in production applications like Social Stream Ninja, which processes hundreds of messages per minute per peer connection
- **Automatic LAN Optimization**: Detects when connections are on the same local network and routes data directly, reducing latency
- **Firewall Traversal**: Enables communication between devices behind different firewalls without port forwarding
- **Cost-Effective**: No server costs or bandwidth charges for data transmission, as everything happens peer-to-peer
- **Low Latency**: Direct connections between peers minimize delay, ideal for real-time applications
- **Scalability**: Each peer connects directly to others, distributing the load across the network
- **AI Integration Ready**: Perfect for distributing AI processing tasks or sharing AI-generated content between users
- **Remote Control Applications**: Enables secure remote control of devices through firewalls without complex networking setups
- **Works Across Platforms**: Functions on mobile, desktop, and various browsers without additional plugins
The creators of VDO.Ninja use these data channels in numerous applications beyond video, demonstrating their versatility and reliability in real-world scenarios.
## Basic Setup
First, set up your VDO.Ninja iframe:
```javascript
// Create the iframe element
var iframe = document.createElement("iframe");
// Set necessary permissions
iframe.allow = "camera;microphone;fullscreen;display-capture;autoplay;";
// Set the source URL (your VDO.Ninja room)
iframe.src = "https://vdo.ninja/?room=your-room-name&cleanoutput";
// Add the iframe to your page
document.getElementById("container").appendChild(iframe);
```
## Setting Up Event Listeners
To receive data from other clients, set up an event listener:
```javascript
// Set up event listener (cross-browser compatible)
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
// Connected peers storage
var connectedPeers = {};
// Add the event listener
eventer(messageEvent, function(e) {
// Make sure the message is from our VDO.Ninja iframe
if (e.source != iframe.contentWindow) return;
// Process connection events to track connected peers
if ("action" in e.data) {
handleConnectionEvents(e.data);
}
// Handle received data
if ("dataReceived" in e.data) {
handleDataReceived(e.data.dataReceived, e.data.UUID);
}
}, false);
function handleConnectionEvents(data) {
if (data.action === "guest-connected" && data.streamID) {
// Store connected peer information
connectedPeers[data.streamID] = data.value?.label || "Guest";
console.log("Guest connected:", data.streamID, "Label:", connectedPeers[data.streamID]);
}
else if (data.action === "push-connection" && data.value === false && data.streamID) {
// Remove disconnected peers
console.log("Guest disconnected:", data.streamID);
delete connectedPeers[data.streamID];
}
}
function handleDataReceived(data, senderUUID) {
console.log("Data received from:", senderUUID, "Data:", data);
// Example: Check for your custom data namespace
if (data.overlayNinja) {
processCustomData(data.overlayNinja, senderUUID);
}
}
function processCustomData(data, senderUUID) {
// Process based on your application's needs
console.log("Processing custom data:", data);
// Example: Handle different data types
if (data.message) {
displayMessage(data.message);
} else if (data.command) {
executeCommand(data.command);
}
}
```
## Sending Data
### Send Data Structure
When sending data via the VDO.Ninja IFRAME API, you use this general format:
```javascript
iframe.contentWindow.postMessage({
sendData: yourDataPayload,
type: "pcs", // Connection type (see below)
UUID: targetUUID // Optional: specific target
}, "*");
```
The components are:
- `sendData`: Your data payload (object)
- `type`: Connection type (string)
- `"pcs"`: Use peer connections (most reliable)
- `"rpcs"`: Use request-based connections
- `UUID` or `streamID`: Optional target identifier
### Sending to All Connected Peers
```javascript
function sendDataToAllPeers(data) {
// Create the data structure with your custom namespace
var payload = {
overlayNinja: data // Your custom data under a namespace
};
// Send to all peers
iframe.contentWindow.postMessage({
sendData: payload,
type: "pcs" // Use peer connection for reliability
}, "*");
}
// Example usage
sendDataToAllPeers({
message: "Hello everyone!",
timestamp: Date.now()
});
```
### Sending to a Specific Peer by UUID
```javascript
function sendDataToPeer(data, targetUUID) {
// Create the data structure
var payload = {
overlayNinja: data // Your custom data
};
// Send to specific UUID
iframe.contentWindow.postMessage({
sendData: payload,
type: "pcs",
UUID: targetUUID
}, "*");
}
// Example usage
sendDataToPeer({
message: "Hello specific peer!",
timestamp: Date.now()
}, "peer-uuid-123");
```
### Sending to Peers with Specific Labels
```javascript
function sendDataByLabel(data, targetLabel) {
// Create the data structure
var payload = {
overlayNinja: data // Your custom data
};
// Iterate through connected peers to find those with matching label
var keys = Object.keys(connectedPeers);
for (var i = 0; i < keys.length; i++) {
try {
var UUID = keys[i];
var label = connectedPeers[UUID];
if (label === targetLabel) {
// Send to this specific peer
iframe.contentWindow.postMessage({
sendData: payload,
type: "pcs",
UUID: UUID
}, "*");
}
} catch (e) {
console.error("Error sending to peer:", e);
}
}
}
// Example usage
sendDataByLabel({
message: "Hello all viewers!",
timestamp: Date.now()
}, "viewer");
```
### Sending to a Peer by StreamID
```javascript
function sendDataByStreamID(data, streamID) {
// Create the data structure
var payload = {
overlayNinja: data // Your custom data
};
// Send to specific streamID
iframe.contentWindow.postMessage({
sendData: payload,
type: "pcs",
streamID: streamID
}, "*");
}
// Example usage
sendDataByStreamID({
message: "Hello by stream ID!",
timestamp: Date.now()
}, "stream-123");
```
## Tracking Connected Peers
To reliably communicate with peers, keep track of connections and disconnections:
```javascript
// Store connected peers
var connectedPeers = {};
function handleConnectionEvents(data) {
// Guest connections
if (data.action === "guest-connected" && data.streamID) {
connectedPeers[data.streamID] = data.value?.label || "Guest";
console.log("Guest connected:", data.streamID, "Label:", connectedPeers[data.streamID]);
}
// View connections
else if (data.action === "view-connection") {
if (data.value && data.streamID) {
connectedPeers[data.streamID] = "Viewer";
console.log("Viewer connected:", data.streamID);
} else if (data.streamID) {
console.log("Viewer disconnected:", data.streamID);
delete connectedPeers[data.streamID];
}
}
// Director connections
else if (data.action === "director-connected") {
console.log("Director connected");
}
// Handle disconnections
else if (data.action === "push-connection" && data.value === false && data.streamID) {
console.log("User disconnected:", data.streamID);
delete connectedPeers[data.streamID];
}
}
```
## Getting All Connected StreamIDs
You can request a list of all connected streams:
```javascript
function getConnectedPeers() {
iframe.contentWindow.postMessage({ getStreamIDs: true }, "*");
}
// In your event listener, handle the response:
if ("streamIDs" in e.data) {
console.log("Connected streams:");
for (var key in e.data.streamIDs) {
console.log("StreamID:", key, "Label:", e.data.streamIDs[key]);
}
}
```
## Detailed State Information
For more comprehensive information about the current state:
```javascript
function getDetailedState() {
iframe.contentWindow.postMessage({ getDetailedState: true }, "*");
}
// Handle the response in your event listener
```
## Data Structure Best Practices
1. **Use a Namespace**: Put your data under a custom namespace to avoid conflicts
```javascript
{
sendData: {
yourAppName: {
// Your data here
}
}
}
```
2. **Include Type Information**: Include type identifiers to differentiate messages
```javascript
{
sendData: {
yourAppName: {
type: "command",
data: { /* command data */ }
}
}
}
```
3. **Include Timestamp**: Add timestamps to help with ordering
```javascript
{
sendData: {
yourAppName: {
type: "update",
data: { /* update data */ },
timestamp: Date.now()
}
}
}
```
## Complete Example: Simple Chat System
Here's a complete example implementing a simple chat system using the P2P data channels:
```javascript
// Create the interface
const container = document.createElement('div');
container.style.width = '100%';
container.style.maxWidth = '800px';
container.style.margin = '0 auto';
document.body.appendChild(container);
// Create VDO.Ninja iframe
const iframe = document.createElement('iframe');
iframe.allow = "camera;microphone;fullscreen;display-capture;autoplay;";
iframe.src = "https://vdo.ninja/?room=chat-demo&cleanoutput";
iframe.style.width = "100%";
iframe.style.height = "360px";
container.appendChild(iframe);
// Create chat interface
const chatContainer = document.createElement('div');
chatContainer.style.marginTop = '20px';
container.appendChild(chatContainer);
const chatMessages = document.createElement('div');
chatMessages.style.height = '300px';
chatMessages.style.border = '1px solid #ccc';
chatMessages.style.padding = '10px';
chatMessages.style.overflowY = 'scroll';
chatContainer.appendChild(chatMessages);
const inputContainer = document.createElement('div');
inputContainer.style.marginTop = '10px';
inputContainer.style.display = 'flex';
chatContainer.appendChild(inputContainer);
const messageInput = document.createElement('input');
messageInput.type = 'text';
messageInput.placeholder = 'Type your message...';
messageInput.style.flexGrow = '1';
messageInput.style.padding = '8px';
inputContainer.appendChild(messageInput);
const sendButton = document.createElement('button');
sendButton.textContent = 'Send';
sendButton.style.marginLeft = '10px';
sendButton.style.padding = '8px 16px';
inputContainer.appendChild(sendButton);
// Store connected peers
const connectedPeers = {};
// Add event listeners
sendButton.addEventListener('click', sendChatMessage);
messageInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendChatMessage();
}
});
function sendChatMessage() {
const message = messageInput.value.trim();
if (message) {
// Create message object
const chatData = {
type: 'chat',
text: message,
sender: 'Me',
timestamp: Date.now()
};
// Add to local chat
addMessageToChat(chatData.sender, chatData.text);
// Send to all peers
sendDataToAllPeers(chatData);
// Clear input
messageInput.value = '';
}
}
function addMessageToChat(sender, text) {
const messageElement = document.createElement('div');
messageElement.style.marginBottom = '8px';
const senderSpan = document.createElement('strong');
senderSpan.textContent = sender + ': ';
messageElement.appendChild(senderSpan);
const textNode = document.createTextNode(text);
messageElement.appendChild(textNode);
chatMessages.appendChild(messageElement);
// Scroll to bottom
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function sendDataToAllPeers(data) {
// Create the data structure
const payload = {
chatApp: data // Using a custom namespace
};
// Send to all peers
iframe.contentWindow.postMessage({
sendData: payload,
type: "pcs"
}, "*");
}
// Set up event listener for messages from iframe
const eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
const eventer = window[eventMethod];
const messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function(e) {
// Make sure the message is from our VDO.Ninja iframe
if (e.source != iframe.contentWindow) return;
// Process connection events
if ("action" in e.data) {
handleConnectionEvents(e.data);
}
// Handle received data
if ("dataReceived" in e.data) {
handleDataReceived(e.data.dataReceived, e.data.UUID);
}
}, false);
function handleConnectionEvents(data) {
if (data.action === "guest-connected" && data.streamID) {
// Store connected peer information
connectedPeers[data.streamID] = data.value?.label || "Guest";
console.log("Guest connected:", data.streamID, "Label:", connectedPeers[data.streamID]);
// Announce new connection in chat
addMessageToChat("System", `${connectedPeers[data.streamID]} joined the chat`);
}
else if (data.action === "push-connection" && data.value === false && data.streamID) {
// Announce disconnection
if (connectedPeers[data.streamID]) {
addMessageToChat("System", `${connectedPeers[data.streamID]} left the chat`);
}
// Remove from tracking
console.log("Guest disconnected:", data.streamID);
delete connectedPeers[data.streamID];
}
}
function handleDataReceived(data, senderUUID) {
// Check for chat messages
if (data.chatApp && data.chatApp.type === 'chat') {
const chatData = data.chatApp;
// Get sender name from our peer tracking if available
const senderName = connectedPeers[senderUUID] || chatData.sender || "Unknown";
// Add to chat
addMessageToChat(senderName, chatData.text);
}
}
```
## Best Practices
1. **Track Connections**: Always maintain a list of connected peers
2. **Use Namespaces**: Organize your data under custom namespaces
3. **Add Type Information**: Include message types for easier processing
4. **Include Timestamps**: Help with ordering and synchronization
5. **Error Handling**: Use try/catch blocks when sending messages
6. **Data Size**: Keep payloads reasonably small to avoid performance issues
7. **UUID vs StreamID**: Prefer UUID for targeting as it's more stable
## Troubleshooting
- **No Data Received**: Verify the UUID or streamID is correct
- **Connection Issues**: Check if peers are properly connected before sending
- **Timing Problems**: Ensure the iframe is fully loaded before sending messages
- **Data Format**: Make sure your data is properly serializable
- **Security Settings**: Check that your iframe permissions are set correctly
By following this guide, you can implement robust P2P data exchange between VDO.Ninja clients for any custom application.

View File

@@ -1,5 +1,5 @@
<html>
<head><title>Dual Input</title>
<head><title>Draggable Multi-View - VDO.Ninja</title>
<style>
body{
padding:0;
@@ -328,4 +328,4 @@ function loadIframe(){
</script>
</body>
</html>
</html>

View File

@@ -0,0 +1,634 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VDO.Ninja SDK - Dynamic Viewer</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.container {
display: grid;
grid-template-columns: 350px 1fr;
gap: 20px;
}
.control-panel {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
height: fit-content;
}
.video-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 10px;
}
.video-container {
background: #000;
border-radius: 8px;
overflow: hidden;
position: relative;
}
.video-container video {
width: 100%;
height: 240px;
object-fit: cover;
}
.video-label {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,0.7);
color: white;
padding: 5px 10px;
font-size: 14px;
}
.section {
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.section:last-child {
border-bottom: none;
}
h2 {
margin-top: 0;
font-size: 20px;
color: #333;
}
h3 {
font-size: 16px;
margin: 10px 0 5px 0;
color: #666;
}
input, select {
width: 100%;
padding: 8px;
margin: 5px 0;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
background: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin: 5px 5px 5px 0;
font-size: 14px;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
button.danger {
background: #dc3545;
}
button.danger:hover {
background: #c82333;
}
button.success {
background: #28a745;
}
button.success:hover {
background: #218838;
}
.status {
padding: 10px;
margin: 10px 0;
background: #f8f9fa;
border-radius: 4px;
font-size: 14px;
border-left: 4px solid #007bff;
}
.status.error {
border-left-color: #dc3545;
background: #f8d7da;
color: #721c24;
}
.status.success {
border-left-color: #28a745;
background: #d4edda;
color: #155724;
}
.participant-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
padding: 5px;
font-size: 14px;
}
.participant {
padding: 5px;
margin: 2px 0;
background: #f8f9fa;
border-radius: 3px;
display: flex;
justify-content: space-between;
align-items: center;
}
.participant.connected {
background: #d4edda;
}
.tab-buttons {
display: flex;
gap: 5px;
margin-bottom: 15px;
}
.tab-button {
background: #f8f9fa;
color: #333;
flex: 1;
}
.tab-button.active {
background: #007bff;
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.stats {
font-family: monospace;
font-size: 12px;
background: #f8f9fa;
padding: 5px;
border-radius: 3px;
margin-top: 5px;
}
</style>
</head>
<body>
<h1>VDO.Ninja SDK - Dynamic Viewer</h1>
<div class="container">
<!-- Control Panel -->
<div class="control-panel">
<div class="section">
<h2>Connection</h2>
<input type="text" id="wss" placeholder="WebSocket Server" value="wss://apibackup.vdo.ninja/">
<button onclick="connect()" id="connectBtn">Connect</button>
<button onclick="disconnect()" id="disconnectBtn" disabled>Disconnect</button>
<div id="connectionStatus" class="status">Not connected</div>
</div>
<!-- Tabs -->
<div class="tab-buttons">
<button class="tab-button active" onclick="showTab('room')">Room View</button>
<button class="tab-button" onclick="showTab('direct')">Direct View</button>
</div>
<!-- Room View Tab -->
<div id="roomTab" class="tab-content active">
<div class="section">
<h2>Join Room</h2>
<input type="text" id="roomName" placeholder="Room Name">
<input type="password" id="roomPassword" placeholder="Password (optional)">
<button onclick="joinRoom()" id="joinRoomBtn" disabled>Join Room</button>
<button onclick="leaveRoom()" id="leaveRoomBtn" disabled>Leave Room</button>
<div id="roomStatus" class="status">Not in a room</div>
</div>
<div class="section">
<h2>Room Participants</h2>
<button onclick="refreshParticipants()" id="refreshBtn" disabled>Refresh</button>
<button onclick="viewAllInRoom()" id="viewAllBtn" disabled class="success">View All</button>
<div id="participantList" class="participant-list">
<em>Join a room to see participants</em>
</div>
</div>
</div>
<!-- Direct View Tab -->
<div id="directTab" class="tab-content">
<div class="section">
<h2>View Stream Directly</h2>
<input type="text" id="directStreamID" placeholder="Stream ID">
<input type="password" id="directPassword" placeholder="Password (optional)">
<button onclick="viewDirect()" id="viewDirectBtn" disabled>View Stream</button>
</div>
<div class="section">
<h2>Active Viewers</h2>
<button onclick="refreshActiveViewers()">Refresh</button>
<div id="activeViewersList" class="participant-list">
<em>No active viewers</em>
</div>
</div>
</div>
<div class="section">
<h2>Quick Actions</h2>
<button onclick="stopAllViewers()" class="danger">Stop All Viewers</button>
<button onclick="showStats()">Show Stats</button>
</div>
</div>
<!-- Video Grid -->
<div class="video-grid" id="videoGrid">
<div style="grid-column: 1/-1; text-align: center; color: #666; padding: 40px;">
<h3>No streams being viewed</h3>
<p>Connect and join a room or view streams directly</p>
</div>
</div>
</div>
<script src="../vdoninja-sdk.js"></script>
<script>
let sdk = null;
const viewers = new Map(); // streamID -> {pc, video, container}
// Initialize
function init() {
updateUI();
}
// Show tab
function showTab(tab) {
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
if (tab === 'room') {
document.querySelector('.tab-button:nth-child(1)').classList.add('active');
document.getElementById('roomTab').classList.add('active');
} else {
document.querySelector('.tab-button:nth-child(2)').classList.add('active');
document.getElementById('directTab').classList.add('active');
}
}
// Connect to signaling server
async function connect() {
const wss = document.getElementById('wss').value;
const status = document.getElementById('connectionStatus');
try {
status.textContent = 'Connecting...';
sdk = new VDONinjaSDK({
wss,
debug: true
});
// Setup event listeners
sdk.addEventListener('connected', () => {
status.className = 'status success';
status.textContent = 'Connected to signaling server';
updateUI();
});
sdk.addEventListener('disconnected', () => {
status.className = 'status error';
status.textContent = 'Disconnected from server';
updateUI();
});
sdk.addEventListener('someoneJoined', (e) => {
console.log('Someone joined:', e.detail);
setTimeout(refreshParticipants, 500);
});
sdk.addEventListener('peerListing', (e) => {
console.log('Peer listing:', e.detail);
refreshParticipants();
});
await sdk.connect();
} catch (error) {
status.className = 'status error';
status.textContent = `Connection failed: ${error.message}`;
sdk = null;
updateUI();
}
}
// Disconnect
async function disconnect() {
if (sdk) {
await stopAllViewers();
await sdk.disconnect();
sdk = null;
updateUI();
}
}
// Join room
async function joinRoom() {
const room = document.getElementById('roomName').value;
const password = document.getElementById('roomPassword').value;
const status = document.getElementById('roomStatus');
if (!room) {
status.className = 'status error';
status.textContent = 'Please enter a room name';
return;
}
try {
status.textContent = 'Joining room...';
await sdk.joinRoom({ room, password });
status.className = 'status success';
status.textContent = `Joined room: ${room}`;
updateUI();
// Refresh participants after a short delay
setTimeout(refreshParticipants, 1000);
} catch (error) {
status.className = 'status error';
status.textContent = `Failed to join room: ${error.message}`;
}
}
// Leave room
async function leaveRoom() {
const status = document.getElementById('roomStatus');
try {
await stopAllViewers();
await sdk.leaveRoom();
status.className = 'status';
status.textContent = 'Left room';
document.getElementById('participantList').innerHTML = '<em>Join a room to see participants</em>';
updateUI();
} catch (error) {
status.className = 'status error';
status.textContent = `Error leaving room: ${error.message}`;
}
}
// Refresh participants
function refreshParticipants() {
if (!sdk || !sdk.state.room) return;
const participants = sdk.getRoomParticipants();
const list = document.getElementById('participantList');
if (participants.length === 0) {
list.innerHTML = '<em>No other participants in room</em>';
return;
}
list.innerHTML = participants.map(p => {
const isViewing = viewers.has(p.streamID);
const label = p.label || p.streamID;
return `
<div class="participant ${p.connected ? 'connected' : ''}">
<span>${label}</span>
${isViewing ?
`<button onclick="stopViewing('${p.streamID}')">Stop</button>` :
`<button onclick="viewStream('${p.streamID}')">View</button>`
}
</div>
`;
}).join('');
}
// View all in room
async function viewAllInRoom() {
try {
const viewers = await sdk.viewAllInRoom();
for (const viewer of viewers) {
handleNewViewer(viewer.streamID, viewer.pc, viewer.label);
}
if (viewers.length === 0) {
alert('No streams to view in this room');
}
} catch (error) {
console.error('Error viewing all:', error);
alert(`Error: ${error.message}`);
}
}
// View specific stream
async function viewStream(streamID) {
try {
const pc = await sdk.view(streamID);
handleNewViewer(streamID, pc);
refreshParticipants();
refreshActiveViewers();
} catch (error) {
console.error(`Error viewing ${streamID}:`, error);
alert(`Failed to view stream: ${error.message}`);
}
}
// View direct (without room)
async function viewDirect() {
const streamID = document.getElementById('directStreamID').value;
const password = document.getElementById('directPassword').value;
if (!streamID) {
alert('Please enter a stream ID');
return;
}
try {
// If password provided, we need to set it temporarily
if (password) {
sdk.state.password = password;
await sdk._updateStreamIDHash();
}
const pc = await sdk.view(streamID);
handleNewViewer(streamID, pc);
refreshActiveViewers();
} catch (error) {
console.error('Error viewing directly:', error);
alert(`Failed to view stream: ${error.message}`);
}
}
// Handle new viewer connection
function handleNewViewer(streamID, pc, label) {
if (viewers.has(streamID)) {
console.log('Already viewing:', streamID);
return;
}
// Create video container
const container = document.createElement('div');
container.className = 'video-container';
container.id = `video-${streamID}`;
const video = document.createElement('video');
video.autoplay = true;
video.playsinline = true;
const labelDiv = document.createElement('div');
labelDiv.className = 'video-label';
labelDiv.innerHTML = `
${label || streamID}
<button style="float: right; background: #dc3545; border: none; color: white; padding: 2px 8px; border-radius: 3px; cursor: pointer;"
onclick="stopViewing('${streamID}')">×</button>
`;
container.appendChild(video);
container.appendChild(labelDiv);
// Handle tracks
pc.ontrack = (event) => {
console.log(`Track received from ${streamID}:`, event.track.kind);
video.srcObject = event.streams[0];
};
// Handle connection state
pc.onconnectionstatechange = () => {
console.log(`Connection state for ${streamID}:`, pc.connectionState);
if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
stopViewing(streamID);
}
};
// Store viewer info
viewers.set(streamID, { pc, video, container });
// Update grid
updateVideoGrid();
}
// Stop viewing specific stream
async function stopViewing(streamID) {
const viewer = viewers.get(streamID);
if (!viewer) return;
// Stop video
if (viewer.video.srcObject) {
viewer.video.srcObject.getTracks().forEach(track => track.stop());
viewer.video.srcObject = null;
}
// Remove from DOM
viewer.container.remove();
// Close connection
await sdk.stopViewing(streamID);
// Remove from map
viewers.delete(streamID);
// Update UI
updateVideoGrid();
refreshParticipants();
refreshActiveViewers();
}
// Stop all viewers
async function stopAllViewers() {
const streamIDs = Array.from(viewers.keys());
for (const streamID of streamIDs) {
await stopViewing(streamID);
}
}
// Refresh active viewers list
function refreshActiveViewers() {
if (!sdk) return;
const active = sdk.getActiveViewers();
const list = document.getElementById('activeViewersList');
if (active.length === 0) {
list.innerHTML = '<em>No active viewers</em>';
return;
}
list.innerHTML = active.map(v => `
<div class="participant ${v.connectionState === 'connected' ? 'connected' : ''}">
<span>${v.streamID} (${v.connectionState})</span>
<button onclick="stopViewing('${v.streamID}')">Stop</button>
</div>
`).join('');
}
// Update video grid
function updateVideoGrid() {
const grid = document.getElementById('videoGrid');
if (viewers.size === 0) {
grid.innerHTML = `
<div style="grid-column: 1/-1; text-align: center; color: #666; padding: 40px;">
<h3>No streams being viewed</h3>
<p>Connect and join a room or view streams directly</p>
</div>
`;
} else {
grid.innerHTML = '';
for (const viewer of viewers.values()) {
grid.appendChild(viewer.container);
}
}
}
// Show stats
async function showStats() {
if (!sdk) {
alert('Not connected');
return;
}
try {
const stats = await sdk.getStats();
console.log('Connection stats:', stats);
let message = 'Connection Statistics:\n\n';
for (const [uuid, stat] of Object.entries(stats)) {
message += `Connection ${uuid}:\n`;
stat.forEach(report => {
if (report.type === 'inbound-rtp' || report.type === 'outbound-rtp') {
message += ` ${report.type}: ${report.bytesReceived || report.bytesSent || 0} bytes\n`;
}
});
}
alert(message);
} catch (error) {
alert(`Error getting stats: ${error.message}`);
}
}
// Update UI state
function updateUI() {
const connected = sdk && sdk.state.connected;
const inRoom = connected && sdk.state.room;
document.getElementById('connectBtn').disabled = connected;
document.getElementById('disconnectBtn').disabled = !connected;
document.getElementById('joinRoomBtn').disabled = !connected || inRoom;
document.getElementById('leaveRoomBtn').disabled = !inRoom;
document.getElementById('refreshBtn').disabled = !inRoom;
document.getElementById('viewAllBtn').disabled = !inRoom;
document.getElementById('viewDirectBtn').disabled = !connected;
}
// Initialize on load
init();
</script>
</body>
</html>

View File

@@ -1,6 +1,6 @@
<html>
<head>
<title>IFRAME Example</title>
<title>Esports POV Toggler - VDO.Ninja</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<style>
@@ -128,4 +128,4 @@
<button onclick="loadIframe();">CONNECT</button>
</div>
</body>
</html>
</html>

View File

@@ -154,7 +154,7 @@ These commands target specific guests when you are the director.
| `display` | Guest ID/slot | N/A | Toggle guest's display |
| `forceKeyframe` | Guest ID/slot | N/A | Fix video artifacts for guest |
| `soloVideo` | Guest ID/slot | N/A | Highlight specific guest's video |
| `volume` | Guest ID/slot | `0` to `100` | Set guest's microphone volume |
| `volume` | Guest ID/slot | `0` to `200` | Set guest's microphone volume |
| `mixorder` | Guest ID/slot | `-1` or `1` | Change guest's position in mixer |
## Target Parameter Explanation

View File

@@ -0,0 +1,434 @@
## Enhanced IFRAME API Documentation - HTTP/WSS API Integration
### Overview
The VDO.Ninja IFRAME API provides access to all HTTP/WSS API commands through the `action` parameter. This means you can use any command from the [HTTP/WSS API](https://github.com/steveseguin/Companion-Ninja) directly through the iframe's postMessage interface.
### Using HTTP/WSS API Commands via IFRAME
All commands available in the HTTP/WSS API can be accessed through the IFRAME API using this format:
```javascript
iframe.contentWindow.postMessage({
action: "commandName",
value: "value",
value2: "optional",
target: "optional", // for director commands
cib: "callback-id" // optional callback identifier
}, "*");
```
### Director Permissions
**Important:** To use director commands for remote control, you must have director permissions:
1. Use `&director=roomname` instead of `&room=roomname` in your iframe URL
2. Or combine with `&codirector=password` to enable multiple directors
3. Without proper permissions, director commands will fail silently
Example iframe URL with director permissions:
```
https://vdo.ninja/?director=myroom&cleanoutput&api=myapikey
```
### Complete Command Reference
#### Self Commands (No Target Required)
These commands affect the local VDO.Ninja instance:
```javascript
// Microphone control
iframe.contentWindow.postMessage({ action: "mic", value: "toggle" }, "*");
// Camera control
iframe.contentWindow.postMessage({ action: "camera", value: false }, "*");
// Speaker control
iframe.contentWindow.postMessage({ action: "speaker", value: true }, "*");
// Volume control (0-200)
iframe.contentWindow.postMessage({ action: "volume", value: 85 }, "*");
// Recording
iframe.contentWindow.postMessage({ action: "record", value: true }, "*");
// Bitrate control
iframe.contentWindow.postMessage({ action: "bitrate", value: 2500 }, "*");
// Layout control
iframe.contentWindow.postMessage({ action: "layout", value: 2 }, "*");
// Custom layout object
iframe.contentWindow.postMessage({
action: "layout",
value: [
{x: 0, y: 0, w: 50, h: 100, slot: 0},
{x: 50, y: 0, w: 50, h: 100, slot: 1}
]
}, "*");
// Group management
iframe.contentWindow.postMessage({ action: "joinGroup", value: "1" }, "*");
iframe.contentWindow.postMessage({ action: "leaveGroup", value: "2" }, "*");
// Get information
iframe.contentWindow.postMessage({ action: "getDetails", cib: "details-123" }, "*");
iframe.contentWindow.postMessage({ action: "getGuestList", cib: "guests-456" }, "*");
// Camera PTZ controls
iframe.contentWindow.postMessage({ action: "zoom", value: 0.1 }, "*"); // Relative
iframe.contentWindow.postMessage({ action: "zoom", value: 1.5, value2: "abs" }, "*"); // Absolute
iframe.contentWindow.postMessage({ action: "pan", value: -0.5 }, "*");
iframe.contentWindow.postMessage({ action: "tilt", value: 0.1 }, "*");
iframe.contentWindow.postMessage({ action: "focus", value: 0.8, value2: "abs" }, "*");
// Other controls
iframe.contentWindow.postMessage({ action: "reload" }, "*");
iframe.contentWindow.postMessage({ action: "hangup" }, "*");
iframe.contentWindow.postMessage({ action: "togglehand" }, "*");
iframe.contentWindow.postMessage({ action: "togglescreenshare" }, "*");
iframe.contentWindow.postMessage({ action: "forceKeyframe" }, "*");
iframe.contentWindow.postMessage({ action: "sendChat", value: "Hello everyone!" }, "*");
```
#### Director Commands (Target Required)
These commands require director permissions and target specific guests:
```javascript
// Target can be:
// - Slot number: "1", "2", "3", etc.
// - Stream ID: "abc123xyz"
// - "*" for all guests (where applicable)
// Guest microphone control
iframe.contentWindow.postMessage({
action: "mic",
target: "1",
value: "toggle"
}, "*");
// Guest camera control
iframe.contentWindow.postMessage({
action: "camera",
target: "streamID123",
value: false
}, "*");
// Add guest to scene
iframe.contentWindow.postMessage({
action: "addScene",
target: "2",
value: 1 // Scene number
}, "*");
// Transfer guest to another room
iframe.contentWindow.postMessage({
action: "forward",
target: "1",
value: "newroom"
}, "*");
// Solo chat with guest
iframe.contentWindow.postMessage({
action: "soloChat",
target: "3"
}, "*");
// Two-way solo chat
iframe.contentWindow.postMessage({
action: "soloChatBidirectional",
target: "2"
}, "*");
// Send private message to guest
iframe.contentWindow.postMessage({
action: "sendChat",
target: "1",
value: "Private message"
}, "*");
// Overlay message on guest's screen
iframe.contentWindow.postMessage({
action: "sendDirectorChat",
target: "2",
value: "You're live in 10 seconds!"
}, "*");
// Guest volume control
iframe.contentWindow.postMessage({
action: "volume",
target: "1",
value: 120 // 0-200
}, "*");
// Disconnect specific guest
iframe.contentWindow.postMessage({
action: "hangup",
target: "3"
}, "*");
// Guest camera PTZ control
iframe.contentWindow.postMessage({
action: "zoom",
target: "1",
value: 0.1
}, "*");
// Timer controls for guest
iframe.contentWindow.postMessage({
action: "startRoomTimer",
target: "1",
value: 600 // 10 minutes in seconds
}, "*");
// Change guest position in mixer
iframe.contentWindow.postMessage({
action: "mixorder",
target: "2",
value: -1 // Move up
}, "*");
```
### Using targetGuest Function (Legacy)
The `targetGuest` function provides another way to control guests:
```javascript
iframe.contentWindow.postMessage({
function: "targetGuest",
target: "1", // Guest slot or stream ID
action: "mic", // Action to perform
value: "toggle" // Value (optional)
}, "*");
```
### Using Commands Function
Access any command from the Commands object:
```javascript
iframe.contentWindow.postMessage({
function: "commands",
action: "zoom",
value: 0.5,
value2: "abs"
}, "*");
```
### Advanced DOM Manipulation
Target specific video elements by stream ID:
```javascript
// Add video to grid
iframe.contentWindow.postMessage({
target: "streamID123",
add: true
}, "*");
// Remove video from grid
iframe.contentWindow.postMessage({
target: "streamID123",
remove: true
}, "*");
// Replace all videos with target
iframe.contentWindow.postMessage({
target: "streamID123",
replace: true
}, "*");
// Apply settings to video element
iframe.contentWindow.postMessage({
target: "streamID123",
settings: {
style: "transform: scale(1.5);",
muted: true,
volume: 0.5
}
}, "*");
```
### Special Functions
```javascript
// Preview local webcam
iframe.contentWindow.postMessage({
function: "previewWebcam"
}, "*");
// Publish screen share
iframe.contentWindow.postMessage({
function: "publishScreen"
}, "*");
// Change HTML content
iframe.contentWindow.postMessage({
function: "changeHTML",
target: "elementId",
value: "<p>New content</p>"
}, "*");
// Route WebSocket message
iframe.contentWindow.postMessage({
function: "routeMessage",
value: { /* message data */ }
}, "*");
// Execute code (use with extreme caution)
iframe.contentWindow.postMessage({
function: "eval",
value: "console.log('Hello from eval');"
}, "*");
```
### Handling Responses
Listen for responses with callback IDs:
```javascript
window.addEventListener("message", function(e) {
if (e.source !== iframe.contentWindow) return;
if (e.data.cib === "my-callback-123") {
console.log("Received response:", e.data);
// Handle different response types
if (e.data.guestList) {
console.log("Guest list:", e.data.guestList);
} else if (e.data.detailedState) {
console.log("State info:", e.data.detailedState);
} else if (e.data.callback) {
console.log("Command result:", e.data.callback.result);
}
}
});
```
### Complete Example: Director Control Panel
```html
<!DOCTYPE html>
<html>
<head>
<title>VDO.Ninja Director Control Panel</title>
</head>
<body>
<h1>Director Control Panel</h1>
<div id="container"></div>
<div id="controls">
<h2>Guest Controls</h2>
<select id="guest-select">
<option value="1">Guest 1</option>
<option value="2">Guest 2</option>
<option value="3">Guest 3</option>
</select>
<button onclick="controlGuest('mic', 'toggle')">Toggle Mic</button>
<button onclick="controlGuest('camera', 'toggle')">Toggle Camera</button>
<button onclick="controlGuest('addScene', 1)">Add to Scene 1</button>
<button onclick="controlGuest('forward', 'lobby')">Send to Lobby</button>
<button onclick="controlGuest('zoom', 0.1)">Zoom In</button>
<button onclick="controlGuest('zoom', -0.1)">Zoom Out</button>
</div>
<div id="log"></div>
<script>
// Create iframe with director permissions
const iframe = document.createElement("iframe");
iframe.allow = "camera;microphone;fullscreen;display-capture;autoplay;";
iframe.src = "https://vdo.ninja/?director=myroom&cleanoutput&api=mykey";
iframe.style.width = "800px";
iframe.style.height = "600px";
document.getElementById("container").appendChild(iframe);
// Control function
function controlGuest(action, value) {
const target = document.getElementById("guest-select").value;
const message = {
action: action,
target: target
};
if (value !== undefined) {
message.value = value;
}
// Generate callback ID
const callbackId = `cb-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
message.cib = callbackId;
iframe.contentWindow.postMessage(message, "*");
log(`Sent: ${JSON.stringify(message)}`);
}
// Listen for responses
window.addEventListener("message", function(e) {
if (e.source !== iframe.contentWindow) return;
log(`Received: ${JSON.stringify(e.data)}`);
// Handle specific events
if (e.data.action === "guest-connected") {
log(`Guest connected: ${e.data.streamID}`);
} else if (e.data.guestList) {
updateGuestList(e.data.guestList);
}
});
// Logging
function log(message) {
const logDiv = document.getElementById("log");
const entry = document.createElement("div");
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logDiv.appendChild(entry);
logDiv.scrollTop = logDiv.scrollHeight;
}
// Update guest list
function updateGuestList(guests) {
const select = document.getElementById("guest-select");
select.innerHTML = "";
guests.forEach((guest, index) => {
const option = document.createElement("option");
option.value = guest.id || (index + 1);
option.textContent = guest.label || `Guest ${index + 1}`;
select.appendChild(option);
});
}
// Get initial guest list
setTimeout(() => {
iframe.contentWindow.postMessage({
action: "getGuestList",
cib: "initial-guests"
}, "*");
}, 2000);
</script>
</body>
</html>
```
### Important Notes
1. **Director Permissions**: Always use `&director=roomname` or `&codirector=password` for director commands
2. **Target Format**: Use slot numbers (1, 2, 3) or stream IDs for targeting
3. **Callback IDs**: Use unique `cib` values to track responses
4. **Error Handling**: Commands may fail silently without proper permissions
5. **Timing**: Wait for iframe to load before sending commands
### Troubleshooting
- **Commands not working**: Check director permissions in iframe URL
- **No response**: Verify callback ID handling and message source
- **Guest not found**: Confirm target value matches slot or stream ID
- **Permission errors**: Ensure using `&director=` not `&room=`
This integration allows you to build powerful control interfaces using the full capabilities of the VDO.Ninja API through simple iframe messaging.

View File

@@ -1,448 +1,540 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>VDO.Ninja Examples Catalog</title>
<link rel="stylesheet" href="https://vdo.ninja/main.css" />
<style>
.container {
max-width: 1200px;
width: 95%;
margin: 0 auto;
:root {
--bg: #0f131d;
--card-bg: #1c2333;
--card-border: rgba(255, 255, 255, 0.08);
--text-muted: #c2cad8;
--accent: #8ecae6;
}
* {
box-sizing: border-box;
}
body {
position:relative!important;
}
h1 {
margin-top: 3em;
text-align: center;
}
.intro {
text-align: center;
margin: 2em auto;
max-width: 800px;
color: #ccc;
}
h2 {
font-size: 1.2em;
padding: 10px;
background-color: #457b9d;
color: white;
border-bottom: 2px solid #3b6a87;
text-align: center;
margin: 0;
min-height: 100vh;
background: var(--bg);
color: #f5f6f8;
font-family: Inter, "Segoe UI", system-ui, -apple-system, sans-serif;
}
h2 a {
color: white !important;
a {
color: #edf6ff;
text-decoration: none;
}
h2 a:hover {
a:hover,
a:focus-visible {
text-decoration: underline;
}
#examples {
margin-top: 3em;
#header {
padding: 16px 24px;
background: rgba(0, 0, 0, 0.25);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
#header a {
color: #f5f6f8;
font-size: 1.6rem;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 8px;
}
.container {
max-width: 1120px;
margin: 0 auto;
padding: 40px 24px 64px;
}
.page-header {
margin-bottom: 32px;
}
.page-header h1 {
margin: 0 0 0.6em;
font-size: clamp(2rem, 2.8vw, 2.6rem);
letter-spacing: -0.01em;
}
.page-header p {
margin: 0 0 0.8em;
color: var(--text-muted);
max-width: 70ch;
line-height: 1.6;
}
#example-groups {
display: flex;
flex-direction: column;
gap: 48px;
}
.group h2 {
margin: 0;
font-size: clamp(1.4rem, 2.2vw, 1.8rem);
}
.group-description {
margin: 8px 0 24px;
color: var(--text-muted);
max-width: 70ch;
line-height: 1.6;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
grid-gap: 1.5em;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 18px;
}
div#examples>div {
background: #2c2c2c;
color: white;
border: 1px solid #444;
border-radius: 8px;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
div#examples>div:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
.example-card {
display: flex;
flex-direction: column;
gap: 10px;
min-height: 190px;
padding: 18px;
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 14px;
box-shadow: 0 18px 40px -32px rgba(9, 11, 19, 0.9);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.description {
padding: 1em;
display: block;
min-height: 60px;
color: #ddd;
font-size: 0.95em;
.example-card:hover {
transform: translateY(-4px);
box-shadow: 0 22px 42px -28px rgba(11, 15, 25, 0.85);
}
.youtube {
display: inline-block;
margin-right: 0.5em;
}
.github {
display: inline-block;
margin-right: 0.5em;
.example-card h3 {
margin: 0;
font-size: 1.15rem;
letter-spacing: -0.01em;
}
.media {
background: rgba(69, 123, 157, 0.3);
display: block;
width: 100%;
padding: 0.5em;
text-align: right;
border-top: 1px solid #457b9d;
.example-card p {
margin: 0;
color: var(--text-muted);
line-height: 1.5;
}
.media img {
width: 20px;
height: 20px;
vertical-align: middle;
.tag-list {
margin: auto 0 0;
padding: 0;
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.category {
grid-column: 1 / -1;
margin-top: 2em;
margin-bottom: 1em;
padding: 0.5em;
background: rgba(69, 123, 157, 0.2);
border-left: 4px solid #457b9d;
font-size: 1.3em;
color: white;
.tag-list li {
padding: 4px 10px;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
border-radius: 999px;
background: rgba(142, 202, 230, 0.14);
color: var(--accent);
}
.deprecated {
opacity: 0.7;
}
.deprecated h2 {
background-color: #666;
border-bottom-color: #555;
}
.new {
border: 2px solid #4CAF50;
}
.new h2 {
background-color: #4CAF50;
border-bottom-color: #45a049;
@media (max-width: 720px) {
.container {
padding: 28px 18px 48px;
}
.card-grid {
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
}
</style>
</head>
<body style='color:white'>
<body>
<div id="header">
<a id="logoname" href="../" style="text-decoration: none; color: white; margin: 2px">
<a id="logoname" href="../">
<span data-translate="logo-header">
<font id="qos">V</font>DO.Ninja
</span>
</a>
</div>
<div class="container">
<div id="info">
<h1>VDO.Ninja Examples & Demos</h1>
<p class="intro">
A collection of examples demonstrating various features and integration possibilities with VDO.Ninja.
These examples show how to use the iframe API, custom overlays, remote control features, and more.
</p>
<div id="examples">
<!-- Core API Examples -->
<div class="category">🔧 Core API & Integration</div>
<div>
<h2><a href='p2p.html'>P2P Data Transport</a></h2>
<div class="description">Demonstrates how to use VDO.Ninja as a peer-to-peer data transport and tunneling service for custom applications</div>
</div>
<div>
<h2><a href='iframe.outbound-stats.html'>Outbound Stats API</a></h2>
<div class="description">Shows how to retrieve detailed outbound streaming statistics using the iframe API</div>
</div>
<div>
<h2><a href='iframe.inbound-stats.html'>Inbound Stats API</a></h2>
<div class="description">Shows how to retrieve detailed inbound streaming statistics using the iframe API</div>
</div>
<div>
<h2><a href='remoteapi.html'>Remote API Control</a></h2>
<div class="description">Demonstrates remote control capabilities through the VDO.Ninja API</div>
</div>
<div>
<h2><a href='httpwssapi.md'>HTTP/WSS API Docs</a></h2>
<div class="description">Documentation for the HTTP and WebSocket API endpoints</div>
</div>
<!-- UI & Layout Examples -->
<div class="category">🎨 UI & Layout Customization</div>
<div>
<h2><a href='draggable.html'>Draggable Windows</a></h2>
<div class="description">Create custom layouts by dragging multiple video windows around the screen (experimental)</div>
</div>
<div>
<h2><a href='grid.html'>Grid Layout</a></h2>
<div class="description">Example of a custom grid layout for multiple video streams</div>
</div>
<div>
<h2><a href='rotated.html'>Rotated Video</a></h2>
<div class="description">Shows how to rotate video elements for creative layouts</div>
</div>
<div>
<h2><a href='custom_overlay.html'>Custom Overlay</a></h2>
<div class="description">How to create custom overlays on top of VDO.Ninja video streams</div>
</div>
<div>
<h2><a href='overlay.html'>Basic Overlay</a></h2>
<div class="description">Simple overlay example for adding graphics to streams</div>
</div>
<div>
<h2><a href='test_overlay.html'>Test Overlay</a></h2>
<div class="description">Testing framework for overlay development</div>
</div>
<div>
<h2><a href='labelonly.html'>Labels Only View</a></h2>
<div class="description">Display only the labels/names without video for a minimal interface</div>
</div>
<div>
<h2><a href='custom_labels.html'>Custom Labels</a></h2>
<div class="description">How to create and style custom labels for video streams</div>
</div>
<!-- Room & Scene Management -->
<div class="category">🎬 Room & Scene Management</div>
<div>
<h2><a href='multi.html?rooms=room1xx,room2xx,room3xx'>Multiple Rooms</a></h2>
<div class="description">Manage multiple director rooms in a single browser tab</div>
</div>
<div>
<h2><a href='addtoscene.html'>Add to Scene</a></h2>
<div class="description">Dynamically add or remove guests to/from scenes using the iframe API</div>
</div>
<div>
<h2><a href='waitingroom.html'>Waiting Room</a></h2>
<div class="description">Implementation of a waiting room for guests before joining the main session</div>
</div>
<div>
<h2><a href='kick.html'>Kick Guest</a></h2>
<div class="description">Example of how to remove/kick guests from a room</div>
</div>
<!-- Control & Interaction -->
<div class="category">🎮 Control & Interaction</div>
<div>
<h2><a href='control.html'>Control Interface</a></h2>
<div class="description">Custom control interface for managing VDO.Ninja sessions</div>
</div>
<div>
<h2><a href='bigmutebutton.html'>Big Mute Button</a></h2>
<div class="description">Mobile-friendly large mute button for easy access during streams</div>
</div>
<div>
<h2><a href='muteguestiframe.html'>Mute Guest (iframe)</a></h2>
<div class="description">Control guest audio through iframe API commands</div>
</div>
<div>
<h2><a href='custom_video_switcher.html'>Video Switcher</a></h2>
<div class="description">Custom video switching interface for multi-camera setups</div>
</div>
<div>
<h2><a href='switchmics.html'>Switch Microphones</a></h2>
<div class="description">Interface for switching between multiple microphone inputs</div>
</div>
<div>
<h2><a href='slidingzoom.html'>Sliding Zoom Control</a></h2>
<div class="description">Smooth zoom control interface with sliding mechanism</div>
</div>
<div>
<h2><a href='ptz.html'>PTZ Camera Control</a></h2>
<div class="description">Pan, Tilt, and Zoom control interface for PTZ cameras</div>
</div>
<!-- Hardware & Sensors -->
<div class="category">📱 Hardware & Sensors</div>
<div>
<h2><a href='sensors.html'>Mobile Sensors</a></h2>
<div class="media">
<a href='https://www.youtube.com/watch?v=SqbufszHKi4' class="youtube">
<img src="youtube.svg" />
</a>
</div>
<div class="description">Transmit accelerometer and gyroscope data from mobile devices</div>
</div>
<div>
<h2><a href='sensoroverlay.html'>Sensor Data Overlay</a></h2>
<div class="description">Display real-time sensor data (like speed) as an overlay on video</div>
</div>
<div>
<h2><a href='gamecontroller.html'>Game Controller</a></h2>
<div class="description">Use game controllers as input devices for stream control</div>
</div>
<div>
<h2><a href='webhid.html'>WebHID Devices</a></h2>
<div class="description">Interface with USB HID devices like Stream Deck (not mouse/keyboard)</div>
</div>
<div>
<h2><a href='wireless.html'>Wireless Control</a></h2>
<div class="description">Wireless device integration and control examples</div>
</div>
<div>
<h2><a href='midi.html'>MIDI Control</a></h2>
<div class="media">
<a href='https://www.youtube.com/watch?v=rnZ8HM9FL4I' class="youtube">
<img src="youtube.svg" />
</a>
</div>
<div class="description">Control VDO.Ninja with MIDI devices and controllers</div>
</div>
<!-- Streaming Platforms -->
<div class="category">📺 Platform Integration</div>
<div>
<h2><a href='twitch.html'>Twitch Integration</a></h2>
<div class="description">Display Twitch chat alongside VDO.Ninja streams</div>
</div>
<div>
<h2><a href='youtube.html'>YouTube Integration</a></h2>
<div class="description">Display YouTube live chat alongside VDO.Ninja streams</div>
</div>
<div>
<h2><a href='obs_remote/index.html'>OBS Remote Control</a></h2>
<div class="media">
<a href='https://github.com/steveseguin/remote_ninja' class="github">
<img src="github.svg" />
</a>
</div>
<div class="description">Remotely control OBS Studio through VDO.Ninja's tunneling</div>
</div>
<div>
<h2><a href='obsremote.html'>OBS Remote (Simple)</a></h2>
<div class="description">Simplified OBS remote control interface</div>
</div>
<!-- Specialized Applications -->
<div class="category">🚀 Specialized Applications</div>
<div>
<h2><a href='teleprompter.html'>Teleprompter</a></h2>
<div class="description">Full-featured teleprompter application with speed control</div>
</div>
<div>
<h2><a href='teleprompt.html'>Teleprompter (Simple)</a></h2>
<div class="description">Simplified teleprompter for basic scrolling text</div>
</div>
<div>
<h2><a href='powerpoint.html'>PowerPoint Control</a></h2>
<div class="description">Remote control for PowerPoint presentations</div>
</div>
<div>
<h2><a href='esports.html'>Esports Layout</a></h2>
<div class="description">Specialized layout for esports streaming and tournaments</div>
</div>
<div>
<h2><a href='mixer.html'>Audio/Video Mixer</a></h2>
<div class="description">Multi-source audio and video mixing interface</div>
</div>
<div>
<h2><a href='socal.html'>Social Overlay</a></h2>
<div class="description">Social media integration and overlay system</div>
</div>
<!-- Communication -->
<div class="category">💬 Communication Features</div>
<div>
<h2><a href='chatoverlay.html'>Chat Overlay</a></h2>
<div class="description">Chat-only interface that can be docked into OBS</div>
</div>
<div>
<h2><a href='googleai.html'>Google AI Integration</a></h2>
<div class="description">Integration with Google AI services for enhanced features</div>
</div>
<!-- Utility Examples -->
<div class="category">🛠️ Utilities</div>
<div>
<h2><a href='dual.html'>Dual Windows</a></h2>
<div class="description">Picture-in-Picture style layout with two VDO.Ninja instances</div>
</div>
<div>
<h2><a href='zoom.html'>Zoom Mode</a></h2>
<div class="description">Fullscreen mode optimized for window capture in Zoom</div>
</div>
<div>
<h2><a href='changepass.html'>Password Generator</a></h2>
<div class="description">Create secure passwords and hash values for VDO.Ninja rooms</div>
</div>
<div>
<h2><a href='status.html'>Connection Status</a></h2>
<div class="description">Monitor connection status and quality metrics</div>
</div>
<div>
<h2><a href='simplelink.html'>Simple Link Generator</a></h2>
<div class="description">Basic interface for generating VDO.Ninja links</div>
</div>
<div>
<h2><a href='testsdp.html'>SDP Testing</a></h2>
<div class="description">Test and debug Session Description Protocol data</div>
</div>
<!-- External -->
<div class="category">🌐 External Projects</div>
<div>
<h2><a href='https://versus.cam'>Versus.cam</a></h2>
<div class="description">Advanced iframe API usage for audio/video transport between frames</div>
</div>
</div>
<div style="margin-top: 4em; padding: 2em; text-align: center; color: #888;">
<p>Looking for more documentation?</p>
<p>
<a href="iframeapi.md" style="color: #457b9d;">📄 iframe API Documentation</a> |
<a href="readme.md" style="color: #457b9d;">📖 Examples README</a> |
<a href="https://docs.vdo.ninja" style="color: #457b9d;">📚 Full Documentation</a>
</p>
</div>
</div>
</div>
</body>
<main class="container">
<section class="page-header">
<h1>Examples &amp; Experiments</h1>
<p>These demos explore different ways to embed, automate, and customize VDO.Ninja. Every page is optional,
URL-driven, and kept separate from the core experience, so feel free to remix them for your own
workflows.</p>
<p>Most pages expect you to launch them with live push/view links or room IDs. Look at the comments or
top-of-page instructions for required parameters, or tweak the URL to try different behaviours.</p>
</section>
<div id="example-groups"></div>
</main>
<script>
const exampleGroups = [
{
name: "Remote Control & Automation",
description: "Control rooms, scenes, or devices remotely using the IFRAME API, data channels, and postMessage hooks.",
items: [
{
href: "addtoscene.html",
title: "Add to Scene Controller",
summary: "Use the IFRAME API to add or remove pre-defined stream IDs from a scene and manage their mic state."
},
{
href: "bigmutebutton.html",
title: "Big Mute Button Remote",
summary: "Mobile-friendly remote that lets talent toggle their microphone with oversized controls."
},
{
href: "control.html",
title: "Legacy Layout Console",
summary: "Send quick layout macros to the director scene to demo the classic control surface."
},
{
href: "custom_video_switcher.html",
title: "Custom Video Switcher",
summary: "Swap between preset stream IDs in a manual scene using targeted postMessage commands."
},
{
href: "esports.html",
title: "Esports POV Toggler",
summary: "Showcase multiple POV feeds with buttons to reveal each player or clear the scene."
},
{
href: "muteguestiframe.html",
title: "Mute Guest via IFRAME",
summary: "Remotely toggle speaker and mic states for a specific stream ID inside an embedded scene."
},
{
href: "obsremote.html",
title: "OBS Remote Mini",
summary: "Lightweight interface that tunnels OBS websocket control through VDO.Ninja."
},
{
href: "obs_remote/index.html",
title: "OBS Remote Dashboard",
summary: "Full-featured OBS controller with previews and macros, proxied through VDO.Ninja."
},
{
href: "powerpoint.html",
title: "PowerPoint Remote",
summary: "Flip through slides remotely while embedding a live view of the deck."
},
{
href: "ptz.html",
title: "PTZ Remote",
summary: "Pan, tilt, and zoom a remote camera by sending PTZ commands over the data channel."
},
{
href: "remoteapi.html",
title: "Remote API Playground",
summary: "Retro-themed sandbox for experimenting with remote control actions and macros."
},
{
href: "slidingzoom.html",
title: "Sliding Zoom Controller",
summary: "Touch-friendly slider that adjusts zoom levels via VDO.Ninja PTZ hooks."
},
{
href: "switchmics.html",
title: "Switch Mics",
summary: "Toggle mute on two remote guests with single-click controls for quick audio checks."
},
{
href: "teleprompt.html",
title: "Teleprompt Controller",
summary: "Producer-facing interface to push script text and settings to the teleprompter view."
}
]
},
{
name: "Production Overlays & Layouts",
description: "Browser sources and layouts you can drop into OBS, a switcher, or a director tab.",
items: [
{
href: "chat.html",
title: "Chat Shout Overlay",
summary: "Monospaced stacked chat overlay tuned for big on-screen shoutouts."
},
{
href: "chatoverlay.html",
title: "Clean Chat Overlay",
summary: "Modern chat overlay for OBS with avatars, styling toggles, and sanitised content."
},
{
href: "custom_labels.html",
title: "Custom Labels Overlay",
summary: "Display lower-third labels that update automatically as guests join with custom names."
},
{
href: "custom_overlay.html",
title: "Dynamic Overlay Frame",
summary: "Trigger branded overlays and nameplates from connection metadata."
},
{
href: "draggable.html",
title: "Draggable Multi-View",
summary: "Arrange several viewer windows manually by dragging and resizing thumbnails."
},
{
href: "dual.html",
title: "Dual View Layout",
summary: "Display two guests side-by-side using the dual director layout logic."
},
{
href: "gamecontroller.html",
title: "Controller Visualizer",
summary: "Render HID and gamepad events as an overlay-friendly controller graphic."
},
{
href: "grid.html",
title: "Grid Builder",
summary: "Drop arbitrary URLs into a responsive iframe grid for multi-angle monitoring."
},
{
href: "labelonly.html",
title: "Label Only Overlay",
summary: "Show only the connected guest's label as a minimal lower-third element."
},
{
href: "mixer.html",
title: "Mixer Sandbox",
summary: "Try the manual scene mixer by dragging streams into layout slots."
},
{
href: "multi.html",
title: "Multi-Room Monitor",
summary: "Open multiple director rooms in one tab using the ?rooms= list parameter."
},
{
href: "overlay.html",
title: "Overlay Helper",
summary: "Combine a VDO.Ninja feed with an external overlay page inside one source."
},
{
href: "rotated.html",
title: "Rotated Scene Output",
summary: "Rotate the scene output for portrait monitors or tall confidence displays."
},
{
href: "sensoroverlay.html",
title: "Sensor Data Overlay",
summary: "Render live speed and telemetry data over an incoming video feed."
},
{
href: "status.html",
title: "Status Ticker",
summary: "Ticker-style overlay for status updates pulled from chat events."
},
{
href: "teleprompter.html",
title: "Teleprompter Display",
summary: "Talent-facing teleprompter view that mirrors incoming script text."
},
{
href: "waitingroom.html",
title: "Waiting Room Overlay",
summary: "Show a standby message until the remote feed connects and starts playing."
}
]
},
{
name: "Social & Platform Integrations",
description: "Pair VDO.Ninja streams with third-party chat or engagement widgets.",
items: [
{
href: "kick.html",
title: "Kick + Video",
summary: "Combine a Kick chat embed with a VDO.Ninja source in one window."
},
{
href: "socal.html",
title: "SocialStream Hub",
summary: "Switch between multiple social chat integrations alongside a VDO.Ninja feed."
},
{
href: "twitch.html",
title: "Twitch + Video",
summary: "Embed Twitch chat next to a VDO.Ninja feed for streamers."
},
{
href: "youtube.html",
title: "YouTube Chat + Video",
summary: "Co-host YouTube live chat with a VDO.Ninja guest feed."
}
]
},
{
name: "Data, Sensors & Messaging",
description: "Examples that push telemetry, sensor readings, or custom data through VDO.Ninja.",
items: [
{
href: "datachannel-pubsub.html",
title: "DataChannel Pub/Sub",
summary: "Publish structured messages and subscribe to updates over VDO.Ninja data channels."
},
{
href: "p2p.html",
title: "P2P Data Tunnel",
summary: "Use paired iframes to pass arbitrary data peer-to-peer via VDO.Ninja."
},
{
href: "sensors.html",
title: "Sensors Dashboard",
summary: "Capture mobile sensor readings and video, rendering telemetry on canvas."
},
{
href: "rip.html",
title: "RIP Canvas Relay",
summary: "Capture a remote view to a hidden canvas for further processing or mixing."
},
{
href: "wireless.html",
title: "Wireless Relay Lab",
summary: "Manual message relay between two peers with testing and compression controls."
}
]
},
{
name: "Developer & SDK Samples",
description: "Deeper dives into the SDK, automation helpers, and hardware integrations.",
items: [
{
href: "dynamic-viewer.html",
title: "Dynamic Viewer SDK",
summary: "UI-driven sample that adds and removes views dynamically with the SDK helpers."
},
{
href: "googleai.html",
title: "Gemini Vision Chat",
summary: "Integrate Google Gemini live video analysis with a VDO.Ninja feed."
},
{
href: "iframe.inbound-stats.html",
title: "IFRAME Inbound Stats",
summary: "Request inbound stats over the IFRAME API and log them for inspection."
},
{
href: "iframe.outbound-stats.html",
title: "IFRAME Outbound Stats",
summary: "Collect outbound stats from an embedded iframe for monitoring."
},
{
href: "midi.html",
title: "MIDI Controller",
summary: "Map MIDI inputs to VDO.Ninja events and test hotkey commands."
},
{
href: "sandbox.html",
title: "Developer API Sandbox",
summary: "All-in-one playground for experimenting with the VDO.Ninja developer API."
},
{
href: "simple-iframe-replacement.html",
title: "DataChannel Replacement",
summary: "Shows how to replace hidden iframes with the DataChannel SDK."
},
{
href: "turn-only-example.html",
title: "TURN Only Example",
summary: "Demonstrates forcing TURN-only routing and inspecting connection details."
},
{
href: "webhid.html",
title: "WebHID Demo",
summary: "Connect WebHID devices such as a StreamDeck and forward events through VDO.Ninja."
}
]
},
{
name: "Utilities & Helpers",
description: "Small helpers for link generation, diagnostics, or local capture workflows.",
items: [
{
href: "changepass.html",
title: "Password Hasher",
summary: "Prompt-driven tool that creates salted room hashes for invite links."
},
{
href: "noisegate.html",
title: "Noise Gate Converter",
summary: "Convert classic OBS noise gate settings into the modern \"My Gate\" format."
},
{
href: "simplelink.html",
title: "Simple Link Generator",
summary: "Generate publish, view, and scene links with common parameters."
},
{
href: "zoom.html",
title: "Zoom Capture Helper",
summary: "Local capture preview that goes fullscreen for easy window capture."
}
]
}
];
const groupsRoot = document.getElementById("example-groups");
for (const group of exampleGroups) {
const section = document.createElement("section");
section.className = "group";
const heading = document.createElement("h2");
heading.textContent = group.name;
section.appendChild(heading);
if (group.description) {
const desc = document.createElement("p");
desc.className = "group-description";
desc.textContent = group.description;
section.appendChild(desc);
}
const grid = document.createElement("div");
grid.className = "card-grid";
for (const item of group.items) {
const card = document.createElement("article");
card.className = "example-card";
const title = document.createElement("h3");
const link = document.createElement("a");
link.href = item.href;
link.textContent = item.title;
title.appendChild(link);
card.appendChild(title);
const summary = document.createElement("p");
summary.textContent = item.summary;
card.appendChild(summary);
if (Array.isArray(item.tags) && item.tags.length) {
const tagList = document.createElement("ul");
tagList.className = "tag-list";
for (const tag of item.tags) {
const tagItem = document.createElement("li");
tagItem.textContent = tag;
tagList.appendChild(tagItem);
}
card.appendChild(tagList);
}
grid.appendChild(card);
}
section.appendChild(grid);
groupsRoot.appendChild(section);
}
</script>
</body>
</html>

View File

@@ -1,6 +1,6 @@
<html>
<head>
<title>IFRAME Example</title>
<title>Manual Mixer Sandbox - VDO.Ninja</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<style>
@@ -448,4 +448,4 @@
<div id="container">
</div>
</body>
</html>
</html>

View File

@@ -1,5 +1,5 @@
<html>
<head><title>Dual Input</title>
<head><title>Multi-Room Monitor - VDO.Ninja</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<style>
@@ -109,4 +109,4 @@ function loadIframes(url){
</script>
</body>
</html>
</html>

View File

@@ -1,6 +1,6 @@
<html>
<head>
<title>IFRAME Example</title>
<title>Mute Guest via IFRAME - VDO.Ninja</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<style>
@@ -316,4 +316,4 @@
Custom guest invites and toggles for add/removing from scene=1 are on the bottom.
</div>
</body>
</html>
</html>

357
examples/noisegate.html Normal file
View File

@@ -0,0 +1,357 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Noise Gate Converter — Classic → "My Gate"</title>
<style>
:root {
--bg: #0e1116;
--panel: #171b23;
--muted: #9aa4b2;
--text: #e7edf3;
--accent: #6ee7b7;
--accent-2: #60a5fa;
--danger: #f87171;
}
* { box-sizing: border-box; }
body {
margin: 0; background: radial-gradient(1200px 600px at 20% -10%, #131826 0%, #0f1320 45%, var(--bg) 100%);
color: var(--text); font: 14px/1.35 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell;
}
.wrap { max-width: 1100px; margin: 32px auto; padding: 0 16px; }
header { display: flex; gap: 16px; align-items: center; justify-content: space-between; }
h1 { font-size: 22px; margin: 0; letter-spacing: 0.2px; }
.card { background: linear-gradient(180deg, rgba(255,255,255,.02), rgba(255,255,255,.0) 30%), var(--panel);
border: 1px solid rgba(255,255,255,.06); border-radius: 16px; padding: 16px; box-shadow: 0 10px 30px rgba(0,0,0,.25); }
.grid { display: grid; grid-template-columns: 1.1fr 1fr; gap: 16px; }
.inputs { display: grid; grid-template-columns: repeat(2, minmax(180px, 1fr)); gap: 12px; }
label { display: block; font-weight: 600; color: #cdd6e1; margin-bottom: 6px; }
input[type="number"] { width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid rgba(255,255,255,.08);
background: #0e1320; color: var(--text); outline: none; }
input[type="number"]:focus { border-color: var(--accent-2); box-shadow: 0 0 0 3px rgba(96,165,250,.25); }
.row { display: flex; gap: 10px; align-items: center; }
.muted { color: var(--muted); }
.pill { padding: 4px 10px; border-radius: 999px; border: 1px solid rgba(255,255,255,.08); background: rgba(255,255,255,.04); }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; font-size: 12.5px; }
pre { margin: 0; white-space: pre-wrap; word-break: break-word; }
button { cursor: pointer; border: 0; padding: 10px 14px; border-radius: 12px; color: #0a0d12; background: var(--accent);
font-weight: 700; letter-spacing: .2px; }
button.secondary { background: #222836; color: #e8eef6; border: 1px solid rgba(255,255,255,.08); }
button.danger { background: var(--danger); color: #0a0d12; }
button:disabled { opacity: .6; cursor: not-allowed; }
.section-title { font-size: 13px; color: #a3b0c2; text-transform: uppercase; letter-spacing: .12em; margin: 16px 0 8px; }
/* meters */
.meters { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px; }
.meter { position: relative; height: 16px; border-radius: 999px; background: #121826;
border: 1px solid rgba(255,255,255,.08); overflow: hidden; }
.meter .fill { position: absolute; left: 0; top: 0; bottom: 0; width: 0%; background: linear-gradient(90deg, var(--accent-2), var(--accent));
transition: width .08s linear; }
.foot { margin-top: 14px; color: #90a1b6; font-size: 12.5px; }
.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.code-block { background: #0b0f19; border: 1px solid rgba(255,255,255,.08); padding: 12px; border-radius: 12px; }
.caption { color: #9eb0c6; font-size: 12.5px; margin: 8px 0 2px; }
.kbd { font-family: ui-monospace, monospace; background: #0e1422; border: 1px solid rgba(255,255,255,.08); border-bottom-width: 2px; padding: 2px 6px; border-radius: 6px; }
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>Classic → <span class="pill mono">My Gate</span> converter</h1>
<div class="row">
<button id="startBtn" class="secondary">Start live test</button>
<button id="resetBtn" class="secondary" title="Reset to sensible defaults">Reset</button>
</div>
</header>
<div class="grid" style="margin-top: 16px;">
<section class="card">
<div class="section-title">Inputs — classic noise gate</div>
<div class="inputs">
<div>
<label for="thr">Threshold (dBFS)</label>
<input id="thr" type="number" step="1" min="-120" max="0" value="-50" />
<div class="muted" style="margin-top:6px">Linear amplitude: <span id="linThresh" class="mono">0.003162</span></div>
</div>
<div>
<label for="att">Attack (ms)</label>
<input id="att" type="number" step="1" min="0" max="2000" value="1" />
</div>
<div>
<label for="rel">Release (ms)</label>
<input id="rel" type="number" step="1" min="0" max="4000" value="100" />
</div>
<div>
<label for="hold">Hold (ms)</label>
<input id="hold" type="number" step="1" min="0" max="4000" value="10" />
</div>
<div>
<label for="range">Range / Depth (% reduction when closed)</label>
<input id="range" type="number" step="1" min="0" max="100" value="80" />
<div class="muted" style="margin-top:6px">Closed gain: <span id="downGain" class="mono">20%</span></div>
</div>
</div>
<div class="section-title">Live meter (optional)</div>
<div class="meters">
<div>
<div class="muted">Input level (approx dBFS)</div>
<div class="meter"><div id="inFill" class="fill" style="--v:0"></div></div>
</div>
<div>
<div class="muted">Gate gain (%)</div>
<div class="meter"><div id="gainFill" class="fill" style="--v:100"></div></div>
</div>
</div>
<div class="foot">Tip: set <span class="kbd">Range</span> for how deep your gate closes. Classic gates often call this <em>Range</em> or <em>Depth</em>. The rest maps 1:1.</div>
</section>
<section class="card">
<div class="section-title">Outputs — what your code expects</div>
<div class="cols">
<div>
<div class="caption">Calls you make when the detector toggles</div>
<div class="code-block mono" id="callsBlock">
<pre>// gate CLOSE (after Hold):
<span id="gateDownCall">changeGatingGain(20, 100)</span>
// gate OPEN (on speech):
<span id="gateUpCall">changeGatingGain(100, 1)</span></pre>
</div>
</div>
<div>
<div class="caption">URL params / compact settings string</div>
<div class="code-block mono">
<pre>?noisegate=1&amp;noisegatesettings=<span id="settingsStr">-50,1,100,10,20</span></pre>
</div>
<div class="caption">Individual values</div>
<div class="code-block mono" id="kv">
<pre>{
thresholdDb: <span id="kv_thr">-50</span>,
attackMs: <span id="kv_att">1</span>,
releaseMs: <span id="kv_rel">100</span>,
holdMs: <span id="kv_hold">10</span>,
closedGainPercent: <span id="kv_closed">20</span>
}</pre>
</div>
<div class="row" style="margin-top:8px; gap:8px;">
<button class="secondary" id="copyUrl">Copy URL</button>
<button class="secondary" id="copyCalls">Copy calls</button>
</div>
</div>
</div>
<div class="section-title">Dropin helper (optional)</div>
<div class="code-block mono" style="max-height: 320px; overflow: auto;">
<pre id="helperCode">// Convert classic gate knobs to your changeGatingGain() usage.
function classicalToMyGate({ thresholdDb, attackMs, releaseMs, holdMs, rangePct = 80 }) {
const clamp = (x, a, b) => Math.min(b, Math.max(a, x));
const downGainPercent = 100 - clamp(rangePct, 0, 100); // % of full when closed
const linearThreshold = Math.pow(10, thresholdDb / 20);
return {
thresholdDb,
linearThreshold, // for detector if you need it
attackMs: Math.max(0, attackMs|0),
releaseMs: Math.max(0, releaseMs|0),
holdMs: Math.max(0, holdMs|0),
downGainPercent, // e.g., 20 means -80% depth
gateDownCall: `changeGatingGain(${downGainPercent}, ${releaseMs|0})`,
gateUpCall: `changeGatingGain(100, ${attackMs|0})`,
// compact string your app can parse
noisegatesettings: [thresholdDb, attackMs|0, releaseMs|0, holdMs|0, downGainPercent|0].join(',')
};
}
// Simple detector that drives your changeGatingGain() based on an AnalyserNode.
// Uses threshold + hold; release/attack are handled by changeGatingGain ramps.
function wireClassicGateDetector({ analyser, audioCtx, thresholdDb, holdMs, attackMs, releaseMs, downGainPercent }) {
const buf = new Float32Array(analyser.fftSize);
let state = 'open';
let holdUntil = 0;
function setGainPct(percent, ms) {
const t = audioCtx.currentTime;
const v = percent / 100;
try {
// mirrors your changeGatingGain() behavior
analyser.disconnect; // no-op keeps linter happy
window.changeGatingGain ? window.changeGatingGain(percent, ms) : null;
} catch (e) {}
}
function tick() {
analyser.getFloatTimeDomainData(buf);
let s = 0; for (let i = 0; i < buf.length; i++) s += buf[i] * buf[i];
const rms = Math.sqrt(s / buf.length) + 1e-12;
const db = 20 * Math.log10(rms);
const now = performance.now();
if (db > thresholdDb) {
holdUntil = now + holdMs;
if (state !== 'open') { setGainPct(100, attackMs); state = 'open'; }
} else if (now > holdUntil) {
if (state !== 'closed') { setGainPct(100 - downGainPercent, releaseMs); state = 'closed'; }
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}
</pre>
</div>
</section>
</div>
</div>
<script>
// ==== Utility ==== //
const $ = (id) => document.getElementById(id);
const clamp = (x, a, b) => Math.min(b, Math.max(a, x));
const dbToLin = (db) => Math.pow(10, db / 20);
function compute() {
const thr = parseFloat($("thr").value || -50);
const att = parseInt($("att").value || 0, 10);
const rel = parseInt($("rel").value || 0, 10);
const hold = parseInt($("hold").value || 0, 10);
const range = clamp(parseInt($("range").value || 80, 10), 0, 100);
const downGainPercent = 100 - range; // % of full volume when closed
const cfg = {
thresholdDb: thr,
linearThreshold: dbToLin(thr),
attackMs: att, releaseMs: rel, holdMs: hold,
downGainPercent
};
$("linThresh").textContent = cfg.linearThreshold.toFixed(6);
$("downGain").textContent = (cfg.downGainPercent).toFixed(0) + '%';
$("gateDownCall").textContent = `changeGatingGain(${cfg.downGainPercent}, ${cfg.releaseMs})`;
$("gateUpCall").textContent = `changeGatingGain(100, ${cfg.attackMs})`;
$("settingsStr").textContent = [cfg.thresholdDb, cfg.attackMs, cfg.releaseMs, cfg.holdMs, cfg.downGainPercent].join(',');
$("kv_thr").textContent = cfg.thresholdDb;
$("kv_att").textContent = cfg.attackMs;
$("kv_rel").textContent = cfg.releaseMs;
$("kv_hold").textContent = cfg.holdMs;
$("kv_closed").textContent = cfg.downGainPercent;
return cfg;
}
// bind inputs
Array.from(document.querySelectorAll('input')).forEach(i => i.addEventListener('input', compute));
compute();
// Copy helpers
$("copyUrl").addEventListener('click', () => {
const s = `?noisegate=1&noisegatesettings=${$("settingsStr").textContent}`;
navigator.clipboard.writeText(s);
notify('Copied URL params');
});
$("copyCalls").addEventListener('click', () => {
const s = `${$("gateDownCall").textContent}\n${$("gateUpCall").textContent}\n`;
navigator.clipboard.writeText(s);
notify('Copied function calls');
});
function notify(msg) {
const el = document.createElement('div');
el.textContent = msg; el.style.position = 'fixed'; el.style.right = '16px'; el.style.bottom = '16px';
el.style.background = 'rgba(20,26,37,.95)'; el.style.border = '1px solid rgba(255,255,255,.08)'; el.style.padding = '10px 12px'; el.style.borderRadius = '10px';
document.body.appendChild(el); setTimeout(() => el.remove(), 1200);
}
$("resetBtn").addEventListener('click', () => {
$("thr").value = -50; $("att").value = 1; $("rel").value = 100; $("hold").value = 10; $("range").value = 80; compute();
});
// ==== Live test (optional) ==== //
let ctx, src, gateNode, analyser, raf, buf;
let state = 'open', holdUntil = 0;
const inFill = $("inFill"), gainFill = $("gainFill");
async function start() {
if (ctx) return stop();
try {
ctx = new (window.AudioContext || window.webkitAudioContext)();
const stream = await navigator.mediaDevices.getUserMedia({audio: { echoCancellation:false, noiseSuppression:false, autoGainControl:false }});
src = ctx.createMediaStreamSource(stream);
gateNode = ctx.createGain(); gateNode.gain.value = 1.0;
src.connect(gateNode);
analyser = ctx.createAnalyser(); analyser.fftSize = 2048; buf = new Float32Array(analyser.fftSize);
gateNode.connect(analyser); gateNode.connect(ctx.destination);
loop();
$("startBtn").textContent = 'Stop live test';
} catch (e) {
notify('Mic permission denied or unavailable');
}
}
function stop() {
cancelAnimationFrame(raf); raf = null;
if (ctx) { try { ctx.close(); } catch (e) {} }
ctx = src = gateNode = analyser = null; state = 'open';
inFill.style.width = '0%'; gainFill.style.width = '100%';
$("startBtn").textContent = 'Start live test';
}
function setGainPct(pct, ms) {
if (!gateNode) return;
const t = ctx.currentTime; const v = clamp(pct,0,100)/100;
try {
gateNode.gain.cancelScheduledValues(t);
gateNode.gain.setValueAtTime(gateNode.gain.value, t);
gateNode.gain.linearRampToValueAtTime(v, t + Math.max(0, ms)/1000);
} catch (e) {
gateNode.gain.value = v;
}
}
function loop() {
const cfg = compute();
analyser.getFloatTimeDomainData(buf);
let s = 0; for (let i = 0; i < buf.length; i++) s += buf[i]*buf[i];
const rms = Math.sqrt(s/buf.length) + 1e-12;
const db = 20*Math.log10(rms);
// simple display: map -100..0 dB to 0..100
const inPct = clamp(100 + db, 0, 100);
inFill.style.width = inPct + '%';
const now = performance.now();
if (db > cfg.thresholdDb) {
holdUntil = now + cfg.holdMs;
if (state !== 'open') { setGainPct(100, cfg.attackMs); state = 'open'; }
} else if (now > holdUntil) {
if (state !== 'closed') { setGainPct(cfg.downGainPercent, cfg.releaseMs); state = 'closed'; }
}
const gv = gateNode.gain.value * 100; gainFill.style.width = clamp(gv, 0, 100) + '%';
raf = requestAnimationFrame(loop);
}
$("startBtn").addEventListener('click', () => ctx ? stop() : start());
// Expose the core converter globally in case you want to copy it out via DevTools
window.classicalToMyGate = function(args){
const range = clamp(args.rangePct ?? 80, 0, 100);
const downGainPercent = 100 - range;
return {
thresholdDb: args.thresholdDb,
linearThreshold: dbToLin(args.thresholdDb),
attackMs: args.attackMs|0,
releaseMs: args.releaseMs|0,
holdMs: args.holdMs|0,
downGainPercent,
gateDownCall: `changeGatingGain(${downGainPercent}, ${args.releaseMs|0})`,
gateUpCall: `changeGatingGain(100, ${args.attackMs|0})`,
noisegatesettings: [args.thresholdDb, args.attackMs|0, args.releaseMs|0, args.holdMs|0, downGainPercent|0].join(',')
};
};
</script>
</body>
</html>

View File

@@ -328,12 +328,22 @@
document.getElementById("setup").style.display = "none";
scenesData = data;
updateSceneList();
var pathname = window.location.pathname.split("/");
pathname.pop();
pathname = pathname.join("/");
var clientLink = window.location.protocol + "//" + window.location.host + pathname + "/interface.html?room="+roomname+"&password="+pwurl;
document.getElementById("client").href = clientLink;
document.getElementById("client").innerHTML = "<b><font style='color:#70c4ff;'>client link:</font></b> "+clientLink;
var pathname = window.location.pathname.split("/");
pathname.pop();
pathname = pathname.join("/");
var clientLink = window.location.protocol + "//" + window.location.host + pathname + "/interface.html?room="+encodeURIComponent(roomname)+"&password="+encodeURIComponent(pwurl);
var clientAnchor = document.getElementById("client");
clientAnchor.href = clientLink;
clientAnchor.textContent = "";
clientAnchor.target = "_blank";
clientAnchor.rel = "noopener";
var labelBold = document.createElement("b");
var labelFont = document.createElement("span");
labelFont.style.color = "#70c4ff";
labelFont.textContent = "client link:";
labelBold.appendChild(labelFont);
clientAnchor.appendChild(labelBold);
clientAnchor.appendChild(document.createTextNode(" " + clientLink));
document.getElementById("info").innerHTML = "<br /><p style='color:#bdffbd;'>Connection to OBS websockets opened.</p>" + document.getElementById("info").innerHTML;
try {
obs._socket.onmessage2 = obs._socket.onmessage; // hijacking the obs-websocket.js framework
@@ -398,4 +408,4 @@
</script>
</body>
</html>
</html>

648
examples/p2pdrawing.md Normal file
View File

@@ -0,0 +1,648 @@
# VDO.Ninja IFRAME API: Transmitting Drawing Data Between Clients
This guide explains how to use the VDO.Ninja IFRAME API to send drawing data (or any custom data) between clients using peer-to-peer (P2P) data channels.
## Understanding the Data Channel
VDO.Ninja allows you to send arbitrary data between connected clients using its P2P data channels. This feature enables applications like:
- Custom drawing/annotation tools
- Chat systems
- Control signals
- Sensor data exchange
- Any other custom data payloads
The creators of VDO.Ninja use VDO.Ninja's data-channel functionality in many of their other applications and services, including Social Stream Ninja that processes hundreds of messages per minute per peer connection.
## Basic Setup
First, set up your VDO.Ninja iframe as described in the basic documentation:
```javascript
// Create the iframe element
var iframe = document.createElement("iframe");
// Set necessary permissions
iframe.allow = "camera;microphone;fullscreen;display-capture;autoplay;";
// Set the source URL (your VDO.Ninja room)
iframe.src = "https://vdo.ninja/?room=your-room-name&cleanoutput";
// Add the iframe to your page
document.getElementById("container").appendChild(iframe);
```
## Setting Up Event Listeners
To receive data from other clients, set up an event listener for messages from the iframe:
```javascript
// Set up event listener (cross-browser compatible)
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
// Connected peers storage
var connectedPeers = {};
// Add the event listener
eventer(messageEvent, function(e) {
// Make sure the message is from our VDO.Ninja iframe
if (e.source != iframe.contentWindow) return;
// Process connection events to track connected peers
if ("action" in e.data) {
if (e.data.action === "guest-connected" && e.data.streamID) {
// Store connected peer information
connectedPeers[e.data.streamID] = e.data.value?.label || "Guest";
console.log("Guest connected:", e.data.streamID, "Label:", connectedPeers[e.data.streamID]);
}
else if (e.data.action === "push-connection" && e.data.value === false && e.data.streamID) {
// Remove disconnected peers
console.log("Guest disconnected:", e.data.streamID);
delete connectedPeers[e.data.streamID];
}
}
// Handle received data
if ("dataReceived" in e.data) {
// Process any custom data received from peers
console.log("Data received:", e.data.dataReceived);
// If our custom data format is detected
if ("overlayNinja" in e.data.dataReceived) {
processReceivedData(e.data.dataReceived.overlayNinja, e.data.UUID);
}
}
}, false);
function processReceivedData(data, senderUUID) {
// Process the data based on your application's needs
console.log("Processing data from UUID:", senderUUID, "Data:", data);
// Example: Handle drawing data
if (data.drawingData) {
updateDrawingCanvas(data.drawingData);
}
}
```
## Sending Data to Peers
### Sending to All Connected Peers
Use this approach to broadcast data to all connected peers:
```javascript
function sendDataToAllPeers(data) {
// Create the data structure
var payload = {
drawingData: data // Your custom drawing data
};
// Send to all peers
iframe.contentWindow.postMessage({
sendData: { overlayNinja: payload },
type: "pcs" // Use peer connection for reliability
}, "*");
}
```
### Sending to a Specific Peer by UUID
Use this approach to send data to a specific peer identified by UUID:
```javascript
function sendDataToPeer(data, targetUUID) {
// Create the data structure
var payload = {
drawingData: data // Your custom drawing data
};
// Send to specific UUID
iframe.contentWindow.postMessage({
sendData: { overlayNinja: payload },
type: "pcs",
UUID: targetUUID
}, "*");
}
```
### Sending to Peers with Specific Labels
Use this approach to send data to all peers with a specific label:
```javascript
function sendDataByLabel(data, targetLabel) {
// Create the data structure
var payload = {
drawingData: data // Your custom drawing data
};
// Iterate through connected peers to find those with matching label
var keys = Object.keys(connectedPeers);
for (var i = 0; i < keys.length; i++) {
try {
var UUID = keys[i];
var label = connectedPeers[UUID];
if (label === targetLabel) {
// Send to this specific peer
iframe.contentWindow.postMessage({
sendData: { overlayNinja: payload },
type: "pcs",
UUID: UUID
}, "*");
}
} catch (e) {
console.error("Error sending to peer:", e);
}
}
}
```
### Sending to a Peer by StreamID
Use this approach when you know the streamID but not the UUID:
```javascript
function sendDataByStreamID(data, streamID) {
// Create the data structure
var payload = {
drawingData: data // Your custom drawing data
};
// Send to specific streamID
iframe.contentWindow.postMessage({
sendData: { overlayNinja: payload },
type: "pcs",
streamID: streamID
}, "*");
}
```
## Drawing-Specific Implementation
For transmitting drawing data specifically, you'll need to:
1. Create a drawing canvas on your page
2. Capture drawing events
3. Format the data appropriately
4. Send the data to peers
5. Process and render received drawing data
Here's a simplified example:
```javascript
// 1. Set up a drawing canvas
const canvas = document.createElement('canvas');
canvas.width = 640;
canvas.height = 480;
document.getElementById('drawing-container').appendChild(canvas);
const ctx = canvas.getContext('2d');
// Drawing state
let isDrawing = false;
let lastX = 0;
let lastY = 0;
let currentPath = [];
// 2. Capture drawing events
canvas.addEventListener('mousedown', (e) => {
isDrawing = true;
[lastX, lastY] = [e.offsetX, e.offsetY];
// Start a new path
currentPath = [];
// Normalize coordinates (0-1 range)
const point = {
x: lastX / canvas.width,
y: lastY / canvas.height
};
currentPath.push(point);
});
canvas.addEventListener('mousemove', (e) => {
if (!isDrawing) return;
// Draw locally
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
// Store normalized point
const point = {
x: e.offsetX / canvas.width,
y: e.offsetY / canvas.height
};
currentPath.push(point);
[lastX, lastY] = [e.offsetX, e.offsetY];
});
canvas.addEventListener('mouseup', () => {
if (isDrawing) {
isDrawing = false;
// 3 & 4. Format and send the path data
if (currentPath.length > 1) {
// Send the complete path
sendDrawingData(currentPath);
}
// Reset current path
currentPath = [];
}
});
// Send drawing data to all peers
function sendDrawingData(pathPoints) {
// Format the data as a path
const drawingData = {
t: 'path', // type: path
p: pathPoints
};
// Send to all peers
iframe.contentWindow.postMessage({
sendData: { overlayNinja: { drawingData: drawingData } },
type: "pcs"
}, "*");
}
// 5. Process received drawing data
function processReceivedData(data, senderUUID) {
if (data.drawingData && data.drawingData.t === 'path') {
const pathPoints = data.drawingData.p;
// Render the received path
if (pathPoints && pathPoints.length > 1) {
ctx.beginPath();
// Convert normalized coordinates back to canvas coordinates
const startX = pathPoints[0].x * canvas.width;
const startY = pathPoints[0].y * canvas.height;
ctx.moveTo(startX, startY);
for (let i = 1; i < pathPoints.length; i++) {
const x = pathPoints[i].x * canvas.width;
const y = pathPoints[i].y * canvas.height;
ctx.lineTo(x, y);
}
ctx.stroke();
}
}
}
```
## Advanced Drawing Commands
You can implement special drawing commands like clear, undo, etc.:
```javascript
// Clear the drawing canvas
function clearDrawing() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Send clear command to all peers
iframe.contentWindow.postMessage({
sendData: { overlayNinja: { drawingData: "clear" } },
type: "pcs"
}, "*");
}
// Undo last drawing action
function undoLastDrawing() {
// Local undo logic...
// Send undo command to all peers
iframe.contentWindow.postMessage({
sendData: { overlayNinja: { drawingData: "undo" } },
type: "pcs"
}, "*");
}
```
## Using VDO.Ninja's Built-in Drawing System
VDO.Ninja has a built-in drawing system you can leverage if you prefer not to implement your own:
```javascript
// Send drawing data using VDO.Ninja's built-in format
function sendVDONinjaDrawing(drawingData) {
iframe.contentWindow.postMessage({
draw: drawingData, // Can be an object with drawing data or commands like "clear", "undo"
type: "pcs",
UUID: targetUUID // Optional: specific target
}, "*");
}
// Clear VDO.Ninja's drawing
function clearVDONinjaDrawing() {
iframe.contentWindow.postMessage({
draw: "clear",
type: "pcs"
}, "*");
}
// Undo last drawing action in VDO.Ninja
function undoVDONinjaDrawing() {
iframe.contentWindow.postMessage({
draw: "undo",
type: "pcs"
}, "*");
}
```
## Complete Example: Drawing Application
Here's a more complete example of a drawing application using the data channel:
```javascript
// Create interface elements
const container = document.createElement('div');
container.id = 'app-container';
document.body.appendChild(container);
// Create VDO.Ninja iframe
const iframe = document.createElement('iframe');
iframe.allow = "camera;microphone;fullscreen;display-capture;autoplay;";
iframe.src = "https://vdo.ninja/?room=drawing-demo&cleanoutput";
iframe.style.width = "640px";
iframe.style.height = "360px";
container.appendChild(iframe);
// Create drawing canvas
const canvasContainer = document.createElement('div');
canvasContainer.style.position = 'relative';
container.appendChild(canvasContainer);
const canvas = document.createElement('canvas');
canvas.width = 640;
canvas.height = 360;
canvas.style.border = '1px solid black';
canvasContainer.appendChild(canvas);
const ctx = canvas.getContext('2d');
ctx.strokeStyle = 'red';
ctx.lineWidth = 3;
ctx.lineCap = 'round';
// Create controls
const controlsDiv = document.createElement('div');
controlsDiv.style.margin = '10px 0';
container.appendChild(controlsDiv);
const clearBtn = document.createElement('button');
clearBtn.textContent = 'Clear';
clearBtn.onclick = clearDrawing;
controlsDiv.appendChild(clearBtn);
const undoBtn = document.createElement('button');
undoBtn.textContent = 'Undo';
undoBtn.onclick = undoLastDrawing;
controlsDiv.appendChild(undoBtn);
// Track connected peers
const connectedPeers = {};
const drawingHistory = [];
let currentPath = [];
let isDrawing = false;
// Set up event handlers for the canvas
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', endDrawing);
canvas.addEventListener('mouseout', endDrawing);
function startDrawing(e) {
isDrawing = true;
const x = e.offsetX / canvas.width;
const y = e.offsetY / canvas.height;
currentPath = [{ x, y }];
ctx.beginPath();
ctx.moveTo(e.offsetX, e.offsetY);
}
function draw(e) {
if (!isDrawing) return;
const x = e.offsetX / canvas.width;
const y = e.offsetY / canvas.height;
currentPath.push({ x, y });
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
}
function endDrawing() {
if (!isDrawing) return;
isDrawing = false;
if (currentPath.length > 1) {
// Save path to history
drawingHistory.push(currentPath);
// Send path to peers
sendDrawingData(currentPath);
}
currentPath = [];
}
function clearDrawing() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawingHistory.length = 0;
// Send clear command
iframe.contentWindow.postMessage({
sendData: { overlayNinja: { drawingData: "clear" } },
type: "pcs"
}, "*");
}
function undoLastDrawing() {
if (drawingHistory.length === 0) return;
// Remove the last path
drawingHistory.pop();
// Redraw everything
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawingHistory.forEach(path => {
if (path.length > 1) {
ctx.beginPath();
ctx.moveTo(path[0].x * canvas.width, path[0].y * canvas.height);
for (let i = 1; i < path.length; i++) {
ctx.lineTo(path[i].x * canvas.width, path[i].y * canvas.height);
}
ctx.stroke();
}
});
// Send undo command
iframe.contentWindow.postMessage({
sendData: { overlayNinja: { drawingData: "undo" } },
type: "pcs"
}, "*");
}
function sendDrawingData(pathPoints) {
const drawingData = {
t: 'path',
p: pathPoints,
c: 'red', // Color
w: 3 // Width
};
iframe.contentWindow.postMessage({
sendData: { overlayNinja: { drawingData: drawingData } },
type: "pcs"
}, "*");
}
// Set up the event listener
const eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
const eventer = window[eventMethod];
const messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function(e) {
// Make sure the message is from our VDO.Ninja iframe
if (e.source != iframe.contentWindow) return;
// Process connection events
if ("action" in e.data) {
if (e.data.action === "guest-connected" && e.data.streamID) {
connectedPeers[e.data.streamID] = e.data.value?.label || "Guest";
console.log("Guest connected:", e.data.streamID, "Label:", connectedPeers[e.data.streamID]);
// Send current drawing state to new peer
if (drawingHistory.length > 0) {
iframe.contentWindow.postMessage({
sendData: { overlayNinja: { drawingHistory: drawingHistory } },
type: "pcs",
UUID: e.data.streamID
}, "*");
}
}
else if (e.data.action === "push-connection" && e.data.value === false && e.data.streamID) {
console.log("Guest disconnected:", e.data.streamID);
delete connectedPeers[e.data.streamID];
}
}
// Handle received data
if ("dataReceived" in e.data) {
if ("overlayNinja" in e.data.dataReceived) {
const data = e.data.dataReceived.overlayNinja;
// Process drawing data
if (data.drawingData) {
if (data.drawingData === "clear") {
// Clear command
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawingHistory.length = 0;
}
else if (data.drawingData === "undo") {
// Undo command
if (drawingHistory.length > 0) {
drawingHistory.pop();
// Redraw everything
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawingHistory.forEach(path => {
if (path.length > 1) {
ctx.beginPath();
ctx.moveTo(path[0].x * canvas.width, path[0].y * canvas.height);
for (let i = 1; i < path.length; i++) {
ctx.lineTo(path[i].x * canvas.width, path[i].y * canvas.height);
}
ctx.stroke();
}
});
}
}
else if (data.drawingData.t === 'path') {
// New path
const pathPoints = data.drawingData.p;
// Add to history
drawingHistory.push(pathPoints);
// Draw it
if (pathPoints && pathPoints.length > 1) {
ctx.beginPath();
ctx.moveTo(pathPoints[0].x * canvas.width, pathPoints[0].y * canvas.height);
for (let i = 1; i < pathPoints.length; i++) {
ctx.lineTo(pathPoints[i].x * canvas.width, pathPoints[i].y * canvas.height);
}
ctx.stroke();
}
}
}
// Handle initial state sync
if (data.drawingHistory) {
// Clear current state
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Apply all paths from history
data.drawingHistory.forEach(path => {
if (path.length > 1) {
ctx.beginPath();
ctx.moveTo(path[0].x * canvas.width, path[0].y * canvas.height);
for (let i = 1; i < path.length; i++) {
ctx.lineTo(path[i].x * canvas.width, path[i].y * canvas.height);
}
ctx.stroke();
}
});
// Update local history
drawingHistory.length = 0;
drawingHistory.push(...data.drawingHistory);
}
}
}
}, false);
```
## Best Practices
1. **Data Structure**: Use a clear, consistent data structure for your payloads
2. **Normalization**: Normalize canvas coordinates (0-1 range) to ensure consistent display across different screen sizes
3. **Throttling**: Consider throttling frequent events like mouse movements to reduce data transmission
4. **Error Handling**: Always include try/catch blocks when sending or processing data
5. **State Synchronization**: When new peers join, send them the current state
6. **UUID vs StreamID**: Use UUID for reliable targeting; StreamIDs change when connections restart
7. **Connection Status**: Monitor connection and disconnection events to maintain a list of active peers
## Common Types of Data to Send
- **Drawing Paths**: Arrays of points representing drawing strokes
- **Commands**: Clear, undo, change color, change brush size
- **Annotations**: Text or shapes to overlay on videos
- **Control Signals**: Camera directions, audio levels, recording commands
- **Chat Messages**: Text messages between users
- **Sensor Data**: Device orientation, location, acceleration
## Troubleshooting
- **Data Not Arriving**: Check that you're using the correct UUID or streamID
- **Timing Issues**: Ensure your iframe is fully loaded before sending messages
- **Cross-Origin Issues**: Make sure your security settings allow communication
- **Format Errors**: Verify your data structure matches what receivers expect
- **Performance Problems**: Large data payloads can cause lag; consider optimizing
By following this guide, you should be able to implement custom drawing tools or any other data-sharing features using VDO.Ninja's P2P data channels.

94
examples/rip.html Normal file
View File

@@ -0,0 +1,94 @@
<html>
<head><title>RIP Canvas Relay - VDO.Ninja</title>
<style>
body{
padding:0;
margin:0;
background-color: #0000;
}
iframe {
border:0;
margin:0;
padding:0;
display:block;
width:100%;
height:100%;
}
#container {
border:0;
margin:0;
padding:0;
display:block;
width:50%;
height:50%;
position:absolute;
top:0;
left:0;
}
button{
border:0;
margin:0;
padding:0;
display:block;
position:absolute;
bottom:0;
right:0;
z-index: 10;
}
</style>
</head>
<body id="main">
<div id="container"></div>
<button onclick="dosomething();">SOMETHING</button>
<script>
var iframe = document.createElement("iframe");
function dosomething(){
var video = iframe.contentWindow.document.body.querySelector("video");
var mediastream = new MediaStream();
video.muted=true;
video.style.display = "none";
video.captureStream().getTracks().forEach(trk=>{
mediastream.addTrack(trk);
});
video.onended = function(){
mediastream = null;
}
video.load();
}
function loadIframe(url=false){
if (url){
var iframesrc = url;
} else {
var iframesrc = document.getElementById("viewlink").value;
}
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
if (iframesrc==""){
iframesrc="./";
}
iframe.src = iframesrc;
document.body.appendChild(iframe);
//////////// LISTEN FOR EVENTS
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
});
}
loadIframe("https://youtu.be/XOJMKdwpTZE");
</script>
</body>
</html>

3171
examples/sandbox.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
<html>
<head>
<title>Video with sensor overlayed data</title>
<title>Sensor Data Overlay - VDO.Ninja</title>
<meta charset="UTF-8">
<style>
body{
@@ -364,4 +364,4 @@ loadIframe("../"+window.location.search);
</script>
</body>
</html>
</html>

View File

@@ -0,0 +1,312 @@
<!DOCTYPE html>
<html>
<head>
<title>Simple IFRAME Replacement Example</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.example {
border: 2px solid #ddd;
padding: 20px;
margin: 20px 0;
border-radius: 8px;
}
.old-way {
background: #fff3e0;
}
.new-way {
background: #e8f5e9;
}
pre {
background: #f5f5f5;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
}
button {
padding: 10px 20px;
font-size: 16px;
margin: 5px;
cursor: pointer;
}
.messages {
border: 1px solid #ddd;
padding: 10px;
height: 200px;
overflow-y: auto;
margin: 10px 0;
background: white;
}
h2 {
margin-top: 0;
}
</style>
</head>
<body>
<h1>Simple IFRAME Replacement with VDO.Ninja DataChannel SDK</h1>
<div class="example old-way">
<h2>❌ Old Way: Hidden IFRAME</h2>
<pre><code>// HTML
&lt;iframe id="vdo-iframe"
src="https://vdo.ninja/?room=myroom&amp;push=sender&amp;view=receiver&amp;datachannel=true"
style="display:none"&gt;&lt;/iframe&gt;
// JavaScript
const iframe = document.getElementById('vdo-iframe');
// Send message
iframe.contentWindow.postMessage({
action: 'send-data',
data: 'Hello from parent'
}, '*');
// Receive messages
window.addEventListener('message', (event) => {
if (event.origin !== 'https://vdo.ninja') return;
console.log('Received:', event.data);
});</code></pre>
<p><strong>Problems:</strong></p>
<ul>
<li>Hidden IFRAME still loads full VDO.Ninja UI</li>
<li>Cross-origin communication limitations</li>
<li>No direct control over connections</li>
<li>Hard to debug</li>
</ul>
</div>
<div class="example new-way">
<h2>✅ New Way: DataChannel SDK</h2>
<pre><code>// JavaScript only - no hidden IFRAME needed!
const node = new VDONinjaDataChannel();
// Connect and join room
await node.joinRoom({
room: 'myroom',
streamID: 'sender',
label: 'my-device'
});
// Send message
node.publish({ data: 'Hello from SDK' });
// Receive messages
node.addEventListener('data', (event) => {
console.log('Received:', event.detail.data);
});</code></pre>
<p><strong>Benefits:</strong></p>
<ul>
<li>No hidden IFRAME overhead</li>
<li>Direct DataChannel control</li>
<li>Label-based routing</li>
<li>Mesh networking support</li>
<li>Better debugging</li>
</ul>
</div>
<div class="example">
<h2>🚀 Live Demo</h2>
<p>Try it out! This demo shows two nodes communicating without any IFRAMEs.</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
<div>
<h3>Node A</h3>
<button onclick="connectNodeA()">Connect</button>
<button onclick="sendFromA()">Send Message</button>
<div class="messages" id="messagesA"></div>
</div>
<div>
<h3>Node B</h3>
<button onclick="connectNodeB()">Connect</button>
<button onclick="sendFromB()">Send Message</button>
<div class="messages" id="messagesB"></div>
</div>
</div>
</div>
<div class="example">
<h2>📝 Common Use Cases</h2>
<h3>1. IoT Sensor Network</h3>
<pre><code>// Sensor publishes data
const sensor = new VDONinjaDataChannel();
await sensor.joinRoom({ room: 'iot', label: 'temperature-sensor' });
setInterval(() => {
sensor.publish({
temp: Math.random() * 30 + 20,
unit: 'celsius',
timestamp: Date.now()
});
}, 5000);</code></pre>
<h3>2. Remote Control</h3>
<pre><code>// Controller subscribes to specific device
const controller = new VDONinjaDataChannel();
await controller.joinRoom({ room: 'iot', label: 'controller' });
// Subscribe to all temperature sensors
controller.subscribe('temperature-sensor');
controller.addEventListener('data', (e) => {
if (e.detail.data.temp > 25) {
// Send command back
controller.publish(
{ command: 'turn-on-fan' },
{ toLabel: e.detail.label }
);
}
});</code></pre>
<h3>3. Multi-User Chat</h3>
<pre><code>// Each user joins with unique ID but same label
const chat = new VDONinjaDataChannel();
await chat.joinRoom({
room: 'chatroom',
streamID: `user-${Date.now()}`,
label: 'chat-user'
});
// Subscribe to all chat users
chat.subscribe('chat-user');
// Send message to all
chat.publish({
message: 'Hello everyone!',
user: 'John'
});</code></pre>
</div>
<script src="../vdoninja-datachannel-sdk.js"></script>
<script>
let nodeA = null;
let nodeB = null;
let messageCount = { A: 0, B: 0 };
async function connectNodeA() {
if (nodeA) nodeA.disconnect();
nodeA = new VDONinjaDataChannel({
wss: 'wss://apibackup.vdo.ninja:443'
});
nodeA.addEventListener('data', (e) => {
addMessage('messagesA', `Received: ${JSON.stringify(e.detail.data)}`);
});
nodeA.addEventListener('peer-connected', (e) => {
addMessage('messagesA', '🔗 Peer connected!');
});
try {
await nodeA.joinRoom({
room: `demo-${window.location.hostname}`,
streamID: 'node-a',
label: 'demo-node'
});
addMessage('messagesA', '✅ Connected to room');
// Auto-discover and connect to peers
setTimeout(async () => {
const peers = Array.from(nodeA.peerStreamIDs.keys());
for (const peer of peers) {
if (peer !== nodeA.uuid) {
await nodeA.view(nodeA.peerStreamIDs.get(peer));
}
}
}, 1000);
} catch (error) {
addMessage('messagesA', `❌ Error: ${error.message}`);
}
}
async function connectNodeB() {
if (nodeB) nodeB.disconnect();
nodeB = new VDONinjaDataChannel({
wss: 'wss://apibackup.vdo.ninja:443'
});
nodeB.addEventListener('data', (e) => {
addMessage('messagesB', `Received: ${JSON.stringify(e.detail.data)}`);
});
nodeB.addEventListener('peer-connected', (e) => {
addMessage('messagesB', '🔗 Peer connected!');
});
try {
await nodeB.joinRoom({
room: `demo-${window.location.hostname}`,
streamID: 'node-b',
label: 'demo-node'
});
addMessage('messagesB', '✅ Connected to room');
// Subscribe to demo nodes
nodeB.subscribe('demo-node');
// Auto-connect to node A
setTimeout(() => {
nodeB.view('node-a');
}, 1000);
} catch (error) {
addMessage('messagesB', `❌ Error: ${error.message}`);
}
}
function sendFromA() {
if (!nodeA || !nodeA.connected) {
alert('Node A not connected!');
return;
}
messageCount.A++;
const data = {
message: `Hello from Node A (#${messageCount.A})`,
timestamp: new Date().toISOString()
};
nodeA.publish(data);
addMessage('messagesA', `Sent: ${JSON.stringify(data)}`);
}
function sendFromB() {
if (!nodeB || !nodeB.connected) {
alert('Node B not connected!');
return;
}
messageCount.B++;
const data = {
message: `Hello from Node B (#${messageCount.B})`,
timestamp: new Date().toISOString()
};
nodeB.publish(data);
addMessage('messagesB', `Sent: ${JSON.stringify(data)}`);
}
function addMessage(containerId, message) {
const container = document.getElementById(containerId);
const div = document.createElement('div');
div.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
if (nodeA) nodeA.disconnect();
if (nodeB) nodeB.disconnect();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,507 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VDO.Ninja SDK - TURN Only Mode Example</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.section {
margin: 20px 0;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.info-box {
background: #e3f2fd;
border-left: 4px solid #2196f3;
padding: 15px;
margin: 20px 0;
}
.warning-box {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 15px;
margin: 20px 0;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
font-size: 16px;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
input, select {
padding: 8px;
margin: 5px;
border: 1px solid #ddd;
border-radius: 4px;
width: 200px;
}
video {
width: 100%;
max-width: 640px;
height: auto;
background: #000;
margin: 10px 0;
}
.status {
padding: 10px;
margin: 10px 0;
background: #f8f9fa;
border-radius: 4px;
font-family: monospace;
font-size: 14px;
}
.candidate-list {
max-height: 200px;
overflow-y: auto;
background: #f8f9fa;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
margin: 10px 0;
font-family: monospace;
font-size: 12px;
}
.candidate {
margin: 2px 0;
padding: 2px;
}
.candidate.host { color: #28a745; }
.candidate.srflx { color: #17a2b8; }
.candidate.relay { color: #dc3545; }
code {
background: #f8f9fa;
padding: 2px 4px;
border-radius: 3px;
font-family: monospace;
}
</style>
</head>
<body>
<div class="container">
<h1>VDO.Ninja SDK - TURN Only Mode Example</h1>
<div class="info-box">
<h3>What is TURN-Only Mode?</h3>
<p>TURN-only mode (relay mode) forces all WebRTC traffic through a TURN server, bypassing direct peer-to-peer connections. This is useful for:</p>
<ul>
<li>Testing TURN server functionality</li>
<li>Ensuring privacy by hiding IP addresses</li>
<li>Corporate networks with strict firewall rules</li>
<li>Scenarios requiring traffic to go through specific servers</li>
</ul>
</div>
<div class="warning-box">
<h3>⚠️ Important Notes</h3>
<ul>
<li>TURN-only mode requires valid TURN server credentials</li>
<li>Connection quality may be lower due to relay overhead</li>
<li>Bandwidth costs increase as all traffic goes through TURN</li>
<li>The default TURN servers may have usage limits</li>
</ul>
</div>
<!-- Configuration Section -->
<div class="section">
<h2>Configuration</h2>
<label>ICE Transport Policy:</label>
<select id="icePolicy">
<option value="all">All (Default - Direct + TURN)</option>
<option value="relay">Relay Only (TURN Only)</option>
</select>
<br><br>
<label>Custom TURN Server (optional):</label><br>
<input type="text" id="turnUrl" placeholder="turn:turn.example.com:3478" style="width: 300px;"><br>
<input type="text" id="turnUser" placeholder="Username"><br>
<input type="password" id="turnPass" placeholder="Password"><br>
<button onclick="testConfiguration()">Test Configuration</button>
</div>
<!-- ICE Candidates Monitor -->
<div class="section">
<h2>ICE Candidates Monitor</h2>
<p>This shows what types of ICE candidates are being gathered:</p>
<button onclick="startCandidateTest()" id="candidateTestBtn">Start Candidate Test</button>
<button onclick="stopCandidateTest()" id="stopCandidateBtn" disabled>Stop Test</button>
<div id="candidateStatus" class="status">Ready to test...</div>
<div id="candidateList" class="candidate-list">
<em>No candidates collected yet</em>
</div>
<div id="candidateSummary" style="margin-top: 10px;">
<strong>Summary:</strong>
<span id="hostCount">Host: 0</span> |
<span id="srflxCount">Server Reflexive: 0</span> |
<span id="relayCount">Relay: 0</span>
</div>
</div>
<!-- Connection Test -->
<div class="section">
<h2>TURN Connection Test</h2>
<p>Test actual connection establishment using selected ICE policy:</p>
<input type="text" id="roomName" placeholder="Room name" value="turn-test-room">
<button onclick="startConnectionTest()" id="connectTestBtn">Start Connection Test</button>
<button onclick="stopConnectionTest()" id="stopConnectBtn" disabled>Stop Test</button>
<div id="connectionStatus" class="status">Ready to test...</div>
<div style="display: flex; gap: 20px; margin-top: 20px;">
<div style="flex: 1;">
<h3>Publisher</h3>
<video id="pubVideo" autoplay playsinline muted></video>
<div id="pubStats" class="status">-</div>
</div>
<div style="flex: 1;">
<h3>Viewer</h3>
<video id="viewVideo" autoplay playsinline></video>
<div id="viewStats" class="status">-</div>
</div>
</div>
</div>
<!-- Code Examples -->
<div class="section">
<h2>Code Examples</h2>
<h3>1. Basic TURN-Only Configuration</h3>
<pre><code>const sdk = new VDONinjaSDK({
iceTransportPolicy: 'relay', // Forces TURN-only mode
turnServers: [
{
urls: 'turn:your-turn-server.com:3478',
username: 'your-username',
credential: 'your-password'
}
]
});</code></pre>
<h3>2. Quick View with TURN-Only</h3>
<pre><code>const sdk = new VDONinjaSDK({
iceTransportPolicy: 'relay'
});
const { pc, cleanup } = await sdk.quickView({
streamID: 'some-stream',
room: 'secure-room'
});
// All connections will use TURN relay</code></pre>
<h3>3. Detecting Connection Type</h3>
<pre><code>pc.onicecandidate = (event) => {
if (event.candidate) {
const candidate = event.candidate.candidate;
if (candidate.includes('typ host')) {
console.log('Host candidate (direct)');
} else if (candidate.includes('typ srflx')) {
console.log('Server reflexive (STUN)');
} else if (candidate.includes('typ relay')) {
console.log('Relay candidate (TURN)');
}
}
};</code></pre>
</div>
</div>
<script src="../vdoninja-sdk.js"></script>
<script>
let testSDK = null;
let pubSDK = null;
let viewSDK = null;
let candidateStats = { host: 0, srflx: 0, relay: 0 };
// Test configuration
async function testConfiguration() {
const policy = document.getElementById('icePolicy').value;
const turnUrl = document.getElementById('turnUrl').value;
const turnUser = document.getElementById('turnUser').value;
const turnPass = document.getElementById('turnPass').value;
console.log('Testing configuration:', { policy, turnUrl });
// Build config
const config = {
debug: true,
iceTransportPolicy: policy
};
// Add custom TURN if provided
if (turnUrl && turnUser && turnPass) {
config.turnServers = [{
urls: turnUrl,
username: turnUser,
credential: turnPass
}];
}
try {
const sdk = new VDONinjaSDK(config);
await sdk.connect();
// Create a test connection to check config
const conn = sdk._createConnection('test', 'publisher');
const pcConfig = conn.pc.getConfiguration();
console.log('RTCPeerConnection config:', pcConfig);
alert(`Configuration valid!\n\nICE Policy: ${pcConfig.iceTransportPolicy || 'all'}\nICE Servers: ${pcConfig.iceServers.length}`);
conn.pc.close();
await sdk.disconnect();
} catch (error) {
alert(`Configuration error: ${error.message}`);
}
}
// Start candidate collection test
async function startCandidateTest() {
const policy = document.getElementById('icePolicy').value;
const status = document.getElementById('candidateStatus');
const list = document.getElementById('candidateList');
// Reset stats
candidateStats = { host: 0, srflx: 0, relay: 0 };
updateCandidateSummary();
status.textContent = `Starting candidate test with policy: ${policy}`;
list.innerHTML = '';
try {
testSDK = new VDONinjaSDK({
debug: true,
iceTransportPolicy: policy
});
await testSDK.connect();
// Create a dummy connection to gather candidates
const conn = testSDK._createConnection('test', 'publisher');
conn.pc.onicecandidate = (event) => {
if (event.candidate) {
const candidate = event.candidate.candidate;
let type = 'unknown';
let className = '';
if (candidate.includes('typ host')) {
type = 'HOST';
className = 'host';
candidateStats.host++;
} else if (candidate.includes('typ srflx')) {
type = 'SRFLX';
className = 'srflx';
candidateStats.srflx++;
} else if (candidate.includes('typ relay')) {
type = 'RELAY';
className = 'relay';
candidateStats.relay++;
}
const candidateDiv = document.createElement('div');
candidateDiv.className = `candidate ${className}`;
candidateDiv.textContent = `[${type}] ${candidate}`;
list.appendChild(candidateDiv);
updateCandidateSummary();
}
};
// Create offer to start gathering
await conn.pc.createOffer();
status.textContent = `Gathering candidates with policy: ${policy}`;
document.getElementById('candidateTestBtn').disabled = true;
document.getElementById('stopCandidateBtn').disabled = false;
} catch (error) {
status.textContent = `Error: ${error.message}`;
stopCandidateTest();
}
}
// Stop candidate test
async function stopCandidateTest() {
if (testSDK) {
await testSDK.disconnect();
testSDK = null;
}
document.getElementById('candidateStatus').textContent = 'Test stopped';
document.getElementById('candidateTestBtn').disabled = false;
document.getElementById('stopCandidateBtn').disabled = true;
}
// Update candidate summary
function updateCandidateSummary() {
document.getElementById('hostCount').textContent = `Host: ${candidateStats.host}`;
document.getElementById('srflxCount').textContent = `Server Reflexive: ${candidateStats.srflx}`;
document.getElementById('relayCount').textContent = `Relay: ${candidateStats.relay}`;
}
// Start connection test
async function startConnectionTest() {
const policy = document.getElementById('icePolicy').value;
const room = document.getElementById('roomName').value;
const status = document.getElementById('connectionStatus');
if (!room) {
alert('Please enter a room name');
return;
}
status.textContent = `Starting connection test with policy: ${policy}`;
try {
// Get user media
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
document.getElementById('pubVideo').srcObject = stream;
// Publisher SDK
pubSDK = new VDONinjaSDK({
debug: true,
iceTransportPolicy: policy
});
const { streamID, cleanup: pubCleanup } = await pubSDK.quickPublish({
stream,
room,
label: `TURN Test - ${policy}`
});
status.textContent = `Publisher ready: ${streamID}. Starting viewer...`;
// Give publisher time to be ready
await new Promise(resolve => setTimeout(resolve, 2000));
// Viewer SDK
viewSDK = new VDONinjaSDK({
debug: true,
iceTransportPolicy: policy
});
const { pc, cleanup: viewCleanup } = await viewSDK.quickView({
streamID,
room
});
// Monitor connection
pc.ontrack = (event) => {
document.getElementById('viewVideo').srcObject = event.streams[0];
};
pc.onconnectionstatechange = () => {
status.textContent = `Connection state: ${pc.connectionState}`;
updateConnectionStats(pc);
};
// Monitor ICE connection specifically
pc.oniceconnectionstatechange = () => {
console.log(`ICE state: ${pc.iceConnectionState}`);
};
document.getElementById('connectTestBtn').disabled = true;
document.getElementById('stopConnectBtn').disabled = false;
} catch (error) {
status.textContent = `Error: ${error.message}`;
stopConnectionTest();
}
}
// Stop connection test
async function stopConnectionTest() {
const status = document.getElementById('connectionStatus');
// Stop video tracks
const pubVideo = document.getElementById('pubVideo');
if (pubVideo.srcObject) {
pubVideo.srcObject.getTracks().forEach(track => track.stop());
pubVideo.srcObject = null;
}
document.getElementById('viewVideo').srcObject = null;
// Disconnect SDKs
if (pubSDK) {
await pubSDK.disconnect();
pubSDK = null;
}
if (viewSDK) {
await viewSDK.disconnect();
viewSDK = null;
}
status.textContent = 'Test stopped';
document.getElementById('pubStats').textContent = '-';
document.getElementById('viewStats').textContent = '-';
document.getElementById('connectTestBtn').disabled = false;
document.getElementById('stopConnectBtn').disabled = true;
}
// Update connection statistics
async function updateConnectionStats(pc) {
try {
const stats = await pc.getStats();
let candidateType = 'Unknown';
stats.forEach(report => {
if (report.type === 'candidate-pair' && report.state === 'succeeded') {
// Get the selected candidate pair
stats.forEach(candidate => {
if (candidate.type === 'local-candidate' && candidate.id === report.localCandidateId) {
candidateType = candidate.candidateType.toUpperCase();
}
});
}
});
document.getElementById('viewStats').textContent = `Connected via: ${candidateType}`;
} catch (error) {
console.error('Stats error:', error);
}
}
</script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
<html>
<head><title>Video with sensor overlayed data</title>
<head><title>Waiting Room Overlay - VDO.Ninja</title>
<style>
body{
padding:0;
@@ -118,4 +118,4 @@ loadIframe("../"+window.location.search);
</script>
</body>
</html>
</html>