Create custom drawing app

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:

  1. Create a drawing canvas on your page

  2. Capture drawing events

  3. Format the data appropriately

  4. Send the data to peers

  5. Process and render received drawing data

Here's a simplified example:

// 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

  1. Data Structure: Use a clear, consistent data structure for your payloads

  2. Normalization: Normalize canvas coordinates (0-1 range) to ensure consistent display across different screen sizes

  3. Throttling: Consider throttling frequent events like mouse movements to reduce data transmission

  4. Error Handling: Always include try/catch blocks when sending or processing data

  5. State Synchronization: When new peers join, send them the current state

  6. UUID vs StreamID: Use UUID for reliable targeting; StreamIDs change when connections restart

  7. Connection Status: Monitor connection and disconnection events to maintain a list of active peers

Common Types of Data to Send

  • Drawing Paths: Arrays of points representing drawing strokes

  • Commands: Clear, undo, change color, change brush size

  • Annotations: Text or shapes to overlay on videos

  • Control Signals: Camera directions, audio levels, recording commands

  • Chat Messages: Text messages between users

  • Sensor Data: Device orientation, location, acceleration

Troubleshooting

  • Data Not Arriving: Check that you're using the correct UUID or streamID

  • Timing Issues: Ensure your iframe is fully loaded before sending messages

  • Cross-Origin Issues: Make sure your security settings allow communication

  • Format Errors: Verify your data structure matches what receivers expect

  • Performance Problems: Large data payloads can cause lag; consider optimizing

By following this guide, you should be able to implement custom drawing tools or any other data-sharing features using VDO.Ninja's P2P data channels.

Last updated

Was this helpful?