diff --git a/SmartAquaViewer.sln b/SmartAquaViewer.sln
index 261bd86..44473df 100644
--- a/SmartAquaViewer.sln
+++ b/SmartAquaViewer.sln
@@ -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
diff --git a/SmartAquaViewer/Controls/FFPlayerControl.xaml b/SmartAquaViewer/Controls/FFPlayerControl.xaml
new file mode 100644
index 0000000..a152e49
--- /dev/null
+++ b/SmartAquaViewer/Controls/FFPlayerControl.xaml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SmartAquaViewer/Controls/FFPlayerControl.xaml.cs b/SmartAquaViewer/Controls/FFPlayerControl.xaml.cs
new file mode 100644
index 0000000..faf50b2
--- /dev/null
+++ b/SmartAquaViewer/Controls/FFPlayerControl.xaml.cs
@@ -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
+{
+ ///
+ /// FFPlayerControl.xaml에 대한 상호 작용 논리
+ ///
+ public partial class FFPlayerControl : UserControl
+ {
+ public FFPlayerControl()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/SmartAquaViewer/Helper/FFHelper/Helper.cs b/SmartAquaViewer/Helper/FFHelper/Helper.cs
new file mode 100644
index 0000000..b0e04be
--- /dev/null
+++ b/SmartAquaViewer/Helper/FFHelper/Helper.cs
@@ -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;
+ }
+ }
+}
diff --git a/SmartAquaViewer/Helper/FFHelper/StreamDecoder.cs b/SmartAquaViewer/Helper/FFHelper/StreamDecoder.cs
new file mode 100644
index 0000000..00d3039
--- /dev/null
+++ b/SmartAquaViewer/Helper/FFHelper/StreamDecoder.cs
@@ -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 GetContextInfo()
+ {
+ AVDictionaryEntry* tag = null;
+ var result = new Dictionary();
+ 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;
+ }
+ }
+}
diff --git a/SmartAquaViewer/Helper/FFHelper/VideoFrameConverter.cs b/SmartAquaViewer/Helper/FFHelper/VideoFrameConverter.cs
new file mode 100644
index 0000000..fb2768a
--- /dev/null
+++ b/SmartAquaViewer/Helper/FFHelper/VideoFrameConverter.cs
@@ -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); // 메모리 해제
+ }
+ }
+ }
+ }
+}
diff --git a/SmartAquaViewer/SmartAquaViewer.csproj b/SmartAquaViewer/SmartAquaViewer.csproj
index aae0fc5..8842b90 100644
--- a/SmartAquaViewer/SmartAquaViewer.csproj
+++ b/SmartAquaViewer/SmartAquaViewer.csproj
@@ -7,6 +7,7 @@
enable
true
AnyCPU;x64
+ true
@@ -36,6 +37,7 @@
+
@@ -51,6 +53,7 @@
+
diff --git a/SmartAquaViewer/View/CCTVView.xaml b/SmartAquaViewer/View/CCTVView.xaml
index 575eb8b..19e3fe0 100644
--- a/SmartAquaViewer/View/CCTVView.xaml
+++ b/SmartAquaViewer/View/CCTVView.xaml
@@ -6,16 +6,18 @@
xmlns:local="clr-namespace:SmartAquaViewer.View"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SmartAquaViewer/View/CCTVView.xaml.cs b/SmartAquaViewer/View/CCTVView.xaml.cs
index fccdc06..ab5e810 100644
--- a/SmartAquaViewer/View/CCTVView.xaml.cs
+++ b/SmartAquaViewer/View/CCTVView.xaml.cs
@@ -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
///
public partial class CCTVView : UserControl
{
+ private Thread _videoThread;
+ private Thread _renderingThread;
+ private CancellationTokenSource _videoCancellationTokenSource;
+ private CancellationTokenSource _renderingCancellationTokenSource;
+ private bool _stopThread = false;
+
+ private ConcurrentQueue _frameQueue = new ConcurrentQueue();
+
+ 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 종료함");
+ }
+ }
+ }
}
}
diff --git a/SmartAquaViewer/ViewModel/CCTVViewModel.cs b/SmartAquaViewer/ViewModel/CCTVViewModel.cs
index a2e63bd..56abcb8 100644
--- a/SmartAquaViewer/ViewModel/CCTVViewModel.cs
+++ b/SmartAquaViewer/ViewModel/CCTVViewModel.cs
@@ -13,7 +13,7 @@ namespace SmartAquaViewer.ViewModel
{
public class CCTVViewModel : INotifyPropertyChanged
{
- public List WaterQualityList { get; } = new List();
+ public List CCTVInfoList { get; } = new List();
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()
+ {
+ 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;
diff --git a/SmartAquaViewer/ViewModel/FFPlayerViewModel.cs b/SmartAquaViewer/ViewModel/FFPlayerViewModel.cs
new file mode 100644
index 0000000..e3e9757
--- /dev/null
+++ b/SmartAquaViewer/ViewModel/FFPlayerViewModel.cs
@@ -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 _frameQueue = new ConcurrentQueue();
+
+ 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();
+ _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));
+ }
+}
diff --git a/SmartAquaViewer/ViewModel/GreenHouseGasViewModel.cs b/SmartAquaViewer/ViewModel/GreenHouseGasViewModel.cs
index b306d13..08a48bc 100644
--- a/SmartAquaViewer/ViewModel/GreenHouseGasViewModel.cs
+++ b/SmartAquaViewer/ViewModel/GreenHouseGasViewModel.cs
@@ -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;