Files
archived-vdo.ninja/whiteboard.html
2025-01-10 18:12:23 -05:00

1011 lines
31 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Free, open-source live streaming whiteboard with secure E2EE P2P sharing via VDO.Ninja and WHIP support">
<meta name="keywords" content="whiteboard, streaming, VDO.Ninja, WHIP, e2ee, p2p, drawing, collaboration">
<meta name="author" content="VDO.Ninja">
<meta property="og:title" content="VDO.Ninja Whiteboard">
<meta property="og:description" content="Free, open-source live streaming whiteboard with secure E2EE P2P sharing">
<meta property="og:image" content="https://vdo.ninja/media/logo.png">
<meta property="og:url" content="https://vdo.ninja/whiteboard">
<link rel="icon" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTQgNGgyNHYyNEg0eiIvPjxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik02IDZoMjB2MjBINnoiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNOCA4aDE2djRsLTQgOHY0SDEydi00TDggMTJ6Ii8+PC9zdmc+">
<title>Whiteboard - Live Streaming Collaboration Tool</title>
<style>
html, body {
height: 100%;
margin: 0;
overflow: hidden;
}
body {
background: #1a1a1a;
color: #fff;
padding: 10px;
font-family: system-ui;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.whiteboard-container {
flex: 1;
background: #2a2a2a;
padding: 10px;
border-radius: 8px;
display: flex;
flex-direction: column;
position: relative;
min-height: 0;
}
#canvas {
cursor: crosshair;
width: auto;
height: auto;
background: white;
border-radius: 4px;
max-width: min(100%, calc((100vh - 140px) * 16/9));
max-height: calc(100vh - 140px);
margin: auto;
display: block;
}
.controls {
display: flex;
gap: 15px;
margin-bottom: 10px;
align-items: center;
flex-wrap: wrap;
margin: auto;
padding-top: 40px;
}
button {
background: #404040;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.controls button {
position: relative;
min-width: 80px;
}
.controls button.active {
background: #606060;
outline: 2px solid #898989;
outline-offset: -2px;
}
button:hover { background: #505050; }
input[type="range"] { accent-color: #898989; }
.text-input {
position: absolute; /* Changed from fixed to absolute */
background: white;
border: 1px solid #000;
outline: none;
font-family: Arial;
padding: 2px;
margin: 0;
display: none;
color: black;
min-width: 100px;
z-index: 1000;
resize: none;
white-space: nowrap;
overflow: hidden;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
overflow-y: auto;
}
.modal {
background: #2a2a2a;
padding: 20px;
border-radius: 8px;
width: 90%;
max-width: 500px;
color: white;
margin: 20px auto;
}
.modal-buttons button {
flex: 1;
position: relative;
}
.modal-buttons button.active {
outline: 2px solid #898989;
box-shadow: 0 0 10px rgba(137, 137, 137, 0.3);
}
.modal-content {
display: none;
}
.modal-content.active {
display: block;
}
.mode-description {
text-align: center;
margin: 15px 0;
color: #898989;
}
.modal-content {
display: none;
}
.modal-content.active {
display: block;
}
.modal input[type="text"], .modal input[type="url"], .modal input[type="password"] {
width: calc(100% - 20px);
padding: 8px;
margin: 8px 0;
border: 1px solid #404040;
border-radius: 4px;
background: #1a1a1a;
color: white;
}
.modal-buttons {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.input-group {
margin-bottom: 15px;
}
.input-group label {
display: block;
margin-bottom: 5px;
}
.view-link-container {
position: absolute;
top: 10px;
left: 10px;
right: 10px;
background: #2a2a2a;
padding: 8px;
border-radius: 4px;
display: none;
align-items: center;
gap: 8px;
z-index: 100;
}
.view-link-container input {
background: #1a1a1a;
color: white;
border: 1px solid #404040;
padding: 8px;
border-radius: 4px;
flex: 1;
min-width: 0;
font-size: 14px;
}
.view-link-container button {
white-space: nowrap;
}
.view-link-container button::after {
content: " View Link";
display: none;
}
@media (max-width: 1600px) {
.view-link-container input {
display: none;
}
.view-link-container button::after {
display: inline;
}
}
@media (min-width: 768px) {
.view-link-container {
left: auto;
width: auto;
max-width: 400px;
}
.controls {
padding-top: 10px;
}
}
@media (max-width: 480px) {
.controls {
padding-top: 50px;
}
.view-link-container {
flex-direction: column;
align-items: stretch;
}
.modal {
margin: 10px;
width: calc(100% - 20px);
}
}
.font-size-select {
background: #404040;
color: white;
border: none;
padding: 8px;
border-radius: 4px;
cursor: pointer;
display: none;
}
.font-size-select.active {
display: block;
}
</style>
</head>
<body>
<div class="whiteboard-container">
<div class="controls">
<input type="color" id="colorPicker" value="#000000">
<input type="range" id="brushSize" min="1" max="50" value="5">
<button id="brush" class="active">Brush</button>
<button id="text" title="Click away to commit text">Text</button>
<select id="fontSize" class="font-size-select">
<option value="12">12px</option>
<option value="14">14px</option>
<option value="16">16px</option>
<option value="20">20px</option>
<option value="24">24px</option>
<option value="32">32px</option>
<option value="48">48px</option>
<option value="72">72px</option>
<option value="96">96px</option>
</select>
<button id="eraser">Eraser</button>
<button id="fill">Fill</button>
<button id="clear">Clear</button>
</div>
<canvas id="canvas"></canvas>
</div>
<input type="text" class="text-input" id="textInput">
<div class="modal-overlay">
<div class="modal">
<h3 style="text-align: center; margin-bottom: 15px;">Whiteboard Publishing Options</h3>
<div class="modal-buttons">
<button onclick="showMode('vdo')" title="Peer-to-peer encrypted streaming via VDO.Ninja">VDO.Ninja</button>
<button onclick="showMode('whip')" title="Stream to any WHIP-compatible service">WHIP</button>
<button onclick="showMode('twitch')" title="Stream directly to Twitch">Twitch</button>
<button onclick="showMode('playground')" title="Local drawing only - no streaming">Playground</button>
</div>
<div class="mode-description" id="modeDescription"></div>
<div class="modal-content" id="vdo-content">
<div class="input-group">
<label>Stream ID (optional)</label>
<input type="text" id="pushId" placeholder="Leave empty for auto-generated ID">
</div>
<div class="input-group">
<label>Room Name (optional if Stream ID provided)</label>
<input type="text" id="roomName">
</div>
<div class="input-group">
<label>Password (optional)</label>
<input type="password" id="password">
</div>
<button onclick="handleVDO()">Start</button>
</div>
<div class="modal-content" id="twitch-content">
<div class="input-group">
<label>Twitch Stream Token</label>
<input type="password" id="twitchToken" required>
</div>
<button onclick="handleTwitch()">Start</button>
</div>
<div class="modal-content" id="whip-content">
<div class="input-group">
<label>WHIP URL</label>
<input type="url" id="whipUrl" required>
</div>
<div class="input-group">
<label>WHIP Token</label>
<input type="password" id="whipToken">
</div>
<button onclick="handleWhip()">Start</button>
</div>
<div class="modal-content" id="playground-content">
<p>Playground mode - no publishing.</p>
<button onclick="handlePlayground()">Start</button>
</div>
</div>
</div>
<div class="view-link-container" title="This view-link can be shared to let others see your canvas output as a streaming video">
<input type="text" id="viewLink" readonly>
<button onclick="copyViewLink()">Copy</button>
</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const colorPicker = document.getElementById('colorPicker');
const brushSize = document.getElementById('brushSize');
const eraser = document.getElementById('eraser');
const fill = document.getElementById('fill');
const clear = document.getElementById('clear');
let savedCanvasState = null;
let textPosition = { x: 0, y: 0 };
function createAndAppendIframe(config) {
if (config.mode === 'playground') return null;
let url = new URL("./index.html", window.location.href);
if (config.mode === 'vdo') {
if (config.room) url.searchParams.set("room", config.room);
if (config.push) url.searchParams.set("push", config.push);
if (config.password) url.searchParams.set("password", config.password);
} else if (config.mode === 'whip') {
url.searchParams.set("whippush", config.whipUrl);
if (config.whipToken) url.searchParams.set("whippushtoken", config.whipToken);
}
url.searchParams.set("framegrab", "");
url.searchParams.set("view", "");
const iframe = document.createElement("iframe");
iframe.style.width = "0";
iframe.style.height = "0";
iframe.src = url.toString()+window.location.search.replace("&","?");
iframe.onload = function(){
setTimeout(function(){
startStreaming();
},100);
}
document.body.appendChild(iframe);
return iframe;
}
function generatePushId() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
return Array.from({length: 8}, () => chars[Math.floor(Math.random() * chars.length)]).join('');
}
function showMode(mode) {
document.querySelectorAll('.modal-content').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.modal-buttons button').forEach(btn => btn.classList.remove('active'));
document.getElementById(`${mode}-content`).classList.add('active');
document.querySelector(`button[onclick="showMode('${mode}')"]`).classList.add('active');
const descriptions = {
'vdo': 'Stream securely peer-to-peer using VDO.Ninja',
'whip': 'Stream to any WHIP-compatible service',
'twitch': 'Stream directly to your Twitch channel',
'playground': 'Local drawing mode - no streaming'
};
localStorage.setItem('lastMode', mode);
document.getElementById('modeDescription').textContent = descriptions[mode];
}
function handleTwitch() {
const twitchToken = document.getElementById('twitchToken').value;
if (!twitchToken) {
alert('Twitch Stream Token is required');
return;
}
localStorage.setItem('lastTwitchToken', twitchToken);
const config = {
mode: 'whip',
whipUrl: 'https://twitch.vdo.ninja',
whipToken: twitchToken
};
window.iframe = createAndAppendIframe(config);
document.querySelector('.modal-overlay').style.display = 'none';
setTimeout(function(){startStreaming();},1000);
}
function handleVDO() {
const push = document.getElementById('pushId').value || generatePushId();
const room = document.getElementById('roomName').value;
const password = document.getElementById('password').value;
if (!push && !room) {
alert('Either Stream ID or Room Name is required');
return;
}
const config = {
mode: 'vdo',
push,
room,
password
};
const viewUrl = new URL("./", window.location.href);
if (push) viewUrl.searchParams.set("view", push);
if (room) viewUrl.searchParams.set("room", room);
if (config.push && config.room) viewUrl.searchParams.set("solo", "")
if (password) viewUrl.searchParams.set("password", password);
document.getElementById('viewLink').value = viewUrl.toString()+"&sharperscreen";
document.querySelector('.view-link-container').style.display = 'flex';
window.iframe = createAndAppendIframe(config);
document.querySelector('.modal-overlay').style.display = 'none';
}
function copyViewLink() {
const viewLink = document.getElementById('viewLink');
if (window.innerWidth <= 1600) {
// Create temporary input for copying when main input is hidden
const tempInput = document.createElement('input');
tempInput.value = viewLink.value;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
} else {
viewLink.select();
document.execCommand('copy');
}
}
function handleWhip() {
const whipUrl = document.getElementById('whipUrl').value;
const whipToken = document.getElementById('whipToken').value;
if (!whipUrl) {
alert('WHIP URL is required');
return;
}
localStorage.setItem('lastWhipUrl', whipUrl);
if (whipToken) localStorage.setItem('lastWhipToken', whipToken);
const config = {
mode: 'whip',
whipUrl,
whipToken
};
window.iframe = createAndAppendIframe(config);
document.querySelector('.modal-overlay').style.display = 'none';
setTimeout(function(){startStreaming();},1000);
}
function handlePlayground() {
window.iframe = null;
document.querySelector('.modal-overlay').style.display = 'none';
}
// Show modal on load instead of creating iframe immediately
window.addEventListener('load', () => {
if (window.location.search){
const getUrlParams = () => Object.fromEntries(new URLSearchParams(window.location.search));
const params = getUrlParams(); // {name: 'john', age: '25'}
const room = params.room || params.r || false;
const push = params.push || params.id || params.permaid || false;
const password = params.password || params.p || params.pw || false;
const whipUrl = params.whip || params.whipout || params.whippush || false;
const whipToken = params.whippushtoken || params.whipouttoken || params.whiptoken || params.token || false;
if (room || push){
const config = {
mode: 'vdo',
push,
room,
password
};
createAndAppendIframe(config);
return;
} else if (whipUrl){
const config = {
mode: 'whip',
whipUrl,
whipToken
};
createAndAppendIframe(config);
return
}
}
// Load saved credentials
const lastWhipUrl = localStorage.getItem('lastWhipUrl');
const lastWhipToken = localStorage.getItem('lastWhipToken');
const lastTwitchToken = localStorage.getItem('lastTwitchToken');
if (lastWhipUrl) document.getElementById('whipUrl').value = lastWhipUrl;
if (lastWhipToken) document.getElementById('whipToken').value = lastWhipToken;
if (lastTwitchToken) document.getElementById('twitchToken').value = lastTwitchToken;
document.querySelector('.modal-overlay').style.display = 'flex';
// Load last used mode if available
const lastMode = localStorage.getItem('lastMode');
showMode(lastMode || 'vdo');
});
let frameGenerator;
let useMediaTrackProcessor = true;
function resizeCanvas() {
const container = canvas.parentElement;
const maxWidth = container.clientWidth - 20;
const maxHeight = container.clientHeight - 60;
const aspectRatio = 16/9;
let width = maxWidth;
let height = width / aspectRatio;
if (height > maxHeight) {
height = maxHeight;
width = height * aspectRatio;
}
if (useMediaTrackProcessor){
canvas.width = Math.min(1920, Math.max(1280, width));
canvas.height = canvas.width * 9/16;
} else {
canvas.width = 1280;
canvas.height = canvas.width * 9/16;
}
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
//window.addEventListener('resize', resizeCanvas);
resizeCanvas();
let isDrawing = false;
let lastColor = '#000000';
let currentMode = 'brush';
const brush = document.getElementById('brush');
const text = document.getElementById('text');
const modes = {brush, eraser, fill, text};
const textInput = document.getElementById('textInput');
const fontSize = document.getElementById('fontSize');
function setMode(mode) {
currentMode = mode;
Object.values(modes).forEach(btn => btn.classList.remove('active'));
modes[mode].classList.add('active');
ctx.strokeStyle = mode === 'eraser' ? '#ffffff' : lastColor;
canvas.style.cursor = mode === 'text' ? 'text' : 'crosshair';
fontSize.classList.toggle('active', mode === 'text');
if (mode !== 'text') {
savedCanvasState = null;
commitText();
}
}
function commitText() {
if (textInput.value && savedCanvasState) {
// The final text is already drawn on the canvas
savedCanvasState = null;
}
textInput.value = '';
textInput.style.display = 'none';
}
brush.addEventListener('click', () => setMode('brush'));
eraser.addEventListener('click', () => setMode('eraser'));
fill.addEventListener('click', () => setMode('fill'));
text.addEventListener('click', () => setMode('text'));
document.addEventListener('blur', commitText);
textInput.addEventListener('keydown', (e) => {
e.stopPropagation();
if (e.key === 'Enter') {
commitText();
} else if (e.key === 'Escape') {
textInput.value = '';
textInput.style.display = 'none';
}
});
document.addEventListener('mousedown', (e) => {
if (e.target !== textInput && e.target !== canvas && textInput.style.display === 'block') {
commitText();
}
});
canvas.addEventListener('mousedown', (e) => {
if (currentMode === 'text') {
commitText();
const rect = canvas.getBoundingClientRect();
const canvasX = e.clientX - rect.left;
const canvasY = e.clientY - rect.top;
// Save canvas state and text position
savedCanvasState = ctx.getImageData(0, 0, canvas.width, canvas.height);
textPosition.x = (canvasX * (canvas.width / rect.width));
textPosition.y = (canvasY * (canvas.height / rect.height));
textInput.style.display = 'block';
textInput.style.left = (rect.left + canvasX) + 'px';
textInput.style.top = (rect.top + canvasY) + 'px';
textInput.style.color = lastColor;
textInput.style.fontSize = fontSize.value + 'px';
textInput.value = '';
textInput.focus();
e.preventDefault();
}
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * (canvas.width / rect.width);
const y = (e.clientY - rect.top) * (canvas.height / rect.height);
if (currentMode === 'fill') {
floodFill(Math.floor(x), Math.floor(y), lastColor);
return;
}
isDrawing = true;
ctx.beginPath();
ctx.moveTo(x, y);
});
textInput.addEventListener('input', () => {
if (!savedCanvasState) return;
// Restore the saved canvas state
ctx.putImageData(savedCanvasState, 0, 0);
if (textInput.value) {
const scaleFactor = canvas.width / canvas.getBoundingClientRect().width;
const adjustedFontSize = Math.floor(parseInt(fontSize.value) * scaleFactor);
ctx.font = `${adjustedFontSize}px Arial`;
ctx.fillStyle = lastColor;
ctx.textBaseline = 'top';
ctx.fillText(textInput.value, textPosition.x, textPosition.y);
}
});
function floodFill(startX, startY, fillColor) {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;
const startPos = (startY * canvas.width + startX) * 4;
const startR = pixels[startPos];
const startG = pixels[startPos + 1];
const startB = pixels[startPos + 2];
const fillR = parseInt(fillColor.slice(1,3), 16);
const fillG = parseInt(fillColor.slice(3,5), 16);
const fillB = parseInt(fillColor.slice(5,7), 16);
if (startR === fillR && startG === fillG && startB === fillB) return;
const stack = [[startX, startY]];
while(stack.length) {
const [x, y] = stack.pop();
const pos = (y * canvas.width + x) * 4;
if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) continue;
if (pixels[pos] !== startR || pixels[pos + 1] !== startG || pixels[pos + 2] !== startB) continue;
pixels[pos] = fillR;
pixels[pos + 1] = fillG;
pixels[pos + 2] = fillB;
pixels[pos + 3] = 255;
stack.push([x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]);
}
ctx.putImageData(imageData, 0, 0);
}
function draw(e) {
if (!isDrawing) return;
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * (canvas.width / rect.width);
const y = (e.clientY - rect.top) * (canvas.height / rect.height);
ctx.lineCap = 'round';
ctx.lineWidth = brushSize.value;
ctx.lineTo(x, y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x, y);
}
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', () => isDrawing = false);
canvas.addEventListener('mouseout', () => isDrawing = false);
colorPicker.addEventListener('input', () => {
lastColor = colorPicker.value;
if (currentMode !== 'eraser') {
ctx.strokeStyle = lastColor;
}
});
clear.addEventListener('click', () => {
resizeCanvas();
});
function oscillatorTimeout(callback, delay) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
const ctx = new AudioContext();
const oscillator = ctx.createOscillator();
const gain = ctx.createGain();
gain.gain.value = 0;
oscillator.connect(gain);
gain.connect(ctx.destination);
let rafId;
let startTime = performance.now();
let isDone = false;
const checkTime = (timestamp) => {
if (isDone) return;
if ((timestamp - startTime) >= delay) {
isDone = true;
oscillator.onended = () => {};
oscillator.disconnect();
gain.disconnect();
ctx.close();
cancelAnimationFrame(rafId);
callback();
return;
}
rafId = requestAnimationFrame(checkTime);
};
oscillator.onended = () => {
if (isDone) return;
isDone = true;
cancelAnimationFrame(rafId);
oscillator.disconnect();
gain.disconnect();
ctx.close();
callback();
};
oscillator.start();
oscillator.stop(ctx.currentTime + delay/1000);
rafId = requestAnimationFrame(checkTime);
return {
clear: () => {
isDone = true;
cancelAnimationFrame(rafId);
oscillator.onended = () => {};
oscillator.disconnect();
gain.disconnect();
ctx.close();
}
};
}
async function createCanvasStream() {
const stream = canvas.captureStream(60);
const tracks = stream.getVideoTracks();
const indicator = new Path2D();
indicator.arc(0, 0, 3, 0, Math.PI * 2);
indicator.moveTo(5, 0);
indicator.arc(0, 0, 5, 0, Math.PI * 2);
let rotation = 0;
const ROTATION_SPEED = 0.01;
const OPACITY_BASE = 0.15;
const PULSE_AMOUNT = 0.05;
function drawIndicator() {
const state = ctx.getImageData(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(canvas.width - 15, canvas.height - 15);
ctx.rotate(rotation);
const opacity = OPACITY_BASE + Math.sin(rotation) * PULSE_AMOUNT;
ctx.strokeStyle = `rgba(200, 200, 200, ${opacity})`;
ctx.lineWidth = 1;
ctx.stroke(indicator);
ctx.restore();
// Restore the previous state before the next frame
ctx.putImageData(state, 0, 0);
rotation += ROTATION_SPEED;
oscillatorTimeout(drawIndicator, 50);
//requestAnimationFrame(drawIndicator);
}
oscillatorTimeout(drawIndicator, 50);
//requestAnimationFrame(drawIndicator);
tracks.forEach(track => track.contentHint = 'detail');
return { stream, tracks };
}
async function startStreaming() {
const iframe = document.querySelector('iframe');
if (!iframe) return;
try {
if (useMediaTrackProcessor){
// Check if MediaStreamTrackProcessor are available
if (typeof MediaStreamTrackProcessor === 'function') {
const { tracks } = await createCanvasStream();
// Process each track using MediaStreamTrackProcessor
await Promise.all(tracks.map(async track => {
const processor = new MediaStreamTrackProcessor(track);
const reader = processor.readable.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
if (value) value.close();
break;
}
try {
iframe.contentWindow.postMessage({
type: 'canvas-frame',
frame: value
}, '*');
} finally {
value.close();
}
}
}));
return;
} else {
useMediaTrackProcessor = false;
}
}
} catch (e) {
console.error("MediaTrackProcessor method failed, will fallback to toDataURL:", e);
useMediaTrackProcessor = false;
canvas.width=1280;
canvas.height=720;
}
// If processor method not used or failed, fallback to toDataURL
if (!useMediaTrackProcessor) {
frameGenerator = setInterval(() => {
const imageData = canvas.toDataURL('image/webp');
iframe.contentWindow.postMessage({
type: 'canvas-frame',
frame: imageData
}, '*');
}, 1000/10); // 10 fps
}
}
function stopStreaming() {
if (frameGenerator) {
clearInterval(frameGenerator);
frameGenerator = null;
}
}
window.addEventListener('unload', stopStreaming);
///////// the following is a loopback webrtc trick to get chrome to not throttle this tab when not visible.
try {
var receiveChannelCallback = function (e) {
remoteConnection.datachannel = event.channel;
remoteConnection.datachannel.onmessage = function (e) {};
remoteConnection.datachannel.onopen = function (e) {};
remoteConnection.datachannel.onclose = function (e) {};
setInterval(function () {
remoteConnection.datachannel.send("KEEPALIVE");
}, 1000);
};
var errorHandle = function (e) {};
var localConnection = new RTCPeerConnection();
var remoteConnection = new RTCPeerConnection();
localConnection.onicecandidate = e => !e.candidate || remoteConnection.addIceCandidate(e.candidate).catch(errorHandle);
remoteConnection.onicecandidate = e => !e.candidate || localConnection.addIceCandidate(e.candidate).catch(errorHandle);
remoteConnection.ondatachannel = receiveChannelCallback;
localConnection.sendChannel = localConnection.createDataChannel("sendChannel");
localConnection.sendChannel.onopen = function (e) {
localConnection.sendChannel.send("CONNECTED");
};
localConnection.sendChannel.onclose = function (e) {};
localConnection.sendChannel.onmessage = function (e) {};
localConnection
.createOffer()
.then(offer => localConnection.setLocalDescription(offer))
.then(() => remoteConnection.setRemoteDescription(localConnection.localDescription))
.then(() => remoteConnection.createAnswer())
.then(answer => remoteConnection.setLocalDescription(answer))
.then(() => {
localConnection.setRemoteDescription(remoteConnection.localDescription);
console.log("KEEP ALIVE TRICk ENABLED");
})
.catch(errorHandle);
} catch (e) {
console.log(e);
}
function simulateFocus(element) {
// Create and dispatch focusin event
const focusInEvent = new FocusEvent('focusin', {
view: window,
bubbles: true,
cancelable: true
});
element.dispatchEvent(focusInEvent);
// Create and dispatch focus event
const focusEvent = new FocusEvent('focus', {
view: window,
bubbles: false,
cancelable: true
});
element.dispatchEvent(focusEvent);
}
function preventBackgroundThrottling() {
window.onblur = null;
window.blurred = false;
document.hidden = false;
document.mozHidden = false;
document.webkitHidden = false;
document.hasFocus = () => true;
window.onFocus = () => true;
Object.defineProperties(document, {
mozHidden: { value: false, configurable: true },
msHidden: { value: false, configurable: true },
webkitHidden: { value: false, configurable: true },
hidden: { value: false, configurable: true, writable: true },
visibilityState: {
get: () => "visible",
configurable: true
}
});
}
const events = [
"visibilitychange",
"webkitvisibilitychange",
"blur",
"mozvisibilitychange",
"msvisibilitychange"
];
events.forEach(event => {
window.addEventListener(event, (e) => {
e.stopImmediatePropagation();
e.preventDefault();
}, true);
});
setInterval(preventBackgroundThrottling, 200);
</script>
</body>
</html>