<?php

namespace XcVm\Domain\Stream;

use XcVm\Core\Config\SettingsManager;
use XcVm\Core\Process\ProcessManager;
use XcVm\Core\Util\GeoIP;
use XcVm\Domain\Server\ServerRepository;

/**
 * ConnectionTracker — live streaming connection management.
 *
 * Tracks connection lifecycle (create, update, close), stores state in Redis
 * sorted sets (LIVE, LINE#, STREAM#, SERVER#, PROXY#) with MySQL fallback.
 * Provides server load calculation, batch connection queries by user/server/stream,
 * and closed connection activity logging.
 *
 * @package XC_VM_Domain_Stream
 * @author  Divarion_D <https://github.com/Divarion-D>
 * @copyright 2025-2026 Vateron Media
 * @link    https://github.com/Vateron-Media/XC_VM
 * @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
 */

class ConnectionTracker {
	use \XcVm\Infrastructure\Database\DatabaseAware;
	/**
	 * Calculate server/proxy load capacity.
	 *
	 * Counts active connections via Redis zCard or MySQL COUNT, then computes
	 * load ratio using the configured strategy: band, maxclients, guar_band, or client count.
	 * Result is cached to a file.
	 *
	 * @param bool $rProxy If true — calculate for proxy servers, otherwise for main servers.
	 * @return array<int, array{online_clients: int, capacity?: float}> Map of serverID => load data.
	 */
	public static function getCapacity(bool $rProxy = false): array {
		global $rSettings, $rServers;
		$db = self::db();
		$rRedis = \XcVm\Infrastructure\Redis\RedisManager::instance();
		$rFile = ($rProxy ? 'proxy_capacity' : 'servers_capacity');
		if ($rSettings['redis_handler'] && $rProxy && $rSettings['split_by'] == 'maxclients') {
			$rSettings['split_by'] = 'guar_band';
		}

		if ($rSettings['redis_handler'] && $rRedis) {
			$rRows = array();
			$rResults = null;
			// One reconnect+retry: phpredis may silently reconnect a broken
			// socket without replaying AUTH, so a healthy-looking connection
			// can suddenly throw NOAUTH (see RedisManager::reconnect()).
			for ($rAttempt = 0; $rAttempt < 2 && $rRedis; $rAttempt++) {
				try {
					$rMulti = $rRedis->multi();
					foreach (array_keys($rServers) as $rServerID) {
						if ($rServers[$rServerID]['server_online']) {
							$rMulti->zCard((($rProxy ? 'PROXY#' : 'SERVER#')) . $rServerID);
						}
					}
					$rResults = $rMulti->exec();
					break;
				} catch (\RedisException $e) {
					$rRedis = \XcVm\Infrastructure\Redis\RedisManager::reconnect();
				}
			}
			if (!is_array($rResults)) {
				$rResults = [];
			}
			$i = 0;
			foreach (array_keys($rServers) as $rServerID) {
				if ($rServers[$rServerID]['server_online']) {
					$rRows[$rServerID] = array('online_clients' => ($rResults[$i] ?? 0));
					$i++;
				}
			}
		} else {
			if ($rProxy) {
				$db->query('SELECT `proxy_id`, COUNT(*) AS `online_clients` FROM `lines_live` WHERE `proxy_id` <> 0 AND `hls_end` = 0 GROUP BY `proxy_id`;');
				$rRows = $db->get_rows(true, 'proxy_id');
			} else {
				$db->query('SELECT `server_id`, COUNT(*) AS `online_clients` FROM `lines_live` WHERE `server_id` <> 0 AND `hls_end` = 0 GROUP BY `server_id`;');
				$rRows = $db->get_rows(true, 'server_id');
			}
		}

		if ($rSettings['split_by'] == 'band') {
			$rServerSpeed = array();
			foreach (array_keys($rServers) as $rServerID) {
				$rServerHardware = json_decode($rServers[$rServerID]['server_hardware'], true);
				if (!empty($rServerHardware['network_speed'])) {
					$rServerSpeed[$rServerID] = (float) $rServerHardware['network_speed'];
				} else {
					if (0 < $rServers[$rServerID]['network_guaranteed_speed']) {
						$rServerSpeed[$rServerID] = $rServers[$rServerID]['network_guaranteed_speed'];
					} else {
						$rServerSpeed[$rServerID] = 1000;
					}
				}
			}
			foreach ($rRows as $rServerID => $rRow) {
				$rCurrentOutput = intval($rServers[$rServerID]['watchdog']['bytes_sent'] / 125000);
				$rRows[$rServerID]['capacity'] = (float) ($rCurrentOutput / (($rServerSpeed[$rServerID] ?: 1000)));
			}
		} else {
			if ($rSettings['split_by'] == 'maxclients') {
				foreach ($rRows as $rServerID => $rRow) {
					$rRows[$rServerID]['capacity'] = (float) ($rRow['online_clients'] / (($rServers[$rServerID]['total_clients'] ?: 1)));
				}
			} else {
				if ($rSettings['split_by'] == 'guar_band') {
					foreach ($rRows as $rServerID => $rRow) {
						$rCurrentOutput = intval($rServers[$rServerID]['watchdog']['bytes_sent'] / 125000);
						$rRows[$rServerID]['capacity'] = (float) ($rCurrentOutput / (($rServers[$rServerID]['network_guaranteed_speed'] ?: 1)));
					}
				} else {
					foreach ($rRows as $rServerID => $rRow) {
						$rRows[$rServerID]['capacity'] = $rRow['online_clients'];
					}
				}
			}
		}

		if (defined('CACHE_TMP_PATH') && is_dir(CACHE_TMP_PATH) && is_writable(CACHE_TMP_PATH)) {
			file_put_contents(CACHE_TMP_PATH . $rFile, json_encode($rRows), LOCK_EX);
		}
		return $rRows;
	}

