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;