Files
archived-vdo.ninja/examples/wireless.html
2025-05-09 03:02:30 -04:00

1739 lines
68 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manual Message Relay with Compression</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
height: 100vh;
}
.container {
display: flex;
flex-direction: column;
height: 100%;
}
.iframe-container {
flex: 1;
display: flex;
}
.iframe-half {
flex: 1;
position: relative;
border-right: 2px solid #ccc;
}
.iframe-half:last-child {
border-right: none;
}
iframe {
width: 100%;
height: 100%;
border: none;
}
.controls {
background: #f0f0f0;
padding: 15px;
border-top: 1px solid #ccc;
}
.control-group {
margin-bottom: 15px;
}
.control-group:last-child {
margin-bottom: 0;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
textarea {
width: 100%;
height: 60px;
font-family: monospace;
padding: 8px;
box-sizing: border-box;
margin-bottom: 5px;
}
button {
padding: 8px 15px;
background: #4285f4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-right: 5px;
}
button:hover {
background: #3367d6;
}
.status {
margin-top: 5px;
font-size: 0.9em;
color: #666;
}
.debug-output {
margin-top: 10px;
border: 1px solid #ddd;
padding: 10px;
font-family: monospace;
font-size: 12px;
max-height: 950px;
overflow-y: auto;
background: #f9f9f9;
}
.debug-output > div {
margin: 10px 0;
}
.connection-status {
padding: 8px;
margin-bottom: 10px;
background: #eee;
border-radius: 4px;
}
.compression-stats {
margin-top: 5px;
color: #666;
font-size: 0.9em;
}
.compression-toggle {
margin-top: 10px;
}
.transfer-buttons {
display: flex;
justify-content: center;
padding: 10px 0;
background: #e0e0e0;
}
.transfer-btn {
background: #ff5722;
margin: 0 10px;
}
.transfer-btn:hover {
background: #e64a19;
}
.iframe-label {
position: absolute;
top: 0;
left: 0;
background: rgba(0,0,0,0.7);
color: white;
padding: 4px 8px;
font-size: 12px;
z-index: 10;
}
.auto-transfer {
background: #4caf50;
}
.auto-transfer:hover {
background: #388e3c;
}
.auto-transfer-active {
background: #f44336;
}
.auto-transfer-active:hover {
background: #d32f2f;
}
</style>
</head>
<body>
<div class="container">
<div class="iframe-container" id="iframe-container">
<div class="iframe-half" id="iframe-left">
<div class="iframe-label">Left Session</div>
<!-- Left iframe will be added here via JS -->
</div>
<div class="iframe-half" id="iframe-right">
<div class="iframe-label">Right Session</div>
<!-- Right iframe will be added here via JS -->
</div>
</div>
<div class="transfer-buttons">
<button id="leftToRight" class="transfer-btn">Transfer Left → Right</button>
<button id="autoTransfer" class="auto-transfer">Enable Auto Transfer</button>
<button id="rightToLeft" class="transfer-btn">Transfer Right → Left</button>
</div>
<div class="controls">
<div class="connection-status" id="connectionStatus">
Connection Status: Waiting for connections...
</div>
<div class="control-group">
<label for="outputArea">Outgoing messages:</label>
<textarea id="outputArea" readonly></textarea>
<button id="debugToggle">Toggle Debug</button>
<span id="compressionStats" class="compression-stats"></span>
<div id="outputStatus" class="status"></div>
<div id="debugOutput" class="debug-output" style="display:none;"></div>
<div class="compression-toggle">
<input type="checkbox" id="enableCompression" checked>
<label for="enableCompression" style="display:inline; font-weight:normal;">Enable Compression</label>
</div>
</div>
</div>
</div>
<script>
// URL Params Helper
(function(w) {
w.URLSearchParams = w.URLSearchParams || function(searchString) {
var self = this;
searchString = searchString.replace("??", "?");
self.searchString = searchString;
self.get = function(name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(self.searchString);
if (results == null) {
return null;
} else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
function compressIP(ip) {
if (ip === '127.0.0.1') return 'L';
if (ip === '0.0.0.0') return 'Z';
if (ip.startsWith('192.168.')) return 'P' + ip.split('.').slice(2).join('');
if (ip.startsWith('10.')) return 'T' + ip.split('.').slice(1).join('');
if (ip.startsWith('172.')) return 'S' + ip.split('.').slice(1).join('');
const parts = ip.split('.');
let num = 0;
for (let i = 0; i < 4; i++) {
num = num * 256 + parseInt(parts[i]);
}
return num.toString(36).toUpperCase();
}
function decompressIP(compressed) {
if (compressed === 'L') return '127.0.0.1';
if (compressed === 'Z') return '0.0.0.0';
if (compressed.startsWith('P')) {
const suffix = compressed.substring(1);
if (suffix.length === 0) return '192.168.0.0';
if (suffix.length === 1) return `192.168.${suffix}.0`;
return `192.168.${suffix.substring(0, 1)}.${suffix.substring(1)}`;
}
if (compressed.startsWith('T')) {
const suffix = compressed.substring(1);
if (suffix.length === 0) return '10.0.0.0';
if (suffix.length === 1) return `10.${suffix}.0.0`;
if (suffix.length === 2) return `10.${suffix.substring(0, 1)}.${suffix.substring(1)}.0`;
return `10.${suffix.substring(0, 1)}.${suffix.substring(1, 2)}.${suffix.substring(2)}`;
}
if (compressed.startsWith('S')) {
const suffix = compressed.substring(1);
if (suffix.length === 0) return '172.0.0.0';
if (suffix.length === 1) return `172.${suffix}.0.0`;
if (suffix.length === 2) return `172.${suffix.substring(0, 1)}.${suffix.substring(1)}.0`;
return `172.${suffix.substring(0, 1)}.${suffix.substring(1, 2)}.${suffix.substring(2)}`;
}
const num = parseInt(compressed, 36);
return [
(num >> 24) & 0xFF,
(num >> 16) & 0xFF,
(num >> 8) & 0xFF,
num & 0xFF
].join('.');
}
function priorityToCompact(priority) {
return priority.toString(36);
}
function compactToPriority(compact) {
return parseInt(compact, 36);
}
function hexToCompact(hex) {
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
let result = '';
for (let i = 0; i < hex.length; i += 6) {
const chunk = hex.substr(i, 6);
if (chunk.length < 6) {
const value = parseInt(chunk, 16);
result += chars[value % 64];
if (chunk.length > 2) {
result += chars[Math.floor(value / 64) % 64];
}
} else {
const value = parseInt(chunk, 16);
result += chars[value & 0x3F];
result += chars[(value >> 6) & 0x3F];
result += chars[(value >> 12) & 0x3F];
result += chars[(value >> 18) & 0x3F];
}
}
return result;
}
function compactToHex(compact) {
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
let result = '';
for (let i = 0; i < compact.length; i += 4) {
let value = 0;
for (let j = 0; j < 4 && i + j < compact.length; j++) {
const char = compact[i + j];
const charValue = chars.indexOf(char);
if (charValue === -1) {
throw new Error(`Invalid character in compact string: ${char}`);
}
value |= charValue << (j * 6);
}
const hex = value.toString(16).padStart(6, '0');
result += hex;
}
// Ensure result is exactly 64 characters (32 bytes)
return result.substring(0, 64);
}
function compressSDPForQR(sdp) {
const iceUfrag = sdp.match(/a=ice-ufrag:([^\r\n]+)/)[1];
const icePwd = sdp.match(/a=ice-pwd:([^\r\n]+)/)[1];
const fingerprintMatch = sdp.match(/a=fingerprint:sha-256 ([^\r\n]+)/);
const fingerprint = fingerprintMatch[1].replace(/:/g, '');
const isOffer = sdp.match(/a=setup:actpass/) !== null;
const candidates = [];
const candidateRegex = /a=candidate:([^\r\n]+)/g;
let candidateMatch;
while ((candidateMatch = candidateRegex.exec(sdp)) !== null) {
const parts = candidateMatch[1].split(' ');
if (parts[4].includes(':')) continue;
const foundation = parts[0];
const component = parts[1];
const protocol = parts[2].toLowerCase() === 'udp' ? '1' : '2';
const priority = parseInt(parts[3]);
const ip = compressIP(parts[4]);
const port = parseInt(parts[5]);
let typeCode;
switch(parts[7]) {
case 'host': typeCode = '1'; break;
case 'srflx': typeCode = '2'; break;
case 'relay': typeCode = '3'; break;
default: typeCode = '0';
}
let relInfo = '';
const rAddrIndex = parts.indexOf('raddr');
const rPortIndex = parts.indexOf('rport');
if (rAddrIndex !== -1 && rPortIndex !== -1 &&
rAddrIndex + 1 < parts.length && rPortIndex + 1 < parts.length) {
relInfo = compressIP(parts[rAddrIndex + 1]) + parts[rPortIndex + 1];
}
candidates.push(`${foundation}${component}${protocol}${compressNumber(priority)}${ip}${port}${typeCode}${relInfo}`);
}
const qrFingerprint = fingerprintToQR(fingerprint);
let result = `Q${isOffer ? '1' : '0'}${iceUfrag}~${icePwd}~${qrFingerprint}`;
if (candidates.length > 0) {
result += `~${candidates.join(',')}`;
}
return result;
}
function decompressSDPFromQR(compressed) {
if (!compressed.startsWith('Q')) {
throw new Error('Unsupported QR compression format');
}
const isOffer = compressed[1] === '1';
const parts = compressed.substring(2).split('~');
const iceUfrag = parts[0];
const icePwd = parts[1];
const fingerprint = qrToFingerprint(parts[2]);
const formattedFingerprint = fingerprint.match(/.{2}/g).join(':');
let sdp = [
'v=0',
'o=- ' + Math.floor(Date.now()) + ' 2 IN IP4 127.0.0.1',
's=-',
't=0 0',
'a=group:BUNDLE 0',
'a=extmap-allow-mixed',
'a=msid-semantic: WMS',
'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',
'c=IN IP4 0.0.0.0',
`a=ice-ufrag:${iceUfrag}`,
`a=ice-pwd:${icePwd}`,
'a=ice-options:trickle',
`a=fingerprint:sha-256 ${formattedFingerprint}`,
`a=setup:${isOffer ? 'actpass' : 'active'}`,
'a=mid:0'
].join('\r\n');
if (parts.length > 3 && parts[3]) {
const candidates = parts[3].split(',');
let candidateLines = '';
for (const candidate of candidates) {
let i = 0;
let j = i;
while (j < candidate.length && /^\d$/.test(candidate[j])) j++;
const foundation = candidate.substring(i, j);
i = j;
if (i >= candidate.length) continue;
const component = candidate.substring(i, i + 1);
i += 1;
if (i >= candidate.length) continue;
const protocolCode = candidate.substring(i, i + 1);
const protocol = protocolCode === '1' ? 'UDP' : 'TCP';
i += 1;
if (i >= candidate.length) continue;
j = i;
while (j < candidate.length && /^[0-9A-Z]$/.test(candidate[j])) j++;
const priorityEncoded = candidate.substring(i, j);
const priority = decompressNumber(priorityEncoded);
i = j;
if (i >= candidate.length) continue;
j = i;
while (j < candidate.length && !/^\d$/.test(candidate[j]) && candidate[j] !== '1' &&
candidate[j] !== '2' && candidate[j] !== '3' && candidate[j] !== '0') j++;
const ipEncoded = candidate.substring(i, j);
const ip = decompressIP(ipEncoded);
i = j;
if (i >= candidate.length) continue;
j = i;
while (j < candidate.length && /^\d$/.test(candidate[j])) j++;
const port = candidate.substring(i, j);
i = j;
if (i >= candidate.length) continue;
const typeCode = candidate.substring(i, i + 1);
i += 1;
let type;
switch(typeCode) {
case '1': type = 'host'; break;
case '2': type = 'srflx'; break;
case '3': type = 'relay'; break;
default: type = 'unknown';
}
candidateLines += `\r\na=candidate:${foundation} ${component} ${protocol} ${priority} ${ip} ${port} typ ${type}`;
if (i < candidate.length && (type === 'srflx' || type === 'relay')) {
j = i;
while (j < candidate.length && !/^\d$/.test(candidate[j])) j++;
const relatedIPEncoded = candidate.substring(i, j);
const relatedIP = decompressIP(relatedIPEncoded);
i = j;
j = i;
while (j < candidate.length && /^\d$/.test(candidate[j])) j++;
const relatedPort = candidate.substring(i, j);
if (relatedIP && relatedPort) {
candidateLines += ` raddr ${relatedIP} rport ${relatedPort}`;
}
}
if (protocol === 'TCP') {
candidateLines += ' tcptype active';
}
}
sdp += candidateLines;
if (candidateLines) {
sdp += '\r\na=end-of-candidates';
}
}
return sdp;
}
function compressNumber(num) {
// Convert to base36 for compactness, prefer uppercase for QR
return num.toString(36).toUpperCase();
}
function decompressNumber(compressed) {
return parseInt(compressed, 36);
}
function fingerprintToQR(hex) {
let result = '';
for (let i = 0; i < hex.length; i += 4) {
const chunk = hex.substr(i, 4);
if (chunk.length < 4) {
const value = parseInt(chunk, 16);
result += value.toString(36).toUpperCase().padStart(3, '0').substring(0, 3);
} else {
const value = parseInt(chunk, 16);
result += value.toString(36).toUpperCase().padStart(3, '0').substring(0, 3);
}
}
return result;
}
function qrToFingerprint(qrFp) {
let result = '';
for (let i = 0; i < qrFp.length; i += 3) {
const chunk = qrFp.substr(i, 3);
const value = parseInt(chunk, 36);
result += value.toString(16).padStart(4, '0').substring(0, 4);
}
return result.padEnd(64, '0');
}
function compressSDP(sdp) {
try {
// Extract key fields from SDP
const iceUfragMatch = sdp.match(/a=ice-ufrag:([^\r\n]+)/);
const icePwdMatch = sdp.match(/a=ice-pwd:([^\r\n]+)/);
const fingerprintMatch = sdp.match(/a=fingerprint:sha-256 ([^\r\n]+)/);
if (!iceUfragMatch || !icePwdMatch || !fingerprintMatch) {
updateDebugOutput("SDP missing required fields");
throw new Error("SDP missing required fields");
}
const iceUfrag = iceUfragMatch[1];
const icePwd = icePwdMatch[1];
const fingerprint = fingerprintMatch[1].replace(/:/g, '');
const isOffer = sdp.match(/a=setup:actpass/) !== null;
// Extract host and srflx candidates only (UDP IPv4 only)
const candidateRegex = /a=candidate:([^\r\n]+)/g;
let candidateMatch;
let hostCandidates = [];
let srflxCandidates = [];
while ((candidateMatch = candidateRegex.exec(sdp)) !== null) {
const parts = candidateMatch[1].split(' ');
// Skip non-UDP, IPv6, mDNS, TCP and relay candidates
if (parts[2].toLowerCase() !== 'udp' ||
parts[4].includes(':') ||
parts[4].includes('.local') ||
parts[7] === 'relay') {
continue;
}
// Create compact representation of candidate
const foundation = parts[0];
const component = parts[1];
const priority = priorityToCompact(parseInt(parts[3]));
const ip = compressIP(parts[4]);
const port = parts[5];
const type = parts[7];
// Format: foundation|component|priority|ip|port
let candidateStr = `${foundation}|${component}|${priority}|${ip}|${port}`;
// Add related address info for srflx candidates
if (type === 'srflx') {
const rAddrIndex = parts.indexOf('raddr');
const rPortIndex = parts.indexOf('rport');
if (rAddrIndex !== -1 && rPortIndex !== -1 &&
rAddrIndex + 1 < parts.length && rPortIndex + 1 < parts.length) {
const relatedIP = compressIP(parts[rAddrIndex + 1]);
candidateStr += `|${relatedIP}|${parts[rPortIndex + 1]}`;
}
srflxCandidates.push(candidateStr);
} else if (type === 'host') {
hostCandidates.push(candidateStr);
}
}
// Compress fingerprint using hexToCompact
const compactFingerprint = hexToCompact(fingerprint);
// Create string format: C2|o/a|ufrag|pwd|fingerprint|host1^host2^...|srflx1^srflx2^...
let result = `C2|${isOffer ? 'o' : 'a'}|${iceUfrag}|${icePwd}|${compactFingerprint}`;
// Add host candidates
result += '|' + hostCandidates.join('^');
// Add srflx candidates
result += '|' + srflxCandidates.join('^');
return result;
} catch (e) {
console.error("Error in compressSDP:", e);
updateDebugOutput(`SDP compression error: ${e.message}`);
throw e;
}
}
function decompressSDP(compressed) {
try {
if (typeof compressed !== 'string') {
compressed = JSON.stringify(compressed);
}
// Handle old format
if (compressed.startsWith('{') && compressed.endsWith('}')) {
try {
const jsonData = JSON.parse(compressed);
// This is our old JSON format, call the JSON handler
return decompressJSONSDP(jsonData);
} catch (e) {
// Not valid JSON, continue with string parsing
updateDebugOutput(`Not valid JSON format: ${e.message}`);
}
}
// Handle old C1 format
if (compressed.startsWith('C1|')) {
return decompressOldSDP(compressed);
}
// Parse the new C2 string format
// C2|o/a|ufrag|pwd|fingerprint|host1^host2^...|srflx1^srflx2^...
const parts = compressed.split('|');
if (parts[0] !== 'C2' || parts.length < 6) {
throw new Error('Invalid compressed SDP format');
}
const isOffer = parts[1] === 'o';
const iceUfrag = parts[2];
const icePwd = parts[3];
// Decompress fingerprint if it doesn't contain colons
let fingerprint = parts[4];
if (fingerprint.indexOf(':') === -1) {
if (fingerprint.length < 32) {
// This is likely a compact fingerprint, convert back to hex
fingerprint = compactToHex(fingerprint);
}
// Format with colons
fingerprint = fingerprint.match(/.{2}/g).join(':').toUpperCase();
}
// Build the basic SDP structure
let sdpLines = [
'v=0',
'o=- ' + Math.floor(Date.now()) + ' 2 IN IP4 127.0.0.1',
's=-',
't=0 0',
'a=group:BUNDLE 0',
'a=extmap-allow-mixed',
'a=msid-semantic: WMS',
'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',
'c=IN IP4 0.0.0.0',
`a=ice-ufrag:${iceUfrag}`,
`a=ice-pwd:${icePwd}`,
'a=ice-options:trickle',
`a=fingerprint:sha-256 ${fingerprint}`,
`a=setup:${isOffer ? 'actpass' : 'active'}`,
'a=mid:0',
'a=sctp-port:5000',
'a=max-message-size:262144'
];
// Add host candidates
if (parts[5] && parts[5] !== '') {
const hostCandidates = parts[5].split('^');
for (const candidateStr of hostCandidates) {
if (!candidateStr) continue;
const candParts = candidateStr.split('|');
if (candParts.length < 5) continue;
const foundation = candParts[0];
const component = candParts[1];
const priority = compactToPriority(candParts[2]).toString();
const ip = decompressIP(candParts[3]);
const port = candParts[4];
sdpLines.push(`a=candidate:${foundation} ${component} udp ${priority} ${ip} ${port} typ host generation 0`);
}
}
// Add srflx candidates
if (parts[6] && parts[6] !== '') {
const srflxCandidates = parts[6].split('^');
for (const candidateStr of srflxCandidates) {
if (!candidateStr) continue;
const candParts = candidateStr.split('|');
if (candParts.length < 5) continue;
const foundation = candParts[0];
const component = candParts[1];
const priority = compactToPriority(candParts[2]).toString();
const ip = decompressIP(candParts[3]);
const port = candParts[4];
let candLine = `a=candidate:${foundation} ${component} udp ${priority} ${ip} ${port} typ srflx`;
// Add raddr/rport if available
if (candParts.length >= 7) {
const relatedIP = decompressIP(candParts[5]);
candLine += ` raddr ${relatedIP} rport ${candParts[6]}`;
}
candLine += ' generation 0';
sdpLines.push(candLine);
}
}
// Join with proper line endings
const result = sdpLines.join('\r\n') + '\r\n';
return result;
} catch (e) {
console.error("Error in decompressSDP:", e);
updateDebugOutput(`SDP decompression error: ${e.message}`);
throw e;
}
}
// Fallback handler for old C1 format
function decompressOldSDP(compressed) {
try {
const parts = compressed.split('|');
const isOffer = parts[1] === 'o';
const iceUfrag = parts[2];
const icePwd = parts[3];
let fingerprint = parts[4];
if (fingerprint.indexOf(':') === -1) {
fingerprint = fingerprint.match(/.{2}/g).join(':').toUpperCase();
}
let sdpLines = [
'v=0',
'o=- ' + Math.floor(Date.now()) + ' 2 IN IP4 127.0.0.1',
's=-',
't=0 0',
'a=group:BUNDLE 0',
'a=extmap-allow-mixed',
'a=msid-semantic: WMS',
'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',
'c=IN IP4 0.0.0.0',
`a=ice-ufrag:${iceUfrag}`,
`a=ice-pwd:${icePwd}`,
'a=ice-options:trickle',
`a=fingerprint:sha-256 ${fingerprint}`,
`a=setup:${isOffer ? 'actpass' : 'active'}`,
'a=mid:0',
'a=sctp-port:5000',
'a=max-message-size:262144'
];
return sdpLines.join('\r\n') + '\r\n';
} catch (e) {
console.error("Error in decompressOldSDP:", e);
updateDebugOutput(`Old SDP decompression error: ${e.message}`);
throw e;
}
}
// Fallback handler for JSON format
function decompressJSONSDP(jsonData) {
try {
const isOffer = jsonData.t === 'o';
const iceUfrag = jsonData.u;
const icePwd = jsonData.p;
let fingerprint = jsonData.f;
if (fingerprint.indexOf(':') === -1) {
fingerprint = fingerprint.match(/.{2}/g).join(':').toUpperCase();
}
let sdpLines = [
'v=0',
'o=- ' + Math.floor(Date.now()) + ' 2 IN IP4 127.0.0.1',
's=-',
't=0 0',
'a=group:BUNDLE 0',
'a=extmap-allow-mixed',
'a=msid-semantic: WMS',
'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',
'c=IN IP4 0.0.0.0',
`a=ice-ufrag:${iceUfrag}`,
`a=ice-pwd:${icePwd}`,
'a=ice-options:trickle',
`a=fingerprint:sha-256 ${fingerprint}`,
`a=setup:${isOffer ? 'actpass' : 'active'}`,
'a=mid:0',
'a=sctp-port:5000',
'a=max-message-size:262144'
];
// Add candidates if they exist
if (jsonData.c && Array.isArray(jsonData.c)) {
sdpLines = sdpLines.concat(jsonData.c);
}
// Add host candidates from h array if it exists
else if (jsonData.h && Array.isArray(jsonData.h)) {
for (const c of jsonData.h) {
if (typeof c === 'string') {
const parts = c.split(',');
if (parts.length >= 5) {
const priority = compactToPriority(parts[2]).toString();
sdpLines.push(`a=candidate:${parts[0]} ${parts[1]} udp ${priority} ${parts[3]} ${parts[4]} typ host generation 0`);
}
} else if (typeof c === 'object') {
sdpLines.push(`a=candidate:${c.f} ${c.c} udp ${c.p} ${c.i} ${c.o} typ host generation 0`);
}
}
// Add srflx candidates from s array if it exists
if (jsonData.s && Array.isArray(jsonData.s)) {
for (const c of jsonData.s) {
if (typeof c === 'string') {
const parts = c.split(',');
if (parts.length >= 5) {
const priority = compactToPriority(parts[2]).toString();
let line = `a=candidate:${parts[0]} ${parts[1]} udp ${priority} ${parts[3]} ${parts[4]} typ srflx`;
if (parts.length >= 7) {
line += ` raddr ${parts[5]} rport ${parts[6]}`;
}
line += ' generation 0';
sdpLines.push(line);
}
} else if (typeof c === 'object') {
let line = `a=candidate:${c.f} ${c.c} udp ${c.p} ${c.i} ${c.o} typ srflx`;
if (c.r && c.s) {
line += ` raddr ${c.r} rport ${c.s}`;
}
line += ' generation 0';
sdpLines.push(line);
}
}
}
}
return sdpLines.join('\r\n') + '\r\n';
} catch (e) {
console.error("Error in decompressJSONSDP:", e);
updateDebugOutput(`JSON SDP decompression error: ${e.message}`);
throw e;
}
}
// Compress ICE candidates to a more compact string format
function compressICECandidates(candidates) {
try {
if (!candidates || !candidates.length) return "";
// Filter for valid candidates - only UDP IPv4 host and srflx
const validCandidates = candidates.filter(c => {
if (!c || !c.candidate) return false;
const parts = c.candidate.split(' ');
return parts.length >= 8 &&
parts[2].toLowerCase() === 'udp' &&
!parts[4].includes(':') &&
!parts[4].includes('.local') &&
(parts[7] === 'host' || parts[7] === 'srflx');
});
if (!validCandidates.length) return "";
// Separate host and srflx candidates
const hostCandidates = [];
const srflxCandidates = [];
validCandidates.forEach(c => {
const parts = c.candidate.split(' ');
const foundation = parts[0].replace('candidate:', '');
const component = parts[1];
const priority = priorityToCompact(parseInt(parts[3]));
const ip = parts[4];
const port = parts[5];
const type = parts[7];
// Format: foundation,component,priority,ip,port
let candidateStr = `${foundation},${component},${priority},${ip},${port}`;
// Add related address info for srflx candidates
if (type === 'srflx') {
const rAddrIndex = parts.indexOf('raddr');
const rPortIndex = parts.indexOf('rport');
if (rAddrIndex !== -1 && rPortIndex !== -1 &&
rAddrIndex + 1 < parts.length && rPortIndex + 1 < parts.length) {
candidateStr += `,${parts[rAddrIndex + 1]},${parts[rPortIndex + 1]}`;
}
srflxCandidates.push(candidateStr);
} else if (type === 'host') {
hostCandidates.push(candidateStr);
}
});
// Format: 'h:' + hosts joined by ^ + '|s:' + srflx joined by ^
let result = '';
if (hostCandidates.length > 0) {
result += 'h:' + hostCandidates.join('^');
}
if (srflxCandidates.length > 0) {
if (result) result += '|';
result += 's:' + srflxCandidates.join('^');
}
return result;
} catch (e) {
console.error("Error compressing ICE candidates:", e);
updateDebugOutput(`ICE compression error: ${e.message}`);
return "";
}
}
// Decompress ICE candidates from compact string format
function decompressICECandidates(compressed) {
try {
if (!compressed) return [];
const candidates = [];
// Handle the old format for backward compatibility
if (compressed.includes(',') && !compressed.includes('h:') && !compressed.includes('s:')) {
return decompressOldICEFormat(compressed);
}
const sections = compressed.split('|');
for (const section of sections) {
if (!section) continue;
let type, candidateStrs;
if (section.startsWith('h:')) {
type = 'host';
candidateStrs = section.substring(2).split('^');
} else if (section.startsWith('s:')) {
type = 'srflx';
candidateStrs = section.substring(2).split('^');
} else {
// Unexpected format, skip this section
updateDebugOutput(`Unexpected section format: ${section}`);
continue;
}
for (const candidateStr of candidateStrs) {
if (!candidateStr) continue;
const parts = candidateStr.split(',');
if (parts.length < 5) continue;
const foundation = parts[0];
const component = parts[1];
const priority = compactToPriority(parts[2]);
const ip = parts[3];
const port = parts[4];
let candidateLine = `candidate:${foundation} ${component} UDP ${priority} ${ip} ${port} typ ${type}`;
// Add raddr/rport for srflx if available
if (type === 'srflx' && parts.length >= 7) {
candidateLine += ` raddr ${parts[5]} rport ${parts[6]}`;
}
// Add generation
candidateLine += ' generation 0';
candidates.push({
candidate: candidateLine,
sdpMid: "0",
sdpMLineIndex: 0
});
}
}
return candidates;
} catch (e) {
console.error("Error decompressing ICE candidates:", e);
updateDebugOutput(`ICE decompression error: ${e.message}`);
return [];
}
}
// Handle the old format for backward compatibility
function decompressOldICEFormat(compressed) {
try {
const candidates = [];
const oldCandidates = compressed.split('/');
for (const c of oldCandidates) {
const parts = c.split(',');
if (parts.length < 6) {
updateDebugOutput(`Skipping incomplete candidate data: ${c}`);
continue;
}
const foundation = parts[0];
const component = parts[1];
const protocol = parts[2] === 'u' ? 'UDP' : 'TCP';
const priority = compactToPriority(parts[3]);
const ip = parts[4];
const port = parts[5];
const typeCode = parts[6];
let type = 'host';
switch(typeCode) {
case 'h': type = 'host'; break;
case 's': type = 'srflx'; break;
case 'r': type = 'relay'; break;
default: type = 'host';
}
// Skip relay candidates - we don't want them
if (type === 'relay') continue;
// Build candidate line
let candidateLine = `candidate:${foundation} ${component} ${protocol} ${priority} ${ip} ${port} typ ${type}`;
// Add raddr/rport if available for srflx candidates
if (type === 'srflx' && parts.length >= 9) {
candidateLine += ` raddr ${parts[7]} rport ${parts[8]}`;
}
// Add generation
candidateLine += ' generation 0';
candidates.push({
candidate: candidateLine,
sdpMid: "0",
sdpMLineIndex: 0
});
}
return candidates;
} catch (e) {
console.error("Error decompressing old ICE format:", e);
updateDebugOutput(`Old ICE format decompression error: ${e.message}`);
return [];
}
}
// Update compressMessage to handle the new compression format
function compressMessage(message) {
try {
// Handle either string or object input
let parsedMsg;
if (typeof message === 'string') {
parsedMsg = JSON.parse(message);
} else {
parsedMsg = message;
}
// For SDP offers/answers with descriptions
if (parsedMsg.description && parsedMsg.description.sdp) {
// Extract key fields
const { UUID, from, session } = parsedMsg;
if (from) {
myUUID = from;
}
// Compress SDP with candidates preserved
return JSON.stringify({
type: "O",
ses: session || null,
sdp: compressSDP(parsedMsg.description.sdp)
});
}
// For ICE candidates
else if (parsedMsg.candidates && parsedMsg.candidates.length) {
return null;
// Extract key fields
const { UUID, type, from, session } = parsedMsg;
if (from) {
myUUID = from;
}
// Compress candidates - only keep UDP IPv4 host and srflx candidates
const validCandidates = parsedMsg.candidates.filter(c => {
if (!c || !c.candidate) return false;
const parts = c.candidate.split(' ');
return parts.length >= 8 &&
parts[2].toLowerCase() === 'udp' &&
!parts[4].includes(':') &&
!parts[4].includes('.local') &&
(parts[7] === 'host' || parts[7] === 'srflx');
});
// Skip empty candidate sets
if (!validCandidates.length) {
return null; // Skip this message completely
}
// Create compact message
return JSON.stringify({
type: "C",
ses: session || null,
cand: compressICECandidates(validCandidates)
});
}
// For basic messages (join, play, etc.)
else if (parsedMsg.request) {
// Extract key fields and create compact message
const { request, from } = parsedMsg;
if (from) {
myUUID = from;
}
return JSON.stringify({
type: "B",
req: request || null
});
}
// If we don't recognize the format, return the original message
updateDebugOutput("Unknown message format, using original");
return typeof message === 'string' ? message : JSON.stringify(message);
} catch (e) {
console.error("Compression error:", e);
updateDebugOutput(`Message compression error: ${e.message}`);
return typeof message === 'string' ? message : JSON.stringify(message);
}
}
// Update decompressMessage to handle the new format
function decompressMessage(message) {
try {
// Parse the message first
let parsedMsg;
if (typeof message === 'string') {
parsedMsg = JSON.parse(message);
} else {
parsedMsg = message;
}
// Check if it's our compressed format
if (parsedMsg.type) {
// SDP offer/answer message
if (parsedMsg.type === "O" && parsedMsg.sdp) {
try {
// Determine if it's an offer or answer based on first character of SDP
let isOffer = false;
const sdpData = parsedMsg.sdp;
if (typeof sdpData === 'string') {
if (sdpData.startsWith('C1|')) {
isOffer = sdpData.split('|')[1] === 'o';
} else if (sdpData.startsWith('C2|')) {
isOffer = sdpData.split('|')[1] === 'o';
} else if (sdpData.startsWith('{') && sdpData.endsWith('}')) {
try {
const sdpObj = JSON.parse(sdpData);
isOffer = sdpObj.t === 'o';
} catch (e) {
updateDebugOutput(`Error parsing SDP JSON: ${e.message}`);
}
}
} else if (typeof sdpData === 'object') {
isOffer = sdpData.t === 'o';
}
// Build original format
const result = {
UUID: myUUID,
streamID: "local",
description: {
type: isOffer ? "offer" : "answer",
sdp: decompressSDP(parsedMsg.sdp)
},
from: "remote",
roomid: "default"
};
if (parsedMsg.ses) result.session = parsedMsg.ses;
return JSON.stringify(result);
} catch (e) {
console.error("Error decompressing SDP:", e);
updateDebugOutput(`SDP decompression failed: ${e.message}`);
throw e;
}
}
// ICE candidates message - no changes needed
else if (parsedMsg.type === "C") {
try {
// Decompress candidates
const candidates = decompressICECandidates(parsedMsg.cand || "");
// Skip if no valid candidates
if (!candidates || candidates.length === 0) {
updateDebugOutput("No valid candidates after decompression, skipping");
return null;
}
// Build original format
const result = {
UUID: myUUID,
candidates: candidates,
from: "remote",
roomid: "default",
type: "remote"
};
if (parsedMsg.ses) result.session = parsedMsg.ses;
return JSON.stringify(result);
} catch (e) {
console.error("Error decompressing candidates:", e);
updateDebugOutput(`Candidate decompression failed: ${e.message}`);
throw e;
}
}
// Basic control message - no changes needed
else if (parsedMsg.type === "B") {
// Build original format
const result = {
request: parsedMsg.req,
streamID: "remote",
from: "remote",
roomid: "default"
};
return JSON.stringify(result);
}
}
// If it's not our compressed format, return it unchanged
return typeof message === 'string' ? message : JSON.stringify(message);
} catch (e) {
console.error("Decompression error:", e);
updateDebugOutput(`Message decompression error: ${e.message}`);
// Return original in case of error
return typeof message === 'string' ? message : JSON.stringify(message);
}
}
// ====================== Application Code ======================
// Initialize variables
var myUUID = "123";
// Initialize variables
let messageQueues = { left: [], right: [] };
let connectionStates = { left: {}, right: {} };
let currentConnectionIds = { left: null, right: null };
let processingTimeouts = { left: null, right: null };
let iframes = { left: null, right: null };
let roomid = "default";
let debugMode = false;
let compressionEnabled = true;
let autoTransferEnabled = false;
let messageBuffers = { left: [], right: [] };
// Parse URL parameters
var urlEdited = window.location.search.replace(/\?\?/g, "?");
urlEdited = urlEdited.replace(/\?/g, "&");
urlEdited = urlEdited.replace(/\&/, "?");
if (urlEdited !== window.location.search) {
console.warn(window.location.search + " changed to " + urlEdited);
window.history.pushState({path: urlEdited.toString()}, '', urlEdited.toString());
}
var urlParams = new URLSearchParams(urlEdited);
// Get room ID
if (urlParams.has("room")) {
roomid = urlParams.get("room");
}
// Helper functions
function updateDebugOutput(message) {
if (!debugMode) return;
const debugEl = document.getElementById("debugOutput");
const timestamp = new Date().toLocaleTimeString();
debugEl.innerHTML += `<div>${timestamp}: ${message}</div>`;
debugEl.scrollTop = debugEl.scrollHeight;
}
function updateConnectionStatus() {
const statusEl = document.getElementById("connectionStatus");
const leftActive = Object.values(connectionStates.left).filter(s => s !== "completed" && s !== "failed").length;
const rightActive = Object.values(connectionStates.right).filter(s => s !== "completed" && s !== "failed").length;
if (leftActive > 0 || rightActive > 0) {
statusEl.innerHTML = `Connection Status: Left (${leftActive} active) | Right (${rightActive} active)`;
} else {
statusEl.innerHTML = `Connection Status: Waiting for connections...`;
}
}
function handleAutoRelay(message, sourceSide) {
try {
// Check if it's a JSON string
let parsedMsg = typeof message === 'string' ? JSON.parse(message) : message;
// Handle both packed and unpacked messages
let innerMessage = parsedMsg;
if (parsedMsg.m) {
// This is a packed message from the queue
innerMessage = JSON.parse(parsedMsg.m);
}
// Check if it's a basic command type with request property
if (innerMessage.type === "B" && innerMessage.req) {
const command = innerMessage.req;
// Handle specifically joinroom and play commands
if (command === "joinroom" || command === "play") {
updateDebugOutput(`Auto-relaying '${command}' command from ${sourceSide}`);
// Create the original format message for both sides
const relayMessage = JSON.stringify({
request: command,
streamID: "remote",
from: "remote",
roomid: "default"
});
if (iframes.left && iframes.left.contentWindow ) {
iframes.left.contentWindow.postMessage({"function":"routeMessage", "value": relayMessage}, '*');
updateDebugOutput(`Auto-relayed '${command}' to left iframe`);
}
// Return true to indicate we handled it
return true;
}
}
// Not a command we auto-relay
return false;
} catch (e) {
updateDebugOutput(`Error in auto-relay: ${e.message}`);
return false;
}
}
// Update the manualSend function to check for auto-relay commands
function manualSend(message, side) {
try {
// Check if this is a duplicate message
if (messageQueues[side].length > 0) {
try {
const lastMsg = JSON.parse(messageQueues[side][messageQueues[side].length - 1]);
const newMsg = JSON.parse(message);
// Compare stringified versions to detect duplicates
if (JSON.stringify(lastMsg.m) === JSON.stringify(newMsg)) {
return;
}
} catch (e) {
// Continue if parsing fails
}
}
// Extract UUID from original message for tracking
let uuid = null;
try {
const parsedMsg = JSON.parse(message);
uuid = parsedMsg.UUID;
if (uuid) {
// Update connection state
if (!connectionStates[side][uuid]) {
connectionStates[side][uuid] = "initializing";
updateConnectionStatus();
}
// If this is a new connection and we're not processing one already
if (!currentConnectionIds[side] && uuid) {
currentConnectionIds[side] = uuid;
updateConnectionStatus();
// Set timeout for connection
if (processingTimeouts[side]) clearTimeout(processingTimeouts[side]);
processingTimeouts[side] = setTimeout(() => {
if (currentConnectionIds[side] === uuid) {
connectionStates[side][currentConnectionIds[side]] = "failed";
currentConnectionIds[side] = null;
updateConnectionStatus();
}
}, 60000); // 60 seconds timeout
}
}
} catch (e) {
updateDebugOutput(`Error parsing message for UUID (${side}): ${e.message}`);
}
let compressedMessage = message;
let originalLength = message.length;
let compressionRatio = "0%";
// Compress the message if enabled
if (compressionEnabled) {
try {
const compressed = compressMessage(message);
console.info(compressed);
// Skip if compression returned null
if (compressed === null) {
updateDebugOutput(`Compression returned null, skipping message (${side})`);
return;
}
compressedMessage = compressed;
compressionRatio = ((compressedMessage.length / originalLength) * 100).toFixed(1) + "%";
// Check if this is a command we should auto-relay
try {
const parsedCompressed = JSON.parse(compressedMessage);
if (handleAutoRelay(parsedCompressed, side)) {
// We handled it with auto-relay, so no need to add to queue
updateDebugOutput(`Auto-relayed command from ${side}, not adding to queue`);
// Update compression stats display anyway
document.getElementById("compressionStats").textContent =
`Compression: ${originalLength}${compressedMessage.length} bytes (${compressionRatio})`;
// Update status
const statusEl = document.getElementById("outputStatus");
statusEl.textContent = `${side.toUpperCase()}: Auto-relayed command - ${new Date().toLocaleTimeString()}`;
return; // Exit early since we handled the command
}
} catch (e) {
updateDebugOutput(`Error checking for auto-relay: ${e.message}`);
}
} catch (e) {
updateDebugOutput(`Compression error: ${e.message}, using original message (${side})`);
compressedMessage = message;
compressionRatio = "100%";
}
} else {
compressionRatio = "100% (disabled)";
// Even if compression is disabled, check if we can auto-relay
try {
if (handleAutoRelay(message, side)) {
// We handled it with auto-relay, so no need to add to queue
updateDebugOutput(`Auto-relayed command from ${side}, not adding to queue (compression disabled)`);
// Update compression stats display anyway
document.getElementById("compressionStats").textContent =
`Compression: ${originalLength} bytes (disabled)`;
// Update status
const statusEl = document.getElementById("outputStatus");
statusEl.textContent = `${side.toUpperCase()}: Auto-relayed command - ${new Date().toLocaleTimeString()}`;
return; // Exit early since we handled the command
}
} catch (e) {
updateDebugOutput(`Error checking for auto-relay: ${e.message}`);
}
}
// Update compression stats display
document.getElementById("compressionStats").textContent =
`Compression: ${originalLength}${compressedMessage.length} bytes (${compressionRatio})`;
const msg = {m: compressedMessage, t: Date.now()};
const packed = JSON.stringify(msg);
// Add to message queue
messageQueues[side].push(packed);
// Add to buffer for auto-transfer if enabled
if (autoTransferEnabled) {
// Store in the buffer
messageBuffers[side].push(packed);
// Determine the target side
const targetSide = side === 'left' ? 'right' : 'left';
// Use setTimeout to ensure messages are processed in the correct order
setTimeout(() => {
try {
transferMessage(targetSide, packed);
} catch (err) {
updateDebugOutput(`Error in auto-transfer: ${err.message}`);
}
}, 100);
}
// Update status
const statusEl = document.getElementById("outputStatus");
statusEl.textContent = `${side.toUpperCase()}: Message added to queue (${messageQueues[side].length} pending) - ${new Date().toLocaleTimeString()}`;
// Update the output area if it's currently empty
const outputArea = document.getElementById("outputArea");
if (outputArea.value === "") {
outputArea.value = `[${side.toUpperCase()}] ${packed}`;
}
} catch (e) {
updateDebugOutput(`Error in manualSend (${side}): ${e.message}`);
}
}
function processIncomingMessage(input, targetSide) {
const statusEl = document.getElementById("outputStatus");
try {
const msg = JSON.parse(input);
if (!msg.t || !msg.m) {
statusEl.textContent = "Error: Invalid message format";
updateDebugOutput("Invalid message format: missing t or m property");
return;
}
// Don't be too strict about message age
if (Date.now() - msg.t > 1200000) { // 20 minutes
updateDebugOutput("Message is old (>20 minutes) but processing anyway");
}
let decompressedMessage = msg.m;
let skipMessage = false;
// Try to decompress if needed and enabled
if (compressionEnabled) {
try {
// First check if it's our compressed format
let parsedIncoming;
try {
parsedIncoming = JSON.parse(msg.m);
// Look for our signature
if (parsedIncoming.type && ['O', 'C', 'B'].includes(parsedIncoming.type)) {
// updateDebugOutput(`Detected compressed message type: ${parsedIncoming.type}`);
// Skip empty candidate sets completely
if (parsedIncoming.type === 'C' && (!parsedIncoming.cand || parsedIncoming.cand === '')) {
updateDebugOutput("Skipping empty candidate set");
skipMessage = true;
statusEl.textContent = "Skipped empty candidate message - " + new Date().toLocaleTimeString();
return;
}
// Decompress
let decompressed = decompressMessage(msg.m);
// Skip if decompression returned null
if (decompressed === null) {
updateDebugOutput("Skipping null decompressed message");
skipMessage = true;
statusEl.textContent = "Skipped empty message - " + new Date().toLocaleTimeString();
return;
}
// Modify the UUID based on target side before sending
try {
let parsedDecompressed = JSON.parse(decompressed);
if (parsedDecompressed.UUID) {
parsedDecompressed.UUID = targetSide === 'left' ? myUUID.left : myUUID.right;
decompressed = JSON.stringify(parsedDecompressed);
}
} catch (uuidErr) {
updateDebugOutput(`Error modifying UUID: ${uuidErr.message}`);
}
decompressedMessage = decompressed;
// updateDebugOutput(`Successfully decompressed message: ${decompressedMessage.substring(0, 50)}...`);
}
} catch (e) {
// Not our compressed format or not JSON
updateDebugOutput(`Message doesn't appear to be compressed: ${e.message}`);
}
} catch (e) {
updateDebugOutput(`Error checking compression format: ${e.message}`);
}
}
if (!skipMessage && iframes[targetSide] && iframes[targetSide].contentWindow) {
// Send the message to the target iframe
updateDebugOutput(`Sending to ${targetSide} iframe: ${decompressedMessage}`);
iframes[targetSide].contentWindow.postMessage({"function":"routeMessage", "value":decompressedMessage}, '*');
statusEl.textContent = `Message processed successfully to ${targetSide} - ${new Date().toLocaleTimeString()}`;
} else if (!iframes[targetSide] || !iframes[targetSide].contentWindow) {
updateDebugOutput(`Target iframe ${targetSide} not ready yet`);
statusEl.textContent = `Error: Target iframe ${targetSide} not ready - ${new Date().toLocaleTimeString()}`;
}
} catch(e) {
statusEl.textContent = "Error: " + e.message;
console.error("Failed to process message:", e);
updateDebugOutput(`Error processing message: ${e.message}`);
}
}
function transferMessage(targetSide, message) {
try {
// Check if this is a message we should auto-relay
try {
const msgObj = JSON.parse(message);
const sourceSide = targetSide === 'left' ? 'right' : 'left';
if (handleAutoRelay(msgObj, sourceSide)) {
// We handled it with auto-relay, so just remove from queue
if (sourceSide === 'left' && messageQueues.left.length > 0) {
messageQueues.left.shift();
} else if (sourceSide === 'right' && messageQueues.right.length > 0) {
messageQueues.right.shift();
}
// Update display
const outputArea = document.getElementById("outputArea");
outputArea.value = "";
return; // Exit early since we handled the command
}
} catch (e) {
updateDebugOutput(`Error checking for auto-relay during transfer: ${e.message}`);
}
// Otherwise, process normally
processIncomingMessage(message, targetSide);
// Remove from the queue after successful transfer
if (targetSide === 'right' && messageQueues.left.length > 0) {
messageQueues.left.shift();
} else if (targetSide === 'left' && messageQueues.right.length > 0) {
messageQueues.right.shift();
}
// Update display
const outputArea = document.getElementById("outputArea");
outputArea.value = "";
} catch (e) {
updateDebugOutput(`Error transferring message: ${e.message}`);
}
}
// Function to toggle auto transfer
function toggleAutoTransfer() {
autoTransferEnabled = !autoTransferEnabled;
const btn = document.getElementById("autoTransfer");
if (autoTransferEnabled) {
btn.textContent = "Disable Auto Transfer";
btn.classList.add("auto-transfer-active");
// updateDebugOutput("Auto transfer enabled");
} else {
btn.textContent = "Enable Auto Transfer";
btn.classList.remove("auto-transfer-active");
// updateDebugOutput("Auto transfer disabled");
}
}
// Initialize the page once DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Create and add left iframe
iframes.left = document.createElement("iframe");
iframes.left.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;";
iframes.left.src = "../?bypass&waitice&password=false&room=default&push=local&s&debug";
document.getElementById("iframe-left").appendChild(iframes.left);
// Create and add right iframe with a slight delay to prevent initialization conflicts
setTimeout(() => {
iframes.right = document.createElement("iframe");
iframes.right.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;";
iframes.right.src = "../?bypass&waitice&password=false&room=default&push=local&solo";
document.getElementById("iframe-right").appendChild(iframes.right);
updateDebugOutput("Both iframes initialized");
}, 500);
// Setup debug toggle
document.getElementById("debugToggle").addEventListener("click", function() {
debugMode = !debugMode;
document.getElementById("debugOutput").style.display = debugMode ? "block" : "none";
});
// Setup compression toggle
document.getElementById("enableCompression").addEventListener("change", function() {
compressionEnabled = this.checked;
// Clear any previous messageQueue when changing compression mode
// to avoid mixing compressed and uncompressed messages
if (messageQueues.left.length > 0 || messageQueues.right.length > 0) {
if (confirm("Changing compression mode will clear the current message queues. Continue?")) {
messageQueues.left = [];
messageQueues.right = [];
document.getElementById("outputArea").value = "";
document.getElementById("outputStatus").textContent = "Message queues cleared due to compression mode change";
} else {
// Revert the checkbox if user cancels
this.checked = compressionEnabled;
return;
}
}
compressionEnabled = this.checked;
// updateDebugOutput(`Compression ${compressionEnabled ? 'enabled' : 'disabled'}`);
});
// Setup manual transfer buttons
document.getElementById("leftToRight").addEventListener("click", function() {
if (messageQueues.left.length > 0) {
const message = messageQueues.left[0];
transferMessage('right', message);
// updateDebugOutput("Manually transferred message from left to right");
// Update output area to show next message if available
if (messageQueues.left.length > 0) {
document.getElementById("outputArea").value = `[LEFT] ${messageQueues.left[0]}`;
} else {
document.getElementById("outputArea").value = "";
}
} else {
// updateDebugOutput("No messages in left queue to transfer");
}
});
document.getElementById("rightToLeft").addEventListener("click", function() {
if (messageQueues.right.length > 0) {
const message = messageQueues.right[0];
transferMessage('left', message);
// updateDebugOutput("Manually transferred message from right to left");
// Update output area to show next message if available
if (messageQueues.right.length > 0) {
document.getElementById("outputArea").value = `[RIGHT] ${messageQueues.right[0]}`;
} else {
document.getElementById("outputArea").value = "";
}
} else {
// updateDebugOutput("No messages in right queue to transfer");
}
});
// Setup auto transfer toggle
document.getElementById("autoTransfer").addEventListener("click", toggleAutoTransfer);
// Set up the message event handler for iframe communication
window.addEventListener("message", function(e) {
// Check if message is from one of our iframes
if (e.source === iframes.left.contentWindow) {
if ("bypass" in e.data) {
manualSend(e.data.bypass, 'left');
}
} else if (e.source === iframes.right.contentWindow) {
if ("bypass" in e.data) {
manualSend(e.data.bypass, 'right');
}
}
});
// Enable debug mode by default for easier troubleshooting
debugMode = true;
document.getElementById("debugOutput").style.display = "block";
// updateDebugOutput("Debug mode enabled by default");
// updateDebugOutput("Dual iframe setup with auto-transfer capability");
});
</script>
</body>
</html>