Added RTSP camera support
This commit is contained in:
parent
9b63740d2a
commit
6284bd0609
@ -1,5 +1,9 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EOI/@EntryIndexedValue">EOI</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EOI/@EntryIndexedValue">EOI</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HTTP/@EntryIndexedValue">HTTP</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HTTPMJPEG/@EntryIndexedValue">HTTPMJPEG</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MJPEG/@EntryIndexedValue">MJPEG</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MJPEG/@EntryIndexedValue">MJPEG</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RTSP/@EntryIndexedValue">RTSP</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SOI/@EntryIndexedValue">SOI</s:String>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SOI/@EntryIndexedValue">SOI</s:String>
|
||||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=URL/@EntryIndexedValue">URL</s:String></wpf:ResourceDictionary>
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=URL/@EntryIndexedValue">URL</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=EnumMember/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String></wpf:ResourceDictionary>
|
@ -1,203 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Drawing;
|
|
||||||
using System.IO;
|
|
||||||
using System.Net;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace IPCamAppBar
|
|
||||||
{
|
|
||||||
public class FrameEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
public Image Image { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class StreamExceptionEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
public Exception Exception { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public delegate void FrameEvent(object sender, FrameEventArgs args);
|
|
||||||
public delegate void StreamExceptionEvent(object sender, StreamExceptionEventArgs args);
|
|
||||||
|
|
||||||
|
|
||||||
internal abstract class CameraStream : IDisposable
|
|
||||||
{
|
|
||||||
public event FrameEvent Frame;
|
|
||||||
public event StreamExceptionEvent StreamException;
|
|
||||||
|
|
||||||
private readonly CancellationTokenSource cancelTaskTokenSource = new CancellationTokenSource();
|
|
||||||
private Task streamTask;
|
|
||||||
private DataMonitor dataMonitor;
|
|
||||||
|
|
||||||
protected CameraStream()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Start(string url)
|
|
||||||
{
|
|
||||||
if (streamTask != null)
|
|
||||||
throw new InvalidOperationException("CameraStream already started");
|
|
||||||
|
|
||||||
streamTask = Task.Run(() => Fetch(url, cancelTaskTokenSource.Token));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
cancelTaskTokenSource.Cancel();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
streamTask?.Wait();
|
|
||||||
}
|
|
||||||
catch (AggregateException e)
|
|
||||||
{
|
|
||||||
if (e.InnerExceptions.Count == 1 && e.InnerExceptions[0] is TaskCanceledException)
|
|
||||||
return;
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private async Task Fetch(string url, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
var uri = new Uri(url);
|
|
||||||
var request = WebRequest.CreateHttp(uri);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(uri.UserInfo))
|
|
||||||
{
|
|
||||||
var parts = uri.UserInfo.Split(':');
|
|
||||||
request.Credentials = new NetworkCredential(parts[0], parts.Length > 1 ? parts[1] : "");
|
|
||||||
}
|
|
||||||
|
|
||||||
request.ReadWriteTimeout = 10;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
HttpWebResponse response;
|
|
||||||
|
|
||||||
using (cancellationToken.Register(() => request.Abort(), false))
|
|
||||||
{
|
|
||||||
response = (HttpWebResponse)await request.GetResponseAsync();
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (response.StatusCode != HttpStatusCode.OK)
|
|
||||||
throw new WebException(response.StatusDescription);
|
|
||||||
|
|
||||||
using (var responseStream = response.GetResponseStream())
|
|
||||||
{
|
|
||||||
var dataMonitorCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
||||||
dataMonitor = new DataMonitor(dataMonitorCancellationTokenSource, TimeSpan.FromSeconds(15));
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await ReadFrames(responseStream, dataMonitorCancellationTokenSource.Token);
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
|
||||||
if (!dataMonitorCancellationTokenSource.IsCancellationRequested)
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
|
||||||
break;
|
|
||||||
|
|
||||||
OnStreamException(new StreamExceptionEventArgs
|
|
||||||
{
|
|
||||||
Exception = e
|
|
||||||
});
|
|
||||||
|
|
||||||
await Task.Delay(5000, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected abstract Task ReadFrames(Stream stream, CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
|
|
||||||
protected virtual void OnFrame(FrameEventArgs args)
|
|
||||||
{
|
|
||||||
dataMonitor.Reset();
|
|
||||||
Frame?.Invoke(this, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected virtual void OnStreamException(StreamExceptionEventArgs args)
|
|
||||||
{
|
|
||||||
StreamException?.Invoke(this, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
internal static class BufferExtensions
|
|
||||||
{
|
|
||||||
public static int Find(this byte[] buffer, byte[] pattern, int limit = int.MaxValue, int startAt = 0)
|
|
||||||
{
|
|
||||||
var patternIndex = 0;
|
|
||||||
var bufferIndex = 0;
|
|
||||||
|
|
||||||
for (bufferIndex = startAt; bufferIndex < buffer.Length && patternIndex < pattern.Length && bufferIndex < limit; bufferIndex++)
|
|
||||||
{
|
|
||||||
if (buffer[bufferIndex] == pattern[patternIndex])
|
|
||||||
{
|
|
||||||
patternIndex++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
patternIndex = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (patternIndex == pattern.Length)
|
|
||||||
return bufferIndex - pattern.Length;
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
internal class DataMonitor
|
|
||||||
{
|
|
||||||
private readonly CancellationTokenSource cancellationTokenSource;
|
|
||||||
private readonly long timeout;
|
|
||||||
|
|
||||||
private readonly Timer timeoutTimer;
|
|
||||||
|
|
||||||
|
|
||||||
public DataMonitor(CancellationTokenSource cancellationTokenSource, TimeSpan timeout)
|
|
||||||
{
|
|
||||||
this.cancellationTokenSource = cancellationTokenSource;
|
|
||||||
this.timeout = (long)timeout.TotalMilliseconds;
|
|
||||||
|
|
||||||
timeoutTimer = new Timer(Tick, null, this.timeout, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Reset()
|
|
||||||
{
|
|
||||||
timeoutTimer.Change(timeout, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void Tick(object state)
|
|
||||||
{
|
|
||||||
cancellationTokenSource.Cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
8
IPCamAppBar/CameraView.Designer.cs
generated
8
IPCamAppBar/CameraView.Designer.cs
generated
@ -28,11 +28,9 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
{
|
{
|
||||||
this.components = new System.ComponentModel.Container();
|
|
||||||
this.ConnectingLabel = new System.Windows.Forms.Label();
|
this.ConnectingLabel = new System.Windows.Forms.Label();
|
||||||
this.StreamView = new System.Windows.Forms.PictureBox();
|
this.StreamView = new System.Windows.Forms.PictureBox();
|
||||||
this.IssueLabel = new System.Windows.Forms.Label();
|
this.IssueLabel = new System.Windows.Forms.Label();
|
||||||
this.NoDataTimer = new System.Windows.Forms.Timer(this.components);
|
|
||||||
((System.ComponentModel.ISupportInitialize)(this.StreamView)).BeginInit();
|
((System.ComponentModel.ISupportInitialize)(this.StreamView)).BeginInit();
|
||||||
this.SuspendLayout();
|
this.SuspendLayout();
|
||||||
//
|
//
|
||||||
@ -73,11 +71,6 @@
|
|||||||
this.IssueLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
this.IssueLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
|
||||||
this.IssueLabel.Visible = false;
|
this.IssueLabel.Visible = false;
|
||||||
//
|
//
|
||||||
// NoDataTimer
|
|
||||||
//
|
|
||||||
this.NoDataTimer.Interval = 1000;
|
|
||||||
this.NoDataTimer.Tick += new System.EventHandler(this.NoDataTimer_Tick);
|
|
||||||
//
|
|
||||||
// CameraView
|
// CameraView
|
||||||
//
|
//
|
||||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||||
@ -98,6 +91,5 @@
|
|||||||
private System.Windows.Forms.Label ConnectingLabel;
|
private System.Windows.Forms.Label ConnectingLabel;
|
||||||
private System.Windows.Forms.PictureBox StreamView;
|
private System.Windows.Forms.PictureBox StreamView;
|
||||||
private System.Windows.Forms.Label IssueLabel;
|
private System.Windows.Forms.Label IssueLabel;
|
||||||
private System.Windows.Forms.Timer NoDataTimer;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,64 +1,63 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Drawing.Drawing2D;
|
using System.Drawing.Drawing2D;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using DateTime = System.DateTime;
|
using FastBitmapLib;
|
||||||
|
using IPCamLib;
|
||||||
|
|
||||||
namespace IPCamAppBar
|
namespace IPCamAppBar
|
||||||
{
|
{
|
||||||
public partial class CameraView : UserControl
|
public partial class CameraView : UserControl, IRetryableCameraObserver
|
||||||
{
|
{
|
||||||
private DateTime lastFrameTime;
|
private readonly bool overlayDateTime;
|
||||||
|
private Bitmap viewBitmap;
|
||||||
|
|
||||||
|
public CameraView(ICamera camera, bool overlayDateTime)
|
||||||
public CameraView(string url)
|
|
||||||
{
|
{
|
||||||
|
this.overlayDateTime = overlayDateTime;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
var cameraStream = new CameraMJPEGStream();
|
var streamCancellationTokenSource = new CancellationTokenSource();
|
||||||
cameraStream.Frame += CameraStreamOnFrame;
|
Task.Run(async () =>
|
||||||
cameraStream.StreamException += CameraStreamOnStreamException;
|
|
||||||
cameraStream.Start(url);
|
|
||||||
|
|
||||||
Disposed += (sender, args) =>
|
|
||||||
{
|
{
|
||||||
cameraStream.Dispose();
|
var retryableCamera = new RetryableCamera(camera);
|
||||||
|
await retryableCamera.Fetch(this, streamCancellationTokenSource.Token);
|
||||||
|
});
|
||||||
|
|
||||||
|
Disposed += (_, _) =>
|
||||||
|
{
|
||||||
|
streamCancellationTokenSource.Cancel();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CameraStreamOnFrame(object sender, FrameEventArgs args)
|
|
||||||
{
|
|
||||||
// The event comes from a background thread, so if needed invoke it on the main thread
|
|
||||||
if (InvokeRequired)
|
|
||||||
{
|
|
||||||
Invoke(new Action(() => { CameraStreamOnFrame(sender, args); }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectingLabel.Visible = false;
|
private Bitmap resizedBitmap;
|
||||||
StreamView.Visible = true;
|
|
||||||
IssueLabel.Visible = false;
|
|
||||||
|
|
||||||
lastFrameTime = DateTime.Now;
|
public Task OnFrame(Image image)
|
||||||
NoDataTimer.Start();
|
|
||||||
|
|
||||||
var viewImage = new Bitmap(Width, Height);
|
|
||||||
using (var graphics = Graphics.FromImage(viewImage))
|
|
||||||
{
|
{
|
||||||
|
if (overlayDateTime || image is not Bitmap bitmap || image.Width != Width || image.Height != Height)
|
||||||
|
{
|
||||||
|
resizedBitmap ??= new Bitmap(Width, Height);
|
||||||
|
|
||||||
|
using var graphics = Graphics.FromImage(resizedBitmap);
|
||||||
graphics.SmoothingMode = SmoothingMode.AntiAlias;
|
graphics.SmoothingMode = SmoothingMode.AntiAlias;
|
||||||
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||||
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||||
|
|
||||||
graphics.DrawImage(args.Image, 0, 0, viewImage.Width, viewImage.Height);
|
graphics.DrawImage(image, 0, 0, resizedBitmap.Width, resizedBitmap.Height);
|
||||||
|
|
||||||
|
if (overlayDateTime)
|
||||||
|
{
|
||||||
using (var path = new GraphicsPath())
|
using (var path = new GraphicsPath())
|
||||||
{
|
{
|
||||||
path.AddString(
|
path.AddString(
|
||||||
lastFrameTime.ToString("G"),
|
DateTime.Now.ToString("G"),
|
||||||
FontFamily.GenericSansSerif,
|
FontFamily.GenericSansSerif,
|
||||||
(int) FontStyle.Regular,
|
(int) FontStyle.Regular,
|
||||||
graphics.DpiY * 14 / 72,
|
graphics.DpiY * 14 / 72,
|
||||||
Rectangle.Inflate(new Rectangle(0, 0, viewImage.Width, viewImage.Height), -4, -4),
|
Rectangle.Inflate(new Rectangle(0, 0, resizedBitmap.Width, resizedBitmap.Height), -4, -4),
|
||||||
new StringFormat
|
new StringFormat
|
||||||
{
|
{
|
||||||
Alignment = StringAlignment.Far,
|
Alignment = StringAlignment.Far,
|
||||||
@ -68,37 +67,73 @@ namespace IPCamAppBar
|
|||||||
graphics.DrawPath(new Pen(Color.Black, 3), path);
|
graphics.DrawPath(new Pen(Color.Black, 3), path);
|
||||||
graphics.FillPath(Brushes.White, path);
|
graphics.FillPath(Brushes.White, path);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
graphics.Flush();
|
graphics.Flush();
|
||||||
|
bitmap = resizedBitmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
var oldImage = StreamView.Image;
|
Invoke(new Action(() => { InvokeFrame(bitmap); }));
|
||||||
StreamView.Image = viewImage;
|
return Task.CompletedTask;
|
||||||
oldImage?.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void CameraStreamOnStreamException(object sender, StreamExceptionEventArgs args)
|
public Task OnFetch()
|
||||||
{
|
{
|
||||||
if (InvokeRequired)
|
Invoke(new Action(InvokeFetch));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Task OnDisconnected(Exception exception, TimeSpan retryDelay)
|
||||||
{
|
{
|
||||||
Invoke(new Action(() => { CameraStreamOnStreamException(sender, args); }));
|
Invoke(new Action(() => { InvokeDisconnected(exception, retryDelay); }));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void InvokeFrame(Bitmap bitmap)
|
||||||
|
{
|
||||||
|
if (IsDisposed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
ConnectingLabel.Visible = false;
|
||||||
|
StreamView.Visible = true;
|
||||||
|
IssueLabel.Visible = false;
|
||||||
|
|
||||||
|
|
||||||
|
if (viewBitmap == null)
|
||||||
|
{
|
||||||
|
viewBitmap = new Bitmap(Width, Height);
|
||||||
|
StreamView.Image = viewBitmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
IssueLabel.Text = args.Exception.Message;
|
using (var fastViewBitmap = viewBitmap.FastLock())
|
||||||
|
{
|
||||||
|
fastViewBitmap.CopyRegion(bitmap, new Rectangle(Point.Empty, bitmap.Size), new Rectangle(Point.Empty, viewBitmap.Size));
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamView.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void InvokeFetch()
|
||||||
|
{
|
||||||
|
if (IsDisposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
IssueLabel.Text = "Connecting...";
|
||||||
IssueLabel.Visible = true;
|
IssueLabel.Visible = true;
|
||||||
IssueLabel.BringToFront();
|
IssueLabel.BringToFront();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void NoDataTimer_Tick(object sender, EventArgs e)
|
private void InvokeDisconnected(Exception exception, TimeSpan retryDelay)
|
||||||
{
|
{
|
||||||
var timeSinceLastFrame = DateTime.Now - lastFrameTime;
|
if (IsDisposed)
|
||||||
if (timeSinceLastFrame.TotalSeconds < 10)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
IssueLabel.Text = $@"No data for {(int)timeSinceLastFrame.TotalSeconds} seconds";
|
IssueLabel.Text = (exception?.Message ?? "Camera disconnected") + $", retrying in {retryDelay.TotalSeconds} seconds";
|
||||||
IssueLabel.Visible = true;
|
IssueLabel.Visible = true;
|
||||||
IssueLabel.BringToFront();
|
IssueLabel.BringToFront();
|
||||||
}
|
}
|
||||||
|
@ -117,7 +117,4 @@
|
|||||||
<resheader name="writer">
|
<resheader name="writer">
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
</resheader>
|
</resheader>
|
||||||
<metadata name="NoDataTimer.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
|
||||||
<value>17, 17</value>
|
|
||||||
</metadata>
|
|
||||||
</root>
|
</root>
|
@ -4,7 +4,7 @@ namespace IPCamAppBar
|
|||||||
{
|
{
|
||||||
public class Config
|
public class Config
|
||||||
{
|
{
|
||||||
public ConfigAppBar AppBar { get; } = new ConfigAppBar();
|
public ConfigAppBar AppBar { get; } = new();
|
||||||
public List<ConfigCamera> Cameras { get; set; }
|
public List<ConfigCamera> Cameras { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,13 +22,24 @@ namespace IPCamAppBar
|
|||||||
public int Monitor { get; set; }
|
public int Monitor { get; set; }
|
||||||
public ConfigSide Side { get; set; }
|
public ConfigSide Side { get; set; }
|
||||||
public int Size { get; set; }
|
public int Size { get; set; }
|
||||||
|
public int Spacing { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ReSharper disable InconsistentNaming
|
||||||
|
public enum ConfigCameraType
|
||||||
|
{
|
||||||
|
HTTPMJPEG,
|
||||||
|
RTSP
|
||||||
|
}
|
||||||
|
// ReSharper restore InconsistentNaming
|
||||||
|
|
||||||
public class ConfigCamera
|
public class ConfigCamera
|
||||||
{
|
{
|
||||||
|
public ConfigCameraType Type { get; set; }
|
||||||
public string URL { get; set; }
|
public string URL { get; set; }
|
||||||
public int Width { get; set; }
|
public int Width { get; set; }
|
||||||
public int Height { get; set; }
|
public int Height { get; set; }
|
||||||
|
public bool OverlayDateTime { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,9 +12,10 @@
|
|||||||
<FileAlignment>512</FileAlignment>
|
<FileAlignment>512</FileAlignment>
|
||||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||||
<Deterministic>true</Deterministic>
|
<Deterministic>true</Deterministic>
|
||||||
|
<LangVersion>9</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<DebugSymbols>true</DebugSymbols>
|
||||||
<DebugType>full</DebugType>
|
<DebugType>full</DebugType>
|
||||||
<Optimize>false</Optimize>
|
<Optimize>false</Optimize>
|
||||||
@ -22,6 +23,7 @@
|
|||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
<ErrorReport>prompt</ErrorReport>
|
<ErrorReport>prompt</ErrorReport>
|
||||||
<WarningLevel>4</WarningLevel>
|
<WarningLevel>4</WarningLevel>
|
||||||
|
<Prefer32Bit>false</Prefer32Bit>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
@ -34,8 +36,11 @@
|
|||||||
<Prefer32Bit>false</Prefer32Bit>
|
<Prefer32Bit>false</Prefer32Bit>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
<Reference Include="FastBitmapLib, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||||
<HintPath>packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
|
<HintPath>..\packages\FastBitmapLib.2.0.0\lib\net452\FastBitmapLib.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
@ -51,8 +56,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="AppBar.cs" />
|
<Compile Include="AppBar.cs" />
|
||||||
<Compile Include="CameraMJPEGStream.cs" />
|
|
||||||
<Compile Include="CameraStream.cs" />
|
|
||||||
<Compile Include="CameraView.cs">
|
<Compile Include="CameraView.cs">
|
||||||
<SubType>UserControl</SubType>
|
<SubType>UserControl</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
@ -100,5 +103,11 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="App.config" />
|
<None Include="App.config" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\IPCamLib\IPCamLib.csproj">
|
||||||
|
<Project>{FD0886FF-F274-4EFC-8A56-F82A4641C3FD}</Project>
|
||||||
|
<Name>IPCamLib</Name>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
</Project>
|
</Project>
|
@ -1,9 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.Contracts;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
using IPCamLib;
|
||||||
|
using IPCamLib.Concrete;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace IPCamAppBar
|
namespace IPCamAppBar
|
||||||
@ -32,6 +33,11 @@ namespace IPCamAppBar
|
|||||||
appBar = new AppBar(Handle);
|
appBar = new AppBar(Handle);
|
||||||
appBar.SetPosition(monitor, position, config.AppBar.Size);
|
appBar.SetPosition(monitor, position, config.AppBar.Size);
|
||||||
|
|
||||||
|
CameraViewContainer.FlowDirection =
|
||||||
|
config.AppBar.Side == ConfigSide.Left || config.AppBar.Side == ConfigSide.Right
|
||||||
|
? FlowDirection.TopDown
|
||||||
|
: FlowDirection.LeftToRight;
|
||||||
|
|
||||||
config.Cameras?.ForEach(AddCamera);
|
config.Cameras?.ForEach(AddCamera);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,12 +77,26 @@ namespace IPCamAppBar
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void AddCamera(ConfigCamera camera)
|
private void AddCamera(ConfigCamera configCamera)
|
||||||
{
|
{
|
||||||
var view = new CameraView(camera.URL)
|
ICamera camera = configCamera.Type switch
|
||||||
{
|
{
|
||||||
Width = camera.Width,
|
ConfigCameraType.HTTPMJPEG => new HTTPMJPEGStreamCamera(new Uri(configCamera.URL)),
|
||||||
Height = camera.Height
|
ConfigCameraType.RTSP => new RTSPStreamCamera(new Uri(configCamera.URL), configCamera.Width, configCamera.Height),
|
||||||
|
_ => throw new ArgumentOutOfRangeException($"Camera type in configuration is not valid: {configCamera.Type}")
|
||||||
|
};
|
||||||
|
|
||||||
|
var margin = new Padding(0);
|
||||||
|
if (CameraViewContainer.FlowDirection == FlowDirection.LeftToRight)
|
||||||
|
margin.Right = config.AppBar.Spacing;
|
||||||
|
else
|
||||||
|
margin.Bottom = config.AppBar.Spacing;
|
||||||
|
|
||||||
|
var view = new CameraView(camera, configCamera.OverlayDateTime)
|
||||||
|
{
|
||||||
|
Width = configCamera.Width,
|
||||||
|
Height = configCamera.Height,
|
||||||
|
Margin = margin
|
||||||
};
|
};
|
||||||
|
|
||||||
CameraViewContainer.Controls.Add(view);
|
CameraViewContainer.Controls.Add(view);
|
||||||
|
@ -2,19 +2,24 @@
|
|||||||
"AppBar": {
|
"AppBar": {
|
||||||
"Monitor": 0,
|
"Monitor": 0,
|
||||||
"Side": "Top",
|
"Side": "Top",
|
||||||
"Size": 480
|
"Size": 480,
|
||||||
|
"Spacing": 2
|
||||||
},
|
},
|
||||||
|
|
||||||
"Cameras": [
|
"Cameras": [
|
||||||
{
|
{
|
||||||
"URL": "http://username:password@ipcamera1/videostream.cgi",
|
"Type": "RTSP",
|
||||||
|
"URL": "rtsp://username:password@ipcamera1/",
|
||||||
"Width": 480,
|
"Width": 480,
|
||||||
"Height": 360
|
"Height": 360,
|
||||||
|
"OverlayDateTime": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"Type": "HTTPMJPEG",
|
||||||
"URL": "http://username:password@ipcamera2/videostream.cgi",
|
"URL": "http://username:password@ipcamera2/videostream.cgi",
|
||||||
"Width": 480,
|
"Width": 480,
|
||||||
"Height": 360
|
"Height": 360,
|
||||||
|
"OverlayDateTime": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<packages>
|
<packages>
|
||||||
<package id="Newtonsoft.Json" version="12.0.2" targetFramework="net472" />
|
<package id="FastBitmapLib" version="2.0.0" targetFramework="net472" />
|
||||||
|
<package id="Newtonsoft.Json" version="13.0.1" targetFramework="net472" />
|
||||||
</packages>
|
</packages>
|
73
IPCamLib/Base/BaseHTTPStreamCamera.cs
Normal file
73
IPCamLib/Base/BaseHTTPStreamCamera.cs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace IPCamLib.Base
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract base class for IP cameras reading an HTTP stream.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BaseHTTPStreamCamera : ICamera
|
||||||
|
{
|
||||||
|
private readonly Uri streamUri;
|
||||||
|
|
||||||
|
|
||||||
|
/// <param name="streamUri">The URI to the camera stream.
|
||||||
|
/// Can include basic credentials in the standard 'username:password@' format.</param>
|
||||||
|
protected BaseHTTPStreamCamera(Uri streamUri)
|
||||||
|
{
|
||||||
|
this.streamUri = streamUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task Fetch(ICameraObserver observer, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var request = WebRequest.CreateHttp(streamUri);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(streamUri.UserInfo))
|
||||||
|
{
|
||||||
|
var parts = streamUri.UserInfo.Split(':');
|
||||||
|
request.Credentials = new NetworkCredential(parts[0], parts.Length > 1 ? parts[1] : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
request.ReadWriteTimeout = (int)TimeSpan.FromMinutes(1).TotalMilliseconds;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpWebResponse response;
|
||||||
|
|
||||||
|
using (cancellationToken.Register(() => request.Abort(), false))
|
||||||
|
{
|
||||||
|
response = (HttpWebResponse)await request.GetResponseAsync();
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (response.StatusCode != HttpStatusCode.OK)
|
||||||
|
throw new WebException(response.StatusDescription);
|
||||||
|
|
||||||
|
|
||||||
|
using var responseStream = response.GetResponseStream();
|
||||||
|
await ReadFrames(observer, responseStream, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
request.Abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implement in concrete descendants to continuously read frames from the HTTP stream.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="observer">The observer implementation passed to Fetch which should receive the decoded frames.</param>
|
||||||
|
/// <param name="stream">The HTTP response stream.</param>
|
||||||
|
/// <param name="cancellationToken">Stop reading frames when this token is cancelled.</param>
|
||||||
|
protected abstract Task ReadFrames(ICameraObserver observer, Stream stream, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
@ -3,10 +3,16 @@ using System.Drawing;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using IPCamLib.Base;
|
||||||
|
|
||||||
namespace IPCamAppBar
|
// ReSharper disable UnusedMember.Global - public API
|
||||||
|
|
||||||
|
namespace IPCamLib.Concrete
|
||||||
{
|
{
|
||||||
internal class CameraMJPEGStream : CameraStream
|
/// <summary>
|
||||||
|
/// Implements the ICamera interface for IP cameras exposing an MJPEG stream over HTTP.
|
||||||
|
/// </summary>
|
||||||
|
public class HTTPMJPEGStreamCamera : BaseHTTPStreamCamera
|
||||||
{
|
{
|
||||||
// MJPEG decoding is a variation on https://github.com/arndre/MjpegDecoder
|
// MJPEG decoding is a variation on https://github.com/arndre/MjpegDecoder
|
||||||
private static readonly byte[] JpegSOI = { 0xff, 0xd8 }; // start of image bytes
|
private static readonly byte[] JpegSOI = { 0xff, 0xd8 }; // start of image bytes
|
||||||
@ -16,7 +22,17 @@ namespace IPCamAppBar
|
|||||||
private const int MaxBufferSize = 1024 * 1024 * 10;
|
private const int MaxBufferSize = 1024 * 1024 * 10;
|
||||||
|
|
||||||
|
|
||||||
protected override async Task ReadFrames(Stream stream, CancellationToken cancellationToken)
|
/// <summary>
|
||||||
|
/// Creates a new instance for an IP camera exposing an MJPEG stream over HTTP.
|
||||||
|
/// </summary>
|
||||||
|
/// <inheritdoc />
|
||||||
|
public HTTPMJPEGStreamCamera(Uri streamUri) : base(streamUri)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task ReadFrames(ICameraObserver observer, Stream stream, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var buffer = new byte[ChunkSize];
|
var buffer = new byte[ChunkSize];
|
||||||
var bufferPosition = 0;
|
var bufferPosition = 0;
|
||||||
@ -51,9 +67,13 @@ namespace IPCamAppBar
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
var frameTimeout = TimeSpan.FromMinutes(1);
|
||||||
|
var frameTimeoutCancellationTokenSource = new CancellationTokenSource(frameTimeout);
|
||||||
|
var combinedCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, frameTimeoutCancellationTokenSource.Token);
|
||||||
|
|
||||||
|
while (!combinedCancellation.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
var bytesRead = await stream.ReadAsync(buffer, bufferPosition, ChunkSize, cancellationToken);
|
var bytesRead = await stream.ReadAsync(buffer, bufferPosition, ChunkSize, combinedCancellation.Token);
|
||||||
if (bytesRead == 0)
|
if (bytesRead == 0)
|
||||||
throw new EndOfStreamException();
|
throw new EndOfStreamException();
|
||||||
|
|
||||||
@ -61,7 +81,7 @@ namespace IPCamAppBar
|
|||||||
|
|
||||||
if (!startOfImage.HasValue)
|
if (!startOfImage.HasValue)
|
||||||
{
|
{
|
||||||
var index = buffer.Find(JpegSOI, bufferPosition);
|
var index = buffer.IndexOf(JpegSOI, bufferPosition);
|
||||||
if (index == -1)
|
if (index == -1)
|
||||||
{
|
{
|
||||||
// No start of image yet, we need to buffer more
|
// No start of image yet, we need to buffer more
|
||||||
@ -72,7 +92,7 @@ namespace IPCamAppBar
|
|||||||
startOfImage = index;
|
startOfImage = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
var endOfImage = buffer.Find(JpegEOI, bufferPosition, lastEndOfSearch.GetValueOrDefault(startOfImage.Value));
|
var endOfImage = buffer.IndexOf(JpegEOI, bufferPosition, lastEndOfSearch.GetValueOrDefault(startOfImage.Value));
|
||||||
if (endOfImage == -1)
|
if (endOfImage == -1)
|
||||||
{
|
{
|
||||||
// No start of image yet, we need to buffer more. Keep track of where we were so we don't
|
// No start of image yet, we need to buffer more. Keep track of where we were so we don't
|
||||||
@ -90,21 +110,42 @@ namespace IPCamAppBar
|
|||||||
}
|
}
|
||||||
|
|
||||||
endOfImage += JpegEOI.Length;
|
endOfImage += JpegEOI.Length;
|
||||||
HandleFrame(buffer, startOfImage.Value, endOfImage);
|
|
||||||
|
using (var image = new Bitmap(new MemoryStream(buffer, startOfImage.Value, endOfImage - startOfImage.Value)))
|
||||||
|
{
|
||||||
|
await observer.OnFrame(image);
|
||||||
|
}
|
||||||
|
|
||||||
ResetBuffer(endOfImage);
|
ResetBuffer(endOfImage);
|
||||||
|
frameTimeoutCancellationTokenSource.CancelAfter(frameTimeout);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected void HandleFrame(byte[] buffer, int start, int end)
|
internal static class BufferExtensions
|
||||||
{
|
{
|
||||||
using (var image = new Bitmap(new MemoryStream(buffer, start, end - start)))
|
public static int IndexOf(this byte[] buffer, byte[] pattern, int limit = int.MaxValue, int startAt = 0)
|
||||||
{
|
{
|
||||||
OnFrame(new FrameEventArgs
|
var patternIndex = 0;
|
||||||
|
int bufferIndex;
|
||||||
|
|
||||||
|
for (bufferIndex = startAt; bufferIndex < buffer.Length && patternIndex < pattern.Length && bufferIndex < limit; bufferIndex++)
|
||||||
{
|
{
|
||||||
Image = image
|
if (buffer[bufferIndex] == pattern[patternIndex])
|
||||||
});
|
{
|
||||||
}
|
patternIndex++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
patternIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patternIndex == pattern.Length)
|
||||||
|
return bufferIndex - pattern.Length;
|
||||||
|
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
113
IPCamLib/Concrete/RTSPStreamCamera.cs
Normal file
113
IPCamLib/Concrete/RTSPStreamCamera.cs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FastBitmapLib;
|
||||||
|
using IPCamLib.FFMPEG;
|
||||||
|
using RtspClientSharp;
|
||||||
|
using RtspClientSharp.RawFrames.Video;
|
||||||
|
|
||||||
|
namespace IPCamLib.Concrete
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implements the ICamera interface for IP cameras exposing an RTSP stream.
|
||||||
|
/// </summary>
|
||||||
|
public class RTSPStreamCamera : ICamera
|
||||||
|
{
|
||||||
|
private readonly Uri streamUri;
|
||||||
|
private readonly Dictionary<FFmpegVideoCodecId, FFmpegVideoDecoder> videoDecodersMap = new();
|
||||||
|
private readonly Bitmap bitmap;
|
||||||
|
private readonly TransformParameters transformParameters;
|
||||||
|
|
||||||
|
/// <param name="streamUri">The URI to the camera stream.
|
||||||
|
/// Can include basic credentials in the standard 'username:password@' format.</param>
|
||||||
|
/// <param name="width">The width of the viewport</param>
|
||||||
|
/// <param name="height">The height of the viewport</param>
|
||||||
|
public RTSPStreamCamera(Uri streamUri, int width, int height)
|
||||||
|
{
|
||||||
|
this.streamUri = streamUri;
|
||||||
|
|
||||||
|
transformParameters = new TransformParameters(RectangleF.Empty, new Size(width, height),
|
||||||
|
ScalingPolicy.Stretch, PixelFormat.Bgra32, ScalingQuality.FastBilinear);
|
||||||
|
|
||||||
|
bitmap = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
|
||||||
|
|
||||||
|
using var fastBitmap = bitmap.FastLock();
|
||||||
|
fastBitmap.Clear(Color.Black);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task Fetch(ICameraObserver observer, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
NetworkCredential credentials = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(streamUri.UserInfo))
|
||||||
|
{
|
||||||
|
var parts = streamUri.UserInfo.Split(':');
|
||||||
|
credentials = new NetworkCredential(parts[0], parts.Length > 1 ? parts[1] : "");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
credentials = new NetworkCredential(null, (string)null);
|
||||||
|
|
||||||
|
|
||||||
|
var connectionParameters = new ConnectionParameters(streamUri, credentials)
|
||||||
|
{
|
||||||
|
RtpTransport = RtpTransportProtocol.TCP
|
||||||
|
};
|
||||||
|
|
||||||
|
using var rtspClient = new RtspClient(connectionParameters);
|
||||||
|
|
||||||
|
rtspClient.FrameReceived += (_, rawFrame) =>
|
||||||
|
{
|
||||||
|
if (rawFrame is not RawVideoFrame rawVideoFrame)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var decoder = GetDecoderForFrame(rawVideoFrame);
|
||||||
|
var decodedFrame = decoder.TryDecode(rawVideoFrame);
|
||||||
|
|
||||||
|
if (decodedFrame == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using (var fastBitmap = bitmap.FastLock())
|
||||||
|
{
|
||||||
|
decodedFrame.TransformTo(fastBitmap.Scan0, fastBitmap.StrideInBytes, transformParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO await
|
||||||
|
observer.OnFrame(bitmap);
|
||||||
|
};
|
||||||
|
|
||||||
|
await rtspClient.ConnectAsync(cancellationToken);
|
||||||
|
await rtspClient.ReceiveAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private FFmpegVideoDecoder GetDecoderForFrame(RawVideoFrame videoFrame)
|
||||||
|
{
|
||||||
|
var codecId = DetectCodecId(videoFrame);
|
||||||
|
if (videoDecodersMap.TryGetValue(codecId, out var decoder))
|
||||||
|
return decoder;
|
||||||
|
|
||||||
|
decoder = FFmpegVideoDecoder.CreateDecoder(codecId);
|
||||||
|
videoDecodersMap.Add(codecId, decoder);
|
||||||
|
|
||||||
|
return decoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static FFmpegVideoCodecId DetectCodecId(RawVideoFrame videoFrame)
|
||||||
|
{
|
||||||
|
return videoFrame switch
|
||||||
|
{
|
||||||
|
RawJpegFrame => FFmpegVideoCodecId.MJPEG,
|
||||||
|
RawH264Frame => FFmpegVideoCodecId.H264,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(videoFrame))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
IPCamLib/FFMPEG/DecodedVideoFrame.cs
Normal file
24
IPCamLib/FFMPEG/DecodedVideoFrame.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Source code originally from RtspClientSharp's player example:
|
||||||
|
* https://github.com/BogdanovKirill/RtspClientSharp
|
||||||
|
*/
|
||||||
|
// ReSharper disable All
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace IPCamLib.FFMPEG
|
||||||
|
{
|
||||||
|
internal class DecodedVideoFrame : IDecodedVideoFrame
|
||||||
|
{
|
||||||
|
private readonly Action<IntPtr, int, TransformParameters> _transformAction;
|
||||||
|
|
||||||
|
public DecodedVideoFrame(Action<IntPtr, int, TransformParameters> transformAction)
|
||||||
|
{
|
||||||
|
_transformAction = transformAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TransformTo(IntPtr buffer, int bufferStride, TransformParameters transformParameters)
|
||||||
|
{
|
||||||
|
_transformAction(buffer, bufferStride, transformParameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
IPCamLib/FFMPEG/DecodedVideoFrameParameters.cs
Normal file
47
IPCamLib/FFMPEG/DecodedVideoFrameParameters.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Source code originally from RtspClientSharp's player example:
|
||||||
|
* https://github.com/BogdanovKirill/RtspClientSharp
|
||||||
|
*/
|
||||||
|
// ReSharper disable All
|
||||||
|
namespace IPCamLib.FFMPEG
|
||||||
|
{
|
||||||
|
internal class DecodedVideoFrameParameters
|
||||||
|
{
|
||||||
|
public int Width { get; }
|
||||||
|
|
||||||
|
public int Height { get; }
|
||||||
|
|
||||||
|
public FFmpegPixelFormat PixelFormat { get; }
|
||||||
|
|
||||||
|
public DecodedVideoFrameParameters(int width, int height, FFmpegPixelFormat pixelFormat)
|
||||||
|
{
|
||||||
|
Width = width;
|
||||||
|
Height = height;
|
||||||
|
PixelFormat = pixelFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool Equals(DecodedVideoFrameParameters other)
|
||||||
|
{
|
||||||
|
return Width == other.Width && Height == other.Height && PixelFormat == other.PixelFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(null, obj)) return false;
|
||||||
|
if (ReferenceEquals(this, obj)) return true;
|
||||||
|
if (obj.GetType() != GetType()) return false;
|
||||||
|
return Equals((DecodedVideoFrameParameters) obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
var hashCode = Width;
|
||||||
|
hashCode = (hashCode * 397) ^ Height;
|
||||||
|
hashCode = (hashCode * 397) ^ (int) PixelFormat;
|
||||||
|
return hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
IPCamLib/FFMPEG/DecoderException.cs
Normal file
39
IPCamLib/FFMPEG/DecoderException.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Source code originally from RtspClientSharp's player example:
|
||||||
|
* https://github.com/BogdanovKirill/RtspClientSharp
|
||||||
|
*/
|
||||||
|
// ReSharper disable All
|
||||||
|
using System;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
|
namespace IPCamLib.FFMPEG
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
internal class DecoderException : Exception
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// For guidelines regarding the creation of new exception types, see
|
||||||
|
// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp
|
||||||
|
// and
|
||||||
|
// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp
|
||||||
|
//
|
||||||
|
|
||||||
|
public DecoderException()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecoderException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecoderException(string message, Exception inner) : base(message, inner)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected DecoderException(
|
||||||
|
SerializationInfo info,
|
||||||
|
StreamingContext context) : base(info, context)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
140
IPCamLib/FFMPEG/FFmpegDecodedVideoScaler.cs
Normal file
140
IPCamLib/FFMPEG/FFmpegDecodedVideoScaler.cs
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Source code originally from RtspClientSharp's player example:
|
||||||
|
* https://github.com/BogdanovKirill/RtspClientSharp
|
||||||
|
*/
|
||||||
|
// ReSharper disable All
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace IPCamLib.FFMPEG
|
||||||
|
{
|
||||||
|
internal class FFmpegDecodedVideoScaler
|
||||||
|
{
|
||||||
|
private const double MaxAspectRatioError = 0.1;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public IntPtr Handle { get; }
|
||||||
|
public int ScaledWidth { get; }
|
||||||
|
public int ScaledHeight { get; }
|
||||||
|
public PixelFormat ScaledPixelFormat { get; }
|
||||||
|
|
||||||
|
private FFmpegDecodedVideoScaler(IntPtr handle, int scaledWidth, int scaledHeight,
|
||||||
|
PixelFormat scaledPixelFormat)
|
||||||
|
{
|
||||||
|
Handle = handle;
|
||||||
|
ScaledWidth = scaledWidth;
|
||||||
|
ScaledHeight = scaledHeight;
|
||||||
|
ScaledPixelFormat = scaledPixelFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
~FFmpegDecodedVideoScaler()
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <exception cref="DecoderException"></exception>
|
||||||
|
public static FFmpegDecodedVideoScaler Create(DecodedVideoFrameParameters decodedVideoFrameParameters,
|
||||||
|
TransformParameters transformParameters)
|
||||||
|
{
|
||||||
|
if (decodedVideoFrameParameters == null)
|
||||||
|
throw new ArgumentNullException(nameof(decodedVideoFrameParameters));
|
||||||
|
if (transformParameters == null)
|
||||||
|
throw new ArgumentNullException(nameof(transformParameters));
|
||||||
|
|
||||||
|
int sourceLeft = 0;
|
||||||
|
int sourceTop = 0;
|
||||||
|
int sourceWidth = decodedVideoFrameParameters.Width;
|
||||||
|
int sourceHeight = decodedVideoFrameParameters.Height;
|
||||||
|
int scaledWidth = decodedVideoFrameParameters.Width;
|
||||||
|
int scaledHeight = decodedVideoFrameParameters.Height;
|
||||||
|
|
||||||
|
if (!transformParameters.RegionOfInterest.IsEmpty)
|
||||||
|
{
|
||||||
|
sourceLeft =
|
||||||
|
(int) (decodedVideoFrameParameters.Width * transformParameters.RegionOfInterest.Left);
|
||||||
|
sourceTop =
|
||||||
|
(int) (decodedVideoFrameParameters.Height * transformParameters.RegionOfInterest.Top);
|
||||||
|
sourceWidth =
|
||||||
|
(int) (decodedVideoFrameParameters.Width * transformParameters.RegionOfInterest.Width);
|
||||||
|
sourceHeight =
|
||||||
|
(int) (decodedVideoFrameParameters.Height * transformParameters.RegionOfInterest.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!transformParameters.TargetFrameSize.IsEmpty)
|
||||||
|
{
|
||||||
|
scaledWidth = transformParameters.TargetFrameSize.Width;
|
||||||
|
scaledHeight = transformParameters.TargetFrameSize.Height;
|
||||||
|
|
||||||
|
ScalingPolicy scalingPolicy = transformParameters.ScalePolicy;
|
||||||
|
|
||||||
|
float srcAspectRatio = (float) sourceWidth / sourceHeight;
|
||||||
|
float destAspectRatio = (float) scaledWidth / scaledHeight;
|
||||||
|
|
||||||
|
if (scalingPolicy == ScalingPolicy.Auto)
|
||||||
|
{
|
||||||
|
float relativeChange = Math.Abs(srcAspectRatio - destAspectRatio) / srcAspectRatio;
|
||||||
|
|
||||||
|
scalingPolicy = relativeChange > MaxAspectRatioError
|
||||||
|
? ScalingPolicy.RespectAspectRatio
|
||||||
|
: ScalingPolicy.Stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scalingPolicy == ScalingPolicy.RespectAspectRatio)
|
||||||
|
{
|
||||||
|
if (destAspectRatio < srcAspectRatio)
|
||||||
|
scaledHeight = sourceHeight * scaledWidth / sourceWidth;
|
||||||
|
else
|
||||||
|
scaledWidth = sourceWidth * scaledHeight / sourceHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PixelFormat scaledPixelFormat = transformParameters.TargetFormat;
|
||||||
|
FFmpegPixelFormat scaledFFmpegPixelFormat = GetFFmpegPixelFormat(scaledPixelFormat);
|
||||||
|
FFmpegScalingQuality scaleQuality = GetFFmpegScaleQuality(transformParameters.ScaleQuality);
|
||||||
|
|
||||||
|
int resultCode = FFmpegVideoPInvoke.CreateVideoScaler(sourceLeft, sourceTop, sourceWidth, sourceHeight,
|
||||||
|
decodedVideoFrameParameters.PixelFormat,
|
||||||
|
scaledWidth, scaledHeight, scaledFFmpegPixelFormat, scaleQuality, out var handle);
|
||||||
|
|
||||||
|
if (resultCode != 0)
|
||||||
|
throw new DecoderException(@"An error occurred while creating scaler, code: {resultCode}");
|
||||||
|
|
||||||
|
return new FFmpegDecodedVideoScaler(handle, scaledWidth, scaledHeight, scaledPixelFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
FFmpegVideoPInvoke.RemoveVideoScaler(Handle);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FFmpegScalingQuality GetFFmpegScaleQuality(ScalingQuality scalingQuality)
|
||||||
|
{
|
||||||
|
if (scalingQuality == ScalingQuality.Nearest)
|
||||||
|
return FFmpegScalingQuality.Point;
|
||||||
|
if (scalingQuality == ScalingQuality.Bilinear)
|
||||||
|
return FFmpegScalingQuality.Bilinear;
|
||||||
|
if (scalingQuality == ScalingQuality.FastBilinear)
|
||||||
|
return FFmpegScalingQuality.FastBilinear;
|
||||||
|
if (scalingQuality == ScalingQuality.Bicubic)
|
||||||
|
return FFmpegScalingQuality.Bicubic;
|
||||||
|
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(scalingQuality));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FFmpegPixelFormat GetFFmpegPixelFormat(PixelFormat pixelFormat)
|
||||||
|
{
|
||||||
|
if (pixelFormat == PixelFormat.Bgra32)
|
||||||
|
return FFmpegPixelFormat.BGRA;
|
||||||
|
if (pixelFormat == PixelFormat.Grayscale)
|
||||||
|
return FFmpegPixelFormat.GRAY8;
|
||||||
|
if (pixelFormat == PixelFormat.Bgr24)
|
||||||
|
return FFmpegPixelFormat.BGR24;
|
||||||
|
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(pixelFormat));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
129
IPCamLib/FFMPEG/FFmpegVideoDecoder.cs
Normal file
129
IPCamLib/FFMPEG/FFmpegVideoDecoder.cs
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
* Source code originally from RtspClientSharp's player example:
|
||||||
|
* https://github.com/BogdanovKirill/RtspClientSharp
|
||||||
|
*/
|
||||||
|
// ReSharper disable All
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using RtspClientSharp.RawFrames.Video;
|
||||||
|
|
||||||
|
namespace IPCamLib.FFMPEG
|
||||||
|
{
|
||||||
|
internal class FFmpegVideoDecoder
|
||||||
|
{
|
||||||
|
private readonly IntPtr _decoderHandle;
|
||||||
|
private readonly FFmpegVideoCodecId _videoCodecId;
|
||||||
|
|
||||||
|
private DecodedVideoFrameParameters _currentFrameParameters =
|
||||||
|
new DecodedVideoFrameParameters(0, 0, FFmpegPixelFormat.None);
|
||||||
|
|
||||||
|
private readonly Dictionary<TransformParameters, FFmpegDecodedVideoScaler> _scalersMap =
|
||||||
|
new Dictionary<TransformParameters, FFmpegDecodedVideoScaler>();
|
||||||
|
|
||||||
|
private byte[] _extraData = new byte[0];
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
private FFmpegVideoDecoder(FFmpegVideoCodecId videoCodecId, IntPtr decoderHandle)
|
||||||
|
{
|
||||||
|
_videoCodecId = videoCodecId;
|
||||||
|
_decoderHandle = decoderHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
~FFmpegVideoDecoder()
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FFmpegVideoDecoder CreateDecoder(FFmpegVideoCodecId videoCodecId)
|
||||||
|
{
|
||||||
|
int resultCode = FFmpegVideoPInvoke.CreateVideoDecoder(videoCodecId, out IntPtr decoderPtr);
|
||||||
|
|
||||||
|
if (resultCode != 0)
|
||||||
|
throw new DecoderException(
|
||||||
|
$"An error occurred while creating video decoder for {videoCodecId} codec, code: {resultCode}");
|
||||||
|
|
||||||
|
return new FFmpegVideoDecoder(videoCodecId, decoderPtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe IDecodedVideoFrame TryDecode(RawVideoFrame rawVideoFrame)
|
||||||
|
{
|
||||||
|
fixed (byte* rawBufferPtr = &rawVideoFrame.FrameSegment.Array[rawVideoFrame.FrameSegment.Offset])
|
||||||
|
{
|
||||||
|
int resultCode;
|
||||||
|
|
||||||
|
if (rawVideoFrame is RawH264IFrame rawH264IFrame)
|
||||||
|
{
|
||||||
|
if (rawH264IFrame.SpsPpsSegment.Array != null &&
|
||||||
|
!_extraData.SequenceEqual(rawH264IFrame.SpsPpsSegment))
|
||||||
|
{
|
||||||
|
if (_extraData.Length != rawH264IFrame.SpsPpsSegment.Count)
|
||||||
|
_extraData = new byte[rawH264IFrame.SpsPpsSegment.Count];
|
||||||
|
|
||||||
|
Buffer.BlockCopy(rawH264IFrame.SpsPpsSegment.Array, rawH264IFrame.SpsPpsSegment.Offset,
|
||||||
|
_extraData, 0, rawH264IFrame.SpsPpsSegment.Count);
|
||||||
|
|
||||||
|
fixed (byte* initDataPtr = &_extraData[0])
|
||||||
|
{
|
||||||
|
resultCode = FFmpegVideoPInvoke.SetVideoDecoderExtraData(_decoderHandle,
|
||||||
|
(IntPtr)initDataPtr, _extraData.Length);
|
||||||
|
|
||||||
|
if (resultCode != 0)
|
||||||
|
throw new DecoderException(
|
||||||
|
$"An error occurred while setting video extra data, {_videoCodecId} codec, code: {resultCode}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resultCode = FFmpegVideoPInvoke.DecodeFrame(_decoderHandle, (IntPtr)rawBufferPtr,
|
||||||
|
rawVideoFrame.FrameSegment.Count,
|
||||||
|
out int width, out int height, out FFmpegPixelFormat pixelFormat);
|
||||||
|
|
||||||
|
if (resultCode != 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (_currentFrameParameters.Width != width || _currentFrameParameters.Height != height ||
|
||||||
|
_currentFrameParameters.PixelFormat != pixelFormat)
|
||||||
|
{
|
||||||
|
_currentFrameParameters = new DecodedVideoFrameParameters(width, height, pixelFormat);
|
||||||
|
DropAllVideoScalers();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DecodedVideoFrame(TransformTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
FFmpegVideoPInvoke.RemoveVideoDecoder(_decoderHandle);
|
||||||
|
DropAllVideoScalers();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DropAllVideoScalers()
|
||||||
|
{
|
||||||
|
foreach (var scaler in _scalersMap.Values)
|
||||||
|
scaler.Dispose();
|
||||||
|
|
||||||
|
_scalersMap.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TransformTo(IntPtr buffer, int bufferStride, TransformParameters parameters)
|
||||||
|
{
|
||||||
|
if (!_scalersMap.TryGetValue(parameters, out FFmpegDecodedVideoScaler videoScaler))
|
||||||
|
{
|
||||||
|
videoScaler = FFmpegDecodedVideoScaler.Create(_currentFrameParameters, parameters);
|
||||||
|
_scalersMap.Add(parameters, videoScaler);
|
||||||
|
}
|
||||||
|
|
||||||
|
int resultCode = FFmpegVideoPInvoke.ScaleDecodedVideoFrame(_decoderHandle, videoScaler.Handle, buffer, bufferStride);
|
||||||
|
|
||||||
|
if (resultCode != 0)
|
||||||
|
throw new DecoderException($"An error occurred while converting decoding video frame, {_videoCodecId} codec, code: {resultCode}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
IPCamLib/FFMPEG/FFmpegVideoPInvoke.cs
Normal file
66
IPCamLib/FFMPEG/FFmpegVideoPInvoke.cs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Source code originally from RtspClientSharp's player example:
|
||||||
|
* https://github.com/BogdanovKirill/RtspClientSharp
|
||||||
|
*/
|
||||||
|
// ReSharper disable All
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace IPCamLib.FFMPEG
|
||||||
|
{
|
||||||
|
internal enum FFmpegVideoCodecId
|
||||||
|
{
|
||||||
|
MJPEG = 7,
|
||||||
|
H264 = 27
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
internal enum FFmpegScalingQuality
|
||||||
|
{
|
||||||
|
FastBilinear = 1,
|
||||||
|
Bilinear = 2,
|
||||||
|
Bicubic = 4,
|
||||||
|
Point = 0x10,
|
||||||
|
Area = 0x20,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum FFmpegPixelFormat
|
||||||
|
{
|
||||||
|
None = -1,
|
||||||
|
BGR24 = 3,
|
||||||
|
GRAY8 = 8,
|
||||||
|
BGRA = 28
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class FFmpegVideoPInvoke
|
||||||
|
{
|
||||||
|
private const string LibraryName = "libffmpeghelper.dll";
|
||||||
|
|
||||||
|
[DllImport(LibraryName, EntryPoint = "create_video_decoder", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
public static extern int CreateVideoDecoder(FFmpegVideoCodecId videoCodecId, out IntPtr handle);
|
||||||
|
|
||||||
|
[DllImport(LibraryName, EntryPoint = "remove_video_decoder", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
public static extern void RemoveVideoDecoder(IntPtr handle);
|
||||||
|
|
||||||
|
[DllImport(LibraryName, EntryPoint = "set_video_decoder_extradata",
|
||||||
|
CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
public static extern int SetVideoDecoderExtraData(IntPtr handle, IntPtr extradata, int extradataLength);
|
||||||
|
|
||||||
|
[DllImport(LibraryName, EntryPoint = "decode_video_frame", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
public static extern int DecodeFrame(IntPtr handle, IntPtr rawBuffer, int rawBufferLength, out int frameWidth,
|
||||||
|
out int frameHeight, out FFmpegPixelFormat framePixelFormat);
|
||||||
|
|
||||||
|
[DllImport(LibraryName, EntryPoint = "scale_decoded_video_frame", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
public static extern int ScaleDecodedVideoFrame(IntPtr handle, IntPtr scalerHandle, IntPtr scaledBuffer,
|
||||||
|
int scaledBufferStride);
|
||||||
|
|
||||||
|
[DllImport(LibraryName, EntryPoint = "create_video_scaler", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
public static extern int CreateVideoScaler(int sourceLeft, int sourceTop, int sourceWidth, int sourceHeight,
|
||||||
|
FFmpegPixelFormat sourcePixelFormat,
|
||||||
|
int scaledWidth, int scaledHeight, FFmpegPixelFormat scaledPixelFormat, FFmpegScalingQuality qualityFlags,
|
||||||
|
out IntPtr handle);
|
||||||
|
|
||||||
|
[DllImport(LibraryName, EntryPoint = "remove_video_scaler", CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
public static extern void RemoveVideoScaler(IntPtr handle);
|
||||||
|
}
|
||||||
|
}
|
14
IPCamLib/FFMPEG/IDecodedVideoFrame.cs
Normal file
14
IPCamLib/FFMPEG/IDecodedVideoFrame.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* Source code originally from RtspClientSharp's player example:
|
||||||
|
* https://github.com/BogdanovKirill/RtspClientSharp
|
||||||
|
*/
|
||||||
|
// ReSharper disable All
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace IPCamLib.FFMPEG
|
||||||
|
{
|
||||||
|
internal interface IDecodedVideoFrame
|
||||||
|
{
|
||||||
|
void TransformTo(IntPtr buffer, int bufferStride, TransformParameters transformParameters);
|
||||||
|
}
|
||||||
|
}
|
14
IPCamLib/FFMPEG/PixelFormat.cs
Normal file
14
IPCamLib/FFMPEG/PixelFormat.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* Source code originally from RtspClientSharp's player example:
|
||||||
|
* https://github.com/BogdanovKirill/RtspClientSharp
|
||||||
|
*/
|
||||||
|
// ReSharper disable All
|
||||||
|
namespace IPCamLib.FFMPEG
|
||||||
|
{
|
||||||
|
internal enum PixelFormat
|
||||||
|
{
|
||||||
|
Grayscale,
|
||||||
|
Bgr24,
|
||||||
|
Bgra32,
|
||||||
|
}
|
||||||
|
}
|
14
IPCamLib/FFMPEG/ScalingPolicy.cs
Normal file
14
IPCamLib/FFMPEG/ScalingPolicy.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* Source code originally from RtspClientSharp's player example:
|
||||||
|
* https://github.com/BogdanovKirill/RtspClientSharp
|
||||||
|
*/
|
||||||
|
// ReSharper disable All
|
||||||
|
namespace IPCamLib.FFMPEG
|
||||||
|
{
|
||||||
|
internal enum ScalingPolicy
|
||||||
|
{
|
||||||
|
Auto,
|
||||||
|
Stretch,
|
||||||
|
RespectAspectRatio
|
||||||
|
}
|
||||||
|
}
|
15
IPCamLib/FFMPEG/ScalingQuality.cs
Normal file
15
IPCamLib/FFMPEG/ScalingQuality.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* Source code originally from RtspClientSharp's player example:
|
||||||
|
* https://github.com/BogdanovKirill/RtspClientSharp
|
||||||
|
*/
|
||||||
|
// ReSharper disable All
|
||||||
|
namespace IPCamLib.FFMPEG
|
||||||
|
{
|
||||||
|
internal enum ScalingQuality
|
||||||
|
{
|
||||||
|
Nearest,
|
||||||
|
Bilinear,
|
||||||
|
FastBilinear,
|
||||||
|
Bicubic
|
||||||
|
}
|
||||||
|
}
|
59
IPCamLib/FFMPEG/TransformParameters.cs
Normal file
59
IPCamLib/FFMPEG/TransformParameters.cs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* Source code originally from RtspClientSharp's player example:
|
||||||
|
* https://github.com/BogdanovKirill/RtspClientSharp
|
||||||
|
*/
|
||||||
|
// ReSharper disable All
|
||||||
|
using System.Drawing;
|
||||||
|
|
||||||
|
namespace IPCamLib.FFMPEG
|
||||||
|
{
|
||||||
|
internal class TransformParameters
|
||||||
|
{
|
||||||
|
public RectangleF RegionOfInterest { get; }
|
||||||
|
|
||||||
|
public Size TargetFrameSize { get; }
|
||||||
|
|
||||||
|
public ScalingPolicy ScalePolicy { get; }
|
||||||
|
|
||||||
|
public PixelFormat TargetFormat { get; }
|
||||||
|
|
||||||
|
public ScalingQuality ScaleQuality { get; }
|
||||||
|
|
||||||
|
public TransformParameters(RectangleF regionOfInterest, Size targetFrameSize, ScalingPolicy scalePolicy,
|
||||||
|
PixelFormat targetFormat, ScalingQuality scaleQuality)
|
||||||
|
{
|
||||||
|
RegionOfInterest = regionOfInterest;
|
||||||
|
TargetFrameSize = targetFrameSize;
|
||||||
|
TargetFormat = targetFormat;
|
||||||
|
ScaleQuality = scaleQuality;
|
||||||
|
ScalePolicy = scalePolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool Equals(TransformParameters other)
|
||||||
|
{
|
||||||
|
return RegionOfInterest.Equals(other.RegionOfInterest) &&
|
||||||
|
TargetFrameSize.Equals(other.TargetFrameSize) &&
|
||||||
|
TargetFormat == other.TargetFormat && ScaleQuality == other.ScaleQuality;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(null, obj)) return false;
|
||||||
|
if (ReferenceEquals(this, obj)) return true;
|
||||||
|
if (obj.GetType() != GetType()) return false;
|
||||||
|
return Equals((TransformParameters) obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
var hashCode = RegionOfInterest.GetHashCode();
|
||||||
|
hashCode = (hashCode * 397) ^ TargetFrameSize.GetHashCode();
|
||||||
|
hashCode = (hashCode * 397) ^ (int) TargetFormat;
|
||||||
|
hashCode = (hashCode * 397) ^ (int) ScaleQuality;
|
||||||
|
return hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
IPCamLib/ICamera.cs
Normal file
38
IPCamLib/ICamera.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace IPCamLib
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Receives frames from an ICamera.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICameraObserver
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Called when a frame has been decoded from an ICamera.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="image">The decoded image.</param>
|
||||||
|
Task OnFrame(Image image);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abstracts an IP camera stream.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICamera
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Starts receiving frames.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The implementation should continue receiving frames until it is disconnected (if streaming),
|
||||||
|
/// a request fails (if polling) or the cancellationToken is cancelled. Reconnecting is done by
|
||||||
|
/// the caller which will call Fetch again. Do not keep connection-specific state between calls to Fetch.
|
||||||
|
/// Running on a separate thread is also handled by the caller, the returning Task should not complete until disconnected.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="observer">The observer implementation to receive frames.</param>
|
||||||
|
/// <param name="cancellationToken">A CancellationToken which will stop the camera stream when cancelled.</param>
|
||||||
|
Task Fetch(ICameraObserver observer, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@
|
|||||||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||||
<FileAlignment>512</FileAlignment>
|
<FileAlignment>512</FileAlignment>
|
||||||
<Deterministic>true</Deterministic>
|
<Deterministic>true</Deterministic>
|
||||||
|
<LangVersion>9</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<DebugSymbols>true</DebugSymbols>
|
||||||
@ -21,6 +22,9 @@
|
|||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
<ErrorReport>prompt</ErrorReport>
|
<ErrorReport>prompt</ErrorReport>
|
||||||
<WarningLevel>4</WarningLevel>
|
<WarningLevel>4</WarningLevel>
|
||||||
|
<DocumentationFile>bin\Debug\IPCamLib.xml</DocumentationFile>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
<DebugType>pdbonly</DebugType>
|
<DebugType>pdbonly</DebugType>
|
||||||
@ -29,10 +33,13 @@
|
|||||||
<DefineConstants>TRACE</DefineConstants>
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
<ErrorReport>prompt</ErrorReport>
|
<ErrorReport>prompt</ErrorReport>
|
||||||
<WarningLevel>4</WarningLevel>
|
<WarningLevel>4</WarningLevel>
|
||||||
|
<DocumentationFile>bin\Release\IPCamLib.xml</DocumentationFile>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Drawing" />
|
||||||
<Reference Include="System.Xml.Linq" />
|
<Reference Include="System.Xml.Linq" />
|
||||||
<Reference Include="System.Data.DataSetExtensions" />
|
<Reference Include="System.Data.DataSetExtensions" />
|
||||||
<Reference Include="Microsoft.CSharp" />
|
<Reference Include="Microsoft.CSharp" />
|
||||||
@ -41,7 +48,61 @@
|
|||||||
<Reference Include="System.Xml" />
|
<Reference Include="System.Xml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="Base\BaseHTTPStreamCamera.cs" />
|
||||||
|
<Compile Include="Concrete\HTTPMJPEGStreamCamera.cs" />
|
||||||
|
<Compile Include="Concrete\RTSPStreamCamera.cs" />
|
||||||
|
<Compile Include="FFMPEG\DecodedVideoFrame.cs" />
|
||||||
|
<Compile Include="FFMPEG\DecodedVideoFrameParameters.cs" />
|
||||||
|
<Compile Include="FFMPEG\DecoderException.cs" />
|
||||||
|
<Compile Include="FFMPEG\FFmpegDecodedVideoScaler.cs" />
|
||||||
|
<Compile Include="FFMPEG\FFmpegVideoDecoder.cs" />
|
||||||
|
<Compile Include="FFMPEG\FFmpegVideoPInvoke.cs" />
|
||||||
|
<Compile Include="FFMPEG\IDecodedVideoFrame.cs" />
|
||||||
|
<Compile Include="FFMPEG\PixelFormat.cs" />
|
||||||
|
<Compile Include="FFMPEG\ScalingPolicy.cs" />
|
||||||
|
<Compile Include="FFMPEG\ScalingQuality.cs" />
|
||||||
|
<Compile Include="FFMPEG\TransformParameters.cs" />
|
||||||
|
<Compile Include="RetryableCamera.cs" />
|
||||||
|
<Compile Include="ICamera.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FastBitmapLib">
|
||||||
|
<Version>2.0.0</Version>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="RtspClientSharp">
|
||||||
|
<Version>1.3.3</Version>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="avcodec-58.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="avdevice-58.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="avfilter-7.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="avformat-58.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="avutil-56.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="libffmpeghelper.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="postproc-55.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="swresample-3.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="swscale-5.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup />
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
</Project>
|
</Project>
|
2
IPCamLib/IPCamLib.csproj.DotSettings
Normal file
2
IPCamLib/IPCamLib.csproj.DotSettings
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=x64/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
82
IPCamLib/RetryableCamera.cs
Normal file
82
IPCamLib/RetryableCamera.cs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
// ReSharper disable UnusedMember.Global - public API
|
||||||
|
|
||||||
|
namespace IPCamLib
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Receives frames from an ICamera as well as status updates.
|
||||||
|
/// </summary>
|
||||||
|
public interface IRetryableCameraObserver : ICameraObserver
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Called when a new Fetch attempt has started.
|
||||||
|
/// </summary>
|
||||||
|
Task OnFetch();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the stream was disconnected.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exception">Contains the exception, if any occured. Null for graceful disconnects and timeouts.</param>
|
||||||
|
/// <param name="retryDelay">The delay until the next Fetch is attempted.</param>
|
||||||
|
Task OnDisconnected(Exception exception, TimeSpan retryDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implements retry logic for an ICamera instance.
|
||||||
|
/// </summary>
|
||||||
|
public class RetryableCamera
|
||||||
|
{
|
||||||
|
private readonly ICamera camera;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of a RetryableCamera.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="camera">The camera instance to fetch from.</param>
|
||||||
|
public RetryableCamera(ICamera camera)
|
||||||
|
{
|
||||||
|
this.camera = camera;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts receiving frames and will retry when disconnected.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="observer">The observer implementation to receive frames and status updates.</param>
|
||||||
|
/// <param name="cancellationToken">A CancellationToken which will stop the camera stream when cancelled.</param>
|
||||||
|
public async Task Fetch(IRetryableCameraObserver observer, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// TODO incremental back-off?
|
||||||
|
var retryDelay = TimeSpan.FromSeconds(5);
|
||||||
|
var exception = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await observer.OnFetch();
|
||||||
|
await camera.Fetch(observer, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
// Empty by design
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
await observer.OnDisconnected(e, retryDelay);
|
||||||
|
exception = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!exception && !cancellationToken.IsCancellationRequested)
|
||||||
|
await observer.OnDisconnected(null, retryDelay);
|
||||||
|
|
||||||
|
await Task.Delay(retryDelay, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
IPCamLib/avcodec-58.dll
Normal file
BIN
IPCamLib/avcodec-58.dll
Normal file
Binary file not shown.
BIN
IPCamLib/avdevice-58.dll
Normal file
BIN
IPCamLib/avdevice-58.dll
Normal file
Binary file not shown.
BIN
IPCamLib/avfilter-7.dll
Normal file
BIN
IPCamLib/avfilter-7.dll
Normal file
Binary file not shown.
BIN
IPCamLib/avformat-58.dll
Normal file
BIN
IPCamLib/avformat-58.dll
Normal file
Binary file not shown.
BIN
IPCamLib/avutil-56.dll
Normal file
BIN
IPCamLib/avutil-56.dll
Normal file
Binary file not shown.
BIN
IPCamLib/libffmpeghelper.dll
Normal file
BIN
IPCamLib/libffmpeghelper.dll
Normal file
Binary file not shown.
BIN
IPCamLib/postproc-55.dll
Normal file
BIN
IPCamLib/postproc-55.dll
Normal file
Binary file not shown.
BIN
IPCamLib/swresample-3.dll
Normal file
BIN
IPCamLib/swresample-3.dll
Normal file
Binary file not shown.
BIN
IPCamLib/swscale-5.dll
Normal file
BIN
IPCamLib/swscale-5.dll
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user