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