diff --git a/IPCamAppBar.sln.DotSettings b/IPCamAppBar.sln.DotSettings
index 20ccd82..82948d8 100644
--- a/IPCamAppBar.sln.DotSettings
+++ b/IPCamAppBar.sln.DotSettings
@@ -1,5 +1,9 @@
EOI
+ HTTP
+ HTTPMJPEG
MJPEG
+ RTSP
SOI
- URL
\ No newline at end of file
+ URL
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
\ No newline at end of file
diff --git a/IPCamAppBar/CameraStream.cs b/IPCamAppBar/CameraStream.cs
deleted file mode 100644
index 2c1b8fd..0000000
--- a/IPCamAppBar/CameraStream.cs
+++ /dev/null
@@ -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();
- }
- }
-}
diff --git a/IPCamAppBar/CameraView.Designer.cs b/IPCamAppBar/CameraView.Designer.cs
index 7a3d2d9..de2c2e3 100644
--- a/IPCamAppBar/CameraView.Designer.cs
+++ b/IPCamAppBar/CameraView.Designer.cs
@@ -28,11 +28,9 @@
///
private void InitializeComponent()
{
- this.components = new System.ComponentModel.Container();
this.ConnectingLabel = new System.Windows.Forms.Label();
this.StreamView = new System.Windows.Forms.PictureBox();
this.IssueLabel = new System.Windows.Forms.Label();
- this.NoDataTimer = new System.Windows.Forms.Timer(this.components);
((System.ComponentModel.ISupportInitialize)(this.StreamView)).BeginInit();
this.SuspendLayout();
//
@@ -73,11 +71,6 @@
this.IssueLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
this.IssueLabel.Visible = false;
//
- // NoDataTimer
- //
- this.NoDataTimer.Interval = 1000;
- this.NoDataTimer.Tick += new System.EventHandler(this.NoDataTimer_Tick);
- //
// CameraView
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
@@ -98,6 +91,5 @@
private System.Windows.Forms.Label ConnectingLabel;
private System.Windows.Forms.PictureBox StreamView;
private System.Windows.Forms.Label IssueLabel;
- private System.Windows.Forms.Timer NoDataTimer;
}
}
diff --git a/IPCamAppBar/CameraView.cs b/IPCamAppBar/CameraView.cs
index f4f554f..4446ee0 100644
--- a/IPCamAppBar/CameraView.cs
+++ b/IPCamAppBar/CameraView.cs
@@ -1,104 +1,139 @@
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
+using System.Threading;
+using System.Threading.Tasks;
using System.Windows.Forms;
-using DateTime = System.DateTime;
+using FastBitmapLib;
+using IPCamLib;
namespace IPCamAppBar
{
- public partial class CameraView : UserControl
+ public partial class CameraView : UserControl, IRetryableCameraObserver
{
- private DateTime lastFrameTime;
-
-
- public CameraView(string url)
+ private readonly bool overlayDateTime;
+ private Bitmap viewBitmap;
+
+ public CameraView(ICamera camera, bool overlayDateTime)
{
+ this.overlayDateTime = overlayDateTime;
InitializeComponent();
- var cameraStream = new CameraMJPEGStream();
- cameraStream.Frame += CameraStreamOnFrame;
- cameraStream.StreamException += CameraStreamOnStreamException;
- cameraStream.Start(url);
-
- Disposed += (sender, args) =>
+ var streamCancellationTokenSource = new CancellationTokenSource();
+ Task.Run(async () =>
{
- cameraStream.Dispose();
+ var retryableCamera = new RetryableCamera(camera);
+ await retryableCamera.Fetch(this, streamCancellationTokenSource.Token);
+ });
+
+ Disposed += (_, _) =>
+ {
+ streamCancellationTokenSource.Cancel();
};
}
- private void CameraStreamOnFrame(object sender, FrameEventArgs args)
+
+ private Bitmap resizedBitmap;
+
+ public Task OnFrame(Image image)
{
- // The event comes from a background thread, so if needed invoke it on the main thread
- if (InvokeRequired)
+ if (overlayDateTime || image is not Bitmap bitmap || image.Width != Width || image.Height != Height)
{
- Invoke(new Action(() => { CameraStreamOnFrame(sender, args); }));
- return;
+ resizedBitmap ??= new Bitmap(Width, Height);
+
+ using var graphics = Graphics.FromImage(resizedBitmap);
+ graphics.SmoothingMode = SmoothingMode.AntiAlias;
+ graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
+ graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
+
+ graphics.DrawImage(image, 0, 0, resizedBitmap.Width, resizedBitmap.Height);
+
+ if (overlayDateTime)
+ {
+ using (var path = new GraphicsPath())
+ {
+ path.AddString(
+ DateTime.Now.ToString("G"),
+ FontFamily.GenericSansSerif,
+ (int) FontStyle.Regular,
+ graphics.DpiY * 14 / 72,
+ Rectangle.Inflate(new Rectangle(0, 0, resizedBitmap.Width, resizedBitmap.Height), -4, -4),
+ new StringFormat
+ {
+ Alignment = StringAlignment.Far,
+ LineAlignment = StringAlignment.Far
+ });
+
+ graphics.DrawPath(new Pen(Color.Black, 3), path);
+ graphics.FillPath(Brushes.White, path);
+ }
+ }
+
+ graphics.Flush();
+ bitmap = resizedBitmap;
}
+ Invoke(new Action(() => { InvokeFrame(bitmap); }));
+ return Task.CompletedTask;
+ }
+
+
+ public Task OnFetch()
+ {
+ Invoke(new Action(InvokeFetch));
+ return Task.CompletedTask;
+ }
+
+
+ public Task OnDisconnected(Exception exception, TimeSpan retryDelay)
+ {
+ Invoke(new Action(() => { InvokeDisconnected(exception, retryDelay); }));
+ return Task.CompletedTask;
+ }
+
+
+ private void InvokeFrame(Bitmap bitmap)
+ {
+ if (IsDisposed)
+ return;
+
ConnectingLabel.Visible = false;
StreamView.Visible = true;
IssueLabel.Visible = false;
- lastFrameTime = DateTime.Now;
- NoDataTimer.Start();
- var viewImage = new Bitmap(Width, Height);
- using (var graphics = Graphics.FromImage(viewImage))
+ if (viewBitmap == null)
{
- graphics.SmoothingMode = SmoothingMode.AntiAlias;
- graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
- graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
-
- graphics.DrawImage(args.Image, 0, 0, viewImage.Width, viewImage.Height);
-
- using (var path = new GraphicsPath())
- {
- path.AddString(
- lastFrameTime.ToString("G"),
- FontFamily.GenericSansSerif,
- (int)FontStyle.Regular,
- graphics.DpiY * 14 / 72,
- Rectangle.Inflate(new Rectangle(0, 0, viewImage.Width, viewImage.Height), -4, -4),
- new StringFormat
- {
- Alignment = StringAlignment.Far,
- LineAlignment = StringAlignment.Far
- });
-
- graphics.DrawPath(new Pen(Color.Black, 3), path);
- graphics.FillPath(Brushes.White, path);
- }
-
- graphics.Flush();
+ viewBitmap = new Bitmap(Width, Height);
+ StreamView.Image = viewBitmap;
}
- var oldImage = StreamView.Image;
- StreamView.Image = viewImage;
- oldImage?.Dispose();
+ using (var fastViewBitmap = viewBitmap.FastLock())
+ {
+ fastViewBitmap.CopyRegion(bitmap, new Rectangle(Point.Empty, bitmap.Size), new Rectangle(Point.Empty, viewBitmap.Size));
+ }
+
+ StreamView.Invalidate();
}
-
-
- private void CameraStreamOnStreamException(object sender, StreamExceptionEventArgs args)
+
+
+ private void InvokeFetch()
{
- if (InvokeRequired)
- {
- Invoke(new Action(() => { CameraStreamOnStreamException(sender, args); }));
+ if (IsDisposed)
return;
- }
- IssueLabel.Text = args.Exception.Message;
+ IssueLabel.Text = "Connecting...";
IssueLabel.Visible = true;
IssueLabel.BringToFront();
}
-
-
- private void NoDataTimer_Tick(object sender, EventArgs e)
+
+
+ private void InvokeDisconnected(Exception exception, TimeSpan retryDelay)
{
- var timeSinceLastFrame = DateTime.Now - lastFrameTime;
- if (timeSinceLastFrame.TotalSeconds < 10)
+ if (IsDisposed)
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.BringToFront();
}
diff --git a/IPCamAppBar/CameraView.resx b/IPCamAppBar/CameraView.resx
index 0c1ffce..1af7de1 100644
--- a/IPCamAppBar/CameraView.resx
+++ b/IPCamAppBar/CameraView.resx
@@ -117,7 +117,4 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
- 17, 17
-
\ No newline at end of file
diff --git a/IPCamAppBar/Config.cs b/IPCamAppBar/Config.cs
index 103a042..451e041 100644
--- a/IPCamAppBar/Config.cs
+++ b/IPCamAppBar/Config.cs
@@ -4,7 +4,7 @@ namespace IPCamAppBar
{
public class Config
{
- public ConfigAppBar AppBar { get; } = new ConfigAppBar();
+ public ConfigAppBar AppBar { get; } = new();
public List Cameras { get; set; }
}
@@ -22,13 +22,24 @@ namespace IPCamAppBar
public int Monitor { get; set; }
public ConfigSide Side { 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 ConfigCameraType Type { get; set; }
public string URL { get; set; }
public int Width { get; set; }
public int Height { get; set; }
+ public bool OverlayDateTime { get; set; }
}
}
diff --git a/IPCamAppBar/IPCamAppBar.csproj b/IPCamAppBar/IPCamAppBar.csproj
index 9e5a688..8fc3774 100644
--- a/IPCamAppBar/IPCamAppBar.csproj
+++ b/IPCamAppBar/IPCamAppBar.csproj
@@ -12,9 +12,10 @@
512
true
true
+ 9
- AnyCPU
+ x64
true
full
false
@@ -22,6 +23,7 @@
DEBUG;TRACE
prompt
4
+ false
AnyCPU
@@ -34,8 +36,11 @@
false
-
- packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll
+
+ ..\packages\FastBitmapLib.2.0.0\lib\net452\FastBitmapLib.dll
+
+
+ ..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll
@@ -51,8 +56,6 @@
-
-
UserControl
@@ -100,5 +103,11 @@
+
+
+ {FD0886FF-F274-4EFC-8A56-F82A4641C3FD}
+ IPCamLib
+
+
\ No newline at end of file
diff --git a/IPCamAppBar/MainForm.cs b/IPCamAppBar/MainForm.cs
index ce1f956..66be7a0 100644
--- a/IPCamAppBar/MainForm.cs
+++ b/IPCamAppBar/MainForm.cs
@@ -1,9 +1,10 @@
using System;
-using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Windows.Forms;
+using IPCamLib;
+using IPCamLib.Concrete;
using Newtonsoft.Json;
namespace IPCamAppBar
@@ -32,6 +33,11 @@ namespace IPCamAppBar
appBar = new AppBar(Handle);
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);
}
@@ -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,
- Height = camera.Height
+ ConfigCameraType.HTTPMJPEG => new HTTPMJPEGStreamCamera(new Uri(configCamera.URL)),
+ 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);
diff --git a/IPCamAppBar/config.json b/IPCamAppBar/config.json
index 3cd5752..be0f0d0 100644
--- a/IPCamAppBar/config.json
+++ b/IPCamAppBar/config.json
@@ -2,19 +2,24 @@
"AppBar": {
"Monitor": 0,
"Side": "Top",
- "Size": 480
+ "Size": 480,
+ "Spacing": 2
},
"Cameras": [
{
- "URL": "http://username:password@ipcamera1/videostream.cgi",
+ "Type": "RTSP",
+ "URL": "rtsp://username:password@ipcamera1/",
"Width": 480,
- "Height": 360
+ "Height": 360,
+ "OverlayDateTime": false
},
{
+ "Type": "HTTPMJPEG",
"URL": "http://username:password@ipcamera2/videostream.cgi",
"Width": 480,
- "Height": 360
+ "Height": 360,
+ "OverlayDateTime": true
}
]
}
\ No newline at end of file
diff --git a/IPCamAppBar/packages.config b/IPCamAppBar/packages.config
index a75532f..5ad419e 100644
--- a/IPCamAppBar/packages.config
+++ b/IPCamAppBar/packages.config
@@ -1,4 +1,5 @@
-
+
+
\ No newline at end of file
diff --git a/IPCamLib/Base/BaseHTTPStreamCamera.cs b/IPCamLib/Base/BaseHTTPStreamCamera.cs
new file mode 100644
index 0000000..2a3b3f5
--- /dev/null
+++ b/IPCamLib/Base/BaseHTTPStreamCamera.cs
@@ -0,0 +1,73 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace IPCamLib.Base
+{
+ ///
+ /// Abstract base class for IP cameras reading an HTTP stream.
+ ///
+ public abstract class BaseHTTPStreamCamera : ICamera
+ {
+ private readonly Uri streamUri;
+
+
+ /// The URI to the camera stream.
+ /// Can include basic credentials in the standard 'username:password@' format.
+ protected BaseHTTPStreamCamera(Uri streamUri)
+ {
+ this.streamUri = streamUri;
+ }
+
+
+ ///
+ 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();
+ }
+ }
+
+
+ ///
+ /// Implement in concrete descendants to continuously read frames from the HTTP stream.
+ ///
+ /// The observer implementation passed to Fetch which should receive the decoded frames.
+ /// The HTTP response stream.
+ /// Stop reading frames when this token is cancelled.
+ protected abstract Task ReadFrames(ICameraObserver observer, Stream stream, CancellationToken cancellationToken);
+ }
+}
diff --git a/IPCamAppBar/CameraMJPEGStream.cs b/IPCamLib/Concrete/HTTPMJPEGStreamCamera.cs
similarity index 57%
rename from IPCamAppBar/CameraMJPEGStream.cs
rename to IPCamLib/Concrete/HTTPMJPEGStreamCamera.cs
index b2cc18b..10c2a07 100644
--- a/IPCamAppBar/CameraMJPEGStream.cs
+++ b/IPCamLib/Concrete/HTTPMJPEGStreamCamera.cs
@@ -3,10 +3,16 @@ using System.Drawing;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using IPCamLib.Base;
-namespace IPCamAppBar
+// ReSharper disable UnusedMember.Global - public API
+
+namespace IPCamLib.Concrete
{
- internal class CameraMJPEGStream : CameraStream
+ ///
+ /// Implements the ICamera interface for IP cameras exposing an MJPEG stream over HTTP.
+ ///
+ public class HTTPMJPEGStreamCamera : BaseHTTPStreamCamera
{
// MJPEG decoding is a variation on https://github.com/arndre/MjpegDecoder
private static readonly byte[] JpegSOI = { 0xff, 0xd8 }; // start of image bytes
@@ -16,7 +22,17 @@ namespace IPCamAppBar
private const int MaxBufferSize = 1024 * 1024 * 10;
- protected override async Task ReadFrames(Stream stream, CancellationToken cancellationToken)
+ ///
+ /// Creates a new instance for an IP camera exposing an MJPEG stream over HTTP.
+ ///
+ ///
+ public HTTPMJPEGStreamCamera(Uri streamUri) : base(streamUri)
+ {
+ }
+
+
+ ///
+ protected override async Task ReadFrames(ICameraObserver observer, Stream stream, CancellationToken cancellationToken)
{
var buffer = new byte[ChunkSize];
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)
throw new EndOfStreamException();
@@ -61,7 +81,7 @@ namespace IPCamAppBar
if (!startOfImage.HasValue)
{
- var index = buffer.Find(JpegSOI, bufferPosition);
+ var index = buffer.IndexOf(JpegSOI, bufferPosition);
if (index == -1)
{
// No start of image yet, we need to buffer more
@@ -72,7 +92,7 @@ namespace IPCamAppBar
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)
{
// 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;
- HandleFrame(buffer, startOfImage.Value, endOfImage);
- ResetBuffer(endOfImage);
- }
- }
-
- protected void HandleFrame(byte[] buffer, int start, int end)
- {
- using (var image = new Bitmap(new MemoryStream(buffer, start, end - start)))
- {
- OnFrame(new FrameEventArgs
+ using (var image = new Bitmap(new MemoryStream(buffer, startOfImage.Value, endOfImage - startOfImage.Value)))
{
- Image = image
- });
+ await observer.OnFrame(image);
+ }
+
+ ResetBuffer(endOfImage);
+ frameTimeoutCancellationTokenSource.CancelAfter(frameTimeout);
}
}
}
+
+
+ internal static class BufferExtensions
+ {
+ public static int IndexOf(this byte[] buffer, byte[] pattern, int limit = int.MaxValue, int startAt = 0)
+ {
+ var patternIndex = 0;
+ int bufferIndex;
+
+ 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;
+ }
+ }
}
diff --git a/IPCamLib/Concrete/RTSPStreamCamera.cs b/IPCamLib/Concrete/RTSPStreamCamera.cs
new file mode 100644
index 0000000..25f1ed0
--- /dev/null
+++ b/IPCamLib/Concrete/RTSPStreamCamera.cs
@@ -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
+{
+ ///
+ /// Implements the ICamera interface for IP cameras exposing an RTSP stream.
+ ///
+ public class RTSPStreamCamera : ICamera
+ {
+ private readonly Uri streamUri;
+ private readonly Dictionary videoDecodersMap = new();
+ private readonly Bitmap bitmap;
+ private readonly TransformParameters transformParameters;
+
+ /// The URI to the camera stream.
+ /// Can include basic credentials in the standard 'username:password@' format.
+ /// The width of the viewport
+ /// The height of the viewport
+ 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);
+ }
+
+
+ ///
+ 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))
+ };
+ }
+ }
+}
diff --git a/IPCamLib/FFMPEG/DecodedVideoFrame.cs b/IPCamLib/FFMPEG/DecodedVideoFrame.cs
new file mode 100644
index 0000000..b3727dd
--- /dev/null
+++ b/IPCamLib/FFMPEG/DecodedVideoFrame.cs
@@ -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 _transformAction;
+
+ public DecodedVideoFrame(Action transformAction)
+ {
+ _transformAction = transformAction;
+ }
+
+ public void TransformTo(IntPtr buffer, int bufferStride, TransformParameters transformParameters)
+ {
+ _transformAction(buffer, bufferStride, transformParameters);
+ }
+ }
+}
\ No newline at end of file
diff --git a/IPCamLib/FFMPEG/DecodedVideoFrameParameters.cs b/IPCamLib/FFMPEG/DecodedVideoFrameParameters.cs
new file mode 100644
index 0000000..4356a2b
--- /dev/null
+++ b/IPCamLib/FFMPEG/DecodedVideoFrameParameters.cs
@@ -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;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/IPCamLib/FFMPEG/DecoderException.cs b/IPCamLib/FFMPEG/DecoderException.cs
new file mode 100644
index 0000000..004632f
--- /dev/null
+++ b/IPCamLib/FFMPEG/DecoderException.cs
@@ -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)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/IPCamLib/FFMPEG/FFmpegDecodedVideoScaler.cs b/IPCamLib/FFMPEG/FFmpegDecodedVideoScaler.cs
new file mode 100644
index 0000000..1decc7c
--- /dev/null
+++ b/IPCamLib/FFMPEG/FFmpegDecodedVideoScaler.cs
@@ -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();
+ }
+
+ ///
+ 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));
+ }
+ }
+}
\ No newline at end of file
diff --git a/IPCamLib/FFMPEG/FFmpegVideoDecoder.cs b/IPCamLib/FFMPEG/FFmpegVideoDecoder.cs
new file mode 100644
index 0000000..18e139a
--- /dev/null
+++ b/IPCamLib/FFMPEG/FFmpegVideoDecoder.cs
@@ -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 _scalersMap =
+ new Dictionary();
+
+ 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}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/IPCamLib/FFMPEG/FFmpegVideoPInvoke.cs b/IPCamLib/FFMPEG/FFmpegVideoPInvoke.cs
new file mode 100644
index 0000000..0b037eb
--- /dev/null
+++ b/IPCamLib/FFMPEG/FFmpegVideoPInvoke.cs
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/IPCamLib/FFMPEG/IDecodedVideoFrame.cs b/IPCamLib/FFMPEG/IDecodedVideoFrame.cs
new file mode 100644
index 0000000..f10438e
--- /dev/null
+++ b/IPCamLib/FFMPEG/IDecodedVideoFrame.cs
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/IPCamLib/FFMPEG/PixelFormat.cs b/IPCamLib/FFMPEG/PixelFormat.cs
new file mode 100644
index 0000000..e4cb9be
--- /dev/null
+++ b/IPCamLib/FFMPEG/PixelFormat.cs
@@ -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,
+ }
+}
\ No newline at end of file
diff --git a/IPCamLib/FFMPEG/ScalingPolicy.cs b/IPCamLib/FFMPEG/ScalingPolicy.cs
new file mode 100644
index 0000000..51b6cce
--- /dev/null
+++ b/IPCamLib/FFMPEG/ScalingPolicy.cs
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/IPCamLib/FFMPEG/ScalingQuality.cs b/IPCamLib/FFMPEG/ScalingQuality.cs
new file mode 100644
index 0000000..f798f89
--- /dev/null
+++ b/IPCamLib/FFMPEG/ScalingQuality.cs
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/IPCamLib/FFMPEG/TransformParameters.cs b/IPCamLib/FFMPEG/TransformParameters.cs
new file mode 100644
index 0000000..713eb96
--- /dev/null
+++ b/IPCamLib/FFMPEG/TransformParameters.cs
@@ -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;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/IPCamLib/ICamera.cs b/IPCamLib/ICamera.cs
new file mode 100644
index 0000000..d59f786
--- /dev/null
+++ b/IPCamLib/ICamera.cs
@@ -0,0 +1,38 @@
+using System.Drawing;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace IPCamLib
+{
+ ///
+ /// Receives frames from an ICamera.
+ ///
+ public interface ICameraObserver
+ {
+ ///
+ /// Called when a frame has been decoded from an ICamera.
+ ///
+ /// The decoded image.
+ Task OnFrame(Image image);
+ }
+
+
+ ///
+ /// Abstracts an IP camera stream.
+ ///
+ public interface ICamera
+ {
+ ///
+ /// Starts receiving frames.
+ ///
+ ///
+ /// 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.
+ ///
+ /// The observer implementation to receive frames.
+ /// A CancellationToken which will stop the camera stream when cancelled.
+ Task Fetch(ICameraObserver observer, CancellationToken cancellationToken);
+ }
+}
diff --git a/IPCamLib/IPCamLib.csproj b/IPCamLib/IPCamLib.csproj
index 3da1379..7d35cf7 100644
--- a/IPCamLib/IPCamLib.csproj
+++ b/IPCamLib/IPCamLib.csproj
@@ -12,6 +12,7 @@
v4.7.2
512
true
+ 9
true
@@ -21,6 +22,9 @@
DEBUG;TRACE
prompt
4
+ bin\Debug\IPCamLib.xml
+ true
+ x64
pdbonly
@@ -29,10 +33,13 @@
TRACE
prompt
4
+ bin\Release\IPCamLib.xml
+ true
+
@@ -41,7 +48,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2.0.0
+
+
+ 1.3.3
+
+
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+
\ No newline at end of file
diff --git a/IPCamLib/IPCamLib.csproj.DotSettings b/IPCamLib/IPCamLib.csproj.DotSettings
new file mode 100644
index 0000000..dec168f
--- /dev/null
+++ b/IPCamLib/IPCamLib.csproj.DotSettings
@@ -0,0 +1,2 @@
+
+ True
\ No newline at end of file
diff --git a/IPCamLib/RetryableCamera.cs b/IPCamLib/RetryableCamera.cs
new file mode 100644
index 0000000..844e4f8
--- /dev/null
+++ b/IPCamLib/RetryableCamera.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+// ReSharper disable UnusedMember.Global - public API
+
+namespace IPCamLib
+{
+ ///
+ /// Receives frames from an ICamera as well as status updates.
+ ///
+ public interface IRetryableCameraObserver : ICameraObserver
+ {
+ ///
+ /// Called when a new Fetch attempt has started.
+ ///
+ Task OnFetch();
+
+ ///
+ /// Called when the stream was disconnected.
+ ///
+ /// Contains the exception, if any occured. Null for graceful disconnects and timeouts.
+ /// The delay until the next Fetch is attempted.
+ Task OnDisconnected(Exception exception, TimeSpan retryDelay);
+ }
+
+
+ ///
+ /// Implements retry logic for an ICamera instance.
+ ///
+ public class RetryableCamera
+ {
+ private readonly ICamera camera;
+
+
+ ///
+ /// Creates a new instance of a RetryableCamera.
+ ///
+ /// The camera instance to fetch from.
+ public RetryableCamera(ICamera camera)
+ {
+ this.camera = camera;
+ }
+
+
+ ///
+ /// Starts receiving frames and will retry when disconnected.
+ ///
+ /// The observer implementation to receive frames and status updates.
+ /// A CancellationToken which will stop the camera stream when cancelled.
+ 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);
+ }
+ }
+ }
+}
diff --git a/IPCamLib/avcodec-58.dll b/IPCamLib/avcodec-58.dll
new file mode 100644
index 0000000..1f139f2
Binary files /dev/null and b/IPCamLib/avcodec-58.dll differ
diff --git a/IPCamLib/avdevice-58.dll b/IPCamLib/avdevice-58.dll
new file mode 100644
index 0000000..f8500c5
Binary files /dev/null and b/IPCamLib/avdevice-58.dll differ
diff --git a/IPCamLib/avfilter-7.dll b/IPCamLib/avfilter-7.dll
new file mode 100644
index 0000000..efd4f88
Binary files /dev/null and b/IPCamLib/avfilter-7.dll differ
diff --git a/IPCamLib/avformat-58.dll b/IPCamLib/avformat-58.dll
new file mode 100644
index 0000000..3c4a26c
Binary files /dev/null and b/IPCamLib/avformat-58.dll differ
diff --git a/IPCamLib/avutil-56.dll b/IPCamLib/avutil-56.dll
new file mode 100644
index 0000000..f06743a
Binary files /dev/null and b/IPCamLib/avutil-56.dll differ
diff --git a/IPCamLib/libffmpeghelper.dll b/IPCamLib/libffmpeghelper.dll
new file mode 100644
index 0000000..1149eb2
Binary files /dev/null and b/IPCamLib/libffmpeghelper.dll differ
diff --git a/IPCamLib/postproc-55.dll b/IPCamLib/postproc-55.dll
new file mode 100644
index 0000000..0532010
Binary files /dev/null and b/IPCamLib/postproc-55.dll differ
diff --git a/IPCamLib/swresample-3.dll b/IPCamLib/swresample-3.dll
new file mode 100644
index 0000000..aa1fecf
Binary files /dev/null and b/IPCamLib/swresample-3.dll differ
diff --git a/IPCamLib/swscale-5.dll b/IPCamLib/swscale-5.dll
new file mode 100644
index 0000000..d9ea87a
Binary files /dev/null and b/IPCamLib/swscale-5.dll differ