mirror of
https://github.com/SrIzan10/vdo.ninja.git
synced 2026-05-01 11:05:24 +00:00
540 lines
22 KiB
HTML
540 lines
22 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>ICE Candidates Viewer</title>
|
||
<style>
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
background: #f5f5f5;
|
||
}
|
||
.container {
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 30px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
}
|
||
h1 {
|
||
color: #333;
|
||
margin-bottom: 30px;
|
||
}
|
||
.controls {
|
||
margin-bottom: 20px;
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
button {
|
||
background: #4CAF50;
|
||
color: white;
|
||
border: none;
|
||
padding: 10px 20px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
transition: background 0.3s;
|
||
}
|
||
button:hover {
|
||
background: #45a049;
|
||
}
|
||
button:disabled {
|
||
background: #ccc;
|
||
cursor: not-allowed;
|
||
}
|
||
.status {
|
||
margin: 20px 0;
|
||
padding: 10px;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
.status.info {
|
||
background: #e3f2fd;
|
||
color: #1976d2;
|
||
}
|
||
.status.error {
|
||
background: #ffebee;
|
||
color: #c62828;
|
||
}
|
||
.status.success {
|
||
background: #e8f5e9;
|
||
color: #2e7d32;
|
||
}
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin-top: 20px;
|
||
}
|
||
th, td {
|
||
text-align: left;
|
||
padding: 12px;
|
||
border-bottom: 1px solid #e0e0e0;
|
||
}
|
||
th {
|
||
background: #f8f9fa;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
tr:hover {
|
||
background: #f5f5f5;
|
||
}
|
||
.type-badge {
|
||
display: inline-block;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
.type-host { background: #e8f5e9; color: #2e7d32; }
|
||
.type-srflx { background: #e3f2fd; color: #1976d2; }
|
||
.type-relay { background: #fff3e0; color: #f57c00; }
|
||
.type-prflx { background: #f3e5f5; color: #7b1fa2; }
|
||
.server-select {
|
||
margin-bottom: 20px;
|
||
}
|
||
select {
|
||
padding: 8px 12px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
font-size: 16px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>WebRTC ICE Candidates Viewer</h1>
|
||
|
||
<div class="server-select">
|
||
<label for="stunServer">STUN Server: </label>
|
||
<select id="stunServer">
|
||
<option value="stun:stun.l.google.com:19302">Google STUN (stun.l.google.com:19302)</option>
|
||
<option value="stun:stun1.l.google.com:19302">Google STUN 1 (stun1.l.google.com:19302)</option>
|
||
<option value="stun:stun2.l.google.com:19302">Google STUN 2 (stun2.l.google.com:19302)</option>
|
||
<option value="stun:stun3.l.google.com:19302">Google STUN 3 (stun3.l.google.com:19302)</option>
|
||
<option value="stun:stun4.l.google.com:19302">Google STUN 4 (stun4.l.google.com:19302)</option>
|
||
<option value="stun:stun.cloudflare.com:3478">Cloudflare STUN (stun.cloudflare.com:3478)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<button id="gatherBtn">Gather ICE Candidates</button>
|
||
<button id="clearBtn">Clear Results</button>
|
||
<button id="checkBtn">Check Browser Settings</button>
|
||
</div>
|
||
|
||
<div id="status" class="status" style="display: none;"></div>
|
||
|
||
<div id="checkResults" class="check-results" style="display: none;"></div>
|
||
|
||
<div id="results" style="display: none;">
|
||
<h2>ICE Candidates</h2>
|
||
<table id="candidatesTable">
|
||
<thead>
|
||
<tr>
|
||
<th>Type</th>
|
||
<th>Protocol</th>
|
||
<th>IP Address</th>
|
||
<th>Port</th>
|
||
<th>Priority</th>
|
||
<th>Foundation</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="candidatesBody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let pc = null;
|
||
const candidates = [];
|
||
|
||
const gatherBtn = document.getElementById('gatherBtn');
|
||
const clearBtn = document.getElementById('clearBtn');
|
||
const checkBtn = document.getElementById('checkBtn');
|
||
const statusDiv = document.getElementById('status');
|
||
const resultsDiv = document.getElementById('results');
|
||
const candidatesBody = document.getElementById('candidatesBody');
|
||
const stunServerSelect = document.getElementById('stunServer');
|
||
const checkResultsDiv = document.getElementById('checkResults');
|
||
|
||
// OBS Mode - hide unnecessary UI and auto-run
|
||
if (window.obsstudio !== undefined) {
|
||
document.querySelector('h1').style.display = 'none';
|
||
document.querySelector('.server-select').style.display = 'none';
|
||
document.querySelector('.controls').style.display = 'none';
|
||
document.querySelector('.container').style.padding = '10px';
|
||
document.body.style.padding = '10px';
|
||
|
||
// Compact styles for OBS
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
.check-item { margin: 5px 0; padding: 8px; }
|
||
.check-description { font-size: 13px; }
|
||
.settings-list li { font-size: 13px; margin: 3px 0; }
|
||
table { font-size: 13px; }
|
||
th, td { padding: 6px; }
|
||
h2 { margin: 10px 0 10px 0; font-size: 18px; }
|
||
.status { margin: 10px 0; padding: 8px; }
|
||
`;
|
||
document.head.appendChild(style);
|
||
}
|
||
|
||
function showStatus(message, type = 'info') {
|
||
statusDiv.textContent = message;
|
||
statusDiv.className = `status ${type}`;
|
||
statusDiv.style.display = 'block';
|
||
}
|
||
|
||
function parseCandidate(candidateString) {
|
||
const parts = candidateString.split(' ');
|
||
return {
|
||
foundation: parts[0],
|
||
component: parts[1],
|
||
protocol: parts[2],
|
||
priority: parts[3],
|
||
ip: parts[4],
|
||
port: parts[5],
|
||
type: parts[7]
|
||
};
|
||
}
|
||
|
||
function addCandidateToTable(candidate) {
|
||
const parsed = parseCandidate(candidate.candidate);
|
||
|
||
const row = document.createElement('tr');
|
||
row.innerHTML = `
|
||
<td><span class="type-badge type-${parsed.type}">${parsed.type.toUpperCase()}</span></td>
|
||
<td>${parsed.protocol.toUpperCase()}</td>
|
||
<td>${parsed.ip}</td>
|
||
<td>${parsed.port}</td>
|
||
<td>${parsed.priority}</td>
|
||
<td>${parsed.foundation}</td>
|
||
`;
|
||
candidatesBody.appendChild(row);
|
||
}
|
||
|
||
async function detectWebRTCRestrictions() {
|
||
const restrictions = [];
|
||
|
||
// Test for mDNS candidates (privacy mode)
|
||
const testConfig = {
|
||
iceServers: [{urls: 'stun:stun.l.google.com:19302'}]
|
||
};
|
||
|
||
try {
|
||
const testPc = new RTCPeerConnection(testConfig);
|
||
const testDc = testPc.createDataChannel('test');
|
||
|
||
let foundHost = false;
|
||
let foundSrflx = false;
|
||
let foundMdns = false;
|
||
let candidateCount = 0;
|
||
|
||
return new Promise((resolve) => {
|
||
const timeout = setTimeout(() => {
|
||
testPc.close();
|
||
|
||
if (candidateCount === 0) {
|
||
restrictions.push('WebRTC might be completely disabled');
|
||
}
|
||
if (!foundHost && !foundMdns) {
|
||
restrictions.push('Local IP addresses are hidden (privacy mode enabled)');
|
||
}
|
||
if (foundMdns) {
|
||
restrictions.push('mDNS candidates detected (local IPs anonymized)');
|
||
}
|
||
if (!foundSrflx) {
|
||
restrictions.push('STUN server connection might be blocked');
|
||
}
|
||
|
||
resolve(restrictions);
|
||
}, 3000);
|
||
|
||
testPc.onicecandidate = (event) => {
|
||
if (event.candidate) {
|
||
candidateCount++;
|
||
const parsed = parseCandidate(event.candidate.candidate);
|
||
|
||
if (parsed.type === 'host') {
|
||
if (parsed.ip.endsWith('.local')) {
|
||
foundMdns = true;
|
||
} else {
|
||
foundHost = true;
|
||
}
|
||
} else if (parsed.type === 'srflx') {
|
||
foundSrflx = true;
|
||
}
|
||
}
|
||
};
|
||
|
||
testPc.createOffer().then(offer => {
|
||
return testPc.setLocalDescription(offer);
|
||
}).catch(err => {
|
||
clearTimeout(timeout);
|
||
testPc.close();
|
||
restrictions.push('WebRTC API error: ' + err.message);
|
||
resolve(restrictions);
|
||
});
|
||
});
|
||
|
||
} catch (error) {
|
||
restrictions.push('WebRTC not available: ' + error.message);
|
||
return restrictions;
|
||
}
|
||
}
|
||
|
||
async function gatherCandidates() {
|
||
if (pc) {
|
||
pc.close();
|
||
pc = null;
|
||
}
|
||
|
||
candidates.length = 0;
|
||
candidatesBody.innerHTML = '';
|
||
gatherBtn.disabled = true;
|
||
|
||
showStatus('Detecting WebRTC restrictions...', 'info');
|
||
|
||
// Check for restrictions first
|
||
const restrictions = await detectWebRTCRestrictions();
|
||
if (restrictions.length > 0) {
|
||
showStatus('Detected restrictions: ' + restrictions.join('; '), 'error');
|
||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||
}
|
||
|
||
showStatus('Gathering ICE candidates...', 'info');
|
||
|
||
const stunServer = stunServerSelect.value;
|
||
const config = {
|
||
iceServers: [{ urls: stunServer }],
|
||
iceCandidatePoolSize: 10
|
||
};
|
||
|
||
try {
|
||
pc = new RTCPeerConnection(config);
|
||
|
||
// Create a data channel to trigger ICE gathering
|
||
const dc = pc.createDataChannel('test');
|
||
|
||
pc.onicecandidate = (event) => {
|
||
if (event.candidate) {
|
||
candidates.push(event.candidate);
|
||
addCandidateToTable(event.candidate);
|
||
resultsDiv.style.display = 'block';
|
||
|
||
// Count candidate types
|
||
const types = candidates.reduce((acc, c) => {
|
||
const type = parseCandidate(c.candidate).type;
|
||
acc[type] = (acc[type] || 0) + 1;
|
||
return acc;
|
||
}, {});
|
||
const typeStr = Object.entries(types).map(([k,v]) => `${k}: ${v}`).join(', ');
|
||
|
||
showStatus(`Gathering candidates... Found ${candidates.length} (${typeStr})`, 'info');
|
||
} else {
|
||
showStatus(`Gathering complete. Found ${candidates.length} candidates.`, 'success');
|
||
gatherBtn.disabled = false;
|
||
}
|
||
};
|
||
|
||
pc.onicegatheringstatechange = () => {
|
||
console.log('ICE gathering state:', pc.iceGatheringState);
|
||
if (pc.iceGatheringState === 'complete') {
|
||
showStatus(`Gathering complete. Found ${candidates.length} candidates.`, 'success');
|
||
gatherBtn.disabled = false;
|
||
}
|
||
};
|
||
|
||
pc.oniceconnectionstatechange = () => {
|
||
console.log('ICE connection state:', pc.iceConnectionState);
|
||
};
|
||
|
||
// Create offer with ICE restart
|
||
const offer = await pc.createOffer({
|
||
offerToReceiveAudio: true,
|
||
offerToReceiveVideo: true,
|
||
iceRestart: true
|
||
});
|
||
await pc.setLocalDescription(offer);
|
||
|
||
// Set a timeout in case gathering takes too long
|
||
setTimeout(() => {
|
||
if (gatherBtn.disabled) {
|
||
showStatus(`Gathering timed out. Found ${candidates.length} candidates.`, 'success');
|
||
gatherBtn.disabled = false;
|
||
}
|
||
}, 10000);
|
||
|
||
} catch (error) {
|
||
showStatus(`Error: ${error.message}`, 'error');
|
||
gatherBtn.disabled = false;
|
||
console.error('WebRTC error:', error);
|
||
}
|
||
}
|
||
|
||
function clearResults() {
|
||
if (pc) {
|
||
pc.close();
|
||
pc = null;
|
||
}
|
||
candidates.length = 0;
|
||
candidatesBody.innerHTML = '';
|
||
resultsDiv.style.display = 'none';
|
||
statusDiv.style.display = 'none';
|
||
checkResultsDiv.style.display = 'none';
|
||
}
|
||
|
||
async function checkBrowserSettings() {
|
||
clearResults();
|
||
checkResultsDiv.innerHTML = '';
|
||
checkResultsDiv.style.display = 'block';
|
||
statusDiv.style.display = 'none';
|
||
|
||
// Check basic WebRTC support
|
||
if (typeof RTCPeerConnection === 'undefined') {
|
||
addCheckItem('error', 'WebRTC Not Supported',
|
||
'WebRTC is not available in this browser. This could mean you\'re using an older browser or WebRTC has been disabled.');
|
||
return;
|
||
}
|
||
|
||
addCheckItem('success', 'WebRTC API Available',
|
||
'The browser supports WebRTC and the API is accessible.');
|
||
|
||
// Test restriction detection
|
||
showStatus('Testing WebRTC restrictions...', 'info');
|
||
const restrictions = await detectWebRTCRestrictions();
|
||
statusDiv.style.display = 'none';
|
||
|
||
// Analyze restrictions
|
||
if (restrictions.length === 0) {
|
||
addCheckItem('success', 'No WebRTC Restrictions',
|
||
'Your browser is configured to allow full WebRTC functionality. You should see all types of ICE candidates including local IP addresses.');
|
||
}
|
||
|
||
if (restrictions.includes('mDNS candidates detected (local IPs anonymized)')) {
|
||
addCheckItem('warning', 'mDNS Anonymization Active',
|
||
'Your browser is hiding local IP addresses by converting them to .local addresses (like abc123.local). ' +
|
||
'This is a privacy feature that prevents websites from seeing your real local network addresses. ' +
|
||
'Expected: 192.168.1.100, but seeing: abc123def.local',
|
||
[
|
||
'In Chrome: Go to chrome://flags/#enable-webrtc-hide-local-ips-with-mdns and set to "Disabled"',
|
||
'This is often enabled by default in newer Chrome versions for privacy'
|
||
]);
|
||
}
|
||
|
||
if (restrictions.includes('Local IP addresses are hidden (privacy mode enabled)')) {
|
||
addCheckItem('warning', 'Local IPs Completely Hidden',
|
||
'Your browser is not exposing ANY local IP addresses. This is a strict privacy setting that prevents WebRTC from revealing network information.',
|
||
[
|
||
'Check browser privacy settings',
|
||
'Disable VPN browser extensions',
|
||
'Check privacy-focused extensions like uBlock Origin settings'
|
||
]);
|
||
}
|
||
|
||
if (restrictions.includes('STUN server connection might be blocked')) {
|
||
addCheckItem('error', 'STUN Server Blocked',
|
||
'Cannot connect to STUN servers to determine your public IP address. This means only local candidates will be available.',
|
||
[
|
||
'Check firewall settings',
|
||
'Verify network allows UDP traffic on port 3478/19302',
|
||
'Try a different network connection'
|
||
]);
|
||
}
|
||
|
||
// Environment checks
|
||
if (window.self !== window.top) {
|
||
addCheckItem('info', 'Running in iFrame',
|
||
'This page is running inside an iframe (like Claude.ai\'s artifact viewer). ' +
|
||
'iFrames often have additional security restrictions that limit WebRTC functionality.');
|
||
}
|
||
|
||
if (location.protocol === 'https:') {
|
||
addCheckItem('success', 'Secure Context (HTTPS)',
|
||
'Running on HTTPS. WebRTC has full functionality in secure contexts.');
|
||
} else if (location.protocol === 'http:') {
|
||
addCheckItem('warning', 'Insecure Context (HTTP)',
|
||
'Running on HTTP. Some browsers restrict WebRTC features on non-secure origins.',
|
||
['Consider moving to HTTPS for full WebRTC support']);
|
||
}
|
||
|
||
// Browser-specific guidance
|
||
const ua = navigator.userAgent;
|
||
if (ua.includes('Chrome')) {
|
||
addCheckItem('info', 'Chrome Browser Detected',
|
||
'Chrome has several flags and settings that control WebRTC behavior:',
|
||
[
|
||
'<b>chrome://flags/#enable-webrtc-hide-local-ips-with-mdns</b> - Controls mDNS anonymization',
|
||
'<b>chrome://settings/content/location</b> - Location permissions can affect WebRTC',
|
||
'<b>chrome://settings/privacy</b> - Check "WebRTC IP handling policy"',
|
||
'Extensions like WebRTC Leak Prevent, uBlock Origin, or Privacy Badger may block WebRTC'
|
||
]);
|
||
} else if (ua.includes('Firefox')) {
|
||
addCheckItem('info', 'Firefox Browser Detected',
|
||
'Firefox has about:config settings that control WebRTC:',
|
||
[
|
||
'<b>media.peerconnection.enabled</b> - Must be true (enables WebRTC)',
|
||
'<b>media.peerconnection.ice.default_address_only</b> - If true, only shows default network interface',
|
||
'<b>media.peerconnection.ice.no_host</b> - If true, hides all local IPs',
|
||
'<b>media.peerconnection.ice.proxy_only_if_behind_proxy</b> - Forces proxy use'
|
||
]);
|
||
}
|
||
|
||
// Check for OBS Studio
|
||
if (window.obsstudio !== undefined) {
|
||
addCheckItem('warning', 'OBS Browser Source',
|
||
'OBS browser sources hide local IPs by default. Restart OBS with:',
|
||
[
|
||
'<code style="word-break: break-all;">obs64.exe --disable-web-security --allow-running-insecure-content --ignore-certificate-errors --use-fake-ui-for-media-stream</code>'
|
||
]);
|
||
}
|
||
}
|
||
|
||
function addCheckItem(type, title, description, solutions = []) {
|
||
const item = document.createElement('div');
|
||
item.className = `check-item ${type}`;
|
||
|
||
let icon = '';
|
||
switch(type) {
|
||
case 'success': icon = '✅'; break;
|
||
case 'warning': icon = '⚠️'; break;
|
||
case 'error': icon = '❌'; break;
|
||
case 'info': icon = 'ℹ️'; break;
|
||
}
|
||
|
||
let html = `<div class="check-title">${icon} ${title}</div>`;
|
||
html += `<div class="check-description">${description}</div>`;
|
||
|
||
if (solutions.length > 0) {
|
||
html += '<ul class="settings-list">';
|
||
solutions.forEach(solution => {
|
||
html += `<li>${solution}</li>`;
|
||
});
|
||
html += '</ul>';
|
||
}
|
||
|
||
item.innerHTML = html;
|
||
checkResultsDiv.appendChild(item);
|
||
}
|
||
|
||
gatherBtn.addEventListener('click', gatherCandidates);
|
||
clearBtn.addEventListener('click', clearResults);
|
||
checkBtn.addEventListener('click', checkBrowserSettings);
|
||
|
||
// Auto-run if in OBS
|
||
if (window.obsstudio !== undefined) {
|
||
setTimeout(async () => {
|
||
// First check browser settings
|
||
await checkBrowserSettings();
|
||
// Then gather candidates
|
||
setTimeout(() => {
|
||
gatherCandidates();
|
||
}, 100);
|
||
}, 500);
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |