agrobot_base/AgroBase/OperationControl/Models/TcpVideoReceiver.cs

1080 lines
37 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
namespace OperationControl.Models
{
/// <summary>
/// Receptor TCP de vídeo orientado a operação de campo.
///
/// Protocolo preservado e compatível com CameraTcpStreamer do rover:
/// 4 bytes uint32 big-endian com o tamanho do JPEG
/// N bytes contendo o JPEG
///
/// Princípios desta implementação:
/// 1. A thread de rede apenas recebe e enquadra os JPEGs.
/// 2. Existe no máximo um JPEG pendente para decode (latest-only).
/// 3. O decode ocorre fora da thread de rede e fora da UI.
/// 4. Existe no máximo um Bitmap pendente para a UI.
/// 5. A UI nunca recebe uma fila ilimitada de BeginInvoke.
/// 6. Uma conexão nova substitui imediatamente uma conexão antiga/zumbi.
/// 7. Tamanhos inválidos são rejeitados antes de qualquer alocação grande.
/// 8. Parada, reinício e reconexão são idempotentes e thread-safe.
/// </summary>
public sealed class TcpVideoReceiver : IDisposable
{
private const int HeaderSize = 4;
private const int MinJpegBytes = 32;
private const int DefaultMaxFrameBytes = 4 * 1024 * 1024;
private const int DefaultReceiveTimeoutMs = 7000;
private const int DefaultReceiveBufferBytes = 256 * 1024;
private readonly int _port;
private readonly System.Windows.Controls.Image _imageControl;
private readonly Action<string> _mostrarLog;
private readonly int _maxFrameBytes;
private readonly int _receiveTimeoutMs;
public readonly string imageName;
private readonly object _lifecycleLock = new object();
private readonly object _clientLock = new object();
private readonly object _metricsLock = new object();
private readonly object _logLock = new object();
private readonly AutoResetEvent _frameReady = new AutoResetEvent(false);
private volatile bool _rodando;
private volatile bool _disposed;
private int _runGeneration;
private int _sessionSequence;
private int _activeSessionId;
private Thread _acceptThread;
private Thread _decodeThread;
private Thread _clientThread;
private TcpListener _listener;
private TcpClient _activeClient;
// Slot latest-only entre rede e decode.
private FramePacket _pendingFrame;
// Slot latest-only entre decode e UI.
private UiFrame _pendingUiFrame;
private int _uiDispatchScheduled;
// Métricas atômicas.
private long _connectionsAccepted;
private long _connectionsReplaced;
private long _disconnects;
private long _framesReceived;
private long _framesDecoded;
private long _framesDisplayed;
private long _framesDroppedPending;
private long _framesDroppedUi;
private long _framesInvalidSize;
private long _framesInvalidJpeg;
private long _framesDecodeErrors;
private long _receiveErrors;
private long _bytesReceived;
// Métricas compostas protegidas por lock.
private bool _clientConnected;
private string _remoteEndpoint;
private string _lastError;
private DateTime? _connectedSinceUtc;
private DateTime? _lastFrameReceivedUtc;
private DateTime? _lastFrameDisplayedUtc;
private double _receiveKbpsEma;
private double _receiveFpsEma;
private double _decodeMsEma;
private long _lastFrameReceivedTicks;
private readonly Dictionary<string, long> _lastLogTicks = new Dictionary<string, long>();
private readonly TimeSpan _logThrottle = TimeSpan.FromSeconds(5);
/// <summary>
/// Mantém compatibilidade com as chamadas existentes de dois argumentos.
/// Os parâmetros adicionais são opcionais.
/// </summary>
public TcpVideoReceiver(
int port,
System.Windows.Controls.Image imageControl,
Action<string> mostrarLog = null,
int maxFrameBytes = DefaultMaxFrameBytes,
int receiveTimeoutMs = DefaultReceiveTimeoutMs)
{
if (port <= 0 || port > 65535)
throw new ArgumentOutOfRangeException(nameof(port));
if (imageControl == null)
throw new ArgumentNullException(nameof(imageControl));
if (maxFrameBytes < 64 * 1024)
throw new ArgumentOutOfRangeException(nameof(maxFrameBytes));
if (receiveTimeoutMs < 2000)
throw new ArgumentOutOfRangeException(nameof(receiveTimeoutMs));
_port = port;
_imageControl = imageControl;
_mostrarLog = mostrarLog ?? (msg => Console.WriteLine(msg));
_maxFrameBytes = maxFrameBytes;
_receiveTimeoutMs = receiveTimeoutMs;
imageName = imageControl.Name;
}
public bool Rodando => _rodando;
public bool ClienteConectado
{
get
{
lock (_metricsLock)
return _clientConnected;
}
}
/// <summary>
/// Inicia listener, decoder e ciclo de recepção.
/// Pode ser chamado novamente após Parar().
/// </summary>
public void Iniciar()
{
ThrowIfDisposed();
lock (_lifecycleLock)
{
if (_rodando)
return;
TcpListener listener = null;
try
{
listener = new TcpListener(IPAddress.Any, _port);
// Impede duas instâncias locais de ocuparem silenciosamente
// a mesma porta no Windows.
try
{
listener.Server.ExclusiveAddressUse = true;
}
catch
{
// Best effort.
}
listener.Start(4);
}
catch (Exception ex)
{
try { listener?.Stop(); } catch { }
SetLastError("Falha ao iniciar listener: " + ex.Message);
Log($"[VideoReceiver:{imageName}] Falha ao ouvir porta {_port}: {ex.Message}");
return;
}
ResetPendingSlots();
ResetSessionMetrics();
int generation = Interlocked.Increment(ref _runGeneration);
_listener = listener;
_rodando = true;
_decodeThread = new Thread(() => DecodeLoop(generation))
{
IsBackground = true,
Name = $"video-decode-{imageName}-{_port}"
};
_acceptThread = new Thread(() => AcceptLoop(generation))
{
IsBackground = true,
Name = $"video-accept-{imageName}-{_port}"
};
_decodeThread.Start();
_acceptThread.Start();
Log($"[VideoReceiver:{imageName}] Ouvindo na porta {_port}.");
}
}
/// <summary>
/// Para listener, conexão e workers. É seguro chamar mais de uma vez.
/// </summary>
public void Parar()
{
Thread acceptThread;
Thread decodeThread;
Thread clientThread;
TcpListener listener;
TcpClient client;
int stoppedGeneration;
lock (_lifecycleLock)
{
if (!_rodando)
{
ScheduleImageClear(Volatile.Read(ref _runGeneration));
return;
}
_rodando = false;
stoppedGeneration = Interlocked.Increment(ref _runGeneration);
listener = _listener;
_listener = null;
acceptThread = _acceptThread;
decodeThread = _decodeThread;
_acceptThread = null;
_decodeThread = null;
lock (_clientLock)
{
client = _activeClient;
_activeClient = null;
_activeSessionId = 0;
clientThread = _clientThread;
_clientThread = null;
}
}
try { listener?.Stop(); } catch { }
CloseClient(client);
// Libera imediatamente o decoder caso esteja aguardando.
_frameReady.Set();
JoinThread(acceptThread, 1500);
JoinThread(clientThread, 1500);
JoinThread(decodeThread, 1500);
ResetPendingSlots();
lock (_metricsLock)
{
_clientConnected = false;
_remoteEndpoint = null;
_connectedSinceUtc = null;
}
ScheduleImageClear(stoppedGeneration);
Log($"[VideoReceiver:{imageName}] Receptor parado.");
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
Parar();
// O AutoResetEvent é intencionalmente mantido até a coleta da
// instância. Isso evita uma corrida rara com uma thread encerrando
// após o timeout defensivo de Join.
}
/// <summary>
/// Snapshot thread-safe para logs, UI ou telemetria da base.
/// </summary>
public TcpVideoReceiverMetrics GetMetrics()
{
bool connected;
string remote;
string lastError;
DateTime? connectedSince;
DateTime? lastReceived;
DateTime? lastDisplayed;
double kbps;
double fps;
double decodeMs;
lock (_metricsLock)
{
connected = _clientConnected;
remote = _remoteEndpoint;
lastError = _lastError;
connectedSince = _connectedSinceUtc;
lastReceived = _lastFrameReceivedUtc;
lastDisplayed = _lastFrameDisplayedUtc;
kbps = _receiveKbpsEma;
fps = _receiveFpsEma;
decodeMs = _decodeMsEma;
}
double lastFrameAgeMs = -1;
if (lastReceived.HasValue)
lastFrameAgeMs = Math.Max(0, (DateTime.UtcNow - lastReceived.Value).TotalMilliseconds);
return new TcpVideoReceiverMetrics
{
ImageName = imageName,
Port = _port,
Running = _rodando,
ClientConnected = connected,
RemoteEndpoint = remote,
ConnectedSinceUtc = connectedSince,
LastFrameReceivedUtc = lastReceived,
LastFrameDisplayedUtc = lastDisplayed,
LastFrameAgeMs = lastFrameAgeMs,
ReceiveKbps = kbps,
ReceiveFps = fps,
DecodeMs = decodeMs,
QueueDepth = Volatile.Read(ref _pendingFrame) == null ? 0 : 1,
UiQueueDepth = Volatile.Read(ref _pendingUiFrame) == null ? 0 : 1,
ConnectionsAccepted = Interlocked.Read(ref _connectionsAccepted),
ConnectionsReplaced = Interlocked.Read(ref _connectionsReplaced),
Disconnects = Interlocked.Read(ref _disconnects),
FramesReceived = Interlocked.Read(ref _framesReceived),
FramesDecoded = Interlocked.Read(ref _framesDecoded),
FramesDisplayed = Interlocked.Read(ref _framesDisplayed),
FramesDroppedPending = Interlocked.Read(ref _framesDroppedPending),
FramesDroppedUi = Interlocked.Read(ref _framesDroppedUi),
FramesInvalidSize = Interlocked.Read(ref _framesInvalidSize),
FramesInvalidJpeg = Interlocked.Read(ref _framesInvalidJpeg),
FramesDecodeErrors = Interlocked.Read(ref _framesDecodeErrors),
ReceiveErrors = Interlocked.Read(ref _receiveErrors),
BytesReceived = Interlocked.Read(ref _bytesReceived),
LastError = lastError
};
}
// =================================================================
// ACCEPT / CLIENT SESSION
// =================================================================
private void AcceptLoop(int generation)
{
while (IsRunActive(generation))
{
TcpClient client = null;
try
{
TcpListener listener = _listener;
if (listener == null)
break;
// Stop() em Parar() libera este bloqueio imediatamente.
client = listener.AcceptTcpClient();
if (!IsRunActive(generation))
{
CloseClient(client);
break;
}
ConfigureClient(client);
StartClientSession(client, generation);
client = null; // a sessão assumiu a propriedade
}
catch (SocketException ex)
{
CloseClient(client);
if (!IsRunActive(generation))
break;
Interlocked.Increment(ref _receiveErrors);
SetLastError("Erro no Accept: " + ex.Message);
LogThrottled("accept_socket", $"[VideoReceiver:{imageName}] Erro ao aceitar conexão: {ex.Message}");
Thread.Sleep(250);
}
catch (ObjectDisposedException)
{
CloseClient(client);
break;
}
catch (Exception ex)
{
CloseClient(client);
if (!IsRunActive(generation))
break;
Interlocked.Increment(ref _receiveErrors);
SetLastError("Erro geral no Accept: " + ex.Message);
LogThrottled("accept_general", $"[VideoReceiver:{imageName}] Erro no listener: {ex.Message}");
Thread.Sleep(500);
}
}
}
private void StartClientSession(TcpClient client, int generation)
{
int sessionId = Interlocked.Increment(ref _sessionSequence);
TcpClient oldClient;
var sessionThread = new Thread(() => ClientReadLoop(client, generation, sessionId))
{
IsBackground = true,
Name = $"video-client-{imageName}-{_port}-{sessionId}"
};
lock (_clientLock)
{
if (!IsRunActive(generation))
{
CloseClient(client);
return;
}
oldClient = _activeClient;
if (oldClient != null)
Interlocked.Increment(ref _connectionsReplaced);
_activeClient = client;
_activeSessionId = sessionId;
_clientThread = sessionThread;
}
// Uma conexão mais nova sempre vence. Isso elimina sessões zumbis
// que ainda pareciam conectadas para o sistema operacional.
CloseClient(oldClient);
string remote = SafeRemoteEndpoint(client);
lock (_metricsLock)
{
_clientConnected = true;
_remoteEndpoint = remote;
_connectedSinceUtc = DateTime.UtcNow;
_lastError = null;
}
Interlocked.Increment(ref _connectionsAccepted);
// Mesmo que Parar() aconteça neste pequeno intervalo, a thread
// inicia, detecta a geração inválida e encerra sem tocar na UI.
sessionThread.Start();
Log($"[VideoReceiver:{imageName}] Cliente conectado: {remote}.");
}
private void ClientReadLoop(TcpClient client, int generation, int sessionId)
{
string disconnectReason = null;
try
{
using (NetworkStream stream = client.GetStream())
{
byte[] header = new byte[HeaderSize];
while (IsSessionActive(client, generation, sessionId))
{
if (!ReadExactly(stream, header, 0, HeaderSize, client, generation, sessionId))
{
disconnectReason = "fim do stream";
break;
}
int size = ReadBigEndianInt32(header);
// Um tamanho inválido significa que não é seguro tentar
// continuar no mesmo fluxo: o enquadramento pode estar perdido.
if (size < MinJpegBytes || size > _maxFrameBytes)
{
Interlocked.Increment(ref _framesInvalidSize);
disconnectReason = $"tamanho de frame inválido: {size} bytes";
SetLastError(disconnectReason);
break;
}
byte[] jpeg = new byte[size];
if (!ReadExactly(stream, jpeg, 0, size, client, generation, sessionId))
{
disconnectReason = "frame incompleto";
break;
}
Interlocked.Add(ref _bytesReceived, size + HeaderSize);
if (!LooksLikeJpeg(jpeg))
{
// O tamanho foi consumido corretamente, portanto podemos
// descartar este frame e continuar no próximo cabeçalho.
Interlocked.Increment(ref _framesInvalidJpeg);
SetLastError("JPEG sem marcadores SOI/EOI válidos.");
continue;
}
RegisterReceivedFrame(size);
var packet = new FramePacket
{
Bytes = jpeg,
Generation = generation,
SessionId = sessionId,
ReceivedUtc = DateTime.UtcNow
};
FramePacket replaced = Interlocked.Exchange(ref _pendingFrame, packet);
if (replaced != null)
Interlocked.Increment(ref _framesDroppedPending);
_frameReady.Set();
}
}
}
catch (IOException ex)
{
if (IsSessionActive(client, generation, sessionId))
{
Interlocked.Increment(ref _receiveErrors);
disconnectReason = IsSocketTimeout(ex)
? $"sem frames por {_receiveTimeoutMs} ms"
: ex.Message;
SetLastError(disconnectReason);
LogThrottled("client_io", $"[VideoReceiver:{imageName}] Sessão interrompida: {disconnectReason}");
}
}
catch (SocketException ex)
{
if (IsSessionActive(client, generation, sessionId))
{
Interlocked.Increment(ref _receiveErrors);
disconnectReason = ex.Message;
SetLastError(disconnectReason);
LogThrottled("client_socket", $"[VideoReceiver:{imageName}] Erro de socket: {ex.Message}");
}
}
catch (ObjectDisposedException)
{
// Normal quando uma conexão nova substitui a antiga ou ao parar.
}
catch (Exception ex)
{
if (IsSessionActive(client, generation, sessionId))
{
Interlocked.Increment(ref _receiveErrors);
disconnectReason = ex.Message;
SetLastError(disconnectReason);
LogThrottled("client_general", $"[VideoReceiver:{imageName}] Erro ao receber vídeo: {ex.Message}");
}
}
finally
{
bool wasActive = false;
lock (_clientLock)
{
if (_activeClient == client && _activeSessionId == sessionId)
{
_activeClient = null;
_activeSessionId = 0;
_clientThread = null;
wasActive = true;
}
}
CloseClient(client);
if (wasActive)
{
lock (_metricsLock)
{
_clientConnected = false;
_remoteEndpoint = null;
_connectedSinceUtc = null;
}
Interlocked.Increment(ref _disconnects);
// Fecha apenas o estado da sessão. O listener permanece pronto
// para o sender Python reconectar após seu backoff.
Log($"[VideoReceiver:{imageName}] Cliente desconectado{FormatReason(disconnectReason)}.");
}
}
}
// =================================================================
// DECODE / UI LATEST-ONLY
// =================================================================
private void DecodeLoop(int generation)
{
while (IsRunActive(generation))
{
_frameReady.WaitOne(500);
if (!IsRunActive(generation))
break;
FramePacket packet = Interlocked.Exchange(ref _pendingFrame, null);
if (packet == null)
continue;
// Descarta frames pertencentes a uma execução anterior.
if (packet.Generation != generation)
continue;
Stopwatch sw = Stopwatch.StartNew();
try
{
BitmapImage bitmap;
using (var ms = new MemoryStream(packet.Bytes, false))
{
bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.CreateOptions = BitmapCreateOptions.None;
bitmap.StreamSource = ms;
bitmap.EndInit();
bitmap.Freeze();
}
sw.Stop();
RegisterDecodedFrame(sw.Elapsed.TotalMilliseconds);
QueueBitmapForUi(bitmap, generation, packet.ReceivedUtc);
}
catch (Exception ex)
{
sw.Stop();
Interlocked.Increment(ref _framesDecodeErrors);
SetLastError("Falha ao decodificar JPEG: " + ex.Message);
LogThrottled("decode", $"[VideoReceiver:{imageName}] JPEG inválido ou erro de decode: {ex.Message}");
}
}
}
private void QueueBitmapForUi(BitmapSource bitmap, int generation, DateTime receivedUtc)
{
var uiFrame = new UiFrame
{
Bitmap = bitmap,
Generation = generation,
ReceivedUtc = receivedUtc
};
UiFrame replaced = Interlocked.Exchange(ref _pendingUiFrame, uiFrame);
if (replaced != null)
Interlocked.Increment(ref _framesDroppedUi);
TryScheduleUiDispatch();
}
private void TryScheduleUiDispatch()
{
if (Interlocked.CompareExchange(ref _uiDispatchScheduled, 1, 0) != 0)
return;
try
{
if (_imageControl.Dispatcher.HasShutdownStarted ||
_imageControl.Dispatcher.HasShutdownFinished)
{
Interlocked.Exchange(ref _uiDispatchScheduled, 0);
return;
}
_imageControl.Dispatcher.BeginInvoke(
DispatcherPriority.Render,
new Action(ApplyLatestBitmapOnUi));
}
catch (Exception ex)
{
Interlocked.Exchange(ref _uiDispatchScheduled, 0);
SetLastError("Falha ao agendar atualização da UI: " + ex.Message);
}
}
private void ApplyLatestBitmapOnUi()
{
try
{
UiFrame frame = Interlocked.Exchange(ref _pendingUiFrame, null);
int currentGeneration = Volatile.Read(ref _runGeneration);
if (frame != null &&
_rodando &&
frame.Generation == currentGeneration)
{
_imageControl.Source = frame.Bitmap;
Interlocked.Increment(ref _framesDisplayed);
lock (_metricsLock)
_lastFrameDisplayedUtc = DateTime.UtcNow;
}
}
catch (Exception ex)
{
SetLastError("Falha ao atualizar imagem na UI: " + ex.Message);
LogThrottled("ui", $"[VideoReceiver:{imageName}] Falha ao atualizar UI: {ex.Message}");
}
finally
{
Interlocked.Exchange(ref _uiDispatchScheduled, 0);
// Se outro bitmap chegou enquanto a UI trabalhava, agenda apenas
// mais uma atualização, sempre com o bitmap mais recente.
if (_rodando && Volatile.Read(ref _pendingUiFrame) != null)
TryScheduleUiDispatch();
}
}
private void ScheduleImageClear(int stoppedGeneration)
{
try
{
Action clear = () =>
{
// Não limpa uma imagem de uma execução nova iniciada antes
// de este callback antigo chegar ao Dispatcher.
if (!_rodando && Volatile.Read(ref _runGeneration) == stoppedGeneration)
_imageControl.Source = null;
};
if (_imageControl.Dispatcher.CheckAccess())
{
clear();
}
else if (!_imageControl.Dispatcher.HasShutdownStarted &&
!_imageControl.Dispatcher.HasShutdownFinished)
{
_imageControl.Dispatcher.BeginInvoke(DispatcherPriority.Normal, clear);
}
}
catch
{
// Encerramento da UI não deve derrubar o serviço.
}
}
// =================================================================
// NETWORK HELPERS
// =================================================================
private void ConfigureClient(TcpClient client)
{
client.NoDelay = true;
client.ReceiveTimeout = _receiveTimeoutMs;
client.ReceiveBufferSize = DefaultReceiveBufferBytes;
try
{
client.Client.SetSocketOption(
SocketOptionLevel.Socket,
SocketOptionName.KeepAlive,
true);
}
catch
{
// Best effort.
}
// Keepalive mais curto no Windows. Se a plataforma ou runtime não
// suportar IOControl, o keepalive padrão continua ativo.
try
{
byte[] keepAlive = new byte[12];
BitConverter.GetBytes((uint)1).CopyTo(keepAlive, 0);
BitConverter.GetBytes((uint)10000).CopyTo(keepAlive, 4);
BitConverter.GetBytes((uint)3000).CopyTo(keepAlive, 8);
client.Client.IOControl(
IOControlCode.KeepAliveValues,
keepAlive,
null);
}
catch
{
// Best effort.
}
}
private bool ReadExactly(
NetworkStream stream,
byte[] buffer,
int offset,
int count,
TcpClient client,
int generation,
int sessionId)
{
int total = 0;
while (total < count)
{
if (!IsSessionActive(client, generation, sessionId))
return false;
int read = stream.Read(buffer, offset + total, count - total);
if (read <= 0)
return false;
total += read;
}
return true;
}
private bool IsSessionActive(TcpClient client, int generation, int sessionId)
{
if (!IsRunActive(generation))
return false;
lock (_clientLock)
{
return _activeClient == client &&
_activeSessionId == sessionId;
}
}
private bool IsRunActive(int generation)
{
return _rodando && Volatile.Read(ref _runGeneration) == generation;
}
private static int ReadBigEndianInt32(byte[] header)
{
return (header[0] << 24) |
(header[1] << 16) |
(header[2] << 8) |
header[3];
}
private static bool LooksLikeJpeg(byte[] bytes)
{
if (bytes == null || bytes.Length < MinJpegBytes)
return false;
return bytes[0] == 0xFF &&
bytes[1] == 0xD8 &&
bytes[bytes.Length - 2] == 0xFF &&
bytes[bytes.Length - 1] == 0xD9;
}
private static bool IsSocketTimeout(IOException ex)
{
var socketEx = ex.InnerException as SocketException;
return socketEx != null && socketEx.SocketErrorCode == SocketError.TimedOut;
}
private static void CloseClient(TcpClient client)
{
if (client == null)
return;
try { client.Client?.Shutdown(SocketShutdown.Both); } catch { }
try { client.Close(); } catch { }
}
private static void JoinThread(Thread thread, int timeoutMs)
{
if (thread == null || thread == Thread.CurrentThread)
return;
try { thread.Join(timeoutMs); } catch { }
}
private static string SafeRemoteEndpoint(TcpClient client)
{
try
{
return client?.Client?.RemoteEndPoint?.ToString() ?? "desconhecido";
}
catch
{
return "desconhecido";
}
}
private static string FormatReason(string reason)
{
return string.IsNullOrWhiteSpace(reason) ? "" : $" ({reason})";
}
// =================================================================
// METRICS / LOG
// =================================================================
private void RegisterReceivedFrame(int jpegBytes)
{
long nowTicks = Stopwatch.GetTimestamp();
DateTime nowUtc = DateTime.UtcNow;
Interlocked.Increment(ref _framesReceived);
lock (_metricsLock)
{
if (_lastFrameReceivedTicks > 0)
{
double dt = (nowTicks - _lastFrameReceivedTicks) /
(double)Stopwatch.Frequency;
if (dt > 0.0001)
{
double fps = 1.0 / dt;
double kbps = (jpegBytes * 8.0) / dt / 1000.0;
_receiveFpsEma = Ema(_receiveFpsEma, fps, 0.20);
_receiveKbpsEma = Ema(_receiveKbpsEma, kbps, 0.20);
}
}
_lastFrameReceivedTicks = nowTicks;
_lastFrameReceivedUtc = nowUtc;
_lastError = null;
}
}
private void RegisterDecodedFrame(double decodeMs)
{
Interlocked.Increment(ref _framesDecoded);
lock (_metricsLock)
_decodeMsEma = Ema(_decodeMsEma, decodeMs, 0.20);
}
private static double Ema(double previous, double current, double alpha)
{
if (previous <= 0)
return current;
return previous * (1.0 - alpha) + current * alpha;
}
private void SetLastError(string error)
{
lock (_metricsLock)
_lastError = error;
}
private void ResetSessionMetrics()
{
Interlocked.Exchange(ref _connectionsAccepted, 0);
Interlocked.Exchange(ref _connectionsReplaced, 0);
Interlocked.Exchange(ref _disconnects, 0);
Interlocked.Exchange(ref _framesReceived, 0);
Interlocked.Exchange(ref _framesDecoded, 0);
Interlocked.Exchange(ref _framesDisplayed, 0);
Interlocked.Exchange(ref _framesDroppedPending, 0);
Interlocked.Exchange(ref _framesDroppedUi, 0);
Interlocked.Exchange(ref _framesInvalidSize, 0);
Interlocked.Exchange(ref _framesInvalidJpeg, 0);
Interlocked.Exchange(ref _framesDecodeErrors, 0);
Interlocked.Exchange(ref _receiveErrors, 0);
Interlocked.Exchange(ref _bytesReceived, 0);
lock (_metricsLock)
{
_clientConnected = false;
_remoteEndpoint = null;
_lastError = null;
_connectedSinceUtc = null;
_lastFrameReceivedUtc = null;
_lastFrameDisplayedUtc = null;
_receiveKbpsEma = 0;
_receiveFpsEma = 0;
_decodeMsEma = 0;
_lastFrameReceivedTicks = 0;
}
}
private void ResetPendingSlots()
{
Interlocked.Exchange(ref _pendingFrame, null);
Interlocked.Exchange(ref _pendingUiFrame, null);
Interlocked.Exchange(ref _uiDispatchScheduled, 0);
_frameReady.Reset();
}
private void Log(string message)
{
try { _mostrarLog(message); } catch { }
}
private void LogThrottled(string key, string message)
{
long now = Stopwatch.GetTimestamp();
bool shouldLog = false;
lock (_logLock)
{
long last;
if (!_lastLogTicks.TryGetValue(key, out last) ||
(now - last) / (double)Stopwatch.Frequency >= _logThrottle.TotalSeconds)
{
_lastLogTicks[key] = now;
shouldLog = true;
}
}
if (shouldLog)
Log(message);
}
private void ThrowIfDisposed()
{
if (_disposed)
throw new ObjectDisposedException(nameof(TcpVideoReceiver));
}
private sealed class FramePacket
{
public byte[] Bytes;
public int Generation;
public int SessionId;
public DateTime ReceivedUtc;
}
private sealed class UiFrame
{
public BitmapSource Bitmap;
public int Generation;
public DateTime ReceivedUtc;
}
}
public sealed class TcpVideoReceiverMetrics
{
public string ImageName { get; set; }
public int Port { get; set; }
public bool Running { get; set; }
public bool ClientConnected { get; set; }
public string RemoteEndpoint { get; set; }
public DateTime? ConnectedSinceUtc { get; set; }
public DateTime? LastFrameReceivedUtc { get; set; }
public DateTime? LastFrameDisplayedUtc { get; set; }
public double LastFrameAgeMs { get; set; }
public double ReceiveKbps { get; set; }
public double ReceiveFps { get; set; }
public double DecodeMs { get; set; }
public int QueueDepth { get; set; }
public int UiQueueDepth { get; set; }
public long ConnectionsAccepted { get; set; }
public long ConnectionsReplaced { get; set; }
public long Disconnects { get; set; }
public long FramesReceived { get; set; }
public long FramesDecoded { get; set; }
public long FramesDisplayed { get; set; }
public long FramesDroppedPending { get; set; }
public long FramesDroppedUi { get; set; }
public long FramesInvalidSize { get; set; }
public long FramesInvalidJpeg { get; set; }
public long FramesDecodeErrors { get; set; }
public long ReceiveErrors { get; set; }
public long BytesReceived { get; set; }
public string LastError { get; set; }
}
}