mirror of
https://github.com/SrIzan10/vdo.ninja.git
synced 2026-05-01 11:05:24 +00:00
634 lines
22 KiB
HTML
634 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>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> |