Add a mesh scope to the Streams card
Local stays the default and the cheap path; mesh asks each partner for its own sessions over SSH, live rather than cached, because a stream is true for minutes and a cached one would be confidently wrong about the only thing the card exists to say.
This commit is contained in:
@@ -258,6 +258,113 @@ function vv_media_sessions(): array {
|
||||
];
|
||||
}
|
||||
|
||||
// Who is watching, across the whole mesh.
|
||||
//
|
||||
// Fetched live rather than read from a cache, unlike vv_media_servers_mesh(). A server's version
|
||||
// and CPU are true for hours; a stream is true for minutes, and a "now playing" card assembled
|
||||
// from a two-hour-old file would be confidently wrong about the one thing it exists to say. The
|
||||
// cost is only paid when the operator asks for the mesh view — the card polls local by default.
|
||||
//
|
||||
// Each partner answers about itself for the reason the arr and media-server collectors do: its
|
||||
// Emby URL is http://localhost:8096, which is true there and meaningless here, and its API keys
|
||||
// never leave it. Same transport as remote_arr_cache_writer.sh, at a faster cadence, so the
|
||||
// connection is multiplexed and every hop is bounded.
|
||||
//
|
||||
// A partner that cannot be reached is reported as unreachable, never as zero streams. Those look
|
||||
// identical in a total and mean opposite things.
|
||||
function vv_media_sessions_mesh(): array {
|
||||
$me = vv_detect_host();
|
||||
$hosts = function_exists('vv_known_hosts') ? vv_known_hosts() : [$me => $me];
|
||||
|
||||
$nodes = [];
|
||||
$sessions = [];
|
||||
$names = [];
|
||||
$count = 0;
|
||||
|
||||
foreach ($hosts as $h => $hostName) {
|
||||
if ($h === $me) {
|
||||
$local = vv_media_sessions();
|
||||
$nodes[] = ['host' => $h, 'name' => $hostName, 'local' => true, 'reachable' => true,
|
||||
'server_count' => $local['server_count'], 'stream_count' => count($local['sessions'])];
|
||||
foreach ($local['sessions'] as $s) {
|
||||
$s['_host'] = $h; $s['_hostName'] = $hostName; $s['_local'] = true;
|
||||
$sessions[] = $s;
|
||||
}
|
||||
$names = array_merge($names, $local['server_names']);
|
||||
$count += $local['server_count'];
|
||||
continue;
|
||||
}
|
||||
|
||||
$remote = vv_media_sessions_remote($h, $hostName);
|
||||
$nodes[] = ['host' => $h, 'name' => $hostName, 'local' => false,
|
||||
'reachable' => $remote !== null,
|
||||
'server_count' => $remote['server_count'] ?? 0,
|
||||
'stream_count' => $remote !== null ? count($remote['sessions']) : 0];
|
||||
if ($remote === null) continue;
|
||||
|
||||
foreach ($remote['sessions'] as $s) {
|
||||
$s['_host'] = $h; $s['_hostName'] = $hostName; $s['_local'] = false;
|
||||
$sessions[] = $s;
|
||||
}
|
||||
$names = array_merge($names, $remote['server_names']);
|
||||
$count += $remote['server_count'];
|
||||
}
|
||||
|
||||
return [
|
||||
'scope' => 'mesh',
|
||||
'nodes' => $nodes,
|
||||
'sessions' => $sessions,
|
||||
'server_names' => $names,
|
||||
'server_count' => $count,
|
||||
];
|
||||
}
|
||||
|
||||
// One partner's sessions, or null when it could not be asked.
|
||||
//
|
||||
// Null rather than an empty payload, deliberately: the caller has to be able to distinguish "that
|
||||
// node has nobody watching" from "that node did not answer", and an empty array cannot carry the
|
||||
// difference.
|
||||
function vv_media_sessions_remote(string $host, string $hostName): ?array {
|
||||
$vars = vv_conf_vars();
|
||||
$sshKey = $vars[strtoupper(vv_detect_host()) . '_SSH_KEY'] ?? '';
|
||||
if (!$sshKey || !is_file($sshKey)) return null;
|
||||
|
||||
$ip = vv_resolve_tailscale_ip($hostName);
|
||||
if (!$ip) return null;
|
||||
|
||||
// The WebGUI symlink, which is where Unraid serves the plugin from on every node whatever its
|
||||
// storage mode — the same path remote_arr_cache_writer.sh uses, and not a guess about where
|
||||
// the partner installed itself.
|
||||
$php = 'php -r ' . escapeshellarg(
|
||||
"require_once '/usr/local/emhttp/plugins/varaverk/include/media.php';"
|
||||
. " echo json_encode(vv_media_sessions());"
|
||||
);
|
||||
|
||||
// ControlPersist because the card polls this every 12 seconds while the mesh view is open, and
|
||||
// a fresh handshake per poll per partner is most of the cost. Timeouts are short: a dark
|
||||
// partner must render as unreachable quickly, not hold the whole card.
|
||||
$sock = rtrim(VV_CACHE_ROOT, '/') . '/ssh';
|
||||
if (!is_dir($sock)) @mkdir($sock, 0700, true);
|
||||
|
||||
$cmd = 'timeout 8 ssh -i ' . escapeshellarg($sshKey)
|
||||
. ' -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=5'
|
||||
. ' -o ControlMaster=auto -o ControlPersist=60s'
|
||||
. ' -o ControlPath=' . escapeshellarg($sock . '/media-%h')
|
||||
. ' root@' . escapeshellarg($ip) . ' ' . escapeshellarg($php) . ' 2>/dev/null';
|
||||
|
||||
$raw = shell_exec($cmd);
|
||||
if (!$raw) return null;
|
||||
|
||||
$d = json_decode(trim($raw), true);
|
||||
if (!is_array($d) || !isset($d['sessions'])) return null;
|
||||
|
||||
return [
|
||||
'sessions' => is_array($d['sessions']) ? $d['sessions'] : [],
|
||||
'server_names' => is_array($d['server_names'] ?? null) ? $d['server_names'] : [],
|
||||
'server_count' => (int)($d['server_count'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
// ── What each media server is actually doing ──────────────────────────────────────────────────
|
||||
// Two halves that only mean something together. The application knows its version, whether an
|
||||
// update is waiting and who is watching; the container knows what that is costing in CPU, memory
|
||||
|
||||
Reference in New Issue
Block a user