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

1072 lines
40 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SDP Compression Tool</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
line-height: 1.6;
color: #333;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
textarea {
width: 100%;
height: 200px;
padding: 10px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 4px;
font-family: monospace;
margin-bottom: 10px;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
margin-bottom: 10px;
transition: background-color 0.3s;
}
button:hover {
background-color: #45a049;
}
.qrcode-container {
margin-top: 20px;
text-align: center;
}
.output {
margin-top: 20px;
width: 100%;
height: max(100px, calc(100vh - 400px));
overflow-y: auto;
background-color: #f5f5f5;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
white-space: pre-wrap;
}
h3 {
margin-top: 20px;
margin-bottom: 10px;
}
.stats {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
}
.stat-box {
background-color: #f0f0f0;
padding: 15px;
border-radius: 4px;
flex: 1;
min-width: 200px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.input-group {
margin-bottom: 15px;
}
.tabs {
display: flex;
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
}
.tab {
padding: 10px 20px;
cursor: pointer;
background-color: #f8f8f8;
border: 1px solid #ddd;
border-bottom: none;
margin-right: 5px;
border-radius: 4px 4px 0 0;
}
.tab.active {
background-color: white;
border-bottom: 1px solid white;
margin-bottom: -1px;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
</style>
</head>
<body>
<h1>SDP Compression Tool</h1>
<div class="tabs">
<div class="tab active" data-tab="compression">Compression</div>
<div class="tab" data-tab="testing">Testing</div>
<div class="tab" data-tab="ice">ICE Candidates</div>
</div>
<div id="compression" class="tab-content active">
<h2>SDP Compression/Decompression</h2>
<div class="container">
<div>
<h3>Input SDP</h3>
<textarea id="sdpInput" placeholder="Paste SDP here..."></textarea>
<button id="compressBtn">Compress</button>
<button id="compressQRBtn">Compress for QR</button>
<button id="generateSampleSDPBtn">Generate Sample SDP</button>
</div>
<div>
<h3>Compressed Output</h3>
<textarea id="compressedOutput" placeholder="Compressed SDP will appear here..."></textarea>
<button id="decompressBtn">Decompress</button>
<button id="decompressQRBtn">Decompress QR Format</button>
<button id="copyCompressedBtn">Copy</button>
</div>
</div>
<div class="stats">
<div class="stat-box">
<strong>Original Size:</strong> <span id="originalSize">0</span> bytes
</div>
<div class="stat-box">
<strong>Compressed Size:</strong> <span id="compressedSize">0</span> bytes
</div>
<div class="stat-box">
<strong>Compression Ratio:</strong> <span id="compressionRatio">0</span>%
</div>
</div>
<div class="qrcode-container">
<h3>QR Code</h3>
<div id="qrcode"></div>
</div>
<h3>Decompressed Result</h3>
<textarea id="decompressedOutput" placeholder="Decompressed SDP will appear here..."></textarea>
</div>
<div id="testing" class="tab-content">
<h2>SDP Compression Testing</h2>
<button id="runTestsBtn">Run All Tests</button>
<button id="validateSDPBtn">Validate Current SDP</button>
<h3>Test Output</h3>
<div id="testOutput" class="output"></div>
</div>
<div id="ice" class="tab-content">
<h2>ICE Candidates Compression</h2>
<div class="input-group">
<h3>ICE Candidates (JSON Array)</h3>
<textarea id="iceInput" placeholder='[{"candidate":"candidate:0 1 UDP 2122260223 192.168.1.2 56789 typ host","sdpMid":"0","sdpMLineIndex":0}]'></textarea>
<button id="compressIceBtn">Compress ICE</button>
<button id="generateSampleIceBtn">Generate Sample ICE</button>
</div>
<div class="input-group">
<h3>Compressed ICE</h3>
<textarea id="iceCompressedOutput" placeholder="Compressed ICE candidates will appear here..."></textarea>
<button id="decompressIceBtn">Decompress ICE</button>
<button id="copyCompressedIceBtn">Copy</button>
</div>
<div class="input-group">
<h3>Decompressed ICE</h3>
<textarea id="iceDecompressedOutput" placeholder="Decompressed ICE candidates will appear here..."></textarea>
</div>
</div>
<script>
function compressSDP(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' ? 'u' : 't';
const priority = parseInt(parts[3]);
const ip = parts[4];
const port = parseInt(parts[5]);
const type = parts[7];
let typeCode;
switch (type) {
case 'host':
typeCode = 'h';
break;
case 'srflx':
typeCode = 's';
break;
case 'relay':
typeCode = 'r';
break;
default:
typeCode = 'x';
}
let relatedIP = '';
let relatedPort = '';
const rAddrIndex = parts.indexOf('raddr');
if (rAddrIndex !== -1 && rAddrIndex + 1 < parts.length) {
relatedIP = parts[rAddrIndex + 1];
}
const rPortIndex = parts.indexOf('rport');
if (rPortIndex !== -1 && rPortIndex + 1 < parts.length) {
relatedPort = parts[rPortIndex + 1];
}
let candidateStr = `${foundation},${component},${protocol},${priorityToCompact(priority)},${ip},${port},${typeCode}`;
if (relatedIP && relatedPort) {
candidateStr += `,${relatedIP},${relatedPort}`;
}
candidates.push(candidateStr);
}
const compactFingerprint = hexToCompact(fingerprint);
let result = `C1|${isOffer ? 'o' : 'a'}|${iceUfrag}|${icePwd}|${compactFingerprint}`;
if (candidates.length > 0) {
result += `|${candidates.join('/')}`;
}
return result;
}
function decompressSDP(compressed) {
try {
const parts = compressed.split('|');
if (parts[0] !== 'C1') {
throw new Error('Unsupported compression version');
}
const isOffer = parts[1] === 'o';
const iceUfrag = parts[2];
const icePwd = parts[3];
const fingerprint = compactToHex(parts[4]);
// Format fingerprint correctly with colons every 2 characters
const formattedFingerprint = 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 ${formattedFingerprint}`,
`a=setup:${isOffer ? 'actpass' : 'active'}`,
'a=mid:0',
'a=sctp-port:5000',
'a=max-message-size:262144'
];
// Process candidates
if (parts.length > 5 && parts[5]) {
const candidates = parts[5].split('/');
for (const candidate of candidates) {
const candParts = candidate.split(',');
if (candParts.length < 7) continue;
// Extract candidate parts
const foundation = candParts[0];
const component = candParts[1];
const protocol = candParts[2] === 'u' ? 'udp' : 'tcp'; // Use lowercase protocol
const priority = compactToPriority(candParts[3]);
const ip = candParts[4];
const port = candParts[5];
let type;
switch (candParts[6]) {
case 'h':
type = 'host';
break;
case 's':
type = 'srflx';
break;
case 'r':
type = 'relay';
break;
default:
type = 'unknown';
}
// Build candidate line
let candidateLine = `a=candidate:${foundation} ${component} ${protocol} ${priority} ${ip} ${port} typ ${type}`;
// Add raddr/rport if available for srflx and relay candidates
if (type === 'srflx' || type === 'relay') {
// If related IP and port are specified in compression, use them
if (candParts.length >= 9 && candParts[7]) {
candidateLine += ` raddr ${candParts[7]} rport ${candParts[8]}`;
} else {
// Otherwise add defaults for Chrome compatibility
candidateLine += ` raddr 0.0.0.0 rport 0`;
}
}
// Add TCP type if needed
if (protocol === 'tcp') {
candidateLine += ' tcptype active';
}
// Add generation and network-cost
candidateLine += ' generation 0 network-cost 999';
sdpLines.push(candidateLine);
}
}
// Join with proper line endings
return sdpLines.join('\r\n') + '\r\n';
} catch (e) {
console.error("Error in decompressSDP:", e);
throw e;
}
}
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 compressNumber(num) {
return num.toString(36).toUpperCase();
}
function decompressNumber(compressed) {
return parseInt(compressed, 36);
}
async function validateSDPWithPeerConnection(sdp) {
return new Promise(async (resolve, reject) => {
try {
const pc = new RTCPeerConnection({
iceServers: [{
urls: 'stun:stun.l.google.com:19302'
}]
});
const type = sdp.includes('a=setup:actpass') ? 'offer' : 'answer';
const timeout = setTimeout(() => {
pc.close();
reject(new Error("SDP validation timed out"));
}, 5000);
pc.onicegatheringstatechange = () => {
if (pc.iceGatheringState === 'complete') {
clearTimeout(timeout);
pc.close();
resolve(true);
}
};
pc.onicecandidateerror = (event) => {
console.warn("ICE candidate error:", event);
};
pc.oniceconnectionstatechange = () => {
if (pc.iceConnectionState === 'failed' || pc.iceConnectionState === 'disconnected') {
clearTimeout(timeout);
pc.close();
resolve(false);
}
};
try {
if (type === 'offer') {
await pc.setRemoteDescription({
type,
sdp
});
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
} else {
pc.createDataChannel('validate');
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
await pc.setRemoteDescription({
type,
sdp
});
}
clearTimeout(timeout);
pc.close();
resolve(true);
} catch (err) {
clearTimeout(timeout);
pc.close();
console.error("Error validating SDP:", err);
reject(err);
}
} catch (err) {
reject(err);
}
});
}
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);
}
async function testSample(name, sdp) {
const logOutput = document.getElementById('testOutput');
logOutput.innerHTML += `\n--- Testing ${name} ---\n`;
try {
logOutput.innerHTML += `##### Original SDP:\n${sdp}\n`;
logOutput.innerHTML += `##### Original SDP length: ${sdp.length} bytes\n`;
const compressed = compressSDP(sdp);
logOutput.innerHTML += `##### Compressed SDP:\n${compressed}\n`;
logOutput.innerHTML += `##### Compressed length: ${compressed.length} bytes\n`;
logOutput.innerHTML += `##### Compression ratio: ${Math.round(compressed.length / sdp.length * 100)}%\n`;
const decompressed = decompressSDP(compressed);
logOutput.innerHTML += `##### Decompression successful: ${decompressed.length > 0}\n`;
logOutput.innerHTML += `##### Decompressed SDP:\n${decompressed}\n`;
logOutput.innerHTML += `##### Testing decompressed SDP with new peer connection...\n`;
try {
const valid = await validateSDPWithPeerConnection(decompressed);
logOutput.innerHTML += `SDP validation: ${valid ? "PASSED" : "FAILED"}\n`;
if (!valid) {
throw new Error(`SDP validation failed for ${name}`);
}
} catch (err) {
logOutput.innerHTML += `SDP validation error: ${err.message}\n`;
}
const qrCompressed = compressSDPForQR(sdp);
logOutput.innerHTML += `QR Compressed length: ${qrCompressed.length} bytes\n`;
logOutput.innerHTML += `QR Compression ratio: ${Math.round(qrCompressed.length / sdp.length * 100)}%\n`;
const qrDecompressed = decompressSDPFromQR(qrCompressed);
logOutput.innerHTML += `QR Decompression successful: ${qrDecompressed.length > 0}\n`;
return true;
} catch (err) {
logOutput.innerHTML += `Error testing ${name}: ${err.message}\n`;
throw err;
}
}
async function collectSDPSamples() {
const samples = {
offer: {
withCandidates: null,
withoutCandidates: null
},
answer: {
withCandidates: null,
withoutCandidates: null
}
};
const logOutput = document.getElementById('testOutput');
logOutput.innerHTML += "Collecting SDP samples...\n";
const pc1 = new RTCPeerConnection({
iceServers: [{
urls: 'stun:stun.l.google.com:19302'
}]
});
const pc2 = new RTCPeerConnection({
iceServers: [{
urls: 'stun:stun.l.google.com:19302'
}]
});
const dataChannel = pc1.createDataChannel('data');
const offer = await pc1.createOffer();
samples.offer.withoutCandidates = offer.sdp;
await pc1.setLocalDescription(offer);
await pc2.setRemoteDescription(offer);
const answer = await pc2.createAnswer();
samples.answer.withoutCandidates = answer.sdp;
await pc2.setLocalDescription(answer);
await pc1.setRemoteDescription(answer);
logOutput.innerHTML += "Waiting for ICE gathering to complete...\n";
await Promise.all([
waitForIceGathering(pc1),
waitForIceGathering(pc2)
]);
samples.offer.withCandidates = pc1.localDescription.sdp;
samples.answer.withCandidates = pc2.localDescription.sdp;
logOutput.innerHTML += "SDP samples collected successfully.\n";
dataChannel.close();
pc1.close();
pc2.close();
return samples;
}
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 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 sdpToQRCode(sdp, element) {
const compressed = compressSDPForQR(sdp);
new QRCode(element, {
text: compressed,
width: 128,
height: 128,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.M
});
return compressed;
}
function compressICECandidates(candidates) {
try {
if (!candidates || !candidates.length) return "";
const hostCandidates = candidates
.filter(c => c.candidate && c.candidate.includes('typ host') && !c.candidate.includes(':'))
.slice(0, 2);
if (!hostCandidates.length) return "";
const compressed = hostCandidates.map(c => {
const parts = c.candidate.split(' ');
return `${parts[0].replace('candidate:', '')},${parts[1]},${parts[2].toLowerCase() === 'udp' ? 'u' : 't'},${parts[4]},${parts[5]},h`;
});
return compressed.join('/');
} catch (e) {
console.error("Error compressing ICE candidates:", e);
return "";
}
}
function decompressICECandidates(compressed) {
try {
if (!compressed) return [];
const candidates = compressed.split('/').map(c => {
const parts = c.split(',');
if (parts.length < 6) return null;
const foundation = parts[0];
const component = parts[1];
const protocol = parts[2] === 'u' ? 'UDP' : 'TCP';
const ip = parts[3];
const port = parts[4];
const type = parts[5] === 'h' ? 'host' :
(parts[5] === 's' ? 'srflx' :
(parts[5] === 'r' ? 'relay' : 'host'));
const priority = type === 'host' ? 2122260223 :
(type === 'srflx' ? 1686052607 :
(type === 'relay' ? 1685987071 : 1684797951));
return {
candidate: `candidate:${foundation} ${component} ${protocol} ${priority} ${ip} ${port} typ ${type}`,
sdpMid: "0",
sdpMLineIndex: 0,
usernameFragment: null
};
}).filter(c => c !== null);
return candidates;
} catch (e) {
console.error("Error decompressing ICE candidates:", e);
return [];
}
}
function waitForIceGathering(pc) {
return new Promise(resolve => {
if (pc.iceGatheringState === 'complete') {
resolve();
return;
}
const checkState = () => {
if (pc.iceGatheringState === 'complete') {
pc.removeEventListener('icegatheringstatechange', checkState);
resolve();
}
};
pc.addEventListener('icegatheringstatechange', checkState);
setTimeout(resolve, 5000);
});
}
async function runAllTests() {
const logOutput = document.getElementById('testOutput');
logOutput.innerHTML = "=== SDP COMPRESSION/DECOMPRESSION TEST TOOL ===\n";
logOutput.innerHTML += "Generating and testing various SDP types...\n";
try {
const samples = await collectSDPSamples();
await testSample("Offer with candidates", samples.offer.withCandidates);
await testSample("Offer without candidates", samples.offer.withoutCandidates);
await testSample("Answer with candidates", samples.answer.withCandidates);
await testSample("Answer without candidates", samples.answer.withoutCandidates);
logOutput.innerHTML += "\n=== ALL TESTS COMPLETED SUCCESSFULLY ===\n";
} catch (err) {
logOutput.innerHTML += `Test failed: ${err.message}\n`;
console.error("Test failed:", err);
}
}
// When the DOM is loaded, set up event handlers
document.addEventListener('DOMContentLoaded', () => {
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
const tabId = tab.getAttribute('data-tab');
// Update active tab
document.querySelectorAll('.tab').forEach(t => {
t.classList.remove('active');
});
tab.classList.add('active');
// Update active content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(tabId).classList.add('active');
});
});
// SDP Compression Tab
document.getElementById('compressBtn').addEventListener('click', () => {
const sdpInput = document.getElementById('sdpInput').value;
try {
const compressed = compressSDP(sdpInput);
document.getElementById('compressedOutput').value = compressed;
document.getElementById('originalSize').textContent = sdpInput.length;
document.getElementById('compressedSize').textContent = compressed.length;
document.getElementById('compressionRatio').textContent =
Math.round(compressed.length / sdpInput.length * 100);
// Clear any previous QR code
document.getElementById('qrcode').innerHTML = '';
} catch (err) {
alert('Error compressing SDP: ' + err.message);
console.error('Error compressing SDP:', err);
}
});
document.getElementById('compressQRBtn').addEventListener('click', () => {
const sdpInput = document.getElementById('sdpInput').value;
try {
const compressed = compressSDPForQR(sdpInput);
document.getElementById('compressedOutput').value = compressed;
document.getElementById('originalSize').textContent = sdpInput.length;
document.getElementById('compressedSize').textContent = compressed.length;
document.getElementById('compressionRatio').textContent =
Math.round(compressed.length / sdpInput.length * 100);
// Generate QR code
document.getElementById('qrcode').innerHTML = '';
new QRCode(document.getElementById('qrcode'), {
text: compressed,
width: 200,
height: 200,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.M
});
} catch (err) {
alert('Error compressing SDP for QR: ' + err.message);
console.error('Error compressing SDP for QR:', err);
}
});
document.getElementById('decompressBtn').addEventListener('click', () => {
const compressedSDP = document.getElementById('compressedOutput').value;
try {
const decompressed = decompressSDP(compressedSDP);
document.getElementById('decompressedOutput').value = decompressed;
} catch (err) {
alert('Error decompressing SDP: ' + err.message);
console.error('Error decompressing SDP:', err);
}
});
document.getElementById('decompressQRBtn').addEventListener('click', () => {
const compressedSDP = document.getElementById('compressedOutput').value;
try {
const decompressed = decompressSDPFromQR(compressedSDP);
document.getElementById('decompressedOutput').value = decompressed;
} catch (err) {
alert('Error decompressing QR SDP: ' + err.message);
console.error('Error decompressing QR SDP:', err);
}
});
document.getElementById('copyCompressedBtn').addEventListener('click', () => {
const compressedOutput = document.getElementById('compressedOutput');
compressedOutput.select();
document.execCommand('copy');
alert('Compressed SDP copied to clipboard');
});
document.getElementById('generateSampleSDPBtn').addEventListener('click', async () => {
try {
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
pc.createDataChannel('sample');
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Wait for a short time to allow some ICE candidates to be generated
await new Promise(resolve => setTimeout(resolve, 1000));
document.getElementById('sdpInput').value = pc.localDescription.sdp;
pc.close();
} catch (err) {
alert('Error generating sample SDP: ' + err.message);
console.error('Error generating sample SDP:', err);
}
});
// Testing Tab
document.getElementById('runTestsBtn').addEventListener('click', async () => {
await runAllTests();
});
document.getElementById('validateSDPBtn').addEventListener('click', async () => {
const sdp = document.getElementById('decompressedOutput').value;
if (!sdp) {
alert('Please decompress an SDP first');
return;
}
const logOutput = document.getElementById('testOutput');
logOutput.innerHTML = "Validating SDP...\n";
try {
const valid = await validateSDPWithPeerConnection(sdp);
logOutput.innerHTML += `SDP validation: ${valid ? "PASSED" : "FAILED"}\n`;
} catch (err) {
logOutput.innerHTML += `SDP validation error: ${err.message}\n`;
console.error('SDP validation error:', err);
}
});
// ICE Candidates Tab
document.getElementById('compressIceBtn').addEventListener('click', () => {
try {
const iceInput = document.getElementById('iceInput').value;
const candidates = JSON.parse(iceInput);
const compressed = compressICECandidates(candidates);
document.getElementById('iceCompressedOutput').value = compressed;
} catch (err) {
alert('Error compressing ICE candidates: ' + err.message);
console.error('Error compressing ICE candidates:', err);
}
});
document.getElementById('decompressIceBtn').addEventListener('click', () => {
try {
const compressedIce = document.getElementById('iceCompressedOutput').value;
const candidates = decompressICECandidates(compressedIce);
document.getElementById('iceDecompressedOutput').value = JSON.stringify(candidates, null, 2);
} catch (err) {
alert('Error decompressing ICE candidates: ' + err.message);
console.error('Error decompressing ICE candidates:', err);
}
});
document.getElementById('copyCompressedIceBtn').addEventListener('click', () => {
const compressedOutput = document.getElementById('iceCompressedOutput');
compressedOutput.select();
document.execCommand('copy');
alert('Compressed ICE copied to clipboard');
});
document.getElementById('generateSampleIceBtn').addEventListener('click', async () => {
try {
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
const candidates = [];
pc.onicecandidate = (event) => {
if (event.candidate) {
candidates.push(event.candidate);
document.getElementById('iceInput').value = JSON.stringify(candidates, null, 2);
}
};
pc.createDataChannel('sample');
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Automatically close after 3 seconds
setTimeout(() => {
pc.close();
if (candidates.length === 0) {
document.getElementById('iceInput').value = JSON.stringify([{
candidate: "candidate:1853936366 1 UDP 2122260223 192.168.1.2 56789 typ host",
sdpMid: "0",
sdpMLineIndex: 0
}], null, 2);
}
}, 3000);
} catch (err) {
alert('Error generating sample ICE candidates: ' + err.message);
console.error('Error generating sample ICE candidates:', err);
}
});
});
</script>
</body>
</html>