mirror of
https://github.com/eduard256/Strix
synced 2026-04-21 13:37:27 +00:00
- Add checked-by-default checkbox to also test popular stream patterns - Move JPEG-only streams (no H264/H265) to Alternative group in test results
689 lines
26 KiB
HTML
689 lines
26 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<meta name="theme-color" content="#0a0a0f">
|
|
<title>Strix - Stream Testing</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
:root {
|
|
--bg-primary: #0a0a0f;
|
|
--bg-secondary: #1a1a24;
|
|
--bg-tertiary: #24242f;
|
|
--bg-elevated: #2a2a38;
|
|
--purple-primary: #8b5cf6;
|
|
--purple-light: #a78bfa;
|
|
--purple-dark: #7c3aed;
|
|
--purple-glow: rgba(139, 92, 246, 0.3);
|
|
--purple-glow-strong: rgba(139, 92, 246, 0.5);
|
|
--text-primary: #e0e0e8;
|
|
--text-secondary: #a0a0b0;
|
|
--text-tertiary: #606070;
|
|
--success: #10b981;
|
|
--warning: #f59e0b;
|
|
--error: #ef4444;
|
|
--border-color: rgba(139, 92, 246, 0.15);
|
|
--font-primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
--font-mono: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
|
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
--transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
html { font-size: 16px; -webkit-font-smoothing: antialiased; }
|
|
|
|
body {
|
|
font-family: var(--font-primary);
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.5;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.screen { padding: 1.5rem; animation: fadeIn var(--transition-base); }
|
|
.container { max-width: 1200px; margin: 0 auto; width: 100%; }
|
|
|
|
@media (min-width: 768px) { .screen { padding: 2rem; } }
|
|
|
|
.btn-back {
|
|
display: inline-flex; align-items: center; gap: 0.5rem;
|
|
background: none; border: none;
|
|
color: var(--text-secondary); font-size: 0.875rem;
|
|
font-family: var(--font-primary); cursor: pointer;
|
|
padding: 0.5rem 0; margin-bottom: 1.5rem;
|
|
transition: color var(--transition-fast);
|
|
}
|
|
.btn-back:hover { color: var(--purple-primary); }
|
|
|
|
.header-row {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
flex-wrap: wrap; gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.screen-title { font-size: 1.5rem; font-weight: 600; }
|
|
.screen-title .mode-badge {
|
|
font-size: 0.75rem; font-weight: 600;
|
|
padding: 0.25rem 0.625rem; border-radius: 4px;
|
|
text-transform: uppercase; letter-spacing: 0.05em;
|
|
background: rgba(139, 92, 246, 0.15); color: var(--purple-light);
|
|
margin-left: 0.75rem; vertical-align: middle;
|
|
}
|
|
|
|
.btn-stop {
|
|
padding: 0.5rem 1rem; border-radius: 6px;
|
|
font-size: 0.75rem; font-weight: 600; font-family: var(--font-primary);
|
|
text-transform: uppercase; letter-spacing: 0.04em;
|
|
background: rgba(239, 68, 68, 0.12); color: var(--error);
|
|
border: 1px solid rgba(239, 68, 68, 0.25);
|
|
cursor: pointer; transition: all var(--transition-fast);
|
|
}
|
|
.btn-stop:hover { background: rgba(239, 68, 68, 0.2); border-color: rgba(239, 68, 68, 0.4); }
|
|
.btn-stop:disabled { opacity: 0.3; cursor: not-allowed; }
|
|
|
|
/* Status bar */
|
|
.status-bar {
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
padding: 1rem 1.25rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.status-top {
|
|
display: flex; align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.status-badge {
|
|
display: inline-flex; align-items: center; gap: 0.375rem;
|
|
font-family: var(--font-mono); font-size: 0.6875rem;
|
|
font-weight: 500; text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
padding: 0.25rem 0.625rem; border-radius: 3px;
|
|
}
|
|
|
|
.status-badge.running { background: rgba(59, 130, 246, 0.12); color: #3b82f6; }
|
|
.status-badge.done { background: rgba(16, 185, 129, 0.12); color: var(--success); }
|
|
|
|
.status-dot {
|
|
width: 6px; height: 6px; border-radius: 50%; background: currentColor;
|
|
}
|
|
.status-badge.running .status-dot { animation: pulse 1.2s infinite; }
|
|
|
|
.status-session { font-family: var(--font-mono); font-size: 0.6875rem; color: var(--text-tertiary); }
|
|
|
|
.counters { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.75rem; }
|
|
|
|
@media (max-width: 640px) { .counters { grid-template-columns: repeat(2, 1fr); } }
|
|
|
|
.counter {
|
|
background: var(--bg-primary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 6px;
|
|
padding: 0.75rem; text-align: center;
|
|
}
|
|
|
|
.counter-value {
|
|
font-family: var(--font-mono); font-size: 1.5rem;
|
|
font-weight: 600; line-height: 1; margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.counter-label {
|
|
font-family: var(--font-mono); font-size: 0.625rem;
|
|
color: var(--text-secondary); text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
}
|
|
|
|
.counter.total .counter-value { color: var(--text-primary); }
|
|
.counter.tested .counter-value { color: #3b82f6; }
|
|
.counter.alive .counter-value { color: var(--success); }
|
|
.counter.screenshots .counter-value { color: var(--warning); }
|
|
|
|
.progress-track {
|
|
height: 3px; background: var(--border-color);
|
|
border-radius: 2px; margin-top: 1rem; overflow: hidden;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%; background: #3b82f6;
|
|
border-radius: 2px; width: 0%;
|
|
transition: width 0.3s ease;
|
|
}
|
|
.progress-fill.complete { background: var(--success); }
|
|
|
|
/* Groups */
|
|
.group { margin-bottom: 1.5rem; }
|
|
|
|
.group-header {
|
|
display: flex; align-items: center; gap: 0.5rem;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 1px solid var(--border-color);
|
|
margin-bottom: 1rem;
|
|
cursor: pointer; user-select: none;
|
|
transition: color var(--transition-fast);
|
|
}
|
|
.group-header:hover { color: var(--purple-primary); }
|
|
|
|
.group-toggle {
|
|
display: flex; background: none; border: none;
|
|
padding: 0; cursor: pointer; color: var(--text-tertiary);
|
|
}
|
|
.group-toggle .chevron { transition: transform var(--transition-fast); }
|
|
.group.collapsed .group-toggle .chevron { transform: rotate(-90deg); }
|
|
.group.collapsed .group-content { display: none; }
|
|
|
|
.group-title {
|
|
font-size: 0.8125rem; font-weight: 600;
|
|
text-transform: uppercase; letter-spacing: 0.05em;
|
|
}
|
|
.group-count { font-size: 0.8125rem; color: var(--text-tertiary); }
|
|
|
|
/* Cards grid */
|
|
.cards-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
|
|
@media (max-width: 640px) { .cards-grid { grid-template-columns: 1fr; } }
|
|
|
|
.card {
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
transition: border-color var(--transition-fast), transform var(--transition-fast);
|
|
animation: cardIn 0.25s ease;
|
|
}
|
|
.card:hover { border-color: var(--purple-primary); transform: translateY(-2px); }
|
|
|
|
.card-thumb {
|
|
position: relative; width: 100%;
|
|
aspect-ratio: 16/9;
|
|
background: var(--bg-primary); overflow: hidden;
|
|
}
|
|
|
|
.card-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
|
|
.card-thumb-placeholder {
|
|
width: 100%; height: 100%;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.card-thumb-placeholder svg { width: 32px; height: 32px; color: var(--text-tertiary); }
|
|
|
|
.card-overlay { position: absolute; top: 0.5rem; right: 0.5rem; display: flex; gap: 0.25rem; }
|
|
|
|
.card-tag {
|
|
font-family: var(--font-mono); font-size: 0.625rem;
|
|
font-weight: 500; padding: 0.125rem 0.375rem; border-radius: 2px;
|
|
background: rgba(0, 0, 0, 0.65); backdrop-filter: blur(4px);
|
|
color: var(--text-primary); border: 1px solid rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
.card-resolution {
|
|
position: absolute; bottom: 0.5rem; left: 0.5rem;
|
|
font-family: var(--font-mono); font-size: 0.625rem;
|
|
font-weight: 600; padding: 0.125rem 0.375rem; border-radius: 2px;
|
|
background: rgba(0, 0, 0, 0.65); backdrop-filter: blur(4px);
|
|
color: var(--purple-light);
|
|
}
|
|
|
|
.card-body { padding: 0.75rem 1rem; }
|
|
|
|
.card-url {
|
|
font-family: var(--font-mono); font-size: 0.6875rem;
|
|
color: var(--text-secondary); line-height: 1.5;
|
|
word-break: break-all;
|
|
display: -webkit-box; -webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical; overflow: hidden;
|
|
}
|
|
.card-url .scheme { color: var(--purple-light); }
|
|
|
|
.card-meta {
|
|
display: flex; align-items: center; gap: 0.75rem;
|
|
margin-top: 0.5rem; padding-top: 0.5rem;
|
|
border-top: 1px solid var(--border-color);
|
|
}
|
|
|
|
.card-meta-item {
|
|
font-family: var(--font-mono); font-size: 0.625rem;
|
|
color: var(--text-tertiary); display: flex;
|
|
align-items: center; gap: 0.25rem;
|
|
}
|
|
.card-meta-item svg { width: 12px; height: 12px; }
|
|
.latency-fast { color: var(--success); }
|
|
.latency-medium { color: var(--warning); }
|
|
.latency-slow { color: var(--error); }
|
|
|
|
.card-action { padding: 0 1rem 0.75rem; }
|
|
|
|
.btn-use {
|
|
width: 100%; padding: 0.5rem;
|
|
background: linear-gradient(135deg, var(--purple-primary), var(--purple-dark));
|
|
color: white; border: none; border-radius: 6px;
|
|
font-size: 0.75rem; font-weight: 600; font-family: var(--font-primary);
|
|
cursor: pointer; transition: all var(--transition-fast);
|
|
box-shadow: 0 2px 8px var(--purple-glow);
|
|
}
|
|
.btn-use:hover { box-shadow: 0 4px 16px var(--purple-glow-strong); transform: translateY(-1px); }
|
|
|
|
.empty-state {
|
|
text-align: center; padding: 3rem;
|
|
color: var(--text-tertiary); font-size: 0.875rem;
|
|
}
|
|
|
|
.toast {
|
|
position: fixed; bottom: 1.5rem; left: 50%;
|
|
transform: translateX(-50%) translateY(100px);
|
|
padding: 1rem 1.5rem;
|
|
background: var(--bg-elevated);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px; box-shadow: var(--shadow-lg);
|
|
font-size: 0.875rem; color: var(--text-primary);
|
|
z-index: 1000; transition: transform var(--transition-base);
|
|
}
|
|
.toast.show { transform: translateX(-50%) translateY(0); }
|
|
.toast.hidden { display: none; }
|
|
|
|
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
@keyframes cardIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="screen">
|
|
<div class="container">
|
|
<button class="btn-back" id="btn-back">
|
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
<path d="M12 4L6 10l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
</svg>
|
|
Back
|
|
</button>
|
|
|
|
<div class="header-row">
|
|
<h2 class="screen-title" id="title">Stream Testing</h2>
|
|
<button class="btn-stop" id="btn-stop">Cancel</button>
|
|
</div>
|
|
|
|
<div class="status-bar">
|
|
<div class="status-top">
|
|
<div class="status-badge running" id="badge">
|
|
<div class="status-dot"></div>
|
|
<span id="badge-text">running</span>
|
|
</div>
|
|
<span class="status-session" id="session-id"></span>
|
|
</div>
|
|
<div class="counters">
|
|
<div class="counter total"><div class="counter-value" id="c-total">0</div><div class="counter-label">total</div></div>
|
|
<div class="counter tested"><div class="counter-value" id="c-tested">0</div><div class="counter-label">tested</div></div>
|
|
<div class="counter alive"><div class="counter-value" id="c-alive">0</div><div class="counter-label">alive</div></div>
|
|
<div class="counter screenshots"><div class="counter-value" id="c-screens">0</div><div class="counter-label">screenshots</div></div>
|
|
</div>
|
|
<div class="progress-track">
|
|
<div class="progress-fill" id="progress"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="results"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="toast" class="toast hidden"></div>
|
|
|
|
<script>
|
|
var params = new URLSearchParams(location.search);
|
|
var sessionId = params.get('id') || '';
|
|
var mode = params.get('mode') || 'main'; // main or sub
|
|
var ip = params.get('ip') || '';
|
|
var mac = params.get('mac') || '';
|
|
var vendor = params.get('vendor') || '';
|
|
var model = params.get('model') || '';
|
|
var server = params.get('server') || '';
|
|
var hostname = params.get('hostname') || '';
|
|
var ports = params.get('ports') || '';
|
|
var user = params.get('user') || '';
|
|
var channel = params.get('channel') || '';
|
|
var ids = params.get('ids') || '';
|
|
var mainStream = params.get('main') || ''; // for sub mode, already selected main
|
|
|
|
var pollTimer = null;
|
|
var renderedCount = 0;
|
|
var allResults = [];
|
|
|
|
// title
|
|
var titleEl = document.getElementById('title');
|
|
titleEl.textContent = mode === 'sub' ? 'Select Sub Stream' : 'Stream Testing';
|
|
if (mode === 'sub') {
|
|
var badge = document.createElement('span');
|
|
badge.className = 'mode-badge';
|
|
badge.textContent = 'sub';
|
|
titleEl.appendChild(badge);
|
|
}
|
|
|
|
document.getElementById('session-id').textContent = sessionId;
|
|
|
|
// back
|
|
document.getElementById('btn-back').addEventListener('click', function() { history.back(); });
|
|
|
|
// stop
|
|
document.getElementById('btn-stop').addEventListener('click', function() {
|
|
if (!sessionId) return;
|
|
fetch('api/test?id=' + encodeURIComponent(sessionId), { method: 'DELETE' });
|
|
stopPolling();
|
|
document.getElementById('btn-stop').disabled = true;
|
|
showToast('Session cancelled');
|
|
});
|
|
|
|
// start polling
|
|
if (sessionId) {
|
|
pollTimer = setInterval(function() { pollSession(); }, 1000);
|
|
pollSession();
|
|
}
|
|
|
|
function stopPolling() {
|
|
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
|
}
|
|
|
|
async function pollSession() {
|
|
try {
|
|
var r = await fetch('api/test?id=' + encodeURIComponent(sessionId));
|
|
var data = await r.json();
|
|
|
|
document.getElementById('c-total').textContent = data.total;
|
|
document.getElementById('c-tested').textContent = data.tested;
|
|
document.getElementById('c-alive').textContent = data.alive;
|
|
document.getElementById('c-screens').textContent = data.with_screenshot;
|
|
|
|
var pct = data.total > 0 ? Math.round((data.tested / data.total) * 100) : 0;
|
|
document.getElementById('progress').style.width = pct + '%';
|
|
|
|
if (data.results && data.results.length > renderedCount) {
|
|
for (var i = renderedCount; i < data.results.length; i++) {
|
|
allResults.push(data.results[i]);
|
|
}
|
|
renderedCount = data.results.length;
|
|
renderResults();
|
|
}
|
|
|
|
if (data.status === 'done') {
|
|
stopPolling();
|
|
document.getElementById('badge').className = 'status-badge done';
|
|
document.getElementById('badge-text').textContent = 'done';
|
|
document.getElementById('progress').classList.add('complete');
|
|
document.getElementById('progress').style.width = '100%';
|
|
document.getElementById('btn-stop').disabled = true;
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
function classifyResult(r) {
|
|
var scheme = r.source.split('://')[0] || '';
|
|
var isRecommended = scheme === 'rtsp' || scheme === 'rtsps' || scheme === 'onvif';
|
|
var isJpegOnly = r.codecs && r.codecs.length > 0 && r.codecs.indexOf('H264') === -1 && r.codecs.indexOf('H265') === -1;
|
|
var isHD = r.width >= 1280;
|
|
|
|
if (isJpegOnly) return 'alt';
|
|
if (isRecommended && isHD) return 'rec-main';
|
|
if (isRecommended) return 'rec-sub';
|
|
return 'alt';
|
|
}
|
|
|
|
function renderResults() {
|
|
var container = document.getElementById('results');
|
|
container.textContent = '';
|
|
|
|
var groups = { 'rec-main': [], 'rec-sub': [], 'alt': [] };
|
|
|
|
allResults.forEach(function(r) {
|
|
groups[classifyResult(r)].push(r);
|
|
});
|
|
|
|
var defs = [
|
|
{ key: 'rec-main', title: 'Recommended - Main', collapsed: mode === 'sub' },
|
|
{ key: 'rec-sub', title: 'Recommended - Sub', collapsed: mode === 'main' },
|
|
{ key: 'alt', title: 'Alternative', collapsed: true }
|
|
];
|
|
|
|
defs.forEach(function(def) {
|
|
var items = groups[def.key];
|
|
if (items.length === 0) return;
|
|
|
|
var group = document.createElement('div');
|
|
group.className = 'group' + (def.collapsed ? ' collapsed' : '');
|
|
|
|
var header = document.createElement('div');
|
|
header.className = 'group-header';
|
|
|
|
var toggle = document.createElement('button');
|
|
toggle.className = 'group-toggle';
|
|
toggle.type = 'button';
|
|
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
svg.setAttribute('width', '12'); svg.setAttribute('height', '12');
|
|
svg.setAttribute('viewBox', '0 0 12 12'); svg.setAttribute('fill', 'none');
|
|
svg.setAttribute('class', 'chevron');
|
|
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
path.setAttribute('d', 'M3 4.5l3 3 3-3');
|
|
path.setAttribute('stroke', 'currentColor');
|
|
path.setAttribute('stroke-width', '1.5');
|
|
path.setAttribute('stroke-linecap', 'round');
|
|
svg.appendChild(path);
|
|
toggle.appendChild(svg);
|
|
header.appendChild(toggle);
|
|
|
|
var title = document.createElement('span');
|
|
title.className = 'group-title';
|
|
title.textContent = def.title;
|
|
header.appendChild(title);
|
|
|
|
var count = document.createElement('span');
|
|
count.className = 'group-count';
|
|
count.textContent = '(' + items.length + ')';
|
|
header.appendChild(count);
|
|
|
|
header.addEventListener('click', function() {
|
|
group.classList.toggle('collapsed');
|
|
});
|
|
|
|
group.appendChild(header);
|
|
|
|
var content = document.createElement('div');
|
|
content.className = 'group-content';
|
|
|
|
var grid = document.createElement('div');
|
|
grid.className = 'cards-grid';
|
|
|
|
items.forEach(function(r) {
|
|
grid.appendChild(createCard(r));
|
|
});
|
|
|
|
content.appendChild(grid);
|
|
group.appendChild(content);
|
|
container.appendChild(group);
|
|
});
|
|
|
|
if (allResults.length === 0) {
|
|
var empty = document.createElement('div');
|
|
empty.className = 'empty-state';
|
|
empty.textContent = 'Waiting for results...';
|
|
container.appendChild(empty);
|
|
}
|
|
}
|
|
|
|
function createCard(r) {
|
|
var card = document.createElement('div');
|
|
card.className = 'card';
|
|
|
|
// thumbnail
|
|
var thumb = document.createElement('div');
|
|
thumb.className = 'card-thumb';
|
|
|
|
if (r.screenshot) {
|
|
var img = document.createElement('img');
|
|
img.src = r.screenshot;
|
|
img.alt = '';
|
|
img.loading = 'lazy';
|
|
thumb.appendChild(img);
|
|
} else {
|
|
var ph = document.createElement('div');
|
|
ph.className = 'card-thumb-placeholder';
|
|
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('fill', 'none');
|
|
svg.setAttribute('stroke', 'currentColor'); svg.setAttribute('stroke-width', '1.5');
|
|
var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
rect.setAttribute('x', '2'); rect.setAttribute('y', '2');
|
|
rect.setAttribute('width', '20'); rect.setAttribute('height', '20');
|
|
rect.setAttribute('rx', '2');
|
|
var circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
circle.setAttribute('cx', '8.5'); circle.setAttribute('cy', '8.5'); circle.setAttribute('r', '1.5');
|
|
var p = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
p.setAttribute('d', 'm21 15-5-5L5 21');
|
|
svg.appendChild(rect); svg.appendChild(circle); svg.appendChild(p);
|
|
ph.appendChild(svg);
|
|
thumb.appendChild(ph);
|
|
}
|
|
|
|
// codec tags
|
|
if (r.codecs && r.codecs.length) {
|
|
var overlay = document.createElement('div');
|
|
overlay.className = 'card-overlay';
|
|
r.codecs.forEach(function(c) {
|
|
var tag = document.createElement('span');
|
|
tag.className = 'card-tag';
|
|
tag.textContent = c;
|
|
overlay.appendChild(tag);
|
|
});
|
|
thumb.appendChild(overlay);
|
|
}
|
|
|
|
// resolution badge
|
|
if (r.width && r.height) {
|
|
var res = document.createElement('span');
|
|
res.className = 'card-resolution';
|
|
res.textContent = r.width + 'x' + r.height;
|
|
thumb.appendChild(res);
|
|
}
|
|
|
|
card.appendChild(thumb);
|
|
|
|
// body
|
|
var body = document.createElement('div');
|
|
body.className = 'card-body';
|
|
|
|
var urlDiv = document.createElement('div');
|
|
urlDiv.className = 'card-url';
|
|
var schemeEnd = r.source.indexOf('://');
|
|
if (schemeEnd > 0) {
|
|
var schemeSpan = document.createElement('span');
|
|
schemeSpan.className = 'scheme';
|
|
schemeSpan.textContent = r.source.substring(0, schemeEnd + 3);
|
|
urlDiv.appendChild(schemeSpan);
|
|
// mask creds in display
|
|
var rest = r.source.substring(schemeEnd + 3);
|
|
var atIdx = rest.indexOf('@');
|
|
if (atIdx > 0) {
|
|
var auth = rest.substring(0, atIdx);
|
|
var colonIdx = auth.indexOf(':');
|
|
if (colonIdx > 0) {
|
|
urlDiv.appendChild(document.createTextNode(auth.substring(0, colonIdx + 1) + '***@'));
|
|
urlDiv.appendChild(document.createTextNode(rest.substring(atIdx + 1)));
|
|
} else {
|
|
urlDiv.appendChild(document.createTextNode(rest));
|
|
}
|
|
} else {
|
|
urlDiv.appendChild(document.createTextNode(rest));
|
|
}
|
|
} else {
|
|
urlDiv.textContent = r.source;
|
|
}
|
|
body.appendChild(urlDiv);
|
|
|
|
// meta
|
|
var meta = document.createElement('div');
|
|
meta.className = 'card-meta';
|
|
|
|
if (r.latency_ms !== undefined) {
|
|
var lat = document.createElement('span');
|
|
lat.className = 'card-meta-item';
|
|
if (r.latency_ms < 200) lat.classList.add('latency-fast');
|
|
else if (r.latency_ms < 500) lat.classList.add('latency-medium');
|
|
else lat.classList.add('latency-slow');
|
|
lat.textContent = r.latency_ms + 'ms';
|
|
meta.appendChild(lat);
|
|
}
|
|
|
|
if (r.width && r.height) {
|
|
var resItem = document.createElement('span');
|
|
resItem.className = 'card-meta-item';
|
|
resItem.textContent = r.width + 'x' + r.height;
|
|
meta.appendChild(resItem);
|
|
}
|
|
|
|
body.appendChild(meta);
|
|
card.appendChild(body);
|
|
|
|
// action button
|
|
var action = document.createElement('div');
|
|
action.className = 'card-action';
|
|
|
|
var btn = document.createElement('button');
|
|
btn.className = 'btn-use';
|
|
btn.textContent = mode === 'sub' ? 'Use as Sub Stream' : 'Use as Main Stream';
|
|
btn.addEventListener('click', function() {
|
|
selectStream(r);
|
|
});
|
|
|
|
action.appendChild(btn);
|
|
card.appendChild(action);
|
|
|
|
return card;
|
|
}
|
|
|
|
function selectStream(r) {
|
|
var p = new URLSearchParams();
|
|
|
|
if (mode === 'sub') {
|
|
// sub mode: go to config with both main and sub
|
|
if (mainStream) p.set('main', mainStream);
|
|
p.set('sub', r.source);
|
|
} else {
|
|
// main mode: go to config with main, session id for potential sub selection
|
|
p.set('main', r.source);
|
|
p.set('session', sessionId);
|
|
}
|
|
|
|
// pass through all known params
|
|
if (ip) p.set('ip', ip);
|
|
if (mac) p.set('mac', mac);
|
|
if (vendor) p.set('vendor', vendor);
|
|
if (model) p.set('model', model);
|
|
if (server) p.set('server', server);
|
|
if (hostname) p.set('hostname', hostname);
|
|
if (ports) p.set('ports', ports);
|
|
if (user) p.set('user', user);
|
|
if (channel) p.set('channel', channel);
|
|
if (ids) p.set('ids', ids);
|
|
if (r.width) p.set('main_width', r.width);
|
|
if (r.height) p.set('main_height', r.height);
|
|
|
|
window.location.href = 'config.html?' + p.toString();
|
|
}
|
|
|
|
function showToast(msg) {
|
|
var t = document.getElementById('toast');
|
|
t.textContent = msg;
|
|
t.classList.remove('hidden');
|
|
t.classList.add('show');
|
|
setTimeout(function() {
|
|
t.classList.remove('show');
|
|
setTimeout(function() { t.classList.add('hidden'); }, 250);
|
|
}, 3000);
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|