	/**
	 * Get connections filtered by server, user, or stream.
	 *
	 * In Redis mode returns [keys[], deserialized data[]].
	 * In MySQL mode performs a JOIN query with lines, streams, streams_servers tables.
	 *
	 * @param int|null $rServerID Server ID to filter by.
	 * @param int|null $rUserID   User ID to filter by.
	 * @param int|null $rStreamID Stream ID to filter by.
	 * @return array Connections: [keys[], data[]] for Redis or rows for MySQL.
	 */
	public static function getConnections(?int $rServerID = null, ?int $rUserID = null, ?int $rStreamID = null): array {
		global $rSettings;
		$db = self::db();
		$rRedis = \XcVm\Infrastructure\Redis\RedisManager::instance();
		if ($rSettings['redis_handler'] && $rRedis) {
			if ($rServerID) {
				$rKeys = $rRedis->zRangeByScore('SERVER#' . $rServerID, '-inf', '+inf');
			} elseif ($rUserID) {
				$rKeys = $rRedis->zRangeByScore('LINE#' . $rUserID, '-inf', '+inf');
			} elseif ($rStreamID) {
				$rKeys = $rRedis->zRangeByScore('STREAM#' . $rStreamID, '-inf', '+inf');
			} else {
				$rKeys = $rRedis->zRangeByScore('LIVE', '-inf', '+inf');
			}

			// zRangeByScore/mGet return false on a failed connection (e.g. an
			// unauthenticated socket during a Redis restart) — degrade to empty.
			if (is_array($rKeys) && count($rKeys) > 0) {
				$rData = $rRedis->mGet($rKeys);
				if (is_array($rData)) {
					return array($rKeys, array_map(static function ($rItem) {
						return is_string($rItem) ? igbinary_unserialize($rItem) : false;
					}, $rData));
				}
			}
			return array([], []);
		}

		$rWhere = array();
		if (!empty($rServerID)) {
			$rWhere[] = 't1.server_id = ' . intval($rServerID);
		}
		if (!empty($rUserID)) {
			$rWhere[] = 't1.user_id = ' . intval($rUserID);
		}
		$rExtra = count($rWhere) ? 'WHERE ' . implode(' AND ', $rWhere) : '';
		$rQuery = 'SELECT t2.*,t3.*,t5.bitrate,t1.*,t1.uuid AS `uuid` 
               FROM `lines_live` t1 
               LEFT JOIN `lines` t2 ON t2.id = t1.user_id 
               LEFT JOIN `streams` t3 ON t3.id = t1.stream_id 
               LEFT JOIN `streams_servers` t5 ON t5.stream_id = t1.stream_id AND t5.server_id = t1.server_id 
               ' . $rExtra . ' 
               ORDER BY t1.activity_id ASC';
		$db->query($rQuery);
		return $db->get_rows(true, 'user_id', false);
	}

	/**
	 * Get the main server ID.
	 *
	 * @return int|null Server ID with is_main flag, or null if not found.
	 */
	public static function getMainID(): ?int {
		global $rServers;
		foreach ($rServers as $rServerID => $rServer) {
			if ($rServer['is_main']) {
				return $rServerID;
			}
		}
		return null;
	}

	/**
	 * Add a process PID to the stream processing queue.
	 *
	 * Queue is stored on disk (igbinary). Dead PIDs are automatically
	 * filtered out on each call.
	 *
	 * @param int $rStreamID Stream ID.
	 * @param int $rAddPID   Process PID to add.
	 * @return void
	 */
	public static function addToQueue(int $rStreamID, int $rAddPID): void {
		$rActivePIDs = $rPIDs = array();
		if (file_exists(SIGNALS_TMP_PATH . 'queue_' . intval($rStreamID))) {
			$rPIDs = igbinary_unserialize(file_get_contents(SIGNALS_TMP_PATH . 'queue_' . intval($rStreamID)));
		}
		foreach ($rPIDs as $rPID) {
			if (ProcessManager::isRunning($rPID, 'php-fpm')) {
				$rActivePIDs[] = $rPID;
			}
		}
		if (!in_array($rAddPID, $rActivePIDs, true)) {
			$rActivePIDs[] = $rAddPID;
		}
		file_put_contents(SIGNALS_TMP_PATH . 'queue_' . intval($rStreamID), igbinary_serialize($rActivePIDs), LOCK_EX);
	}

	/**
	 * Remove a process PID from the stream processing queue.
	 *
	 * If the queue becomes empty, the file is deleted.
	 *
	 * @param int $rStreamID Stream ID.
	 * @param int $rPID      Process PID to remove.
	 * @return void
	 */
	public static function removeFromQueue(int $rStreamID, int $rPID): void {
		$rActivePIDs = array();
		foreach ((igbinary_unserialize(file_get_contents(SIGNALS_TMP_PATH . 'queue_' . intval($rStreamID))) ?: array()) as $rActivePID) {
			if (ProcessManager::isRunning($rActivePID, 'php-fpm') && $rPID != $rActivePID) {
				$rActivePIDs[] = $rActivePID;
			}
		}
		if (0 < count($rActivePIDs)) {
			file_put_contents(SIGNALS_TMP_PATH . 'queue_' . intval($rStreamID), igbinary_serialize($rActivePIDs), LOCK_EX);
		} else {
			@unlink(SIGNALS_TMP_PATH . 'queue_' . intval($rStreamID));
		}
	}

	/**
	 * Update a connection in Redis with applied changes.
	 *
	 * With $rOption='open' — adds UUID to all sorted sets (LIVE, LINE#, STREAM#, SERVER#, etc.).
	 * With $rOption='close' — removes UUID from sorted sets and marks as ENDED.
	 * Data is igbinary-serialized and saved atomically via MULTI/EXEC.
	 *
	 * @param array       $rData    Current connection data.
	 * @param array       $rChanges Fields to update (key => value).
	 * @param string|null $rOption  Action: 'open', 'close', or null (data update only).
	 * @return array|null Updated connection data, or null on exec failure.
	 */
	public static function updateConnection(array $rData, array $rChanges = [], ?string $rOption = null): ?array {
		$rRedis = \XcVm\Infrastructure\Redis\RedisManager::instance();
		if (!$rRedis) {
			return null;
		}
		$rOrigData = $rData;
		foreach ($rChanges as $rKey => $rValue) {
			$rData[$rKey] = $rValue;
		}
		$rMulti = $rRedis->multi();
		if ($rOption == 'open') {
			$rMulti->sRem('ENDED', $rData['uuid']);
			$rMulti->zAdd('LIVE', $rData['date_start'], $rData['uuid']);
			$rMulti->zAdd('LINE#' . $rData['identity'], $rData['date_start'], $rData['uuid']);
			$rMulti->zAdd('STREAM#' . $rData['stream_id'], $rData['date_start'], $rData['uuid']);
			$rMulti->zAdd('SERVER#' . $rData['server_id'], $rData['date_start'], $rData['uuid']);
			if ($rData['proxy_id']) {
				$rMulti->zAdd('PROXY#' . $rData['proxy_id'], $rData['date_start'], $rData['uuid']);
			}
			if ($rData['hls_end'] == 1) {
				$rData['hls_end'] = 0;
				if ($rData['user_id']) {
					$rMulti->zAdd('SERVER_LINES#' . $rData['server_id'], $rData['user_id'], $rData['uuid']);
				}
			}
		} else {
			if ($rOption == 'close') {
				$rMulti->sAdd('ENDED', $rData['uuid']);
				$rMulti->zRem('LIVE', $rData['uuid']);
				$rMulti->zRem('LINE#' . $rOrigData['identity'], $rData['uuid']);
				$rMulti->zRem('STREAM#' . $rOrigData['stream_id'], $rData['uuid']);
				$rMulti->zRem('SERVER#' . $rOrigData['server_id'], $rData['uuid']);
				if ($rData['proxy_id']) {
					$rMulti->zRem('PROXY#' . $rOrigData['proxy_id'], $rData['uuid']);
				}
				if ($rData['hls_end'] == 0) {
					$rData['hls_end'] = 1;
					if ($rData['user_id']) {
						$rMulti->zRem('SERVER_LINES#' . $rOrigData['server_id'], $rData['uuid']);
					}
				}
			}
		}
		$rMulti->set($rData['uuid'], igbinary_serialize($rData));
		if ($rMulti->exec()) {
			return $rData;
		}
		return null;
	}

