From a47bf94ee798283d81a83c8fe3b34437bf55dc6f Mon Sep 17 00:00:00 2001 From: hj615 Date: Thu, 28 Aug 2025 15:19:21 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20CCTV=20=EC=9E=AC=EC=83=9D=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=B0=A9=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SmartAquaViewer/Classes/RtspImageBehaviour.cs | 221 ------------------ SmartAquaViewer/Controls/FFPlayerControl.xaml | 4 +- .../Controls/FFPlayerControl.xaml.cs | 2 + SmartAquaViewer/View/CCTVView.xaml | 37 +-- SmartAquaViewer/ViewModel/CCTVViewModel.cs | 11 + .../ViewModel/FFPlayerViewModel.cs | 7 +- 6 files changed, 24 insertions(+), 258 deletions(-) delete mode 100644 SmartAquaViewer/Classes/RtspImageBehaviour.cs diff --git a/SmartAquaViewer/Classes/RtspImageBehaviour.cs b/SmartAquaViewer/Classes/RtspImageBehaviour.cs deleted file mode 100644 index d3cc4b6..0000000 --- a/SmartAquaViewer/Classes/RtspImageBehaviour.cs +++ /dev/null @@ -1,221 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Drawing; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics.X86; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Threading; -using FFmpeg.AutoGen; -using SmartAquaViewer.Helper.FFHelper; - -namespace SmartAquaViewer.Classes -{ - public static class RtspImageBehavior - { - public static readonly DependencyProperty SourceUrlProperty = - DependencyProperty.RegisterAttached( - "SourceUrl", typeof(string), typeof(RtspImageBehavior), - new PropertyMetadata(null, OnSourceUrlChanged)); - - public static void SetSourceUrl(DependencyObject obj, string value) => obj.SetValue(SourceUrlProperty, value); - public static string GetSourceUrl(DependencyObject obj) => (string)obj.GetValue(SourceUrlProperty); - - private static readonly DependencyProperty PlayerProperty = - DependencyProperty.RegisterAttached( - "Player", typeof(ImageRtspAdapter), typeof(RtspImageBehavior), new PropertyMetadata(null)); - - private static void OnSourceUrlChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is not System.Windows.Controls.Image img) return; - - // 1) 기존 플레이어 정리 - if (img.GetValue(PlayerProperty) is ImageRtspAdapter old) - { - old.Dispose(); - img.ClearValue(PlayerProperty); - img.Source = null; - img.Visibility = Visibility.Collapsed; - } - - // 2) 신규 URL 연결 - var url = e.NewValue as string; - if (string.IsNullOrWhiteSpace(url)) return; - - var player = new ImageRtspAdapter(img, url); - img.SetValue(PlayerProperty, player); - player.Start(); - - img.Unloaded -= OnUnloaded; - img.Unloaded += OnUnloaded; - } - - private static void OnUnloaded(object sender, RoutedEventArgs e) - { - if (sender is System.Windows.Controls.Image img && img.GetValue(PlayerProperty) is ImageRtspAdapter p) - { - p.Dispose(); - img.ClearValue(PlayerProperty); - } - if (sender is System.Windows.Controls.Image i) i.Unloaded -= OnUnloaded; - } - } - - public sealed class ImageRtspAdapter : IDisposable - { - private readonly System.Windows.Controls.Image _img; - private readonly string _url; - - private Thread? _videoThread; - private Thread? _renderThread; - private CancellationTokenSource? _videoCts; - private CancellationTokenSource? _renderCts; - - private readonly Queue _frameQueue = new(); // lock 불필요 - private volatile bool _disposed; - - private object _lockObject = new(); - - public ImageRtspAdapter(System.Windows.Controls.Image img, string url) - { - _img = img; - _url = url; - } - - public void Start() - { - _videoCts = new CancellationTokenSource(); - _renderCts = new CancellationTokenSource(); - - _videoThread = new Thread(OpenMedia) { IsBackground = true, Name = "RTSP-Decode" }; - _renderThread = new Thread(RenderLoop) { IsBackground = true, Name = "RTSP-Render" }; - - _videoThread.Start(); - _renderThread.Start(); - } - - private unsafe void OpenMedia() - { - try - { - using (var sd = new StreamDecoder(_url)) - using (var vfc = new VideoFrameConverter(sd.FrameSize, sd.PixelFormat, sd.FrameSize, AVPixelFormat.AV_PIX_FMT_BGR24)) - { - while (!_videoCts!.IsCancellationRequested) - { - if (!sd.TryDecodeNextFrame(out var frame)) - { - // 디코드 실패: 너무 바쁘지 않게 살짝 쉼 - Thread.Sleep(2); - continue; - } - - try - { - AVFrame convertedFrame = vfc.Convert(frame); - _frameQueue.Enqueue(convertedFrame); // 큐 삽입 (길이 제한 적용) - } - catch (Exception ex) - { - Debug.WriteLine("Decode/Enqueue error: " + ex.Message); - } - } - } - } - catch (Exception ex) - { - Debug.WriteLine("OpenMedia() : " + ex); - } - } - - private unsafe void RenderLoop() - { - try - { - while (!_renderCts!.IsCancellationRequested) - { - if (_frameQueue.Count > 0) - { - AVFrame convertedFrame; - lock (_lockObject) - { - if (_frameQueue.Count > 0) - { - convertedFrame = _frameQueue.Dequeue(); - } - else - { - continue; - } - } - try - { - Bitmap bitmap = new Bitmap(convertedFrame.width, convertedFrame.height, convertedFrame.linesize[0], System.Drawing.Imaging.PixelFormat.Format24bppRgb, (IntPtr)convertedFrame.data[0]); - - var src = CreateBitmapSource(bitmap); - src.Freeze(); - - _img.Dispatcher.BeginInvoke(new Action(() => - { - if (_disposed) { return; } - _img.Source = src; - if (_img.Visibility != Visibility.Visible) - _img.Visibility = Visibility.Visible; - }), DispatcherPriority.Render); - } - catch (Exception ex) - { - Debug.WriteLine("RenderLoop() : " + ex.Message); - } - } - } - } - catch (OperationCanceledException) { /* 정상 종료 */ } - finally - { - //ClearQueue(); - } - } - - private static BitmapSource CreateBitmapSource(Bitmap bitmap) - { - // GDI 핸들 사용 X. LockBits → BitmapSource.Create 경로만 사용. - var rect = new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height); - var data = bitmap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadOnly, bitmap.PixelFormat); - try - { - // Format24bppRgb ↔ PixelFormats.Bgr24 매칭 - return BitmapSource.Create( - data.Width, data.Height, 96, 96, - PixelFormats.Bgr24, null, - data.Scan0, data.Stride * data.Height, data.Stride); - } - finally - { - bitmap.UnlockBits(data); - } - } - - public void Dispose() - { - if (_disposed) return; - _disposed = true; - - try { _videoCts?.Cancel(); } catch { } - try { _renderCts?.Cancel(); } catch { } - - try { if (_videoThread?.IsAlive == true) _videoThread.Join(300); } catch { } - try { if (_renderThread?.IsAlive == true) _renderThread.Join(300); } catch { } - - _frameQueue.Clear(); - - _videoCts?.Dispose(); - _renderCts?.Dispose(); - } - } -} diff --git a/SmartAquaViewer/Controls/FFPlayerControl.xaml b/SmartAquaViewer/Controls/FFPlayerControl.xaml index 7ee0277..ea2f968 100644 --- a/SmartAquaViewer/Controls/FFPlayerControl.xaml +++ b/SmartAquaViewer/Controls/FFPlayerControl.xaml @@ -12,7 +12,7 @@ - + @@ -28,7 +28,7 @@ FontSize="17" Foreground="#414961"> - - + @@ -16,39 +17,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/SmartAquaViewer/ViewModel/CCTVViewModel.cs b/SmartAquaViewer/ViewModel/CCTVViewModel.cs index 482a7d1..caf58f4 100644 --- a/SmartAquaViewer/ViewModel/CCTVViewModel.cs +++ b/SmartAquaViewer/ViewModel/CCTVViewModel.cs @@ -15,6 +15,8 @@ namespace SmartAquaViewer.ViewModel { public List CCTVInfoList { get; } = new List(); + public ObservableCollection PlayerVMs { get; } = new(); + private int _columnCount; public int ColumnCount { @@ -107,6 +109,15 @@ namespace SmartAquaViewer.ViewModel Status = "Active" } }; + + BuildPlayers(CCTVInfoList); + } + + public void BuildPlayers(IEnumerable infos) + { + PlayerVMs.Clear(); + foreach (var info in infos) + PlayerVMs.Add(new FFPlayerViewModel(info)); } public event PropertyChangedEventHandler? PropertyChanged; diff --git a/SmartAquaViewer/ViewModel/FFPlayerViewModel.cs b/SmartAquaViewer/ViewModel/FFPlayerViewModel.cs index 89d6fae..47a7176 100644 --- a/SmartAquaViewer/ViewModel/FFPlayerViewModel.cs +++ b/SmartAquaViewer/ViewModel/FFPlayerViewModel.cs @@ -41,9 +41,14 @@ namespace SmartAquaViewer.ViewModel private readonly Queue _frameQueue = new(); // lock 불필요 private volatile bool _disposed; + private readonly Dispatcher _ui; + public FFPlayerViewModel(CCTVInfo cctvInfo) { CCTVInfo = cctvInfo; + _ui = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher; // UI 디스패처 보관 + + StartMedia(CCTVInfo.RtspUrl!); } public void StartMedia(string rtspURL) @@ -119,7 +124,7 @@ namespace SmartAquaViewer.ViewModel var src = CreateBitmapSource(bitmap); src.Freeze(); - CurrentFrame!.Dispatcher.BeginInvoke(new Action(() => + _ui.BeginInvoke(new Action(() => { if (_disposed) return; CurrentFrame = src;