Added RTSP camera support

This commit is contained in:
Mark van Renswoude 2021-07-13 18:09:03 +02:00
parent 9b63740d2a
commit 6284bd0609
37 changed files with 1156 additions and 314 deletions

View File

@ -1,5 +1,9 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EOI/@EntryIndexedValue">EOI</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EOI/@EntryIndexedValue">EOI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HTTP/@EntryIndexedValue">HTTP</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HTTPMJPEG/@EntryIndexedValue">HTTPMJPEG</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MJPEG/@EntryIndexedValue">MJPEG</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MJPEG/@EntryIndexedValue">MJPEG</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RTSP/@EntryIndexedValue">RTSP</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SOI/@EntryIndexedValue">SOI</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SOI/@EntryIndexedValue">SOI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=URL/@EntryIndexedValue">URL</s:String></wpf:ResourceDictionary> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=URL/@EntryIndexedValue">URL</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=EnumMember/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String></wpf:ResourceDictionary>

View File

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

View File

@ -28,11 +28,9 @@
/// </summary> /// </summary>
private void InitializeComponent() private void InitializeComponent()
{ {
this.components = new System.ComponentModel.Container();
this.ConnectingLabel = new System.Windows.Forms.Label(); this.ConnectingLabel = new System.Windows.Forms.Label();
this.StreamView = new System.Windows.Forms.PictureBox(); this.StreamView = new System.Windows.Forms.PictureBox();
this.IssueLabel = new System.Windows.Forms.Label(); this.IssueLabel = new System.Windows.Forms.Label();
this.NoDataTimer = new System.Windows.Forms.Timer(this.components);
((System.ComponentModel.ISupportInitialize)(this.StreamView)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.StreamView)).BeginInit();
this.SuspendLayout(); this.SuspendLayout();
// //
@ -73,11 +71,6 @@
this.IssueLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; this.IssueLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
this.IssueLabel.Visible = false; this.IssueLabel.Visible = false;
// //
// NoDataTimer
//
this.NoDataTimer.Interval = 1000;
this.NoDataTimer.Tick += new System.EventHandler(this.NoDataTimer_Tick);
//
// CameraView // CameraView
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
@ -98,6 +91,5 @@
private System.Windows.Forms.Label ConnectingLabel; private System.Windows.Forms.Label ConnectingLabel;
private System.Windows.Forms.PictureBox StreamView; private System.Windows.Forms.PictureBox StreamView;
private System.Windows.Forms.Label IssueLabel; private System.Windows.Forms.Label IssueLabel;
private System.Windows.Forms.Timer NoDataTimer;
} }
} }

View File