	/**
	 * Send a signal to a server via Redis.
	 *
	 * Creates an entry in SIGNALS#{serverID} set and stores signal data.
	 * Used for remote termination of RTMP/HLS streams on other servers.
	 *
	 * @param int        $rPID        Process PID to terminate.
	 * @param int        $rServerID   Target server ID.
	 * @param int        $rRTMP       1 — RTMP, 0 — regular process.
	 * @param mixed|null $rCustomData Additional signal data.
	 * @return array|false MULTI/EXEC result.
	 */
	public static function redisSignal(int $rPID, int $rServerID, int $rRTMP, $rCustomData = null) {
		$rRedis = \XcVm\Infrastructure\Redis\RedisManager::instance();
		if (!$rRedis) {
			return false;
		}
		$rKey = 'SIGNAL#' . md5($rServerID . '#' . $rPID . '#' . $rRTMP);
		$rData = array('pid' => $rPID, 'server_id' => $rServerID, 'rtmp' => $rRTMP, 'time' => time(), 'custom_data' => $rCustomData, 'key' => $rKey);
		return $rRedis->multi()->sAdd('SIGNALS#' . $rServerID, $rKey)->set($rKey, igbinary_serialize($rData))->exec();
	}

	/**
	 * Get connections for multiple users (batch).
	 *
	 * Uses MULTI pipeline for parallel LINE# sorted set queries.
	 *
	 * @param int[] $rUserIDs  Array of user IDs.
	 * @param bool  $rCount    If true — return only connection count per user.
	 * @param bool  $rKeysOnly If true — return only Redis keys (UUIDs) without deserialization.
	 * @return array Map of userID => connections[] (or count, or keys).
	 */
	public static function getUserConnections(array $rUserIDs, bool $rCount = false, bool $rKeysOnly = false): array {
		$rRedis = \XcVm\Infrastructure\Redis\RedisManager::instance();
		if (!$rRedis) {
			return [];
		}
		$rMulti = $rRedis->multi();
		foreach ($rUserIDs as $rUserID) {
			$rMulti->zRevRangeByScore('LINE#' . $rUserID, '+inf', '-inf');
		}
		$rGroups = $rMulti->exec();
		$rConnectionMap = $rRedisKeys = array();
		if (!is_array($rGroups)) {
			return ($rKeysOnly ? $rRedisKeys : $rConnectionMap);
		}
		foreach ($rGroups as $rGroupID => $rKeys) {
			if ($rCount) {
				$rConnectionMap[$rUserIDs[$rGroupID]] = count($rKeys);
			} else {
				if (0 < count($rKeys)) {
					$rRedisKeys = array_merge($rRedisKeys, $rKeys);
				}
			}
		}
		$rRedisKeys = array_unique($rRedisKeys);
		if (!$rKeysOnly) {
			if (!$rCount && !empty($rRedisKeys)) {
				foreach ($rRedis->mGet($rRedisKeys) as $rRow) {
					$rRow = igbinary_unserialize($rRow);
					$rConnectionMap[$rRow['user_id']][] = $rRow;
				}
			}
			return $rConnectionMap;
		}
		return $rRedisKeys;
	}

	/**
	 * Get connections for multiple servers/proxies (batch).
	 *
	 * Uses MULTI pipeline for parallel SERVER#/PROXY# sorted set queries.
	 *
	 * @param int[] $rServerIDs Array of server IDs.
	 * @param bool  $rProxy     If true — query PROXY# instead of SERVER#.
	 * @param bool  $rCount     If true — return only count.
	 * @param bool  $rKeysOnly  If true — return only UUID keys.
	 * @return array Map of serverID => connections[] (or count, or keys).
	 */
	public static function getServerConnections(array $rServerIDs, bool $rProxy = false, bool $rCount = false, bool $rKeysOnly = false): array {
		$rRedis = \XcVm\Infrastructure\Redis\RedisManager::instance();
		if (!$rRedis) {
			return [];
		}
		$rMulti = $rRedis->multi();
		foreach ($rServerIDs as $rServerID) {
			$rMulti->zRevRangeByScore(($rProxy ? 'PROXY#' . $rServerID : 'SERVER#' . $rServerID), '+inf', '-inf');
		}
		$rGroups = $rMulti->exec();
		$rConnectionMap = $rRedisKeys = array();
		if (!is_array($rGroups)) {
			return ($rKeysOnly ? $rRedisKeys : $rConnectionMap);
		}
		foreach ($rGroups as $rGroupID => $rKeys) {
			if ($rCount) {
				$rConnectionMap[$rServerIDs[$rGroupID]] = count($rKeys);
			} else {
				if (0 < count($rKeys)) {
					$rRedisKeys = array_merge($rRedisKeys, $rKeys);
				}
			}
		}
		$rRedisKeys = array_unique($rRedisKeys);
		if (!$rKeysOnly) {
			if (!$rCount && !empty($rRedisKeys)) {
				foreach ($rRedis->mGet($rRedisKeys) as $rRow) {
					$rRow = igbinary_unserialize($rRow);
					$rConnectionMap[$rRow['server_id']][] = $rRow;
				}
			}
			return $rConnectionMap;
		}
		return $rRedisKeys;
	}

	/**
	 * Get the most recent connection for each user.
	 *
	 * Queries LINE# with LIMIT 0,1 via MULTI pipeline and deserializes results.
	 *
	 * @param int[] $rUserIDs Array of user IDs.
	 * @return array<int, array> Map of userID => connection data.
	 */
	public static function getFirstConnection(array $rUserIDs): array {
		$rRedis = \XcVm\Infrastructure\Redis\RedisManager::instance();
		if (!$rRedis) {
			return [];
		}
		$rMulti = $rRedis->multi();
		foreach ($rUserIDs as $rUserID) {
			$rMulti->zRevRangeByScore('LINE#' . $rUserID, '+inf', '-inf', array('limit' => array(0, 1)));
		}
		$rGroups = $rMulti->exec();
		$rConnectionMap = $rRedisKeys = array();
		if (!is_array($rGroups)) {
			return $rConnectionMap;
		}
		foreach ($rGroups as $rKeys) {
			if (0 < count($rKeys)) {
				$rRedisKeys[] = $rKeys[0];
			}
		}
		if (empty($rRedisKeys)) {
			return $rConnectionMap;
		}
		foreach ($rRedis->mGet(array_unique($rRedisKeys)) as $rRow) {
			$rRow = igbinary_unserialize($rRow);
			$rConnectionMap[$rRow['user_id']] = $rRow;
		}
		return $rConnectionMap;
	}

	/**
	 * Get connections for multiple streams (batch).
	 *
	 * Uses MULTI pipeline for parallel STREAM# sorted set queries.
	 *
	 * @param int[] $rStreamIDs Array of stream IDs.
	 * @param bool  $rGroup     If true — group by stream_id, otherwise by stream_id + server_id.
	 * @param bool  $rCount     If true — return only connection count per stream.
	 * @return array Map of streamID => connections[] (or count).
	 */
	public static function getStreamConnections(array $rStreamIDs, bool $rGroup = true, bool $rCount = false): array {
		$rRedis = \XcVm\Infrastructure\Redis\RedisManager::instance();
		if (!$rRedis) {
			return [];
		}
		$rMulti = $rRedis->multi();
		foreach ($rStreamIDs as $rStreamID) {
			$rMulti->zRevRangeByScore('STREAM#' . $rStreamID, '+inf', '-inf');
		}
		$rGroups = $rMulti->exec();
		$rConnectionMap = $rRedisKeys = array();
		if (!is_array($rGroups)) {
			return $rConnectionMap;
		}
		foreach ($rGroups as $rGroupID => $rKeys) {
			if ($rCount) {
				$rConnectionMap[$rStreamIDs[$rGroupID]] = count($rKeys);
			} else {
				if (0 < count($rKeys)) {
					$rRedisKeys = array_merge($rRedisKeys, $rKeys);
				}
			}
		}
		if (!$rCount && !empty($rRedisKeys)) {
			foreach ($rRedis->mGet(array_unique($rRedisKeys)) as $rRow) {
				$rRow = igbinary_unserialize($rRow);
				if ($rGroup) {
					$rConnectionMap[$rRow['stream_id']][] = $rRow;
				} else {
					$rConnectionMap[$rRow['stream_id']][$rRow['server_id']][] = $rRow;
				}
			}
		}
		return $rConnectionMap;
	}

	/**
	 * Stream IDs this server is actively serving on-demand — an on_demand row
	 * with a running feed (pid set). Used by the on-demand killer to know which
	 * streams to check for idleness.
	 *
	 * @param int $rServerID This server's id.
	 * @return array<int,int> List of stream ids.
	 */
	public static function activeOnDemandStreamIDs(int $rServerID): array {
		$db = self::db();
		$db->query("SELECT stream_id FROM streams_servers WHERE server_id = ? AND on_demand = 1 AND pid IS NOT NULL AND pid > 0", $rServerID);
		return $db->get_column();
	}

	/**
	 * For each given stream, how many child servers are actively restreaming it
	 * from this server (a live parent_id row with both a running feed and
	 * monitor). A stream with attached restreamers must not be killed.
	 *
	 * @param array<int,int> $rStreamIDs Streams to count for.
	 * @param int            $rServerID  This server's id (the parent).
	 * @return array<int,int> stream_id => restreamer count.
	 */
	public static function attachedRestreamCounts(array $rStreamIDs, int $rServerID): array {
		if (empty($rStreamIDs)) {
			return [];
		}
		$db = self::db();
		$rPlaceholders = str_repeat('?,', count($rStreamIDs) - 1) . '?';
		$db->query("SELECT stream_id, COUNT(*) AS cnt FROM streams_servers WHERE parent_id = ? AND pid > 0 AND monitor_pid > 0 AND stream_id IN ($rPlaceholders) GROUP BY stream_id", $rServerID, ...$rStreamIDs);
		$rCounts = [];
		foreach ($db->get_rows(true, 'stream_id') as $rID => $rRow) {
			$rCounts[$rID] = (int) $rRow['cnt'];
		}
		return $rCounts;
	}

	/**
	 * DB fallback for the live viewer count per stream on this server (used when
	 * the Redis connection store is off — otherwise getStreamConnections covers
	 * it). Counts open live lines (hls_end = 0).
	 *
	 * @param array<int,int> $rStreamIDs Streams to count for.
	 * @param int            $rServerID  This server's id.
	 * @return array<int,int> stream_id => viewer count.
	 */
	public static function onlineClientCounts(array $rStreamIDs, int $rServerID): array {
		if (empty($rStreamIDs)) {
			return [];
		}
		$db = self::db();
		$rPlaceholders = str_repeat('?,', count($rStreamIDs) - 1) . '?';
		$db->query("SELECT stream_id, COUNT(*) AS cnt FROM lines_live WHERE server_id = ? AND hls_end = 0 AND stream_id IN ($rPlaceholders) GROUP BY stream_id", $rServerID, ...$rStreamIDs);
		$rCounts = [];
		foreach ($db->get_rows(true, 'stream_id') as $rID => $rRow) {
			$rCounts[$rID] = (int) $rRow['cnt'];
		}
		return $rCounts;
	}

