226 lines
5.8 KiB
JavaScript
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();
|