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

3171 lines
105 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VDO.Ninja Developer API Sandbox</title>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-primary: #1a1a1a;
--bg-secondary: #252525;
--bg-tertiary: #2d2d2d;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--accent-primary: #6366f1;
--accent-secondary: #818cf8;
--accent-success: #10b981;
--accent-warning: #f59e0b;
--accent-danger: #ef4444;
--border-color: #404040;
--radius: 4px;
--spacing: 8px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
color: var(--text-primary);
background-color: var(--bg-primary);
line-height: 1.5;
padding: var(--spacing);
}
.container {
display: grid;
grid-template-columns: 3fr 1fr;
grid-template-rows: auto 1fr;
grid-template-areas:
"header header"
"main logger";
gap: var(--spacing);
height: calc(100vh - var(--spacing) * 2);
max-width: 100%;
}
header {
grid-area: header;
padding: calc(var(--spacing) * 2);
background-color: var(--bg-secondary);
border-radius: var(--radius);
border: 1px solid var(--border-color);
}
main {
grid-area: main;
overflow-y: auto;
padding: var(--spacing);
background-color: var(--bg-secondary);
border-radius: var(--radius);
border: 1px solid var(--border-color);
}
.logger-container {
grid-area: logger;
display: flex;
flex-direction: column;
background-color: var(--bg-secondary);
border-radius: var(--radius);
border: 1px solid var(--border-color);
}
h1, h2, h3, h4 {
font-weight: 600;
margin-bottom: calc(var(--spacing) * 1.5);
color: var(--text-primary);
}
h1 {
font-size: 1.8rem;
color: var(--accent-primary);
}
h2 {
font-size: 1.25rem;
margin-top: calc(var(--spacing) * 3);
border-bottom: 1px solid var(--border-color);
padding-bottom: var(--spacing);
}
h3 {
font-size: 1rem;
margin-top: calc(var(--spacing) * 2);
}
a {
color: var(--accent-secondary);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
p {
margin-bottom: var(--spacing);
}
small {
font-size: 0.85rem;
color: var(--text-secondary);
}
pre, code {
font-family: 'Fira Code', monospace;
}
pre {
background-color: var(--bg-tertiary);
padding: var(--spacing);
border-radius: var(--radius);
overflow-x: auto;
white-space: pre-wrap;
margin-bottom: var(--spacing);
}
.api-key {
font-family: 'Fira Code', monospace;
padding: calc(var(--spacing) * 0.75) var(--spacing);
background-color: var(--bg-tertiary);
border-radius: var(--radius);
display: inline-block;
margin: calc(var(--spacing) * 0.5) 0;
cursor: pointer;
user-select: all;
border: 1px solid var(--border-color);
}
/* Tabs styling */
.tabs {
display: flex;
margin-bottom: var(--spacing);
border-bottom: 1px solid var(--border-color);
flex-wrap: wrap;
}
.tab {
padding: calc(var(--spacing) * 0.75) calc(var(--spacing) * 1.5);
font-weight: 500;
cursor: pointer;
position: relative;
border-bottom: 2px solid transparent;
font-size: 0.9rem;
}
.tab.active {
border-bottom: 2px solid var(--accent-primary);
color: var(--accent-primary);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Button grid */
.button-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
gap: var(--spacing);
margin-bottom: calc(var(--spacing) * 2);
}
/* Button styling */
button {
background-color: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: calc(var(--spacing) * 0.75) var(--spacing);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
min-height: 3.5rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
button:hover {
background-color: #333;
border-color: var(--accent-secondary);
}
button:active {
transform: translateY(1px);
}
button[data-type="true"] {
border-left: 3px solid var(--accent-success);
}
button[data-type="false"] {
border-left: 3px solid var(--accent-danger);
}
button[data-type="toggle"] {
border-left: 3px solid var(--accent-warning);
}
.section {
padding: calc(var(--spacing) * 2);
margin-bottom: calc(var(--spacing) * 2);
border-radius: var(--radius);
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
}
.section-title {
display: flex;
align-items: center;
margin-bottom: calc(var(--spacing) * 1.5);
font-weight: 600;
color: var(--accent-primary);
padding-bottom: var(--spacing);
border-bottom: 1px solid var(--border-color);
}
/* PTZ Controls */
.ptz-controls {
display: flex;
flex-wrap: wrap;
gap: calc(var(--spacing) * 2);
margin-top: var(--spacing);
}
.control-group {
flex: 1;
min-width: 200px;
}
.slider-wrapper {
margin-bottom: var(--spacing);
}
.slider-label {
display: block;
margin-bottom: calc(var(--spacing) * 0.5);
font-weight: 500;
font-size: 0.9rem;
}
.slider-container {
display: flex;
align-items: center;
gap: calc(var(--spacing) * 0.5);
}
.slider {
flex: 1;
-webkit-appearance: none;
width: 100%;
height: 6px;
border-radius: 3px;
background: var(--border-color);
outline: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--accent-primary);
cursor: pointer;
}
.slider-value {
font-size: 0.875rem;
min-width: 40px;
text-align: center;
font-family: 'Fira Code', monospace;
}
/* Request/Response Logger */
.api-logger-header {
padding: var(--spacing);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.api-logger {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
background-color: #111;
color: #ddd;
font-family: 'Fira Code', monospace;
padding: var(--spacing);
font-size: 0.8rem;
}
.log-entry {
margin-bottom: calc(var(--spacing) * 0.75);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: calc(var(--spacing) * 0.5);
word-wrap: break-word;
}
.log-time {
color: #a8a8a8;
}
.log-method {
display: inline-block;
padding: 2px 4px;
border-radius: 3px;
margin: 0 4px;
font-weight: 600;
font-size: 0.75rem;
}
.log-get {
background: #388e3c;
color: white;
}
.log-post {
background: #1976d2;
color: white;
}
.log-ws {
background: #7b1fa2;
color: white;
}
.log-sse {
background: #c2185b;
color: white;
}
.log-midi {
background: #ff8f00;
color: white;
}
.log-request {
color: #64b5f6;
}
.log-response {
color: #81c784;
}
.log-error {
color: #ef5350;
}
/* Tooltip */
.tooltip {
position: relative;
cursor: pointer;
}
.tooltip .tooltip-text {
visibility: hidden;
width: 320px;
background-color: #333;
color: white;
text-align: left;
border-radius: var(--radius);
padding: calc(var(--spacing) * 0.75);
position: absolute;
z-index: 100;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.2s;
font-size: 0.75rem;
font-family: 'Fira Code', monospace;
white-space: pre-wrap;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
/* Connection status */
.connection-status {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.85rem;
margin-left: var(--spacing);
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: calc(var(--spacing) * 0.75);
}
.status-connected {
background-color: var(--accent-success);
}
.status-disconnected {
background-color: var(--accent-danger);
}
.status-connecting {
background-color: var(--accent-warning);
}
/* API Request Builders */
.request-builder {
background-color: var(--bg-tertiary);
padding: var(--spacing);
border-radius: var(--radius);
margin-bottom: calc(var(--spacing) * 2);
border: 1px solid var(--border-color);
}
.form-group {
margin-bottom: var(--spacing);
}
label {
display: block;
margin-bottom: 4px;
font-size: 0.9rem;
}
select, input[type="text"], textarea {
width: 100%;
padding: calc(var(--spacing) * 0.75);
background-color: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius);
color: var(--text-primary);
font-family: 'Fira Code', monospace;
font-size: 0.9rem;
}
textarea {
min-height: 100px;
resize: vertical;
}
.flex-row {
display: flex;
gap: var(--spacing);
}
.flex-row > * {
flex: 1;
}
/* Checkbox styling */
.checkbox-wrapper {
display: flex;
align-items: center;
margin-bottom: calc(var(--spacing) * 0.5);
}
.checkbox-wrapper input[type="checkbox"] {
margin-right: 6px;
}
/* MIDI config */
.midi-config {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: var(--spacing);
margin-top: var(--spacing);
}
.midi-mapping {
background-color: var(--bg-tertiary);
padding: var(--spacing);
border-radius: var(--radius);
border: 1px solid var(--border-color);
}
.midi-mapping-title {
font-weight: 600;
margin-bottom: calc(var(--spacing) * 0.5);
}
.midi-mapping-details {
font-size: 0.85rem;
color: var(--text-secondary);
}
/* Code examples */
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: calc(var(--spacing) * 0.5) var(--spacing);
background-color: #333;
border-top-left-radius: var(--radius);
border-top-right-radius: var(--radius);
font-size: 0.8rem;
border: 1px solid var(--border-color);
border-bottom: none;
}
.code-block {
background-color: #111;
padding: var(--spacing);
border-radius: 0 0 var(--radius) var(--radius);
overflow-x: auto;
margin-bottom: var(--spacing);
border: 1px solid var(--border-color);
font-size: 0.85rem;
}
.copy-btn {
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 2px 6px;
font-size: 0.7rem;
border-radius: 3px;
cursor: pointer;
}
.copy-btn:hover {
background-color: #444;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: calc(var(--spacing) * 2);
font-size: 0.9rem;
}
th, td {
padding: calc(var(--spacing) * 0.75);
text-align: left;
border-bottom: 1px solid var(--border-color);
}
th {
background-color: var(--bg-tertiary);
font-weight: 600;
}
tr:nth-child(even) {
background-color: rgba(255, 255, 255, 0.03);
}
tr:hover {
background-color: rgba(255, 255, 255, 0.05);
}
/* Responsive adjustments */
@media (max-width: 1024px) {
.container {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header"
"main"
"logger";
}
.logger-container {
height: 300px;
}
}
@media (max-width: 768px) {
.button-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
.ptz-controls {
flex-direction: column;
gap: var(--spacing);
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>VDO.Ninja Developer API Sandbox</h1>
<div style="display: flex; flex-wrap: wrap; gap: var(--spacing); align-items: center;">
<div>
<p>Your API Key: <span class="api-key" id="api-key" onclick="copyToClipboard(this)">Loading...</span></p>
<div id="connection-container">
<span class="connection-status">
<span class="status-indicator status-connecting" id="connection-indicator"></span>
<span id="connection-status">Connecting...</span>
</span>
</div>
</div>
<div style="flex: 1;">
<p>Your Ninja Link: <a id="ninja-link" href="#" target="_blank">Loading...</a></p>
<p><small>You can append VDO.Ninja parameters to this link, treating it like a normal VDO.Ninja link.</small></p>
</div>
<div>
<a href="https://github.com/bitfocus/companion-module-vdo-ninja" target="_blank" style="margin-right: 10px;">Companion Module</a>
<a href="https://docs.vdo.ninja/guides/iframe-api-documentation" target="_blank">IFrame API Docs</a>
</div>
</div>
</header>
<main>
<div class="tabs">
<div class="tab active" data-tab="websocket">WebSocket API</div>
<div class="tab" data-tab="http">HTTP API</div>
<div class="tab" data-tab="sse">Server-Sent Events</div>
<div class="tab" data-tab="post">HTTP POST</div>
<div class="tab" data-tab="ptz">Camera Controls</div>
<div class="tab" data-tab="midi">MIDI Integration</div>
<div class="tab" data-tab="guest">Guest Controls</div>
<div class="tab" data-tab="docs">API Reference</div>
</div>
<!-- WebSocket API Tab -->
<div class="tab-content active" id="websocket-tab">
<div class="section">
<div class="section-title">WebSocket API Request Builder</div>
<div class="request-builder">
<div class="form-group">
<label for="ws-action">Action:</label>
<select id="ws-action">
<option value="mic">mic</option>
<option value="camera">camera</option>=
<option value="speaker">speaker</option>
<option value="volume">volume</option>
<option value="record">record</option>
<option value="bitrate">bitrate</option>
<option value="togglehand">togglehand</option>
<option value="reload">reload</option>
<option value="hangup">hangup</option>
<option value="forceKeyframe">forceKeyframe</option>
<option value="getDetails">getDetails</option>
<option value="soloVideo">soloVideo (highlight)</option>
<option value="layout">layout</option>
<option value="sendChat">sendChat</option>
<option value="sendDirectorChat">sendDirectorChat</option>
<option value="zoom">zoom</option>
<option value="focus">focus</option>
<option value="pan">pan</option>
<option value="tilt">tilt</option>
<option value="exposure">exposure</option>
<option value="startRoomTimer">startRoomTimer</option>
<option value="pauseRoomTimer">pauseRoomTimer</option>
<option value="stopRoomTimer">stopRoomTimer</option>
</select>
</div>
<div class="form-group">
<label for="ws-value">Value:</label>
<input type="text" id="ws-value" placeholder="true, false, toggle, or numeric value">
</div>
<div class="form-group">
<label for="ws-value2">Value2 (optional):</label>
<input type="text" id="ws-value2" placeholder="Secondary value (e.g. 'abs' for absolute zoom)">
</div>
<div class="form-group">
<label for="ws-target">Target (optional - for director commands):</label>
<input type="text" id="ws-target" placeholder="Guest slot number or stream ID">
</div>
<div class="form-group">
<label for="ws-callback">Callback ID (optional):</label>
<input type="text" id="ws-callback" placeholder="Numeric ID for tracking responses">
</div>
<div class="form-group">
<label for="ws-custom-json">Custom JSON (advanced):</label>
<textarea id="ws-custom-json" placeholder='{"action":"mic","value":true}'></textarea>
</div>
<div class="flex-row">
<button onclick="sendWSRequest()" style="flex: 2;">Send WebSocket Request</button>
<button onclick="clearWSFields()" style="flex: 1;">Clear Fields</button>
</div>
</div>
<div class="section-title">Common Controls (WebSocket)</div>
<div class="button-grid" id="ws-controls"></div>
<div class="section-title">Layout Controls (WebSocket)</div>
<div class="button-grid" id="ws-layouts"></div>
<h3>WebSocket API Code Example</h3>
<div>
<div class="code-header">
<span>JavaScript</span>
<button class="copy-btn" onclick="copyCode('ws-code-example')">Copy</button>
</div>
<pre class="code-block" id="ws-code-example">// Connect to WebSocket API
const socket = new WebSocket("wss://api.vdo.ninja:443");
// Track connection state
socket.onopen = function() {
console.log("Connected to VDO.Ninja API");
// Join with your API ID
socket.send(JSON.stringify({ "join": "YOUR_API_ID" }));
};
// Handle messages
socket.onmessage = function(event) {
if (event.data) {
const data = JSON.parse(event.data);
console.log("Received:", data);
}
};
// Reconnection logic (crucial for production)
socket.onclose = function() {
console.log("Connection closed, attempting to reconnect...");
setTimeout(() => {
// Implement reconnection logic here
}, 1000);
};
// Example commands
function sendCommand(action, value = null, target = null) {
const msg = { action };
if (value !== null) msg.value = value;
if (target !== null) msg.target = target;
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(msg));
} else {
console.error("Socket not connected");
}
}
// Command examples:
// sendCommand("mic", "toggle");
// sendCommand("camera", false);
// sendCommand("soloVideo", "toggle");
// sendCommand("layout", 2);
// For directors - target a guest:
// sendCommand("mic", "toggle", "1"); // Target guest in slot 1</pre>
</div>
</div>
</div>
<!-- HTTP API Tab -->
<div class="tab-content" id="http-tab">
<div class="section">
<div class="section-title">HTTP GET API Request Builder</div>
<div class="request-builder">
<div class="form-group">
<label for="http-action">Action:</label>
<select id="http-action">
<option value="mic">mic</option>
<option value="video">video</option>
<option value="speaker">speaker</option>
<option value="volume">volume</option>
<option value="record">record</option>
<option value="bitrate">bitrate</option>
<option value="reload">reload</option>
<option value="hangup">hangup</option>
<option value="forceKeyframe">forceKeyframe</option>
<option value="group">group</option>
<option value="joinGroup">joinGroup</option>
<option value="leaveGroup">leaveGroup</option>
<option value="viewGroup">viewGroup</option>
<option value="joinViewGroup">joinViewGroup</option>
<option value="leaveViewGroup">leaveViewGroup</option>
<option value="getDetails">getDetails</option>
<option value="nextSlide">nextSlide</option>
<option value="prevSlide">prevSlide</option>
<option value="zoom">zoom</option>
<option value="focus">focus</option>
<option value="pan">pan</option>
<option value="tilt">tilt</option>
<option value="exposure">exposure</option>
</select>
</div>
<div class="form-group">
<label for="http-target">Target (optional - for director commands):</label>
<input type="text" id="http-target" placeholder="null or guest slot number or stream ID">
</div>
<div class="form-group">
<label for="http-value">Value:</label>
<input type="text" id="http-value" placeholder="true, false, toggle, or numeric value">
</div>
<div class="form-group">
<label>Generated URL:</label>
<pre id="http-url-preview" style="background-color: var(--bg-primary); padding: 8px; white-space: pre-wrap; word-break: break-all;">https://api.vdo.ninja/YOUR_API_ID/action/target/value</pre>
</div>
<div class="flex-row">
<button onclick="sendHTTPRequest()" style="flex: 2;">Send HTTP GET Request</button>
<button onclick="clearHTTPFields()" style="flex: 1;">Clear Fields</button>
</div>
</div>
<div class="section-title">Common Controls (HTTP)</div>
<div class="button-grid" id="http-controls"></div>
<div class="section-title">Group Controls (HTTP)</div>
<div class="button-grid" id="http-groups"></div>
<h3>HTTP API Code Example</h3>
<div>
<div class="code-header">
<span>JavaScript</span>
<button class="copy-btn" onclick="copyCode('http-code-example')">Copy</button>
</div>
<pre class="code-block" id="http-code-example">// HTTP GET API Example
function sendHTTPRequest(apiID, action, target = "null", value = "null") {
// Construct URL
const url = `https://api.vdo.ninja/${apiID}/${action}/${target}/${value}`;
// Send request
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.text();
})
.then(data => {
// Handle response (could be text or JSON)
try {
const jsonData = JSON.parse(data);
console.log("Response JSON:", jsonData);
} catch (e) {
console.log("Response text:", data);
}
})
.catch(error => {
console.error("Error:", error);
});
}
// Usage examples:
// sendHTTPRequest("YOUR_API_ID", "mic", "null", "toggle");
// sendHTTPRequest("YOUR_API_ID", "camera", "null", "false");
// sendHTTPRequest("YOUR_API_ID", "joinGroup", "null", "1");
// For directors targeting a specific guest
// sendHTTPRequest("YOUR_API_ID", "mic", "1", "toggle"); // Target guest in slot 1
// sendHTTPRequest("YOUR_API_ID", "addScene", "2", "1"); // Add guest 2 to scene 1</pre>
</div>
</div>
</div>
<!-- SSE API Tab -->
<div class="tab-content" id="sse-tab">
<div class="section">
<div class="section-title">Server-Sent Events (SSE) Listener</div>
<p>SSE provides a one-way communication channel to receive events from VDO.Ninja without sending requests.</p>
<div class="request-builder">
<div class="form-group">
<label for="sse-status">SSE Connection Status:</label>
<div id="sse-status" style="padding: 8px; background-color: var(--bg-primary); border-radius: var(--radius);">Disconnected</div>
</div>
<div class="flex-row">
<button id="sse-connect-btn" onclick="connectSSE()">Connect SSE</button>
<button id="sse-disconnect-btn" onclick="disconnectSSE()" disabled>Disconnect SSE</button>
</div>
</div>
<h3>SSE API Code Example</h3>
<div>
<div class="code-header">
<span>JavaScript</span>
<button class="copy-btn" onclick="copyCode('sse-code-example')">Copy</button>
</div>
<pre class="code-block" id="sse-code-example">// Server-Sent Events (SSE) Example
function connectToSSE(apiID) {
// Create EventSource connection
const eventSource = new EventSource(`https://api.vdo.ninja/sse/${apiID}`);
// Connection opened
eventSource.onopen = function() {
console.log("SSE connection established");
};
// Listen for messages
eventSource.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
console.log("SSE event:", data);
// Handle different types of events
if (data.type === "state_update") {
// Handle state updates
updateUI(data.state);
} else if (data.type === "guest_joined") {
// Handle guest joining
console.log(`Guest joined: ${data.guestID}`);
}
// Add more event handlers as needed
} catch (e) {
console.error("Error parsing SSE event:", e);
}
};
// Handle errors
eventSource.onerror = function(error) {
console.error("SSE connection error:", error);
// Close connection
eventSource.close();
// Implement reconnection logic
setTimeout(() => {
console.log("Attempting to reconnect SSE...");
connectToSSE(apiID);
}, 3000);
};
// Return EventSource instance (for later disconnection if needed)
return eventSource;
}
// Usage:
// const sseConnection = connectToSSE("YOUR_API_ID");
//
// // To disconnect later:
// // sseConnection.close();</pre>
</div>
</div>
</div>
<!-- HTTP POST Tab -->
<div class="tab-content" id="post-tab">
<div class="section">
<div class="section-title">HTTP POST API Request Builder</div>
<p>Although HTTP GET is the primary API method, POST can be useful for complex data or when query parameters need to be hidden.</p>
<div class="request-builder">
<div class="form-group">
<label for="post-action">Action:</label>
<select id="post-action">
<option value="mic">mic</option>
<option value="camera">camera</option>
<option value="video">video</option>
<option value="speaker">speaker</option>
<option value="volume">volume</option>
<option value="sendChat">sendChat</option>
<option value="sendDirectorChat">sendDirectorChat</option>
<option value="layout">layout</option>
<option value="getDetails">getDetails</option>
<option value="zoom">zoom</option>
<option value="exposure">exposure</option>
</select>
</div>
<div class="form-group">
<label for="post-value">Value:</label>
<input type="text" id="post-value" placeholder="true, false, toggle, or numeric value">
</div>
<div class="form-group">
<label for="post-target">Target (optional - for director commands):</label>
<input type="text" id="post-target" placeholder="Guest slot number or stream ID">
</div>
<div class="form-group">
<label for="post-json">Request Body JSON:</label>
<textarea id="post-json" placeholder='{"action":"mic","value":true}'></textarea>
</div>
<div class="flex-row">
<button onclick="sendPOSTRequest()" style="flex: 2;">Send HTTP POST Request</button>
<button onclick="clearPOSTFields()" style="flex: 1;">Clear Fields</button>
</div>
</div>
<h3>HTTP POST API Code Example</h3>
<div>
<div class="code-header">
<span>JavaScript</span>
<button class="copy-btn" onclick="copyCode('post-code-example')">Copy</button>
</div>
<pre class="code-block" id="post-code-example">// HTTP POST API Example
function sendPOSTRequest(apiID, data) {
// Construct URL
const url = `https://api.vdo.ninja/${apiID}`;
// Configure request
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
};
// Send request
fetch(url, options)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log("Response:", data);
})
.catch(error => {
console.error("Error:", error);
});
}
// Usage examples:
// sendPOSTRequest("YOUR_API_ID", {
// action: "mic",
// value: "toggle"
// });
//
// sendPOSTRequest("YOUR_API_ID", {
// action: "layout",
// value: [
// {"x":0,"y":0,"w":50,"h":100,"slot":0},
// {"x":50,"y":0,"w":50,"h":100,"slot":1}
// ]
// });
//
// For directors targeting a specific guest:
// sendPOSTRequest("YOUR_API_ID", {
// action: "sendDirectorChat",
// target: "1",
// value: "You're live in 5 seconds!"
// });</pre>
</div>
</div>
</div>
<!-- PTZ Controls Tab -->
<div class="tab-content" id="ptz-tab">
<div class="section">
<div class="section-title">Camera Controls (PTZ)</div>
<p>Remote control compatible camera hardware using pan, tilt, zoom, focus, and exposure commands.</p>
<div class="ptz-controls">
<div class="control-group">
<h3>Zoom</h3>
<div class="button-grid">
<button onclick="sendPTZCommand('zoom', 0.1)">Zoom In (0.1)</button>
<button onclick="sendPTZCommand('zoom', -0.1)">Zoom Out (-0.1)</button>
</div>
<div class="slider-wrapper">
<span class="slider-label">Absolute Zoom:</span>
<div class="slider-container">
<input type="range" min="0" max="100" value="50" class="slider" id="zoom-slider">
<span class="slider-value" id="zoom-value">0.5</span>
</div>
</div>
</div>
<div class="control-group">
<h3>Focus</h3>
<div class="button-grid">
<button onclick="sendPTZCommand('focus', 0.1)">Focus Far (0.1)</button>
<button onclick="sendPTZCommand('focus', -0.1)">Focus Near (-0.1)</button>
</div>
</div>
<div class="control-group">
<h3>Pan & Tilt</h3>
<div class="button-grid">
<button onclick="sendPTZCommand('pan', -0.1)">Pan Left (-0.1)</button>
<button onclick="sendPTZCommand('pan', 0.1)">Pan Right (0.1)</button>
<button onclick="sendPTZCommand('tilt', 0.1)">Tilt Up (0.1)</button>
<button onclick="sendPTZCommand('tilt', -0.1)">Tilt Down (-0.1)</button>
</div>
</div>
<div class="control-group">
<h3>Exposure</h3>
<div class="slider-wrapper">
<span class="slider-label">Exposure Level:</span>
<div class="slider-container">
<input type="range" min="0" max="100" value="50" class="slider" id="exposure-slider">
<span class="slider-value" id="exposure-value">0.5</span>
</div>
</div>
</div>
</div>
<h3>PTZ Controls Code Example</h3>
<div>
<div class="code-header">
<span>JavaScript</span>
<button class="copy-btn" onclick="copyCode('ptz-code-example')">Copy</button>
</div>
<pre class="code-block" id="ptz-code-example">// Camera Control (PTZ) Example
function sendCameraCommand(apiID, action, value, value2 = null, target = null) {
// Using WebSocket (preferred for responsive camera control)
if (socket && socket.readyState === WebSocket.OPEN) {
const command = { action, value };
if (value2 !== null) command.value2 = value2;
if (target !== null) command.target = target;
socket.send(JSON.stringify(command));
}
// Using HTTP as fallback
else {
let url = `https://api.vdo.ninja/${apiID}/${action}`;
if (target !== null) {
url += `/${target}`;
} else {
url += `/null`;
}
url += `/${value}`;
// value2 can't be directly used in the standard HTTP API, so it's ignored here
fetch(url)
.then(response => response.text())
.then(data => console.log("Response:", data))
.catch(error => console.error("Error:", error));
}
}
// Usage examples:
// Relative adjustments
// sendCameraCommand("YOUR_API_ID", "zoom", 0.1); // Zoom in
// sendCameraCommand("YOUR_API_ID", "zoom", -0.1); // Zoom out
// sendCameraCommand("YOUR_API_ID", "focus", 0.1); // Focus farther
// sendCameraCommand("YOUR_API_ID", "pan", -0.1); // Pan left
// sendCameraCommand("YOUR_API_ID", "tilt", 0.1); // Tilt up
// Absolute adjustment (for zoom)
// sendCameraCommand("YOUR_API_ID", "zoom", 0.75, "abs"); // Set zoom to 75%
// Targeting a specific guest's camera (for directors)
// sendCameraCommand("YOUR_API_ID", "zoom", 0.1, null, "2"); // Zoom guest 2's camera in</pre>
</div>
</div>
</div>
<!-- MIDI Integration Tab -->
<div class="tab-content" id="midi-tab">
<div class="section">
<div class="section-title">MIDI Integration</div>
<p>Control VDO.Ninja using MIDI controllers with various mapping options.</p>
<div class="request-builder">
<div class="form-group">
<label>MIDI Device Status:</label>
<div id="midi-status" style="padding: 8px; background-color: var(--bg-primary); border-radius: var(--radius);">MIDI access not initialized</div>
</div>
<div class="form-group">
<label for="midi-device">Select MIDI Device:</label>
<select id="midi-device" disabled>
<option value="">No devices available</option>
</select>
</div>
<div class="form-group">
<label>MIDI Monitor (last received message):</label>
<pre id="midi-monitor" style="background-color: var(--bg-primary); padding: 8px;">No MIDI message received</pre>
</div>
<div class="flex-row">
<button id="midi-connect-btn" onclick="initMIDI()">Initialize MIDI</button>
<button id="midi-test-btn" onclick="testMIDI()" disabled>Test MIDI Connection</button>
</div>
</div>
<h3>Default MIDI Mappings</h3>
<p>VDO.Ninja supports several MIDI mapping schemes, which can be configured using the <code>midiHotkeys</code> parameter.</p>
<div class="midi-config">
<div class="midi-mapping">
<div class="midi-mapping-title">Standard Mapping (midiHotkeys=1)</div>
<div class="midi-mapping-details">
<ul>
<li>G3: Toggle chat</li>
<li>A3: Toggle mic</li>
<li>B3: Toggle video</li>
<li>C4: Toggle screenshare</li>
<li>D4: Hangup</li>
<li>E4: Raise hand</li>
<li>F4: Toggle recording</li>
<li>G4: Director audio</li>
<li>A4: Director hangup</li>
<li>B4: Toggle speaker</li>
</ul>
</div>
</div>
<div class="midi-mapping">
<div class="midi-mapping-title">Alternate Mapping (midiHotkeys=2)</div>
<div class="midi-mapping-details">
<ul>
<li>G1: Toggle chat</li>
<li>A1: Toggle mic</li>
<li>B1: Toggle video</li>
<li>C2: Toggle screenshare</li>
<li>D2: Hangup</li>
<li>E2: Raise hand</li>
<li>F2: Toggle recording</li>
<li>G2: Director audio</li>
<li>A2: Director hangup</li>
<li>B2: Toggle speaker</li>
</ul>
</div>
</div>
<div class="midi-mapping">
<div class="midi-mapping-title">CC-Based Controls (midiHotkeys=3)</div>
<div class="midi-mapping-details">
<p>Uses note C1 with different velocity values:</p>
<ul>
<li>C1 + velocity 0: Toggle chat</li>
<li>C1 + velocity 1: Toggle mic</li>
<li>C1 + velocity 2: Toggle video</li>
<li>C1 + velocity 3: Toggle screenshare</li>
<li>C1 + velocity 4: Hangup</li>
<li>C1 + velocity 5: Raise hand</li>
<li>C1 + velocity 6: Toggle recording</li>
<li>C1 + velocity 7: Director audio</li>
<li>C1 + velocity 8: Director hangup</li>
<li>C1 + velocity 9: Toggle speaker</li>
</ul>
</div>
</div>
<div class="midi-mapping">
<div class="midi-mapping-title">PTZ MIDI Controls</div>
<div class="midi-mapping-details">
<p>Use MIDI to control camera:</p>
<ul>
<li>C5 + velocity (0-127): Zoom (absolute)</li>
<li>D5 + velocity (0-127): Focus</li>
<li>E5 + velocity (0-127): Pan</li>
<li>F5 + velocity (0-127): Tilt</li>
<li>G5 + velocity (0-127): Exposure</li>
</ul>
</div>
</div>
</div>
<h3>MIDI API Code Example</h3>
<div>
<div class="code-header">
<span>JavaScript</span>
<button class="copy-btn" onclick="copyCode('midi-code-example')">Copy</button>
</div>
<pre class="code-block" id="midi-code-example">// MIDI Integration Example
function initializeMIDI() {
// Check if Web MIDI API is supported
if (!navigator.requestMIDIAccess) {
console.error("Web MIDI API not supported in this browser");
return Promise.reject("MIDI not supported");
}
// Request MIDI access
return navigator.requestMIDIAccess({ sysex: false })
.then(midiAccess => {
console.log("MIDI Access granted");
// Get MIDI inputs
const inputs = midiAccess.inputs.values();
const devices = [];
// Setup devices
for (let input = inputs.next(); input && !input.done; input = inputs.next()) {
devices.push(input.value);
setupMIDIDevice(input.value);
}
// Listen for device connection/disconnection
midiAccess.onstatechange = function(e) {
console.log("MIDI state change:", e.port.name, e.port.state);
};
return { midiAccess, devices };
})
.catch(error => {
console.error("Failed to get MIDI access:", error);
throw error;
});
}
function setupMIDIDevice(inputDevice) {
// Set up MIDI message handler
inputDevice.onmidimessage = function(message) {
// Extract MIDI data
const command = message.data[0];
const note = message.data[1];
const velocity = message.data[2];
console.log(`MIDI message - Command: ${command}, Note: ${note}, Velocity: ${velocity}`);
// Handle Note On messages (144 = Note On, channel 1)
if (command === 144 && velocity > 0) {
handleNoteOn(note, velocity);
}
// Handle Note Off messages (128 = Note Off, channel 1)
else if (command === 128 || (command === 144 && velocity === 0)) {
handleNoteOff(note);
}
// Handle Control Change messages (176 = CC, channel 1)
else if (command === 176) {
handleControlChange(note, velocity);
}
};
}
function handleNoteOn(note, velocity) {
// Convert MIDI note number to note name (e.g. 60 = C4)
const noteName = getNoteNameFromNumber(note);
// Example mapping for midiHotkeys=1
switch (noteName) {
case "G3":
sendCommand("toggleChat");
break;
case "A3":
sendCommand("mic", "toggle");
break;
case "B3":
sendCommand("camera", "toggle");
break;
case "C4":
sendCommand("togglescreenshare");
break;
case "D4":
sendCommand("hangup");
break;
// Add more mappings as needed
}
}
function handleNoteOff(note) {
// Handle note off events if needed
}
function handleControlChange(cc, value) {
// Map control change messages to actions
switch (cc) {
case 20: // Example CC for zoom
const zoomValue = value / 127; // Normalize to 0-1 range
sendCommand("zoom", zoomValue, "abs");
break;
case 21: // Example CC for focus
const focusValue = (value - 64) / 64; // Normalize to -1 to 1 range
sendCommand("focus", focusValue);
break;
// Add more CC mappings as needed
}
}
// Helper function to convert MIDI note number to note name
function getNoteNameFromNumber(noteNumber) {
const notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
const octave = Math.floor(noteNumber / 12) - 1;
const noteName = notes[noteNumber % 12];
return noteName + octave;
}
// Usage:
// initializeMIDI()
// .then(midi => console.log("MIDI ready with devices:", midi.devices))
// .catch(error => console.error("MIDI setup failed:", error));</pre>
</div>
</div>
</div>
<!-- Guest Controls Tab -->
<div class="tab-content" id="guest-tab">
<div class="section">
<div class="section-title">Guest Controls (Director Mode)</div>
<p>These controls allow directors to manage remote guests via the API.</p>
<div class="request-builder">
<div class="form-group">
<label for="guest-slot">Target Guest:</label>
<select id="guest-slot" class="guest-slot-select">
<option value="1">Guest 1</option>
<option value="2">Guest 2</option>
<option value="3">Guest 3</option>
<option value="4">Guest 4</option>
</select>
</div>
<div class="form-group">
<label for="guest-action">Action:</label>
<select id="guest-action">
<option value="mic">mic (toggle)</option>
<option value="camera">camera (toggle)</option>
<option value="video">video (toggle)</option>
<option value="hangup">hangup</option>
<option value="soloChat">soloChat</option>
<option value="soloChatBidirectional">soloChatBidirectional</option>
<option value="speaker">speaker (toggle)</option>
<option value="display">display (toggle)</option>
<option value="forceKeyframe">forceKeyframe (rainbow fix)</option>
<option value="soloVideo">soloVideo (highlight)</option>
<option value="addScene">addScene</option>
<option value="muteScene">muteScene</option>
<option value="group">group</option>
<option value="forward">forward (transfer)</option>
<option value="sendChat">sendChat</option>
<option value="sendDirectorChat">sendDirectorChat</option>
<option value="volume">volume</option>
<option value="startRoomTimer">startRoomTimer</option>
<option value="pauseRoomTimer">pauseRoomTimer</option>
<option value="stopRoomTimer">stopRoomTimer</option>
<option value="zoom">zoom</option>
<option value="focus">focus</option>
<option value="pan">pan</option>
<option value="tilt">tilt</option>
<option value="exposure">exposure</option>
<option value="mixorder">mixorder</option>
</select>
</div>
<div class="form-group">
<label for="guest-value">Value:</label>
<input type="text" id="guest-value" placeholder="true, false, toggle, scene number, or text">
</div>
<div class="form-group">
<label for="guest-method">API Method:</label>
<select id="guest-method">
<option value="websocket">WebSocket</option>
<option value="http">HTTP GET</option>
</select>
</div>
<div class="flex-row">
<button onclick="sendGuestCommand()" style="flex: 2;">Send Guest Command</button>
<button onclick="clearGuestFields()" style="flex: 1;">Clear Fields</button>
</div>
</div>
<div class="button-grid" id="guest-controls"></div>
<div class="slider-wrapper" style="margin-top: 1rem;">
<span class="slider-label">Guest Mic Volume:</span>
<div class="slider-container">
<input type="range" min="0" max="200" value="100" class="slider" id="guest-volume-slider">
<span class="slider-value" id="guest-volume-value">100</span>
</div>
</div>
<h3>Guest Control Code Example</h3>
<div>
<div class="code-header">
<span>JavaScript</span>
<button class="copy-btn" onclick="copyCode('guest-code-example')">Copy</button>
</div>
<pre class="code-block" id="guest-code-example">// Guest Control Example (Director Mode)
function controlGuest(apiID, target, action, value = null, method = "websocket") {
if (method === "websocket") {
// WebSocket method
if (socket && socket.readyState === WebSocket.OPEN) {
const msg = {
target: target,
action: action
};
if (value !== null) {
msg.value = value;
}
socket.send(JSON.stringify(msg));
console.log("Sent guest command via WebSocket:", msg);
} else {
console.error("WebSocket not connected");
}
} else if (method === "http") {
// HTTP method
const targetStr = target || "null";
const valueStr = value !== null ? value : "null";
const url = `https://api.vdo.ninja/${apiID}/${action}/${targetStr}/${valueStr}`;
fetch(url)
.then(response => response.text())
.then(data => console.log("Guest command response:", data))
.catch(error => console.error("Error sending guest command:", error));
}
}
// Usage examples:
// controlGuest("YOUR_API_ID", "1", "mic", "toggle"); // Toggle mic for guest 1
// controlGuest("YOUR_API_ID", "2", "camera", false); // Turn off camera for guest 2
// controlGuest("YOUR_API_ID", "3", "addScene", 1); // Add guest 3 to scene 1
// controlGuest("YOUR_API_ID", "1", "sendDirectorChat", "You're live in 10 seconds");
// controlGuest("YOUR_API_ID", "2", "volume", 85); // Set guest 2 volume to 85%
// controlGuest("YOUR_API_ID", "4", "forward", "room123"); // Transfer guest 4 to room123</pre>
</div>
</div>
</div>
<!-- API Reference Tab -->
<div class="tab-content" id="docs-tab">
<div class="section">
<div class="section-title">VDO.Ninja API Reference</div>
<p>Complete reference for all supported API commands, parameters, and response formats.</p>
<h3>API Overview</h3>
<p>The VDO.Ninja API supports multiple communication methods:</p>
<ul>
<li><strong>WebSocket API</strong>: Real-time bidirectional communication (preferred for most uses)</li>
<li><strong>HTTP GET API</strong>: Simple REST-style requests, compatible with hotkey systems</li>
<li><strong>HTTP POST API</strong>: For complex data structures like layout objects</li>
<li><strong>Server-Sent Events (SSE)</strong>: One-way communication channel for receiving updates</li>
<li><strong>MIDI Integration</strong>: Control via MIDI devices using various mapping schemes</li>
</ul>
<p>For integrations with BitFocus Companion, check out the <a href="https://github.com/bitfocus/companion-module-vdo-ninja" target="_blank">official Companion module</a>.</p>
<p>For custom embeds, refer to the <a href="https://docs.vdo.ninja/guides/iframe-api-documentation" target="_blank">IFrame API Documentation</a>.</p>
<h3>Command Reference</h3>
<p>The following tables list all available commands, their parameters, and what they do.</p>
<h4>Self Controls</h4>
<table>
<thead>
<tr>
<th>Action</th>
<th>Target</th>
<th>Value</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr>
<td>speaker</td>
<td>null</td>
<td>true/false/toggle</td>
<td>Control local speaker (audio playback)</td>
</tr>
<tr>
<td>mic</td>
<td>null</td>
<td>true/false/toggle</td>
<td>Control local microphone</td>
</tr>
<tr>
<td>camera</td>
<td>null</td>
<td>true/false/toggle</td>
<td>Control local camera (same as video)</td>
</tr>
<tr>
<td>video</td>
<td>null</td>
<td>true/false/toggle</td>
<td>Control local camera (alternate command)</td>
</tr>
<tr>
<td>volume</td>
<td>null</td>
<td>true/false/0-100</td>
<td>Control playback volume of all audio tracks (true=100%, false=0%)</td>
</tr>
<tr>
<td>record</td>
<td>null</td>
<td>true/false</td>
<td>Start/stop recording the local video stream</td>
</tr>
<tr>
<td>bitrate</td>
<td>null</td>
<td>true/false/integer</td>
<td>Control video bitrate (true=reset, false=pause, integer=kbps)</td>
</tr>
<tr>
<td>panning</td>
<td>null</td>
<td>true/false/0-180</td>
<td>Control stereo panning (90=center)</td>
</tr>
<tr>
<td>reload</td>
<td>null</td>
<td>null</td>
<td>Reload the current page</td>
</tr>
<tr>
<td>hangup</td>
<td>null</td>
<td>null</td>
<td>Disconnect current session</td>
</tr>
<tr>
<td>togglehand</td>
<td>null</td>
<td>null</td>
<td>Toggle raised hand status</td>
</tr>
<tr>
<td>togglescreenshare</td>
<td>null</td>
<td>null</td>
<td>Toggle screen sharing</td>
</tr>
<tr>
<td>forceKeyframe</td>
<td>null</td>
<td>null</td>
<td>Force keyframe ("rainbow puke fix")</td>
</tr>
<tr>
<td>sendChat</td>
<td>null</td>
<td>string</td>
<td>Send chat message to all connected users</td>
</tr>
<tr>
<td>getDetails</td>
<td>null</td>
<td>null</td>
<td>Get detailed state information</td>
</tr>
<tr>
<td>soloVideo</td>
<td>null</td>
<td>true/false/toggle</td>
<td>Highlight video for all guests (director only)</td>
</tr>
</tbody>
</table>
<h4>Layout Controls</h4>
<table>
<thead>
<tr>
<th>Action</th>
<th>Value</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr>
<td>layout</td>
<td>integer/object/array</td>
<td>Switch to layout by index (0=auto-mix, 1-8=custom layouts) or set custom layout with object</td>
</tr>
</tbody>
</table>
<h4>Group Controls</h4>
<table>
<thead>
<tr>
<th>Action</th>
<th>Value</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr>
<td>group</td>
<td>1-8</td>
<td>Toggle in/out of specified group</td>
</tr>
<tr>
<td>joinGroup</td>
<td>1-8</td>
<td>Join specified group</td>
</tr>
<tr>
<td>leaveGroup</td>
<td>1-8</td>
<td>Leave specified group</td>
</tr>
<tr>
<td>viewGroup</td>
<td>1-8</td>
<td>Toggle preview of specified group</td>
</tr>
<tr>
<td>joinViewGroup</td>
<td>1-8</td>
<td>Preview specified group</td>
</tr>
<tr>
<td>leaveViewGroup</td>
<td>1-8</td>
<td>Stop previewing specified group</td>
</tr>
</tbody>
</table>
<h4>Camera Control (PTZ)</h4>
<table>
<thead>
<tr>
<th>Action</th>
<th>Value</th>
<th>Value2</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr>
<td>zoom</td>
<td>decimal value</td>
<td>abs/true (optional)</td>
<td>Adjust zoom (positive=in, negative=out, or absolute with Value2)</td>
</tr>
<tr>
<td>focus</td>
<td>decimal value</td>
<td>n/a</td>
<td>Adjust focus (positive=far, negative=near)</td>
</tr>
<tr>
<td>pan</td>
<td>decimal value</td>
<td>n/a</td>
<td>Adjust pan (positive=right, negative=left)</td>
</tr>
<tr>
<td>tilt</td>
<td>decimal value</td>
<td>n/a</td>
<td>Adjust tilt (positive=up, negative=down)</td>
</tr>
<tr>
<td>exposure</td>
<td>0-1</td>
<td>n/a</td>
<td>Set exposure level (0=dark, 1=bright)</td>
</tr>
</tbody>
</table>
<h4>Guest Controls (Director Mode)</h4>
<table>
<thead>
<tr>
<th>Action</th>
<th>Target</th>
<th>Value</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr>
<td>forward</td>
<td>guest slot/ID</td>
<td>room name</td>
<td>Transfer guest to specified room</td>
</tr>
<tr>
<td>addScene</td>
<td>guest slot/ID</td>
<td>0-8 or scene name</td>
<td>Toggle guest in/out of specified scene</td>
</tr>
<tr>
<td>muteScene</td>
<td>guest slot/ID</td>
<td>scene (optional)</td>
<td>Toggle guest's mic in scenes</td>
</tr>
<tr>
<td>mic</td>
<td>guest slot/ID</td>
<td>true/false/toggle</td>
<td>Control guest's microphone</td>
</tr>
<tr>
<td>camera</td>
<td>guest slot/ID</td>
<td>true/false/toggle</td>
<td>Control guest's camera</td>
</tr>
<tr>
<td>video</td>
<td>guest slot/ID</td>
<td>true/false/toggle</td>
<td>Control guest's camera (alternate command)</td>
</tr>
<tr>
<td>hangup</td>
<td>guest slot/ID</td>
<td>null</td>
<td>Disconnect guest</td>
</tr>
<tr>
<td>soloChat</td>
<td>guest slot/ID</td>
<td>null</td>
<td>Toggle one-way solo chat with guest</td>
</tr>
<tr>
<td>soloChatBidirectional</td>
<td>guest slot/ID</td>
<td>null</td>
<td>Toggle two-way solo chat with guest</td>
</tr>
<tr>
<td>speaker</td>
<td>guest slot/ID</td>
<td>null</td>
<td>Toggle guest's speaker</td>
</tr>
<tr>
<td>display</td>
<td>guest slot/ID</td>
<td>null</td>
<td>Toggle guest's display (ability to see video)</td>
</tr>
<tr>
<td>group</td>
<td>guest slot/ID</td>
<td>1-8</td>
<td>Toggle guest in/out of specified group</td>
</tr>
<tr>
<td>forceKeyframe</td>
<td>guest slot/ID</td>
<td>null</td>
<td>Trigger keyframe for guest (fix rainbow artifacts)</td>
</tr>
<tr>
<td>soloVideo</td>
<td>guest slot/ID</td>
<td>null</td>
<td>Toggle highlighting guest's video</td>
</tr>
<tr>
<td>sendChat</td>
<td>guest slot/ID</td>
<td>message text</td>
<td>Send private chat message to guest</td>
</tr>
<tr>
<td>sendDirectorChat</td>
<td>guest slot/ID</td>
<td>message text</td>
<td>Send message and overlay it on guest's screen</td>
</tr>
<tr>
<td>volume</td>
<td>guest slot/ID</td>
<td>0-200</td>
<td>Set guest's microphone volume</td>
</tr>
<tr>
<td>startRoomTimer</td>
<td>guest slot/ID</td>
<td>seconds</td>
<td>Start countdown timer for guest</td>
</tr>
<tr>
<td>pauseRoomTimer</td>
<td>guest slot/ID</td>
<td>null</td>
<td>Pause timer for guest</td>
</tr>
<tr>
<td>stopRoomTimer</td>
<td>guest slot/ID</td>
<td>null</td>
<td>Stop timer for guest</td>
</tr>
<tr>
<td>mixorder</td>
<td>guest slot/ID</td>
<td>-1 or 1</td>
<td>Adjust guest's position in the mixer (-1=up, 1=down)</td>
</tr>
</tbody>
</table>
<h3>Response Formats</h3>
<p>Responses from the API depend on the method used:</p>
<ul>
<li><strong>HTTP GET</strong>: Responses include simple text values like <code>true</code>, <code>false</code>, <code>null</code>, <code>fail</code>, or a specific value. For object responses like <code>getDetails</code>, you'll receive JSON.</li>
<li><strong>WebSocket</strong>: Responses are JSON objects containing the result along with the original request parameters and any custom data fields you included.</li>
<li><strong>HTTP POST</strong>: Responses are JSON objects with similar structure to WebSocket responses.</li>
<li><strong>SSE</strong>: Events are JSON objects containing state updates and other notifications.</li>
</ul>
<h3>Custom Layout Format</h3>
<p>When using the <code>layout</code> command, you can provide a custom layout object or array:</p>
<div>
<div class="code-header">
<span>Layout Object Example</span>
<button class="copy-btn" onclick="copyCode('layout-example')">Copy</button>
</div>
<pre class="code-block" id="layout-example">// Single layout example
{
"action": "layout",
"value": [
{"x": 0, "y": 0, "w": 50, "h": 100, "slot": 0},
{"x": 50, "y": 0, "w": 50, "h": 100, "slot": 1}
]
}
// Using URL parameter to define multiple layouts
// ?layouts=[[{"x":0,"y":0,"w":100,"h":100,"slot":0}],[{"x":0,"y":0,"w":50,"h":100,"slot":0},{"x":50,"y":0,"w":50,"h":100,"slot":1}]]
// Layout properties:
// x, y: Position (percentage of container)
// w, h: Width and height (percentage of container)
// slot: Guest slot number (0-indexed)
// c: Crop to fit (true/false)
// z: Z-index (layer)
// To switch layouts:
// {"action": "layout", "value": 0} // Auto-mix
// {"action": "layout", "value": 1} // First custom layout
// {"action": "layout", "value": 2} // Second custom layout</pre>
</div>
</div>
</div>
</main>
<div class="logger-container">
<div class="api-logger-header">
<span><strong>API Request/Response Logger</strong></span>
<button class="copy-btn" onclick="clearLogs()">Clear</button>
</div>
<div class="api-logger" id="api-logger"></div>
</div>
</div>
<script>
// Utility functions
function generateStreamID() {
const possible = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
let text = "";
for (let i = 0; i < 10; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
// Copy to clipboard
function copyToClipboard(element) {
const text = element.textContent;
navigator.clipboard.writeText(text).then(() => {
// Show tooltip
const tooltip = document.createElement('div');
tooltip.className = 'tooltip-text';
tooltip.style = 'position: fixed; background: #333; color: white; padding: 5px 10px; border-radius: 3px; z-index: 100; opacity: 0; transition: opacity 0.3s;';
tooltip.textContent = 'Copied to clipboard!';
element.appendChild(tooltip);
setTimeout(() => {
tooltip.style.opacity = '1';
}, 10);
setTimeout(() => {
tooltip.style.opacity = '0';
setTimeout(() => {
element.removeChild(tooltip);
}, 300);
}, 2000);
});
}
// Copy code example
function copyCode(elementId) {
const code = document.getElementById(elementId).textContent;
navigator.clipboard.writeText(code).then(() => {
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
});
}
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.getElementById(`${tab.dataset.tab}-tab`).classList.add('active');
});
});
// Sliders
document.getElementById('zoom-slider').addEventListener('input', function() {
const value = this.value / 100;
document.getElementById('zoom-value').textContent = value.toFixed(2);
});
document.getElementById('zoom-slider').addEventListener('change', function() {
const value = this.value / 100;
sendPTZCommand('zoom', value, 'abs');
});
document.getElementById('exposure-slider').addEventListener('input', function() {
const value = this.value / 100;
document.getElementById('exposure-value').textContent = value.toFixed(2);
});
document.getElementById('exposure-slider').addEventListener('change', function() {
const value = this.value / 100;
sendPTZCommand('exposure', value);
});
document.getElementById('guest-volume-slider').addEventListener('input', function() {
document.getElementById('guest-volume-value').textContent = this.value;
});
document.getElementById('guest-volume-slider').addEventListener('change', function() {
const guestSlot = document.getElementById('guest-slot').value;
sendGuestCommand(guestSlot, "volume", parseInt(this.value));
});
// HTTP URL preview
function updateHTTPUrlPreview() {
const action = document.getElementById('http-action').value;
const target = document.getElementById('http-target').value || "null";
const value = document.getElementById('http-value').value || "null";
const apiID = WID;
document.getElementById('http-url-preview').textContent = `https://api.vdo.ninja/${apiID}/${action}/${target}/${value}`;
}
document.getElementById('http-action').addEventListener('change', updateHTTPUrlPreview);
document.getElementById('http-target').addEventListener('input', updateHTTPUrlPreview);
document.getElementById('http-value').addEventListener('input', updateHTTPUrlPreview);
// Form field clearing
function clearWSFields() {
document.getElementById('ws-value').value = '';
document.getElementById('ws-target').value = '';
document.getElementById('ws-value2').value = '';
document.getElementById('ws-callback').value = '';
document.getElementById('ws-custom-json').value = '';
}
function clearHTTPFields() {
document.getElementById('http-target').value = '';
document.getElementById('http-value').value = '';
updateHTTPUrlPreview();
}
function clearPOSTFields() {
document.getElementById('post-value').value = '';
document.getElementById('post-target').value = '';
// Reset JSON to template based on action
const action = document.getElementById('post-action').value;
let template = { action };
document.getElementById('post-json').value = JSON.stringify(template, null, 2);
}
function clearGuestFields() {
document.getElementById('guest-value').value = '';
}
// Logging functions
function clearLogs() {
document.getElementById('api-logger').innerHTML = '';
}
function addLogEntry(type, method, endpoint, data, response) {
const logger = document.getElementById('api-logger');
const logEntry = document.createElement('div');
logEntry.className = 'log-entry';
const time = new Date().toLocaleTimeString();
let html = `<span class="log-time">[${time}]</span>`;
html += `<span class="log-method log-${method.toLowerCase()}">${method}</span>`;
if (type === 'request') {
html += `<span class="log-request">${endpoint}</span>`;
if (data) {
html += `<br><span class="log-request">Data: ${typeof data === 'object' ? JSON.stringify(data) : data}</span>`;
}
} else if (type === 'response') {
html += `<span class="log-response">Response: ${typeof response === 'object' ? JSON.stringify(response) : response}</span>`;
} else if (type === 'error') {
html += `<span class="log-error">Error: ${data}</span>`;
}
logEntry.innerHTML = html;
logger.appendChild(logEntry);
logger.scrollTop = logger.scrollHeight;
}
// Parse URL parameters
(function (w) {
w.URLSearchParams = w.URLSearchParams || function (searchString) {
var self = this;
self.searchString = searchString;
self.get = function (name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(self.searchString);
if (results == null) {
return null;
} else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
const urlParams = new URLSearchParams(window.location.search);
let WID = "testOBSN";
// Handle API ID
if (urlParams.has("api")) {
WID = urlParams.get("api");
} else if (urlParams.has("osc")) {
WID = urlParams.get("osc");
} else if (urlParams.has("id")) {
WID = urlParams.get("id");
} else if (urlParams.has("ID")) {
WID = urlParams.get("ID");
} else if (urlParams.has("wid")) {
WID = urlParams.get("wid");
} else {
WID = generateStreamID();
const href = window.location.href;
const arr = href.split('?');
let newurl;
if (arr.length > 1 && arr[1] !== '') {
newurl = href + '&api=' + WID;
} else {
newurl = href + '?api=' + WID;
}
window.history.pushState({ path: newurl.toString() }, '', newurl.toString());
}
// Update the displayed API key
document.getElementById('api-key').textContent = WID;
// Create the VDO.Ninja link
const ninjaPath = "vdo.ninja";
const ninjaLink = document.getElementById('ninja-link');
ninjaLink.href = `https://${ninjaPath}/?api=${WID}`;
ninjaLink.textContent = `https://${ninjaPath}/?api=${WID}`;
// WebSocket setup
let socket = null;
let connecting = false;
let failedCount = 0;
let callbackID = parseInt(Math.random() * 1000);
function connect() {
clearTimeout(connecting);
if (socket) {
if (socket.readyState === socket.OPEN) {
updateConnectionStatus('connected');
return;
}
try {
socket.close();
} catch (e) { }
}
updateConnectionStatus('connecting');
socket = new WebSocket("wss://api.vdo.ninja:443");
socket.onclose = function() {
updateConnectionStatus('disconnected');
failedCount += 1;
clearTimeout(connecting);
connecting = setTimeout(function() {
connect();
}, 100 * (failedCount - 1));
};
socket.onerror = function() {
updateConnectionStatus('disconnected');
failedCount += 1;
clearTimeout(connecting);
connecting = setTimeout(function() {
connect();
}, 100 * failedCount);
};
socket.onopen = function() {
updateConnectionStatus('connected');
failedCount = 0;
try {
socket.send(JSON.stringify({ "join": WID }));
addLogEntry('request', 'WS', 'Connection', { "join": WID });
} catch (e) {
connecting = setTimeout(function() {
connect();
}, 1);
}
};
socket.addEventListener('message', function(event) {
if (event.data) {
const data = JSON.parse(event.data);
addLogEntry('response', 'WS', 'Message', null, data);
if ("callback" in data) {
if (data.callback.value == callbackID) {
console.log("Received callback response:", data);
}
}
}
});
}
function updateConnectionStatus(status) {
const indicator = document.getElementById('connection-indicator');
const statusText = document.getElementById('connection-status');
indicator.className = 'status-indicator';
if (status === 'connected') {
indicator.classList.add('status-connected');
statusText.textContent = 'Connected';
statusText.style.color = 'var(--accent-success)';
} else if (status === 'disconnected') {
indicator.classList.add('status-disconnected');
statusText.textContent = 'Disconnected';
statusText.style.color = 'var(--accent-danger)';
} else {
indicator.classList.add('status-connecting');
statusText.textContent = 'Connecting...';
statusText.style.color = 'var(--accent-warning)';
}
}
// Connect to the WebSocket server
connect();
// Send messages via WebSocket
function sendMessage(msg) {
if (!socket || socket.readyState !== socket.OPEN) {
addLogEntry('error', 'WS', 'Not connected', "WebSocket not connected");
connect();
return;
}
try {
if (typeof msg === 'object') {
msg = JSON.stringify(msg);
}
socket.send(msg);
addLogEntry('request', 'WS', 'Send Message', JSON.parse(msg));
} catch (e) {
addLogEntry('error', 'WS', 'Send Error', e.message);
connecting = setTimeout(function() {
connect();
}, 100);
}
}
// Send WebSocket request based on form
function sendWSRequest() {
try {
const customJson = document.getElementById('ws-custom-json').value.trim();
if (customJson) {
// Send custom JSON
const jsonObj = JSON.parse(customJson);
sendMessage(jsonObj);
} else {
// Build from form fields
const action = document.getElementById('ws-action').value;
const value = document.getElementById('ws-value').value;
const value2 = document.getElementById('ws-value2').value;
const target = document.getElementById('ws-target').value;
const callback = document.getElementById('ws-callback').value;
const msg = { action };
// Parse value if needed
if (value) {
if (value === 'true') msg.value = true;
else if (value === 'false') msg.value = false;
else if (!isNaN(value)) msg.value = Number(value);
else msg.value = value;
}
// Add optional params
if (value2) msg.value2 = value2;
if (target) msg.target = target;
if (callback) msg.cid = callback;
sendMessage(msg);
}
} catch (e) {
addLogEntry('error', 'WS', 'Invalid Request', e.message);
}
}
// Send HTTP GET request
function sendHTTPRequest() {
try {
const action = document.getElementById('http-action').value;
const target = document.getElementById('http-target').value || "null";
const value = document.getElementById('http-value').value || "null";
const url = `https://api.vdo.ninja/${WID}/${action}/${target}/${value}`;
addLogEntry('request', 'GET', url);
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.text();
})
.then(data => {
// Try to parse as JSON, fall back to text
try {
const jsonData = JSON.parse(data);
addLogEntry('response', 'GET', url, null, jsonData);
} catch (e) {
addLogEntry('response', 'GET', url, null, data);
}
})
.catch(error => {
addLogEntry('error', 'GET', url, error.message);
});
} catch (e) {
addLogEntry('error', 'GET', 'Invalid Request', e.message);
}
}
// Send HTTP POST request
function sendPOSTRequest() {
try {
const customJson = document.getElementById('post-json').value.trim();
let jsonData;
if (customJson) {
jsonData = JSON.parse(customJson);
} else {
const action = document.getElementById('post-action').value;
const value = document.getElementById('post-value').value;
const target = document.getElementById('post-target').value;
jsonData = { action };
// Parse value if needed
if (value) {
if (value === 'true') jsonData.value = true;
else if (value === 'false') jsonData.value = false;
else if (!isNaN(value)) jsonData.value = Number(value);
else jsonData.value = value;
}
// Add target if specified
if (target) jsonData.target = target;
}
const url = `https://api.vdo.ninja/${WID}`;
addLogEntry('request', 'POST', url, jsonData);
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(jsonData)
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
addLogEntry('response', 'POST', url, null, data);
})
.catch(error => {
addLogEntry('error', 'POST', url, error.message);
});
} catch (e) {
addLogEntry('error', 'POST', 'Invalid Request', e.message);
}
}
// SSE Connection
let eventSource = null;
function connectSSE() {
if (eventSource) {
disconnectSSE();
}
const sseStatus = document.getElementById('sse-status');
sseStatus.textContent = 'Connecting...';
sseStatus.style.color = 'var(--accent-warning)';
eventSource = new EventSource(`https://api.vdo.ninja/sse/${WID}`);
addLogEntry('request', 'SSE', `https://api.vdo.ninja/sse/${WID}`);
eventSource.onopen = function() {
sseStatus.textContent = 'Connected - Listening for events';
sseStatus.style.color = 'var(--accent-success)';
document.getElementById('sse-connect-btn').disabled = true;
document.getElementById('sse-disconnect-btn').disabled = false;
addLogEntry('response', 'SSE', 'Connection established');
};
eventSource.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
addLogEntry('response', 'SSE', 'Event received', null, data);
} catch (e) {
addLogEntry('error', 'SSE', 'Parse Error', e.message);
}
};
eventSource.onerror = function(error) {
sseStatus.textContent = 'Error - Connection closed';
sseStatus.style.color = 'var(--accent-danger)';
document.getElementById('sse-connect-btn').disabled = false;
document.getElementById('sse-disconnect-btn').disabled = true;
addLogEntry('error', 'SSE', 'Connection Error', error);
eventSource.close();
eventSource = null;
};
}
function disconnectSSE() {
if (eventSource) {
eventSource.close();
eventSource = null;
const sseStatus = document.getElementById('sse-status');
sseStatus.textContent = 'Disconnected';
sseStatus.style.color = 'var(--text-secondary)';
document.getElementById('sse-connect-btn').disabled = false;
document.getElementById('sse-disconnect-btn').disabled = true;
addLogEntry('request', 'SSE', 'Connection closed manually');
}
}
// PTZ camera controls
function sendPTZCommand(action, value, value2 = null) {
const command = { action, value };
if (value2) command.value2 = value2;
sendMessage(command);
}
// Guest control commands
function sendGuestCommand(target, action, value = null) {
if (!target) {
target = document.getElementById('guest-slot').value;
}
if (!action) {
action = document.getElementById('guest-action').value;
}
if (value === undefined && document.getElementById('guest-value').value) {
const rawValue = document.getElementById('guest-value').value;
// Parse value appropriately
if (rawValue === 'true') value = true;
else if (rawValue === 'false') value = false;
else if (rawValue === 'toggle') value = 'toggle';
else if (!isNaN(rawValue)) value = Number(rawValue);
else value = rawValue;
}
const method = document.getElementById('guest-method').value;
if (method === 'websocket') {
// WebSocket method
const msg = { target, action };
if (value !== null) msg.value = value;
sendMessage(msg);
} else if (method === 'http') {
// HTTP method
const targetStr = target || "null";
const valueStr = value !== null ? value : "null";
const url = `https://api.vdo.ninja/${WID}/${action}/${targetStr}/${valueStr}`;
addLogEntry('request', 'GET', url);
fetch(url)
.then(response => response.text())
.then(data => {
try {
const jsonData = JSON.parse(data);
addLogEntry('response', 'GET', url, null, jsonData);
} catch (e) {
addLogEntry('response', 'GET', url, null, data);
}
})
.catch(error => {
addLogEntry('error', 'GET', url, error.message);
});
}
}
// MIDI integration
let midiAccess = null;
let activeInputs = [];
function initMIDI() {
if (!navigator.requestMIDIAccess) {
const midiStatus = document.getElementById('midi-status');
midiStatus.textContent = 'Web MIDI API not supported in this browser';
midiStatus.style.color = 'var(--accent-danger)';
addLogEntry('error', 'MIDI', 'MIDI not supported', 'Web MIDI API not available in this browser');
return;
}
navigator.requestMIDIAccess({ sysex: false })
.then(access => {
midiAccess = access;
updateMIDIStatus();
populateMIDIDevices();
midiAccess.onstatechange = function(e) {
updateMIDIStatus();
populateMIDIDevices();
};
document.getElementById('midi-test-btn').disabled = false;
addLogEntry('response', 'MIDI', 'MIDI access granted');
})
.catch(error => {
const midiStatus = document.getElementById('midi-status');
midiStatus.textContent = `Error: ${error.message}`;
midiStatus.style.color = 'var(--accent-danger)';
addLogEntry('error', 'MIDI', 'MIDI access denied', error.message);
});
}
function updateMIDIStatus() {
if (!midiAccess) return;
const midiStatus = document.getElementById('midi-status');
const inputs = Array.from(midiAccess.inputs.values());
const outputs = Array.from(midiAccess.outputs.values());
midiStatus.textContent = `Available: ${inputs.length} input(s), ${outputs.length} output(s)`;
midiStatus.style.color = inputs.length > 0 ? 'var(--accent-success)' : 'var(--text-secondary)';
}
function populateMIDIDevices() {
if (!midiAccess) return;
const deviceSelect = document.getElementById('midi-device');
deviceSelect.innerHTML = '';
deviceSelect.disabled = false;
// Add placeholder option
const noneOption = document.createElement('option');
noneOption.value = '';
noneOption.textContent = '-- Select MIDI device --';
deviceSelect.appendChild(noneOption);
// Add all inputs
let anyDevices = false;
midiAccess.inputs.forEach(input => {
anyDevices = true;
const option = document.createElement('option');
option.value = input.id;
option.textContent = input.name || `MIDI Input ${input.id}`;
deviceSelect.appendChild(option);
});
// Set the select status appropriately
if (!anyDevices) {
const noDevicesOption = document.createElement('option');
noDevicesOption.value = '';
noDevicesOption.textContent = 'No MIDI devices available';
deviceSelect.innerHTML = '';
deviceSelect.appendChild(noDevicesOption);
deviceSelect.disabled = true;
}
// Setup device change handler
deviceSelect.onchange = function() {
setupMIDIDevice(this.value);
};
}
function setupMIDIDevice(deviceId) {
// Clear all existing MIDI input handlers
activeInputs.forEach(input => {
input.onmidimessage = null;
});
activeInputs = [];
if (!deviceId) return;
const input = midiAccess.inputs.get(deviceId);
if (!input) return;
// Setup new handler
input.onmidimessage = handleMIDIMessage;
activeInputs.push(input);
addLogEntry('request', 'MIDI', `Connected to ${input.name || deviceId}`);
}
function handleMIDIMessage(message) {
const monitor = document.getElementById('midi-monitor');
const command = message.data[0];
const note = message.data[1];
const velocity = message.data[2];
const commandType =
command >= 144 && command <= 159 ? 'Note On' :
command >= 128 && command <= 143 ? 'Note Off' :
command >= 176 && command <= 191 ? 'Control Change' :
'Other';
// Display MIDI message
monitor.textContent = `Command: ${command} (${commandType}), Note/CC: ${note}, Velocity/Value: ${velocity}`;
// Log the MIDI message
const noteOrCC = commandType === 'Control Change' ? `CC ${note}` : getNoteNameFromNumber(note);
addLogEntry('request', 'MIDI', `${commandType} ${noteOrCC} ${velocity}`);
// Process MIDI mapping (basic example)
if (commandType === 'Note On' && velocity > 0) {
processMIDICommand(note, velocity);
}
}
function processMIDICommand(note, velocity) {
const noteName = getNoteNameFromNumber(note);
// Example mapping for midiHotkeys=1
if (noteName === 'G3') {
sendMessage({ action: 'toggleChat' });
} else if (noteName === 'A3') {
sendMessage({ action: 'mic', value: 'toggle' });
} else if (noteName === 'B3') {
sendMessage({ action: 'camera', value: 'toggle' });
}
// Process camera controls (C5-G5 notes)
if (noteName === 'C5') {
const zoomValue = velocity / 127; // Normalize to 0-1
sendPTZCommand('zoom', zoomValue, 'abs');
} else if (noteName === 'D5') { /* Add this */
const focusValue = velocity / 127; // Normalize to 0-1
sendPTZCommand('focus', focusValue);
} else if (noteName === 'E5') { /* Add this */
const panValue = (velocity - 64) / 64; // Normalize to -1 to 1
sendPTZCommand('pan', panValue);
} else if (noteName === 'F5') { /* Add this */
const tiltValue = (velocity - 64) / 64; // Normalize to -1 to 1
sendPTZCommand('tilt', tiltValue);
} else if (noteName === 'G5') { /* Add this */
const exposureValue = velocity / 127; // Normalize to 0-1
sendPTZCommand('exposure', exposureValue);
}
}
function testMIDI() {
// Send a test MIDI message to the console
addLogEntry('request', 'MIDI', 'Test message sent');
// Simulate a MIDI message
const monitor = document.getElementById('midi-monitor');
monitor.textContent = 'Test MIDI message: Note On C4 (60) velocity 127';
}
// Helper function to convert MIDI note number to note name
function getNoteNameFromNumber(noteNumber) {
const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const octave = Math.floor(noteNumber / 12) - 1;
const noteName = notes[noteNumber % 12];
return noteName + octave;
}
// Create control buttons
function createWebSocketControls() {
const container = document.getElementById('ws-controls');
container.innerHTML = '';
// Define control buttons
const controls = [
{ name: "Mic On", action: "mic", value: true, type: "true" },
{ name: "Mic Off", action: "mic", value: false, type: "false" },
{ name: "Mic Toggle", action: "mic", value: "toggle", type: "toggle" },
{ name: "Camera On", action: "camera", value: true, type: "true" },
{ name: "Camera Off", action: "camera", value: false, type: "false" },
{ name: "Camera Toggle", action: "camera", value: "toggle", type: "toggle" },
{ name: "Video On", action: "video", value: true, type: "true" },
{ name: "Video Off", action: "video", value: false, type: "false" },
{ name: "Video Toggle", action: "video", value: "toggle", type: "toggle" },
{ name: "Speaker On", action: "speaker", value: true, type: "true" },
{ name: "Speaker Off", action: "speaker", value: false, type: "false" },
{ name: "Speaker Toggle", action: "speaker", value: "toggle", type: "toggle" },
{ name: "Volume 100%", action: "volume", value: 100, type: "true" },
{ name: "Volume 0%", action: "volume", value: 0, type: "false" },
{ name: "Volume 50%", action: "volume", value: 50, type: "toggle" },
{ name: "Record Start", action: "record", value: true, type: "true" },
{ name: "Record Stop", action: "record", value: false, type: "false" },
{ name: "Bitrate Reset", action: "bitrate", value: true, type: "true" },
{ name: "Bitrate 0 (Pause)", action: "bitrate", value: false, type: "false" },
{ name: "Bitrate 1000kb", action: "bitrate", value: 1000, type: "toggle" },
{ name: "Toggle Hand", action: "togglehand", value: null, type: "toggle" },
{ name: "Reload Page", action: "reload", value: true, type: "toggle" },
{ name: "Hangup", action: "hangup", value: true, type: "false" },
{ name: "Force Keyframe", action: "forceKeyframe", value: null, type: "toggle" },
{ name: "Say Hello", action: "sendChat", value: "Hello", type: "toggle" },
{ name: "Overlay Hello", action: "sendDirectorChat", value: "Hello", type: "toggle" },
{ name: "Get Details", action: "getDetails", value: null, type: "toggle" },
{ name: "Highlight Toggle", action: "soloVideo", value: "toggle", type: "toggle" },
];
// Create and add buttons to the container
controls.forEach(control => {
const button = document.createElement('button');
button.textContent = control.name;
button.setAttribute('data-type', control.type);
// Create tooltip with request details
const tooltip = document.createElement('span');
tooltip.className = 'tooltip-text';
// Format the request info based on the control
let apiFormat = `WS: {"action":"${control.action}"`;
if (control.value !== null) {
apiFormat += `,"value":${typeof control.value === 'string' ? `"${control.value}"` : control.value}`;
}
apiFormat += "}";
tooltip.textContent = apiFormat;
// Add tooltip to button
button.classList.add('tooltip');
button.appendChild(tooltip);
// Set onclick handler
button.onclick = function() {
const msg = { action: control.action };
if (control.value !== null) msg.value = control.value;
sendMessage(JSON.stringify(msg));
};
container.appendChild(button);
});
}
// Create layout control buttons
function createLayoutControls() {
const container = document.getElementById('ws-layouts');
container.innerHTML = '';
// Create buttons for layouts 0-8
for (let i = 0; i <= 8; i++) {
const button = document.createElement('button');
const name = i === 0 ? "Auto-Mix" : `Layout ${i}`;
button.textContent = name;
button.setAttribute('data-type', "toggle");
// Create tooltip with request details
const tooltip = document.createElement('span');
tooltip.className = 'tooltip-text';
tooltip.textContent = `WS: {"action":"layout","value":${i}}`;
// Add tooltip to button
button.classList.add('tooltip');
button.appendChild(tooltip);
// Set onclick handler
button.onclick = function() {
sendMessage(JSON.stringify({ "action": "layout", "value": i }));
};
container.appendChild(button);
}
// Add a few sample grid layouts
const sampleLayouts = [
{
name: "50/50 Split",
layout: [
{"x":0,"y":0,"w":50,"h":100,"slot":0},
{"x":50,"y":0,"w":50,"h":100,"slot":1}
]
},
{
name: "Picture in Picture",
layout: [
{"x":0,"y":0,"w":100,"h":100,"slot":0},
{"x":70,"y":70,"w":30,"h":30,"z":1,"c":true,"slot":1}
]
},
{
name: "2x2 Grid",
layout: [
{"x":0,"y":0,"w":50,"h":50,"slot":0},
{"x":50,"y":0,"w":50,"h":50,"slot":1},
{"x":0,"y":50,"w":50,"h":50,"slot":2},
{"x":50,"y":50,"w":50,"h":50,"slot":3}
]
}
];
sampleLayouts.forEach(sample => {
const button = document.createElement('button');
button.textContent = sample.name;
button.setAttribute('data-type', "toggle");
// Create tooltip with request details
const tooltip = document.createElement('span');
tooltip.className = 'tooltip-text';
tooltip.textContent = `WS: {"action":"layout","value":${JSON.stringify(sample.layout)}}`;
// Add tooltip to button
button.classList.add('tooltip');
button.appendChild(tooltip);
// Set onclick handler
button.onclick = function() {
sendMessage(JSON.stringify({ "action": "layout", "value": sample.layout }));
};
container.appendChild(button);
});
}
// Create HTTP API buttons
function createHTTPControls() {
const container = document.getElementById('http-controls');
container.innerHTML = '';
// Define HTTP API buttons
const controls = [
{ name: "Mic On", action: "mic", value: true, type: "true" },
{ name: "Mic Off", action: "mic", value: false, type: "false" },
{ name: "Mic Toggle", action: "mic", value: "toggle", type: "toggle" },
{ name: "Camera On", action: "camera", value: true, type: "true" },
{ name: "Camera Off", action: "camera", value: false, type: "false" },
{ name: "Camera Toggle", action: "camera", value: "toggle", type: "toggle" },
{ name: "Video On", action: "video", value: true, type: "true" },
{ name: "Video Off", action: "video", value: false, type: "false" },
{ name: "Video Toggle", action: "video", value: "toggle", type: "toggle" },
{ name: "Speaker On", action: "speaker", value: true, type: "true" },
{ name: "Speaker Off", action: "speaker", value: false, type: "false" },
{ name: "Speaker Toggle", action: "speaker", value: "toggle", type: "toggle" },
{ name: "Volume 100%", action: "volume", value: 100, type: "true" },
{ name: "Volume 0%", action: "volume", value: 0, type: "false" },
{ name: "Volume 50%", action: "volume", value: 50, type: "toggle" },
{ name: "Record Start", action: "record", value: true, type: "true" },
{ name: "Record Stop", action: "record", value: false, type: "false" },
{ name: "Bitrate Reset", action: "bitrate", value: true, type: "true" },
{ name: "Bitrate 0 (Pause)", action: "bitrate", value: false, type: "false" },
{ name: "Bitrate 1000kb", action: "bitrate", value: 1000, type: "toggle" },
];
// Create and add buttons to the container
controls.forEach(control => {
const button = document.createElement('button');
button.textContent = control.name;
button.setAttribute('data-type', control.type);
// Create tooltip with request details
const tooltip = document.createElement('span');
tooltip.className = 'tooltip-text';
// Format the request info based on the control
let apiFormat = `GET: https://api.vdo.ninja/${WID}/${control.action}/null/`;
if (control.value !== null) {
apiFormat += typeof control.value === 'string' ? control.value : control.value;
}
tooltip.textContent = apiFormat;
// Add tooltip to button
button.classList.add('tooltip');
button.appendChild(tooltip);
// Set onclick handler
button.onclick = function() {
const url = `https://api.vdo.ninja/${WID}/${control.action}/null/${control.value !== null ? control.value : 'null'}`;
addLogEntry('request', 'GET', url);
fetch(url)
.then(response => response.text())
.then(data => {
try {
const jsonData = JSON.parse(data);
addLogEntry('response', 'GET', url, null, jsonData);
} catch (e) {
addLogEntry('response', 'GET', url, null, data);
}
})
.catch(error => {
addLogEntry('error', 'GET', url, error.message);
});
};
container.appendChild(button);
});
}
// Create HTTP Group Controls
function createHTTPGroupControls() {
const container = document.getElementById('http-groups');
container.innerHTML = '';
// Define group controls
const groups = [1, 2];
const actions = [
{ name: "Toggle", action: "group" },
{ name: "Join", action: "joinGroup" },
{ name: "Leave", action: "leaveGroup" }
];
// Create and add group buttons
groups.forEach(group => {
actions.forEach(actionObj => {
const button = document.createElement('button');
button.textContent = `Group ${group}: ${actionObj.name}`;
button.setAttribute('data-type', "toggle");
// Create tooltip with request details
const tooltip = document.createElement('span');
tooltip.className = 'tooltip-text';
tooltip.textContent = `GET: https://api.vdo.ninja/${WID}/${actionObj.action}/null/${group}`;
// Add tooltip to button
button.classList.add('tooltip');
button.appendChild(tooltip);
// Set onclick handler
button.onclick = function() {
const url = `https://api.vdo.ninja/${WID}/${actionObj.action}/null/${group}`;
addLogEntry('request', 'GET', url);
fetch(url)
.then(response => response.text())
.then(data => {
try {
const jsonData = JSON.parse(data);
addLogEntry('response', 'GET', url, null, jsonData);
} catch (e) {
addLogEntry('response', 'GET', url, null, data);
}
})
.catch(error => {
addLogEntry('error', 'GET', url, error.message);
});
};
container.appendChild(button);
});
});
// Create view group buttons
groups.forEach(group => {
const viewActions = [
{ name: "View Toggle", action: "viewGroup" },
{ name: "View Join", action: "joinViewGroup" },
{ name: "View Leave", action: "leaveViewGroup" }
];
viewActions.forEach(actionObj => {
const button = document.createElement('button');
button.textContent = `${actionObj.name} ${group}`;
button.setAttribute('data-type', "toggle");
// Create tooltip with request details
const tooltip = document.createElement('span');
tooltip.className = 'tooltip-text';
tooltip.textContent = `GET: https://api.vdo.ninja/${WID}/${actionObj.action}/null/${group}`;
// Add tooltip to button
button.classList.add('tooltip');
button.appendChild(tooltip);
// Set onclick handler
button.onclick = function() {
const url = `https://api.vdo.ninja/${WID}/${actionObj.action}/null/${group}`;
addLogEntry('request', 'GET', url);
fetch(url)
.then(response => response.text())
.then(data => {
try {
const jsonData = JSON.parse(data);
addLogEntry('response', 'GET', url, null, jsonData);
} catch (e) {
addLogEntry('response', 'GET', url, null, data);
}
})
.catch(error => {
addLogEntry('error', 'GET', url, error.message);
});
};
container.appendChild(button);
});
});
// Add PowerPoint control buttons
const ppControls = [
{ name: "Next Slide", action: "nextSlide" },
{ name: "Prev Slide", action: "prevSlide" }
];
ppControls.forEach(control => {
const button = document.createElement('button');
button.textContent = control.name;
button.setAttribute('data-type', "toggle");
// Create tooltip with request details
const tooltip = document.createElement('span');
tooltip.className = 'tooltip-text';
tooltip.textContent = `GET: https://api.vdo.ninja/${WID}/${control.action}/null/null`;
// Add tooltip to button
button.classList.add('tooltip');
button.appendChild(tooltip);
// Set onclick handler
button.onclick = function() {
const url = `https://api.vdo.ninja/${WID}/${control.action}/null/null`;
addLogEntry('request', 'GET', url);
fetch(url)
.then(response => response.text())
.then(data => {
try {
const jsonData = JSON.parse(data);
addLogEntry('response', 'GET', url, null, jsonData);
} catch (e) {
addLogEntry('response', 'GET', url, null, data);
}
})
.catch(error => {
addLogEntry('error', 'GET', url, error.message);
});
};
container.appendChild(button);
});
}
// Create guest control buttons
function createGuestControls() {
const container = document.getElementById('guest-controls');
container.innerHTML = '';
// Define guest controls
const controls = [
{ name: "Mute Mic", action: "mic", type: "toggle" },
{ name: "Toggle Camera", action: "camera", type: "toggle" },
{ name: "Toggle Video", action: "video", type: "toggle" },
{ name: "Hangup", action: "hangup", type: "false" },
{ name: "Solo Chat", action: "soloChat", type: "toggle" },
{ name: "Two-way Solo Chat", action: "soloChatBidirectional", type: "toggle" },
{ name: "Toggle Speaker", action: "speaker", type: "toggle" },
{ name: "Toggle Display", action: "display", type: "toggle" },
{ name: "Fix Rainbow", action: "forceKeyframe", type: "toggle" },
{ name: "Highlight", action: "soloVideo", type: "toggle" },
{ name: "Add to Scene 1", action: "addScene", value: 1, type: "toggle" },
{ name: "Add to Scene 2", action: "addScene", value: 2, type: "toggle" },
{ name: "Add to Group 1", action: "group", value: 1, type: "toggle" },
{ name: "Add to Group 2", action: "group", value: 2, type: "toggle" },
{ name: "Say Hello", action: "sendChat", value: "hello", type: "toggle" },
{ name: "Overlay Hello", action: "sendDirectorChat", value: "Hello", type: "toggle" },
{ name: "Start Timer (10m)", action: "startRoomTimer", value: 600, type: "toggle" },
{ name: "Pause Timer", action: "pauseRoomTimer", type: "toggle" },
{ name: "Stop Timer", action: "stopRoomTimer", type: "toggle" },
{ name: "Forward to room321", action: "forward", value: "room321", type: "toggle" },
];
// Create and add buttons to the container
controls.forEach(control => {
const button = document.createElement('button');
button.textContent = control.name;
button.setAttribute('data-type', control.type);
// Create tooltip with request details
const tooltip = document.createElement('span');
tooltip.className = 'tooltip-text';
// Set onclick handler
button.onclick = function() {
const guestSlot = document.getElementById('guest-slot').value;
const method = document.getElementById('guest-method').value;
let tooltipText;
if (method === 'websocket') {
tooltipText = `WS: {"target":"${guestSlot}","action":"${control.action}"${control.value !== undefined ? `,"value":${typeof control.value === 'string' ? `"${control.value}"` : control.value}` : ''}}`;
} else {
tooltipText = `GET: https://api.vdo.ninja/${WID}/${control.action}/${guestSlot}/${control.value !== undefined ? control.value : 'null'}`;
}
tooltip.textContent = tooltipText;
sendGuestCommand(
guestSlot,
control.action,
control.value !== undefined ? control.value : null
);
};
// Add tooltip to button
button.classList.add('tooltip');
button.appendChild(tooltip);
container.appendChild(button);
});
}
// Initialize all controls
document.addEventListener('DOMContentLoaded', function() {
createWebSocketControls();
createLayoutControls();
createHTTPControls();
createHTTPGroupControls();
createGuestControls();
updateHTTPUrlPreview();
// Initialize form handlers for POST
document.getElementById('post-action').addEventListener('change', function() {
let template = { action: this.value };
document.getElementById('post-json').value = JSON.stringify(template, null, 2);
});
});
// Prevent accidental page reloads
window.onbeforeunload = function() {
return "Are you sure you want to leave this page? All connection settings will be lost.";
}
</script>
</body>
</html>