Added RTSP camera support
This commit is contained in:
parent
9b63740d2a
commit
6284bd0609
|
@ -1,5 +1,9 @@
|
|||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EOI/@EntryIndexedValue">EOI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HTTP/@EntryIndexedValue">HTTP</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HTTPMJPEG/@EntryIndexedValue">HTTPMJPEG</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MJPEG/@EntryIndexedValue">MJPEG</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RTSP/@EntryIndexedValue">RTSP</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SOI/@EntryIndexedValue">SOI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=URL/@EntryIndexedValue">URL</s:String></wpf:ResourceDictionary>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=URL/@EntryIndexedValue">URL</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=EnumMember/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String></wpf:ResourceDictionary>
|
|
@ -1,203 +0,0 @@
|
|||
using System;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IPCamAppBar
|
||||
{
|
||||
public class FrameEventArgs : EventArgs
|
||||
{
|
||||
public Image Image { get; set; }
|
||||
}
|
||||
|
||||
public class StreamExceptionEventArgs : EventArgs
|
||||
{
|
||||
public Exception Exception { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public delegate void FrameEvent(object sender, FrameEventArgs args);
|
||||
public delegate void StreamExceptionEvent(object sender, StreamExceptionEventArgs args);
|
||||
|
||||
|
||||
internal abstract class CameraStream : IDisposable
|
||||
{
|
||||
public event FrameEvent Frame;
|
||||
public event StreamExceptionEvent StreamException;
|
||||
|
||||
private readonly CancellationTokenSource cancelTaskTokenSource = new CancellationTokenSource();
|
||||
private Task streamTask;
|
||||
private DataMonitor dataMonitor;
|
||||
|
||||
protected CameraStream()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public void Start(string url)
|
||||
{
|
||||
if (streamTask != null)
|
||||
throw new InvalidOperationException("CameraStream already started");
|
||||
|
||||
streamTask = Task.Run(() => Fetch(url, cancelTaskTokenSource.Token));
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
cancelTaskTokenSource.Cancel();
|
||||
|
||||
try
|
||||
{
|
||||
streamTask?.Wait();
|
||||
}
|
||||
catch (AggregateException e)
|
||||
{
|
||||
if (e.InnerExceptions.Count == 1 && e.InnerExceptions[0] is TaskCanceledException)
|
||||
return;
|
||||
|
||||
throw;
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
}
|
||||
|
||||
|
||||
private async Task Fetch(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var uri = new Uri(url);
|
||||
var request = WebRequest.CreateHttp(uri);
|
||||
|
||||
if (!string.IsNullOrEmpty(uri.UserInfo))
|
||||
{
|
||||
var parts = uri.UserInfo.Split(':');
|
||||
request.Credentials = new NetworkCredential(parts[0], parts.Length > 1 ? parts[1] : "");
|
||||
}
|
||||
|
||||
request.ReadWriteTimeout = 10;
|
||||
|
||||
try
|
||||
{
|
||||
HttpWebResponse response;
|
||||
|
||||
using (cancellationToken.Register(() => request.Abort(), false))
|
||||
{
|
||||
response = (HttpWebResponse)await request.GetResponseAsync();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
|
||||
if (response.StatusCode != HttpStatusCode.OK)
|
||||
throw new WebException(response.StatusDescription);
|
||||
|
||||
using (var responseStream = response.GetResponseStream())
|
||||
{
|
||||
var dataMonitorCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
dataMonitor = new DataMonitor(dataMonitorCancellationTokenSource, TimeSpan.FromSeconds(15));
|
||||
|
||||
try
|
||||
{
|
||||
await ReadFrames(responseStream, dataMonitorCancellationTokenSource.Token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
if (!dataMonitorCancellationTokenSource.IsCancellationRequested)
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
OnStreamException(new StreamExceptionEventArgs
|
||||
{
|
||||
Exception = e
|
||||
});
|
||||
|
||||
await Task.Delay(5000, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected abstract Task ReadFrames(Stream stream, CancellationToken cancellationToken);
|
||||
|
||||
|
||||
protected virtual void OnFrame(FrameEventArgs args)
|
||||
{
|
||||
dataMonitor.Reset();
|
||||
Frame?.Invoke(this, args);
|
||||
}
|
||||
|
||||
|
||||
protected virtual void OnStreamException(StreamExceptionEventArgs args)
|
||||
{
|
||||
StreamException?.Invoke(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal static class BufferExtensions
|
||||
{
|
||||
public static int Find(this byte[] buffer, byte[] pattern, int limit = int.MaxValue, int startAt = 0)
|
||||
{
|
||||
var patternIndex = 0;
|
||||
var bufferIndex = 0;
|
||||
|
||||
for (bufferIndex = startAt; bufferIndex < buffer.Length && patternIndex < pattern.Length && bufferIndex < limit; bufferIndex++)
|
||||
{
|
||||
if (buffer[bufferIndex] == pattern[patternIndex])
|
||||
{
|
||||
patternIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
patternIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (patternIndex == pattern.Length)
|
||||
return bufferIndex - pattern.Length;
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal class DataMonitor
|
||||
{
|
||||
private readonly CancellationTokenSource cancellationTokenSource;
|
||||
private readonly long timeout;
|
||||
|
||||
private readonly Timer timeoutTimer;
|
||||
|
||||
|
||||
public DataMonitor(CancellationTokenSource cancellationTokenSource, TimeSpan timeout)
|
||||
{
|
||||
this.cancellationTokenSource = cancellationTokenSource;
|
||||
this.timeout = (long)timeout.TotalMilliseconds;
|
||||
|
||||
timeoutTimer = new Timer(Tick, null, this.timeout, -1);
|
||||
}
|
||||
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
timeoutTimer.Change(timeout, -1);
|
||||
}
|
||||
|
||||
|
||||
private void Tick(object state)
|
||||
{
|
||||
cancellationTokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,11 +28,9 @@
|
|||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -117,7 +117,4 @@
|
|||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="NoDataTimer.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
</root>
|
|
@ -4,7 +4,7 @@ namespace IPCamAppBar
|
|||
{
|
||||
public class Config
|
||||
{
|
||||
public ConfigAppBar AppBar { get; } = new ConfigAppBar();
|
||||
public ConfigAppBar AppBar { get; } = new();
|
||||
public List<ConfigCamera> 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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,10 @@
|
|||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
<LangVersion>9</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
|
@ -22,6 +23,7 @@
|
|||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
|
@ -34,8 +36,11 @@
|
|||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
<Reference Include="FastBitmapLib, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\FastBitmapLib.2.0.0\lib\net452\FastBitmapLib.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
|
@ -51,8 +56,6 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="AppBar.cs" />
|
||||
<Compile Include="CameraMJPEGStream.cs" />
|
||||
<Compile Include="CameraStream.cs" />
|
||||
<Compile Include="CameraView.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
|
@ -100,5 +103,11 @@
|
|||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\IPCamLib\IPCamLib.csproj">
|
||||
<Project>{FD0886FF-F274-4EFC-8A56-F82A4641C3FD}</Project>
|
||||
<Name>IPCamLib</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="12.0.2" targetFramework="net472" />
|
||||
<package id="FastBitmapLib" version="2.0.0" targetFramework="net472" />
|
||||
<package id="Newtonsoft.Json" version="13.0.1" targetFramework="net472" />
|
||||
</packages>
|
|
@ -0,0 +1,73 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IPCamLib.Base
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstract base class for IP cameras reading an HTTP stream.
|
||||
/// </summary>
|
||||
public abstract class BaseHTTPStreamCamera : ICamera
|
||||
{
|
||||
private readonly Uri streamUri;
|
||||
|
||||
|
||||
/// <param name="streamUri">The URI to the camera stream.
|
||||
/// Can include basic credentials in the standard 'username:password@' format.</param>
|
||||
protected BaseHTTPStreamCamera(Uri streamUri)
|
||||
{
|
||||
this.streamUri = streamUri;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Fetch(ICameraObserver observer, CancellationToken cancellationToken)
|
||||
{
|
||||
var request = WebRequest.CreateHttp(streamUri);
|
||||
|
||||
if (!string.IsNullOrEmpty(streamUri.UserInfo))
|
||||
{
|
||||
var parts = streamUri.UserInfo.Split(':');
|
||||
request.Credentials = new NetworkCredential(parts[0], parts.Length > 1 ? parts[1] : "");
|
||||
}
|
||||
|
||||
request.ReadWriteTimeout = (int)TimeSpan.FromMinutes(1).TotalMilliseconds;
|
||||
try
|
||||
{
|
||||
HttpWebResponse response;
|
||||
|
||||
using (cancellationToken.Register(() => request.Abort(), false))
|
||||
{
|
||||
response = (HttpWebResponse)await request.GetResponseAsync();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
|
||||
if (response.StatusCode != HttpStatusCode.OK)
|
||||
throw new WebException(response.StatusDescription);
|
||||
|
||||
|
||||
using var responseStream = response.GetResponseStream();
|
||||
await ReadFrames(observer, responseStream, cancellationToken);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
request.Abort();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Implement in concrete descendants to continuously read frames from the HTTP stream.
|
||||
/// </summary>
|
||||
/// <param name="observer">The observer implementation passed to Fetch which should receive the decoded frames.</param>
|
||||
/// <param name="stream">The HTTP response stream.</param>
|
||||
/// <param name="cancellationToken">Stop reading frames when this token is cancelled.</param>
|
||||
protected abstract Task ReadFrames(ICameraObserver observer, Stream stream, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
|
@ -3,10 +3,16 @@ using System.Drawing;
|
|||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using IPCamLib.Base;
|
||||
|
||||
namespace IPCamAppBar
|
||||
// ReSharper disable UnusedMember.Global - public API
|
||||
|
||||
namespace IPCamLib.Concrete
|
||||
{
|
||||
internal class CameraMJPEGStream : CameraStream
|
||||
/// <summary>
|
||||
/// Implements the ICamera interface for IP cameras exposing an MJPEG stream over HTTP.
|
||||
/// </summary>
|
||||
public class HTTPMJPEGStreamCamera : BaseHTTPStreamCamera
|
||||
{
|
||||
// MJPEG decoding is a variation on https://github.com/arndre/MjpegDecoder
|
||||
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)
|
||||
/// <summary>
|
||||
/// Creates a new instance for an IP camera exposing an MJPEG stream over HTTP.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public HTTPMJPEGStreamCamera(Uri streamUri) : base(streamUri)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ReadFrames(ICameraObserver observer, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var buffer = new byte[ChunkSize];
|
||||
var 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FastBitmapLib;
|
||||
using IPCamLib.FFMPEG;
|
||||
using RtspClientSharp;
|
||||
using RtspClientSharp.RawFrames.Video;
|
||||
|
||||
namespace IPCamLib.Concrete
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the ICamera interface for IP cameras exposing an RTSP stream.
|
||||
/// </summary>
|
||||
public class RTSPStreamCamera : ICamera
|
||||
{
|
||||
private readonly Uri streamUri;
|
||||
private readonly Dictionary<FFmpegVideoCodecId, FFmpegVideoDecoder> videoDecodersMap = new();
|
||||
private readonly Bitmap bitmap;
|
||||
private readonly TransformParameters transformParameters;
|
||||
|
||||
/// <param name="streamUri">The URI to the camera stream.
|
||||
/// Can include basic credentials in the standard 'username:password@' format.</param>
|
||||
/// <param name="width">The width of the viewport</param>
|
||||
/// <param name="height">The height of the viewport</param>
|
||||
public RTSPStreamCamera(Uri streamUri, int width, int height)
|
||||
{
|
||||
this.streamUri = streamUri;
|
||||
|
||||
transformParameters = new TransformParameters(RectangleF.Empty, new Size(width, height),
|
||||
ScalingPolicy.Stretch, PixelFormat.Bgra32, ScalingQuality.FastBilinear);
|
||||
|
||||
bitmap = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
|
||||
|
||||
using var fastBitmap = bitmap.FastLock();
|
||||
fastBitmap.Clear(Color.Black);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Fetch(ICameraObserver observer, CancellationToken cancellationToken)
|
||||
{
|
||||
NetworkCredential credentials = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(streamUri.UserInfo))
|
||||
{
|
||||
var parts = streamUri.UserInfo.Split(':');
|
||||
credentials = new NetworkCredential(parts[0], parts.Length > 1 ? parts[1] : "");
|
||||
}
|
||||
else
|
||||
credentials = new NetworkCredential(null, (string)null);
|
||||
|
||||
|
||||
var connectionParameters = new ConnectionParameters(streamUri, credentials)
|
||||
{
|
||||
RtpTransport = RtpTransportProtocol.TCP
|
||||
};
|
||||
|
||||
using var rtspClient = new RtspClient(connectionParameters);
|
||||
|
||||
rtspClient.FrameReceived += (_, rawFrame) =>
|
||||
{
|
||||
if (rawFrame is not RawVideoFrame rawVideoFrame)
|
||||
return;
|
||||
|
||||
var decoder = GetDecoderForFrame(rawVideoFrame);
|
||||
var decodedFrame = decoder.TryDecode(rawVideoFrame);
|
||||
|
||||
if (decodedFrame == null)
|
||||
return;
|
||||
|
||||
using (var fastBitmap = bitmap.FastLock())
|
||||
{
|
||||
decodedFrame.TransformTo(fastBitmap.Scan0, fastBitmap.StrideInBytes, transformParameters);
|
||||
}
|
||||
|
||||
// TODO await
|
||||
observer.OnFrame(bitmap);
|
||||
};
|
||||
|
||||
await rtspClient.ConnectAsync(cancellationToken);
|
||||
await rtspClient.ReceiveAsync(cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private FFmpegVideoDecoder GetDecoderForFrame(RawVideoFrame videoFrame)
|
||||
{
|
||||
var codecId = DetectCodecId(videoFrame);
|
||||
if (videoDecodersMap.TryGetValue(codecId, out var decoder))
|
||||
return decoder;
|
||||
|
||||
decoder = FFmpegVideoDecoder.CreateDecoder(codecId);
|
||||
videoDecodersMap.Add(codecId, decoder);
|
||||
|
||||
return decoder;
|
||||
}
|
||||
|
||||
|
||||
private static FFmpegVideoCodecId DetectCodecId(RawVideoFrame videoFrame)
|
||||
{
|
||||
return videoFrame switch
|
||||
{
|
||||
RawJpegFrame => FFmpegVideoCodecId.MJPEG,
|
||||
RawH264Frame => FFmpegVideoCodecId.H264,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(videoFrame))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Source code originally from RtspClientSharp's player example:
|
||||
* https://github.com/BogdanovKirill/RtspClientSharp
|
||||
*/
|
||||
// ReSharper disable All
|
||||
using System;
|
||||
|
||||
namespace IPCamLib.FFMPEG
|
||||
{
|
||||
internal class DecodedVideoFrame : IDecodedVideoFrame
|
||||
{
|
||||
private readonly Action<IntPtr, int, TransformParameters> _transformAction;
|
||||
|
||||
public DecodedVideoFrame(Action<IntPtr, int, TransformParameters> transformAction)
|
||||
{
|
||||
_transformAction = transformAction;
|
||||
}
|
||||
|
||||
public void TransformTo(IntPtr buffer, int bufferStride, TransformParameters transformParameters)
|
||||
{
|
||||
_transformAction(buffer, bufferStride, transformParameters);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Source code originally from RtspClientSharp's player example:
|
||||
* https://github.com/BogdanovKirill/RtspClientSharp
|
||||
*/
|
||||
// ReSharper disable All
|
||||
using System;
|
||||
|
||||
namespace IPCamLib.FFMPEG
|
||||
{
|
||||
internal class FFmpegDecodedVideoScaler
|
||||
{
|
||||
private const double MaxAspectRatioError = 0.1;
|
||||
private bool _disposed;
|
||||
|
||||
public IntPtr Handle { get; }
|
||||
public int ScaledWidth { get; }
|
||||
public int ScaledHeight { get; }
|
||||
public PixelFormat ScaledPixelFormat { get; }
|
||||
|
||||
private FFmpegDecodedVideoScaler(IntPtr handle, int scaledWidth, int scaledHeight,
|
||||
PixelFormat scaledPixelFormat)
|
||||
{
|
||||
Handle = handle;
|
||||
ScaledWidth = scaledWidth;
|
||||
ScaledHeight = scaledHeight;
|
||||
ScaledPixelFormat = scaledPixelFormat;
|
||||
}
|
||||
|
||||
~FFmpegDecodedVideoScaler()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
/// <exception cref="DecoderException"></exception>
|
||||
public static FFmpegDecodedVideoScaler Create(DecodedVideoFrameParameters decodedVideoFrameParameters,
|
||||
TransformParameters transformParameters)
|
||||
{
|
||||
if (decodedVideoFrameParameters == null)
|
||||
throw new ArgumentNullException(nameof(decodedVideoFrameParameters));
|
||||
if (transformParameters == null)
|
||||
throw new ArgumentNullException(nameof(transformParameters));
|
||||
|
||||
int sourceLeft = 0;
|
||||
int sourceTop = 0;
|
||||
int sourceWidth = decodedVideoFrameParameters.Width;
|
||||
int sourceHeight = decodedVideoFrameParameters.Height;
|
||||
int scaledWidth = decodedVideoFrameParameters.Width;
|
||||
int scaledHeight = decodedVideoFrameParameters.Height;
|
||||
|
||||
if (!transformParameters.RegionOfInterest.IsEmpty)
|
||||
{
|
||||
sourceLeft =
|
||||
(int) (decodedVideoFrameParameters.Width * transformParameters.RegionOfInterest.Left);
|
||||
sourceTop =
|
||||
(int) (decodedVideoFrameParameters.Height * transformParameters.RegionOfInterest.Top);
|
||||
sourceWidth =
|
||||
(int) (decodedVideoFrameParameters.Width * transformParameters.RegionOfInterest.Width);
|
||||
sourceHeight =
|
||||
(int) (decodedVideoFrameParameters.Height * transformParameters.RegionOfInterest.Height);
|
||||
}
|
||||
|
||||
if (!transformParameters.TargetFrameSize.IsEmpty)
|
||||
{
|
||||
scaledWidth = transformParameters.TargetFrameSize.Width;
|
||||
scaledHeight = transformParameters.TargetFrameSize.Height;
|
||||
|
||||
ScalingPolicy scalingPolicy = transformParameters.ScalePolicy;
|
||||
|
||||
float srcAspectRatio = (float) sourceWidth / sourceHeight;
|
||||
float destAspectRatio = (float) scaledWidth / scaledHeight;
|
||||
|
||||
if (scalingPolicy == ScalingPolicy.Auto)
|
||||
{
|
||||
float relativeChange = Math.Abs(srcAspectRatio - destAspectRatio) / srcAspectRatio;
|
||||
|
||||
scalingPolicy = relativeChange > MaxAspectRatioError
|
||||
? ScalingPolicy.RespectAspectRatio
|
||||
: ScalingPolicy.Stretch;
|
||||
}
|
||||
|
||||
if (scalingPolicy == ScalingPolicy.RespectAspectRatio)
|
||||
{
|
||||
if (destAspectRatio < srcAspectRatio)
|
||||
scaledHeight = sourceHeight * scaledWidth / sourceWidth;
|
||||
else
|
||||
scaledWidth = sourceWidth * scaledHeight / sourceHeight;
|
||||
}
|
||||
}
|
||||
|
||||
PixelFormat scaledPixelFormat = transformParameters.TargetFormat;
|
||||
FFmpegPixelFormat scaledFFmpegPixelFormat = GetFFmpegPixelFormat(scaledPixelFormat);
|
||||
FFmpegScalingQuality scaleQuality = GetFFmpegScaleQuality(transformParameters.ScaleQuality);
|
||||
|
||||
int resultCode = FFmpegVideoPInvoke.CreateVideoScaler(sourceLeft, sourceTop, sourceWidth, sourceHeight,
|
||||
decodedVideoFrameParameters.PixelFormat,
|
||||
scaledWidth, scaledHeight, scaledFFmpegPixelFormat, scaleQuality, out var handle);
|
||||
|
||||
if (resultCode != 0)
|
||||
throw new DecoderException(@"An error occurred while creating scaler, code: {resultCode}");
|
||||
|
||||
return new FFmpegDecodedVideoScaler(handle, scaledWidth, scaledHeight, scaledPixelFormat);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_disposed = true;
|
||||
FFmpegVideoPInvoke.RemoveVideoScaler(Handle);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private static FFmpegScalingQuality GetFFmpegScaleQuality(ScalingQuality scalingQuality)
|
||||
{
|
||||
if (scalingQuality == ScalingQuality.Nearest)
|
||||
return FFmpegScalingQuality.Point;
|
||||
if (scalingQuality == ScalingQuality.Bilinear)
|
||||
return FFmpegScalingQuality.Bilinear;
|
||||
if (scalingQuality == ScalingQuality.FastBilinear)
|
||||
return FFmpegScalingQuality.FastBilinear;
|
||||
if (scalingQuality == ScalingQuality.Bicubic)
|
||||
return FFmpegScalingQuality.Bicubic;
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(scalingQuality));
|
||||
}
|
||||
|
||||
private static FFmpegPixelFormat GetFFmpegPixelFormat(PixelFormat pixelFormat)
|
||||
{
|
||||
if (pixelFormat == PixelFormat.Bgra32)
|
||||
return FFmpegPixelFormat.BGRA;
|
||||
if (pixelFormat == PixelFormat.Grayscale)
|
||||
return FFmpegPixelFormat.GRAY8;
|
||||
if (pixelFormat == PixelFormat.Bgr24)
|
||||
return FFmpegPixelFormat.BGR24;
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(pixelFormat));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Source code originally from RtspClientSharp's player example:
|
||||
* https://github.com/BogdanovKirill/RtspClientSharp
|
||||
*/
|
||||
// ReSharper disable All
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RtspClientSharp.RawFrames.Video;
|
||||
|
||||
namespace IPCamLib.FFMPEG
|
||||
{
|
||||
internal class FFmpegVideoDecoder
|
||||
{
|
||||
private readonly IntPtr _decoderHandle;
|
||||
private readonly FFmpegVideoCodecId _videoCodecId;
|
||||
|
||||
private DecodedVideoFrameParameters _currentFrameParameters =
|
||||
new DecodedVideoFrameParameters(0, 0, FFmpegPixelFormat.None);
|
||||
|
||||
private readonly Dictionary<TransformParameters, FFmpegDecodedVideoScaler> _scalersMap =
|
||||
new Dictionary<TransformParameters, FFmpegDecodedVideoScaler>();
|
||||
|
||||
private byte[] _extraData = new byte[0];
|
||||
private bool _disposed;
|
||||
|
||||
private FFmpegVideoDecoder(FFmpegVideoCodecId videoCodecId, IntPtr decoderHandle)
|
||||
{
|
||||
_videoCodecId = videoCodecId;
|
||||
_decoderHandle = decoderHandle;
|
||||
}
|
||||
|
||||
~FFmpegVideoDecoder()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public static FFmpegVideoDecoder CreateDecoder(FFmpegVideoCodecId videoCodecId)
|
||||
{
|
||||
int resultCode = FFmpegVideoPInvoke.CreateVideoDecoder(videoCodecId, out IntPtr decoderPtr);
|
||||
|
||||
if (resultCode != 0)
|
||||
throw new DecoderException(
|
||||
$"An error occurred while creating video decoder for {videoCodecId} codec, code: {resultCode}");
|
||||
|
||||
return new FFmpegVideoDecoder(videoCodecId, decoderPtr);
|
||||
}
|
||||
|
||||
public unsafe IDecodedVideoFrame TryDecode(RawVideoFrame rawVideoFrame)
|
||||
{
|
||||
fixed (byte* rawBufferPtr = &rawVideoFrame.FrameSegment.Array[rawVideoFrame.FrameSegment.Offset])
|
||||
{
|
||||
int resultCode;
|
||||
|
||||
if (rawVideoFrame is RawH264IFrame rawH264IFrame)
|
||||
{
|
||||
if (rawH264IFrame.SpsPpsSegment.Array != null &&
|
||||
!_extraData.SequenceEqual(rawH264IFrame.SpsPpsSegment))
|
||||
{
|
||||
if (_extraData.Length != rawH264IFrame.SpsPpsSegment.Count)
|
||||
_extraData = new byte[rawH264IFrame.SpsPpsSegment.Count];
|
||||
|
||||
Buffer.BlockCopy(rawH264IFrame.SpsPpsSegment.Array, rawH264IFrame.SpsPpsSegment.Offset,
|
||||
_extraData, 0, rawH264IFrame.SpsPpsSegment.Count);
|
||||
|
||||
fixed (byte* initDataPtr = &_extraData[0])
|
||||
{
|
||||
resultCode = FFmpegVideoPInvoke.SetVideoDecoderExtraData(_decoderHandle,
|
||||
(IntPtr)initDataPtr, _extraData.Length);
|
||||
|
||||
if (resultCode != 0)
|
||||
throw new DecoderException(
|
||||
$"An error occurred while setting video extra data, {_videoCodecId} codec, code: {resultCode}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resultCode = FFmpegVideoPInvoke.DecodeFrame(_decoderHandle, (IntPtr)rawBufferPtr,
|
||||
rawVideoFrame.FrameSegment.Count,
|
||||
out int width, out int height, out FFmpegPixelFormat pixelFormat);
|
||||
|
||||
if (resultCode != 0)
|
||||
return null;
|
||||
|
||||
if (_currentFrameParameters.Width != width || _currentFrameParameters.Height != height ||
|
||||
_currentFrameParameters.PixelFormat != pixelFormat)
|
||||
{
|
||||
_currentFrameParameters = new DecodedVideoFrameParameters(width, height, pixelFormat);
|
||||
DropAllVideoScalers();
|
||||
}
|
||||
|
||||
return new DecodedVideoFrame(TransformTo);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_disposed = true;
|
||||
FFmpegVideoPInvoke.RemoveVideoDecoder(_decoderHandle);
|
||||
DropAllVideoScalers();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void DropAllVideoScalers()
|
||||
{
|
||||
foreach (var scaler in _scalersMap.Values)
|
||||
scaler.Dispose();
|
||||
|
||||
_scalersMap.Clear();
|
||||
}
|
||||
|
||||
private void TransformTo(IntPtr buffer, int bufferStride, TransformParameters parameters)
|
||||
{
|
||||
if (!_scalersMap.TryGetValue(parameters, out FFmpegDecodedVideoScaler videoScaler))
|
||||
{
|
||||
videoScaler = FFmpegDecodedVideoScaler.Create(_currentFrameParameters, parameters);
|
||||
_scalersMap.Add(parameters, videoScaler);
|
||||
}
|
||||
|
||||
int resultCode = FFmpegVideoPInvoke.ScaleDecodedVideoFrame(_decoderHandle, videoScaler.Handle, buffer, bufferStride);
|
||||
|
||||
if (resultCode != 0)
|
||||
throw new DecoderException($"An error occurred while converting decoding video frame, {_videoCodecId} codec, code: {resultCode}");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
using System.Drawing;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IPCamLib
|
||||
{
|
||||
/// <summary>
|
||||
/// Receives frames from an ICamera.
|
||||
/// </summary>
|
||||
public interface ICameraObserver
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when a frame has been decoded from an ICamera.
|
||||
/// </summary>
|
||||
/// <param name="image">The decoded image.</param>
|
||||
Task OnFrame(Image image);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Abstracts an IP camera stream.
|
||||
/// </summary>
|
||||
public interface ICamera
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts receiving frames.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The implementation should continue receiving frames until it is disconnected (if streaming),
|
||||
/// a request fails (if polling) or the cancellationToken is cancelled. Reconnecting is done by
|
||||
/// the caller which will call Fetch again. Do not keep connection-specific state between calls to Fetch.
|
||||
/// Running on a separate thread is also handled by the caller, the returning Task should not complete until disconnected.
|
||||
/// </remarks>
|
||||
/// <param name="observer">The observer implementation to receive frames.</param>
|
||||
/// <param name="cancellationToken">A CancellationToken which will stop the camera stream when cancelled.</param>
|
||||
Task Fetch(ICameraObserver observer, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@
|
|||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
<LangVersion>9</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
|
@ -21,6 +22,9 @@
|
|||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<DocumentationFile>bin\Debug\IPCamLib.xml</DocumentationFile>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
|
@ -29,10 +33,13 @@
|
|||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<DocumentationFile>bin\Release\IPCamLib.xml</DocumentationFile>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
|
@ -41,7 +48,61 @@
|
|||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Base\BaseHTTPStreamCamera.cs" />
|
||||
<Compile Include="Concrete\HTTPMJPEGStreamCamera.cs" />
|
||||
<Compile Include="Concrete\RTSPStreamCamera.cs" />
|
||||
<Compile Include="FFMPEG\DecodedVideoFrame.cs" />
|
||||
<Compile Include="FFMPEG\DecodedVideoFrameParameters.cs" />
|
||||
<Compile Include="FFMPEG\DecoderException.cs" />
|
||||
<Compile Include="FFMPEG\FFmpegDecodedVideoScaler.cs" />
|
||||
<Compile Include="FFMPEG\FFmpegVideoDecoder.cs" />
|
||||
<Compile Include="FFMPEG\FFmpegVideoPInvoke.cs" />
|
||||
<Compile Include="FFMPEG\IDecodedVideoFrame.cs" />
|
||||
<Compile Include="FFMPEG\PixelFormat.cs" />
|
||||
<Compile Include="FFMPEG\ScalingPolicy.cs" />
|
||||
<Compile Include="FFMPEG\ScalingQuality.cs" />
|
||||
<Compile Include="FFMPEG\TransformParameters.cs" />
|
||||
<Compile Include="RetryableCamera.cs" />
|
||||
<Compile Include="ICamera.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FastBitmapLib">
|
||||
<Version>2.0.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="RtspClientSharp">
|
||||
<Version>1.3.3</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="avcodec-58.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="avdevice-58.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="avfilter-7.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="avformat-58.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="avutil-56.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="libffmpeghelper.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="postproc-55.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="swresample-3.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="swscale-5.dll">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
|
@ -0,0 +1,2 @@
|
|||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=x64/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
|
@ -0,0 +1,82 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
// ReSharper disable UnusedMember.Global - public API
|
||||
|
||||
namespace IPCamLib
|
||||
{
|
||||
/// <summary>
|
||||
/// Receives frames from an ICamera as well as status updates.
|
||||
/// </summary>
|
||||
public interface IRetryableCameraObserver : ICameraObserver
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when a new Fetch attempt has started.
|
||||
/// </summary>
|
||||
Task OnFetch();
|
||||
|
||||
/// <summary>
|
||||
/// Called when the stream was disconnected.
|
||||
/// </summary>
|
||||
/// <param name="exception">Contains the exception, if any occured. Null for graceful disconnects and timeouts.</param>
|
||||
/// <param name="retryDelay">The delay until the next Fetch is attempted.</param>
|
||||
Task OnDisconnected(Exception exception, TimeSpan retryDelay);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Implements retry logic for an ICamera instance.
|
||||
/// </summary>
|
||||
public class RetryableCamera
|
||||
{
|
||||
private readonly ICamera camera;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of a RetryableCamera.
|
||||
/// </summary>
|
||||
/// <param name="camera">The camera instance to fetch from.</param>
|
||||
public RetryableCamera(ICamera camera)
|
||||
{
|
||||
this.camera = camera;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Starts receiving frames and will retry when disconnected.
|
||||
/// </summary>
|
||||
/// <param name="observer">The observer implementation to receive frames and status updates.</param>
|
||||
/// <param name="cancellationToken">A CancellationToken which will stop the camera stream when cancelled.</param>
|
||||
public async Task Fetch(IRetryableCameraObserver observer, CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// TODO incremental back-off?
|
||||
var retryDelay = TimeSpan.FromSeconds(5);
|
||||
var exception = false;
|
||||
|
||||
try
|
||||
{
|
||||
await observer.OnFetch();
|
||||
await camera.Fetch(observer, cancellationToken);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Empty by design
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await observer.OnDisconnected(e, retryDelay);
|
||||
exception = true;
|
||||
}
|
||||
|
||||
|
||||
if (!exception && !cancellationToken.IsCancellationRequested)
|
||||
await observer.OnDisconnected(null, retryDelay);
|
||||
|
||||
await Task.Delay(retryDelay, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue