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 = "";