mirror of
https://github.com/SrIzan10/vdo.ninja.git
synced 2026-05-01 11:05:24 +00:00
Add files via upload
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<html>
|
||||
<head>
|
||||
<head>
|
||||
<title>Legacy Layout Console - VDO.Ninja</title>
|
||||
<style>
|
||||
body {
|
||||
margin:0;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
489
examples/datachannel-pubsub.html
Normal file
489
examples/datachannel-pubsub.html
Normal 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><iframe src="https://vdo.ninja/?room=myroom&push=cam1&datachannel=true"
|
||||
style="display:none"></iframe>
|
||||
|
||||
// Communicate via postMessage
|
||||
iframe.contentWindow.postMessage({data: 'hello'}, '*');</code></pre>
|
||||
|
||||
<h4>After (with SDK):</h4>
|
||||
<pre><code>const node = new VDONinjaDataChannel();
|
||||
await node.joinRoom({room: 'myroom', streamID: 'cam1'});
|
||||
|
||||
// Direct data channel communication
|
||||
node.publish({data: 'hello'});
|
||||
|
||||
// Subscribe to specific labels
|
||||
node.subscribe('sensors');
|
||||
node.addEventListener('data', (e) => {
|
||||
console.log('Received:', e.detail.data);
|
||||
});</code></pre>
|
||||
</div>
|
||||
|
||||
<script src="../vdoninja-datachannel-sdk.js"></script>
|
||||
<script>
|
||||
let publisher = null;
|
||||
let subscriber = null;
|
||||
const meshNodes = new Map();
|
||||
|
||||
// Target selection handler
|
||||
document.getElementById('pubTarget').addEventListener('change', (e) => {
|
||||
const targetInput = document.getElementById('pubTargetValue');
|
||||
if (e.target.value === 'all') {
|
||||
targetInput.style.display = 'none';
|
||||
} else {
|
||||
targetInput.style.display = 'block';
|
||||
targetInput.placeholder = e.target.value === 'label' ? 'Target label' : 'Target stream ID';
|
||||
}
|
||||
});
|
||||
|
||||
async function connectPublisher() {
|
||||
if (publisher) {
|
||||
publisher.disconnect();
|
||||
}
|
||||
|
||||
publisher = new VDONinjaDataChannel({
|
||||
debug: true,
|
||||
meshMode: 'partial'
|
||||
});
|
||||
|
||||
// Setup event handlers
|
||||
publisher.addEventListener('connected', () => {
|
||||
updateStatus('pubStatus', 'Connected', true);
|
||||
addMessage('pubMessages', 'Connected to server', 'system');
|
||||
});
|
||||
|
||||
publisher.addEventListener('disconnected', () => {
|
||||
updateStatus('pubStatus', 'Disconnected', false);
|
||||
});
|
||||
|
||||
publisher.addEventListener('peer-connected', (e) => {
|
||||
addMessage('pubMessages', `Peer connected: ${e.detail.uuid}`, 'system');
|
||||
updateMeshVisualization();
|
||||
});
|
||||
|
||||
publisher.addEventListener('data', (e) => {
|
||||
addMessage('pubMessages',
|
||||
`Received from ${e.detail.label || e.detail.streamID}: ${JSON.stringify(e.detail.data)}`,
|
||||
'received'
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
await publisher.connect();
|
||||
await publisher.joinRoom({
|
||||
room: document.getElementById('pubRoom').value,
|
||||
streamID: `pub-${Date.now()}`,
|
||||
label: document.getElementById('pubLabel').value
|
||||
});
|
||||
|
||||
meshNodes.set(publisher.uuid, {
|
||||
sdk: publisher,
|
||||
type: 'publisher',
|
||||
label: publisher.label
|
||||
});
|
||||
|
||||
updateStatus('pubStatus', `Connected as ${publisher.label}`, true);
|
||||
} catch (error) {
|
||||
updateStatus('pubStatus', `Error: ${error.message}`, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function connectSubscriber() {
|
||||
if (subscriber) {
|
||||
subscriber.disconnect();
|
||||
}
|
||||
|
||||
subscriber = new VDONinjaDataChannel({
|
||||
debug: true,
|
||||
meshMode: 'partial'
|
||||
});
|
||||
|
||||
// Setup event handlers
|
||||
subscriber.addEventListener('connected', () => {
|
||||
updateStatus('subStatus', 'Connected', true);
|
||||
addMessage('subMessages', 'Connected to server', 'system');
|
||||
});
|
||||
|
||||
subscriber.addEventListener('disconnected', () => {
|
||||
updateStatus('subStatus', 'Disconnected', false);
|
||||
});
|
||||
|
||||
subscriber.addEventListener('peer-connected', (e) => {
|
||||
addMessage('subMessages', `Peer connected: ${e.detail.uuid}`, 'system');
|
||||
updateMeshVisualization();
|
||||
});
|
||||
|
||||
subscriber.addEventListener('data', (e) => {
|
||||
addMessage('subMessages',
|
||||
`From ${e.detail.label || e.detail.streamID}: ${JSON.stringify(e.detail.data)}`,
|
||||
'received'
|
||||
);
|
||||
});
|
||||
|
||||
subscriber.addEventListener('stream', (e) => {
|
||||
handleReceivedStream(e.detail);
|
||||
});
|
||||
|
||||
try {
|
||||
await subscriber.connect();
|
||||
await subscriber.joinRoom({
|
||||
room: document.getElementById('subRoom').value,
|
||||
streamID: `sub-${Date.now()}`,
|
||||
label: document.getElementById('subLabel').value
|
||||
});
|
||||
|
||||
meshNodes.set(subscriber.uuid, {
|
||||
sdk: subscriber,
|
||||
type: 'subscriber',
|
||||
label: subscriber.label
|
||||
});
|
||||
|
||||
updateStatus('subStatus', `Connected as ${subscriber.label}`, true);
|
||||
} catch (error) {
|
||||
updateStatus('subStatus', `Error: ${error.message}`, false);
|
||||
}
|
||||
}
|
||||
|
||||
function publishMessage() {
|
||||
if (!publisher || !publisher.connected) {
|
||||
alert('Publisher not connected');
|
||||
return;
|
||||
}
|
||||
|
||||
const message = document.getElementById('pubMessage').value;
|
||||
const target = document.getElementById('pubTarget').value;
|
||||
const targetValue = document.getElementById('pubTargetValue').value;
|
||||
|
||||
if (!message) return;
|
||||
|
||||
const data = {
|
||||
message,
|
||||
timestamp: new Date().toISOString(),
|
||||
from: publisher.label
|
||||
};
|
||||
|
||||
let options = {};
|
||||
if (target === 'label' && targetValue) {
|
||||
options.toLabel = targetValue;
|
||||
} else if (target === 'streamid' && targetValue) {
|
||||
options.toStreamID = targetValue;
|
||||
}
|
||||
|
||||
publisher.publish(data, options);
|
||||
|
||||
addMessage('pubMessages',
|
||||
`Sent to ${target === 'all' ? 'all' : `${target}: ${targetValue}`}: ${message}`,
|
||||
'sent'
|
||||
);
|
||||
|
||||
document.getElementById('pubMessage').value = '';
|
||||
}
|
||||
|
||||
async function subscribeToLabel() {
|
||||
if (!subscriber || !subscriber.connected) {
|
||||
alert('Subscriber not connected');
|
||||
return;
|
||||
}
|
||||
|
||||
const label = document.getElementById('subToLabel').value;
|
||||
if (!label) return;
|
||||
|
||||
subscriber.subscribe(label);
|
||||
|
||||
// Update UI
|
||||
const subDiv = document.getElementById('subscriptions');
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'label-badge';
|
||||
badge.textContent = label;
|
||||
subDiv.appendChild(badge);
|
||||
|
||||
addMessage('subMessages', `Subscribed to label: ${label}`, 'system');
|
||||
|
||||
// Auto-connect to peers with this label
|
||||
subscriber.peerLabels.forEach((peerLabel, uuid) => {
|
||||
if (peerLabel === label) {
|
||||
const streamID = subscriber.peerStreamIDs.get(uuid);
|
||||
if (streamID) {
|
||||
subscriber.view(streamID);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function streamFile() {
|
||||
if (!publisher || !publisher.connected) {
|
||||
alert('Publisher not connected');
|
||||
return;
|
||||
}
|
||||
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const file = fileInput.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const streamId = `file-${Date.now()}`;
|
||||
publisher.streamData(streamId, e.target.result, {
|
||||
fileName: file.name,
|
||||
fileType: file.type,
|
||||
fileSize: file.size
|
||||
});
|
||||
|
||||
addMessage('pubMessages', `Streaming file: ${file.name} (${file.size} bytes)`, 'sent');
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
|
||||
function handleReceivedStream(stream) {
|
||||
const streamsDiv = document.getElementById('streams');
|
||||
const streamDiv = document.createElement('div');
|
||||
streamDiv.className = 'message';
|
||||
|
||||
const blob = new Blob([stream.data], { type: stream.metadata.fileType || 'application/octet-stream' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
streamDiv.innerHTML = `
|
||||
<strong>Received Stream:</strong> ${stream.metadata.fileName || 'Unknown'}
|
||||
(${stream.metadata.fileSize || stream.data.byteLength} bytes)
|
||||
<a href="${url}" download="${stream.metadata.fileName || 'download'}">Download</a>
|
||||
`;
|
||||
|
||||
streamsDiv.appendChild(streamDiv);
|
||||
}
|
||||
|
||||
function updateStatus(elementId, text, connected) {
|
||||
const element = document.getElementById(elementId);
|
||||
element.textContent = text;
|
||||
element.className = 'status' + (connected ? ' connected' : '');
|
||||
}
|
||||
|
||||
function addMessage(containerId, text, type = 'info') {
|
||||
const container = document.getElementById(containerId);
|
||||
const message = document.createElement('div');
|
||||
message.className = 'message';
|
||||
|
||||
const time = new Date().toLocaleTimeString();
|
||||
const typeEmoji = {
|
||||
system: '🔧',
|
||||
sent: '📤',
|
||||
received: '📥',
|
||||
info: 'ℹ️'
|
||||
};
|
||||
|
||||
message.innerHTML = `${typeEmoji[type] || ''} [${time}] ${text}`;
|
||||
container.appendChild(message);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
function updateMeshVisualization() {
|
||||
const canvas = document.getElementById('meshCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Simple mesh visualization
|
||||
const nodes = Array.from(meshNodes.values());
|
||||
const angleStep = (2 * Math.PI) / nodes.length;
|
||||
const radius = 150;
|
||||
const centerX = canvas.width / 2;
|
||||
const centerY = canvas.height / 2;
|
||||
|
||||
// Draw nodes
|
||||
nodes.forEach((node, index) => {
|
||||
const x = centerX + radius * Math.cos(index * angleStep);
|
||||
const y = centerY + radius * Math.sin(index * angleStep);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 20, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = node.type === 'publisher' ? '#4CAF50' : '#2196F3';
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = '12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(node.label || 'Node', x, y + 4);
|
||||
|
||||
// Draw connections
|
||||
if (node.sdk && node.sdk.peers) {
|
||||
node.sdk.peers.forEach((pc, peerUuid) => {
|
||||
const peerNode = meshNodes.get(peerUuid);
|
||||
if (peerNode) {
|
||||
const peerIndex = nodes.indexOf(peerNode);
|
||||
const peerX = centerX + radius * Math.cos(peerIndex * angleStep);
|
||||
const peerY = centerY + radius * Math.sin(peerIndex * angleStep);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(peerX, peerY);
|
||||
ctx.strokeStyle = '#666';
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update status text
|
||||
const statusDiv = document.getElementById('meshStatus');
|
||||
statusDiv.innerHTML = `<strong>Nodes:</strong> ${nodes.length} |
|
||||
<strong>Publishers:</strong> ${nodes.filter(n => n.type === 'publisher').length} |
|
||||
<strong>Subscribers:</strong> ${nodes.filter(n => n.type === 'subscriber').length}`;
|
||||
}
|
||||
|
||||
// Auto-connect on load for demo
|
||||
window.addEventListener('load', () => {
|
||||
// Optional: auto-connect for easier testing
|
||||
// connectPublisher();
|
||||
// connectSubscriber();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
531
examples/dataiframes.md
Normal file
531
examples/dataiframes.md
Normal 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.
|
||||
@@ -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>
|
||||
|
||||
634
examples/dynamic-viewer.html
Normal file
634
examples/dynamic-viewer.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
434
examples/iframe_api_enhanced.md
Normal file
434
examples/iframe_api_enhanced.md
Normal 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.
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
357
examples/noisegate.html
Normal 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&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">Drop‑in 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>
|
||||
@@ -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
648
examples/p2pdrawing.md
Normal 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
94
examples/rip.html
Normal 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
3171
examples/sandbox.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
312
examples/simple-iframe-replacement.html
Normal file
312
examples/simple-iframe-replacement.html
Normal 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
|
||||
<iframe id="vdo-iframe"
|
||||
src="https://vdo.ninja/?room=myroom&push=sender&view=receiver&datachannel=true"
|
||||
style="display:none"></iframe>
|
||||
|
||||
// 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>
|
||||
507
examples/turn-only-example.html
Normal file
507
examples/turn-only-example.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user