mirror of
https://github.com/SrIzan10/vdo.ninja.git
synced 2026-05-01 11:05:24 +00:00
507 lines
18 KiB
HTML
507 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>VDO.Ninja SDK - TURN Only Mode Example</title>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: #f5f5f5;
|
|
}
|
|
.container {
|
|
background: white;
|
|
padding: 30px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
.section {
|
|
margin: 20px 0;
|
|
padding: 20px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 8px;
|
|
}
|
|
.info-box {
|
|
background: #e3f2fd;
|
|
border-left: 4px solid #2196f3;
|
|
padding: 15px;
|
|
margin: 20px 0;
|
|
}
|
|
.warning-box {
|
|
background: #fff3cd;
|
|
border-left: 4px solid #ffc107;
|
|
padding: 15px;
|
|
margin: 20px 0;
|
|
}
|
|
button {
|
|
background: #007bff;
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
margin: 5px;
|
|
font-size: 16px;
|
|
}
|
|
button:hover {
|
|
background: #0056b3;
|
|
}
|
|
button:disabled {
|
|
background: #ccc;
|
|
cursor: not-allowed;
|
|
}
|
|
input, select {
|
|
padding: 8px;
|
|
margin: 5px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
width: 200px;
|
|
}
|
|
video {
|
|
width: 100%;
|
|
max-width: 640px;
|
|
height: auto;
|
|
background: #000;
|
|
margin: 10px 0;
|
|
}
|
|
.status {
|
|
padding: 10px;
|
|
margin: 10px 0;
|
|
background: #f8f9fa;
|
|
border-radius: 4px;
|
|
font-family: monospace;
|
|
font-size: 14px;
|
|
}
|
|
.candidate-list {
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
background: #f8f9fa;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
padding: 10px;
|
|
margin: 10px 0;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
}
|
|
.candidate {
|
|
margin: 2px 0;
|
|
padding: 2px;
|
|
}
|
|
.candidate.host { color: #28a745; }
|
|
.candidate.srflx { color: #17a2b8; }
|
|
.candidate.relay { color: #dc3545; }
|
|
code {
|
|
background: #f8f9fa;
|
|
padding: 2px 4px;
|
|
border-radius: 3px;
|
|
font-family: monospace;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>VDO.Ninja SDK - TURN Only Mode Example</h1>
|
|
|
|
<div class="info-box">
|
|
<h3>What is TURN-Only Mode?</h3>
|
|
<p>TURN-only mode (relay mode) forces all WebRTC traffic through a TURN server, bypassing direct peer-to-peer connections. This is useful for:</p>
|
|
<ul>
|
|
<li>Testing TURN server functionality</li>
|
|
<li>Ensuring privacy by hiding IP addresses</li>
|
|
<li>Corporate networks with strict firewall rules</li>
|
|
<li>Scenarios requiring traffic to go through specific servers</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="warning-box">
|
|
<h3>⚠️ Important Notes</h3>
|
|
<ul>
|
|
<li>TURN-only mode requires valid TURN server credentials</li>
|
|
<li>Connection quality may be lower due to relay overhead</li>
|
|
<li>Bandwidth costs increase as all traffic goes through TURN</li>
|
|
<li>The default TURN servers may have usage limits</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Configuration Section -->
|
|
<div class="section">
|
|
<h2>Configuration</h2>
|
|
|
|
<label>ICE Transport Policy:</label>
|
|
<select id="icePolicy">
|
|
<option value="all">All (Default - Direct + TURN)</option>
|
|
<option value="relay">Relay Only (TURN Only)</option>
|
|
</select>
|
|
|
|
<br><br>
|
|
|
|
<label>Custom TURN Server (optional):</label><br>
|
|
<input type="text" id="turnUrl" placeholder="turn:turn.example.com:3478" style="width: 300px;"><br>
|
|
<input type="text" id="turnUser" placeholder="Username"><br>
|
|
<input type="password" id="turnPass" placeholder="Password"><br>
|
|
|
|
<button onclick="testConfiguration()">Test Configuration</button>
|
|
</div>
|
|
|
|
<!-- ICE Candidates Monitor -->
|
|
<div class="section">
|
|
<h2>ICE Candidates Monitor</h2>
|
|
<p>This shows what types of ICE candidates are being gathered:</p>
|
|
|
|
<button onclick="startCandidateTest()" id="candidateTestBtn">Start Candidate Test</button>
|
|
<button onclick="stopCandidateTest()" id="stopCandidateBtn" disabled>Stop Test</button>
|
|
|
|
<div id="candidateStatus" class="status">Ready to test...</div>
|
|
<div id="candidateList" class="candidate-list">
|
|
<em>No candidates collected yet</em>
|
|
</div>
|
|
|
|
<div id="candidateSummary" style="margin-top: 10px;">
|
|
<strong>Summary:</strong>
|
|
<span id="hostCount">Host: 0</span> |
|
|
<span id="srflxCount">Server Reflexive: 0</span> |
|
|
<span id="relayCount">Relay: 0</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Connection Test -->
|
|
<div class="section">
|
|
<h2>TURN Connection Test</h2>
|
|
<p>Test actual connection establishment using selected ICE policy:</p>
|
|
|
|
<input type="text" id="roomName" placeholder="Room name" value="turn-test-room">
|
|
<button onclick="startConnectionTest()" id="connectTestBtn">Start Connection Test</button>
|
|
<button onclick="stopConnectionTest()" id="stopConnectBtn" disabled>Stop Test</button>
|
|
|
|
<div id="connectionStatus" class="status">Ready to test...</div>
|
|
|
|
<div style="display: flex; gap: 20px; margin-top: 20px;">
|
|
<div style="flex: 1;">
|
|
<h3>Publisher</h3>
|
|
<video id="pubVideo" autoplay playsinline muted></video>
|
|
<div id="pubStats" class="status">-</div>
|
|
</div>
|
|
<div style="flex: 1;">
|
|
<h3>Viewer</h3>
|
|
<video id="viewVideo" autoplay playsinline></video>
|
|
<div id="viewStats" class="status">-</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Code Examples -->
|
|
<div class="section">
|
|
<h2>Code Examples</h2>
|
|
|
|
<h3>1. Basic TURN-Only Configuration</h3>
|
|
<pre><code>const sdk = new VDONinjaSDK({
|
|
iceTransportPolicy: 'relay', // Forces TURN-only mode
|
|
turnServers: [
|
|
{
|
|
urls: 'turn:your-turn-server.com:3478',
|
|
username: 'your-username',
|
|
credential: 'your-password'
|
|
}
|
|
]
|
|
});</code></pre>
|
|
|
|
<h3>2. Quick View with TURN-Only</h3>
|
|
<pre><code>const sdk = new VDONinjaSDK({
|
|
iceTransportPolicy: 'relay'
|
|
});
|
|
|
|
const { pc, cleanup } = await sdk.quickView({
|
|
streamID: 'some-stream',
|
|
room: 'secure-room'
|
|
});
|
|
|
|
// All connections will use TURN relay</code></pre>
|
|
|
|
<h3>3. Detecting Connection Type</h3>
|
|
<pre><code>pc.onicecandidate = (event) => {
|
|
if (event.candidate) {
|
|
const candidate = event.candidate.candidate;
|
|
|
|
if (candidate.includes('typ host')) {
|
|
console.log('Host candidate (direct)');
|
|
} else if (candidate.includes('typ srflx')) {
|
|
console.log('Server reflexive (STUN)');
|
|
} else if (candidate.includes('typ relay')) {
|
|
console.log('Relay candidate (TURN)');
|
|
}
|
|
}
|
|
};</code></pre>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="../vdoninja-sdk.js"></script>
|
|
<script>
|
|
let testSDK = null;
|
|
let pubSDK = null;
|
|
let viewSDK = null;
|
|
let candidateStats = { host: 0, srflx: 0, relay: 0 };
|
|
|
|
// Test configuration
|
|
async function testConfiguration() {
|
|
const policy = document.getElementById('icePolicy').value;
|
|
const turnUrl = document.getElementById('turnUrl').value;
|
|
const turnUser = document.getElementById('turnUser').value;
|
|
const turnPass = document.getElementById('turnPass').value;
|
|
|
|
console.log('Testing configuration:', { policy, turnUrl });
|
|
|
|
// Build config
|
|
const config = {
|
|
debug: true,
|
|
iceTransportPolicy: policy
|
|
};
|
|
|
|
// Add custom TURN if provided
|
|
if (turnUrl && turnUser && turnPass) {
|
|
config.turnServers = [{
|
|
urls: turnUrl,
|
|
username: turnUser,
|
|
credential: turnPass
|
|
}];
|
|
}
|
|
|
|
try {
|
|
const sdk = new VDONinjaSDK(config);
|
|
await sdk.connect();
|
|
|
|
// Create a test connection to check config
|
|
const conn = sdk._createConnection('test', 'publisher');
|
|
const pcConfig = conn.pc.getConfiguration();
|
|
|
|
console.log('RTCPeerConnection config:', pcConfig);
|
|
alert(`Configuration valid!\n\nICE Policy: ${pcConfig.iceTransportPolicy || 'all'}\nICE Servers: ${pcConfig.iceServers.length}`);
|
|
|
|
conn.pc.close();
|
|
await sdk.disconnect();
|
|
|
|
} catch (error) {
|
|
alert(`Configuration error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Start candidate collection test
|
|
async function startCandidateTest() {
|
|
const policy = document.getElementById('icePolicy').value;
|
|
const status = document.getElementById('candidateStatus');
|
|
const list = document.getElementById('candidateList');
|
|
|
|
// Reset stats
|
|
candidateStats = { host: 0, srflx: 0, relay: 0 };
|
|
updateCandidateSummary();
|
|
|
|
status.textContent = `Starting candidate test with policy: ${policy}`;
|
|
list.innerHTML = '';
|
|
|
|
try {
|
|
testSDK = new VDONinjaSDK({
|
|
debug: true,
|
|
iceTransportPolicy: policy
|
|
});
|
|
|
|
await testSDK.connect();
|
|
|
|
// Create a dummy connection to gather candidates
|
|
const conn = testSDK._createConnection('test', 'publisher');
|
|
|
|
conn.pc.onicecandidate = (event) => {
|
|
if (event.candidate) {
|
|
const candidate = event.candidate.candidate;
|
|
let type = 'unknown';
|
|
let className = '';
|
|
|
|
if (candidate.includes('typ host')) {
|
|
type = 'HOST';
|
|
className = 'host';
|
|
candidateStats.host++;
|
|
} else if (candidate.includes('typ srflx')) {
|
|
type = 'SRFLX';
|
|
className = 'srflx';
|
|
candidateStats.srflx++;
|
|
} else if (candidate.includes('typ relay')) {
|
|
type = 'RELAY';
|
|
className = 'relay';
|
|
candidateStats.relay++;
|
|
}
|
|
|
|
const candidateDiv = document.createElement('div');
|
|
candidateDiv.className = `candidate ${className}`;
|
|
candidateDiv.textContent = `[${type}] ${candidate}`;
|
|
list.appendChild(candidateDiv);
|
|
|
|
updateCandidateSummary();
|
|
}
|
|
};
|
|
|
|
// Create offer to start gathering
|
|
await conn.pc.createOffer();
|
|
|
|
status.textContent = `Gathering candidates with policy: ${policy}`;
|
|
|
|
document.getElementById('candidateTestBtn').disabled = true;
|
|
document.getElementById('stopCandidateBtn').disabled = false;
|
|
|
|
} catch (error) {
|
|
status.textContent = `Error: ${error.message}`;
|
|
stopCandidateTest();
|
|
}
|
|
}
|
|
|
|
// Stop candidate test
|
|
async function stopCandidateTest() {
|
|
if (testSDK) {
|
|
await testSDK.disconnect();
|
|
testSDK = null;
|
|
}
|
|
|
|
document.getElementById('candidateStatus').textContent = 'Test stopped';
|
|
document.getElementById('candidateTestBtn').disabled = false;
|
|
document.getElementById('stopCandidateBtn').disabled = true;
|
|
}
|
|
|
|
// Update candidate summary
|
|
function updateCandidateSummary() {
|
|
document.getElementById('hostCount').textContent = `Host: ${candidateStats.host}`;
|
|
document.getElementById('srflxCount').textContent = `Server Reflexive: ${candidateStats.srflx}`;
|
|
document.getElementById('relayCount').textContent = `Relay: ${candidateStats.relay}`;
|
|
}
|
|
|
|
// Start connection test
|
|
async function startConnectionTest() {
|
|
const policy = document.getElementById('icePolicy').value;
|
|
const room = document.getElementById('roomName').value;
|
|
const status = document.getElementById('connectionStatus');
|
|
|
|
if (!room) {
|
|
alert('Please enter a room name');
|
|
return;
|
|
}
|
|
|
|
status.textContent = `Starting connection test with policy: ${policy}`;
|
|
|
|
try {
|
|
// Get user media
|
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
video: true,
|
|
audio: true
|
|
});
|
|
|
|
document.getElementById('pubVideo').srcObject = stream;
|
|
|
|
// Publisher SDK
|
|
pubSDK = new VDONinjaSDK({
|
|
debug: true,
|
|
iceTransportPolicy: policy
|
|
});
|
|
|
|
const { streamID, cleanup: pubCleanup } = await pubSDK.quickPublish({
|
|
stream,
|
|
room,
|
|
label: `TURN Test - ${policy}`
|
|
});
|
|
|
|
status.textContent = `Publisher ready: ${streamID}. Starting viewer...`;
|
|
|
|
// Give publisher time to be ready
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
// Viewer SDK
|
|
viewSDK = new VDONinjaSDK({
|
|
debug: true,
|
|
iceTransportPolicy: policy
|
|
});
|
|
|
|
const { pc, cleanup: viewCleanup } = await viewSDK.quickView({
|
|
streamID,
|
|
room
|
|
});
|
|
|
|
// Monitor connection
|
|
pc.ontrack = (event) => {
|
|
document.getElementById('viewVideo').srcObject = event.streams[0];
|
|
};
|
|
|
|
pc.onconnectionstatechange = () => {
|
|
status.textContent = `Connection state: ${pc.connectionState}`;
|
|
updateConnectionStats(pc);
|
|
};
|
|
|
|
// Monitor ICE connection specifically
|
|
pc.oniceconnectionstatechange = () => {
|
|
console.log(`ICE state: ${pc.iceConnectionState}`);
|
|
};
|
|
|
|
document.getElementById('connectTestBtn').disabled = true;
|
|
document.getElementById('stopConnectBtn').disabled = false;
|
|
|
|
} catch (error) {
|
|
status.textContent = `Error: ${error.message}`;
|
|
stopConnectionTest();
|
|
}
|
|
}
|
|
|
|
// Stop connection test
|
|
async function stopConnectionTest() {
|
|
const status = document.getElementById('connectionStatus');
|
|
|
|
// Stop video tracks
|
|
const pubVideo = document.getElementById('pubVideo');
|
|
if (pubVideo.srcObject) {
|
|
pubVideo.srcObject.getTracks().forEach(track => track.stop());
|
|
pubVideo.srcObject = null;
|
|
}
|
|
|
|
document.getElementById('viewVideo').srcObject = null;
|
|
|
|
// Disconnect SDKs
|
|
if (pubSDK) {
|
|
await pubSDK.disconnect();
|
|
pubSDK = null;
|
|
}
|
|
|
|
if (viewSDK) {
|
|
await viewSDK.disconnect();
|
|
viewSDK = null;
|
|
}
|
|
|
|
status.textContent = 'Test stopped';
|
|
document.getElementById('pubStats').textContent = '-';
|
|
document.getElementById('viewStats').textContent = '-';
|
|
|
|
document.getElementById('connectTestBtn').disabled = false;
|
|
document.getElementById('stopConnectBtn').disabled = true;
|
|
}
|
|
|
|
// Update connection statistics
|
|
async function updateConnectionStats(pc) {
|
|
try {
|
|
const stats = await pc.getStats();
|
|
let candidateType = 'Unknown';
|
|
|
|
stats.forEach(report => {
|
|
if (report.type === 'candidate-pair' && report.state === 'succeeded') {
|
|
// Get the selected candidate pair
|
|
stats.forEach(candidate => {
|
|
if (candidate.type === 'local-candidate' && candidate.id === report.localCandidateId) {
|
|
candidateType = candidate.candidateType.toUpperCase();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
document.getElementById('viewStats').textContent = `Connected via: ${candidateType}`;
|
|
|
|
} catch (error) {
|
|
console.error('Stats error:', error);
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |