diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index 1ff0c42..0000000
--- a/.gitattributes
+++ /dev/null
@@ -1,63 +0,0 @@
-###############################################################################
-# Set default behavior to automatically normalize line endings.
-###############################################################################
-* text=auto
-
-###############################################################################
-# Set default behavior for command prompt diff.
-#
-# This is need for earlier builds of msysgit that does not have it on by
-# default for csharp files.
-# Note: This is only used by command line
-###############################################################################
-#*.cs diff=csharp
-
-###############################################################################
-# Set the merge driver for project and solution files
-#
-# Merging from the command prompt will add diff markers to the files if there
-# are conflicts (Merging from VS is not affected by the settings below, in VS
-# the diff markers are never inserted). Diff markers may cause the following
-# file extensions to fail to load in VS. An alternative would be to treat
-# these files as binary and thus will always conflict and require user
-# intervention with every merge. To do so, just uncomment the entries below
-###############################################################################
-#*.sln merge=binary
-#*.csproj merge=binary
-#*.vbproj merge=binary
-#*.vcxproj merge=binary
-#*.vcproj merge=binary
-#*.dbproj merge=binary
-#*.fsproj merge=binary
-#*.lsproj merge=binary
-#*.wixproj merge=binary
-#*.modelproj merge=binary
-#*.sqlproj merge=binary
-#*.wwaproj merge=binary
-
-###############################################################################
-# behavior for image files
-#
-# image files are treated as binary by default.
-###############################################################################
-#*.jpg binary
-#*.png binary
-#*.gif binary
-
-###############################################################################
-# diff behavior for common document formats
-#
-# Convert binary document formats to text before diffing them. This feature
-# is only available from the command line. Turn it on by uncommenting the
-# entries below.
-###############################################################################
-#*.doc diff=astextplain
-#*.DOC diff=astextplain
-#*.docx diff=astextplain
-#*.DOCX diff=astextplain
-#*.dot diff=astextplain
-#*.DOT diff=astextplain
-#*.pdf diff=astextplain
-#*.PDF diff=astextplain
-#*.rtf diff=astextplain
-#*.RTF diff=astextplain
diff --git a/.gitignore b/.gitignore
index 3c4efe2..5d7695b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,261 +1,5 @@
-## Ignore Visual Studio temporary files, build results, and
-## files generated by popular Visual Studio add-ons.
-
-# User-specific files
-*.suo
-*.user
-*.userosscache
-*.sln.docstates
-
-# User-specific files (MonoDevelop/Xamarin Studio)
-*.userprefs
-
-# Build results
-[Dd]ebug/
-[Dd]ebugPublic/
-[Rr]elease/
-[Rr]eleases/
-x64/
-x86/
-bld/
-[Bb]in/
-[Oo]bj/
-[Ll]og/
-
-# Visual Studio 2015 cache/options directory
-.vs/
-# Uncomment if you have tasks that create the project's static files in wwwroot
-#wwwroot/
-
-# MSTest test Results
-[Tt]est[Rr]esult*/
-[Bb]uild[Ll]og.*
-
-# NUNIT
-*.VisualState.xml
-TestResult.xml
-
-# Build Results of an ATL Project
-[Dd]ebugPS/
-[Rr]eleasePS/
-dlldata.c
-
-# DNX
-project.lock.json
-project.fragment.lock.json
-artifacts/
-
-*_i.c
-*_p.c
-*_i.h
-*.ilk
-*.meta
-*.obj
-*.pch
-*.pdb
-*.pgc
-*.pgd
-*.rsp
-*.sbr
-*.tlb
-*.tli
-*.tlh
-*.tmp
-*.tmp_proj
-*.log
-*.vspscc
-*.vssscc
-.builds
-*.pidb
-*.svclog
-*.scc
-
-# Chutzpah Test files
-_Chutzpah*
-
-# Visual C++ cache files
-ipch/
-*.aps
-*.ncb
-*.opendb
-*.opensdf
-*.sdf
-*.cachefile
-*.VC.db
-*.VC.VC.opendb
-
-# Visual Studio profiler
-*.psess
-*.vsp
-*.vspx
-*.sap
-
-# TFS 2012 Local Workspace
-$tf/
-
-# Guidance Automation Toolkit
-*.gpState
-
-# ReSharper is a .NET coding add-in
-_ReSharper*/
-*.[Rr]e[Ss]harper
-*.DotSettings.user
-
-# JustCode is a .NET coding add-in
-.JustCode
-
-# TeamCity is a build add-in
-_TeamCity*
-
-# DotCover is a Code Coverage Tool
-*.dotCover
-
-# NCrunch
-_NCrunch_*
-.*crunch*.local.xml
-nCrunchTemp_*
-
-# MightyMoose
-*.mm.*
-AutoTest.Net/
-
-# Web workbench (sass)
-.sass-cache/
-
-# Installshield output folder
-[Ee]xpress/
-
-# DocProject is a documentation generator add-in
-DocProject/buildhelp/
-DocProject/Help/*.HxT
-DocProject/Help/*.HxC
-DocProject/Help/*.hhc
-DocProject/Help/*.hhk
-DocProject/Help/*.hhp
-DocProject/Help/Html2
-DocProject/Help/html
-
-# Click-Once directory
-publish/
-
-# Publish Web Output
-*.[Pp]ublish.xml
-*.azurePubxml
-# TODO: Comment the next line if you want to checkin your web deploy settings
-# but database connection strings (with potential passwords) will be unencrypted
-#*.pubxml
-*.publishproj
-
-# Microsoft Azure Web App publish settings. Comment the next line if you want to
-# checkin your Azure Web App publish settings, but sensitive information contained
-# in these scripts will be unencrypted
-PublishScripts/
-
-# NuGet Packages
-*.nupkg
-# The packages folder can be ignored because of Package Restore
-**/packages/*
-# except build/, which is used as an MSBuild target.
-!**/packages/build/
-# Uncomment if necessary however generally it will be regenerated when needed
-#!**/packages/repositories.config
-# NuGet v3's project.json files produces more ignoreable files
-*.nuget.props
-*.nuget.targets
-
-# Microsoft Azure Build Output
-csx/
-*.build.csdef
-
-# Microsoft Azure Emulator
-ecf/
-rcf/
-
-# Windows Store app package directories and files
-AppPackages/
-BundleArtifacts/
-Package.StoreAssociation.xml
-_pkginfo.txt
-
-# Visual Studio cache files
-# files ending in .cache can be ignored
-*.[Cc]ache
-# but keep track of directories ending in .cache
-!*.[Cc]ache/
-
-# Others
-ClientBin/
-~$*
-*~
-*.dbmdl
-*.dbproj.schemaview
-*.jfm
-*.pfx
-*.publishsettings
-node_modules/
-orleans.codegen.cs
-
-# Since there are multiple workflows, uncomment next line to ignore bower_components
-# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
-#bower_components/
-
-# RIA/Silverlight projects
-Generated_Code/
-
-# Backup & report files from converting an old project file
-# to a newer Visual Studio version. Backup files are not needed,
-# because we have git ;-)
-_UpgradeReport_Files/
-Backup*/
-UpgradeLog*.XML
-UpgradeLog*.htm
-
-# SQL Server files
-*.mdf
-*.ldf
-
-# Business Intelligence projects
-*.rdl.data
-*.bim.layout
-*.bim_*.settings
-
-# Microsoft Fakes
-FakesAssemblies/
-
-# GhostDoc plugin setting file
-*.GhostDoc.xml
-
-# Node.js Tools for Visual Studio
-.ntvs_analysis.dat
-
-# Visual Studio 6 build log
-*.plg
-
-# Visual Studio 6 workspace options file
-*.opt
-
-# Visual Studio LightSwitch build output
-**/*.HTMLClient/GeneratedArtifacts
-**/*.DesktopClient/GeneratedArtifacts
-**/*.DesktopClient/ModelManifest.xml
-**/*.Server/GeneratedArtifacts
-**/*.Server/ModelManifest.xml
-_Pvt_Extensions
-
-# Paket dependency manager
-.paket/paket.exe
-paket-files/
-
-# FAKE - F# Make
-.fake/
-
-# JetBrains Rider
-.idea/
-*.sln.iml
-
-# CodeRush
-.cr/
-
-# Python Tools for Visual Studio (PTVS)
-__pycache__/
-*.pyc
\ No newline at end of file
+.vs
+bin
+obj
+packages
+*.user
\ No newline at end of file
diff --git a/AppBar.cs b/AppBar.cs
new file mode 100644
index 0000000..519a816
--- /dev/null
+++ b/AppBar.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Windows.Forms;
+
+// ReSharper disable InconsistentNaming
+
+namespace IPCamAppBar
+{
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct RECT
+ {
+ public int left;
+ public int top;
+ public int right;
+ public int bottom;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct APPBARDATA
+ {
+ public int cbSize;
+ public IntPtr hWnd;
+ public int uCallbackMessage;
+ public int uEdge;
+ public RECT rc;
+ public IntPtr lParam;
+ }
+
+ internal enum ABMsg
+ {
+ ABM_NEW = 0,
+ ABM_REMOVE,
+ ABM_QUERYPOS,
+ ABM_SETPOS,
+ ABM_GETSTATE,
+ ABM_GETTASKBARPOS,
+ ABM_ACTIVATE,
+ ABM_GETAUTOHIDEBAR,
+ ABM_SETAUTOHIDEBAR,
+ ABM_WINDOWPOSCHANGED,
+ ABM_SETSTATE
+ }
+
+ internal enum ABNotify
+ {
+ ABN_STATECHANGE = 0,
+ ABN_POSCHANGED,
+ ABN_FULLSCREENAPP,
+ ABN_WINDOWARRANGE
+ }
+
+ internal enum ABEdge
+ {
+ ABE_LEFT = 0,
+ ABE_TOP,
+ ABE_RIGHT,
+ ABE_BOTTOM
+ }
+
+
+ public enum AppBarPosition
+ {
+ Top,
+ Left,
+ Bottom,
+ Right
+ }
+
+
+ public class AppBar : IDisposable
+ {
+ [DllImport("SHELL32", CallingConvention = CallingConvention.StdCall)]
+ private static extern uint SHAppBarMessage(int dwMessage, ref APPBARDATA pData);
+
+ [DllImport("User32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
+ private static extern bool MoveWindow(IntPtr hWnd, int x, int y, int cx, int cy, bool repaint);
+
+ [DllImport("User32.dll", CharSet = CharSet.Auto)]
+ private static extern int RegisterWindowMessage(string msg);
+
+
+ private class PositionParameters
+ {
+ public Screen Monitor { get; set; }
+ public AppBarPosition Position { get; set; }
+ public int Size { get; set; }
+ }
+
+
+ private bool disposed;
+ private APPBARDATA appBarData;
+ private PositionParameters lastPosition;
+
+
+
+ public AppBar(IntPtr windowHandle)
+ {
+ appBarData.cbSize = Marshal.SizeOf(appBarData);
+ appBarData.hWnd = windowHandle;
+ appBarData.uCallbackMessage = RegisterWindowMessage("AppBarMessage");
+
+ SHAppBarMessage((int)ABMsg.ABM_NEW, ref appBarData);
+ }
+
+
+ public void Dispose()
+ {
+ if (disposed)
+ throw new ObjectDisposedException(GetType().Name);
+
+ SHAppBarMessage((int)ABMsg.ABM_REMOVE, ref appBarData);
+ disposed = true;
+ }
+
+
+ public void SetPosition(Screen monitor, AppBarPosition position, int size)
+ {
+ appBarData.rc.top = monitor.Bounds.Top;
+ appBarData.rc.left = monitor.Bounds.Left;
+ appBarData.rc.bottom = monitor.Bounds.Bottom;
+ appBarData.rc.right= monitor.Bounds.Right;
+
+ ApplyPosition(position, size);
+ SHAppBarMessage((int)ABMsg.ABM_QUERYPOS, ref appBarData);
+
+ ApplyPosition(position, size);
+ SHAppBarMessage((int)ABMsg.ABM_SETPOS, ref appBarData);
+
+ MoveWindow(
+ appBarData.hWnd,
+ appBarData.rc.left,
+ appBarData.rc.top,
+ appBarData.rc.right - appBarData.rc.left,
+ appBarData.rc.bottom - appBarData.rc.top,
+ true);
+
+ lastPosition = new PositionParameters
+ {
+ Monitor = monitor,
+ Position = position,
+ Size = size
+ };
+ }
+
+
+ public void HandleMessage(Message m)
+ {
+ if (m.Msg == appBarData.uCallbackMessage && m.WParam.ToInt32() == (int)ABNotify.ABN_POSCHANGED)
+ UpdatePosition();
+ }
+
+
+ private void UpdatePosition()
+ {
+ if (lastPosition == null)
+ return;
+
+ SetPosition(lastPosition.Monitor, lastPosition.Position, lastPosition.Size);
+ }
+
+
+ private void ApplyPosition(AppBarPosition position, int size)
+ {
+ switch (position)
+ {
+ case AppBarPosition.Top:
+ appBarData.uEdge = (int)ABEdge.ABE_TOP;
+ appBarData.rc.bottom = appBarData.rc.top + size;
+ break;
+
+ case AppBarPosition.Left:
+ appBarData.uEdge = (int)ABEdge.ABE_LEFT;
+ appBarData.rc.right = appBarData.rc.left + size;
+ break;
+
+ case AppBarPosition.Bottom:
+ appBarData.uEdge = (int)ABEdge.ABE_BOTTOM;
+ appBarData.rc.top = appBarData.rc.bottom - size;
+ break;
+
+ case AppBarPosition.Right:
+ appBarData.uEdge = (int)ABEdge.ABE_RIGHT;
+ appBarData.rc.left = appBarData.rc.right - size;
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(position), position, @"Invalid position value");
+ }
+ }
+ }
+}
diff --git a/CameraMJPEGStream.cs b/CameraMJPEGStream.cs
new file mode 100644
index 0000000..b2cc18b
--- /dev/null
+++ b/CameraMJPEGStream.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Drawing;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace IPCamAppBar
+{
+ internal class CameraMJPEGStream : CameraStream
+ {
+ // 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[] JpegEOI = { 0xff, 0xd9 }; // end of image bytes
+
+ private const int ChunkSize = 1024;
+ private const int MaxBufferSize = 1024 * 1024 * 10;
+
+
+ protected override async Task ReadFrames(Stream stream, CancellationToken cancellationToken)
+ {
+ var buffer = new byte[ChunkSize];
+ var bufferPosition = 0;
+ int? startOfImage = null;
+ int? lastEndOfSearch = null;
+
+
+ void ExpandBuffer()
+ {
+ // Make sure we have at least ChunkSize remaining
+ if (buffer.Length >= bufferPosition + ChunkSize)
+ return;
+
+ // If we pass the MaxBufferSize (10mb unless changed above), this is likely not an MJPEG stream, abort
+ if (bufferPosition + ChunkSize > MaxBufferSize)
+ throw new IOException("Buffer size exceeded before encountering JPEG image");
+
+ Array.Resize(ref buffer, bufferPosition + ChunkSize);
+ }
+
+
+ void ResetBuffer(int untilPosition)
+ {
+ // Don't resize the buffer down, it is very likely the next image is of a similar size.
+ // Instead move whatever's remaining to the start.
+ if (untilPosition < buffer.Length - 1)
+ Array.Copy(buffer, untilPosition, buffer, 0, bufferPosition - untilPosition);
+
+ bufferPosition = 0;
+ startOfImage = null;
+ lastEndOfSearch = null;
+ }
+
+
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ var bytesRead = await stream.ReadAsync(buffer, bufferPosition, ChunkSize, cancellationToken);
+ if (bytesRead == 0)
+ throw new EndOfStreamException();
+
+ bufferPosition += bytesRead;
+
+ if (!startOfImage.HasValue)
+ {
+ var index = buffer.Find(JpegSOI, bufferPosition);
+ if (index == -1)
+ {
+ // No start of image yet, we need to buffer more
+ ExpandBuffer();
+ continue;
+ }
+
+ startOfImage = index;
+ }
+
+ var endOfImage = buffer.Find(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
+ // need to scan it all again
+ lastEndOfSearch = bufferPosition - JpegEOI.Length;
+ ExpandBuffer();
+ continue;
+ }
+
+ if (endOfImage < startOfImage.Value)
+ {
+ // Oops, wut?! Uhm. yeah. let's pretend this never happened, ok?
+ ResetBuffer(startOfImage.Value + JpegSOI.Length);
+ continue;
+ }
+
+ 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
+ {
+ Image = image
+ });
+ }
+ }
+ }
+}
diff --git a/CameraStream.cs b/CameraStream.cs
new file mode 100644
index 0000000..5da2b44
--- /dev/null
+++ b/CameraStream.cs
@@ -0,0 +1,126 @@
+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 delegate void FrameEvent(object sender, FrameEventArgs args);
+
+
+ internal abstract class CameraStream : IDisposable
+ {
+ public event FrameEvent Frame;
+
+ private readonly CancellationTokenSource cancelTaskTokenSource = new CancellationTokenSource();
+ private Task streamTask;
+
+
+ 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();
+ streamTask?.Wait();
+ }
+
+
+ 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] : "");
+ }
+
+ 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(responseStream, cancellationToken);
+ }
+ }
+ catch (Exception e)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ break;
+
+ // TODO onException
+ await Task.Delay(5000, cancellationToken);
+ }
+ }
+ }
+
+
+ protected abstract Task ReadFrames(Stream stream, CancellationToken cancellationToken);
+
+
+ protected virtual void OnFrame(FrameEventArgs args)
+ {
+ Frame?.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;
+ }
+
+ }
+}
diff --git a/CameraView.Designer.cs b/CameraView.Designer.cs
new file mode 100644
index 0000000..e4ca926
--- /dev/null
+++ b/CameraView.Designer.cs
@@ -0,0 +1,95 @@
+namespace IPCamAppBar
+{
+ partial class CameraView
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.ConnectingLabel = new System.Windows.Forms.Label();
+ this.StreamView = new System.Windows.Forms.PictureBox();
+ this.NoDataLabel = new System.Windows.Forms.Label();
+ ((System.ComponentModel.ISupportInitialize)(this.StreamView)).BeginInit();
+ this.SuspendLayout();
+ //
+ // ConnectingLabel
+ //
+ this.ConnectingLabel.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.ConnectingLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
+ this.ConnectingLabel.ForeColor = System.Drawing.Color.White;
+ this.ConnectingLabel.Location = new System.Drawing.Point(0, 0);
+ this.ConnectingLabel.Name = "ConnectingLabel";
+ this.ConnectingLabel.Size = new System.Drawing.Size(150, 150);
+ this.ConnectingLabel.TabIndex = 0;
+ this.ConnectingLabel.Text = "Connecting...";
+ this.ConnectingLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
+ //
+ // StreamView
+ //
+ this.StreamView.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.StreamView.Location = new System.Drawing.Point(0, 0);
+ this.StreamView.Name = "StreamView";
+ this.StreamView.Size = new System.Drawing.Size(150, 150);
+ this.StreamView.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
+ this.StreamView.TabIndex = 1;
+ this.StreamView.TabStop = false;
+ this.StreamView.Visible = false;
+ //
+ // NoDataLabel
+ //
+ this.NoDataLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
+ | System.Windows.Forms.AnchorStyles.Right)));
+ this.NoDataLabel.BackColor = System.Drawing.Color.Black;
+ this.NoDataLabel.ForeColor = System.Drawing.Color.DarkRed;
+ this.NoDataLabel.Location = new System.Drawing.Point(0, 129);
+ this.NoDataLabel.Name = "NoDataLabel";
+ this.NoDataLabel.Size = new System.Drawing.Size(150, 21);
+ this.NoDataLabel.TabIndex = 2;
+ this.NoDataLabel.Text = "No data for x seconds";
+ this.NoDataLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
+ this.NoDataLabel.Visible = false;
+ //
+ // CameraView
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.BackColor = System.Drawing.Color.Gray;
+ this.Controls.Add(this.NoDataLabel);
+ this.Controls.Add(this.StreamView);
+ this.Controls.Add(this.ConnectingLabel);
+ this.Margin = new System.Windows.Forms.Padding(0);
+ this.Name = "CameraView";
+ ((System.ComponentModel.ISupportInitialize)(this.StreamView)).EndInit();
+ this.ResumeLayout(false);
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.Label ConnectingLabel;
+ private System.Windows.Forms.PictureBox StreamView;
+ private System.Windows.Forms.Label NoDataLabel;
+ }
+}
diff --git a/CameraView.cs b/CameraView.cs
new file mode 100644
index 0000000..f845250
--- /dev/null
+++ b/CameraView.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Windows.Forms;
+using DateTime = System.DateTime;
+
+namespace IPCamAppBar
+{
+ public partial class CameraView : UserControl
+ {
+ private DateTime lastFrameTime;
+
+
+ public CameraView(string url)
+ {
+ InitializeComponent();
+
+ var cameraStream = new CameraMJPEGStream();
+ cameraStream.Frame += CameraStreamOnFrame;
+ cameraStream.Start(url);
+
+ var noDataTimer = new Timer();
+ noDataTimer.Interval = 1000;
+ noDataTimer.Tick += CheckNoData;
+ noDataTimer.Start();
+
+ Disposed += (sender, args) =>
+ {
+ noDataTimer?.Dispose();
+ cameraStream?.Dispose();
+ };
+ }
+
+
+ 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;
+ StreamView.Visible = true;
+ NoDataLabel.Visible = false;
+
+ lastFrameTime = DateTime.Now;
+
+ var viewImage = new Bitmap(Width, Height);
+ using (var graphics = Graphics.FromImage(viewImage))
+ {
+ 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();
+ }
+
+ var oldImage = StreamView.Image;
+ StreamView.Image = viewImage;
+ oldImage?.Dispose();
+ }
+
+
+ private void CheckNoData(object sender, EventArgs e)
+ {
+ var timeSinceLastFrame = DateTime.Now - lastFrameTime;
+ if (timeSinceLastFrame.TotalSeconds < 10)
+ return;
+
+ NoDataLabel.Text = $@"No data for {timeSinceLastFrame.TotalSeconds} seconds";
+ NoDataLabel.Visible = true;
+ }
+ }
+}
diff --git a/CameraView.resx b/CameraView.resx
new file mode 100644
index 0000000..1af7de1
--- /dev/null
+++ b/CameraView.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/Config.cs b/Config.cs
new file mode 100644
index 0000000..103a042
--- /dev/null
+++ b/Config.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+
+namespace IPCamAppBar
+{
+ public class Config
+ {
+ public ConfigAppBar AppBar { get; } = new ConfigAppBar();
+ public List Cameras { get; set; }
+ }
+
+
+ public enum ConfigSide
+ {
+ Top,
+ Left,
+ Bottom,
+ Right
+ }
+
+ public class ConfigAppBar
+ {
+ public int Monitor { get; set; }
+ public ConfigSide Side { get; set; }
+ public int Size { get; set; }
+ }
+
+
+ public class ConfigCamera
+ {
+ public string URL { get; set; }
+ public int Width { get; set; }
+ public int Height { get; set; }
+ }
+}
diff --git a/Form1.Designer.cs b/Form1.Designer.cs
deleted file mode 100644
index e6f3816..0000000
--- a/Form1.Designer.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-namespace IPCamAppBar
-{
- partial class Form1
- {
- ///
- /// Required designer variable.
- ///
- private System.ComponentModel.IContainer components = null;
-
- ///
- /// Clean up any resources being used.
- ///
- /// true if managed resources should be disposed; otherwise, false.
- protected override void Dispose(bool disposing)
- {
- if (disposing && (components != null))
- {
- components.Dispose();
- }
- base.Dispose(disposing);
- }
-
- #region Windows Form Designer generated code
-
- ///
- /// Required method for Designer support - do not modify
- /// the contents of this method with the code editor.
- ///
- private void InitializeComponent()
- {
- this.components = new System.ComponentModel.Container();
- this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
- this.ClientSize = new System.Drawing.Size(800, 450);
- this.Text = "Form1";
- }
-
- #endregion
- }
-}
-
diff --git a/Form1.cs b/Form1.cs
deleted file mode 100644
index 581bdd7..0000000
--- a/Form1.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Data;
-using System.Drawing;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows.Forms;
-
-namespace IPCamAppBar
-{
- public partial class Form1 : Form
- {
- public Form1()
- {
- InitializeComponent();
- }
- }
-}
diff --git a/IPCamAppBar.csproj b/IPCamAppBar.csproj
index 2c3ce12..f5568df 100644
--- a/IPCamAppBar.csproj
+++ b/IPCamAppBar.csproj
@@ -33,6 +33,9 @@
4
+
+ packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll
+
@@ -46,14 +49,30 @@
-
+
+
+
+
+ UserControl
+
+
+ CameraView.cs
+
+
+
Form
-
- Form1.cs
+
+ MainForm.cs
+
+ CameraView.cs
+
+
+ MainForm.cs
+
ResXFileCodeGenerator
Resources.Designer.cs
@@ -63,6 +82,10 @@
True
Resources.resx
+
+ Always
+
+
SettingsSingleFileGenerator
Settings.Designer.cs
diff --git a/IPCamAppBar.sln.DotSettings b/IPCamAppBar.sln.DotSettings
new file mode 100644
index 0000000..20ccd82
--- /dev/null
+++ b/IPCamAppBar.sln.DotSettings
@@ -0,0 +1,5 @@
+
+ EOI
+ MJPEG
+ SOI
+ URL
\ No newline at end of file
diff --git a/MainForm.Designer.cs b/MainForm.Designer.cs
new file mode 100644
index 0000000..f265fd7
--- /dev/null
+++ b/MainForm.Designer.cs
@@ -0,0 +1,65 @@
+namespace IPCamAppBar
+{
+ partial class MainForm
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.CameraViewContainer = new System.Windows.Forms.FlowLayoutPanel();
+ this.SuspendLayout();
+ //
+ // CameraViewContainer
+ //
+ this.CameraViewContainer.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.CameraViewContainer.Location = new System.Drawing.Point(0, 0);
+ this.CameraViewContainer.Margin = new System.Windows.Forms.Padding(0);
+ this.CameraViewContainer.Name = "CameraViewContainer";
+ this.CameraViewContainer.Size = new System.Drawing.Size(269, 211);
+ this.CameraViewContainer.TabIndex = 0;
+ this.CameraViewContainer.Click += new System.EventHandler(this.CameraViewContainer_Click);
+ //
+ // MainForm
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.BackColor = System.Drawing.Color.Black;
+ this.ClientSize = new System.Drawing.Size(269, 211);
+ this.Controls.Add(this.CameraViewContainer);
+ this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
+ this.Name = "MainForm";
+ this.ShowInTaskbar = false;
+ this.Text = "IPCam AppBar";
+ this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.MainForm_FormClosed);
+ this.ResumeLayout(false);
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.FlowLayoutPanel CameraViewContainer;
+ }
+}
+
diff --git a/MainForm.cs b/MainForm.cs
new file mode 100644
index 0000000..25cb2d5
--- /dev/null
+++ b/MainForm.cs
@@ -0,0 +1,93 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Windows.Forms;
+using Newtonsoft.Json;
+
+namespace IPCamAppBar
+{
+ public partial class MainForm : Form
+ {
+ private readonly Config config;
+ private readonly AppBar appBar;
+
+
+ public MainForm()
+ {
+ InitializeComponent();
+
+ using (var file = File.OpenText(@"config.json"))
+ {
+ var serializer = new JsonSerializer();
+ config = (Config)serializer.Deserialize(file, typeof(Config));
+ }
+
+ var monitor = GetMonitor(config.AppBar.Monitor);
+ var position = GetAppBarPosition(config.AppBar.Side);
+
+ appBar = new AppBar(Handle);
+ appBar.SetPosition(monitor, position, config.AppBar.Size);
+
+ config.Cameras?.ForEach(AddCamera);
+ }
+
+
+ protected override void WndProc(ref Message m)
+ {
+ appBar?.HandleMessage(m);
+ base.WndProc(ref m);
+ }
+
+
+ private static Screen GetMonitor(int number)
+ {
+ // Since I could not find any way to get the numbers as assigned in the
+ // display control panel, we'll just order them left-to-right.
+ var orderedScreens = Screen.AllScreens.OrderBy(s => s.Bounds.Left).ToList();
+
+ // 0 means primary. Yeah. Totally not losing geek-creds for using a 1-based
+ // index here. No sir. 'tis a feature!
+ return number > 0 && number <= orderedScreens.Count
+ ? orderedScreens[number - 1]
+ : Screen.PrimaryScreen;
+ }
+
+
+ private static AppBarPosition GetAppBarPosition(ConfigSide side)
+ {
+ switch (side)
+ {
+ case ConfigSide.Top: return AppBarPosition.Top;
+ case ConfigSide.Bottom: return AppBarPosition.Bottom;
+ case ConfigSide.Left: return AppBarPosition.Left;
+ case ConfigSide.Right: return AppBarPosition.Right;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(side), side, @"Invalid side value");
+ }
+ }
+
+
+ private void AddCamera(ConfigCamera camera)
+ {
+ var view = new CameraView(camera.URL)
+ {
+ Width = camera.Width,
+ Height = camera.Height
+ };
+
+ CameraViewContainer.Controls.Add(view);
+ }
+
+
+ private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
+ {
+ appBar.Dispose();
+ }
+
+
+ private void CameraViewContainer_Click(object sender, EventArgs e)
+ {
+ //Close();
+ }
+ }
+}
diff --git a/MainForm.resx b/MainForm.resx
new file mode 100644
index 0000000..1af7de1
--- /dev/null
+++ b/MainForm.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
index 1010321..dfd93f6 100644
--- a/Program.cs
+++ b/Program.cs
@@ -16,7 +16,7 @@ namespace IPCamAppBar
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
- Application.Run(new Form1());
+ Application.Run(new MainForm());
}
}
}
diff --git a/config.json b/config.json
new file mode 100644
index 0000000..87d2c1f
--- /dev/null
+++ b/config.json
@@ -0,0 +1,20 @@
+{
+ "AppBar": {
+ "Monitor": 1,
+ "Side": "Top",
+ "Size": 480
+ },
+
+ "Cameras": [
+ {
+ "URL": "http://username:password@ipcamera1/videostream.cgi",
+ "Width": 480,
+ "Height": 360
+ },
+ {
+ "URL": "http://username:password@ipcamera2/videostream.cgi",
+ "Width": 480,
+ "Height": 360
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages.config b/packages.config
new file mode 100644
index 0000000..a75532f
--- /dev/null
+++ b/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file