First implementation

This commit is contained in:
Mark van Renswoude 2019-08-23 11:13:10 +02:00
parent b17d0fd6ab
commit 346d3f5913
19 changed files with 1111 additions and 388 deletions

63
.gitattributes vendored
View File

@ -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

266
.gitignore vendored
View File

@ -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
.vs
bin
obj
packages
*.user

191
AppBar.cs Normal file
View File

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

110
CameraMJPEGStream.cs Normal file
View File

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

126
CameraStream.cs Normal file
View File

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

95
CameraView.Designer.cs generated Normal file
View File

@ -0,0 +1,95 @@
namespace IPCamAppBar
{
partial class CameraView
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
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;
}
}

96
CameraView.cs Normal file
View File

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

120
CameraView.resx Normal file
View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

34
Config.cs Normal file
View File

@ -0,0 +1,34 @@
using System.Collections.Generic;
namespace IPCamAppBar
{
public class Config
{
public ConfigAppBar AppBar { get; } = new ConfigAppBar();
public List<ConfigCamera> 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; }
}
}

40
Form1.Designer.cs generated
View File

@ -1,40 +0,0 @@
namespace IPCamAppBar
{
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
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
}
}

View File

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

View File

@ -33,6 +33,9 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
@ -46,14 +49,30 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Form1.cs">
<Compile Include="AppBar.cs" />
<Compile Include="CameraMJPEGStream.cs" />
<Compile Include="CameraStream.cs" />
<Compile Include="CameraView.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="CameraView.Designer.cs">
<DependentUpon>CameraView.cs</DependentUpon>
</Compile>
<Compile Include="Config.cs" />
<Compile Include="MainForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Form1.Designer.cs">
<DependentUpon>Form1.cs</DependentUpon>
<Compile Include="MainForm.Designer.cs">
<DependentUpon>MainForm.cs</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<EmbeddedResource Include="CameraView.resx">
<DependentUpon>CameraView.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="MainForm.resx">
<DependentUpon>MainForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
@ -63,6 +82,10 @@
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<None Include="config.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="packages.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>

View File

@ -0,0 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EOI/@EntryIndexedValue">EOI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MJPEG/@EntryIndexedValue">MJPEG</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SOI/@EntryIndexedValue">SOI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=URL/@EntryIndexedValue">URL</s:String></wpf:ResourceDictionary>

65
MainForm.Designer.cs generated Normal file
View File

@ -0,0 +1,65 @@
namespace IPCamAppBar
{
partial class MainForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
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;
}
}

93
MainForm.cs Normal file
View File

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

120
MainForm.resx Normal file
View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -16,7 +16,7 @@ namespace IPCamAppBar
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
Application.Run(new MainForm());
}
}
}

20
config.json Normal file
View File

@ -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
}
]
}

4
packages.config Normal file
View File

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