Files
archived-vdo.ninja/examples/turn-only-example.html
2025-10-21 20:52:45 -04:00

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>