From 346d3f591373ab53dc8b1758caaf2c657fce88e1 Mon Sep 17 00:00:00 2001 From: Mark van Renswoude Date: Fri, 23 Aug 2019 11:13:10 +0200 Subject: [PATCH] First implementation --- .gitattributes | 63 --------- .gitignore | 266 +----------------------------------- AppBar.cs | 191 ++++++++++++++++++++++++++ CameraMJPEGStream.cs | 110 +++++++++++++++ CameraStream.cs | 126 +++++++++++++++++ CameraView.Designer.cs | 95 +++++++++++++ CameraView.cs | 96 +++++++++++++ CameraView.resx | 120 ++++++++++++++++ Config.cs | 34 +++++ Form1.Designer.cs | 40 ------ Form1.cs | 20 --- IPCamAppBar.csproj | 29 +++- IPCamAppBar.sln.DotSettings | 5 + MainForm.Designer.cs | 65 +++++++++ MainForm.cs | 93 +++++++++++++ MainForm.resx | 120 ++++++++++++++++ Program.cs | 2 +- config.json | 20 +++ packages.config | 4 + 19 files changed, 1111 insertions(+), 388 deletions(-) delete mode 100644 .gitattributes create mode 100644 AppBar.cs create mode 100644 CameraMJPEGStream.cs create mode 100644 CameraStream.cs create mode 100644 CameraView.Designer.cs create mode 100644 CameraView.cs create mode 100644 CameraView.resx create mode 100644 Config.cs delete mode 100644 Form1.Designer.cs delete mode 100644 Form1.cs create mode 100644 IPCamAppBar.sln.DotSettings create mode 100644 MainForm.Designer.cs create mode 100644 MainForm.cs create mode 100644 MainForm.resx create mode 100644 config.json create mode 100644 packages.config 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