DreamineVMS 1.0.0.0
Windows PC의 RTSP·USB 카메라 영상을 HLS로 변환해 원격 CCTV Viewer에 전달하는 데스크톱 에이전트입니다.
로딩중...
검색중...
일치하는것 없음
MainWindow.xaml.cs
이 파일의 문서화 페이지로 가기
1using Dreamine.Hybrid.Wpf.Controls;
2using Dreamine.Hybrid.Wpf.Hosting;
3using DreamineVMS.Blazor.Components;
6using Microsoft.Extensions.Options;
7using Microsoft.Web.WebView2.Core;
8using Microsoft.Web.WebView2.Wpf;
9using System;
10using System.ComponentModel;
11using System.Diagnostics;
12using System.Net.Http;
13using System.Threading;
14using System.Threading.Tasks;
15using System.Windows;
16
17namespace DreamineVMS.Views;
18
27public partial class MainWindow : Window
28{
55 private WebView2? _wpfLiveWebView;
64 private WebView2? _camerasWebView;
73 private bool _wpfLiveInitialized;
82 private bool _camerasInitialized;
83
108 public MainWindow(IOptions<VmsServerOptions> serverOptions)
109 {
110 _serverOptions = serverOptions?.Value ?? throw new ArgumentNullException(nameof(serverOptions));
111 InitializeComponent();
112
113 if (DesignerProperties.GetIsInDesignMode(this))
114 {
115 return;
116 }
117
118 DataContextChanged += OnDataContextChanged;
119 Loaded += OnLoaded;
120 Closed += OnClosed;
121 MainTabControl.SelectionChanged += OnMainTabSelectionChanged;
122 }
123
148 private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
149 {
150 if (_attachedViewModel is not null)
151 {
152 _attachedViewModel.OpenLiveTabRequested -= OnOpenLiveTabRequested;
153 _attachedViewModel = null;
154 }
155
156 if (DataContext is MainWindowViewModel viewModel)
157 {
158 _attachedViewModel = viewModel;
159 _attachedViewModel.OpenLiveTabRequested += OnOpenLiveTabRequested;
160 }
161 }
162
187 private void OnOpenLiveTabRequested(object? sender, EventArgs e)
188 {
189 // 레거시 메시지 호환용입니다. Blazor Server의 첫 화면은 Live View이며, 탭 전환은 수행하지 않습니다.
190 Dispatcher.Invoke(() =>
191 {
192 if (!IsActive)
193 {
194 Activate();
195 }
196 });
197 }
198
223 private async void OnLoaded(object sender, RoutedEventArgs e)
224 {
225 HybridHostControl embeddedDashboard = new()
226 {
227 HostPage = "wwwroot/index.html",
229 RootComponentType = typeof(VmsLocalDashboard),
230 RootSelector = "#app"
231 };
232
233 EmbeddedDashboardTab.Content = embeddedDashboard;
234
235 // Live WebView는 동시에 두 개를 바로 띄우지 않습니다.
236 // 두 Live 탭이 동시에 /live에 접속하면 Blazor Circuit과 hls.js player가 중복 생성되어
237 // StopAll → StartAll 이후 source/session 동기화가 꼬일 수 있습니다.
238 await EnsureSelectedLiveWebViewAsync().ConfigureAwait(true);
239 }
240
265 private void OnClosed(object? sender, EventArgs e)
266 {
267 if (_attachedViewModel is not null)
268 {
269 _attachedViewModel.OpenLiveTabRequested -= OnOpenLiveTabRequested;
270 _attachedViewModel = null;
271 }
272
273 // WebView2 인스턴스를 명시적으로 정리합니다.
274 // Dispose하지 않으면 Chromium 서브프로세스가 살아남아 앱 종료를 막을 수 있습니다.
277
278 // WPF Shutdown을 명시적으로 트리거합니다.
279 // host.RunDreamineWpfApp의 구현에 따라 자동 종료가 안 될 수 있으므로 안전망입니다.
280 Application.Current?.Shutdown();
281 }
282
307 private async void OnMainTabSelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
308 {
309 if (!IsLoaded) return;
310
311 // 에이전트 설정 탭이 처음 열릴 때 PasswordBox에 저장된 비밀번호를 채워줍니다.
312 // PasswordBox.Password는 DependencyProperty가 아니라 직접 바인딩이 불가능합니다.
313 if (AgentPasswordBox is not null
314 && DataContext is MainWindowViewModel vm
315 && string.IsNullOrEmpty(AgentPasswordBox.Password)
316 && !string.IsNullOrEmpty(vm.AgentSettings.Password))
317 {
318 AgentPasswordBox.Password = vm.AgentSettings.Password;
319 }
320
321 await EnsureSelectedLiveWebViewAsync().ConfigureAwait(true);
322 }
323
348 private void AgentPasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
349 {
350 if (DataContext is MainWindowViewModel vm)
351 vm.AgentSettings.Password = AgentPasswordBox.Password;
352 }
353
371 {
372 if (MainTabControl.SelectedItem == WpfLiveTab)
373 {
374 await EnsureWpfLiveWebViewAsync().ConfigureAwait(true);
375 return;
376 }
377
378 if (MainTabControl.SelectedItem == CamerasTab)
379 {
380 await EnsureCamerasWebViewAsync().ConfigureAwait(true);
381 }
382 }
383
400 private async Task EnsureWpfLiveWebViewAsync()
401 {
403 {
404 return;
405 }
406
407 WebView2 webView = HybridWebViewHost.CreateWebView();
408 WpfLiveTab.Content = webView;
409 _wpfLiveWebView = webView;
410
411 string liveUrl = GetLiveUrl();
412 RegisterWebViewRecovery(webView, () => liveUrl);
413 await NavigateServerAsync(webView, liveUrl).ConfigureAwait(true);
414 _wpfLiveInitialized = true;
415 }
416
433 private async Task EnsureCamerasWebViewAsync()
434 {
436 {
437 return;
438 }
439
440 WebView2 webView = HybridWebViewHost.CreateWebView();
441 CamerasTab.Content = webView;
442 _camerasWebView = webView;
443
444 string camerasUrl = $"http://localhost:{_serverOptions.Port}/cameras";
445 RegisterWebViewRecovery(webView, () => camerasUrl);
446 await NavigateServerAsync(webView, camerasUrl).ConfigureAwait(true);
447 _camerasInitialized = true;
448 }
449
466 private string GetLiveUrl()
467 {
468 return $"http://localhost:{_serverOptions.Port}/live";
469 }
470
495 private void RegisterWebViewRecovery(WebView2 webView, Func<string> urlFactory)
496 {
497 webView.CoreWebView2InitializationCompleted += (_, e) =>
498 {
499 if (!e.IsSuccess || webView.CoreWebView2 is null)
500 {
501 Debug.WriteLine($"[DreamineVMS] WebView2 initialization failed: {e.InitializationException}");
502 return;
503 }
504
505 webView.CoreWebView2.ProcessFailed += async (_, args) =>
506 {
507 Debug.WriteLine($"[DreamineVMS] WebView2 process failed: {args.ProcessFailedKind}. Reloading...");
508
509 await Dispatcher.InvokeAsync(async () =>
510 {
511 await Task.Delay(1000).ConfigureAwait(true);
512 TryNavigateWebView(webView, urlFactory());
513 });
514 };
515 };
516
517 webView.NavigationCompleted += (_, e) =>
518 {
519 if (e.IsSuccess)
520 {
521 return;
522 }
523
524 Debug.WriteLine($"[DreamineVMS] WebView2 navigation failed: {e.WebErrorStatus}. Reloading...");
525 Dispatcher.InvokeAsync(async () =>
526 {
527 await Task.Delay(1500).ConfigureAwait(true);
528 TryNavigateWebView(webView, urlFactory());
529 });
530 };
531 }
532
557 private static void TryNavigateWebView(WebView2 webView, string url)
558 {
559 try
560 {
561 webView.Source = new Uri(url);
562 }
563 catch (Exception ex)
564 {
565 Debug.WriteLine($"[DreamineVMS] WebView2 recovery navigation failed: {ex}");
566 }
567 }
568
585 private static void TryDisposeWebView(ref WebView2? webView)
586 {
587 if (webView is null)
588 {
589 return;
590 }
591
592 try
593 {
594 webView.Dispose();
595 }
596 catch (Exception ex)
597 {
598 Debug.WriteLine($"[DreamineVMS] WebView2 dispose failed: {ex}");
599 }
600
601 webView = null;
602 }
603
652 private static async Task NavigateServerAsync(WebView2 webView, string url, int timeoutMs = 15000, int intervalMs = 500)
653 {
654 try
655 {
656 await Task.Delay(1500).ConfigureAwait(true);
657
658 using CancellationTokenSource cts = new(timeoutMs);
659 using HttpClient http = new();
660 bool alive = false;
661
662 while (!cts.IsCancellationRequested)
663 {
664 try
665 {
666 HttpResponseMessage response = await http.GetAsync(url, cts.Token).ConfigureAwait(true);
667 if ((int)response.StatusCode >= 200 && (int)response.StatusCode < 400)
668 {
669 alive = true;
670 break;
671 }
672 }
673 catch (HttpRequestException)
674 {
675 // Server may not be ready yet.
676 }
677 catch (TaskCanceledException)
678 {
679 // Retry until overall timeout.
680 }
681
682 await Task.Delay(intervalMs, cts.Token).ConfigureAwait(true);
683 }
684
685 if (!alive)
686 {
687 Debug.WriteLine($"[DreamineVMS] Server dashboard timeout: {url}");
688 await HybridWebViewHost.ShowOfflineMessageAsync(webView, url).ConfigureAwait(true);
689 return;
690 }
691
692 webView.Source = new Uri(url);
693 }
694 catch (TaskCanceledException)
695 {
696 Debug.WriteLine("[DreamineVMS] Server navigation canceled.");
697 }
698 catch (Exception ex)
699 {
700 Debug.WriteLine($"[DreamineVMS] Server navigation failed: {ex}");
701 }
702 }
703}
static ? IServiceProvider ServiceProvider
Definition App.xaml.cs:28
static void TryNavigateWebView(WebView2 webView, string url)
void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
static async Task NavigateServerAsync(WebView2 webView, string url, int timeoutMs=15000, int intervalMs=500)
readonly VmsServerOptions _serverOptions
void OnOpenLiveTabRequested(object? sender, EventArgs e)
static void TryDisposeWebView(ref WebView2? webView)
MainWindow(IOptions< VmsServerOptions > serverOptions)
async void OnMainTabSelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
void RegisterWebViewRecovery(WebView2 webView, Func< string > urlFactory)
async Task EnsureSelectedLiveWebViewAsync()
MainWindowViewModel? _attachedViewModel
void AgentPasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
async void OnLoaded(object sender, RoutedEventArgs e)
void OnClosed(object? sender, EventArgs e)