	/**
	 * Universal Redis connection query with multiple filters.
	 *
	 * Reads from LIVE/LINE#/STREAM#/SERVER# depending on provided filters.
	 * Supports counting, grouping by user identity, and HLS filtering.
	 *
	 * @param int|null $rUserID    Filter by user.
	 * @param int|null $rServerID  Filter by server.
	 * @param int|null $rStreamID  Filter by stream.
	 * @param bool     $rOpenOnly  Only open connections (hls_end=0).
	 * @param bool     $rCountOnly Return [total, unique_users] instead of data.
	 * @param bool     $rGroup     Group by user/identity.
	 * @param bool     $rHLSOnly   Exclude HLS connections.
	 * @return array Connections grouped by identity, or [count, unique].
	 */
	public static function getRedisConnections(?int $rUserID = null, ?int $rServerID = null, ?int $rStreamID = null, bool $rOpenOnly = false, bool $rCountOnly = false, bool $rGroup = true, bool $rHLSOnly = false): array {
		$rRedis = \XcVm\Infrastructure\Redis\RedisManager::instance();
		if (!$rRedis) {
			return ($rCountOnly ? array(0, 0) : array());
		}
		$rReturn = ($rCountOnly ? array(0, 0) : array());
		$rUniqueUsers = array();
		$rUserID = (0 < intval($rUserID) ? intval($rUserID) : null);
		$rServerID = (0 < intval($rServerID) ? intval($rServerID) : null);
		$rStreamID = (0 < intval($rStreamID) ? intval($rStreamID) : null);

		if ($rUserID) {
			$rKeys = $rRedis->zRangeByScore('LINE#' . $rUserID, '-inf', '+inf');
		} else {
			if ($rStreamID) {
				$rKeys = $rRedis->zRangeByScore('STREAM#' . $rStreamID, '-inf', '+inf');
			} else {
				if ($rServerID) {
					$rKeys = $rRedis->zRangeByScore('SERVER#' . $rServerID, '-inf', '+inf');
				} else {
					$rKeys = $rRedis->zRangeByScore('LIVE', '-inf', '+inf');
				}
			}
		}

		if (0 < count($rKeys)) {
			foreach ($rRedis->mGet(array_unique($rKeys)) as $rRow) {
				$rRow = igbinary_unserialize($rRow);
				if (!($rServerID && $rServerID != $rRow['server_id']) && !($rStreamID && $rStreamID != $rRow['stream_id']) && !($rUserID && $rUserID != $rRow['user_id']) && !($rHLSOnly && $rRow['container'] == 'hls')) {
					$rUUID = ($rRow['user_id'] ?: $rRow['hmac_id'] . '_' . $rRow['hmac_identifier']);
					if ($rCountOnly) {
						$rReturn[0]++;
						$rUniqueUsers[] = $rUUID;
					} else {
						if ($rGroup) {
							if (!isset($rReturn[$rUUID])) {
								$rReturn[$rUUID] = array();
							}
							$rReturn[$rUUID][] = $rRow;
						} else {
							$rReturn[] = $rRow;
						}
					}
				}
			}
		}

		if ($rCountOnly) {
			$rReturn[1] = count(array_unique($rUniqueUsers));
		}
		return $rReturn;
	}

	/**
	 * Get a single connection by UUID.
	 *
	 * @param string $rUUID Connection UUID (Redis key).
	 * @return array|null Deserialized connection data, or null if not found.
	 */
	public static function getConnection(string $rUUID): ?array {
		$rRedis = \XcVm\Infrastructure\Redis\RedisManager::instance();
		if (!$rRedis) {
			return null;
		}
		$raw = $rRedis->get($rUUID);
		return ($raw !== false) ? igbinary_unserialize($raw) : null;
	}

	/**
	 * Create a new connection in Redis.
	 *
	 * Atomically (MULTI/EXEC) adds UUID to all sorted sets:
	 * LINE#, LINE_ALL#, STREAM#, SERVER#, SERVER_LINES#, PROXY#,
	 * CONNECTIONS, LIVE, and stores igbinary-serialized data.
	 *
	 * @param array $rData Connection data (uuid, identity, stream_id, server_id, etc.).
	 * @return array|false MULTI/EXEC result.
	 */
	public static function createConnection(array $rData) {
		$rRedis = \XcVm\Infrastructure\Redis\RedisManager::instance();
		if (!$rRedis) {
			return false;
		}
		$rMulti = $rRedis->multi();
		$rMulti->zAdd('LINE#' . $rData['identity'], $rData['date_start'], $rData['uuid']);
		$rMulti->zAdd('LINE_ALL#' . $rData['identity'], $rData['date_start'], $rData['uuid']);
		$rMulti->zAdd('STREAM#' . $rData['stream_id'], $rData['date_start'], $rData['uuid']);
		$rMulti->zAdd('SERVER#' . $rData['server_id'], $rData['date_start'], $rData['uuid']);
		if ($rData['user_id']) {
			$rMulti->zAdd('SERVER_LINES#' . $rData['server_id'], $rData['user_id'], $rData['uuid']);
		}
		if ($rData['proxy_id']) {
			$rMulti->zAdd('PROXY#' . $rData['proxy_id'], $rData['date_start'], $rData['uuid']);
		}
		$rMulti->zAdd('CONNECTIONS', $rData['date_start'], $rData['uuid']);
		$rMulti->zAdd('LIVE', $rData['date_start'], $rData['uuid']);
		$rMulti->set($rData['uuid'], igbinary_serialize($rData));
		return $rMulti->exec();
	}

