diff --git a/Hi.Webapi.csproj b/Hi.Webapi.csproj index c11c764..b04ed81 100644 --- a/Hi.Webapi.csproj +++ b/Hi.Webapi.csproj @@ -10,7 +10,7 @@ Hi.Webapi $(AssemblyName) HiNC webapi class library. - 23 + 24 3.1.$(VersionBuild) HiAPI Debug;Release diff --git a/Hubs/RenderingHub.cs b/Hubs/RenderingHub.cs index 3974e2d..d1f4194 100644 --- a/Hubs/RenderingHub.cs +++ b/Hubs/RenderingHub.cs @@ -7,14 +7,13 @@ using Hi.Webapi.Services; namespace Hi.Webapi.Hubs { /// - /// SignalR Hub 用於處理渲染畫布的實時通信 + /// SignalR Hub for Rendering. /// public class RenderingHub : Hub { - private readonly Dictionary _lastFrameCache = []; - private readonly Dictionary _sketchViewCache = []; public RenderingService RenderingService { get; } public ILogger Logger { get; } + Dictionary SketchViewCache { get; } = []; public RenderingHub(RenderingService renderingService, ILogger logger) { @@ -27,19 +26,19 @@ namespace Hi.Webapi.Hubs /// public async Task InitializeCanvas(int width, int height) { - var sessionId = Context.ConnectionId; - Logger.LogInformation($"InitializeCanvas called - SessionId: {sessionId}, Width: {width}, Height: {height}"); + var connectionId = Context.ConnectionId; + Logger.LogInformation($"InitializeCanvas called - ConnectionId: {connectionId}, Width: {width}, Height: {height}"); - var engine = RenderingService.GetOrCreateEngine(sessionId); - Logger.LogInformation($"Engine created/retrieved - SessionId: {sessionId}"); + var engine = RenderingService.GetOrCreateEngine(connectionId); + Logger.LogInformation($"Engine created/retrieved - ConnectionId: {connectionId}"); engine.Start(width, height); - Logger.LogInformation($"Engine started with size {width}x{height} - SessionId: {sessionId}"); + Logger.LogInformation($"Engine started with size {width}x{height} - ConnectionId: {connectionId}"); - // 在設置回調之前,先捕獲客戶端代理 - var clientProxy = Clients.Caller; + // 捕獲必要的參數,避免閉包問題 var logger = Logger; var frameCount = 0; + var renderingService = RenderingService; // 設置渲染回調 unsafe @@ -51,11 +50,11 @@ namespace Hi.Webapi.Hubs frameCount++; if (frameCount <= 5) // 只記錄前5幀 - logger.LogInformation($"Frame {frameCount} rendered - Size: {w}x{h}, SessionId: {sessionId}"); + logger.LogInformation($"Frame {frameCount} rendered - Size: {w}x{h}, ConnectionId: {connectionId}"); if (w <= 0 || h <= 0) { - logger.LogWarning($"Invalid image size: {w}x{h} - SessionId: {sessionId}"); + logger.LogWarning($"Invalid image size: {w}x{h} - ConnectionId: {connectionId}"); return; } @@ -71,15 +70,6 @@ namespace Hi.Webapi.Hubs rgba[i * 4 + 3] = bgra[i * 4 + 3]; } - if (_lastFrameCache.TryGetValue( - sessionId, out byte[] preImageRgba)) - { - if (preImageRgba != null && - preImageRgba.SequenceEqual(rgba)) - return; - } - _lastFrameCache[sessionId] = rgba; - if (frameCount <= 5) logger.LogInformation($"Frame {frameCount} sending - Size: {w}x{h}, Data: {rgba.Length} bytes"); @@ -98,8 +88,9 @@ namespace Hi.Webapi.Hubs if (frameCount <= 5) logger.LogInformation($"Compressed: {compressedRgbaArray.Length} bytes ({compressionRatio:F1}% of original)"); - // 發送壓縮數據到客戶端(注意:第二個參數應該是原始長度,不是壓縮後的長度) - _ = clientProxy.SendAsync("ImageUpdate", + // 使用 RenderingService 的線程安全方法發送數據 + _ = renderingService.SendImageToClient( + connectionId, compressedRgbaArray, rgba.Length, w, @@ -108,40 +99,35 @@ namespace Hi.Webapi.Hubs { if (t.IsFaulted) { - logger.LogError($"Failed to send frame: {t.Exception?.GetBaseException().Message} - SessionId: {sessionId}"); + logger.LogError($"Failed to send frame: {t.Exception?.GetBaseException().Message} - ConnectionId: {connectionId}"); } }); } - catch (ObjectDisposedException) - { - // Hub 已經被釋放,忽略這個錯誤 - logger.LogDebug($"Hub已釋放,忽略渲染回調 - SessionId: {sessionId}"); - } catch (Exception ex) { - logger.LogError(ex, $"渲染回調錯誤 - SessionId: {sessionId}"); + logger.LogError(ex, $"渲染回調錯誤 - ConnectionId: {connectionId}"); } }; } // 設置初始視圖 - if (_sketchViewCache.TryGetValue(sessionId, out var cachedView)) + if (SketchViewCache.TryGetValue(connectionId, out var cachedView)) { engine.SketchView = cachedView; } else { engine.SetViewToHomeView(); - _sketchViewCache[sessionId] = engine.SketchView; + SketchViewCache[connectionId] = engine.SketchView; } - Logger.LogInformation($"View initialized - SessionId: {sessionId}"); + Logger.LogInformation($"View initialized - ConnectionId: {connectionId}"); // 確保引擎可見並開始渲染 engine.IsVisible = true; - Logger.LogInformation($"Engine visibility set to true - SessionId: {sessionId}"); + Logger.LogInformation($"Engine visibility set to true - ConnectionId: {connectionId}"); - await Clients.Caller.SendAsync("CanvasInitialized", sessionId); - Logger.LogInformation($"Canvas initialized for session: {sessionId}"); + await Clients.Caller.SendAsync("CanvasInitialized", connectionId); + Logger.LogInformation($"Canvas initialized for session: {connectionId}"); } /// @@ -149,10 +135,10 @@ namespace Hi.Webapi.Hubs /// public Task HandleMouseMove(double x, double y, int buttonMask) { - var sessionId = Context.ConnectionId; - Logger.LogDebug($"HandleMouseMove - SessionId: {sessionId}, X: {x}, Y: {y}, ButtonMask: {buttonMask}"); + var connectionId = Context.ConnectionId; + Logger.LogDebug($"HandleMouseMove - ConnectionId: {connectionId}, X: {x}, Y: {y}, ButtonMask: {buttonMask}"); - var engine = RenderingService.GetOrCreateEngine(sessionId); + var engine = RenderingService.GetOrCreateEngine(connectionId); var p = new Vec2i((int)x, (int)y); // 移動鼠標 @@ -167,7 +153,7 @@ namespace Hi.Webapi.Hubs LEFT_BUTTON = 0, RIGHT_BUTTON = 2 }); - _sketchViewCache[sessionId] = engine.SketchView; + SketchViewCache[connectionId] = engine.SketchView; } return Task.CompletedTask; @@ -178,10 +164,10 @@ namespace Hi.Webapi.Hubs /// public Task HandleMouseDown(double x, double y, int button) { - var sessionId = Context.ConnectionId; - Logger.LogInformation($"HandleMouseDown - SessionId: {sessionId}, X: {x}, Y: {y}, Button: {button}"); + var connectionId = Context.ConnectionId; + Logger.LogInformation($"HandleMouseDown - ConnectionId: {connectionId}, X: {x}, Y: {y}, Button: {button}"); - var engine = RenderingService.GetOrCreateEngine(sessionId); + var engine = RenderingService.GetOrCreateEngine(connectionId); engine.MouseButtonDown(button); return Task.CompletedTask; } @@ -191,10 +177,10 @@ namespace Hi.Webapi.Hubs /// public Task HandleMouseUp(double x, double y, int button) { - var sessionId = Context.ConnectionId; - Logger.LogInformation($"HandleMouseUp - SessionId: {sessionId}, X: {x}, Y: {y}, Button: {button}"); + var connectionId = Context.ConnectionId; + Logger.LogInformation($"HandleMouseUp - ConnectionId: {connectionId}, X: {x}, Y: {y}, Button: {button}"); - var engine = RenderingService.GetOrCreateEngine(sessionId); + var engine = RenderingService.GetOrCreateEngine(connectionId); engine.MouseButtonUp(button); return Task.CompletedTask; } @@ -204,10 +190,10 @@ namespace Hi.Webapi.Hubs /// public Task HandleMouseWheel(double x, double y, double deltaX, double deltaY, string browserBrand = "chrome") { - var sessionId = Context.ConnectionId; - Logger.LogInformation($"HandleMouseWheel - SessionId: {sessionId}, X: {x}, Y: {y}, DeltaX: {deltaX}, DeltaY: {deltaY}, Browser: {browserBrand}"); + var connectionId = Context.ConnectionId; + Logger.LogInformation($"HandleMouseWheel - ConnectionId: {connectionId}, X: {x}, Y: {y}, DeltaX: {deltaX}, DeltaY: {deltaY}, Browser: {browserBrand}"); - var engine = RenderingService.GetOrCreateEngine(sessionId); + var engine = RenderingService.GetOrCreateEngine(connectionId); // 根據瀏覽器類型獲取縮放比例 double scale = GetWheelScaling(browserBrand); @@ -218,7 +204,7 @@ namespace Hi.Webapi.Hubs engine.MouseWheelTransform( (int)(deltaX * scale * 1000), -(int)(deltaY * scale * 1000), 0.1 / 1000); - _sketchViewCache[sessionId] = engine.SketchView; + SketchViewCache[connectionId] = engine.SketchView; return Task.CompletedTask; } @@ -228,20 +214,18 @@ namespace Hi.Webapi.Hubs /// public Task HandleResize(int width, int height) { - var sessionId = Context.ConnectionId; - Logger.LogInformation($"HandleResize - SessionId: {sessionId}, Width: {width}, Height: {height}"); - - var engine = RenderingService.GetOrCreateEngine(sessionId); - - // 重置圖像緩存以強制重新渲染 - if (_lastFrameCache.ContainsKey(sessionId)) - { - _lastFrameCache.Remove(sessionId); - } + var connectionId = Context.ConnectionId; + Logger.LogInformation($"HandleResize - ConnectionId: {connectionId}, Width: {width}, Height: {height}"); + var engine = RenderingService.GetOrCreateEngine(connectionId); + //since the resize from js is floating value, + //the resize event may raise with the same integer size. + //the canvas will be blank after resize event. + //ClearCache to trigger the rendering process. + engine.ClearCache(); engine.Resize(width, height); - Logger.LogInformation($"Resize completed - SessionId: {sessionId}"); + Logger.LogInformation($"Resize completed - ConnectionId: {connectionId}"); return Task.CompletedTask; } @@ -250,10 +234,10 @@ namespace Hi.Webapi.Hubs /// public Task HandleVisibilityChange(string visibilityState) { - var sessionId = Context.ConnectionId; - Logger.LogInformation($"HandleVisibilityChange - SessionId: {sessionId}, State: {visibilityState}"); + var connectionId = Context.ConnectionId; + Logger.LogInformation($"HandleVisibilityChange - ConnectionId: {connectionId}, State: {visibilityState}"); - var engine = RenderingService.GetOrCreateEngine(sessionId); + var engine = RenderingService.GetOrCreateEngine(connectionId); engine.IsVisible = visibilityState == "visible"; return Task.CompletedTask; @@ -264,10 +248,10 @@ namespace Hi.Webapi.Hubs /// public Task SetView(string viewType) { - var sessionId = Context.ConnectionId; - Logger.LogInformation($"SetView called - SessionId: {sessionId}, ViewType: {viewType}"); + var connectionId = Context.ConnectionId; + Logger.LogInformation($"SetView called - ConnectionId: {connectionId}, ViewType: {viewType}"); - var engine = RenderingService.GetOrCreateEngine(sessionId); + var engine = RenderingService.GetOrCreateEngine(connectionId); switch (viewType.ToLower()) { @@ -299,11 +283,11 @@ namespace Hi.Webapi.Hubs engine.SetViewToHomeView(); break; default: - Logger.LogWarning($"Unknown view type: {viewType} - SessionId: {sessionId}"); + Logger.LogWarning($"Unknown view type: {viewType} - ConnectionId: {connectionId}"); break; } - _sketchViewCache[sessionId] = engine.SketchView; + SketchViewCache[connectionId] = engine.SketchView; return Task.CompletedTask; } @@ -313,10 +297,10 @@ namespace Hi.Webapi.Hubs /// public Task HandleKeyDown(string key, string code, bool ctrlKey, bool shiftKey, bool altKey) { - var sessionId = Context.ConnectionId; - Logger.LogInformation($"HandleKeyDown - SessionId: {sessionId}, Key: {key}, Code: {code}, Ctrl: {ctrlKey}, Shift: {shiftKey}, Alt: {altKey}"); + var connectionId = Context.ConnectionId; + Logger.LogInformation($"HandleKeyDown - ConnectionId: {connectionId}, Key: {key}, Code: {code}, Ctrl: {ctrlKey}, Shift: {shiftKey}, Alt: {altKey}"); - var engine = RenderingService.GetOrCreateEngine(sessionId); + var engine = RenderingService.GetOrCreateEngine(connectionId); // 使用 key 的 HashCode(與 Blazor 版本一致) long keyCode = key.ToLower().GetHashCode(); @@ -339,7 +323,7 @@ namespace Hi.Webapi.Hubs ARROW_UP = "arrowup".ToLower().GetHashCode() }); - _sketchViewCache[sessionId] = engine.SketchView; + SketchViewCache[connectionId] = engine.SketchView; return Task.CompletedTask; } @@ -349,10 +333,10 @@ namespace Hi.Webapi.Hubs /// public Task HandleKeyUp(string key, string code, bool ctrlKey, bool shiftKey, bool altKey) { - var sessionId = Context.ConnectionId; - Logger.LogInformation($"HandleKeyUp - SessionId: {sessionId}, Key: {key}, Code: {code}, Ctrl: {ctrlKey}, Shift: {shiftKey}, Alt: {altKey}"); + var connectionId = Context.ConnectionId; + Logger.LogInformation($"HandleKeyUp - ConnectionId: {connectionId}, Key: {key}, Code: {code}, Ctrl: {ctrlKey}, Shift: {shiftKey}, Alt: {altKey}"); - var engine = RenderingService.GetOrCreateEngine(sessionId); + var engine = RenderingService.GetOrCreateEngine(connectionId); long keyCode = key.ToLower().GetHashCode(); engine.KeyUp(keyCode); @@ -365,10 +349,10 @@ namespace Hi.Webapi.Hubs /// public Task HandleTouchDown(int pointerId, double x, double y) { - var sessionId = Context.ConnectionId; - Logger.LogInformation($"HandleTouchDown - SessionId: {sessionId}, PointerId: {pointerId}, X: {x}, Y: {y}"); + var connectionId = Context.ConnectionId; + Logger.LogInformation($"HandleTouchDown - ConnectionId: {connectionId}, PointerId: {pointerId}, X: {x}, Y: {y}"); - var engine = RenderingService.GetOrCreateEngine(sessionId); + var engine = RenderingService.GetOrCreateEngine(connectionId); engine.TouchDown(pointerId, (int)x, (int)y); return Task.CompletedTask; @@ -379,10 +363,10 @@ namespace Hi.Webapi.Hubs /// public Task HandleTouchMove(int pointerId, double x, double y) { - var sessionId = Context.ConnectionId; - Logger.LogDebug($"HandleTouchMove - SessionId: {sessionId}, PointerId: {pointerId}, X: {x}, Y: {y}"); + var connectionId = Context.ConnectionId; + Logger.LogDebug($"HandleTouchMove - ConnectionId: {connectionId}, PointerId: {pointerId}, X: {x}, Y: {y}"); - var engine = RenderingService.GetOrCreateEngine(sessionId); + var engine = RenderingService.GetOrCreateEngine(connectionId); engine.TouchMove(pointerId, (int)x, (int)y); return Task.CompletedTask; @@ -393,10 +377,10 @@ namespace Hi.Webapi.Hubs /// public Task HandleTouchUp(int pointerId) { - var sessionId = Context.ConnectionId; - Logger.LogInformation($"HandleTouchUp - SessionId: {sessionId}, PointerId: {pointerId}"); + var connectionId = Context.ConnectionId; + Logger.LogInformation($"HandleTouchUp - ConnectionId: {connectionId}, PointerId: {pointerId}"); - var engine = RenderingService.GetOrCreateEngine(sessionId); + var engine = RenderingService.GetOrCreateEngine(connectionId); engine.TouchUp(pointerId); return Task.CompletedTask; @@ -424,14 +408,12 @@ namespace Hi.Webapi.Hubs /// public override async Task OnDisconnectedAsync(Exception exception) { - var sessionId = Context.ConnectionId; + var connectionId = Context.ConnectionId; - // 清理緩存 - _lastFrameCache.Remove(sessionId); - _sketchViewCache.Remove(sessionId); + SketchViewCache.Remove(connectionId); - RenderingService.RemoveEngine(sessionId); - Logger.LogInformation($"Client disconnected: {sessionId}"); + RenderingService.RemoveEngine(connectionId); + Logger.LogInformation($"Client disconnected: {connectionId}"); await base.OnDisconnectedAsync(exception); } } diff --git a/Services/RenderingService.cs b/Services/RenderingService.cs index 12d2e1e..c367638 100644 --- a/Services/RenderingService.cs +++ b/Services/RenderingService.cs @@ -1,6 +1,7 @@ using Hi.Disp; using Hi.Geom; -using Hi.Native; +using Microsoft.AspNetCore.SignalR; +using Hi.Webapi.Hubs; using System.Collections.Concurrent; namespace Hi.Webapi.Services @@ -16,21 +17,23 @@ namespace Hi.Webapi.Services /// public ConcurrentDictionary EngineDictionary { get; } = new(); ILogger Logger { get; } + IHubContext HubContext { get; } private bool disposedValue; - public RenderingService(ILogger logger) + public RenderingService(ILogger logger, IHubContext hubContext) { Logger = logger; + HubContext = hubContext; } - /// - /// 創建或獲取一個 DispEngine 實例 - /// - public DispEngine GetOrCreateEngine(string sessionId) + /// + /// Get Or Create an DispEngine. + /// + public DispEngine GetOrCreateEngine(string connectionId) { - return EngineDictionary.GetOrAdd(sessionId, id => + return EngineDictionary.GetOrAdd(connectionId, id => { - Logger.LogInformation($"創建新的 DispEngine,SessionId: {id}"); + Logger.LogInformation($"Create new DispEngine,ConnectionId: {id}"); var engine = new DispEngine(); engine.BackgroundColor = new Vec3d(0.1, 0.1, 0.5); engine.BackgroundOpacity = 0.1; @@ -39,25 +42,38 @@ namespace Hi.Webapi.Services } /// - /// 移除指定的 DispEngine + /// Send image to specific client. /// - public bool RemoveEngine(string sessionId) + public async Task SendImageToClient(string connectionId, byte[] compressedData, int originalLength, int width, int height) { - if (EngineDictionary.TryRemove(sessionId, out var engine)) + try { - Logger.LogInformation($"移除 DispEngine,SessionId: {sessionId}"); + await HubContext.Clients.Client(connectionId).SendAsync("ImageUpdate", + compressedData, originalLength, width, height); + } + catch (Exception ex) + { + Logger.LogError(ex, $"Failed to send image to client: {connectionId}"); + } + } + + /// + /// Remove DispEngine + /// + public bool RemoveEngine(string connectionId) + { + if (EngineDictionary.TryRemove(connectionId, out var engine)) + { + Logger.LogInformation($"Remove DispEngine,ConnectionId: {connectionId}"); try { - // 停止渲染 engine.IsVisible = false; - - // 釋放資源 engine.Dispose(); } catch (Exception ex) { - Logger.LogError(ex, $"清理 DispEngine 時發生錯誤,SessionId: {sessionId}"); + Logger.LogError(ex, $"清理 DispEngine 時發生錯誤,ConnectionId: {connectionId}"); } return true; @@ -65,10 +81,10 @@ namespace Hi.Webapi.Services return false; } - /// - /// 獲取當前活動的引擎數量 - /// - public int GetActiveEngineCount() => EngineDictionary.Count; + /// + /// Get Active Engine Count. + /// + public int GetActiveEngineCount() => EngineDictionary.Count; /// protected virtual void Dispose(bool disposing) { @@ -85,7 +101,7 @@ namespace Hi.Webapi.Services } catch (Exception ex) { - Logger.LogError(ex, $"Dispose 時清理引擎錯誤,SessionId: {kvp.Key}"); + Logger.LogError(ex, $"Dispose DispEngine Error,ConnectionId: {kvp.Key}"); } } EngineDictionary.Clear(); @@ -102,4 +118,4 @@ namespace Hi.Webapi.Services } } -} \ No newline at end of file +} \ No newline at end of file