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