	/**
	 * Create a live connection record for the current request, transparently
	 * targeting Redis or the `lines_live` table depending on redis_handler. The
	 * HLS and TS delivery arms share this; they differ only in the container and
	 * the pid recorded.
	 *
	 * @param array    $rSettings  Settings (reads redis_handler).
	 * @param array    $rCtx       Request-scoped fields: is_hmac, identifier,
	 *                             user_id, stream_id, server_id, proxy_id,
	 *                             user_agent, user_ip, date_start,
	 *                             geoip_country_code, isp, external_device,
	 *                             on_demand, uuid, time_offset.
	 * @param string   $rContainer Container: `hls` or the TS extension.
	 * @param int|null $rPid       Owning pid (NULL for HLS).
	 * @return mixed Truthy on success (Redis MULTI result or DB write result).
	 */
	public static function createLive(array $rSettings, array $rCtx, string $rContainer, $rPid) {
		$rConn = array(
			"stream_id" => $rCtx["stream_id"],
			"server_id" => $rCtx["server_id"],
			"proxy_id" => $rCtx["proxy_id"],
			"user_agent" => $rCtx["user_agent"],
			"user_ip" => $rCtx["user_ip"],
			"container" => $rContainer,
			"pid" => $rPid,
			"date_start" => $rCtx["date_start"],
			"geoip_country_code" => $rCtx["geoip_country_code"],
			"isp" => $rCtx["isp"],
			"external_device" => $rCtx["external_device"],
			"hls_end" => 0,
			"hls_last_read" => time() - $rCtx["time_offset"],
			"on_demand" => $rCtx["on_demand"],
			"uuid" => $rCtx["uuid"],
		);

		if (is_null($rCtx["is_hmac"])) {
			$rConn["user_id"] = $rCtx["user_id"];
			$rConn["identity"] = $rCtx["user_id"];
		} else {
			$rConn["hmac_id"] = $rCtx["is_hmac"];
			$rConn["hmac_identifier"] = $rCtx["identifier"];
			$rConn["identity"] = $rCtx["is_hmac"] . "_" . $rCtx["identifier"];
		}

		if ($rSettings["redis_handler"]) {
			return self::createConnection($rConn);
		}

		$db = self::db();

		if (is_null($rCtx["is_hmac"])) {
			return $db->query('INSERT INTO `lines_live` (`user_id`,`stream_id`,`server_id`,`proxy_id`,`user_agent`,`user_ip`,`container`,`pid`,`uuid`,`date_start`,`geoip_country_code`,`isp`,`external_device`,`hls_last_read`) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)', $rConn["user_id"], $rConn["stream_id"], $rConn["server_id"], $rConn["proxy_id"], $rConn["user_agent"], $rConn["user_ip"], $rConn["container"], $rConn["pid"], $rConn["uuid"], $rConn["date_start"], $rConn["geoip_country_code"], $rConn["isp"], $rConn["external_device"], $rConn["hls_last_read"]);
		}

		return $db->query('INSERT INTO `lines_live` (`hmac_id`,`hmac_identifier`,`stream_id`,`server_id`,`proxy_id`,`user_agent`,`user_ip`,`container`,`pid`,`uuid`,`date_start`,`geoip_country_code`,`isp`,`external_device`,`hls_last_read`) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)', $rConn["hmac_id"], $rConn["hmac_identifier"], $rConn["stream_id"], $rConn["server_id"], $rConn["proxy_id"], $rConn["user_agent"], $rConn["user_ip"], $rConn["container"], $rConn["pid"], $rConn["uuid"], $rConn["date_start"], $rConn["geoip_country_code"], $rConn["isp"], $rConn["external_device"], $rConn["hls_last_read"]);
	}

	/**
	 * Find the current live connection for this request (Redis or lines_live).
	 * The HLS and TS arms differ in the columns they need and their filters,
	 * expressed here as explicit flags rather than hidden branches. The dynamic
	 * pieces are built from those booleans only — never from request input.
	 *
	 * @param array  $rSettings      Settings (reads redis_handler).
	 * @param array  $rCtx           Request-scoped fields (uuid, is_hmac,
	 *                               identifier, user_id, server_id, stream_id,
	 *                               adaptive).
	 * @param string $rContainer     Container: `hls` or the TS extension.
	 * @param bool   $rWithPid       Also select the `pid` column (TS).
	 * @param bool   $rOpenOnly      Restrict to open rows (`hls_end = 0`) (HLS).
	 * @param bool   $rAllowAdaptive Honour an adaptive token (HLS only).
	 * @return array|null The connection row, or null when none is open.
	 */
	public static function lookupLive(array $rSettings, array $rCtx, string $rContainer, bool $rWithPid, bool $rOpenOnly, bool $rAllowAdaptive): ?array {
		if ($rSettings["redis_handler"]) {
			return self::getConnection($rCtx["uuid"]);
		}

		$db = self::db();
		$rCols = $rWithPid ? "`activity_id`, `pid`, `user_ip`" : "`activity_id`, `user_ip`";
		$rOpen = $rOpenOnly ? " AND `hls_end` = 0" : "";

		if ($rAllowAdaptive && !empty($rCtx["adaptive"])) {
			$db->query("SELECT $rCols FROM `lines_live` WHERE `uuid` = ? AND `user_id` = ? AND `container` = ?" . $rOpen, $rCtx["uuid"], $rCtx["user_id"], $rContainer);
		} elseif (is_null($rCtx["is_hmac"])) {
			$db->query("SELECT $rCols FROM `lines_live` WHERE `uuid` = ? AND `user_id` = ? AND `server_id` = ? AND `container` = ? AND `stream_id` = ?" . $rOpen, $rCtx["uuid"], $rCtx["user_id"], $rCtx["server_id"], $rContainer, $rCtx["stream_id"]);
		} else {
			$db->query("SELECT $rCols FROM `lines_live` WHERE `uuid` = ? AND `hmac_id` = ? AND `hmac_identifier` = ? AND `server_id` = ? AND `container` = ? AND `stream_id` = ?" . $rOpen, $rCtx["uuid"], $rCtx["is_hmac"], $rCtx["identifier"], $rCtx["server_id"], $rContainer, $rCtx["stream_id"]);
		}

		return $db->num_rows() > 0 ? $db->get_row() : null;
	}

	/**
	 * Refresh an existing live connection (Redis or lines_live), applying the
	 * given column changes and re-opening the row (`hls_end = 0`). On the Redis
	 * path $rConnection is updated in place with the stored record.
	 *
	 * @param array $rSettings  Settings (reads redis_handler).
	 * @param array $rConnection Connection row (needs activity_id for the DB path).
	 * @param array $rChanges    Column => value pairs to write (code-controlled keys).
	 * @return bool True on a successful write.
	 */
	public static function updateLive(array $rSettings, array &$rConnection, array $rChanges): bool {
		if ($rSettings["redis_handler"]) {
			$rUpdated = self::updateConnection($rConnection, $rChanges, "open");
			if ($rUpdated) {
				$rConnection = $rUpdated;
				return true;
			}
			return false;
		}

		$db = self::db();
		$rSet = array();
		$rParams = array();
		foreach ($rChanges as $rColumn => $rValue) {
			$rSet[] = "`" . $rColumn . "` = ?";
			$rParams[] = $rValue;
		}
		$rParams[] = $rConnection["activity_id"];

		return (bool) $db->query('UPDATE `lines_live` SET ' . implode(", ", $rSet) . ", `hls_end` = 0 WHERE `activity_id` = ?", ...$rParams);
	}