@ -1,64 +1,63 @@
using System; using System;
using System.Drawing; using System.Drawing;
using System.Drawing.Drawing2D; using System.Drawing.Drawing2D;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using DateTime = System.DateTime; using FastBitmapLib;
using IPCamLib;
namespace IPCamAppBar namespace IPCamAppBar
{ {
public partial class CameraView : UserControl public partial class CameraView : UserControl, IRetryableCameraObserver
{ {
private DateTime lastFrameTime; private readonly bool overlayDateTime;
private Bitmap viewBitmap;
public CameraView(ICamera camera, bool overlayDateTime)
public CameraView(string url)
{ {
this.overlayDateTime = overlayDateTime;
InitializeComponent(); InitializeComponent();
var cameraStream = new CameraMJPEGStream(); var streamCancellationTokenSource = new CancellationTokenSource();
cameraStream.Frame += CameraStreamOnFrame; Task.Run(async () =>
cameraStream.StreamException += CameraStreamOnStreamException;
cameraStream.Start(url);
Disposed += (sender, args) =>
{ {
cameraStream.Dispose(); var retryableCamera = new RetryableCamera(camera);
await retryableCamera.Fetch(this, streamCancellationTokenSource.Token);
});
Disposed += (_, _) =>
{
streamCancellationTokenSource.Cancel();
}; };
} }
private void CameraStreamOnFrame(object sender, FrameEventArgs args)
{
// The event comes from a background thread, so if needed invoke it on the main thread
if (InvokeRequired)
{
Invoke(new Action(() => { CameraStreamOnFrame(sender, args); }));
return;
}
ConnectingLabel.Visible = false; private Bitmap resizedBitmap;
StreamView.Visible = true;
IssueLabel.Visible = false;
lastFrameTime = DateTime.Now; public Task OnFrame(Image image)
NoDataTimer.Start();
var viewImage = new Bitmap(Width, Height);
using (var graphics = Graphics.FromImage(viewImage))
{ {
if (overlayDateTime || image is not Bitmap bitmap || image.Width != Width || image.Height != Height)
{
resizedBitmap ??= new Bitmap(Width, Height);
using var graphics = Graphics.FromImage(resizedBitmap);
graphics.SmoothingMode = SmoothingMode.AntiAlias; graphics.SmoothingMode = SmoothingMode.AntiAlias;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
graphics.DrawImage(args.Image, 0, 0, viewImage.Width, viewImage.Height); graphics.DrawImage(image, 0, 0, resizedBitmap.Width, resizedBitmap.Height);
if (overlayDateTime)
{
using (var path = new GraphicsPath()) using (var path = new GraphicsPath())
{ {
path.AddString( path.AddString(
lastFrameTime.ToString("G"), DateTime.Now.ToString("G"),
FontFamily.GenericSansSerif, FontFamily.GenericSansSerif,
(int) FontStyle.Regular, (int) FontStyle.Regular,
graphics.DpiY * 14 / 72, graphics.DpiY * 14 / 72,
Rectangle.Inflate(new Rectangle(0, 0, viewImage.Width, viewImage.Height), -4, -4), Rectangle.Inflate(new Rectangle(0, 0, resizedBitmap.Width, resizedBitmap.Height), -4, -4),
new StringFormat new StringFormat
{ {
Alignment = StringAlignment.Far, Alignment = StringAlignment.Far,
@ -68,37 +67,73 @@ namespace IPCamAppBar
graphics.DrawPath(new Pen(Color.Black, 3), path); graphics.DrawPath(new Pen(Color.Black, 3), path);
graphics.FillPath(Brushes.White, path); graphics.FillPath(Brushes.White, path);
} }
}
graphics.Flush(); graphics.Flush();
bitmap = resizedBitmap;
} }
var oldImage = StreamView.Image; Invoke(new Action(() => { InvokeFrame(bitmap); }));
StreamView.Image = viewImage; return Task.CompletedTask;
oldImage?.Dispose();
} }
private void CameraStreamOnStreamException(object sender, StreamExceptionEventArgs args) public Task OnFetch()
{ {
if (InvokeRequired) Invoke(new Action(InvokeFetch));
return Task.CompletedTask;
}
public Task OnDisconnected(Exception exception, TimeSpan retryDelay)
{ {
Invoke(new Action(() => { CameraStreamOnStreamException(sender, args); })); Invoke(new Action(() => { InvokeDisconnected(exception, retryDelay); }));
return Task.CompletedTask;
}
private void InvokeFrame(Bitmap bitmap)
{
if (IsDisposed)
return; return;
ConnectingLabel.Visible = false;
StreamView.Visible = true;
IssueLabel.Visible = false;
if (viewBitmap == null)
{
viewBitmap = new Bitmap(Width, Height);
StreamView.Image = viewBitmap;
} }
IssueLabel.Text = args.Exception.Message; using (var fastViewBitmap = viewBitmap.FastLock())
{
fastViewBitmap.CopyRegion(bitmap, new Rectangle(Point.Empty, bitmap.Size), new Rectangle(Point.Empty, viewBitmap.Size));
}
StreamView.Invalidate();
}
private void InvokeFetch()
{
if (IsDisposed)
return;
IssueLabel.Text = "Connecting...";
IssueLabel.Visible = true; IssueLabel.Visible = true;
IssueLabel.BringToFront(); IssueLabel.BringToFront();
} }
private void NoDataTimer_Tick(object sender, EventArgs e) private void InvokeDisconnected(Exception exception, TimeSpan retryDelay)
{ {
var timeSinceLastFrame = DateTime.Now - lastFrameTime; if (IsDisposed)
if (timeSinceLastFrame.TotalSeconds < 10)
return; return;
IssueLabel.Text = $@"No data for {(int)timeSinceLastFrame.TotalSeconds} seconds"; IssueLabel.Text = (exception?.Message ?? "Camera disconnected") + $", retrying in {retryDelay.TotalSeconds} seconds";
IssueLabel.Visible = true; IssueLabel.Visible = true;
IssueLabel.BringToFront(); IssueLabel.BringToFront();
} }

View File

@ -117,7 +117,4 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<metadata name="NoDataTimer.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root> </root>

View File

@ -4,7 +4,7 @@ namespace IPCamAppBar
{ {
public class Config public class Config
{ {
public ConfigAppBar AppBar { get; } = new ConfigAppBar(); public ConfigAppBar AppBar { get; } = new();
public List<ConfigCamera> Cameras { get; set; } public List<ConfigCamera> Cameras { get; set; }
} }
@ -22,13 +22,24 @@ namespace IPCamAppBar
public int Monitor { get; set; } public int Monitor { get; set; }
public ConfigSide Side { get; set; } public ConfigSide Side { get; set; }
public int Size { get; set; } public int Size { get; set; }
public int Spacing { get; set; }
} }
// ReSharper disable InconsistentNaming
public enum ConfigCameraType
{
HTTPMJPEG,
RTSP
}
// ReSharper restore InconsistentNaming
public class ConfigCamera public class ConfigCamera
{ {
public ConfigCameraType Type { get; set; }
public string URL { get; set; } public string URL { get; set; }
public int Width { get; set; } public int Width { get; set; }
public int Height { get; set; } public int Height { get; set; }
public bool OverlayDateTime { get; set; }
} }
} }

View File

@ -12,9 +12,10 @@
<FileAlignment>512</FileAlignment> <FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic> <Deterministic>true</Deterministic>
<LangVersion>9</LangVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget> <PlatformTarget>x64</PlatformTarget>
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType> <DebugType>full</DebugType>
<Optimize>false</Optimize> <Optimize>false</Optimize>
@ -22,6 +23,7 @@
<DefineConstants>DEBUG;TRACE</DefineConstants> <DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget> <PlatformTarget>AnyCPU</PlatformTarget>
@ -34,8 +36,11 @@
<Prefer32Bit>false</Prefer32Bit> <Prefer32Bit>false</Prefer32Bit>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> <Reference Include="FastBitmapLib, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll</HintPath> <HintPath>..\packages\FastBitmapLib.2.0.0\lib\net452\FastBitmapLib.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
@ -51,8 +56,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="AppBar.cs" /> <Compile Include="AppBar.cs" />
<Compile Include="CameraMJPEGStream.cs" />
<Compile Include="CameraStream.cs" />
<Compile Include="CameraView.cs"> <Compile Include="CameraView.cs">
<SubType>UserControl</SubType> <SubType>UserControl</SubType>
</Compile> </Compile>
@ -100,5 +103,11 @@
<ItemGroup> <ItemGroup>
<None Include="App.config" /> <None Include="App.config" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\IPCamLib\IPCamLib.csproj">
<Project>{FD0886FF-F274-4EFC-8A56-F82A4641C3FD}</Project>
<Name>IPCamLib</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project> </Project>

View File

@ -1,9 +1,10 @@
using System; using System;
using System.Diagnostics.Contracts;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Windows.Forms; using System.Windows.Forms;
using IPCamLib;
using IPCamLib.Concrete;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace IPCamAppBar namespace IPCamAppBar
@ -32,6 +33,11 @@ namespace IPCamAppBar
appBar = new AppBar(Handle); appBar = new AppBar(Handle);
appBar.SetPosition(monitor, position, config.AppBar.Size); appBar.SetPosition(monitor, position, config.AppBar.Size);
CameraViewContainer.FlowDirection =
config.AppBar.Side == ConfigSide.Left || config.AppBar.Side == ConfigSide.Right
? FlowDirection.TopDown
: FlowDirection.LeftToRight;
config.Cameras?.ForEach(AddCamera); config.Cameras?.ForEach(AddCamera);
} }
@ -71,12 +77,26 @@ namespace IPCamAppBar
} }
private void AddCamera(ConfigCamera camera) private void AddCamera(ConfigCamera configCamera)
{ {
var view = new CameraView(camera.URL) ICamera camera = configCamera.Type switch
{ {
Width = camera.Width, ConfigCameraType.HTTPMJPEG => new HTTPMJPEGStreamCamera(new Uri(configCamera.URL)),
Height = camera.Height ConfigCameraType.RTSP => new RTSPStreamCamera(new Uri(configCamera.URL), configCamera.Width, configCamera.Height),
_ => throw new ArgumentOutOfRangeException($"Camera type in configuration is not valid: {configCamera.Type}")
};
var margin = new Padding(0);
if (CameraViewContainer.FlowDirection == FlowDirection.LeftToRight)
margin.Right = config.AppBar.Spacing;
else
margin.Bottom = config.AppBar.Spacing;
var view = new CameraView(camera, configCamera.OverlayDateTime)
{
Width = configCamera.Width,
Height = configCamera.Height,
Margin = margin
}; };
CameraViewContainer.Controls.Add(view); CameraViewContainer.Controls.Add(view);

View File

@ -2,19 +2,24 @@
"AppBar": { "AppBar": {
"Monitor": 0, "Monitor": 0,
"Side": "Top", "Side": "Top",
"Size": 480 "Size": 480,
"Spacing": 2
}, },
"Cameras": [ "Cameras": [
{ {
"URL": "http://username:password@ipcamera1/videostream.cgi", "Type": "RTSP",
"URL": "rtsp://username:password@ipcamera1/",
"Width": 480, "Width": 480,
"Height": 360 "Height": 360,
"OverlayDateTime": false
}, },
{ {
"Type": "HTTPMJPEG",
"URL": "http://username:password@ipcamera2/videostream.cgi", "URL": "http://username:password@ipcamera2/videostream.cgi",
"Width": 480, "Width": 480,
"Height": 360 "Height": 360,
"OverlayDateTime": true
} }
] ]
} }

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="Newtonsoft.Json" version="12.0.2" targetFramework="net472" /> <package id="FastBitmapLib" version="2.0.0" targetFramework="net472" />
<package id="Newtonsoft.Json" version="13.0.1" targetFramework="net472" />
</packages> </packages>

View File

@ -0,0 +1,73 @@
using System;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace IPCamLib.Base
{
/// <summary>
/// Abstract base class for IP cameras reading an HTTP stream.
/// </summary>
public abstract class BaseHTTPStreamCamera : ICamera
{
private readonly Uri streamUri;
/// <param name="streamUri">The URI to the camera stream.
/// Can include basic credentials in the standard 'username:password@' format.</param>
protected BaseHTTPStreamCamera(Uri streamUri)
{
this.streamUri = streamUri;
}
/// <inheritdoc />
public async Task Fetch(ICameraObserver observer, CancellationToken cancellationToken)
{
var request = WebRequest.CreateHttp(streamUri);
if (!string.IsNullOrEmpty(streamUri.UserInfo))
{
var parts = streamUri.UserInfo.Split(':');
request.Credentials = new NetworkCredential(parts[0], parts.Length > 1 ? parts[1] : "");
}
request.ReadWriteTimeout = (int)TimeSpan.FromMinutes(1).TotalMilliseconds;
try
{
HttpWebResponse response;
using (cancellationToken.Register(() => request.Abort(), false))
{
response = (HttpWebResponse)await request.GetResponseAsync();
cancellationToken.ThrowIfCancellationRequested();
}
if (response.StatusCode != HttpStatusCode.OK)
throw new WebException(response.StatusDescription);
using var responseStream = response.GetResponseStream();
await ReadFrames(observer, responseStream, cancellationToken);
}
catch (TaskCanceledException)
{
}
finally
{
request.Abort();
}
}
/// <summary>
/// Implement in concrete descendants to continuously read frames from the HTTP stream.
/// </summary>
/// <param name="observer">The observer implementation passed to Fetch which should receive the decoded frames.</param>
/// <param name="stream">The HTTP response stream.</param>
/// <param name="cancellationToken">Stop reading frames when this token is cancelled.</param>
protected abstract Task ReadFrames(ICameraObserver observer, Stream stream, CancellationToken cancellationToken);
}
}

View File

@ -3,10 +3,16 @@ using System.Drawing;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using IPCamLib.Base;
namespace IPCamAppBar // ReSharper disable UnusedMember.Global - public API
namespace IPCamLib.Concrete
{ {
internal class CameraMJPEGStream : CameraStream /// <summary>
/// Implements the ICamera interface for IP cameras exposing an MJPEG stream over HTTP.
/// </summary>
public class HTTPMJPEGStreamCamera : BaseHTTPStreamCamera
{ {
// MJPEG decoding is a variation on https://github.com/arndre/MjpegDecoder // MJPEG decoding is a variation on https://github.com/arndre/MjpegDecoder
private static readonly byte[] JpegSOI = { 0xff, 0xd8 }; // start of image bytes private static readonly byte[] JpegSOI = { 0xff, 0xd8 }; // start of image bytes
@ -16,7 +22,17 @@ namespace IPCamAppBar
private const int MaxBufferSize = 1024 * 1024 * 10; private const int MaxBufferSize = 1024 * 1024 * 10;
protected override async Task ReadFrames(Stream stream, CancellationToken cancellationToken) /// <summary>
/// Creates a new instance for an IP camera exposing an MJPEG stream over HTTP.
/// </summary>
/// <inheritdoc />
public HTTPMJPEGStreamCamera(Uri streamUri) : base(streamUri)
{
}
/// <inheritdoc />
protected override async Task ReadFrames(ICameraObserver observer, Stream stream, CancellationToken cancellationToken)
{ {
var buffer = new byte[ChunkSize]; var buffer = new byte[ChunkSize];
var bufferPosition = 0; var bufferPosition = 0;
@ -51,9 +67,13 @@ namespace IPCamAppBar
} }
while (!cancellationToken.IsCancellationRequested) var frameTimeout = TimeSpan.FromMinutes(1);
var frameTimeoutCancellationTokenSource = new CancellationTokenSource(frameTimeout);
var combinedCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, frameTimeoutCancellationTokenSource.Token);
while (!combinedCancellation.IsCancellationRequested)
{ {
var bytesRead = await stream.ReadAsync(buffer, bufferPosition, ChunkSize, cancellationToken); var bytesRead = await stream.ReadAsync(buffer, bufferPosition, ChunkSize, combinedCancellation.Token);
if (bytesRead == 0) if (bytesRead == 0)
throw new EndOfStreamException(); throw new EndOfStreamException();
@ -61,7 +81,7 @@ namespace IPCamAppBar
if (!startOfImage.HasValue) if (!startOfImage.HasValue)
{ {
var index = buffer.Find(JpegSOI, bufferPosition); var index = buffer.IndexOf(JpegSOI, bufferPosition);
if (index == -1) if (index == -1)
{ {
// No start of image yet, we need to buffer more // No start of image yet, we need to buffer more
@ -72,7 +92,7 @@ namespace IPCamAppBar
startOfImage = index; startOfImage = index;
} }
var endOfImage = buffer.Find(JpegEOI, bufferPosition, lastEndOfSearch.GetValueOrDefault(startOfImage.Value)); var endOfImage = buffer.IndexOf(JpegEOI, bufferPosition, lastEndOfSearch.GetValueOrDefault(startOfImage.Value));
if (endOfImage == -1) if (endOfImage == -1)
{ {
// No start of image yet, we need to buffer more. Keep track of where we were so we don't // No start of image yet, we need to buffer more. Keep track of where we were so we don't
@ -90,21 +110,42 @@ namespace IPCamAppBar
} }
endOfImage += JpegEOI.Length; endOfImage += JpegEOI.Length;
HandleFrame(buffer, startOfImage.Value, endOfImage);
using (var image = new Bitmap(new MemoryStream(buffer, startOfImage.Value, endOfImage - startOfImage.Value)))
{
await observer.OnFrame(image);
}
ResetBuffer(endOfImage); ResetBuffer(endOfImage);
frameTimeoutCancellationTokenSource.CancelAfter(frameTimeout);
}
} }
} }
protected void HandleFrame(byte[] buffer, int start, int end) internal static class BufferExtensions
{ {
using (var image = new Bitmap(new MemoryStream(buffer, start, end - start))) public static int IndexOf(this byte[] buffer, byte[] pattern, int limit = int.MaxValue, int startAt = 0)
{ {
OnFrame(new FrameEventArgs var patternIndex = 0;
int bufferIndex;
for (bufferIndex = startAt; bufferIndex < buffer.Length && patternIndex < pattern.Length && bufferIndex < limit; bufferIndex++)
{ {
Image = image if (buffer[bufferIndex] == pattern[patternIndex])
}); {
} patternIndex++;
}
else
{
patternIndex = 0;
}
}
if (patternIndex == pattern.Length)
return bufferIndex - pattern.Length;
return -1;
} }
} }
} }

View File

@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using FastBitmapLib;
using IPCamLib.FFMPEG;
using RtspClientSharp;
using RtspClientSharp.RawFrames.Video;
namespace IPCamLib.Concrete
{
/// <summary>
/// Implements the ICamera interface for IP cameras exposing an RTSP stream.
/// </summary>
public class RTSPStreamCamera : ICamera
{
private readonly Uri streamUri;
private readonly Dictionary<FFmpegVideoCodecId, FFmpegVideoDecoder> videoDecodersMap = new();
private readonly Bitmap bitmap;
private readonly TransformParameters transformParameters;
/// <param name="streamUri">The URI to the camera stream.
/// Can include basic credentials in the standard 'username:password@' format.</param>
/// <param name="width">The width of the viewport</param>
/// <param name="height">The height of the viewport</param>
public RTSPStreamCamera(Uri streamUri, int width, int height)
{
this.streamUri = streamUri;
transformParameters = new TransformParameters(RectangleF.Empty, new Size(width, height),
ScalingPolicy.Stretch, PixelFormat.Bgra32, ScalingQuality.FastBilinear);
bitmap = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
using var fastBitmap = bitmap.FastLock();
fastBitmap.Clear(Color.Black);
}
/// <inheritdoc />
public async Task Fetch(ICameraObserver observer, CancellationToken cancellationToken)
{
NetworkCredential credentials = null;
if (!string.IsNullOrEmpty(streamUri.UserInfo))
{
var parts = streamUri.UserInfo.Split(':');
credentials = new NetworkCredential(parts[0], parts.Length > 1 ? parts[1] : "");
}
else
credentials = new NetworkCredential(null, (string)null);
var connectionParameters = new ConnectionParameters(streamUri, credentials)
{
RtpTransport = RtpTransportProtocol.TCP
};
using var rtspClient = new RtspClient(connectionParameters);
rtspClient.FrameReceived += (_, rawFrame) =>
{
if (rawFrame is not RawVideoFrame rawVideoFrame)
return;
var decoder = GetDecoderForFrame(rawVideoFrame);
var decodedFrame = decoder.TryDecode(rawVideoFrame);
if (decodedFrame == null)
return;
using (var fastBitmap = bitmap.FastLock())
{
decodedFrame.TransformTo(fastBitmap.Scan0, fastBitmap.StrideInBytes, transformParameters);
}
// TODO await
observer.OnFrame(bitmap);
};
await rtspClient.ConnectAsync(cancellationToken);
await rtspClient.ReceiveAsync(cancellationToken);
}
private FFmpegVideoDecoder GetDecoderForFrame(RawVideoFrame videoFrame)
{
var codecId = DetectCodecId(videoFrame);
if (videoDecodersMap.TryGetValue(codecId, out var decoder))
return decoder;
decoder = FFmpegVideoDecoder.CreateDecoder(codecId);
videoDecodersMap.Add(codecId, decoder);
return decoder;
}
private static FFmpegVideoCodecId DetectCodecId(RawVideoFrame videoFrame)
{
return videoFrame switch
{
RawJpegFrame => FFmpegVideoCodecId.MJPEG,
RawH264Frame => FFmpegVideoCodecId.H264,
_ => throw new ArgumentOutOfRangeException(nameof(videoFrame))
};
}
}
}

View File

@ -0,0 +1,24 @@
/*
* Source code originally from RtspClientSharp's player example:
* https://github.com/BogdanovKirill/RtspClientSharp
*/
// ReSharper disable All
using System;
namespace IPCamLib.FFMPEG
{
internal class DecodedVideoFrame : IDecodedVideoFrame
{
private readonly Action<IntPtr, int, TransformParameters> _transformAction;
public DecodedVideoFrame(Action<IntPtr, int, TransformParameters> transformAction)
{
_transformAction = transformAction;
}
public void TransformTo(IntPtr buffer, int bufferStride, TransformParameters transformParameters)
{
_transformAction(buffer, bufferStride, transformParameters);
}
}
}

View File

@ -0,0 +1,47 @@
/*
* Source code originally from RtspClientSharp's player example:
* https://github.com/BogdanovKirill/RtspClientSharp
*/
// ReSharper disable All
namespace IPCamLib.FFMPEG
{
internal class DecodedVideoFrameParameters
{
public int Width { get; }
public int Height { get; }
public FFmpegPixelFormat PixelFormat { get; }
public DecodedVideoFrameParameters(int width, int height, FFmpegPixelFormat pixelFormat)
{
Width = width;
Height = height;
PixelFormat = pixelFormat;
}
protected bool Equals(DecodedVideoFrameParameters other)
{
return Width == other.Width && Height == other.Height && PixelFormat == other.PixelFormat;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((DecodedVideoFrameParameters) obj);
}
public override int GetHashCode()
{
unchecked
{
var hashCode = Width;
hashCode = (hashCode * 397) ^ Height;
hashCode = (hashCode * 397) ^ (int) PixelFormat;
return hashCode;
}
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Source code originally from RtspClientSharp's player example:
* https://github.com/BogdanovKirill/RtspClientSharp
*/
// ReSharper disable All
using System;
using System.Runtime.Serialization;
namespace IPCamLib.FFMPEG
{
[Serializable]
internal class DecoderException : Exception
{
//
// For guidelines regarding the creation of new exception types, see
// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp
// and
// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp
//
public DecoderException()
{
}
public DecoderException(string message) : base(message)
{
}
public DecoderException(string message, Exception inner) : base(message, inner)
{
}
protected DecoderException(
SerializationInfo info,
StreamingContext context) : base(info, context)
{
}
}
}

View File

@ -0,0 +1,140 @@
/*
* Source code originally from RtspClientSharp's player example:
* https://github.com/BogdanovKirill/RtspClientSharp
*/
// ReSharper disable All
using System;
namespace IPCamLib.FFMPEG
{
internal class FFmpegDecodedVideoScaler
{
private const double MaxAspectRatioError = 0.1;
private bool _disposed;
public IntPtr Handle { get; }
public int ScaledWidth { get; }
public int ScaledHeight { get; }
public PixelFormat ScaledPixelFormat { get; }
private FFmpegDecodedVideoScaler(IntPtr handle, int scaledWidth, int scaledHeight,
PixelFormat scaledPixelFormat)
{
Handle = handle;
ScaledWidth = scaledWidth;
ScaledHeight = scaledHeight;
ScaledPixelFormat = scaledPixelFormat;
}
~FFmpegDecodedVideoScaler()
{
Dispose();
}
/// <exception cref="DecoderException"></exception>
public static FFmpegDecodedVideoScaler Create(DecodedVideoFrameParameters decodedVideoFrameParameters,
TransformParameters transformParameters)
{
if (decodedVideoFrameParameters == null)
throw new ArgumentNullException(nameof(decodedVideoFrameParameters));
if (transformParameters == null)
throw new ArgumentNullException(nameof(transformParameters));
int sourceLeft = 0;
int sourceTop = 0;
int sourceWidth = decodedVideoFrameParameters.Width;
int sourceHeight = decodedVideoFrameParameters.Height;
int scaledWidth = decodedVideoFrameParameters.Width;
int scaledHeight = decodedVideoFrameParameters.Height;
if (!transformParameters.RegionOfInterest.IsEmpty)
{
sourceLeft =
(int) (decodedVideoFrameParameters.Width * transformParameters.RegionOfInterest.Left);
sourceTop =
(int) (decodedVideoFrameParameters.Height * transformParameters.RegionOfInterest.Top);
sourceWidth =
(int) (decodedVideoFrameParameters.Width * transformParameters.RegionOfInterest.Width);
sourceHeight =
(int) (decodedVideoFrameParameters.Height * transformParameters.RegionOfInterest.Height);
}
if (!transformParameters.TargetFrameSize.IsEmpty)
{
scaledWidth = transformParameters.TargetFrameSize.Width;
scaledHeight = transformParameters.TargetFrameSize.Height;
ScalingPolicy scalingPolicy = transformParameters.ScalePolicy;
float srcAspectRatio = (float) sourceWidth / sourceHeight;
float destAspectRatio = (float) scaledWidth / scaledHeight;
if (scalingPolicy == ScalingPolicy.Auto)
{
float relativeChange = Math.Abs(srcAspectRatio - destAspectRatio) / srcAspectRatio;
scalingPolicy = relativeChange > MaxAspectRatioError
? ScalingPolicy.RespectAspectRatio
: ScalingPolicy.Stretch;
}
if (scalingPolicy == ScalingPolicy.RespectAspectRatio)
{
if (destAspectRatio < srcAspectRatio)
scaledHeight = sourceHeight * scaledWidth / sourceWidth;
else
scaledWidth = sourceWidth * scaledHeight / sourceHeight;
}
}
PixelFormat scaledPixelFormat = transformParameters.TargetFormat;
FFmpegPixelFormat scaledFFmpegPixelFormat = GetFFmpegPixelFormat(scaledPixelFormat);
FFmpegScalingQuality scaleQuality = GetFFmpegScaleQuality(transformParameters.ScaleQuality);
int resultCode = FFmpegVideoPInvoke.CreateVideoScaler(sourceLeft, sourceTop, sourceWidth, sourceHeight,
decodedVideoFrameParameters.PixelFormat,
scaledWidth, scaledHeight, scaledFFmpegPixelFormat, scaleQuality, out var handle);
if (resultCode != 0)
throw new DecoderException(@"An error occurred while creating scaler, code: {resultCode}");
return new FFmpegDecodedVideoScaler(handle, scaledWidth, scaledHeight, scaledPixelFormat);
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
FFmpegVideoPInvoke.RemoveVideoScaler(Handle);
GC.SuppressFinalize(this);
}
private static FFmpegScalingQuality GetFFmpegScaleQuality(ScalingQuality scalingQuality)
{
if (scalingQuality == ScalingQuality.Nearest)
return FFmpegScalingQuality.Point;
if (scalingQuality == ScalingQuality.Bilinear)
return FFmpegScalingQuality.Bilinear;
if (scalingQuality == ScalingQuality.FastBilinear)
return FFmpegScalingQuality.FastBilinear;
if (scalingQuality == ScalingQuality.Bicubic)
return FFmpegScalingQuality.Bicubic;
throw new ArgumentOutOfRangeException(nameof(scalingQuality));
}
private static FFmpegPixelFormat GetFFmpegPixelFormat(PixelFormat pixelFormat)
{
if (pixelFormat == PixelFormat.Bgra32)
return FFmpegPixelFormat.BGRA;
if (pixelFormat == PixelFormat.Grayscale)
return FFmpegPixelFormat.GRAY8;
if (pixelFormat == PixelFormat.Bgr24)
return FFmpegPixelFormat.BGR24;
throw new ArgumentOutOfRangeException(nameof(pixelFormat));
}
}
}

View File

@ -0,0 +1,129 @@
/*
* Source code originally from RtspClientSharp's player example:
* https://github.com/BogdanovKirill/RtspClientSharp
*/
// ReSharper disable All
using System;
using System.Collections.Generic;
using System.Linq;
using RtspClientSharp.RawFrames.Video;
namespace IPCamLib.FFMPEG
{
internal class FFmpegVideoDecoder
{
private readonly IntPtr _decoderHandle;
private readonly FFmpegVideoCodecId _videoCodecId;
private DecodedVideoFrameParameters _currentFrameParameters =
new DecodedVideoFrameParameters(0, 0, FFmpegPixelFormat.None);
private readonly Dictionary<TransformParameters, FFmpegDecodedVideoScaler> _scalersMap =
new Dictionary<TransformParameters, FFmpegDecodedVideoScaler>();
private byte[] _extraData = new byte[0];
private bool _disposed;
private FFmpegVideoDecoder(FFmpegVideoCodecId videoCodecId, IntPtr decoderHandle)
{
_videoCodecId = videoCodecId;
_decoderHandle = decoderHandle;
}
~FFmpegVideoDecoder()
{
Dispose();
}
public static FFmpegVideoDecoder CreateDecoder(FFmpegVideoCodecId videoCodecId)
{
int resultCode = FFmpegVideoPInvoke.CreateVideoDecoder(videoCodecId, out IntPtr decoderPtr);
if (resultCode != 0)
throw new DecoderException(
$"An error occurred while creating video decoder for {videoCodecId} codec, code: {resultCode}");
return new FFmpegVideoDecoder(videoCodecId, decoderPtr);
}
public unsafe IDecodedVideoFrame TryDecode(RawVideoFrame rawVideoFrame)
{
fixed (byte* rawBufferPtr = &rawVideoFrame.FrameSegment.Array[rawVideoFrame.FrameSegment.Offset])
{
int resultCode;
if (rawVideoFrame is RawH264IFrame rawH264IFrame)
{
if (rawH264IFrame.SpsPpsSegment.Array != null &&
!_extraData.SequenceEqual(rawH264IFrame.SpsPpsSegment))
{
if (_extraData.Length != rawH264IFrame.SpsPpsSegment.Count)
_extraData = new byte[rawH264IFrame.SpsPpsSegment.Count];
Buffer.BlockCopy(rawH264IFrame.SpsPpsSegment.Array, rawH264IFrame.SpsPpsSegment.Offset,
_extraData, 0, rawH264IFrame.SpsPpsSegment.Count);
fixed (byte* initDataPtr = &_extraData[0])
{
resultCode = FFmpegVideoPInvoke.SetVideoDecoderExtraData(_decoderHandle,
(IntPtr)initDataPtr, _extraData.Length);
if (resultCode != 0)
throw new DecoderException(
$"An error occurred while setting video extra data, {_videoCodecId} codec, code: {resultCode}");
}
}
}
resultCode = FFmpegVideoPInvoke.DecodeFrame(_decoderHandle, (IntPtr)rawBufferPtr,
rawVideoFrame.FrameSegment.Count,
out int width, out int height, out FFmpegPixelFormat pixelFormat);
if (resultCode != 0)
return null;
if (_currentFrameParameters.Width != width || _currentFrameParameters.Height != height ||
_currentFrameParameters.PixelFormat != pixelFormat)
{
_currentFrameParameters = new DecodedVideoFrameParameters(width, height, pixelFormat);
DropAllVideoScalers();
}
return new DecodedVideoFrame(TransformTo);
}
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
FFmpegVideoPInvoke.RemoveVideoDecoder(_decoderHandle);
DropAllVideoScalers();
GC.SuppressFinalize(this);
}
private void DropAllVideoScalers()
{
foreach (var scaler in _scalersMap.Values)
scaler.Dispose();
_scalersMap.Clear();
}
private void TransformTo(IntPtr buffer, int bufferStride, TransformParameters parameters)
{
if (!_scalersMap.TryGetValue(parameters, out FFmpegDecodedVideoScaler videoScaler))
{
videoScaler = FFmpegDecodedVideoScaler.Create(_currentFrameParameters, parameters);
_scalersMap.Add(parameters, videoScaler);
}
int resultCode = FFmpegVideoPInvoke.ScaleDecodedVideoFrame(_decoderHandle, videoScaler.Handle, buffer, bufferStride);
if (resultCode != 0)
throw new DecoderException($"An error occurred while converting decoding video frame, {_videoCodecId} codec, code: {resultCode}");
}
}
}

View File

@ -0,0 +1,66 @@
/*
* Source code originally from RtspClientSharp's player example:
* https://github.com/BogdanovKirill/RtspClientSharp
*/
// ReSharper disable All
using System;
using System.Runtime.InteropServices;
namespace IPCamLib.FFMPEG
{
internal enum FFmpegVideoCodecId
{
MJPEG = 7,
H264 = 27
}
[Flags]
internal enum FFmpegScalingQuality
{
FastBilinear = 1,
Bilinear = 2,
Bicubic = 4,
Point = 0x10,
Area = 0x20,
}
internal enum FFmpegPixelFormat
{
None = -1,
BGR24 = 3,
GRAY8 = 8,
BGRA = 28
}
internal static class FFmpegVideoPInvoke
{
private const string LibraryName = "libffmpeghelper.dll";
[DllImport(LibraryName, EntryPoint = "create_video_decoder", CallingConvention = CallingConvention.Cdecl)]
public static extern int CreateVideoDecoder(FFmpegVideoCodecId videoCodecId, out IntPtr handle);
[DllImport(LibraryName, EntryPoint = "remove_video_decoder", CallingConvention = CallingConvention.Cdecl)]
public static extern void RemoveVideoDecoder(IntPtr handle);
[DllImport(LibraryName, EntryPoint = "set_video_decoder_extradata",
CallingConvention = CallingConvention.Cdecl)]
public static extern int SetVideoDecoderExtraData(IntPtr handle, IntPtr extradata, int extradataLength);
[DllImport(LibraryName, EntryPoint = "decode_video_frame", CallingConvention = CallingConvention.Cdecl)]
public static extern int DecodeFrame(IntPtr handle, IntPtr rawBuffer, int rawBufferLength, out int frameWidth,
out int frameHeight, out FFmpegPixelFormat framePixelFormat);
[DllImport(LibraryName, EntryPoint = "scale_decoded_video_frame", CallingConvention = CallingConvention.Cdecl)]
public static extern int ScaleDecodedVideoFrame(IntPtr handle, IntPtr scalerHandle, IntPtr scaledBuffer,
int scaledBufferStride);
[DllImport(LibraryName, EntryPoint = "create_video_scaler", CallingConvention = CallingConvention.Cdecl)]
public static extern int CreateVideoScaler(int sourceLeft, int sourceTop, int sourceWidth, int sourceHeight,
FFmpegPixelFormat sourcePixelFormat,
int scaledWidth, int scaledHeight, FFmpegPixelFormat scaledPixelFormat, FFmpegScalingQuality qualityFlags,
out IntPtr handle);
[DllImport(LibraryName, EntryPoint = "remove_video_scaler", CallingConvention = CallingConvention.Cdecl)]
public static extern void RemoveVideoScaler(IntPtr handle);
}
}

View File

@ -0,0 +1,14 @@
/*
* Source code originally from RtspClientSharp's player example:
* https://github.com/BogdanovKirill/RtspClientSharp
*/
// ReSharper disable All
using System;
namespace IPCamLib.FFMPEG
{
internal interface IDecodedVideoFrame
{
void TransformTo(IntPtr buffer, int bufferStride, TransformParameters transformParameters);
}
}

View File

@ -0,0 +1,14 @@
/*
* Source code originally from RtspClientSharp's player example:
* https://github.com/BogdanovKirill/RtspClientSharp
*/
// ReSharper disable All
namespace IPCamLib.FFMPEG
{
internal enum PixelFormat
{
Grayscale,
Bgr24,
Bgra32,
}
}

View File

@ -0,0 +1,14 @@
/*
* Source code originally from RtspClientSharp's player example:
* https://github.com/BogdanovKirill/RtspClientSharp
*/
// ReSharper disable All
namespace IPCamLib.FFMPEG
{
internal enum ScalingPolicy
{
Auto,
Stretch,
RespectAspectRatio
}
}

View File

@ -0,0 +1,15 @@
/*
* Source code originally from RtspClientSharp's player example:
* https://github.com/BogdanovKirill/RtspClientSharp
*/
// ReSharper disable All
namespace IPCamLib.FFMPEG
{
internal enum ScalingQuality
{
Nearest,
Bilinear,
FastBilinear,
Bicubic
}
}

View File

@ -0,0 +1,59 @@
/*
* Source code originally from RtspClientSharp's player example:
* https://github.com/BogdanovKirill/RtspClientSharp
*/
// ReSharper disable All
using System.Drawing;
namespace IPCamLib.FFMPEG
{
internal class TransformParameters
{
public RectangleF RegionOfInterest { get; }
public Size TargetFrameSize { get; }
public ScalingPolicy ScalePolicy { get; }
public PixelFormat TargetFormat { get; }
public ScalingQuality ScaleQuality { get; }
public TransformParameters(RectangleF regionOfInterest, Size targetFrameSize, ScalingPolicy scalePolicy,
PixelFormat targetFormat, ScalingQuality scaleQuality)
{
RegionOfInterest = regionOfInterest;
TargetFrameSize = targetFrameSize;
TargetFormat = targetFormat;
ScaleQuality = scaleQuality;
ScalePolicy = scalePolicy;
}
protected bool Equals(TransformParameters other)
{
return RegionOfInterest.Equals(other.RegionOfInterest) &&
TargetFrameSize.Equals(other.TargetFrameSize) &&
TargetFormat == other.TargetFormat && ScaleQuality == other.ScaleQuality;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((TransformParameters) obj);
}
public override int GetHashCode()
{
unchecked
{
var hashCode = RegionOfInterest.GetHashCode();
hashCode = (hashCode * 397) ^ TargetFrameSize.GetHashCode();
hashCode = (hashCode * 397) ^ (int) TargetFormat;
hashCode = (hashCode * 397) ^ (int) ScaleQuality;
return hashCode;
}
}
}
}

38
IPCamLib/ICamera.cs Normal file
View File

@ -0,0 +1,38 @@
using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
namespace IPCamLib
{
/// <summary>
/// Receives frames from an ICamera.
/// </summary>
public interface ICameraObserver
{
/// <summary>
/// Called when a frame has been decoded from an ICamera.
/// </summary>
/// <param name="image">The decoded image.</param>
Task OnFrame(Image image);
}
/// <summary>
/// Abstracts an IP camera stream.
/// </summary>
public interface ICamera
{
/// <summary>
/// Starts receiving frames.
/// </summary>
/// <remarks>
/// The implementation should continue receiving frames until it is disconnected (if streaming),
/// a request fails (if polling) or the cancellationToken is cancelled. Reconnecting is done by
/// the caller which will call Fetch again. Do not keep connection-specific state between calls to Fetch.
/// Running on a separate thread is also handled by the caller, the returning Task should not complete until disconnected.
/// </remarks>
/// <param name="observer">The observer implementation to receive frames.</param>
/// <param name="cancellationToken">A CancellationToken which will stop the camera stream when cancelled.</param>
Task Fetch(ICameraObserver observer, CancellationToken cancellationToken);
}
}

View File

@ -12,6 +12,7 @@
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion> <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment> <FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic> <Deterministic>true</Deterministic>
<LangVersion>9</LangVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
@ -21,6 +22,9 @@
<DefineConstants>DEBUG;TRACE</DefineConstants> <DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
<DocumentationFile>bin\Debug\IPCamLib.xml</DocumentationFile>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType> <DebugType>pdbonly</DebugType>
@ -29,10 +33,13 @@
<DefineConstants>TRACE</DefineConstants> <DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
<DocumentationFile>bin\Release\IPCamLib.xml</DocumentationFile>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" /> <Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
@ -41,7 +48,61 @@
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Base\BaseHTTPStreamCamera.cs" />
<Compile Include="Concrete\HTTPMJPEGStreamCamera.cs" />
<Compile Include="Concrete\RTSPStreamCamera.cs" />
<Compile Include="FFMPEG\DecodedVideoFrame.cs" />
<Compile Include="FFMPEG\DecodedVideoFrameParameters.cs" />
<Compile Include="FFMPEG\DecoderException.cs" />
<Compile Include="FFMPEG\FFmpegDecodedVideoScaler.cs" />
<Compile Include="FFMPEG\FFmpegVideoDecoder.cs" />
<Compile Include="FFMPEG\FFmpegVideoPInvoke.cs" />
<Compile Include="FFMPEG\IDecodedVideoFrame.cs" />
<Compile Include="FFMPEG\PixelFormat.cs" />
<Compile Include="FFMPEG\ScalingPolicy.cs" />
<Compile Include="FFMPEG\ScalingQuality.cs" />
<Compile Include="FFMPEG\TransformParameters.cs" />
<Compile Include="RetryableCamera.cs" />
<Compile Include="ICamera.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="FastBitmapLib">
<Version>2.0.0</Version>
</PackageReference>
<PackageReference Include="RtspClientSharp">
<Version>1.3.3</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Content Include="avcodec-58.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="avdevice-58.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="avfilter-7.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="avformat-58.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="avutil-56.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="libffmpeghelper.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="postproc-55.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="swresample-3.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="swscale-5.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project> </Project>

View File

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=x64/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -0,0 +1,82 @@
using System;
using System.Threading;
using System.Threading.Tasks;
// ReSharper disable UnusedMember.Global - public API
namespace IPCamLib
{
/// <summary>
/// Receives frames from an ICamera as well as status updates.
/// </summary>
public interface IRetryableCameraObserver : ICameraObserver
{
/// <summary>
/// Called when a new Fetch attempt has started.
/// </summary>
Task OnFetch();
/// <summary>
/// Called when the stream was disconnected.
/// </summary>
/// <param name="exception">Contains the exception, if any occured. Null for graceful disconnects and timeouts.</param>
/// <param name="retryDelay">The delay until the next Fetch is attempted.</param>
Task OnDisconnected(Exception exception, TimeSpan retryDelay);
}
/// <summary>
/// Implements retry logic for an ICamera instance.
/// </summary>
public class RetryableCamera
{
private readonly ICamera camera;
/// <summary>
/// Creates a new instance of a RetryableCamera.
/// </summary>
/// <param name="camera">The camera instance to fetch from.</param>
public RetryableCamera(ICamera camera)
{
this.camera = camera;
}
/// <summary>
/// Starts receiving frames and will retry when disconnected.
/// </summary>
/// <param name="observer">The observer implementation to receive frames and status updates.</param>
/// <param name="cancellationToken">A CancellationToken which will stop the camera stream when cancelled.</param>
public async Task Fetch(IRetryableCameraObserver observer, CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
// TODO incremental back-off?
var retryDelay = TimeSpan.FromSeconds(5);
var exception = false;
try
{
await observer.OnFetch();
await camera.Fetch(observer, cancellationToken);
}
catch (TaskCanceledException)
{
// Empty by design
}
catch (Exception e)
{
await observer.OnDisconnected(e, retryDelay);
exception = true;
}
if (!exception && !cancellationToken.IsCancellationRequested)
await observer.OnDisconnected(null, retryDelay);
await Task.Delay(retryDelay, cancellationToken);
}
}
}
}

BIN
IPCamLib/avcodec-58.dll Normal file

Binary file not shown.

BIN
IPCamLib/avdevice-58.dll Normal file

Binary file not shown.

BIN
IPCamLib/avfilter-7.dll Normal file

Binary file not shown.

BIN
IPCamLib/avformat-58.dll Normal file

Binary file not shown.

BIN
IPCamLib/avutil-56.dll Normal file

Binary file not shown.

Binary file not shown.

BIN
IPCamLib/postproc-55.dll Normal file

Binary file not shown.

BIN
IPCamLib/swresample-3.dll Normal file

Binary file not shown.

BIN
IPCamLib/swscale-5.dll Normal file

Binary file not shown.