mirror of
git://f0xx.org/ac/ac-ms-sfu-signaling
synced 2026-07-29 04:18:07 +03:00
- JanusClient.php: HTTP REST client for Janus Gateway API - SfuRoomRepository.php: room + participant + stats persistence - bootstrap.php: PDO, auth, Janus client factory - public/index.php: front-controller routing - public/health.php: Janus online check - public/api/rooms.php: list/create/destroy rooms - public/api/join.php: SDP offer relay → Janus VideoRoom - public/api/stats.php: bitrate/packet-loss analytics ingest - public/dashboard.php: SFU rooms UI (common BE dark-theme styles) Co-authored-by: Cursor <cursoragent@cursor.com>
125 lines
5.5 KiB
PHP
125 lines
5.5 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
require_once dirname(__DIR__) . '/src/bootstrap.php';
|
|
|
|
$user = sfuRequireAuth();
|
|
$repo = new \AndroidCast\Sfu\SfuRoomRepository(sfuPdo());
|
|
$janus = janusClient();
|
|
$janusOnline = isset($janus->info()['janus']);
|
|
$rooms = $repo->listActive();
|
|
?>
|
|
<!doctype html>
|
|
<html lang="en" data-theme="dark">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>SFU Rooms — Android Cast</title>
|
|
<link rel="stylesheet" href="/app/androidcast_project/issues/assets/css/app.css">
|
|
<style>
|
|
.sfu-badge { display:inline-flex; align-items:center; gap:6px; padding:3px 10px; border-radius:20px; font-size:.78rem; font-weight:600; }
|
|
.sfu-badge.online { background:rgba(16,185,129,.15); color:#34d399; border:1px solid rgba(16,185,129,.3); }
|
|
.sfu-badge.offline { background:rgba(248,113,113,.12); color:#f87171; border:1px solid rgba(248,113,113,.25); }
|
|
.dot { width:7px; height:7px; border-radius:50%; background:currentColor; }
|
|
.room-card { background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:16px 20px; margin-bottom:12px; display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
|
.room-card h3 { margin:0 0 4px; font-size:1rem; }
|
|
.room-meta { color:var(--muted); font-size:.82rem; }
|
|
.btn-sm { padding:5px 14px; border-radius:6px; border:none; cursor:pointer; font-size:.82rem; font-weight:600; }
|
|
.btn-join { background:var(--accent); color:#fff; }
|
|
.btn-close { background:rgba(248,113,113,.18); color:var(--danger); border:1px solid rgba(248,113,113,.3); }
|
|
#create-form { background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:18px 20px; margin-bottom:20px; display:none; }
|
|
#create-form.open { display:block; }
|
|
.form-row { display:flex; gap:10px; align-items:flex-end; flex-wrap:wrap; }
|
|
.form-row input, .form-row select { background:var(--surface2); border:1px solid var(--border); color:var(--text); border-radius:6px; padding:7px 12px; font-size:.9rem; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="layout" style="max-width:860px;margin:0 auto;padding:24px 16px">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:20px">
|
|
<div>
|
|
<h1 style="margin:0;font-size:1.4rem">SFU Rooms</h1>
|
|
<a href="/app/androidcast_project/issues/" style="color:var(--muted);font-size:.82rem">← Back to dashboard</a>
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:12px">
|
|
<span class="sfu-badge <?= $janusOnline ? 'online' : 'offline' ?>">
|
|
<span class="dot"></span>
|
|
Janus <?= $janusOnline ? 'online' : 'offline' ?>
|
|
</span>
|
|
<button class="btn-sm btn-join" onclick="toggleCreate()">+ New Room</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="create-form">
|
|
<h3 style="margin:0 0 14px;font-size:1rem">Create Room</h3>
|
|
<div class="form-row">
|
|
<div>
|
|
<label style="font-size:.82rem;color:var(--muted);display:block;margin-bottom:4px">Room name</label>
|
|
<input id="room-name" type="text" placeholder="My screen cast" style="min-width:200px">
|
|
</div>
|
|
<div>
|
|
<label style="font-size:.82rem;color:var(--muted);display:block;margin-bottom:4px">Purpose</label>
|
|
<select id="room-purpose">
|
|
<option value="screen_cast">Screen Cast</option>
|
|
<option value="education">Education</option>
|
|
<option value="conference">Conference</option>
|
|
</select>
|
|
</div>
|
|
<button class="btn-sm btn-join" onclick="createRoom()">Create</button>
|
|
<button class="btn-sm btn-close" onclick="toggleCreate()">Cancel</button>
|
|
</div>
|
|
<p id="create-err" style="color:var(--danger);font-size:.82rem;margin:8px 0 0;display:none"></p>
|
|
</div>
|
|
|
|
<div id="rooms-list">
|
|
<?php if (empty($rooms)): ?>
|
|
<p style="color:var(--muted);text-align:center;padding:40px 0">No active rooms. Create one above.</p>
|
|
<?php else: foreach ($rooms as $r): ?>
|
|
<div class="room-card" id="room-<?= (int)$r['janus_room_id'] ?>">
|
|
<div>
|
|
<h3><?= htmlspecialchars($r['name']) ?></h3>
|
|
<span class="room-meta">
|
|
<?= htmlspecialchars($r['purpose']) ?> ·
|
|
<?= (int)$r['participant_count'] ?> participant(s) ·
|
|
Janus #<?= (int)$r['janus_room_id'] ?>
|
|
</span>
|
|
</div>
|
|
<div style="display:flex;gap:8px">
|
|
<button class="btn-sm btn-close" onclick="closeRoom(<?= (int)$r['janus_room_id'] ?>)">Close</button>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API = '/app/androidcast_project/sfu/api';
|
|
function toggleCreate() {
|
|
document.getElementById('create-form').classList.toggle('open');
|
|
}
|
|
async function createRoom() {
|
|
const name = document.getElementById('room-name').value.trim() || 'Room';
|
|
const purpose = document.getElementById('room-purpose').value;
|
|
const errEl = document.getElementById('create-err');
|
|
errEl.style.display = 'none';
|
|
try {
|
|
const r = await fetch(`${API}/rooms`, {
|
|
method: 'POST',
|
|
headers: {'Content-Type':'application/json'},
|
|
body: JSON.stringify({name, purpose})
|
|
});
|
|
const j = await r.json();
|
|
if (!j.ok) throw new Error(j.error || 'failed');
|
|
location.reload();
|
|
} catch (e) {
|
|
errEl.textContent = e.message;
|
|
errEl.style.display = 'block';
|
|
}
|
|
}
|
|
async function closeRoom(janusId) {
|
|
if (!confirm('Close this room?')) return;
|
|
await fetch(`${API}/rooms/${janusId}`, {method:'DELETE'});
|
|
document.getElementById(`room-${janusId}`)?.remove();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|