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