feat: CCTV 기능 개발 진행중

hhsung_work
HyungJune Kim 10 months ago
parent 1be29e82a0
commit 0a4900689e

@ -5,8 +5,6 @@ VisualStudioVersion = 17.13.35931.197
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartAquaViewer", "SmartAquaViewer\SmartAquaViewer.csproj", "{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FFMPEGPlayer", "..\FFMPEGPlayer\FFMPEGPlayer\FFMPEGPlayer.csproj", "{1A33B46E-B95E-491A-B7A1-6D9EDCA40FD4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -23,14 +21,6 @@ Global
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Release|Any CPU.Build.0 = Release|Any CPU
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Release|x64.ActiveCfg = Release|x64
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Release|x64.Build.0 = Release|x64
{1A33B46E-B95E-491A-B7A1-6D9EDCA40FD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A33B46E-B95E-491A-B7A1-6D9EDCA40FD4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A33B46E-B95E-491A-B7A1-6D9EDCA40FD4}.Debug|x64.ActiveCfg = Debug|x64
{1A33B46E-B95E-491A-B7A1-6D9EDCA40FD4}.Debug|x64.Build.0 = Debug|x64
{1A33B46E-B95E-491A-B7A1-6D9EDCA40FD4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A33B46E-B95E-491A-B7A1-6D9EDCA40FD4}.Release|Any CPU.Build.0 = Release|Any CPU
{1A33B46E-B95E-491A-B7A1-6D9EDCA40FD4}.Release|x64.ActiveCfg = Release|x64
{1A33B46E-B95E-491A-B7A1-6D9EDCA40FD4}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

@ -0,0 +1,50 @@
<UserControl x:Class="SmartAquaViewer.Controls.FFPlayerControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.Controls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Border Margin="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" BorderThickness="1">
<Grid Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="24"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Border Grid.RowSpan="2" x:Name="bdrNoSignalContainer">
<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>
</Grid>
</Border>
<Image x:Name="imgPlayer" Grid.RowSpan="2" Stretch="Fill"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Source="{Binding CurrentFrame}"/>
<Label HorizontalAlignment="Left" Grid.RowSpan="2" Margin="20, 5, 5, 0" x:Name="lblCCTVID"
FontFamily="Verna" FontSize="15" FontWeight='Bold' Foreground="White"
Content="{Binding CCTVInfo.DeviceId}"/>
<Grid x:Name="grdTopMenuBar" Grid.Row="0" Background="Black" Opacity="0.5" Visibility="Hidden" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" WindowChrome.IsHitTestVisibleInChrome="True" >
</Grid>
<!--<StackPanel Name="spTopMenuBar" Visibility="Hidden" Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Right">
<Image Name="imgMaximize" Source="../Images/maximize.png" Width="24" Margin="1, 5, 0, 5" MouseLeftButtonUp="imgMaximize_MouseUp" Opacity="0.7" MouseEnter="imgTopmenu_MouseEnter" MouseLeave="imgTopmenu_MouseLeave"/>
</StackPanel>-->
<Grid Grid.Row="1">
<StackPanel VerticalAlignment="Bottom" Orientation="Vertical">
<Label HorizontalAlignment="Right" Margin="20, 5, 5, 0" x:Name="lblCCTVName"
FontFamily="Verna" FontSize="15" FontWeight='Bold' Foreground="White"
Content="{Binding CCTVInfo.DeviceName}"/>
</StackPanel>
</Grid>
</Grid>
</Border>
</UserControl>

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SmartAquaViewer.Controls
{
/// <summary>
/// FFPlayerControl.xaml에 대한 상호 작용 논리
/// </summary>
public partial class FFPlayerControl : UserControl
{
public FFPlayerControl()
{
InitializeComponent();
}
}
}

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using FFmpeg.AutoGen;
namespace SmartAquaViewer.Helper.FFHelper
{
internal static class Helper
{
public static unsafe string av_strerror(int error)
{
var bufferSize = 1024;
var buffer = stackalloc byte[bufferSize];
ffmpeg.av_strerror(error, buffer, (ulong)bufferSize);
var message = Marshal.PtrToStringAnsi((IntPtr)buffer);
return message;
}
public static int ThrowExceptionIfError(this int error)
{
if (error < 0) throw new ApplicationException(av_strerror(error));
return error;
}
}
}

@ -0,0 +1,368 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using FFmpeg.AutoGen;
using System.Windows;
namespace SmartAquaViewer.Helper.FFHelper
{
public sealed unsafe class StreamDecoder : IDisposable
{
public delegate void ThreadStoppedHandler(object obj);
public static event ThreadStoppedHandler hThreadStoppedHandler;
private readonly AVFormatContext* _pFormatContext;
public readonly AVFrame* _pFrame;
private readonly AVPacket* _pPacket;
AVCodec* codec;
public string CodecName { get; }
public Size FrameSize { get; }
public AVPixelFormat PixelFormat { get; }
public int videoStreamIndex { get; }
public AVCodecContext* _vcodecContext { get; }
public bool _isContextNotInitialized = false;
public double _fps;
public StreamDecoder(string url)
{
//Application.Current.Dispatcher.Invoke(() =>
//{
// ProgressWindow.GetInstance().Show();
//});
lock (this)
{
AVFormatContext* pFormatContext = null;
AVCodecContext* vcodecContext = null;
AVDictionary* options = null;
AVStream* videoStream = null;
try
{
ffmpeg.av_log_set_level(ffmpeg.AV_LOG_DEBUG);
_pFormatContext = ffmpeg.avformat_alloc_context();
if (_pFormatContext == null)
{
throw new InvalidOperationException("Failed to allocate format context.");
}
pFormatContext = _pFormatContext;
// Get the codec context
_vcodecContext = ffmpeg.avcodec_alloc_context3(codec);
if (_vcodecContext == null)
{
// Out of memory
ffmpeg.avformat_close_input(&pFormatContext);
}
ffmpeg.av_dict_set(&options, "stimeout", "600000000", 0); // 60 seconds timeout
ffmpeg.av_dict_set(&options, "reconnect", "1", 0);
ffmpeg.av_dict_set(&options, "reconnect_at_eof", "1", 0);
ffmpeg.av_dict_set(&options, "reconnect_streamed", "1", 0);
ffmpeg.av_dict_set(&options, "rtsp_transport", "tcp", 0);
int ret = ffmpeg.avformat_open_input(&pFormatContext, url, null, &options).ThrowExceptionIfError();
if (options != null)
{
ffmpeg.av_dict_free(&options);
}
ret = ffmpeg.avformat_find_stream_info(_pFormatContext, null).ThrowExceptionIfError();
for (int i = 0; i < pFormatContext->nb_streams; i++)
{
if (pFormatContext->streams[i]->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
{
videoStream = pFormatContext->streams[i];
break;
}
}
if (videoStream == null)
{
Console.WriteLine("Failed to find video stream");
return;
}
AVRational frameRate = videoStream->avg_frame_rate;
_fps = (double)frameRate.num / frameRate.den;
videoStreamIndex = ffmpeg.av_find_best_stream(_pFormatContext, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, null, 0);
int context = ffmpeg.avcodec_parameters_to_context(_vcodecContext, _pFormatContext->streams[videoStreamIndex]->codecpar).ThrowExceptionIfError();
if (context < 0)
{
//Log4NetManager.GetLog().Error("StreamDecoder() : avcodec_parameters_to_context Error");
return;
}
if (videoStreamIndex >= 0)
{
AVCodecContext* avctx = OpenStream(_vcodecContext);
if (avctx == null)
{
throw new InvalidOperationException("Failed to open codec context.");
}
FrameSize = new Size(avctx->width, avctx->height);
PixelFormat = avctx->pix_fmt;
}
else
{
throw new InvalidOperationException("No video stream found.");
}
_pPacket = ffmpeg.av_packet_alloc();
if (_pPacket == null)
{
throw new InvalidOperationException("Failed to allocate packet.");
}
_pFrame = ffmpeg.av_frame_alloc();
if (_pFrame == null)
{
throw new InvalidOperationException("Failed to allocate frame.");
}
}
catch (Exception ex)
{
_isContextNotInitialized = true;
//Log4NetManager.GetLog().Error("StreamDecoder() : " + ex.Message);
MessageBox.Show("StreamDecoder() : " + ex.Message);
if (_pFrame != null)
{
ffmpeg.av_frame_unref(_pFrame);
ffmpeg.av_free(_pFrame);
}
if (_pPacket != null)
{
ffmpeg.av_packet_unref(_pPacket);
ffmpeg.av_free(_pPacket);
}
if (_vcodecContext != null)
{
vcodecContext = _vcodecContext;
ffmpeg.avcodec_close(vcodecContext);
ffmpeg.avcodec_free_context(&vcodecContext);
}
if (_pFormatContext != null)
{
ffmpeg.avformat_close_input(&pFormatContext);
ffmpeg.avformat_free_context(pFormatContext);
}
//Log4NetManager.GetLog().Error("StreamDecoder() : " + ex.Message);
}
finally
{
if (options != null)
{
ffmpeg.av_dict_free(&options);
}
//Application.Current.Dispatcher.Invoke(() =>
//{
// ProgressWindow.GetInstance().Close();
//});
}
}
}
private AVCodecContext* OpenStream(AVCodecContext* avctx)
{
AVCodec* codec = ffmpeg.avcodec_find_decoder(avctx->codec_id);
if (codec == null) throw new InvalidOperationException("No codec could be found.");
avctx->codec_id = codec->id;
avctx->lowres = 0;
if (avctx->lowres > codec->max_lowres)
avctx->lowres = codec->max_lowres;
avctx->idct_algo = ffmpeg.FF_IDCT_AUTO;
avctx->error_concealment = 3;
ffmpeg.avcodec_open2(avctx, codec, null).ThrowExceptionIfError();
return avctx;
}
//20240326 LHB - 원본. 안될경우 이 주석을 해제하고 아래의 함수와 교체하세요
//public void Dispose()
//{
// ffmpeg.av_frame_unref(_pFrame);
// ffmpeg.av_free(_pFrame);
// ffmpeg.av_packet_unref(_pPacket);
// ffmpeg.av_free(_pPacket);
// ffmpeg.avcodec_close(_vcodecContext);
// //ffmpeg.avcodec_close(acodecContext);
// var pFormatContext = _pFormatContext;
// ffmpeg.avformat_close_input(&pFormatContext);
//}
public void Dispose()
{
lock (this)
{
if (_pFrame != null)
{
ffmpeg.av_frame_unref(_pFrame);
ffmpeg.av_free(_pFrame);
}
if (_pPacket != null)
{
ffmpeg.av_packet_unref(_pPacket);
ffmpeg.av_free(_pPacket);
}
if (_vcodecContext != null && !_isContextNotInitialized)
{
ffmpeg.avcodec_close(_vcodecContext);
}
//ffmpeg.avcodec_close(acodecContext);
if (_pFormatContext != null && !_isContextNotInitialized)
{
var pFormatContext = _pFormatContext;
ffmpeg.avformat_close_input(&pFormatContext);
ffmpeg.avformat_free_context(pFormatContext);
}
}
}
//20240326 LHB - 원본. 안될경우 이 주석을 해제하고 아래의 함수와 교체하세요
//public bool TryDecodeNextFrame(out AVFrame frame)
//{
// ffmpeg.av_frame_unref(_pFrame);
// int error = 0;
// do
// {
// try
// {
// do
// {
// error = ffmpeg.av_read_frame(_pFormatContext, _pPacket);
// if (error == ffmpeg.AVERROR_EOF)
// {
// frame = *_pFrame;
// return false;
// }
// error.ThrowExceptionIfError();
// } while (_pPacket->stream_index != videoStreamIndex);
// ffmpeg.avcodec_send_packet(_vcodecContext, _pPacket).ThrowExceptionIfError();
// }
// catch (Exception ex)
// {
// Log4NetManager.GetLog().Error("TryDecodeNextFrame() : " + ex.Message);
// MessageBox.Show("TryDecodeNextFrame() : " + ex.Message);
// }
// finally
// {
// ffmpeg.av_packet_unref(_pPacket);
// }
// error = ffmpeg.avcodec_receive_frame(_vcodecContext, _pFrame);
// } while (error == ffmpeg.AVERROR(ffmpeg.EAGAIN));
// error.ThrowExceptionIfError();
// frame = *_pFrame;
// return true;
//}
public bool TryDecodeNextFrame(out AVFrame frame)
{
ffmpeg.av_frame_unref(this._pFrame);
int error;
// repeated-try avcodec_receive_frame until enough packets haven been send
while (true)
{
// try read frame; maybe last avcodec_send_packet resulted in multiple frames to be read; will respond with EAGAIN if more packets are needed
error = ffmpeg.avcodec_receive_frame(this._vcodecContext, this._pFrame);
if (error != ffmpeg.AVERROR(ffmpeg.EAGAIN))
{
frame = *this._pFrame;
if (error == ffmpeg.AVERROR_EOF)
return false;
error.ThrowExceptionIfError();
return true;
}
try
{
// feed all stream-matching packets to decoder
while (true)
{
error = ffmpeg.av_read_frame(this._pFormatContext, this._pPacket);
if (error == ffmpeg.AVERROR_EOF)
{
// no more packets to read -> trigger draining/flushing of remaining frames
ffmpeg.avcodec_send_packet(this._vcodecContext, null).ThrowExceptionIfError();
break; // don't throw error, just exit packet-feed-loop
}
error.ThrowExceptionIfError();
if (this._pPacket->stream_index == this.videoStreamIndex)
{
var sendPacketResult = ffmpeg.avcodec_send_packet(this._vcodecContext, this._pPacket);
if (sendPacketResult == 0)
break;
// no reason to abort/crash (it was just an invalid packet from demuxer) -> just retrieve and feed next packet; most of time the codec will resume without problems
var errorMsg = Helper.av_strerror(sendPacketResult);
Trace.TraceError(errorMsg);
//Log4NetManager.GetLog().Error("TryDecodeNextFrame()" + " : av_strerror 오류 " + errorMsg);
}
// unref all packets, no matter if sent to avcodec or ignored
ffmpeg.av_packet_unref(this._pPacket);
}
}
catch (Exception ex)
{
Debug.WriteLine("TryDecodeNextFrame()" + ex.Message);
if (ex.Message.Equals("End of file"))
{
//Log4NetManager.GetLog().Error("TryDecodeNextFrame()" + " : " + ex.Message);
}
//쓰레드가 망함,
}
finally
{
ffmpeg.av_packet_unref(this._pPacket);
}
}
}
public IReadOnlyDictionary<string, string> GetContextInfo()
{
AVDictionaryEntry* tag = null;
var result = new Dictionary<string, string>();
while ((tag = ffmpeg.av_dict_get(_pFormatContext->metadata, "", tag, ffmpeg.AV_DICT_IGNORE_SUFFIX)) != null)
{
var key = Marshal.PtrToStringAnsi((IntPtr)tag->key);
var value = Marshal.PtrToStringAnsi((IntPtr)tag->value);
result.Add(key, value);
}
return result;
}
}
}

@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Media;
using FFmpeg.AutoGen;
namespace SmartAquaViewer.Helper.FFHelper
{
public sealed unsafe class VideoFrameConverter : IDisposable
{
private readonly IntPtr _convertedFrameBufferPtr;
private readonly System.Windows.Size _destinationSize;
private readonly byte_ptrArray4 _dstData;
private readonly int_array4 _dstLinesize;
private readonly SwsContext* _pConvertContext;
private bool _disposed = false;
public VideoFrameConverter(System.Windows.Size sourceSize, AVPixelFormat sourcePixelFormat,
System.Windows.Size destinationSize, AVPixelFormat destinationPixelFormat)
{
try
{
_destinationSize = destinationSize;
_pConvertContext = ffmpeg.sws_getContext(
(int)sourceSize.Width,
(int)sourceSize.Height,
sourcePixelFormat,
(int)destinationSize.Width,
(int)destinationSize.Height,
destinationPixelFormat,
ffmpeg.SWS_FAST_BILINEAR, null, null, null);
if (_pConvertContext == null)
throw new ApplicationException("Could not initialize the conversion context.");
var convertedFrameBufferSize = ffmpeg.av_image_get_buffer_size(destinationPixelFormat,
(int)destinationSize.Width, (int)destinationSize.Height, 1);
_convertedFrameBufferPtr = Marshal.AllocHGlobal(convertedFrameBufferSize);
_dstData = new byte_ptrArray4();
_dstLinesize = new int_array4();
ffmpeg.av_image_fill_arrays(
ref _dstData,
ref _dstLinesize,
(byte*)_convertedFrameBufferPtr,
destinationPixelFormat,
(int)destinationSize.Width,
(int)destinationSize.Height, 1);
}
catch (Exception ex)
{
Debug.WriteLine("VideoFrameConverter() : " + ex.Message);
}
}
public void Dispose()
{
if (!_disposed)
{
if (_convertedFrameBufferPtr != IntPtr.Zero)
Marshal.FreeHGlobal(_convertedFrameBufferPtr);
if (_pConvertContext != null)
ffmpeg.sws_freeContext(_pConvertContext);
_disposed = true;
}
}
public AVFrame Convert(AVFrame sourceFrame)
{
ffmpeg.sws_scale(_pConvertContext,
sourceFrame.data, sourceFrame.linesize, 0, sourceFrame.height, _dstData, _dstLinesize);
var data = new byte_ptrArray8();
data.UpdateFrom(_dstData);
var linesize = new int_array8();
linesize.UpdateFrom(_dstLinesize);
return new AVFrame
{
data = data,
linesize = linesize,
width = (int)_destinationSize.Width,
height = (int)_destinationSize.Height
};
}
public unsafe Bitmap DeepCopyFrame(AVFrame sourceFrame)
{
if (_disposed)
throw new ObjectDisposedException(nameof(VideoFrameConverter));
AVFrame* dstFramePtr = ffmpeg.av_frame_alloc();
if (dstFramePtr == null)
{
throw new Exception("Failed to allocate destination frame");
}
try
{
dstFramePtr->format = (int)AVPixelFormat.AV_PIX_FMT_BGR24;
dstFramePtr->width = sourceFrame.width;
dstFramePtr->height = sourceFrame.height;
int ret = ffmpeg.av_frame_get_buffer(dstFramePtr, 32);
if (ret < 0)
{
throw new Exception($"Failed to allocate buffer for destination frame: {Helper.av_strerror(ret)}");
}
ret = ffmpeg.av_frame_make_writable(dstFramePtr);
if (ret < 0)
{
throw new Exception($"Failed to make frame writable: {Helper.av_strerror(ret)}");
}
ffmpeg.sws_scale(_pConvertContext,
sourceFrame.data, sourceFrame.linesize, 0, sourceFrame.height, dstFramePtr->data, dstFramePtr->linesize);
ret = ffmpeg.av_frame_copy_props(dstFramePtr, &sourceFrame);
if (ret < 0)
{
throw new Exception($"Failed to copy frame properties: {Helper.av_strerror(ret)}");
}
dstFramePtr->pts = sourceFrame.pts;
dstFramePtr->pkt_dts = sourceFrame.pkt_dts;
dstFramePtr->best_effort_timestamp = sourceFrame.best_effort_timestamp;
var dstDataArr = new byte_ptrArray8();
dstDataArr.UpdateFrom(dstFramePtr->data);
var dstLinesizeArr = new int_array8();
dstLinesizeArr.UpdateFrom(dstFramePtr->linesize);
AVFrame frame = new AVFrame
{
data = dstDataArr,
linesize = dstLinesizeArr,
width = dstFramePtr->width,
height = dstFramePtr->height,
format = (int)AVPixelFormat.AV_PIX_FMT_BGR24,
pts = dstFramePtr->pts,
pkt_dts = dstFramePtr->pkt_dts,
best_effort_timestamp = dstFramePtr->best_effort_timestamp
};
Bitmap bitmap = new Bitmap(
dstFramePtr->width,
dstFramePtr->height,
dstFramePtr->linesize[0],
System.Drawing.Imaging.PixelFormat.Format24bppRgb,
(IntPtr)dstFramePtr->data[0]
);
// Bitmap의 데이터를 따로 복사
Bitmap finalBitmap = new Bitmap(bitmap);
bitmap.Dispose(); // 중간 비트맵 해제
return finalBitmap; // 데이터 복사된 최종 비트맵 리턴
}
catch (Exception ex)
{
//Log4NetManager.GetLog().Error($"Exception during frame copy: {ex.Message}");
throw;
}
finally
{
// 포인터 유효성 검사 후 안전하게 해제
if (dstFramePtr != null)
{
ffmpeg.av_frame_unref(dstFramePtr); // 참조 해제
ffmpeg.av_frame_free(&dstFramePtr); // 메모리 해제
}
}
}
}
}

@ -7,6 +7,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<Platforms>AnyCPU;x64</Platforms>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
@ -36,6 +37,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="FFmpeg.AutoGen" Version="7.1.1" />
<PackageReference Include="MaterialDesignColors" Version="5.2.1" />
<PackageReference Include="MaterialDesignThemes" Version="5.2.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
@ -51,6 +53,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="OxyPlot.Wpf" Version="2.2.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-rc.1.efcore.9.0.0" />
<PackageReference Include="System.Drawing.Common" Version="9.0.8" />
</ItemGroup>
<ItemGroup>

@ -6,16 +6,18 @@
xmlns:local="clr-namespace:SmartAquaViewer.View"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<ItemsControl ItemsSource="{Binding WaterQualityList}">
<Grid Background="#243851">
<ItemsControl ItemsSource="{Binding CCTVInfoList}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="{Binding RowCount}" Columns="{Binding ColumnCount}" />
<UniformGrid x:Name="ugrdFFPlayer" Rows="{Binding RowCount}" Columns="{Binding ColumnCount}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="Green" Margin="2" />
<Border/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</UserControl>

@ -1,5 +1,8 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -20,9 +23,65 @@ namespace SmartAquaViewer.View
/// </summary>
public partial class CCTVView : UserControl
{
private Thread _videoThread;
private Thread _renderingThread;
private CancellationTokenSource _videoCancellationTokenSource;
private CancellationTokenSource _renderingCancellationTokenSource;
private bool _stopThread = false;
private ConcurrentQueue<Bitmap> _frameQueue = new ConcurrentQueue<Bitmap>();
private readonly object _lockObject = new object();
public CCTVView()
{
InitializeComponent();
}
public void StartMedia(string rtspURL)
{
ClosePlayer();
_stopThread = false;
_videoThread = new Thread(new ThreadStart(OpenMedia));
_renderingThread = new Thread(new ThreadStart(RenderImage));
_videoThread.Priority = ThreadPriority.Highest; // 우선순위 설정
_videoThread.Start();
_renderingThread.Start();
}
private void OpenMedia()
{
throw new NotImplementedException();
}
private void RenderImage()
{
throw new NotImplementedException();
}
public void ClosePlayer()
{
_stopThread = true;
lock (_lockObject)
{
if (_videoCancellationTokenSource != null)
{
_videoCancellationTokenSource.Cancel();
Debug.WriteLine("ClosePlayer(): videoThread 종료함");
}
if (_renderingCancellationTokenSource != null)
{
_renderingCancellationTokenSource.Cancel();
Debug.WriteLine("ClosePlayer(): RenderingThread 종료함");
}
}
}
}
}

@ -13,7 +13,7 @@ namespace SmartAquaViewer.ViewModel
{
public class CCTVViewModel : INotifyPropertyChanged
{
public List<WaterQualityVO> WaterQualityList { get; } = new List<WaterQualityVO>();
public List<CCTVInfo> CCTVInfoList { get; } = new List<CCTVInfo>();
private int _columnCount;
public int ColumnCount
@ -48,7 +48,51 @@ namespace SmartAquaViewer.ViewModel
ColumnCount = 4; // Default value
RowCount = 2; // Default value
WaterQualityList = Datas.GetWaterQualityVO().Take(5).ToList();
CCTVInfoList = new List<CCTVInfo>()
{
new()
{
DeviceId = "000001",
DeviceName = "CCTV 1",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-211",
Status = "Active"
},
new()
{
DeviceId = "000002",
DeviceName = "CCTV 2",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-212",
Status = "Active"
},
new()
{
DeviceId = "000003",
DeviceName = "CCTV 3",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-211",
Status = "Active"
},
new()
{
DeviceId = "000004",
DeviceName = "CCTV 4",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-212",
Status = "Active"
},
new()
{
DeviceId = "000005",
DeviceName = "CCTV 5",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-211",
Status = "Active"
},
new()
{
DeviceId = "000006",
DeviceName = "CCTV 6",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-212",
Status = "Active"
}
};
}
public event PropertyChangedEventHandler? PropertyChanged;

@ -0,0 +1,218 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Threading;
using FFmpeg.AutoGen;
using SmartAquaViewer.Helper.FFHelper;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.ViewModel
{
public class FFPlayerViewModel : INotifyPropertyChanged
{
public CCTVInfo CCTVInfo { get; set; }
private ImageSource? _currentFrame;
public ImageSource? CurrentFrame
{
get => _currentFrame;
set { _currentFrame = value; OnPropertyChanged(); }
}
private readonly object _lockObject = new object();
private Thread _videoThread;
private Thread _renderingThread;
private CancellationTokenSource _videoCancellationTokenSource;
private CancellationTokenSource _renderingCancellationTokenSource;
private bool _stopThread = false;
private ConcurrentQueue<Bitmap> _frameQueue = new ConcurrentQueue<Bitmap>();
public FFPlayerViewModel(CCTVInfo cctvInfo)
{
CCTVInfo = cctvInfo;
}
public void StartMedia(string rtspURL)
{
ClosePlayer();
_stopThread = false;
_videoThread = new Thread(new ThreadStart(OpenMedia));
_renderingThread = new Thread(new ThreadStart(RenderImage));
_videoThread.Priority = ThreadPriority.Highest; // 우선순위 설정
_videoThread.Start();
_renderingThread.Start();
}
private unsafe void OpenMedia()
{
int failCount = 0;
int frameCount = 0;
_frameQueue = new ConcurrentQueue<Bitmap>();
_videoCancellationTokenSource = new CancellationTokenSource();
try
{
using (StreamDecoder sd = new StreamDecoder(CCTVInfo.RtspUrl))
{
using (var vfc = new VideoFrameConverter(sd.FrameSize, sd.PixelFormat, sd.FrameSize, AVPixelFormat.AV_PIX_FMT_BGR24))
{
while (!_videoCancellationTokenSource.Token.IsCancellationRequested)
{
bool decodeSuccess = sd.TryDecodeNextFrame(out var frame);
//if (!decodeSuccess)
//{
// failCount++;
// HandleDecodeFailure(ref failCount, maxFailCount);
// continue; // 다음 반복으로 이동
//}
// 디코딩 성공 시 프레임 처리
HandleDecodedFrame(frame, vfc, ref frameCount);
failCount = 0; // 실패 카운트 초기화
}
}
}
}
catch (Exception ex)
{
//Log4NetManager.GetLog().Error("OpenMedia() : " + ex.Message);
Debug.WriteLine("OpenMedia() : " + ex.Message.ToString());
}
finally
{
if (!_stopThread || !_videoCancellationTokenSource.Token.IsCancellationRequested)
{
Debug.WriteLine($"Restarting media");
StartMedia(CCTVInfo.RtspUrl);
}
}
}
private void HandleDecodedFrame(AVFrame frame, VideoFrameConverter vfc, ref int frameCount)
{
Bitmap convertedFrame = null;
try
{
convertedFrame = vfc.DeepCopyFrame(frame);
lock (_lockObject)
{
_frameQueue.Enqueue(convertedFrame);
frameCount++;
}
}
catch (Exception ex)
{
//Log4NetManager.GetLog().Error("HandleDecodedFrame() : " + ex.Message);
Debug.WriteLine("HandleDecodedFrame() : " + ex.Message);
convertedFrame?.Dispose();
}
}
private void RenderImage()
{
int dequeCount = 0;
_renderingCancellationTokenSource = new CancellationTokenSource();
try
{
while (!_renderingCancellationTokenSource.Token.IsCancellationRequested)
{
Bitmap bitmap = null;
lock (_lockObject)
{
if (_frameQueue.Count > 0)
{
_frameQueue.TryDequeue(out bitmap);
}
else
{
continue;
}
}
if (bitmap == null)
{
Thread.Sleep(10); // CPU 과부하 방지
continue;
}
try
{
//Dispatcher.BeginInvoke((Action)(() =>
//{
// try
// {
// DivideAndDisplayBitmap(bitmap);
// }
// finally
// {
// bitmap.Dispose(); // 자원 해제
// }
//}));
}
catch (ArgumentException ex)
{
Console.WriteLine("RenderImage() : " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("RenderImage() : " + ex.ToString());
}
_renderingCancellationTokenSource.Token.ThrowIfCancellationRequested();
}
}
catch (OperationCanceledException)
{
Console.WriteLine("RenderImage() : Render loop canceled.");
}
finally
{
// 필요한 자원 정리
//ClearFrameQueue();
}
}
public void ClosePlayer()
{
_stopThread = true;
lock (_lockObject)
{
if (_videoCancellationTokenSource != null)
{
_videoCancellationTokenSource.Cancel();
Debug.WriteLine("ClosePlayer(): videoThread 종료함");
}
if (_renderingCancellationTokenSource != null)
{
_renderingCancellationTokenSource.Cancel();
Debug.WriteLine("ClosePlayer(): RenderingThread 종료함");
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

@ -94,6 +94,12 @@ namespace SmartAquaViewer.ViewModel
private bool _showLegends;
public bool ShowLegends { get => _showLegends; set { _showLegends = value; OnPropertyChanged(); } }
private bool _useAverage;
public bool UseAverage { get => _useAverage; set { _useAverage = value; OnPropertyChanged(); } }
private bool _isDonut;
public bool IsDonut { get => _isDonut; set { _isDonut = value; OnPropertyChanged(); } }
public ICommand DrawGraphCommand { get; }
public GreenHouseGasViewModel()
@ -123,7 +129,7 @@ namespace SmartAquaViewer.ViewModel
GraphControlVM.SetStackAreaPlot(WaterQualityList, SelectedYFields, DataType.GreenhouseGas, ShowMarkers, ShowLegends);
break;
case GraphType.PIE:
GraphControlVM.SetPieChart(WaterQualityList, SelectedYFields, DataType.GreenhouseGas);
GraphControlVM.SetPieChart(WaterQualityList, SelectedYFields, DataType.GreenhouseGas, UseAverage, IsDonut);
break;
default:
break;

Loading…
Cancel
Save