Files
archived-vdo.ninja/examples/dynamic-viewer.html
2025-10-21 20:52:45 -04:00

634 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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 - Dynamic Viewer</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.container {
display: grid;
grid-template-columns: 350px 1fr;
gap: 20px;
}
.control-panel {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
height: fit-content;
}
.video-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 10px;
}
.video-container {
background: #000;
border-radius: 8px;
overflow: hidden;
position: relative;
}
.video-container video {
width: 100%;
height: 240px;
object-fit: cover;
}
.video-label {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,0.7);
color: white;
padding: 5px 10px;
font-size: 14px;
}
.section {
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.section:last-child {
border-bottom: none;
}
h2 {
margin-top: 0;
font-size: 20px;
color: #333;
}
h3 {
font-size: 16px;
margin: 10px 0 5px 0;
color: #666;
}
input, select {
width: 100%;
padding: 8px;
margin: 5px 0;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button {
background: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin: 5px 5px 5px 0;
font-size: 14px;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
button.danger {
background: #dc3545;
}
button.danger:hover {
background: #c82333;
}
button.success {
background: #28a745;
}
button.success:hover {
background: #218838;
}
.status {
padding: 10px;
margin: 10px 0;
background: #f8f9fa;
border-radius: 4px;
font-size: 14px;
border-left: 4px solid #007bff;
}
.status.error {
border-left-color: #dc3545;
background: #f8d7da;
color: #721c24;
}
.status.success {
border-left-color: #28a745;
background: #d4edda;
color: #155724;
}
.participant-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
padding: 5px;
font-size: 14px;
}
.participant {
padding: 5px;
margin: 2px 0;
background: #f8f9fa;
border-radius: 3px;
display: flex;
justify-content: space-between;
align-items: center;
}
.participant.connected {
background: #d4edda;
}
.tab-buttons {
display: flex;
gap: 5px;
margin-bottom: 15px;
}
.tab-button {
background: #f8f9fa;
color: #333;
flex: 1;
}
.tab-button.active {
background: #007bff;
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.stats {
font-family: monospace;
font-size: 12px;
background: #f8f9fa;
padding: 5px;
border-radius: 3px;
margin-top: 5px;
}
</style>
</head>
<body>
<h1>VDO.Ninja SDK - Dynamic Viewer</h1>
<div class="container">
<!-- Control Panel -->
<div class="control-panel">
<div class="section">
<h2>Connection</h2>
<input type="text" id="wss" placeholder="WebSocket Server" value="wss://apibackup.vdo.ninja/">
<button onclick="connect()" id="connectBtn">Connect</button>
<button onclick="disconnect()" id="disconnectBtn" disabled>Disconnect</button>
<div id="connectionStatus" class="status">Not connected</div>
</div>
<!-- Tabs -->
<div class="tab-buttons">
<button class="tab-button active" onclick="showTab('room')">Room View</button>
<button class="tab-button" onclick="showTab('direct')">Direct View</button>
</div>
<!-- Room View Tab -->
<div id="roomTab" class="tab-content active">
<div class="section">
<h2>Join Room</h2>
<input type="text" id="roomName" placeholder="Room Name">
<input type="password" id="roomPassword" placeholder="Password (optional)">
<button onclick="joinRoom()" id="joinRoomBtn" disabled>Join Room</button>
<button onclick="leaveRoom()" id="leaveRoomBtn" disabled>Leave Room</button>
<div id="roomStatus" class="status">Not in a room</div>
</div>
<div class="section">
<h2>Room Participants</h2>
<button onclick="refreshParticipants()" id="refreshBtn" disabled>Refresh</button>
<button onclick="viewAllInRoom()" id="viewAllBtn" disabled class="success">View All</button>
<div id="participantList" class="participant-list">
<em>Join a room to see participants</em>
</div>
</div>
</div>
<!-- Direct View Tab -->
<div id="directTab" class="tab-content">
<div class="section">
<h2>View Stream Directly</h2>
<input type="text" id="directStreamID" placeholder="Stream ID">
<input type="password" id="directPassword" placeholder="Password (optional)">
<button onclick="viewDirect()" id="viewDirectBtn" disabled>View Stream</button>
</div>
<div class="section">
<h2>Active Viewers</h2>
<button onclick="refreshActiveViewers()">Refresh</button>
<div id="activeViewersList" class="participant-list">
<em>No active viewers</em>
</div>
</div>
</div>
<div class="section">
<h2>Quick Actions</h2>
<button onclick="stopAllViewers()" class="danger">Stop All Viewers</button>
<button onclick="showStats()">Show Stats</button>
</div>
</div>
<!-- Video Grid -->
<div class="video-grid" id="videoGrid">
<div style="grid-column: 1/-1; text-align: center; color: #666; padding: 40px;">
<h3>No streams being viewed</h3>
<p>Connect and join a room or view streams directly</p>
</div>
</div>
</div>
<script src="../vdoninja-sdk.js"></script>
<script>
let sdk = null;
const viewers = new Map(); // streamID -> {pc, video, container}
// Initialize
function init() {
updateUI();
}
// Show tab
function showTab(tab) {
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
if (tab === 'room') {
document.querySelector('.tab-button:nth-child(1)').classList.add('active');
document.getElementById('roomTab').classList.add('active');
} else {
document.querySelector('.tab-button:nth-child(2)').classList.add('active');
document.getElementById('directTab').classList.add('active');
}
}
// Connect to signaling server
async function connect() {
const wss = document.getElementById('wss').value;
const status = document.getElementById('connectionStatus');
try {
status.textContent = 'Connecting...';
sdk = new VDONinjaSDK({
wss,
debug: true
});
// Setup event listeners
sdk.addEventListener('connected', () => {
status.className = 'status success';
status.textContent = 'Connected to signaling server';
updateUI();
});
sdk.addEventListener('disconnected', () => {
status.className = 'status error';
status.textContent = 'Disconnected from server';
updateUI();
});
sdk.addEventListener('someoneJoined', (e) => {
console.log('Someone joined:', e.detail);
setTimeout(refreshParticipants, 500);
});
sdk.addEventListener('peerListing', (e) => {
console.log('Peer listing:', e.detail);
refreshParticipants();
});
await sdk.connect();
} catch (error) {
status.className = 'status error';
status.textContent = `Connection failed: ${error.message}`;
sdk = null;
updateUI();
}
}
// Disconnect
async function disconnect() {
if (sdk) {
await stopAllViewers();
await sdk.disconnect();
sdk = null;
updateUI();
}
}
// Join room
async function joinRoom() {
const room = document.getElementById('roomName').value;
const password = document.getElementById('roomPassword').value;
const status = document.getElementById('roomStatus');
if (!room) {
status.className = 'status error';
status.textContent = 'Please enter a room name';
return;
}
try {
status.textContent = 'Joining room...';
await sdk.joinRoom({ room, password });
status.className = 'status success';
status.textContent = `Joined room: ${room}`;
updateUI();
// Refresh participants after a short delay
setTimeout(refreshParticipants, 1000);
} catch (error) {
status.className = 'status error';
status.textContent = `Failed to join room: ${error.message}`;
}
}
// Leave room
async function leaveRoom() {
const status = document.getElementById('roomStatus');
try {
await stopAllViewers();
await sdk.leaveRoom();
status.className = 'status';
status.textContent = 'Left room';
document.getElementById('participantList').innerHTML = '<em>Join a room to see participants</em>';
updateUI();
} catch (error) {
status.className = 'status error';
status.textContent = `Error leaving room: ${error.message}`;
}
}
// Refresh participants
function refreshParticipants() {
if (!sdk || !sdk.state.room) return;
const participants = sdk.getRoomParticipants();
const list = document.getElementById('participantList');
if (participants.length === 0) {
list.innerHTML = '<em>No other participants in room</em>';
return;
}
list.innerHTML = participants.map(p => {
const isViewing = viewers.has(p.streamID);
const label = p.label || p.streamID;
return `
<div class="participant ${p.connected ? 'connected' : ''}">
<span>${label}</span>
${isViewing ?
`<button onclick="stopViewing('${p.streamID}')">Stop</button>` :
`<button onclick="viewStream('${p.streamID}')">View</button>`
}
</div>
`;
}).join('');
}
// View all in room
async function viewAllInRoom() {
try {
const viewers = await sdk.viewAllInRoom();
for (const viewer of viewers) {
handleNewViewer(viewer.streamID, viewer.pc, viewer.label);
}
if (viewers.length === 0) {
alert('No streams to view in this room');
}
} catch (error) {
console.error('Error viewing all:', error);
alert(`Error: ${error.message}`);
}
}
// View specific stream
async function viewStream(streamID) {
try {
const pc = await sdk.view(streamID);
handleNewViewer(streamID, pc);
refreshParticipants();
refreshActiveViewers();
} catch (error) {
console.error(`Error viewing ${streamID}:`, error);
alert(`Failed to view stream: ${error.message}`);
}
}
// View direct (without room)
async function viewDirect() {
const streamID = document.getElementById('directStreamID').value;
const password = document.getElementById('directPassword').value;
if (!streamID) {
alert('Please enter a stream ID');
return;
}
try {
// If password provided, we need to set it temporarily
if (password) {
sdk.state.password = password;
await sdk._updateStreamIDHash();
}
const pc = await sdk.view(streamID);
handleNewViewer(streamID, pc);
refreshActiveViewers();
} catch (error) {
console.error('Error viewing directly:', error);
alert(`Failed to view stream: ${error.message}`);
}
}
// Handle new viewer connection
function handleNewViewer(streamID, pc, label) {
if (viewers.has(streamID)) {
console.log('Already viewing:', streamID);
return;
}
// Create video container
const container = document.createElement('div');
container.className = 'video-container';
container.id = `video-${streamID}`;
const video = document.createElement('video');
video.autoplay = true;
video.playsinline = true;
const labelDiv = document.createElement('div');
labelDiv.className = 'video-label';
labelDiv.innerHTML = `
${label || streamID}
<button style="float: right; background: #dc3545; border: none; color: white; padding: 2px 8px; border-radius: 3px; cursor: pointer;"
onclick="stopViewing('${streamID}')">×</button>
`;
container.appendChild(video);
container.appendChild(labelDiv);
// Handle tracks
pc.ontrack = (event) => {
console.log(`Track received from ${streamID}:`, event.track.kind);
video.srcObject = event.streams[0];
};
// Handle connection state
pc.onconnectionstatechange = () => {
console.log(`Connection state for ${streamID}:`, pc.connectionState);
if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
stopViewing(streamID);
}
};
// Store viewer info
viewers.set(streamID, { pc, video, container });
// Update grid
updateVideoGrid();
}
// Stop viewing specific stream
async function stopViewing(streamID) {
const viewer = viewers.get(streamID);
if (!viewer) return;
// Stop video
if (viewer.video.srcObject) {
viewer.video.srcObject.getTracks().forEach(track => track.stop());
viewer.video.srcObject = null;
}
// Remove from DOM
viewer.container.remove();
// Close connection
await sdk.stopViewing(streamID);
// Remove from map
viewers.delete(streamID);
// Update UI
updateVideoGrid();
refreshParticipants();
refreshActiveViewers();
}
// Stop all viewers
async function stopAllViewers() {
const streamIDs = Array.from(viewers.keys());
for (const streamID of streamIDs) {
await stopViewing(streamID);
}
}
// Refresh active viewers list
function refreshActiveViewers() {
if (!sdk) return;
const active = sdk.getActiveViewers();
const list = document.getElementById('activeViewersList');
if (active.length === 0) {
list.innerHTML = '<em>No active viewers</em>';
return;
}
list.innerHTML = active.map(v => `
<div class="participant ${v.connectionState === 'connected' ? 'connected' : ''}">
<span>${v.streamID} (${v.connectionState})</span>
<button onclick="stopViewing('${v.streamID}')">Stop</button>
</div>
`).join('');
}
// Update video grid
function updateVideoGrid() {
const grid = document.getElementById('videoGrid');
if (viewers.size === 0) {
grid.innerHTML = `
<div style="grid-column: 1/-1; text-align: center; color: #666; padding: 40px;">
<h3>No streams being viewed</h3>
<p>Connect and join a room or view streams directly</p>
</div>
`;
} else {
grid.innerHTML = '';
for (const viewer of viewers.values()) {
grid.appendChild(viewer.container);
}
}
}
// Show stats
async function showStats() {
if (!sdk) {
alert('Not connected');
return;
}
try {
const stats = await sdk.getStats();
console.log('Connection stats:', stats);
let message = 'Connection Statistics:\n\n';
for (const [uuid, stat] of Object.entries(stats)) {
message += `Connection ${uuid}:\n`;
stat.forEach(report => {
if (report.type === 'inbound-rtp' || report.type === 'outbound-rtp') {
message += ` ${report.type}: ${report.bytesReceived || report.bytesSent || 0} bytes\n`;
}
});
}
alert(message);
} catch (error) {
alert(`Error getting stats: ${error.message}`);
}
}
// Update UI state
function updateUI() {
const connected = sdk && sdk.state.connected;
const inRoom = connected && sdk.state.room;
document.getElementById('connectBtn').disabled = connected;
document.getElementById('disconnectBtn').disabled = !connected;
document.getElementById('joinRoomBtn').disabled = !connected || inRoom;
document.getElementById('leaveRoomBtn').disabled = !inRoom;
document.getElementById('refreshBtn').disabled = !inRoom;
document.getElementById('viewAllBtn').disabled = !inRoom;
document.getElementById('viewDirectBtn').disabled = !connected;
}
// Initialize on load
init();
</script>
</body>
</html>