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