	/**
	 * Get connections for a specific user/line.
	 *
	 * @param int  $rUserID User ID.
	 * @param bool $rActive If true — only active (LINE#), otherwise all (LINE_ALL#).
	 * @param bool $rKeys   Unused (overwritten internally).
	 * @return array UUID keys or deserialized connection data.
	 */
	public static function getLineConnections(int $rUserID, bool $rActive = false, bool $rKeys = false): array {
		$rRedis = \XcVm\Infrastructure\Redis\RedisManager::instance();
		if (!$rRedis) {
			return [];
		}
		// zRangeByScore returns false on a failed connection — degrade to empty.
		$rKeys = $rRedis->zRangeByScore((($rActive ? 'LINE#' : 'LINE_ALL#')) . $rUserID, '-inf', '+inf');
		if (is_array($rKeys) && count($rKeys) > 0) {
			return $rKeys;
		}
		return array();
	}

	/**
	 * Get all ended (ENDED) connections.
	 *
	 * Reads members from ENDED set and deserializes data via mGet.
	 *
	 * @return array Array of deserialized ended connection data.
	 */
	public static function getEnded(): array {
		$rRedis = \XcVm\Infrastructure\Redis\RedisManager::instance();
		if (!$rRedis) {
			return [];
		}
		// sMembers/mGet return false on a failed connection — degrade to empty.
		$rKeys = $rRedis->sMembers('ENDED');
		if (!is_array($rKeys) || 0 >= count($rKeys)) {
			return array();
		}
		$rData = $rRedis->mGet($rKeys);
		if (!is_array($rData)) {
			return array();
		}
		return array_map(static function ($rItem) {
			return is_string($rItem) ? igbinary_unserialize($rItem) : false;
		}, $rData);
	}

	/**
	 * Get proxy servers attached to the specified server.
	 *
	 * @param int  $rServerID Parent server ID.
	 * @param bool $rOnline   If true — only online proxies.
	 * @return array<int, array> Map of proxyID => server data.
	 */
	public static function getProxies(int $rServerID, bool $rOnline = true): array {
		global $rServers;
		$rReturn = array();
		foreach ($rServers as $rProxyID => $rServerInfo) {
			if ($rServerInfo['server_type'] == 1 && in_array($rServerID, $rServerInfo['parent_id']) && ($rServerInfo['server_online'] || !$rOnline)) {
				$rReturn[$rProxyID] = $rServerInfo;
			}
		}
		return $rReturn;
	}

	/**
	 * Close an active connection.
	 *
	 * Performs the full close cycle: kills the process (RTMP drop client,
	 * posix_kill, or Redis signal), removes from Redis sorted sets,
	 * cleans tmp files, and writes to the activity log.
	 *
	 * @param array|string $rActivityInfo Connection data or UUID/activity_id.
	 * @param bool         $rRemove       Remove connection from Redis/MySQL.
	 * @param bool         $rEnd          Mark HLS connection as ended.
	 * @return bool True on successful close, false otherwise.
	 */
	public static function closeConnection($rActivityInfo, bool $rRemove = true, bool $rEnd = true): bool {
		if (!empty($rActivityInfo)) {
			global $rSettings, $rServers;
			$db = self::db();
			if (!$rSettings['redis_handler'] || is_object(\XcVm\Infrastructure\Redis\RedisManager::instance())) {
			} else {
				\XcVm\Infrastructure\Redis\RedisManager::ensureConnected();
			}
			$rRedisObj = \XcVm\Infrastructure\Redis\RedisManager::instance();
			if (!$rRedisObj && $rSettings['redis_handler']) {
				return false;
			}
			if (is_array($rActivityInfo)) {
			} else {
				if (!$rSettings['redis_handler']) {
					if (strlen(strval($rActivityInfo)) == 32) {
						$db->query('SELECT * FROM `lines_live` WHERE `uuid` = ?', $rActivityInfo);
					} else {
						$db->query('SELECT * FROM `lines_live` WHERE `activity_id` = ?', $rActivityInfo);
					}
					$rActivityInfo = $db->get_row();
				} else {
					$raw = $rRedisObj->get($rActivityInfo);
					$rActivityInfo = ($raw !== false) ? igbinary_unserialize($raw) : null;
				}
			}
			if (is_array($rActivityInfo)) {
				if (($rActivityInfo['container'] ?? '') == 'rtmp') {
					if ($rActivityInfo['server_id'] == SERVER_ID) {
						shell_exec('wget --timeout=2 -O /dev/null -o /dev/null "' . $rServers[SERVER_ID]['rtmp_mport_url'] . 'control/drop/client?clientid=' . intval($rActivityInfo['pid']) . '" >/dev/null 2>/dev/null &');
					} else {
						if ($rSettings['redis_handler']) {
							self::redisSignal($rActivityInfo['pid'], $rActivityInfo['server_id'], 1);
						} else {
							$db->query('INSERT INTO `signals` (`pid`,`server_id`,`rtmp`,`time`) VALUES(?,?,?,UNIX_TIMESTAMP())', $rActivityInfo['pid'], $rActivityInfo['server_id'], 1);
						}
					}
				} else {
					if (($rActivityInfo['container'] ?? '') == 'hls') {
						if (!(!$rRemove && $rEnd && $rActivityInfo['hls_end'] == 0)) {
						} else {
							if ($rSettings['redis_handler']) {
								self::updateConnection($rActivityInfo, array(), 'close');
							} else {
								$db->query('UPDATE `lines_live` SET `hls_end` = 1 WHERE `activity_id` = ?', $rActivityInfo['activity_id']);
							}
							@unlink(CONS_TMP_PATH . $rActivityInfo['stream_id'] . '/' . $rActivityInfo['uuid']);
						}
					} else {
						if ($rActivityInfo['server_id'] == SERVER_ID) {
							if (!($rActivityInfo['pid'] != getmypid() && is_numeric($rActivityInfo['pid']) && 0 < $rActivityInfo['pid'])) {
							} else {
								posix_kill(intval($rActivityInfo['pid']), 9);
							}
						} else {
							if ($rSettings['redis_handler']) {
								self::redisSignal($rActivityInfo['pid'], $rActivityInfo['server_id'], 0);
							} else {
								$db->query('INSERT INTO `signals` (`pid`,`server_id`,`time`) VALUES(?,?,UNIX_TIMESTAMP())', $rActivityInfo['pid'], $rActivityInfo['server_id']);
							}
						}
					}
				}
				if ($rActivityInfo['server_id'] == SERVER_ID) {
					@unlink(CONS_TMP_PATH . $rActivityInfo['uuid']);
				}
				if ($rRemove) {
					if ($rActivityInfo['server_id'] == SERVER_ID) {
						@unlink(CONS_TMP_PATH . $rActivityInfo['stream_id'] . '/' . $rActivityInfo['uuid']);
					}
					if ($rSettings['redis_handler']) {
						$rRedis = $rRedisObj->multi();
						$rRedis->zRem('LINE#' . $rActivityInfo['identity'], $rActivityInfo['uuid']);
						$rRedis->zRem('LINE_ALL#' . $rActivityInfo['identity'], $rActivityInfo['uuid']);
						$rRedis->zRem('STREAM#' . $rActivityInfo['stream_id'], $rActivityInfo['uuid']);
						$rRedis->zRem('SERVER#' . $rActivityInfo['server_id'], $rActivityInfo['uuid']);
						if ($rActivityInfo['user_id']) {
							$rRedis->zRem('SERVER_LINES#' . $rActivityInfo['server_id'], $rActivityInfo['uuid']);
						}
						if ($rActivityInfo['proxy_id']) {
							$rRedis->zRem('PROXY#' . $rActivityInfo['proxy_id'], $rActivityInfo['uuid']);
						}
						$rRedis->del($rActivityInfo['uuid']);
						$rRedis->zRem('CONNECTIONS', $rActivityInfo['uuid']);
						$rRedis->zRem('LIVE', $rActivityInfo['uuid']);
						$rRedis->sRem('ENDED', $rActivityInfo['uuid']);
						$rRedis->exec();
					} else {
						$db->query('DELETE FROM `lines_live` WHERE `activity_id` = ?', $rActivityInfo['activity_id']);
					}
				}
				self::writeOfflineActivity($rSettings, $rActivityInfo['server_id'] ?? 0, intval($rActivityInfo['proxy_id'] ?? 0), $rActivityInfo['user_id'] ?? 0, $rActivityInfo['stream_id'] ?? 0, $rActivityInfo['date_start'] ?? 0, $rActivityInfo['user_agent'] ?? '', $rActivityInfo['user_ip'] ?? '', $rActivityInfo['container'] ?? '', $rActivityInfo['geoip_country_code'] ?? '', strval($rActivityInfo['isp'] ?? ''), $rActivityInfo['external_device'] ?? '', $rActivityInfo['divergence'] ?? 0, $rActivityInfo['hmac_id'] ?? null, $rActivityInfo['hmac_identifier'] ?? '');
				return true;
			}
			return false;
		}
		return false;
	}

