srsViewer/public/js/index.js
creations ae3224c18b
Some checks failed
Code quality checks / biome (push) Failing after 13s
first commit
2025-06-08 17:17:18 -04:00

226 lines
5.8 KiB
JavaScript

class StreamDashboard {
constructor() {
this.streams = [];
this.filteredStreams = [];
this.searchTimeout = null;
this.refreshInterval = null;
this.initializeElements();
this.bindEvents();
this.loadStreams();
this.startAutoRefresh();
}
initializeElements() {
this.elements = {
searchInput: document.getElementById("searchInput"),
totalStreams: document.getElementById("totalStreams"),
onlineStreams: document.getElementById("onlineStreams"),
loadingState: document.getElementById("loadingState"),
errorState: document.getElementById("errorState"),
streamsContainer: document.getElementById("streamsContainer"),
noResults: document.getElementById("noResults"),
retryBtn: document.getElementById("retryBtn"),
};
}
bindEvents() {
this.elements.searchInput.addEventListener("input", (e) => {
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => {
this.filterStreams(e.target.value);
}, 300);
});
this.elements.retryBtn.addEventListener("click", () => {
this.loadStreams();
});
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
this.stopAutoRefresh();
} else {
this.startAutoRefresh();
}
});
}
startAutoRefresh() {
this.refreshInterval = setInterval(() => {
this.loadStreams(true);
}, 10000);
}
stopAutoRefresh() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
}
async loadStreams(silentRefresh = false) {
if (!silentRefresh) {
this.showLoading();
}
try {
const response = await fetch("/api/streams");
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || "Failed to load streams");
}
const allStreams = result.data.streams || [];
this.streams = allStreams.filter(
(stream) => stream.publish?.active === true,
);
this.filteredStreams = [...this.streams];
this.updateStats();
this.renderStreams();
if (!silentRefresh) {
this.showStreams();
}
} catch {
if (!silentRefresh) {
this.showError();
}
}
}
filterStreams(searchTerm) {
const term = searchTerm.toLowerCase().trim();
if (!term) {
this.filteredStreams = [...this.streams];
} else {
this.filteredStreams = this.streams.filter(
(stream) =>
stream.name?.toLowerCase().includes(term) ||
stream.url?.toLowerCase().includes(term) ||
stream.app?.toLowerCase().includes(term),
);
}
this.renderStreams();
if (this.filteredStreams.length === 0 && term) {
this.showNoResults();
} else {
this.showStreams();
}
}
updateStats() {
const total = this.streams.length;
this.elements.totalStreams.textContent = total;
this.elements.onlineStreams.textContent = total;
}
renderStreams() {
this.elements.streamsContainer.innerHTML = "";
for (const stream of this.filteredStreams) {
const streamCard = this.createStreamCard(stream);
this.elements.streamsContainer.appendChild(streamCard);
}
}
createStreamCard(stream) {
const card = document.createElement("div");
card.className = "stream-card";
const formatBytes = (bytes) => {
if (!bytes) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
};
const watchName = stream.name?.replace(/\/live\//, "/");
const watchUrl = watchName
? `${window.location.origin}/watch/${encodeURIComponent(watchName)}`
: "Unknown Stream";
card.innerHTML = `
<div class="stream-header">
<h3 class="stream-name">${this.escapeHtml(stream.name || "Unknown Stream")}</h3>
</div>
<div class="stream-info">
<div class="info-row">
<span class="info-label">Clients:</span>
<span class="info-value">${stream.clients || 0}</span>
</div>
<div class="info-row">
<span class="info-label">Bitrate:</span>
<span class="info-value">${formatBytes((stream.kbps?.recv_30s || 0) * 1024)}/s</span>
</div>
<div class="info-row">
<span class="info-label">Video:</span>
<span class="info-value">${stream.video?.codec || "N/A"} ${stream.video?.profile || ""}</span>
</div>
<div class="info-row">
<span class="info-label">Audio:</span>
<span class="info-value">${stream.audio?.codec || "N/A"}</span>
</div>
</div>
<a href="${watchUrl}" class="stream-watch-link">
<div class="stream-url" onclick="navigator.clipboard.writeText('${this.escapeHtml(watchUrl)}').then(() => this.style.background = 'rgba(34, 197, 94, 0.2)').catch(() => {})" title="Click to copy">
<span class="stream-url-value">${this.escapeHtml(watchUrl)}</span>
</div>
</a>
`;
return card;
}
escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
showLoading() {
this.elements.loadingState.style.display = "block";
this.elements.errorState.style.display = "none";
this.elements.streamsContainer.style.display = "none";
this.elements.noResults.style.display = "none";
}
showError() {
this.elements.loadingState.style.display = "none";
this.elements.errorState.style.display = "block";
this.elements.streamsContainer.style.display = "none";
this.elements.noResults.style.display = "none";
}
showStreams() {
this.elements.loadingState.style.display = "none";
this.elements.errorState.style.display = "none";
this.elements.streamsContainer.style.display = "grid";
this.elements.noResults.style.display = "none";
}
showNoResults() {
this.elements.loadingState.style.display = "none";
this.elements.errorState.style.display = "none";
this.elements.streamsContainer.style.display = "none";
this.elements.noResults.style.display = "block";
}
}
new StreamDashboard();