fix: CCTV 재생 기능 방식 수정

prototype
HyungJune Kim 10 months ago
parent 0396389ff9
commit a47bf94ee7

@ -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<AVFrame> _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();
}
}
}

@ -12,7 +12,7 @@
<RowDefinition Height="24"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Border Grid.RowSpan="2" x:Name="bdrNoSignalContainer">
<Border Grid.RowSpan="2" x:Name="bdrNoSignalContainer" Visibility="Collapsed">
<Border.BorderBrush>
<SolidColorBrush Color="#394861" Opacity="0.4"></SolidColorBrush>
</Border.BorderBrush>
@ -28,7 +28,7 @@
FontSize="17" Foreground="#414961"></TextBlock>
</Grid>
</Border>
<Image x:Name="imgPlayer" Grid.RowSpan="2" Stretch="Fill"
<Image x:Name="imgPlayer" Grid.RowSpan="2" Stretch="Fill" Source="{Binding CurrentFrame}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
<TextBlock HorizontalAlignment="Left" Grid.RowSpan="2" Margin="20, 5, 5, 0" x:Name="lblCCTVID"
FontFamily="Verna" FontSize="15" FontWeight='Bold' Foreground="White"

@ -12,6 +12,8 @@ using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using SmartAquaViewer.Model;
using SmartAquaViewer.ViewModel;
namespace SmartAquaViewer.Controls
{

@ -5,10 +5,11 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.View"
xmlns:classes="clr-namespace:SmartAquaViewer.Classes"
xmlns:controls="clr-namespace:SmartAquaViewer.Controls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Background="Transparent">
<ItemsControl ItemsSource="{Binding CCTVInfoList}">
<ItemsControl ItemsSource="{Binding PlayerVMs}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid x:Name="ugrdFFPlayer" Rows="{Binding RowCount}" Columns="{Binding ColumnCount}" />
@ -16,39 +17,7 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border>
<Grid Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.RowSpan="2" BorderThickness="1" x:Name="bdrNoSignalContainer" Visibility="Collapsed">
<Border.BorderBrush>
<SolidColorBrush Color="#394861" Opacity="0.4"></SolidColorBrush>
</Border.BorderBrush>
<Border.Background>
<SolidColorBrush Color="#22262D" Opacity="0.4"></SolidColorBrush>
</Border.Background>
<Grid VerticalAlignment="Center" HorizontalAlignment="Center" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Label Name="lblVideoStatus" Content="연결 없음" Grid.Row="0" FontSize="17" Foreground="#414961" HorizontalContentAlignment="Center"></Label>
</Grid>
</Border>
<Image x:Name="imgPlayer" Visibility="Collapsed" Grid.RowSpan="2" Stretch="Fill" Margin="0"
classes:RtspImageBehavior.SourceUrl="{Binding RtspUrl}"/>
<TextBlock Text="{Binding DeviceId}" Margin="20 5 5 0" x:Name="lblCCTVId"
FontFamily="{StaticResource SCDream6}" FontSize="15" Foreground="White"/>
<Grid Grid.Row="1">
<StackPanel VerticalAlignment="Bottom" Orientation="Vertical">
<TextBlock Text="{Binding DeviceName}" HorizontalAlignment="Right" Margin="20, 5, 5, 0" x:Name="lblCCTVName"
FontFamily="{StaticResource SCDream6}" FontSize="15" Foreground="White"/>
</StackPanel>
</Grid>
</Grid>
</Border>
<controls:FFPlayerControl/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

@ -15,6 +15,8 @@ namespace SmartAquaViewer.ViewModel
{
public List<CCTVInfo> CCTVInfoList { get; } = new List<CCTVInfo>();
public ObservableCollection<FFPlayerViewModel> PlayerVMs { get; } = new();
private int _columnCount;
public int ColumnCount
{
@ -107,6 +109,15 @@ namespace SmartAquaViewer.ViewModel
Status = "Active"
}
};
BuildPlayers(CCTVInfoList);
}
public void BuildPlayers(IEnumerable<CCTVInfo> infos)
{
PlayerVMs.Clear();
foreach (var info in infos)
PlayerVMs.Add(new FFPlayerViewModel(info));
}
public event PropertyChangedEventHandler? PropertyChanged;

@ -41,9 +41,14 @@ namespace SmartAquaViewer.ViewModel
private readonly Queue<AVFrame> _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;

Loading…
Cancel
Save