mirror of
https://github.com/SrIzan10/vdo.ninja.git
synced 2026-05-01 11:05:24 +00:00
1299 lines
33 KiB
HTML
1299 lines
33 KiB
HTML
<!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 + " ");
|
||
} 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> |