	/**
	 * Write closed connection data to the activity log file.
	 *
	 * Log is written as base64(json) per line to LOGS_TMP_PATH/activity.
	 * Only writes if save_closed_connection setting is enabled.
	 *
	 * @param array       $rSettings       Global settings.
	 * @param int         $rServerID       Server ID.
	 * @param int         $rProxyID        Proxy ID (0 if no proxy).
	 * @param int         $rUserID         User ID.
	 * @param int         $rStreamID       Stream ID.
	 * @param int         $rStart          Connection start Unix timestamp.
	 * @param string      $rUserAgent      Client User-Agent.
	 * @param string      $rIP             Client IP address.
	 * @param string      $rExtension      Container type (rtmp, hls, etc.).
	 * @param string      $rGeoIP          GeoIP country code.
	 * @param string      $rISP            ISP name.
	 * @param string      $rExternalDevice External device identifier.
	 * @param int         $rDivergence     Divergence value.
	 * @param int|null    $rIsHMAC         HMAC ID.
	 * @param string      $rIdentifier     HMAC identifier.
	 * @return void
	 */
	public static function writeOfflineActivity(array $rSettings, int $rServerID, int $rProxyID, int $rUserID, int $rStreamID, int $rStart, string $rUserAgent, string $rIP, string $rExtension, string $rGeoIP, string $rISP, string $rExternalDevice = '', int $rDivergence = 0, ?int $rIsHMAC = null, string $rIdentifier = ''): void {
		if ($rSettings['save_closed_connection'] != 0) {
			if ($rServerID && $rUserID && $rStreamID) {
				$rActivityInfo = array('user_id' => intval($rUserID), 'stream_id' => intval($rStreamID), 'server_id' => intval($rServerID), 'proxy_id' => intval($rProxyID), 'date_start' => intval($rStart), 'user_agent' => $rUserAgent, 'user_ip' => htmlentities($rIP), 'date_end' => time(), 'container' => $rExtension, 'geoip_country_code' => $rGeoIP, 'isp' => $rISP, 'external_device' => htmlentities($rExternalDevice), 'divergence' => intval($rDivergence), 'hmac_id' => $rIsHMAC, 'hmac_identifier' => $rIdentifier);
				file_put_contents(LOGS_TMP_PATH . 'activity', base64_encode(json_encode($rActivityInfo)) . "\n", FILE_APPEND | LOCK_EX);
			}
		} else {
			return;
		}
	}

	/**
	 * Count active (live) connections on a server or proxy.
	 *
	 * In Redis mode counts via getRedisConnections.
	 * In MySQL mode performs COUNT(*) on lines_live.
	 *
	 * @param int  $rServerID Server or proxy ID.
	 * @param bool $rProxy    If true — count for proxy.
	 * @return int Number of active connections.
	 */
	public static function getLiveConnections(int $rServerID, bool $rProxy = false): int {
		$db = self::db();

		if (SettingsManager::getAll()['redis_handler']) {
			$rCount = 0;

			if ($rProxy) {
				$rParentIDs = ServerRepository::getAll()[$rServerID]['parent_id'];

				foreach ($rParentIDs as $rParentID) {
					foreach (self::getRedisConnections(null, $rParentID, null, true, false, false) as $rConnection) {
						if ($rConnection['proxy_id'] != $rServerID) {
						} else {
							$rCount++;
						}
					}
				}
			} else {
				list($rCount) = self::getRedisConnections(null, $rServerID, null, true, true, false);
			}

			return $rCount;
		} else {
			if ($rProxy) {
				$db->query('SELECT COUNT(*) AS `count` FROM `lines_live` WHERE `proxy_id` = ? AND `hls_end` = 0;', $rServerID);
			} else {
				$db->query('SELECT COUNT(*) AS `count` FROM `lines_live` WHERE `server_id` = ? AND `hls_end` = 0;', $rServerID);
			}

			return $db->get_row()['count'];
		}
	}
}
