diff --git a/flake.nix b/flake.nix
index 16d7fcf..b6e952b 100644
--- a/flake.nix
+++ b/flake.nix
@@ -35,6 +35,7 @@
])
nuget
mono
+ vlc
# msbuild
omnisharp-roslyn
csharpier
diff --git a/server/server.csproj b/server/server.csproj
index df81424..776a67d 100644
--- a/server/server.csproj
+++ b/server/server.csproj
@@ -19,6 +19,7 @@
+
@@ -30,6 +31,7 @@
+
diff --git a/server/src/Controllers/VideoStreamController.cs b/server/src/Controllers/VideoStreamController.cs
index 22438bd..e25f4d4 100644
--- a/server/src/Controllers/VideoStreamController.cs
+++ b/server/src/Controllers/VideoStreamController.cs
@@ -146,7 +146,7 @@ public class VideoStreamController : ControllerBase
}
[HttpPost("SetVideoStreamEnable")]
- [ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
public async Task SetVideoStreamEnable(bool enable)
{
@@ -155,7 +155,7 @@ public class VideoStreamController : ControllerBase
var boardId = TryGetBoardId().OrThrow(() => new ArgumentException("Board ID is required"));
await _videoStreamService.SetVideoStreamEnableAsync(boardId.ToString(), enable);
- return Ok($"HDMI transmission for board {boardId} disabled.");
+ return Ok($"HDMI transmission for board {boardId} {enable.ToString()}.");
}
catch (Exception ex)
{
diff --git a/server/src/MsgBus.cs b/server/src/MsgBus.cs
index 18527ab..d2d12a0 100644
--- a/server/src/MsgBus.cs
+++ b/server/src/MsgBus.cs
@@ -6,6 +6,8 @@ public sealed class MsgBus
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
+ // private static RtspStreamService _rtspStreamService = new RtspStreamService(new UsbCameraCapture());
+
private static readonly UDPServer udpServer = new UDPServer(1234, 12);
///
/// 获取UDP服务器
@@ -49,7 +51,7 @@ public sealed class MsgBus
/// 通信总线初始化
///
/// 无
- public static void Init()
+ public static async void Init()
{
if (!ArpClient.IsAdministrator())
{
@@ -57,6 +59,10 @@ public sealed class MsgBus
// throw new Exception($"非管理员运行,ARP无法更新,请用管理员权限运行");
}
udpServer.Start();
+
+ // _rtspStreamService.ConfigureVideo(1920, 1080, 30);
+ // await _rtspStreamService.StartAsync();
+
isRunning = true;
}
diff --git a/server/src/Services/HttpVideoStreamService.cs b/server/src/Services/HttpVideoStreamService.cs
index 0a08552..d45e788 100644
--- a/server/src/Services/HttpVideoStreamService.cs
+++ b/server/src/Services/HttpVideoStreamService.cs
@@ -3,7 +3,6 @@ using System.Text;
using System.Collections.Concurrent;
using DotNext;
using DotNext.Threading;
-using FlashCap;
namespace server.Services;
@@ -108,7 +107,7 @@ public class HttpVideoStreamService : BackgroundService
var devices = camera.GetDevices();
for (int i = 0; i < devices.Count; i++)
logger.Info($"Device[{i}]: {devices[i].Name}");
- await camera.StartAsync(1, 3840, 2160, 30);
+ await camera.StartAsync(1, 2592, 1994, 30);
return camera;
}
catch (Exception ex)
@@ -120,14 +119,11 @@ public class HttpVideoStreamService : BackgroundService
private Optional TryGetClient(string boardId)
{
- if (_clientDict.TryGetValue(boardId, out var client))
- {
- return client;
- }
- return null;
+ return _clientDict.TryGetValue(boardId, out var client) ? client : null;
}
- private Optional GetOrCreateClient(string boardId, int initWidth, int initHeight)
+ private Optional GetOrCreateClient(
+ string boardId, int initWidth, int initHeight)
{
if (_clientDict.TryGetValue(boardId, out var client))
{
@@ -185,6 +181,8 @@ public class HttpVideoStreamService : BackgroundService
{
var client = _clientDict[clientKey];
client.CTS.Cancel();
+ if (!client.Camera.IsValueCreated) continue;
+
using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
{
var camera = await client.Camera.WithCancellation(cancellationToken);
@@ -251,26 +249,28 @@ public class HttpVideoStreamService : BackgroundService
}
var client = clientOpt.Value;
+ var token = CancellationTokenSource.CreateLinkedTokenSource(
+ client.CTS.Token, cancellationToken).Token;
- var clientToken = client.CTS.Token;
try
{
+ token.ThrowIfCancellationRequested();
logger.Info("新HTTP客户端连接: {RemoteEndPoint}", context.Request.RemoteEndPoint);
if (path == "/video")
{
// MJPEG 流请求(FPGA)
- await HandleMjpegStreamAsync(context.Response, client, cancellationToken);
+ await HandleMjpegStreamAsync(context.Response, client, token);
}
else if (path == "/usbCamera")
{
// USB Camera MJPEG流请求
- await HandleUsbCameraStreamAsync(context.Response, client, cancellationToken);
+ await HandleUsbCameraStreamAsync(context.Response, client, token);
}
else if (path == "/snapshot")
{
// 单帧图像请求
- await HandleSnapshotRequestAsync(context.Response, client, cancellationToken);
+ await HandleSnapshotRequestAsync(context.Response, client, token);
}
else if (path == "/html")
{
@@ -300,10 +300,26 @@ public class HttpVideoStreamService : BackgroundService
private async Task HandleUsbCameraStreamAsync(
HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken)
{
+ var camera = await _usbCamera.WithCancellation(cancellationToken);
+
+ Action frameHandler = async (jpegData) =>
+ {
+ try
+ {
+ var header = Encoding.ASCII.GetBytes("--boundary\r\nContent-Type: image/jpeg\r\nContent-Length: " + jpegData.Length + "\r\n\r\n");
+ await response.OutputStream.WriteAsync(header, 0, header.Length, cancellationToken);
+ await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken);
+ await response.OutputStream.WriteAsync(new byte[] { 0x0D, 0x0A }, 0, 2, cancellationToken); // \r\n
+ await response.OutputStream.FlushAsync(cancellationToken);
+ }
+ catch
+ {
+ logger.Error("Error sending MJPEG frame");
+ }
+ };
+
try
{
- var camera = await _usbCamera.WithCancellation(cancellationToken);
-
if (!camera.IsCapturing)
{
logger.Error("USB Camera is not capturing");
@@ -320,32 +336,17 @@ public class HttpVideoStreamService : BackgroundService
logger.Info("Start USB Camera MJPEG Stream");
+ camera.FrameReady += frameHandler;
+
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
-
- var jpegData = camera.GetLatestFrame();
- if (jpegData == null)
- {
- logger.Warn("USB Camera MJPEG帧获取失败");
- await Task.Delay(1000 / client.FrameRate, cancellationToken);
- continue;
- }
-
- // MJPEG帧头
- var header = Encoding.ASCII.GetBytes("--boundary\r\nContent-Type: image/jpeg\r\nContent-Length: " + jpegData.Length + "\r\n\r\n");
- await response.OutputStream.WriteAsync(header, 0, header.Length, cancellationToken);
- await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken);
- await response.OutputStream.WriteAsync(new byte[] { 0x0D, 0x0A }, 0, 2, cancellationToken); // \r\n
- await response.OutputStream.FlushAsync(cancellationToken);
-
- await Task.Delay(1000 / client.FrameRate, cancellationToken);
- logger.Info("USB Camera MJPEG帧发送成功");
+ await Task.Delay(-1, cancellationToken);
}
}
- catch (OperationCanceledException ex)
+ catch (OperationCanceledException)
{
- logger.Info(ex, "USB Camera MJPEG 串流取消");
+ logger.Info("USB Camera MJPEG 串流取消");
}
catch (Exception ex)
{
@@ -353,7 +354,8 @@ public class HttpVideoStreamService : BackgroundService
}
finally
{
-
+ camera.FrameReady -= frameHandler;
+ logger.Info("Usb Camera Stream Stopped");
try { response.Close(); } catch { }
}
}
@@ -744,15 +746,14 @@ public class HttpVideoStreamService : BackgroundService
using (await client.Lock.AcquireWriteLockAsync())
{
- if (enable)
- {
- client.CTS = new CancellationTokenSource();
- }
- else
+ if (!enable || client.CTS.IsCancellationRequested)
{
client.CTS.Cancel();
+ client.CTS = new CancellationTokenSource();
}
+ if (!client.Camera.IsValueCreated) return;
+
var camera = await client.Camera.WithCancellation(client.CTS.Token);
var disableResult = await camera.EnableHardwareTrans(enable);
if (disableResult.IsSuccessful && disableResult.Value)
@@ -763,7 +764,7 @@ public class HttpVideoStreamService : BackgroundService
}
catch (Exception ex)
{
- logger.Error(ex, $"Exception occurred while disabling HDMI transmission for camera {boardId}");
+ logger.Error(ex, $"Exception occurred while disabling video transmission for {boardId}");
}
}
diff --git a/server/src/Services/RtspStreamService.cs b/server/src/Services/RtspStreamService.cs
new file mode 100644
index 0000000..4bb4ae4
--- /dev/null
+++ b/server/src/Services/RtspStreamService.cs
@@ -0,0 +1,576 @@
+using System.Net;
+using System.Net.Sockets;
+using System.Collections.Concurrent;
+using System.Text;
+using Rtsp;
+using Rtsp.Messages;
+using Rtsp.Sdp;
+using server.Services;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+
+namespace server.Services;
+
+///
+/// RTSP streaming service that integrates with UsbCameraCapture
+/// Uses simplified RTSP server architecture with RTSPDispatcher
+/// Provides Motion JPEG stream over RTP/RTSP
+/// Compatible with Windows and Linux
+///
+public class RtspStreamService : IDisposable
+{
+ private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
+
+ private readonly UsbCameraCapture _cameraCapture;
+ private readonly ConcurrentDictionary _activeListeners = new();
+
+ // RTSP configuration
+ private readonly int _rtspPort;
+ private readonly string _streamPath;
+ private TcpListener? _rtspServerListener;
+ private ManualResetEvent? _stopping;
+ private Thread? _listenThread;
+
+ // Video encoding parameters
+ private int _videoWidth = 640;
+ private int _videoHeight = 480;
+ private int _frameRate = 30;
+ private int _jpegQuality = 75;
+
+ private bool _isStreaming;
+ private bool _disposed;
+
+ // Frame timing and RTP sequencing
+ private DateTime _lastFrameTime = DateTime.UtcNow;
+ private readonly TimeSpan _frameInterval;
+ private uint _rtpTimestamp = 0;
+ private ushort _sequenceNumber = 0;
+ private readonly uint _ssrc = (uint)Random.Shared.Next();
+
+ // Current frame data for broadcasting
+ private byte[]? _currentFrame;
+ private readonly object _frameLock = new object();
+
+ public event Action? Error;
+ public event Action? StatusChanged;
+
+ public bool IsStreaming => _isStreaming;
+ public int Port => _rtspPort;
+ public string StreamUrl => $"rtsp://localhost:{_rtspPort}/{_streamPath}";
+ public int ActiveSessions => _activeListeners.Count;
+
+ public RtspStreamService(UsbCameraCapture cameraCapture, int port = 8554, string streamPath = "camera")
+ {
+ _cameraCapture = cameraCapture ?? throw new ArgumentNullException(nameof(cameraCapture));
+ _rtspPort = port;
+ _streamPath = streamPath;
+ _frameInterval = TimeSpan.FromSeconds(1.0 / _frameRate);
+
+ // Register RTSP URI scheme
+ RtspUtils.RegisterUri();
+
+ // Subscribe to camera events
+ _cameraCapture.FrameReady += OnFrameReady;
+ _cameraCapture.Error += OnCameraError;
+ }
+
+ ///
+ /// Configure video encoding parameters
+ ///
+ public void ConfigureVideo(int width, int height, int frameRate, int jpegQuality = 75)
+ {
+ if (_isStreaming)
+ throw new InvalidOperationException("Cannot configure video while streaming");
+
+ _videoWidth = width;
+ _videoHeight = height;
+ _frameRate = frameRate;
+ _jpegQuality = jpegQuality;
+
+ logger.Info($"Video configured: {width}x{height} @ {frameRate}fps, JPEG quality {jpegQuality}%");
+ }
+
+ ///
+ /// Start RTSP server and begin streaming
+ ///
+ public async Task StartAsync()
+ {
+ if (_isStreaming)
+ return;
+
+ try
+ {
+ // Validate port range
+ if (_rtspPort < IPEndPoint.MinPort || _rtspPort > IPEndPoint.MaxPort)
+ throw new ArgumentOutOfRangeException(nameof(_rtspPort), _rtspPort, "Port number must be between System.Net.IPEndPoint.MinPort and System.Net.IPEndPoint.MaxPort");
+
+ // Initialize RTSP server
+ _rtspServerListener = new TcpListener(IPAddress.Any, _rtspPort);
+ _rtspServerListener.Start();
+
+ // Start listening for connections
+ _stopping = new ManualResetEvent(false);
+ _listenThread = new Thread(AcceptConnections)
+ {
+ Name = "RTSP-Listener",
+ IsBackground = true
+ };
+ _listenThread.Start();
+
+ // Start camera capture if not already running
+ if (!_cameraCapture.IsCapturing)
+ {
+ await _cameraCapture.StartAsync(1, _videoWidth, _videoHeight, _frameRate);
+ }
+
+ _isStreaming = true;
+ StatusChanged?.Invoke("Streaming started");
+ logger.Info($"RTSP stream started on {StreamUrl}");
+ }
+ catch (Exception ex)
+ {
+ await StopAsync();
+ Error?.Invoke(ex);
+ throw;
+ }
+ }
+
+ ///
+ /// Stop RTSP server and streaming
+ ///
+ public async Task StopAsync()
+ {
+ if (!_isStreaming)
+ return;
+
+ _isStreaming = false;
+
+ try
+ {
+ // Signal stop and wait for listen thread
+ _stopping?.Set();
+ if (_listenThread != null && _listenThread.IsAlive)
+ {
+ _listenThread.Join(TimeSpan.FromSeconds(5));
+ }
+
+ // Stop RTSP server
+ _rtspServerListener?.Stop();
+
+ // Clean up active listeners
+ foreach (var listener in _activeListeners.Values.ToArray())
+ {
+ try
+ {
+ listener.Stop();
+ }
+ catch (Exception ex)
+ {
+ logger.Warn(ex, "Error stopping RTSP listener");
+ }
+ }
+ _activeListeners.Clear();
+
+ StatusChanged?.Invoke("Streaming stopped");
+ logger.Info("RTSP stream stopped");
+ }
+ catch (Exception ex)
+ {
+ Error?.Invoke(ex);
+ }
+
+ await Task.CompletedTask;
+ }
+
+ ///
+ /// Get current stream statistics
+ ///
+ public StreamStats GetStats()
+ {
+ return new StreamStats
+ {
+ IsStreaming = _isStreaming,
+ ActiveSessions = _activeListeners.Count,
+ VideoWidth = _videoWidth,
+ VideoHeight = _videoHeight,
+ FrameRate = _frameRate,
+ StreamUrl = StreamUrl
+ };
+ }
+
+ ///
+ /// Accept incoming RTSP connections
+ ///
+ private void AcceptConnections()
+ {
+ try
+ {
+ while (!(_stopping?.WaitOne(0) ?? true))
+ {
+ TcpClient client = _rtspServerListener!.AcceptTcpClient();
+ var transport = new RtspTcpTransport(client);
+ var listener = new RtspListener(transport);
+
+ var listenerId = Guid.NewGuid().ToString();
+ _activeListeners[listenerId] = listener;
+
+ // Handle listener events
+ listener.MessageReceived += (sender, args) => HandleRtspMessage(listenerId, args);
+
+ // Store listener for later cleanup
+ // We'll rely on exception handling to detect disconnections
+
+ // Start the listener
+ listener.Start();
+
+ logger.Info($"New RTSP client connected: {listenerId} from {client.Client.RemoteEndPoint}");
+ }
+ }
+ catch (SocketException ex)
+ {
+ if (_isStreaming) // Only log if we're still supposed to be running
+ {
+ logger.Warn(ex, "Socket error while accepting connections (may be normal during shutdown)");
+ }
+ }
+ catch (Exception ex)
+ {
+ if (_isStreaming)
+ {
+ logger.Error(ex, "Error accepting RTSP connections");
+ Error?.Invoke(ex);
+ }
+ }
+ }
+
+ ///
+ /// Handle RTSP messages from clients
+ ///
+ private void HandleRtspMessage(string listenerId, RtspChunkEventArgs args)
+ {
+ try
+ {
+ if (args.Message is RtspRequest request)
+ {
+ HandleRtspRequest(listenerId, request);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, $"Error handling RTSP message for listener {listenerId}");
+ }
+ }
+
+ ///
+ /// Handle RTSP requests
+ ///
+ private void HandleRtspRequest(string listenerId, RtspRequest request)
+ {
+ if (!_activeListeners.TryGetValue(listenerId, out var listener))
+ return;
+
+ var response = new RtspResponse();
+ response.OriginalRequest = request;
+
+ // 1. 返回 CSeq 字段
+ if (request.Headers.TryGetValue("CSeq", out var cseq))
+ {
+ response.Headers["CSeq"] = cseq;
+ }
+
+ switch (request.RequestTyped)
+ {
+ case RtspRequest.RequestType.OPTIONS:
+ response.Headers["Public"] = "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE";
+ response.ReturnCode = 200;
+ break;
+
+ case RtspRequest.RequestType.DESCRIBE:
+ if (request.RtspUri?.AbsolutePath.TrimStart('/') == _streamPath)
+ {
+ var sdp = CreateSdp();
+ response.Headers["Content-Type"] = "application/sdp";
+ response.Data = Encoding.UTF8.GetBytes(sdp);
+ response.ReturnCode = 200;
+ }
+ else
+ {
+ response.ReturnCode = 404;
+ }
+ break;
+
+ case RtspRequest.RequestType.SETUP:
+ // 2. 解析客户端 Transport 字段
+ string clientTransport = request.Headers.TryGetValue("Transport", out var transport) ? transport : "";
+ string serverTransport;
+ if (clientTransport.Contains("TCP", StringComparison.OrdinalIgnoreCase) || clientTransport.Contains("interleaved"))
+ {
+ // 客户端要求TCP
+ serverTransport = "RTP/AVP/TCP;unicast;interleaved=0-1";
+ }
+ else if (clientTransport.Contains("UDP", StringComparison.OrdinalIgnoreCase) || clientTransport.Contains("client_port"))
+ {
+ // 客户端要求UDP
+ // 这里假设端口号格式为 client_port=xxxx-xxxx
+ var match = System.Text.RegularExpressions.Regex.Match(clientTransport, @"client_port=(\d+)-(\d+)");
+ if (match.Success)
+ {
+ var clientPort1 = match.Groups[1].Value;
+ var clientPort2 = match.Groups[2].Value;
+ // 你可以自定义 server_port
+ serverTransport = $"RTP/AVP;unicast;client_port={clientPort1}-{clientPort2};server_port=9000-9001";
+ }
+ else
+ {
+ // 默认UDP
+ serverTransport = "RTP/AVP;unicast;client_port=8000-8001;server_port=9000-9001";
+ }
+ }
+ else
+ {
+ // 默认TCP
+ serverTransport = "RTP/AVP/TCP;unicast;interleaved=0-1";
+ }
+ response.Headers["Transport"] = serverTransport;
+ response.Headers["Session"] = listenerId;
+ response.ReturnCode = 200;
+ break;
+
+ case RtspRequest.RequestType.PLAY:
+ response.Headers["Session"] = listenerId;
+ response.ReturnCode = 200;
+ // Start sending frames to this client
+ StartFrameBroadcastForListener(listenerId);
+ break;
+
+ case RtspRequest.RequestType.TEARDOWN:
+ response.ReturnCode = 200;
+ // Stop and remove the listener
+ Task.Run(() =>
+ {
+ listener.Stop();
+ _activeListeners.TryRemove(listenerId, out _);
+ });
+ break;
+
+ default:
+ response.ReturnCode = 501; // Not implemented
+ break;
+ }
+
+ // Send response
+ try
+ {
+ listener.SendMessage(response);
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, $"Error sending RTSP response to listener {listenerId}");
+ }
+ }
+
+ ///
+ /// Create SDP description for the stream
+ ///
+ private string CreateSdp()
+ {
+ var sessionId = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+
+ return $@"v=0
+o=- {sessionId} {sessionId} IN IP4 127.0.0.1
+s=FPGA WebLab Camera Stream
+c=IN IP4 0.0.0.0
+t=0 0
+m=video 0 RTP/AVP 26
+a=rtpmap:26 JPEG/90000
+a=control:track1
+a=framerate:{_frameRate}";
+ }
+
+ ///
+ /// Start broadcasting frames to a specific listener
+ ///
+ private void StartFrameBroadcastForListener(string listenerId)
+ {
+ // For now, we'll use a simple approach where we send the current frame
+ // In a full implementation, you'd want to manage RTP streaming per client
+ lock (_frameLock)
+ {
+ if (_currentFrame != null && _activeListeners.TryGetValue(listenerId, out var listener))
+ {
+ try
+ {
+ // Send current frame (simplified - in real implementation you'd send RTP packets)
+ // This is a placeholder for actual RTP packet creation and sending
+ logger.Debug($"Started frame broadcast for listener {listenerId}");
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, $"Error starting frame broadcast for listener {listenerId}");
+ }
+ }
+ }
+ }
+
+ ///
+ /// Handle new frame from camera
+ ///
+ private void OnFrameReady(byte[] frameData)
+ {
+ if (!_isStreaming || frameData == null || _activeListeners.IsEmpty)
+ return;
+
+ try
+ {
+ // Throttle frame rate
+ var now = DateTime.UtcNow;
+ if (now - _lastFrameTime < _frameInterval)
+ return;
+
+ _lastFrameTime = now;
+
+ // Process and encode frame
+ var processedFrame = ProcessFrame(frameData);
+ if (processedFrame != null)
+ {
+ lock (_frameLock)
+ {
+ _currentFrame = processedFrame;
+ }
+
+ BroadcastFrame(processedFrame);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Error processing camera frame");
+ Error?.Invoke(ex);
+ }
+ }
+
+ ///
+ /// Process raw frame data
+ ///
+ private byte[]? ProcessFrame(byte[] frameData)
+ {
+ try
+ {
+ // Convert frame to JPEG for Motion JPEG streaming
+ using var image = Image.Load(frameData);
+
+ // Resize if necessary
+ if (image.Width != _videoWidth || image.Height != _videoHeight)
+ {
+ image.Mutate(x => x.Resize(_videoWidth, _videoHeight));
+ }
+
+ // Encode as JPEG with specified quality
+ using var stream = new MemoryStream();
+ image.SaveAsJpeg(stream, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder
+ {
+ Quality = _jpegQuality
+ });
+
+ return stream.ToArray();
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Error processing frame");
+ return null;
+ }
+ }
+
+ ///
+ /// Broadcast frame to all active listeners
+ ///
+ private void BroadcastFrame(byte[] frameData)
+ {
+ if (_activeListeners.IsEmpty)
+ return;
+
+ var timestamp = _rtpTimestamp;
+ _rtpTimestamp += (uint)(90000 / _frameRate); // 90kHz clock
+ var sequenceNumber = ++_sequenceNumber;
+
+ var listenersToRemove = new List();
+
+ foreach (var kvp in _activeListeners)
+ {
+ try
+ {
+ var listener = kvp.Value;
+ // Try to send data to test if listener is still active
+ // In a full implementation, you would create and send RTP packets here
+ // For now, this is a placeholder that just checks if we can access the listener
+ try
+ {
+ var _ = listener.RemoteEndPoint; // Test if listener is still valid
+ // SendRtpFrame(listener, frameData, timestamp, sequenceNumber, _ssrc);
+ }
+ catch
+ {
+ listenersToRemove.Add(kvp.Key);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Warn(ex, $"Error sending frame to listener {kvp.Key}");
+ listenersToRemove.Add(kvp.Key);
+ }
+ }
+
+ // Remove failed listeners
+ foreach (var listenerId in listenersToRemove)
+ {
+ if (_activeListeners.TryRemove(listenerId, out var listener))
+ {
+ try
+ {
+ listener.Stop();
+ }
+ catch (Exception ex)
+ {
+ logger.Warn(ex, $"Error stopping failed listener {listenerId}");
+ }
+ }
+ }
+ }
+
+ ///
+ /// Handle camera capture errors
+ ///
+ private void OnCameraError(Exception error)
+ {
+ logger.Error(error, "Camera capture error");
+ Error?.Invoke(error);
+ }
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+
+ StopAsync().Wait();
+
+ _cameraCapture.FrameReady -= OnFrameReady;
+ _cameraCapture.Error -= OnCameraError;
+
+ _rtspServerListener?.Stop();
+ _stopping?.Dispose();
+
+ _disposed = true;
+ }
+}
+
+///
+/// Stream statistics data structure
+///
+public class StreamStats
+{
+ public bool IsStreaming { get; set; }
+ public int ActiveSessions { get; set; }
+ public int VideoWidth { get; set; }
+ public int VideoHeight { get; set; }
+ public int FrameRate { get; set; }
+ public string StreamUrl { get; set; } = string.Empty;
+}
diff --git a/server/src/Services/UsbCameraCapture.cs b/server/src/Services/UsbCameraCapture.cs
index 85e1748..440b002 100644
--- a/server/src/Services/UsbCameraCapture.cs
+++ b/server/src/Services/UsbCameraCapture.cs
@@ -1,4 +1,3 @@
-// using System.Drawing;
using FlashCap;
namespace server.Services;
@@ -70,7 +69,8 @@ public class UsbCameraCapture : IDisposable
{
_descriptor = descriptor;
_characteristics = characteristics;
- _device = await descriptor.OpenAsync(characteristics, OnFrameCaptured);
+ _device = await descriptor.OpenAsync(
+ characteristics, TranscodeFormats.DoNotTranscode, true, 10, OnFrameCaptured);
await _device.StartAsync();
_isCapturing = true;
@@ -104,26 +104,6 @@ public class UsbCameraCapture : IDisposable
return _latestFrame;
}
- // ///
- // /// Get latest frame as bitmap
- // ///
- // public Bitmap? GetLatestFrameAsBitmap()
- // {
- // var frameData = _latestFrame;
- // if (frameData == null)
- // return null;
-
- // try
- // {
- // using var ms = new MemoryStream(frameData);
- // return new Bitmap(ms);
- // }
- // catch
- // {
- // return null;
- // }
- // }
-
///
/// Get supported video characteristics for current device
///
@@ -170,7 +150,6 @@ public class UsbCameraCapture : IDisposable
private void OnFrameCaptured(PixelBufferScope bufferScope)
{
- logger.Info("Frame captured");
if (!_isCapturing)
return;
@@ -180,6 +159,7 @@ public class UsbCameraCapture : IDisposable
var imageData = bufferScope.Buffer.CopyImage();
_latestFrame = imageData;
FrameReady?.Invoke(imageData);
+ // logger.Info("USB Camera frame captured");
}
catch (Exception ex)
{
diff --git a/src/views/Project/VideoStream.vue b/src/views/Project/VideoStream.vue
index beb3828..62749d1 100644
--- a/src/views/Project/VideoStream.vue
+++ b/src/views/Project/VideoStream.vue
@@ -387,8 +387,6 @@ import { VideoStreamClient, ResolutionConfigRequest } from "@/APIClient";
import { useEquipments } from "@/stores/equipments";
import { AuthManager } from "@/utils/AuthManager";
-const eqps = useEquipments();
-
// 状态管理
const loading = ref(false);
const configing = ref(false);
@@ -510,7 +508,7 @@ const toggleStreamType = async () => {
"success",
`已切换到${streamType.value === "usbCamera" ? "USB摄像头" : "视频流"}`,
);
- stopStream();
+ await stopStream();
} catch (error) {
addLog("error", `切换视频流类型失败: ${error}`);
console.error("切换视频流类型失败:", error);
@@ -647,7 +645,8 @@ const tryReconnect = () => {
// 执行对焦
const performFocus = async () => {
- if (isFocusing.value || !isPlaying.value) return;
+ if (isFocusing.value || !isPlaying.value || streamType.value === "usbCamera")
+ return;
try {
isFocusing.value = true;
@@ -711,7 +710,7 @@ const startStream = async () => {
try {
addLog("info", "正在启动视频流...");
videoStatus.value = "正在连接视频流...";
- videoClient.setVideoStreamEnable(true);
+ await videoClient.setVideoStreamEnable(true);
// 刷新状态
await refreshStatus();
@@ -778,7 +777,7 @@ const changeResolution = async () => {
// 如果正在播放,先停止视频流
if (wasPlaying) {
- stopStream();
+ await stopStream();
await new Promise((resolve) => setTimeout(resolve, 1000)); // 等待1秒
}
@@ -815,10 +814,10 @@ const changeResolution = async () => {
};
// 停止视频流
-const stopStream = () => {
+const stopStream = async () => {
try {
addLog("info", "正在停止视频流...");
- videoClient.setVideoStreamEnable(false);
+ await videoClient.setVideoStreamEnable(false);
// 清除视频源
currentVideoSource.value = "";