Files
archived-vdo.ninja/iframe.html
steveseguin a631bc074c v29.0
2026-01-18 03:27:00 -05:00

1299 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html>
<head>
<title>VDO.Ninja IFRAME API - Interactive Developer Console</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" />
<link rel="stylesheet" href="./iframe.css" />
<script src="iframe-examples.js"></script>
<script>
// Global state management
const ExpandableCards = {};
const ActiveConnections = new Map(); // Track active connections by streamID
const MessageHistory = [];
const MAX_HISTORY = 100;
/**
* Main function to load and initialize a VDO.Ninja iframe
* This demonstrates best practices for iframe integration
*/
function loadIframe() {
var exampleId = generateId();
// Initialize expandable card states
ExpandableCards[exampleId] = {
iframe: false,
companion: false,
}
// Process and validate the iframe URL
var iframesrc = document.getElementById("viewlink").value;
if (!(iframesrc.startsWith("?") || iframesrc.startsWith("&") || iframesrc.startsWith("http"))) {
if (iframesrc.split(".").length > 1) {
iframesrc = "https://" + iframesrc;
}
}
// Create main container elements
var container = document.getElementById("container");
var iframeExample = createIframeExample(exampleId, iframesrc);
container.prepend(iframeExample.container);
// Set up message listener for this specific iframe
setupMessageListener(iframeExample.iframe, exampleId);
}
/**
* Creates the complete iframe example structure
*/
function createIframeExample(exampleId, iframesrc) {
// Create iframe with proper permissions
var iframe = newElement("iframe", {
allow: "document-domain;encrypted-media;sync-xhr;usb;web-share;cross-origin-isolated;midi *;geolocation;camera *;microphone *;fullscreen;picture-in-picture;display-capture;accelerometer;autoplay;gyroscope;screen-wake-lock;",
src: addUrlParams(iframesrc, ["cleanoutput", "transparent", "hidemenu"])
});
iframe.setAttribute("allowtransparency", "true");
iframe.setAttribute("crossorigin", "anonymous");
iframe.setAttribute("credentialless", "true");
// Create UI structure
var container = newElement("div", { id: exampleId, classList: "iframe-example" });
var header = createExampleHeader(exampleId, iframesrc, container);
var body = createExampleBody(exampleId, iframe);
container.append(header, body);
return { container, iframe, id: exampleId };
}
/**
* Creates the header section with URL display and controls
*/
function createExampleHeader(exampleId, iframesrc, container) {
var header = newElement("div", {
classList: "example-header",
innerHTML: `<span title="${iframesrc}">${iframesrc}</span>`
});
// Add connection status indicator
var statusIndicator = newElement("span", {
id: `status-${exampleId}`,
classList: "status-indicator",
innerHTML: "⚪ Initializing"
});
// Add remove button
var removeButton = newElement("button", {
innerHTML: "❌ REMOVE",
onclick: function() {
if (confirm("Remove this iframe instance?")) {
container.parentNode.removeChild(container);
delete ExpandableCards[exampleId];
}
}
});
header.append(statusIndicator, removeButton);
return header;
}
/**
* Creates the main body section with controls and output
*/
function createExampleBody(exampleId, iframe) {
var body = newElement("div", { classList: "example-body" });
// Left panel - Controls
var controlsContainer = createControlsPanel(exampleId, iframe);
// Right panel - Output
var outputContainer = createOutputPanel(exampleId, iframe);
// Bottom panel - Stream data
var streamDataLogs = newElement("div", {
classList: "stream-data-logs",
id: `stream-data-${exampleId}`
});
body.append(controlsContainer, outputContainer, streamDataLogs);
return body;
}
/**
* Creates the controls panel with organized API sections
*/
function createControlsPanel(exampleId, iframe) {
var controlsContainer = newElement("div", { classList: "controls" });
// Add quick access section
var quickAccess = createQuickAccessSection(exampleId, iframe);
controlsContainer.appendChild(quickAccess);
// Add API sections
var iframeSection = createAPISection(
IFRAME_API,
{
id: exampleId,
guestId: 'iframe',
title: 'IFRAME API',
description: 'Commands exclusive to the IFRAME API for direct control',
iframe: iframe
}
);
var companionSection = createAPISection(
COMPANION_API,
{
id: exampleId,
guestId: 'companion',
title: 'HTTP/WSS API',
description: 'Companion.Ninja API commands available via iframe',
info: '<a href="https://github.com/steveseguin/Companion-Ninja#api-commands" target="_blank">📚 Full API Documentation</a>',
iframe: iframe
}
);
// Add guest targeting section
var guestTargeting = createGuestTargetingSection(exampleId, iframe, controlsContainer);
controlsContainer.append(
iframeSection.header,
iframeSection.content,
companionSection.header,
companionSection.content,
guestTargeting
);
return controlsContainer;
}
/**
* Creates a quick access section for common commands
*/
function createQuickAccessSection(exampleId, iframe) {
var section = newElement("div", { classList: "quick-access" });
section.innerHTML = `
<h3>🚀 Quick Actions</h3>
<div class="quick-buttons">
<button onclick="postToFrame('${exampleId}', { mic: 'toggle' })">🎤 Toggle Mic</button>
<button onclick="postToFrame('${exampleId}', { camera: 'toggle' })">📹 Toggle Camera</button>
<button onclick="postToFrame('${exampleId}', { speaker: 'toggle' })">🔊 Toggle Speaker</button>
<button onclick="postToFrame('${exampleId}', { getStreamIDs: true })">📋 List Streams</button>
<button onclick="postToFrame('${exampleId}', { getStats: true })">📊 Get Stats</button>
<button onclick="postToFrame('${exampleId}', { getDeviceList: true })">🎛️ List Devices</button>
</div>
`;
return section;
}
/**
* Creates the output panel with iframe and logs
*/
function createOutputPanel(exampleId, iframe) {
var outputContainer = newElement("div", { classList: "output-container" });
// Add iframe
outputContainer.appendChild(iframe);
// Add tabs for different log types
var tabsContainer = createLogTabs(exampleId);
outputContainer.appendChild(tabsContainer);
// Add custom post message input
var customPost = createCustomPostSection(exampleId, iframe);
outputContainer.appendChild(customPost);
return outputContainer;
}
/**
* Creates tabbed log interface
*/
function createLogTabs(exampleId) {
var tabsContainer = newElement("div", { classList: "tabs-container" });
var tabs = newElement("div", { classList: "tabs" });
tabs.innerHTML = `
<button class="tab active" onclick="switchTab('${exampleId}', 'all')">All Logs</button>
<button class="tab" onclick="switchTab('${exampleId}', 'events')">Events</button>
<button class="tab" onclick="switchTab('${exampleId}', 'commands')">Commands</button>
<button class="tab" onclick="switchTab('${exampleId}', 'stats')">Stats</button>
`;
var logContainer = newElement("div", {
id: `${exampleId}-logs`,
classList: "main-log active-tab-content",
dataset: { tab: "all" }
});
tabsContainer.append(tabs, logContainer);
return tabsContainer;
}
/**
* Creates custom post message section with JSON validation
*/
function createCustomPostSection(exampleId, iframe) {
var container = newElement("div", { classList: "custom-post" });
var input = newElement("textarea", {
classList: "custom-post-input",
placeholder: "Enter custom JSON message (e.g., { \"action\": \"value\" })",
rows: 3
});
var helpText = newElement("span", {
classList: "help-text",
innerHTML: "💡 Tip: Use Ctrl+Enter to send"
});
var sendButton = newElement("button", {
innerText: "📤 SEND JSON",
onclick: () => sendCustomMessage(exampleId, input, iframe)
});
// Add keyboard shortcut
input.onkeydown = function(e) {
if (e.ctrlKey && e.key === 'Enter') {
sendCustomMessage(exampleId, input, iframe);
}
};
container.append(input, helpText, sendButton);
return container;
}
/**
* Sends custom JSON message with validation
*/
function sendCustomMessage(exampleId, input, iframe) {
try {
const message = JSON.parse(input.value);
postToFrame(exampleId, message, iframe);
input.value = ''; // Clear on success
input.classList.remove('error');
} catch (error) {
input.classList.add('error');
logOutput(`❌ JSON Error: ${error.message}`, {
exampleId: exampleId,
classList: "error-log"
});
}
}
/**
* Creates an API section with collapsible commands
*/
function createAPISection(api, config) {
var sectionId = `${config.id}-${config.guestId}`;
var header = newElement("div", { classList: "api-section-header" });
var toggleBtn = newElement("button", {
innerText: "▶ Show",
onclick: function() {
toggleSection(config.id, config.guestId, this);
}
});
var headerText = newElement("div");
headerText.innerHTML = `
<h3>${config.title}</h3>
<p><em>${config.description}</em></p>
${config.info || ''}
`;
header.append(headerText, toggleBtn);
var content = newElement("div", {
classList: "hidden",
id: sectionId
});
// Group commands by category
var categorizedCommands = categorizeCommands(api);
Object.entries(categorizedCommands).forEach(([category, commands]) => {
var categoryDiv = newElement("div", { classList: "command-category" });
categoryDiv.innerHTML = `<h4>📁 ${category}</h4>`;
Object.entries(commands).forEach(([name, data]) => {
var commandDiv = createCommandInterface(name, data, config.iframe, config.id);
categoryDiv.appendChild(commandDiv);
});
content.appendChild(categoryDiv);
});
return { header, content };
}
/**
* Categorizes commands for better organization
*/
function categorizeCommands(api) {
const categories = {
'Audio Controls': ['mic', 'mute', 'speaker', 'volume', 'panning', 'audiobitrate', 'targetAudioBitrate', 'PPT'],
'Video Controls': ['camera', 'bitrate', 'targetBitrate', 'scale', 'keyframe', 'forceKeyframe'],
'Recording': ['record'],
'Communication': ['sendChat', 'sendMessage', 'sendRequest'],
'System': ['reload', 'close', 'hangup'],
'Information': ['getStats', 'getStreamIDs', 'getDeviceList', 'getDetailedState', 'getLoudness'],
'Layout': ['add', 'remove', 'layout', 'style'],
'Groups': ['group', 'groups', 'groupView'],
'Other': []
};
const categorized = {};
Object.keys(categories).forEach(cat => categorized[cat] = {});
Object.entries(api).forEach(([name, data]) => {
let placed = false;
for (const [category, commands] of Object.entries(categories)) {
if (commands.includes(name)) {
categorized[category][name] = data;
placed = true;
break;
}
}
if (!placed) {
categorized['Other'][name] = data;
}
});
// Remove empty categories
Object.keys(categorized).forEach(cat => {
if (Object.keys(categorized[cat]).length === 0) {
delete categorized[cat];
}
});
return categorized;
}
/**
* Creates command interface with proper controls
*/
function createCommandInterface(name, data, iframe, exampleId) {
var container = newElement("div", { classList: "command-container" });
var header = newElement("h5", { innerHTML: name });
container.appendChild(header);
if (data.options) {
data.options.forEach((option, i) => {
var button = newElement("button", {
innerHTML: data.labels?.[i] || JSON.stringify(option),
onclick: () => postToFrame(exampleId, data.result(option), iframe)
});
container.appendChild(button);
});
}
if (data.input) {
var inputWrapper = newElement("div", { classList: "input-wrapper" });
var input = newElement("input", data.input);
if (input.type === 'range') {
var valueDisplay = newElement("span", {
classList: "range-value",
innerText: input.value
});
input.oninput = function() {
valueDisplay.innerText = this.value;
postToFrame(exampleId, data.result(this.value), iframe);
};
inputWrapper.append(input, valueDisplay);
} else {
var sendBtn = newElement("button", {
innerText: "Send",
onclick: () => postToFrame(exampleId, data.result(input.value), iframe)
});
inputWrapper.append(input, sendBtn);
}
container.appendChild(inputWrapper);
}
return container;
}
/**
* Creates guest targeting section
*/
function createGuestTargetingSection(exampleId, iframe, controlsContainer) {
var section = newElement("div", { classList: "target-guest" });
section.innerHTML = `
<h3>🎯 Target Guest Commands</h3>
<p><em>Directors can target commands to individual guests</em></p>
`;
var targetType = newElement("select", {
id: `${exampleId}-target-type`
});
targetType.innerHTML = `
<option value="slot">By Slot Number</option>
<option value="streamID">By Stream ID</option>
<option value="UUID">By UUID</option>
`;
var targetValue = newElement("input", {
id: `${exampleId}-target-value`,
type: "text",
placeholder: "Enter target value"
});
var generateBtn = newElement("button", {
innerText: "Generate Guest Commands",
onclick: () => {
const type = targetType.value;
const value = targetValue.value;
if (!value) {
alert("Please enter a target value");
return;
}
generateGuestCommands(exampleId, type, value, iframe, controlsContainer);
}
});
section.append(targetType, targetValue, generateBtn);
return section;
}
/**
* Generates guest-specific commands
*/
function generateGuestCommands(exampleId, targetType, targetValue, iframe, container) {
const guestId = `guest-${targetType}-${targetValue}`;
if (document.getElementById(`${exampleId}-${guestId}`)) {
alert("Commands for this target already generated");
return;
}
const guestAPI = guestTargetedAPI(targetValue);
const section = createAPISection(guestAPI, {
id: exampleId,
guestId: guestId,
title: `Guest Commands: ${targetType} = ${targetValue}`,
description: `Target specific commands to ${targetType}: ${targetValue}`,
iframe: iframe
});
container.append(section.header, section.content);
}
/**
* Sets up message listener for iframe communication
*/
function setupMessageListener(iframe, exampleId) {
// Media handling for frame capture
const media = {
tracks: {},
streams: {}
};
// Event listener
const messageHandler = function(e) {
if (e.source !== iframe.contentWindow) return;
// Update status indicator
updateConnectionStatus(exampleId, "🟢 Connected");
if (typeof e.data !== "object") {
console.log(e.data);
return;
}
// Store in history
MessageHistory.push({
timestamp: new Date(),
exampleId: exampleId,
data: e.data
});
if (MessageHistory.length > MAX_HISTORY) {
MessageHistory.shift();
}
// Handle different message types
handleIncomingMessage(e.data, exampleId, media);
};
window.addEventListener("message", messageHandler);
window.addEventListener("messageerror", e => console.error(e));
}
/**
* Handles incoming messages from iframe
*/
function handleIncomingMessage(data, exampleId, media) {
// Handle frame capture
if (data.frame) {
handleFrameCapture(data, exampleId, media);
return;
}
// Log based on message type
let logType = "event";
let formatted = "";
// Handle specific message types
if (data.action) {
handleActionMessage(data, exampleId);
return;
}
if (data.stats) {
handleStatsMessage(data.stats, exampleId);
return;
}
if (data.streamIDs) {
handleStreamIDsMessage(data.streamIDs, exampleId);
return;
}
if (data.deviceList) {
handleDeviceListMessage(data.deviceList, exampleId);
return;
}
if (data.loudness) {
handleLoudnessMessage(data.loudness, exampleId);
return;
}
if (data.sensors) {
handleSensorsMessage(data.sensors, exampleId);
return;
}
// Default logging
console.log(data);
logOutput(`📥 ${JSON.stringify(data, null, 2)}`, {
exampleId: exampleId,
type: "event"
});
}
/**
* Handles frame capture for video/audio
*/
function handleFrameCapture(data, exampleId, media) {
if (!media.tracks[data.trackID]) {
media.tracks[data.trackID] = {};
media.tracks[data.trackID].generator = new MediaStreamTrackGenerator({kind: data.kind});
media.tracks[data.trackID].stream = new MediaStream([media.tracks[data.trackID].generator]);
media.tracks[data.trackID].frameWriter = media.tracks[data.trackID].generator.writable.getWriter();
media.tracks[data.trackID].frameWriter.write(data.frame);
if (!media.streams[data.streamID]) {
const video = createVideoElement(data.streamID);
video.srcObject = media.tracks[data.trackID].stream;
document.getElementById(`${exampleId}-logs`).appendChild(video);
media.streams[data.streamID] = video;
} else {
updateVideoStream(media.streams[data.streamID], media.tracks[data.trackID].stream, data.kind);
}
} else {
media.tracks[data.trackID].frameWriter.write(data.frame);
}
}
/**
* Creates video element for captured streams
*/
function createVideoElement(streamID) {
const video = document.createElement("video");
video.id = `video_${streamID}`;
video.autoplay = true;
video.muted = true;
video.controls = true;
video.style.maxWidth = "300px";
return video;
}
/**
* Updates video stream tracks
*/
function updateVideoStream(video, stream, kind) {
if (kind === "video") {
video.srcObject.getVideoTracks().forEach(trk => {
video.srcObject.removeTrack(trk);
});
} else if (kind === "audio") {
video.srcObject.getAudioTracks().forEach(trk => {
video.srcObject.removeTrack(trk);
});
}
stream.getTracks().forEach(trk => {
video.srcObject.addTrack(trk);
});
}
/**
* Handles action messages
*/
function handleActionMessage(data, exampleId) {
const actionHandlers = {
"guest-connected": () => {
ActiveConnections.set(data.streamID, data.value);
logOutput(`✅ Guest connected: ${data.streamID} (${data.value?.label || 'No label'})`, {
exampleId: exampleId,
type: "event",
classList: "success-log"
});
},
"guest-disconnected": () => {
ActiveConnections.delete(data.streamID);
logOutput(`❌ Guest disconnected: ${data.streamID}`, {
exampleId: exampleId,
type: "event",
classList: "error-log"
});
},
"director-connected": () => {
logOutput(`👑 Director connected`, {
exampleId: exampleId,
type: "event",
classList: "success-log"
});
},
"image-frame-capture": () => {
const img = document.createElement("img");
img.src = URL.createObjectURL(data.value.imageData);
img.style.maxWidth = "260px";
document.getElementById(`${exampleId}-logs`).appendChild(img);
}
};
const handler = actionHandlers[data.action];
if (handler) {
handler();
} else if (data.action !== "loudness" && data.action !== "view-stats-updated") {
logOutput(`📌 Action: ${data.action}${data.value ? ` - ${JSON.stringify(data.value)}` : ''}`, {
exampleId: exampleId,
type: "event"
});
}
}
/**
* Handles stats messages
*/
function handleStatsMessage(stats, exampleId) {
let formatted = `📊 <strong>Connection Stats</strong><br/>`;
formatted += `Inbound: ${stats.total_inbound_connections} | Outbound: ${stats.total_outbound_connections}<br/>`;
for (const streamID in stats.inbound) {
formatted += `<br/><strong>Stream: ${streamID}</strong><br/>`;
formatted += formatObject(stats.inbound[streamID]);
}
logOutput(formatted, {
exampleId: exampleId,
type: "stats"
});
}
/**
* Handles stream IDs message
*/
function handleStreamIDsMessage(streamIDs, exampleId) {
let formatted = `📋 <strong>Active Streams</strong><br/>`;
for (const [id, label] of Object.entries(streamIDs)) {
formatted += `${id}: ${label || 'No label'}<br/>`;
}
logOutput(formatted, {
exampleId: exampleId,
type: "event"
});
}
/**
* Handles device list message
*/
function handleDeviceListMessage(deviceList, exampleId) {
let formatted = `🎛️ <strong>Available Devices</strong><br/>`;
const grouped = deviceList.reduce((acc, device) => {
if (!acc[device.kind]) acc[device.kind] = [];
acc[device.kind].push(device);
return acc;
}, {});
for (const [kind, devices] of Object.entries(grouped)) {
formatted += `<br/><strong>${kind}:</strong><br/>`;
devices.forEach(device => {
formatted += `${device.label || 'Unnamed device'}<br/>`;
});
}
logOutput(formatted, {
exampleId: exampleId,
type: "event"
});
}
/**
* Handles loudness data
*/
function handleLoudnessMessage(loudness, exampleId) {
const container = document.getElementById(`stream-data-${exampleId}`);
let loudnessDiv = document.getElementById(`${exampleId}-loudness`);
if (!loudnessDiv) {
loudnessDiv = newElement("div", {
id: `${exampleId}-loudness`,
classList: "loudness-display"
});
container.appendChild(loudnessDiv);
}
let html = `<h4>🔊 Audio Levels</h4>`;
for (const [streamID, level] of Object.entries(loudness)) {
const barWidth = Math.min(100, Math.max(0, (level + 40) * 2.5));
html += `
<div class="loudness-meter">
<span>${streamID}:</span>
<div class="loudness-bar">
<div class="loudness-fill" style="width: ${barWidth}%"></div>
</div>
<span>${level.toFixed(1)} dB</span>
</div>
`;
}
loudnessDiv.innerHTML = html;
}
/**
* Handles sensor data
*/
function handleSensorsMessage(sensors, exampleId) {
const container = document.getElementById(`stream-data-${exampleId}`);
let sensorsDiv = document.getElementById(`${exampleId}-sensors`);
if (!sensorsDiv) {
sensorsDiv = newElement("div", {
id: `${exampleId}-sensors`,
classList: "sensors-display"
});
container.appendChild(sensorsDiv);
}
let html = `<h4>📡 Sensor Data</h4>`;
const sensorTypes = {
lin: "Linear",
acc: "Acceleration",
gyro: "Gyroscope",
mag: "Magnetometer"
};
for (const [type, label] of Object.entries(sensorTypes)) {
if (sensors[type]) {
html += `<strong>${label}:</strong><br/>`;
for (const [axis, value] of Object.entries(sensors[type])) {
html += `${axis}: ${(Math.round(value * 100000) / 100000)}<br/>`;
}
}
}
sensorsDiv.innerHTML = html;
}
// Helper functions
/**
* Posts a message to the iframe
*/
function postToFrame(exampleId, message, iframe) {
if (!iframe) {
const container = document.getElementById(exampleId);
iframe = container?.querySelector('iframe');
}
if (!iframe) {
console.error("Iframe not found for example:", exampleId);
return;
}
iframe.contentWindow.postMessage(message, '*');
logOutput(`📤 Sent: ${JSON.stringify(message)}`, {
exampleId: exampleId,
type: "command",
classList: "post-log"
});
}
// Make function globally accessible
window.postToFrame = postToFrame;
/**
* Logs output to the appropriate window
*/
function logOutput(content, options = {}) {
const logWindow = document.getElementById(`${options.exampleId}-logs`);
if (!logWindow) return;
const entry = newElement("div", {
classList: options.classList || "log-entry",
innerHTML: `<span class="timestamp">${new Date().toLocaleTimeString()}</span> ${content}`
});
if (options.type) {
entry.dataset.logType = options.type;
}
logWindow.appendChild(entry);
logWindow.scrollTop = logWindow.scrollHeight;
// Limit log entries
while (logWindow.children.length > 100) {
logWindow.removeChild(logWindow.firstChild);
}
}
/**
* Updates connection status indicator
*/
function updateConnectionStatus(exampleId, status) {
const indicator = document.getElementById(`status-${exampleId}`);
if (indicator) {
indicator.innerHTML = status;
}
}
/**
* Toggles section visibility
*/
function toggleSection(exampleId, sectionId, button) {
ExpandableCards[exampleId][sectionId] = !ExpandableCards[exampleId][sectionId];
const section = document.getElementById(`${exampleId}-${sectionId}`);
if (ExpandableCards[exampleId][sectionId]) {
section.classList.remove("hidden");
button.innerText = "▼ Hide";
} else {
section.classList.add("hidden");
button.innerText = "▶ Show";
}
}
/**
* Switches between log tabs
*/
function switchTab(exampleId, tabName) {
const container = document.getElementById(`${exampleId}-logs`).parentElement;
// Update tab buttons
container.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');
// Filter logs
const logWindow = document.getElementById(`${exampleId}-logs`);
logWindow.querySelectorAll('.log-entry').forEach(entry => {
if (tabName === 'all' || entry.dataset.logType === tabName) {
entry.style.display = 'block';
} else {
entry.style.display = 'none';
}
});
}
// Make function globally accessible
window.switchTab = switchTab;
/**
* Formats an object for display
*/
function formatObject(obj, indent = "") {
let formatted = "";
for (const [key, value] of Object.entries(obj)) {
if (typeof value === "object" && value !== null) {
formatted += `${indent}<strong>${key}:</strong><br/>`;
formatted += formatObject(value, indent + "&nbsp;&nbsp;");
} else {
formatted += `${indent}<strong>${key}:</strong> ${value}<br/>`;
}
}
return formatted;
}
/**
* Helper to create elements with attributes
*/
function newElement(type, attributes) {
const el = document.createElement(type);
for (const attr in attributes) {
if (attr === 'dataset') {
Object.assign(el.dataset, attributes[attr]);
} else {
el[attr] = attributes[attr];
}
}
return el;
}
/**
* Generates a random ID
*/
function generateId() {
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10);
}
/**
* Adds URL parameters based on checkboxes
*/
function addUrlParams(_url, paramCheckboxes) {
let url = _url || "./";
for (const param of paramCheckboxes) {
if (document.getElementById(param)?.checked) {
url += url.includes("?") ? '&' : '?';
url += param;
}
}
return url;
}
/**
* Global error handler
*/
window.addEventListener('error', function(e) {
console.error('Global error:', e);
});
/**
* Initialize on page load
*/
window.addEventListener('DOMContentLoaded', function() {
// Add example URL if empty
const viewlink = document.getElementById('viewlink');
if (!viewlink.value) {
viewlink.placeholder = "e.g., https://vdo.ninja/?view=teststream or just 'vdo.ninja/?room=myroom'";
}
// Add keyboard shortcuts
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 'k') {
e.preventDefault();
document.querySelector('.custom-post-input')?.focus();
}
});
});
</script>
</head>
<body>
<div class="header-section">
<h1>🚀 VDO.Ninja IFRAME API - Interactive Developer Console</h1>
<p>Test and explore the VDO.Ninja IFRAME API with this interactive tool. Enter a VDO.Ninja URL below to get started.</p>
</div>
<div class="url-input-section">
<input placeholder="Enter a VDO.Ninja View URL (e.g., vdo.ninja/?view=abc123)" id="viewlink" />
<button onclick="loadIframe();" class="primary-button"> ADD IFRAME</button>
<div class="url-options">
<label><input type="checkbox" id="cleanoutput" checked> Clean Output</label>
<label><input type="checkbox" id="transparent"> Transparent</label>
<label><input type="checkbox" id="hidemenu" checked> Hide Menu</label>
</div>
</div>
<div class="info-section">
<details>
<summary>📖 Quick Start Guide</summary>
<div class="guide-content">
<h3>Getting Started:</h3>
<ol>
<li>Enter a VDO.Ninja URL in the input field above</li>
<li>Click "ADD IFRAME" to create a new instance</li>
<li>Use the control panel to send commands</li>
<li>Monitor responses in the log window</li>
</ol>
<h3>Tips:</h3>
<ul>
<li>Use <kbd>Ctrl</kbd>+<kbd>K</kbd> to focus the JSON input</li>
<li>Use <kbd>Ctrl</kbd>+<kbd>Enter</kbd> in JSON input to send</li>
<li>Click on log entries to copy their content</li>
<li>Check the browser console for detailed debugging</li>
</ul>
<h3>Common Use Cases:</h3>
<ul>
<li><strong>Testing:</strong> Verify API commands work correctly</li>
<li><strong>Debugging:</strong> Monitor events and responses</li>
<li><strong>Development:</strong> Prototype integrations</li>
<li><strong>Learning:</strong> Explore API capabilities</li>
</ul>
</div>
</details>
<details>
<summary>🔗 Useful Links</summary>
<div class="links-content">
<a href="https://github.com/steveseguin/vdo.ninja" target="_blank">📦 VDO.Ninja GitHub</a>
<a href="https://docs.vdo.ninja" target="_blank">📚 Documentation</a>
<a href="https://discord.vdo.ninja" target="_blank">💬 Discord Community</a>
<a href="https://github.com/steveseguin/Companion-Ninja" target="_blank">🤖 Companion API</a>
</div>
</details>
</div>
<div id="container"></div>
<style>
/* Additional developer-friendly styles */
.header-section {
background: #1a1a2e;
color: white;
padding: 20px;
margin: -10px -10px 20px -10px;
text-align: center;
}
.header-section h1 {
margin: 0 0 10px 0;
font-weight: 300;
}
.url-input-section {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.url-options {
margin-top: 10px;
display: flex;
gap: 20px;
}
.url-options label {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
}
.primary-button {
background: #4d66a8;
color: white;
font-weight: bold;
padding: 10px 20px;
}
.info-section {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.info-section details {
flex: 1;
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.info-section summary {
cursor: pointer;
font-weight: bold;
padding: 5px;
}
.guide-content, .links-content {
margin-top: 15px;
}
.links-content {
display: flex;
flex-direction: column;
gap: 10px;
}
.links-content a {
color: #4d66a8;
text-decoration: none;
padding: 5px;
}
.links-content a:hover {
text-decoration: underline;
}
kbd {
background: #f0f0f0;
border: 1px solid #ccc;
padding: 2px 5px;
border-radius: 3px;
font-family: monospace;
font-size: 0.9em;
}
.status-indicator {
font-size: 14px;
padding: 5px 10px;
background: rgba(255,255,255,0.2);
border-radius: 15px;
}
.quick-access {
padding: 10px;
border-bottom: 1px solid #ccc;
}
.quick-buttons {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 10px;
}
.quick-buttons button {
font-size: 12px;
padding: 8px 12px;
}
.tabs-container {
border-top: 1px solid #6e6e6e;
}
.tabs {
display: flex;
background: #1a1a2e;
border-bottom: 1px solid #6e6e6e;
}
.tab {
background: transparent;
color: #aaa;
border: none;
padding: 10px 20px;
cursor: pointer;
border-radius: 0;
margin: 0;
}
.tab:hover {
background: rgba(255,255,255,0.1);
}
.tab.active {
background: #0b0e15;
color: white;
border-bottom: 2px solid #4d66a8;
}
.command-category {
margin-bottom: 15px;
padding: 10px;
background: #f5f5f5;
border-radius: 5px;
}
.command-category h4 {
margin-top: 0;
color: #4d66a8;
}
.command-container {
margin-bottom: 10px;
padding: 8px;
background: white;
border-radius: 3px;
}
.command-container h5 {
margin: 0 0 5px 0;
font-size: 14px;
}
.input-wrapper {
display: flex;
align-items: center;
gap: 10px;
margin-top: 5px;
}
.range-value {
min-width: 50px;
font-family: monospace;
}
.error {
border-color: #ff4444 !important;
}
.error-log {
color: #ff4444;
}
.success-log {
color: #44ff44;
}
.timestamp {
color: #666;
font-size: 11px;
margin-right: 8px;
}
.log-entry {
margin: 2px 0;
padding: 4px;
border-radius: 3px;
cursor: pointer;
word-break: break-word;
}
.log-entry:hover {
background: rgba(255,255,255,0.05);
}
.loudness-display, .sensors-display {
padding: 15px;
background: rgba(255,255,255,0.05);
margin: 10px;
border-radius: 5px;
}
.loudness-meter {
display: flex;
align-items: center;
gap: 10px;
margin: 5px 0;
}
.loudness-bar {
flex: 1;
height: 20px;
background: rgba(255,255,255,0.1);
border-radius: 10px;
overflow: hidden;
}
.loudness-fill {
height: 100%;
background: linear-gradient(to right, #44ff44, #ffff44, #ff4444);
transition: width 0.2s;
}
.help-text {
font-size: 12px;
color: #666;
display: block;
margin-top: 5px;
}
.custom-post textarea {
resize: vertical;
font-size: 13px;
}
</style>
</body>
</html>